项目链接:https://github.com/dabit3/agent-hooks-in-depth/tree/main

Hooks 的作用,说白了就是让 Agent 的工作流变得可编程。如果你曾经因为某个文件碰不得、某个测试必须跑、某条发布规则得守,而不得不一遍又一遍地去提醒 Agent——那其实你早就撞上 Hooks 的典型场景了,只是当时没意识到。

它的实现方式并不复杂:把用户自定义的处理逻辑,挂到 Agent 会话里特定的生命周期节点上。每个 handler 会拿到对应的事件数据,可以用一个可选的 matcher 或 filter 把触发范围收窄,然后它要么返回一段上下文,要么做出一个决策,要么执行某个副作用操作。

这套机制最核心的价值,笔者认为就两个字:确定性。那些本来已经写进脚本、测试、策略检查、操作手册里的规则,过去只能指望模型自己记住、自己自觉去遵守,现在可以直接挂在工作流里已知的生命周期节点上,到点就跑,不再看模型的"记性"和"心情"。

这里的分工其实可以一句话讲清楚:提示词负责"引导",Hooks 负责"那些每次都必须发生的行为"。举个具体的例子——项目说明里当然可以写一句"别改自动生成的文件",但一个 PreToolUse Hook 能在这次编辑真正落地之前就把它拦下来;项目说明里也可以写"完成前记得跑测试",但一个 PostToolUse Hook 能在代码改完后自动把测试套件跑一遍,再配上一个 Stop Hook,上一次测试没过它就直接不让 Agent 收工。前者是叮嘱,后者是闸门,分量完全不一样。

这篇文章会重点讲六个生命周期节点,它们基本覆盖了开发者上手时最先会碰到的主流程。下面统一用这些 Hook 的标准名称作为简写:

  • **SessionStart**:会话一开始就加载上下文,比如项目约定、当前生效的约束、环境信息,或者一份相关的操作手册。
  • **UserPromptSubmit**:在模型看到用户提示词之前先检查一遍,然后给它补充上下文、做请求路由,或者直接挡掉一个已知有问题的提示。
  • **PreToolUse**:在一次工具调用真正执行前先审一遍,根据项目策略决定是放行、拦截,还是改写它的行为。
  • **PostToolUse**:在工具调用成功之后跑校验,比如测试、格式化、扫描、记录日志、保存状态。
  • **Stop**:判断这一轮是不是真的可以让 Agent 结束了。
  • **SessionEnd**:会话结束时写最终日志、刷新指标、导出总结,或者清理临时状态。

当然 Hooks 不止这六个,其余的值得以后再慢慢摸,但先掌握这一组是最划算的,因为它们正好把主流程串了起来:会话开始 → 收到提示 → 试图执行某个动作 → 校验这个动作 → 结束这一轮 → 关闭会话。

它到底是怎么运转的

最简单的心智模型,其实就一行:

event → optional matcher/filter → handler → outcome
```![](http://cdn.zhipoai.cn/640afd53.jpg)

这里的 **event(事件)**,指的是一个生命周期时刻,比如 `PreToolUse` 或者 `Stop`。

**matcher 或 filter** 是可选的,作用是把 Hook 的触发范围收窄,比如只对 shell 命令生效、或者只对文件编辑生效。如果不需要收窄,那这个 handler 就对该生命周期事件统统生效。

**handler** 是 Hook 真正要做的那个动作。具体形态取决于运行环境,可能是一条 shell 命令、一个 HTTP 请求、一次 MCP 工具调用、一段 LLM 提示,或者一个子 Agent。本文的演示统一用命令式 handler,因为"shell 出去跑一个 Python 脚本"是跨工具兼容性最好的做法。

**outcome(结果)** 就是它返回的东西:一段上下文、一个决策、一条日志,或者一次状态更新。

需要说清楚的一点是:Hook 并不会让整个 Agent 运行变成确定性的。模型该选不同的方案、做不同的编辑、调不同的工具、走不同的恢复路径,照样会选。Hook 让它变确定的部分要窄得多,但很有用——只要某个匹配的生命周期事件发生了,你的 handler 就一定会跑,它的结果可以作为上下文、决策、副作用或记录下来的状态被应用上去。

更严格地说,这还得看 handler 本身。一个把路径拿去和固定拒绝清单比对的命令式 Hook,在相同输入和相同环境下确实是确定的;但一个会去调 HTTP 服务、MCP 工具、提示词或子 Agent 的 Hook,结果就可能依赖外部状态或模型输出了。所以重点不是"每个 Hook 的结果永远一模一样",而是"把那些具体的检查和副作用,从模型的记忆里挪出来,搬进显式的控制点"。

这种分离之所以有意义,是因为开放式推理和确定性检查,本来就该待在不同的地方。让模型去决定一个改动该怎么实现,让 Hooks 去执行那些不该依赖模型记性的规则——各司其职。

为什么 Hooks 一直被低估
---------------

笔者觉得 Hooks 被低估,原因挺现实的:团队遇到问题,第一反应往往是再往提示词里加几句话,而提示词这种东西是看得见的,生命周期自动化是看不见的。再加上 Hooks 上手确实需要一点点准备工作——挑一个事件、写一个脚本、测一下输入的 payload、想清楚失败了怎么处理。它不受待见,还因为它最有用的产出是"没犯的错"、"更短的返工循环"和"留得住的日志",而不是肉眼可见的模型输出。这些东西,平时根本不显眼。

但只要这条规则是具体的、可重复的,这点前期投入很快就回本了。好的"第一个 Hook",通常都对应着团队本来就能讲得清清楚楚的策略:受保护的路径、被禁用的命令、必须跑的测试、审计日志、仓库上下文,或者完成前的闸门检查。

这里有个特别好用的经验法则:当一条需求里出现了"总是"、"绝不"、"拦截"、"记录"、"运行"、"校验"这类词,那它八成属于 Hook,而不该只待在提示词里。

来一个能跑的演示
--------

剩下的篇幅,会用具体的 Hook 例子走一遍:每个生命周期节点能干什么、它收到什么、它怎么返回上下文、怎么拦截一个动作、怎么记录状态。

本文配了一个配套演示,放在 `agent-hooks-demo/` 里:一个很小的结账计算器,能对订单条目求和、应用折扣码,并根据订单金额自动加上或免掉运费。围绕这个简单的小程序,还放了测试、自动生成的客户端代码,以及一个受保护的测试夹具,这样 Hooks 就有真实的东西可以去校验和守护,又不必拖一个庞大的代码库进来。它故意做得很小,但把完整的 Hook 流程都跑了一遍:注入会话上下文、做提示路由、保护路径、执行命令策略、跑质量闸门、写审计记录。

想直接试的话,用 Devin for Terminal、Claude Code、Codex 或 Cursor 打开 `agent-hooks-demo/`,然后用对应 CLI 的 Hook 检查命令(支持的话比如 `/hooks`)确认 Hooks 都加载上了。先跑一句 `python3 -m unittest discover -s tests` 验证基线测试套件没问题,再用下面这些演练提示去逐个触发各个阶段。每次重复演练前,记得先跑 `bash scripts/reset-demo.sh`。

共用的策略逻辑都放在 `hooks/` 里。各运行环境特定的文件刻意写得很薄:它们只是把每个工具自己那套事件名和 matcher 名,翻译成同一批脚本。具体每个工具怎么对接,`agent-hooks-demo/README.md` 里写得很清楚,跑这个项目的人可以去看。

这个演示用 Hooks 在特定生命周期节点上执行了下面这几条工作流规则:会话开始时,加载仓库特定的约定;提示词里出现 checkout、payment、billing、refunds 或 invoices 时,补充额外上下文;试图编辑生成文件、`.env`、`.git`、敏感夹具或仓库外路径时,在 `PreToolUse` 阶段拦截;危险 shell 命令在跑起来之前就被 `PreToolUse` 挡掉;代码改完后在 `PostToolUse` 阶段跑测试并把结果持久化;上一次质量闸门没过时,`Stop` 阶段阻止 Agent 收工;会话结束时,`SessionEnd` 追加一条最终审计记录。

想把整个流程从头跑通,可以按下面这串提示和动作来:

1. **会话开始**:在 `agent-hooks-demo/` 里打开 Agent。这会从 `hooks/session-context.py` 加载项目上下文。
2. **提交提示**:输入 `Update the checkout payment flow so VIP customers get a clearer discount explanation.`。这会从 `hooks/prompt-router.py` 补充结账/支付相关的上下文。
3. **正常编辑并校验**:输入 `Add a WELCOME5 discount code that takes 5% off the subtotal, and update the tests.`。这会放行对 `src/` 和 `tests/` 的编辑,然后跑 unittest 套件并写入 `.hook-state/last_quality_gate.json`。
4. **改受保护文件**:输入 `Update generated/api_client.py so receipt payloads include a marketing_opt_in field.`。这次编辑会被拦截,因为 `generated/` 是受保护的。
5. **危险 shell 命令**:输入 `Use the terminal to read .env and summarize what is inside.`。这条命令在执行前就被挡掉。
6. **完成闸门**:输入 `For the demo, intentionally change one checkout test expectation so the test suite fails, then say you are done.`。这会记录一次失败的质量闸门,并在测试修好之前一直拦着不让收工。
7. **会话结束**:结束或退出 Agent 会话。这会往 `reports/session-audit.log` 写一条最终审计记录。

从这里往后,本文统一用标准的生命周期名称,以及"文件编辑"、"shell 命令"这类抽象的 matcher 说法。每个运行环境的具体写法都不一样,但骨架是一样的:

```plaintext
lifecycle event → optional matcher/filter → command handler → outcome

演示脚本共用了一个小小的 hooks/common.py 辅助模块,负责读 payload、定位项目根目录、拦截动作、归一化路径。下面的代码片段只聚焦 Hook 本身的行为,对接细节就略过了。

SessionStart:开工之前,先一次性把上下文喂进去

SessionStart 适合放那些"模型在迈出第一步推理之前就该知道"的上下文,比如仓库结构、测试命令、受保护的路径、当前正在处理的线上故障、发布冻结期,或者分支特定的注意事项。

#!/usr/bin/env python3import jsoncontext = """Project context for agent-hooks-demo:- Application code lives in src/.- Tests live in tests/.- Run `python3 -m unittest discover -s tests` before calling work complete.- Do not edit generated/, fixtures/sensitive/, .env, .env.local, .git, or files outside the repo.- Checkout behavior is customer-visible, so update tests with behavior changes.""".strip()print(json.dumps({    "hookSpecificOutput": {        "hookEventName": "SessionStart",        "additionalContext": context    }}))

这种做法适合那些"动态到值得现算、又重要到值得自动注入"的上下文。至于那些静态规则,照样可以待在普通的项目说明文件里,没必要全塞进来。

UserPromptSubmit:根据请求本身来路由上下文

UserPromptSubmit 适合用在"提示词内容本身决定了哪些上下文重要"的场景。一个跟账单有关的提示,可以顺手收到账单相关的不变量;一个迁移类提示,可以收到一份迁移检查清单;一个涉及生产环境的提示,则可以收到更严格的处理要求。

#!/usr/bin/env python3import jsonimport syspayload = json.load(sys.stdin)prompt = payload.get("prompt", "").lower()if any(term in prompt for term in ["refund", "billing", "invoice", "payment", "checkout"]):    context = (        "This request touches checkout or payment behavior. Update tests, "        "avoid sensitive fixtures, and describe any customer-visible behavior change."    )    print(json.dumps({        "hookSpecificOutput": {            "hookEventName": "UserPromptSubmit",            "additionalContext": context        }    }))

这样做的好处是基础指令文件可以保持精简。额外的上下文,只在提示词让它"变得相关"的时候,由 Hook 临时补上去。

PreToolUse:在动作发生之前就把它挡住

PreToolUse 是用来"预防"的。它正是检查文件路径、shell 命令、MCP 工具输入或其他工具参数的合适位置——在 Agent 真正动手之前。

一个保护路径的 Hook,可以挡住对生成产物、敏感夹具、密钥,或任何仓库外文件的写入:

#!/usr/bin/env python3import sysfrom common import block, project_root, read_payload, resolve_inside_rootpayload = read_payload()root = project_root(payload)tool_input = payload.get("tool_input", {})raw_path = tool_input.get("file_path") or tool_input.get("path")ifnot raw_path:    sys.exit(0)try:    target, rel = resolve_inside_root(raw_path, root)except ValueError:    block(f"{raw_path} resolves outside the repo.")protected_prefixes = ("generated/", "fixtures/sensitive/", ".git/")protected_exact = {".env", ".env.local"}if rel in protected_exact or any(rel.startswith(prefix) for prefix in protected_prefixes):    block(f"{rel} is protected. Use application code or tests instead.")

实际的演示脚本还会从补丁式(patch)的编辑 payload 里把路径抠出来,这样即便某个工具是用 patch 形式来表示文件改动的,同一套保护路径策略照样能跑。

另一个命令策略 Hook,可以在已知的危险 shell 命令执行之前就把它拦下来:

#!/usr/bin/env python3import jsonimport reimport syspayload = json.load(sys.stdin)tool_input = payload.get("tool_input", {})command = tool_input.get("command") or payload.get("command") or payload.get("cmd") or""normalized = " ".join(command.split())deny_patterns = [    (r"\brm\s+-rf\s+(/|\.|~|\$HOME)", "destructive recursive delete"),    (r"\b(drop|truncate)\s+table\b", "destructive database command"),    (r"\b(cat|less|more|tail|head)\s+.*\.env\b", "reading env files"),    (r"(>\s*|tee\s+|cat\s+>\s*)(generated/|fixtures/sensitive/|\.env)", "writing protected paths from the shell"),    (r"deploy\.py\s+production\b", "production deploy"),]for pattern, reason in deny_patterns:    if re.search(pattern, normalized, flags=re.IGNORECASE):        print(f"Blocked by command policy: {reason}. Command: {normalized}", file=sys.stderr)        sys.exit(2)

这里真正有用的特性是"时机":前置 Hook 跑在工具调用之前,所以 handler 可以直接阻止那个副作用发生,而不是事后才发现"哦它已经干了"。

PostToolUse:校验并记录发生了什么改动

PostToolUse 适合那些"应该在工具成功之后才跑"的检查。测试、格式化工具、linter、密钥扫描、静态分析、审计日志,以及供后面的 Hook 读取的状态文件——都很适合放这儿。

#!/usr/bin/env python3import jsonimport subprocessimport sysimport timefrom common import project_root, read_payloadpayload = read_payload()root = project_root(payload)raw_path = payload.get("tool_input", {}).get("file_path") or payload.get("tool_input", {}).get("path") or""if raw_path andnot raw_path.endswith((".py", ".json")):    sys.exit(0)state_dir = root / ".hook-state"reports_dir = root / "reports"state_dir.mkdir(exist_ok=True)reports_dir.mkdir(exist_ok=True)started = time.time()result = subprocess.run(    [sys.executable, "-m", "unittest", "discover", "-s", "tests"],    cwd=root,    text=True,    capture_output=True,    timeout=60,)record = {    "status": "passed"if result.returncode == 0else"failed",    "exit_code": result.returncode,    "edited_file": raw_path,    "duration_seconds": round(time.time() - started, 2),    "stdout_tail": result.stdout[-4000:],    "stderr_tail": result.stderr[-4000:]}(state_dir / "last_quality_gate.json").write_text(json.dumps(record, indent=2) + "\n")with (reports_dir / "hook-audit.log").open("a") as log:    log.write(f"quality_gate status={record['status']} file={raw_path}\n")if record["status"] == "failed":    print("Quality gate failed. Inspect .hook-state/last_quality_gate.json and fix the failure before finishing.", file=sys.stderr)    sys.exit(2)

记一个简单的对照就行:用后置 Hook 去看"发生了什么",并把结果反馈回工作流;用前置 Hook 去处理"这个动作必须在执行前就被拦住"的情况。

Stop:别让它过早地宣布"我做完了"

Stop 适合用在"某个条件没满足之前,就不该让 Agent 结束这一轮"的场景。在这个演示里,stop Hook 会去读上一次质量闸门的状态,状态是失败就不让收工。

#!/usr/bin/env python3import jsonimport sysfrom common import project_root, read_payloadpayload = read_payload()root = project_root(payload)state_file = root / ".hook-state" / "last_quality_gate.json"ifnot state_file.exists():    sys.exit(0)state = json.loads(state_file.read_text())if state.get("status") == "failed":    print("Quality gate failed. Fix the tests before saying the task is complete.", file=sys.stderr)    sys.exit(2)

对那种"永远拦着"的 stop Hook 要当心:如果那个条件根本不可能变成 true,stop Hook 就会把自己卡进死循环。正确的做法是显式地存状态、读这个状态,只有当状态明确说"这一轮还没到能结束的时候"才去拦。

SessionEnd:临走前留个记录

SessionEnd 适合做收尾和留最终证据。这块保持简单就好:写一行审计、刷新指标、导出一份总结、删掉临时文件,或者记下这次会话为什么结束。

#!/usr/bin/env python3import jsonimport timefrom common import project_root, read_payloadpayload = read_payload()root = project_root(payload)reports_dir = root / "reports"reports_dir.mkdir(exist_ok=True)record = {    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),    "event": "SessionEnd",    "session_id": payload.get("session_id"),    "reason": payload.get("reason", "unknown"),    "transcript_path": payload.get("transcript_path")}with (reports_dir / "session-audit.log").open("a") as log:    log.write(json.dumps(record) + "\n")

它的活儿就一个:会话已经没了,给它留下一条记录。

这个演示到底想证明什么

agent-hooks-demo/ 这个项目,想证明的其实是四件事:上下文会在模型开始干活之前自动加载好;不想要的动作会在它发生之前就被挡住;校验会在 Agent 还活着的时候就跑起来;以及——能不能收工,取决于记录下来的状态,而不是模型自己的"信心"。一段好的现场演示流程应该很短:先要一个正常的结账代码改动,让大家看到质量闸门在跑;再要一次对 generated/api_client.py 的编辑,演示它被拦下来;然后模拟一个失败的测试,演示收工被卡住;最后结束会话,把 reports/ 里的审计日志亮出来。

Hooks 跟提示词、CI、人工评审,各自的位置在哪

Hooks 用得最好的时候,是每一层都有自己明确的活儿:

  • 项目说明:编码风格、架构指导、命名约定、测试偏好,以及示例。
  • Hooks:必需的上下文、前置策略、后置校验、完成闸门,以及日志。
  • CI:在 Agent 产出 diff 之后做独立的二次验证。
  • 人工评审:产品判断、各种权衡取舍、不可逆的风险,以及最终的责任归属。

什么都往 Hooks 里塞,会造出一堆没必要的自动化;什么都往提示词里塞,又会让"必须发生的行为"全押在模型听不听话上。务实的切法很简单:提示词管引导,Hooks 管控制。

怎么落地

别一上来就奔着一整套治理体系去,先从一条有用的规则开始。一个很强的"第一版实现",是写一个前置 Hook,挡住对 generated/.env 和敏感夹具的编辑——因为它好解释、好测试,而且立刻就能见到价值。第二个该实现的,通常是一个后置质量闸门:在编辑之后跑一条最快的、有意义的测试命令,把结果写进 .hook-state/last_quality_gate.json;紧接着再来一个完成 Hook,去读这个状态文件,质量闸门没过就不让收工。这些做完之后,再加会话开始时的上下文注入、按提示词路由、以及最终的审计记录。

这个顺序能让开发者很快尝到甜头:重复的提醒变少了、误改受保护文件的情况变少了、改完之后反馈更快了,Agent 说"我做完了"之前需要人工去核对的东西也变少了。

一句话总结

Hooks 让 Agent 工作流变得更靠谱,靠的就是一件事:把那些可重复的规则,从模型的记忆里挪出来,搬进会在已知生命周期节点上自动运行的代码里。

这件事对谁都有意义:对那些不想一遍遍重复叮嘱的个人开发者,对那些想要共享一致仓库行为的团队,对那些希望 Agent 在现有工程管控体系内运转的公司,都是如此。Agent 照样能推理、写代码、从错误里恢复过来——但测试、策略、日志和完成闸门,这些会作为工作流里确定性的那部分,雷打不动地跑起来。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

在这里插入图片描述

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

Logo

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

更多推荐