文章生成服务重构:从一键全自动的智能体编排,到三步人机协作

1、智能体串行执行

旧版的设计思路很直接------用户输入选题,AI 包办一切。整个系统对外只暴露一个接口,像按下自动洗衣机的启动键一样,标题、大纲、正文、配图一次性全搞定。

调用链路

bash 复制代码
用户输入选题
    │
    ▼
POST /article/create
    │
    ▼
ArticleAsyncService.executeArticleGeneration(taskId, topic)   // @Async 异步任务
    │
    ▼
ArticleAgentService.executeArticleGeneration(state, handler)  // 5 个智能体串行编排
    │
    ├─ 智能体1:生成标题(单个)
    ├─ 智能体2:生成大纲(流式输出)
    ├─ 智能体3:生成正文(流式输出)
    ├─ 智能体4:分析配图需求
    ├─ 智能体5:检索配图(串行)
    └─ 图文合成:按章节标题插入图片
    │
    ▼
保存到数据库 + 状态置为 COMPLETED + SSE 推送 ALL_COMPLETE

核心代码:一个方法串行编排 5 个智能体

智能体编排层是一个同步顺序方法,5 个 Agent 像工厂流水线一样依次执行,前一个不出结果,后一个不能开工:

java 复制代码
public void executeArticleGeneration(ArticleState state, Consumer<String> streamHandler) {
    // 智能体1:生成标题
    agent1GenerateTitle(state);
    streamHandler.accept(SseMessageTypeEnum.AGENT1_COMPLETE.getValue());

    // 智能体2:生成大纲(流式输出)
    agent2GenerateOutline(state, streamHandler);
    streamHandler.accept(SseMessageTypeEnum.AGENT2_COMPLETE.getValue());

    // 智能体3:生成正文(流式输出)
    agent3GenerateContent(state, streamHandler);
    streamHandler.accept(SseMessageTypeEnum.AGENT3_COMPLETE.getValue());

    // 智能体4:分析配图需求
    agent4AnalyzeImageRequirements(state);
    streamHandler.accept(SseMessageTypeEnum.AGENT4_COMPLETE.getValue());

    // 智能体5:生成配图
    agent5GenerateImages(state, streamHandler);
    streamHandler.accept(SseMessageTypeEnum.AGENT5_COMPLETE.getValue());

    // 图文合成:将配图插入正文
    mergeImagesIntoContent(state);
    streamHandler.accept(SseMessageTypeEnum.MERGE_COMPLETE.getValue());
}

异步入口层同样"大包大揽"------一个 @Async 方法把状态流转、智能体调用、数据落库、SSE 收尾全包了:

java 复制代码
@Async("articleExecutor")
public void executeArticleGeneration(String taskId, String topic) {
    // 1. 更新状态为处理中
    articleService.updateArticleStatus(taskId, ArticleStatusEnum.PROCESSING, null);
    
    // 2. 创建状态对象,一次性跑完全链路
    ArticleState state = new ArticleState();
    state.setTaskId(taskId);
    state.setTopic(topic);
    
    // 3. 5 个智能体串行执行,SSE 实时推送进度
    articleAgentService.executeArticleGeneration(state, message -> {
        handleAgentMessage(taskId, message, state);
    });
    
    // 4. 保存结果、更新状态、推送完成、关闭 SSE
    articleService.saveArticleContent(taskId, state);
    articleService.updateArticleStatus(taskId, ArticleStatusEnum.COMPLETED, null);
    sendSseMessage(taskId, SseMessageTypeEnum.ALL_COMPLETE, Map.of("taskId", taskId));
    sseEmitterManager.complete(taskId);
}

旧版的痛点:全自动,但不等于好用

"一键生成"听起来很美好,实际落地后却暴露出一系列内容创作场景下的致命矛盾:

痛点 具体表现
黑盒不可控 标题、大纲、正文一次性生成,用户只能在最后看到成品。AI 一旦跑偏------比如标题不抓人、大纲结构混乱------用户只能整篇推翻重来,中间没有任何纠偏机会
中间产物无价值 标题和大纲本是高价值的创意资产,生成后却只在内存里 transient 存在,不落库、不保留,用完即弃,无法复用或二次编辑
错误成本极高 全链路中任何一环失败(比如 Pexels 配图检索超时),整篇文章直接作废,状态变为 FAILED,前面所有 LLM 调用成本全部浪费
无法个性化干预 用户除了输入一个选题,全程零参与。AI 只能靠 prompt 里那点上下文猜测用户意图,无法接收"我想要更口语化""第三章多展开点"这类定向反馈
配图体验粗糙 只靠 Pexels 图片检索,失败就降级到 Picsum 占位图;无法区分写实照片、示意图、表情包等不同场景需求,配图经常"文不对图"

2、用户交互增强

核心思想:把"黑盒"变成"协作工作台"

旧版最大的问题是AI 包办一切,用户只能等结果。重构的核心思路很朴素------把"一次生成"拆成三个可独立触发的阶段,在阶段之间插入人工确认点:

阶段1:生成标题方案(3-5 个) → 用户挑选标题 + 补充描述

阶段2:生成大纲(流式输出) → 用户编辑大纲 / 让 AI 改大纲

阶段3:生成正文 + 配图 + 合成 → 完成

AI 负责"生成",人负责"决策"。每一步的中间产物都落库持久化,任何一个阶段失败都可以独立重试,而不是推倒重来。

2 阶段机设计:用 phase 补全 status 的语义盲区

为了支撑这种人机协作流程,我引入了一个与 ArticleStatusEnum 并行的 ArticlePhaseEnum:

  1. status:描述文章的生命周期状态(PENDING → PROCESSING → COMPLETED / FAILED),粗粒度,面向全局监控;
  2. phase:描述人机协作的当前进度(走到哪一步了),细粒度,面向流程编排和接口守卫。

两者的关系就像地铁的"运行状态"和"当前站点"------status 告诉你车在跑还是停了,phase 告诉你现在到哪个站、该谁上下车。

阶段流转图如下:

bash 复制代码
PENDING
  │  create
  ▼
TITLE_GENERATING ──AI 生成标题方案──▶ TITLE_SELECTING  ←─ ① 用户选标题(confirm-title)
                                            │
                                            ▼
OUTLINE_GENERATING ──AI 生成大纲──▶ OUTLINE_EDITING    ←─ ② 用户改/确认大纲(ai-modify-outline / confirm-outline)
                                            │
                                            ▼
CONTENT_GENERATING ──AI 生成正文+配图──▶ COMPLETED

phase 不只是展示用的状态标签,更是操作守卫------每个确认接口都会校验当前阶段,防止乱序调用(比如没选标题就直接确认大纲,后端会直接拒绝)。

新版实现逐层拆解

接口层:从 1 个入口变成 4 个独立动作

Controller 的变化最直观。旧版只有一个 create 接口包办全链路;新版把"一次点击"拆成了四个可独立触用的动作,每个动作对应一个用户决策点:

java 复制代码
// ① 创建文章任务:只触发阶段1(生成标题方案)
@PostMapping("/create")
public BaseResponse<String> createArticle(@RequestBody ArticleCreateRequest request, ...) {
    String taskId = articleService.createArticleTask(request.getTopic(), loginUser);
    articleAsyncService.executePhase1(taskId, request.getTopic());   // 只跑阶段1
    return ResultUtils.success(taskId);
}

// ② 用户确认标题并输入补充描述 → 触发阶段2(生成大纲)
@PostMapping("/confirm-title")
public BaseResponse<Void> confirmTitle(@RequestBody ArticleConfirmTitleRequest request, ...) {
    articleService.confirmTitle(taskId, mainTitle, subTitle, userDescription, loginUser);
    articleAsyncService.executePhase2(request.getTaskId());
    return ResultUtils.success(null);
}

// ③ AI 修改大纲(可反复调用,同步接口)
@PostMapping("/ai-modify-outline")
public BaseResponse<List<ArticleState.OutlineSection>> aiModifyOutline(...) {
    List<ArticleState.OutlineSection> modified = articleService.aiModifyOutline(taskId, suggestion, loginUser);
    return ResultUtils.success(modified);
}

// ④ 用户确认大纲 → 触发阶段3(生成正文+配图)
@PostMapping("/confirm-outline")
public BaseResponse<Void> confirmOutline(@RequestBody ArticleConfirmOutlineRequest request, ...) {
    articleService.confirmOutline(taskId, request.getOutline(), loginUser);
    articleAsyncService.executePhase3(request.getTaskId());
    return ResultUtils.success(null);
}

关键变化:每个接口只负责"推进到下一个阶段",而不是一次性跑完全链路。前端可以根据 phase 状态渲染不同的交互界面,用户随时知道自己该做什么。

异步服务层:一个 @Async 方法拆成三个

旧版的 executeArticleGeneration 是一个"大包大揽"的异步方法;新版拆成三个独立的 @Async 方法,各自触发、各自收尾、互不耦合:

java 复制代码
/** 阶段1:异步生成标题方案 */
@Async("articleExecutor")
public void executePhase1(String taskId, String topic) {
    // 状态推进:PROCESSING + TITLE_GENERATING
    articleService.updateArticleStatus(taskId, ArticleStatusEnum.PROCESSING, null);
    articleService.updatePhase(taskId, ArticlePhaseEnum.TITLE_GENERATING);

    ArticleState state = new ArticleState();
    state.setTaskId(taskId);
    state.setTopic(topic);

    // 只生成标题方案
    articleAgentService.executePhase1_GenerateTitles(state, this::handleAgentMessage);

    // 标题方案落库,阶段推进到"等待选择标题",SSE 通知前端
    articleService.saveTitleOptions(taskId, state.getTitleOptions());
    articleService.updatePhase(taskId, ArticlePhaseEnum.TITLE_SELECTING);
    sendSseMessage(taskId, SseMessageTypeEnum.TITLES_GENERATED, 
        Map.of("titleOptions", state.getTitleOptions()));
}

阶段 2 和阶段 3 的骨架与阶段 1 完全一致,差异只在"从库里读什么、调哪个智能体、写到哪个字段":

阶段 读取数据 调用智能体 落库字段 阶段推进
Phase1 无(只有 topic) 生成标题方案 titleOptions TITLE_SELECTING
Phase2 标题 + 用户补充描述 生成大纲(流式) outline OUTLINE_EDITING
Phase3 标题 + 大纲 + 配图方式 生成正文 + 配图 + 合成 content / fullContent / images COMPLETED

设计亮点:每个阶段都会从数据库重建 ArticleState,而不是沿用内存对象。这意味着阶段之间天然解耦------即使服务重启、进程崩溃,也能从库里恢复任意一步继续往下走,中间产物永不丢失。

智能体编排层:executeXxx 拆成 executePhaseXxx

编排层同样从一个大方法拆成三个职责边界清晰的方法:

java 复制代码
/** 阶段1:生成标题方案(3-5 个) */
public void executePhase1_GenerateTitles(ArticleState state, Consumer<String> streamHandler) {
    agent1GenerateTitleOptions(state);            // 生成 List<TitleOption>
    streamHandler.accept(SseMessageTypeEnum.AGENT1_COMPLETE.getValue());
}

/** 阶段2:生成大纲(用户选择标题后触发) */
public void executePhase2_GenerateOutline(ArticleState state, Consumer<String> streamHandler) {
    agent2GenerateOutline(state, streamHandler);  // 支持插入用户补充描述
    streamHandler.accept(SseMessageTypeEnum.AGENT2_COMPLETE.getValue());
}

/** 阶段3:生成正文+配图(用户确认大纲后触发) */
public void executePhase3_GenerateContent(ArticleState state, Consumer<String> streamHandler) {
    agent3GenerateContent(state, streamHandler);          // 正文(流式输出)
    agent4AnalyzeImageRequirements(state);                // 分析配图需求 + 插入占位符
    agent5GenerateImages(state, streamHandler);           // 按策略配图 + 上传 COS
    mergeImagesIntoContent(state);                        // 占位符替换,完成图文合成
    streamHandler.accept(SseMessageTypeEnum.MERGE_COMPLETE.getValue());
}

三个值得单独说的变化:

① 智能体 1:从"生成 1 个标题"升级为"生成 3-5 个标题方案"

java 复制代码
// 旧版:AI 直接拍板一个标题
String content = callLlm(prompt);
ArticleState.TitleResult titleResult = parseJsonResponse(content, ArticleState.TitleResult.class, "标题");
state.setTitle(titleResult);

// 新版:AI 出方案,用户来挑
String content = callLlm(prompt);
List<ArticleState.TitleOption> titleOptions = parseJsonListResponse(
        content, new TypeToken<List<ArticleState.TitleOption>>(){}, "标题方案");
state.setTitleOptions(titleOptions);

Prompt 也从"返回单个标题 JSON"改为"返回 3-5 个方案的 JSON 数组",覆盖不同角度。把"选哪个"的决策权从 AI 手里交还给用户。

② 智能体 2:支持用户补充描述

用户在选标题时可以输入补充要求(如"重点写避坑细节""语气更活泼"),系统通过 AGENT2_DESCRIPTION_SECTION 动态拼进大纲 Prompt:

java 复制代码
String descriptionSection = "";
if (state.getUserDescription() != null && !state.getUserDescription().trim().isEmpty()) {
    descriptionSection = PromptConstant.AGENT2_DESCRIPTION_SECTION
            .replace("{userDescription}", state.getUserDescription());
}
String prompt = PromptConstant.AGENT2_OUTLINE_PROMPT
        .replace("{mainTitle}", state.getTitle().getMainTitle())
        .replace("{subTitle}", state.getTitle().getSubTitle())
        .replace("{descriptionSection}", descriptionSection);

这样 AI 生成大纲时不再是"盲猜",而是带着用户的明确意图出稿。

服务层:阶段守卫防止流程乱序

每个确认操作都要校验当前 phase,防止用户跳过步骤或重复提交:

java 复制代码
@Override
public void confirmTitle(String taskId, String mainTitle, String subTitle, 
                         String userDescription, User loginUser) {
    Article article = getByTaskId(taskId);
    ThrowUtils.throwIf(article == null, ErrorCode.NOT_FOUND_ERROR, "文章不存在");
    checkArticlePermission(article, loginUser);

    // 阶段守卫:必须是 TITLE_SELECTING,否则拒绝
    ArticlePhaseEnum currentPhase = ArticlePhaseEnum.getByValue(article.getPhase());
    ThrowUtils.throwIf(currentPhase != ArticlePhaseEnum.TITLE_SELECTING,
            ErrorCode.OPERATION_ERROR, "当前阶段不允许此操作");

    // 保存用户选择的标题和补充描述,推进到 OUTLINE_GENERATING
    article.setMainTitle(mainTitle);
    article.setSubTitle(subTitle);
    article.setUserDescription(userDescription);
    article.setPhase(ArticlePhaseEnum.OUTLINE_GENERATING.getValue());
    this.updateById(article);
}

数据模型演进:为中间产物落库做准备

为了支撑阶段流转与中间产物持久化,article 表新增了 4 个字段:

sql 复制代码
ALTER TABLE article
    ADD COLUMN phase VARCHAR(50) DEFAULT 'PENDING'
        COMMENT '当前阶段:PENDING/TITLE_GENERATING/TITLE_SELECTING/OUTLINE_GENERATING/OUTLINE_EDITING/CONTENT_GENERATING' 
        AFTER status,
    ADD COLUMN titleOptions JSON NULL 
        COMMENT '标题方案列表(3-5个方案)' 
        AFTER subTitle,
    ADD COLUMN userDescription TEXT NULL 
        COMMENT '用户补充描述' 
        AFTER topic,
    ADD COLUMN enabledImageMethods JSON NULL 
        COMMENT '允许的配图方式列表' 
        AFTER userDescription;

SSE 消息演进:从"进度通知"到"交互信号"

前端靠 SSE 实时感知阶段推进。新版新增了两条"等待用户"的语义消息:

  • TITLES_GENERATED:标题方案已生成,等待用户选择
  • OUTLINE_GENERATED:大纲已生成,等待用户编辑

配合原有的 AGENTx_STREAMING / AGENTx_COMPLETE,前端可以实现这样的交互体验:

AI 流式生成内容 → 到达确认点自动停下 → 前端弹出交互面板 → 用户操作后触发下一阶段 → AI 继续流式生成...

SSE 不再是单纯的"进度条",而变成了驱动前端界面状态切换的信号源。

相关推荐
cui_hao_nan8 天前
大模型负责“聪明“,策略模式负责“执行“:智能配图系统如何自动选择图片来源
策略模式·ai开发
geminigoth22 天前
Spring AI Alibaba 入门开发一
大模型开发·ai开发·springai开发·deepseek开发·ollama部署开发
xcLeigh1 个月前
Doubao-Seed-Evolving大模型接入教程|搭建全品类提示词+AI工具导航网页
前端·人工智能·python·ai·html·ai开发·豆包
阿木实验室2 个月前
两周复现 FUEL:AI辅助无人机算法开发实战,附源码地址
ai开发·fuel算法·无人机探索
咋吃都不胖lyh2 个月前
大模型云端部署标准操作流程(SOP)
ai开发
HelloFYW3 个月前
Superpowers 5.1.0 技能使用手册(中文版)
开发工具·ai开发·claude code·superpowers·技能手册
断春风3 个月前
企业级 AI 应用开发实战:从 Demo 到生产系统的完整架构
人工智能·架构·ai开发
唯刻V3 个月前
谷歌官方 Android CLI 深度解读
android·cli·ai开发·ai时代·android cli
ftpeak3 个月前
深入浅出 LoongSuite Python Agent:让你的 AI 应用「透明化」(下篇)
开发语言·人工智能·ai·ai编程·ai开发