本文翻译自 What I learned building an opinionated and minimal coding agent,版权归原作者所有。

虽然微不足道,但却是我的作品
过去三年里,我一直使用大语言模型辅助编程。如果你在读这篇文章,你可能经历了同样的演变:从将代码复制粘贴到 ChatGPT,到 Copilot 自动补全(这对我从来没用),再到 Cursor,最后是 2025 年成为我们日常驱动的新一代编程代理框架,如 Claude Code、Codex、Amp、Droid 和 opencode。
我大部分工作都偏爱 Claude Code。早在四月份,在使用 Cursor 一年半后,我就第一次尝试了它。当时它要简单得多。这完美契合我的工作流,因为我就是个喜欢简单、可预测工具的简单人。在过去的几个月里,Claude Code 变成了一艘飞船,其中 80% 的功能我根本用不上。系统提示词和工具每次发布都会变化,这会破坏我的工作流并改变模型行为。我讨厌这样。而且,它还会闪烁。
多年来,我也构建过各种复杂程度的代理。例如,Sitegeist,我的小浏览器使用代理,本质上是一个生活在浏览器内的编程代理。在所有这些工作中,我学到了上下文工程至关重要。精确控制进入模型上下文的内容会产生更好的输出,尤其是在编写代码时。现有的框架通过在背后注入甚至未在 UI 中显示的内容,使得这一点极其困难或不可能。
说到暴露内容,我想检查与模型交互的方方面面。基本上没有框架允许这样做。我还想要一个清晰记录的会话格式,可以自动后处理,以及一种在代理核心之上构建替代 UI 的简单方法。虽然其中一些在现有框架中是可能的,但这些 API 闻起来像是有机演化的产物。这些解决方案在过程中积累了包袱,这在开发者体验中体现出来。我不是在责怪任何人。如果成千上万的人使用你的东西,你需要某种向后兼容性,这就是你要付出的代价。
我还尝试过自托管,无论是在本地还是在 DataCrunch。虽然像 opencode 这样的框架支持自托管模型,但通常效果不佳。主要是因为它们依赖于像 Vercel AI SDK 这样的库,出于某种原因,它与自托管模型不太兼容,特别是在工具调用方面。
那么,一个对 Claude 大喊大叫的老家伙会怎么做呢?他会编写自己的编程代理框架,并给它起一个完全无法在 Google 上搜索到的名字,这样就不会有任何用户。这意味着 GitHub 问题跟踪器上也永远不会有任何问题。这能有多难呢?
为了让这一切奏效,我需要构建:
- pi-ai: 一个统一的 LLM API,支持多提供者(Anthropic、OpenAI、Google、xAI、Groq、Cerebras、OpenRouter 和任何 OpenAI 兼容端点)、流式传输、带 TypeBox 架构的工具调用、思考/推理支持、无缝跨提供者上下文交接,以及令牌和成本跟踪。
- pi-agent-core: 一个代理循环,处理工具执行、验证和事件流式传输。
- pi-tui: 一个极简的终端 UI 框架,具有差异渲染、同步输出以实现(几乎)无闪烁更新,以及带自动补全和 Markdown 渲染的编辑器等组件。
- pi-coding-agent: 实际的 CLI,将所有内容串联在一起,具有会话管理、自定义工具、主题和项目上下文文件。
我在所有这些方面的哲学是:如果我不需要它,就不会构建它。而我需要的东西不多。
pi-ai 和 pi-agent-core
我不会用这个包的 API 细节来烦你。你可以在 README.md 中阅读所有内容。相反,我想记录我在创建统一 LLM API 时遇到的问题以及如何解决它们。我并不声称我的解决方案是最好的,但它们在各种代理和非代理 LLM 项目中一直运作良好。
只有四种 API
实际上,你只需要掌握四种 API 就可以与几乎所有 LLM 提供者通信:OpenAI 的 Completions API、他们更新的 Responses API、Anthropic 的 Messages API 和 Google 的 Generative AI API。
它们在功能上都非常相似,因此在它们之上构建抽象并不是火箭科学。当然,你必须照顾提供者特定的怪癖。这对于 Completions 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 获得的功能。测试在所有支持的提供者和流行模型上运行。虽然这是一个很好的努力,但它仍然不能保证新模型和新提供者会开箱即用。
另一个重大差异是提供者如何报告令牌和缓存读取/写入。Anthropic 的方法最合理,但总的来说这里就像狂野西部——一片混乱、各行其是。有些在 SSE 流开始时报告令牌计数,有些只在结束时报告,如果请求被中止,这使得准确的成本跟踪变得不可能。更糟糕的是,你无法提供唯一的 ID 来之后与他们的计费 API 关联,弄清楚你的哪个用户消耗了多少令牌。所以 pi-ai 在尽力而为的基础上进行令牌和缓存跟踪。对于个人使用来说足够了,但如果你最终用户通过你的服务消耗令牌,则不适合准确的计费。
特别点名 Google,他们到目前为止似乎不支持工具调用流式传输,这非常 Google。
pi-ai 也可以在浏览器中工作,这对于构建基于 Web 的界面很有用。一些提供者使这特别容易,通过支持 CORS,特别是 Anthropic 和 xAI。
上下文交接
提供者之间的上下文交接是 pi-ai 从一开始就设计的功能。由于每个提供者都有自己的跟踪工具调用和思考跟踪的方式,这只能是尽力而为的事情。例如,如果你在中途从 Anthropic 切换到 OpenAI,Anthropic 思考跟踪会被转换为助手消息中的内容块,用 <thinking></thinking> 标签分隔。这是否合理还有待商榷,因为 Anthropic 和 OpenAI 返回的思考跟踪实际上并不代表幕后发生的事情。
这些提供者还会在事件流中插入签名 blob,你必须在包含相同消息的后续请求中重放它们。这也适用于提供者内切换模型。这使得后台的抽象和转换管道变得繁琐。
我很高兴地报告,跨提供者上下文交接和上下文序列化/反序列化在 pi-ai 中运作良好:
import { getModel, complete, Context } from '@mariozechner/pi-ai';
// Start with 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);
// Switch to GPT - it will see Claude's thinking as <thinking> tagged text
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);
// Switch to 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);
// Serialize context to JSON (for storage, transfer, etc.)
const serialized = JSON.stringify(context);
// Later: deserialize and continue with any model
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。这包括令牌成本和图像输入及思考支持等功能。
如果我需要添加一个不在注册表中的模型,我想要一个类型系统,使其易于创建新模型。这在处理自托管模型、尚未在 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 doesn't need a real key
});
许多统一 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();
// Abort after 2 seconds
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);
}
}
// Get results (may be partial if aborted)
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 架构和 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 {
// Text for the LLM
output: `Temperature in ${args.city}: ${temp}°C`,
// Structured data for the UI
details: { temp }
};
}
};
// Tools can also return images
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 中显示部分结果。例如,你可以在代理重写文件时显示流式传输的 diff。
极简代理脚手架
最后,pi-ai 提供了一个 代理循环,处理完整的编排:处理用户消息、执行工具调用、将结果反馈给 LLM,并重复直到模型产生不带工具调用的响应。循环还通过回调支持消息队列:每轮之后,它会询问队列中的消息并在下一个助手响应之前注入它们。循环为所有事情发出事件,使得构建响应式 UI 变得容易。
代理循环不允许你指定最大步骤或你在其他统一 LLM API 中找到的类似旋钮。我从未找到过这种用例,所以为什么要添加它?循环只是循环直到代理说它完成了。然而,在循环之上,pi-agent-core 提供了一个 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 所做的。
编程代理有一个很好的属性,它们基本上是一个聊天界面。用户编写提示,然后是代理的回复、工具调用及其结果。一切都是 nicely 线性的,这非常适合与"原生"终端模拟器一起工作。你可以使用所有内置功能,如自然滚动和滚动缓冲区内的搜索。它也在一定程度上限制了你的 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 闪烁得少。
这种方法有多浪费?我们存储了整个滚动缓冲区 worth 的之前渲染的行,并且每次 TUI 被要求渲染自身时都会重绘行。这通过我上面描述的缓存得到缓解,所以重绘不是大问题。我们仍然必须比较大量行。实际上,在 25 年以下的计算机上,这在性能和内存使用方面都不是大问题(非常大的会话只需几百 KB)。感谢 V8。我得到的是一个死简单的编程模型,让我可以快速迭代。
pi-coding-agent
我不需要解释你应该从编程代理框架期望什么功能。pi 具备你从其他工具中习惯的大多数便利功能:
- 运行在 Windows、Linux 和 macOS 上(或任何具有 Node.js 运行时和终端的设备)
- 支持多提供者,支持会话中模型切换
- 会话管理,支持继续、恢复和分支
- 项目上下文文件(AGENTS.md)从全局到项目特定层次加载
- 常用操作的斜杠命令
- 带参数支持的自定义斜杠命令作为 Markdown 模板
- Claude Pro/Max 订阅的 OAuth 认证
- 通过 JSON 自定义模型和提供者配置
- 支持实时重载的自定义主题
- 带模糊文件搜索、路径补全、拖放和多行粘贴的编辑器
- 代理工作时的消息队列
- 视觉能力模型的图像支持
- 会话的 HTML 导出
- 通过 JSON 流式传输和 RPC 模式的无头操作
- 完整的成本和令牌跟踪
如果你想要完整 rundown,阅读 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 训练,所以他们天生就理解什么是编程代理。似乎不需要 10,000 令牌的系统提示词,我们将在后面的基准测试部分发现这一点,正如我在过去几周 exclusively 使用 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)
如果你想要限制代理修改文件或运行任意命令,还有额外的只读工具(grep、find、ls)。默认情况下这些被禁用,所以代理只获得上述四个工具。
事实证明,这四个工具就是有效编程代理所需的全部。模型知道如何使用 bash,并且已经在具有类似输入架构的 read、write 和 edit 工具上训练过。与 Claude Code 的工具定义 或 opencode 的工具定义(明显源自 Claude Code 的,相同结构、相同示例、相同 git 提交流)相比。值得注意的是,Codex 的工具定义 与 pi 的同样极简。
pi 的系统提示词和工具定义加起来低于 1000 令牌。
默认 YOLO 模式
pi 运行在完全 YOLO 模式,假设你知道自己在做什么。它对你的文件系统有无限制访问,可以执行任何命令而无需权限检查或安全护栏。文件操作或命令没有权限提示。没有 通过 Haiku 预先检查 bash 命令 的恶意内容。完全文件系统访问。可以以你的用户权限执行任何命令。
如果你看看其他编程代理中的安全措施,它们大多是安全作秀。一旦你的代理可以编写代码和运行代码,基本上就完蛋了。你唯一能防止数据泄露的方法是切断代理运行环境的网络连接,这使得代理基本无用。另一种选择是允许列表域名,但这也可以通过其他方式绕过。
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
代理可以根据需要读取和更新此文件。使用复选框跟踪已完成和剩余的内容。简单、可见且在你控制之下。
无计划模式
pi 没有也不会拥有内置计划模式。告诉代理与你一起思考问题,而不修改文件或执行命令,通常就足够了。
如果你需要跨会话的持久计划,将其写入文件:
# 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
代理可以读取、更新和引用计划。与仅存在于会话内的临时计划模式不同,基于文件的计划可以在会话之间共享,并可以与代码版本化。
有趣的是,Claude Code 现在有一个 计划模式,基本上是只读分析,它最终会将 Markdown 文件写入磁盘。而且你基本上无法使用计划模式而不批准大量命令调用,因为如果没有那个,计划基本上是不可能的。
与 pi 的区别是我对一切都有完全的可观测性。我可以看到代理实际查看了哪些来源,完全错过了哪些。在 Claude Code 中,编排 Claude 实例通常会生成一个子代理,你对子代理做什么零可见性。我可以立即看到 Markdown 文件。我可以与代理协作编辑它。简而言之,我需要计划的可观测性,而我在 Claude Code 的计划模式中得不到。
如果你必须在计划期间限制代理,你可以通过 CLI 指定它可以访问哪些工具:
pi --tools read,grep,find,ls
这为你提供了探索和计划的只读模式,代理不会修改任何内容或能够运行 bash 命令。但你不会满意那个。
无 MCP 支持
pi 不支持也不会支持 MCP。我已经 广泛撰写 过这个,但 TL;DR 是:MCP 服务器对大多数用例来说过于复杂,并且带来了显著的上下文开销。
流行的 MCP 服务器如 Playwright MCP(21 个工具,13.7k 令牌)或 Chrome DevTools MCP(26 个工具,18k 令牌)在每次会话时将它们的完整工具描述倾倒入你的上下文。那是 7-9% 的上下文窗口在你甚至开始工作之前就消失了。这些工具中的许多你在给定会话中永远不会使用。
替代方案很简单:构建带有 README 文件的 CLI 工具。代理在需要工具时读取 README,仅在必要时支付令牌成本(渐进式披露),并可以使用 bash 调用工具。这种方法可组合(管道输出、链命令)、易于扩展(只需添加另一个脚本)且令牌高效。
以下是我如何为 pi 添加网络搜索:
我在 github.com/badlogic/agent-tools 维护了这些工具的集合。每个工具都是一个简单的 CLI,带有代理按需读取的 README。
如果你绝对必须使用 MCP 服务器,查看 Peter Steinberger 的 mcporter 工具,它将 MCP 服务器包装为 CLI 工具。
无后台 bash
pi 的 bash 工具同步运行命令。没有内置方法启动开发服务器、在后台运行测试或在命令仍在运行时与 REPL 交互。
这是故意的。后台进程管理增加了复杂性:你需要进程跟踪、输出缓冲、退出清理和向运行进程发送输入的方法。Claude Code 通过他们的后台 bash 功能处理其中一些,但它具有较差的可观测性(Claude Code 的常见主题),并迫使代理跟踪运行实例而不提供查询它们的工具。在早期 Claude Code 版本中,代理在上下文压缩后忘记了所有后台进程,无法查询它们,所以你必须手动杀死它们。这已经修复了。
改用 tmux。以下是 pi 在 LLDB 中调试崩溃的 C 程序:
那可观测性如何?同样的方法适用于长时间运行的开发服务器、监视日志输出和类似用例。如果你愿意,你还可以通过 tmux 跳入上面的 LLDB 会话并与代理共同调试。Tmux 还给你列出所有活动会话的 CLI 参数。多好。
根本不需要后台 bash。你知道 Claude Code 也可以使用 tmux。bash 就是你所需要的一切。
无子代理
pi 没有专用的子代理工具。当 Claude Code 需要做复杂的事情时,它通常会生成一个子代理来处理任务的一部分。你对子代理做什么零可见性。它是黑盒中的黑盒。代理之间的上下文传输也很差。编排代理决定将什么初始上下文传递给子代理,你通常对此几乎没有控制。如果子代理犯错,调试很痛苦,因为你无法看到完整对话。
如果你需要 pi 生成自己,只需让它通过 bash 运行自己。你甚至可以让它在 tmux 会话中生成自己,以获得完全可观测性和直接与子代理交互的能力。

但更重要的是:修复你的工作流,至少是关于上下文收集的那些。人们在会话中使用子代理,认为他们在节省上下文空间,这是真的。但那是错误思考子代理的方式。在会话中途使用子代理进行上下文收集是一个信号,表明你没有提前计划。如果你需要收集上下文,先在它自己的会话中完成。创建一个你可以在新会话中使用的工件,给你的代理它所需的所有上下文,而不会用工具输出污染其上下文窗口。该工件对下一个功能也有用,你获得完全可观测性和可导向性,这在上下文收集期间很重要。
因为尽管普遍相信,模型仍然不擅长找到实现新功能或修复 bug 所需的所有上下文。我将此归因于模型被训练为只读取文件的部分而不是完整文件,所以它们不愿意读取所有内容。这意味着它们错过了重要上下文,无法看到正确完成任务所需的内容。
只需查看 pi-mono 问题跟踪器 和拉取请求。许多被关闭或修订,因为代理无法完全理解需要什么。这不是贡献者的错,我真正感激,因为即使不完整的 PR 也帮助我更快移动。这只是意味着我们过于信任我们的代理。
我并非完全否定子代理。有有效用例。我最常用的是代码审查:我告诉 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 会话中跳入。或者说这是一个临时会话,不应该保存到磁盘。所有这些都转换为提示,主代理读取并基于它再次通过 bash 执行自己。虽然我没有子代理内部运作的完全可观测性,但我对其输出有完全可观测性。其他框架不提供这个,这对我来说没有意义。
当然,这有点模拟用例。实际上,我会生成一个新的 pi 会话并要求它审查拉取请求,可能将其拉入本地分支。看到它的初始审查后,我给出自己的审查,然后我们一起工作直到它变好。那是我用来不合并垃圾代码的工作流。
在我的书中,生成多个子代理并行实现各种功能是一个反模式,行不通,除非你不在乎你的代码库退化成垃圾堆。
基准测试
我做了很多夸大的声明,但我有数值证明我上面说的所有反传统的东西实际上有效吗?我有我的生活经验,但这很难在博客文章中传输,你只能相信我。所以我为 pi 创建了一个 Terminal-Bench 2.0 测试运行,使用 Claude Opus 4.5,让它与 Codex、Cursor、Windsurf 和其他编程框架及其各自的本地模型竞争。显然,我们都知道基准测试不代表实际性能,但这是我能提供的最好的,作为一种证明我不是完全胡说八道的方式。
我执行了完整运行,每个任务五次试验,这使得结果有资格提交到排行榜。我还启动了第二个仅运行在 CET 期间的运行,因为我发现一旦 PST 上线,错误率(因此基准测试结果)会变差。以下是第一次运行的结果:

以下是截至 2025 年 12 月 2 日 pi 在当前排行榜上的排名:

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

这还需要一天左右才能完成。完成后我会更新这篇博客文章。
还要注意排行榜上 Terminus 2 的排名。Terminus 2 是 Terminal-Bench 团队自己的极简代理,只是给模型一个 tmux 会话。模型将命令作为文本发送到 tmux 并自己解析终端输出。没有花哨的工具,没有文件操作,只有原始终端交互。它与具有更复杂工具的代理势均力敌,并与多样化的模型集一起工作。更多证据证明极简方法可以做得同样好。
总结
基准测试结果挺逗的,但真正的考验在于实际使用。而我的结果是我的日常工作,pi 在那里表现令人钦佩。Twitter 充满了上下文工程帖子和博客,但我觉得我们目前没有任何框架实际上让你做上下文工程。pi 是我尝试为自己构建一个工具,让我尽可能多地控制。
我对 pi 的现状相当满意。还有一些我想添加的功能,如 压缩 或 工具结果流式传输,但我不认为我个人需要更多了。缺少压缩对我个人来说不是问题。出于某种原因,我能够将 数百次交流 塞进单个会话,而我在没有压缩的情况下无法用 Claude Code 做到这一点。
话虽如此,我欢迎贡献。但像我的所有开源项目一样,我在项目管理上比较专断。这是我多年来在大项目中艰难学到的教训。如果我关闭你发送的问题或 PR,我希望没有硬感情。我也会尽力给你理由。我只想保持这个专注和可维护。如果 pi 不符合你的需求,我恳请你 fork 它。我是认真的。如果你创建了更符合我需求的东西,我会愉快地加入你的努力。
我认为上面的一些经验教训也可以转移到其他框架。告诉我你的情况如何。