🪝 自定义 Hook
# Hook 是什么
ReActAgent 在执行过程中会在若干关键节点(推理前后、工具调用前后、总结前后、出错时)发出内部事件。Hook 就是监听这些事件的扩展点:你可以用它做审计、埋点、指标统计,甚至改写事件内容。
它和流式输出里的 FlowEvent(agent.reasoning 等)不是一回事:
FlowEvent/eventListener是 LiteFlow 上层封装好的事件流,面向"把过程推给前端"这类场景- Hook 是更底层的 agentscope 事件拦截点,能拿到原始的推理消息、工具调用块、token 用量等结构化数据
事实上,框架内置的 Re-Act 日志和 token 用量统计本身就是用 Hook 实现的(见下文)。
# Hook 接口
Hook 是 agentscope 的 io.agentscope.core.hook.Hook 接口,只有一个核心方法:
public interface Hook {
<T extends HookEvent> Mono<T> onEvent(T event);
default int priority() {
return 100;
}
}
onEvent接收一个事件,返回一个Mono(Reactor 响应式类型)。放行就返回Mono.just(event);也可以返回改写后的事件priority()决定多个 Hook 的执行顺序,默认100(框架内置日志 Hook 用的是900)
# 事件类型
通过 instanceof 判断事件类型,再取出对应数据。完整的事件类型如下:
事件类(io.agentscope.core.hook.*) | 触发时机 | 常用访问方法 |
|---|---|---|
PreReasoningEvent | 模型推理前 | getModelName()、getInputMessages() |
PostReasoningEvent | 模型推理后 | getReasoningMessage() → Msg |
ReasoningChunkEvent | 流式推理的增量片段 | 增量文本 |
PreActingEvent | 工具调用前 | getToolUse() → ToolUseBlock(getName() / getInput()) |
PostActingEvent | 工具调用后 | getToolResult() → ToolResultBlock(getName() / getOutput()) |
ActingChunkEvent | 流式工具结果的增量片段 | 增量内容 |
PreSummaryEvent | 达到迭代上限、生成 Summary 前 | — |
PostSummaryEvent | Summary 生成后 | — |
SummaryChunkEvent | 流式 Summary 的增量片段 | — |
PreCallEvent / PostCallEvent | 一次完整调用的开始 / 结束 | — |
ErrorEvent | 执行出错 | getError() |
Summary 相关事件与迭代次数与 Summary 对应:只有当 Re-Act 循环达到 maxIterations 被强制收尾时才会触发。
# 注册 Hook
在 Agent 组件中覆写 hooks(),返回要注册的 Hook 列表:
@Override
protected List<Hook> hooks() {
return List.of(new ToolAuditHook());
}
一个简单的"工具调用审计" Hook 示例——在每次工具调用前记录工具名和入参:
import io.agentscope.core.hook.Hook;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.PreActingEvent;
import io.agentscope.core.message.ToolUseBlock;
import reactor.core.publisher.Mono;
public class ToolAuditHook implements Hook {
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PreActingEvent e) {
ToolUseBlock tool = e.getToolUse();
// 这里可以写审计日志、上报指标、做调用计数等
System.out.println("即将调用工具:" + tool.getName() + " 入参=" + tool.getInput());
}
// 必须把事件放行,否则会中断事件流
return Mono.just(event);
}
@Override
public int priority() {
return 100;
}
}
# Hook 与框架内置 Hook 的关系
框架会把你覆写的 hooks() 和它自己的内置 Hook 合并后一起注册,按 priority() 排序执行。内置 Hook 包括:
| 内置 Hook | 作用 | 开关 |
|---|---|---|
ReActLoggingHook | 把 reason / act / error 写入 SLF4J 日志 | enableReActLogging() / liteflow.agent.logging.react-enabled |
ChatUsageTrackingHook | 收集本轮 token 用量,供 ctx().getChatUsage() 读取 | 始终启用 |
SkillTrackingHook | 记录本轮加载过的技能,供 usedSkills() 读取 | 开启 Skills 时启用 |
所以即使你不写任何自定义 Hook,框架内部也已经在用 Hook 机制。ReActLoggingHook 就是一份现成的完整范例——它演示了如何区分各类事件、如何从 PostReasoningEvent 取出推理文本和 ChatUsage、如何安全地截断超长文本。
# 注意事项
Hook 是构建期能力声明
和 tools()、systemPrompt() 一样,hooks() 只在同一 (conversationId, agentKey) 首次构建并缓存 Agent 时生效。不要让它依赖单次请求数据;需要按请求隔离时,应把请求维度体现在 agentKey() 或 conversationId() 中。详见扩展点速查。
不要在 Hook 中持有 ctx() 引用
Hook 会被缓存并跨多次调用复用。不要在 Hook 内保存某次调用的 ReActAgentContext 引用,否则会拿到过期的上下文。如需访问当前 Slot/workspace,应保存组件实例,运行时通过组件间接调用 ctx()(与自定义工具的处理方式一致)。
不要阻塞、不要抛异常
Hook 运行在 agentscope 的响应式执行管线中。请只做轻量处理(打点、入队),不要执行耗时阻塞 I/O;并自行 try/catch,避免 Hook 内部异常影响 Agent 主流程。框架内置的 ReActLoggingHook 就把整段逻辑包在 try/catch 里。


