Hook 系统技术文档

1. 概述

Hook 系统允许 Skill 在 Agent 流程的 5 个检查点声明式注入/拔出提示词消息。

设计理念:insert → remove(源自酒馆破限模式)。非持久 hook 在 LLM 调用前插入、调用后拔出;持久 hook 注入后 yield 回前端,由前端持久化到 conversationHistory

核心约束:后端无状态。持久 hook 必须经前端存储才能跨请求生存。


2. 模块结构

skills_engine/
├── hooks_config.py       HookInfo dataclass + scan_hooks()
├── hook_manager.py       HookManager(inject / remove 逻辑)
├── skills_config.py      已有
├── skills_context_manager.py  已有
└── __init__.py           导出 HookInfo, scan_hooks, HookManager

文件级配置位于各 Skill 目录:

skills/<skill-name>/hooks/
├── hooks.json            元数据
└── *.md                  提示词文本

3. hooks.json 配置

{
  "hooks": [
    {
      "name": "truncated-content-hint",
      "file": "truncated-content-hint.md",
      "timing": "after_tool_call",
      "role": "system",
      "persistent": false,
      "tool_filter": ["read_file", "search_text"]
    }
  ]
}
字段 类型 必填 说明
name string hook 唯一标识
file string .md 文件名(相对于 hooks/)
timing string 5 种检查点之一
role string "system""user"
persistent bool 是否持久
tool_filter string[] after_tool_call 时有效

4. 检查点总览

timing 代码位置 作用域
after_user_input chat_stream() 开头 整个请求
before_planning _run_planning_round() 开头 规划轮
before_first_agent Agent 循环第一轮 整个 Agent loop
before_each_agent Agent 循环每一轮 当轮
after_tool_call 工具执行完毕后 当轮注入 → 下一轮 LLM 可见

before_first_agentbefore_each_agent 不互斥,第一轮同时触发。


5. Agent 流程与检查点位置

flowchart TD START([chat_stream 开始]) --> BUILD[_build_messages] BUILD --> HM[创建 HookManager] HM --> CP1{{"① after_user_input<br/>inject"}} CP1 --> TOOLS_CHECK{有工具且<br/>enable_tools?} TOOLS_CHECK -- 否 --> DIRECT_LLM["直接 LLM 调用<br/>(无规划轮、无 Agent 循环)"] DIRECT_LLM --> END_STREAM([结束]) TOOLS_CHECK -- 是 --> CP2{{"② before_planning<br/>inject"}} CP2 --> PLAN_REF["inject_prompt(planning_prompt)"] PLAN_REF --> PLAN_LLM["规划轮 LLM(无工具)"] PLAN_LLM --> PLAN_REMOVE["remove_prompt(planning_ref)<br/>remove before_planning"] PLAN_REMOVE --> NEED_TOOL{need_tool?} NEED_TOOL -- 否 --> END_STREAM NEED_TOOL -- 是 --> LOOP_START subgraph AGENT_LOOP ["Agent 循环"] LOOP_START["remove before_each_agent<br/>(拔上一轮)"] --> CP3{{"③ before_each_agent<br/>inject"}} CP3 --> FIRST{first_agent_step?} FIRST -- 是 --> CP4{{"④ before_first_agent<br/>inject"}} FIRST -- 否 --> STEP_REF CP4 --> STEP_REF["inject_prompt(step_prompt)"] STEP_REF --> AGENT_LLM["LLM 调用(带工具)"] AGENT_LLM --> HAS_TC{有工具调用?} HAS_TC -- 是 --> EXEC_TOOL["执行工具"] EXEC_TOOL --> ATC_REMOVE["remove after_tool_call<br/>(拔上一轮)"] ATC_REMOVE --> CP5{{"⑤ after_tool_call<br/>inject(per tool)"}} CP5 --> STEP_REMOVE_TC["remove_prompt(step_ref)"] STEP_REMOVE_TC --> LOOP_START HAS_TC -- 否 --> STEP_REMOVE_NOTC["remove_prompt(step_ref)"] STEP_REMOVE_NOTC --> LOOP_EXIT end LOOP_EXIT["remove before_first_agent<br/>remove after_tool_call"] --> END_STREAM

6. 各 Hook 类型完整生命周期

6.1 after_user_input(非持久)

请求级作用域。整个 chat_stream() 期间存活,不主动 remove。

sequenceDiagram participant FM as full_messages participant HM as HookManager participant LLM as LLM 调用 Note over FM: _build_messages() 完成 HM->>FM: inject("after_user_input") → append msg Note over FM: hook 消息在列表末尾 LLM->>FM: 规划轮读取 → 看到 hook ✓ LLM->>FM: Agent R1 读取 → 看到 hook ✓ LLM->>FM: Agent R2 读取 → 看到 hook ✓ LLM->>FM: Agent R3 读取 → 看到 hook ✓ Note over FM: chat_stream() 返回<br/>full_messages 被 GC 回收<br/>hook 随之消失

6.2 after_user_input(持久)

与非持久版本相同的注入位置,但额外 yield 回前端存储。每次请求都注入(无去重),跨请求通过前端 conversationHistory 带回。

sequenceDiagram participant FE as 前端 participant CH as ChatHandler participant AC as AgentClient participant HM as HookManager participant FM as full_messages Note over AC: 请求 1 HM->>FM: inject → append msg AC->>CH: yield LLMChunk(hook=h) CH->>FE: WebSocket type="hook" FE->>FE: audioQueue → historySegments → flushHistorySegments<br/>写入 conversationHistory Note over AC: 请求 2(skill 仍启用) Note over FM: 前端带回的 messages 已含 hook HM->>FM: inject → 再次 append(不去重) AC->>CH: yield LLMChunk(hook=h) Note over FM: messages 中有两份 hook<br/>(前端带回 + 本次注入) Note over AC: 请求 3(skill 关闭) Note over HM: hooks=[] → inject 不触发 Note over FM: 前端带回的 hook 仍在 messages 中<br/>作为普通消息被 LLM 读取 Note over FE: 对话清空 → hook 自然消失

6.3 before_planning

规划轮作用域。仅影响规划轮 LLM 调用。

sequenceDiagram participant FM as full_messages participant HM as HookManager participant LLM as 规划轮 LLM HM->>FM: inject("before_planning") HM->>FM: inject_prompt(planning_prompt) → planning_ref LLM->>FM: 读取 → 看到 hook + planning_prompt ✓ HM->>FM: remove_prompt(planning_ref) HM->>FM: remove("before_planning") Note over FM: hook 已不在列表中 Note over FM: Agent 循环 LLM 看不到 ✗

6.4 before_each_agent

单轮作用域。每轮 inject 前先 remove 上一轮的。

sequenceDiagram participant FM as full_messages participant HM as HookManager participant LLM as LLM rect rgb(230, 240, 255) Note over FM: Agent Round 1 HM->>FM: remove("before_each_agent") — 空操作 HM->>FM: inject("before_each_agent") HM->>FM: inject_prompt(step) → step_ref LLM->>FM: 读取 → 看到 hook ✓ HM->>FM: remove_prompt(step_ref) end rect rgb(230, 255, 230) Note over FM: Agent Round 2 HM->>FM: remove("before_each_agent") — 拔 R1 的 HM->>FM: inject("before_each_agent") — 注入新的 HM->>FM: inject_prompt(step) → step_ref LLM->>FM: 读取 → 看到新 hook ✓(R1 的已不在) HM->>FM: remove_prompt(step_ref) end rect rgb(255, 240, 230) Note over FM: Agent Round 3 HM->>FM: remove("before_each_agent") — 拔 R2 的 HM->>FM: inject("before_each_agent") — 注入新的 LLM->>FM: 读取 → 无工具调用 → break HM->>FM: remove_prompt(step_ref) end

6.5 before_first_agent

Agent loop 作用域。第一轮注入,所有轮次可见,循环结束后 remove。

sequenceDiagram participant FM as full_messages participant HM as HookManager participant LLM as LLM rect rgb(230, 240, 255) Note over FM: Agent Round 1(first_agent_step=true) HM->>FM: inject("before_first_agent") LLM->>FM: 读取 → 看到 hook ✓ end rect rgb(230, 255, 230) Note over FM: Agent Round 2(first_agent_step=false) Note over HM: 不再 inject("before_first_agent") LLM->>FM: 读取 → hook 仍在列表中 ✓ end rect rgb(255, 240, 230) Note over FM: Agent Round 3 LLM->>FM: 读取 → hook 仍在 ✓ → 无工具 → break end Note over FM: 循环结束 HM->>FM: remove("before_first_agent") Note over FM: hook 已不在列表中

6.6 after_tool_call

跨轮作用域。当轮工具完成后注入 → 下一轮 LLM 可见 → 下一次 inject 前 remove。

sequenceDiagram participant FM as full_messages participant HM as HookManager participant LLM as LLM participant TOOL as 工具执行 rect rgb(230, 240, 255) Note over FM: Agent Round 1 LLM->>FM: 请求工具 A TOOL->>FM: 工具 A 结果 HM->>FM: remove("after_tool_call") — 空操作 HM->>FM: inject("after_tool_call", tool_name="A") Note over FM: hook(R1) 在列表中 end rect rgb(230, 255, 230) Note over FM: Agent Round 2 LLM->>FM: 读取 → 看到 hook(R1) ✓ → 请求工具 B TOOL->>FM: 工具 B 结果 HM->>FM: remove("after_tool_call") — 拔 R1 的 HM->>FM: inject("after_tool_call", tool_name="B") Note over FM: hook(R2) 替换了 hook(R1) end rect rgb(255, 240, 230) Note over FM: Agent Round 3 LLM->>FM: 读取 → 看到 hook(R2) ✓ → 无工具 → break end Note over FM: 循环结束 HM->>FM: remove("after_tool_call") — 清理 R2 遗留

7. Remove 规则总表

timing remove 时机 代码位置 设计意图
after_user_input 不主动 remove 函数返回时 GC 回收
before_planning 规划轮 LLM 调用后 _run_planning_round() 末尾 仅规划轮可见
before_each_agent 下一轮 inject 前 循环体开头 每轮 LLM 看完再拔
before_first_agent Agent 循环结束后 while 之后 生命周期 = 整个 loop
after_tool_call 下一次 inject 前 工具执行分支内 下一轮 LLM 看完再拔
系统 step_ref 每步末尾 两个分支各自 remove 与 hook 系统独立

通用原则:remove 紧跟在对应 inject 之前,保证 LLM 在调用时能看到 hook。


8. 持久 hook 行为

持久 hook 与非持久 hook 的区别:

非持久 持久
inject() 注入 messages,不返回 注入 messages,返回 hook 信息供 yield 给前端
remove() 从 messages 移除 不移除(no-op,放回 _injected 记录)
去重 (每次触发都注入,不做任何去重)
跨请求 不跨请求(函数返回即消失) 前端存入 conversationHistory,下次请求带回

注意:持久 hook 不去重意味着跨请求时 messages 中可能存在多份相同 hook(前端带回的 + 本次新注入的)。这是设计如此——hook 内容通常是短小提示,冗余不影响效果。


9. 数据流

flowchart LR subgraph 启动时 SCAN["scan_hooks(enabled_skills)"] end subgraph websocket_server.py SCAN -->|"List[HookInfo]"| CH["ChatHandler(hooks=hooks)"] end subgraph chat_handler.py CH -->|hooks| CS["chat_stream(hooks=hooks)"] end subgraph agent_client.py CS --> HM["HookManager(full_messages, hooks)"] HM -->|"inject(timing)"| MSG["full_messages.append(msg)"] MSG -->|"持久 hook"| YIELD["yield LLMChunk(hook=h)"] end subgraph chat_handler.py 推送 YIELD -->|"TTSResult(hook=...)"| PUSH["websocket.send_json<br/>type='hook'"] end subgraph 前端 PUSH --> QUEUE["audioQueue.push<br/>{type: 'hook', ...}"] QUEUE --> PLAY["playNextAudio()"] PLAY --> SEG["historySegments.push<br/>{type: 'hook', ...}"] SEG --> FLUSH["flushHistorySegments()"] FLUSH --> STORE["conversationHistory.push<br/>{role, content, _hook}"] STORE -->|"下次请求带回"| CS end

10. 核心 API

HookManager

class HookManager:
    def __init__(self, full_messages: list, hooks: List[HookInfo])
    def inject(self, timing: str, **kwargs) -> List[dict]
    def remove(self, timing: str) -> None
    def inject_prompt(self, content: str, role: str = "system") -> dict
    def remove_prompt(self, msg_ref: dict) -> None
方法 作用 返回
inject(timing) 注入匹配 hook(无去重,每次触发都注入)。持久 hook 返回信息供 yield [{"name", "role", "content"}, ...]
remove(timing) 非持久 hook:从 messages 移除;持久 hook:不移除(no-op) None
inject_prompt(content) 插入系统内置提示词 消息引用(dict)
remove_prompt(ref) 按引用移除 None

scan_hooks

def scan_hooks(enabled_skills: List[str], skills_dir: str = None) -> List[HookInfo]

扫描已启用 skills 的 hooks/ 目录,校验并加载 hooks.json + .md 文件。


11. 与系统内置提示词的关系

系统内置 3 个提示词(planning_promptfirst_step_promptagent_loop_prompt)通过 inject_prompt() / remove_prompt() 管理。两者共用 HookManager 操作同一个 full_messages,但逻辑独立:

系统提示词 Skill Hook
配置方式 config.py 硬编码 hooks.json 声明式
生命周期 每步结束必拔 按 timing 规则管理
持久化 不持久 可选
前端感知 type: "hook" 消息