降SpringAI阿里第10掌-双龙取水-百阵流程
降SpringAI阿里第10掌-双龙取水-百阵流程
流程是确定的,就别再交给采样来决定
[读者] 已经用 ReactAgent 跑通数字人,但回答开始飘的 Java 后端
[痛点] 同样输入两次结果不同,该查的没查,该问的没问
[现在读] 你要把流程控制权从模型手里收回到 Java 编排层
[读完] 能用顺序/并行/路由/循环四类 Flow Agent 重构数字人链路
[旧方案] 一个 ReactAgent + 全量工具 + 超长 System Prompt
|
v
[新需求] 售前售后分流、知识与业务并行查询、缺信息主动追问
|
v
[冲突] 步骤走不走、走几遍,全由模型逐 token 采样决定
|
v
[后果] 不可复现、延迟与 token 不可控、关键节点无法埋点与校验
我是老李,一线 Java 后端,这些年主要在深圳做企业级 AI 应用。这个系列我陪着一个真实项目往前走:一个能注册登录、能配标题主题色、能挂在 Web 运行页上也接 LiveKit 实时语音的数字人。框架版本固定 alibaba/spring-ai-alibaba v1.1.2.2,JDK 21 LTS,练习仓库在 https://github.com/lifuchun522/springaialibabapractice 。
到第 9 掌为止,数字人的主链路已经跑起来了:用户一句话进来,后台一个 ReactAgent 带着 RAG 检索工具、订单查询工具、知识库工具,自己决定先干什么、后干什么。第一次看它自己先查订单再解释政策的时候,我是真的拍了下桌子——这东西看起来有脑子。
但演示和试运行之间隔着一整个世界的距离。产品同学第二天就提了三条要求:售前咨询走咨询话术、售后问题走工单流程;知识库检索和业务系统查询要同时做,不能串行等;用户没说清是哪台设备时必须追问,不许猜。
这三条,我该写进 System Prompt,还是写成 Java 代码?
如果继续塞 Prompt,等于把流程的决定权留在概率系统里;如果改成代码编排,那我这套已经跑起来的 ReactAgent 架构要动手术。
选错了会怎样?不是回答得不够聪明,而是售后问题被答成了售前承诺——用户拿着这句话去找客服,那就是事故。
01、故事
试运行第一天,数字人遇到一个提问:我这台 X1 送修大概要多久?
标准链路应该是:先判断属于售后,再查工单系统里这台设备的状态,然后检索售后政策里的返修时效,最后组织回答。
我把这段话原样写进了 System Prompt。第 10 次调用,它开始不一样了。十次请求里,三次没调知识库,直接凭常识答了句大概三个工作日;两次走了售前话术,说我们的服务响应很快;还有一次反问用户买的是哪款产品,而产品型号就在用户第一句话里。
任务是:把这四条链路做成可复现的流程,而不是每次重新赌一次采样结果。
当前方案只有一个 ReactAgent,所有决策都在它的循环里。变化来了——从一个人问一句,变成一个流程要同时覆盖四类场景。冲突在于,ReactAgent 的每一次工具选择都是一次采样,采样就有方差,方差在演示里看不出来,在试运行里就是事故。
演示看惊艳
试运见漂移
十次三不同
方知权在谁
02、问题
旧方案为什么顶不住?因为 System Prompt 是概率约束,不是结构约束。
业务影响有三层:用户拿到错误的时效承诺;客服侧出现无法追溯的对话;运营想统计有多少问题根本没走检索时,数据根本拿不到。
技术表现同样清晰:同输入多次调用路径不一致;单次耗时方差大;token 消耗方差大;没有地方插入检索前的权限校验和脱敏;没有地方对分流做 A/B 对照。
可验证的完成标准,我定成四条:
- 同一问题连续调用二十次,顺序模式下的节点序列完全一致;
- 并行模式两条分支都有独立产出,且互不覆盖;
- 售前、售后各十组样本,路由命中全部正确;
- 每次响应都能打印出经过的节点名与各自耗时。
这四条不是性能指标,是结构指标。结构指标没过,谈优化就是空谈。
提示是建议
代码是契约
路径可复现
才叫做交付
03、原理
Flow Agent 的本质,是把控制流的决策权从模型侧移到 Java 侧。v1.1.2.2 的 agent framework 内置了四类:
- SequentialAgent:子 Agent 按声明顺序执行,前一个的产出作为后一个的输入。
- ParallelAgent:子 Agent 并发执行,结果汇聚。
- RoutingAgent / LlmRoutingAgent:由一次 LLM 分类调用决定走哪个子 Agent,而不是让模型在循环里自己挑。
- LoopAgent:子 Agent 反复执行,直到满足退出条件或达到最大迭代次数。
反直觉判断:Flow Agent 不是能力更弱的 ReactAgent,恰恰相反。它把 LLM 从流程规划者降级成节点执行者,整个系统的可用性反而上升了。因为 LLM 最擅长的是语义判断和自然语言生成,最不擅长的恰恰是保证第三步一定在第二步之后发生。
第二个反直觉判断:并行不一定更快。ParallelAgent 的两条分支同时在跑,token 消耗是叠加的,服务端还有并发限流。如果两条分支里只有一条的结果最终会被用上,并行就是纯粹的浪费。并行解决的是两条结果都需要的延迟问题,不是猜哪条对的问题。
第三点,是路由能用 LLM 还稳的根本原因:分类比规划简单得多。让模型输出一个标签,远比让它输出一串动作序列稳定。LlmRoutingAgent 把 LLM 调用压缩成一次分类,这才是确定性流程里唯一保留 LLM 的地方——因为它本来就该由语义判断决定。
模型善辨义
不善守流程
编排归代码
判断归模型
04、架构
[输入] 用户一句话(Web 运行页输入 / LiveKit 语音转写)
|
v
[模块] LlmRoutingAgent 售前售后路由 + 四类 Flow Agent 编排层
|
v
[数据/状态] 会话状态:sessionId、命中分支、检索片段、业务记录、缺失字段
|
v
[处理] Sequential 主链 / Parallel 并行支链 / Loop 补信息环
|
v
[输出] 数字人回复 + 节点级埋点(分支、节点序列、耗时)
边界要说清楚:编排层只负责谁在什么条件下跑,不负责内容怎么生成。任何需要语义判断的地方——意图分类、答案组织、追问措辞——仍然是 LLM。
收益:链路可复现、节点可埋点、可以插入校验(权限、脱敏、限流),单点失败可以降级。
代价有两个。一是流程之外的问题必须显式兜底,否则会被硬塞进某个分支;二是子 Agent 之间的输出契约要明确定义,否则上一个节点的自由文本会污染下一个节点的输入。
适用条件也很直白:流程边界能被清楚画出来的场景。画不出来,就别急着编排。
路由定分支
顺序定链路
并行定吞吐
循环定完整
05、实战一次
环境与版本:JDK 21 LTS,Spring Boot 3.x,alibaba/spring-ai-alibaba v1.1.2.2,模型走 DashScope 的 qwen 系列。
说明放在前面:以下代码按 v1.1.2.2 的 agent-framework 模块组织,具体包路径与 builder 方法名请对照官方 examples 与源码核验;未在当前环境实测,以下为预期结果。
依赖(坐标与版本以官方 Releases 的 BOM 为准核验):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>1.1.2.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
</dependency>
配置:
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen-plus
temperature: 0.2
路由节点与抽取节点建议低 temperature,生成节点可以单独放开,这一点在后面的实战里会体现。
核心实现第一部分,顺序链的三个原子节点:
@Configuration
public class Ch10FlowConfig {
@Bean
public ReactAgent understandAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("understand")
.model(chatModel)
.instruction("""
你是数字人的理解节点,只做三件事:
1) 用一句话复述用户诉求;
2) 抽取实体:产品名、问题类型、时间范围、订单号;
3) 列出当前缺失的信息。
禁止直接回答用户,只输出结构化结果。
""")
.build();
}
@Bean
public ReactAgent retrieveAgent(ChatModel chatModel, VectorStore vectorStore) {
return ReactAgent.builder()
.name("retrieve")
.model(chatModel)
.instruction("""
你是检索节点。基于上游抽取出的实体调用检索工具,
只返回检索到的原文片段与来源标识,不要总结,不要作答。
""")
.tools(new KnowledgeBaseTool(vectorStore))
.build();
}
@Bean
public ReactAgent answerAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("answer")
.model(chatModel)
.instruction("""
你是回答节点。只能基于上游给出的检索片段与业务记录组织回答,
片段里没有的内容不要补充。回答完请附上引用的来源标识。
""")
.build();
}
}
检索工具本身是一个普通的 Spring Bean,用注解方式暴露给模型:
public class KnowledgeBaseTool {
private final VectorStore vectorStore;
public KnowledgeBaseTool(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@Tool(description = "按语义检索数字人产品知识库,返回最相关的原文片段")
public String search(String query) {
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.builder().query(query).topK(4).build());
return docs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n---\n"));
}
}
核心实现第二部分,把三个节点串成顺序链:
@Bean
public SequentialAgent qaFlow(ReactAgent understandAgent,
ReactAgent retrieveAgent,
ReactAgent answerAgent) {
return SequentialAgent.builder()
.name("qa-sequential")
.description("顺序流程:理解 → 检索 → 回答")
.subAgents(List.of(understandAgent, retrieveAgent, answerAgent))
.build();
}
核心实现第三部分,并行召回。关键点不在并行本身,而在每条分支写自己的状态键:
@Bean
public ParallelAgent parallelRecall(ChatModel chatModel,
VectorStore vectorStore,
BizQueryService bizQueryService) {
ReactAgent knowledgeQuery = ReactAgent.builder()
.name("knowledge-query")
.model(chatModel)
.instruction("你是知识库查询节点,只返回原文片段与来源,不要总结。")
.tools(new KnowledgeBaseTool(vectorStore))
.outputKey("knowledge_hits")
.build();
ReactAgent bizQuery = ReactAgent.builder()
.name("biz-query")
.model(chatModel)
.instruction("你是业务查询节点,只返回订单与工单的结构化字段,不要解释。")
.tools(new BizQueryTool(bizQueryService))
.outputKey("biz_records")
.build();
return ParallelAgent.builder()
.name("parallel-recall")
.description("并行召回:知识库 + 业务系统")
.subAgents(List.of(knowledgeQuery, bizQuery))
.build();
}
核心实现第四部分,售前售后路由。这里 LLM 只做一次分类:
@Bean
public LlmRoutingAgent presaleAfterSaleRouter(ChatModel chatModel,
ReactAgent presaleAgent,
ReactAgent aftersaleAgent) {
return LlmRoutingAgent.builder()
.name("presale-aftersale-router")
.model(chatModel)
.description("""
售前:产品能力、价格、试用、选型、私有化部署咨询。
售后:已购买产品的安装、故障、退款、工单、进度查询。
只看用户当前这一句话做判断,不要推理多轮。
""")
.subAgents(List.of(presaleAgent, aftersaleAgent))
.build();
}
核心实现第五部分,补信息循环。缺信息不能猜,但也不能无限追问:
@Bean
public LoopAgent infoCompletionLoop(ReactAgent checkCompletenessAgent,
ReactAgent askBackAgent) {
return LoopAgent.builder()
.name("info-completion-loop")
.description("信息补齐:校验完整度,缺失则追问,最多两轮")
.subAgents(List.of(checkCompletenessAgent, askBackAgent))
.maxIterations(2)
.build();
}
对外入口保持普通 Controller,数字人前端不用感知内部编排:
@RestController
@RequestMapping("/api/digital-human")
public class DigitalHumanWorkflowController {
private final LlmRoutingAgent routingAgent;
public DigitalHumanWorkflowController(LlmRoutingAgent routingAgent) {
this.routingAgent = routingAgent;
}
@PostMapping("/ask")
public Map<String, Object> ask(@RequestBody AskRequest request) {
String answer = routingAgent.invoke(request.question());
return Map.of("answer", answer);
}
public record AskRequest(String question) {
}
}
启动与请求:
export DASHSCOPE_API_KEY=sk-你的key
mvn -q spring-boot:run
curl -s -X POST http://localhost:8080/api/digital-human/ask -H 'Content-Type: application/json' -d '{"question":"你们的数字人支持私有化部署吗?"}'
示例输出(格式示意,非实测数据):
{
"branch": "presale",
"nodes": ["router", "presale"],
"answer": "支持。我们提供私有化部署方案,具体取决于知识库规模与并发要求……"
}
验收按三条走:顺序链看日志里的节点顺序是否恒为 understand → retrieve → answer;并行链看两条分支是否各自打印耗时、最终合并结果里两个来源是否都在;路由链构造售前与售后样本,看是否命中预期分支。
先束其筋骨
再任其才思
三条链跑通
方知何处柔
06、排查
第一组现象:同一句 X1 送修要多久,十次里有三次回答中没有任何知识库片段。
怀疑方向是检索工具坏了,或者向量库索引为空。
检查动作:打开知识库工具自身的调用日志,再单独直接调用 search 方法,返回完全正常;然后回头看那三次请求的完整 trace。
证据:trace 里这三次请求根本没有出现 retrieve 节点的任何工具调用记录,模型从 understand 直接跳到了 answer。
根因:检索是否发生,是模型在每一轮 decide 出来的,不是流程保证的。Prompt 写得再重,也只是把去检索的采样概率抬高了一点。
修复:用 SequentialAgent 把 understand → retrieve → answer 固定成顺序链,让跳过这一步在结构上不存在。
第二组现象:并行改造上线后,回复里只剩业务查询结果,知识库内容不见了。
怀疑方向是 ParallelAgent 没生效,只有一条分支真的跑了。
检查动作:打开两条分支各自的耗时日志,再打印最终状态对象里的 key 集合。
证据:两条分支的耗时都有记录,说明都在跑;但状态里只有一个检索相关的 key,且被写了两次。
根因:两条分支写了同一个状态键,后写覆盖先写。这不是并发问题,是命名冲突。
修复:每条分支写自己的 key(knowledge_hits 与 biz_records),再加一个汇总节点读取两者做合并。
错误尝试: 我一开始的修法是把 System Prompt 扩到一千五百字,逐条罗列必须先检索再回答、售后必须走工单、信息不足必须追问。为什么错:这是概率约束而不是结构约束。长上下文里指令会被稀释,模型不遵守时你没有任何可观测点——没有节点,就没有埋点,也就没有证据。更糟的是,每一轮都要为这一千五百字付 token 和延迟,成本花在了说服模型守规矩上,而不是解决问题上。
怪模型不听话
实为键名相撞
证据看 trace
根因看状态
07、优化
把根因归纳成一句:流程属于控制流,控制流不能由采样决定。
V2 做四处修改。
第一处,顺序链。修改:understand → retrieve → answer 用 SequentialAgent 固定。原因:检索必须发生。新行为:trace 中节点序列恒定。验证:同一输入跑二十次,对比节点序列是否完全一致。
第二处,并行链。修改:两条分支写独立 key,并新增 merge 节点显式归并。原因:消除状态键冲突。新行为:合并结果同时包含知识库片段与业务记录。验证:断言最终状态里两个 key 都存在且非空。
第三处,路由。修改:加 LlmRoutingAgent,只做一次分类。原因:把该走哪条流程,从多步推理压成单次分类。新行为:分支决策可复现、可统计。验证:构造售前售后样本各十组,比对命中分支。
第四处,补信息。修改:加 LoopAgent,最多两轮。原因:缺信息时不能猜,但也不能无限追问。新行为:最多追问两轮,超限走兜底话术转人工。验证:构造信息严重缺失的样本,观察轮次与终止条件是否如预期。
这里没有实测数据,所以我不下任何性能结论,只留验证方法。想拿结论,先跑样本。
收权于编排
还语义于模型
埋在节点里
判在证据上
08、演进
[同一输入:我这台 X1 送修要多久?]
|
+--[V1 单体 ReactAgent]
| 行为:模型自己决定是否检索、是否追问
| 代价:路径不可复现;失败点不可观测;每轮为 Prompt 付 token
|
+--[V2 Flow Agent 编排]
| 行为:路由一次分类 → 顺序链固定节点 → 必要处并行 → 缺信息循环
| 代价:流程外问题需显式兜底;节点间数据结构要约定
|
[Trade-off]
得到:可复现、可埋点、可校验、成本可估算
失去:对流程外问题的顺手一答
适用:流程边界清晰、对稳定性有要求的业务链路
逐项比较。正确性上,V1 靠模型自觉,V2 靠结构保证,但 V2 的正确性上限取决于每个节点内部 LLM 的输出质量。稳定性上,V2 明显高于 V1。复杂度上,V2 更高,多了编排配置和状态契约两笔账。成本上,V2 的 token 花在节点上而不是 Prompt 上,通常更可控,但并行分支会叠加消耗。适用范围上,V2 适合流程清晰的链路,开放式探索型任务仍应保留 ReactAgent。
遗留问题也列清楚:流程外的长尾问题怎么兜;跨轮会话状态怎么清理;多分支结果冲突怎么归并。这三条会一直带到后面的章节里。
自主换灵活
确定换复现
二者非对立
分层即兼得
09、洞见
9.1 编排不是降级,是放大器
反直觉判断:Flow Agent 常被理解成能力更弱的 Agent。实际上,把 LLM 从流程规划者降级成节点执行者之后,系统的整体可用性是上升的。因为流程规划本质上是状态机问题,状态机不该用采样实现。你拿走的是它做不好的那部分,留下的是它做得最好的那部分。
9.2 分类比规划稳得多
反直觉判断:路由之所以敢用 LLM 还稳,是因为它只要求模型做一次分类。让模型输出一个标签,比让它输出一串动作序列,方差小一个量级。这也是 LlmRoutingAgent 能在确定性流程里保留 LLM 的原因——它保留的是判断,不是流程。
9.3 没有节点就没有埋点
可观测性不是加日志加出来的,是架构给的。V1 的失败点藏在一段自由文本的推理里,你想查也无处下手;V2 的每个节点都是可以命名、可以计时、可以断言的单元。想要线上可诊断,先要有可命名的边界。
9.4 并行的代价是叠加
并行缩短的是关键路径延迟,代价是总量翻倍。只有在两条结果都需要时,并行才是赚的。判断标准很简单:如果有一条分支的结果可以被直接丢掉,那它就不该并行。
9.5 边界之外必须留出口
确定性流程最大的风险,是遇到不属于任何分支的问题时硬塞。每个编排入口都要有兜底分支,而兜底分支完全可以是一个 ReactAgent——这就是两种范式的接缝。
稳在分类易
漂在规划难
骨架宜刚性
肌肉宜柔韧
10、系统落地
原来有什么:第 1 到第 9 掌搭起来的数字人底座,注册登录、项目 CRUD、标题与主题色与背景图、开场白与结束语、Agent 模型与 System Prompt、Web 运行页、LiveKit 实时交互,再加上第 9 掌的单体 ReactAgent 主链路。
本篇新增什么:agent framework 的四类 Flow Agent 编排层。数字人对话从「一个 Agent 自己看着办」变成「路由 + 顺序 + 并行 + 循环」的显式流程。分支落在 chapter/10-workflow-agents,建议提交信息是 feat(ch10): add workflow agent patterns。
现在能做什么:售前售后能自动分流;知识和业务数据能同时查;关键信息缺失时会追问;每次回答都能列出经过的节点和各自耗时。
还缺什么:这套流程依然是单进程内的单体编排,会话状态还在内存里,没有跨服务的能力拆解,也没有评测集来判断改了编排之后到底变好还是变坏。
下一步如何演进:把编排从四类固定模板下沉到状态图,拿到更细的控制粒度、可持久化的状态与中断恢复能力;再往后才是多 Agent 与分布式协同。
底座尚能跑
流程今有骨
未及多体时
先补评测眼
11、小结
Q1 → Flow Agent 解决的是控制流不该由采样决定的问题:步骤需要结构保证,而不是概率保证
Q2 → 四类边界:顺序管链路、并行管吞吐、路由管分流、循环管完整性,都只管谁跑,不管内容怎么生成
Q3 → 选择依据:流程能画出来就用确定性编排,语义判断、开放探索、长尾兜底留给 ReactAgent
状态 → 数字人主链路具备可复现、可埋点、可校验的编排能力,顺序/并行/路由三种模式已跑通
再强调一次那个接缝:编排层负责形状,Agent 负责内容。两者都在,系统才完整。
三问皆有答
四型皆可跑
边界心中明
落地有章法
12、作业
12.1 理解题:为什么把「必须先检索再回答」写进 System Prompt,不等于一定会先检索?
参考答案:System Prompt 是上下文,不是控制流。模型是否发起工具调用,是每一轮基于当前上下文采样出来的决策,本身存在方差。要让它一定发生,必须由编排层保证:放进 SequentialAgent 的固定节点序列里,让跳过这个选项在结构上不存在。另外,即使模型某次遵守了,你也没有节点级的观测点来证明它遵守了——能证明,才是工程。
12.2 实战题:给数字人加一个退款咨询分支,这个分支放在路由层还是节点层?
参考答案:放在路由层。退款属于意图分流,是一次分类问题,适合由 LlmRoutingAgent 在 description 里声明边界,和售前、售后区分开。判断依据是:这类决策只需要看这句话属于哪一类,不需要多步推理,也不需要工具调用,压缩成一次分类既省 token 也更稳定。如果放到节点层,你得先有一个能稳定抽取是否退款的结构化输出源,反而绕远。
12.3 排障题:并行分支的结果出现丢失,如何定位?
参考答案:分三步。第一,确认两条分支是否都执行了,看各自的耗时日志或 trace 节点记录。第二,打印最终状态对象的 key 集合,确认是否存在两个分支写同一个 key 的情况。第三,如果是 key 冲突,改为每个分支写独立 key,并新增合并节点显式归并。关键是区分并发没生效和结果被覆盖这两类完全不同的根因:前者查调度,后者查命名。
12.4 架构判断题:是否应该把所有链路都改成 Flow Agent?
参考答案:不应该。Flow Agent 的前提是流程边界能被清楚画出来。对于开放式探索型任务,比如帮用户设计一套方案、做多轮头脑风暴,把步骤写死会直接切断模型的价值。合理做法是分层:外层用确定性编排保证该做的步骤一定做,内层在需要探索的节点里放 ReactAgent。判断口诀是——步骤能不能提前画完,能画完就编排,画不完就留给 Agent。
13、思考
本篇的核心冲突其实是一句话:控制流的决定权该放在哪。
模型是概率系统,你让它多数时候对是容易的,让它每一次都对是做不到的。而工程对链路的要求恰恰是后者。Flow Agent 的价值不在于它更聪明,而在于它把是否执行某一步,从一个采样问题变回了一个代码问题。
可复用的判断:凡是能用 if/else 和顺序表达清楚的东西,就不要用自然语言去请求模型遵守;凡是需要语义理解和开放生成的环节,就不要用代码去硬编码规则,把它交给 Agent,并且给它一个明确的输入输出契约。
确定性流程和自主推理不是两套方案,而是同一套系统里的骨骼和肌肉。骨骼负责形状和稳定,肌肉负责适应和发力。难的从来不是选边站,而是把两者的接缝画在哪里——这个接缝,就是你的节点契约。
能确定的别猜
该问清的就问
边界画在纸上
稳定长在架构

浙公网安备 33010602011771号