大家好,我是小木头。如果你还没有用过 Pi 那在国庆期间一定要来尝试一下。它是一个运行在终端里的 AI Agent,官方对它的定义是极简、可扩展:核心保持得很小,再通过扩展工具和配置来适配不同的工作流。也因为它的 Agent harness 足够透明,我们非常适合用它来观察工具究竟怎样进入到模型的上下文。
Pi 以前对 MCP 的态度非常明确,官网直接写着不支持 MCP。团队的成员在播客和文章里也不止一次地解释过为什么他们并不太喜欢 MCP。但到了 Pi 0.99,MCP 不但来了,而且直接加进到了 Pi 的核心。这并不只是一次兼容性的更新,更值得关注的是:Pi 没有简单地把一批 MCP 工具全部放进模型的上下文,而是重新设计了模型看到这些工具的方式。
本期视频我们并不讨论 MCP 应不应该流行,咱们通过技术浅析来尝试回答 Pi Agent 对 MCP 支持的三个问题:第一,MCP 工具在不同模式下究竟怎样出现在模型的上下文里;第二,Code Mode 到底怎样调用 MCP;第三,大家过去对 MCP 的那些批评,Pi 解决了多少又留下了什么。
MCP 经常被概括成让 Agent 连接外部工具的统一协议,统一连接方式当然是有价值的。但很多人的不满并不在连接这一层,而在连接之后:一个 MCP Server 可能提供几十个工具。传统做法是把每个工具的名称、描述和参数 Schema 全部放进模型的上下文,Server 一多,模型还没有开始工作,工具定义已经占掉了一大块 token。
第二个问题是组合:如果模型需要读取两个文件、筛选数据,再计算一个结果,常见过程是先调用一次工具把结果放回对话,再调用下一次。结果又回到对话,每一步都要经过 Agent loop 也都可能把中间数据写进历史。
第三个问题是工具的输出:很多 MCP Server 为了照顾聊天式 Agent,返回的是大段文本而不是方便继续计算的结构化数据。此外还有像 Server 的质量、安全边界以及权限控制等等,换一种调用语法并不会让这些问题自动消失。开发团队在官方文章里面其实也承认了这一点,今天的 MCP 仍然很难组合,很多 Server 仍然假设 harness 会把所有工具直接放进上下文。
所以 Pi 这次更新的重点不是终于支持 MCP,而是把连接方式和暴露方式分开来了。我把本地的 Pi 更新到了 0.99.2,然后安装了官方 Filesystem MCP Server。这是一个专门操作文件系统的 MCP Server,一共提供 14 个工具:包括读取和写入文件、列目录、搜索移动文件以及读取文件信息。
它的能力很好理解,又足够覆盖单次调用、并发调用和大段的结果,所以很适合拿来比较三种 Pi 所支持的模式。这次验证里面我把它限制在一个临时的演示目录,目录里只有 projects.json 和 notes.txt 两个测试文件。Server 不能访问其他的项目,现在先看看 Pi 识别到了什么。
在这里可以看到 Server 已经连接,当前 exposure 设置为 codemode。Server 一共提供 14 个工具:包括读取文件、列出目录、搜索和获取文件信息。那 MCP 究竟具体该怎么配置呢?Pi 从全局的 ~/.pi/agent/mcp.json 以及可信项目里的 .pi/mcp.json 配置文件读取 MCP Server。
exposure 不是整个 MCP 配置的一个全局开关,而是 mcpServers 下面每个 Server 自己的属性。你可以让文件 Server 使用 codemode,可以让另一个 Server 使用 direct。可选值一共有 4 个:direct、deferred、codemode 和 hidden。
除了直接编辑 json,也可以在 Pi 会话里面输入 /mcp,选择 Server 再修改 exposure。修改会写回定义这个 Server 的 mcp.json,最后再用 pi mcp list --json 检查每个 Server 实际生效的模式。同一个 Server 修改 exposure 之后呢,模型看到的上下文会完全不同。
第一种模式是 direct,这也是最接近传统 MCP harness 的方式。我把 Filesystem Server 改成 direct,Pi 在第一次模型调用中加入了全部的 14 个工具。每个工具都有名称、说明和 JSON Schema,模型可以直接调用 mcp__demo_files__read_text_file。它并不需要搜索也不需要进入 Code Mode,这种方式最直接,调用路径也最短。
代价自然也很明显:无论这一轮是否需要这些工具,它们的完整定义都已经进入上下文。一个 Server 的 14 个工具还可以接受,如果同时连接多个大型的 Server,工具定义会迅速地膨胀。这也是大家在 MCP 使用时会感到头痛的。
第二种是 deferred,这时 Pi 首轮不会把 14 个 MCP 工具交给模型。模型只看到一个 tool_search 以及系统提示词中的 Server index,也就是索引。模型先用自然语言搜索需要的工具:tool_search 的参数只有查询文本和可选的数量上限,它返回的不是一串孤立的工具名。
而是 "Loaded N tools" 这种文本,再逐项地给出工具名和简介。这里还会看到一个叫 "toolsAdded" 这么一个东西,它不是要执行的函数也不是模型生成的工具参数。这是一个 Pi 内部的东西:Pi 内部系统消息上的一个字段,里面保存这一刻新增的完整的工具定义。
也就是名称、说明、参数 Schema。搜索结束以后 Pi 的会话记录里会同时留下两样东西:第一样是普通的工具调用结果,也就是比如 "Loaded 8 tools" 以及工具名称简介;第二样呢是紧随其后的内部工具状态 "toolsAdded",保存 8 份完整的 Schema。
前者会进入下一轮请求对话消息;后者由 Pi 来编译,进入下一轮请求的 tools 部分。它并不是追加到上下文末尾的一段系统提示词的文本,在下一次请求模型之前 Pi 会按顺序重放这些系统消息。计算当前有效的工具集合,toolsAdded 不会以这段内部 json 原样发给模型。
它也会在 provider 适配层把它转换成目标 API 的传输格式:例如 OpenAI Responses 支持 Tool Search,那么会生成一组 tool_search_call 和 tool_search_output 输入项。完整的工具定义放在后者的 tools 字段里面;支持 additional_tools 的模型呢,会收到对应的增量工具项。
如果模型不支持对话中途增加工具,Pi 会整合这些状态把当前完整的工具集合重新放进请求顶层的 tools 字段。对模型来说最终效果相同,下一次请求里面就多出了这 8 个可以直接调用的工具。要注意的是是下一次调用,当前这次调用不能立即使用刚刚找到的工具。
在我的实测里 tool_search 从 14 个工具中加载了 8 个相关的工具。Pi 把这次的工具集合变化记录到了 SystemMessage.toolsAdded,下一轮模型才真正看到这 8 份 Schema。然后用 read_multiple_files 一次读取两个文件,deferred 模式解决的是启动时的上下文膨胀。
它没有改变工具调用本身:加载以后 MCP 工具调用和返回内容仍然会写入 Agent 主循环维护的对话消息。代价是多了一次模型的往返:模型先搜索,下轮才能用。Deferred 用额外的延迟换来了更小的初始上下文。
第三种是本期重点介绍的 MCP codemode exposure,也是 Pi 0.99.2 连接 MCP Server 的默认方式。这里先明确本期的范围:Pi 自己还有一个全局设置 codemode.mode: "only",它可以把内置工具和扩展工具统一收进 Code Mode。
它和这里的 MCP exposure codemode 很容易混淆,但不是同一个配置。今天我们只讨论 MCP 工具怎样通过 Code Mode 暴露。Pi 的通用 Code Mode 我们留到后面单独介绍。在首轮模型请求的工具列表里,我没有看到 14 个独立的 MCP 工具:对于这组 MCP 能力主模型只通过一个叫 codemode 这样的工具访问。
Pi 原本的内置工具是否直接可见,不属于这项 MCP exposure 控制的范围。codemode 的参数 Schema 很简单:只有一个字符串字段的 code,模型把要执行的 JavaScript 放进这个字段。这里要分清外层工具和内层脚本:codemode 是 Pi 暴露给主模型的顶层工具,不是在沙箱里运行的 JavaScript 函数。
模型调用它时把一整段 JavaScript 源码作为 code 字符串参数形式给到 Pi。在 harness 侧创建一个新的 QuickJS 运行环境,把这个字符串当做异步函数的函数体来执行。画面右侧显示的就是左侧 code 字段里的原始内容,不是 Pi 另外生成的一段代码。
不过这并不等于模型对内部工具一无所知:codemode 自己的 description 会提供沙箱辅助函数、MCP namespace,以及在预算内能够放下的工具声明。默认预算大约是 3000 个 token,模型先阅读这些信息理解任务、选择工具,再把调用流程写成 JavaScript。
这里尤其不要把 QuickJS 想成另一个 Agent:语义判断发生主模型生成代码的时候,QuickJS 只是按照代码来执行。工具没有内联时,模型可以在脚本里调用 searchTools、ALL_TOOLS 和 describeTool。其中 searchTools 使用 BM25 对工具名称和描述做文本相关性排名,并不是另一次的 LLM 调用也不会自己去理解任务。
如果搜索结果会影响下一步要生成代码,那么第一次脚本只能先返回候选工具;下一次模型调用再生成真正的执行脚本。Code Mode 因此不是凭空消灭发现这一步,而是提供了一套可以由脚本调用的工具目录。真正的读取发生在同一段 JavaScript 里面:这里我让它并发读 projects.json 和 notes.txt。
解析 JSON,筛选 active project,按照 issues 排序再只输出最后需要的结果。那最终的结果是 cedar:11 个 issue,辅助笔记也给出了同一个结论。关键并不只是少了几个工具定义:读取两个文件、解析筛选和排序都发生在了一次 Code Mode 调用内部。
中间数据不需要逐步地回到主对话 Agent 主循环维护的消息历史里,只会出现一条 codemode 调用。这里要区分两个层次:JavaScript 里的 text、image 以及顶层 return 会被 Pi 收集成 content,这是 codemode 工具返回正文会作为 tool result 交给模型。
details 则不是 JavaScript 返回,它是 Pi 在沙箱外自动附加的执行元数据。其中 details.calls 记录每次嵌套调用的名称、参数摘要、状态和耗时,它供会话记录和界面展示使用不会作为工具返回正文发给模型。所以更准确的说法是 codemode 工具的执行结果是一个 AgentToolResult:脚本控制其中的 content,Pi 负责补充 details。
内部 MCP 工具的完整返回不会逐条写进主对话的消息,这就是它和普通 MCP tool call 最本质的区别。把三种模式放在一起就很直观了:direct 是 14 份完整的工具定义加上直接的 MCP call 以及 result;deferred 首轮只有 tool_search 这么一个工具,搜索完成后相关工具的定义通过系统消息进入后续的上下文。
调用和结果仍然是展开的;MCP Code Mode 不会把这 14 个 MCP 工具作为顶层的工具暴露,而是提供了一个编排器和 Server namespace。MCP 调用发生在沙箱内部脚本可以组合、并发和过滤结果,再把少量的内容带回到对话。Pi 还提供了 hidden 选项:这个模式呢会连接 Server,但不向当前 Agent 暴露工具。
适合由其他扩展或控制逻辑来进行管理。这里有一个容易混淆的地方:没有把 MCP 工具作为独立的顶层工具声明,不等于模型完全看不到工具信息。预算内的声明仍然会出现在 codemode description 当中;其余元数据由 Agent harness 管理,可以通过 searchTools 和 describeTool 来查询。
Code Mode 改变的是声明格式和执行边界,不是把工具知识凭空的删除掉。在真正运行时我还遇到三个蛮有代表性的问题:第一是 namespace 命名,系统提示词写的是规范化以后的 mcp__demo_files。模型第一次尝试配置文件里的 demo-files,结果呢找不到 namespace:这就说明按需来发现虽然节省了上下文。
但发现过程本身可能会失败,命名、描述以及模型能否正确使用发现 API 都会影响可靠性。第二是工具的过滤:为了验证模型只能使用 Code Mode,我在启动时加了参数 --tools codemode。结果这个参数不仅关闭了主模型可直接调用的工具,也把供 Code Mode 调用的嵌套 MCP 工具一并过滤掉了。
正确做法是保持正常的工具配置,再在任务里明确要求只使用 MCP。第三个是路径的安全边界:这个例子正好说明了 Pi 这次没有解决什么 direct、deferred、Code Mode 改变的是工具进入上下文和被调用的方式,它们不会代替 MCP Server 做权限与输入的校验。
比如在 macOS 上面 /tmp 实际上指向的是 /private/tmp,而 Filesystem Server 授权的是规范路径。所以字面上的 /tmp 被判定为允许目录之外,调用直接就失败了;改用 /private/tmp 才成功。这个错误反而提供了一个很直观的例子:它说明 MCP Server 的文件边界不是一句提示词,而是 Server 实际执行的访问控制。
现在咱们就回到最开始的问题:Pi 的方案解决了 MCP 的哪些问题呢?首先是上下文的膨胀:direct 并没有去解决;deferred 按需加入顶层的 Schema;Code Mode 避免了逐个注册顶层工具,但仍会在 description 中携带预算内的工具信息。工具很多时它的优势才更加明显。
第二,工具组合:Code Mode 明显做了改善。循环、并发、条件判断、解析聚合都可以在 JavaScript 里面完成;下一个是模型的往返,也就是模型调用轮次:Deferred 会增加发现的步骤,Code Mode 可以把多个内部调用合并成一次主模型工具调用。
关于中间结果污染主对话,Code Mode 可以过滤以后再返回:所以呢明显减少了污染,但仍会留下调用的摘要和最终的输出。关于 Server 的输出质量这个呢并没有自动解决:这个是 Server 端的问题,Server 端需要去考虑的。如果 Server 仍然返回难以解析大段文本,JavaScript 也只能继续处理这样的文本。
关于安全 Pi 提供了沙箱和 exposure 选择,Server 也可以实施自己的权限边界。但一个高权限 MCP Server 仍然是高权限的工具:协议本身不会替你解决或决定哪些操作应该被允许。所以更准确说法显然并不是 Pi 修好了 MCP,它解决的或它修正的只是 MCP 工具进入 Agent 的方式。
过去大家把 MCP 和 CLI 放在一起比较时,CLI 往往更受欢迎:因为模型可以用 shell 组合多个命令、过滤输出只留下真正需要的信息。Pi 的 Code Mode 做的是把这种组合能力带到了 MCP tools:连接仍然由 MCP 负责,发现、组合、并发、过滤和少量状态则交给 harness 侧的 JavaScript 沙箱。
这并不意味着每个 MCP Server 都应该按照这种模式被接入,也不意味着 CLI 从此不重要。它意味着我们终于可以吧这两个问题分开讨论:一个 Server 应该怎样提供能力,一个 Agent 又应该怎样看见和调用这些能力。Pi 0.99 最有价值的变化倒不是说工具列表里多了 MCP,而是把工具的暴露方式变成了可配置的选项。
如果你的 server 很小 direct 可能最简单;如果工具很多但每次只用少数几个 deferred 可能更合适。如果任务需要组合多个工具、处理结构化的结果,并且你在意上下文的成本 Code Mode 就是这次更新重点提供的方式。这可能不是 MCP 的最终答案,但它至少让 MCP 更接近 Agent 原生工具。
而不只是另一套远程的函数调用。如果这期视频对你有帮助,欢迎关注我们的频道,也给咱们来个一键三连吧!我是小木头,我们下期见。