第一时间捕获有价值的信号
本文译自 What I learned building an opinionated and minimal coding agent。Pi 是 Openclaw 背后的极简主义编码 Agent,它用不到 1000 Token 的系统提示词和四个核心工具,实现了与 Claude Code、Codex 等主流工具相媲美的性能,同时提供了完全的上下文可控性。

在过去三年里,我一直在使用 LLM 进行辅助编程。如果你正在阅读这篇文章,很可能也经历过同样的演化过程:从把代码复制粘贴到 ChatGPT,到 Copilot 自动补全(对我来说从来没好用过),再到 Cursor,最后是 Claude Code、Codex、Amp、Droid、opencode 这些 2025 年成为我们日常工具的新一代编码 Agent 框架。
大部分工作我都更倾向于使用 Claude Code。在使用 Cursor 一年半之后,它是我四月份尝试的第一个工具。那时候它还很基础,这恰恰完美契合我的工作流,因为我就是个喜欢简单、可预测工具的简单人。但在过去几个月里,Claude Code 变成了一艘宇宙飞船,80% 的功能我都用不上。而且系统提示词和工具定义在每个版本都会变化,这会破坏我的工作流并改变模型行为。我讨厌这点。另外,它还会闪烁。
这些年来我也构建了不少各种复杂度的 Agent。比如 Sitegeist,我的小浏览器 Agent,本质上就是一个运行在浏览器里的编码 Agent。在所有这些工作中,我学到了上下文工程(context engineering)是至关重要的。精确控制进入模型上下文的内容能带来更好的输出,特别是在写代码的时候。而现有的框架通过在背后偷偷注入 UI 里甚至都不显示的内容,让这变得极其困难甚至不可能。
说到显示内容,我想要检查与模型交互的每个方面。基本上没有框架允许这点。我还想要一个有清晰文档的会话格式,能够自动进行后处理,以及一个简单的方式在 Agent 核心之上构建替代 UI。虽然现有的框架部分能做到这些,但它们的 API 闻起来就像是自然演化出来的。这些解决方案沿途积累了很多包袱,这体现在开发者体验上。我不是在指责任何人。如果有很多人在用你的东西,而你需要某种向后兼容性,这就是你要付出的代价。
我也尝试过自托管,无论是本地还是在 DataCrunch 上。虽然有些框架比如 opencode 支持自托管模型,但通常效果不太好。主要是因为它们依赖像 Vercel AI SDK 这样的库,由于某些原因,这些库与自托管模型配合得不好,特别是在工具调用方面。
那么,一个对着 Claude 大喊大叫的老家伙该怎么办呢?他得写自己的编码 Agent 框架,还给它起一个完全没法搜索的名字,这样就永远不会有用户了。这意味着 GitHub Issue 追踪器里也永远不会有问题。这能有多难呢?
为了实现这个目标,我需要构建:
- pi-ai:一个统一的 LLM API,支持多提供商(Anthropic、OpenAI、Google、xAI、Groq、Cerebras、OpenRouter,以及任何兼容 OpenAI 的端点)、流式输出、带 TypeBox Schema 的工具调用、思考/推理支持、无缝的跨提供商上下文切换,以及 Token 和成本追踪。
- pi-agent-core:一个处理工具执行、验证和事件流的 Agent 循环。
- pi-tui:一个极简的终端 UI 框架,支持差分渲染、同步输出实现(几乎)无闪烁更新,以及带自动补全和 Markdown 渲染的编辑器等组件。
- pi-coding-agent:真正的 CLI 工具,把所有东西整合在一起,提供会话管理、自定义工具、主题和项目上下文文件。
我在这一切中的理念是:如果我不需要它,就不会构建它。而我不需要的东西可多了。
pi-ai 和 pi-agent-core
我不打算用这个包的 API 细节来烦你。你可以在 README.md 里读到所有内容。相反,我想记录在创建统一 LLM API 时遇到的问题以及我是如何解决的。我不是说我的解决方案是最好的,但它们在各种 Agent 和非 Agent LLM 项目中一直工作得很好。
其实只有四个 API
要和几乎所有 LLM 提供商对话,其实真的只需要四个 API:OpenAI 的 Completions API、他们更新的 Responses API、Anthropic 的 Messages API,以及 Google 的 Generative AI API。
它们在功能上都非常相似,所以在它们之上构建抽象并不是火箭科学。当然,你必须处理提供商特有的特性。对于 Completions API 来说尤其如此,因为几乎所有提供商都使用这个 API,但每个提供商对这个 API 应该做什么有不同的理解。例如,虽然 OpenAI 在他们的 Completions API 中不支持推理追踪,但其他提供商在他们版本的 Completions API 中支持。这也适用于像 llama.cpp、Ollama、vLLM 和 LM Studio 这样的推理引擎。
例如,在 openai-completions.ts 中:
- Cerebras、xAI、Mistral 和 Chutes 不喜欢
store字段 - Mistral 和 Chutes 使用
max_tokens而非max_completion_tokens - Cerebras、xAI、Mistral 和 Chutes 不支持系统提示词的
developer角色 - Grok 模型不喜欢
reasoning_effort - 不同的提供商在不同的字段中返回推理内容(
reasoning_contentvsreasoning)
为了确保所有功能在无数提供商中真正可用,pi-ai 有一个相当广泛的测试套件,覆盖图像输入、推理追踪、工具调用,以及你期望从 LLM API 获得的其他功能。测试在所有支持的提供商和流行模型上运行。虽然这是一个很好的努力,但它仍然不能保证新模型和提供商会开箱即用。
另一个很大的区别是提供商如何报告 Token 和缓存读取/写入。Anthropic 的方法最明智,但总的来说这就是狂野西部。有些在 SSE 流开始时报告 Token 数量,有些只在结束时报告,这使得如果请求被中止,准确的成本追踪就不可能了。雪上加霜的是,你不能提供唯一 ID 来稍后与他们的账单 API 关联,弄清楚你的哪个用户消耗了多少 Token。所以 pi-ai 只能尽最大努力进行 Token 和缓存追踪。对于个人使用来说足够好,但如果你有最终用户通过你的服务消耗 Token,这对准确计费来说还不够。
特别表扬 Google,直到今天他们似乎仍然不支持工具调用流式输出,这非常 Google。
pi-ai 也可以在浏览器中工作,这对于构建基于 Web 的界面很有用。有些提供商让这变得特别容易,因为它们支持 CORS,特别是 Anthropic 和 xAI。
上下文切换
在提供商之间进行上下文切换是 pi-ai 从一开始就设计支持的功能。由于每个提供商都有自己追踪工具调用和思考追踪的方式,这只能是一个尽力而为的事情。例如,如果你在会话中途从 Anthropic 切换到 OpenAI,Anthropic 的思考追踪会被转换为助手消息内的内容块,用 <thinking></thinking> 标签分隔。这可能合理也可能不合理,因为 Anthropic 和 OpenAI 返回的思考追踪实际上并不代表幕后发生的事情。
这些提供商还会在事件流中插入签名的 blob,你必须在包含相同消息的后续请求中重放这些 blob。当在同一个提供商内切换模型时,这也适用。这在后台造成了一个繁琐的抽象和转换管道。
我很高兴地报告,pi-ai 中的跨提供商上下文切换和上下文序列化/反序列化工作得非常好:
import { getModel, complete, Context } from '@mariozechner/pi-ai';
// 从 Claude 开始
const claude = getModel('anthropic', 'claude-sonnet-4-5');
const context: Context = {
messages: []
};
context.messages.push({ role: 'user', content: 'What is 25 * 18?' });
const claudeResponse = await complete(claude, context, {
thinkingEnabled: true
});
context.messages.push(claudeResponse);
// 切换到 GPT — 它会将 Claude 的思考视为 <thinking> 标签文本
const gpt = getModel('openai', 'gpt-5.1-codex');
context.messages.push({ role: 'user', content: 'Is that correct?' });
const gptResponse = await complete(gpt, context);
context.messages.push(gptResponse);
// 切换到 Gemini
const gemini = getModel('google', 'gemini-2.5-flash');
context.messages.push({ role: 'user', content: 'What was the question?' });
const geminiResponse = await complete(gemini, context);
// 将上下文序列化为 JSON(用于存储、传输等)
const serialized = JSON.stringify(context);
// 稍后:反序列化并继续使用任何模型
const restored: Context = JSON.parse(serialized);
restored.messages.push({ role: 'user', content: 'Summarize our conversation' });
const continuation = await complete(claude, restored);
我们生活在多模型世界
说到模型,我想要一个类型安全的方式在 getModel 调用中指定它们。为此我需要一个可以转换为 TypeScript 类型的模型注册表。我将来自 OpenRouter 和 models.dev(由 opencode 的人创建,谢谢,这超级有用)的数据解析到 models.generated.ts 中。这包括 Token 成本和功能,如图像输入和思考支持。
如果我需要添加一个不在注册表中的模型,我想要一个类型系统让创建新模型变得容易。这在使用自托管模型、尚未出现在 models.dev 或 OpenRouter 上的新版本,或者尝试一个更小众的 LLM 提供商时特别有用:
import { Model, stream } from '@mariozechner/pi-ai';
const ollamaModel: Model<'openai-completions'> = {
id: 'llama-3.1-8b',
name: 'Llama 3.1 8B (Ollama)',
api: 'openai-completions',
provider: 'ollama',
baseUrl: 'http://localhost:11434/v1',
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000
};
const response = await stream(ollamaModel, context, {
apiKey: 'dummy' // Ollama 不需要真实的密钥
});
很多统一 LLM API 完全忽略了提供中止请求的方法。如果你想把 LLM 集成到任何生产系统中,这是完全不可接受的。很多统一 LLM API 也不向你返回部分结果,这有点荒谬。pi-ai 从一开始就设计为在整个管道中支持中止,包括工具调用。它是这样工作的:
import { getModel, stream } from '@mariozechner/pi-ai';
const model = getModel('openai', 'gpt-5.1-codex');
const controller = new AbortController();
// 2 秒后中止
setTimeout(() => controller.abort(), 2000);
const s = stream(model, {
messages: [{ role: 'user', content: 'Write a long story' }]
}, {
signal: controller.signal
});
for await (const event of s) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta);
} else if (event.type === 'error') {
console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);
}
}
// 获取结果(如果被中止可能是部分的)
const response = await s.result();
if (response.stopReason === 'aborted') {
console.log('Partial content:', response.content);
}
结构化分离的工具结果
我在任何统一 LLM API 中都没见过的另一个抽象是将工具结果分成两部分:一部分交给 LLM,一部分用于 UI 显示。LLM 部分通常只是文本或 JSON,不一定包含你想在 UI 中显示的所有信息。解析文本工具输出并为 UI 显示重新构建它们也非常糟糕。pi-ai 的工具实现允许同时返回给 LLM 的内容块和单独的 UI 渲染内容块。工具还可以返回像图像这样的附件,以相应提供商的原生格式附加。工具参数使用 TypeBox Schema 和 AJV 自动验证,验证失败时提供详细的错误消息:
import { Type, AgentTool } from '@mariozechner/pi-ai';
const weatherSchema = Type.Object({
city: Type.String({ minLength: 1 }),
});
const weatherTool: AgentTool<typeof weatherSchema, { temp: number }> = {
name: 'get_weather',
description: 'Get current weather for a city',
parameters: weatherSchema,
execute: async (toolCallId, args) => {
const temp = Math.round(Math.random() * 30);
return {
// 给 LLM 的文本
output: `Temperature in ${args.city}: ${temp}°C`,
// 给 UI 的结构化数据
details: { temp }
};
}
};
// 工具也可以返回图像
const chartTool: AgentTool = {
name: 'generate_chart',
description: 'Generate a chart from data',
parameters: Type.Object({ data: Type.Array(Type.Number()) }),
execute: async (toolCallId, args) => {
const chartImage = await generateChartImage(args.data);
return {
content: [
{ type: 'text', text: `Generated chart with ${args.data.length} data points` },
{ type: 'image', data: chartImage.toString('base64'), mimeType: 'image/png' }
]
};
}
};
仍然缺少的是工具结果流式输出。想象一个 bash 工具,你想在 ANSI 序列进来时显示它们。这目前不可能,但这是一个简单的修复,最终会加入到这个包中。
在工具调用流式输出期间进行部分 JSON 解析对于良好的 UX 至关重要。当 LLM 流式输出工具调用参数时,pi-ai 逐步解析它们,这样你可以在调用完成前在 UI 中显示部分结果。例如,你可以在 Agent 重写文件时显示差异流式输出。
极简 Agent 脚手架
最后,pi-ai 提供了一个 Agent 循环,处理完整的编排:处理用户消息、执行工具调用、将结果反馈给 LLM,并重复直到模型产生不带工具调用的响应。循环还支持通过回调进行消息排队:在每个回合之后,它询问排队的消息并在下一个助手响应之前注入它们。循环为所有事情发出事件,使得构建响应式 UI 变得容易。
Agent 循环不允许你指定最大步数或你在其他统一 LLM API 中找到的类似旋钮。我从来没有发现它的用例,那为什么要添加它呢?循环只是一直循环,直到 Agent 说它完成了。然而,在循环之上,pi-agent-core 提供了一个带有真正有用东西的 Agent 类:状态管理、简化的事件订阅、两种模式的消息排队(一次一个或一次全部)、附件处理(图像、文档),以及一个传输抽象,让你可以直接或通过代理运行 Agent。
我对 pi-ai 满意吗?在大多数情况下,是的。像任何统一 API 一样,由于泄漏的抽象,它永远不可能完美。但它已在七个不同的生产项目中使用,并为我提供了极好的服务。
为什么要构建这个而不是使用 Vercel AI SDK 呢?Armin 的博客文章反映了我的经验。直接在提供商 SDK 之上构建给了我完全的控制权,让我可以按照自己想要的方式设计 API,表面积小得多。Armin 的博客给了你更深入的理由来解释为什么要构建自己的。去读那个吧。
pi-tui
我成长在 DOS 时代,所以终端用户界面是我从小就熟悉的。从 Doom 花哨的安装程序到 Borland 产品,TUI 一直陪伴我到 90 年代末。当我最终切换到 GUI 操作系统时,我真的非常高兴。虽然 TUI 大多是可移植的且易于流式传输,但它们在信息密度方面也很糟糕。说了这么多,我认为为 pi 从终端用户界面开始是最有意义的。以后只要我觉得需要,我可以随时加上 GUI。
那为什么要构建自己的 TUI 框架呢?我看过替代方案,比如 Ink、Blessed、OpenTUI 等等。我确信它们各有各的优点,但我绝对不想像写 React 应用那样写我的 TUI。Blessed 似乎大多没有维护,而 OpenTUI 明确表示不适合生产使用。而且,在 Node.js 之上写自己的 TUI 框架看起来是一个有趣的小挑战。
两种 TUI
编写终端用户界面本身并不是火箭科学。你只需要选择你的毒药。基本上有两种方法。一种是接管终端视口(你实际能看到的终端内容部分)并把它当成像素缓冲区。你拥有的不是像素,而是包含带有背景色、前景色和样式(如斜体和粗体)的字符的单元格。我把这些称为全屏 TUI。Amp 和 opencode 使用这种方法。
缺点是你失去了回滚缓冲区,这意味着你必须实现自定义搜索。你也失去了滚动,这意味着你必须自己在视口内模拟滚动。虽然这不难实现,但这意味着你必须重新实现你的终端模拟器已经提供的所有功能。鼠标滚动在这种 TUI 中总是感觉有点奇怪。
第二种方法就是像任何 CLI 程序一样写入终端,将内容追加到回滚缓冲区,只是偶尔在可见视口内稍微向上移动「渲染光标」一点,来重绘像动画微调器或文本编辑字段这样的东西。不完全是那么简单,但你懂的。这就是 Claude Code、Codex 和 Droid 所做的。
编码 Agent 有一个很好的特性,它们基本上就是聊天界面。用户写一个提示,后面跟着 Agent 的回复和工具调用及其结果。一切都是很好的线性关系,这很适合与「原生」终端模拟器一起工作。你可以使用所有内置功能,如自然滚动和在回滚缓冲区中搜索。这也在一定程度上限制了你的 TUI 能做什么,我觉得这很有吸引力,因为约束造就了只做它们应该做的事情而没有多余装饰的极简程序。这是我为 pi-tui 选择的方向。
保留模式 UI
如果你做过任何 GUI 编程,你可能听说过保留模式与即时模式。在保留模式 UI 中,你构建一个跨帧持续存在的组件树。每个组件知道如何渲染自己,如果没有变化可以缓存其输出。在即时模式 UI 中,你每帧从头重绘所有东西(虽然实际上,即时模式 UI 也做缓存,否则它们会崩溃)。
pi-tui 使用简单的保留模式方法。一个 Component 只是一个带有 render(width) 方法的对象,该方法返回一个字符串数组(水平适合视口的行,带有用于颜色和样式的 ANSI 转义序列),以及一个可选的 handleInput(data) 方法用于键盘输入。一个 Container 保存一个垂直排列的组件列表,并收集它们所有的渲染行。TUI 类本身就是一个编排一切的容器。
当 TUI 需要更新屏幕时,它要求每个组件渲染。组件可以缓存它们的输出:一个完全流式传输的助手消息不需要每次都重新解析 Markdown 和重新渲染 ANSI 序列。它只返回缓存的行。容器收集所有子组件的行。TUI 收集所有这些行并将它们与之前为之前组件树渲染的行进行比较。它保留一种后台缓冲区,记住写入回滚缓冲区的内容。
然后它只使用我称为差分渲染的方法重绘变化的部分。我很不擅长起名字,这可能有一个官方名称。
差分渲染
这里有一个简单的演示来说明究竟什么被重绘了。
算法很简单:
- 首次渲染:只需将所有行输出到终端
- 宽度变化:完全清除屏幕并重绘所有内容(软换行变化)
- 正常更新:找到与屏幕上第一行不同的行,将光标移动到该行,并从那里重绘到末尾
有一个问题:如果第一行变化的行在可见视口上方(用户向上滚动),我们必须进行完全清除和重绘。终端不允许你写入视口上方的回滚缓冲区。
为了防止更新期间闪烁,pi-tui 将所有渲染包装在同步输出转义序列(CSI ?2026h 和 CSI ?2026l)中。这告诉终端缓冲所有输出并原子地显示它。大多数现代终端都支持这一点。
它工作得有多好,闪烁有多少?在任何功能强大的终端中,如 Ghostty 或 iTerm2,这工作得非常出色,你永远看不到任何闪烁。在不太幸运的终端实现中,如 VS Code 的内置终端,你会根据时间、你的显示大小、窗口大小等等得到一些闪烁。鉴于我非常习惯 Claude Code,我没有花更多时间优化这一点。我对 VS Code 中得到的一点点闪烁很满意。否则我不会有宾至如归的感觉。而且它仍然比 Claude Code 闪烁得少。
这种方法有多浪费?我们存储了整个回滚缓冲区的先前渲染行,并且每次要求 TUI 渲染自身时都会重绘行。这通过我上面描述的缓存得到缓解,所以重绘不是什么大问题。我们仍然必须比较很多行。实际上,在 25 年内的计算机上,这在性能和内存使用方面都不是什么大问题(非常大的会话也就几百千字节)。谢谢 V8。我得到的回报是一个死简单的编程模型,让我可以快速迭代。
pi-coding-agent
我不需要解释你应该期望编码 Agent 框架有什么功能。pi 带有你从其他工具习惯的大多数舒适功能:
- 在 Windows、Linux 和 macOS 上运行(或任何带有 Node.js 运行时和终端的东西)
- 多提供商支持,会话中可切换模型
- 会话管理,支持继续、恢复和分支
- 项目上下文文件(AGENTS.md)从全局到项目特定的层次结构加载
- 常用操作的斜杠命令
- 自定义斜杠命令作为带参数支持的 Markdown 模板
- Claude Pro/Max 订阅的 OAuth 身份验证
- 通过 JSON 自定义模型和提供商配置
- 带实时重载的可定制主题
- 带模糊文件搜索、路径补全、拖放和多行粘贴的编辑器
- Agent 工作时的消息排队
- 支持视觉能力模型的图像
- 会话的 HTML 导出
- 通过 JSON 流式和 RPC 模式的无头操作
- 完整的成本和 Token 追踪
如果你想要完整的细节,阅读 README。更有趣的是 pi 在理念和实现上与其他框架的不同之处。
极简系统提示词
这是系统提示词:
You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
Available tools:
- read: Read file contents
- bash: Execute bash commands
- edit: Make surgical edits to files
- write: Create or overwrite files
Guidelines:
- Use bash for file operations like ls, grep, find
- Use read to examine files before editing
- Use edit for precise changes (old text must match exactly)
- Use write only for new files or complete rewrites
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
- Be concise in your responses
- Show file paths clearly when working with files
Documentation:
- Your own documentation (including custom model setup and theme creation) is at: /path/to/README.md
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, or create a custom theme.
就这些了。唯一在底部注入的是你的 AGENTS.md 文件。全局的(适用于你所有会话)和存储在项目目录中的项目特定的。这是你可以根据自己的喜好自定义 pi 的地方。如果你愿意,你甚至可以替换完整的系统提示词。相比之下,例如,Claude Code 的系统提示词、Codex 的系统提示词,或 opencode 的模型特定提示词(Claude 那个是他们复制的原始 Claude Code 提示词的精简版本)。
你可能认为这很疯狂。很可能,模型对它们的原生编码框架有一些训练。所以使用原生系统提示词或像 opencode 那样接近它的东西会是最理想的。但事实证明,所有前沿模型都经过了大量的 RL 训练,所以它们本质上理解什么是编码 Agent。似乎不需要 10000 Token 的系统提示词,正如我们稍后在基准测试部分会发现的,以及我在过去几周专门使用 pi 的轶事发现。Amp 虽然复制了原生系统提示词的某些部分,似乎也能用他们自己的提示词做得很好。
极简工具集
这是工具定义:
read
Read the contents of a file. Supports text files and images (jpg, png,
gif, webp). Images are sent as attachments. For text files, defaults to
first 2000 lines. Use offset/limit for large files.
- path: Path to the file to read (relative or absolute)
- offset: Line number to start reading from (1-indexed)
- limit: Maximum number of lines to read
write
Write content to a file. Creates the file if it doesn't exist, overwrites
if it does. Automatically creates parent directories.
- path: Path to the file to write (relative or absolute)
- content: Content to write to the file
edit
Edit a file by replacing exact text. The oldText must match exactly
(including whitespace). Use this for precise, surgical edits.
- path: Path to the file to edit (relative or absolute)
- oldText: Exact text to find and replace (must match exactly)
- newText: New text to replace the old text with
bash
Execute a bash command in the current working directory. Returns stdout
and stderr. Optionally provide a timeout in seconds.
- command: Bash command to execute
- timeout: Timeout in seconds (optional, no default timeout)
如果你想限制 Agent 修改文件或运行任意命令,还有额外的只读工具(grep、find、ls)。默认情况下这些是禁用的,所以 Agent 只获得上面四个工具。
事实证明,这四个工具就是一个高效编码 Agent 所需要的全部。模型知道如何使用 bash,并且已经用具有类似输入模式的 read、write 和 edit 工具进行过训练。相比之下,Claude Code 的工具定义或 opencode 的工具定义(它们显然源自 Claude Code 的,相同的结构、相同的示例、相同的 git 提交流程)。值得注意的是,Codex 的工具定义与 pi 的类似地极简。
pi 的系统提示词和工具定义加起来不到 1000 Token。
默认 YOLO 模式
pi 在完全 YOLO 模式下运行,并假设你知道自己在做什么。它可以不受限制地访问你的文件系统,可以在没有权限检查或安全护栏的情况下执行任何命令。没有针对文件操作或命令的权限提示。没有由 Haiku 对 bash 命令进行的恶意内容预检查。完全文件系统访问。可以用你的用户权限执行任何命令。
如果你看看其他编码 Agent 中的安全措施,它们大多是安全戏剧。一旦你的 Agent 可以写代码和运行代码,游戏基本就结束了。唯一可以防止数据泄露的方法是切断 Agent 运行的执行环境的所有网络访问,这会让 Agent 基本没用。另一种方法是允许列表域名,但这也可以通过其他方式绕过。
Simon Willison 已经广泛撰写关于这个问题的文章。他的「双 LLM」模式试图解决困惑代理攻击和数据泄露问题,但即使他也承认「这个解决方案非常糟糕」并引入了巨大的实现复杂性。核心问题仍然存在:如果 LLM 有权限可以读取私有数据和发出网络请求的工具,你就是在和攻击向量玩打地鼠。
既然我们无法解决这种能力三重奏(读取数据、执行代码、网络访问),pi 就直接屈服了。反正每个人都是在 YOLO 模式下运行以完成任何有成效的工作,那为什么不把它设为默认且唯一的选项呢?
默认情况下,pi 没有网页搜索或获取工具。然而,它可以使用 curl 或从磁盘读取文件,这两者都为提示注入攻击提供了充足的攻击面。文件或命令输出中的恶意内容可能会影响行为。如果你对完全访问感到不舒服,请在容器中运行 pi,或者如果你需要(虚假的)护栏,请使用不同的工具。
没有内置待办事项
pi 不支持也不会支持内置待办事项。根据我的经验,待办事项列表通常会让模型更加困惑而不是帮助它们。它们添加了模型必须追踪和更新的状态,这引入了更多出错的机会。
如果你需要任务追踪,让它通过写入文件成为外部有状态的:
# TODO.md
- [x] Implement user authentication
- [x] Add database migrations
- [ ] Write API documentation
- [ ] Add rate limiting
Agent 可以根据需要读取和更新此文件。使用复选框可以追踪已完成和剩余的内容。简单、可见且在你的控制之下。
没有计划模式
pi 不支持也不会有内置计划模式。告诉 Agent 和你一起思考问题,而不修改文件或执行命令,通常就足够了。
如果你需要跨会话的持久计划,将其写入文件:
# PLAN.md
## Goal
Refactor authentication system to support OAuth
## Approach
1. Research OAuth 2.0 flows
2. Design token storage schema
3. Implement authorization server endpoints
4. Update client-side login flow
5. Add tests
## Current Step
Working on step 3 - authorization endpoints
Agent 可以在工作时读取、更新和引用计划。与仅存在于会话中的临时计划模式不同,基于文件的计划可以跨会话共享,并且可以与你的代码一起版本化。
有趣的是,Claude Code 现在有一个计划模式,本质上是只读分析,它最终会将一个 Markdown 文件写入磁盘。而且你基本上不能在不批准大量命令调用的情况下使用计划模式,因为没有那个,计划基本是不可能的。
pi 的不同之处在于我对所有事情都有完全的可观察性。我可以看到 Agent 实际查看了哪些来源,完全错过了哪些。在 Claude Code 中,编排的 Claude 实例通常会生成一个子 Agent,而你对子 Agent 做了什么完全没有可见性。我可以立即看到 Markdown 文件。我可以与 Agent 协作编辑它。简而言之,我需要计划的可观察性,而我从 Claude Code 的计划模式中得不到这一点。
如果你必须在计划期间限制 Agent,你可以通过 CLI 指定它可以访问哪些工具:
pi --tools read,grep,find,ls
这为你提供了用于探索和计划的只读模式,而 Agent 不会修改任何东西或能够运行 bash 命令。不过你不会对此感到满意。
没有 MCP 支持
pi 不支持也不会支持 MCP。我已经就此写过很多,但简而言之是:MCP 服务器对于大多数用例来说过于复杂,并且它们带来了显著的上下文开销。
流行的 MCP 服务器如 Playwright MCP(21 个工具,13.7k Token)或 Chrome DevTools MCP(26 个工具,18k Token)在每个会话中都会将它们的整个工具描述倾倒入你的上下文。在你甚至开始工作之前,就已经失去了 7-9% 的上下文窗口。这些工具中的很多你在给定会话中永远都不会使用。
替代方案很简单:构建带有 README 文件的 CLI 工具。Agent 在需要工具时读取 README,仅在必要时支付 Token 成本(渐进式披露),并且可以使用 bash 来调用工具。这种方法是可组合的(管道输出、链式命令)、易于扩展(只需添加另一个脚本)并且 Token 高效。
这是我如何向 pi 添加网页搜索的方法:
我在 github.com/badlogic/agent-tools 维护这些工具的集合。每个工具都是一个简单的 CLI,带有 Agent 按需读取的 README。
如果你绝对必须使用 MCP 服务器,看看 Peter Steinberger 的 mcporter 工具,它将 MCP 服务器包装为 CLI 工具。
没有后台 bash
pi 的 bash 工具同步运行命令。没有内置方法可以启动开发服务器、在后台运行测试,或在命令仍在运行时与 REPL 交互。
这是故意的。后台进程管理增加了复杂性:你需要进程追踪、输出缓冲、退出时清理,以及向运行中的进程发送输入的方法。Claude Code 通过他们的后台 bash 功能处理了其中一些,但它的可观察性很差(Claude Code 的一个常见主题)并迫使 Agent 追踪运行中的实例而不提供查询它们的工具。在早期的 Claude Code 版本中,Agent 在上下文压缩后忘记了所有后台进程,并且无法查询它们,所以你必须手动杀死它们。这已经修复了。
改用 tmux。这是 pi 在 LLDB 中调试崩溃的 C 程序:
可观察性怎么样?相同的方法适用于长时间运行的开发服务器、查看日志输出和类似用例。如果你愿意,你可以通过 tmux 进入上面那个 LLDB 会话并与 Agent 一起调试。Tmux 还为你提供了一个 CLI 参数来列出所有活动会话。多好啊。
根本不需要后台 bash。Claude Code 也可以使用 tmux,你知道的。Bash 就是你所需要的全部。
没有子 Agent
pi 没有专门的子 Agent 工具。当 Claude Code 需要做一些复杂的事情时,它经常会生成一个子 Agent 来处理部分任务。你对子 Agent 做了什么完全没有可见性。这是黑盒中的黑盒。Agent 之间的上下文传输也很差。编排 Agent 决定传递给子 Agent 的初始上下文,而你通常对此几乎没有控制权。如果子 Agent 犯了错误,调试会很痛苦,因为你看不到完整的对话。
如果你需要 pi 生成自己,只需让它通过 bash 运行自己。你甚至可以让它在 tmux 会话中生成自己,以获得完全的可观察性和直接与该子 Agent 交互的能力。

但更重要的是:修复你的工作流,至少是那些完全关于上下文收集的工作流。人们在会话中使用子 Agent,认为他们在节省上下文空间,这是真的。但这是思考子 Agent 的错误方式。在会话中途使用子 Agent 进行上下文收集表明你没有提前计划。如果你需要收集上下文,首先在它自己的会话中完成。创建一个工件,你可以稍后在新会话中使用它来给你的 Agent 所需的所有上下文,而不用工具输出污染它的上下文窗口。那个工件也可以用于下一个功能,你获得了完全的可观察性和可引导性,这在上下文收集期间很重要。
因为尽管人们普遍相信,模型仍然不善于找到实现新功能或修复错误所需的所有上下文。我将此归因于模型被训练为只读取文件的部分而不是完整文件,所以它们不愿意读取所有内容。这意味着它们错过了重要的上下文,看不到它们需要的内容来正确完成任务。
只要看看 pi-mono 问题追踪器和拉取请求。很多被关闭或修改,因为 Agent 无法完全理解需要什么。这不是贡献者的错,我真的很感激,因为即使是不完整的 PR 也帮助我更快地前进。这只是意味着我们过于信任我们的 Agent。
我不是完全否定子 Agent。有有效的用例。我最常见的是代码审查:我告诉 pi 通过自定义斜杠命令生成自己并使用代码审查提示词,然后它获得输出。
---
description: Run a code review sub-agent
---
Spawn yourself as a sub-agent via bash to do a code review: $@
Use `pi --print` with appropriate arguments. If the user specifies a model,
use `--provider` and `--model` accordingly.
Pass a prompt to the sub-agent asking it to review the code for:
- Bugs and logic errors
- Security issues
- Error handling gaps
Do not read the code yourself. Let the sub-agent do that.
Report the sub-agent's findings.
这是我如何使用它来审查 GitHub 上的拉取请求:
通过一个简单的提示,我可以选择我想要审查的具体内容以及使用什么模型。如果我愿意,我甚至可以设置思考级别。我还可以将完整的审查会话保存到文件中,并在另一个 pi 会话中进入它。或者我可以说这是一个临时会话,不应该保存到磁盘。所有这些都被翻译成一个提示,主 Agent 读取它,然后基于它通过 bash 再次执行自己。虽然我没有获得子 Agent 内部工作的完全可观察性,但我获得了其输出的完全可观察性。其他框架没有真正提供的东西,这对我来说毫无意义。
当然,这有点模拟用例。实际上,我只会生成一个新的 pi 会话并让它审查拉取请求,可能把它拉到本地分支。在我看到它的初步审查后,我给出自己的审查,然后我们一起处理直到它变好。这是我用来不合并垃圾代码的工作流。
生成多个子 Agent 来并行实现各种功能在我看来是一种反模式,并且不起作用,除非你不关心你的代码库是否退化成一堆垃圾。
基准测试
我做了很多宏大的主张,但我有数字证据证明我上面说的所有这些 contrarian 的东西实际上有效吗?我有我的生活经验,但这很难在博客文章中传达,你只能相信我。所以我为 pi 创建了一个使用 Claude Opus 4.5 的 Terminal-Bench 2.0 测试运行,让它与 Codex、Cursor、Windsurf 和其他编码框架及其各自的原生模型竞争。显然,我们都知道基准测试不代表真实世界的性能,但这是我能提供给你的最好证据,证明我所说的并非完全是胡说八道。
我进行了一次完整的运行,每个任务进行了五次试验,这使得结果有资格提交到排行榜。我还开始了第二次运行,只在 CET 期间运行,因为我发现一旦 PST 上线,错误率(以及因此基准测试结果)会变差。这是第一次运行的结果:

这是截至 2025 年 12 月 2 日 pi 在当前排行榜上的位置:

这是我提交给 Terminal-Bench 人员以包含在排行榜中的 results.json 文件。pi 的基准运行器可以在这个仓库中找到,如果你想重现结果。我建议你使用你的 Claude 计划而不是按需付费。
最后,这是仅 CET 运行的一瞥:

这还需要一天左右才能完成。一旦完成,我会更新这篇博客文章。
还要注意排行榜上 Terminus 2 的排名。Terminus 2 是 Terminal-Bench 团队自己的极简 Agent,它只给模型一个 tmux 会话。模型将命令作为文本发送到 tmux 并自己解析终端输出。没有花哨的工具,没有文件操作,只有原始终端交互。而且它在与具有更复杂工具的 Agent 竞争中表现不俗,并且可以与各种模型一起工作。更多证据表明极简方法可以做得同样好。
总结
基准测试结果很有趣,但真正的证明在于实际使用。而我的证明就是我的日常工作,pi 一直表现出色。Twitter 上充满了上下文工程的帖子和博客,但我觉得我们目前拥有的框架中没有一个真正让你进行上下文工程。pi 是我为自己构建一个尽可能让我掌控的工具的尝试。
我对 pi 的现状相当满意。还有一些功能我想添加,比如压缩或工具结果流式输出,但我认为我个人不会需要更多东西了。缺少压缩对我个人来说不是问题。出于某种原因,我能够在单个会话中塞进数百次交流,这在没有压缩的情况下我用 Claude Code 是做不到的。
也就是说,我欢迎贡献。但就像我所有的开源项目一样,我倾向于独裁。这是我多年来从我的更大项目中吸取的教训。如果我关闭了你发送的问题或 PR,我希望没有硬 feelings。我也会尽我最大的努力给你原因。我只是想保持这个专注和可维护。如果 pi 不满足你的需求,我恳请你 Fork 它。我是认真的。如果你创建了更符合我需求的东西,我会很高兴加入你的努力。
我认为上面的一些经验也可以转移到其他框架。让我知道你的进展如何。