Skip to content

Latest commit

 

History

History
255 lines (195 loc) · 6.94 KB

File metadata and controls

255 lines (195 loc) · 6.94 KB

第一章 · 三层架构

Pi 的核心设计决策:把 LLM 通信Agent 运行时产品交互 严格分离。
同一个底层引擎,可以长出完全不同形态的产品。


1.1 为什么要分层

一个 AI Agent 至少需要解决三个问题:

  1. 如何跟 LLM 对话?(调哪个 API、怎么传参、怎么解析流式输出)
  2. 如何循环执行?(调 LLM → 执行工具 → 把结果喂回去 → 再调 LLM)
  3. 如何跟用户交互?(终端 UI、Web UI、Slack Bot、API Server)

如果把这三件事混在一起,换个 LLM 提供商就要改 Agent 逻辑,加个 Web 界面就要重写循环引擎。Pi 的解法是垂直分层

@startuml
skinparam backgroundColor transparent
skinparam componentStyle rectangle

package "Layer 3 — 产品层" #E3F2FD {
  [pi-coding-agent] as L3
  note right of L3
    终端 CLI、扩展系统、
    技能系统、UI 交互
  end note
}

package "Layer 2 — Agent 运行时" #FFF3E0 {
  [pi-agent-core] as L2
  note right of L2
    Agent Loop、工具执行、
    状态管理、事件发射
  end note
}

package "Layer 1 — LLM 通信层" #E8F5E9 {
  [pi-ai] as L1
  note right of L1
    Provider 抽象、模型注册、
    流式输出、跨 API 兼容
  end note
}

L3 --> L2 : 调用
L2 --> L1 : 调用
L1 -[hidden]-> L2
L2 -[hidden]-> L3
@enduml

关键约束:每一层只依赖下面的层,绝不反向依赖

1.2 Layer 1 — pi-ai:LLM 通信层

packages/ai/ — 纯粹的 LLM 调用抽象,不知道"Agent"是什么。

职责

  • 定义统一的 ModelMessageContext 类型
  • 为 30+ Provider(OpenAI、Anthropic、Google、Bedrock…)提供统一接口
  • 处理流式输出,发射 AssistantMessageEvent
  • 自动处理跨 Provider 兼容问题(ID 长度、thinking block 格式等)

核心类型packages/ai/src/types.ts):

// 模型定义
interface Model<TApi extends Api> {
  id: string;          // 如 "claude-opus-4-6"
  name: string;        // 如 "Claude Opus 4.6"
  api: TApi;           // 如 "anthropic-messages"
  provider: string;    // 如 "anthropic"
  baseUrl: string;
  reasoning: boolean;  // 是否支持 thinking
  contextWindow: number;
  maxTokens: number;
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
}

// 统一消息格式
type Message = UserMessage | AssistantMessage | ToolResultMessage;

// LLM 调用上下文
interface Context {
  systemPrompt: string;
  messages: Message[];
  tools?: Tool[];
}

关键函数stream()

// 所有 Provider 都通过同一个接口调用
const response = await stream(model, context, options);

for await (const event of response) {
  // event.type: "start" | "text_delta" | "toolcall_start" | "done" | ...
}

1.3 Layer 2 — pi-agent-core:Agent 运行时

packages/agent/ — Agent 的引擎,不关心用什么 UI 展示。

职责

  • 实现 Agent Loop(双循环引擎)
  • 管理 Agent 状态(消息历史、工具列表、模型配置)
  • 执行工具调用(并行/顺序)
  • 发射生命周期事件
  • 提供 Steering(转向)和 Follow-up(追加)消息队列

核心类Agentpackages/agent/src/agent.ts):

const agent = new Agent({
  streamFn: stream,                    // 来自 pi-ai
  initialState: {
    systemPrompt: "You are a helpful assistant.",
    model: claudeOpus,
    tools: [readTool, writeTool, bashTool],
  },
});

// 订阅事件
agent.subscribe((event, signal) => {
  if (event.type === "message_update") {
    // 实时渲染 LLM 的流式输出
  }
  if (event.type === "tool_execution_end") {
    // 工具执行完毕
  }
});

// 发送提示
await agent.prompt("帮我读取 package.json 的内容");

// 中途注入消息(Steering)
agent.steer({ role: "user", content: [{ type: "text", text: "停下来" }], timestamp: Date.now() });

核心文件结构

文件 职责
agent.ts Agent 类:状态管理 + 事件分发
agent-loop.ts runLoop():双循环引擎
types.ts AgentMessageAgentToolAgentEvent 等核心类型
stream-fn.ts 默认的 LLM 调用函数

1.4 Layer 3 — pi-coding-agent:产品层

packages/coding-agent/ — 面向开发者的编码 Agent CLI,也是 Pi 的旗舰产品。

职责

  • 终端 UI(基于 pi-tui)
  • 扩展系统(Extensions、Skills、Prompt Templates)
  • 内置工具(read、write、edit、bash、grep、find、ls)
  • 认证与 Provider 管理
  • 会话持久化(JSONL)
  • 上下文压缩(Compaction)

产品层不等于框架。coding-agent 是建立在 pi-ai + agent-core 之上的一个应用。你完全可以用同样的底层构建自己的 Agent。

1.5 验证分层的案例:OpenClaw

OpenClaw 是一个支持 46 个消息渠道(WhatsApp、Telegram、Discord 等)的多渠道 AI 助手。它直接复用了 Pi 的前两层:

@startuml
skinparam backgroundColor transparent

package "Pi Coding Agent" {
  [CLI + TUI] as cli
}

package "OpenClaw" {
  [Gateway\n(46 channels)] as gw
}

package "pi-agent-core" #FFF3E0 {
  [Agent Loop] as loop
}

package "pi-ai" #E8F5E9 {
  [LLM API] as llm
}

cli --> loop
gw --> loop
loop --> llm
@enduml

这证明了分层设计的价值:

  • OpenClaw 没有修改 pi-ai 和 agent-core 的任何代码
  • 它只替换了产品层(CLI → Gateway)
  • 同一个 Agent 引擎,长出了两个完全不同的产品

1.6 三层之间的数据流

一次完整的用户请求,数据在三层之间的流动:

@startuml
skinparam backgroundColor transparent

actor User
participant "coding-agent\n(Layer 3)" as L3
participant "agent-core\n(Layer 2)" as L2
participant "pi-ai\n(Layer 1)" as L1
participant "LLM API" as API

User -> L3: 输入 "读取 foo.ts"
L3 -> L2: agent.prompt("读取 foo.ts")
L2 -> L2: 构建 AgentContext
L2 -> L2: convertToLlm(AgentMessage[] → Message[])
L2 -> L1: stream(model, context, options)
L1 -> API: HTTP POST /v1/messages
API --> L1: SSE stream
L1 --> L2: AssistantMessageEvent[]
L2 -> L2: 解析 toolCall: read({path: "foo.ts"})
L2 -> L3: beforeToolCall hook
L3 --> L2: {block: false}
L2 -> L2: 执行 readTool.execute()
L2 -> L3: afterToolCall hook
L2 -> L2: 工具结果 → ToolResultMessage
L2 -> L1: stream(model, context + toolResult, options)
L1 -> API: HTTP POST(带上工具结果)
API --> L1: SSE stream(最终回复)
L1 --> L2: AssistantMessage
L2 --> L3: agent_end 事件
L3 --> User: 渲染最终输出

@enduml

1.7 本章检查清单

  • 能说出 Pi 的三个层各自的职责
  • 理解"Layer 1 不知道 Agent 是什么"的含义
  • 理解为什么 OpenClaw 可以复用 Pi 的底层
  • 能画出一次请求在三层之间的完整数据流

上一章第零章 · 全景导读
下一章第二章 · LLM 抽象层 (pi-ai) — 如何用一套接口调 30+ 模型