理解什么是Hook、为什么需要Hook、本项目的15种Hook类型与核心实现
Hook就像生活中的"插队点"——在某个流程执行到特定位置时,允许外部代码"插进来"做点事情。
Hook(钩子)是一种观察者模式的应用:系统在关键位置发布事件,注册的监听器(Hook)可以响应这些事件,执行自定义逻辑。
Hook模式的优势: ┌──────────────────────────────────────────────────┐ │ · 解耦: 核心流程不需要知道Hook的存在 │ │ · 可扩展: 新增功能只需添加Hook,不修改核心代码 │ │ · 可插拔: Hook可以随时注册/注销 │ │ · 顺序控制: 通过Order字段控制执行顺序 │ └──────────────────────────────────────────────────┘ Hook模式的代价: ┌──────────────────────────────────────────────────┐ │ · 调试困难: 逻辑分散在多个Hook中,不易追踪 │ │ · 执行顺序: 多个Hook的顺序可能影响结果 │ │ · 性能开销: 每个Hook点都有方法调用开销 │ │ · 错误传播: 一个Hook出错可能影响整个流程 │ └──────────────────────────────────────────────────┘ 常见Hook框架对比: ┌──────────────────────────────────────────────────┐ │ 框架 Hook类型 特点 │ │─────────────────────────────────────────────────│ │ Git Hooks pre-commit等 代码提交流程控制 │ │ React Hooks useState等 组件状态管理 │ │ Webpack tapable 构建流程插件 │ │ Spring @PostConstruct Bean生命周期 │ │ 本项目 AgentHook Agent执行生命周期 │ └──────────────────────────────────────────────────┘
Hook系统核心类:
┌────────────────────────────────────────────────────────┐
│ AgentHook 接口 │
│ · supportedTypes() → 监听哪些Hook类型 │
│ · getOrder() → 执行顺序 (越小越先执行) │
│ · isAsync() → 是否异步执行 │
│ · onHook(event) → Hook的实际逻辑 │
│ 返回 ALLOW / WARN / BLOCK / │
│ NEEDS_APPROVAL │
└───────────────────────┬────────────────────────────────┘
│ @Component 自动注册
▼
┌────────────────────────────────────────────────────────┐
│ AgentHookPublisher (发布器) │
│ │
│ 构造函数注入 List<AgentHook> → 按 AgentHookType 分组 │
│ 每组按 order 排序 │
│ │
│ publish(type, event): │
│ for each hook in hooks[type]: ← 按order排序 │
│ if async → 提交到线程池,不阻塞 │
│ else → 同步调用 onHook() │
│ if result == BLOCK && isBeforeType: │
│ → 立即返回,后续Hook和主流程都不执行 │
└────────────────────────────────────────────────────────┘
支持类:
├── AgentHookType ← 15种Hook类型枚举
├── AgentHookEvent ← 事件数据(agentId, taskId, request, response...)
├── AgentHookResult ← 返回结果(ALLOW/WARN/BLOCK)
└── HookContext ← ConcurrentHashMap, Hook间传递数据
| # | Hook类型 | 触发时机 | 能阻断? | 触发位置 |
|---|---|---|---|---|
| 1 | BEFORE_AGENT_EXECUTION 可阻断 | Agent执行前(熔断/限流之后) | 是 | AgentTaskOrchestrator |
| 2 | AFTER_AGENT_EXECUTION | Agent执行成功完成后 | 否 | AgentTaskOrchestrator |
| 3 | ON_AGENT_ERROR | Agent执行失败时 | 否 | AgentTaskOrchestrator |
| 4 | ON_AGENT_TIMEOUT | Agent执行超时时 | 否 | AgentTaskOrchestrator |
| 5 | ON_ITERATION_START | 每轮 ReAct think-act-observe 循环开始 | 否 | ReAct/Graph 执行链路 |
| 6 | ON_ITERATION_END | 每轮 ReAct 循环结束 | 否 | ReAct/Graph 执行链路 |
| 7 | ON_INSTRUCTION_INJECT | 每轮 LLM 调用前动态追加指令 | 否 | InstructionAgentHook |
| 8 | BEFORE_LLM_CALL 可阻断 | 消息列表组装完成,LLM API调用前 | 是 | AbstractLlmAgent |
| 9 | AFTER_LLM_CALL | LLM调用返回后 | 否 | AbstractLlmAgent |
| 10 | BEFORE_TOOL_CALL 可阻断/可审批 | 工具调用前,适合权限、人机审批和风险控制 | 是 | AbstractLlmAgent / Graph工具节点 |
| 11 | AFTER_TOOL_CALL | 工具执行完成后 | 否 | AbstractLlmAgent / Graph工具节点 |
| 12 | ON_USER_MESSAGE 可阻断 | 用户消息预处理后,传给Agent前 | 是 | ConversationController |
| 13 | ON_SYSTEM_PROMPT | 系统提示词构建后,LLM调用前 | 否 | AbstractLlmAgent |
| 14 | ON_OUTPUT 预留 | 输出发送给客户端前 | 否 | 输出链路扩展点 |
| 15 | HOOK_STREAMING | Hook执行过程中的流式事件 | 否 | SSE 推送/前端观测 |
ON_SYSTEM_PROMPT 是 Agent 启动或系统提示词构建阶段的静态扩展点,ON_INSTRUCTION_INJECT 是 ReAct 每轮迭代前的动态指令扩展点;两者都用于提示词治理,但触发粒度不同。
Agent执行的完整流程 + Hook触发层次:
用户发送消息
│
▼
┌─────────────────────────────────────────────┐
│ 消息级 Hook │
│ ON_USER_MESSAGE ← 可修改/阻断用户输入 │
│ 可以: 修改用户消息内容 / 阻断请求 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ Agent级 Hook │
│ BEFORE_AGENT_EXECUTION ← 执行前,可阻断 │
│ AFTER_AGENT_EXECUTION ← 成功后 │
│ ON_AGENT_ERROR / ON_AGENT_TIMEOUT │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 提示词与消息级 Hook │
│ ON_SYSTEM_PROMPT ← 系统提示词构建后 │
│ ON_INSTRUCTION_INJECT ← 每轮动态指令注入 │
│ BEFORE_LLM_CALL ← LLM调用前,可阻断 │
│ AFTER_LLM_CALL ← LLM调用后 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ ReAct迭代级 Hook │
│ ON_ITERATION_START ← 每轮循环开始 │
│ BEFORE_TOOL_CALL ← 工具调用前/可审批 │
│ AFTER_TOOL_CALL ← 工具调用后 │
│ ON_ITERATION_END ← 每轮循环结束 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 输出与观测 Hook │
│ ON_OUTPUT ← 输出发送前预留扩展 │
│ HOOK_STREAMING ← Hook流式事件观测 │
└─────────────────────────────────────────────┘
典型执行位置:
┌─────────────────────────────────────────────┐
│ ConversationController → AgentTaskOrchestrator│
│ ├── 熔断检查 │
│ ├── 限流检查 │
│ └── AbstractLlmAgent / Graph ReAct Loop │
│ ├── LLM 调用 │
│ ├── 工具调用 / HITL 审批 │
│ └── SSE 流式输出 │
└─────────────────────────────────────────────┘
项目当前不止一个 Hook 实现。它们分别覆盖安全审计、链路追踪、上下文摘要、人机审批、动态指令、消息模型治理和 Skill 注入等场景:
| Hook实现 | 主要职责 | 典型触发点 |
|---|---|---|
SecurityEventHook | 异步记录用户消息、Agent错误和超时审计日志 | ON_USER_MESSAGE / ON_AGENT_ERROR / ON_AGENT_TIMEOUT |
TelemetryHook | 基于 Micrometer Tracing 创建和关闭 Agent 执行 Span | BEFORE_AGENT_EXECUTION / AFTER_AGENT_EXECUTION |
SummarizationHook | 长上下文接近阈值时自动摘要压缩 | BEFORE_LLM_CALL |
HumanInTheLoopHook | 工具调用前根据策略返回放行、拒绝或需要人工审批 | BEFORE_TOOL_CALL |
InstructionAgentHook | 在 ReAct 迭代中动态注入额外指令 | ON_INSTRUCTION_INJECT |
MessagesModelHook | 对模型消息列表进行治理和扩展 | BEFORE_LLM_CALL |
SkillsAgentHook | 把可用 Skill 信息接入 Agent 执行上下文 | Agent/消息构建链路 |
SecurityEventHook
文件: hub-api/src/main/java/com/example/agenthub/api/security/SecurityEventHook.java
┌──────────────────────────────────────────────────┐
│ @Component │
│ public class SecurityEventHook implements AgentHook {│
│ │
│ 支持的Hook类型: │
│ ├── ON_USER_MESSAGE ← 记录用户消息 │
│ ├── ON_AGENT_ERROR ← 记录Agent错误 │
│ └── ON_AGENT_TIMEOUT ← 记录Agent超时 │
│ │
│ 执行顺序: 10 (很早执行) │
│ 是否异步: true (不阻塞主流程) │
│ │
│ 依赖: AuditLogService (审计日志服务) │
│ │
│ 实现逻辑: │
│ ├── ON_USER_MESSAGE: │
│ │ auditLogService.log(userId, agentId, │
│ │ message.substring(0, 200)) │
│ │ // 截取前200字符记录 │
│ │ │
│ ├── ON_AGENT_ERROR: │
│ │ auditLogService.log(userId, agentId, │
│ │ "ERROR: " + errorMessage) │
│ │ │
│ └── ON_AGENT_TIMEOUT: │
│ auditLogService.log(userId, agentId, │
│ "TIMEOUT after " + timeout + "s") │
└──────────────────────────────────────────────────┘
TelemetryHook 使用 Micrometer Tracing 抽象为每次 Agent 执行创建追踪 Span,底层可接 OpenTelemetry、Zipkin 等实现:
TelemetryHook
文件: hub-agent-core/.../hook/TelemetryHook.java
┌──────────────────────────────────────────────────┐
│ TelemetryHookConfiguration │
│ @ConditionalOnClass("io.micrometer.tracing.Tracer")│
│ @ConditionalOnBean(Tracer) 注册 TelemetryHook │
│ │
│ 支持的Hook类型: │
│ ├── BEFORE_AGENT_EXECUTION ← 创建 Span │
│ ├── AFTER_AGENT_EXECUTION ← 标记成功,关闭 │
│ ├── ON_AGENT_ERROR ← 标记错误,关闭 │
│ └── ON_AGENT_TIMEOUT ← 标记超时,关闭 │
│ │
│ 执行顺序: 10 (高优先级) │
│ 是否异步: false │
│ │
│ Span 标签: │
│ ├── agent.id ← Agent 唯一标识 │
│ ├── task.id ← 任务 ID │
│ ├── conversation.id ← 会话 ID │
│ └── model.id ← 模型 ID │
│ │
│ 跨生命周期 Span 管理: │
│ ConcurrentHashMap<taskId, Span> activeSpans │
│ BEFORE 创建 → AFTER/ERROR/TIMEOUT 关闭 │
│ │
│ ON_AGENT_TIMEOUT 特殊处理: │
│ 防止超时场景下 Span 泄漏(永不关闭), │
│ 确保 Span 正确结束并标记 status=timeout │
└──────────────────────────────────────────────────┘
Tracer 且容器中已有 Tracer Bean 时才注册 TelemetryHook;没有追踪依赖时不会触发类加载问题。
Hook返回值的四种结果:
AgentHookResult.ALLOW
┌──────────────────────────────────────┐
│ 继续执行,一切正常 │
│ 后续Hook和主流程继续 │
└──────────────────────────────────────┘
AgentHookResult.WARN("警告信息")
┌──────────────────────────────────────┐
│ 记录警告日志 │
│ 继续执行,不阻断 │
└──────────────────────────────────────┘
AgentHookResult.NEEDS_APPROVAL
┌──────────────────────────────────────┐
│ 仅用于 BEFORE_TOOL_CALL │
│ 暂停工具执行,创建审批单并通过 SSE │
│ 通知前端,审批通过后继续执行工具 │
└──────────────────────────────────────┘
AgentHookResult.BLOCK("拒绝原因")
┌──────────────────────────────────────┐
│ 只有 BEFORE_* 和 ON_USER_MESSAGE │
│ 类型的Hook才能生效: │
│ │
│ · 立即停止执行后续Hook │
│ · 主流程也停止 │
│ · 返回错误响应给用户 │
│ │
│ 其他类型的BLOCK被忽略(只记录日志) │
└──────────────────────────────────────┘
阻断流程示例:
┌──────────────────────────────────────────────┐
│ Hook A (order=10): ALLOW ✅ │
│ Hook B (order=20): BLOCK("敏感内容") ❌ │
│ Hook C (order=30): (不执行) │
│ 主流程: (不执行) │
│ → 用户收到: "请求被拦截: 敏感内容" │
└──────────────────────────────────────────────┘
HookContext 预定义的键: ┌──────────────────────────────────────────────────┐ │ HookContext (ConcurrentHashMap) │ │ │ │ 标准键: │ │ ├── MODIFIED_MESSAGE ← ON_USER_MESSAGE用 │ │ │ Hook写入修改后的消息,Controller读取 │ │ │ │ │ ├── MODIFIED_SYSTEM_PROMPT ← ON_SYSTEM_PROMPT用 │ │ │ Hook写入修改后的提示词,Agent读取 │ │ │ │ │ ├── SKIP_AGENT ← BEFORE_AGENT用 │ │ │ Hook设置跳过Agent执行 │ │ │ │ │ └── CONFIDENCE_SCORE ← Pipeline用 │ │ Pipeline审核者写入置信度分数 │ │ │ │ 使用示例: │ │ // Hook中修改用户消息 │ │ event.getContext().put( │ │ HookContext.MODIFIED_MESSAGE, │ │ sanitizedMessage │ │ ); │ │ │ │ // Controller中读取修改后的消息 │ │ String msg = event.getContext() │ │ .get(HookContext.MODIFIED_MESSAGE); │ └──────────────────────────────────────────────────┘
// 示例: 添加一个内容审核Hook
// 文件: hub-api/src/main/java/com/example/agenthub/api/hook/ContentModerationHook.java
@Component
public class ContentModerationHook implements AgentHook {
@Override
public AgentHookType[] supportedTypes() {
// 监听用户消息,在执行前审核内容
return new AgentHookType[]{ AgentHookType.ON_USER_MESSAGE };
}
@Override
public int getOrder() {
return 20; // 在SecurityEventHook(10)之后执行
}
@Override
public boolean isAsync() {
return false; // 同步执行,可以阻断流程
}
@Override
public AgentHookResult onHook(AgentHookEvent event) {
String message = event.getRequest().getMessage();
// 检查是否包含违禁词
if (containsBannedContent(message)) {
return AgentHookResult.BLOCK("内容包含违禁信息,请求已拦截");
}
return AgentHookResult.ALLOW;
}
private boolean containsBannedContent(String text) {
// 实现内容审核逻辑...
return false;
}
}
// 就这么简单!@Component注解会自动注册到AgentHookPublisher
// 无需修改任何其他代码
| 机制 | 触发方式 | 能阻断? | 适用场景 | 本项目使用 |
|---|---|---|---|---|
| Hook | 事件驱动 | 是(BEFORE类型) | 审计、安全、监控、审批、上下文治理 | SecurityEventHook / TelemetryHook / SummarizationHook / HumanInTheLoopHook 等 |
| Filter | 请求拦截 | 是 | 认证、CORS、编码 | JwtAuthenticationFilter |
| Interceptor | 方法拦截 | 是 | 日志、事务 | Spring AOP |
| Listener | 事件监听 | 否 | 异步通知、统计 | DelayedResponseEvent |
| Plugin | 动态加载 | 否 | 运行时扩展Agent | PluginManager |
当前项目的 PII 能力不是独立的 PiiDetectionHook,而是接入 Prompt Guard 的 InputScanner 和 OutputScanner。它负责检测并处理个人隐私信息(PII — Personally Identifiable Information),与 Hook 系统共同服务于 Agent 安全链路,但实现位置不同。
PIIScanner。
| PII 类型 | 模式特征 | 实现要点 | 当前状态 |
|---|---|---|---|
| 手机号 | 11 位,1[3-9] 开头 | 数字边界避免匹配更长数字串 | 内置 |
| 身份证号 | 18 位身份证格式 | 日期格式 + GB 11643 校验位 | 内置 |
| 银行卡号 | 16-19 位数字 | Luhn 校验辅助判定 | 内置 |
| 邮箱地址 | 标准 email 格式 | 匹配用户名和域名结构 | 内置 |
| IP 地址 | IPv4 地址 | 默认关闭,避免日志/示例误报 | 可配置 |
| 姓名 / 地址 | 上下文强依赖 | 纯正则误报高,适合 NER 或业务规则 | 未内置 |
// PromptGuardAutoConfiguration 中按配置组装检测器
List<PIIDetector> detectors = new ArrayList<>();
if (piiConfig.isDetectPhone()) {
detectors.add(PIIDetectors.phone());
}
if (piiConfig.isDetectIdCard()) {
detectors.add(PIIDetectors.idCard());
}
if (piiConfig.isDetectBankCard()) {
detectors.add(PIIDetectors.bankCard());
}
if (piiConfig.isDetectEmail()) {
detectors.add(PIIDetectors.email());
}
if (piiConfig.isDetectIp()) {
detectors.add(PIIDetectors.ipAddress());
}
| 策略 | 效果示例 | 适用场景 |
|---|---|---|
| BLOCK | 拒绝继续处理 | 高敏感场景,PII 不允许进入后续链路 |
| REDACT | [手机号] / [身份证号] | 保留语义位置,便于模型理解 |
| MASK | 138****1234 | 保留前后缀,便于用户核对 |
| HASH | [hash:3f2a...] | 审计、去重、不可逆指纹 |
InputScanner — 输入侧扫描:注入检查通过后再做 PII 检测,可阻断或返回带脱敏文本的扫描结果。OutputScanner — 输出侧扫描:在最终文本返回用户前检测金丝雀、敏感模式和 PII,并交给 OutputSanitizer 清洗。
PII 扫描链路:
User Input
│
▼
┌────────────────────────────┐
│ InputScanner │
│ ├── 注入 / 黑名单 / 长度检查 │
│ └── PIIScanner │
│ ├── 身份证检测 │
│ ├── 手机号检测 │
│ ├── 银行卡检测 │
│ ├── 邮箱检测 │
│ └── IP检测(可选) │
└──────────────┬─────────────┘
▼
Agent / LLM / Tool
│
▼
┌────────────────────────────┐
│ OutputScanner │
│ ├── 金丝雀令牌检测 │
│ ├── API Key / Token 检测 │
│ └── PIIScanner │
└──────────────┬─────────────┘
▼
OutputSanitizer → User
SummarizationHook 在对话历史 Token 数接近模型上下文窗口极限时,自动压缩历史消息。默认阈值为 80000 tokens,超出后触发摘要流程。
Token 超阈值时的消息处理策略:
原始消息列表 (超过 80000 tokens):
┌──────────────────────────────────────────────────┐
│ [0] System Prompt ← 始终保留 │
│ [1] User: 你好 │
│ [2] Assistant: 你好! │
│ [3] User: 帮我分析数据... ┐ │
│ [4] Assistant: 好的... │ │
│ [5] User: 继续深入分析... ├── 中间消息 → 摘要 │
│ [6] Assistant: 分析结果... │ │
│ [7] Tool Call: query_db... │ │
│ [8] Tool Result: {...} ┘ │
│ [9] User: 最新的问题 ┐ │
│ [10] Assistant: 最新回复 ├── 最近 N 条保留 │
│ [11] User: 当前问题 ┘ │
└──────────────────────────────────────────────────┘
压缩后:
┌──────────────────────────────────────────────────┐
│ [0] System Prompt ← 始终保留 │
│ [1] Summary: "用户请求数据分析,经过多轮讨论 │
│ 和数据库查询,得出了XXX结论..." ← LLM摘要 │
│ [2] User: 最新的问题 ← preserveRecent │
│ [3] Assistant: 最新回复 ← preserveRecent │
│ [4] User: 当前问题 ← preserveRecent │
└──────────────────────────────────────────────────┘
// 摘要 Prompt 模板
private static final String SUMMARIZE_PROMPT = """
请将以下对话历史压缩为一段简洁的摘要。要求:
1. 保留所有关键信息:用户意图、重要数据、工具调用结果、中间结论
2. 保留专有名词、数值、ID 等不可推断的信息
3. 使用第三人称描述("用户请求了..."、"系统返回了...")
4. 控制在 500 字以内
对话历史:
{messages}
""";
@Override
public AgentHookResult onHook(AgentHookEvent event) {
HookContext ctx = event.getContext();
// 防重入:同一轮 ReAct 循环中只摘要一次
if (ctx.containsKey(CTX_SUMMARIZED)) {
return AgentHookResult.ALLOW;
}
int tokenCount = tokenCounter.count(event.getMessages());
if (tokenCount < tokenThreshold) {
return AgentHookResult.ALLOW;
}
try {
String summary = chatModel.call(buildSummarizePrompt(event));
event.getMessages().replaceMiddle(summary, preserveRecentMessages);
ctx.put(CTX_SUMMARIZED, true);
} catch (Exception e) {
// Fallback: chatModel 不可用时,简单截断保留最近 N 条
log.warn("Summarization failed, falling back to truncation", e);
event.getMessages().truncateKeepRecent(preserveRecentMessages);
}
return AgentHookResult.ALLOW;
}
| 指标名 | 类型 | 说明 |
|---|---|---|
hook.summarization.triggered | Counter | 摘要触发次数 |
hook.summarization.tokens.before | Gauge | 摘要前 Token 数 |
hook.summarization.tokens.after | Gauge | 摘要后 Token 数 |
hook.summarization.fallback | Counter | 降级截断次数 |
hook.summarization.duration_ms | Timer | 摘要耗时 |
HumanInTheLoopHook 为敏感操作(如工具调用)提供人类审批环节,确保 Agent 不会自动执行高风险动作。
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AUTO | 直接放行,无需审批 | 低风险工具(查询天气、搜索) |
| APPROVE | 暂停执行,等待人类审批 | 中高风险工具(发邮件、修改数据) |
| DENY | 直接拒绝,不允许执行 | 禁止工具(删除数据库、系统命令) |
# application.yml — 工具审批策略配置
agent:
human-in-the-loop:
enabled: true
default-policy: APPROVE # 默认策略:需要审批
timeout: 300s # 审批超时时间(默认 300 秒)
tool-policies:
weather_query: AUTO # 天气查询:自动放行
web_search: AUTO # 网页搜索:自动放行
send_email: APPROVE # 发送邮件:需要审批
execute_sql: APPROVE # 执行 SQL:需要审批
delete_record: DENY # 删除记录:直接拒绝
shell_command: DENY # Shell 命令:直接拒绝
HumanInTheLoop 审批流程:
Agent ReAct Loop
│
▼ BEFORE_TOOL_CALL
┌─────────────────────────────────────────────────────┐
│ HumanInTheLoopHook.onHook() │
│ │
│ toolName = event.getToolCall().getName() │
│ policy = toolPolicies.getOrDefault(toolName, │
│ defaultPolicy) │
│ │
│ switch(policy): │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ AUTO │ │ APPROVE │ │ DENY │ │
│ │ return │ │ return │ │ return │ │
│ │ ALLOW │ │NEEDS_APP │ │ BLOCK │ │
│ └────┬────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
└────────┼────────────┼──────────────┼─────────────────┘
│ │ │
▼ ▼ ▼
继续执行 ┌──────────┐ 拒绝并返回
│ 前端审批 │ 错误信息
│ UI │
│ │
│ ✅ 批准 │──────▶ 恢复 RUNNING 并继续执行工具
│ ❌ 拒绝 │──────▶ 返回"操作已被管理员拒绝"
│ ⏰ 超时 │──────▶ 返回"审批超时(300s)"
└──────────┘
// 审批等待核心逻辑
AgentHookResult hookResult = humanInTheLoopHook.onHook(event);
if (hookResult.isNeedsApproval()) {
// 1. 创建审批单;ToolApprovalService 会把任务标记为 WAITING_APPROVAL
ApprovalTicket ticket = toolApprovalProvider.createApproval(
taskId, conversationId, userId, nodeId, toolName, toolArgs, timeoutSeconds);
// 2. 通过 SSE 通知前端展示审批 UI
sink.next(AgentResponse.toolApprovalRequiredEvent(
ticket.token(), toolName, "工具调用需要人工审批: " + toolName, argsJson));
// 3. 等待审批结果;通过/拒绝/超时都会把任务状态恢复为 RUNNING
ApprovalDecision decision = toolApprovalProvider.waitForDecision(
ticket.token(), ticket.timeoutSeconds());
if (!decision.approved()) {
return toolResult("审批未通过: " + decision.message());
}
// 4. 审批通过后继续执行当前工具调用
return executeToolCall(toolName, toolArgs);
}
NEEDS_APPROVAL 表示“暂停并等待用户审批”;BLOCK 表示“直接拒绝”,不会进入审批恢复流程。Graph Checkpoint 是图执行恢复能力的一部分,但本段代码层面的审批等待主要由审批单、SSE 事件和任务状态流转完成。
Hook(本项目自定义机制)与 Interceptor(项目运行时扩展层的洋葱模型拦截器链,并与 Spring AI Alibaba 调用链路集成)在 Agent 执行过程中交替触发,共同构成完整的扩展点体系:
完整执行流程 — Hook 与 Interceptor 协作:
Request (用户请求)
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ON_USER_MESSAGE (Hook) ← 用户消息改写、内容审核扩展 │
└──────────────────┬───────────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────────┐
│ BEFORE_AGENT_EXECUTION (Hook) ← 遥测Span创建、限流检查 │
└──────────────────┬───────────────────────────────────────────────────┘
▼
╔══════════════════════════════════════════════════════════════════════╗
║ ReAct Loop (循环执行) ║
║ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ ON_ITERATION_START (Hook) ← 摘要检查、Token统计 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ Interceptor.beforeModel() ← 洋葱模型外层→内层 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ BEFORE_LLM_CALL (Hook) ← 消息列表最终修改 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌─────────────────┐ ║
║ │ LLM 调用 │ ║
║ └────────┬────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ AFTER_LLM_CALL (Hook) ← 响应后处理 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ Interceptor.afterModel() ← 洋葱模型内层→外层 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ BEFORE_TOOL_CALL (Hook) ← 人机协同审批 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ Interceptor.beforeTool() ← 工具调用前拦截 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌─────────────────┐ ║
║ │ Tool 执行 │ ║
║ └────────┬────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ Interceptor.afterTool() ← 工具结果后处理 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ AFTER_TOOL_CALL (Hook) ← 工具结果审计 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────┐ ║
║ │ ON_ITERATION_END (Hook) ← 循环结束统计 │ ║
║ └──────────────────┬───────────────────────────────────────────┘ ║
║ │ ║
║ ▼ (判断是否继续循环) ║
╚══════════════════════════════════════════════════════════════════════╝
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ AFTER_AGENT_EXECUTION (Hook) ← 遥测Span关闭、结果统计 │
└──────────────────┬───────────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ON_OUTPUT (Hook) ← 输出后处理扩展(当前预留) │
└──────────────────┬───────────────────────────────────────────────────┘
▼
Response (返回用户)
supportedTypes()。
CTX_SUMMARIZED)防重复;设计 Hook 为幂等操作;使用 AtomicBoolean 等 CAS 操作避免并发重入。例如 SummarizationHook 在首次执行后设置标记,后续调用直接返回 ALLOW。对于审计类 Hook,重复写入不影响正确性(幂等天然成立)。
CTX_SUMMARIZED 标记防止同一轮 ReAct 循环中重复触发;preserveRecentMessages 保留最近 N 条消息不被摘要;只在 Token 超阈值(默认 80000)时触发;摘要 Prompt 明确要求保留关键信息(工具调用结果、中间结论、专有名词和数值)。
APPROVE 策略会返回 NEEDS_APPROVAL,调用方创建工具审批单,通过 SSE 推送 tool_approval_required 给前端,并等待 ToolApprovalProvider 的审批结果。审批通过后任务状态恢复为 RUNNING,继续执行当前工具;审批拒绝或超时则把拒绝结果写回工具结果。BLOCK 只表示直接拒绝,不进入审批恢复流程。
hookAsyncExecutor),异常被 catch 并记录日志但不中断管道。这是刻意设计——异步 Hook 适合遥测、审计日志等非关键路径操作。如果业务要求 Hook 失败时阻断主流程,必须使用同步 Hook(isAsync() = false)。
AgentHookResult 可携带 JumpTo 指令,Publisher 记录最后一个非 NONE 的跳转目标并最终返回给调度器。场景举例:SecurityEventHook 或自定义安全 Hook 检测到高风险输入 → 返回 JumpTo("safe_response_node") → Graph 跳过正常 Agent 节点,直接执行安全响应节点返回预设的拒绝消息。PII 当前走 Prompt Guard 扫描链路,不要作为 JumpTo 示例。
KeyStrategy 机制:LAST_WINS(后执行的 Hook 覆盖前者)、MERGE_LIST(列表类型值自动合并)、MERGE_MAP(Map 类型值深度合并)。Hook 可在 getKeyStrategies() 方法中声明每个 key 的合并策略。未声明时默认 LAST_WINS。这确保了多个 Hook 协作修改状态时的行为可预测。