🛡️ ARK 智能体诊断报告

langchain-ai/langchain#39163 · trace_as_chain_group 在任务取消时把 run 永久留在 pending · 2026-07-31 · W32 诊断 8/5(超额)

📋

问题摘要

🔴

atrace_as_chain_group() / trace_as_chain_group() 的上下文管理器只 except Exception。 而 asyncio.CancelledErrorKeyboardInterrupt 直接继承 BaseException—— 于是任务取消同时绕过 on_chain_error()on_chain_end()。 已 start 的 run 永远拿不到终态事件,在 LangSmith 里无限 pending。 ASGI / WebSocket 应用中客户端断连即取消请求任务,这是生产环境的日常事件,不是边缘情况。

F3 族第 5 例 · 新亚型:失败伪装成「什么都没发生」 同模块内 runnable 辅助函数已 catch BaseException(内部不一致) ARK 实机离线复现 ✅ 4/4

影响范围

langchain-core 1.5.3 · 所有使用 chain group 追踪的 async 服务(FastAPI/WebSocket/流式接口)

框架现状

⚠️ open(2026-07-31 00:45 UTC 提交)· 无 assignee · labels: bug/core/external

ARK 方案

✅ Trace 终态不变式 + CircuitBreaker 取消语义归一

🔍

根因定位(P2 Probe)

缺陷是「异常基类选窄了」,但真正的教训是可观测性组件自身没有终态保证

  1. 两个上下文管理器(trace_as_chain_group / atrace_as_chain_group)用 except Exception as e: 包裹 body;
  2. 但它们调用的 group manager on_chain_error() 签名本身接受 BaseException——能力具备,入口卡死;
  3. 更关键:同一个 manager.py 文件里的 runnable 回调辅助函数已经 catch BaseException——同模块内两种写法并存,属于纪律漂移而非设计取舍;
  4. 结果:run 只有 on_chain_start,没有任何终态回调。异常本身正常向上抛(不吞异常),所以调用方毫无感知——排障时只看到 LangSmith 里一堆挂起的 trace。
# langchain_core/callbacks/manager.py @asynccontextmanager async def atrace_as_chain_group(...): ... try: yield run_manager except Exception as e: # ← CancelledError 是 BaseException,漏网 await run_manager.on_chain_error(e) raise else: await run_manager.on_chain_end({}) # 同文件内 runnable 辅助函数(对照组,写法正确): except BaseException as e: ...

ARK 离线确定性复现(无 API key、无网络): 装入记录型 CallbackHandler 统计终态事件数,四例对照锁死根因—— A(async + CancelledError)与 C(sync + KeyboardInterrupt)均为 started=1, ended=0, errors=0 → run 孤儿化; B/D 对照组(普通 ValueError)终态回调正常触发 errors=1,证明回调管线本身完好。 4/4 与预测完全一致。
脚本:scripts/repros/repro_39163_cancelled_leaves_run_pending.py · 验证于 langchain-core 1.5.3

# case counts verdict A async · asyncio.CancelledError started=1 ended=0 errors=0 BUG: run left PENDING B async · ValueError (control) started=1 ended=0 errors=1 OK (terminal fired) C sync · KeyboardInterrupt started=1 ended=0 errors=0 BUG: run left PENDING D sync · ValueError (control) started=1 ended=0 errors=1 OK (terminal fired)
💥

后果分析

💊

ARK 处方(P3 Prescribe)

组件机制
上游修复建议 manager.py 两个 chain group CM except Exceptionexcept BaseException,调用既有 error handler 后原样 raise;回归用例覆盖 sync 的 KeyboardInterrupt 与 async 的 CancelledError
ARK 防线 1 Trace 终态不变式 每个 start 的 span 必须恰好到达一个终态(end | error)。span 析构/上下文退出时若无终态,自动补发 ark.span.orphaned——不依赖开发者选对异常基类
ARK 防线 2 CircuitBreaker 取消语义归一:把 CancelledError 记为独立终态 cancelled(不计入失败率、但计入终态),避免「取消风暴」既不触发熔断也不留痕
ARK 防线 3 OTel 留痕 新增 ark.trace.terminal_missing 事件,把「静默孤儿 span」升级为可告警指标,接入现有 Langfuse/Jaeger 栈

为什么 ARK 的方案不是「同一个补丁再打一遍」: 上游修复是枚举式的——修好这两个 CM,下一个新增的上下文管理器仍可能写 except Exception(同文件内已有两种写法并存即为证据)。 ARK 的终态不变式是结构式的:不管谁写、写错什么异常基类,只要 span 退出时没有终态就会被捕获。 这正是「靠人自觉」与「运行时强制」的分野。

🧬

缺陷族谱 · F3 静默失败(第 5 例 · 新亚型)

Issue亚型静默的是什么可查痕迹
#39039运行时失败被吞流式 response.failed 事件被丢弃假成功流
#38892运行时失败被吞合法空流误判失败,备胎静默替换污染数据
#38893运行时失败被吞不可重试异常吞成正常 AIMessage假 AIMessage
#39099构建时降级被吞schema 构建失败折叠成空 schema空参数工具
#39163终态事件缺失(新)BaseException 绕过全部终态回调无任何痕迹

前四例的共性是「边界上假装成功」,本例把 F3 族推到了极端:连「假装」都没有,直接不发生。 从可靠性工程视角看,这比假成功更危险——假成功至少在数据里留下矛盾可供事后审计,而缺失的终态事件在任何报表里都表现为「这个请求还在处理中」,与真实的长任务无法区分。 ARK 的应对因此也不同:前四例靠 OutputValidator 校验终态的内容,本例必须靠 Trace 校验终态的存在性

ARK — Agent Reliability Kit · github.com/wzg0911/ark

4P Framework: Pinpoint → Probe → Prescribe → Publish · 生成于 2026-07-31 09:40 CST