我给 AI Agent 装了个飞机黑匣子:录下每一次 LLM 调用,崩了能确定性回放

做 Agent 做久了,你一定会撞上这样一个晚上。

线上跑得好好的系统,某个用户来反馈:「它今天给我的回答很奇怪。」你打开日志,把同样的输入再喂一遍想复现。结果这次它表现得完全正常。再跑一遍,还是正常。那次真正出问题的运行,像从来没发生过一样,你怎么都抓不回来。

这种 bug 折磨人,是因为它跟我们熟悉的那种根本不是一回事:

传统软件 bug Agent bug
能不能复现 有堆栈、有步骤,照着走就出现 一次性的,发生过就消失
手里有什么证据 完整日志 经常只剩最后一条输出
怎么定位根因 单步调试 连那次调用序列都抓不回来

为什么 Agent 的 bug 这么难抓?根因就三条,但每一条都致命:

  • LLM 本身非确定。 同样的输入,下一次的输出不保证一样。
  • 调用序列重放不了。 agent 那一轮跑了几步、每步模型返回了什么、触发了哪些工具,全过去了。
  • 中间状态没留下。 日志里大概率只有最后那条输出,中间每步模型回了什么、工具收到什么参数,根本没记。

一个反直觉的事实:唯独 LLM 这层,我们没做回放

我后来想明白一件事:整个技术栈几乎每一层都是可回放的,唯独 LLM 调用这一层,我们把它留成了不可回放的。

  • 数据库挂了,有快照,能恢复到出事那一刻。
  • HTTP 请求出问题,那条 curl 原样再发一次就复现。
  • 分布式系统这种最难缠的,业界都在用确定性模拟去重放同一串事件。

我们对「可复现」是有执念的,也有一整套工具。可偏偏到了 agent 这儿,那个最不确定、最容易出妖蛾子的环节,我们却任由它非确定、不留痕、过完就忘。

飞机为什么要装黑匣子?恰恰因为空难罕见、严重、而且几乎不可能事后复现。你没法让飞机再炸一次给你看,所以得在它正常飞的时候就把一切录下来,出事之后靠录像倒推。

Agent 的 bug 是同一种形状的问题。所以我写了 FlightBox,给 AI Agent 装一个这样的黑匣子。项目地址:

github.com/he-yufeng/F...

怎么用:录、放、对、固,四步

第一步,录。 一个 with 把 agent 代码包起来,块里的调用一个都不漏,业务代码一行不动:

python 复制代码
import flightbox
from openai import OpenAI

client = OpenAI()

with flightbox.record("debug-session") as rec:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "..."}],
    )

print(f"录制 ID: {rec.run_id}")

这一块里每一次 chat.completions.create(),连同完整的请求、响应、延迟、token 用量、工具调用,都写进一个本地 SQLite(默认在 .flightbox/recordings.db)。纯本地,不走云,不上报。

第二步,放。 拿到一个 run_id,用 replay 把同样的代码再包一次:

python 复制代码
with flightbox.replay("abc123def4"):
    response = client.chat.completions.create(...)
    # 拿回来的就是当初录下的那个响应,一模一样

回放时,被接管的方法不再真打 API,而是把当初录下的那条响应原样还给你。你的 agent 在本地就变成了完全确定性的:可以在那次「奇怪的回答」上反复打断点、加日志、改逻辑,每次都精确重演同一条轨迹,直到揪出根因。这跟「再跑几遍碰碰运气」是两个世界。

跑起来是这个样子,同一段代码在 replay 下逐步命中录制、输出和首次一模一样:

scss 复制代码
$ python debug.py
[flightbox] mode=replay  run=abc123def4
  step 1/3  chat.completions.create   ↳ replayed (cache hit)   12ms
  step 2/3  chat.completions.create   ↳ replayed (cache hit)    9ms
  step 3/3  chat.completions.create   ↳ replayed (cache hit)   11ms
[flightbox] 3/3 calls replayed deterministically · 0 live API hits
最终回答:(与首次录制逐字一致)

第三步,对。 改了 prompt 或 agent 逻辑,想知道「这次跟上次哪儿不一样」:

bash 复制代码
flightbox diff <run-a> <run-b>

它精确告诉你第几步、哪个字段开始对不上,不用你拿两份日志肉眼比:

vbscript 复制代码
$ flightbox diff abc123def4 9f7e21aa01
step 1  request.messages   ✓ identical
step 2  request.system     ✗ changed
        - You are a helpful assistant.
        + You are a concise assistant. Prefer tool calls over prose.
step 2  response.tool_calls  ✗ 0 → 1   (search_docs)
step 3  ✓ identical
1 step diverged · first divergence at step 2 (request.system)

第四步,固(我自己最偏爱的用法)。把一次真实的、尤其是出过问题的运行,直接固化成 pytest 回放测试:

bash 复制代码
flightbox export <run-id> -f pytest -o test_replay.py
shell 复制代码
$ flightbox export abc123def4 -f pytest -o test_replay.py
wrote test_replay.py · 1 replay test · 3 recorded calls pinned
$ pytest test_replay.py -q
.                                                            [100%]
1 passed in 0.18s

这一步的意义在于,你抓到的那个 bug 从此变成了一条回归测试 ,以后改代码再也别想让它悄悄复活。再往大里想一层:你线上每一次有价值的真实流量都能沉淀成测试用例,回归测试集是从真实世界里长出来的,而不是你拍脑袋编的。也可以 -f jsonl 导成评测数据集。

为什么是 patch SDK 这一层,而不是 HTTP 或框架

这部分我觉得最值得说。FlightBox 去 monkey-patch 的是 OpenAI 和 Anthropic 两个官方 SDK 的方法(chat.completions.createmessages.create),而不是拦更底层的 HTTP,也不是耦合某个框架的内部。挑这一层有三个理由:

  • 它是最窄、最稳定的那道缝。 上层框架绕来绕去,最后都会收敛到 openai.chat.completions.create 这一下。
  • 补一道缝接住所有框架。 LangChain、CrewAI、Pydantic AI 都从这里过,你不用理解任何一个框架的内部。
  • 不会随框架升级而碎。 拦的是稳定的 SDK 公开方法,不是某框架的私有实现。

一句话:选对要拦的那一层,比拦本身更重要。

边界要说清楚

免得你期待错位。FlightBox 录的是「经过 OpenAI/Anthropic SDK 的调用」这一层的真相:

  • 录得到:每次 LLM 调用的完整请求、响应、延迟、token、工具调用。
  • 录不到 :你 agent 内部那些非 LLM 的随机性,比如自己代码里的 random、当前时间、某个外部接口的返回。

它解决的是「LLM 调用不可复现」这个最大的不确定性来源,不负责消灭你代码里所有的不确定。把这条记住,它就是个非常趁手的东西。

和 AgentProbe 配合

如果你也在做 agent 的测试,它能和我另一个项目 AgentProbe(一个给 agent 行为做断言的 pytest 插件)串起来用:

  • FlightBox 负责把真实运行录下来、变成确定性的回放。
  • AgentProbe 负责在这条确定的轨迹上断言:它该调几次工具、该不该走某条分支。

一个管录下真相,一个管在真相上立规矩。

如果这篇说中了你某个调试到崩溃的夜晚,去仓库点个 star,或者提个 issue 告诉我你最想录下来的是哪一类调用。

项目地址:github.com/he-yufeng/F...


我是何宇峰,在做 AI Agent 相关的工程和开源。如果觉得有用:

相关推荐
JieE2122 小时前
从"无状态"到"懂你":深入理解 LLM 对话的本质,以及 Prompt/Context/Loop 三层工程进化之路
人工智能·llm·ai编程
Hector_zh2 小时前
实战·第八篇:当模型陷入死循环——FACA破解JSON生成的架构陷阱
人工智能·agent·vibecoding
嘻嘻仙人2 小时前
Claude Code CLI 实战案例——不同场景案例实操
agent
Lkstar2 小时前
Function Calling 原理深度拆解:让 LLM 调用外部工具的机制与工具设计原则
人工智能·llm
codedx3 小时前
LangChain 和 LangGraph 构建的 Agent 项目模版
后端·langchain·agent
小七-七牛开发者3 小时前
周一上线 | SpaceX 收购 Cursor、支付宝进入 AI 时代、DeepSeek 完成 500 亿元融资
ai·agent·token·glm·智谱·claudecode·ai coding·周一上线
葫芦和十三4 小时前
图解 MongoDB 08|ESR 原则:复合索引的字段顺序怎么定
后端·mongodb·agent
葫芦和十三11 小时前
图解 MongoDB 07|索引类型:七种索引,七种访问形状
后端·mongodb·agent