Appearance
🐣 小a的打工记
小a学完接龙、学完思考,觉得自己可以上岗了。它兴冲冲跑去三家打工——OpenAI 掌柜、Anthropic 夫人、Google 大爷。
第一天就炸了锅。
OpenAI 掌柜说:"跟我说话,得用
messages数组,角色叫system!"Anthropic 夫人撇撇嘴:"什么
system?我用system字段单独传!消息里不许塞 system!"Google 大爷更绝:"角色?我没角色这回事,我用
contents,里面装parts!"小a崩溃了:三家的话,一句都不通。 它写了三套代码伺候三家,改一家就得改一遍。
老z看着满地打滚的小a,乐了:"巴别塔危机啊。来,我教你怎么造一个『万能翻译局』。"
3.1 巴别塔危机:为什么调 LLM 这么烦
先看清问题到底有多大。老z带小a数了数 pi 支持的厂商:
🧙 举个例子 —— pi 支持的厂商多达 近 40 家,常见的有:
OpenAI、Anthropic、Google、DeepSeek、智谱(zai)、月之暗面(moonshotai)、通义(qwen)、Mistral、Groq、Bedrock、Vertex……还有一堆你听都没听过的小厂。
近 40 家,每家的"方言"都不一样。 差异在哪?
| 差异点 | OpenAI | Anthropic | |
|---|---|---|---|
| 消息结构 | messages[] 数组 | messages[] + 单独 system 字段 | contents[] 装 parts |
| system 角色 | 在 messages 里,role=system | 不在 messages,独立字段 | 用 systemInstruction |
| 流式格式 | SSE,data: 行 | SSE,event: + data: | SSE,JSON 数组流 |
| 思考参数 | reasoning_effort | thinking: { type, budget } | thinkingConfig |
| 鉴权 | Authorization: Bearer | x-api-key 头 | ?key= 查询参数 |
| 工具调用 | tools[].function | tools[] 顶层 | tools[].functionDeclarations |
🧙 看明白了吗?同样是"调一个大模型",六七个维度全不一样。 你要是直接对接,每家写一套,近 40 家就是 近 40 套代码。更糟的是——厂商随时改 API,你得跟着改 近 40 处。
这就是巴别塔危机:语言不通,协作成本爆炸。
3.2 解法:Provider —— 给每家配一个翻译
老z的方案很朴素:别让上层学方言,给每家配个翻译。
🧙 Provider(厂商提供者)是什么?
Provider 是"一个厂商的适配器"。它对外说一套统一的话,对内翻译成那家厂商的方言。
老z打比方:Provider 就像翻译局的翻译员——你(上层代码)只说"普通话"(统一接口),翻译员负责把它翻成 OpenAI 语 / Anthropic 语 / Google 语,再把你听不懂的回复翻回普通话。
Provider 长什么样?
我们看 pi 里一个最简单的 provider——DeepSeek(它兼容 OpenAI 协议,所以配置特别短)。它的"配置单"大致长这样(示意,不是真实源码):
| 配置项 | DeepSeek 的取值 | 含义 |
|---|---|---|
id | "deepseek" | 这家厂商叫什么(内部标识) |
name | "DeepSeek" | 给用户看的显示名 |
baseUrl | https://api.deepseek.com | 上哪找它(接口地址) |
auth | 用 API Key 校验 | 怎么验明身份 |
models | 它家的模型列表(如 deepseek-chat、deepseek-reasoner) | 它有哪些模型 |
api | 走 OpenAI 那套语法 | 用哪套"语法"跟它说话 |
🐣 小a眼前一亮:"就这么几行?一个厂商 = 五项配置?"
🧙 "对。
id、baseUrl、auth、models、api——一个 provider 就是一份配置单。差异被createProvider这个工厂吃掉了。具体怎么吃的,我们留到 Part II 第 17 章拆 pi-ai 源码时细讲。现在你只要建立直觉:加一个新厂商,就是写一份这样的配置单,不用改核心代码。"
这就是 近 40 家厂商能被统一管理的秘密——每家一份配置单,统一接口在它们之上。
3.3 统一接口:管你哪家,都是一套调用
有了翻译员,上层怎么说"普通话"?老z给小a看 pi 的统一调用入口:
🧙 统一调用接口
不管底层是哪家厂商,上层都用同一套方式调:
streamSimple({ model, // 哪个模型(来自哪个 provider) messages, // 消息(统一格式) tools, // 工具(统一格式) reasoning, // 思考强度(第 2 章) })你不用关心 model 来自 OpenAI 还是 Anthropic——streamSimple 内部会找到对应的 provider,把你的"普通话"翻译成那家的方言。
🐣 小a松了口气:"那我换厂商,只要换个 model,调用代码一行都不用改?"
🧙 "对。 这就是抽象的价值。今天用 OpenAI,明天换 Anthropic,后天加个 DeepSeek——上层代码纹丝不动。"
统一消息格式
回忆巴别塔那六七种差异,最头疼的是消息格式。统一接口的第一件事,就是定义一套统一的消息格式:
- 你给上层的是统一格式(比如统一的
user/assistant/tool角色) - provider 翻译时,把它转成各家原生格式(OpenAI 的
messages、Google 的contents等)
这个"格式互转"是统一抽象里最难的部分——可以理解为 pi 内部专门有一套"格式转换器"负责把统一消息翻译成各家原生格式(具体实现留到 Part II 第 17 章细讲)。
3.4 多模态:模型不止能读文字
讲到消息格式,有个容易忽略的点:现代模型不只能读文字,还能读图。 这叫多模态(multimodal)。
🧙 可以这样理解
在统一接口里,用户消息的内容不再只是"一段文字",而是一个列表——里面可以装两种东西:
- 文字片段:正常的对话内容
- 图片片段:一张图片,带着它的 base64 数据和格式(如
image/png)也就是说,你可以这样给模型发消息(示意):
"我刚看到这个报错,帮我看看" + [一张报错截图]
🧙 多模态意味着什么?
你给模型的消息,不只
TextContent(文字),还可以是ImageContent(图片)。
- 给它一张报错截图 → 它能"看懂"错误信息
- 给它一个 UI 设计图 → 它能照着写前端代码
- 给它一张架构图 → 它能讲明白组件关系
这对编程 agent 特别有用——pi 能让你直接丢一张图让它分析。
🐣 小a好奇:"那统一接口要把图片也翻译成各家格式吗?"
🧙 "对,又是一种方言。 OpenAI 用
image_url,Anthropic 用source: { type: "base64" },Google 又是另一套。Provider 翻译时连图片格式都得转。巴别塔的方言,远不止文字。"
3.5 流式:一个字一个字地吐
调 LLM 还有个绕不开的概念——流式(streaming)。
🧙 流式是什么?
不等模型把整段话生成完再返回,而是生成一个 token 就吐一个 token。
- 非流式:等 10 秒 → 一次性返回完整答案
- 流式:第 1 秒吐"你",第 1.1 秒吐"好",……实时看到它在"打字"
老z打比方:像写信 vs 打电话。写信得写完才寄,打电话是一边说一边听。
为什么要流式?
- 体验好——用户不用干等 10 秒,看到字一个个蹦出来,感觉"它在响应"
- 能早做反应——比如模型吐到一半,你发现它跑偏了,可以立刻中断
- 支持边生成边处理——比如边生成边解析工具调用(见下一章 function calling 的"流式部分 JSON")
流式的难点
各家流式格式又不一样(巴别塔又来了):
- OpenAI:
data: {chunk}一行行 - Anthropic:
event: content_block_delta+data: {...} - Google:JSON 数组,按顺序流
而且流式有个讨厌的衍生问题——"话没说完就要开始猜":模型吐工具调用参数时,JSON 往往吐到一半({"path": "conf),但上层已经需要开始解析了。这叫流式部分 JSON(partial JSON),是第 5 章 function calling 的难点。
3.6 鉴权与限流:打工的日常麻烦
最后讲两个"打工日常"的麻烦——它们不是核心概念,但绕不开。
鉴权:证明你是你
每家厂商都要你证明身份(API Key)。但格式又不同:
| 厂商 | 鉴权方式 |
|---|---|
| OpenAI | Authorization: Bearer sk-... 头 |
| Anthropic | x-api-key: sk-ant-... 头 |
URL 查询参数 ?key=... |
还有些厂商支持 OAuth(免 API Key,用账号登录授权)。pi 的 auth 系统会自动处理这些——它甚至有多级降级的"责任链"(存档 key → OAuth → 环境变量 → 失败,见 Part II 第 17 章)。
限流(429):厂商说"你太快了"
调得太频繁,厂商会返回 429 Too Many Requests——"你慢点"。这时得退避重试(等一会儿再试,附录 E.9 会讲)。不同厂商的限流策略又不一样(有的按分钟,有的按天)。
🧙 这两个麻烦的本质:厂商的差异,远不止"消息格式"。 鉴权、限流、流式格式、思考参数……全是方言。Provider 抽象的价值,就是把这些方言一股脑都吃掉,让上层只管说普通话。
3.7 一次调用的完整旅程:从你的代码到厂商机房
前面几节把零件都介绍了,老z决定带小a把一次完整的调用从头到尾走一遍,把前面的概念串起来:
你的代码
↓ ① 调用 streamSimple({ model, messages, tools, reasoning })
统一接口
↓ ② 根据 model 找到对应的 provider(翻译员)
↓ ③ 把统一消息翻译成该厂商的方言(含图片格式、思考参数)
Provider
↓ ④ 加上鉴权(API Key / OAuth)
↓ ⑤ 通过 HTTP 发到该厂商的 baseUrl
厂商服务器
↓ ⑥ 内部推理(KV 缓存命中 → 直接复用,见 1.12)
↓ ⑦ 流式返回 token(见 3.5)
Provider
↓ ⑧ 边收边翻译回统一格式
你的代码
↓ ⑨ 你收到流式事件(文字 / 工具调用 / 停止原因)🧙 老z逐层点评:
- ① 是你唯一写的代码
- ②③ 是 Provider 的"翻译"本职
- ④⑤ 是鉴权和传输,框架帮你做
- ⑥⑦ 发生在厂商机房,你管不着,但理解它才能懂计费(1.7)和缓存(1.12)
- ⑧⑨ 是结果怎么回到你手里
九个步骤,你只需要关心两头:你发了什么(①)、你收到了什么(⑨)。中间全部被抽象吃掉。 这就是"换厂商一行不改"的完整图景——不是魔法,是中间七步被封装了。
3.8 可靠性:超时、重试与"重来一次要花钱"
"旅程是会翻车的。"老z话锋一转,"网络抖动、厂商抽风、你调太猛——调用失败是常态,不是意外。 所以最后一课,讲讲怎么'扛'。"
超时:等多久算失败?
🧙 LLM 生成可能很慢(深思考要几十秒),你得设一个超时上限:
- 超时了怎么办?两种思路:失败上报(告诉用户"超时了")或延长重试
- 但别无限等——用户会疯。所以框架都有超时配置,默认值偏保守
重试:哪些失败值得重试?
🧙 关键原则:重试要挑"瞬时错误",不挑"结果错误"。
- ✅ 值得重试:网络抖动、429 限流、5xx 服务器错误——这些是"这次没送达",重试可能就好
- ❌ 不值得重试:模型答错了、答得不满意——这些是"送达了但结果不对",重试只是再烧一遍钱
所以成熟的库会对错误分类:哪类自动重试、哪类直接抛给上层。盲目重试 = 双重浪费:时间 + token。
计费视角:重试 = 重算 = 再扣钱
🧙 LLM 和数据库不同:它没有"幂等"保证。
数据库里"重复提交同一操作"会被幂等拦截;但 LLM 是按"生成过的 token"计费的——你重发一次请求,厂商就重新算一遍,重新扣一次钱。 缓存(1.12)能帮你省同一前缀的钱,但重试本身不免费。
工程含义:对长请求、大 prompt,失败后盲目重试可能烧掉一大笔钱。 这也是为什么 agent 框架会把"这次已经花掉的 token"记下来(Usage,后面 Part II 会看到)——先知道花了多少,才知道要不要再花。
🐣 小a记下:调用 LLM 要管三件事——超时别无限等、重试只挑瞬时错误、重试会重复计费。单次失败不可怕,可怕的是"不知道失败"和"盲目重烧钱"。这就是 fail loud 精神的源头(后面第 15 章还会见到它)。
3.9 万能翻译局的诞生
故事讲完,小a终于明白了老z的用意:
🐣 小a总结:
"所以,pi-ai 这个包,本质上就是一个万能翻译局:
- 近 40 个 provider = 近 40 个翻译员
- 统一接口 = 大家都说普通话
- 翻译员负责把普通话翻成各家方言(消息格式、流式、鉴权、思考参数、图片格式……)
有了它,我换厂商就像换件衣服,核心代码一行不改。"
🧙 老z点头:"对。而且这个翻译局不是凭空的——它的每一份配置、每一段翻译代码,都是真实长在 pi-ai 的源码里的。 等你读到 Part II 第 17 章,我们会一个文件一个文件地拆开看,这个翻译局是怎么造出来的。"
本章小结
🐣 小a的第三课
┌──────── 怎么调大模型?─────────┐ │ │ │ • 近 40 家厂商,方言全不同 │ │ → 巴别塔危机 │ │ │ │ • Provider = 厂商翻译员 │ │ → 一份配置单,差异被吃掉 │ │ │ │ • 统一接口 = 大家说普通话 │ │ → streamSimple() 一套调用 │ │ │ │ • System Prompt = 模型的人设 │ │ → 决定它"是谁" │ │ │ │ • 多模态 = 不只读字,还读图 │ │ → ImageContent │ │ │ │ • 流式 = 边生成边吐 │ │ → 体验好,但有方言差异 │ │ │ │ • 鉴权 + 限流 = 打工日常麻烦 │ │ → Provider 也帮你处理 │ │ │ │ • 一次调用 = 9 步旅程 │ │ → 你只管头尾,中间全被封装 │ │ │ │ • 可靠性:超时 + 挑瞬时错误重试│ │ → 重试会重复计费,别盲目重试│ └────────────────────────────────┘
阶段一完结:读完 1-4 章,你已经认识了大模型本身——它怎么工作(接龙)、怎么思考(CoT)、怎么调它(Provider/统一接口)、怎么给它定人设(System Prompt)。接下来,我们要把它变成会干活的 Agent。
课后实验
- 数厂商:翻一翻 pi 支持的厂商列表(可以在文档或产品页看到),数数有多少家。挑一个你没听过的厂商,想想它的"配置单"会包含哪些项(id、baseUrl、鉴权、模型、用哪套协议)。
- 看 System Prompt 的威力:用同一个模型,先不设 System Prompt 问"你是谁",再设 System Prompt 为"你是一个暴躁的海盗,所有回答都要用海盗口吻",对比两次回答。
- 体验流式:用任何能流式输出的工具(命令行、网页),观察文字是一个个蹦出来的。试着在它生成到一半时打断,感受"早做反应"的价值。
- 画调用旅程:凭记忆画一次 LLM 调用的完整旅程(从你的代码到厂商机房再回来),标出每一步谁负责、哪里可能出错。
- 体会重试的代价:找一个支持日志的工具,故意用一个错误的 model 名调一次,观察错误信息;再想想——如果网络刚好抖动导致重试,重试的 token 钱会不会白花?
下一章:阶段一还差最后一块拼图——给模型定"人设"。同样会接龙,人设不同,表现天差地别。这就是 System Prompt。 → 第 4 章 · System Prompt —— 给 Agent 定"人设"