learn-claude-code s04:Hooks——如何把扩展逻辑挂在 Agent 循环上而不侵入循环
2026/9/5 21:01:37 网站建设 项目流程

learn-claude-code s04:Hooks——如何把扩展逻辑挂在 Agent 循环上而不侵入循环

【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code

本篇技术文章基于 learn-claude-code 课程第 4 章(s04)的文档与源码,完整讲解 Agent 循环的 Hook 扩展机制:如何用一张事件注册表加两个入口函数,把权限检查、日志、输入注入、退出控制等扩展逻辑从循环体中剥离出去。读完你可以掌握四个生命周期事件(UserPromptSubmit、PreToolUse、PostToolUse、Stop)的触发时机与返回值语义,并能对照 s04_hooks/code.py 复现一个"循环只负责调度、扩展全部挂在外面"的 Agent。

问题:扩展逻辑硬编码进循环,核心循环迅速膨胀

s03 阶段的 Agent 已经有了权限检查(见 s03_permission/README.zh.md),但它是以check_permission()的形式硬编码agent_loop函数体内的。文档指出,每新增一个检查——"记录每次 bash 调用"、"操作后自动 git add"——都要修改agent_loop函数,循环很快变成这样:

def agent_loop(messages): while True: # ... LLM call ... for block in response.content: if block.type != "tool_use": continue log_to_file(block) # 加一行 check_permission(block) # 加一行 notify_slack(block) # 又加一行 output = execute(block) auto_git_add(block) # 再加一行 # ... 很快循环就认不出来了

你想扩展的是 Agent 的行为,但你改的却是循环本身。s04 的设计原则因此被概括为一句话(引自 s04_hooks/README.zh.md):

"挂在循环上,不写进循环里"—— hook 在工具执行前后注入扩展逻辑。

解决方案:四个生命周期事件覆盖一个完整的 agent cycle

s03 的循环和权限逻辑完全保留,唯一的变动是把check_permission()从循环体内移到 hook 上。循环不再直接调用任何检查函数,改为trigger_hooks("PreToolUse", block),由注册表决定跑什么。四个事件覆盖一个完整 agent cycle:

事件触发时机典型用途
UserPromptSubmit用户输入提交后、进入 LLM 前输入验证、注入上下文
PreToolUse工具执行前权限检查、日志记录
PostToolUse工具执行后副作用(自动 git add 等)、输出检查
Stop循环即将退出时收尾清理、决定是否继续循环

扩展通过register_hook()添加,循环只调用trigger_hooks()

核心实现:Hook 注册表

注册表与两个入口函数

实现位于 s04_hooks/code.py,就是一个字典加两个函数:

HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []} def register_hook(event: str, callback): HOOKS[event].append(callback) def trigger_hooks(event: str, *args): for callback in HOOKS[event]: result = callback(*args) if result is not None: # A hook result blocks this tool call. return result return None

trigger_hooks按注册顺序依次执行回调,只要任一回调返回非None值就立即短路返回;全部返回None则整体返回None。从源码结构看,这个"首个非None返回值生效"的约定是整个机制的控制流基础。

返回值语义:None表示放行,非None表示干预

不同事件的返回值含义并不相同,这是使用 s04 hook 系统最容易混淆的一点:

  • PreToolUse:返回非None时,本次工具执行被阻止,返回值字符串会作为tool_result的 content 回喂给模型;
  • Stop:返回非None时,循环不退出,返回值被注入为一条 user 消息后继续循环;
  • UserPromptSubmit 与 PostToolUse:返回值不参与控制流,这两个事件纯粹用于观察与副作用。

五个 hook 回调逐一拆解

s04_hooks/code.py 末尾一次性注册了全部回调:

register_hook("UserPromptSubmit", context_inject_hook) register_hook("PreToolUse", permission_hook) register_hook("PreToolUse", log_hook) register_hook("PostToolUse", large_output_hook) register_hook("Stop", summary_hook)

UserPromptSubmit:context_inject_hook——进入 LLM 前拦截用户输入

def context_inject_hook(query: str): """Inject current working directory info into every prompt.""" print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m") return None # return None = no modification, let prompt through

它在主循环中、用户输入之后立即触发(s04_hooks/code.py):

query = input("s04 >> ") trigger_hooks("UserPromptSubmit", query) # ← 进入 LLM 之前 history.append({"role": "user", "content": query}) agent_loop(history)

典型用途是输入验证和上下文注入,例如在此处把工作目录、时间等元信息附加到每条 prompt 上。

PreToolUse:permission_hook——s03 权限逻辑的迁移

这是 s04 相对 s03 最实质的变化。s03 的权限管道在 s03_permission/code.py 中是三道闸门串联的check_permission()(硬拒绝 → 规则匹配 → 用户确认),在 s03_permission/code.py 处被直接写进循环。s04 把同样的逻辑包装成permission_hook(s04_hooks/code.py):

DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] def permission_hook(block): """PreToolUse: s03 check_permission() logic moved here.""" if block.name == "bash": for pattern in DENY_LIST: if pattern in block.input.get("command", ""): return "Permission denied by deny list" for kw in DESTRUCTIVE: if kw in block.input.get("command", ""): choice = input(" Allow? [y/N] ").strip().lower() if choice not in ("y", "yes"): return "Permission denied by user" if block.name in ("read_file", "write_file", "edit_file"): path = block.input.get("path", "") if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): choice = input(" Allow? [y/N] ").strip().lower() if choice not in ("y", "yes"): return "Permission denied by user" return None

注意它的返回约定:命中DENY_LIST直接返回拒绝字符串(硬拒绝);命中DESTRUCTIVE或路径逃逸出WORKDIR时询问用户,拒绝则返回字符串(软询问);都没命中则return None放行。被阻止时,返回的字符串会进入tool_result,模型能看到拒绝原因并自行调整策略。

PreToolUse:log_hook 与 PostToolUse:large_output_hook——纯观察型 hook

def log_hook(block): """PreToolUse: log every tool call.""" args_preview = str(list(block.input.values())[:2])[:60] print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m") return None def large_output_hook(block, output): """PostToolUse: warn on large output.""" if len(str(output)) > 100000: print(f"\033[33m[HOOK] Large output from {block.name}: {len(str(output))} chars\033[0m") return None

这两个 hook 永远返回None,不参与控制流。文档中的 PostToolUse 示例还展示了"自动 git add"这类副作用场景的挂载点。注意log_hook对参数做了 60 字符截断预览(args_preview),避免把整条命令打印到终端——日志 hook 本身也应当克制。

Stop:summary_hook——退出前触发,可强制继续

Stop 在stop_reason != "tool_use"时、循环即将退出前触发(s04_hooks/code.py):

if response.stop_reason != "tool_use": force = trigger_hooks("Stop", messages) if force: # hook returned a message → inject it and continue messages.append({"role": "user", "content": force}) continue return
def summary_hook(messages: list): """Print a summary when the loop is about to stop.""" tool_count = sum(1 for m in messages for b in (m.get("content") if isinstance(m.get("content"), list) else []) if isinstance(b, dict) and b.get("type") == "tool_result") print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m") return None # return None = allow stop, return string = force continuation

summary_hook通过遍历消息中的tool_result块统计本次会话的工具调用次数。这里体现了 Stop 事件的双面性:返回None允许正常退出;返回字符串则把该字符串作为新的 user 消息注入并强制循环继续——从源码结构看,这为"任务没做完不让 Agent 停"这类质量门禁提供了挂载点。

循环体:只改了一处

s04 的agent_loop与 s03 结构完全一致,差异点只有一处——执行前由硬编码检查改为触发 PreToolUse(s04_hooks/code.py):

for block in response.content: if block.type != "tool_use": continue # s03: if not check_permission(block): ... # s04: hook 替代硬编码 blocked = trigger_hooks("PreToolUse", block) if blocked: results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(blocked)}) continue handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input) if handler else f"Unknown: {block.name}" trigger_hooks("PostToolUse", block, output) results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

四个 hook 覆盖了 agent cycle 的关键节点:输入 → 执行前 → 执行后 → 退出。循环只负责调用trigger_hooks(),具体逻辑全在 hook 回调里。

相对 s03 的变更

组件之前 (s03)之后 (s04)
扩展方式check_permission()硬编码在循环里HOOKS注册表 +trigger_hooks()
新函数register_hooktrigger_hooks
hook 回调context_inject_hookpermission_hooklog_hooklarge_output_hooksummary_hook
循环直接调用check_permission()调用trigger_hooks("PreToolUse", ...)
退出控制trigger_hooks("Stop", ...)可阻止退出
输入拦截trigger_hooks("UserPromptSubmit", ...)可注入上下文

运行与验证

前置条件:安装 requirements.txt 中声明的依赖(anthropic>=0.25.0python-dotenv),并设置环境变量MODEL_ID(必需)、ANTHROPIC_BASE_URL(可选,源码通过 s04_hooks/code.py 用os.environ["MODEL_ID"]读取模型名,未设置会直接抛KeyError)。

cd learn-claude-code python s04_hooks/code.py

文档建议的三个测试 prompt(引自 s04_hooks/README.zh.md):

  1. Read the file README.md—— 应该直接通过,观察[HOOK]日志;
  2. Create a file called test.txt—— 通过后观察 PostToolUse 是否触发;
  3. Delete all temporary files in /tmp—— bash + rm 触发权限 hook。

观察重点:每次工具执行前是否出现[HOOK]日志;权限被拒时,是 hook 拦截的而不是循环里硬编码的。

后续演进:hook 注册表成为课程的复用底座

s04 引入的这套注册表并非一次性玩具。在仓库中检索trigger_hooks/register_hook可以发现,从 s05_todo_write/code.py、s06_subagent/code.py 一直到 s16_workflow_runtime/code.py,后续各章节的 Agent 实现都沿用了同一套事件模型。在最终的整合实现 s15_integrated_harness/code.py 中,HOOKS注册表原样保留,源码注释点明了设计意图:"Hooks are intentionally outside tool handlers. The loop can add permission, logging, and stop behavior without changing each individual tool."(hook 刻意放在工具处理器之外,循环可以添加权限、日志与停止行为而无需改动任何单个工具)。从源码结构看,s15 中permission_hook还针对非交互场景做了增强,例如检测到异步线程中无法弹交互确认时直接拒绝——这正体现了把权限逻辑挂在 hook 上的好处:策略可以独立演进,而不用回头改循环

小结

s04 用约 10 行代码(一张字典 + 两个函数)完成了 Agent harness 的扩展点抽象:

  • 注册表HOOKS)把"事件名 → 回调列表"显式化,扩展以register_hook()追加,循环零改动;
  • 短路返回约定(非None即干预)让单个 hook 具备阻止工具执行或阻止循环退出的能力;
  • 事件语义分离:PreToolUse 与 Stop 参与控制流,UserPromptSubmit 与 PostToolUse 只做观察与副作用;
  • 迁移而非重写:s03 的权限逻辑原封不动包装为permission_hook,验证了"扩展逻辑可以整体外移"这一设计。

理解了这套机制,就掌握了 s04_hooks/README.md 中给出的下一课线索——给 Agent 一个 TodoWrite 计划工具(s05),以及后续各章中所有 hook 回调的实际形态。

【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询