首页 / pi-agent 入门教程 / pi 的内部世界:架构概览

pi-agent 入门教程

pi 的内部世界:架构概览

本教程共 30 篇 · 第 26 篇 · 更新于 2026-08-10 · 约 20 分钟阅读

pi-agent架构monorepopi-aipi-agent-core源码pi-tuipi-coding-agent

本节目标:站在高处看清 pi 项目的内部结构——源码仓库里四个包各自解决什么问题、一个请求从你按下回车到模型返回答案经过了哪些步骤、以及 pi 和 OpenClaw 这些”宿主”之间的边界在哪。读完这章,你翻源码不会再迷路。

前面 24 章我们都在 pi 的”用户界面”上活动——怎么装、怎么配、用什么模式、怎么调工具、怎么压缩。现在换个视角:打开 pi 的源码仓库,里面到底有什么?

pi 是一个 monorepo——一个 Git 仓库里放了多个 npm 包,每个包有自己独立的 package.json、独立的导出、甚至独立发布。根目录用 npm workspaces 管理它们。

源码本章锚定 echovic 手册的 v0.83.0(commit 845d6ff1)。包结构跨小版本基本稳定,cli 入口和模式在 0.84.1 保持一致。


先看一眼代码长什么样

把 pi 的仓库克隆到本地,packages/ 目录下现在有五个子包:

packages/
├── ai/              pi-ai           模型抽象层
├── agent/           pi-agent-core   通用 Agent 引擎
├── tui/             pi-tui          终端界面库
├── coding-agent/    pi-coding-agent  CLI 产品(最厚)
└── orchestrator/    pi-orchestrator 多 Agent 编排(实验性)
Note

pi-orchestrator 是 v0.80.x 新增的实验性包,负责多 Agent 协调和 RPC 进程通信。它依赖 coding-agent,站在上面做编排。学习主线不看它,核心三件套是 ai / agent-core / coding-agent,外加一个辅助的 tui。

一眼看过去,包名很直白——“ai”管模型、“agent”管循环、“tui”管显示、“coding-agent”把它们拼成产品。这是直觉没错,但直觉有时候会骗人。


不是简单的”上层调下层”

打开 packages/coding-agent/package.jsondependencies 字段:

"dependencies": {
    "@earendil-works/pi-agent-core": "^0.80.2",
    "@earendil-works/pi-ai": "^0.80.2",
    "@earendil-works/pi-tui": "^0.80.2"
}

发现了什么?coding-agent 同时直接依赖了 ai 和 agent-core。如果你以为分层就是”顶层只能通过中间层间接用底层”(像网络协议栈那样),这里会让你愣一下。

跨层引用不是 bug

打开 packages/agent/src/types.ts,第一行:

import type { Message, Model, Tool, ImageContent } from "@earendil-works/pi-ai";

pi-agent-core 的类型定义里大量引用了 pi-ai 的基础类型。MessageModelTool——这些是整个系统的基础数据类型。就像一个项目里,User 类型定义在底层模块,上层模块到处引用它。

coding-agent 也一样。用户的截图到底是什么格式?答案是 pi-ai 里定义的 ImageContent 类型——所以 coding-agent 必须直接依赖 pi-ai。

真正的分层规则:依赖方向单向向上

看每个包的依赖方向:

pi-ai(底层)
  ↑         ↑
  │         │
  │    pi-agent-core(中间层)
  │         ↑
  │         │
  └─── pi-coding-agent(顶层)

箭头全部朝上。底层不知道上层的存在。 pi-ai 的代码里没有任何 import 指向 pi-agent-core 或 pi-coding-agent。pi-agent-core 也不会 import pi-coding-agent。

这就是分层的真正规则:不是限制引用层级,是控制依赖方向必须单向向上

那 pi-tui 呢?它在哪里?

pi-tui 的运行时依赖只有 marked(Markdown 渲染)和一个字符宽度计算库。它没有任何 pi-xxx 包的依赖——不依赖 pi-ai,不依赖 pi-agent-core。pi-coding-agent 依赖 pi-tui,把它当工具用。不存在循环依赖。


四个包,四个问题

现在从每个包自己的视角看它解决了什么问题。

pi-ai:只和模型说话

@earendil-works/pi-aipackages/ai/)只做一件事:用一套代码调用 30+ 种不同的 LLM。

它的 package.json 描述是:“Unified LLM API with automatic model discovery and provider configuration”

具体三件事:

  1. 定义统一类型。不管你用 OpenAI、Anthropic、Google 还是 DeepSeek,消息格式都一样——UserMessageAssistantMessageToolResultMessage。模型定义统一为 Model<TApi>

  2. 统一流式调用。所有 provider 的调用方式统一成 streamSimple() 函数,返回一个可以逐 token 读取的事件流。

  3. 适配 30+ 提供商。OpenAI、Claude、Gemini、DeepSeek、Groq、小米……每个 provider 一个适配器文件。

看它的导出就知道它有多”纯”:

// packages/ai/src/index.ts(节选)
export * from "./models.ts"       // 模型定义
export * from "./types.ts"        // 统一类型(Message、Tool 等)
export * from "./api/lazy.ts"     // 各 Provider API 懒加载入口
export * from "./auth/..."        // 认证上下文

没有 agent(代理),没有 tool execution(工具执行),没有 loop(循环)。它就是一层薄薄的统一皮——把 LLM 世界的巴别塔抹平。

pi-agent-core:只管跑 Agent 循环

@earendil-works/pi-agent-corepackages/agent/)解决的是:怎么让 LLM 反复思考和行动?

它的关键字是 “general-purpose”(通用)。这个包不知道你在写代码还是在点外卖。它只知道:

  • 怎么维护对话状态(AgentState
  • 怎么跑”调 LLM → 解析工具 → 执行工具 → 拼结果 → 再调 LLM”的循环(agentLoop
  • 怎么在循环过程中发出事件(AgentEvent),让外部知道发生了什么
  • 怎么管理会话历史、做上下文压缩(Sessioncompact

看导出:

// packages/agent/src/index.ts(节选)
export * from "./agent.js"               // Agent 类
export * from "./agent-loop.js"          // 循环函数
export * from "./harness/session/..."    // 会话管理
export * from "./harness/compaction/..." // 上下文压缩
export * from "./types.js"               // 类型定义

没有 read、没有 bash、没有 edit、没有 write。它不关心具体做什么——只关心”怎么把 Agent 跑起来”。你往它里面注册什么工具,它就执行什么工具。

pi-tui:只负责画界面

@earendil-works/pi-tuipackages/tui/)只做一件事:在终端里渲染 Markdown、代码高亮、diff 对比。

它和 AI 没有任何关系——依赖列表里只有 marked 和东亚字符宽度计算。它就是个渲染库。交互模式(你在终端里看到的那个 diff 效果、流式输出、面板布局)靠它。

pi-coding-agent:把它们拼成产品

@earendil-works/pi-coding-agentpackages/coding-agent/)是整个仓库里最厚的包——上百个源文件,比前两层加起来还多。

它的 package.json 描述是:“Coding agent CLI with read, bash, edit, write tools and session management”

它知道所有具体的事:

  • 7 个编程工具(read、bash、edit、write、grep、find、ls)怎么实现
  • 扩展系统怎么加载、怎么运行
  • 会话怎么持久化到 JSONL 文件
  • CLI 怎么解析参数、怎么在终端渲染输出
  • 认证信息怎么存储和管理
  • 项目信任怎么判断
  • 配置怎么从全局、项目、CLI 三层合并

入口是简简单单的 cli.ts

// packages/coding-agent/src/cli.ts
#!/usr/bin/env node
import { main } from "./main.js";
main(process.argv.slice(2));

main() 有几百行——因为它在做前面所有章节描述过的事:解析参数、选择运行模式、加载配置、创建会话、绑定 I/O。


一张四层关系图

把它画出来:

┌─────────────────────────────────────────────────────┐
│                   pi-coding-agent                    │
│  编程工具(read/bash/edit/write/grep/find/ls)        │
│  CLI 入口(交互/print/json/RPC 四种模式)              │
│  扩展系统、会话持久化(JSONL)、认证管理                │
│  项目信任、配置合并(全局 + 项目 + CLI)                │
│  依赖:pi-agent-core + pi-ai + pi-tui                │
├─────────────────────────────────────────────────────┤
│                   pi-agent-core                      │
│  Agent 状态管理、Agent Loop(思考→行动→再思考)         │
│  会话树与 leaf 导航、上下文压缩(compaction)           │
│  通用事件系统(AgentEvent)                           │
│  工具注册框架(AgentTool,只定义接口不实现具体工具)     │
│  依赖:pi-ai                                         │
├─────────────────────────────────────────────────────┤
│                      pi-ai                           │
│  统一 LLM API(30+ 提供商适配)                        │
│  统一消息类型(UserMessage / AssistantMessage / ...)   │
│  流式调用(streamSimple / stream)                    │
│  模型定义、Token 用量、认证上下文                       │
│  依赖:各 LLM SDK(openai / @anthropic-ai/sdk 等)     │
└─────────────────────────────────────────────────────┘

辅助层:
┌──────────────────┐
│     pi-tui       │  ← 终端渲染(无 AI 依赖)
│  Markdown / 代码高亮 / diff 显示     │
└──────────────────┘
Tip

用”谁依赖谁”而不是”谁包含谁”来看这张图。pi-ai 是最底层——它不知道任何关于 Agent 的事。pi-agent-core 知道 pi-ai 的类型定义,但不知道”编程工具”是什么。pi-coding-agent 知道一切——它把前两层拼成了一把完整的瑞士军刀。


一次请求的旅程:从输入到模型返回

理解了四层架构,来跟踪一条最简单的请求。“帮我看看这个文件”——你输入了,按了回车。发生了什么?

阶段 1:CLI 入口 → 模式判定

cli.ts → main(process.argv.slice(2))

resolveAppMode() 判定模式:
  - stdin/stdout 都是 TTY?→ interactive
  - 带了 -p?→ print
  - --mode rpc?→ rpc
  - --mode json?→ json

源码依据:packages/coding-agent/src/main.tsresolveAppMode(),v0.83.0。

阶段 2:恢复或创建会话

根据 --session / --resume / 默认逻辑

确定 SessionManager(可能是恢复旧会话、或创建新的)

从 session header 读取有效 cwd

这一步很关键——--resume 可能打开另一项目的会话,cwd 会变。所以必须先确定 cwd,再加载那个 cwd 下的配置和资源

源码依据:packages/coding-agent/src/main.ts,session 选择和 cwd 确定逻辑,v0.83.0。

阶段 3:加载配置和资源

有效 cwd 确定后

createRuntime() 开始:
  1. 判断项目是否需要信任
  2. createAgentSessionServices() 组装基础设施:
     - SettingsManager(全局 + 项目配置合并)
     - ModelRuntime(从配置和 provider 注册解析可用模型)
     - ResourceLoader(加载扩展、skills、prompts、themes)
     - 扩展注册的 provider 交给 model runtime

如果项目需要信任但尚未信任,ResourceLoader 会做两轮加载:第一轮 projectTrusted=false,用可信资源渲染 trust UI;用户确认后,第二轮按最终信任状态重新加载所有资源。

源码依据:packages/coding-agent/src/core/resource-loader.tsreload() 和两轮加载逻辑,v0.83.0。

阶段 4:创建 Agent 会话

createAgentSessionFromServices(services, modelScope, sessionOptions)

复原当前 model 和 thinking level

计算工具集合(编程工具 + 扩展注册的工具)

构造底层 Agent 对象(pi-agent-core 的 Agent 类)

构造 AgentSession:订阅 Agent 事件、建立工具 registry、启动 ExtensionRunner

包装成 AgentSessionRuntime(session + services 的绑定体)

此时会话对象已经可以 await session.prompt("帮我看看这个文件") 了。

源码依据:packages/coding-agent/src/main.tscreateAgentSessionFromServices() 调用,v0.83.0。

阶段 5:绑定 I/O 模式

根据 AppMode 分派:
  interactive → 构造 InteractiveMode(TUI 启动)
  print/json  → runPrintMode()(订阅事件输出)
  rpc         → runRpcMode()(长驻 stdin/stdout JSON 协议)

四种模式共享同一个 AgentSession。交互模式启动 pi-tui 渲染;print 模式等待任务完成取最后一个 assistant 文本;JSON 模式逐行输出事件流。

源码依据:packages/coding-agent/src/main.ts,模式分派,v0.83.0。

阶段 6:Agent Loop → 模型调用

session.prompt("帮我看看这个文件")

Agent Loop 启动:
  1. 用户消息 + 系统提示词 → 拼成消息列表
  2. 经过 context building(如果有 compaction,套用摘要)
  3. 消息类型从 AgentMessage 转换为 LLM 能理解的 Message
     (coding-agent 层 → pi-agent-core 层 → pi-ai 层)
  4. pi-ai 的 streamSimple() → provider 适配器 → 发送 HTTP 请求

模型返回后,如果有工具调用(比如 read 文件路径),Agent Loop 执行工具,将结果追加到消息列表,再调一次 LLM。如此循环直到模型判断任务完成。

阶段 7:结果持久化

每个 assistant 消息 + 工具结果 → append 到 SessionManager

SessionManager._appendEntry()
  - parentId = 当前 leaf
  - 新 entry 变成新的 leaf

JSONL 文件 append 一行
Note

首次写入有个延迟:第一条 user message 输入后文件还不存在,等第一条 assistant message 完成,_persist() 才一次写入 header + 所有累积 entry。之后每条 entry 逐行追加。这是为了不在磁盘上留下一堆”只输入了一句就退出”的空会话文件。

源码依据:packages/coding-agent/src/core/session-manager.ts_persist() 延迟创建逻辑,v0.83.0。

一张图总结全流程

输入 "帮我看看文件"

  ├─ cli.ts          ── 接收参数
  ├─ main.ts         ── 判定模式、恢复/创建 session
  ├─ SettingsManager ── 合并全局 + 项目配置
  ├─ ResourceLoader  ── 加载扩展、skills、prompts、themes
  ├─ ModelRuntime    ── 解析可用模型列表

  ├─ AgentSession     ── 组装工具、注册事件
  │   └─ Agent        ── 管理状态
  │       └─ agentLoop ── 思考→行动→再思考
  │           │
  │           ├─ pi-ai: 统一 API → provider 适配器 → HTTP 请求
  │           ├─ Tool: read/bash/edit/write/grep/find/ls
  │           └─ Event: 流式输出 token → pi-tui 差分渲染

  └─ SessionManager  ── append JSONL

pi 和 OpenClaw:底座 vs 宿主

如果 pi 本身就是一个完整的 Agent 运行时,那 OpenClaw 是什么?

OpenClaw 是 pi 的”宿主”。

打个比方:pi 是汽车的发动机和底盘——负责跑起来的核心能力。OpenClaw 是整辆车的方向盘、仪表盘和车载系统——它提供一个完整的产品界面,让用户和 pi 交互。

具体来说:

  • pi 提供 Agent 运行时、工具执行、模型调用、会话管理。它是”底座”
  • OpenClaw 在 pi 之上构建了多平台接入(IM 消息、REST API、Web 界面)、扩展市场、用户管理、计费等产品层功能
  • OpenClaw 通过 pi 的 SDK 和 RPC 模式来驱动 pi 的 Agent 能力
  • 你通过 OpenClaw 的 WebChat 聊 pi 时,底层就是 pi 在跑 Agent Loop

换句话说:你可以只装 pi,在终端里用。也可以把 pi 作为 OpenClaw 的底座,获得一个多平台、有界面的完整产品。

pi-orchestrator 也是这个方向的延伸——它让多个 pi 实例可以协同工作:一个 supervisor 管多个子 Agent,通过 RPC 通信。OpenClaw 的多 Agent 场景可以站在 orchestration 层上编排。


四层真都需要吗?

看完上面,你可能想:我一个简单需求,真需要四层包?

不需要。层数取决于你的场景。

场景你用什么你自己做什么
只想调 LLMpi-ai自己管状态、自己写循环
需要 Agent 循环但有自己的业务pi-ai + pi-agent-core写自己的工具、自己的入口
做 pi 同类的编程助手三件套全用直接用,或写扩展
多 Agent 协作+ pi-orchestrator编管子 Agent

pi-ai 可以独立使用——import { streamSimple } 就行。pi-agent-core 也可以独立——注册自己的工具,跑自己的 Agent Loop。pi-coding-agent 只是在这之上加了编程场景的默认配置:7 个编程工具、CLI 界面、扩展系统、会话持久化。

但有一条底线在所有层级上不变:底层代码里不能出现任何对上层的引用。 pi-ai 不 import pi-agent-core。pi-agent-core 不 import pi-coding-agent。这条规则保证你可以把任何一层换成自己的实现,不影响其他层。


类型在三层间的流转

把三层间类型变化的轨迹画出来,能帮你更快理解每一层”加什么”:

pi-ai(底层):定义原子
  Message   = UserMessage | AssistantMessage | ToolResultMessage
  Tool      = { name, description, parameters }
  Model     = { id, name, api, contextWindow, ... }

        ↓ agent-core 扩展

pi-agent-core(中间层):组装分子
  AgentMessage  = Message | CustomAgentMessages  ← 超集
  AgentTool     extends Tool {
    label: string
    execute: (...) => Promise<AgentToolResult>   ← 新增
    executionMode?: "sequential" | "parallel"     ← 新增
  }

        ↓ coding-agent 扩展

pi-coding-agent(顶层):做成材料
  ToolDefinition {
    // 继承 AgentTool 全部字段
    // + 渲染器(renderShell / renderCall / ...) ← 新增
    // + 权限控制、UI 组件、快捷键                ← 新增
    // + ctx: ExtensionContext 参数              ← 扩展的注入点
  }

每一层只加自己关心的字段。底层不修改——pi-ai 的 Tool 里没有 execute,因为 LLM 不需要知道工具怎么执行。coding-agent 的 ToolDefinition 需要知道渲染器,因为用户要看。


三个可以带走的设计思路

pi 的分层设计里,有几个思路在你自己的项目里同样好用。

“依赖漏斗”:底层不知道外面

设计包结构时先画依赖箭头。底层是”不知道外面世界的”,中间是”知道底层但不知道业务的”,顶层是”知道一切的”。验证方法很简单:去掉上层,底层还能跑吗?能——依赖方向对。不能——上层的东西泄漏到了下层。

“类型递进扩展”:不改底层,只往上加

底层定义最小的类型接口。上层通过继承(extends)和联合类型(|)来扩展。底层类型从不因上层需求而修改。pi-ai 定义 Tool = { name, description, parameters } 就够了——不要为了加一个 execute 去改它。

“可独立使用”测试

每层设计完后做一件事:去掉上层,这层还能正常工作吗?

  • 去掉 agent-core 和 coding-agent,pi-ai 可以独立调 LLM ✅
  • 去掉 coding-agent,pi-ai + agent-core 可以跑自定义 Agent ✅
  • 三层全用,就是一个完整的编程助手 ✅

如果某层必须靠上层才能编译通过,那依赖方向反了。


从哪里开始读源码

如果你真的想深入 pi 源码,建议按这个顺序:

  1. packages/ai/src/types.ts —— 所有基础类型定义。认识 MessageModelTool
  2. packages/agent/src/agent-loop.ts —— Agent 循环的核心。看”调 LLM → 解析工具 → 执行 → 再调”怎么跑。
  3. packages/coding-agent/src/main.ts —— 启动组装。看 CLI 参数怎么变成 AgentSession。
  4. packages/coding-agent/src/core/session-manager.ts —— 会话树。看 entry 类型、追加逻辑、上下文构建。
  5. packages/coding-agent/src/core/* —— 逐个看:settings-manager、resource-loader、tools。

不要从头到尾按文件顺序看——先抓核心循环和类型定义,再沿着调用链扩展。有 git grep 帮你找引用点。


这一章是全书最高层的一章——我们在 30000 英尺看了 pi 的全景。从前 24 章的功能到这一章的架构,再到下一章的源码精读——现在你已经有地图了。接下来该钻进去看细节了。