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.CancelledError 与
KeyboardInterrupt 直接继承
BaseException——
于是任务取消同时绕过 on_chain_error() 和 on_chain_end()。
已 start 的 run 永远拿不到终态事件,在 LangSmith 里无限 pending。
ASGI / WebSocket 应用中客户端断连即取消请求任务,这是生产环境的日常事件,不是边缘情况。
影响范围
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 取消语义归一
缺陷是「异常基类选窄了」,但真正的教训是可观测性组件自身没有终态保证:
trace_as_chain_group / atrace_as_chain_group)用 except Exception as e: 包裹 body;on_chain_error() 签名本身接受 BaseException——能力具备,入口卡死;manager.py 文件里的 runnable 回调辅助函数已经 catch BaseException——同模块内两种写法并存,属于纪律漂移而非设计取舍;on_chain_start,没有任何终态回调。异常本身正常向上抛(不吞异常),所以调用方毫无感知——排障时只看到 LangSmith 里一堆挂起的 trace。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
| 层 | 组件 | 机制 |
|---|---|---|
| 上游修复建议 | manager.py 两个 chain group CM |
except Exception → except 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 退出时没有终态就会被捕获。
这正是「靠人自觉」与「运行时强制」的分野。
| 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