OpenClaw 的框架内核:Pi

在 AI Agent 领域,大家很容易陷入一种“堆料”思路:工具越来越多,配置越来越复杂,系统越来越像一台难以维护的巨型机器。

Pi 反其道而行。它选择做减法,把框架压缩到最小,只保留真正必要的部分。

它的核心设计理念,可以概括成一句话:

一个调用 LLM 的 while 循环,加上四个基础工具。

Pi 是一个极简、可扩展的 AI 编码智能体框架(harness)。它并不试图替模型“想太多”,而是默认一个前提:像 Claude Sonnet 这样的模型,本身已经具备足够强的推理与规划能力。框架真正要做的,不是替它设计复杂工作流,而是给它一个稳定、清晰、低摩擦的执行环境。

这也是 OpenClaw 建立在 Pi 之上后依然能保持高效的原因。它强大,不是因为设计复杂,而是因为设计克制。

一句话理解 Pi

如果要用最通俗的话解释 Pi,可以把它理解为:

  1. 用户提出任务。
  2. LLM 决定下一步要做什么。
  3. 框架执行工具调用,并把结果返回给 LLM。
  4. 重复这个过程,直到任务完成。

也就是说,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 工具链的访问能力。grepfindgitcurlnpm,这些成熟工具都可以直接复用。与其为每个场景单独造一个“搜索工具”“下载工具”“测试工具”,不如把现成的系统能力交给模型。

换句话说,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 文件。里面不是代码插件,而是自然语言形式的操作说明:什么场景下该用它、该如何一步步完成任务。

它的关键价值在于“按需加载”:

  1. Pi 启动时,只告诉模型“有哪些技能存在”。
  2. 模型判断当前任务需要某个技能时,再主动去读取对应的 SKILL.md
  3. 读完之后,再按照里面的步骤执行。

这样做有两个直接好处:

  • 避免一开始就把大量说明塞进上下文,减少信息负担。
  • 扩展成本极低,新增能力往往只需要增加一个文档化的技能文件。

这使 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 设计带来的一个重要提醒:很多时候,真正难的不是“再加一个能力”,而是判断什么不该加。

相关文章