长链路 Agent 的累积错误率:从 60% 到 99% 的工程实践
一个让人头疼的乘法
做长链路 Agent 的人,迟早会撞上这个问题。
假设一条工作流有 10 步(取数 → pivot → 派生 → regime → 回测 → 画图 …),每一步看起来都挺可靠,单步成功率 95%。但串行连乘之后:
0.95 ^ 10 ≈ 60%
也就是说,整条链路只有大约 60% 的概率能一次跑通。每一步只有 5% 的出错概率,听起来微不足道,但累积起来,整体效果就开始"时好时坏"。
关键的认知转变在这里:
解决这个问题,方向不是"把每一步的错误率压到 0"(其实很难做到),而是"让错误可恢复、让系统对错误的反应是确定的"。
下面分两层讲:第一层是基础细节部分,第二层是架构升级。
第一层:三个"理所当然"的细节
这三件事,单看每一个都很小、很基础,甚至有点"这还用说?"的感觉。但恰恰是它们,决定了一条长链路稳不稳。
① 文件持久化
核心思想:每步完成后把结果存到磁盘,下次跳过已完成的步骤。
类比写论文:每写完一章就保存。电脑崩了,不用从头写,从上次保存处继续。
没有检查点时,第 5 步回测挂了,重跑要从取数重新开始。加了检查点后,前 4 步有缓存,直接跳到回测重试。
import os, pandas as pd
def run_step(name, fn, *args):
cache = f"cache/{name}.parquet"
if os.path.exists(cache):
print(f"[跳过] {name},读缓存")
return pd.read_parquet(cache)
print(f"[执行] {name}")
result = fn(*args)
result.to_parquet(cache)
return result
它把失败的代价从"重跑全流程"降到"重跑一步"。
② 输出校验
核心思想:每个阶段结束后,主动检查数据"看起来对不对",而不是等到最后才发现问题。
类比流水线质检员:每道工序后检查,而不是等产品出厂才发现某个零件装错了。
没有验证门时,一个静默的错误(比如列名拼错 → pivot 静默填了 NaN)会一路传到最后,图都画出来了,却没人知道哪里错了,只能从头 debug。
加了验证门,错误在跨界的那一刻就被拦下:
def validate_after_fetch(df):
assert 'date' in df.columns, f"缺少 date 列,实际:{df.columns.tolist()}"
assert df.shape[0] > 100, f"数据太少:{df.shape[0]} 行,疑似取数失败"
assert df['close'].isna().mean() < 0.01, "收盘价空值超过 1%"
它把错误"被发现的时刻"从最后一步提前到出错的那一步。
③ 语义化报错
核心思想:报错不只说"哪里错了",还要说"为什么错、下一步怎么办"。
同样是取数失败,对比一下:
# 普通报错
KeyError: 'date'
# 语义化 + 带下一步
取数失败:返回数据缺少 'date' 列,实际列为 ['Date', 'ts']。
原因:数据源列名变了。
建议:在取数后加一步列名标准化,把 'Date' 映射成 'date',
然后只重跑取数这一步(后面的缓存还在)。
第二种,不管是你自己还是 LLM,一看就知道怎么修,不用对着一行裸 traceback 瞎猜。
三件套各管一段:文件持久化管"失败后从哪续跑",输出校验管"错误在哪一步被发现",语义化报错管"发现后多快能修好"。
它们都不需要改架构,性价比极高。但很多长链路项目恰恰栽在"这三件太基础、懒得做扎实"上。
第二层:让系统对错误的反应变确定
基础三件套做完,能把成功率拉上来一截。但如果你像我们一样,已经把代码固化、模型只负责读 skill 调脚本传参,却仍然时好时坏——那问题的性质已经变了,需要动架构。
认知转变:问题不是"链太长",是"节点对异常的反应不确定"
外部世界的随机性(取数有时取不到)你基本消不掉,这是现实。
但你的系统对这个随机性的反应,可以也必须是确定的。
"时好时坏"的根因,几乎都是:每个节点遇到上游异常时的行为是隐式的——这次报错炸了、下次静默传了垃圾、再下次直接卡住。
你要消除的不是数据的 flakiness,是你系统行为的 flakiness。
1. 先解决"卡住"——它比报错更危险
报错会传播、能被 catch、能进重试分支。卡住什么都不触发,它就那么坐着,把整条链拖死。出现"卡住"基本可以断定:某个外部 I/O 没有超时。
原则是 把"挂起"变成"错误",因为挂起不可处理,错误可处理:
resp = requests.get(url, timeout=(3, 30)) # 连接 3 秒,读 30 秒
把所有"裸"的网络/IO 调用都包上超时,"卡住"这个失败模式就消失了,全部转化成可以走重试或降级的显式错误。这通常是升级层里最高性价比的一步。
2. "取数取不到"其实是三种完全不同的事
这是最典型的"被混为一谈"的坑。"取不到"至少有三种,解法完全相反:
| 类型 | 例子 | 正确反应 |
|---|---|---|
| API 层失败 | 超时、5xx、限流 | 瞬时性 → 重试 + 退避能救 |
| 成功但返回空 | 停牌、未上市、节假日,本来就没数据 | 业务上合法的空 → 重试无用,标记成合法状态跳过,别当错误炸 |
| 返回部分数据 | 拉了一半断了 | 最危险,因为"看起来成功了" → 需要完整性校验(数量、覆盖度够不够) |
如果你现在这三种走同一条 except,"时好时坏"立刻能解释:瞬时失败时本该重试却被当死错、合法空值时本该跳过却被炸、部分数据时反而静默通过污染了下游。先把"取不到"拆成这三类分别定义行为,立竿见影。
3. 给每个节点定义显式的"失败契约"
这是真正把"随机行为"变成"确定行为"的关键。每个大节点都要明确声明三件事:
- 入口前置条件(precondition):开工需要的输入满足什么?不满足就立刻 fail-fast,并把错误归因到正确的上游——而不是带着烂输入往下跑,让 pivot 去莫名其妙填 NaN。
- 失败时的契约行为(failure behavior):产不出正常输出时,固定做什么?停、降级、返回标记过的空、还是用上次缓存顶上?这件事必须写死。
- 出口后置条件(postcondition):交出去的东西保证满足什么?(就是第一层的验证门。)
中间那条 failure behavior 是大多数人最缺的。“时好时坏"的根源往往就在这一条是隐式的、随上下文飘。把它写死,系统行为就从"掷骰子"变成"确定状态机”。
4. 把"时好时坏"逼成"可复现"
非确定的 bug 根本没法 debug,因为复现不了。所以要在每个节点边界记录足够状态:
- 进来的输入指纹(行数 / 日期范围 / 关键列摘要)、出去的输出、耗时、走了哪条分支(正常 / 重试 / 降级)
- 出错那次,把导致失败的输入快照存下来,能直接重放
一旦"偶发失败"变成"在这批输入下稳定复现",它就从一个玄学问题,降级成一个普通 bug。
5. 重试、熔断要在对的层级;缓存要做对
- 不要在整链层面重试(重跑全流程),要在节点层面重试——只重试取数那一步,前面缓存留着。
- 外部依赖加熔断 + 预检:数据源连续失败 N 次就快速失败并报警,而不是让每个标的都去撞墙超时几十秒。预检(开工前发个轻量探活)能让你第一时间知道"今天数据源挂了",而不是跑到第 8 步才发现。
- 第一层的"文件持久化"要做对:缓存 key 用输入哈希 + 代码哈希(内容寻址),上游一变下游自动失效,避免读到"看起来存在、其实是脏的"产物;写入用先写临时文件再 rename,避免半截损坏文件。
补充一个常见疑问:超时重试是"后台"做吗?
不是独立的后台服务。超时和重试就是写在节点脚本里的普通代码,跟着脚本一起跑。
import time, requests
def fetch_data(url):
for attempt in range(3): # 最多试 3 次
try:
return requests.get(url, timeout=(3, 30)).json()
except (requests.Timeout, requests.ConnectionError):
if attempt < 2:
time.sleep(2 ** attempt) # 退避:1秒、2秒
continue
raise # 试满还不行才真报错
关键点:能自己重试解决的(网络抽风这种),脚本内部就消化掉,LLM 压根不用知道。 只有脚本搞不定的(试满还失败、或列名变了这种重试也救不了的),才往上抛给 LLM。这恰好也减轻了 LLM 在关键路径上被调用的次数。
我的核心观点:那些"理所当然",模型并不知道
回头看,第一层那三件套——文件持久化、输出校验、语义化报错——有个共同点:
它们都是我们人类工程师"默认就会做、视为理所当然"的事。
写脚本时,遇到关键计算会下意识存个中间结果;交付前会下意识检查数据对不对;写报错会下意识写得让后人看懂。这些是经验沉淀成的本能,不需要别人教。
但模型没有这种本能。
它不会自动给你存检查点,不会自动校验输出,不会自动把报错写成人话,更不会自动区分"合法的空"和"真的失败"——除非你在 skill 里明确要求它这么做。
这就是我越来越坚信的一点:做长链路 Agent,真正拉开可靠性差距的,往往不是什么高深架构,而是把那些你以为"不用说"的工程常识,一条条显式地写下来,喂给模型。
尤其是语义化——不只是报错要语义化,skill 里对每一步的描述、对每种失败的说明、对下一步的指引,都要写得像在对一个聪明但毫无上下文的新人交代工作。你觉得"这还用说"的地方,正是模型最容易出错的地方。
在所有这些细节上做到极致——存盘、校验、语义化、超时、错误分类、失败契约——单看每一条都小到不值一提,但它们累乘起来,正好就是把整条链路从 60% 拉到 99% 的那个乘法。
一句话收尾
长链路 Agent 的可靠性,不靠让每一步永不出错,而靠两层功夫:先把三个理所当然的基础细节做扎实(文件持久化、输出校验、语义化报错),再让系统对错误的反应变确定(超时、错误分类、失败契约)。
而把这些"理所当然的工程常识"显式地写给模型——这件最不起眼的事,才是差距真正所在。
更多推荐

所有评论(0)