Skip to content

Commit 752cfcc

Browse files
Albert-PZYclaude
andcommitted
docs: 新增第10-13章(Provider与认证/Session树/Compaction/SDK)并优化阅读页
- 新增4章深入篇, 术语表移至第14章, 全景导读校准到15章 - 前端: 侧边栏收窄居中修正、UI柔化、暗色降刺眼、图片lightbox缩放 - 修复Markdown渲染: 表格内|转义、CJK加粗紧贴行内代码导致的**泄漏 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 59d1dcc commit 752cfcc

9 files changed

Lines changed: 1437 additions & 54 deletions

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
Pi 是 Mario Zechner(libGDX 作者)开源的 TypeScript AI Agent 框架,核心理念是 **LLM + Tools + A Loop**——用最少的代码实现最大的灵活性,`agent-core` 仅约 1500 行。
1010

11-
本文档从源码出发,用 11 个章节讲清 Pi 的完整架构,目标是**两三天内建立完整心智模型**:不只讲"怎么用",更讲"为什么这么设计"。
11+
本文档从源码出发,用 15 个章节讲清 Pi 的完整架构,目标是**两三天内建立完整心智模型**:不只讲"怎么用",更讲"为什么这么设计"。
1212

1313
## 章节目录
1414

@@ -24,7 +24,11 @@ Pi 是 Mario Zechner(libGDX 作者)开源的 TypeScript AI Agent 框架,
2424
| [07](docs/07-context-engineering.md) | 上下文工程 | Compaction 压缩、分支摘要、动态 System Prompt |
2525
| [08](docs/08-session-management.md) | 会话管理 | Session Tree、JSONL 存储、Fork 分叉 |
2626
| [09](docs/09-extension-system.md) | 扩展系统 | Skills / Extensions / Prompt Templates |
27-
| [10](docs/10-glossary.md) | 术语表 | 60+ 术语按逻辑分组速查 |
27+
| [10](docs/10-provider-and-auth.md) | Provider 与认证 | 认证优先级、CredentialStore、OAuth 刷新、跨厂商 handoff |
28+
| [11](docs/11-session-tree.md) | Session 树与上下文构建 | Entry vs Message、`buildContextEntries` / `buildSessionContext`、分支操作 |
29+
| [12](docs/12-compaction-internals.md) | Compaction 内部机制 | 切点规则、Split Turn、检查点、结构化摘要 |
30+
| [13](docs/13-sdk-integration.md) | SDK 与嵌入集成 | `createAgentSession``AgentSessionRuntime`、三种 Run Mode |
31+
| [14](docs/14-glossary.md) | 术语表 | 60+ 术语按逻辑分组速查 |
2832

2933
每章包含源码示例、PlantUML 架构图、检查清单,章节之间环环相扣。
3034

docs/00-overview.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,35 @@ note right: Agent 的"历史"
161161
:第9章 扩展系统;
162162
note right: Agent 的"成长"
163163
164-
:第10章 术语表;
164+
:第10章 Provider 与认证;
165+
note right: 深入:认证怎么解析
166+
167+
fork
168+
:第11章 Session 树与上下文构建;
169+
note right: 深入第8章
170+
fork again
171+
:第12章 Compaction 内部机制;
172+
note right: 深入第7章
173+
end fork
174+
175+
:第13章 SDK 与嵌入集成;
176+
note right: 把 Pi 当库用
177+
178+
:第14章 术语表;
165179
note right: 速查手册
166180
167181
stop
168182
@enduml
169183
```
170184

185+
前 9 章是**主干**,建立从抽象层到扩展系统的完整认知;第 10–13 章是**深入篇**,往主干的关键节点再钻一层(认证、会话树、压缩、嵌入集成),可按需选读。
186+
171187
**建议学习方式**
172188

173189
1. **第一天**:读完第 0-3 章,建立全局认知
174190
2. **第二天**:读完第 4-6 章,理解核心机制
175191
3. **第三天**:读完第 7-9 章,掌握高级特性
192+
4. **进阶**:按兴趣挑第 10-13 章深入;第 14 章术语表随时速查
176193

177194
每章末尾有"检查清单",确认自己理解后再进入下一章。
178195

docs/09-extension-system.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,5 +332,5 @@ note bottom of MCP: Pi 没有内置 MCP\n但扩展可以桥接
332332
---
333333

334334
**上一章**[第八章 · 会话管理](08-session-management.md)
335-
**下一章**[第十章 · 术语表](10-glossary.md)所有关键术语速查
335+
**下一章**[第十章 · Provider 与认证](10-provider-and-auth.md)认证如何被解析
336336

docs/10-provider-and-auth.md

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
# 第十章 · Provider 与认证
2+
3+
> 第二章讲了 pi-ai 如何用 `stream()` 统一 30+ 厂商。这一章往下钻一层:
4+
> **一个请求的认证是怎么被解析出来的**——从环境变量、凭据存储,到 OAuth 自动刷新。
5+
6+
---
7+
8+
## 10.1 Provider 是运行时的最小单元
9+
10+
第二章把注意力放在 `stream()``Model` 上。但真正**持有状态**的是 Provider:
11+
12+
```plantuml
13+
@startuml
14+
skinparam backgroundColor transparent
15+
16+
rectangle "Models 集合" as M #FFF3E0 {
17+
rectangle "anthropicProvider" as P1 #E8F5E9
18+
rectangle "openaiProvider" as P2 #E3F2FD
19+
rectangle "openrouterProvider" as P3 #F3E5F5
20+
}
21+
22+
note bottom of P1
23+
每个 Provider 自己拥有:
24+
· 模型目录 (catalog)
25+
· 认证 (API key / OAuth)
26+
· stream 行为
27+
end note
28+
29+
M --> P1
30+
M --> P2
31+
M --> P3
32+
33+
@enduml
34+
```
35+
36+
一句话区分:
37+
38+
- **Provider** = 运行时单元,拥有目录、认证、流式行为
39+
- **Models** = 集合,把请求路由到"拥有该模型"的 Provider
40+
- **API 实现** = 底层线路协议(`anthropic-messages` / `openai-responses` / `openai-completions`),被多个 Provider 共享
41+
42+
所以 xAI、Groq、Cerebras、OpenRouter 等大多复用 `openai-completions` 这一套 API 实现,差异只在 Provider 层的认证和 baseUrl。
43+
44+
### 构建一个 Models 集合
45+
46+
```typescript
47+
import { createModels } from '@earendil-works/pi-ai';
48+
import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
49+
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
50+
51+
const models = createModels();
52+
models.setProvider(anthropicProvider());
53+
models.setProvider(openaiProvider());
54+
55+
// 或者:一次注册所有内置 Provider(体积大,按需选择)
56+
// import { builtinModels } from '@earendil-works/pi-ai/providers/all';
57+
// const models = builtinModels();
58+
```
59+
60+
按需注册(tree-shaking 友好)还是全量注册,是**包体积****便利性**的取舍——见第二章 2.3 的懒加载策略。
61+
62+
## 10.2 认证解析的优先级
63+
64+
当你调用 `models.stream()`,集合会通过"拥有该模型的 Provider"解析认证,再合并进请求。**显式传入的值永远最优先**
65+
66+
```plantuml
67+
@startuml
68+
skinparam backgroundColor transparent
69+
skinparam ActivityBackgroundColor #f8f9fa
70+
skinparam ActivityBorderColor #dee2e6
71+
72+
start
73+
:models.stream(model, ctx, options);
74+
75+
if (options.apiKey 显式传入?) then (yes)
76+
:直接使用显式 key;
77+
note right: 显式值胜过一切
78+
else (no)
79+
if (CredentialStore 有存储凭据?) then (yes)
80+
:使用存储凭据;
81+
note right
82+
存储凭据"拥有"该 Provider
83+
有存储就不再看环境变量
84+
end note
85+
else (no)
86+
if (环境变量已设置?) then (yes)
87+
:使用环境变量;
88+
else (no)
89+
:解析失败 → stream error;
90+
endif
91+
endif
92+
endif
93+
94+
:合并 headers / baseUrl;
95+
:发起请求;
96+
stop
97+
@enduml
98+
```
99+
100+
关键规则:**存储凭据一旦存在,就"独占"该 Provider**。环境变量只在没有存储凭据时才被查询;OAuth 刷新失败绝不会静默回退到环境变量的 key——这是防止"看似成功、实则用错身份"的安全设计。
101+
102+
### 不发请求也能查认证
103+
104+
`getAuth()` 让你在不真正调用 LLM 的情况下检查解析结果:
105+
106+
```typescript
107+
// 传 Provider ID:Provider 级认证
108+
const providerAuth = await models.getAuth(model.provider);
109+
110+
// 传 model:额外并入该模型的静态 model.headers
111+
const modelAuth = await models.getAuth(model);
112+
113+
if (modelAuth) {
114+
console.log(`configured via ${modelAuth.source}`);
115+
// e.g. "ANTHROPIC_API_KEY" / "OAuth" / "stored credential"
116+
console.log(modelAuth.auth.headers);
117+
} else {
118+
console.log('not configured');
119+
}
120+
```
121+
122+
两种重载都会解析凭据、必要时刷新过期 OAuth,可能返回 `apiKey` / `headers` / `baseUrl`。未配置的 Provider 解析为 `undefined`;真正坏掉时抛 `ModelsError``"oauth"`=刷新失败但保留凭据以便重登,`"auth"`=key 解析或存储故障)。请求路径会把同样的失败作为 stream error 暴露。
123+
124+
## 10.3 环境变量:最省事的入口
125+
126+
Node 环境下,内置 Provider 会自动读取约定的环境变量(浏览器里没有环境变量,需显式传 `apiKey`):
127+
128+
| Provider | 环境变量 |
129+
|----------|---------|
130+
| OpenAI | `OPENAI_API_KEY` |
131+
| Anthropic | `ANTHROPIC_API_KEY``ANTHROPIC_OAUTH_TOKEN` |
132+
| Google | `GEMINI_API_KEY` |
133+
| DeepSeek | `DEEPSEEK_API_KEY` |
134+
| xAI | `XAI_API_KEY` |
135+
| Groq | `GROQ_API_KEY` |
136+
| OpenRouter | `OPENROUTER_API_KEY` |
137+
| Mistral | `MISTRAL_API_KEY` |
138+
139+
> 完整表格见 [pi-ai README](https://github.com/earendil-works/pi/blob/main/packages/ai/README.md#environment-variables)。Amazon Bedrock 解析环境里的 AWS 凭据链(`AWS_PROFILE`、access key、`AWS_BEARER_TOKEN_BEDROCK` 等);Vertex AI 解析显式 key 或 gcloud ADC + 项目/区域。
140+
141+
## 10.4 CredentialStore:凭据存储
142+
143+
交互式输入的 API key、OAuth token 都存在 `CredentialStore` 里——每个 Provider 一条带类型标签的凭据。pi-ai 默认给一个内存实现,应用注入持久化实现:
144+
145+
```typescript
146+
import { createModels, type CredentialStore } from '@earendil-works/pi-ai';
147+
148+
const models = createModels({ credentials: myFileBackedStore });
149+
```
150+
151+
接口很小,只有五个动作:
152+
153+
```typescript
154+
interface CredentialStore {
155+
read(providerId): Promise<Credential | undefined>;
156+
list(): Promise<Array<{ providerId; type }>>; // 只返回非敏感元数据
157+
modify(providerId, fn): Promise<void>; // 唯一写入路径
158+
delete(providerId): Promise<void>;
159+
}
160+
```
161+
162+
三个设计要点:
163+
164+
1. **`modify` 是唯一写入路径**——它是一次串行化的 read-modify-write。OAuth token 刷新就跑在 `modify` 里,所以并发请求、多个进程都不会重复刷新同一个已轮换的 token。
165+
2. **`list()` 不得解析 secret**,也不能执行配置的 key 命令——枚举必须廉价且安全。
166+
3. API key 凭据用和 pi 的 `auth.json` 相同的判别式,可携带 Provider 作用域的 env/config 值(如 `CLOUDFLARE_ACCOUNT_ID`)。
167+
168+
## 10.5 OAuth:登录与自动刷新
169+
170+
部分 Provider 支持 OAuth,而非静态 API key:
171+
172+
- **Anthropic**(Claude Pro/Max 订阅)
173+
- **OpenAI Codex**(ChatGPT Plus/Pro 订阅)
174+
- **GitHub Copilot**(Copilot 订阅)
175+
- **OpenRouter**(PKCE 流程,铸造一个用户可控的 API key)
176+
177+
每个这样的 Provider 在 `provider.auth.oauth` 上带一个 `OAuthAuth`,有三个操作:
178+
179+
```plantuml
180+
@startuml
181+
skinparam backgroundColor transparent
182+
183+
rectangle "OAuthAuth" {
184+
rectangle "login(interaction)" as L #E8F5E9
185+
rectangle "refresh(credential)" as R #E3F2FD
186+
rectangle "toAuth(credential)" as T #FFF3E0
187+
}
188+
189+
note bottom of L: 用中立的 prompt/notify 协议\n驱动登录,返回凭据
190+
note bottom of R: 刷新即将过期的凭据\n(OpenRouter 是 no-op)
191+
note bottom of T: 从凭据派生请求认证\n(Copilot 的 baseUrl 来自这里)
192+
193+
@enduml
194+
```
195+
196+
**刷新是自动的**`models.getAuth(providerId)` 和请求路径都会在凭据存储的锁内刷新过期 token,所以并发请求、多进程都不会重复刷新。OpenRouter 因为拿到的是永久 key,其 refresh 是空操作。
197+
198+
驱动登录的入口是 `models.login()`
199+
200+
```typescript
201+
const models = createModels({ credentials: myStore }); // 持久化 store
202+
models.setProvider(anthropicProvider());
203+
204+
await models.login('anthropic', 'oauth', {
205+
prompt: async (p) => {
206+
// p.type: 'text' | 'secret' | 'select' | 'manual_code'
207+
return await askUser(p.message);
208+
},
209+
notify: (event) => {
210+
if (event.type === 'auth_url') console.log(`打开: ${event.url}`);
211+
if (event.type === 'device_code')
212+
console.log(`Code: ${event.userCode} at ${event.verificationUri}`);
213+
},
214+
});
215+
216+
// 之后请求自动解析并刷新 token
217+
await models.complete(models.getModel('anthropic', 'claude-sonnet-4-5')!, ctx);
218+
219+
// 登出
220+
await models.logout('anthropic');
221+
```
222+
223+
CLI 里最快的登录方式:
224+
225+
```bash
226+
npx @earendil-works/pi-ai login # 交互式选择 Provider
227+
npx @earendil-works/pi-ai login anthropic # 指定 Provider
228+
```
229+
230+
凭据保存到当前目录的 `auth.json`
231+
232+
## 10.6 跨厂商 Handoff
233+
234+
Pi 支持在**同一段对话中途切换 Provider**,同时保留上下文——包括 thinking block、工具调用、工具结果。当一个 Provider 的消息发给另一个 Provider 时,库会自动做兼容转换:
235+
236+
```plantuml
237+
@startuml
238+
skinparam backgroundColor transparent
239+
240+
participant "用户" as U
241+
participant "Claude\n(anthropic)" as C
242+
participant "GPT-5\n(openai)" as G
243+
participant "Gemini\n(google)" as Ge
244+
245+
U -> C: "25 * 18 = ?"
246+
C --> U: [thinking] + "450"
247+
U -> G: "算得对吗?"
248+
note right of G
249+
Claude 的 thinking 被转为
250+
<thinking> 标签文本
251+
end note
252+
G --> U: "对"
253+
U -> Ge: "原题是什么?"
254+
Ge --> U: "25 * 18"
255+
256+
@enduml
257+
```
258+
259+
转换规则:
260+
261+
| 消息类型 | 处理方式 |
262+
|---------|---------|
263+
| user / toolResult 消息 | 原样透传 |
264+
| 同 Provider/API 的 assistant 消息 | 原样保留 |
265+
| **不同 Provider** 的 assistant 消息 | thinking block 转为 `<thinking>` 标签文本 |
266+
| 工具调用与普通文本 | 原样保留 |
267+
268+
这让"先用快模型、复杂推理再切强模型"或"在 Provider 故障时切换"成为可能。所有 Provider 都能消费其他 Provider 的消息,包括带部分内容的 aborted 消息。
269+
270+
## 10.7 自定义 Provider
271+
272+
本地推理服务器(Ollama、vLLM、LM Studio)或任何 OpenAI/Anthropic 兼容端点,用 `createProvider()` 接入:
273+
274+
```typescript
275+
import { createModels, createProvider, envApiKeyAuth } from '@earendil-works/pi-ai';
276+
import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy';
277+
278+
const ollama = createProvider({
279+
id: 'ollama',
280+
name: 'Ollama',
281+
baseUrl: 'http://localhost:11434/v1',
282+
// 无 key 的本地服务:resolve 返回空认证即可
283+
auth: { apiKey: { name: 'Ollama', resolve: async () => ({ auth: {} }) } },
284+
models: [ollamaModel],
285+
api: openAICompletionsApi(),
286+
});
287+
288+
const models = createModels();
289+
models.setProvider(ollama);
290+
```
291+
292+
有真实 key 的 Provider 用 `envApiKeyAuth(displayName, envVars)` 得到标准行为(存储凭据优先,然后第一个命中的环境变量):
293+
294+
```typescript
295+
const proxy = createProvider({
296+
id: 'my-proxy',
297+
auth: { apiKey: envApiKeyAuth('My proxy API key', ['MY_PROXY_API_KEY']) },
298+
models: [/* ... */],
299+
api: openAICompletionsApi(),
300+
});
301+
```
302+
303+
### compat:抹平兼容差异
304+
305+
`openai-completions` 被很多 Provider 实现,细节各有不同。默认会按 baseUrl 自动检测;未知端点可用 `compat` 字段覆盖:
306+
307+
```typescript
308+
interface OpenAICompletionsCompat {
309+
supportsStore?: boolean; // 是否支持 store 字段
310+
supportsDeveloperRole?: boolean; // developer 角色 vs system
311+
supportsReasoningEffort?: boolean; // reasoning_effort
312+
maxTokensField?: 'max_completion_tokens' | 'max_tokens';
313+
thinkingFormat?: 'openai' | 'deepseek' | 'qwen' | /* ... */;
314+
// ...更多
315+
}
316+
```
317+
318+
常见场景:Ollama、vLLM、SGLang 等不认识 reasoning 模型用的 `developer` 角色,就设 `compat.supportsDeveloperRole: false`,让 system prompt 作为 `system` 消息发送。
319+
320+
## 10.8 本章检查清单
321+
322+
- [ ] 能区分 Provider、Models、API 实现三者的职责
323+
- [ ] 说得出认证解析的优先级:显式 > 存储凭据 > 环境变量
324+
- [ ] 理解"存储凭据独占 Provider"这条安全规则
325+
- [ ] 知道 `modify` 为什么是 CredentialStore 唯一写入路径
326+
- [ ] 理解 OAuth 自动刷新如何避免并发重复刷新
327+
- [ ] 知道跨厂商 handoff 时 thinking block 如何被转换
328+
329+
---
330+
331+
**上一章**[第九章 · 扩展系统](09-extension-system.md)
332+
**下一章**[第十一章 · Session 树与上下文构建](11-session-tree.md) — 对话历史如何组织成树

0 commit comments

Comments
 (0)