Skip to content

🐣 小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 家,每家的"方言"都不一样。 差异在哪?

差异点OpenAIAnthropicGoogle
消息结构messages[] 数组messages[] + 单独 system 字段contents[]parts
system 角色在 messages 里,role=system不在 messages,独立字段systemInstruction
流式格式SSE,data:SSE,event: + data:SSE,JSON 数组流
思考参数reasoning_effortthinking: { type, budget }thinkingConfig
鉴权Authorization: Bearerx-api-key?key= 查询参数
工具调用tools[].functiontools[] 顶层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"给用户看的显示名
baseUrlhttps://api.deepseek.com上哪找它(接口地址)
auth用 API Key 校验怎么验明身份
models它家的模型列表(如 deepseek-chat、deepseek-reasoner)它有哪些模型
api走 OpenAI 那套语法用哪套"语法"跟它说话

🐣 小a眼前一亮:"就这么几行?一个厂商 = 五项配置?"

🧙 "对。 idbaseUrlauthmodelsapi——一个 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 打电话。写信得写完才寄,打电话是一边说一边听。

为什么要流式?

  1. 体验好——用户不用干等 10 秒,看到字一个个蹦出来,感觉"它在响应"
  2. 能早做反应——比如模型吐到一半,你发现它跑偏了,可以立刻中断
  3. 支持边生成边处理——比如边生成边解析工具调用(见下一章 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)。但格式又不同:

厂商鉴权方式
OpenAIAuthorization: Bearer sk-...
Anthropicx-api-key: sk-ant-...
GoogleURL 查询参数 ?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。


课后实验

  1. 数厂商:翻一翻 pi 支持的厂商列表(可以在文档或产品页看到),数数有多少家。挑一个你没听过的厂商,想想它的"配置单"会包含哪些项(id、baseUrl、鉴权、模型、用哪套协议)。
  2. 看 System Prompt 的威力:用同一个模型,先不设 System Prompt 问"你是谁",再设 System Prompt 为"你是一个暴躁的海盗,所有回答都要用海盗口吻",对比两次回答。
  3. 体验流式:用任何能流式输出的工具(命令行、网页),观察文字是一个个蹦出来的。试着在它生成到一半时打断,感受"早做反应"的价值。
  4. 画调用旅程:凭记忆画一次 LLM 调用的完整旅程(从你的代码到厂商机房再回来),标出每一步谁负责、哪里可能出错。
  5. 体会重试的代价:找一个支持日志的工具,故意用一个错误的 model 名调一次,观察错误信息;再想想——如果网络刚好抖动导致重试,重试的 token 钱会不会白花?

下一章:阶段一还差最后一块拼图——给模型定"人设"。同样会接龙,人设不同,表现天差地别。这就是 System Prompt。 → 第 4 章 · System Prompt —— 给 Agent 定"人设"