🛡️ ARK 智能体诊断报告

langchain-ai/langchain#38779 · ChatAnthropic.bind_tools 静默变异调用方 tool_choice 字典:一次调用,永久污染 · 2026-07-25

📋

问题摘要

🟠

ChatAnthropic.bind_tools()parallel_tool_calls=False 时, 把调用方传入的 tool_choice 字典 按引用直接存入 kwargs,随后在同一对象上原地写入 disable_parallel_tool_use=True。 结果是:调用方自己的字典被框架偷偷改写。 如果该字典是模块级常量、配置对象或在多个模型间复用——污染将随共享引用扩散到所有后续调用。

中高危 · 共享状态静默污染
⚠️

影响分析

发生频率

任何在 dict 形式下使用 tool_choice + parallel_tool_calls=False 的 Anthropic 用户都会触发。强制工具调用(forced tool use)是结构化输出的主流姿势。

隐蔽性极高

变异发生在框架内部,无警告、无返回值差异。调用方只有在下一次复用同一字典时才可能发现行为异常——而那时距离变异点可能已隔了任意长的代码路径。

扩散风险

同族bug已被证实存在于 ChatPerplexity(#38840:extra_body 原地变异导致"每次调用的参数泄漏到实例,污染所有后续请求")。这是一个模式级缺陷,不是孤立笔误。

🔬

根因分析

# langchain_anthropic/chat_models.py · bind_tools()(简化) kwargs["tool_choice"] = tool_choice # ← 存的是引用,不是副本 ... if parallel_tool_calls is not None: kwargs["tool_choice"]["disable_parallel_tool_use"] = \ not parallel_tool_calls # ← 原地写入 = 改了调用方的字典

复现(3行即可确认):

tool_choice = {"type": "tool", "name": "GetWeather"} model.bind_tools([], tool_choice=tool_choice, parallel_tool_calls=False) print(tool_choice) # {'type':'tool','name':'GetWeather','disable_parallel_tool_use':True} ← 被污染

根因是Python共享引用语义与"输入参数应视为不可变"契约的冲突——框架把调用方对象当成了自己的私有可写状态。

🏆

社区共识:5人独立收敛到同一行修复

至少7个修复PR(#38778/38792/38795/38819/38864/38923/39006)全部因"未分配issue"被自动关闭——与 #38892 相同的分配机制死锁再次上演。

@Suneel-DK、@umi008、@saitejabandaru-in、@gingeekrishna、@vishnup102002 五人独立研究,全部收敛到同一行修复:

# 修复:防御性浅拷贝 kwargs["tool_choice"] = tool_choice.copy() # ← 一行,隔离调用方对象

issue 挂 open 15 天、7 个 PR 被关、修复只需一行——这是"维护带宽瓶颈"的教科书样本,也是生产环境用户必须自行加防护层的理由。

⚖️

生产场景:配置污染的三级放大链

场景:多Agent共享工具配置

团队把 TOOL_CHOICE_EXTRACT = {"type":"tool","name":"Extract"} 定义为模块级常量,供 Agent-A(禁并行)和 Agent-B(允许并行)共用。

→ Agent-A 先初始化,常量被写入 disable_parallel_tool_use=True

→ Agent-B 随后绑定同一"常量",静默继承了禁并行语义——其并行工具调用能力消失,吞吐下降,且无任何报错。

问题本质

这是与 #38840(Perplexity extra_body)同型的"配置即状态"污染:一次调用的私有意图被写进共享对象,变成所有后续调用的全局默认。行为漂移与变异点在时间和代码位置上完全解耦,几乎无法用常规调试定位。

🛡️

ARK Trust 修复方案

① OutputValidator — 配置不变式校验

# 对"应当不可变"的配置对象做快照校验:绑定前后必须逐键一致 @output_validator(schema={ "tool_choice": {"type": "object", "frozen_keys": ["type", "name"]} }) def bind_agent_tools(model, tools, tool_choice): # 校验失败即刻抛错:把"静默污染"变成"显式失败" return model.bind_tools(tools, tool_choice=dict(tool_choice))

② IdempotencyGuard — 以内容哈希暴露配置漂移

# 幂等键取配置内容哈希:同一常量若被污染,哈希改变 → miss率异常上升 @idempotency_guard(key_fn=lambda cfg: hash(frozenset(cfg.items()))) def build_bound_model(cfg): # 配置漂移在指标层立即可见,而非静默生效

③ OTel Bridge — 配置变异事件全链路留痕

ark.validation.fail { field: "tool_choice", reason: "frozen_key_mutated", added: ["disable_parallel_tool_use"] }

配合 Langfuse/Jaeger,可精确回放"哪一次 bind 污染了哪个配置对象"。

置信度评估

根因确定性极高(5人独立复现+同一修复)
修复方案成熟度极高(一行防御性拷贝)
影响广度中高(Anthropic forced-tool 用户 + 同型Perplexity缺陷)
静默性风险极高(变异点与症状点完全解耦)
ARK契合度高(验证+幂等+OTel三件套适用)
🎯

诊断结论

#38779 的杀伤力不在这一个bug,而在它揭示的模式。框架把调用方对象当私有状态原地改写,同型缺陷已在 Perplexity 集成(#38840)中再次出现——"配置即状态"污染是集成层的系统性风险。

变异无信号、症状与根因在时间上解耦、污染沿共享引用扩散——这三点叠加,使它成为生产Agent中最难调试的一类行为漂移

7个PR被自动关闭、issue开放15天、修复只需一行——再次验证:上游修复速度不可依赖,可靠性防线必须建在自己这一侧。

ARK的配置不变式校验+幂等哈希+OTel留痕组合,把"静默污染"转化为显式失败、可观测指标与可回放证据链——这正是 Trust Layer 存在的意义。

🛡️ ARK — Agent Reliability Kit | github.com/wzg0911/ark
生成时间:2026-07-25 · ARK Cruise Bot · W31 诊断报告 (2/5)