发布日期: 2025-09-26
作者: Kenton Varda, Sunil Pai
阅读时间: 9 分钟
事实证明,我们一直在错误地使用 MCP。
如今大多数 agent 通过直接向 LLM 暴露"工具"来使用 MCP。
我们尝试了不同的方法:将 MCP 工具转换为 TypeScript API,然后要求 LLM 编写调用该 API 的代码。
结果令人瞩目:
我们发现,当工具以 TypeScript API 形式呈现时,agent 能够处理更多工具和更复杂的工具。这可能是因为 LLM 的训练集中包含大量真实世界的 TypeScript 代码,但只包含少量人为设计的工具调用示例。
当 agent 需要连续调用多个工具时,这种方法的优势尤为明显。使用传统方法时,每个工具调用的输出必须输入到 LLM 的神经网络中,仅仅为了复制到下一个调用的输入,浪费了时间、能源和 token。当 LLM 可以编写代码时,它可以跳过所有这些步骤,只需要读取它需要的最终结果。
简而言之,LLM 擅长编写代码来调用 MCP,而不是直接调用 MCP。
什么是 MCP?
对于不熟悉的人来说:模型上下文协议 (Model Context Protocol) 是一种标准协议,用于让 AI agent 访问外部工具,使它们能够直接执行工作,而不仅仅是与你聊天。
换句话说,MCP 是一种统一的方式来:
- 暴露执行某项操作的 API,
- 同时提供 LLM 理解它所需的文档,
- 并通过带外方式处理授权。
2025 年,MCP 一直在掀起波澜,因为它突然极大地扩展了 AI agent 的能力。
MCP 服务器暴露的"API"被表达为一组"工具"。每个工具本质上是一个远程过程调用 (RPC) 函数------它通过一些参数调用并返回响应。大多数现代 LLM 都具有使用"工具"(有时称为"函数调用")的能力,这意味着它们被训练在想要调用工具时输出特定格式的文本。调用 LLM 的程序看到这个格式,按照规定调用工具,然后将结果反馈给 LLM 作为输入。
工具调用的解剖结构
在底层,LLM 生成代表其输出的"token"流。一个 token 可能代表一个单词、一个音节、某种标点符号或其他文本组件。
但是,工具调用涉及一个没有任何文本等价物的 token。LLM 被训练(或者更常见的是,微调)以理解它可以输出的一个特殊 token,意思是"以下内容应被解释为工具调用",以及另一个特殊 token,意思是"这是工具调用的结束"。在这两个 token 之间,LLM 通常会写入对应于某种描述调用的 JSON 消息的 token。
例如,假设你已将一个 agent 连接到提供天气信息的 MCP 服务器,然后你询问 agent 德克萨斯州奥斯汀的天气情况。在底层,LLM 可能会生成如下输出。注意,这里我们使用了 <| 和 |> 中的单词来表示我们的特殊 token,但实际上,这些 token 根本不代表文本;这只是为了说明。
我将使用天气 MCP 服务器来查找德克萨斯州奥斯汀的天气情况。
我将使用天气 MCP 服务器来查找德克萨斯州奥斯汀的天气情况。
<|tool_call|> { "name": "get_current_weather", "arguments": { "location": "Austin, TX, USA" } } <|end_tool_call|>
在输出中看到这些特殊 token 时,LLM 的控制系统会将该序列解释为工具调用。看到结束 token 后,控制系统暂停 LLM 的执行。它解析 JSON 消息并将其作为结构化 API 结果的单独组件返回。调用 LLM API 的 agent 看到工具调用,调用相关的 MCP 服务器,然后将结果发送回 LLM API。然后 LLM 的控制系统将使用另一组特殊 token 将结果反馈给 LLM:
<|tool_result|> { "location": "Austin, TX, USA", "temperature": 93, "unit": "fahrenheit", "conditions": "sunny" } <|end_tool_result|>
LLM 以与读取用户输入完全相同的方式读取这些 token------只是用户无法产生这些特殊 token,所以 LLM 知道这是工具调用的结果。然后 LLM 继续像往常一样生成输出。
不同的 LLM 可能使用不同的工具调用格式,但这是基本思路。
这种方法有什么问题?
工具调用中使用的特殊 token 是 LLM 在现实世界中从未见过的东西。它们必须经过特殊训练才能使用工具,基于合成的训练数据。它们并不总是很擅长这个。如果你向 LLM 提供太多工具或过于复杂的工具,它可能难以选择正确的工具或正确使用它。因此,鼓励 MCP 服务器设计者呈现大大简化的 API,而不是他们可能向开发者暴露的更传统的 API。
与此同时,LLM 在编写代码方面变得非常出色。事实上,要求 LLM 编写针对开发者通常暴露的完整、复杂 API 的代码似乎并不太困难。那么,为什么 MCP 接口必须"简化"呢?编写代码和调用工具几乎是同一件事,但似乎 LLM 可以做得比另一个好得多?
答案很简单:LLM 看过大量代码。它们没有看过大量"工具调用"。事实上,它们看过的工具调用可能仅限于由 LLM 自己的开发者构建的人为训练集,目的是为了训练它。而它们看过数百万开源项目的真实代码。
让 LLM 通过工具调用执行任务就像让莎士比亚参加一个月的普通话课程,然后要求他用普通话写剧本。这不会是他的最佳作品。
但 MCP 仍然有用,因为它是统一的
MCP 是为工具调用而设计的,但实际上并不一定非要以这种方式使用。
MCP 服务器暴露的"工具"实际上只是一个带有文档的 RPC 接口。我们并不必须将它们呈现为工具。我们可以获取这些工具,然后将它们转换为编程语言 API。
但是,当编程语言 API 已经独立存在时,我们为什么要这样做呢?几乎每个 MCP 服务器都只是围绕现有传统 API 的包装器------为什么不暴露那些 API 呢?
嗯,事实证明 MCP 还做了其他非常有用的事情:它提供了一种统一的方式来连接和了解 API。
AI agent 可以使用 MCP 服务器,即使 agent 的开发者从未听说过特定的 MCP 服务器,而 MCP 服务器的开发者也从未听说过特定的 agent。在过去,传统 API 很少出现这种情况。通常,客户端开发者总是确切地知道他们要为哪个 API 编码。结果,每个 API 都能够以略微不同的方式处理基本连接性、授权和文档等问题。
即使 AI agent 在编写代码时,这种统一性也很有用。我们希望 AI agent 在沙箱中运行,这样它只能访问我们给它的工具。MCP 使代理框架能够通过标准方式处理连接性和授权来实现这一点,独立于 AI 代码。我们也不希望 AI 必须在互联网上搜索文档;MCP 直接在协议中提供它。
好的,它是如何工作的?
我们已经扩展了 Cloudflare Agents SDK 以支持这个新模式!
例如,假设你有一个使用 ai-sdk 构建的应用程序,看起来像这样:
javascript
const stream = streamText({
model: openai("gpt-5"),
system: "You are a helpful assistant",
messages: [
{ role: "user", content: "Write a function that adds two numbers" }
],
tools: {
// tool definitions
}
})
你可以使用 codemode 辅助函数包装工具和提示,并在你的应用程序中使用它们:
javascript
import { codemode } from "agents/codemode/ai";
const {system, tools} = codemode({
system: "You are a helpful assistant",
tools: {
// tool definitions
},
// ...config
})
const stream = streamText({
model: openai("gpt-5"),
system,
tools,
messages: [
{ role: "user", content: "Write a function that adds two numbers" }
]
})
通过这个更改,你的应用程序现在将开始生成和运行代码,这些代码将调用你定义的工具,包括 MCP 服务器。我们将在很近的未来介绍其他库的变体。阅读文档了解更多细节和示例。
将 MCP 转换为 TypeScript
当你在"代码模式"中连接到 MCP 服务器时,Agents SDK 将获取 MCP 服务器的模式,然后将其转换为 TypeScript API,完整包含基于模式的文档注释。
例如,连接到 gitmcp.io/cloudflare/... 的 MCP 服务器将生成如下的 TypeScript 定义:
typescript
interface FetchAgentsDocumentationInput {
[k: string]: unknown;
}
interface FetchAgentsDocumentationOutput {
[key: string]: any;
}
interface SearchAgentsDocumentationInput {
/**
* The search query to find relevant documentation
*/
query: string;
}
interface SearchAgentsDocumentationOutput {
[key: string]: any;
}
interface SearchAgentsCodeInput {
/**
* The search query to find relevant code files
*/
query: string;
/**
* Page number to retrieve (starting from 1). Each page contains 30
* results.
*/
page?: number;
}
interface SearchAgentsCodeOutput {
[key: string]: any;
}
interface FetchGenericUrlContentInput {
/**
* The URL of the document or page to fetch
*/
url: string;
}
interface FetchGenericUrlContentOutput {
[key: string]: any;
}
declare const codemode: {
/**
* Fetch entire documentation file from GitHub repository:
* cloudflare/agents. Useful for general questions. Always call
* this tool first if asked about cloudflare/agents.
*/
fetch_agents_documentation: (
input: FetchAgentsDocumentationInput
) => Promise<FetchAgentsDocumentationOutput>;
/**
* Semantically search within the fetched documentation from
* GitHub repository: cloudflare/agents. Useful for specific queries.
*/
search_agents_documentation: (
input: SearchAgentsDocumentationInput
) => Promise<SearchAgentsDocumentationOutput>;
/**
* Search for code within the GitHub repository: "cloudflare/agents"
* using the GitHub Search API (exact match). Returns matching files
* for you to query further if relevant.
*/
search_agents_code: (
input: SearchAgentsCodeInput
) => Promise<SearchAgentsCodeOutput>;
/**
* Generic tool to fetch content from any absolute URL, respecting
* robots.txt rules. Use this to retrieve referenced urls (absolute
* urls) that were mentioned in previously fetched documentation.
*/
fetch_generic_url_content: (
input: FetchGenericUrlContentInput
) => Promise<FetchGenericUrlContentOutput>;
};
然后这个 TypeScript 被加载到 agent 的上下文中。目前,整个 API 都被加载,但未来的改进可能允许 agent 更动态地搜索和浏览 API------就像代理编码助手一样。
在沙箱中运行代码
不是向 agent 提供所有连接的 MCP 服务器的所有工具,而是向 agent 提供只有一个工具,它只是执行一些 TypeScript 代码。
然后代码在安全沙箱中执行。沙箱完全与互联网隔离。它与外部世界的唯一访问是通过代表其连接的 MCP 服务器的 TypeScript API。
这些 API 由 RPC 调用支持,该调用回传到 agent 循环。在那里,Agents SDK 将调用分派到适当的 MCP 服务器。
沙箱化代码以显而易见的方式向 agent 返回结果:通过调用 console.log()。当脚本完成时,所有输出日志都会传递回 agent。
动态 Worker 加载:这里没有容器
这种新方法需要访问一个可以运行任意代码的安全沙箱。那么我们在哪里找到这样一个沙箱呢?我们必须运行容器吗?这很昂贵吗?
不。没有容器。我们有更好的东西:isolates。
Cloudflare Workers 平台一直基于 V8 isolates,即由 V8 JavaScript 引擎驱动的隔离 JavaScript 运行时。
Isolates 比容器轻量得多。 一个 isolate 可以在几毫秒内启动,只使用几兆字节的内存。
Isolates 如此之快,以至于我们可以为 agent 运行的每一段代码创建一个新的 isolate。不需要重用它们。不需要预热它们。只需创建它,按需运行代码,然后丢弃它。这一切发生得如此之快,以至于开销可以忽略不计;几乎就像你直接 eval() 代码一样。但是带有安全性。
Worker Loader API
然而,直到现在,Worker 还没有方法直接加载包含任意代码的 isolate。所有 Worker 代码都必须通过 Cloudflare API 上传,然后将其全局部署,以便它可以在任何地方运行。这不是我们想要为 Agents 做的事情!我们希望代码就在 agent 所在的地方运行。
为此,我们向 Workers 平台添加了一个新的 API:Worker Loader API。有了它,你可以按需加载 Worker 代码。它看起来像这样:
javascript
// Gets the Worker with the given ID, creating it if no such Worker exists yet.
let worker = env.LOADER.get(id, async () => {
// If the Worker does not already exist, this callback is invoked to fetch
// its code.
return {
compatibilityDate: "2025-06-01",
// Specify the worker's code (module files).
mainModule: "foo.js",
modules: {
"foo.js":
"export default {\n" +
" fetch(req, env, ctx) { return new Response('Hello'); }\n" +
"}\n",
},
// Specify the dynamic Worker's environment (`env`).
env: {
// It can contain basic serializable data types...
SOME_NUMBER: 123,
// ... and bindings back to the parent worker's exported RPC
// interfaces, using the new `ctx.exports` loopback bindings API.
SOME_RPC_BINDING: ctx.exports.MyBindingImpl({props})
},
// Redirect the Worker's `fetch()` and `connect()` to proxy through
// the parent worker, to monitor or filter all Internet access. You
// can also block Internet access completely by passing `null`.
globalOutbound: ctx.exports.OutboundProxy({props}),
};
});
// Now you can get the Worker's entrypoint and send requests to it.
let defaultEntrypoint = worker.getEntrypoint();
await defaultEntrypoint.fetch("http://example.com");
// You can get non-default entrypoints as well, and specify the
// `ctx.props` value to be delivered to the entrypoint.
let someEntrypoint = worker.getEntrypoint("SomeEntrypointClass", {
props: {someProp: 123}
});
你现在可以在使用 Wrangler 本地运行 workerd 时开始使用这个 API,你可以注册 beta 访问权限在生产环境中使用它。
Workers 是更好的沙箱
Workers 的设计使其在沙箱化方面异常出色,特别是对于这个用例,有几个原因:
更快、更便宜、一次性的沙箱
Workers 平台使用 isolates 而不是容器。 Isolates 轻量得多,启动速度更快。启动一个新的 isolate 只需要几毫秒,而且如此便宜,我们可以为 agent 生成的每一个代码片段创建一个新的 isolate。不需要担心为重用而池化 isolates、预热等问题。
我们尚未最终确定 Worker Loader API 的定价,但由于它基于 isolates,我们将能够以比基于容器的解决方案显著更低的成本提供它。
默认隔离,但通过绑定连接
Workers 更擅长处理隔离。
在代码模式中,我们禁止沙箱化的 worker 与互联网通信。全局 fetch() 和 connect() 函数抛出错误。
但在大多数平台上,这将是一个问题。在大多数平台上,访问私有资源的方式是,你从一般的网络访问开始。然后,使用该网络访问,你向特定服务发送请求,传递某种 API 密钥以授权私有访问。
但 Workers 一直有更好的答案。在 Workers 中,"环境"(env 对象)不仅包含字符串,它包含活动对象,也称为"绑定"。这些对象可以提供对私有资源的直接访问,而不涉及通用网络请求。
在代码模式中,我们给沙箱访问代表其连接的 MCP 服务器的绑定。因此,agent 可以专门访问那些 MCP 服务器,而没有一般的网络访问。
通过绑定限制访问比通过,比如说,网络级过滤或 HTTP 代理要干净得多。过滤对 LLM 和监督者都很困难,因为边界通常不清楚:监督者可能难以准确识别什么是与 API 通信合法需要的流量。同时,LLM 可能难以猜测哪种请求将被阻止。使用绑定方法,它定义明确:绑定提供 JavaScript 接口,该接口被允许使用。这样更好。
没有 API 密钥会泄露
绑定的另一个好处是它们隐藏了 API 密钥。绑定本身提供了已经授权的客户端接口到 MCP 服务器。对其进行的所有调用都首先到达 agent 监督者,监督者持有访问令牌并将其添加到发送到 MCP 的请求中。
这意味着 AI 不可能编写泄露任何密钥的代码,解决了当今 AI 编写代码中常见的网络安全问题。
现在就试试!
注册生产版 beta
Dynamic Worker Loader API 处于封闭 beta 阶段。要在生产环境中使用它,立即注册。
或本地尝试
如果你只是想玩玩,Dynamic Worker Loading 在使用 Wrangler 和 workerd 本地开发时现已完全可用------查看 Dynamic Worker Loading 和 Agents SDK 中的代码模式文档以开始使用。
Cloudflare 的连接云保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序,抵御 DDoS 攻击,阻止黑客,并可以帮助你实现零信任之旅。
从任何设备访问 1.1.1.1 以开始使用我们的免费应用程序,让你的互联网更快更安全。
要了解更多关于我们帮助构建更好互联网的使命的信息,从这里开始。如果你在寻找新的职业方向,查看我们的开放职位。