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

03 · 审计日志与风险拦截

用 after_tool hook 记录工具调用日志,用 before_tool hook 拦截危险操作——让 agent 的行为可追溯、可控制。

审计日志

当 agent 在真实仓库中工作时,用户需要知道它做了什么。特别是当结果不符合预期时,需要回溯"agent 在第几步做了什么操作"。

审计日志 hook 是 after_tool 类型——它在每次工具执行后记录调用信息:

export function createAuditLogHook(): Hook {
  const logs: Array<{
    timestamp: string;
    tool: string;
    args: string;
    resultPreview: string;
  }> = [];

  return {
    name: "audit_log",
    timing: "after_tool",
    handler: async (ctx: AfterToolContext) => {
      const timestamp = new Date().toISOString();
      const argsPreview = JSON.stringify(ctx.args).slice(0, 100);
      const resultPreview = ctx.result.slice(0, 100);
      logs.push({ timestamp, tool: ctx.tool, args: argsPreview, resultPreview });
    },
  };
}

记录的信息包括:

  • 时间戳——精确到毫秒,方便定位
  • 工具名——知道是搜索、读取还是修改
  • 参数摘要——搜索了什么关键词、修改了什么文件
  • 结果摘要——执行结果的前 100 个字符

日志存在 hook 的闭包中,不会写入文件。这在教学项目中足够了——如果需要持久化审计日志,可以把 logs.push(...) 改成写文件。

before_tool 拦截

before_tool hook 可以在工具执行前拦截调用。返回 skip: true 时,工具不会执行:

const result = await manager.executeBeforeTool({ tool: "search", args: {} });
if (result.skip) {
  // 工具不会执行,用 result.skipResult 作为替代
}

这和权限系统的 deny 类似,但更灵活——hook 可以看到工具参数的具体内容,做更精细的判断。

例如,一个防止修改 package.json 的 hook:

const protectPackageJson: Hook = {
  name: "protect_package_json",
  timing: "before_tool",
  handler: async (ctx: BeforeToolContext) => {
    if (
      (ctx.tool === "write_file" || ctx.tool === "patch_file") &&
      String(ctx.args.path ?? "").endsWith("package.json")
    ) {
      return {
        skip: true,
        skipResult: "package.json 不允许被直接修改,请使用 npm/pnpm 命令操作依赖",
      };
    }
  },
};

Hook 链的执行规则

多个 hook 形成一条处理链。理解链的执行规则很重要:

before_tool 链:

hook A 执行 → hook B 执行 → hook C 执行

              如果 B 返回 skip: true
              C 不执行,直接返回

after_tool 链:

hook A 执行 → hook B 执行 → hook C 执行

              如果 B 报错
              记录错误,C 继续执行

区别在于:before_toolskip 会中断后续 hook(因为工具已经被跳过了,后续 hook 没必要执行);after_tool 中错误不会中断(因为已经执行了,日志还是要记的)。

Hook 报错时的安全设计

Hook 的核心设计原则:hook 报错不中断 agent 主流程。

try {
  const result = await hook.handler(ctx);
  // ...
} catch (error) {
  console.error(`Hook ${hook.name} 错误: ${error.message}`);
  // 不 throw,继续执行后续 hook
}

为什么?因为 hook 是"附加功能"——格式化、日志、验证。它们不应该成为 agent 的单点故障。如果 Prettier 格式化失败了,agent 应该继续工作,只是少了格式化这一步。

如果某个 hook 真的需要"强制中断",它应该在 before_tool 中返回 skip: true,而不是抛出异常。

在 main.ts 中的使用

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

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

然后在调用 runAgent 时传入:

const result = await runAgent(state, model, tools, {
  permissionGuard,
  emitter,
  contextManager,
  projectRules,
  skill: skillMatch?.skill,
  hookManager,
});

如果需要更多 hook,只需多注册几个:

hookManager.register(createAuditLogHook());
hookManager.register(createAutoFormatHook());

不需要改 agent 的代码——这就是 hook 系统的可扩展性。

下一节总结 hook 系统的设计和产品价值。

登录以继续阅读

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

立即登录

On this page