Hook系统:8扩展点的插件机制
agent的核心循环是固定的——ReAct循环反复执行”推理→行动→观察”。但在循环的每个关键节点,经常需要插入一些”侧动作”:记日志、上报监控、审计工具调用、自动注入steering信息、拦截危险的命令……
如果这些侧动作都硬编码进agent循环,会发生什么?循环本体会被各种if分支和可选回调污染,读代码的人分不清哪些是核心逻辑、哪些是附加功能。更糟糕的是,每增加一个侧动作就得改agent循环——改的人越多,循环越脆,越不敢动。
Hook系统就是为了解决这个矛盾而生的:循环保持纯粹,侧动作通过扩展点插入。这篇文章从hook设计的基本问题讲起,对比三种扩展方案各自的取舍,最后深入看aptbot如何在8个hook点上做文章。
一、概念:什么是hook,为什么需要hook系统
1.1 hook的定义
hook(钩子) 是软件工程中一种常见的扩展模式。它在主流程的关键位置预留”插入点”,允许外部代码在不修改主流程的前提下,在特定时机执行自定义逻辑。
类比现实:一家餐厅的后厨有一条流水线——洗菜 → 切菜 → 烹饪 → 装盘。如果你想在”装盘”之前加一道”摆盘”步骤,不需要拆掉整条流水线重新设计,只需要在”烹饪→装盘”之间插入一个”摆盘”工作台。这个”插入点”就是hook。
在agent系统中,hook扮演类似的角色:agent循环是一条流水线,hook是流水线上预留的插槽,用户可以把自己的代码插进去,在特定时机执行。
1.2 hook与skill、tool的区别
在aptbot的三层扩展体系中,三者的定位截然不同:
- Tool(工具):agent用来”做事”的基本操作。读文件、执行命令、搜索网络——这些是agent行动能力的原子单位。
- Skill(技能):告诉agent”怎么用工具做事”的知识。比如”如何调试TypeScript错误”是一篇skill,它指导agent按步骤使用read、edit、run等工具。
- Hook(钩子):开发者在agent循环的特定时机插入自定义逻辑。它不是agent调用的,而是agent框架调用的。hook不参与agent的决策过程,而是在决策过程的”间隙”执行。
用一句话总结:Tool是agent的”手”,Skill是agent的”脑中的知识”,Hook是开发者介入agent循环的”接口”。
1.3为什么不能所有侧动作都内置
有的项目选择”把所有侧动作内置”——agent循环自带日志、自带监控、自带审计、自带限流……一切功能都在循环本体里。
这条路在小规模可行,但随着功能增多会面临三个问题:
- 循环膨胀:每个”还算有用”的侧动作都在循环里加几行,300行的循环膨胀到1000行,核心的ReAct逻辑淹没在附属代码中。新成员读代码要花大量时间区分”哪些是核心、哪些是可选的”。
- 关闭困难:用户不想用某个侧动作?要么不能关(硬编码进去了),要么加一个开关(
if (config.enableAudit) { ... }),循环里的条件分支越来越多。每个条件分支都增加了一条测试路径——代码覆盖率看似100%,实际上有效的组合测试几乎为零。 - 定制受限:用户的日志格式和项目内置的不一样?用户想在上报监控时附加自定义标签?如果侧动作是内置的,用户只能”接受或放弃”,不能定制。
Hook系统把”可选的侧动作”从循环中剥离出去,让循环只保留”必须的核心逻辑”,侧动作通过hook插拔。这样就解决了循环膨胀、关闭困难、定制受限三个问题。
二、通用设计方案:扩展agent循环的常见模式
在agent循环中插入侧动作,有几种常见的设计模式。理解这些模式是分析具体方案的基础。
2.1扩展点的选择标准
一个好的扩展系统需要回答四个问题:
- 在哪里插:哪些时机提供扩展点?太少(只有start/end)不够用,太多(每行代码都提供)则过度设计。
- 怎么执行:同步还是异步?串行还是并行?能否阻断主流程?
- 怎么通信:hook之间能否交换数据?hook能否修改主流程的状态?
- 怎么管理:多个hook注册到同一点,执行顺序怎么定?hook如何发现和加载?
这四个问题的不同答案,构成了不同的设计方案。
2.2扩展点的粒度选择
扩展点的粒度是一个权衡。粒度越粗(只在agent启动和结束时提供扩展点),实现越简单,但能做的事有限——你无法在工具执行前拦截修改参数。粒度越细(每个函数调用前后都提供扩展点),灵活性越高,但实现复杂度剧增,而且大部分扩展点可能从未被使用。
实践中最常见的做法是按agent循环的天然层次划分扩展点。agent循环自然有四层:整个agent生命周期、每个用户回合(turn)、每次LLM调用、每次工具调用。每层再按before/after分为两个时机,总共8个点。这不是巧合——它是agent循环的本质结构决定的。
2.3执行模型的设计空间
执行模型有三个关键维度:
同步vs异步:同步执行意味着hook完成前主流程等待,优点是行为可预测(hook的结果立即可见),缺点是hook不能做耗时操作。异步执行允许主流程继续,hook在后台完成,优点是性能好,缺点是hook结果可能”迟到”——主流程已经进入下一阶段,hook才完成,此时修改状态的时机已过。
串行vs并行:串行执行保证hook顺序,适合需要链式传递数据的场景。并行执行性能更好,但无法保证hook之间的数据依赖。
阻断vs非阻断:阻断式hook可以阻止主流程继续(比如在tool_before中”这个工具不能执行,返回错误”)。非阻断式hook只能”观察”不能”干涉”。
这些维度组合成不同的执行模型,每种模型对应不同的使用场景。
2.4插件的发现与加载
扩展点定义好了,hook代码如何被发现和加载?常见方式有:
- 配置注册:在配置文件中显式声明哪些hook启用。最可控,但每次加hook都要改配置。
- 目录扫描:约定一个目录,所有hook文件放在里面,系统自动扫描注册。最无感,但用户可能不清楚系统加载了哪些hook。
- 装饰器/注解:在代码中使用装饰器标记hook函数,系统通过反射发现。对类型系统有要求。
每种方式都有适用场景,没有绝对优劣。
三、市面其他hook / 扩展方案对比
理解了扩展系统的设计空间,接下来看三种有代表性的方案。它们代表了”不加扩展””异步扩展””同步扩展”三条不同的技术路线。
3.1方案A:无hook系统,侧动作硬编码
这条路线的选择是”干脆不做扩展系统”——所有侧动作直接硬编码在agent循环中。需要日志?在agent loop里加 log() 调用。需要监控?在每次LLM调用前后加 metric() 调用。需要审计?在每个工具调用前后加 audit() 调用。
设计特点:
- 零抽象:没有hook接口、没有扩展点、没有插件目录。侧动作就是普通函数调用。
- 全量内置:所有”可能有人需要的”侧动作都写在循环里,由配置开关控制启用。
- 改动即改循环:任何侧动作的增删改都直接修改agent loop的核心代码。
优势:
- 实现最简单——不需要设计扩展API,不需要实现插件加载器
- 行为最透明——所有侧动作都在循环里,读循环代码就能看到全部
- 调试最容易——不需要跟踪hook调用链,侧动作和主流程在一起
劣势:
- 扩展性差:每个新侧动作都要改核心循环。循环维护者成为瓶颈——一个agent项目有10个用户,每个用户想要一个不同的侧动作,循环会膨胀到无法维护。
- 定制成本高:用户想要不同的日志格式?必须fork agent循环。这意味着无法跟随上游更新——上游修复了一个bug,但用户fork的版本已经改了循环结构,合并冲突不可避免。
- 关闭不干净:通过配置开关可以”禁用”某个侧动作,但条件分支仍然存在。在极端情况下,每个条件分支还需要额外的测试覆盖。
适用场景: 极简agent(核心循环100-150行,几乎没有侧动作需求)、教学demo(展示agent循环本身,不需要扩展能力)。
3.2方案B:事件emit异步监听
这条路线的做法是:agent循环在关键时机”发射”事件(event emit),外部监听器异步接收并处理。事件监听器和agent循环在不同的事件循环中运行,互不阻塞。
设计特点:
- 事件驱动:agent循环在特定时机触发事件(如
"llm:before"、"tool:after"),不等待监听器处理完成就继续执行。 - 异步非阻塞:监听器在独立的事件循环中运行。即使某个监听器耗时很长(如写入远程数据库),agent循环不受影响。
- 一对多:同一个事件可以被多个监听器订阅。日志、监控、审计各自独立订阅自己感兴趣的事件。
- 无返回值:事件监听器是”发后即忘”的——它们不会向agent循环返回任何值,也无法修改主流程的状态。
优势:
- 性能好——agent循环不需要等待hook完成,hook在后台并行
- 隔离性强——hook出错不会影响主流程(异步错误通常被吞掉或写入错误日志)
- 添加方便——在任意代码中
on("llm:after", handler)即可,不需要修改循环
劣势:
- 无法保证顺序一致性:这是最根本的问题。假设两个hook:hook A(记录日志)和hook B(修改LLM响应内容)。如果hook A和hook B都监听
llm_after事件,由于异步执行,无法保证hook A在hook B之前看到的是”修改前”还是”修改后”的内容。更糟的是,agent循环可能在hook处理完之前就进入了下一轮推理——agent基于”旧”状态决策,而hook在处理”新”数据,二者脱节。 - 无法阻断主流程:事件监听器无法说”这个工具调用不合法,别执行了”。
tool_before监听器即使发现参数危险,也只能记录警告,无法阻止工具执行。 - 状态同步困难:如果hook需要修改agent循环的状态(如注入一段steering到system prompt),异步模型下无法安全做到——修改发生在agent循环进入下一阶段之后。
适用场景: 对”副效应”无顺序要求的场景(纯日志收集、纯指标上报)、对主流程性能敏感的端到端系统。
3.3方案C:同步hook链式注入
这条路线的选择是”在主流程的关键节点同步执行hook链”——hook是同步函数,返回一个可选的修改后的上下文,后一个hook在前一个的基础上继续处理。
设计特点:
- 同步执行:hook函数必须同步返回(或返回Promise,但主流程await等待完成)。主流程在全部hook执行完毕后才继续。
- 链式传递:每个hook接收当前上下文,可以选择修改后传递给下一个hook。最后一个hook返回的结果影响主流程。
- 优先级排序:多个hook可以注册到同一个扩展点,通过优先级字段控制执行顺序。
- 可阻断:hook可以通过返回”停止”信号来阻断主流程(如”工具调用参数非法,拒绝执行”)。
优势:
- 顺序一致性:hook的执行顺序完全可控,后一个hook能看到前一个hook的修改结果。agent循环在所有hook完成后才继续,不存在”状态脱节”。
- 可修改主流程:hook可以修改上下文中的任意字段——比如在
llm_before中注入额外的system prompt内容,agent循环会使用修改后的内容调用LLM。 - 可阻断主流程:
tool_beforehook可以拒绝非法工具调用,返回自定义错误信息,agent循环会把这个错误交给LLM而不是真的执行工具。 - 行为可预测:给定一组hook和输入,执行结果完全确定。调试时只需要跟踪hook链的执行路径。
劣势:
- 所有hook必须轻量快速——任何一个hook耗时过长都会拖慢整个agent循环
- 同步模型对”后台任务”不友好(如远程日志上报),需要hook自己fire-and-forget
- hook链中的错误处理需要小心:一个hook抛错需要决定”跳过它””终止整个链”还是”吞掉继续”
适用场景: 需要精确控制hook顺序和数据的场景(审计、steering注入、参数校验),项目规模可控的场景。
3.4三种方案对比
| 维度 | 方案A(无hook系统) | 方案B(事件异步监听) | 方案C(同步hook链式注入) |
|---|---|---|---|
| 核心哲学 | 侧动作内置,零抽象 | 事件发射后异步处理 | 关键节点同步链式注入 |
| 执行模型 | 同步(与主流程混合) | 异步(独立事件循环) | 同步(主流程等待) |
| 能否修改主流程 | 能(直接改循环代码) | 不能(监听器无返回值) | 能(mutate上下文) |
| 能否阻断主流程 | 能(条件分支控制) | 不能 | 能(返回阻断信号) |
| hook顺序控制 | 不适用(代码顺序) | 无法保证 | 优先级排序 |
| 性能影响 | 低(直接调用) | 极低(异步非阻塞) | 中等(同步等待) |
| 实现复杂度 | 低 | 中(需要事件总线) | 中高(需要hook管理器) |
| 可定制性 | 差(必须改核心代码) | 好(外部监听) | 好(插拔式hook) |
| 调试难度 | 低 | 高(异步跟踪困难) | 中(可串行跟踪) |
三种方案没有绝对优劣。方案A适合极简教学项目,方案B适合对性能有极致要求的在线服务,方案C适合需要精确控制扩展行为的项目。选择哪条路线取决于项目的核心诉求。
四、aptbot的设计特点
aptbot选择了方案C——同步hook链式注入,并在其基础上做了三项定制:8个hook点的拓扑设计、两层插件目录的加载策略、以及无沙箱的设计哲学。
4.1 8 hook点拓扑:agent循环四层 × before/after的自然收敛
aptbot在agent循环中设置了8个hook点,按循环的天然层次分为四组:
Agent级(整个agent生命周期)
agent_before:agent启动时触发,适合做全局初始化(如加载外部配置、建立数据库连接)agent_after:agent退出时触发,适合做全局清理(如关闭连接池、写入最终状态)
Turn级(每个用户回合)
turn_before:每轮用户交互开始前触发,可以修改这一轮的初始context(如根据当前时间注入”现在是下班时间,建议简短回答”的提示)turn_after:每轮交互结束后触发,可以观察这一轮的最终状态(如记录本轮耗时、整理本轮关键信息到长期记忆)
LLM级(每次LLM调用)
llm_before:调用LLM前触发,可以修改messages、tools、systemPrompt等输入(如自动注入项目约定的代码规范)llm_after:LLM返回结果后触发,可以观察LLM输出、进行输出后处理(如检测是否触发了敏感内容)
Tool级(每次工具调用)
tool_before:工具执行前触发,可以拦截非法调用、修改调用参数(如给bash命令自动加上超时限制)tool_after:工具执行后触发,可以修改返回值、记录审计日志(如屏蔽包含敏感信息的工具输出)

这8个点的设计并非随意的选择。如果你把agent循环画出来,它天然就有四层边界:agent启动/退出、每轮开始/结束、每次LLM调用前后、每次工具调用前后。每层边界都有before/after两个时机,4 × 2 = 8,刚好8个点。
这不是巧合。任何认真设计的agent框架,只要它试图在循环中提供合理的扩展点,最终都会收敛到相似的8点拓扑。因为agent循环的结构是有限的——它必须处理生命周期、对话轮次、推理调用和执行动作,这四层的边界就是扩展点的最佳位置。
相比于方案B的”事件随意发射”(可能在任意代码位置发射事件,监听器需要自己过滤),aptbot的8点拓扑提供了结构清晰的扩展位置——每个hook点有明确的语义(这是什么时机、我在这里能做什么事),用户看到 llm_before 就知道它是在LLM调用前触发,能修改system prompt。
4.2同步执行 + ctx mutate链式传递 + priority升序
多个hook可能注册到同一个扩展点(比如两个hook都注册到 llm_before,一个负责注入steering,一个负责注入项目规范)。aptbot的执行模型保证它们有序、可预测地协作:
同步执行:所有hook都是同步函数(或返回Promise但被await等待)。agent循环在调用所有 llm_before hook之前,不会进入LLM调用阶段。这保证了”hook说改了什么,就真的改了”——不存在hook还没执行完、LLM已经调用的竞态条件。
同步执行的代价是hook不能做重活。如果一个hook需要发HTTP请求上报数据,它应该自己fire-and-forget(执行HTTP请求但不await,让请求在后台完成),不阻塞hook链。aptbot的sync模型要求hook作者对”什么应该同步完成、什么可以异步执行”有清晰的判断。
ctx mutate链式传递:每个hook接收一个context对象(包含当前执行阶段的所有相关数据),可以直接mutate它。后一个hook看到的是前一个hook修改后的context。
举个具体例子:假设有两个 llm_before hook——hook A在system prompt中注入”当前时间:2026-07-02”,hook B在system prompt中注入”项目约定:使用pnpm作为包管理器”。执行流程如下:
- agent循环构造初始context,system prompt为空
- hook A执行,在context.systemPrompt中追加时间信息
- hook B执行,它看到的context.systemPrompt已经包含时间信息,在此基础上追加项目约定
- agent循环使用最终的systemPrompt调用LLM
这种链式传递让多个hook能协作——几个hook各自负责不同的注入内容,最终组合成完整的system prompt。如果其中一个hook出错了(抛异常),aptbot会吞掉错误、记录到stderr,当前context保持上一个成功hook后的状态,链继续执行。
priority升序:每个hook注册时带一个 priority 数字字段。数字小的先执行(升序)。这让用户能精确控制hook的执行顺序。
1 | // priority 10 的先执行 |
如果所有hook的priority相同,执行顺序由注册顺序决定(先注册先执行)。但依赖”隐含顺序”是危险的——更好的做法是显式设priority,让顺序一目了然。
这套执行模型的设计取向是”简单优先“——同步(不用处理并发)、mutate(不用学习不可变数据模式)、固定优先级(不用设计复杂的排序策略)。每个决策都选了最朴素的方案,代价是hook不能做重活,但收益是hook的行为可预测、易调试、新手可以在一小时内理解。
4.3两层插件目录:workspace覆盖builtin
Hook的存放和加载方式与Skills系统完全一致,也分两层:
builtin hooks(内置层):随aptbot代码发布的默认hook。项目维护者编写,提供通用的功能——比如基础日志hook、默认的tool_after审计hook。用户clone aptbot后,这些hook已经可用。
workspace hooks(工作区层):用户项目本地目录 .aptbot/hooks/ 下的自定义hook。用户为自己的项目编写的定制hook。加载时,workspace层同名hook覆盖builtin层。
两层加载解决的核心问题和Skills系统一样:开箱即用与项目定制的矛盾。没有builtin hook,用户clone后是一个”空”系统,需要自己写hook才能有基本功能;没有workspace hook,用户无法定制内置hook的行为,只能接受项目默认。
与Skills系统的两层加载共用同一套机制,意味着用户只需要学习一次”两层覆盖”的模式,就能理解hooks和skills的加载逻辑。这是aptbot跨子系统的一致设计语言——Config、Memory、Skills、Hooks都遵循”builtin提供默认值 / workspace提供覆盖”的约定。
4.4无沙箱设计哲学
aptbot的hook 没有沙箱——hook是普通TypeScript代码,直接import aptbot内部模块,能访问文件系统、网络、进程等任何资源。如果一个hook写错了导致进程崩溃,那是用户的责任。
这是有意识的设计选择,它基于”信任边界内“的核心假设:
- hook由用户自己编写:用户在自己的workspace目录下写hook,不是从互联网下载并安装不明来源的插件。用户已经能直接修改aptbot的代码,自然也能写hook——hook的信任等级和aptbot本体一致。
- hook不需要权限隔离:不像浏览器插件需要沙箱(因为插件来源不可信),aptbot hook与aptbot本体在同一信任层。用户信任自己写的代码,不需要操作系统级别的隔离。
- 沙箱会限制能力:如果hook在沙箱里运行,它能做的事大幅受限。很多有价值的hook就做不了了——比如一个
llm_beforehook想读取当前项目的.env文件来注入环境信息,如果hook在沙箱中且没有”读文件”权限,这个能力就丧失了。
无沙箱的代价是安全责任在用户。用户要为自己写的hook负责——写错了可能让agent崩溃、泄露数据、或执行危险操作。但aptbot提供了一道安全网:hook抛错会被吞掉,写入stderr,不影响主流程。这让用户敢于写实验性的hook——错了就改,agent还能继续工作。
这道安全网是”无沙箱哲学”的必需品。没有它,一个hook的错误就能拖垮整个agent。有了它,用户可以放心地编写和调试hook,即使hook写得不够完善,也不会影响agent的基本可用性。
这和方案B的”事件异步监听”形成了有趣的对比:方案B通过异步隔离来保护主流程(hook在独立事件循环中运行,出错了不影响主流程),aptbot通过”吞错误 + stderr”来保护主流程。两种方式都能达到”主流程不受hook错误影响”的目标,但前者限制了hook的能力(不能同步修改主流程状态),后者要求用户对hook的质量负责。
4.5典型使用场景
8个hook点的典型应用场景:
日志(turn_before / turn_after):在每轮交互的开始和结束时记录输入输出。相比agent内置的日志系统,hook日志更灵活——用户可以自定义格式、写入独立的日志文件、或同时输出到不同目标。
监控(llm_before / llm_after):在每次LLM调用前后记录token用量、请求延迟、是否失败。这些数据可以上报到Prometheus、Datadog或简单的统计文件。agent循环本身不需要知道监控系统的存在——监控是hook的责任。
审计(tool_before / tool_after):记录每次工具调用的参数和返回结果。对于bash这类”高危”工具,审计hook可以在 tool_before 中记录”谁、什么时候、调用了什么命令”,在 tool_after 中记录”命令的退出码和输出摘要”。这在多人共享agent的场景中尤其重要。
自动注入steering(llm_before):检测当前对话的上下文,自动注入相关的指导信息。比如检测到对话中出现了”测试”相关的关键词,自动注入”项目测试约定”段落。这是”动态system prompt”的基础——不需要把所有可能的信息都塞进静态system prompt,按需注入,上下文更精简、更相关。
这四类场景的共同特征:它们都是”侧动作”,与agent核心循环的解耦逻辑。把它们做成hook而不是硬编码进agent循环,不仅让循环保持纯粹,也让这些侧动作能独立演进、独立配置、独立启停。
五、发展方向
5.1有限异步支持
当前aptbot的hook执行模型是纯同步的。这在大多数场景下足够,但有些hook天然需要异步——比如上报监控指标到远程服务,如果同步等待HTTP请求完成,会拖慢agent循环。
一个可能的方向是引入”标注式异步”:hook可以声明自己是”fire-and-forget”类型,aptbot在执行这类hook时不await它的完成,让它在后台运行。这样既保持了hook链的同步语义(顺序执行、链式传递),又为”不需要等待结果”的hook提供了异步出口。
5.2条件式hook执行
当前所有注册到某个扩展点的hook都会执行。随着hook数量增多,可能出现”99% 的场景不需要这个hook,但它每次都执行”的情况——虽然很少,但存在浪费。
条件式hook执行允许hook声明一个”触发条件”(predicate),aptbot在调用hook前先检查条件是否满足。条件不满足就跳过。这可以减少不必要的hook调用,同时保持”hook在需要时一定会触发”的可靠性。
5.3可选的沙箱模式
对于”从社区安装hook”的场景(市场或插件生态),信任假设会发生变化——用户不再自己写hook,而是从社区下载他人编写的hook。这时无沙箱设计的安全风险就暴露了。
长期来看,aptbot可能引入可选的沙箱模式:默认不启用(兼容现有hook),但当用户从社区安装hook时,可以选择在沙箱中运行。沙箱模式限制hook的文件系统访问、网络访问、进程创建等能力。这是”自由优先、安全可选”的路线。
小结
Hook系统是agent循环的”扩展点机制”,让侧动作从核心循环中解耦出来,以插拔的方式插入。
概念层面:hook是在主流程关键节点插入自定义逻辑的机制,与tool(行动能力)和skill(使用知识)互补。内置所有侧动作会导致循环膨胀、关闭困难、定制受限三个问题。
方案对比:方案A(无hook系统)简单但扩展性差,所有侧动作侵入核心循环;方案B(事件异步监听)性能好但无法保证侧动作与核心循环的顺序一致性,且不能修改或阻断主流程;方案C(同步hook链式注入)最可控但要求所有hook轻量快速。
aptbot的设计:8个hook点覆盖agent/turn/llm/tool四层的before/after,是agent循环结构的自然收敛;同步执行 + ctx mutate + priority升序的模型追求简单可预测;两层插件目录与skills系统一致,降低学习成本;无沙箱哲学在”信任边界内”假设下提供最大灵活性,用”吞错误 + stderr”替代隔离来保护主流程。
理解Hook系统的设计后,下一篇我们向上走一层,看Channel与多端接入:当agent循环跑起来之后,如何让多个客户端同时接入同一个agent会话。