针对AIAgent中LLM调用非确定、不可复现的痛点,设计FlightBox黑匣子工具。通过录制每次LLM调用的完整请求与响应,实现确定性回放、差异对比及导出测试用例,从而精准定位并固化Agent运行中的异常行为。
当你在AI Agent开发中越陷越深,迟早会遇到这样一个夜晚:线上系统运行正常,但用户反馈“它今天给我的回答很奇怪”。你打开日志,将同样的输入重新喂给系统,试图复现问题——结果一切正常。再试一次,依然正常。那一次真正出错的运行,仿佛从未发生过,无论怎样都无法复现。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
这种bug之所以折磨人,是因为它和我们熟悉的传统软件bug完全不同:
| 传统软件bug | Agent bug | |
|---|---|---|
| 能否复现 | 有堆栈、有步骤,照着走就出现 | 一次性的,发生后消失 |
| 手中有什么证据 | 完整日志 | 经常只剩最后一条输出 |
| 如何定位根因 | 单步调试 | 连那次的调用序列都抓不回来 |
为什么Agent的bug如此难以捕捉?根因有三条,每一条都致命:
后来想明白一件事:整个技术栈几乎每一层都是可回放的,唯独LLM调用这一层,我们把它留成了不可回放的。
我们对“可复现”是有执念的,也有一整套工具。可偏偏到了Agent这里,那个最不确定、最容易出妖蛾子的环节,我们却任由它非确定、不留痕、过完就忘。
飞机为什么要装黑匣子?恰恰因为空难罕见、严重、而且几乎不可能事后复现。你没法让飞机再炸一次给你看,所以得在它正常飞的时候就把一切录下来,出事之后靠录像倒推。
Agent的bug是同一种形状的问题。所以有了FlightBox,给AI Agent装一个这样的黑匣子。
第一步,录。一个with把Agent代码包起来,块里的调用一个都不漏,业务代码一行不动:
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把同样的代码再包一次:
with flightbox.replay("abc123def4"):
response = client.chat.completions.create(...)
# 拿回来的就是当初录下的那个响应,一模一样
回放时,被接管的方法不再真正调用API,而是把当初录下的那条响应原样还给你。你的Agent在本地就变成了完全确定性的:可以在那次“奇怪的回答”上反复打断点、加日志、改逻辑,每次都精确重演同一条轨迹,直到揪出根因。这跟“再跑几遍碰碰运气”是完全不同的世界。
运行起来像这样,同一段代码在replay下逐步命中录制,输出和首次一模一样:
$ 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逻辑,想知道“这次跟上次哪儿不一样”:
flightbox diff
它精确告诉你第几步、哪个字段开始对不上,不用你拿两份日志肉眼比较:
$ 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回放测试:
flightbox export -f pytest -o test_replay.py
$ 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导成评测数据集。
这部分最值得说。FlightBox去Monkey-patch的是OpenAI和Anthropic两个官方SDK的方法(chat.completions.create和messages.create),而不是拦更底层的HTTP,也不是耦合某个框架的内部。挑这一层有三个理由:
openai.chat.completions.create这一下。一句话:选对要拦的那一层,比拦本身更重要。
免得你期待错位。FlightBox录的是“经过OpenAI/Anthropic SDK的调用”这一层的真相:
random、当前时间、某个外部接口的返回。它解决的是“LLM调用不可复现”这个最大的不确定性来源,不负责消灭你代码里所有的不确定。把这条记住,它就是个非常趁手的东西。
如果你也在做Agent的测试,它能和另一个项目AgentProbe(一个给Agent行为做断言的pytest插件)串起来用:
一个管录下真相,一个管在真相上立规矩。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述