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_agent 和 before_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 类型完整生命周期
请求级作用域。整个 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 随之消失
与非持久版本相同的注入位置,但额外 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)
end6.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 已不在列表中
跨轮作用域。当轮工具完成后注入 → 下一轮 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_prompt、first_step_prompt、agent_loop_prompt)通过 inject_prompt() / remove_prompt() 管理。两者共用 HookManager 操作同一个 full_messages,但逻辑独立:
|
系统提示词 |
Skill Hook |
| 配置方式 |
config.py 硬编码 |
hooks.json 声明式 |
| 生命周期 |
每步结束必拔 |
按 timing 规则管理 |
| 持久化 |
不持久 |
可选 |
| 前端感知 |
无 |
type: "hook" 消息 |
评论区