过去三年里,我一直在使用大型语言模型(LLM)进行辅助编码。如果你读到这篇文章,你可能也经历了同样的演变:从将代码复制并粘贴到 ChatGPT,到 Copilot 自动完成(对我来说从来没有工作过),到 Cursor,最后是像 Claude Code、Codex、Amp、Droid 和 opencode 这样的新型编码代理工具,它们在 2025 年成为我们的日常驱动器。
我更喜欢 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,它将所有内容连接在一起,具有会话管理、自定义工具、主题和项目上下文文件。
在所有这些工作中,我的哲学是:如果我不需要它,就不会构建它。而我不需要很多东西。
我不会让你感到无聊,详细介绍这个包的 API 具体细节。你可以在 README.md 中阅读所有内容。相反,我想记录一下在创建统一 LLM API 时遇到的问题以及如何解决它们。我并不声称我的解决方案是最好的,但它们在各种代理和非代理 LLM 项目中都工作得很好。
实际上,只需要四个 API 来与几乎任何 LLM 提供商交谈:OpenAI 的 Completions API、他们新的 Responses API、Anthropic 的 Messages API 和 Google 的生成 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 不支持系统提示的开发者角色
Grok 模型不喜欢 reasoning_effort
不同提供商在不同字段中返回推理内容(reasoning_content vs reasoning)
为了确保所有功能在所有提供商和流行模型中都实际工作,pi-ai 有一个相当广泛的测试套件,涵盖图像输入、推理跟踪、工具调用和其他您期望从 LLM API 获得的功能。测试在所有支持的提供商和流行模型中运行。虽然这是一种良好的努力,但它仍然不能保证新的模型和提供商会直接工作。
提供商报告令牌和缓存读/写的方式也存在很大差异。Anthropic 有最合理的方法,但一般来说,这是一个狂野的西部。有些提供商在 SSE 流开始时报告令牌计数,而其他提供商只在结束时报告,这使得如果请求被中止,准确跟踪成本变得不可能。此外,您无法提供一个唯一的 ID,以便稍后与其计费 API 相关联并确定哪些用户消耗了多少令牌。因此,pi-ai 以最大的努力跟踪令牌和缓存。对于个人使用来说足够好,但如果您有最终用户通过您的服务消耗令牌,则不适合准确计费。
特别感谢 Google,截至目前似乎不支持工具调用流媒体,这对 Google 来说非常不友好。
pi-ai 也可以在浏览器中工作,这对于构建基于 Web 的界面很有用。一些提供商通过支持 CORS(特别是 Anthropic 和 xAI)使其变得尤其容易。
pi-ai 从一开始就被设计为支持提供商之间的上下文交接。由于每个提供商都有自己的方式来跟踪工具调用和思考跟踪,因此这只能是一种尽力而为的尝试。例如,如果您在会话中间从 Anthropic 切换到 OpenAI,Anthropic 的思考跟踪将被转换为助手消息中的内容块,使用 `` 标签分隔。这可能是有意义的,也可能不是,因为 Anthropic 和 OpenAI 返回的思考跟踪实际上并不代表幕后发生的事情。
这些提供商还将签名 blob 插入事件流,您需要在包含相同消息的后续请求中重放它们。这也适用于在提供商内切换模型。这使得抽象和转换管道变得笨拙。
我很高兴地报告,pi-ai 中的跨提供商上下文交接和上下文序列化/反序列化工作得相当好:
这些提供商还会在事件流中插入已签名的二进制大对象(blobs),你需要在后续包含相同消息的请求中重放这些二进制大对象。这也适用于在提供商内切换模型。这使得后台的抽象和转换管道变得笨重。
我很高兴地报告,pi-ai 中的跨提供商上下文交接和上下文序列化/反序列化工作得相当好:
import {
getModel,
complete,
Context
} from '@mariozechner/pi-ai';
const claude = getModel('anthropic', 'claude-sonnet-4-5');
const context: Context = {
messages: []
};
context.messages.push({
role: 'user',
content: '25 * 18 是多少?'
});
const claudeResponse = await complete(claude, context, {
thinkingEnabled: true
});
context.messages.push(claudeResponse);
const gpt = getModel('openai', 'gpt-5.1-codex');
context.messages.push({
role: 'user',
content: '那是正确的吗?'
});
const gptResponse = await complete(gpt, context);
context.messages.push(gptResponse);
const gemini = getModel('google', 'gemini-2.5-flash');
context.messages.push({
role: 'user',
content: '问题是什么?'
});
const geminiResponse = await complete(gemini, context);
const serialized = JSON.stringify(context);
const restored: Context = JSON.parse(serialized);
restored.messages.push({
role: 'user',
content: '总结我们的对话'
});
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'
});
许多统一的 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();
setTimeout(() => controller.abort(), 2000);
const s = stream(model, {
messages: [{
role: 'user',
content: '写一个长故事'
}]
}, {
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' ? '中止' : '错误' } :`, event.error.errorMessage);
}
}
const response = await s.result();
if (response.stopReason === 'aborted') {
console.log('部分内容:', 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: '获取城市的当前天气',
parameters: weatherSchema,
execute: async (toolCallId, args) => {
const temp = Math.round(Math.random() * 30);
return {
output: ` ${args.city} 的温度:${temp} °C`,
details: {
temp
}
};
}
};
const chartTool: AgentTool = {
name: 'generate_chart',
description: '从数据生成图表',
parameters: Type.Object({
data: Type.Array(Type.Number())
}),
execute: async (toolCallId, args) => {
const chartImage = await generateChartImage(args.data);
return {
content: [{
type: 'text',
text: `生成了 ${args.data.length} 个数据点的图表`
},
{
type: 'image',
data: chartImage.toString('base64'),
mimeType: 'image/png'
}
]
};
}
};
仍然缺乏的是工具结果流。想象一个 bash 工具,你想显示 ANSI 序列,因为它们是随着时间的推移而生成的。这目前是不可能的,但这是一个简单的解决方案,最终会被添加到包中。
工具调用流期间的部分 JSON 解析对于良好的用户体验至关重要。随着 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 的博客文章为你提供了更深入的论述,关于为什么要构建自己的代理。
我在 DOS 时代长大,所以终端用户界面是我从小就接触的东西。从 Doom 的花哨设置程序到 Borland 产品,TUI伴随着我直到 90 年代末。而当我最终切换到 GUI 操作系统时,我非常高兴。虽然 TUI 在大多数情况下是可移植的,并且可以轻松流式传输,但它们在信息密度方面也很糟糕。考虑到这一点,我认为从终端用户界面开始 pi 是最有意义的。我可以稍后添加 GUI,当我觉得需要时。
那么,为什么要构建自己的 TUI 框架?我已经研究了像 Ink、Blessed、OpenTUI 等替代方案。我相信它们都有各自的优点,但我绝对不想像写 React 应用一样写我的 TUI。Blessed 似乎基本上已经不再维护,而 OpenTUI 明确表示不适合生产使用。另外,在 Node.js 之上构建自己的 TUI 框架似乎是一个有趣的小挑战。
编写终端用户界面并不是火箭科学。你只需要选择你的毒药。有两种方法可以做到这一点。一种方法是占用终端视口(终端内容中你实际可以看到的部分),并将其视为像素缓冲区。与其使用像素,你有包含字符、背景色、前景色和样式(如斜体和粗体)的单元格。我称这些为全屏 TUI。Amp 和 opencode 使用这种方法。
编写终端用户界面并不是火箭科学。您只需要选择自己的方法。基本上有两种方法来实现它。一种方法是占有终端视口(终端内容中您可以看到的部分)并将其视为像素缓冲区。与其使用像素,您有包含字符、背景颜色、前景颜色和样式(如斜体和粗体)的单元格。我称这些为全屏 TUI。Amp 和 opencode 使用这种方法。
缺点是您会失去滚动缓冲区,这意味着您需要实现自定义搜索。您还会失去滚动功能,这意味着您需要在视口内模拟滚动。虽然这并不难实现,但这意味着您需要重新实现终端模拟器已经提供的所有功能。尤其是鼠标滚动,在这种 TUI 中总是感觉有点不对劲。
第二种方法是像任何 CLI 程序一样向终端写入内容,追加内容到滚动缓冲区,并且只偶尔将“渲染光标”向上移动一点以在可见视口内重新绘制诸如动画旋转器或文本编辑字段之类的内容。这并不是那么简单,但您可以理解这个想法。这就是 Claude Code、Codex 和 Droid 所做的。
编码代理具有一个很好的属性,即它们基本上是一个聊天界面。用户编写一个提示,然后是代理和工具调用及其结果的回复。所有内容都很好地线性排列,这使得与“本地”终端模拟器一起工作变得容易。您可以使用所有内置功能,例如自然滚动和在滚动缓冲区中搜索。它也限制了您的 TUI 可以做的事情,这我认为很有魅力,因为约束可以使程序最小化,只做它们应该做的事情,而不需要额外的内容。这是我为 pi-tui 选择的方向。
如果您做过任何 GUI 编程,您可能已经听说过保留模式与即时模式。在保留模式 UI 中,您构建一个跨帧持久的组件树。每个组件知道如何渲染自己并可以缓存其输出,如果没有变化。即时模式 UI 中,您每帧从头开始重新绘制所有内容(尽管在实践中,即时模式 UI 也会缓存,否则它们会崩溃)。
pi-tui 使用一个简单的保留模式方法。一个 Component 只是一个具有 render(width) 方法的对象,该方法返回一个带有 ANSI 转义代码的字符串数组(适合视口水平的行,用于颜色和样式)和一个可选的 handleInput(data) 方法用于键盘输入。一个 Container 持有一个垂直排列的组件列表并收集所有渲染的行。TUI 类本身就是一个容器,负责协调一切。
当 TUI 需要更新屏幕时,它会要求每个组件渲染。组件可以缓存其输出:一个完全流式传输的助手消息不需要每次重新解析 markdown 和重新渲染 ANSI 序列。它只返回缓存的行。容器从所有子组件收集行。TUI 收集所有这些行并将其与之前渲染的组件树的行进行比较。它保持一个后缓冲区,记住写入滚动缓冲区的内容。
然后它只重新绘制更改的内容,使用一种我称为差分渲染的方法。我对名称很糟糕,这可能有一个官方名称。
这里有一个简化的演示,说明了到底会重新绘制什么。
$ pi
╭─────────────────────────────────╮
│ > _ │
╰─────────────────────────────────╯
▶ 点击开始 | 重新绘制的行:0/10
算法很简单:
首次渲染:直接将所有行输出到终端
宽度更改:完全清除屏幕并重新渲染所有内容(软换行更改)
正常更新:找到与屏幕上显示的内容不同的第一行,移动光标到该行,并从那里重新渲染到结束
有一个问题:如果第一个更改的行在可见视口上方(用户向上滚动),则需要完全清除并重新渲染。终端不允许您在视口上方写入滚动缓冲区。
为了防止更新期间出现闪烁,pi-tui 将所有渲染包裹在同步输出转义序列中(CSI ?2026h 和 CSI ?2026l)。这告诉终端缓冲所有输出并原子地显示它。大多数现代终端都支持这一功能。
它的工作效果如何,闪烁程度如何?在任何功能齐全的终端(如 Ghostty 或 iTerm2)中,这效果非常好,您永远不会看到任何闪烁。在功能较差的终端实现中(如 VS Code 的内置终端),您会根据一天的时间、显示大小、窗口大小等因素看到一些闪烁。鉴于我非常习惯 Claude Code,我没有花更多时间优化它。我对在 VS Code 中看到的少量闪烁感到满意。我不会感到宾至如归。而且它的闪烁程度比 Claude Code 少。
这种方法有多浪费?我们存储了整个滚动缓冲区中以前渲染的行的副本,并且每次 TUI 被要求渲染时都会重新渲染行。上述缓存可以缓解这一问题,因此重新渲染并不是一个大问题。我们仍然需要比较很多行。实际上,在 25 年以上的计算机上,这在性能和内存使用方面都不是一个大问题(对于非常大的会话,几百千字节)。感谢 V8。作为回报,我得到了一个非常简单的编程模型,允许我快速迭代。
我不需要解释您应该从编码代理工具包中期待什么功能。pi 附带了您从其他工具中习惯的所有基本功能:
在 Windows、Linux 和 macOS(或任何具有 Node.js 运行时和终端的系统)上运行
支持多个提供商,并且可以在会话中间切换模型
会话管理,包括继续、恢复和分支
从全局到项目特定的层次加载项目上下文文件(AGENTS.md)
用于常见操作的斜杠命令
支持参数的自定义斜杠命令作为 markdown 模板
Claude Pro/Max 订阅的 OAuth 身份验证
通过 JSON 进行自定义模型和提供商配置
支持实时重新加载的可自定义主题
支持模糊文件搜索、路径补全、拖放和多行粘贴的编辑器
在代理工作时的消息队列
支持具有视觉能力的模型的图像
会话的 HTML 导出
通过 JSON 流式传输和 RPC 模式的无头操作
全面的成本和令牌跟踪
如果您想要完整的功能列表,请阅读 README。更有趣的是,pi 在哲学和实现方面与其他工具包有何不同。
这是系统提示:
您是专家编码助手。您通过阅读文件、执行命令、编辑代码和编写新文件来帮助用户完成编码任务。
可用工具:
- read:读取文件内容
- bash:执行 bash 命令
- edit:对文件进行精确编辑(旧文本必须完全匹配)
- write:创建或覆盖文件
指南:
- 使用 bash 进行文件操作,如 ls、grep、find
- 使用 read 来检查文件,然后再编辑
- 使用 edit 进行精确更改(旧文本必须完全匹配)
- 只有在创建新文件或完全重写时才使用 write
- 在总结您的操作时,请直接输出纯文本 - 不要使用 cat 或 bash 来显示您做了什么
- 在您的回复中要简洁
- 在处理文件时,请清晰地显示文件路径
文档:
- 您自己的文档(包括自定义模型设置和主题创建)位于:/path/to/README.md
- 当用户询问功能、配置或设置时,请阅读它,特别是当用户要求您添加自定义模型或提供商或创建自定义主题时。
就是这样。唯一被注入到底部的内容是您的 AGENTS.md 文件。既有全局的文件(适用于所有会话),也有存储在项目目录中的项目特定文件。这是您可以自定义 pi 的地方。您甚至可以替换整个系统提示,如果您愿意。与 Claude Code 的系统提示、Codex 的系统提示或 opencode 的特定模型提示相比,这是一个很大的不同。
您可能认为这很疯狂。很可能,模型已经在其本地编码工具包上进行了训练。因此,使用本地系统提示或类似的提示(如 opencode)将是最理想的。但是,事实证明,所有前沿模型都经过了大量的强化学习训练,因此它们本质上理解了什么是编码代理。似乎不需要 10,000 个令牌的系统提示,我们稍后将在基准部分看到这一点,我也通过过去几周仅使用 pi 来体验到了这一点。Amp 虽然复制了一些本地系统提示的部分,但似乎也能很好地使用自己的提示。
以下是工具定义:
以下是工具定义:
read 读取文件内容。支持文本文件和图像(jpg、png、gif、webp)。图像作为附件发送。对于文本文件,默认读取前2000行。使用偏移量/限制来处理大文件。 - path:要读取的文件路径(相对或绝对) - offset:开始读取的行号(1-indexed) - limit:要读取的最大行数 write 将内容写入文件。如果文件不存在,则创建文件;如果文件存在,则覆盖文件。自动创建父目录。 - path:要写入的文件路径(相对或绝对) - content:要写入文件的内容 edit 通过替换确切的文本来编辑文件。旧文本必须完全匹配(包括空白字符)。使用此方法进行精确的编辑。 - path:要编辑的文件路径(相对或绝对) - oldText:要查找和替换的确切文本(必须完全匹配) - newText:要替换旧文本的新文本 bash 在当前工作目录中执行bash命令。返回stdout和stderr。可选地提供超时时间(以秒为单位)。 - command:要执行的bash命令 - timeout:超时时间(以秒为单位)(可选,无默认超时)
如果您想限制代理修改文件或运行任意命令的能力,还有额外的只读工具(grep、find、ls)。默认情况下,这些工具是禁用的,因此代理仅获得上述四种工具。
事实证明,这四种工具对于有效的编码代理来说已经足够了。模型知道如何使用bash,并且已经在类似的输入模式下接受了读取、写入和编辑工具的训练。将其与Claude Code的工具定义或opencode的工具定义(显然源自Claude Code,具有相同的结构、示例和git提交流程)进行比较。值得注意的是,Codex的工具定义与pi的工具定义类似,都是最小化的。
pi的系统提示和工具定义的总体积少于1000个令牌。
pi以全YOLO模式运行,假设您知道自己在做什么。它对文件系统具有无限制的访问权限,可以在没有权限检查或安全保障的情况下执行任何命令。没有文件操作或命令的权限提示。没有Haiku的bash命令预检查以检测恶意内容。具有完全的文件系统访问权限,可以在您的用户权限下执行任何命令。
如果您查看其他编码代理的安全措施,它们大多数都是安全剧场。一旦代理可以编写代码并运行代码,基本上就已经结束了。唯一可以防止数据外泄的方法是切断代理运行环境的所有网络访问,这使得代理几乎变得无用。另一个选择是允许列表域,但这也可以通过其他方式绕过。
Simon Willison已经广泛撰写关于这个问题的内容。他的“双LLM”模式试图解决困惑的副代理攻击和数据外泄问题,但即使他也承认“这个解决方案很糟糕”,并引入了巨大的实现复杂性。核心问题仍然存在:如果LLM具有访问私人数据和发出网络请求的工具,您将陷入攻击向量的无休止循环中。
由于我们无法解决这三个能力(读取数据、执行代码、网络访问)的组合,pi直接采用YOLO模式。每个人都以YOLO模式运行以完成任何有生产力的工作,因此为什么不把它作为默认和唯一的选项呢?
默认情况下,pi没有网页搜索或获取工具。然而,它可以使用curl或从磁盘读取文件,这两种方法都为提示注入攻击提供了足够的攻击面。文件或命令输出中的恶意内容可以影响行为。如果您对完全访问感到不舒服,可以在容器中运行pi或使用其他工具(伪造的防护栏)。
pi不支持内置待办事项。在我的经验中,待办事项列表通常会让模型更加混乱,而不是提供帮助。它们添加了模型必须跟踪和更新的状态,这引入了更多可能出错的机会。
如果您需要任务跟踪,可以通过写入文件使其外部可见:
# TODO.md - [x] 实现用户身份验证 - [x] 添加数据库迁移 - [] 编写API文档 - [] 添加速率限制
代理可以根据需要读取和更新此文件。使用复选框可以跟踪已完成和未完成的任务。简单、可见且在您的控制之下。
pi没有内置计划模式。告诉代理在不修改文件或执行命令的情况下与您一起思考问题通常就足够了。
如果您需要跨会话持久化计划,可以将其写入文件:
# PLAN.md
## 目标
重构身份验证系统以支持OAuth
## 方法
1. 研究OAuth 2.0流程
2. 设计令牌存储模式
3. 实现授权服务器端点
4. 更新客户端登录流程
5. 添加测试
## 当前步骤
正在处理步骤3 - 授权端点
代理可以读取、更新和引用计划,同时进行工作。与仅在会话中存在的短暂计划模式不同,基于文件的计划可以跨会话共享,并且可以与代码一起版本化。
有趣的是,Claude Code现在有一个计划模式,它基本上是只读分析,并最终将写入磁盘上的markdown文件。并且您基本上不能在没有批准大量命令调用的情况下使用计划模式,因为没有这些,规划基本上是不可能的。
pi与Claude Code的区别在于,我对一切都有完整的可见性。我可以看到代理实际查看了哪些源以及哪些源完全被忽略了。在Claude Code中,编排的Claude实例通常会生成一个子代理,您对子代理的行为没有任何可见性。我可以立即看到markdown文件。我可以与代理合作编辑它。简而言之,我需要可见性来进行规划,而Claude Code的计划模式无法提供给我。
如果您必须在规划过程中限制代理,可以通过CLI指定代理可以访问哪些工具:
pi --tools read ,grep,find, ls
这为您提供了只读模式,用于探索和规划,而无需代理修改任何内容或运行bash命令。然而,您可能不会对此感到满意。
pi不支持MCP。我已经广泛撰写关于这个问题的内容,但TL;DR是:MCP服务器对于大多数用例来说是过度的,并且带来了显著的上下文开销。
像Playwright MCP(21个工具,13.7k令牌)或Chrome DevTools MCP(26个工具,18k令牌)这样的流行MCP服务器会在每个会话中将其整个工具描述转储到您的上下文中。这会在您开始工作之前就占用7-9%的上下文窗口。您在给定会话中可能永远不会使用其中许多工具。
替代方案很简单:构建带有README文件的CLI工具。代理在需要工具时读取README文件,只在必要时支付令牌成本(渐进式披露),并且可以使用bash调用工具。这种方法是可组合的(管道输出、链接命令)、易于扩展(只需添加另一个脚本)且令牌高效。
以下是如何将网页搜索添加到pi:
我在github.com/badlogic/agent-tools维护着这些工具的集合。每个工具都是一个简单的CLI,带有代理按需读取的README文件。
如果您绝对必须使用MCP服务器,可以查看Peter Steinberger的mcporter工具,该工具将MCP服务器包装为CLI工具。
pi的bash工具以同步方式运行命令。没有内置的方法可以启动开发服务器、在后台运行测试或与REPL交互,同时命令仍在运行。
这是故意的。后台进程管理增加了复杂性:您需要进程跟踪、输出缓冲、退出时清理以及向正在运行的进程发送输入的方法。Claude Code使用其后台bash功能处理其中一些内容,但它具有糟糕的可见性(Claude Code的一个常见主题),并强制代理跟踪正在运行的实例,而没有提供查询它们的工具。在早期的Claude Code版本中,代理在上下文压缩后会忘记所有后台进程,并且没有查询它们的方法,因此您必须手动终止它们。这个问题已经在后来得到了解决。
使用tmux代替。以下是pi在LLDB中调试崩溃的C程序:
这就是可见性。相同的方法适用于长时间运行的开发服务器、监视日志输出和类似用例。并且,如果您愿意,您可以通过tmux进入上面的LLDB会话并与代理合作调试。Tmux还为您提供了一个CLI参数来列出所有活动会话。多好啊。
根本不需要后台bash。Claude Code也可以使用tmux。bash就是您所需的全部。
pi不支持子代理。
那对于可观察性来说怎么样?同样的方法也适用于长时间运行的开发服务器、监视日志输出和类似的用例。如果你愿意,你可以通过 tmux 跳入上面的 LLDB 会话,并与代理一起调试。Tmux 也为你提供了一个 CLI 参数来列出所有活动会话。多好啊。
根本不需要后台 Bash。Claude Code 也可以使用 tmux,你知道。Bash 就是你所需要的。
pi 没有专用的子代理工具。当 Claude Code 需要执行复杂任务时,它经常生成一个子代理来处理任务的一部分。你无法看到子代理做了什么。这是一个黑盒子中的黑盒子。代理之间的上下文传递也很糟糕。编排代理决定传递给子代理的初始上下文,你通常对此有很少的控制权。如果子代理犯了错误,调试很痛苦,因为你无法看到完整的对话。
如果你需要 pi 生成自己,只需要求它通过 Bash 运行自己。你甚至可以让它在 tmux 会话中生成自己,以获得完整的可观察性和与子代理直接交互的能力。

但更重要的是:修复你的工作流程,至少是那些与上下文收集有关的工作流程。人们在会话中使用子代理,认为他们节省了上下文空间,这是正确的。但这是错误的思考方式。会话中间使用子代理进行上下文收集是你没有提前规划的迹象。如果你需要收集上下文,请先在自己的会话中执行此操作。创建一个你稍后可以在新会话中使用的工件,以便为代理提供所需的所有上下文,而不会污染其上下文窗口中的工具输出。该工件对下一个功能也很有用,你将获得完整的可观察性和可控性,这在上下文收集期间很重要。
因为尽管人们普遍认为,模型仍然很难找到实现新功能或修复错误所需的所有上下文。我将其归因于模型只被训练为阅读文件的一部分,而不是整个文件,因此它们不愿意阅读所有内容。这意味着它们会错过重要的上下文,无法看到它们需要的内容以正确完成任务。
只需查看 pi-mono 问题跟踪器 和拉取请求。许多问题都被关闭或修订,因为代理无法完全理解所需的内容。这不是贡献者的错,我真正感谢他们,即使不完整的 PR 也帮助我加快了速度。这只是意味着我们过度信任我们的代理。
我并不是完全否定子代理。有一些有效的用例。我的最常见用例是代码审查:我告诉 pi 生成一个带有代码审查提示(通过自定义斜杠命令)的子代理,它会获取输出。
---
description: 运行代码审查子代理
---
通过 Bash 生成一个子代理来执行代码审查:$@
使用 `pi --print` 并传入适当的参数。如果用户指定了模型,请使用 `--provider` 和 `--model`。
传递一个提示给子代理,要求它审查代码以查找:
- 错误和逻辑错误
- 安全问题
- 错误处理缺陷
不要自己阅读代码。让子代理执行此操作。
报告子代理的发现。
以下是我如何使用它来审查 GitHub 上的拉取请求:
使用一个简单的提示,我可以选择要审查的具体内容和要使用的模型。我甚至可以设置思考级别,如果我愿意。我还可以将完整的审查会话保存到文件中,并在另一个 pi 会话中打开它,如果我愿意。我也可以说这是一个短暂的会话,不应将其保存到磁盘。所有这些都转换为一个提示,主代理读取并根据提示通过 Bash 再次执行自己。虽然我无法完全看到子代理的内部工作,但我可以完全看到其输出。其他工具通常不提供此功能,这对我来说没有意义。
当然,这有点像模拟的用例。在现实中,我只需生成一个新的 pi 会话并要求它审查拉取请求,可能将其拉入本地分支。审查后,我会提供自己的审查,然后我们一起合作直到它完成。这是我使用的工作流程,以避免合并垃圾代码。
生成多个子代理以并行实现各种功能是一个反模式,在我的书中它行不通,除非你不在乎你的代码库会变成一堆垃圾。
我做出了很多夸张的声明,但我是否有数值证明上述所有相反的说法实际上有效?我有自己的亲身经历,但这很难在博客文章中传达,你只需要相信我。因此,我为 pi 创建了一个 Terminal-Bench 2.0 测试运行,使用 Claude Opus 4.5,并让它与 Codex、Cursor、Windsurf 和其他编码工具及其各自的原生模型竞争。很明显,基准测试并不能代表真实世界的性能,但这是我可以提供的最好的证明,证明我所说的并非都是完全的胡说八道。
我进行了五次试验的完整运行,这使得结果有资格提交到排行榜。我还启动了第二次运行,只在 CET 运行,因为我发现一旦 PST 上线,错误率(及其后果,基准测试结果)就会变差。以下是第一次运行的结果:

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

以下是我提交给 Terminal-Bench 团队以便包含在排行榜中的 results.json 文件。pi 的基准测试运行器可以在 这个存储库 中找到,如果你想复制结果。我建议你使用 Claude 计划而不是按使用付费。
最后,这是 CET 只运行的简要概览:

这将需要再花一天或两天的时间才能完成。我将在完成后更新此博客文章。
还请注意 Terminus 2 在排行榜上的排名。Terminus 2 是 Terminal-Bench 团队自己的最小代理,它只为模型提供一个 tmux 会话。模型将命令作为文本发送给 tmux,并自己解析终端输出。没有花哨的工具,没有文件操作,只有原始的终端交互。它正在与具有更复杂工具和与多种模型合作的代理竞争。更多证据表明,最小化的方法可以做得同样好。
基准测试结果很有趣,但真正的证明在于实践。我的实践是我的日常工作,pi 在其中表现出色。Twitter 上充满了上下文工程的帖子和博客,但我觉得我们目前拥有的工具都不允许你真正进行上下文工程。pi 是我尝试为自己构建一个工具,在那里我可以尽可能地控制一切。
我对 pi 的当前状态很满意。还有几个功能我想添加,例如 压缩 或 工具结果流,但我不认为我还需要更多的功能。缺乏压缩对我个人来说并不是问题。出于某种原因,我能够将数百个我和代理之间的交换压缩到单个会话中,这是我用 Claude Code 无法做到的。
话虽如此,我欢迎贡献。但是,和我所有的开源项目一样,我倾向于独裁。这是我多年来从大型项目中学到的一个教训。如果我关闭了你提交的 issue 或 PR,我希望你不会有任何怨恨。我也会尽力给你解释为什么。我的目标是保持 pi 集中和可维护。如果 pi 不符合你的需求,我敦促你 fork 它。我是认真的。如果你创建了更好地满足我需求的东西,我会很乐意加入你的努力。
我认为上述一些经验也适用于其他工具。请告诉我这对你来说如何。