- Published on
Pi 源码解读(四):换一个模型,为什么不用重写 Agent?
- Authors

- Name
- Stone
本篇目录
第三课讲清了“模型提出工具请求,Pi 执行工具”。现在往前追一步:同样是修改 Vue 页面标题,换一家模型服务,为什么读文件、改文件的执行流程不用跟着重写?
NOTE
Agent 使用自己的统一格式;pi-ai 负责对接不同模型服务,并把结果转成这套格式。
可以把它理解成一个双向翻译层:出去时翻译请求,回来时翻译结果。
本课对照 Pi v0.84.1。继续使用教学场景,只讲模型接入的主线;伪代码不需要运行。
一、问题不是“把请求地址换一下”那么简单
你还是提出:
把 src/App.vue 的页面标题从 Hello 改成你好,其他地方不要动。
假设我们分别让模型 A 和模型 B 处理这件事。两者都可能请求读取文件,但它们使用的接口不一定相同:消息怎么组织、工具怎么描述、输出如何分批返回,都可能有差别。Pi 的模型层区分服务提供方和底层 API 实现,就是为了处理这类差异。1
如果没有适配层,Agent Loop 就得一边推进任务,一边判断:“A 的工具请求在哪里?B 的结果又该怎么解析?”每多接一家,就可能多出一批判断。
这像你做前端时,对接两个返回格式不同的后端。更容易维护的做法,通常不是让每个页面分别判断,而是先在接口层转换,再交给页面使用。这里只是职责上的类比,不表示 Pi 使用了前端的请求库。
二、它统一的不是答案,而是“沟通方式”
发出去:同一份任务材料,转换成对方能接收的格式
Agent 准备的材料主要是规则、消息记录和工具说明。对接具体服务时,再按照相应接口组织请求。Pi 的 Context 类型就把这些部分组织在一起。2
这里不用重新发明一套“读取 Vue 文件”的流程。上一课的 read 工具仍然负责读取,模型层负责把它的说明交给模型。
收回来:不同服务的输出,变成 Agent 能理解的消息
模型可能输出文字,也可能提出工具请求。Pi 用自己的助手消息表示这些内容,因此 Agent 可以继续判断:“有没有工具请求?需要执行哪个工具?”不必把这些判断全部绑在某一家接口的原始字段上。2
统一的是消息和事件的表达,不是把两个模型的回答变得一模一样。
可以用一句话记住方向:任务材料 → 适配后的请求 → 模型服务 → 统一后的输出 → Agent 继续处理。
三、Pi 怎么知道该找哪一家服务?
先分清两个名字:Model 是选用的具体模型;Provider 是负责提供模型调用能力的服务单元。 一个 Provider 可以提供多个模型。
本课版本里,Models 集合持有已注册的 Provider。发起调用时,它会根据模型找到对应 Provider,处理鉴权信息,再交给 Provider 调用。你可以把这个集合理解成“知道不同模型该交给谁处理的接线台”。3
主流程可以压缩成下面这段伪代码。这里把多个模块合作的过程合在了一起,不是某个函数的原文:
给定模型和当前任务材料:
找到负责这个模型的服务
准备访问凭据和请求配置
按对应接口发送请求
把返回内容转成统一消息或事件
交给 Agent,继续原来的执行流程
这不代表 Pi 会自动替你挑最好的模型,也不代表一家失败就一定自动换另一家。 本节解释的是:已经选定模型后,请求如何到达正确的服务。自动选模型或故障切换,是另外的策略问题。
四、流式输出,也要有统一的说法
第二课讲过,模型的一次回答可以分批返回。现在问题变成:不同服务分批返回的格式不同,Agent 怎么处理?
Pi 定义了一套统一事件。例如 text_delta 表示新增了一段文字,toolcall_end 表示一个工具请求内容已接收完成;done 和 error 则表达这次模型响应的结束状态。先理解它们的意思,不用背完整事件表。2
在我们的例子里,可以这样理解:模型先输出“我先读取文件”,随后给出读取请求;模型层把这些内容整理成统一的更新,Agent 再按自己的流程处理。
这里尤其注意,toolcall_end 表示:工具请求已经接收完整,不是文件已经读完。 真正读取文件,仍然是第三课讲的工具执行阶段。这是从事件和执行职责的区分直接得出的结论。
五、换模型之后,什么不变,什么仍然可能变化?
可以复用的是执行框架。 仍然是把材料交给模型、接收工具请求、执行工具、把结果交回去。适配层的意义,就是减少模型接口变化对这条流程的影响。
不能保证相同的是模型能力与行为。 Pi 的模型描述保留了输入类型、上下文窗口、最大输出等信息。统一的调用入口,不会凭空让一个不支持图片的模型具备看图能力。2
再想一个交接场景:模型 A 已经读过文件,你准备让模型 B 接着修改。
不能只把模型名字改掉,然后指望 B 自动知道 A 之前看到了什么。需要把后续工作所需的消息和工具结果交给 B,并处理格式兼容。 Pi 支持跨模型继续对话,但这是上下文的交接,不是把一个模型的内部状态搬进另一个模型。1
至于 B 是否提出同样的修改方案、能否一次完成任务,仍然要看实际输出,不能由“接口接通了”推导出来。
六、回到源码,只验证“请求交给谁”
这次先看 models.ts,找到 streamSimple:它如何找到 Provider,又如何把调用交出去? 看清这条线,再到 types.ts 看统一返回的消息和事件。
需要定位时,再展开这三个路标
ModelsImpl.streamSimple:把调用交给对应 Provider。
requireProvider:根据模型找到已注册的 Provider。
AssistantMessageEvent:不同实现共同遵守的输出事件类型。
前两个在 packages/ai/src/models.ts,最后一个在 packages/ai/src/types.ts。第一遍不用展开每一家服务的底层实现。
面试时,可以这样讲
对照源码看过之后,你可以用自己的话表达:
我看了 Pi 的模型适配层。它把 Agent 使用的消息和工具说明,与不同模型服务的接口格式隔开;请求交给对应 Provider,返回结果则统一成 Agent 能处理的消息和事件。所以换模型时,工具执行和循环逻辑可以复用。但统一接口不等于能力完全一致,仍然要考虑上下文长度、输入类型,以及旧消息能否兼容地交给新模型。
一个不用写代码的小练习
模型 A 支持图片,模型 B 只支持文字。调用入口完全一样,是否就可以把带图片的任务原样交给 B?
想过之后再看答案
不可以仅凭接口相同就作出保证。统一入口解决的是调用方式,不会改变模型能力。系统需要检查目标模型能力;必要时选择合适的模型,或者明确处理无法支持的输入,而不是假装图片已经被理解。
这一课记住:模型适配层统一沟通方式,Agent Loop 继续负责推进任务。
