ReactAgent 运行时拆解:State、Hooks 与 Interceptors 工程实践
票小蜜上线不到一周,某天早上打开账单:当日 Token 消耗是前几天均值的两倍出头。
账单里没有明细,只知道"贵了"。想搞清楚为什么,得去翻 Spring Boot 的运行日志——那是一堆混在一起的 INFO 行,没有调用次数,没有上下文大小,也没有"这一次 ReAct 循环执行了多少轮"这种结构化信息。拼了将近一个小时,才把一条可疑记录的执行链拼出来:
一个用户问了一句"帮我改签下午的航班"。从第一次 LLM 调用到最后停止,ReAct 循环跑了 9 轮——查余票、查价格、查改签政策,反复确认,最后还是没改成,模型在第 9 轮输出了一句"抱歉,我无法完成此操作"然后停了。每次 LLM 调用都把完整的消息历史塞进去,到最后一轮上下文已经超过 7000 token。
更让人头疼的是:那一个小时,是花在看懂发生了什么上,而不是修问题上。没有调用计数,不知道这 9 轮是正常还是异常;没有每轮的 token 统计,不知道上下文什么时候开始膨胀;不知道模型在哪一轮开始"迷失",也不知道什么时候该踩刹车。"能跑的 Agent"和"可控的 Agent"之间的差距,就在这一刻变成了真实的工程负担。
这一章要打开的,就是 ReactAgent 的运行时控制面。不是 API 文档的翻译,而是从工程判断的角度,把三件事讲清楚:
- OverAllState:图的共享内存,数据怎么存、怎么更新、会话间怎么活着
- Hook:生命周期钩子,在哪个位置挂载、能干什么、怎么优雅地停止
- Interceptor:调用链过滤器,和 Hook 的边界在哪、什么场景才用它
把这三件事搞清楚,票小蜜才算真正可控。
系列目标:从零构建机票客服型 Agent「票小蜜」 本篇位置:第 10 章 / Agent Runtime 控制面 前置知识:第 09 章《从 ChatClient 到 Agent Runtime》
一、运行时控制面的三个层次
先把全局结构放出来,建立坐标系,后面每一节都在这个框架里展开。
| 层次 | 作用域 | 核心能力 | 工程类比 |
|---|---|---|---|
| State 层 | 整个图的共享数据 | 读写运行时状态,跨节点传数据 | 数据库事务的共享内存 |
| Hook 层 | 图节点级别 | 在特定位置插入自定义逻辑,可以改 State | Spring AOP 的 @Before/@After |
| Interceptor 层 | 单次模型/工具调用 | 拦截请求和响应,做过滤和修改 | Servlet Filter Chain |
三个层次的边界不是随意划定的——它们对应三种不同的控制粒度。State 是数据层,Hook 是流程层,Interceptor 是调用层。搞混了就会出现"用 Hook 做了应该用 Interceptor 做的事"或者反过来。
二、OverAllState:图的共享内存
先从问题说起
票小蜜在改签航班时,需要在多个步骤之间传递信息:用户说"改到下午三点那班",查余票时查出来了 CA1234,改签确认前还要再用一次这个航班号。
在传统的方法调用链里,这很简单——A 方法返回 flightNo,B 方法接收它作为参数。但 ReactAgent 内部是 StateGraph,AgentLlmNode 和 AgentToolNode 在 ReAct 循环里反复交替执行,它们之间没有直接的方法调用关系,无法通过参数传数据。
最简单的想法是:把 flightNo 塞进 messages,让它随着对话历史流转。但这会带来两个问题:
- 每轮对话消息都发给模型——你的
flightNo=CA1234会出现在 LLM 的上下文里,消耗 token,甚至干扰模型的判断 - 没有结构,只能靠自然语言——你没法做
state.get("flightNo"),只能从消息文本里解析,这本质上是把结构化数据非结构化了
OverAllState 就是解决这个问题的:它是一个独立于 messages 的共享容器,所有节点和 Hook 都能读写它,数据不会发给 LLM,但会随着图的执行一直"活着"。
OverAllState 的结构
OverAllState 本质是 Map<String, Object>。框架默认只有一个 key:
messages → List<Message> // 对话历史,发给 LLM
你可以加任意业务 key:
messages → List<Message> // 默认,对话历史
flightNo → "CA1234" // 当前处理的航班号,不进 LLM 上下文
userTier → "vip" // 用户等级,Hook 读取用于动态限流
llmCallCount → 3 // 本轮调用次数,Hook 写入用于监控
什么时候需要加自定义 key? 判断标准很简单:
| 场景 | 用 messages | 用自定义 key |
|---|---|---|
| 对话历史,需要发给 LLM | ✓ | |
| 业务状态(订单号、航班号),不需要 LLM 知道 | ✓ | |
| Hook 需要读取的上下文(用户等级、调用计数) | ✓ | |
| 节点间传递的结构化中间结果 | ✓ |
OverAllState 固定是 Map<String, Object>,不能换成强类型 POJO——这是框架为了让所有节点和 Hook 都能无感访问状态的设计权衡。读取时自己做类型转换:(String) state.value("flightNo").orElse("")。
和 ToolContext、Context Engineering 的区别
引入 OverAllState 之前,Spring AI 已经有一个叫 toolContext 的机制,很容易和它混淆;Context Engineering 这个概念也经常在同一个场景里被提到。三者的边界需要说清楚。
ToolContext:请求级的只读注入
toolContext 是在发起 agent.call() 时一次性传入的键值对,工具实现里可以读到它,但写不回去——它是单向的、请求范围内有效的。
// 调用方设置:把当前用户信息注入给工具
RunnableConfig config = RunnableConfig.builder()
.threadId(sessionId)
.build();
// 工具里通过 ToolContext 读取
@Tool("queryUserOrders")
public String queryOrders(String status, ToolContext ctx) {
String userId = (String) ctx.getContext().get("userId"); // 读,不能改
return orderService.query(userId, status);
}
OverAllState 解决的是不同的问题:工具执行完之后,它的结果(或从结果里提取的结构化数据)如何流转到下一个节点。ToolContext 给不了这个——它没有"写回"的能力,也不跨节点存活。
| 维度 | ToolContext | OverAllState |
|---|---|---|
| 生命周期 | 单次 agent.call() | 整个图执行,跨 call 持久化 |
| 读写 | 只读(工具只能读) | 节点和 Hook 均可读写 |
| 数据来源 | 调用方在外部注入 | 节点和 Hook 在执行中写入 |
| 典型用途 | 传入租户 ID、登录用户等执行环境 | 在节点间流转的业务状态 |
Context Engineering:管理"发给 LLM 的 token 里装什么"
Context Engineering 不是具体 API,而是一种设计视角:LLM 的上下文窗口是有限的,要有意识地决定哪些数据进去、用什么形式进去、什么时候清理。
OverAllState 里存着所有数据(messages + 自定义 key),但自定义 key 的数据不会自动进 LLM 上下文——只有 messages 列表才会被 AgentLlmNode 送给模型。Context Engineering 发生在 messages 这条链路上:
OverAllState.messages(原始对话历史)
│
├── SummarizationHook:把旧消息压缩成摘要,写回 messages
│ (改变 State 里存的内容)
│
└── ContextEditingInterceptor:发送前临时删掉旧工具结果
(不改 State,只过滤发出去的内容)
│
↓
最终发给 LLM 的上下文
简单记:ToolContext 是"进场时发的工牌",OverAllState 是"场内共享的白板",Context Engineering 是"决定白板上哪些内容要念给 LLM 听"。
两种更新策略,决定数据命运
往 OverAllState 写数据不是简单的 map.put()。每个 key 必须绑定一个 KeyStrategy,决定新数据进来时如何与已有数据合并:
AppendStrategy:追加。messages 用这个——每轮对话的消息自动累积成完整历史。
ReplaceStrategy:覆盖。业务状态 key 用这个——"当前航班号"只需要最新的那一个,旧的没有意义。
自定义 key 在哪里注册策略? 在 Hook 的 getKeyStrategys() 方法里声明,框架构建时自动收集,不需要在 Builder 上额外配置:
@Override
public Map<String, KeyStrategy> getKeyStrategys() {
// 框架构建时会把这里的 key 和策略注册到 StateGraph schema
return Map.of("llmCallCount", new ReplaceStrategy());
}
工程陷阱:不要在 Hook 里往 messages 写东西
AgentLlmNode 每轮执行后会往 messages 写一条 AssistantMessage,AppendStrategy 把它追加进去。如果你的 Hook 返回的 Map 里也带了 messages key,同样会被 AppendStrategy 追加——同一条消息就进了列表两遍,下次发给 LLM 时 token 翻倍,模型还会看到重复上下文。
// ❌ 错误:Hook 返回 Map 里带 messages,会触发 AppendStrategy 再追加一次
return CompletableFuture.completedFuture(Map.of("messages", List.of(someMsg)));
// ✓ 正确:只观测不写,返回空 Map
return CompletableFuture.completedFuture(Map.of());
// ✓ 正确:需要存业务数据,写自定义 key(ReplaceStrategy,覆盖语义,安全)
return CompletableFuture.completedFuture(Map.of("llmCallCount", count + 1));
Hook 返回的 Map 里有什么,就追加/覆盖什么——这是"写入"语义,不是"更新"语义。读 state 不会改变 state,但只要返回 Map 里包含 messages,就会触发追加。
CheckpointSaver:状态快照与集群问题
有了 OverAllState,还需要解决跨请求的持久化问题。MemorySaver(BaseCheckpointSaver 的默认实现)在每次图执行完毕后,以 threadId 为 key 把整个 OverAllState 做一次快照,下次用同一个 threadId 请求时恢复。
单机没问题,集群必须替换。MemorySaver 是纯 JVM 内存,多实例部署时:
请求1(threadId=abc)→ 节点A → 状态写入节点A内存
请求2(同一threadId)→ 节点B → 节点B内存里没有 → 状态丢失,会话断裂
扩展点是 BaseCheckpointSaver,实现它接入 Redis 或数据库:
public class RedisCheckpointSaver extends BaseCheckpointSaver {
private final RedisTemplate<String, String> redis;
@Override
public Optional<Checkpoint> get(RunnableConfig config) {
String json = redis.opsForValue().get("agent:" + config.threadId());
return json == null ? Optional.empty() : Optional.of(deserialize(json));
}
@Override
public RunnableConfig put(RunnableConfig config, Checkpoint checkpoint) throws Exception {
redis.opsForValue().set("agent:" + config.threadId(), serialize(checkpoint),
Duration.ofHours(24));
return config;
}
@Override
public Collection<Checkpoint> list(RunnableConfig config) { ... }
}
注册时用 .saver() 方法(不是 .checkpointSaver()):
ReactAgent.builder()
.saver(new RedisCheckpointSaver(redisTemplate))
.build();
这意味着 MemorySaver 保存的不只是消息历史,而是完整的运行时状态,包括你所有自定义业务 key 的值。这是它和普通 ChatMemory 的本质区别。
三、Hook:生命周期的控制点
Hook 是图节点,不是回调
这里有一个理解上的关键弯:Hook 不是监听器,不是回调函数,而是以图节点的形式插入到 StateGraph 的执行流程中。
ReactAgent 内部的 StateGraph 执行流是这样的:
图上清楚地展示了四个 Hook 位置(HookPosition 枚举):
| Hook 位置 | 触发时机 | 触发次数 |
|---|---|---|
BEFORE_AGENT | 整个 agent.call() 开始前 | 每次 call 触发 1 次 |
AFTER_AGENT | 整个 agent.call() 完成后 | 每次 call 触发 1 次 |
BEFORE_MODEL | 每次 LLM 调用前 | ReAct 循环内,可能多次 |
AFTER_MODEL | 每次 LLM 调用后 | ReAct 循环内,可能多次 |
重要区分:BEFORE_AGENT/AFTER_AGENT 是每次 agent.call() 只触发一次的"外层钩子",而 BEFORE_MODEL/AFTER_MODEL 是在 ReAct 循环内部触发的,工具调用越多,触发次数越多。
Hook 的返回值会合并回 State
Hook 的方法签名返回 CompletableFuture<Map<String, Object>>——这个 Map 会被合并回 OverAllState,按照各 key 注册的 KeyStrategy 处理。
这是 Hook 比普通日志拦截器更强大的地方:Hook 不只能观测,还能修改运行时状态。
内置 Hook 先用起来:ModelCallLimitHook
先把账单问题解决掉。框架内置了 ModelCallLimitHook,它已经包含了计数和限制两件事,不需要自己写:
ModelCallLimitHook limitHook = ModelCallLimitHook.builder()
.runLimit(5) // 单次 call 最多 5 次 LLM 调用
.threadLimit(30) // 同一会话累计最多 30 次
.exitBehavior(ExitBehavior.END) // 触发时优雅退出,而不是抛异常
.build();
runLimit:单次agent.call()内最多调用几次 LLM。票小蜜改签一个航班,正常不超过 5 次就该结束。threadLimit:同一threadId(整个会话)的累计上限。防止一个用户反复追问导致整体费用失控。
另一个内置 Hook 是 SummarizationHook:当消息历史超过 token 阈值时,自动把旧消息摘要成一条,控制上下文长度。
大多数场景,用这两个内置 Hook 就够了。
什么情况下才需要自定义 Hook?
内置 Hook 解决的是"统一规则":超过 N 次就停,超过 M token 就摘要。但生产环境里往往有和业务状态绑定的差异化逻辑,这是内置 Hook 没法做的。
场景 1:不同用户等级,限制不同
免费用户单次 call 最多 3 次 LLM,VIP 用户最多 10 次。ModelCallLimitHook 的 runLimit 是静态的,没法动态读用户等级:
@Component
public class UserTierLimitHook extends ModelHook {
@Override
public String getName() { return "userTierLimitHook"; }
@Override
public Map<String, KeyStrategy> getKeyStrategys() {
return Map.of("llmCallCount", new ReplaceStrategy());
}
@Override
public CompletableFuture<Map<String, Object>> beforeModel(
OverAllState state, RunnableConfig config) {
int count = (Integer) state.value("llmCallCount").orElse(0) + 1;
// 从 State 读用户等级(由前置节点或 BEFORE_AGENT Hook 写入)
String userTier = (String) state.value("userTier").orElse("free");
int limit = "vip".equals(userTier) ? 10 : 3;
if (count > limit) {
// 写入 jump_to=END,图会优雅退出
return CompletableFuture.completedFuture(
Map.of("llmCallCount", count, "jump_to", "END"));
}
return CompletableFuture.completedFuture(Map.of("llmCallCount", count));
}
}
关键点:这个 Hook 从 OverAllState 里读 userTier——这是自定义 Hook 才能做的事,内置 Hook 没有业务上下文。
场景 2:对接监控系统,而不只是打日志
ModelCallLimitHook 知道调用了几次,但它不会把数据推到 Prometheus 或 SkyWalking。如果你需要按 threadId、用户 ID 统计调用量、计算平均轮次,需要自定义 Hook 把数据写到监控系统:
@Component
public class MetricsHook extends ModelHook {
private final MeterRegistry meterRegistry;
public MetricsHook(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public String getName() { return "metricsHook"; }
@Override
public CompletableFuture<Map<String, Object>> afterModel(
OverAllState state, RunnableConfig config) {
// 每次 LLM 调用完成,向监控系统打点
meterRegistry.counter("agent.llm.calls",
"threadId", config.threadId()).increment();
return CompletableFuture.completedFuture(Map.of()); // 不修改 State
}
}
判断要不要自定义 Hook 的经验规则:
| 需求 | 用内置 Hook | 自定义 Hook |
|---|---|---|
| 超过 N 次停止 | ModelCallLimitHook | — |
| 上下文太长自动摘要 | SummarizationHook | — |
| 不同用户有不同上限 | — | 读 State 里的用户信息,动态决策 |
| 把调用数据推监控系统 | — | afterModel 里对接 Micrometer/SkyWalking |
| 在 State 里存业务数据供后续节点用 | — | 在 Hook 里写 OverAllState |
| 需要访问 OverAllState | — | 只有 Hook 能做 |
四、Hook vs Interceptor:两个层次,两种职责
这是最容易混淆的部分。
图中清楚地展示了核心差异:
| 维度 | Hook | Interceptor |
|---|---|---|
| 作用域 | 图节点级别,跨越整个执行阶段 | 单次模型调用或工具调用 |
| 能否访问 State | 可以读写 OverAllState | 不能,只能看到请求和响应 |
| 能否改变执行流 | 能(写入 jump_to 改变图跳转) | 不能,只能修改请求/响应内容 |
| 触发时机 | 图节点执行前后 | 模型 API 调用前后 |
| 注册方式 | 构建 ReactAgent 时传入 hooks 列表 | 传入 modelInterceptors / toolInterceptors 列表 |
判断用哪个的经验规则:
- 需要访问 OverAllState?→ 用 Hook
- 需要修改 LLM 的请求(prompt、参数)或响应?→ 用 ModelInterceptor
- 需要修改工具调用的入参或结果?→ 用 ToolInterceptor
- 需要控制图的执行流(提前退出、跳转)?→ 只能用 Hook
内置 Interceptor:ContextEditingInterceptor
框架提供了 ContextEditingInterceptor,在消息历史过长时,自动清理旧的工具调用结果(tool_result 类型消息),只保留最近的若干条:
ContextEditingInterceptor contextInterceptor = ContextEditingInterceptor.builder()
.trigger(6000) // 超过 6000 token 时触发
.keep(2000) // 清理后保留最近 2000 token 的内容
.clearAtLeast(1) // 至少清理 1 条旧的工具结果
.excludeTools(List.of("searchKnowledge")) // 这个工具的结果不清理
.build();
excludeTools 是个关键参数:有些工具(比如知识库查询)的结果是整个对话的"参考基础",不能随便清掉。把这些工具加入排除列表,确保它们的结果在整个会话中持续可见。
工程判断:SummarizationHook 和 ContextEditingInterceptor 看起来功能相似,但定位不同。Hook 做的是"把旧消息摘要成新消息,保留语义",Interceptor 做的是"在发送给模型前临时删掉一些内容,不修改 State"。通常两者配合使用,而不是二选一。
五、三层停止控制
Agent 的停止问题比看起来复杂。
ReactAgent 有三层停止机制,优先级从高到低:
第一层:模型主动停止(优先)
模型输出中没有 tool_call,ReAct 循环判断"没有工具要调",正常结束。这是最健康的停止方式——说明模型认为任务已经完成,或者信息不足以继续。
第二层:ModelCallLimitHook 优雅退出(兜底)
达到 runLimit 或 threadLimit,Hook 往 State 写入 jump_to=END,图跳转到结束节点,还能输出一条"已达到调用上限"的提示消息给用户。
第三层:maxRecursion 硬中断(最后保护) StateGraph 本身有一个最大递归深度限制。如果前两层都没有生效(比如 Hook 没有正确注册),这个兜底机制会强制停止,但不会给出友好提示——直接中断。
// 配置 maxRecursion
ReactAgent agent = ReactAgent.builder()
.maxRecursion(20) // 硬上限,一般设置为 runLimit 的 2-3 倍
.hooks(List.of(limitHook))
.build();
工程建议:永远不要只依赖第一层(模型主动停止)——模型有时会陷入循环。永远不要让第三层成为主要停止机制——用户会看到不友好的错误。合理的设置是:第一层作为正常路径,第二层作为优雅兜底,第三层作为安全网。
实践篇:票小蜜 AgentConfig 升级
把前面所有组件组装起来。注意 Hook 的分工:内置 Hook 处理通用规则,自定义 Hook 处理业务差异。
@Configuration
public class AgentConfig {
@Bean
public ReactAgent ticketAgent(
ChatModel chatModel,
List<ToolCallback> tools,
MeterRegistry meterRegistry) throws Exception {
// 内置 Hook 1:统一限流(所有用户都适用的硬上限)
ModelCallLimitHook limitHook = ModelCallLimitHook.builder()
.runLimit(10) // 最高上限,兜底用
.threadLimit(50)
.exitBehavior(ExitBehavior.END)
.build();
// 内置 Hook 2:上下文摘要(控制 Token 费用)
SummarizationHook summarizationHook = SummarizationHook.builder()
.tokenThreshold(4000)
.summaryModel(chatModel)
.build();
// 自定义 Hook 1:按用户等级动态限制(从 OverAllState 读 userTier)
// 适用场景:免费用户和 VIP 用户的调用上限不同
UserTierLimitHook tierLimitHook = new UserTierLimitHook();
// 自定义 Hook 2:对接监控系统(每次 LLM 调用后向 Micrometer 打点)
// 适用场景:需要在 Grafana 上看各 threadId 的 LLM 调用趋势
MetricsHook metricsHook = new MetricsHook(meterRegistry);
// Interceptor:上下文裁剪(发送给模型前临时剔除旧工具结果)
ContextEditingInterceptor contextInterceptor = ContextEditingInterceptor.builder()
.trigger(6000)
.keep(2000)
.clearAtLeast(1)
.excludeTools(List.of("searchKnowledge")) // 知识库查询结果不裁剪
.build();
return ReactAgent.builder()
.name("ticketAgent")
.model(chatModel)
.tools(tools)
.systemPrompt("你是票小蜜,专业的机票客服助手...")
.saver(new MemorySaver()) // 单机;集群部署换 RedisCheckpointSaver
// Hook 顺序:tierLimitHook 先判断,limitHook 兜底,summarizationHook 控长度,metricsHook 收尾打点
.hooks(List.of(tierLimitHook, limitHook, summarizationHook, metricsHook))
.interceptors(List.of(contextInterceptor))
.build();
}
}
两类 Hook 的分工一目了然:
ModelCallLimitHook+SummarizationHook:内置,处理所有 Agent 都适用的通用规则,开箱即用UserTierLimitHook:自定义,因为它需要从 OverAllState 读userTier来做动态判断——这是内置 Hook 做不到的MetricsHook:自定义,因为它要对接外部监控系统——这是业务基础设施集成,框架不可能预置
票小蜜的账单问题现在从两个维度解决:tierLimitHook 按用户等级卡上限(免费用户 3 次,VIP 10 次),limitHook 作为绝对兜底,SummarizationHook 控制单次 call 的上下文长度。
架构演进视角
从第 09 章的"最小闭环"到第 10 章的"可控运行时",架构发生了质变:
左边是第 09 章的起点:3 个核心组件,能跑。右边是本章的终点:7 个组成部分,4 个控制层次。每个标注 NEW 的组件,都对应解决了一个具体的工程问题:
| 新增组件 | 解决的问题 |
|---|---|
| SafeGuardAdvisor | 敏感内容在进入 Agent 之前拦截,不浪费工具调用 |
| UserTierLimitHook | 免费/VIP 用户差异化限流,避免资源被单一用户耗尽 |
| ModelCallLimitHook | 绝对上限兜底,防止模型陷入循环失控 |
| SummarizationHook | 对话历史自动摘要,Token 费用可控 |
| ContextEditingInterceptor | 上下文窗口临时裁剪,不影响 State 持久化 |
| MetricsHook | LLM 调用次数打点,为监控告警提供数据源 |
加了这 6 个组件之后,执行路径确实变长了。但每一步都有了可观测性和可控性——这不是过度工程,而是让 Agent 从玩具变成生产可用组件的必经之路。
评论区聊聊
这四个坑我在实际开发中都踩过,写出来给你做参考。你在接入 ReactAgent 时有没有遇到类似的问题?欢迎在评论区说说你的情况,一起看看是不是同一个根因。
坑 1:messages key 双写导致消息重复
症状:日志里同一条消息出现两次,Token 消耗翻倍。
原因:在两个节点里都往 messages 写了内容,AppendStrategy 把两次写入都追加了。
解决:messages 的写入权归 AgentLlmNode 独占,自定义节点不要直接写 messages,而是写自定义 key。
坑 2:SummarizationHook 和 ContextEditingInterceptor 配置冲突
症状:摘要完的消息在发给模型前又被裁剪了,摘要内容消失。
原因:两者阈值设置不合理,Interceptor 触发后把刚生成的摘要消息也删掉了。
解决:让 Interceptor 的 trigger 阈值高于 Hook 的 tokenThreshold,确保两者不会在同一轮同时触发。
坑 3:Hook getName() 重复导致注册失败
症状:应用启动时抛异常,提示 Hook 名称冲突。
原因:自定义 Hook 没有覆盖 getName() 方法,多个 Hook 用了同一个默认名称。
解决:每个自定义 Hook 都覆盖 getName(),返回唯一的字符串标识。
坑 4:runLimit 和 threadLimit 混淆
症状:某个用户会话触发限制,但短会话用户完全正常,或反过来。
原因:runLimit 是单次 call 的限制,threadLimit 是同一 threadId 的累计限制。
解决:两者配合使用,runLimit 防单次超限,threadLimit 防长期滥用。
你在接入 Hook 或 Interceptor 时有没有遇到这四个之外的问题?或者对 OverAllState 的更新策略有疑问?评论区见。
本文代码仓库:[GitHub 链接](完成项目后补充)
系列目录:[Spring AI Alibaba Agent 实战系列]
上一篇:为什么你的 AI 助手只会回一句话——用 Spring AI Alibaba 实现真正的多步推理 Agent
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区交流。