Spring AI 工具调用踩坑实录:Think-Execute 模式下的状态一致性问题
最近在使用 Spring AI 框架做 Agent 开发时,遇到了一系列围绕工具调用(Tool Call)的坑。从 Think 阶段的记忆丢失,到消息持久化与实时推送的矛盾,再到刷新页面导致的状态错乱——每一个问题都指向同一个核心命题:如何在 LLM 应用中保证状态的干净与一致。这篇文章把我的排查和解决思路完整记录下来,希望对同样在用 Spring AI 的朋友有所帮助。
一、Think-Execute 模式下的工具调用异常
问题现象
在使用 Spring AI 框架时,我发现它的工具调用在大部分情况下是正常的,但极端场景下会暴露一个问题:
模型在 Think 阶段的默认情况下,会在 think 阶段决定工具调用,这一步会把之前的工具调用的消息存入上下文中。但如果在 Execute 阶段系统发生了崩溃或中断,就会导致记忆里多写出一条"只有调用请求、却没有返回结果"的消息。等系统恢复后,大模型再次从上下文中取回历史记录时,面对这种不完整的状态,模型在格式检查上就过不去,直接报错。
解决方案
核心思路是:取消 Think 阶段对动态管理执行权力的实现,延迟写入,保证原子性。
具体做法是,禁止在 Think 阶段就马上持有记忆,而是把记忆的持久化动作延迟到 Execute 阶段的最后一步——当工具调用执行完毕(无论成功还是失败),浏览器会在默认条件下把结果信息返回大模型,让大模型出最终结果。此时,才把模型的调用意图和工具执行结果一起成对地、原子性地更新到记忆当中。
为什么这样设计
这种设计就像数据库的事务一样:要么全部成功,包含完整的两条消息(调用请求 + 返回结果);要么遇到系统性的问题就直接回滚,什么都不写入。
这样做有两个好处:
- 兼容了框架层面的自纠正能力。 即使在 Execute 阶段真的出现崩溃,由于 Think 阶段的调用意图还没有被持久化,上下文不会被"脏数据"污染。
- 在应用层上加了一道保险。 绝对保证了 LLM 上下文状态的干净和一致性。
二、消息持久化与实时推送之间的矛盾
问题背景
紧接着上面的问题,我又遇到了另一个纠结点:用户体验与数据一致性之间的冲突。
如果为了保证数据绝对干净,非要等到工具全部 Execute 结束才把消息加载入库、再统一返回给前端,那么在这个(可能很漫长的)等待过程中,用户界面就是卡死的——没有任何反馈,体验极差。但如果我们为了体验,立刻把大模型的调用意图写入上下文,一旦后续出错,又会造成前面提到的脏数据问题。
解决方案:数据库模型重新设计
为了同时解决这两个问题,我重新设计了数据库模型和消息流转机制。
我设计了三张表:
- Agent 表:记录 Agent 的基本信息。
- Chat_Session 表:会话表,每个会话绑定一个 Agent。
- Chat_Message 表:记录一段会话中所有的语义片段,绑定
session_id。
关键的改动是,在 Chat_Message 表中引入了一个状态机制,包括三个状态:
| 状态 | 含义 |
|---|---|
PENDING |
消息已创建,等待工具执行 |
EXECUTING |
工具正在执行中 |
COMPLETED |
工具执行完成,消息完整可用 |
整个流程
1. 即时反馈
当大模型在 Think 阶段输出工具调用后,我们立即把消息推送到前端展示给用户(比如展示"AI 正在调用 XX 工具……"的状态提示),同时消息落库,状态标记为 PENDING。
2. 状态流转
进入 Execute 阶段,状态更新为 EXECUTING(执行中)。
3. 事务性闭环
当工具调用执行完毕后,需要在一个事务中完成以下操作:
- 更新对应的执行结果消息,推送终态给前端。
- 把状态字段修改为
COMPLETED。 - 持久化工具调用信息到数据库,并且修改状态字段为执行完毕。
这里有一个重要的设计考量:把上面的两步数据科操作抽取为一个新方法,并加上 @Transactional 事务注解,来实现原子性操作。注意,只能包含这两步数据库操作,不能有工具调用部分。 避免主事务太长,打满数据库连接池。
之后再把这步消息推送到前端。
大模型上下文的加载过滤
最后也是最核心的一步:大模型上下文向加载过滤。
当下一次大模型拼装上下文时,在 SQL 查询层面加一个过滤条件,只读取状态为 COMPLETED 的消息。通过这样设计,如果真的出现了 Execute 阶段崩溃,也不会污染上下文。
三、严重 Bug:用户刷新页面的问题
问题现象
上面的方案上线后,很快暴露了一个严重的 Bug——用户刷新页面。
由于前端的逻辑,刷新后会重新加载"完成的"消息。但如果用户在某条消息"正在执行"的时候刷新了页面,那么这条消息就会从前端丢失(因为它还不是 COMPLETED)。更麻烦的是,后端工具其实已经执行完了,但前端不知道——又没有再次刷新的话,就永远看不到完整的结果。
解决方案
后端侧:
- 引入
FAILED状态。 前端或后端在每次拉取消息向接口时,增加一层逻辑判断:遍历所有处于EXECUTING状态的数据,如果发现其创建时间距离现在已经超过了 5 分钟(可配置的超时阈值),直接将其修改为FAILED。 - 加上 Session 级别的乐观锁。 防止多个请求因并发而重复执行同一条消息的工具调用。
前端侧:
前端也需要特殊处理。当前端发现一个 F5 刷新后底层 TCP 连接已经断开了,后端的工具调用执行完毕后,向已关闭的连接推送消息时会出现 IO 异常。此时需要用 try-catch 包裹推送逻辑,捕获异常后将消息透过备选写入数据库落库,保证后端一致性。这样用户再次刷新时,就能从数据库中拿到完整的数据。
总结
这一系列问题的本质,都是在 LLM 应用中如何处理异步工具调用的状态一致性。几个核心原则:
- 写入要原子。 调用意图和执行结果必须成对写入,不能只写一半。
- 推送要即时。 用户体验不能为了数据一致性而牺牲,通过状态机来兼顾两者。
- 过滤要严格。 大模型的上下文加载必须只读取已完成的消息,避免脏数据污染。
- 异常要兜底。 超时检测、乐观锁、IO 异常捕获——每一层都要有自己的防御机制。
在 AI Agent 的工程化落地中,LLM 本身的能力只是一部分,围绕它的状态管理、消息流转、异常处理才是真正决定系统是否可靠的关键。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)