Skip to content

第 2 章 · pi-ai 快速入门:统一 LLM API

本章目标:掌握 pi-ai 的核心抽象 Models 集合,学会用 createModels() / setProvider() 注册厂商,并用 streamSimple 发起第一次流式调用。

2.1 核心心智模型:Models 集合

pi-ai 的一切都围绕一个概念:Models 集合。它是一个「路由器」——持有若干 Provider,每个请求根据模型 ID 被转发给拥有该模型的 Provider:

text
models.complete(model, context)


┌──────────────────┐     ┌──────────────────┐
│ anthropicProvider│     │  openaiProvider  │   …其他已注册 Provider
│  (认证+目录+流式) │     │  (认证+目录+流式)  │
└──────────────────┘     └──────────────────┘

Provider 是运行时单元,它负责三件事:模型目录(有哪些模型)、认证(API Key 如何解析)、流式行为(如何调 wire API)。

2.2 创建集合的两种方式

typescript
// 方式一:按需注册(推荐生产使用,bundle 更小)
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";

const models = createModels();
// 每个 factory 子路径只引入该厂商的目录与懒加载 SDK
models.setProvider(anthropicProvider());
models.setProvider(deepseekProvider());

// 方式二:一键注册全部内置厂商(原型开发最省事)
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
const all = builtinModels(); // 等价于把所有 factory 都 setProvider 一遍

bundle 体积

providers/all 会拉起所有内置目录与 SDK 包装。对体积敏感的应用(尤其浏览器端)请用方式一按需注册。

2.3 streamSimple:最简单的流式调用

streamSimple 提供跨提供商统一的简化选项(如 reasoning: 'medium'),是 Agent 场景的默认选择:

typescript
// 完整的最小流式示例
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";

const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6")!;

// Context 是纯数据:系统提示词 + 消息数组 + 工具列表
const context = {
  systemPrompt: "你是一个简洁的助手。",
  messages: [
    // 每条消息都必须带 timestamp(毫秒)
    { role: "user", content: "解释什么是闭包", timestamp: Date.now() },
  ],
};

// 发起流式请求;认证由 Provider 自动从 ANTHROPIC_API_KEY 解析
const stream = models.streamSimple(model, context);

for await (const event of stream) {
  switch (event.type) {
    case "text_delta":
      // 增量文本:直接写入终端实现打字机效果
      process.stdout.write(event.delta);
      break;
    case "thinking_delta":
      // 推理型模型的思考增量(若开启 reasoning)
      process.stdout.write(`🤔 ${event.delta}`);
      break;
    case "done":
      // 结束原因:stop / length / toolUse / error / aborted
      console.log("\n[完成]", event.reason);
      break;
  }
}

// result() 返回最终完整消息(含 usage 计费信息)
const final = await stream.result();
console.log(`输入 ${final.usage.input} tokens,输出 ${final.usage.output} tokens`);
console.log(`花费 $${final.usage.cost.total.toFixed(4)}`);

2.4 completeSimple:不关心流式时

如果只是要最终答案(例如批处理任务),用 completeSimple 一行搞定:

typescript
// 非流式:等待完整响应再返回
const response = await models.completeSimple(model, context);

// 响应的 content 是内容块数组:text / thinking / toolCall
for (const block of response.content) {
  if (block.type === "text") console.log(block.text);
}
API适用场景返回
streamSimpleUI 实时输出、需要观察中间事件异步事件迭代器 + .result()
completeSimple后台任务、简单问答最终 AssistantMessage

2.5 统一接口的意义

同一份业务代码,切换模型只需改一行查找参数:

typescript
// 同一个 context 可以喂给任何已注册厂商的模型
const gpt = all.getModel("openai", "gpt-5-mini")!;
const ds = all.getModel("deepseek", "deepseek-chat")!;

// Anthropic 的回答
await models.completeSimple(model, context);
// DeepSeek 的回答 —— 业务代码零改动
await models.completeSimple(ds, context);

消息格式、工具协议、错误语义全部由 pi-ai 抹平。这就是第 16 章「跨提供商 Handoffs」能无缝工作的根基。

2.6 本章小结

  • Models 集合是路由器:按需 setProvider() 或用 builtinModels() 全量注册;
  • streamSimple 用统一选项发起流式调用,text_delta 是增量文本事件;
  • completeSimple 用于不需要流的场景;
  • 所有响应统一携带 usage(token 数与成本),换模型不改业务代码。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Provider 在 pi-ai 中负责哪三件事?

2. `streamSimple` 与 `stream` 的区别是什么?

3. 为什么生产环境推荐用 provider factory 按需注册而不是 providers/all?

4. Context 对象中每条消息都必须携带什么字段?

🛠️ 动手实践

  1. 分别用 streamSimplecompleteSimple 问同一个问题,对比终端输出形态与耗时感受。
  2. 注册两个不同的 Provider(如 Anthropic + DeepSeek),用同一个 context 各问一次「1+1」,验证业务代码零改动。
  3. 打印 finalMessage.usage 全部字段,算出这次对话每百万 token 的实际成本。

掌握了统一 API 后,进入第 3 章深挖模型目录与 Provider 查询。