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:
- status:描述文章的生命周期状态(PENDING → PROCESSING → COMPLETED / FAILED),粗粒度,面向全局监控;
- 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 不再是单纯的"进度条",而变成了驱动前端界面状态切换的信号源。