工具重入:你的函数调用层尚未察觉的 Bug 类别
智能体用 400 毫秒回答了一个简单的问题,然后因递归限制错误(recursion-limit error)崩溃。Trace 显示了 25 次工具调用。从上到下阅读 Trace,工程师会得出结论:智能体糊涂了 —— 以略有不同的顺序反复调用那几个工具,始终无法收敛。这个结论是错误的。智能体并没有糊涂。它陷入了一个死循环:工具 A 调用了模型,模型选择了工具 B,工具 B 的实现再次调用模型来格式化其输出,而格式化程序又选择了工具 A。Trace UI 将四个嵌套调用渲染为扁平列表中的四个兄弟调用,导致唯一能发现问题的开发者也无法察觉这个循环。
这就是工具重入(tool reentrancy),这是一种你的函数调用层几乎肯定没有建模的 Bug 类别。并发安全的代码对此已有数十年的原语支持:记录同一线程嵌套获取次数的重入互斥锁(reentrant mutexes)、语言层面的递归限制、堆栈检查 API,以及一种文化共识:任何回调运行时的函数都需要一个明确的契约,规定允许何种重入。工具调用层默认采用“发后即忘”(fire-and-forget)模式。运行时没有可供检查的调用栈,调度前没有循环 检测器,工具定义上没有重入属性,Trace UI 的形式像日志而非图。结果就是,任何超过十几个条目的工具目录都会悄悄变成框架无法察觉的递归。
隐藏在“成功”工具调用中的故障模式
团队在生产环境中发现工具重入的方式很少是“智能体循环了”。那是明显到足以进入事后分析的故障。更常见的情况是:请求成功完成,但成本是成本模型预测的三到四倍,由于最终输出正确,评估套件得分正常。循环发生了,模型消耗了 Token 来发现它,当外部轮次预算耗尽时循环中断,由于模型擅长从自身的困惑中恢复,答案最终还是输出了。账单是该 Bug 显现的唯一地方。
LangGraph 默认的 25 次递归限制正是为了捕捉这种情况而设置的 —— 并非因为 25 对于复杂工作流是一个合理的超级步(supersteps)数值,而是因为它是一道防止智能体停不下来的防线。错误消息告诉你循环撞墙了,但它没告诉你原因。生产团队看到 GraphRecursionError 后的第一反应通常是提高限制。这把一个喧闹的 Bug 变成了一个安静的 Bug。循环依然在发生,只是在放弃之前运行得更久了。DeepAgents 用户报告称,子代理会静默继承默认限制,而无视父级的配置。因此,一个配置了 50 个超级步的父 图生成的子图会在达到 25 次时崩溃,且不向调用者显现正确的原因。循环和限制发生在不同的地方,诊断也是如此。
最近的文献开始指出一种更丑陋的变体:隐藏循环,在这种情况下,轨迹在结构上看是非循环的 —— 不同的工具、不同的参数、不同的检索文档 —— 但智能体实际上正在重新访问相同的逻辑状态。对调用栈的结构化分析完全无法发现这些。只有当你能比较连续状态的语义内容时,它们才会显现,而大多数可观测性工具并不具备这种能力。
应该叫“重入”,而非“递归”
将此称为“递归”会将其与智能体合法地将任务分解为子任务并向自身寻求帮助的情况混为一谈。那种递归是没问题的。ReDel 和更广泛的递归语言模型文献展示了一些有用的模式:父代理生成子代理来处理复杂的子问题,且调用树是格式良好且有界的。而那些病态的情况并不是格式良好意义上的递归。它们是重入。
重入(Reentrancy)是系统编程中的一个属性:问题在于一个函数是否可以在其前一次调用完成之前安全地再次进入。可重入函数是指可以这样做的函数。一个不可重入函数被以重入方式调用就是 Bug。系统编程领域通过给每个函数一个明确的答案来解决这个问题:“如果我在前一次调用仍在栈中时再次被调用,会发生什么?”有些函数是安全的(纯函数、设计良好的库代码)。有些函数需要重入互斥锁(reentrant mutex)来计算同一调用者的嵌套获取次数。有些函数明确是不可重入的,运行时应该捕捉到这种违规。
智能体框架中的工具没有在任何地方声明这一属性。框架不知道在工具 A 执行期间模型再次选择工具 A 是否安全。这里没有注解,没有强制执行,最常见的运行时行为是像第一次调用一样调度该调用。如果工具 A 持有外部状态(一个开启的事务、一个分页游标、一个部分写入的文件),以重入方式调用它所产生的 Bug,其堆栈轨迹可能位于三跳之外的另一台机器上。
Trace UI 正在通过遗漏隐瞒事实
大多数智能体(Agent)观测平台将工具调用渲染为 Span 的层级树,在智能体只是简单的“模型-工具-模型”扁平循环的情况下,这种方式效果很好。层级结构会退化为一个扁平列表,时间轴视图能提供你所需的一切信息。
它无法处理的情况是:一个工具的实现再次调用了模型,而模型又选择了另一个工具。那个嵌套的模型调用在逻辑上是该工具的子节点,而第二个工具则是第一个工具的孙节点。大多数追踪库要么将整个过程扁平化 为外部智能体循环的兄弟节点(因为内部模型调用使用了相同的客户端,且追踪上下文没有传播),要么在技术上正确地渲染了父子关系,但其 UI 是为浅层树优化的,深层嵌套会超出屏幕右侧并在视觉上丢失。
- https://en.wikipedia.org/wiki/Reentrant_mutex
- https://en.wikipedia.org/wiki/Reentrancy_(computing)
- https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT
- https://github.com/langchain-ai/langgraph/discussions/1725
- https://github.com/langchain-ai/langgraph/issues/6731
- https://github.com/langchain-ai/deepagents/issues/1698
- https://arxiv.org/html/2511.10650
- https://arxiv.org/html/2408.02248v1
- https://www.braintrust.dev/articles/agent-observability-tracing-tool-calls-memory
- https://www.langchain.com/articles/agent-observability
