@dabit3: https://x.com/dabit3/status/2055319214202777894

X AI KOLs Following 工具

摘要

一份技术指南,介绍了 Agent Hooks 这一概念,通过生命周期钩子为智能体工作流添加确定性控制点,使开发者能够在关键时刻强制执行规则并运行验证。

https://t.co/7UuFh2jVTl
查看原文
查看缓存全文

缓存时间: 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、尝试动作、验证动作、完成本轮操作、关闭会话。

运行模型

最简单的思维模型是:

事件 → 可选匹配器/过滤器 → 处理器 → 结果

事件是一个生命周期时刻,例如 PreToolUseStop

可选的匹配器或过滤器限定了 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 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:根据请求路由上下文

当 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 可能会创建一个循环。显式存储状态,读取该状态,并且只

相似文章

@djfarrelly: https://x.com/djfarrelly/status/2052779234234380479

X AI KOLs Timeline

本文主张,AI Agent 的开发应基于稳定的执行原语,而非会随新兴编排模式频繁更迭的僵化框架。文章强调,采用持久化步骤、持久状态、并行协调、事件驱动流程以及可观测性设计,可有效避免因最佳实践不断演进而付出的高昂重写代价。