Pi Agent 学习笔记:不同模型如何接入同一套工具循环

hello 我是逆境不可逃

上一篇梳理了 Pi 的工具调用循环:模型提出动作,程序执行工具,把结果交回模型,再决定下一步。

顺着这个过程,会遇到一个问题:换一个模型服务之后,工具请求的格式变了,这套循环还能继续用吗?

Pi 把模型接口的转换工作放在 pi-ai 中。理解这一层,可以先从一个具体动作开始:读取 README.md

同一个动作,为什么有不同格式

不同模型接口可以用不同结构表达工具请求。以 Pi 接入的两种接口为例,只保留普通函数工具调用的核心字段:

信息 OpenAI Chat Completions 格式 Anthropic Messages 格式
工具名称 放在 function.name 放在 name 中
工具参数 放在 function.arguments 中,是 JSON 字符串 放在 input 中,是对象
调用编号 id id

JSON 字符串是一段需要解析的文本,对象则是已经能按字段取值的数据。两种表示都可以表达"读取 README.md"。

如果在 Agent 循环里分别读取这些格式,每增加一种接口,就可能多出一组判断。工具执行、消息记录也容易跟着依赖某个接口的结构。

Pi 在适配层完成转换,上层使用统一的工具请求:

text 复制代码
调用编号:call_A
工具名称:read
工具参数:路径 = README.md

Agent 得到这份请求后,可以按原来的规则找到工具、检查参数并执行。字段来自哪种外部格式,由适配层处理。

这里的统一针对接口协议。一家模型服务可能提供多种接口,多家服务也可能使用同一种兼容格式,因此"接入一家服务"和"新增一套协议转换"不一定是同一件事。

适配要做两个方向

请求模型时,Pi 需要把内部的对话记录、系统指令和工具说明转换成目标接口接受的格式。收到响应后,又要把外部数据整理成内部消息和事件。

text 复制代码
请求方向:
Pi 的对话记录和工具说明 → 接口适配 → 模型服务

响应方向:
模型服务 → 接口适配 → Pi 的统一消息 → Agent 循环

只统一请求还不够。如果返回的数据仍然保持各家格式,Agent 每次收到响应时,依旧要分别判断哪里是文本、哪里是工具请求。

Pi 的内部消息可以表示文本、思考内容和工具调用。工具执行结果也有自己的消息结构。适配层负责把这些结构转换成目标接口的表达方式。

转换必须保留动作的含义。字段从 input 变成 arguments 没有问题,但路径从 README.md 变成 package.json,就已经改变了任务。即使后面成功读到了文件,这次转换仍然是错的。

调用编号为什么不能丢

假设模型一次请求读取两个文件:

text 复制代码
请求 A:read,路径 = README.md
请求 B:read,路径 = package.json

两个请求的工具名相同,完成顺序也可能不同。程序需要知道每份结果属于哪次请求,不能只凭工具名称或完成顺序配对。

Pi 的工具请求带有调用编号,工具结果则记录自己对应的调用编号。下一轮请求模型时,这个关系会一起提交。

如果把 README.md 的内容标成请求 B 的结果,模型就可能把它当作 package.json 的内容。文件读取本身没有失败,错误发生在请求和结果的对应关系上。

切换模型接口时,编号还可能需要转换。不同接口对编号的字符和长度有不同要求。Pi 会记录旧编号到新编号的映射,并同步调整对应结果中的编号。

text 复制代码
转换前:请求编号 A,结果对应 A
转换后:请求编号 A_new,结果也必须对应 A_new

所以需要保持的是配对关系,编号文本未必原样不动。

调用编号也不等于工单编号。模型可以请求"创建工单",实际创建成功后,工单系统再返回真实工单编号。至于防止重复创建所需的业务幂等编号,则需要业务服务实现去重逻辑,不能靠工具调用编号自动获得。

流式参数要等到什么时候

工具参数也可能分段到达。下面是一个简化示例:

text 复制代码
第 1 段:{"path":
第 2 段:"README
第 3 段:.md"}

合起来才是完整的读取请求。适配层会积累片段,更新当前工具调用的内容,并发出进度事件。

Pi 可以在生成过程中解析部分参数,供上层观察进度。不过,当前 Agent 循环会先等待完整的模型响应,再进入工具调度。某个参数已经能读出来,并不意味着应该立即执行。

假设一个读取工具的起始行默认是第 1 行。生成中途只有路径,随后模型才补上"从第 1000 行开始"。提前执行虽然可能成功,却没有执行模型最终表达的动作。

这里有两个不同的检查:生成是否已经结束,以及最终参数是否符合工具要求。前者不能由"这段 JSON 能解析"代替,后者也不能由"模型已经生成完"代替。

Pi 还处理了输出长度上限这个边界:如果模型响应因为长度限制被截断,其中的工具参数可能不完整。当前循环会把这批工具调用转成错误结果,而不会实际执行它们。

模型请求失败和工具失败,分别处理

两种错误发生在不同阶段:

text 复制代码
模型请求失败:网络断开,没拿到完整响应
工具执行失败:模型请求完整,但读取的文件不存在

模型流以错误或取消状态结束时,当前 Agent 循环会先处理本轮结束,不执行这条失败响应中的工具调用。

如果读取工具已经执行,只是文件不存在,工具会返回错误结果。这条结果仍然可以交给模型,供它检查目录、修改路径,或者解释为什么无法继续。

这一区分也影响重试。模型请求失败,需要考虑重新请求模型;文件不存在,则应先处理路径问题。没有区分失败发生的位置,盲目重试可能只会重复同一个错误。

如果工具有写入副作用,还要额外考虑重复执行。例如创建工单后响应丢失,调用方看到的是超时,但服务端可能已经创建成功。重试同一次业务操作时,需要沿用业务幂等编号,并由服务端识别重复请求。这属于工具背后的业务设计。

统一格式,也要处理能力差异

统一消息结构能携带图片,不代表所有模型都能看图。Pi 的模型信息中会记录支持的输入类型,消息转换也会参考这个信息。

在当前实现中,目标模型不支持图片时,转换逻辑会把图片替换成文字占位标记,说明图片因为模型不支持而被省略。用户图片和工具返回的图片都有对应处理。

这能让后续模型知道原来存在一张图片,但它仍然看不到图片内容。假设用户只发了一张报错截图,再问"怎么解决",剩下的文字不足以回答原问题。

从产品设计上,可以在发起请求前提示用户切换模型,或采用明确告知的文字识别方案。无论采用哪种方式,都需要承认转换后的信息发生了变化。接口适配不能凭空补齐模型能力。

怎样检查适配是否正确

验证时,可以围绕一次文件读取检查具体行为:

  • 工具名称和参数含义是否保留,路径有没有变化。
  • 请求与结果是否正确配对,编号转换后是否仍然一致。
  • 流式片段是否正确合并,初始内容有没有被后续增量覆盖或丢失。
  • 模型响应失败或被截断时,是否避免执行不完整的工具请求。

这些检查不必全部依赖真实模型。用预设响应模拟文本增量、工具参数和错误结束,可以更稳定地重现边界情况。

本地复核使用了仓库已有的流式解析与跨接口消息转换测试。其中,流式测试检查初始内容与增量的合并、工具参数解析和错误信息保留;消息转换测试检查跨模型历史的处理,以及如何为缺少结果的调用补上明确的错误消息。

例如,一条历史记录有工具请求,却没有对应结果,Pi 的消息转换会补充"没有提供结果"的错误消息。它补齐的是协议需要的对应关系,并没有编造一次成功执行。

把这些细节放回工具循环里,就能看清适配层的作用:Agent 得到含义明确、关系完整的消息,才能正确调度工具并决定下一步。换一种编程语言实现,同样需要处理格式转换、流式状态、调用关联和能力差异。


相关推荐
词却1 小时前
OpenCV学习:dlib 人脸检测
人工智能·opencv·学习
爱吃苹果的日记本1 小时前
数据结构第三课补充(空间复杂度)
数据结构·学习
♪飙♪2 小时前
电气笔记——表的认识
笔记
PC2005-cloud2 小时前
Rust学习笔记:所有权、借用与生命周期——Rust的核心机制
笔记·学习·rust
弘毅 失败的 mian2 小时前
数组和指针基础
c语言·开发语言·经验分享·笔记
问心无愧05132 小时前
ctf show web 177
前端·笔记
霍金的微笑3 小时前
fuccc
学习
潜心一志3 小时前
HALCON软件——数据结构
学习·计算机视觉
秦哈哈3 小时前
【HelloAgents】学习笔记(一)
学习·ai·agent