PI分析-模型抽象层
写 agent 内核那篇的时候,我留了个黑盒没拆:主循环把对话内容交给一个叫 streamFn 的函数,它吐回一串事件,中间发生了什么完全没管。
我们顺着这条线往下拆一拆
StreamFn
1 | |
model是什么?
1 | |
所以就是provider决定走什么接入通道,一个接入通道可以实现多种api格式,然后通过id,name去选择模型
streamFn的具体实现
这个我们需要沿着链条追踪一下
1 | |
也就是会调用provider的streamSimple,继续往下看看
provider
1 | |
provider的实现
这里我们就可以看看某个provider的具体实现了, 这个是实现都在packages/ai/src/providers
1 | |
这里核心是传入的CreateProviderOptions.api,最后调用的都是他的streamSimple方法
1 | |
但其实这里根本使用的import进来的方法lazyApi(() => import("./openai-responses.ts") 导入的ProviderStreams是什么东西
import(…) 长得像函数调用,但它其实是个语法结构(不是真函数,不能 const f = import 这样引用)。它返回 Promise<模块命名空间对象>:
1 | |
所以接下来需要看的就是具体到api层的streamSimple
1 | |
最后调用的也是stream, 这应该算是与llm的最后一层了
1 | |
这就是具体的协议了,协议的细节很多,这里先不展开,简而言之就是获取到llm的openaiStream往外部返回的stream推消息
不过我们可以稍微看看AssistantMessageEventStream的实现,因为这是整个provider与pi交流的桥梁
整体上是实现了一个同步返回,异步填充的队列
AssistantMessageEventStream
1 | |
extend了EventStream
1 | |
总结
链条追到这里就完整了,回头看一眼全貌

四个概念的关系
| 概念 | 是什么 | 关键字段/方法 |
|---|---|---|
Model |
纯数据描述符,没有任何行为 | provider 决定走哪条接入通道,api 决定用哪种线格式 |
Provider |
运行时单元,一条接入通道 | auth / getModels() / stream / streamSimple,内部按 model.api 分发 |
Api(ProviderStreams) |
线格式实现,只管协议不管鉴权 | stream / streamSimple,实现都在 packages/ai/src/api |
AssistantMessageEventStream |
统一返回通道 | push / end / result(),屏蔽掉所有 provider 的协议差异 |
所以这套抽象的分层是很干净的:Model 描述「用什么」,Provider 决定「从哪进」,Api 负责「怎么说」,EventStream 统一「怎么回」。
一个 provider 可以挂多种 api(byApi 分发),一种 api 也能被多个 provider 复用(openai / azure / openrouter 都走 openai-responses),两边靠 Model 上的两个字段撮合。
两个反复出现的套路
一是懒加载,出现了两层。lazyStream 是「同步返回外层流、异步接线内层流」,让整条链上任何一层都不用把 async 泄漏给调用方;lazyApi 则把具体协议模块的 import 推迟到首次调用,不用为了支持一堆 provider 在启动时全量加载。
二是错误不抛,只推事件。lazyStream 的 .catch 把 setup 阶段的异常转成 error 事件,stream 的 try/catch 也是同样处理。整条链上只有一个出错的表达方式,主循环不用同时写 try/catch 和事件处理两套逻辑。