首页
/ 如何用 MCP 生命周期钩子(_Stop、_PostCompact)扩展 buzz-agent 的执行循环?

如何用 MCP 生命周期钩子(_Stop、_PostCompact)扩展 buzz-agent 的执行循环?

2026-09-11 21:35:13作者:魏献源Searcher

buzz-agent 是一个通过 ACP(JSON-RPC 2.0 over stdio)接收 session/prompt 的 LLM agent 子进程,它的主循环是:调用 LLM → 执行 MCP 工具调用 → 把结果喂回去 → 重复,直到 LLM 停止调用工具。有些场景下你希望在这个循环里插入自己的逻辑:比如 LLM 已经发出 end_turn,但测试还是红的,你希望 agent 继续干活;或者上下文被压缩(compaction/handoff)之后,希望把 todo 状态重新注入新的上下文。

buzz 用一套 MCP 生命周期钩子解决这件事:任何 MCP server 只要暴露以 _ 开头的工具,agent 就会在定义好的生命周期点调用它。这套约定不需要任何 MCP 协议改动——钩子就是普通的 MCP 工具,通过 tools/list 发现、tools/call 调用,对 LLM 不可见,且默认关闭,必须由操作者通过环境变量显式开启。本文按「实现钩子 server → 在 agent 侧开启 → 验证行为」的顺序走一遍完整路径。

两个钩子点的契约

目前 buzz-agent 实现了两个钩子点,命名与 Open Plugin Spec 的事件约定对齐(_Stop 对应 Stop 事件,_PostCompact 对应 PostCompact),详见 docs/MCP_DRIVEN_HOOKS.md

钩子 触发时机 输入 输出语义 典型用途
_Stop LLM 发出 end_turn 之后、agent 真正接受它之前 {} 非空文本 = 反对(objection),agent 继续循环;空 = 不反对,agent 停止 强制 todo 完成——只要还有未完成任务就反对停止
_PostCompact 上下文压缩/交接之后、下一次 LLM 请求之前 {} 非空文本 = 注入到新的上下文;空 = 什么都不注入 历史被总结重置后,重新注入 todo 列表状态

信任模型上有几条硬约束,写钩子前需要知道:

  • 钩子对 LLM 不可见:以 _ 开头的工具名会被从发给 LLM 的工具列表里过滤掉;LLM 如果直接调用钩子,调用会被拒绝(按未知工具处理)。
  • 钩子是建议性(advisory)而非权威的:agent 用超时和预算限制防止一个有 bug 或恶意的钩子把 agent 拖死。
  • 钩子的响应被作为 tool-result 消息注入历史(信任级别低于 system 消息),并且输出会做 JSON 编码,用于防 prompt 注入。

在 MCP server 中实现钩子

实现侧没有任何特殊 API:让你的 MCP server 在工具列表里返回一个裸名(bare name)以 _ 开头的工具即可。buzz-agent 文档给出的示例是一个测试 runner server,在测试失败时阻止 end_turn

{
  "name": "_Stop",
  "description": "Returns failing test summary if suite is red.",
  "inputSchema": { "type": "object" }
}

返回非空文本即表示反对,返回空字符串即允许 agent 停止。

需要注意 buzz-agent 对 MCP 的两条接入约束(见 crates/buzz-agent/README.md):

  • 传输只有 stdio,没有 HTTP/SSE。agent 在 initialize 响应中显式声明 mcpCapabilities.http: false, sse: false
  • MCP server 由 ACP 客户端在 session/new 里传入,agent 把每个 server 作为 stdio 子进程拉起。也就是说,钩子 server 的注册不经过 agent 的环境变量——你需要在你使用的 ACP 客户端(buzz-acp、Zed 等)的 session/new 参数里添加该 server:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "session/new",
  "params": {
    "cwd": "/work",
    "mcpServers": [
      {
        "name": "testguard",
        "command": "/usr/local/bin/testguard-mcp",
        "args": [],
        "env": []
      }
    ]
  }
}

上面的 testguard/usr/local/bin/testguard-mcp 按你的实际 server 名与二进制路径替换;name 字段就是后续 agent 侧 allowlist 要填的 server 名。工具名在 agent 内部以 server__tool(双下划线分隔)做命名空间,所以 _Stop 钩子的全限定名形如 testguard___Stop

在 agent 侧开启钩子并调参

钩子默认关闭。开启方式是给 buzz-agent 进程设置环境变量(agent 的全部配置都走环境变量,没有配置文件),相关项来自 docs/MCP_DRIVEN_HOOKS.md 的配置表:

环境变量 默认 说明
MCP_HOOK_SERVERS 未设置 = 无钩子 allowlist:* 表示所有 server,或逗号分隔的 server 名
BUZZ_AGENT_HOOK_TIMEOUT_MS 2500 单次钩子调用超时(毫秒)
BUZZ_AGENT_STOP_MAX_REJECTIONS 3 每个 prompt 的 _Stop 反对预算;0 = 禁用 _Stop

MCP_HOOK_SERVERS 被设计为标准环境变量名,意图是跨 agent 采用;allowlist 的解析逻辑(空值/未设置视为关闭、* 通配、逗号分隔名单)在 crates/buzz-agent/src/config.rsHookServersparse_hook_servers 中。

结合 crates/buzz-agent/README.md 的 Quick Start,一个开启了钩子的启动命令长这样(sk-ant-... 替换为你自己的 API key,testguard 替换为你在 session/new 中注册的名字):

cargo build --release -p buzz-agent

BUZZ_AGENT_PROVIDER=anthropic \
ANTHROPIC_API_KEY=sk-ant-... \
ANTHROPIC_MODEL=claude-sonnet-4-5 \
MCP_HOOK_SERVERS=testguard \
./target/release/buzz-agent

两个参数怎么取值,文档给出的约束是:

  • 超时:单次钩子调用默认 2.5 秒,超时按“不反对”处理(fail-open);agent 只会在连续第二次超时时才杀掉该 server 的进程组,容忍一次性的偶发慢调用。相关逻辑在 crates/buzz-agent/src/mcp.rscall_hooks
  • 反对预算:每个 prompt 内 _Stop 最多被接受 3 次反对;耗尽后 agent 无论如何都停止,预算在下一个 prompt 时重置。call_hooks 会并行调用 allowlist 内所有暴露了该钩子的 server,按注册顺序返回结果,丢弃空响应、错误和超时——钩子永不阻塞 agent。

验证钩子是否生效

仓库自带的集成测试就是现成的验证路径。buzz-agent 的测试策略是“真实子进程、无 mock”:tests/fake_llm.rs 起真实的 TCP listener 返回脚本化 JSON,tests/bin/fake_mcp.rs 是一个可用环境变量操控行为的假 MCP server 二进制。运行:

cargo test -p buzz-agent

其中 crates/buzz-agent/tests/permission_boundary.rs 直接驱动了钩子链路:它设置 MCP_HOOK_SERVERS=fake,让一个纯文本轮次触发 _Stop gate、钩子反对一次、agent 继续循环,然后断言 _Stop 恰好通过 call_hooks 到达 MCP server 一次,且该调用不产生权限询问。如果你的钩子 server 行为符合 _Stop 契约,这类断言在真实 server 上对应的可观察现象是:

  • agent 在 LLM 发出 end_turn没有结束轮次,而是继续执行(_Stop 返回了非空文本,反对被接受);
  • _Stop 返回空或超时时,agent 正常结束轮次;
  • 连续超时会在 agent 日志(stderr)里出现警告,连续第二次超时后该 server 被终止并标记为 dead。

边界与限制

  • 钩子不是插件系统crates/buzz-agent/README.md 明确说它们是 advisory、fail-open、有预算上限的——不要把它们当成可以强制执行外部策略的机制。
  • reply guard 不是钩子。buzz-agent 在同一个 _Stop gate 上还有一个进程内的反对:BUZZ_AGENT_REQUIRE_REPLY=1 时,轮次即将以“没有任何发布到 Buzz 的尝试”结束时,会给模型一条提醒。它没有对应的 hook 工具、没有 server 可加进 allowlist,但它的提醒占用同一个 BUZZ_AGENT_STOP_MAX_REJECTIONS 预算——同一轮同时带钩子反对和提醒只计一次拒绝、两段文本都会送达;预算设为 0 会同时禁用两者。详见 crates/buzz-agent/README.md 的 Reply Guard 一节。
  • 目前只有 _Stop_PostCompact 两个钩子点已实现;_PreToolUse_PostToolUse 因与 MCP Interceptors 工作组(SEP-2624)在协议层解决的问题重叠而推迟,SessionStartSessionEnd 等是后续候选——如果你的扩展方案依赖这些钩子点,文档明确说它们尚未提供。

完成 cargo test -p buzz-agent 通过后,剩下的调优空间只有两个旋钮:BUZZ_AGENT_HOOK_TIMEOUT_MS(你的钩子逻辑实际耗时多少)和 BUZZ_AGENT_STOP_MAX_REJECTIONS(允许模型被“打回”多少次),两者都按上面的约束取值即可。

登录后查看全文
热门项目推荐
相关项目推荐