5小时前  AI大模型 |   抢沙发  2 
文章评分 0 次,平均分 0.0

原来我们都用错了MCP

如今,大多数代理通过直接向大型语言模型(LLM)展示“工具”来使用多模态预训练(MCP)。

我们尝试了一种不同的方法:将MCP工具转换为TypeScript API,然后让大型语言模型(LLM)编写调用该API的代码。

结果令人震惊:

1. 我们发现,当工具以TypeScript API的形式呈现时,代理能够处理更多且更复杂的工具,而直接呈现则无法达到这一效果。这或许是因为大型语言模型(LLM)的训练集中包含了大量的真实世界TypeScript示例,但只有一小部分是人为设计的工具调用示例。

2. 当代理需要将多个调用串联起来时,这种方法就真正显现出了其优势。采用传统方法时,每个工具调用的输出都必须输入到大型语言模型(LLM)的神经网络中,然后才能被复制到下一个调用的输入中,这既浪费时间、精力,又浪费代币。当LLM能够编写代码时,它就可以跳过所有这些步骤,而只需读取它需要的最终结果。

简而言之,与直接调用MCP相比,大型语言模型(LLMs)更擅长编写代码来调用MCP。

什么是MCP?

对于那些不熟悉的人:模型上下文协议是一种标准协议,旨在让AI代理能够访问外部工具,以便它们可以直接执行工作,而不仅仅是与你聊天。

从另一个角度来看,MCP是一种统一的方法,用于:

  • 提供一个API来做某事,
  • 以及让大型语言模型(LLM)理解它所需的文档,
  • 授权处理在带外进行。

2025年,MCP(多智能体协作协议)因其突然大幅扩展了人工智能代理的能力而引起了轰动。

MCP服务器所暴露的“API”被表示为一组“工具”。每个工具本质上都是一个远程过程调用(RPC)函数——它通过某些参数被调用并返回一个响应。大多数现代大型语言模型(LLM)都具有使用“工具”(有时称为“函数调用”)的能力,这意味着它们经过训练,在想要调用工具时能够以特定格式输出文本。调用大型语言模型的程序会识别这种格式并按指定方式调用工具,然后将结果作为输入反馈给大型语言模型。

工具剖析

所谓“底层”,大型语言模型(LLM)会生成一串代表其输出的“标记”。一个标记可能代表一个单词、一个音节、某种标点符号或文本的其他组成部分。

不过,工具调用涉及一个没有任何文本等效项的标记。大型语言模型(LLM)经过训练(或更常见的是经过微调)来理解一个特殊标记,该标记可以输出表示“以下内容应被解释为工具调用”的意思,以及另一个表示“这是工具调用的结束”的特殊标记。在这两个标记之间,大型语言模型通常会写出与描述调用的某种JSON消息相对应的标记。

例如,想象一下,你已经将一个代理连接到提供天气信息的MCP服务器,然后你问这个代理德克萨斯州奥斯汀的天气如何。在幕后,大型语言模型(LLM)可能会生成如下输出。请注意,这里我们用<|和|>中的单词来表示我们的特殊标记,但实际上,这些标记并不代表任何文本;这只是为了说明。

我将使用Weather MCP服务器来查询德克萨斯州奥斯汀的天气情况。

I will use the Weather MCP server to find out the weather in Austin, TX.

<|tool_call|>
{
  "name": "get_current_weather",
  "arguments": {
    "location": "Austin, TX, USA"
  }
}
<|end_tool_call|>

在输出中看到这些特殊标记后,大型语言模型(LLM)的 harness 会将该序列解释为工具调用。在看到结束标记后,harness 会暂停大型语言模型的执行。它会解析 JSON 消息,并将其作为结构化 API 结果的一个单独组件返回。调用大型语言模型 API 的代理会看到工具调用,调用相关的 MCP 服务器,然后将结果发送回大型语言模型 API。然后,大型语言模型的 harness 会使用另一组特殊标记将结果反馈给大型语言模型:

<|tool_result|>
{
  "location": "Austin, TX, USA",
  "temperature": 93,
  "unit": "fahrenheit",
  "conditions": "sunny"
}
<|end_tool_result|>

大型语言模型(LLM)读取这些标记的方式与读取用户输入的方式完全相同——只是用户无法生成这些特殊标记,因此大型语言模型知道这是工具调用的结果。然后,大型语言模型会像往常一样继续生成输出。

不同的LLM(大型语言模型)在调用工具时可能会使用不同的格式,但基本思路都是如此。

这有什么不对吗?

在工具调用中使用的特殊标记是大型语言模型(LLM)在真实环境中从未见过的。它们必须基于合成训练数据经过特殊训练才能使用工具。但它们并不总是那么擅长。如果向大型语言模型展示太多工具或过于复杂的工具,它可能很难选择正确的工具或正确使用。因此,与可能向开发人员展示的更传统的API相比,多模态处理器(MCP)服务器设计者被鼓励提供大大简化的API。

与此同时,大型语言模型(LLMs)在编写代码方面表现得越来越出色。事实上,当要求大型语言模型根据通常向开发者开放的完整、复杂的API编写代码时,它们似乎并没有遇到太多困难。那么,为什么机器学习平台(MCP)的界面必须“简化”呢?编写代码和调用工具几乎是一回事,但大型语言模型在执行其中一项任务时似乎比另一项更擅长?

答案很简单:大型语言模型(LLM)已经看过大量的代码。但它们并没有见过大量的“工具调用”。事实上,它们所见的工具调用可能仅限于由LLM自身开发者构建的、人为设计的训练集,以便对其进行训练。而它们已经从数以百万计的开源项目中看到了真实世界的代码。

让一个大型语言模型(LLM)通过调用工具来执行任务,就像让莎士比亚上一个月的普通话课,然后让他用普通话写剧本一样。这肯定不会是他最好的作品。

但MCP仍然有用,因为它是统一的。

MCP是为工具调用设计的,但实际上并不一定非得这样使用。

MCP服务器所展现的“工具”实际上只是一个附带文档的RPC接口。我们其实不必将其呈现为工具。我们可以将这些工具提取出来,并将其转化为一种编程语言API。

但是,既然编程语言API已经独立存在,我们为什么要这样做呢?几乎每个MCP服务器都只是对现有传统API的封装——为什么不直接公开这些API呢?

事实证明,MCP还有一项非常实用的功能:它提供了一种统一的方式来连接并了解API。

即使AI代理的开发者从未听说过特定的MCP服务器,且MCP服务器的开发者也从未听说过特定的代理,该代理仍可使用MCP服务器。这在过去的传统API中很少见。通常,客户端开发者总是确切地知道他们正在为哪个API编码。因此,每个API在执行基本连接、授权和文档记录等功能时,都会略有不同。

这种一致性在AI代理编写代码时也很有用。我们希望AI代理能在沙箱中运行,这样它只能访问我们提供的工具。MCP通过以标准方式处理连接和授权,使代理框架能够实现这一点,而与AI代码无关。我们也不希望AI必须在网上搜索文档;MCP直接在协议中提供了这些文档。

好的,它是如何工作的?

我们已经扩展了Cloudflare Agents SDK以支持这种新模式!

例如,假设你有一个使用ai-sdk构建的应用程序,其外观如下:

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助手函数来包装这些工具和提示,并在你的应用中使用它们:

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,并根据该模式添加文档注释。

例如,连接到位于https://gitmcp.io/cloudflare/agents的MCP服务器,将生成如下所示的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代码会被加载到代理的上下文中。目前,整个API都会被加载,但未来的改进可能会让代理能够更动态地搜索和浏览API,就像代理式编码助手那样。

在沙箱中运行代码

我们的代理程序并未被赋予所有已连接的MCP服务器的所有工具,而是仅被赋予了一个工具,该工具仅能执行一些TypeScript代码。

然后,代码会在一个安全的沙箱中执行。该沙箱与互联网完全隔离。它对外部世界的唯一访问是通过代表其连接的MCP服务器的TypeScript API。

这些API由RPC调用支持,这些调用会回调到代理循环。在代理循环中,Agents SDK会将调用分派到相应的MCP服务器。

沙盒代码以显而易见的方式向代理返回结果:通过调用console.log()。脚本执行完毕后,所有输出日志都会被传回给代理。

Code Mode:使用MCP的更好方式

动态工作负载:这里没有容器

这种新方法需要访问一个安全的沙箱,以便在其中运行任意代码。那么,我们该去哪里找到这样的沙箱呢?我们必须运行容器吗?这样做成本高吗?

不,这里没有容器。我们拥有更好的东西:隔离物。

Cloudflare Workers平台一直基于V8 isolates,即由V8 JavaScript引擎驱动的独立JavaScript运行时环境。

隔离体远比容器轻量级。一个隔离体可以在几毫秒内启动,且仅占用几兆字节的内存。

隔离环境非常快,我们可以为代理运行的每段代码创建一个新的隔离环境。没有必要重复使用它们,也没有必要预热它们。只需按需创建,运行代码,然后丢弃即可。这一切发生得如此之快,开销几乎可以忽略不计;就好像你直接在eval()代码一样。但这样做是有安全性的。

工作线程加载器API

然而,到目前为止,工作线程还无法直接加载包含任意代码的隔离体。所有工作线程代码都必须通过Cloudflare API上传,然后由该API在全球范围内部署,以便其可以在任何地方运行。这并不是我们为代理所希望的!我们希望代码能在代理所在的位置直接运行。

为此,我们在Workers平台上新增了一个API:Worker Loader API。借助它,您可以按需加载Worker代码。其界面如下:

// 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,并且你可以注册以获取测试版访问权限,以便在生产环境中使用它。

Workers是更好的沙箱环境

Workers的设计使其在沙箱环境方面表现出色,尤其适用于此用例,原因有以下几点:

更快、更廉价、可丢弃的沙箱 Workers平台使用隔离体而非容器。

隔离体体积更小,启动速度更快。启动一个全新的隔离体只需几毫秒,而且成本极低,我们甚至可以为代理生成的每个代码片段都创建一个新的隔离体。无需担心隔离体的池化以供重用、预热等问题。

我们尚未最终确定Worker Loader API的定价,但由于它是基于隔离的,因此我们将能够以远低于基于容器的解决方案的成本来提供它。

默认情况下是隔离的,但通过绑定实现连接。

工作进程在处理隔离方面更为擅长。

在代码模式下,我们禁止沙盒中的工作进程与互联网通信。全局的fetch()和connect()函数会抛出错误。

但在大多数平台上,这都会是个问题。在大多数平台上,获取私有资源的方式是,首先获得一般网络访问权限。然后,利用该网络访问权限,向特定服务发送请求,并向它们传递某种API密钥以授权私有访问。

但Workers总是有更好的解决方案。在Workers中,“环境”(env对象)不仅包含字符串,还包含活动对象,也称为“绑定”。这些对象可以直接访问私有资源,而无需进行通用的网络请求。

在代码模式下,我们为代理提供了对其所连接的MCP服务器的绑定访问权限。因此,代理可以专门访问这些MCP服务器,而无需具备一般的网络访问权限。

通过绑定来限制访问比通过(比如)网络级过滤或HTTP代理要清晰得多。过滤对大型语言模型(LLM)和监管者来说都很困难,因为边界往往不明确:监管者可能很难准确识别与API通信所需的合法流量。同时,大型语言模型可能难以猜测哪些类型的请求会被阻止。而使用绑定方法则定义明确:绑定提供了一个JavaScript接口,并且该接口被允许使用。这样更好。

无API密钥泄露

绑定的另一个好处是它们隐藏了API密钥。绑定本身为MCP服务器提供了一个已授权的客户端接口。所有在其上进行的调用都会首先发送到代理监管器,代理监管器保存访问令牌并将其添加到发送给MCP的请求中。

这意味着人工智能不可能编写出泄露任何密钥的代码,从而解决了当今人工智能编写的代码中常见的一个安全问题。

原文链接: https://blog.cloudflare.com/code-mode

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/3069.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册