第二章:核心类型系统 -- 对话的基本构建块
对应源文件:
packages/ai/src/types.ts
概述
类型系统是整个项目的"词汇表"。所有的 provider 适配器、agent 运行时、编码代理,都使用这套类型来描述"对话"这件事。把这些类型搞懂,后面的一切都顺理成章。
我们从最直观的概念开始,逐层深入。
1. 消息(Message)-- 对话的最小单元
一次 AI 对话本质上是一系列"消息"的来回。pi-ai 定义了三种消息角色:
UserMessage -- 用户说的话
{
role: "user",
content: "帮我写一个排序函数", // 可以是纯文本字符串
timestamp: 1714300000000 // 毫秒时间戳
}
content 也可以是一个数组,包含文本和图片的混合内容:
{
role: "user",
content: [
{ type: "text", text: "这张图片里有什么?" },
{ type: "image", data: "base64编码的图片数据...", mimeType: "image/png" }
],
timestamp: 1714300000000
}
AssistantMessage -- AI 助手的回复
这是三种消息中最复杂的一个,因为 AI 的回复可能包含多种内容:
{
role: "assistant",
content: [ // 内容块数组
{ type: "thinking", thinking: "让我先分析一下需求..." }, // 思考过程
{ type: "text", text: "这是一个快速排序实现:..." }, // 文本回复
{ type: "toolCall", id: "tc_1", name: "write", arguments: { path: "sort.ts", content: "..." } } // 工具调用
],
api: "anthropic-messages", // 使用的 API 协议
provider: "anthropic", // 服务商
model: "claude-sonnet-4", // 具体模型
usage: { // token 使用统计
input: 1200, // 输入 token 数
output: 800, // 输出 token 数
cacheRead: 0, // 从缓存读取的 token
cacheWrite: 0, // 写入缓存的 token
totalTokens: 2000,
cost: { input: 0.0036, output: 0.012, ... } // 费用计算
},
stopReason: "stop", // 停止原因
timestamp: 1714300005000
}
什么是 content 内容块
AI 的一次回复可能包含多个"块"(block),每个块有不同的类型:
| 块类型 | 名称 | 说明 |
|---|---|---|
TextContent | 文本 | AI 输出的正文,{ type: "text", text: "..." } |
ThinkingContent | 思考 | AI 的内部推理过程(不是所有模型都支持),{ type: "thinking", thinking: "..." } |
ToolCall | 工具调用 | AI 决定调用一个工具,{ type: "toolCall", id: "...", name: "read", arguments: { path: "foo.ts" } } |
一个 AssistantMessage 可以同时包含多个块。比如,AI 可能先思考,然后输出一段文字,接着调用一个工具:[thinking, text, toolCall]。
什么是 stopReason(停止原因)
LLM 不会无限生成内容,它会在某个时刻停下来。stopReason 告诉你它为什么停了:
| 值 | 含义 |
|---|---|
"stop" | 正常完成,AI 说完了 |
"length" | 达到了最大 token 限制,被截断了 |
"toolUse" | AI 想调用工具,需要你提供工具结果后继续 |
"error" | 出错了 |
"aborted" | 被用户主动取消了 |
ToolResultMessage -- 工具执行结果
当 AI 调用了一个工具后,你需要执行那个工具并把结果告诉 AI:
{
role: "toolResult",
toolCallId: "tc_1", // 对应哪个 toolCall 的 id
toolName: "read", // 工具名
content: [ // 结果内容(也可以包含图片)
{ type: "text", text: "文件内容:function sort() { ... }" }
],
isError: false, // 执行是否出错
timestamp: 1714300006000
}
完整对话示例
一次典型的对话流程如下:
Message 1: UserMessage "读一下 src/main.ts"
Message 2: AssistantMessage [toolCall: read({ path: "src/main.ts" })] stopReason: "toolUse"
Message 3: ToolResultMessage toolCallId: "tc_1", content: "文件内容..."
Message 4: AssistantMessage [text: "这个文件是入口文件,它做了以下事情..."] stopReason: "stop"
2. 上下文(Context)-- 发给 LLM 的完整信息
Context 是你发给 LLM 的所有信息的打包:
{
systemPrompt: "你是一个编码助手,可以读写文件...", // 系统提示词
messages: [ ... ], // 上面的消息数组
tools: [ ... ] // 可用工具列表
}
什么是 systemPrompt(系统提示词)
System prompt 是给 AI 的"角色设定"和"行为规则"。它告诉 AI "你是谁"、"你能做什么"、"有哪些规则要遵守"。用户看不到这个提示词,但它深刻影响 AI 的行为。
比如 pi 的默认系统提示会告诉 AI:你是一个编码助手,你有 read、write、edit、bash 这些工具可以用,你应该先读文件再修改...
3. 工具(Tool)-- 让 AI 能"做事"
纯文字聊天的 AI 只能"说",不能"做"。工具机制让 AI 可以调用你预定义的函数来执行实际操作。
工具的定义
{
name: "read", // 工具名称,AI 用这个名字来调用
description: "读取文件内容", // 描述,帮助 AI 理解何时该用这个工具
parameters: Type.Object({ // 参数的 schema(模式定义)
path: Type.String({ description: "文件路径" }),
startLine: Type.Optional(Type.Number()),
endLine: Type.Optional(Type.Number())
})
}
什么是 Schema
Schema 是"数据结构的描述"。它定义了数据应该长什么样。比如上面的 parameters schema 说的是:
- 必须是一个对象
- 必须有一个
path字段,类型是字符串 - 可选有
startLine和endLine字段,类型是数字
pi-ai 使用 TypeBox 库来定义 schema。TypeBox 的好处是:
- 在 TypeScript 中有完整的类型提示
- 可以自动验证 AI 返回的参数是否符合格式
- 可以序列化为 JSON(因为 schema 需要发送给 LLM)
工具调用的流程
1. 你在 Context.tools 中告诉 LLM 有哪些工具可用
2. LLM 分析用户请求,决定要调用哪个工具
3. LLM 返回一个 ToolCall 块:{ name: "read", arguments: { path: "foo.ts" } }
4. 你的代码执行这个工具,得到结果
5. 你把结果包装成 ToolResultMessage 放回 messages
6. 再次调用 LLM,让它根据工具结果继续回答
这就是所谓的 tool-call loop(工具调用循环),是 AI Agent 的核心机制。
4. 模型(Model)-- 描述一个具体的 AI 模型
每个可用的 AI 模型被描述为一个 Model 对象:
{
id: "claude-sonnet-4-20250514", // 模型的唯一标识
name: "Claude Sonnet 4", // 人类可读名称
api: "anthropic-messages", // 使用哪种 API 协议
provider: "anthropic", // 服务商名称
baseUrl: "https://api.anthropic.com", // API 地址
reasoning: true, // 是否支持推理/思考功能
input: ["text", "image"], // 支持的输入类型
cost: { // 定价(每百万 token 美元)
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 3.75
},
contextWindow: 200000, // 上下文窗口大小(token)
maxTokens: 16384 // 单次最大输出 token
}
api 和 provider 的区别
这是一个容易混淆的概念:
- Provider(服务商):谁提供这个模型。比如
"anthropic","openai","xai" - Api(API 协议):模型使用哪种通信协议。比如
"anthropic-messages","openai-completions"
它们不是一一对应的。很多服务商(xAI、Groq、Cerebras 等)虽然是不同的公司,但它们的 API 都兼容 OpenAI 的 openai-completions 协议。所以:
OpenAI → openai-responses (自家协议)
Anthropic → anthropic-messages (自家协议)
Google → google-generative-ai (自家协议)
xAI → openai-completions (兼容 OpenAI)
Groq → openai-completions (兼容 OpenAI)
Cerebras → openai-completions (兼容 OpenAI)
Mistral → mistral-conversations (自家协议)
这样设计的好处是:同一个 API 协议的适配器代码只需要写一次,所有使用该协议的 provider 都能复用。
什么是 Context Window(上下文窗口)
LLM 不能处理无限长的文本。每个模型有一个"上下文窗口"大小,通常用 token 数衡量。所有输入(system prompt + 历史消息 + 工具定义)加上输出,总和不能超过这个窗口。
Token 与自然语言的换算比例因 tokenizer(分词器)和语言而异,不同 provider 使用不同的 tokenizer,无法给出精确统一的数字。粗略参考:
- 英文:1 token 约 4 个字符,约 0.75 个单词。200,000 token 约 15 万个英文单词
- 中文:现代 tokenizer(如 OpenAI 的 cl100k_base、Anthropic 的 tokenizer)通常 1 个汉字消耗 1-2 个 token。200,000 token 约 10-15 万个汉字
注意:早期 tokenizer 对中文效率更低(1 个汉字可能消耗 2-4 个 token),而某些针对中文优化的模型(如 DeepSeek)可以做到接近 1:1。实际使用中应以各 provider 的 tokenizer 工具为准。
当对话太长超过窗口时,就需要"压缩"(compaction)-- 这是后面 coding-agent 包要解决的问题。
5. Usage(使用量统计)
每次 LLM 调用都会返回 token 使用量和费用:
usage: {
input: 1200, // 发送给 LLM 的 token 数
output: 800, // LLM 生成的 token 数
cacheRead: 5000, // 从 prompt cache 读取的 token(节省费用)
cacheWrite: 1200, // 写入 prompt cache 的 token
totalTokens: 8200, // 总计
cost: {
input: 0.0036, // 美元
output: 0.012,
cacheRead: 0.0015,
cacheWrite: 0.0045,
total: 0.0216
}
}
什么是 Token
Token 是 LLM 处理文本的基本单位。它不完全等于"字"或"词",而是一种中间粒度的分词单位。大致估算:
- 英文:1 个 token 约 4 个字符(约 3/4 个单词)
- 中文:1 个汉字通常消耗 1-2 个 token(因 tokenizer 而异,见上文说明)
LLM 的收费和能力限制都以 token 为单位。
什么是 Prompt Cache(提示缓存)
当你多次调用 LLM 且前半部分的内容相同时(比如 system prompt 和早期对话不变),服务商可以缓存这些内容,下次调用时直接复用,不需要重新处理。这样能显著降低费用和延迟。
cacheRead 表示复用了多少 token,cacheWrite 表示新写入缓存了多少 token。通常 cache 读取的费用比正常输入便宜很多。
6. StreamOptions -- 调用选项
调用 LLM 时可以传入的可选参数:
| 参数 | 类型 | 说明 |
|---|---|---|
temperature | number | 控制随机性。0 = 确定性输出,1 = 更有创造性 |
maxTokens | number | 最大输出 token 数 |
signal | AbortSignal | 用于取消请求(类似浏览器中取消 fetch) |
apiKey | string | API 密钥 |
cacheRetention | "none"|"short"|"long" | 缓存保留策略 |
sessionId | string | 会话 ID,用于 provider 的缓存路由 |
onPayload | function | 调试用回调,可以查看发给 provider 的原始请求 |
SimpleStreamOptions 在此基础上增加了 reasoning 字段,用统一的方式控制"思考力度":
{ reasoning: "medium" } // "minimal" | "low" | "medium" | "high" | "xhigh"
不同 provider 对"思考"的实现方式不同(OpenAI 用 reasoning_effort,Anthropic 用 thinking budget),但通过 SimpleStreamOptions 你只需要说"中等思考"就够了。
小结
这一章的核心概念用一句话概括:
一次 LLM 调用 = 把一个
Context(包含系统提示、消息历史、工具列表)发给一个Model,得到一个AssistantMessage(包含文本、思考、工具调用、使用量统计)。
Context ──────────────► Model ──────────────► AssistantMessage
(systemPrompt, (id, api, (content blocks,
messages, provider, usage,
tools) cost, ...) stopReason)
下一章我们会看这个调用过程是如何流式进行的。