要建设,就必须有知识,必须掌握科学。而要有知识,就必须学习,顽强地、耐心地学习。向所有的人学习,不论向敌人或朋友都要学习,特别是向敌人学习。——斯大林

Pi:一套从模型调用到编码智能体的 AI Agent 工具箱

项目地址:https://github.com/earendil-works/pi

当 AI 应用开始从一次简单的文本生成,走向会调用工具、维护上下文、执行任务、处理状态并持续与外部环境互动的智能体工作流时,真正复杂的问题便浮出水面。

模型来自哪里。

认证如何处理。

工具调用如何定义、验证与执行。

流式输出如何反馈到界面。

会话状态如何保存。

一个任务被中断后,怎样恢复。

不同模型之间切换时,上下文如何交接。

Pi 正是在这样的背景下出现的一套 AI Agent 工具箱。

它提供统一的大模型 API、智能体运行循环、终端用户界面库与交互式编码智能体命令行工具。项目希望将构建智能体时最容易分散的能力收拢起来,让开发者既能使用现成的编码智能体,也能从底层开始搭建属于自己的 Agent 系统。

Pi 的核心并不是把智能体包装成一个不可拆解的黑盒,而是提供一组可以单独使用、也可以彼此协作的模块。

Pi 的组成:从底层模型到交互式编码 Agent

Pi 项目包含多个核心软件包,每个包各自承担不同职责。

@earendil-works/pi-ai 是统一的多供应商大模型 API。

@earendil-works/pi-agent-core 是具备工具调用与状态管理能力的 Agent 运行时。

@earendil-works/pi-coding-agent 是交互式编码智能体命令行工具。

@earendil-works/pi-tui 是支持差分渲染的终端界面库。

@earendil-works/pi-telemetry 则提供与供应商无关的遥测契约、参考适配器、一致性测试与类型化模式。

这几部分像一组彼此咬合的齿轮。

模型 API 负责连接不同的大模型提供商。

Agent 运行时负责让模型、上下文、工具和状态持续运转。

终端界面库负责把过程呈现在命令行里。

编码 Agent 则将这些能力聚合为可以直接交互使用的开发工具。

当需要构建自己的智能体应用时,可以从 pi-aipi-agent-core 起步。当需要在终端中使用交互式编码智能体时,则可以使用 pi-coding-agent

统一模型接口,让多供应商不再割裂

不同大模型供应商通常有不同的模型目录、认证机制、请求格式、流式事件、工具调用协议和能力边界。

这会让应用层代码很快被供应商差异占据。

Pi 的 pi-ai 提供统一的大模型 API,用于处理提供商集合、自动认证解析、令牌与成本追踪,以及在会话中将上下文交接给其他模型。

它支持多种大模型提供商,包括 OpenAI、Anthropic、Google、Vertex AI、Azure OpenAI、DeepSeek、NVIDIA NIM、Mistral、Groq、Cerebras、Cloudflare AI Gateway、Cloudflare Workers AI、xAI、OpenRouter、Vercel AI Gateway、Together AI、Baseten、Hugging Face、Amazon Bedrock、GitHub Copilot,以及兼容 OpenAI API 的服务,例如 Ollama、vLLM 和 LM Studio。

Pi 只收录支持工具调用的大模型。因为在 Agent 工作流中,工具调用并不是可有可无的附属功能,而是让模型能够与文件、服务、系统和应用逻辑产生真实互动的重要能力。

开发者可以将多个提供商注册到一个模型集合中,再按提供商与模型标识进行获取和调用。

1
2
3
4
5
import { builtinModels } from "@earendil-works/pi-ai/providers/all";

const models = builtinModels();

const model = models.getModel("openai", "gpt-4o-mini");

这种方式让应用可以在一个统一入口下管理多个模型,而不必为每一种模型服务单独组织一套调用逻辑。

模型调用不只返回答案,也返回过程

对普通聊天应用来说,拿到最终文本或许已经足够。

但对 Agent 系统而言,过程同样重要。

模型什么时候开始生成。

文本何时开始输出。

推理内容如何流动。

工具调用何时出现。

工具参数是否还在生成。

请求是正常完成、因长度停止、准备调用工具,还是发生错误。

Pi 的流式接口将这些状态作为事件持续提供给开发者。

1
2
3
4
5
6
7
const stream = models.stream(model, context);

for await (const event of stream) {
if (event.type === "text_delta") {
process.stdout.write(event.delta);
}
}

流式事件可以覆盖文本生成、推理内容、工具调用与最终完成状态。

这使得终端界面、桌面界面或 Web 界面不必等待完整结果才开始更新。模型的输出过程、工具调用过程与运行状态,都能够被实时呈现。

对于一个需要让人理解并参与智能体执行过程的应用来说,这种事件流就像一条可见的河流。它让系统不只是沉默地计算,然后丢出一个结果,而是将任务推进的节奏保留在界面之中。

工具调用:让模型走出对话框

智能体与普通文本助手的区别,往往不在于说得多好,而在于能否调用工具完成任务。

Pi 使用 TypeBox 模式定义工具参数,并结合自动验证能力,让工具描述、参数结构与执行逻辑能够形成更清晰的边界。

1
2
3
4
5
6
7
8
9
10
11
import { Type, type Tool } from "@earendil-works/pi-ai";

const weatherTool: Tool = {
name: "get_weather",
description: "Get current weather for a location",
parameters: Type.Object({
location: Type.String({
description: "City name or coordinates"
})
})
};

工具定义完成后,模型可以在生成过程中发起调用请求。

Pi 会将工具调用作为消息内容的一部分交给应用处理。应用执行工具后,再将结果写回对话上下文,模型便可以继续生成后续内容。

工具结果可以包含文本,也可以包含图像。

这意味着,Agent 的工具执行不局限于返回一段字符串。对于支持视觉输入的模型,工具也可以将图像结果带回上下文,继续参与后续推理与回应。

Pi 还支持在流式过程中逐步解析工具参数。

当模型正在生成工具调用参数时,应用可以提前观察到不完整的 JSON 内容。例如,在写文件场景中,界面可以先显示模型准备写入的路径,再随着参数逐渐完整更新预览。

这种能力让工具调用不必等到最后一刻才变得可见。

严格的工具参数与受限采样

工具调用真正进入生产工作流之后,参数可靠性会变得格外重要。

Pi 支持对工具参数进行验证。开发者可以在真正执行工具前,根据工具定义检查模型返回的参数是否符合预期。

如果参数验证失败,应用可以将错误作为工具结果返回给模型,让模型获得重新调整调用方式的机会。

Pi 还支持让工具选择使用提供商侧的受限采样能力。对于 JSON Schema 工具,可以优先使用提供商支持的严格模式,并在不支持时回退到普通工具调用。

这让工具调用不仅是模型给出一个看似合理的对象,更可以被放在更明确的结构约束之下。

Agent Core:让模型、状态与工具真正跑起来

如果说 pi-ai 解决的是如何统一调用模型,那么 pi-agent-core 解决的则是如何让智能体持续工作。

它是一个带状态的 Agent 运行时,支持工具执行与事件流。

智能体并不只是一次请求。

它有系统提示词。

它有当前模型。

它有工具列表。

它有历史消息。

它有正在流式输出的消息。

它可能还有待执行的工具调用与错误信息。

Pi 将这些信息组织为 Agent 状态,让开发者能够通过 agent.state 访问和调整它们。

1
2
3
4
5
6
7
const agent = new Agent({
initialState: {
systemPrompt: "You are a helpful assistant.",
model,
},
streamFn: models.streamSimple.bind(models),
});

随后,Agent 可以接收提示词并开始运行。

1
await agent.prompt("Hello!");

在运行过程中,开发者可以订阅事件,将每一个阶段映射到自己的界面、日志或业务逻辑。

1
2
3
4
5
6
7
8
agent.subscribe((event) => {
if (
event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta"
) {
process.stdout.write(event.assistantMessageEvent.delta);
}
});

一个 Agent 的运行过程,会从开始处理任务、启动回合、接收用户消息、生成助手消息,到工具执行、回写工具结果、继续下一回合,最终结束整次任务。

这些事件让开发者能够准确把握智能体正在做什么,而不是只在任务结束时才知道结果。

工具可以并行,也可以顺序执行

一个智能体在同一轮中可能会请求多个工具。

Pi 默认使用并行工具执行模式。

在该模式下,工具调用会先按顺序进行预检查,通过检查的工具可以并发执行。每个工具完成后,系统会尽快发出对应的完成事件,而最终写入消息上下文的工具结果仍会遵循助手消息中原始工具调用的顺序。

这种设计兼顾了执行效率与上下文顺序。

如果某些工具必须按顺序执行,也可以将全局执行模式设为 sequential,或在单个工具上单独指定顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const readFileTool: AgentTool = {
name: "read_file",
label: "Read File",
description: "Read a file's contents",
parameters: Type.Object({
path: Type.String({
description: "File path"
})
}),
executionMode: "sequential",
execute: async (toolCallId, params) => {
const content = await fs.readFile(params.path, "utf-8");

return {
content: [
{
type: "text",
text: content
}
],
details: {
path: params.path,
size: content.length
}
};
}
};

这种可配置性让 Agent 不必被固定在唯一的工具调度方式中。

需要并行的任务可以并行推进。

具有前后依赖的操作则可以保持顺序。

在工具执行前后建立自己的控制点

Pi 为工具执行提供了前置与后置钩子。

在工具参数完成验证后、实际执行之前,beforeToolCall 可以决定是否阻止执行。

在工具返回结果之后,afterToolCall 可以对结果进行后处理。

例如,可以禁止某些工具调用。

1
2
3
4
5
6
7
8
9
beforeToolCall: async ({ toolCall }) => {
if (toolCall.name === "bash") {
return {
block: true,
reason: "bash is disabled",
terminate: true
};
}
}

也可以在工具正常执行后,为结果附加额外信息。

1
2
3
4
5
6
7
8
9
10
afterToolCall: async ({ result, isError }) => {
if (!isError) {
return {
details: {
...result.details,
audited: true
}
};
}
}

这些钩子让工具调用不只是模型直接触发函数的通道,而是可以经过应用层规则、审计逻辑和后处理流程的受控过程。

中断、继续与任务队列

真实任务不会永远一帆风顺。

模型请求可能被取消。

工具运行可能耗时。

用户可能在 Agent 工作到一半时改变主意。

Pi 为这些情况提供了相应的控制方式。

调用 agent.abort() 可以取消当前操作。

调用 agent.waitForIdle() 可以等待 Agent 完成当前工作。

调用 agent.continue() 可以从现有上下文继续运行,适合在错误之后重新尝试。

1
await agent.continue();

Pi 还支持引导消息与后续消息。

引导消息可以在 Agent 执行工具时插入,用于打断当前方向并给出新的任务要求。

后续消息则可以在 Agent 当前工作完成后排队执行。

1
2
3
4
5
6
7
8
9
10
11
agent.steer({
role: "user",
content: "Stop! Do this instead.",
timestamp: Date.now()
});

agent.followUp({
role: "user",
content: "Also summarize the result.",
timestamp: Date.now()
});

引导消息与后续消息让人类不必总是等待 Agent 完整结束后才能重新介入。

智能体可以持续推进任务,人也可以在合适的时刻重新掌舵。

消息不必只有用户、助手与工具结果

大模型通常理解用户消息、助手消息和工具结果消息。

但应用本身常常还会有更多类型的信息。

例如,界面通知、运行状态、系统事件、审计记录或业务侧自定义内容。

Pi 的 AgentMessage 支持通过声明合并扩展自定义消息类型。

1
2
3
4
5
6
7
8
9
declare module "@earendil-works/pi-agent-core" {
interface CustomAgentMessages {
notification: {
role: "notification";
text: string;
timestamp: number;
};
}
}

当智能体向模型请求生成时,可以通过 convertToLlm 将应用专属的消息过滤或转换为模型真正需要理解的格式。

这让应用层状态与模型上下文能够同时存在。

有些消息服务于人和界面。

有些消息服务于模型。

两者可以在同一个 Agent 状态中共存,但不必强迫模型理解每一种应用内部事件。

上下文转换,让会话保持可管理

长期运行的 Agent 很容易遇到上下文增长问题。

消息越积越多,模型输入也会越来越长。

Pi 在 Agent 消息与大模型消息之间提供了两层处理过程。

第一层是 transformContext

它可以用于裁剪旧消息、压缩上下文或注入外部信息。

第二层是 convertToLlm

它负责将 AgentMessage 转换为模型能够理解的标准消息。

这条处理路径可以概括为:

1
AgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → Message[] → LLM

它让上下文管理不必和模型调用代码纠缠在一起。

开发者可以明确决定,哪些历史内容应该保留,哪些应用专属消息应该被过滤,哪些外部信息应该在某个回合开始前被注入。

认证由提供商管理,而不是散落在业务代码里

不同提供商的认证方式差异很大。

有的使用环境变量中的 API Key。

有的支持 OAuth 登录与刷新。

有的依赖云环境中的凭据。

Pi 将认证解析放在各个提供商内部。

当应用调用 models.stream()models.complete() 时,模型集合会通过所属提供商解析认证信息,并将认证结果合并到请求中。

显式传入的请求配置会优先于提供商解析得到的认证信息。

1
2
3
await models.complete(model, context, {
apiKey: "sk-explicit"
});

Pi 还支持凭据存储。

交互输入的 API Key 与 OAuth 令牌可以保存在 CredentialStore 中。默认情况下,pi-ai 提供内存实现,应用也可以注入持久化存储方案。

认证信息、模型目录与请求行为因此能够围绕提供商被组织起来,而不必散落到每一个业务调用点。

跨模型交接,让会话不被单一模型锁住

Pi 的统一模型 API 支持简单的上下文持久化,也支持在会话中交接到其他模型。

这对于需要根据任务变化调整模型的场景很有意义。

一个会话不必从开始到结束都绑定在同一个模型上。

在需要时,开发者可以保留已有上下文,并在另一个模型上继续任务。

模型可以变化,任务的线索不必因此断裂。

这种能力并不是要抹平不同模型之间的差异,而是让应用在面对不同提供商、不同模型选择与不同任务节奏时,拥有更灵活的调度空间。

图像输入与图像生成

除了文本和工具调用,Pi 也支持图像输入。

如果模型支持视觉输入,应用可以将文本与图像一起作为用户消息提供给模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const response = await models.complete(model, {
messages: [
{
role: "user",
content: [
{
type: "text",
text: "What is in this image?"
},
{
type: "image",
data: base64Image,
mimeType: "image/png"
}
],
timestamp: Date.now()
}
]
});

Pi 还提供了独立于聊天模型的图像生成接口。

图像生成模型由单独的 ImagesModels 集合管理,并通过 generateImages() 执行。图像生成输出可以包含 Base64 编码的图像内容,也可以包含文本内容。

聊天模型与图像生成模型保持为两套独立接口。

前者服务于对话、工具调用和视觉理解。

后者服务于图像生成。

这种边界让不同类型的模型能力能够以更明确的方式被使用。

推理能力,也有统一的调用层

许多模型提供推理或思考能力。

Pi 允许开发者通过统一接口设置推理级别,例如 minimallowmediumhighxhighmax

1
2
3
4
5
6
7
8
9
10
11
const response = await models.completeSimple(model, {
messages: [
{
role: "user",
content: "Solve: 2x + 5 = 13",
timestamp: Date.now()
}
]
}, {
reasoning: "medium"
});

如果模型支持推理,流式过程中也可以接收相应的推理事件。

文本、推理与工具调用不再是互相割裂的输出通道,而是 Agent 运行过程中的不同内容块。

Pi Coding Agent:在终端中与代码协作

Pi 项目包含交互式编码智能体 CLI。

它是项目面向实际编码工作的一部分,让用户能够在终端中与 Agent 进行交互。

终端本身并不是限制。借助 pi-tui 提供的差分渲染能力,编码 Agent 可以将流式输出、工具状态和交互过程组织为更适合终端环境的体验。

Pi 也为开发和测试提供了从源码运行的方式。

1
2
3
4
5
6
npm install --ignore-scripts
npm run build
npm run build:offline
npm run check
./test.sh
./pi-test.sh

其中,npm install --ignore-scripts 会在不运行生命周期脚本的情况下安装依赖。

npm run build 会刷新模型数据并构建全部软件包。

npm run build:offline 会使用现有模型数据进行离线构建。

npm run check 用于执行代码检查、格式检查与类型检查。

./test.sh 用于运行测试,在没有 API Key 时会跳过依赖大模型的测试。

./pi-test.sh 则可以从源码运行 Pi,并且能够从任意目录执行。

对供应链变化保持警惕

Pi 对 npm 依赖变更采取了较为严格的处理方式。

项目将 npm 依赖的变化视为需要审查的代码变化。

外部直接依赖会被固定到精确版本。

.npmrc 使用精确版本保存设置,并设置依赖发布最短年龄,以避免在 npm 解析时直接引入当天刚发布的依赖版本。

package-lock.json 被视为依赖关系的事实依据。

预提交机制会阻止意外提交锁文件变更,除非明确设置相应环境变量。

检查命令会验证直接依赖是否固定、原生 TypeScript 导入兼容性,以及编码 Agent 的生成式 shrinkwrap。

发布的 CLI 包含生成的 npm-shrinkwrap.json,用于为 npm 用户固定传递依赖。

CI 安装依赖时也会使用不运行生命周期脚本的方式,并通过定时工作流执行依赖审计与签名审计。

这些做法体现出一个明确态度:依赖并不是理所当然的背景材料。对于会运行在开发环境、可能执行工具、处理凭据和连接模型服务的 Agent 工具来说,依赖链同样需要被认真看待。

权限边界需要由运行环境承担

Pi 不包含用于限制文件系统、进程、网络或凭据访问的内置权限系统。

默认情况下,它会使用启动它的用户与进程所拥有的权限运行。

这是一条非常重要的边界。

Pi 的能力可以延伸到工具执行、文件操作和终端命令,因此运行它的环境本身决定了它能够接触什么。

当需要更强的隔离边界时,项目建议通过容器化或沙箱方式运行 Pi。

README 提供了三种模式。

Gondolin 扩展模式可以将 Pi 与模型提供商认证保留在宿主机,同时把内置工具和 ! 命令路由到本地 Linux 微型虚拟机中。

Plain Docker 模式可以将整个 Pi 进程运行在本地容器中,用于实现相对直接的隔离。

OpenShell 模式则可以让整个 Pi 进程运行在受策略控制的沙箱中。

Pi 并不假装权限问题已经自动解决,而是把运行环境与隔离方式明确留给使用者进行选择。

用开放的方式构建 Agent

Pi 采用 MIT 许可证。

它将统一模型调用、Agent 运行时、终端界面、编码智能体、工具调用、会话状态、遥测与供应链管理放进同一个开源项目中。

这套工具箱的意义,并不只是提供另一个可以调用大模型的 SDK。

它更像是一套面向 Agent 工程实践的基础设施。

模型可以来自不同提供商。

工具可以拥有明确的参数结构与执行策略。

消息可以包含应用专属状态。

会话可以被裁剪、恢复与交接。

界面可以实时观察 Agent 的工作过程。

编码任务可以进入交互式终端。

而权限边界、依赖链与运行环境,也被放在需要认真面对的位置。

从一次模型请求,到一个能够使用工具、管理状态、处理流式事件并持续完成任务的智能体,Pi 提供了一条可以逐层进入的路径。

它让 AI Agent 不只是一个会说话的接口,而是一套能够被组合、被观察、被控制,并真正进入开发工作流的运行系统。