文章 / Hook 系统——工具调用的前与后
Harness

Hook 系统——工具调用的前与后

2026.07.07 · 约 4500 字

一、一个被低估的设计决策

Agent 的工作流程,本质上就是"接收指令→调用工具→返回结果"的循环。

但这个描述太简化了。真正的问题是:在工具调用之前和之后,我们有没有机会插一脚?

举几个我实际遇到的场景:

这些场景有一个共同点:它们都不是"主流程"的一部分,但少了它们,系统就是残废的。

这就是 Hook 系统 要解决的问题。

二、ECC 的 Hook 架构

ECC 定义了 6 种 Hook 事件:

事件触发时机典型用途
SessionStart会话开始时加载前序上下文、检测项目状态
PreToolUse工具执行之前安全检查、提醒、验证
PostToolUse工具执行之后格式化、类型检查、连续学习记录
UserPromptSubmit用户发送消息时消息预处理
PreCompact上下文压缩之前保存重要状态
Stop模型停止响应时审计检查、持久化摘要

每个 Hook 由三部分组成:

  1. Matcher(匹配器)——什么时候触发。ECC 用表达式来匹配,比如 tool == "Bash" && tool_input.command matches "npm|pytest"
  2. Hook 动作——触发后做什么。可以是执行一个命令行、运行一段脚本、或者调用一个 API。
  3. Profile(执行档位)——minimal / standard / strict,控制 Hook 的激进程度。

下面是 ECC 的一个实际 Hook 配置:

{
  "PreToolUse": [
    {
      "matcher": "tool == \"Bash\" && tool_input.command matches \"(npm|pnpm|yarn|cargo|pytest)\"",
      "hooks": [{
        "type": "command",
        "command": "if [ -z \"$TMUX\" ]; then echo '[Hook] 建议在 tmux 中运行长命令' >&2; fi"
      }]
    },
    {
      "matcher": "tool == \"Write\" && tool_input.file_path matches \"\\.md$\"",
      "hooks": [{
        "type": "command",
        "command": "echo '[Hook] 非文档文件不要写 .md' >&2;"
      }]
    }
  ],
  "PostToolUse": [
    {
      "matcher": "tool == \"Edit\" && tool_input.file_path matches \"\\.(ts|tsx)$\"",
      "hooks": [{
        "type": "command",
        "command": "tsc --noEmit 2>&1"
      }]
    }
  ]
}

这里的关键设计是 Matcher 是可编程的——不是硬编码的"在写文件前检查",而是可以在运行时通过表达式匹配任意条件。这让 Hook 系统的扩展性远远超过简单的"前后回调"。

三、Hook 系统的三层价值

安全层:PreToolUse 是最便宜的保险

在 ECC 的真实使用中,PreToolUse 被大量用于安全检查。你可以在工具执行之前做:

这些检查在系统层面做一次,比在每次对话中反复提醒模型要可靠得多。Hook 的保险作用在于:它不依赖模型的"自觉"。 模型可能在某次对话中被你提醒过"不要删文件",但换个场景它可能就忘了。PreToolUse 的检查是系统级的,每次都会执行。

质量层:PostToolUse 是自动化的质检员

PostToolUse 的典型用法是"写完后自动检查":

ECC 甚至有一个 Stop 阶段的 Hook,专门扫描所有被修改的文件,看有没有残留的调试代码。这个 Hook 是"阻塞模式"——检查失败会阻止整个操作被标记为完成。

一个反常识的洞察: 人是最不可靠的质检员。模型改完代码,你手动跑一次测试,十次有八次会忘记。PostToolUse 把"手动想起来"变成"每次自动做",质量不是提升了,是质变了。

学习层:Hook 是连续学习的传感器

ECC 的 Continuous Learning v2 系统,核心传感器就是 PostToolUse Hook。

每次工具调用完成后,Hook 记录下来"用了什么工具、做了什么、结果如何"。后台的分析器会从这些记录中提取模式,形成 Instinct(本能),最终演化为可重用的 Skill。

这个过程不需要人工干预——Hook 自动采集数据,分析器自动提炼模式,最终输出可复用的 Skill。如果你没有 Hook 系统,你就没有数据来源,连续学习就无从谈起。

四、没有 Hook 系统的 Agent 缺陷

场景有 Hook 系统没有 Hook 系统
执行危险命令PreToolUse 自动拦截靠模型自觉,或者弹窗确认
写完代码检查PostToolUse 自动跑检查手动想起来再跑
跨会话记忆Stop Hook 自动保存摘要每次会话归零
连续学习PostToolUse 记录数据靠人工回顾
安全审计所有操作都有日志没有系统化记录

Hook 系统的本质,是把"偶发的、靠人记着做"的事情,变成"必然的、系统自动做"的事情。

五、如何构建自己的 Hook 系统

如果你的 Agent 系统没有原生的 Hook 支持,你可以在工具调用包装层实现一个简化版。

第一步:定义 Hook 注册表

用一个 JSON 文件管理所有 Hook:

{
  "preToolUse": [
    { "matcher": "terminal", "severity": "high", "action": "check_command_safety" },
    { "matcher": "write_file", "action": "check_file_extension" }
  ],
  "postToolUse": [
    { "matcher": "terminal", "action": "record_tool_usage" },
    { "matcher": "write_file", "action": "lint_modified_file" }
  ],
  "onStop": [
    { "action": "save_session_summary" }
  ]
}

第二步:在工具调用前后包装

def execute_with_hooks(tool_name, tool_input):
    # PreToolUse
    for hook in registry.get("preToolUse", []):
        if matches(hook["matcher"], tool_name):
            execute_action(hook["action"], tool_name, tool_input)

    # 实际的工具调用
    result = execute_tool(tool_name, tool_input)

    # PostToolUse
    for hook in registry.get("postToolUse", []):
        if matches(hook["matcher"], tool_name):
            execute_action(hook["action"], tool_name, tool_input, result)

第三步:从偶发走向必然

Hook 系统的最终目标不是"加更多钩子",而是把重复的、容易遗忘的事情自动化。一个好的衡量标准是:

你的 Agent 系统,在日常使用中,还有多少事情需要你"记得去做"? 如果答案大于 0,就说明还有 Hook 可以加。

六、从今天可以做的三件事

1. 列出你的"遗忘清单"。 有哪些事情你经常忘记做?跑测试、格式化代码、保存笔记。每个"忘记"都是一个潜在的 Hook。

2. 从最简单的 PostToolUse 开始。 不需要一开始就搭完整的 Hook 框架。先选一个你最常忘记的检查(比如改完代码跑 lint),在工具调用后自动执行它。

3. 在 PreToolUse 建一个安全门。 至少对 rm -rfDROP TABLEgit push --force 这三个操作做拦截。这是投入产出比最高的 Hook。

📌
实践建议

今天就在你的工具调用流程中加上一条 PreToolUse 检查——拦截任何包含 rm -rf 的 terminal 命令。只需要三行代码,就能避免一次灾难性事故。

参考资源

💬 评论

加载中...
请友善发言