第 18 章 · 上下文序列化与浏览器使用
本章目标:掌握 Context 的 JSON 序列化/反序列化实现聊天记录持久化与服务间迁移,了解在浏览器环境运行 pi-ai 的方式、限制与安全边界。
18.1 Context 就是纯 JSON
pi-ai 的 Context 对象(systemPrompt + messages)是普通可 JSON 序列化的结构——不需要任何自定义编解码器:
const context: Context = {
systemPrompt: 'You are a helpful assistant.',
messages: [
{ role: 'user', content: 'What is TypeScript?', timestamp: Date.now() },
],
};
const model = models.getModel('openai', 'gpt-4o-mini')!;
const response = await models.complete(model, context);
context.messages.push(response);
// 一行序列化整个上下文
const serialized = JSON.stringify(context);
// 存数据库、localStorage、文件……随你便
localStorage.setItem('conversation', serialized);18.2 反序列化并继续对话
恢复同样简单——JSON.parse 回来就能直接用,甚至可以换一个模型继续:
// 之后:反序列化并接着聊
const restored: Context = JSON.parse(localStorage.getItem('conversation')!);
restored.messages.push({
role: 'user',
content: 'Tell me more about its type system',
timestamp: Date.now(),
});
// 换个模型继续,上下文无缝衔接
const newModel = models.getModel('anthropic', 'claude-3-5-haiku-20241022')!;
const continuation = await models.complete(newModel, restored);这组能力组合出的典型产品功能:会话分享链接(把序列化串放进 URL)、客服工单转移(A 客服的上下文交给 B)、离线草稿箱。
18.3 浏览器端运行
pi-ai 支持浏览器环境。核心入口与 Provider 工厂都是无副作用的,可以干净地打包进前端 bundle:
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-3-5-haiku-20241022')!;
const response = await models.complete(model, {
messages: [{ role: 'user', content: 'Hello!', timestamp: Date.now() }],
}, {
apiKey: 'your-api-key', // 浏览器没有环境变量,显式传 key
});浏览器与 Node 的关键差异:
| 能力 | Node | 浏览器 |
|---|---|---|
| 环境变量解析 API Key | ✅ | ❌ 需显式传 key 或注入 CredentialStore |
| Amazon Bedrock (converse-stream) | ✅ | ❌ 运行时报错 |
| OAuth 登录流程 | ✅ | ❌ Node-only(惰性加载不污染 bundle) |
CredentialStore 注入
文档建议:pass API keys explicitly — or inject a CredentialStore (e.g. localStorage-backed)——可以实现一个基于 localStorage 的凭据存储注入进去,让 Provider 认证自动从中解析。
18.4 安全警告:前端不放生产 Key
官方原话非常直白:
Security Warning: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys.
正确姿势是后端代理:浏览器 → 你的服务器(持有 key)→ LLM。前端直连只适合内部工具或 demo。
// 生产架构推荐:前端调自家后端,key 留在服务端
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ messages: context.messages }),
});18.5 打包与 Tree-shaking
两个打包相关的实用结论:
- OAuth 惰性加载——注册支持 OAuth 的 Provider 不会把 Node-only 代码拖进浏览器包;只有真正执行登录才会加载;
- 按需引入 Provider——从
@earendil-works/pi-ai/providers/xxx子路径导入,未引用的 Provider 实现会被 tree-shaking 掉,显著减小产物体积。
// 只导入需要的 Provider,其余被 tree-shake 掉
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';本章小结
- Context 是纯 JSON 结构,
JSON.stringify/parse即完成持久化与恢复; - 序列化能力支撑会话分享、工单转移等典型产品功能;
- 浏览器可用但有三点限制:无环境变量、Bedrock 不可用、OAuth 仅 Node;
- 前端暴露 API Key 极其危险,生产必须走后端代理;
- Provider 从子路径按需导入 + OAuth 惰性加载,bundle 保持干净。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 持久化一个 pi-ai 会话上下文的标准做法是?
2. 浏览器环境中获取 API 认证的正确说法是?
3. 关于在生产前端代码中直接放 API Key,官方的态度是?
4. 把支持 OAuth 的 Provider 注册进浏览器应用,会导致什么?
🛠️ 动手实践
- 实现"导出对话为 .json 文件 / 导入恢复"功能,做成第 19 章 CLI 的子命令。
- 用 Vite 搭建最小 React 页面,浏览器端跑通一次 complete 调用(demo key)。
- 写一个后端代理路由(Express):接收前端消息数组,服务端注入 key 转发 pi-ai。
掌握了全部积木,下一章开始拼装真实应用——迷你编码助手。