Claude API 在请求之间不记任何东西。每次 API 调用都是全新的开始。如果你不发完整的对话历史——包括之前所有的 tool result——模型不知道自己做过什么、发现过什么、当初为什么调用那些工具。
正确的格式
模型请求工具调用后,你执行工具并把结果追加到对话历史:
- 角色:
"user"(不是 “assistant”——tool result 来自 client 端) - 内容类型:
"tool_result",带匹配的tool_use_id - 然后:在下一次 API 请求中发送完整的 message 数组(所有之前的消息 + 这个新的 tool_result)
如果模型在一条响应中发起了多个工具调用(比如 get_customer 和 get_order),把所有结果放在一条 user 消息里,包含多个 tool_result content block,每个带各自匹配的 tool_use_id。不是多条 user 消息——一条消息,多个 content block。
“模型忘了所有事”的 bug
典型症状:模型无视 tool result,重复发起它已经调用过的完全相同的工具调用。这种现象是一致的,不是间歇性的。
根因:tool_result 根本没有被追加到 messages 数组。API 是无状态的——从模型视角看,它请求了工具但从未收到输出。所以它再请求一次。又一次。无限循环。
这是 agentic 循环中最常见的实现 bug。任何”模型在工具调用后丢失上下文”问题的第一个排查步骤:确认完整的对话历史是否随每次请求一起发送。
工具错误也是 tool result
工具执行失败时(非零退出码、API 超时、权限不足),把错误作为 tool_result 送回去,带上错误详情(MCP 场景下带 is_error 标记)。继续循环。模型需要看到失败——它可以换参数重试、换个工具、或者报告问题。
在工具失败时终止循环,剥夺了模型适应的能力。悄悄吞掉错误,让模型等一个永远不会来的结果。
管理不断增长的历史
一个 30+ 次工具调用的会话,每次返回约 2,000 token,历史会膨胀到巨大。某个点上它会超出上下文窗口。三种策略:
追加前精简:从冗长的工具结果中只提取相关字段。一个 3,000 token 的客户记录可能只有约 200 token 的相关数据(姓名、订单号、状态)。这减少了后续所有请求的重传成本。某生产系统用这种方式把 tool-result token 成本降低了 65%。
渐进式摘要:较旧的 tool result 被压缩成精简摘要,近期的保留完整细节。模型以更低的 token 成本保持对早期发现的感知。
持久发现块:维护一个”关键发现”或”分析结果”区块,累积关键结论。即使旧的 tool result 被摘要化,关键结论仍然存在。这防止模型在上下文被压缩后和自己之前的结论矛盾。
反模式:直接丢掉旧的 tool result。模型丧失对早期发现的感知,可能重新请求相同的工具,并有和自己之前结论矛盾的风险。
一句话总结: Tool result 放在 role: "user" 里并匹配 tool_use_id,每次请求必须发完整历史(无状态 API),错误也是结果——冗长的输出在追加前先精简。