Mario Zechner
虽然不多,但这是我的。过去三年,我一直在使用LLM进行辅助编码。如果你读到这篇文章,你可能也经历过类似的演变:从复制粘贴代码到ChatGPT,到Copilot的自动补全(对我来说从来没用过),再到Cursor,最终到2025年成为我们日常主力工具的新一代编码辅助工具,例如Claude Code、Codex、Amp、Droid和opencode。
我之前大部分工作都用 Claude Code。今年四月,我用了 Cursor 一年半之后,第一个尝试的就是 Claude Code。那时候它的功能还比较基础,非常符合我的工作流程,因为我这个人比较简单,喜欢简单易用的工具。但是最近几个月,Claude Code 变得像一艘宇宙飞船,里面 80% 的功能我都用不上。而且每次更新系统提示和工具都会变化,这打乱了我的工作流程,也改变了我的模型操作习惯。我讨厌这样。另外,它还会闪烁。
多年来,我也构建过许多不同复杂程度的智能体。例如,我的小型浏览器智能体Sitegeist,本质上是一个驻留在浏览器内部的编码智能体。在所有这些工作中,我认识到上下文工程至关重要。精确控制模型上下文的内容可以产生更好的输出,尤其是在编写代码时。现有的框架会在后台注入一些甚至不会显示在用户界面上的东西,使得这一点变得极其困难,甚至不可能。
说到可视化,我希望能够检查与模型交互的方方面面。基本上,目前没有任何框架能够做到这一点。我还想要一个文档清晰的会话格式,可以自动进行后处理,以及一种在代理核心之上构建替代用户界面的简便方法。虽然现有的框架可以实现其中一些功能,但它们的 API 感觉像是自然演进的结果。这些解决方案在发展过程中积累了不少包袱,这在开发者体验中体现得淋漓尽致。我并不是在责怪任何人。如果很多人使用你的产品,而你需要某种程度的向后兼容性,那么这就是你必须付出的代价。
我也尝试过自托管,包括本地和DataCrunch。虽然有些工具(例如 OpenCode)支持自托管模型,但通常效果不佳。这主要是因为它们依赖于像Vercel AI SDK这样的库,而这些库不知何故与自托管模型不兼容,尤其是在工具调用方面。
所以,一个冲着克劳德大吼大叫的老头会怎么做呢?他会自己写一个编码代理程序,然后取一个谷歌搜不到的名字,这样就永远不会有用户。这意味着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
要与几乎所有 LLM 提供商进行交互,实际上只需要使用四个 API:OpenAI 的 Completions API、他们较新的Responses API、Anthropic 的 Messages API和Google 的 Generative AI API。
它们的功能大同小异,因此基于它们构建抽象层并不难。当然,你还需要考虑一些特定于提供商的特性。这一点在补全 API 上尤为突出,几乎所有提供商都支持该 API,但每个提供商对它的理解却不尽相同。例如,OpenAI 的补全 API 不支持推理轨迹,而其他提供商则在其版本的补全 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_contentvs reasoning)
为了确保所有功能都能在海量的服务提供商中正常运行,pi-ai 提供了一套相当全面的测试套件,涵盖图像输入、推理过程、工具调用以及其他您期望从 LLM API 中获得的功能。测试会在所有受支持的服务提供商和常用模型上运行。虽然这已经是一项不错的努力,但仍然无法保证新的模型和服务提供商能够开箱即用。
另一个显著区别在于服务提供商如何报告令牌和缓存的读写操作。Anthropic 的方法最为合理,但总体而言,情况比较混乱。有些服务提供商在 SSE 流开始时报告令牌数量,有些则只在结束时报告,这导致如果请求中止,就无法准确追踪成本。更糟糕的是,你无法提供唯一的 ID 来进行后续的计费 API 关联,从而确定哪些用户消耗了多少令牌。因此,pi-ai 只能尽力追踪令牌和缓存。这对于个人用户来说足够了,但如果你拥有通过你的服务消耗令牌的最终用户,就无法进行准确的计费。
特别要提到谷歌,他们至今似乎仍然不支持工具调用流,这真是非常符合谷歌的风格。
pi-ai 也支持浏览器运行,这对于构建基于 Web 的界面非常有用。一些提供商通过支持 CORS 使这一点更加便捷,例如 Anthropic 和 xAI。
上下文交接
pi-ai 从设计之初就考虑到了不同提供商之间的上下文切换。由于每个提供商都有自己追踪工具调用和思维轨迹的方式,因此只能尽力而为。例如,如果在会话中途从 Anthropic 切换到 OpenAI,Anthropic 的思维轨迹会被转换成助手消息中的内容块,并用<thinking></thinking>标签分隔。这种做法是否合理尚待商榷,因为 Anthropic 和 OpenAI 返回的思维轨迹实际上并不能反映幕后发生的情况。
这些提供程序还会将已签名的数据块插入到事件流中,后续包含相同消息的请求必须重放这些数据块。在同一提供程序内切换模型时,也会出现这种情况。这导致后台存在繁琐的抽象和转换管道。
我很高兴地报告,pi-ai 中的跨提供商上下文交接和上下文序列化/反序列化功能运行良好:
import { getModel, complete, Context } from'@mariozechner/pi-ai';// Start with Claudeconst claude = getModel('anthropic', 'claude-sonnet-4-5');constcontext: Context = {messages: []};context.messages.push({ role: 'user', content: 'What is 25 * 18?' });const claudeResponse = awaitcomplete(claude, context, {thinkingEnabled: true});context.messages.push(claudeResponse);// Switch to GPT - it will see Claude's thinking as <thinking> tagged textconst gpt = getModel('openai', 'gpt-5.1-codex');context.messages.push({ role: 'user', content: 'Is that correct?' });const gptResponse = awaitcomplete(gpt, context);context.messages.push(gptResponse);// Switch to Geminiconst gemini = getModel('google', 'gemini-2.5-flash');context.messages.push({ role: 'user', content: 'What was the question?' });const geminiResponse = awaitcomplete(gemini, context);// Serialize context to JSON (for storage, transfer, etc.)const serialized = JSON.stringify(context);// Later: deserialize and continue with any modelconstrestored: Context = JSON.parse(serialized);restored.messages.push({ role: 'user', content: 'Summarize our conversation' });const continuation = awaitcomplete(claude, restored);
我们生活在一个多模型的世界里
说到模型,我需要一种类型安全的方式来在getModel调用中指定它们。为此,我需要一个模型注册表,可以将其转换为 TypeScript 类型。我正在将来自OpenRouter和models.dev(由 opencode 团队创建,非常感谢,它超级实用)的数据解析到models.generated.ts中。这包括令牌成本以及图像输入和思维支持等功能。
如果我需要添加注册表中没有的模型,我希望有一个类型系统能够轻松创建新模型。这在使用自托管模型、尚未发布到 models.dev 或 OpenRouter 的新版本,或者尝试一些不太常见的 LLM 提供商时尤其有用:
import { Model, stream } from'@mariozechner/pi-ai';constollamaModel: 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 = awaitstream(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 = newAbortController();// Abort after 2 secondssetTimeout(() => controller.abort(), 2000);const s = stream(model, {messages: [{ role: 'user', content: 'Write a long story' }]}, {signal: controller.signal});forawait (const event of s) {if (event.type === 'text_delta') { process.stdout.write(event.delta); } elseif (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 }),});constweatherTool: 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 LLMoutput: `Temperature in ${args.city}: ${temp}°C`,// Structured data for the UIdetails: { temp } }; }};// Tools can also return imagesconstchartTool: AgentTool = {name: 'generate_chart',description: 'Generate a chart from data',parameters: Type.Object({ data: Type.Array(Type.Number()) }),execute: async (toolCallId, args) => {const chartImage = awaitgenerateChartImage(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 解析对于良好的用户体验至关重要。当 LLM 传输工具调用参数时,pi-ai 会逐步解析这些参数,以便在调用完成之前在 UI 中显示部分结果。例如,您可以在代理重写文件时显示差异流。
最小代理框架
最后,pi-ai 提供了一个代理循环来处理完整的流程编排:处理用户消息、执行工具调用、将结果反馈给 LLM,并重复此过程,直到模型无需工具调用即可生成响应。该循环还支持通过回调进行消息排队:每次循环结束后,它会请求队列中的消息,并在下一次助手响应之前注入这些消息。该循环会为所有操作发出事件,从而可以轻松构建响应式 UI。
代理循环不允许您指定最大步数或类似其他统一 LLM API 中常见的参数。我从未发现过需要这些参数的情况,所以为什么要添加它们呢?循环会一直运行,直到代理发出完成指令。不过,除了循环之外,pi-agent-core还提供了一个Agent包含真正实用功能的类:状态管理、简化的事件订阅、两种模式(一次一条或全部同时)的消息队列、附件处理(图像、文档)以及传输抽象,允许您直接运行代理或通过代理运行代理。
我对 pi-ai 满意吗?总体来说,是的。任何统一的 API 都存在抽象层泄漏的问题,所以它永远不可能完美。但它已经在七个不同的生产项目中使用过,而且表现非常出色。
为什么不直接使用 Vercel AI SDK 而要自己开发呢?Armin 的博客文章与我的经历不谋而合。直接基于提供商的 SDK 进行开发,让我可以完全掌控 API,并根据自己的需求进行精确设计,而且代码量也小得多。Armin 的博客更深入地探讨了自行开发的原因,建议去读一读。
劈腿
我成长于DOS时代,所以终端用户界面(TUI)伴随了我整个童年。从Doom的精美安装程序到Borland的产品,TUI一直陪伴我到90年代末。最终换到图形界面操作系统时,我真是高兴极了。虽然TUI大多便携且易于串流,但它们的信息密度实在太低。综上所述,我认为从树莓派的终端用户界面入手是最合理的。以后如果需要,我可以随时添加图形界面。
那么,我为什么要自己构建 TUI 框架呢?我研究过Ink、Blessed、OpenTUI等替代方案。我相信它们各有千秋,但我绝对不想像写 React 应用那样编写我的 TUI。Blessed 似乎已经停止维护,而 OpenTUI 也明确表示不适用于生产环境。此外,在 Node.js 之上编写自己的 TUI 框架似乎也是一个有趣的小挑战。
两种类型的TUI
编写终端用户界面本身并不难,只是需要根据自身情况选择合适的方法。基本上有两种方法。一种方法是获取终端视口(即你实际能看到的终端内容部分)的所有权,并将其视为像素缓冲区。这里不是像素,而是包含字符的单元格,每个字符都有背景色、前景色以及斜体和粗体等样式。我称这种终端用户界面为全屏终端用户界面。AMP 和 OpenCode 都采用了这种方法。
缺点是你会失去回滚缓冲区,这意味着你必须实现自定义搜索。你也会失去滚动功能,这意味着你必须自己模拟视口内的滚动。虽然这实现起来并不难,但这意味着你必须重新实现终端模拟器已经提供的所有功能。尤其是在这种终端用户界面中,鼠标滚动总是感觉不太顺畅。
第二种方法是像任何命令行程序一样直接向终端写入内容,将内容追加到回滚缓冲区,只偶尔将“渲染光标”在可见视口内向上移动一点,以重新绘制诸如动画加载指示器或文本编辑框之类的元素。实际情况并非如此简单,但你应该明白我的意思。Claude Code、Codex 和 Droid 就是这么做的。
编码代理有一个很棒的特性,那就是它们本质上就是一个聊天界面。用户输入提示,代理会回复,工具会调用并给出结果。整个流程非常线性,这使得它们非常适合与“原生”终端模拟器配合使用。你可以使用所有内置功能,例如自然滚动和在回滚缓冲区内搜索。这也在一定程度上限制了你的终端用户界面(TUI)的功能,而我恰恰觉得这很吸引人,因为限制使得程序更加精简,只做它们应该做的事情,没有多余的功能。这就是我为 pi-tui 选择的方向。
保留模式 UI
如果你做过任何 GUI 编程,你可能听说过保留模式和立即模式。在保留模式下的 UI 中,你会构建一个组件树,这些组件会在帧与帧之间保持不变。每个组件都知道如何渲染自身,并且在没有变化的情况下可以缓存其输出。在立即模式下的 UI 中,每一帧都会从头开始重新绘制所有内容(尽管实际上,立即模式的 UI 也会进行缓存,否则它们会崩溃)。
pi-tui 采用了一种简单的保留模式方法。AComponent只是一个对象,它有一个render(width)方法返回一个字符串数组(水平方向适合视口的线条,颜色和样式使用 ANSI 转义码),以及一个可选的handleInput(data)键盘输入方法。AContainer保存一个垂直排列的组件列表,并收集它们渲染的所有线条。该类TUI本身就是一个容器,负责协调所有操作。
当 TUI 需要更新屏幕时,它会请求每个组件进行渲染。组件可以缓存其输出:完全流式传输的助手消息无需每次都重新解析 Markdown 和重新渲染 ANSI 序列,只需返回缓存的行即可。容器会收集所有子组件的行。TUI 会收集所有这些行,并将它们与之前为前一个组件树渲染的行进行比较。它维护着一个类似后备缓冲区的东西,用于记录写入回滚缓冲区的内容。
然后它只会重新绘制发生变化的部分,我称之为差异渲染。我记性很差,这种方法可能有个正式名称。
差异化渲染
这里有一个简化的演示,说明了究竟哪些内容会被重新绘制。
算法很简单:
- 首次渲染:直接将所有行输出到终端。
- 宽度已更改:完全清除屏幕并重新渲染所有内容(软换行更改)
- 正常更新:找到与屏幕上显示内容不同的第一行,将光标移动到该行,然后从该行重新渲染到末尾。
但有一点需要注意:如果第一行修改的内容位于可见视口上方(用户向上滚动了页面),我们需要完全清除并重新渲染。终端不允许向视口上方的回滚缓冲区写入数据。
为了防止更新过程中出现闪烁,pi-tui 会将所有渲染内容封装在同步输出转义序列(`\n`CSI ?2026h和 `\ CSI ?2026ln`)中。这会指示终端缓冲所有输出并以原子方式显示。大多数现代终端都支持此功能。
它的效果如何?闪烁情况如何?在像 Ghostty 或 iTerm2 这样功能强大的终端中,它的表现非常出色,完全不会出现闪烁。但在像 VS Code 内置终端这样不太理想的终端实现中,闪烁情况会根据一天中的时间、显示器尺寸、窗口大小等因素而有所不同。由于我非常习惯使用 Claude Code,所以没有花更多时间去优化它。我对 VS Code 中出现的轻微闪烁感到满意。否则我会感觉很不习惯。而且它的闪烁程度仍然比 Claude Code 低。
这种方法有多浪费?我们存储了一整个回滚缓冲区之前渲染过的行,每次 TUI 需要渲染时,我们都要重新渲染这些行。我上面提到的缓存机制缓解了这个问题,所以重新渲染的开销并不大。但我们仍然需要比较大量的行。实际上,对于 25 年以内的计算机来说,无论从性能还是内存使用(即使是大型会话也只占用几百 KB),这都不是问题。感谢 V8。它给我带来了一个极其简单的编程模型,让我可以快速迭代。
pi-coding-agent
我无需赘述编码代理框架应具备哪些功能。Pi 几乎包含了你在其他工具中常用的所有便捷功能:
可在 Windows、Linux 和 macOS 上运行(或任何具有 Node.js 运行时和终端的操作系统)
支持多提供商,并可在会话期间切换模式。
会话管理,包括继续、恢复和分支
项目上下文文件(AGENTS.md)按层级结构从全局到项目特定加载。
常用操作的斜杠命令
支持带参数的自定义斜杠命令作为 Markdown 模板
Claude Pro/Max 订阅的 OAuth 身份验证
通过 JSON 配置自定义模型和提供程序
可自定义主题,支持实时重载
编辑器具备模糊文件搜索、路径自动补全、拖放和多行粘贴功能
代理工作期间的消息排队
支持具备视觉功能的模型的图像支持
会话的 HTML 导出
通过 JSON 流和 RPC 模式进行无头操作
完整成本和代币追踪
如果你想了解全部细节,请阅读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 filesGuidelines:- 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 filesDocumentation:- 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 文件。包括适用于所有会话的全局 AGENTS.md 文件和存储在项目目录中的项目特定 AGENTS.md 文件。你可以在这里根据自己的喜好自定义 pi。你甚至可以替换整个系统提示符。例如,可以与Claude Code 的系统提示符、Codex 的系统提示符或opencode 的特定模型提示符(Claude Code 的提示符是他们复制的原始 Claude Code 提示符的精简版)进行比较。
你可能会觉得这很疯狂。这些模型很可能已经使用其原生编码框架进行过一些训练。因此,使用原生系统提示符或类似 OpenCode 的提示符是最理想的选择。但事实证明,所有 Frontier 模型都经过了大量的强化学习训练,因此它们天生就理解编码代理的概念。正如我们将在基准测试部分看到的,以及我过去几周只使用 Pi 的经验所表明的那样,似乎并不需要 10,000 个系统提示符。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 readwrite 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 fileedit 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 withbash 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,并且已经使用类似的输入模式对读取、写入和编辑工具进行了训练。相比之下,Claude Code 的工具定义或opencode 的工具定义(显然源自 Claude Code,结构、示例和 Git 提交流程都相同)则显得简陋得多。值得注意的是,Codex 的工具定义也和树莓派一样简洁。
pi 的系统提示和工具定义加起来不到 1000 个令牌。
默认 YOLO
pi 以完全 YOLO 模式运行,并假定您清楚自己在做什么。它拥有对您文件系统的无限制访问权限,可以执行任何命令而无需权限检查或安全措施。文件操作或命令不会弹出权限提示。Haiku 不会预先检查 bash 命令是否存在恶意内容。完全文件系统访问权限。可以以您的用户权限执行任何命令。
如果你看看其他编码代理的安全措施,就会发现它们大多只是表面功夫。一旦你的代理能够编写和运行代码,基本上就完了。唯一能阻止数据泄露的方法是切断代理运行环境的所有网络访问权限,但这会让代理几乎失去作用。另一种方法是将某些域名加入白名单,但这也可以通过其他方式绕过。
Simon Willison曾就此问题撰写过大量文章。他的“双重LLM”模式试图解决混淆代理攻击和数据泄露问题,但他自己也承认“这个方案相当糟糕”,而且引入了巨大的实现复杂性。核心问题依然存在:如果LLM拥有可以读取私有数据和发起网络请求的工具,那么你就如同在玩打地鼠游戏,疲于应对各种攻击途径。
既然我们无法同时满足读取数据、执行代码和网络访问这三项功能,树莓派就只能妥协了。反正大家都在拼命工作,为什么不把它设为默认且唯一的选项呢?
默认情况下,pi 没有网页搜索或抓取工具。但是,它可以读取curl磁盘上的文件,这两种操作都为注入攻击提供了充足的攻击面。文件或命令输出中的恶意内容会影响其行为。如果您不希望拥有完全访问权限,请在容器中运行 pi,或者如果您需要(伪)安全防护,请使用其他工具。
没有内置待办事项
树莓派现在不支持,将来也不会支持内置待办事项列表。根据我的经验,待办事项列表通常会给模型带来更多麻烦,而不是帮助。它们会增加模型需要跟踪和更新的状态,从而增加出错的可能性。
如果需要跟踪任务,请通过写入文件的方式使其具有外部状态:
# TODO.md- [x] Implement user authentication- [x] Add database migrations- [ ] Write API documentation- [ ] Add rate limiting
代理可以根据需要读取和更新此文件。使用复选框可以跟踪已完成和待完成的工作。简单、清晰,一切尽在您的掌控之中。
无计划模式
pi 没有也不会有内置的计划模式。通常情况下,只需告诉代理程序与你一起思考问题,而无需修改文件或执行命令,就足够了。
如果需要跨会话保持计划的一致性,请将其写入文件:
# PLAN.md## GoalRefactor authentication system to support OAuth## Approach1. Research OAuth 2.0 flows2. Design token storage schema3. Implement authorization server endpoints4. Update client-side login flow5. Add tests## Current StepWorking on step 3 - authorization endpoints
代理程序可以读取、更新和引用运行中的计划。与仅存在于会话中的临时计划模式不同,基于文件的计划可以跨会话共享,并且可以与您的代码一起进行版本控制。
有趣的是,Claude Code 现在有一个计划模式,本质上是一个只读分析,最终会将一个 Markdown 文件写入磁盘。而且,如果你不批准一大堆命令调用,基本上就无法使用计划模式,因为没有这些命令调用,计划就根本无法进行。
与 Pi 的区别在于,我可以完全观察所有情况。我可以看到代理实际查看了哪些资源,以及它完全忽略了哪些资源。在 Claude Code 中,负责编排的 Claude 实例通常会生成一个子代理,而你完全无法了解该子代理的具体操作。我可以立即看到 Markdown 文件,并且可以与代理协作编辑它。简而言之,我需要可观察性来进行规划,而 Claude Code 的规划模式无法满足我的需求。
如果在规划过程中必须限制代理的访问权限,可以通过 CLI 指定其可以访问哪些工具:
pi --tools read,grep,find,ls
这样一来,你就可以在只读模式下进行探索和规划,代理程序不会修改任何内容,也无法运行 bash 命令。不过,你肯定不会满意这种模式。
不支持MCP
树莓派现在不支持,将来也不会支持 MCP。我之前已经详细讨论过这个问题,但简单来说:MCP 服务器对于大多数使用场景来说都过于复杂,而且会带来显著的上下文开销。
像 Playwright MCP(21 个工具,13.7k 个令牌)或 Chrome DevTools MCP(26 个工具,18k 个令牌)这样的热门 MCP 服务器,会在每次会话开始时将所有工具的描述信息都显示在上下文窗口中。这意味着在你开始工作之前,上下文窗口的 7-9% 就已经被占用了。其中很多工具你在一个会话中根本用不到。
另一种方法很简单:构建带有 README 文件的 CLI 工具。代理会在需要该工具时读取 README 文件,仅在必要时支付令牌费用(渐进式披露),并且可以使用 bash 脚本调用该工具。这种方法具有可组合性(管道输出、链式命令)、易于扩展(只需添加另一个脚本)和高效的令牌效率。
以下是我为树莓派添加网络搜索功能的方法:
我在github.com/badlogic/agent-tools上维护着这些工具的集合。每个工具都是一个简单的命令行界面,带有一个 README 文件,代理程序会根据需要读取该文件。
如果您绝对必须使用 MCP 服务器,请了解一下Peter Steinberger 的mcporter工具,该工具将 MCP 服务器封装成 CLI 工具。
没有背景噪音
树莓派的 bash 工具以同步方式运行命令。它没有内置的方法可以在命令运行期间启动开发服务器、在后台运行测试或与 REPL 交互。
这是有意为之。后台进程管理会增加复杂性:你需要进程跟踪、输出缓冲、退出清理,以及向正在运行的进程发送输入的方法。Claude Code 通过其后台 bash 功能处理了其中的一些问题,但它的可观测性很差(这是 Claude Code 的通病),并且强制代理跟踪正在运行的实例,却没有提供查询它们的工具。在早期版本的 Claude Code 中,代理在上下文压缩后会忘记所有后台进程,并且无法查询它们,因此你必须手动终止它们。这个问题现在已经修复了。
请改用tmux。以下是 pi 在 LLDB 中调试崩溃的 C 程序的示例:
这种可观测性怎么样?同样的方法也适用于长时间运行的开发服务器、日志输出监控以及类似的用例。如果你愿意,还可以通过 tmux 进入上面提到的 LLDB 会话,与代理进行协同调试。tmux 还提供了一个命令行参数,可以列出所有活动会话。真不错。
完全没必要在后台运行 Bash。你知道吗,Claude Code 也能用 tmux。Bash 就足够了。
没有二级代理
pi 没有专门的子代理工具。当 Claude Code 需要执行复杂任务时,它通常会生成一个子代理来处理部分任务。你完全无法了解该子代理的具体操作。它就像一个黑盒中的黑盒。代理之间的上下文传递也很差。编排代理决定将哪些初始上下文传递给子代理,而你通常对此几乎没有控制权。如果子代理出错,调试起来会非常痛苦,因为你无法查看完整的对话过程。
如果需要 Pi 自行启动,只需通过 bash 命令让它运行即可。你甚至可以让它在 tmux 会话中启动,以便完全可观察并直接与该子代理交互。
但更重要的是:改进你的工作流程,至少是那些与上下文收集相关的流程。人们在会话中使用子代理,以为这样可以节省上下文空间,这没错。但这并非理解子代理的正确方式。在会话中途使用子代理来收集上下文,表明你没有提前规划。如果你需要收集上下文,请首先在单独的会话中进行。创建一个工件,之后可以在新的会话中使用它,为你的代理提供所需的所有上下文,而不会用工具输出污染其上下文窗口。这个工件对下一个功能也可能有用,而且你还能获得完整的可观察性和可控性,这在上下文收集过程中至关重要。
尽管人们普遍认为模型已经能够很好地获取实现新功能或修复错误所需的所有上下文信息,但事实并非如此。我认为这是因为模型训练时只读取文件的部分内容而非完整文件,所以它们不愿读取全部内容。这意味着它们会错过重要的上下文信息,无法找到正确完成任务所需的信息。
看看pi-mono 的问题跟踪器和拉取请求就知道了。很多请求都被关闭或修改了,因为代理程序没能完全理解需求。这并非贡献者的错,我对此非常感激,因为即使是不完整的拉取请求也能帮助我加快开发进度。这仅仅意味着我们对代理程序过于信任了。
我并非完全否定子代理。它们确实有合理的应用场景。我最常用的场景是代码审查:我通过自定义斜杠命令让树莓派启动自身并显示代码审查提示,然后它会获取输出结果。
---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 gapsDo not read the code yourself. Let the sub-agent do that.Report the sub-agent's findings.
以下是我如何使用它在 GitHub 上审查拉取请求:
通过简单的提示,我可以选择要复习的具体内容以及要使用的模型。如果需要,我甚至可以设置思考级别。我还可以将完整的复习会话保存到文件中,并在另一个树莓派会话中打开该文件。或者,我可以指定这是一个临时会话,不应将其保存到磁盘。所有这些操作都会被转换成主代理读取的提示,并根据提示通过 bash 再次执行自身。虽然我无法完全观察子代理的内部运作,但我可以完全观察其输出。其他一些框架并没有提供这种功能,这让我感到困惑。
当然,这只是一个模拟用例。实际上,我会启动一个新的树莓派会话,让它审查拉取请求,并可能将其拉取到本地分支。在看到它的初步审查后,我会给出自己的审查意见,然后我们一起修改,直到代码完善为止。这就是我避免合并垃圾代码的工作流程。
在我看来,生成多个子代理来并行实现各种功能是一种反模式,而且行不通,除非你不在乎你的代码库最终变成一堆垃圾。
基准测试
我确实经常夸大其词,但我有数据证明我上面说的那些看似反常的理论真的有效吗?我有自己的亲身经历,但这很难用一篇博客文章来概括,你只能相信我。所以我用 Claude Opus 4.5 为树莓派创建了一个Terminal-Bench 2.0测试环境,并让它与 Codex、Cursor、Windsurf 和其他一些使用各自原生模型的编程工具进行对比。当然,我们都知道基准测试并不能代表实际性能,但这已经是我能提供的最佳证明,至少可以证明我说的并非全是胡说八道。
我进行了一次完整的测试,每个任务进行了五次试验,因此测试结果符合排行榜的提交标准。我还进行了第二次测试,这次测试只在欧洲中部时间 (CET) 进行,因为我发现一旦太平洋标准时间 (PST) 上线,错误率(以及相应的基准测试结果)就会恶化。以下是第一次测试的结果:
以下是截至2025年12月2日,π在当前排行榜上的排名:
这是我提交给 Terminal-Bench 团队的results.json文件,他们希望将其纳入排行榜。如果您想复现结果,可以在此仓库中找到适用于树莓派的基准测试程序。我建议您使用 Claude 套餐,而不是按需付费。
最后,这里简要介绍一下仅限中欧时间运行的情况:
这项工作还需要一天左右的时间才能完成。完成后我会更新这篇博文。
另请注意Terminus 2在排行榜上的排名。Terminus 2 是 Terminal-Bench 团队自行开发的极简代理,它只为模型提供一个 tmux 会话。模型将命令以文本形式发送到 tmux,并自行解析终端输出。没有花哨的工具,没有文件操作,只有原始的终端交互。它与那些工具更复杂的代理相比毫不逊色,并且能够与各种不同的模型兼容。这进一步证明了极简方法同样可以取得好成绩。
总结
基准测试结果固然滑稽,但真正的检验标准在于实践。而我的实践就是我的日常工作,在日常工作中,pi 的表现一直非常出色。Twitter 上充斥着关于上下文工程的帖子和博客,但我感觉我们目前拥有的所有工具都无法真正实现上下文工程。pi 是我尝试构建一个工具,让我能够尽可能地掌控一切。
我对 Pi 目前的状况相当满意。虽然还有一些我想添加的功能,比如数据压缩或工具结果流式传输,但我觉得我个人已经不需要太多其他功能了。缺少数据压缩功能对我个人来说并不是什么问题。不知何故,我竟然能够将我和代理之间的数百次交互压缩到一个会话中,而使用 Claude Code 时,如果没有数据压缩功能,这是无法实现的。
话虽如此,我欢迎大家贡献代码。但就像我所有的开源项目一样,我有时会比较固执己见。这是我多年来在大型项目中付出惨痛代价才学到的教训。如果我关闭了你提交的问题或 PR,希望你不要介意。我也会尽力解释原因。我只是想保持项目的专注性和可维护性。如果 pi 不符合你的需求,我恳请你 fork 一下。我是真心这么想的。如果你开发出了更符合我需求的东西,我非常乐意加入你的团队。
我认为以上一些经验也适用于其他类型的安全带。请告诉我你的使用体验如何。
mariozechner.at