Skip to content

第 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_retrymax_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.pytool_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、空壳识别,那些不性感但保命的底线。