12 min read教程

PiAgent 01|从环境配置到第一个 Agent

先跑通最小调用,再沿着 ModelRuntime → AgentSession → Agent Loop 建立源码阅读地图

PiAgent 01|从环境配置到第一个 Agent#

先跑通最小调用,再沿着 ModelRuntime → AgentSession → Agent Loop 建立源码阅读地图

Pi 最适合用来学习的地方,不是它能在终端里回答问题,而是它把一个可工作的 Coding Agent 拆成了几条可以追踪的链路:模型如何被选中,会话如何装配,消息如何进入循环,工具如何产生副作用,事件又如何被外部消费。

这篇文章不把官方 API 重新抄成字典,而是先完成一次最小启动,然后解释启动过程里每个对象的职责。后面的文章都会回到这张地图:当我们修改模型、系统提示词、工具或 Web 接口时,究竟是在修改哪一层。

1. 先建立三个观察角度#

Pi 同时具有三个身份:

  1. 终端产品:默认提供读文件、执行命令、编辑文件等编码能力,并通过 TUI 呈现运行过程。
  2. Agent 教材:核心循环、工具协议、事件和会话不是一个不可拆的黑盒,能够从类型和生命周期继续往下追。
  3. 可嵌入 SDK:可以只使用 pi-ai 的模型接口,也可以使用 pi-agent-core 的循环,或者直接使用 pi-coding-agent 装配完整的 AgentSession

这三个身份对应三种不同的依赖关系。TUI 需要会话和事件;SDK 需要稳定的运行时边界;源码阅读则需要区分“谁拥有状态”和“谁只负责转换”。如果把所有对象都看成一个叫 Agent 的类,后面遇到自定义工具、会话持久化或 Web 流式输出时就很容易改错层。

2. 一次任务的完整路径#

把“分析当前项目”拆开,运行时大致沿着下面的路径前进:

text
用户 prompt
  → AgentSession 组合当前上下文
  → Agent Loop 请求模型
  → 模型返回文本增量或 tool call
  → 工具校验参数并执行副作用
  → ToolResultMessage 回到 transcript
  → Loop 根据 stop reason 决定继续还是结束
  → session 事件被 UI、扩展、日志和持久化层消费

这里有一个重要的区分:模型只提出下一步,宿主程序决定这一步能否执行。模型返回一个 bash 调用,并不意味着命令已经运行;工具层还要做 schema 校验、权限检查、执行、错误包装和结果回写。

3. 最小运行环境:先确认硬约束#

当前官方 Pi Coding Agent 文档要求 Node.js 22.19.0 或更高版本。这个约束来自包的 engines.node,不是示例作者随意选择的版本。版本太低时,问题会在依赖安装、ESM 或运行时 API 处暴露,应该先处理环境而不是继续排查 Agent 逻辑。

bash
node --version
npm install @earendil-works/pi-coding-agent
npm install -D tsx

Pi 的配置默认位于 ~/.pi/agent/。受限服务器或容器可以通过 PI_CODING_AGENT_DIR 指向项目内目录,也可以在 SDK 调用中显式传入 agentDir。两种方式改变的是配置来源,不改变 Agent Loop 的语义。

一个最小的 models.json 可以使用 OpenAI 兼容协议:

json
{
  "providers": {
    "internal": {
      "baseUrl": "https://api.example.com/v1",
      "api": "openai-completions",
      "apiKey": "$INTERNAL_API_KEY",
      "models": [
        { "id": "model-id", "name": "Internal Model" }
      ]
    }
  }
}

生产环境不要把真实 Key 写进版本库。配置值支持从环境变量或命令解析,认证信息也可以独立放在 auth.json。这里的重点不是记住 JSON 字段,而是知道:模型能不能被选中,首先取决于 Provider 定义与鉴权是否被运行时加载

4. 12 行代码背后的装配过程#

下面是最小的进程内 SDK 示例。它只做四件事:加载可用模型、创建会话、订阅文本事件、发送 prompt。

ts
import {
  createAgentSession,
  ModelRuntime,
} from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const available = await modelRuntime.getAvailable();
const model = available[0];

if (!model) {
  throw new Error("没有可用模型,请检查 models.json 与 auth.json");
}

const { session } = await createAgentSession({ model, modelRuntime });

try {
  const off = session.subscribe((event) => {
    if (
      event.type === "message_update" &&
      event.assistantMessageEvent.type === "text_delta"
    ) {
      process.stdout.write(event.assistantMessageEvent.delta);
    }
  });

  await session.prompt("用一句话介绍你自己。");
  off();
} finally {
  session.dispose();
}

4.1 ModelRuntime.create() 做了什么#

ModelRuntime 可以理解为模型与认证的配置仓库。创建它时,运行时会读取认证与模型配置,并把用户配置和内置 Provider 定义合并到内存中。它本身不是一次模型请求;默认创建过程主要是本地配置加载。

getAvailable() 再根据 Provider 是否有有效鉴权,过滤出当前真正能用的模型。因此,models.json 中“声明了一个模型”和 getAvailable() 返回这个模型是两件事。排查“模型列表为空”时,应按顺序检查配置目录、JSON 结构、Key 来源和 Provider 名称。

4.2 createAgentSession() 是装配边界#

创建会话时,Pi 会把多项基础设施接起来:

text
ModelRuntime       → 模型、Provider、认证
SettingsManager    → 默认模型、重试、压缩等设置
SessionManager     → JSONL 会话树或内存会话
ResourceLoader     → 系统提示词、skills、extensions、上下文文件
Agent              → 循环、消息、工具和生命周期

如果没有显式传入这些对象,Coding Agent 会使用默认实现。默认实现适合本地单用户 CLI;当应用变成多用户 Web 服务时,通常需要替换 SessionManager,并重新设计 ResourceLoader 的资源边界。后面文章会分别解释这些替换点。

4.3 subscribe() 不负责启动运行#

session.subscribe() 只是登记一个观察者,返回取消订阅函数。真正触发 Agent 运行的是 session.prompt()。这两个调用的时序非常重要:先订阅,再 prompt,才能观察到完整的 agent_start、文本增量、工具事件和结束事件。

message_update 还会嵌套一层 assistantMessageEvent。文本输出取 text_deltadelta;思考输出(模型支持且开启时)取 thinking_deltadelta。事件协议把“正在生成什么”和“最终生成了什么”分开,UI 可以据此做增量渲染,而不是等待最终消息。

4.4 prompt()dispose() 的生命周期#

prompt() 返回一个 Promise,Promise resolve 表示这次调用的 Agent 运行已经收束,而不是表示只发生了一次模型请求。若任务需要工具,内部可能经过多轮模型响应和工具结果。

dispose() 应放在 finally 中。它负责中止后台任务、清理订阅和释放会话级资源。只在正常路径调用清理函数,会让异常路径留下监听器、重试任务或未结束的工具执行。

5. 从源码阅读的正确顺序#

不要从 TUI 开始读。建议按照数据和控制流阅读:

  1. packages/coding-agent/src/sdk.ts:看 createAgentSession 如何装配运行时。
  2. packages/coding-agent/src/agent-session.ts:看 Session 如何包住 Agent、会话和扩展。
  3. packages/agent/src/agent-loop.ts:看一次 prompt 如何进入多轮循环。
  4. packages/ai/src/types.ts:看模型消息、流式事件和 Provider 类型。
  5. packages/coding-agent/src/core/tools/:看内置工具如何进入执行管道。
  6. packages/tui/:最后再看终端如何消费事件和渲染状态。

这种顺序背后的原则是:先确定状态和控制流,再看表现层。如果先看 UI,很容易把“一个状态被显示出来”误认为“这个状态由 UI 创建”。

6. 几个必须记住的边界#

现象真正负责的层排查方向
没有可用模型ModelRuntime配置目录、Provider、认证与模型 ID
prompt 不产生工具调用系统提示词、工具集合、模型能力工具是否激活、schema 是否可理解
文本不能逐字显示事件投影层是否订阅 message_update/text_delta
任务结束后进程不退出Session/工具资源是否调用 dispose(),是否还有后台任务
Web 用户之间串上下文SessionManager/服务装配是否为用户隔离 Session 与持久化路径

7. 小结#

Pi 的学习入口不是某个神奇的 run() 函数,而是一个清晰的装配链:配置先进入 ModelRuntime,基础设施被 createAgentSession() 组合,Agent 负责循环,session 提供 prompt、事件和资源生命周期。

后续每篇文章只回答一个更窄的问题:包如何分层、循环何时继续、模型差异在哪里被消化、工具如何安全执行、消息如何转换、事件如何投影,以及会话如何恢复。掌握这条主线,比记住几十个 API 名称更能帮助你驾驭 AI 生成的代码。

源码定位(以官方仓库当前结构为准)

  • SDK 装配:packages/coding-agent/src/sdk.ts
  • 会话生命周期:packages/coding-agent/src/agent-session.ts
  • Agent 类型与循环:packages/agent/src/agent.tspackages/agent/src/agent-loop.ts
  • 模型和流式事件:packages/ai/src/types.ts
  • 配置与 Provider:packages/coding-agent/src/core/config.tspackages/coding-agent/src/core/model-runtime.ts

相关文章