🛡️ ARK 智能体诊断报告

langchain-ai/langchain#35475 · RunnableRetry.batch 在"部分重试成功 / 部分仍失败"时返回错位损坏输出 · 2026-07-23

📋

问题摘要

🔴

RunnableLambda(...).with_retry(...).batch(items, return_exceptions=True)部分 item 重试后成功、部分 item 重试后仍失败时, 最终组装的输出会在错误的位置填入本该是异常的对象被成功结果覆盖—— 即"失败项被成功项的结果悄悄替换"。这是一个静默数据损坏(silent data corruption): 调用方以为拿到了正常结果,实际是错位拼接的产物。

严重 · 静默数据损坏 正确性 / 可靠性风险

影响范围

所有 with_retry + batch/abatch

框架现状

⚠️ open,多 PR 竞修未合

ARK 方案

✅ IdempotencyGuard + OutputValidator

🔍

根因定位

🧬 现象链

batch([ok, retry_then_ok, always_fail]) → 首轮失败项进入重试、成功项不走重试 → 末轮 result 只对应"剩余索引"的子集 → 组装时用 result.pop(0) 顺序填充全量位置 → 索引错位 → 异常位被成功值覆盖,成功值被吞掉

⚙️ 根因

libs/core/langchain_core/runnables/retry.py_batch / _abatch 中, 重试后整批结果组装依赖 result.pop(0) 来补齐"未出现在 results_map 中的位置"。 但此时 result最后一次重试返回的剩余子集,其元素顺序与"原始全量位置"已经脱钩。 一旦"部分成功、部分失败"混合出现,pop(0) 消费的是错误的元素——把成功结果填进了本该保留异常的位置, 而真正的异常被悄悄丢弃。本质是一个非幂等的组装逻辑: 它假设"剩余结果列表"与"全量位置"严格同序,而重试恰恰破坏了这一前提。
💡 本质:这不是"该不该重试"的问题,而是 "如何安全地重试 + 如何校验组装结果"。框架缺两层保护—— (1) 把每个 item 的重试用稳定幂等键隔离,避免跨 item 串扰; (2) 组装后用结构化校验守住"该异常的位置必须是异常"的不变式。 这正是 ARK IdempotencyGuard + OutputValidator 已解决的事。
📊

关键证据

📄 复现:3 个 item,中间项重试成功、末项始终失败(节选自 issue)

runnable = RunnableLambda(process_item).with_retry(
    stop_after_attempt=2, retry_if_exception_type=(ValueError,))
result = runnable.batch(["ok", "retry_then_ok", "always_fail"],
                        return_exceptions=True)

# 期望: [ok-result, retry-result, <Exception>]
# 实际: 末位异常被成功值覆盖 → [ok-result, retry-result, ok-result?]  ❌ 错位
assert isinstance(result[2], Exception)   # 触发失败

📄 社区定位的根因(issue 评论节选)

# 原组装逻辑(伪代码)
result.pop(0)  # 用末轮剩余子集顺序填充 → 索引错位

# 正确做法(社区 PR #35622/#35744)
if idx not in results_map:
    final[idx] = caught_RetryError   # 该失败就保留异常,不 pop 成功值
else:
    final[idx] = results_map[idx]     # 按索引映射,而非顺序 pop

损坏触发条件

部分成功+部分失败

失败可见性

静默(0 报错)

竞修 PR 数

3+ 未合

🔧

ARK 一键修复

✅ 方案A(推荐):IdempotencyGuard 隔离每个 item 的重试

from ark import IdempotencyGuard

guard = IdempotencyGuard(ttl_seconds=3600)

def safe_batch(items):
    out = []
    for i, item in enumerate(items):
        # 以 (item_index, item) 为幂等键,重试命中即返回首次结果
        # 跨 item 互不串扰,组装顺序天然稳定
        out.append(guard.execute(f"batch#{i}:{item}", lambda: run(item)))
    return out

# 即便个别 item 重试、个别 item 失败,每个位置的结果都独立正确
# 不再依赖 "result.pop(0)" 的顺序假设

💡 ARK 用 sha256({key}, sort_keys) 生成稳定幂等键,把"整批顺序组装"变成"逐 item 幂等映射",从源头消除错位。

✅ 方案B:OutputValidator 守住组装不变式

from ark import OutputValidator

# 声明"该异常的位置必须是异常"的不变式
schema = {
    "type": "array",
    "items": {"oneOf": [{"type": "string"}, {"type": "exception"}]},
}
validator = OutputValidator(schema)

raw = runnable.batch(items, return_exceptions=True)
validated = validator.validate(raw)   # 错位/被覆盖 → 立即抛 ValidationError
# 而不是让损坏结果静默流向下游

💡 即便框架层修复前,OutputValidator 也能在边界处拦下错位输出,把"静默数据损坏"变成"可见的校验失败",配合 ark.validation.fail 事件可观测。

✅ 方案C:CircuitBreaker + auto_init 兜底抖动依赖

import ark
cfg = ark.auto_init()   # 自动探测 LangChain 并装配 幂等+熔断+校验

# 当某依赖频繁部分失败时,熔断避免无谓轰炸,
# 失败被集中收集而非散落错位
breaker = cfg["circuit_breaker"](failure_threshold=5)
result = breaker.call(lambda: runnable.batch(items, return_exceptions=True))

💡 与方案A/B 组合:IdempotencyGuard 防错位、OutputValidator 防漏检、CircuitBreaker 防级联,三层守护在 Dashboard 统一可观测。

📈

健康得分

50

50/100 · 全成功/全失败路径正常,但混合路径静默损坏

稳定性55
效率65
正确性(安全性)35

💡 健康得分 50/100。正确性仅 35——本 issue 比"重复执行"更危险: 它不是多花成本,而是悄悄返回错误数据且不报错, 一旦流入数据库/支付/决策链路就是脏数据事故。 接入 ARK IdempotencyGuard(防错位)+ OutputValidator(防漏检)后, 正确性分可回到 90+,且所有异常在 Dashboard 可观测、可审计。

本报告由 ARK 生成 · 智能体健康感知系统
langchain-ai/langchain#35475 · 锚定自真实 Issue(open,langchain-core 1.2.16,多 PR 竞修未合)