降SpringAI阿里第9掌-或跃在渊-ReactAgent
降SpringAI阿里第9掌-或跃在渊-ReactAgent
从 ChatClient 换到 ReactAgent,第一次请求就停不下来。
[读者] 已用 ChatClient + Tool + RAG 跑通数字人后端的工程师
[痛点] 多步工具调用靠手写 if-else 编排,循环、重试、上下文全失控
[现在读] 前八掌的单轮编排顶不住多步任务,Agent Framework 是下一层地基
[读完] 能把数字人升级成受控的主 ReactAgent,看懂事件流并约束调用与重试
[旧方案] ChatClient 单次调用 + 业务层手写工具编排
|
v
[新需求] 多步推理:先查知识库、再调工具、最后才回答
|
v
[冲突] 循环判断写在业务代码里,模型想调几次就调几次
|
v
[后果] 一次请求几十次模型调用、工具异常打断会话、成本与延迟不可预期
我是老李,这一年一直在把 spring-ai-alibaba 往真实项目里用。数字人这个仓库从第 2 掌搭环境开始,走到今天:注册登录、项目管理、标题主题色、开场白结束语、Web 运行页都在,语音链路是 STT → 文本 → 后端 → TTS。
前面八掌,后端主链路是 ChatClient:一个客户端、一个 System Prompt,工具用 .tools(...) 注册,知识库用 RAG 的 Advisor 挂上。这套东西在"一轮问答"里非常稳,稳到我都快忘了它有个前提——一轮。
今年产品要的已经不是一轮问答了。用户会说"帮我看看这周还剩哪些时段,然后按我上次说的偏好排一下"。这句话里有检索、有工具、有追问。那多步编排到底该写在哪?继续在业务代码里堆 if-else,还是把控制流交给框架?
如果这一层选错,后面每一掌都会更难受:多 Agent、A2A、评测、K8s,全都建立在这一层的抽象上。
01、故事
数字人 Web 运行页上,用户点"开始对话",浏览器把麦克风音频推到 STT,识别出的文本进后端,后端产出回复文本,再交给 TTS 播出去。后端这一层,前面的实现是这样一个方法:
public String chat(String sessionId, String question) {
// 第 8 掌留下的 RAG 检索
String context = knowledgeSearchService.searchAsText(question, 4);
return chatClient.prompt()
.system(props.getSystemPrompt())
.user(u -> u.text("{question}\n\n参考资料:\n{context}")
.param("question", question)
.param("context", context))
.call()
.content();
}
这段代码把"要不要检索"写死了。不管用户问什么,先检索一遍;不管检索结果有没有用,都塞进 Prompt。第 6 掌加工具的时候,我又在下面加了一层判断:命中日程关键词就调日程工具。到第 8 掌,这个方法已经长到一百多行,里面是三层 if-else 和两处 try-catch。
任务是:把这条链路上的"决定权"从业务代码交出去——让模型自己决定要不要检索、要不要调工具、要不要再补一轮,同时后端还能看清它每一步做了什么。
一次真实联调里,问题来了。用户在运行页说"帮我查一下我下周的日程,跟课程表对一下,冲突的帮我标出来"。日志里出现了三次模型调用:第一次返回一个 tool call,第二次又返回一个 tool call,第三次直接给答案。答案是错的——它只对了一半数据,因为第二次工具调用的参数里,模型把一个日期写成了上周。而我的代码,全程没意识到哪里不对,把那段话原样交给了 TTS。
语音链路照常播报,听起来毫无异常。这才是最让人后背发凉的地方。
编排写进业务
循环便失了控
决定权交出去
才谈得上稳定
02、问题
旧方案失效得很彻底,但失效方式不只有一种。
第一种是决策写死。检索与否、调什么工具、调几次,全是我在 Java 里预判的。用户换一种说法,预判就落空:他没提"课程表"三个字,我就不会调日程工具,回答自然只能靠检索拼凑。
第二种是循环没有上界。工具调用只能进不能出:模型返回 tool call,我执行、把结果塞回去、再问一次。这个 while 在我的代码里,退出条件是"模型不再返回 tool call"——等于把循环的终止权完全交给了模型。
第三种是异常没有边界。知识库服务偶发 503 时,工具方法抛出的异常会一路冒到 Controller,整个会话直接 500,用户在运行页看到的是"对话中断"。
业务上的影响很直接:运行页的对话质量不可预期,答错的时候看不出来,答对的时候也不知道为什么对。技术上表现为三件事——模型调用次数不可控、工具失败不可恢复、上下文无差别膨胀。
所以这一掌要有一个能验收的完成标准,我给自己定了四条:
- 一次
POST /api/agent/chat请求,日志里能看到完整的节点事件序列:Agent → Model → Tool/RAG → Model → 最终响应; - 单次用户请求的模型调用次数有硬上界,超界行为可预期(不是超时,是显式结束);
- 工具调用失败后有重试,重试耗尽后 Agent 仍能产出面向用户的回答;
- 前端拿到的仍然是一个字符串,语音链路 STT → Agent → TTS 对外表现不变。
旧链写死检索
新需求要多步
无上界即风险
先定验收口径
03、原理
要理解 ReactAgent,先把 ReAct 这个词拆开:Reason + Act。模型先推理出"我该做什么",然后执行一个动作(工具调用),拿到观察结果,再推理,再行动,直到它认为可以回答了。
听起来像提示词工程,其实是控制流工程。
在 spring-ai-alibaba 的 Agent Framework 里,BaseAgent 是抽象基类,ReactAgent 是它最常用的实现。ReactAgent 内部不是一段循环代码,而是一张图:一个 agent 节点、一个 model 节点、一个 tools 节点,加上一条从 tools 回到 model 的条件边。所谓 ReAct,就是沿着这条边转圈,直到不再产生 tool call。
这里有一个反直觉判断:Agent 的稳定性不来自模型变强,而来自你把"什么时候停"从模型手里收回来。
直觉上,模型越聪明,循环越不需要管。实际恰恰相反:越强的模型越倾向于多调几次工具去"确认一下",工具越多、覆盖面越广,它探索的空间就越大。你交出的是循环终止权,而模型并不知道你的预算、你的延迟要求、你的成本红线。
第二个反直觉判断:instruction(System Prompt)不会约束循环,只有图上的钩子才会。 我在联调时第一反应就是在提示词里写"不要重复调用同一个工具",模型答应了,然后照调不误。因为提示词影响的是 token 概率,而循环次数是运行时状态——两件事在不同的层。
这就是本篇所有代码的依据:instruction 管行为风格,Hooks 管运行时边界,Interceptors 管单次调用的前后处理,Context Engineering 管送进模型的上下文形状,HITL 管人类介入的中断点。四件事,四种位置,不能互相替代。
循环不是提示
边界才是能力
图里画清停点
模型方有分寸
04、架构
[输入] 用户一句话(来自 STT 的文本)
|
v
[模块] ReactAgent(BaseAgent 实现)内部是一张 StateGraph
|
v
[数据/状态] OverAllState:messages、上下文、中断与恢复标记
|
v
[处理] agent 节点 -> model 节点 ->(有 tool_call)tools 节点 -> 回 model
| |
| 无 tool_call
| v
| 结束,产出最终 AIMessage
v
[输出] 事件流 NodeOutput(可观测)/ 最终文本(给 TTS)
边界先说清楚。ReactAgent 负责的是单 Agent 内部的循环:一次请求内,模型和工具之间可以来回多次。它不负责多 Agent 之间的路由与交接,那是第 12、13 掌的事;它也不负责语音链路本身,STT 和 TTS 在它外面。
收益有三个:一是决策从业务代码搬进图,业务代码从一百多行塌缩成一次调用;二是循环过程变成事件流,每一步都能被日志、被观测系统看见;三是循环的边界可以挂在钩子上,和业务逻辑解耦。
代价也要说清楚:多了一层框架抽象,出问题时要先看图的节点序列而不是看自己的方法栈;stream 返回的是事件而不是最终字符串,调用方要自己处理收尾;Hooks 的语义(在哪个节点前后生效、能否改写状态)需要逐一看官方文档确认。
适用条件很明确:需要多步决策、需要工具介入的主链路,用 ReactAgent;一次成型的简单改写、纯单轮问答,继续用 ChatClient 更省心。
输入一句问话
图中三步流转
状态写在边上
输出收敛成答
05、实战一次
先立分支:
git checkout -b chapter/09-react-agent
环境按系列基线:JDK 21 LTS,spring-ai-alibaba v1.1.2.2。依赖上,把 ChatClient 那一套保留(RAG 检索服务还在用),额外引入 agent framework 模块:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>1.1.2.2</version>
</dependency>
artifactId 与 spring-ai-alibaba-agent-framework 模块在仓库中的坐标以该版本 pom 为准,若本地解析不到请对照仓库对应 tag 的模块结构核验。
配置沿用第 4 掌:
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen-plus
核心实现分三步。第一步,把第 8 掌的知识库检索包成一个工具:
package com.lifuchun.digitalhuman.agent.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import java.util.List;
public class KnowledgeSearchTool {
private final KnowledgeSearchService knowledgeSearchService;
public KnowledgeSearchTool(KnowledgeSearchService knowledgeSearchService) {
this.knowledgeSearchService = knowledgeSearchService;
}
@Tool(description = "检索当前数字人项目绑定的知识库,返回与问题最相关的片段")
public String search(@ToolParam(description = "用户问题或检索关键词") String query) {
List<Doc> docs = knowledgeSearchService.search(query, 4);
if (docs.isEmpty()) {
return "知识库中没有找到相关内容,请如实告知用户,不要编造。";
}
StringBuilder sb = new StringBuilder();
for (Doc doc : docs) {
sb.append("- ").append(doc.text()).append('\n');
}
return sb.toString();
}
}
第二步,声明主 Agent:
package com.lifuchun.digitalhuman.agent;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class DigitalHumanAgentConfig {
@Bean
public ReactAgent digitalHumanAgent(ChatModel chatModel,
KnowledgeSearchTool knowledgeSearchTool,
ScheduleQueryTool scheduleQueryTool,
DigitalHumanProperties props) {
return ReactAgent.builder()
.name("digital_human_agent")
.model(chatModel)
.instruction(props.getSystemPrompt())
.tools(List.of(knowledgeSearchTool, scheduleQueryTool))
.build();
}
}
第三步,服务层从"拼 Prompt"改成"跑图 + 看事件":
package com.lifuchun.digitalhuman.agent;
import com.alibaba.cloud.ai.graph.NodeOutput;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Map;
@Service
public class DigitalHumanAgentService {
private static final Logger log = LoggerFactory.getLogger(DigitalHumanAgentService.class);
private final ReactAgent agent;
public DigitalHumanAgentService(ReactAgent agent) {
this.agent = agent;
}
public Flux<NodeOutput> stream(String sessionId, String question) {
Map<String, Object> input = Map.of(
"messages", List.of(new UserMessage(question)));
return agent.stream(input)
.doOnNext(out -> log.info("agent-event session={} node={}", sessionId, out.node()));
}
}
Controller 把事件流收尾成一段文本,交给 TTS:
@PostMapping(value = "/api/agent/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestBody ChatRequest request) {
StringBuilder buffer = new StringBuilder();
return agentService.stream(request.sessionId(), request.question())
.doOnNext(out -> buffer.append(out.toString()))
.thenMany(Flux.just(buffer.toString()));
}
启动应用,发一次请求:
curl -N -X POST http://localhost:8080/api/agent/chat \
-H 'Content-Type: application/json' \
-d '{"sessionId":"s-001","question":"帮我查一下我下周的日程"}'
预期日志(示例输出,未在当前环境实测,以下为预期结果;节点名以本地实际事件流为准,常见为 agent / model / tools):
agent-event session=s-001 node=agent
agent-event session=s-001 node=model
agent-event session=s-001 node=tools
agent-event session=s-001 node=model
agent-event session=s-001 node=agent
V1 跑通了,但只跑通了"顺利路径"。上界没有,重试没有,中断没有——三个坑都在前面等着。
先跑通最小环
再看事件流痕
一次请求走完
骨架才算立住
06、排查
诊断一:一次请求 17 次模型调用
现象:请求 帮我查一下我下周的日程,跟课程表对一下,接口 30 秒超时。日志里 node=model 出现了 17 次,node=tools 出现 8 次。
怀疑:模型陷入了工具循环——反复用相近参数调用同一个工具。
检查:把事件流按顺序打出来看节点序列。
node=agent
node=model
node=tools query="下周日程"
node=model
node=tools query="下周 日程 课程表"
node=model
node=tools query="下周日程安排"
node=model
...
证据:8 次 tools 调用的参数高度相似,语义上重复;且工具返回内容每次都一样,模型没有任何新信息可用,只能再试一次。
根因:循环上界缺失。ReactAgent 的循环终止条件默认是"模型不再返回 tool call",业务侧没有任何硬性截断,而工具返回值又是同质的,构不成收敛信号。
修复:给 Agent 挂 ModelCallLimitHook,限制单次运行的模型调用次数;同时让工具在"结果为空或高度重复"时返回一句明确的收敛提示,而不是空字符串。
错误尝试: 我第一反应是去改 instruction,加一句"请不要重复调用同一个工具"。为什么错:提示词作用在 token 采样概率上,循环次数是图运行时的状态;而且这个循环之所以停不下来,是因为工具每次都返回同样的内容,模型没有获得终止所需的"新信息",它继续调用是合理的。约束模型的语言习惯,解决不了约束运行时预算。
诊断二:知识库 503 打断整段会话
现象:知识库服务偶发 503,用户侧表现为"对话中断,请重试",后端日志里是一条从工具方法冒到 Controller 的异常栈。
怀疑:工具异常没有被任何一层接住。
检查:翻异常栈的起点。
org.springframework.web.reactive.function.client.WebClientResponseException$ServiceUnavailable: 503
the request for knowledge search failed
at ...KnowledgeSearchService.search(KnowledgeSearchService.java:63)
at ...KnowledgeSearchTool.search(KnowledgeSearchTool.java:24)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(...)
证据:栈顶是 WebClient 的 503,没有任何一层做过重试或降级;异常直接沿着调用链上升到 Web 层。
根因:工具边界没有错误策略。在 ReAct 循环里,工具失败应该是"一次可观察、可重试的观察结果",而不是"整段会话的终止信号"。
修复:挂 ToolRetryHook 做有限次重试,工具内部对最终失败做兜底返回,让模型拿到"当前检索不可用"这个事实,由它决定如何对用户表达。
错误尝试: 我试过在整个 chat 方法外面套一个大 try-catch,失败就返回兜底文案。为什么错:这把"工具失败"和"会话失败"混成一件事,用户会以为整个数字人坏了;而且它切断了 Agent 的推理链——模型本来可以换一种方式回答(比如不查知识库、直接基于已有信息回应)。边界应该画在工具那一层。
现象先看清
证据再说话
错路走一遍
根因才落地
07、优化
V2 只做第 06 章证据支持的事,不顺手加料。
根因一:循环无上界 → 修改:挂 ModelCallLimitHook。 在 Agent 构建时加上模型调用次数上限,超限时按显式行为收尾,而不是继续等待。这样做的原因是把预算控制放回工程侧;新行为是"任何一次用户请求消耗的模型调用数有硬顶";验证方式是构造一个必然重复调用的问句,观察事件流在到达上限后停止产出 model 节点。上限的具体数值、builder 参数名与超限行为枚举以官方文档与示例核验。
根因二:工具异常冒泡 → 修改:挂 ToolRetryHook 并在工具内兜底。 原因是把失败留在工具边界,让它变成可观察结果;新行为是知识库抖动时仍能产出回答,只是答案里会说明检索暂时不可用;验证方式是临时把知识库地址指向一个必然失败的端口,观察请求是否仍能返回一段完整文本。
return ReactAgent.builder()
.name("digital_human_agent")
.model(chatModel)
.instruction(props.getSystemPrompt())
.tools(List.of(knowledgeSearchTool, scheduleQueryTool))
.hooks(List.of(
ModelCallLimitHook.builder().runLimit(8).build(),
ToolRetryHook.builder().maxRetries(2).build()))
.build();
根因三:长会话上下文无差别膨胀 → 修改:引入 Context Engineering 与 ContextEditingHook。 原因是第 5 掌的 Memory 把整段历史都塞回去,多步工具调用后 token 迅速上升,模型注意力被旧工具结果稀释;新行为是按需裁剪旧工具输出,只保留与当前任务相关的片段。这一步的触发条件与裁剪策略需要按官方文档核验后再定,我先把钩子挂上,把行为差异用事件流记录下来,不做无证据的调参结论。
根因四:高风险动作没有人类介入点 → 修改:接 HumanInTheLoopHook。 数字人后续会接入"修改日程"这类写操作,在写之前必须停下来等人确认。原因是这类动作的不可逆性远高于读操作,重试和兜底都救不回来;新行为是图运行到该工具前中断,前端确认后恢复执行。中断与恢复依赖状态持久化(checkpoint/saver),具体恢复入口与配置以官方文档核验。
另外两项一起做:把 TodoListHook(规划/待办)挂上,让多步任务先出计划再执行,事件流里能看到计划节点;把 Interceptors 用在模型调用和工具调用的前后,做统一的日志、脱敏和耗时统计,而不是散落在各个 Service 里。
V2 完成后,第 02 章定下的四条验收标准逐条对照:事件序列完整可看、模型调用有上界、工具失败可恢复、对前端接口不变。
上界加在环上
重试收在工具
上下文按需裁
人是最后防线
08、演进
[同一输入] "查我下周日程并与课程表比对"
|
+--[V1] 业务代码手写编排 + ReactAgent 裸跑
| 代价:模型自旋、参数漂移、异常冒泡、上下文膨胀
|
+--[V2] ReactAgent + Hooks + Context Engineering + HITL
| 代价:多一层框架抽象,需理解事件流与中断语义
|
[Trade-off]
得到:可观测的循环、可控的调用次数、可恢复的失败、可介入的写操作
失去:部分直写逻辑的简单性;调试要先看节点序列再看方法栈
适用边界:需要多步决策与工具介入的主链路;纯单轮问答仍用 ChatClient 更划算
逐项比较。
正确性上,V1 的正确性依赖模型自觉,V2 的正确性来自边界——工具失败会转成可观察结果,写操作会等人确认,超限会显式结束。稳定性上,V1 的失败面是全链路,V2 把失败面收敛到工具边界和图节点。
复杂度上,V2 明显更高:多了一个模块依赖、一套 Hooks 语义、一个事件流的理解成本。这是真实代价,不要假装没有。
成本上,V1 的模型调用次数不可预测,V2 有上界——但具体数值差异需要在自己的业务流量上测,没有实测数据之前,任何百分比都是编的。
适用范围上,V2 适合作为数字人的主脑,也就是 STT 之后、TTS 之前那一段。它不是万能的:纯改写、纯分类、纯单轮检索,用 ChatClient 更快更省。
遗留问题有两个:一是中断恢复依赖状态持久化,第 18 掌之前这套状态还在内存里;二是多 Agent 还没开始拆,路由与交接是第 12 掌的事。
同问两条路
一旧一新增
取舍写在纸
边界记心里
09、洞见
9.1 Agent 的价值不在"更聪明",在"循环可观测"
回到第 01 章那次错误联调:答案错了,但语音照常播报,用户听不出来。换 Agent Framework 之后,最大的变化不是答对了,而是我能看见事件序列——agent、model、tools、model、agent。
反直觉判断:引入 Agent 框架的第一收益不是能力提升,是可观测性提升。 如果你引入之后仍然只能看到"输入一句话、输出一段文本",那这次升级等于没做。第 02 章定的验收标准里,第一条就是事件序列,不是答案质量,这是刻意的。
9.2 循环的终止权必须收回
模型不是在"犯错",它是在你给的空间里探索。你让它调工具,它就会调到觉得够了为止;它不知道你的延迟预算、你的成本红线、你的用户已经等了 12 秒。
反直觉判断:越强的模型越需要硬边界。 因为它的探索意愿更强、工具调用更主动。ModelCallLimitHook 这类东西看起来像"不信任模型",实际上是在替模型承担它看不到的约束。
9.3 HITL 不是给用户加弹窗,是给图加中断点
我一开始把 HITL 理解成前端弹个确认框。真正做的时候才发现,它的核心是图运行时可以在某个节点前停下来并保持状态,等人把决定写回去再继续。这意味着它和状态持久化是绑在一起的——没有 checkpoint,中断就没法恢复。
把 HITL 当 UI 功能做,会做出一堆散落在前端的确认逻辑;把它当运行时能力做,才会得到统一的中断与恢复语义。
9.4 上下文不是越多越好,多步之后尤其
单轮问答里,多塞一点上下文代价不大。多步工具调用之后,每一次工具返回都会进历史,几次下来上下文里全是过期的中间结果。ContextEditingHook 这类能力的价值就在这里:不是压缩文本,而是管理"哪些中间结果还有资格留在上下文里"。
越聪明的模型
越需要硬边界
越多的工具
越要少而稳
10、系统落地
数字人仓库原来有什么:注册登录、项目 CRUD、标题主题色背景图、开场白结束语、Agent 模型与 System Prompt 配置、Web 运行页、LiveKit 实时交互;后端主链路是 ChatClient + Tool + RAG,一轮成型。
本篇新增了什么:一个 ReactAgent 作为主脑,包住原有的工具与知识库检索;一条可观测的事件流;一组运行时边界——模型调用上限、工具重试、上下文编辑、人类介入;语音链路仍是 STT → ReactAgent → TTS,对外接口没变。
现在能做什么:用户一句话进来,Agent 自己决定检索还是调工具,多步之后收敛成一段话;后端能回答"这次请求走了哪些节点、调了几次模型、哪次工具失败重试过"。
还缺什么:状态还在内存里,多实例部署会丢中断现场;没有多 Agent 的路由与交接;没有评测集来量化回答质量;观测指标没有接到治理面板上。
下一步怎么走:先在这一个 Agent 上做稳——把状态落盘、把评测集建起来、把事件流指标化。多 Agent 的拆分留到第 12 掌,等单 Agent 的边界真正清晰了再拆,否则拆出来的只是多个不可观测的小黑盒。
基线已在前
本篇补主脑
多 Agent 未至
先把单点做稳
11、小结
Q1 → 多步编排写在图里,不写在业务 if-else 里
Q2 → 循环必须有上界:ModelCallLimit 兜住模型调用次数
Q3 → 工具异常在工具边界重试,不上升到整段会话
状态 → 数字人主链路 = STT → ReactAgent(Tool/RAG/Hooks)→ TTS,事件可观测
本篇最终可交付的能力,是数字人有了一颗受控的主脑:一次请求能看到 Agent → 模型 → Tool/RAG → 最终响应的完整事件,且调用次数与错误重试都在工程侧的掌控之内。
三问皆已答
一环已闭环
能力可交付
下步才敢走
12、作业
12.1 理解题:为什么在 instruction 里写"不要重复调用工具"几乎无效?
参考答案:instruction 影响的是模型生成 token 时的概率分布,属于采样层;而循环是否继续,取决于图运行时对"是否还有 tool call"的判断,属于状态层。要约束循环,得在运行时加边界,比如模型调用次数钩子。另外,模型重复调用往往是因为它每次拿到的观察结果没有新信息,不是因为它"忘了你的要求"。
12.2 实战题:给数字人接一次"写日程"的工具,并保证它不会自行写入。
参考答案:把工具注册进 Agent 后,在该工具前挂人类介入钩子(HITL),让图执行到此处中断并保存状态;前端拿到中断信息后展示确认,用户确认再把决定写回并恢复执行。关键是中断必须配合状态持久化,否则进程重启后无法恢复;同时确认信息里要带上工具名与参数,人能看懂自己要批准什么。
12.3 排障题:一次请求事件流里出现 20 次 model 节点,但日志里没有任何异常,如何定位?
参考答案:先看 tools 节点的参数序列,判断是重复调用还是逐步深入;再看工具返回值是否每次都同质(同质说明模型没有获得收敛信号);再看是否挂了模型调用上限。定位顺序是:事件序列 → 工具参数 → 工具返回 → 边界配置。先加硬上界止损,再改工具返回的收敛提示,不要先调提示词。
12.4 架构判断题:把 STT 和 TTS 也做成 Agent 的节点,是否更好?
参考答案:不建议。STT 和 TTS 是确定性的 I/O 转换,没有决策空间,放进图里只会增加状态复杂度和调试成本。Agent 的位置应该在"需要对不确定输入做多步决策"的那一段,也就是 STT 之后、TTS 之前。边界的原则是:有决策的地方进图,无决策的地方留在管道里。
答案不在书里
在你跑通的日志
排障先看证据
架构先看边界
13、思考
这一掌的核心冲突只有一句话:决策写在业务代码里,还是交给图。
V1 的失败不是模型不行,是我把"何时停"这个工程决策外包给了一个看不见预算的采样过程。V2 做的事情也很朴素:把预算、重试、上下文、人类介入这四件事,各自放到该在的层上。
可以带走的判断是:当你把一个执行体从"被调用"改成"自己循环",你同时失去的还有可预测性。补回来的方式不是换更贵的模型,而是在循环上、在工具边界上、在状态上,各加一道显式的约束。
单 Agent 稳不住的时候拆多 Agent,只是把不稳定性复制了几份。
一问定边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号