编者按:一篇将 MCP 与其出现之前的函数调用(function calling)机制进行对照的对比分析。文章还原了整条问题链:工程师曾经需要手动筛选信息填入提示词;随后各平台专属的函数调用实现了自动调用,却把开发者锁定在每家 LLM 厂商各自的 API 上,底层模型一换就不得不重写代码。MCP 则被定位为一个通用适配层,任何兼容的模型都能通过同一批工具服务器使用它,从而降低切换成本。文章还分析了模型如何决定调用哪个工具或智能体,并指出 MCP 正推动提示词工程走向由本地文件、数据库和实时网络数据构成的更丰富的结构化上下文。适合在原生函数调用与基于协议的集成之间做决策的工程师阅读。核心要点:把工具一次性写成一个 MCP 服务器,它就能在各种客户端和模型厂商之间保持可用,而不必针对每个平台重写。阅读时值得放在手边的官方页面:MCP 规范、MCP 参考服务器。
MCP 的发展背景
MCP 的诞生标志着提示词工程进入了新的发展阶段。它通过提供更结构化的上下文信息,显著增强了模型的能力。在设计提示词时,我们的目标是整合更具体的信息(如本地文件、数据库内容或实时网络数据),使模型能够更好地理解和解决实际问题。

传统方式的局限
在 MCP 出现之前,要解决复杂问题,我们不得不:
- 手动从数据库中筛选信息
- 借助工具检索相关信息
- 把筛选出的信息逐条添加到提示词中
这种方式处理简单问题(比如让大模型做摘要)效果尚可,但随着问题复杂度的增加,处理起来会越来越吃力。
函数调用的出现
为了克服这些挑战,许多大语言模型(LLM)平台(如 OpenAI 和 Google)引入了函数调用(function call)功能。这种机制允许模型按需调用预定义的函数来获取数据或执行特定操作,大幅提升了自动化程度。
函数调用的局限
不过,函数调用也存在明显的局限:
- 对平台依赖程度高
- 不同 LLM 平台的 API 实现存在差异
- 开发者更换模型时必须重写代码,推高了开发成本
- 在安全性、交互性等方面存在挑战
MCP 的设计理念
事实上,数据和工具一直都在,关键在于如何更智能、更统一地把它们接入模型。Anthropic 正是基于这样的需求设计了 MCP。作为 AI 模型的“通用适配器”,它让 LLM 能够轻松访问数据或调用工具。
MCP 的优势
模型如何智能选择智能体/工具
MCP 的核心在于让我们能够方便地调用多个工具。那么,LLM(模型)是在什么时候决定该使用哪些工具的?
工具选择流程
Anthropic 给出了详细的说明。当用户提出一个问题时:
- 客户端(Claude Desktop/Cursor)把问题发送给 LLM
- LLM 分析可用的工具,并决定使用哪一个(或哪些)
- 客户端通过 MCP 服务器执行选定的工具
- 工具的执行结果被送回 LLM
- LLM 总结执行结果并生成自然语言呈现给用户
模型如何确定该使用哪些工具?
通过分析 MCP 官方提供的客户端示例代码可以看出,模型依靠提示词来识别当前可用的工具。具体做法如下:
- 以文本形式把每个工具的用法说明传给模型
- 让模型知道有哪些工具可供选择
- 根据实时情况做出最优选择
关键代码分析
async def start(self):
# Initialize all mcp servers
for server in self.servers:
await server.initialize()
# Get all tools named all_tools
all_tools = []
for server in self.servers:
tools = await server.list_tools()
all_tools.extend(tools)
# Format the functional descriptions of all tools into a string for the LLM to use
tools_description = "\\n".join(
[tool.format_for_llm() for tool in all_tools]
)
# Ask the LLM (Claude) which tools should be used
system_message = (
"You are a helpful assistant with access to these tools:\\n\\n"
f"{tools_description}\\n"
"Choose the appropriate tool based on the user's question. "
"If no tool is needed, reply directly.\\n\\n"
"IMPORTANT: When you need to use a tool, you must ONLY respond with "
"the exact JSON object format below, nothing else:\\n"
"{\\n"
' "tool": "tool-name",\\n'
' "arguments": {\\n'
' "argument-name": "value"\\n'
" }\\n"
"}\\n\\n"
"After receiving a tool's response:\\n"
"1. Transform the raw data into a natural, conversational response\\n"
"2. Keep responses concise but informative\\n"
"3. Focus on the most relevant information\\n"
"4. Use appropriate context from the user's question\\n"
"5. Avoid simply repeating the raw data\\n\\n"
"Please use only the tools that are explicitly defined above."
)
messages = [{"role": "system", "content": system_message}]
while True:
# Assume that the user message input has been processed here
messages.append({"role": "user", "content": user_input})
# Send the system_message and the user message input to the LLM together
llm_response = self.llm_client.get_response(messages)
工具的格式化
工具的描述信息是如何被格式化的?
class Tool:
"""Represents a tool with its properties and formatting."""
def __init__(
self, name: str, description: str, input_schema: dict[str, Any]
) -> None:
self.name: str = name
self.description: str = description
self.input_schema: dict[str, Any] = input_schema
# Convert the tool's name/tool's purpose (description) and the tool's required parameters (args_desc) into text
def format_for_llm(self) -> str:
"""Format tool information for LLM.
Returns:
A formatted string describing the tool.
"""
args_desc = []
if "properties" in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():
arg_desc = (
f"- {param_name}: {param_info.get('description', 'No description')}"
)
if param_name in self.input_schema.get("required", []):
arg_desc += " (required)"
args_desc.append(arg_desc)
return f""" Tool: {self.name} Description: {self.description} Arguments: {chr(10).join(args_desc)} """
工具信息的来源
工具的 description 和 input_schema 是从哪里来的?通过分析 MCP 的 Python SDK 源码可知:
大多数情况下,在使用装饰器 @mcp.tool() 装饰函数时,对应的名称和描述直接来自用户自定义函数的函数名、函数的 docstring 等。
@classmethod
def from_function(
cls,
fn: Callable,
name: str | None = None,
description: str | None = None,
context_kwarg: str | None = None,
) -> "Tool":
"""Create a Tool from a function."""
func_name = name or fn.__name__ # Get the function name
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
func_doc = description or fn.__doc__ or "" # Get the function docstring
is_async = inspect.iscoroutinefunction(fn)
小结:模型通过提示词工程来决定使用哪些工具,也就是提供所有工具的结构化描述和少样本示例。另一方面,Anthropic 想必对 Claude 做过专门训练——毕竟这是它自家的协议,Claude 能更好地理解工具提示词,并输出结构化的工具调用 JSON 代码。
工具执行与结果反馈机制
工具的执行相对简单直接。承接上一步,我们把系统提示词(指令和工具调用说明)连同用户消息一起发给模型,然后接收模型的回复。
执行流程
模型分析用户的请求后,决定是否调用工具:
- 不需要工具时:模型直接生成自然语言回复
- 需要工具时:模型输出结构化的 JSON 格式工具调用请求
如果回复中包含结构化的 JSON 格式工具调用请求,客户端就会基于这段 JSON 代码执行相应的工具。
代码实现
async def start(self):
# As introduced above, how the model selects tools
while True:
# Assume that the user message input has been processed here
messages.append({"role": "user", "content": user_input})
# Get the output of the LLM
llm_response = self.llm_client.get_response(messages)
# Process the output of the LLM (if there is a tool call, execute the corresponding tool)
result = await self.process_llm_response(llm_response)
# If the result is different from the llm_response, it means a tool call has been executed (there is additional information)
# Then resend the result of the tool call to the LLM for processing
if result != llm_response:
messages.append({"role": "assistant", "content": llm_response})
messages.append({"role": "system", "content": result})
final_response = self.llm_client.get_response(messages)
logging.info("\\nFinal response: %s", final_response)
messages.append(
{"role": "assistant", "content": final_response}
)
# Otherwise, it means no tool call has been executed, so directly return the output of the LLM to the user
else:
messages.append({"role": "assistant", "content": llm_response})
错误处理
如果工具调用的 JSON 代码存在问题,或者模型出现了幻觉,系统会跳过无效的调用请求。
结论与实用建议
基于上述原理分析可以看出,工具文档至关重要。模型依靠工具描述文本来理解和选择适用的工具,这意味着精心编写的工具名称、docstring 和参数描述尤为重要。
鉴于 MCP 的选择机制基于提示词,从理论上讲,任何能够提供相应工具描述的模型都可以兼容地使用 MCP。