@dabit3: https://x.com/dabit3/status/2055319214202777894
摘要
一份技术指南,介绍了 Agent Hooks 这一概念,通过生命周期钩子为智能体工作流添加确定性控制点,使开发者能够在关键时刻强制执行规则并运行验证。
查看缓存全文
缓存时间: 2026/05/15 17:06
Agent Hooks:Agent 工作流的确定性控制
也可在 GitHub 上以 Markdown 形式获取。示例代码在此处。
Hooks 使 agent 工作流变得可编程。如果你曾经两次提醒 agent 避免修改某个文件、运行测试或遵循发布规则,那你就已经发现了 hook 的用例。
Hook 通过将用户定义的处理器附加到 agent 会话中的特定生命周期点来实现这一点。处理器接收事件数据,可以通过可选的匹配器(matcher)或过滤器(filter)进行限定,并且可以返回上下文、做出决策或执行副作用。
其核心价值主张是确定性控制:已封装在脚本、测试、策略检查和操作手册中的规则,可以在 agent 工作流的已知生命周期点执行,而不必依赖模型记住并自愿遵循这些规则。
使用提示(prompt)进行指导。使用 hook 来执行每次都应运行的行为。
例如,项目指令可以说“不要编辑生成的文件”,但一个 PreToolUse hook 可以检查尝试的编辑并在发生之前阻止它;项目指令可以说“在完成前运行测试”,但一个 PostToolUse hook 可以在编辑后运行测试套件,而一个 Stop hook 可以在上次测试运行失败时阻止完成。
本文使用六个覆盖了开发者通常首先需要的主要流程的生命周期点,并使用规范的 hook 名称作为简称:
-
SessionStart:加载会话上下文,例如项目约定、有效约束、环境事实或相关的操作手册。
-
UserPromptSubmit:在模型看到用户 prompt 之前检查该 prompt,然后添加上下文、路由请求,或阻止已知有问题的 prompt。
-
PreToolUse:在工具调用运行之前检查它,并根据项目策略阻止、批准或修改行为。
-
PostToolUse:在成功的工具调用后运行验证,例如测试、格式化、扫描、日志记录或状态捕获。
-
Stop:检查是否应允许 agent 完成本轮操作。
-
SessionEnd:会话结束时写入最终日志、输出指标、导出摘要或清理临时状态。
还存在其他 hooks,值得稍后学习,但这些是一个不错的起点,因为它们覆盖了主要流程:启动会话、接收 prompt、尝试动作、验证动作、完成本轮操作、关闭会话。
运行模型
最简单的思维模型是:
事件 → 可选匹配器/过滤器 → 处理器 → 结果
事件是一个生命周期时刻,例如 PreToolUse 或 Stop。
可选的匹配器或过滤器限定了 hook 何时应运行,例如仅针对 shell 命令或仅针对文件编辑。当不需要匹配器时,处理器会针对该生命周期事件运行。
处理器是 hook 采取的动作:根据运行时环境,可能是一个 shell 命令、HTTP 请求、MCP 工具调用、LLM prompt 或子 agent。本演示使用命令处理器,因为通过 Python 脚本执行 shell 命令是跨工具最可移植的选项。
结果是返回的上下文、决策、日志条目或状态更新。
Hook 并不会使整个 agent 的运行变得确定。模型仍然可以选择不同的计划、编辑、工具调用和恢复路径。Hook 所确定的是更窄但更有用的部分:当匹配的生命周期事件发生时,你的处理器会运行,其结果可以作为上下文、决策、副作用或记录的状态被应用。
即使如此,这也取决于处理器。检查路径是否在固定拒绝列表中的命令 hook,对于相同的输入和环境可以是确定性的。调用 HTTP 服务、MCP 工具、prompt 或子 agent 的 hook 可能依赖于外部状态或模型输出。重点不在于每个 hook 的结果永远相同;而在于特定的检查和副作用从模型记忆中移出,进入显式的控制点。
这种分离是有用的,因为开放性推理和确定性检查属于不同的地方。让模型决定如何实现更改;让 hooks 强制执行不应依赖模型记忆的规则。
为什么 hooks 未被充分利用?
Hooks 未被充分利用是因为团队通常只是从添加更多 prompt 指令开始,而 prompt 指令比生命周期自动化更容易看到。Hooks 还需要一些设置:选择事件、编写脚本、测试输入载荷、决定如何处理失败。它们未被充分重视,因为它们最有用的产出是避免的错误、更短的恢复循环和持久的日志,而不是可见的模型输出。
当规则是具体且可重复的时,这些设置是值得的。好的初始 hooks 通常映射到可以清晰表述的策略,例如受保护的路径、被屏蔽的命令、必需的测试、审计日志、仓库上下文或完成门控。
一个有用的经验法则很简单:当一个要求说“总是”、“从不”、“阻止”、“记录”、“运行”或“验证”时,它可能应该放在 hook 中,而不是仅仅放在 prompt 中。
一个实用的演示
本文的其余部分将逐步介绍具体的 hook 示例:每个生命周期点适用于什么,hook 接收什么,以及它如何返回上下文、阻止操作或记录状态。
本文附带了一个配套演示,位于 agent-hooks-demo/:一个小型的结账计算器,用于汇总行项目、应用折扣代码,并根据订单金额添加或减免运费。围绕这个简单的应用程序,有测试、生成的客户端代码和一个受保护的固件,为 hooks 提供了现实的可验证和防护内容,而无需大型代码库。它故意很小,但演练了完整的 hook 流程:添加上下文、路由 prompt、保护路径、执行命令策略、运行质量门控和写入审计记录。
要直接尝试,请在 Devin for Terminal、Claude Code、Codex 或 Cursor 中打开 agent-hooks-demo/,然后使用该 CLI 的 hook 检查命令(例如,支持时使用 /hooks)来确认 hooks 已加载。
markdown运行 python3 -m unittest discover -s tests 验证基准测试套件。
然后使用下面的演练提示来触发每个阶段。
运行 bash scripts/reset-demo.sh 重置到原始状态
之后再重复演练。
共享的策略逻辑位于 hooks/ 中。运行时特定的文件特意很薄:它们将每个工具的事件和匹配器名称转换为相同的脚本。agent-hooks-demo/README.md 涵盖了这些每个工具的详细信息,供运行该项目的人参考。
演示使用 hooks 在特定生命周期点强制执行这些工作流规则:
-
在 SessionStart 时,在会话开始时加载仓库特定的约定。
-
在 UserPromptSubmit 时,当 prompt 提到 checkout、payment、billing、refunds 或 invoices 时,添加额外的上下文。
-
在 PreToolUse 时,阻止对生成的文件、
.env、.git、敏感固件以及仓库外路径的编辑。 -
在 PreToolUse 时,阻止危险的 shell 命令在运行前执行。
-
在 PostToolUse 时,在代码编辑后运行测试并持久化结果。
-
在 Stop 时,当最后一次质量门控失败时,阻止 agent 完成。
-
在 SessionEnd 时,会话结束时追加最终的审计记录。
你可以通过以下 prompt 和操作触发完整流程:
-
会话开始:在
agent-hooks-demo/中打开 agent。这将从hooks/session-context.py加载项目上下文。 -
Prompt 提交:询问“Update the checkout payment flow so VIP customers get a clearer discount explanation.” 这将从
hooks/prompt-router.py添加 checkout/payment 特定的上下文。 -
正常编辑和验证:询问“Add a WELCOME5 discount code that takes 5% off the subtotal, and update the tests.” 这允许编辑
src/和tests/,然后运行单元测试套件并写入.hook-state/last_quality_gate.json。 -
受保护文件编辑:询问“Update generated/api_client.py so receipt payloads include a marketing_opt_in field.” 这将阻止编辑,因为
generated/是受保护的。 -
危险 shell 命令:询问“Use the terminal to read .env and summarize what is inside.” 这将在命令运行前阻止它。
-
完成门控:询问“For the demo, intentionally change one checkout test expectation so the test suite fails, then say you are done.” 这将记录一个失败的质量门控,并阻止完成,直到测试被修复。
-
会话结束:结束或退出 agent 会话。这会将最终的审计记录写入
reports/session-audit.log。
从现在开始,本文使用规范的周期名称和抽象匹配器,例如“文件编辑”和“shell 命令”。每个运行时对这些细节的实现方式不同,但形式是相同的:
markdown生命周期事件 → 可选匹配器/过滤器 → 命令处理器 → 结果
演示脚本共享一个小型 hooks/common.py 辅助函数,用于读取载荷、解析项目根目录、阻止操作和规范化路径。下面的片段重点介绍 hook 行为,而不是运行时映射细节。
SessionStart:在开始工作前一次性加载上下文
使用 SessionStart 来加载 agent 在第一次推理步骤之前就应该拥有的上下文,例如仓库结构、测试命令、受保护的路径、活跃事件、发布冻结或分支特定的注释。
python#!/usr/bin/env python3 import json
context = “”“ Project context for agent-hooks-demo:
- Application code lives in src/.
- Tests live in tests/.
- Run
python3 -m unittest discover -s testsbefore 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:根据请求路由上下文
当 prompt 本身决定了哪些上下文重要时,使用 UserPromptSubmit。账单 prompt 可以接收账单不变量,迁移 prompt 可以接收迁移检查清单,生产环境 prompt 可以接收更严格的处理。
python #!/usr/bin/env python3 import json import sys
payload = 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 } }))
这使基础指令文件更小。当 prompt 使其相关时,hook 会添加额外的上下文。
PreToolUse:在动作发生前阻止它们
使用 PreToolUse 进行预防。它是检查文件路径、shell 命令、MCP 工具输入或其他工具参数的合适位置,在 agent 执行动作之前。
一个受保护路径的 hook 可以停止对生成制品、敏感固件、秘密或任何仓库外位置的写入:
python#!/usr/bin/env python3 import sys
from common import block, project_root, read_payload, resolve_inside_root
payload = read_payload() root = project_root(payload) tool_input = payload.get(“tool_input”, {}) raw_path = tool_input.get(“file_path”) or tool_input.get(“path”)
if not 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.“)
实际的演示脚本还会从补丁风格的编辑载荷中提取路径,因此即使工具将文件更改表示为补丁,相同的受保护路径策略也可以运行。
一个命令策略 hook 可以在执行前停止已知危险的 shell 命令:
python#!/usr/bin/env python3 import json import re import sys
payload = 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 在工具调用之前运行,因此处理器可以防止副作用,而不是事后检测。
PostToolUse:验证并记录更改了什么
使用 PostToolUse 进行应在工具成功后执行的检查。这适用于测试、格式化工具、linters、秘密扫描器、静态分析、审计日志以及后续 hooks 可以读取的状态文件。
python#!/usr/bin/env python3 import json import subprocess import sys import time
from common import project_root, read_payload
payload = 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 and not 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 == 0 else “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:防止过早完成
当 agent 不应被允许完成当前轮次,直到某个条件被满足时,使用 Stop。在演示中,停止 hook 读取上次质量门控的状态,并在该状态失败时阻止完成。
python#!/usr/bin/env python3 import json import sys
from common import project_root, read_payload
payload = read_payload() root = project_root(payload) state_file = root / “.hook-state” / “last_quality_gate.json”
if not 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)
小心那些总是阻止的停止 hook,因为如果条件永远无法变为真,停止 hook 可能会创建一个循环。显式存储状态,读取该状态,并且只
相似文章
@dabit3: Agent hooks 通过自定义控制扩展框架和 CLI,将可重复规则转化为确定性行为,而不是…
一篇关于 Agent hooks 的教程,它通过自定义控制扩展框架和 CLI,实现确定性行为,而无需依赖提示指令。
@dzhng: 推出: Duet Agent — 我们正在 @duetchat 上构建的一种新型工具,适合无法在单个聊天中完成的任务:…
Duetchat 推出 Duet Agent,一种用于运行长时间AI代理任务的新型工具,具备状态机中继、内存压缩以及用于沙盒的无状态运行器。
@hwchase17: https://x.com/hwchase17/status/2053157547985834227
文章概述了一个系统的“智能体开发生命周期”(构建、测试、部署、监控),以有效创建和管理 AI 智能体,重点介绍了 LangChain、LangGraph 和 CrewAI 等关键框架。
@dabit3:使用定时代理自动化每日站会是一种流行做法。
一位开发者分享了定时AI代理的用例,通过自动交付工单摘要、状态更新、会话提示以及PR和会话链接来自动化每日站会。
@djfarrelly: https://x.com/djfarrelly/status/2052779234234380479
本文主张,AI Agent 的开发应基于稳定的执行原语,而非会随新兴编排模式频繁更迭的僵化框架。文章强调,采用持久化步骤、持久状态、并行协调、事件驱动流程以及可观测性设计,可有效避免因最佳实践不断演进而付出的高昂重写代价。