主题
第 12 章 · 错误分流与韧性
pixiu 锚点:
agent/llm.py(R2 熔断器 + R1 client 工厂)、data/providers/tushare_provider.py(R1-1 retry)、tools.py:execute_tool(错误兜底)、tool_result_message(错误不截断) 关键案例:R2(熔断根治"空转一整夜")、W1-1(先证伪再动手)
开篇:连续失败会让系统空转一整夜
讲一个 pixiu 真实发生过的事。
pixiu 的 scheduler 定时调 LLM 生成报告。有一天,LLM 服务出了问题(网络抖动 + 限流),调用持续失败。而 pixiu 当时的逻辑是——失败了就重试,再失败再重试。结果 scheduler 就这么一整夜地空转,每隔一会儿调一次 LLM,每次都失败,循环往复,日志刷爆,什么有用的事也没干。
这件事让我意识到:agent 系统的韧性,不是"出错时能重试"那么简单。 你得知道——什么错该重试、什么错该让模型自己改、什么错该找用户、什么错该直接上抛;而且,连续失败时得熔断,别让系统在死胡同里空耗。
这一章讲 pixiu 怎么把这些想清楚的。核心是一张"错误分流表"。
四层错误分流
pixiu(和大多数 agent 系统)的错误,可以分成四层,每一层交给不同的"责任方":
| 层 | 例子 | 责任方 | pixiu 实现 |
|---|---|---|---|
| 瞬时错误 | 网络、限流、超时 | 机制层自动重试(模型根本不知道出过错) | @provider_retry、max_retries=3 |
| 模型可自修正 | 参数格式错、缺必填字段、路径不存在 | 错误信息回灌给模型,它自己改了重试 | execute_tool 返回可操作错误 |
| 需用户介入 | 权限不足、API Key 未配置 | 中断执行,告诉用户该做什么 | 工具返回引导文本 |
| 未预期异常 | 代码 bug、DB 连不上 | 上抛异常、记日志,开发者来查 | execute_tool 兜底捕获 |
这张表的精髓是:不同错误,责任方不同。 你不能把所有错误都交给同一种处理方式——瞬时错误该静默重试(别打扰模型),可自修正的该让模型自己改(回灌错误),需要人的该中断(找用户),彻底崩了的该上抛(找开发者)。
下面逐层看 pixiu 的实现。
第一层:瞬时错误,静默重试(R1)
网络抖动、API 限流、偶发超时——这类错误重试一下就好了,不该让模型知道(它知道了也帮不上忙)。pixiu 在两个地方做了这层处理:
LLM client 工厂(R1)。 标注 R1-2 的发现:原来 llm.py 里裸 OpenAI() 调用,没有 timeout、没有 retry,流式调用连 read timeout 都没有,会无限阻塞。修复是抽了个工厂函数 make_llm_client:
python
def make_llm_client(api_key, base_url=None) -> OpenAI:
return OpenAI(api_key=api_key, base_url=..., timeout=60.0, max_retries=3)把 timeout=60、max_retries=3 封进工厂,新代码都用工厂创建 client,而不是裸调。这叫鲁棒性默认化(R1)——把"该有的韧性"做成默认,而不是每个调用点自己记得加。
数据 provider 重试(R1-1)。 tushare_provider 原来没有 retry,而同项目的 akshare provider 有——策略不一致。修复:7 个 fetch 方法都加了 @provider_retry,但有个关键区分——频率超限抛 RuntimeError 时不重试(重试只会让限流更糟),只有瞬时错误才重试。这是"瞬时错误重试、非瞬时错误不重试"的精确落地。
R1 的修正命题(R1-7):装饰器形态的鲁棒性(@provider_retry)很难被 lint 强制,而 client 工厂(make_llm_client)可以——因为新代码要么用工厂要么裸调,lint 能抓出裸调。所以"默认化"也有边界,工厂比装饰器更可强制。
第二层:模型可自修正的错误,回灌给它
有些错误,模型改改参数就能解决——传错了日期格式、漏了必填字段、标的没下载。这类错误,把可操作的错误信息回灌给模型,它自己就能改(第 4 章讲过"错误可操作")。
pixiu 的 execute_tool 兜底(tools.py:1745)把这层贯彻到了每个分支。比如缺参数:
"缺少必填参数 'symbol'。请提供 6 位股票/ETF 代码,如 '601088'。"模型拿到这种错误,下一轮会自己把 symbol 补上重试。整个过程不需要人介入——这就是"模型可自修正"层。关键在于错误信息要可操作(说清缺什么、给格式、指下一步),否则模型也改不动。
第三层 & 第四层:找用户 / 上抛
权限不足、API Key 没配——这类错误模型改不了,得中断并找用户。pixiu 的工具会返回引导文本("请在设置页配置智谱 API Key"),让用户去解决。
代码 bug、DB 连不上——这类未预期异常,pixiu 的 execute_tool 兜底捕获后,返回带异常类型的错误信息并记日志,留给开发者查。不静默吞掉(第 13 章会讲静默失败的危害)。
还有一个细节:llm.py 的 tool_result_message——错误信息不截断,正常结果超长才截断。为什么?因为错误信息是模型自救的依据,截断了模型就失去了修正线索;而正常结果(一堆行情数据)截断无害。
熔断器:连续失败就别空转(R2)
四层分流之外,还有一种特殊情况:连续失败。单次失败重试是合理的,但连续失败时,再重试就是空耗——这就是开头那个"空转一整夜"的故事。
修复是 client 层的熔断器(CircuitOpenError),标注 R2。逻辑在 llm.py:
python
class DeepSeekClient:
def __init__(self, ...):
self._consecutive_failures = 0
self._circuit_open_until = 0.0 # 断开到何时;0 = 未断开
self._failure_threshold = 5 # 连续失败阈值
self._open_duration = 60.0 # 断开时长(秒)
def _check_circuit(self):
now = time.monotonic()
if now < self._circuit_open_until:
raise CircuitOpenError(
f"LLM 熔断中(连续 {self._consecutive_failures} 次失败),"
f"{self._circuit_open_until - now:.0f}s 后半开试探"
)
def _on_failure(self):
self._consecutive_failures += 1
if self._consecutive_failures >= self._failure_threshold and not self._circuit_open_until:
self._circuit_open_until = time.monotonic() + self._open_duration机制是经典的熔断器三态:连续失败 5 次 → 熔断(断开 60 秒,期间直接拒绝调用,不调 LLM)→ 60 秒后半开试探(放一次请求试探恢复)→ 成功则复位,失败则继续断开。
这一下就根治了空转——LLM 挂了,系统不再傻等,而是快速失败、记日志、等 60 秒再试。日志里那句"LLM 熔断中……{N}s 后半开试探",比一整夜的失败刷屏清爽一万倍。
而且熔断要跨层协作(R2-1)。 熔断抛的是 CircuitOpenError,上层 scheduler 会专门识别它——优雅跳过(StepResult skipped,不视为失败),而不是当普通故障处理(第 10 章讲过)。熔断器定义"什么时候停",上层定义"停了怎么降级",缺一不可。
一个反例:先证伪,再动手(W1-1)
讲完"怎么修",讲一个"别瞎修"的故事,标注 W1-1。
有一阵子,pixiu 的 Web 端点慢得离谱——4 到 59 秒(dashboard/signals 最严重,59 秒)。乍一看是个严重的性能 bug,本能反应是去优化查询。
但我没急着改代码,先去查根因。查明机制是:DB 迁到了远端宿主机 PG(RTT 约 500ms),端点里 chatty 顺序查询 5-50 次,每次往返 0.5 秒,叠起来就 8-59 秒。
关键反转来了——我去问用户(就是我自己),澄清后发现:500ms 的 RTT 只是本地连远端测试库导致的;生产环境 app 和 DB 同机,RTT≈0,根本没这个问题。我又做了个证伪实验:同一条查询,本地库 0.002 秒 vs 远端库 1.57 秒。
结论:这不是 bug,无需修复。 findings 里这条标的是"已关闭(非问题)"。
这个故事的价值不在那个结论,在方法:遇到"明显的问题",先证伪,再动手。 如果我直接去"优化"端点(加缓存、改查询),不仅白费功夫,还可能引入新问题。一个本地 vs 生产的证伪实验,五分钟就省下了一整天的瞎优化。
这一章的工具:韧性自检
- [ ] 你的错误有"分流"吗?还是一股脑重试 / 一股脑上抛?
- [ ] 瞬时错误有没有静默重试?(别打扰模型)
- [ ] 模型可自修正的错误,错误信息够可操作吗?(模型能自己改吗)
- [ ] 连续失败有没有熔断?(还是无限重试空耗)
- [ ] 熔断之后,上层知道怎么优雅降级吗?(跨层协作)
- [ ] 遇到"明显的问题",你是先证伪还是先动手?
小结
agent 系统的韧性,核心是错误分流——瞬时错误静默重试、可自修正的回灌模型、需用户的中断、未预期的上抛,四层各司其职。再叠加一个熔断器,防止连续失败时的空转。
但 pixiu 的经验还多一条:韧性不等于"出了错就修"。 有时候"明显的问题"不是问题(W1-1),先证伪再动手,比盲目优化更省、更稳。
下一章是工程部分的收尾——除了韧性,一个生产级 agent 还需要哪些工程底线。
下一章
第 13 章 · 工程严谨性 —— 双引擎、CI、空壳识别,那些不性感但保命的底线。