Agent Harness 有了“眼睛”和“手”:更细的 Trace 事件 + 自动错误恢复

系列博客第三篇 · 2026-06-29
从“看得清”到“自己会修”


一、前情回顾

前两天的成果:

  • Day 1:跑通最小闭环,用户输入 → 模型 → 工具 → 输出,能跑了。
  • Day 2:抽离 ContextBuilder 统一管理上下文,新增 Trace Report 把 JSON 变成可读的复盘报告。

到了 Day 2 结束时,我手上有这样一个 Agent Harness:

  1. 能跑 —— 基础闭环完整。
  2. 能复盘 —— Trace Report 让我知道哪里出错了。

但复盘完发现问题后,还是得手动改代码、重新跑。而且 Trace 本身也不够细——我只能看到“调了哪个工具、成功还是失败”,但看不到“ContextBuilder 给了模型什么信息”“用了哪个 Engine”“工具耗时多少”。

所以 Day 3 的目标很明确:

  • 让 Trace 更细,把执行过程中的每个关键节点都记录下来。
  • 新增两个实用工具,让 Agent 能搜网页、能跑 shell 命令。
  • 让系统学会自己修,遇到常见错误时自动恢复,而不是直接放弃。

二、让 Trace 长出“骨架”:context_built 和 engine_started

2.1 之前的 Trace 长这样

之前的事件流:

run_started → tool_call → tool_result → done

但如果我想知道:

  • 这次任务用的是哪个 Engine?(Chat 还是 Tool?)
  • ContextBuilder 给模型注入了什么规则?
  • 每次工具调用耗时多久?成功了还是失败了?

抱歉,JSON 里没有。

2.2 本次新增的两个关键事件

我往 src/runtime/events.ts 里加了两个新事件:

context_built
记录 ContextBuilder 构造完上下文后的快照:

{
  "type": "context_built",
  "payload": {
    "mode": "tool",
    "workspaceRoot": "C:/Users/ts/Desktop/zcy/mini-agent-harness",
    "systemPromptLength": 2048,
    "toolWorkflowSteps": 5
  }
}

engine_started
记录这次执行用的是哪个 Engine、最大轮数是多少:

{
  "type": "engine_started",
  "payload": {
    "engine": "DeepSeekToolEngine",
    "maxTurns": 5
  }
}

2.3 增强 tool_result:把关键指标抽出来

以前 tool_result 只记录原始返回,现在我会额外抽取:

{
  "type": "tool_result",
  "payload": {
    "tool": "read_file",
    "ok": false,
    "durationMs": 312,
    "errorCode": "FILE_NOT_FOUND"
  }
}

这样一来,Trace Report 就能直接统计:

  • 工具调用总次数
  • 失败次数和失败率
  • 总耗时
  • 最常见的错误类型

2.4 现在的事件流更丰满了

run_started
  -> context_built        ← 新增:知道模型看到了什么
  -> engine_started       ← 新增:知道用了哪个引擎
  -> text_delta / tool_call / tool_result / error
  -> done

Trace Report 里也多了几个关键字段:

  • toolFailureCount
  • totalToolDurationMs
  • engine

2.5 一个小小的感悟

Trace 事件的设计,本质上是在回答一个问题:

如果这次执行出了岔子,我需要哪些信息才能快速定位?

你越能精准地回答这个问题,你的系统就越容易调优。很多开源框架的 Trace 做得非常重,但我选择逐步加事件——每次只加当前阶段最需要的那几个,保持轻量,但足够用。


三、给 Agent 装上两只“新手”:search_web 和 run_shell

之前 Agent 只有三个本地工具:read_filegrepwrite_file。它只能在项目文件夹里读读写写,出不去。

但很多时候,我需要它去外面看看:

  • “搜索一下 DeepSeek MLA 是什么”
  • “在项目里跑一下 npm run check,告诉我有没有类型错误”

所以这次我加了两个新工具。

3.1 search_web:让 Agent 能上网查资料

实现方式很简单——通过 DuckDuckGo 的 HTML 搜索,不需要申请 API Key,完全免费。

输入:搜索关键词 + limit(默认 5 条)
输出:title / url / snippet 结构化列表

我在 ContextBuildertoolWorkflow 里加了一条规则:

如果用户问题涉及实时信息或公开知识,优先使用 search_web 获取参考资料。

举个例子:

npm run dev -- "搜索一下 DeepSeek MLA 是什么,然后总结"

Agent 会先调用 search_web 获取搜索结果,再根据搜索结果生成总结。

3.2 run_shell:让 Agent 能执行命令

这个工具让 Agent 可以在 workspaceRoot 下执行任意 shell 命令:

输入:shell 命令 + timeout(默认 30 秒)
输出:stdout / stderr / exitCode

但 shell 命令很危险,所以我在 shellSafety.ts 里维护了一个黑名单

const BLACKLIST = [
  'rm -rf',
  'sudo',
  'chmod 777',
  'dd if=',
  'mkfs',
  // ...
];

任何匹配黑名单的命令都会被拒绝执行,并返回友好的错误提示。

同时,run_shell 还有两个保护机制:

  • 超时限制:默认 30 秒,防止命令卡死。
  • 输出大小限制:最多返回 1MB,防止日志爆炸。

3.3 策略层同步更新

TaskPolicy 里新增了对 shell / web / npm / git 等关键词的识别,如果用户输入包含这些词,就会自动走 Tool Engine,而不是 Chat Engine。


四、让 Agent 学会“自己修”:Error Recovery

这部分是我今天最兴奋的一个改动。

4.1 之前失败就失败了,没有第二次机会

以前工具执行失败后,ToolResult 里虽然有 error.suggestion,但能不能恢复完全看模型自己——它得读懂建议、想清楚怎么补救、再发起新调用。

现实是:大部分情况下模型就直接放弃了。

4.2 系统级恢复:失败后自动“补一刀”

我新增了 src/recovery/ErrorRecovery.ts,它的职责很简单:

当工具返回 ok=falseerror.recoverable=true 时,系统自动判断能不能帮模型“补一刀”。

目前支持三种恢复场景:

失败场景 自动恢复动作
read_file / grep / write_file 路径错误 自动调用 list_files 列出目录内容
list_files 参数无效 回退到 workspace 根目录列表
TOOL_NOT_FOUND(工具不存在) 自动调用 list_files 帮助重新规划

4.3 恢复后的闭环

恢复流程是这样的:

tool_result (failed)
  -> tool_error (记录失败详情)
  -> recovery_started (如果有恢复计划)
  -> recovery_result (执行恢复,成功或失败)
  -> 把「原始失败信息 + 恢复结果」一起喂回模型

模型拿到的是这样的上下文:

你刚才调用 read_file("README2.md") 失败了,因为文件不存在。
我帮你执行了 list_files,当前目录下有这些文件:

  • README.md
  • package.json
  • src/

请根据这个信息重新规划你的操作。

这样模型就不用“猜”了,它拿到了真实的环境信息。

4.4 防止无限恢复的保险机制

  • 每次 run 最多自动恢复 3 次,超过就停止。
  • 同一种失败不会重复恢复(比如同一路径失败两次,第二次不再尝试)。

4.5 Trace 里的恢复记录

新增了三个 Trace 事件:

  • tool_error:记录工具失败详情和 suggestion
  • recovery_started:记录准备执行哪种恢复
  • recovery_result:记录恢复是否成功

Trace Report 里也会新增:

  • recoveryAttemptCount
  • recoverySuccessCount

这样一来,我可以清楚地知道:系统帮模型救回了多少次失败。


五、一个实际的运行例子

今天我用这个命令测试了一下:

npm run dev -- "在项目里运行 npm run check,告诉我有没有类型错误"

流程是这样的:

  1. TaskPolicy 识别到 npm run check,判定为 tool 任务。
  2. ContextBuilder 构造上下文,注入了工具工作流规则。
  3. DeepSeekToolEngine 启动,调用 run_shell("npm run check")
  4. 命令执行成功,返回 stdout。
  5. 模型根据输出,告诉我“没有类型错误”。
  6. 所有事件(context_builtengine_startedtool_calltool_result)都记录在 trace JSON 里。
  7. 我运行 npm run trace:report -- runs/run_xxx.json,生成复盘报告。

整个过程不到 10 秒,Agent 自己完成了从“理解任务”到“执行命令”到“反馈结果”的闭环。


六、记录

6.1 Trace 是越挖越深的

一开始我只记录 tool_calltool_result,已经觉得够了。但随着问题变复杂,我发现需要 context_built 来确认模型“看到了什么”,需要 engine_started 来确认“用了什么策略”。Trace 不是一次性设计好的,它是随着你对系统理解的深入,逐步生长的。

6.2 让系统自己修,比让模型自己悟更可靠

Error Recovery 让我意识到,有些失败的修复是“机械的”——路径错了就列一下目录,工具不存在就看一看可用清单。这些逻辑用代码写死比等模型“读懂 suggestion”要可靠得多、快得多。

这就像开车:你可以让司机自己看路牌找路(模型读 suggestion),也可以直接给导航(系统自动恢复)。导航更稳。

6.3 工具不是越多越好,而是越“安全”越好

新增 run_shell 的时候,我花了一半时间在写黑名单、超时、输出限制。一个工具如果会炸掉系统,那它再强大也等于零。安全是工具的第一性。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐