“你说过不要 MCP!”
如果你过去访问过 pi.dev,你会发现一个自豪的声明,称 Pi 不支持 MCP。如果你听过我们谈论 Pi 的播客,你会发现我们不止一次对 MCP 发表过轻蔑的言论。包括 Mario 关于它的一篇帖子。然而,如果你升级到 Pi,你会发现 MCP 现在是一项受支持的功能。发生了什么?
事情会变
首先要记住的是,世界不是静止的。过去一年我们一直在关注 MCP,而今天的 MCP 已不是过去的 MCP。然而,仅凭这一点还不足以成为将其纳入核心的理由。如你所知,Pi 拥有一个出色的扩展生态系统,MCP 难道不能作为一个扩展吗?甚至可以是 Earendil 认可的扩展。是的,你说得对,MCP 本可以是一个扩展,就像它曾经那样。MCP 现在成为核心的一部分,是我们共同思考并重新审视它的结果。
究竟改变了什么?
我们将 MCP 引入核心的原因不仅仅在于 MCP 本身的变化,还因为我们发现它所需的改动总体上是有用的。例如,我们对 MCP 所做的改动也使得在 Pi 中更容易使用 Jev。最终,Pi 所需要的与 MCP 所需要的非常相似:一个以解释器形式存在的沙盒,供其使用。
尽管 MCP 的许多方面都有所改进,但仍有不少没有。MCP 最大的问题仍然是它难以组合。即使有了 codemode——一个允许组合工具调用的精巧小沙盒——MCP 也未能完全实现这一点。但此时这与其说是 MCP 的问题,不如说是现有 MCP 服务器以及不同 harness 与之协作方式的问题。
许多 MCP 服务器仍然是为那些只是将工具倾倒入上下文、并试图通过返回文本来优化 token 效率的 harness 而构建的。我们目前对 MCP 的看法是,它应该更接近带有智能工具发现的 OpenAPI。这意味着工具应返回结构化数据,并且工具应能通过其文档和描述被发现。
CLI 之所以如此实用,是因为智能体和模型只是用高效的 bash 技巧将东西连接起来。但原则上,用 MCP 也可以做到这一点。Pi 中的 MCP 只是建立在将这些工具暴露给 JavaScript 沙盒的基础上,就像 Codex 等其他 harness 所做的那样。
现代 LLM 中的 MCP
这会引出一个问题:为什么我们不直接做 Codemode 而不做 MCP?部分答案与 Pi 目前表达工具的方式有关。近几个月我们做了大量工作,使 Pi 能够适应那些支持延迟工具加载、对话中途系统消息和推理级别变化的新模型。然而,我们尚未升级我们的工具配置,以更好地适应这些新能力。
在 Codemode 的世界里,我们需要决定工具是可供 LLM 使用,还是仅供 LLM 的 Codemode 部分使用。普通的 MCP 扩展无法从 Pi 的工具配置中获取足够的元数据来让这种体验良好运作。因此我们需要确保工具可以被配置为仅延迟加载,或者成为 Codemode 专属的东西。
虽然我们本可以仅仅接好元数据来支持更好的 MCP 扩展,但我们也认为带有 Codemode 的 MCP 解决了它传统上存在的不少问题。我们相信积极影响某件事物的最佳方式是拥抱它。虽然我们认为现代 MCP 比以往任何时候都处于更好的位置,但服务器和模式仍有改进空间。所以我们想参与这场对话,帮助塑造它使其在小型 harness 中良好运作,而不是袖手旁观。
什么是 Codemode?
我们已经谈了很多关于 Codemode 的内容,也许值得解释一下它到底是什么。当一个 harness 执行工具时,大部分情况下它有两面:它可以在 bash 运行的地方执行,也可以在 harness agent 循环运行的地方执行。两边的信任级别非常不同。harness 循环通常运行在受信任的环境中,而它执行的工具通常运行在并不那么受信任的沙箱中。
Codemode 的特殊之处在于它运行在 harness 运行的地方。最好将其理解为一种编排和协调工具调用的机制。它是一个沙箱,允许 agent 以更灵活的方式发出这些工具调用,决定应该以什么顺序执行,并允许它使用 JavaScript 将它们组合在一起。由于 Codemode 也运行在 harness 一侧,它的状态也作为会话记录的一部分维护,而不是文件系统。
理论上任何语言都可以,但 JavaScript 相当有吸引力,因为小版本的 JavaScript 可以作为 WASM 二进制文件发布,并提供合理程度的保护。
在 Pi 中,当配置了 MCP 时 Codemode 会自动加载,也可以作为默认工具添加到配置中。只需让 pi 重新配置自己以启用 codemode!然后你可以用它做相当有趣的事情,不仅仅是 MCP。例如,当你登录了提供“Jev”的提供商时,你可以这样做:
使用 typesafe/jev 通过 codemode 找到我们问题跟踪器上最沮丧的 20 位评论者
它会巧妙地将 Linear MCP 和 Jev 等组合起来进行分析,直接在 Pi 内部完成,完全不浪费任何上下文。
通过 codemode 使用 typesafe/jev 来找出我们问题追踪器中最沮丧的人。我会从 Linear 拉取未关闭的 issue,然后让 Jev 对每个讨论串的语气进行评分,每次四个。
const { issues } = await tools.mcp__linear__list_issues({
team: "Pi",
state: "open",
limit: 250,
});
const jev = await models.getModelOfType(
"classifier",
"cloudflare-workers-ai",
"typesafe/jev",
);
const questions = {
frustration: {
type: "choice",
instructions:
"Judge ONLY the emotional tone of the people writing. " +
"Ignore how severe the bug is.",
criteria: {
none: "Neutral, factual, or friendly, even about a serious bug",
mild: "Explicit annoyance, impatience, or disappointment",
high: "Clearly angry, exasperated, sarcastic, or fed up",
},
},
};
const results = [];
let next = 0;
async function worker() {
while (next < issues.length) {
const issue = issues[next++];
const { comments } = await tools.mcp__linear__list_comments({
issueId: issue.identifier,
});
const c = await models.classify(jev, {
state: { ...issue, comments },
questions,
});
results.push({
id: issue.identifier,
title: issue.title,
...c.answers.frustration,
});
}
}
await Promise.all([worker(), worker(), worker(), worker()]);
store("frustration", results);
const score = (r) =>
r.probabilities.mild * 0.5 + r.probabilities.high;
const counts = {};
for (const r of results) counts[r.choice] = (counts[r.choice] ?? 0) + 1;
const flagged = results.filter((r) => r.choice !== "none");
flagged.sort((a, b) => score(b) - score(a));
return {
total: results.length,
counts,
flagged: flagged.map((r) => `${r.id} ${r.title}`),
};
...(此前还有 331 次调用)
✓ mcp__linear__list_comments {"issueId":"PI-4714"} 255ms
✓ mcp__linear__list_comments {"issueId":"PI-3200"} 354ms
✓ models.classify cloudflare-workers-ai/typesafe/jev 756ms
✓ models.classify cloudflare-workers-ai/typesafe/jev 750ms
{
"total": 167,
"counts": {
"none": 156,
"mild": 11
},
"flagged": [
"PI-7740 TUI after /reload does not follows custom tool's re…",
"PI-9566 context size defaults to 128k despite the real size…",
"PI-10031 Pi sporadically stuck in \"Working...\" when thinking…",
"PI-4714 [feature] /update TUI command instead of pi update",
"PI-5916 Support provider extensions with model aliases and…",
"PI-7547 [Windows] [sink-thread] How do you use Pi on window…",
"PI-6907 Add an Installation section to the README",
"PI-6212 Proposal: Bedrock path should honor `compat.forceAd…",
"PI-9932 before_agent_start: forced system prompt keeps tool…",
"PI-7730 High CPU usage on Mac OS with long session",
"PI-6600 pi update --extensions and npm scripts: npm 11.16.0…"
]
}
追踪器中的大部分内容都很平静。Jev 将 167 个未关闭 issue 中的 156 个评为中性,11 个评为轻度沮丧,没有评为高度沮丧的。
最明显的几个案例:
- PI-6907 README 中没有安装章节(“It's frustrating”)
- PI-10031 Pi 在思考时按下 Esc 后会卡在“Working...”
- PI-4714 一个 /update 命令请求(“a pain in the butt”)
- PI-7730 macOS 上长时间会话时 CPU 占用过高
每个 issue 的判定结果都存储在 codemode 的 frustration 下,因此我可以深入查看其中任何一个,而无需再次获取这些 issue。
关于 Jev 和 Codemode 之类的东西,我们之后还会有更多要说的,但我们希望这篇文章能作为一个例子,展示我们如何随着世界的不断演变,持续深思熟虑地调整和更新 Pi。
