拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

孤儿 tool_call:对账补漏

孤儿 tool_call:对账补漏

「手写中文版 claude code」教学系列:codeAgent 是我从零手写的 claude code 复刻——不套壳不翻译,从界面到 Agent 主循环一行行实现,注释全是中文大白话,适合当 Agent 开发教材从头读到尾。
上一课解决了"合并着端回去";这一课处理端回去之前的对账——压缩和裁剪会把「调用↔结果」拆散,拆散了就是 400。
仓库:https://github.com/Harvil1/codeAgent

什么叫孤儿 tool_call

协议要求严格配对:assistant 发出的每个工具调用(tool_calls 里的 id),后面必须跟着同 id 的工具结果;反过来,工具结果前面必须有发起它的调用。

压缩、裁剪、恢复会话——任何从中间动消息的操作,都可能把一对搭档切成两半:

  • 正向孤儿:调用了,结果没了(被裁掉)
  • 反向孤儿:结果还在,发起它的调用被裁了

两种都让 API 直接 400,报错信息还不直说是配对问题。

对账函数(源码)

agent/context_compressor.py:

def_fix_tool_call_pairs(messages:list)->list:"""修复压缩切坏了的「工具调用 ↔ 工具结果」配对。 处理两类问题: - 正向孤儿(调用了但没结果):在 assistant(tool_calls) 后面的结果序列末尾 补一条假结果(内容注明「因压缩丢失」)。⚠️ 位置必须紧贴结果序列末尾, **不能**补到整个消息列表最后——中间隔了别的消息照样违反「紧跟其后」 (Anthropic 查得最严),还是 400 - 反向孤儿(有结果但前面没发起它的调用):直接删掉 (压缩边界或上游 bug 都可能造出这种孤儿) """# 反向孤儿按「截至当前位置见过哪些 id」判断(逐步累加);# 用全量集合会漏判错序——结果 B 出现在发起 B 的调用之前也会放行# (B 已在全量集合里),导致 API 400。seen_so_far=set()...

这个函数在我项目里是压缩流水线的固定收尾动作——七层压缩不管哪层动了刀(昨天第 3 篇的 compress_if_needed),最后统一过一遍对账。紧急压缩(第 2 篇的 reactive_compact)砍完消息也调它。

注释里藏的两个深坑

坑一:补假结果的位置。正向孤儿补结果,直觉是补到消息列表末尾——错。协议要求结果紧跟assistant 的调用序列,中间隔了别的消息照样 400。必须紧贴该轮结果序列的末尾补。

坑二:判断反向孤儿不能用全量集合。我一开始的写法是先收集全部 tool_call id 再查结果——会漏判"结果出现在发起它的调用之前"这种错序(全量集合里已经有这个 id,检查放行,实际顺序是非法的)。正确做法是逐步累加的seen_so_far:走到哪查到哪,错序当场现形。

设计启示

把它做成独立纯函数 + 流水线固定收尾,而不是散在各处打补丁,好处是:任何新加的"动消息"操作(未来的压缩层、会话编辑功能)自动获得对账保护,忘了调用手动刀的风险归零。

小结

  1. 动历史必产生孤儿配对,孤儿必 400——对账是压缩的标配收尾
  2. 补结果的位置要紧贴调用序列;判孤儿要用增量集合防错序漏判
  3. 收尾动作做成统一纯函数,新操作自动继承保护

下一篇:数硬币别占收银台——asyncio 的 to_thread 与 contextvars 一坑两命。


仓库在这,注释全中文,欢迎 Star ⭐:https://github.com/Harvil1/codeAgent

标签:PythonasyncioAgent大模型API

返回列表