在 AI Agent 领域,大家很容易陷入一种“堆料”思路:工具越来越多,配置越来越复杂,系统越来越像一台难以维护的巨型机器。
Pi 反其道而行。它选择做减法,把框架压缩到最小,只保留真正必要的部分。
它的核心设计理念,可以概括成一句话:
一个调用 LLM 的 while 循环,加上四个基础工具。
Pi 是一个极简、可扩展的 AI 编码智能体框架(harness)。它并不试图替模型“想太多”,而是默认一个前提:像 Claude Sonnet 这样的模型,本身已经具备足够强的推理与规划能力。框架真正要做的,不是替它设计复杂工作流,而是给它一个稳定、清晰、低摩擦的执行环境。
这也是 OpenClaw 建立在 Pi 之上后依然能保持高效的原因。它强大,不是因为设计复杂,而是因为设计克制。
一句话理解 Pi
如果要用最通俗的话解释 Pi,可以把它理解为:
- 用户提出任务。
- LLM 决定下一步要做什么。
- 框架执行工具调用,并把结果返回给 LLM。
- 重复这个过程,直到任务完成。
也就是说,Pi 的本质并不是一套庞大的 Agent 基础设施,而是一个非常稳定的“对话 - 执行 - 反馈”循环。
核心循环:Pi 如何驱动一个 Agent
下面这段代码,基本就是 Pi 的核心运行方式:
// 外层循环:处理后续消息队列
while (true) {
let hasMoreToolCalls = true;
let steeringAfterTools: AgentMessage[] | null = null;
// 内层循环:处理工具调用和转向消息
while (hasMoreToolCalls || pendingMessages.length > 0) {
// 1. 处理待处理消息(用户新输入或转向消息)
if (pendingMessages.length > 0) {
for (const message of pendingMessages) {
currentContext.messages.push(message);
}
pendingMessages = [];
}
// 2. 调用LLM获取响应
const message = await streamAssistantResponse(currentContext, config, signal, stream);
// 3. 检查是否有工具调用
const toolCalls = message.content.filter((c) => c.type === "toolCall");
hasMoreToolCalls = toolCalls.length > 0;
// 4. 执行工具调用
if (hasMoreToolCalls) {
const toolExecution = await executeToolCalls(
currentContext.tools, message, signal, stream, config.getSteeringMessages
);
// 工具结果会作为新消息加入上下文
for (const result of toolExecution.toolResults) {
currentContext.messages.push(result);
}
}
// 5. 检查转向消息(实时干预)
pendingMessages = (await config.getSteeringMessages?.()) || [];
}
// 6. 检查后续消息队列
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
if (followUpMessages.length > 0) {
pendingMessages = followUpMessages;
continue; // 继续外层循环
}
break; // 没有更多消息,结束
}
这个循环看上去朴素,但正是 Pi 的关键。
你可以把它理解成一个非常聪明的实习生:
- 你先告诉它任务,例如“帮我定位这个 bug”。
- 它会先想一想,然后决定“我要读这个文件”或“我要跑一个命令”。
- Pi 负责把这些动作执行掉,再把结果原样返回给它。
- 它再基于新结果继续判断下一步。
整个过程会一直持续,直到模型判断任务已经完成。
这里最重要的一点是:Pi 没有引入复杂状态机,也没有预定义大量工作流。它只是把“思考”和“行动”组织成一个稳定循环,让模型自己主导问题求解。
四个基础工具:少,但足够强
Pi 的另一层极简,体现在工具设计上。
// 四个基础工具:read, bash, edit, write
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool];
// 只读工具集(用于探索阶段)
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];
Pi 不追求提供几十上百个专用工具,而是只保留四个最核心的能力:
read:读取文件内容,相当于 AI 的眼睛。bash:执行 Shell 命令,相当于 AI 的通用操作入口。edit:对已有文件做精确修改。write:创建新文件。
这四个工具之所以够用,关键在于 bash。
一旦 Agent 能稳定执行 Bash,它实际上就获得了对整个 Unix 工具链的访问能力。grep、find、git、curl、npm,这些成熟工具都可以直接复用。与其为每个场景单独造一个“搜索工具”“下载工具”“测试工具”,不如把现成的系统能力交给模型。
换句话说,Pi 的思路不是“给 AI 更多玩具”,而是“给 AI 一把真正的万能钥匙”。
技能系统:按需加载,而不是一开始全塞进去
Pi 虽然基础工具很少,但并不意味着它不能扩展。它的扩展方式是 Skill。
export interface Skill {
name: string; // 技能名称
description: string; // 技能描述(AI根据这个决定何时加载)
filePath: string; // 技能文件路径
baseDir: string; // 技能根目录
source: string; // 来源(user/project/path)
disableModelInvocation: boolean; // 是否禁用自动调用
}
// 加载技能
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
// 从多个位置加载:
// - ~/.pi/agent/skills/ (全局)
// - .pi/skills/ (项目本地)
// - CLI --skill 参数指定的路径
}
// 将技能格式化为系统提示词
export function formatSkillsForPrompt(skills: Skill[]): string {
const lines = [
"The following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
...
"<available_skills>",
];
for (const skill of visibleSkills) {
lines.push(` <skill>`);
lines.push(` <name>${skill.name}</name>`);
lines.push(` <description>${skill.description}</description>`);
lines.push(` <location>${skill.filePath}</location>`);
lines.push(` </skill>`);
}
return lines.join("\n");
}
一个 Skill 本质上就是一个 SKILL.md 文件。里面不是代码插件,而是自然语言形式的操作说明:什么场景下该用它、该如何一步步完成任务。
它的关键价值在于“按需加载”:
- Pi 启动时,只告诉模型“有哪些技能存在”。
- 模型判断当前任务需要某个技能时,再主动去读取对应的
SKILL.md。 - 读完之后,再按照里面的步骤执行。
这样做有两个直接好处:
- 避免一开始就把大量说明塞进上下文,减少信息负担。
- 扩展成本极低,新增能力往往只需要增加一个文档化的技能文件。
这使 Pi 的扩展机制更像“说明书系统”,而不是传统意义上的插件系统。
转向与后续:用户可以随时介入
很多 Agent 框架的问题不在于能力不够,而在于一旦跑起来,用户很难中途纠偏。
Pi 专门设计了两类消息队列来解决这个问题:
export class Agent {
private steeringQueue: AgentMessage[] = []; // 转向队列
private followUpQueue: AgentMessage[] = []; // 后续队列
/**
* 发送转向消息:中断当前执行
* 在当前工具执行完成后立即处理,跳过剩余工具
*/
steer(m: AgentMessage) {
this.steeringQueue.push(m);
}
/**
* 发送后续消息:在当前轮次完全结束后处理
*/
followUp(m: AgentMessage) {
this.followUpQueue.push(m);
}
}
在 agent-loop.ts 中,它们的作用也很直接:
// 处理转向消息
async function executeToolCalls(...) {
for (let index = 0; index < toolCalls.length; index++) {
const toolCall = toolCalls[index];
// 执行工具...
const result = await tool.execute(...);
// 关键:每个工具执行后检查转向消息
if (getSteeringMessages) {
const steering = await getSteeringMessages();
if (steering.length > 0) {
steeringMessages = steering;
// 跳过剩余工具调用
const remainingCalls = toolCalls.slice(index + 1);
for (const skipped of remainingCalls) {
results.push(skipToolCall(skipped, stream));
}
break;
}
}
}
return { toolResults: results, steeringMessages };
}
这两个队列分别对应两种不同的交互意图:
steer:立刻纠偏。当前工具执行完后,马上处理新的指令,并跳过原计划中剩余的工具调用。followUp:稍后处理。等这一轮任务结束后,再接着处理补充要求。
这个设计非常重要,因为它把控制权保留在用户手里。Agent 不是一旦开始就必须“一路跑到底”,而是可以像与人协作一样,随时被提醒、打断、修正方向。
记忆策略:代码是真实来源,摘要只是压缩手段
Pi 在记忆问题上,也保持了同样的克制。
export interface CompactionSettings {
enabled: boolean;
reserveTokens: number; // 预留token数(默认16384)
keepRecentTokens: number; // 保留最近对话token数(默认20000)
}
// 当上下文窗口快满时,压缩历史
export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
return contextTokens > contextWindow - settings.reserveTokens;
}
// 压缩策略:保留最近N个token的对话,更早的用LLM总结
export function findCutPoint(
entries: SessionEntry[],
startIndex: number,
endIndex: number,
keepRecentTokens: number,
): CutPointResult {
// 从最新消息开始,向前累积token
// 当超过keepRecentTokens时,在那个点"切断"
// 更早的内容会被LLM总结成摘要
}
很多 Agent 系统喜欢引入外部记忆库、向量数据库,试图让模型“记住更多”。Pi 的判断更直接:在编码任务里,代码库本身才是最可靠的信息源。
因此,它采用的是“上下文压缩”而不是“外部长期记忆”:
- 最近的对话保留原文,保证精确性。
- 更早的历史压缩成摘要,节省上下文空间。
- 真正需要确认的事实,始终回到代码和文件本身去读取。
可以把它理解成下面这样:
┌─────────────────────────────────────────────────────────┐
│ 上下文窗口(有限空间) │
├─────────────────────────────────────────────────────────┤
│ 【远期】LLM 总结的摘要 │
├─────────────────────────────────────────────────────────┤
│ 【近期】完整的详细对话 │
└─────────────────────────────────────────────────────────┘
这种策略的优势很现实:
- 不需要维护额外的记忆基础设施。
- 降低因过期记忆带来的偏差与幻觉。
- 强制 Agent 养成“以代码为准”的工作方式。
Pi 为什么有吸引力
Pi 最有意思的地方,不是它加了多少能力,而是它刻意不加什么。
它没有试图用复杂系统替代模型本身的能力,也没有把框架做成一套沉重的工作流平台。它只是围绕几个关键问题给出足够好的答案:
- 如何让模型稳定地循环思考与执行。
- 如何用最少工具覆盖最多场景。
- 如何按需扩展,而不是预先灌入一切。
- 如何让用户可以随时介入。
- 如何在长上下文下仍然保持信息可信。
Pi 的价值,不在于功能堆叠,而在于它对边界的克制。一个循环、四个工具、按需加载的技能,以及清晰的用户干预机制,就足以构成一个强大、稳定、可扩展的 AI 编码框架。
这也是它给 Agent 设计带来的一个重要提醒:很多时候,真正难的不是“再加一个能力”,而是判断什么不该加。