AI Agent 调试别再堆日志:执行树是更好的调试模型
原文:https://dev.to/raju_dandigam/execution-trees-not-more-logs-a-better-debugging-model-for-ai-agents-3d4g(作者 @raju_dandigam)
一条扁平的日志能告诉你发生了五件事,却常常说不清是哪个操作引发了下一个操作、哪次失败触发了回退(fallback),也说不清三次工具调用究竟是一个规划步骤的子节点,还是三件互不相关的工作。
这个区别对 AI Agent 至关重要,因为执行路径本身就是行为的一部分。
AgentInspect 是一个开源的 TypeScript 工具包,用于在本地检查 agent 的执行过程。本文将说明为什么选择执行树(execution tree)作为主要的调试模型,文中用到的合成测试夹具均已在 `agent-inspect@6.17.4` 版本上验证过。
把 agent 的一次运行当成时间线来读,问题出在哪
设想一个执行了以下操作的客服 agent:
09:00:00.000 plan started 09:00:00.020 inventory request started 09:00:00.060 inventory request failed: 503 09:00:00.061 inventory request started 09:00:00.120 inventory request succeeded 09:00:00.150 answer completed
这些信息足够拼出一个简单的故事,但整个还原过程都发生在你的脑子里。一旦加入嵌套的 agent、并行的工具调用、重复使用的操作名,再混入应用自身的日志,时间戳就不再是因果关系的可靠写照。
执行树把这些关系显式地呈现出来:
support-agent ├── plan ├── fetch-inventory (failed: 503) ├── fetch-inventory (success) └── draft-answer
执行树并不取代原始事件数据。它是这些数据的一种投影,专门回答开发者通常最先问的那个问题:这次运行到底走了哪条路径?
在 TypeScript 中捕获有意义的边界
AgentInspect 为一次运行和各个命名步骤提供了包装器。下面是一个刻意精简的示例:
import { inspectRun, step } from "agent-inspect";
await inspectRun(
"travel-planner",
async () => {
const plan = await step("plan", async () => ({
destinations: ["SFO", "SEA"],
}));
const [flights, hotels] = await Promise.all([
step.tool("search-flights", async () => [
{ id: "F-101", price: 220 },
]),
step.tool("search-hotels", async () => [
{ id: "H-202", nightly: 180 },
]),
]);
return step.llm("rank-options", async () => ({
plan,
flights,
hotels,
}));
},
{ traceDir: "./.agent-inspect" },
);
这是手动埋点。它并不宣称包装器能自动发现框架内部的每一个操作。它的目的是记录你真正关心的边界:整个运行、其中的规划步骤、两个同级的工具调用,以及最后面向模型的步骤。
然后在本地查看这次运行:
npx agent-inspect view travel-planner \ --dir .agent-inspect \ --summary
四种形态,暴露四类不同的 bug
1. 嵌套结构暴露归属关系
一个三层的合成夹具渲染出来是这样的:
Execution Tree:
✔ outer (120ms)
✔ middle (80ms)
✔ inner (50ms)
那两格缩进不是装饰。它们告诉我们 inner 属于 middle,而 middle 又属于 outer。一旦 inner 失败,我们立刻知道是哪个上层操作拥有它。换作扁平日志,要推断出同样的结构,就得靠匹配 ID 或比对前后时间戳。
当一个 agent 把任务委托给另一个 agent、一个工具内部执行多个子操作,或者一个检索步骤同时掌管查询改写和向量搜索时,嵌套结构尤其有用。
2. 回退暴露恢复行为
再看一个错误恢复夹具:
Execution Tree:
✖ tool:primary-search (100ms)
Error: primary search unavailable
✔ tool:fallback-search (200ms)
✔ handle-recovered-result (50ms)
最终这次运行可能仍然是成功的。如果只看答案,主搜索失败这件事就会从调试叙事中消失。而执行树把两个事实都保留了下来:
- 主路径失败了;
- 恢复路径完成了。
这个区别可能改变工程决策。由回退产出的成功答案或许可以接受,但回退使用量突然上升,仍可能意味着某个依赖已经劣化,或者路由发生了代价高昂的变更。
3. 重复的同级节点暴露重试
重试值得拥有自己可见的形态:
Execution Tree:
✖ tool:fetch-inventory (40ms)
Error: synthetic 503 from upstream
✖ tool:fetch-inventory (45ms)
Error: synthetic 503 from upstream
✔ tool:fetch-inventory (60ms)
✔ handle-recovered-result (30ms)
一个最终成功的状态会掩盖达成成功付出的代价。重复出现的工具名让重试序列清晰可见。它还给确定性检查提供了具体的评估对象:比如 fetch-inventory 是否超出了允许的调用次数。
执行树本身并不能告诉我们重试策略是否正确,它提供的是这个策略确实被触发过的证据。
4. 并行的同级节点暴露并发
一个并行夹具会渲染成一组同级操作:
Execution Tree: ✔ tool:search-hotels (300ms) ✔ tool:search-flights (200ms) ✔ tool:search-cars (100ms)
这些耗时不应被加总。这些步骤是同级的,可能相互重叠。这让我们避开一个常见的时间线误区:以为每个带时间戳的操作都在等前一个操作完成。
执行树不能证明并发的实现是最优的,但它准确保留了调查并发问题所需的结构关系。
树是一种视图,不是完整的证据模型
把一棵可读的树变成唯一存储的产物,这种做法很诱人。我刻意避免了它,因为面向人类阅读的视图必然会对信息做压缩。
底层的 trace 可能包含标识符、时间戳、状态、输入或输出(受捕获策略约束)、观察记录以及元数据。不同的问题需要不同的投影:
structured trace ├── tree -> 走了哪条路径? ├── check -> 不变量是否成立? ├── diff -> 两次运行之间变了什么? ├── report -> 评审者应该读什么? └── bundle -> 哪些证据可以共享?
执行树是最快的入口,但它不能替代检查与分析。
把可疑形态变成确定性检查
假设重试树显示某个库存工具(inventory tool)可能运行三次。如果预期策略只允许至多两次调用,那就把这条预期编码成规则,而不是寄望于日后的人工目视检查。
在 CLI 层面,一条轨迹检查(trajectory check)可以指定必需的工具,并在记录到特定观察结果时判定失败:
npx agent-inspect check travel-planner \ --dir .agent-inspect \ --preset trajectory \ --required-tool search-flights \ --fail-on-observation failed
对于更复杂的规则,AgentInspect 提供了一个实验性的 TraceContract API,可以表达工具要求、禁止使用的工具、最大调用次数、调用顺序、运行状态、耗时、模型白名单以及 token 上限。由于该 API 在所引用的版本中仍处于 beta 阶段,在把它用作 CI 门禁之前,请先固定版本号并验证其确切语义。
真正关键的工作流比某一个 API 更宽泛:
- 检查执行树;
- 识别出稳定的行为不变量;
- 把它编码为确定性检查;
- 把人工判断留给依赖上下文的问题。
树无法告诉你什么
一棵干净的树并不能证明答案是正确的。必需的检索步骤可能返回无关文档;模型调用可能产出缺乏依据的论断;一个工具可能在技术上执行成功,返回的却是过期数据。
执行树最擅长回答的是结构性问题:
- 哪些操作运行了?
- 失败归属于哪个操作?
- 是否使用了回退或重试?
- 哪些工作是以兄弟节点并列发生的?
- 运行在哪里停止了?
内容质量请交给语义评估器、领域测试和人工评审。最可靠的智能体调试工作流是把这几层组合起来,而不是指望一张可视化图回答所有问题。
调试路径,而不只是答案
最终响应是用户看到的东西,而执行路径才是工程师能够改进的东西。树把这条路径从一段靠推断补全的叙事,变成一件具体的产物。
这正是 AgentInspect 本地视图背后的设计原则:保留因果结构,即使最终恢复成功也把失败过的工作暴露出来,并让可疑模式能够方便地转化成可重复执行的检查。
你可以在 GitHub 上查看本文使用的确切版本。如果你想上手试试,建议从一个人工构造的失败后回退(failure-and-fallback)测试夹具开始——完美的快乐路径(happy path)是调试器最无聊的测试用例。
原文:https://dev.to/raju_dandigam/execution-trees-not-more-logs-a-better-debugging-model-for-ai-agents-3d4g(作者 @raju_dandigam)