别再被误导了!MCP无需依赖Function Calling!扒Cline源码给你看!

这位 AI 爱好者你好呀!如果觉得我的文章还不错,欢迎一键三连支持一下!文章会定期更新,同时可以微信搜索【凑个整数1024】第一时间收到文章更新提醒⏰

Function Calling 指的是 LLM 根据用户侧的自然语言输入,自主决定调用哪些工具(tools),并输出格式化的工具调用的能力 ;Model Context Protocol(MCP,模型上下文协议)则是 LLM Agent 应用与外部系统的交互的标准化协议

MCP 只是规范了如何执行 tools,而调用哪些 tools 以及 tools 的入参还是需要 LLM 提供的。因此很多人甚至很多技术文章中都会误认为 MCP 必须依赖于 LLM 的 function calling 的能力。但实际上 MCP 并不关心 LLM 本身的任何能力,只要 LLM 应用通过 MCP client 向 MCP server 传递了符合 MCP tools 调用格式的请求,那么就能正确实现 MCP 的能力。

MCP 虽然不依赖于 LLM 的 function calling 能力,但是 LLM 应用产生 MCP 标准的请求也需要 LLM 产生固定格式的 tools 调用。那么对于没有 function calling 能力的 LLM 该怎么做呢?

Cline

VSCode 的 Cline 插件就是一个符合 MCP client 标准的 LLM 应用,它支持通过 OpenRouter 这样的 LLM 集成接口来选择所使用的 LLM。显然在有着上百款 LLM 的 OpenRouter 中有 function calling 能力的 LLM 只是少数,但是却依然不影响 MCP server 的接入和使用。Cline 采用的办法就是给 LLM 提供强大的系统提示词(System Prompt)

源码浅析

通过翻阅 Cline 的开源源码,能发现在 src/core/prompts/system.ts 中的 SYSTEM_PROMPT 函数提供了近 1000 行的 system prompt。

其中在第 20 行中向 LLM 提示了 tools 调用的格式,是一种规定好的 XML 格式:

plaintext 复制代码
TOOL USE

You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.

# Tool Use Formatting

Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:

<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>

For example:

<read_file>
<path>src/main.js</path>
</read_file>

Always adhere to this format for the tool use to ensure proper parsing and execution.

根据 system prompt 可以看到 Cline 预先定义了一些 tools,比如 execute_commandread_filewrite_to_file 等,以实现 Cline 的基础应用功能(根据源码这几个 tools 的执行是通过 VSCode API 实现的,没有走 MCP):

plaintext 复制代码
# Tools

## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}
Parameters:
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
- requires_approval: (required) A boolean indicating whether this command requires explicit user approval before execution in case the user has auto-approve mode enabled. Set to 'true' for potentially impactful operations like installing/uninstalling packages, deleting/overwriting files, system configuration changes, network operations, or any commands that could have unintended side effects. Set to 'false' for safe operations like reading files/directories, running development servers, building projects, and other non-destructive operations.
Usage:
<execute_command>
<command>Your command here</command>
<requires_approval>true or false</requires_approval>
</execute_command>

## read_file
Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string.
Parameters:
- path: (required) The path of the file to read (relative to the current working directory ${cwd.toPosix()})
Usage:
<read_file>
<path>File path here</path>
</read_file>

## write_to_file
Description: Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to write to (relative to the current working directory ${cwd.toPosix()})
- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified.
Usage:
<write_to_file>
<path>File path here</path>
<content>
Your file content here
</content>
</write_to_file>
...

SYSTEM_PROMPT 函数的入参有一个 mcpHub 用于封装 MCP 的管理,通过 mcpHub.getMode() 方法能够判断是否启用 MCP(用户配置),如果启用则在 system prompt 中加入使用 MCP server 能力的 tools,即 use_mcp_toolaccess_mcp_resource(看起来目前只支持 MCP server 的 tools 和 resources,还不支持 prompts):

javascript 复制代码
const a = mcpHub.getServers().length > 0
		? `${mcpHub
				.getServers()
				.filter((server) => server.status === "connected")
				.map((server) => {
					const tools = server.tools
						?.map((tool) => {
							const schemaStr = tool.inputSchema
								? `    Input Schema:
    ${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
								: ""

							return `- ${tool.name}: ${tool.description}\n${schemaStr}`
						})
						.join("\n\n")

					const templates = server.resourceTemplates
						?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
						.join("\n")

					const resources = server.resources
						?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
						.join("\n")

					const config = JSON.parse(server.config)

					return (
						`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
						(tools ? `\n\n### Available Tools\n${tools}` : "") +
						(templates ? `\n\n### Resource Templates\n${templates}` : "") +
						(resources ? `\n\n### Direct Resources\n${resources}` : "")
					)
				})
				.join("\n\n")}`
		: "(No MCP servers currently connected)"
}`

获取 MCP server 的 tools 和 resources 并产生对应的 system prompt 的代码在源码的 398 行左右:

javascript 复制代码
mcpHub.getServers().length > 0
		? `${mcpHub
				.getServers()
				.filter((server) => server.status === "connected")
				.map((server) => {
					const tools = server.tools
						?.map((tool) => {
							const schemaStr = tool.inputSchema
								? `    Input Schema:
    ${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
								: ""

							return `- ${tool.name}: ${tool.description}\n${schemaStr}`
						})
						.join("\n\n")

					const templates = server.resourceTemplates
						?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
						.join("\n")

					const resources = server.resources
						?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
						.join("\n")

					const config = JSON.parse(server.config)

					return (
						`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
						(tools ? `\n\n### Available Tools\n${tools}` : "") +
						(templates ? `\n\n### Resource Templates\n${templates}` : "") +
						(resources ? `\n\n### Direct Resources\n${resources}` : "")
					)
				})
				.join("\n\n")}`
		: "(No MCP servers currently connected)"
}`

根据这段代码,如果存在 MCP server,则会产生类似这样的 system prompt:

plaintext 复制代码
## example-weather-server (`node /path/to/weather-server/build/index.js`)

### Available Tools
- get_forecast: Get weather forecast for a city
    Input Schema:
    {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "City name"
        },
        "days": {
          "type": "number",
          "description": "Number of days (1-5)",
          "minimum": 1,
          "maximum": 5
        }
      },
      "required": ["city"]
    }

### Resource Templates
- weather://{city}/current (Current weather for a given city): Real-time weather data for a specified city

### Direct Resources
- weather://San Francisco/current (Current weather in San Francisco): Real-time weather data for San Francisco including temperature, conditions, humidity, and wind speed

关于使用 System Prompt 的一些思考

Cline 通过 system prompt 的方式实现了对所有 LLM 的兼容,但这样的做法显然也是存在些问题的。其中 Token 消耗就是各非常大的问题:根据源码可以看到,Cline 的 system prompt 大概占用了 60KB 左右,且会随着 MCP server 的增加而增大,对于一些 token limit 比较低的模型,会占据较大部分的上下文窗口,留给用户提示词的空间就很少了。

Cline 实际可以考虑通过一些手段率先对 LLM 是否具有 function calling 的能力进行一个 "询问",如果支持,则直接利用其 function calling 的能力,而无需提供这么庞大的 system prompt。判断 LLM 是否具有 function calling 能力可以考虑通过对模型名字判断,或与 LLM 协商相应的 API 等。

读者还有可能会关心的一个问题就是:对于具有 function calling 能力的 LLM,如果像 Cline 这样都使用 system prompt 的方式,会不会浪费了 LLM 的能力,今儿达不到理想的效果?关于这个问题其实可以看一下 Berkeley Function-Calling Leaderboard,这个排行榜通过多维度的评价指标对各 LLM 的 function calling 能力做了比较。在该排行中,也加入了使用 system prompt 的方式来使 LLM 间接实现 function calling 能力的比较。意外地发现例如 OpenAI 的 GPT-4o 和 o1 居然使用 system prompt 反而会有更好的表现!甚至 o1 在某些测试维度上,使用 system prompt 要明显更具优势。

但大部分 LLM 还是使用自身 function calling 能力的表现更好,不过也没有太大的差距,因此只要 system prompt 写得好,那么 Cline 目前的做法就是平衡了其 LLM 的通用性与 tool 调用的准确性。

总结

本文旨在说明 MCP 的实现无需依赖于大语言模型的 function calling 能力,二者在概念上并没有任何依赖关系。通过对很流行的 VSCode LLM 插件应用------Cline 的源码浅析,了解到了可以通过使用强大的 system prompt 来让 LLM 具有 tools 调用的能力,无需依赖 LLM 本身 function calling 的能力。

相关推荐
飞火流星020271 小时前
BERT、T5、ViT 和 GPT-3 架构概述及代表性应用
人工智能·gpt-3·bert·t5·vit·人工智能模型架构
程序小K1 小时前
自然语言处理Hugging Face Transformers
人工智能·自然语言处理
恒拓高科WorkPlus1 小时前
BeeWorks:打造安全可控的企业内网即时通讯平台
大数据·人工智能·安全
newxtc2 小时前
【指纹浏览器系列-chromium编译】
人工智能·安全
轻闲一号机2 小时前
【机器学习】机器学习笔记
人工智能·笔记·机器学习
光锥智能2 小时前
傅利叶发布首款开源人形机器人N1:开发者可实现完整复刻
人工智能
恒拓高科WorkPlus2 小时前
一款安全好用的企业即时通讯平台,支持统一门户
大数据·人工智能·安全
天下琴川2 小时前
Dify智能体平台源码二次开发笔记(5) - 多租户的SAAS版实现(2)
人工智能·笔记
qq_365911603 小时前
中英文提示词对AI IDE编程能力影响有多大?
人工智能
jndingxin3 小时前
OpenCV 图形API(31)图像滤波-----3x3 腐蚀操作函数erode3x3()
人工智能·opencv·计算机视觉