第 6 章 · LLM API 基础与 pi-ai
6.1 消息协议:四种 role
所有主流 LLM 的 Chat API 都围绕一个「消息列表」工作。以 OpenAI 兼容格式为例:
type LLMMessage =
| { role: 'system'; content: string } // 设定人设与规则
| { role: 'user'; content: string } // 用户输入
| { role: 'assistant'; content: string; tool_calls?: ToolCall[] } // 模型回复(可带工具调用)
| { role: 'tool'; content: string; tool_call_id: string } // 工具执行结果
一轮完整交互的消息演变:
关键认知:LLM 是无状态的。所谓「多轮对话」,就是客户端每次都把完整历史发一遍。OpenBudy 的 agent-core/loop.ts 中 historyToLLMMessages 函数干的就是这件事——把本地存储的历史消息还原成标准格式(详见第 8 章)。
6.2 流式输出:chunk 是什么
非流式调用要等全文生成完才返回;流式(stream: true)则通过 SSE 逐块推送:
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: {"choices":[{"delta":{"content":"!"},finish_reason":"stop"}]}
data: [DONE]
每个 chunk 只携带增量(delta)。客户端把增量拼起来就是完整回答,同时可以边收边渲染——这就是打字机效果的来源。工具调用也是流式的:tool_calls 的函数名和参数 JSON 会被拆成多个片段,需要拼接。
6.3 为什么需要 pi-ai
直接用 fetch 调 OpenAI 兼容接口完全可行,但你会很快遇到一堆脏活:
- 智谱 GLM-4.7 / GLM-5.3 是推理模型,思考流的格式与普通模型不同
- 各厂商的 baseURL 拼接规则、认证方式、参数名有细微差异
- 流式解析(含 tool_call 分片拼接)每家都要重写一遍
@earendil-works/pi-ai 的解法是提供:
| 抽象 | 说明 |
|---|---|
Model | 一份模型定义:endpoint 类型、上下文长度、是否支持工具、推理模型兼容标记(如 thinkingFormat: "zai") |
| 模型目录 | 内置 zai / zai-coding-cn / deepseek / openai 等厂商的模型清单 |
streamSimple | 统一的流式调用入口,吐出标准化 chunk |
OpenBudy 用一个桥接层 agent-core/llm/pi-ai.ts 把它接进来,对外保持自己的 LLMMessage / LLMChunk 协议——loop.ts 完全不知道底层是 pi-ai。
6.4 精读:ModelConfig 与预设
OpenBudy 的用户配置模型(存于 ~/.openbudy/config.json,见第 4 章):
interface ModelConfig {
id: string // 如 'glm-4.5-flash'
name: string // 展示名
provider: string // 'zhipu' | 'deepseek' | 自定义
baseUrl: string // OpenAI 兼容端点
apiKey: string // 主进程持有,不进渲染进程
maxInputTokens: number
maxOutputTokens: number
supportsToolCalling: boolean
}
内置的智谱预设(节选):
agent-core/llm/pi-ai.ts(节选)
export const ZHIPU_PRESETS: ModelConfig[] = [
{
id: 'glm-4.5-flash',
name: 'GLM-4.5-Flash(免费)',
provider: 'zhipu',
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
apiKey: '',
maxInputTokens: 128000,
maxOutputTokens: 4096,
supportsToolCalling: true,
},
// glm-4.6 / glm-4.7 / glm-5.3 ...
]