第 8 章 · Hooks 与生命周期扩展

05 · 实战:Hook 执行效果

用 agent 修改文件的任务,展示 after_tool 审计日志 hook 在工具调用链中的实际表现。

Hook 不只是概念

前三节实现了 HookManager、三个内置 hook,并把 hook 集成到了 agent 循环中。这一节用真实任务展示 hook 在运行中的实际效果。

审计日志 Hook:记录每一次工具调用

main.ts 中默认注册了审计日志 hook:

const hookManager = new HookManager();
hookManager.register(createAuditLogHook());

给 agent 一个需要修改文件的任务:

> 在 src/tools/search.ts 的 execute 函数开头添加空查询校验:if (!query.trim()) return '搜索关键词不能为空'

真实运行结果:

>> 开始处理: 在 src/tools/search.ts 的 execute 函数开头添加空查询校验...
  ... 思考中
  > read_file: src/tools/search.ts
  + read_file: 文件: src/tools/search.ts (94 行)
  >  执行操作
  > patch_file: src/tools/search.ts
  + patch_file: 已修改 src/tools/search.ts
  >  执行操作
  = 完成 [3次模型调用, 2次工具调用]

审计日志 hook 在每次工具调用后默默记录。任务结束后:

const auditHooks = hookManager.getHooks("after_tool");
// hook 名称: audit_log
// 工具调用总次数: 2

每条审计记录包含时间戳、工具名、参数摘要和结果预览。这些信息在 agent 运行时不会显示,但在任务结束后可以回溯——agent 做了什么、改了哪个文件、结果是什么。

Hook 的触发时机

patch_file 调用放到 agent 循环中看 hook 的触发时机:

模型返回: patch_file({ path: "src/tools/search.ts", old_content: "...", new_content: "..." })

  ├─ before_tool hook 链     ← 可以拦截或修改参数
  │   (无 hook 注册,跳过)

  ├─ 权限检查                 ← 独立于 hook 的安全层
  │   (自动放行,跳过)

  ├─ 执行 patch_file          ← 实际工具调用
  │   → "已修改 src/tools/search.ts"

  ├─ after_tool hook 链       ← 审计日志在这里记录
  │   audit_log: 记录 { tool: "patch_file", args: "...", result: "已修改..." }

  └─ after_edit hook 链       ← 自动格式化在这里触发
      (auto_format hook 未注册,跳过)

一次 patch_file 调用触发了两个 hook 时机:先 after_tool(记录日志),再 after_edit(如果注册了格式化 hook)。每个 hook 时机独立,按注册顺序执行。

Hook 的容错:报错不中断

看 HookManager 中 after_tool 的执行逻辑:

async executeAfterTool(ctx: AfterToolContext): Promise<void> {
  const hooks = this.getHooks("after_tool");
  for (const hook of hooks) {
    try {
      await (hook.handler as (ctx: AfterToolContext) => Promise<void>)(ctx);
    } catch (error) {
      console.error(`Hook ${hook.name} 错误: ${error.message}`);
    }
  }
}

即使某个 hook 抛出异常,它会被 catch 住,打印错误日志,然后继续执行下一个 hook。agent 的主流程不会因为 hook 的 bug 而中断。

这个设计很重要——hook 是"附加逻辑",不是"核心逻辑"。如果审计日志 hook 写文件失败,agent 的修改仍然有效,只是没被记录而已。

自动格式化 Hook:编辑后自动 prettier

如果注册了自动格式化 hook,patch_file 修改 .ts 文件后会自动运行 prettier:

hookManager.register(createAutoFormatHook());

// after_edit hook handler 内部:
const supportedExtensions = [".ts", ".tsx", ".js", ".jsx"];
if (ctx.path.endsWith(".ts")) {
  await execFileAsync("npx", ["prettier", "--write", ctx.path], {
    timeout: 10000,
  });
}

agent 可能写出缩进不规范的代码,但最终文件会被自动格式化。如果 prettier 不存在或执行失败,hook 会静默跳过——不影响 agent 的主流程。

before_finish Hook:退出前的最终检查

当 agent 即将返回结果时,before_finish hook 被触发。agent.ts 中的三个退出路径都调用了它:

// 路径一:模型给出纯文本回答
await options.hookManager.executeBeforeFinish({ answer, stats });

// 路径二:所有计划步骤完成
await options.hookManager.executeBeforeFinish({ answer, stats });

// 路径三:达到最大迭代次数
await options.hookManager.executeBeforeFinish({ answer, stats });

如果某个 hook 需要在 agent 结束时做汇总(比如输出审计统计、发送通知),注册为 before_finish 时机就行。

这一章做了什么

回顾第 8 章的成果:

  • Hook 类型系统:4 个时机(before_tool、after_tool、after_edit、before_finish),每个时机有独立的上下文类型
  • HookManager:按时机分组管理 hook,顺序执行,报错不中断
  • 审计日志 hook:记录每次工具调用的时间、参数、结果
  • 自动格式化 hook:文件编辑后自动运行 prettier
  • 编辑验证 hook:文件编辑后自动运行对应测试
  • agent 集成:所有 hook 调用都是可选的,不注册则静默跳过

agent 从第 7 章的"能力固定的工具"进化为"行为可插拔的平台"。下一章会实现 MCP 协议,让 agent 能接入外部工具服务器。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page