Skip to content

Demo:最小 Faux Provider

这个 Demo 不依赖 pi 的任何安装,用工程化的目录结构复现 @pi/ai 的核心抽象:消息模型 + 事件流 + StreamFunction 契约 + Faux Provider

工程化结构

Demo 不再是一个单文件,而是仿照 pi 的包组织,分模块放在 pi-principles/

text
pi-principles/
├── package.json            # scripts:cli / chat / agent / app / repl / ai ...
├── tsconfig.json
├── src/                    # 各模块源码(复现 pi 各包)
│   ├── config.ts               # .env 配置加载(OPENAI_BASE_URL/KEY/MODEL)
│   ├── ai/                 #   ← 本 Demo 相关
│   │   ├── types.ts            # 消息模型(user/assistant/toolResult、Context、Tool)
│   │   ├── event-stream.ts     # EventStream + AssistantMessageEventStream
│   │   ├── stream-function.ts  # StreamFn 契约
│   │   ├── stream-resolver.ts  # 真实 OpenAI / 离线假模型自动选择
│   │   ├── faux-provider.ts    # 假模型:切片流式输出
│   │   ├── api/                # 协议实现(适配器)
│   │   │   ├── openai-responses.ts  # createOpenAIStreamFn(真实模型)
│   │   │   └── openai-messages.ts   # pi ⇄ OpenAI 双向转换
│   │   └── utils/sse.ts        # SSE 解析
│   ├── agent/ ✓
│   ├── tools/ ✓            # calculator / bash / read
│   ├── protocol/ ✓
│   ├── transport/ ✓
│   ├── tui/ ✓
│   └── cli/ ✓              # CLI:args/main/chat/agent/app/repl
└── play/                   # 模块化离线入口
    ├── ai.ts               #   ← 本 Demo 的入口
    ├── event-stream.ts / protocol.ts / transport.ts / tui.ts / mini-agent.ts

关键实现

消息模型(src/ai/types.ts

ts
export type Message =
	| { role: "user"; content: string | TextContent[]; timestamp: number }
	| { role: "assistant"; content: ContentBlock[]; stopReason: StopReason; timestamp: number }
	| { role: "toolResult"; toolCallId: string; toolName: string; content: TextContent[]; isError; timestamp };

事件流(src/ai/event-stream.ts

ts
export class EventStream<T, R> implements AsyncIterable<T> {
	// ...
	push(event: T): void;          // 生产者推送
	end(result?: R): void;         // 生产者结束
	result(): Promise<R>;          // 消费者等最终结果
	async *[Symbol.asyncIterator]() { ... }  // 消费者逐个消费
}

Faux Provider(src/ai/faux-provider.ts

ts
export function createFauxProvider(): FauxCore;
// FauxCore.stream(context, options) → AssistantMessageEventStream
// 按 2 个字符切成 text_delta 增量,模拟打字机;错误也编码进流,绝不 throw。

入口与运行

play/ai.ts 演示:消费事件流看到增量 → result() 拿最终消息 → 验证"中止也编码进流而非抛错"。

bash
cd pi-principles
bun play/ai.ts          # 或 npm run ai

输出:

当前累积: 假模
当前累积: 假模型:
...(逐字累积)
最终消息: 假模型:收到 1 条消息,系统提示「你是教学用假模型」
中止时通过 result() 拿到(而非抛错),stopReason = aborted

对应到真实源码

工程化 Demo真实 pi 源码
src/ai/event-stream.tspackages/ai/src/utils/event-stream.ts
src/ai/stream-function.tspackages/agent/src/types.ts:28
src/ai/faux-provider.tspackages/ai/src/providers/faux.ts

理解要点

  • 事件流"可迭代 + 可等待"for await 消费增量,result() 拿最终结果。
  • 契约"不 throw":中止也通过 error 事件结束,保证事件序列完整。
  • 工程化分文件后,每个类一个职责,和 pi 真实的包组织一致。

下一步:2.1 核心 Agent 循环