第 4 章:万能遥控器
故事场景
你已经能跟 DeepSeek 流畅对话了。老板走过来看了一眼,说:
"挺好的。不过下个月我们可能换成月之暗面------他们新出的模型测评分很高。"
你心里一惊。当前代码里,DeepSeek 的信息散落在三四个文件里:
baseURL 在这里,model 名字在那里,apiKey 的环境变量名又不一样。
要换一家 AI 公司?你会是一场噩梦:每个文件搜一遍,改 URL、改模型名、改密钥变量名。
你需要的是一个万能遥控器 。按"开机"键------不管是索尼电视还是三星电视------它都能开。
代码说"我要对话"------不管是 DeepSeek 还是月之暗面------它都能调。
这就是 Provider 模式。Pi 支持 30+ 家 AI 公司,靠的就是这一层抽象。
本章我们只实现 1 个 Provider(DeepSeek),但接口设计要能容纳 30 个。
这一章要做什么
- 补充 Model 类型------定义"每个 AI 大脑长什么样"
- 把"怎么跟 AI 公司通信"抽象成
Provider接口 - 把"去哪里找模型"抽象成
Models集合 - 把认证逻辑独立出来------auth resolver
- 用
createModels()和createProvider()两个工厂函数组装一切
做完后,切换 AI 公司只需改一行代码:
typescript
// 之前(散落各处的配置)
const client = new OpenAI({ apiKey: "...", baseURL: "https://..." });
const response = await client.chat.completions.create({ model: "...", ... });
// 之后(集中管理)
const models = createModels();
models.setProvider(deepseekProvider()); // DeepSeek
// models.setProvider(moonshotProvider()); // 将来换成月之暗面------只改这一行
const model = models.getModel("deepseek", "deepseek-v4-flash")!;
const stream = models.stream(model, context);
实现
第一步:补充 Model 类型
Provider 需要知道"每个模型长什么样"------名字、价格、上下文窗口大小。
我们先在 src/types.ts 末尾追加 Model 类型定义(和第二章的风格一致------类型先行):
typescript
// ─── 模型描述符 ───
// 每一个"大脑型号"都有一套完整的"说明书"
export const Model = Type.Object({
// 基本信息
id: Type.String(), // "deepseek-v4-flash"
name: Type.String(), // "DeepSeek V4 Flash"
provider: Type.String(), // 所属公司:"deepseek"
// 能力说明
reasoning: Type.Boolean(), // 是否支持推理/思考过程暴露
input: Type.Array( // 支持哪些输入类型
Type.Union([Type.Literal("text"), Type.Literal("image")])
),
// 价格(每百万 token,单位美元)
cost: Type.Object({
input: Type.Number(), // 输入价格
output: Type.Number(), // 输出价格
cacheRead: Type.Number(), // 缓存命中读取价格(DeepSeek 支持)
cacheWrite: Type.Number(), // 缓存写入价格
}),
// 容量限制
contextWindow: Type.Number(), // 最多能"记住"多少 token 的上下文
maxTokens: Type.Number(), // 一次最多输出多少 token
});
export type Model = Static<typeof Model>;
有了 Model,下一步就是定义"谁来提供这个模型"------Provider 接口。
第二步:Provider 和 Models 接口
创建 src/models.ts:
typescript
import type {
Model, Context, Message,
AssistantMessageEvent, AssistantMessage,
} from "./types.js";
// ─── 认证相关类型 ───
// 每个 Provider 必须告诉我们:怎么拿出 API Key、怎么在请求里带上它
export interface ApiKeyAuth {
type: "apiKey";
// 从环境变量里找出 API Key 的函数
resolve: () => { apiKey: string } | undefined;
}
export interface ProviderAuth {
apiKey: ApiKeyAuth;
}
// ─── Provider 接口 ───
// 这就是"万能遥控器"的按钮定义
// 每个 AI 公司都要提供这些"功能按钮":
// 1. 我是谁(id, name)
// 2. 怎么认证(auth)
// 3. 我支持哪些模型(getModels)
// 4. 怎么跟我对话(stream)
export interface Provider {
// 基本信息
readonly id: string; // 唯一标识,如 "deepseek"
readonly name: string; // 显示名,如 "DeepSeek"
// 我的服务器地址
readonly baseUrl?: string;
// 认证方式------目前只支持 API Key,后面可以扩展 OAuth
readonly auth: ProviderAuth;
// 返回我支持的模型列表
getModels(): readonly Model[];
// 发起流式对话------这是我们第 3 章做的事情,但要包成通用接口
stream(
model: Model,
context: Context,
options?: { apiKey?: string; signal?: AbortSignal }
): ReadableStream<AssistantMessageEvent>;
}
// ─── Models 集合 ───
// 管理多个 Provider 的"登记册"
// 你可以往里注册 DeepSeek、月之暗面、OpenAI...
// 然后统一查询"哪个模型在哪个公司?"
export interface Models {
// 列出所有已注册的 Provider
getProviders(): readonly Provider[];
// 按名字查一个 Provider
getProvider(id: string): Provider | undefined;
// 查某个公司旗下的所有模型
getModels(provider?: string): readonly Model[];
// 精确查询一个模型:公司名 + 模型名
getModel(provider: string, id: string): Model | undefined;
// 发流式请求------找到模型所属的 Provider,委托给它
stream(model: Model, context: Context): ReadableStream<AssistantMessageEvent>;
// 等全部回复(和流式相反,用于脚本场景)
complete(model: Model, context: Context): Promise<AssistantMessage>;
}
// ─── 可变版本的 Models(可以往里加 Provider) ───
export interface MutableModels extends Models {
setProvider(provider: Provider): void;
deleteProvider(id: string): void;
}
第三步:创建 Models 集合的实现
继续在 src/models.ts 里写:
typescript
// ─── Models 集合的具体实现 ───
class ModelsImpl implements MutableModels {
// 用 Map 存 Provider------key 是 "deepseek",value 是 Provider 对象
private providers = new Map<string, Provider>();
// 注册一个 Provider(重复名字的后注册会覆盖前面的)
setProvider(provider: Provider): void {
this.providers.set(provider.id, provider);
}
deleteProvider(id: string): void {
this.providers.delete(id);
}
getProviders(): readonly Provider[] {
return Array.from(this.providers.values());
}
getProvider(id: string): Provider | undefined {
return this.providers.get(id);
}
// 查模型------先找到 Provider,再从它的模型列表里找
getModels(provider?: string): readonly Model[] {
if (provider) {
return this.providers.get(provider)?.getModels() ?? [];
}
// 没指定公司名------返回所有公司的所有模型
const allModels: Model[] = [];
for (const p of this.providers.values()) {
allModels.push(...p.getModels());
}
return allModels;
}
// 精确查一个模型
getModel(providerId: string, modelId: string): Model | undefined {
return this.getModels(providerId).find((m) => m.id === modelId);
}
// ─── 发请求 ───
// 整个调用链:找到 Provider → 解析认证 → 调用 Provider 的 stream 方法
stream(model: Model, context: Context): ReadableStream<AssistantMessageEvent> {
// 先找到这个模型属于哪个 Provider
const provider = this.providers.get(model.provider);
if (!provider) {
// 找不到------创建一个"报错流"
return this.createErrorStream(
`未知的 Provider: ${model.provider}`
);
}
// 解析认证------找出 API Key
const authResult = provider.auth.apiKey.resolve();
if (!authResult) {
return this.createErrorStream(
`${provider.name} 没有配置 API Key`
);
}
// 委托给 Provider 自己去处理请求
return provider.stream(model, context, { apiKey: authResult.apiKey });
}
// complete 就是 stream 的简化版------等流跑完,取最终结果
async complete(model: Model, context: Context): Promise<AssistantMessage> {
const stream = this.stream(model, context);
const reader = stream.getReader();
let finalMessage: AssistantMessage | null = null;
while (true) {
const { done, value: event } = await reader.read();
if (done) break;
if (event.type === "done" || event.type === "error") {
finalMessage = event.message;
}
}
if (!finalMessage) {
throw new Error("流结束但没有收到 done 或 error 事件");
}
return finalMessage;
}
// 创建一个只包含错误事件的流------用于报错时的统一处理
private createErrorStream(errorMessage: string): ReadableStream<AssistantMessageEvent> {
return new ReadableStream({
start(controller) {
controller.enqueue({ type: "start" as const });
controller.enqueue({
type: "error" as const,
message: {
role: "assistant" as const,
content: [],
usage: { input: 0, output: 0, total: 0 },
stopReason: "error" as const,
errorMessage: errorMessage,
timestamp: Date.now(),
},
});
controller.close();
},
});
}
}
// ─── 工厂函数:创建一个空的 Models 集合 ───
export function createModels(): MutableModels {
return new ModelsImpl();
}
第四步:实现 DeepSeek Provider
创建 src/providers/deepseek.ts------这是我们的第一个 Provider 实现:
typescript
import OpenAI from "openai";
import type { Provider, ProviderAuth } from "../models.js";
import type { Model, Context, AssistantMessageEvent } from "../types.js";
import { streamCompletion, EventStream } from "../event-stream.js";
// ─── DeepSeek 支持的模型列表 ───
// 这是硬编码的"模型目录"------产线环境里可以从 API 动态获取
const DEEPSEEK_MODELS: Model[] = [
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
provider: "deepseek",
reasoning: true, // 支持思考模式(通过 toggle 开关)
input: ["text"],
cost: {
input: 0.14, // $0.14 / 百万输入 token
output: 0.28, // $0.28 / 百万输出 token
cacheRead: 0.0028, // 缓存命中:$0.0028
cacheWrite: 0.14,
},
contextWindow: 1048576, // 1M 上下文窗口
maxTokens: 8192, // 默认输出 8K(最大可达 384K)
},
// V4 Pro------更强但更贵,同样支持思考模式
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
provider: "deepseek",
reasoning: true, // 支持思考模式
input: ["text"],
cost: {
input: 0.435, // $0.435 / 百万输入 token
output: 0.87, // $0.87 / 百万输出 token
cacheRead: 0.003625, // 缓存命中:$0.003625
cacheWrite: 0.435,
},
contextWindow: 1048576, // 1M 上下文窗口
maxTokens: 8192, // 默认输出 8K(最大可达 384K)
},
];
// ─── 认证配置 ───
const auth: ProviderAuth = {
apiKey: {
type: "apiKey",
// 从环境变量里找出密钥
// Pi 的做法:先查 DEEPSEEK_API_KEY,作为备选可以用通用的 OPENAI_API_KEY
resolve: () => {
const key = process.env.DEEPSEEK_API_KEY;
if (!key) return undefined;
return { apiKey: key };
},
},
};
// ─── 创建 Provider ───
// 注意:这个函数返回的是 Provider 接口,不是具体实现
// 这就是抽象的意义------调用方不需要知道内部用了"openai" npm 包
export function deepseekProvider(): Provider {
return {
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://api.deepseek.com",
// 认证方式
auth,
// 模型列表
getModels: () => DEEPSEEK_MODELS,
// 流式对话------这里才是真正调用 DeepSeek API 的地方
stream(model, context, options) {
const client = new OpenAI({
apiKey: options?.apiKey,
baseURL: "https://api.deepseek.com",
});
// 把 Context → OpenAI API 的消息格式
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [];
if (context.systemPrompt) {
messages.push({ role: "system", content: context.systemPrompt });
}
for (const msg of context.messages) {
if (msg.role === "user") {
messages.push({
role: "user",
content: typeof msg.content === "string" ? msg.content : "(非文本内容)",
});
} else if (msg.role === "assistant") {
const textParts = msg.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("");
if (textParts) {
messages.push({ role: "assistant", content: textParts });
}
} else if (msg.role === "toolResult") {
messages.push({
role: "tool",
tool_call_id: msg.toolCallId,
content: typeof msg.content === "string" ? msg.content : "(工具结果)",
});
}
}
// 使用 AbortSignal 支持取消请求
const signal = options?.signal;
// 返回一个 Promise...但异步函数不能直接返回 ReadableStream
// 所以用一个"手动创建的流"来桥接
// Pi 用了一个更优雅的 async generator 模式,这里简化了
return new ReadableStream<AssistantMessageEvent>({
async start(controller) {
try {
// 如果没有信号或者还没被取消,执行请求
if (signal?.aborted) {
controller.enqueue({
type: "error" as const,
message: {
role: "assistant" as const,
content: [],
usage: { input: 0, output: 0, total: 0 },
stopReason: "aborted" as const,
errorMessage: "请求已被取消",
timestamp: Date.now(),
},
});
controller.close();
return;
}
const stream = await client.chat.completions.create({
model: model.id,
messages,
stream: true,
});
// 接入我们第 3 章写的 EventStream
const eventStream = streamCompletion(stream);
for await (const event of eventStream) {
controller.enqueue(event);
}
controller.close();
} catch (error: any) {
controller.enqueue({
type: "error" as const,
message: {
role: "assistant" as const,
content: [],
usage: { input: 0, output: 0, total: 0 },
stopReason: "error" as const,
errorMessage: error?.message ?? "未知错误",
timestamp: Date.now(),
},
});
controller.close();
}
},
});
},
};
}
第五步:用新架构重写入口
更新 src/index.ts:
typescript
import { createModels } from "./models.js";
import { deepseekProvider } from "./providers/deepseek.js";
import type { Context, Message, AssistantMessageEvent } from "./types.js";
// ─── 创建 Models 集合并注册 DeepSeek ───
const models = createModels();
models.setProvider(deepseekProvider());
// ─── 找一个模型 ───
const model = models.getModel("deepseek", "deepseek-v4-flash");
if (!model) {
console.error("❌ 找不到模型 deepseek/deepseek-v4-flash");
process.exit(1);
}
// ─── 构建上下文 ───
const context: Context = {
systemPrompt: "你是一个编程助手。回答要简洁,用中文。",
messages: [],
tools: [],
};
// 把用户本轮的话追加到上下文里
const userMessage: Message = {
role: "user",
content: "用一句话解释什么是 Provider 模式,然后再写两行代码示例",
timestamp: Date.now(),
};
context.messages.push(userMessage);
// ─── 发起流式对话 ───
console.log("🔌 正在通过 Provider 模式呼叫 DeepSeek...\n");
const stream = models.stream(model, context);
const reader = stream.getReader();
while (true) {
const { done, value: event } = await reader.read();
if (done) break;
switch (event.type) {
case "text_delta":
process.stdout.write(event.delta);
break;
case "text_end":
process.stdout.write("\n");
break;
case "done":
console.log("\n✅ 完成!(用了", event.message.usage.input, "+", event.message.usage.output, "tokens)");
break;
case "error":
console.log("\n❌ 错误:", event.message.errorMessage);
break;
}
}
为什么 Provider 模式这么重要
| 如果没有 Provider 模式 | 有了 Provider 模式 |
|---|---|
代码里到处散落 baseURL、apiKey 读取 |
统一在 Provider 工厂里管理 |
| 换 AI 公司要改 N 个文件 | 换 AI 公司只改注册那行 |
| 没法"跑一半换个更强的模型" | models.getModel(...) 随时切换 |
| 测试时用真金白银调 API | 可以注册一个 faux provider 返回假数据 |
| 新加 AI 公司要从头写一遍 HTTP 调用 | 新 Provider 实现同一个接口即可 |
🔧 运行验证
bash
npm start
你应该看到和之前完全一样的流式输出效果------但是背后已经多了一层 Provider 抽象。
试着在 src/index.ts 里打印所有注册的模型:
typescript
// 加在 models.setProvider(...) 后面
console.log("📋 已注册的模型:");
for (const m of models.getModels()) {
console.log(` ${m.provider}/${m.id} --- ${m.name} (context: ${m.contextWindow} tokens)`);
}
你应该看到:
📋 已注册的模型:
deepseek/deepseek-v4-flash --- DeepSeek V4 Flash (context: 1048576 tokens)
deepseek/deepseek-v4-pro --- DeepSeek V4 Pro (context: 1048576 tokens)
试试 V4 Pro(更强但更贵):
typescript
const model = models.getModel("deepseek", "deepseek-v4-pro")!;
V4 将"思考模式"做成了模型内开关------同一个模型既能快速答,也能深度推理,不再像旧版那样拆成 Chat 和 Reasoner 两个独立模型。
📚 这一章你学到了
| 概念 | 一句话理解 |
|---|---|
| Provider 接口 | "万能遥控器"------不管什么牌子的 AI,都实现同一套按钮 |
| Models 集合 | AI 公司的"黄页"------查公司、查模型、统一呼叫 |
| Auth resolver | 从环境变量里自动找密钥的函数------不用每个请求手动传 |
| Provider 工厂函数 | 每个 AI 公司写一个函数,返回实现了 Provider 接口的对象 |
createErrorStream() |
把错误也包装成流------调用方不需要区分"正常流"和"报错" |
🔗 对应的 Pi 源码
packages/ai/src/models.ts:32-72---Provider接口packages/ai/src/models.ts:79-128---Models/MutableModels接口packages/ai/src/models.ts:142-293---ModelsImpl实现packages/ai/src/providers/openai.ts--- OpenAI Provider 工厂(我们写的是 DeepSeek 版本)packages/ai/src/auth/resolve.ts--- Auth resolver 实现
下一步
Part 1 完成!你现在有了一个可以流式对话、Provider 抽象的 LLM 通信层。
但这还只是一问一答------AI 不会"做"事情。
Part 2 开始,我们给 AI 装上大脑 ------让它能多轮思考、调用工具、管理状态。
我们将实现 Pi 最核心的部分:Agent 循环。
下一章:Agent 的日记本 →