为什么需要 hooks
系统提示词中的指令是建议。足够固执或困惑的 LLM 可以忽略它们。对于大多数智能体行为,这正是你想要的——指令给模型适应的空间。 但某些要求不是建议。“每个敏感工具调用都必须被记录。” “当组织处于只读模式时,写操作被阻止。” “超过 ¥50k 的付款在执行前需要人工确认。” 这些是不变量——关于系统的事实,无论模型在任何给定轮次中做什么决定,都必须成立。 Hook 是在智能体执行生命周期中明确定义的点在 LLM 循环之外运行的代码。LLM 看不到 hook。LLM 无法与 hook 争论。LLM 无法说服 hook 跳过某个步骤。如果PreToolUse hook 返回 allow=False,工具调用就不会发生——无论推理跟踪有多坚持。
这是关键的架构区别:
Hooks 是 FIM One 如何将”智能体应该…”转变为”智能体无法绕过…”的方式。
钩子的插入点
目前定义了三个钩子点。每个都标记了智能体在一次循环迭代中跨越的边界:
同一点的多个钩子按优先级顺序运行。较早的
PreToolUse 钩子重写的参数会传递给后续钩子,因此中间件可以组合。
何时使用 hook 与指令
决定是否用提示词指令或 hook 来解决需求,与”运行时断言 vs. 代码注释”的计算方式相同:
经验法则:如果错误行为会导致事故,使用 hook。如果错误行为只是小烦恼,指令就足够了。
hook 合约
hook 是PreToolUseHook、PostToolUseHook 或 SessionStartHook 的子类,具有一个必需方法:
HookContext 包含 tool_name、tool_args、agent_id、user_id 和一个灵活的 metadata 字典,引擎会填充每个请求的事实(组织 id、对话 id、连接器操作的 requires_confirmation 标志等)。
返回的 HookResult 控制结果:
allow: bool = True— 工具调用是否继续进行(对于PostToolUse/SessionStart忽略)error: str | None— 人类可读的原因,在被阻止时作为观察结果呈现给 LLMmodified_args: dict | None— 如果设置,在执行前替换工具参数modified_result: Any | None— 如果设置(PostToolUse),在返回给 LLM 前替换观察结果side_effects: list[str]— hook 所做操作的审计跟踪,合并到智能体的跟踪中
案例研究:FeishuGateHook
在这个系统之上发布的第一个钩子是 FeishuGateHook — 一个 PreToolUse 钩子,它将任何标记为 requires_confirmation=True 的工具转换为发送到组织 Feishu 群组的人工审批卡片。
这个钩子演示了完整的生命周期:
这个设计的优势:
- 工具调用被真正暂停。 智能体的 SSE 流在”我将调用
oa__purchase_pay”和观察结果之间暂停。用户看到智能体在等待,这与底层实际发生的情况相匹配。 - 审批在进程重启后仍然存在。 待处理行在数据库中,而不是在内存中。如果后端在卡片未处理时重启,下一次轮询会从中断处继续。
- 决策被审计。
ConfirmationRequest保存payload、responded_at、responded_by_open_id和最终状态 — 一份谁在何时批准了什么的可审计记录。 - 决策循环中没有 LLM。 模型生成工具调用。人类生成判决。钩子是确定性的桥梁。
FeishuGateHook 依赖于配置的 Feishu 频道 — 钩子通过频道的 send_interactive_card() 方法发送卡片,并监听频道解析的回调事件。这种分离是有意的:钩子拥有”审批状态机”,频道拥有”IM 平台机制”。同一个钩子明天可以针对 Slack 或 WeCom,而无需改变其逻辑 — 只需改变频道实现。
候选钩子(暂未规划)
有四种钩子模式适用于相同的生命周期。目前的路线图中都没有这些模式;在有部署需求之前,它们会暂时搁置:
自定义钩子层也会按相同原则暂时搁置:通过每个智能体的 YAML 配置(
hooks: [...])声明在匹配的工具事件上运行的 shell 命令或 Python 可调用对象。这遵循了现代智能体框架(Claude Code、OpenDevin)逐渐采用的相同模式——基于钩子的执行机制可以将“必须始终执行”的逻辑从提示词中移出。
Hooks vs. Channels
这两个抽象解决了正交的问题:
Hooks 消费 Channels — 需要与外部世界通信的 hook(发送卡片、发布警报、上报给群组)会调用组织的 Channel。没有任何 hook 使用的 channel 仍然有用(例如智能体可以通过工具主动发送通知),但审批门模式特别需要两个部分都到位。
换句话说:Channels 是”我在哪里与人类交谈”的管道,Hooks 是”我何时必须与人类交谈”的策略。生产环境中的人机协作工作流需要两者。
当前状态(v0.8.4)
已交付内容与后续计划一览:-
✅
HookRegistry、HookContext、HookResult基础组件已接入 ReAct 和 DAG -
✅
PreToolUseHook/PostToolUseHook/SessionStartHook抽象基类 -
✅
FeishuGateHook——已完整实现,包括ConfirmationRequest表、轮询循环、超时/过期处理,以及由回调驱动的状态变更 -
✅ Feishu 通道回调端点,可解码
card.action.trigger并更新待处理记录 -
✅ 智能体级钩子声明:
agent.model_config_json.hooks.class_hooks会在每个 ReAct/DAG 会话中解析为已实例化的HookRegistry -
✅ 钩子会在所有执行入口触发:主聊天路径(门户、API、DAG)、委派的子智能体(
CallAgentTool),以及工作流AGENT节点。Eval Center 有意绕过钩子(自动化评估不得因等待人工审批而阻塞)。 委派的智能体不会继承调用方的注册表——它会根据自身的model_config_json构建自己的注册表。由于确认闸门会自动附加到每个智能体,重新构建的结果包含的内容比继承更多:被委派的智能体会获得该闸门,以及它自身声明的其他钩子;闸门读取的是被委派智能体自身的require_confirmation_for_all,而不是调用方的配置。两个执行入口都会默认拒绝:如果无法构建智能体的钩子,就不会运行该智能体。 -
❌
AuditLogHook、ReadOnlyGuard、ResultTruncateHook、ConnectorRateLimitHook(不在计划内) - ❌ 用户自定义的 YAML 钩子声明(不在计划内)
FeishuGateHook)本身也是一项生产功能,因此我们选择提前交付基础框架,以赶上 2026-04-24 路演,而不是等到完整的钩子目录都准备就绪。