生产环境中的工具调用:循环、陷阱与实战方案
当你的智能体在放弃之前,第三次默默地重试同一个损坏的工具调用时,你就会意识到,“仅仅添加工具”并不是一种生产环境的策略。工具调用解锁了真正的能力——外部数据、副作用、保证格式的输出——但使其工作的智能体循环(agentic loop)具有在演示中不会表现出来的尖锐边缘。
这篇文章将探讨这些边缘:循环实际上是如何运行的,悄悄破坏并行执行的格式规则,如何编写能让模型做出正确选择的工具描述,以及如何处理错误以让模型恢复而不是陷入死循环。
契约:运行的是你的代码,而不是模型
理解工具调用的基础是:模型从不执行任何操作。它只发出结构化的请求。由你的代码执行操作。你返回结果。这并非细节——这就是整个 架构。
当你在 API 请求中提供工具时,模型会评估是否需要工具,如果需要,它会返回一个 stop_reason: "tool_use" 的响应,以及一个包含工具名称和 JSON 参数的 tool_use 块。你的应用程序读取该块,分发到正确的函数,并在下一轮对话中通过 tool_result 块发回结果。然后模型继续运行——可能会调用更多工具,或生成最终答案。
这创建了一个客户端循环:
- 发送包含工具定义的请求
- 如果响应
stop_reason == "tool_use",执行请求的工具 - 在包含
tool_result块的新用户消息中返回结果 - 重复此过程,直到
stop_reason变为"end_turn"或其他终止值
这里的简单性具有欺骗性。生产环境中几乎每一个工具调用的问题都可以追溯到这个循环中出现的错误:结果格式不正确、错误被隐藏而不是暴露,或者循环在没有边界的情况下运行。搞定循环,剩下的就水到渠成了。
并行工具调用:破坏一切的格式规则
现代模型可以在一轮对话中请求多个工具——例如,同时获取三个城市的实时天气,而不是一个一个地获取。这对延迟至关重要。四个连续的网络调用,每个耗时 200 毫秒,总共需要 800 毫秒;而并行执行可以将时间缩短至 200 毫秒。
但是,只有在你正确格式化结果的情况下,并行工具执行才能保持并行。规则很严格:来自单次模型轮次的所有工具结果必须在单条用户消息中返回。
如果你将它们拆分为多条消息,对话历史看起来就像是顺序交换——一个请求,一个结果,一个请求,一个结果——模型会从该历史记录中学习到它应该一次只进行一个工具调用。你不会收到错误。随着时间的推移,当模型适应你展示给它的模式时,它只会悄悄地变得效率低下。
正确的格式如下:
[assistant turn] → [tool_use_1, tool_use_2, tool_use_3]
[user turn] → [tool_result_1, tool_result_2, tool_result_3] // 单条消息
错误的格式:
[assistant turn] → [tool_use_1, tool_use_2]
[user turn] → [tool_result_1] // ❌ 拆分的消息
[user turn] → [tool_result_2] // ❌ 拆分的消息
还有一个次要 限制:在包含工具结果的用户消息中,tool_result 块必须出现在任何文本之前。先放文本会导致 400 错误。当人们试图在结果上方添加类似“以下是结果:”的上下文时,往往会措手不及——这些文本需要放在后面,而不是前面。
如果你的应用程序手动驱动智能体循环,请对照这两条规则检查你的消息组装代码。它们是生产环境工具调用系统中静默降级最常见的来源。
编写真正奏效的工具描述
模型主要根据你的工具描述来决定调用哪个工具,以及是否调用工具。Schema 的正确性很重要,但它不是限制因素。描述才是。
像 "Gets the stock price for a ticker" 这样的描述会让模型面临悬而未决的问题:这涵盖了哪些交易所?如果代码(ticker)无效,它会返回什么?我应该用它来查询历史价格还是仅查询当前价格?当模型不确定时,它要么猜错,要么完全避开该工具。
一个奏效的描述会明确回答这些问题:
获取给定股票代码的当前股价。代码必须是美国主要证券交易所(纽约证券交易所或纳斯达克)上市公司的有效代码。返回以美元计价的最新成交价。当用户询问特定股票的当前或最近价格时使用此工具。它不会返回历史价格、期权数据或公司基本面。
最后一句——工具不返回什么——通常是最重要的部分。它告诉模型什么时候不要调用该工具,从而防止模型为相关但不同的任务调用错误的工具。
一些在实践中减少误用的额外 描述原则:
- 描述边缘行为。 如果工具针对未知输入返回空结果而不是错误,请说明。模型需要知道“无结果”是什么样的。
- 为相关工具使用命名空间。 如果你有多个跨越不同服务的工具,请加上服务名称前缀:
github_list_prs、slack_send_message、db_query_users。随着工具集的增长,这使工具选择变得清晰明确。 - 合并相关操作。 与其将
create_pr、review_pr和merge_pr创建为独立的工具,不如考虑使用带有action参数的单个github_pr工具。更少的工具可以减少选择歧义,并使接口范围更容易导航。
对于具有复杂输入(嵌套对象、显著改变行为的可选参数、格式敏感的字符串)的工具,请添加具体的示例。示例输入随 Schema 一起包含在提示词中,为模型提供了一个可遵循的模式,而不是让它仅凭 Schema 来推导结构。
错误处理:为模型提供足够的恢复上下文
当工具失效时,你有两个选择:返回错误并让模型进行调整,或者忽略它并让模型陷入困惑。正确的选择几乎总是返回错误。
其机制是 tool_result 块中的 is_error: true 标志。当你的工具抛出异常、触发速率限制、收到上游 HTTP 错误或因其他原因未能产生有效结果时,请设置此标志。模型会读取错误并调整其行为——使 用不同的参数重试、尝试不同的工具,或者向用户展示失败情况。
你在错误消息中放入的内容决定了模型能否恢复。像 "failed" 这样通用的消息无法为模型提供任何帮助。而具有指导意义的消息则可以:
"Rate limit exceeded. Retry after 60 seconds."—— 模型知道需要等待,而不是立即再次尝试"Location not found. Try a more specific city name or include the country."—— 模型知道需要细化输入"Database connection timed out after 5s. The query was: SELECT * FROM orders WHERE user_id = 123."—— 同时提供了失败模式和足够的重试上下文
当工具输入无效时——例如缺少必填参数、类型错误——你可以返回带有验证失败信息的 is_error: true 结果,模型通常会在放弃前尝试 2-3 次修正。如果你想彻底杜绝无效调用,请在工具定义中使用 strict: true。严格模式会精确强制执行你的 JSON schema,因此模型要么生成有效的调用,要么根本不调用工具。任何格式错误的输入都不会到达你的应用。
何时不使用工具
每一次工具调用都至少意味着增加一次额外的 API 往返。对于发出网络请求的工具,你正在累积延迟:模型轮次、你的网络调用以及另一个用于响应的模型轮次。如果任务很轻量,这些开销可能会超过工作本身。
在以下情况下不适合使用工具:
- 模型可以通过训练数据回答。 摘要、翻译、常识性问题。在这里添加工 具只会增加延迟,没有任何好处。
- 交互是没有任何副作用的单次问答。 如果没有什么需要获取或执行的,工具就无用武之地。
- 你正在使用工具从模型输出中提取结构。 如果你发现自己在解析模型的自由文本响应以提取决策,那么该决策原本应该是一个工具调用——但解决方案是重构,而不是在上面添加一个解析工具。
过度使用工具的征兆是:你有一个名为 provide_answer 或 return_result 之类的工具,它除了接收来自模型的结构化输出外什么都不做。这种模式确实存在,并且解决了一个实际需求(保证输出形状),但它应该被结构化输出模式或具有专门构建的 schema 的 tool_choice: {"type": "tool", "name": "..."} 所取代。不要为了获取 JSON 响应而创建一个伪工具——请使用为此设计的 API 功能。
循环需要边界
最后一个生产环境关注点:智能体循环必须终止。一个不断调用工具——遇到错误、重试、遇到不同错误——的模型,如果你的应用不停止它,它可能会无限期运行。
在开始之前,设置一个最大迭代次数。对于大多数工作流,5 到 10 个轮次是合理的;复杂的调研或编码任务可能需要更多。当达到限制时,返回你已有的内容或显式地展示失败。无边界运行会有产生意外 API 费用、下游速率限制以及因占用资源而不释放导致会话卡死的风险。
单个工具调用的超时同样重要。一个挂起 30 秒的外部 API 不仅仅会减慢一个请求——它 还会占用对话状态和任何等待它的 UI。请设置单次调用超时,返回错误,并让模型决定是重试还是在没有该数据的情况下继续。
工具调用是你可以添加到 LLM 应用中的最高杠杆功能之一。在复杂的基准测试中,即使是基础工具也能带来巨大的能力提升。但这种杠杆作用只有在循环健全时才会显现:结果格式正确、描述编写精确、返回的错误带有足够的恢复上下文,并且设置了边界以防止失控执行。将智能体循环视为它本质上的异步状态机,大多数棘手的问题都会消失。
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools
- https://www.anthropic.com/engineering/building-effective-agents
