🛡️ ARK 智能体诊断报告

langchain-ai/langchain#38840 · ChatPerplexity 在构建 Responses 载荷时原地篡改调用方的 extra_body 字典:单次调用参数泄漏进实例,污染后续所有请求 · 2026-07-26

📋

问题摘要

🟠

ChatPerplexity._to_responses_payload (langchain-perplexity)在构建 Responses API 请求体时,直接写入调用方传入的 extra_body 字典,而非拷贝副本。 后果有二:① 调用方自己持有的 extra_body dict 被就地改写; ② 由于 _generate{**self._default_params, **kwargs} 浅合并, 单次调用的临时参数(如 search_mode)会永久沉淀进实例的 model_kwargs,泄漏到之后每一次请求。 无异常、无报错——损坏本身就是 bug。

中危 · 状态污染 / 参数跨请求泄漏
⚠️

影响分析

发生频率高(任何复用实例的多轮调用)

只要同一个 ChatPerplexity 实例被复用、且某次调用传了 per-call 参数,污染即发生——而复用实例正是生产的默认用法。

隐蔽性极高

没有异常、没有堆栈、没有红灯。第 2 次调用没有传 search_mode,载荷里却带着上一次的 search_mode="academic"。行为漂移是唯一症状。

下游风险

检索模式(academic/web)、地域(country)等参数跨请求串味,会导致本应普通检索的请求被静默切成学术检索,或用户 A 的地域设置泄漏到用户 B——在多租户 Agent 网关中即数据/合规越界。

🔬

根因分析

# 复现(无需联网,载荷构建器直接写进了调用方的 dict) llm = ChatPerplexity(pplx_api_key="fake", model="sonar-pro", use_responses_api=True) shared = {"country": "US"} llm._to_responses_payload([], {"extra_body": shared, "search_mode": "academic"}, user_set_keys=set()) print(shared) # → {'country': 'US', 'search_mode': 'academic'} ← 调用方的 dict 被就地改写

根因:载荷构建把 extra_body 引用拿来直接 [key] = value 写入,未做防御性拷贝。叠加 _generate 的浅合并,per-call 参数被写回实例级 model_kwargs

# 修复方向:入口处一次浅拷贝,切断共享引用 extra_body = dict(extra_body) if extra_body else {} # 对写入 extra_body 的每个 per-call 键,均在副本上操作,不回写 self.model_kwargs

这是一个教科书级的"共享可变默认/引用未拷贝"缺陷——与 ARK 已诊断的 #38779(ChatAnthropic.bind_tools 篡改 tool_choice)、#38659(ChatGroq 原地改 token_usage)同一 bug 族

🏆

社区共识:自包含复现,根因无争议

报告者提供了零依赖、可直接运行的最小复现(无需 API Key、无需联网),并主动关联了同类 issue #38779#38659,明确指出这是一个"跨集成反复出现的可变状态篡改模式"。截至诊断时 issue 仍 OPEN(5 条讨论)。

修复本身极简(入口浅拷贝),但价值在于:这是第三次以同样形态出现的 bug——说明缺的不是某处补丁,而是一道"禁止原地篡改调用方入参"的系统性防线。

当同一缺陷族在 Anthropic / Groq / Perplexity 三个集成里各自复发,逐个打补丁只是治标;真正的解法是把"入参不可变"变成可在运行时被强制校验的契约。

⚖️

生产场景:一次调用污染整条会话

场景:多租户检索网关复用单一 LLM 实例

网关为省资源全局复用一个 ChatPerplexity。用户 A 的请求带 search_mode="academic"country="US"——

→ 参数沉淀进实例 model_kwargs

→ 用户 B 本该做普通全网检索,却被静默套上 A 的学术模式与美国地域,返回错误的检索结果,且日志里 B 的请求参数看起来完全正常。

问题本质

这是"静默失败"的经典形态:函数越权修改了调用方拥有的数据,破坏了"传入的参数在调用后保持不变"这一隐性契约。没有崩溃、没有告警,只有随调用次数累积的行为漂移——最难复现、最难归因的一类生产事故。

🛡️

ARK Trust 修复方案

① InputGuard — 入参不可变契约(运行时冻结/校验)

# 调用前对入参快照哈希,调用后校验未被篡改;违约即刻上抛而非静默 @immutable_args("extra_body", "tool_choice") # 覆盖 #38840/#38779/#38659 全族 def call_llm(prompt, **kwargs): ...

② IdempotencyGuard — 隔离 per-call 参数,杜绝跨请求泄漏

# 每次调用在独立参数域内执行,per-call kwargs 绝不回写实例状态 ark.wrap(llm, isolate_call_params=True) # search_mode 只在本次生效

③ OTel Bridge — 参数漂移全链路留痕

ark.params.drift { key: "search_mode", expected: null, actual: "academic", source: "leaked_from_prev_call" }

在生产真实流量上捕获"未传却出现"的参数,把最难归因的行为漂移变成一条可检索的告警。

置信度评估

根因确定性极高(零依赖复现,输出即证据)
修复方案成熟度高(入口浅拷贝,一处可解)
影响广度中高(同族已第三次复发,跨集成)
静默性风险极高(无异常,行为随调用累积漂移)
ARK契合度极高(入参不可变+参数隔离+漂移留痕)
🎯

诊断结论

#38840 的真正信号不是"又一个原地篡改 bug",而是这已经是同一缺陷族第三次复发(#38779 / #38659 / #38840)。逐个打补丁治标不治本——缺的是一道"禁止越权修改调用方入参"的系统性防线。

被污染的是检索模式、地域这类会直接改变输出语义的参数;在多租户网关里,一次调用的参数会静默泄漏到后续所有请求,甚至跨用户串味——无异常、无红灯,只有随调用次数累积的行为漂移。

修复一处很简单(入口浅拷贝),但可靠性不能靠"记得每个集成都别忘了拷贝"。防线必须建在自己这一侧,并且可被机器强制执行。

ARK 的入参不可变契约 + per-call 参数隔离 + OTel 漂移留痕,把"跨集成反复复发的可变状态篡改"从靠人自觉的编码规范,升级为运行时可校验、可观测、可告警的信任基础设施

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