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 本身的能力只是一部分围绕它的状态管理、消息流转、异常处理才是真正决定系统是否可靠的关键。