Pi Agent 学习笔记:扩展如何介入工具调用

hello 我是逆境不可逃

模型决定修改一个文件后,Agent 就应该直接执行吗?如果项目要求某些配置不能改,或者工具返回的日志需要先脱敏,这些逻辑应该放在哪里?

把所有规则都写进 Agent 循环当然能工作,但每增加一种项目要求,就要改动核心流程。Pi 提供了扩展机制:扩展注册处理函数,在约定的时机接收事件,检查或调整这次操作。

本文只讨论工具调用前后的两个入口。重点是它们能改变什么,以及哪些事情不会随之发生。

工具执行前后,各留一个入口

可以把扩展理解为一段接入运行流程的程序。"钩子"就是程序预留的接入位置,不需要先了解 TypeScript,也能理解它的作用。

在 Pi 中,扩展通过 pi.on 注册事件处理函数。工具调用的这条路径可以简化为:

text 复制代码
模型提出工具调用
    ↓
查找工具,准备并校验参数
    ↓
执行前扩展:tool_call
    ├─ 阻断或抛出异常 → 生成这次调用的错误结果
    └─ 未阻断
          ↓
       执行工具
          ↓
       执行后扩展:tool_result
          ↓
       整理工具结果,写回对话上下文

执行前处理器可以拿到工具名称、调用编号和输入参数。例如,模型请求修改某个文件,扩展可以检查目标路径,再决定是否阻断。

执行后处理器拿到的则包括结果内容和错误状态,可以调整返回内容。例如,把一段冗长日志整理成摘要,再交给模型。

这两个时机不能互换。如果要求文件不能被修改,就要在执行前做判断;等文件改完以后,再把返回文字改成"不允许修改",已经无法阻止刚才的操作。

多个执行前检查,不是投票

假设一次文件修改要经过三个处理器。以下规则是讲解示例,不是 Pi 默认内置的权限策略:

  • A 检查参数是否包含路径和修改内容。
  • B 检查目标文件是否在允许修改的范围内。
  • C 记录一条执行前日志。

如果 A 没有阻断,B 明确阻断,Pi 会立即返回阻断结果,不再调用 C,也不会执行这次工具操作。

A 的结论只覆盖自己的检查。参数齐全,不能证明目标文件允许修改;先前处理器没有反对,也不能抵消后面的阻断。

这还带来一个实际影响:如果把某个日志处理器放在阻断处理器后面,它就可能收不到被阻断的调用。需要记录所有尝试时,不能只看"有没有注册日志处理器",还要看它位于什么位置。

阻断针对的是这次调用,不自动等于停止整个 Agent。错误结果可以进入后续上下文,让模型知道为什么没有执行,再决定是否调整方案。并行批次中其他工具是否执行,也要看各自的处理结果和取消状态。

修改结果,不会撤销真实操作

工具执行完成后,需要区分两件事:外部状态发生了什么变化,模型收到什么描述。

以追加文件为例:

text 复制代码
工具向文件末尾追加一行 → 已成功
执行后扩展把返回文字改成"执行失败,请重试"
模型相信失败描述 → 再次追加

文件可能因此多出一行。后处理器改变的是结果描述,之前成功的写入没有被撤销。这个例子说明了错误结果可能带来的风险,并不是一次实测记录。

反过来也一样:工具实际执行失败,扩展把内容和错误标记改成成功,并不能让缺失的文件或未完成的修改凭空出现。

因此,做摘要、脱敏或格式整理时,需要保留影响下一步判断的信息。尤其不要把"已经修改,尚未验证"缩写成"修改成功并验证通过",也不要把"部分成功"变成"全部失败"。

多个 tool_result 处理器按顺序处理同一份逐步更新的结果。后面的处理器能看到前面返回的修改;某个处理器只返回错误标记时,之前已经更新的内容和详情会保留。这里按返回字段更新,并不意味着任意嵌套对象都会自动深度合并。

为什么执行前后,异常处理不同

假设执行前的扩展负责检查修改权限,但读取权限规则时抛出了异常。

此时得到的是"没有检查结论",而不是"检查通过"。如果异常就放行,原本禁止的操作可能在检查故障时反而获得执行机会。

Pi 的这条执行前路径会把异常转换成工具错误,不进入实际工具执行。这个行为可以理解为失败时拒绝,但拒绝范围是本次调用,不能据此推断整个会话已经终止。

执行后的情况不同。假设工具已经修改文件,随后整理结果的处理器出错,错误发生在后处理阶段,不能推断文件没改过。

Pi 对这两个扩展事件采用了不同处理方式:

发生异常的位置 当前实现的处理
tool_call 处理器 异常向上传递,在工具准备阶段生成错误结果,不执行这次工具操作
tool_result 处理器 分发器捕获并报告扩展错误,继续调用后面的结果处理器

这里说的是两个扩展事件的处理器,不是所有钩子的通用规则。后处理器抛出异常,也不同于它正常返回一份标记为失败的结果:前者走扩展异常处理,后者是在主动修改工具结果。

无论如何,后处理异常不会自动回滚文件。恢复状态需要另外的恢复操作。

检查过参数,为什么还可能不够

Pi 的执行前扩展还有一个能力:直接修改输入参数。后面的处理器会看到前面的修改,工具执行时也使用这份参数。

这让扩展可以调整调用,但也让顺序变得重要:

text 复制代码
扩展 A:检查 notes.txt,允许修改
扩展 B:把目标路径改成 config.json
工具:实际修改 config.json

A 检查的是旧目标,结论无法覆盖后来的新目标。

当前实现先进行工具参数校验,再调用执行前扩展;扩展修改参数以后,不会自动再次进行参数校验。不能因为前面出现过一次"校验通过",就认为最终参数也经过了同样的检查。

还要区分参数校验和权限检查。路径字段要求是字符串,"config.json" 符合这个要求,但这只能说明格式符合约定,不能证明 Agent 有权修改这个文件。

如果要设计一条更严格的检查链,我会把参数调整放在前面,再针对最终参数检查格式和权限,并避免后续处理器继续改动影响授权的字段。这是根据上述行为提出的设计建议,不是 Pi 自动提供的保证。

即使安排好了顺序,也不能只靠这一点解决全部文件安全问题。实际执行仍需要考虑路径解析、符号链接,以及检查到执行之间文件状态可能变化等情况。

扩展需要被信任

能在执行前拦截工具,不代表扩展机制本身就是安全沙箱。

Pi 的扩展是可执行代码。项目文档明确提醒,扩展拥有运行进程的系统权限,可以执行任意代码,需要只安装可信来源的扩展。

因此,这里的工具钩子约束的是经过这条调度路径的调用,不能把它理解成操作系统级别的文件访问隔离。一个扩展自己直接执行的其他代码,也不能仅凭这张工具调用流程图就认定受到同样的检查。

这也是参数修改能力需要谨慎使用的原因:扩展既能补充规则,也能改变其他规则原本检查的对象。

本地验证了哪些行为

本地运行了仓库已有的 4 个定向测试,全部通过,使用模拟模型和测试工具,没有调用真实模型服务。

其中两个测试覆盖会话中的工具调用:执行前扩展返回阻断原因后,后续模型上下文能够看到对应错误;执行后扩展返回新内容后,后续模型能够看到改写后的结果。

另两个测试检查多个结果处理器如何协作:前面的内容修改会传给后面的处理器,后面的局部更新不会清除之前已修改的其他字段。

多处理器遇到阻断后提前返回、执行前后异常处理的差异,以及修改参数后不自动重新校验,来自对当前实现的核对。这次没有另外构造这些行为的故障测试,也没有实际修改文件来测量重复执行或回滚。

理解扩展机制,除了知道在哪里注册函数,还需要追踪它影响了什么:是执行资格、实际参数,还是模型看到的结果。遇到一次异常时,先确定它发生在哪个阶段,才能判断工具有没有执行、状态是否已经改变,以及下一步是否适合重试。


相关推荐
白远山3 小时前
上海24小时自助健身房系统软件开发实战指南:从需求到部署
java·架构·uni-app·需求分析
MayBaymax4 小时前
Spring AI Alibaba Graph 快速上手:黑板、节点、边
java·spring·ai·ai编程
斑鸠喳喳4 小时前
可重入锁 ReentrantLock
java·源码
IT枫斗者枫哥4 小时前
Spring AI 聊天记忆落库:重启后,怎样接上上一轮对话
java
devpotato4 小时前
HashMap 扩容机制:从源码细节到工程实践
java
yueping24 小时前
如何用idea打开jar包
java
IT_Octopus4 小时前
从一个应用开发者的角度,搞懂大数据查询的完整链路
java·大数据·数据库
许彰午4 小时前
51-BpmnDesigner集成
java·低代码·架构
蜗牛互联网4 小时前
Claude放宽生命科学限制:代价是验证、分级和30天留存
java·人工智能·后端
泡海椒4 小时前
评分系统最佳实践:JQuick-Java实现权重、阈值动态配置评分
java·人工智能·python