MCP的发展背景
MCP的诞生标志着prompt engineering进入了一个新的发展阶段。它通过提供更结构化的上下文信息,显著提升了模型的能力。在设计prompt时,我们的目标是能够将更具体的信息(如本地文件、数据库内容或网络实时数据等)集成进来,使模型能够更好地理解和解决实际问题。
传统方法的局限性
在没有MCP的时代,为了解决复杂问题,我们不得不:
- 手动从数据库中筛选信息
- 使用工具来检索相关信息
- 将筛选的信息逐一添加到prompt中
这种方法在处理简单问题(如需要大模型做归纳总结)时效果不错,但随着问题复杂度增加,变得越来越难以应对。
Function Call的出现
为了克服这些挑战,许多大型语言模型(LLM)平台(如OpenAI和Google)引入了function call功能。这一机制允许模型根据需要调用预定义函数以获取数据或执行特定操作,大大提高了自动化程度。
Function Call的局限性
然而,function call也存在明显的局限性:
- 对平台的高度依赖
- 不同LLM平台间API实现的差异
- 开发者在切换模型时必须重写代码,增加开发成本
- 安全性和交互性等方面的挑战
MCP的设计理念
实际上,数据和工具一直都在那里,关键在于如何更智能、更统一地将它们与模型连接起来。Anthropic基于这样的需求设计了MCP,作为AI模型的"万能适配器",让LLM能够轻松访问数据或调用工具。
MCP的优势
模型如何智能选择Agent/工具
MCP的核心是让我们能方便地调用多个工具,那么LLM(模型)是在什么时候确定使用哪些工具的呢?
工具选择流程
Anthropic为我们提供了详细的解释,当用户提出一个问题时:
- 客户端(Claude Desktop/Cursor)将问题发送给LLM
- LLM分析可用的工具,并决定使用哪一个(或多个)
- 客户端通过MCP Server执行所选的工具
- 工具的执行结果被送回给LLM
- LLM结合执行结果,归纳总结后生成自然语言展示给用户
模型如何确定使用哪些工具?
通过分析MCP官方提供的client example代码,可以看出模型是依靠prompt来识别当前可用的工具。具体做法是:
- 将各个工具的使用描述以文本形式传递给模型
- 使模型能够了解有哪些工具可供选择
- 基于实时情况做出最佳选择
关键代码分析
async def start(self):
# 初始化所有的 mcp server
for server in self.servers:
await server.initialize()
# 获取所有的 tools 命名为 all_tools
all_tools = []
for server in self.servers:
tools = await server.list_tools()
all_tools.extend(tools)
# 将所有的 tools 的功能描述格式化成字符串供 LLM 使用
tools_description = "\\n".join(
[tool.format_for_llm() for tool in all_tools]
)
# 询问 LLM(Claude) 应该使用哪些工具
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:
# 假设这里已经处理了用户消息输入
messages.append({"role": "user", "content": user_input})
# 将 system_message 和用户消息输入一起发送给 LLM
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
# 把工具的名字/工具的用途(description)和工具所需要的参数(args_desc)转化为文本
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)} """
工具信息来源
工具的描述和input_schema是从哪里来的?通过分析MCP的Python SDK源代码:
大部分情况下,当使用装饰器@mcp.tool()
来装饰函数时,对应的name和description等直接源自用户定义函数的函数名以及函数的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__ # 获取函数名
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
func_doc = description or fn.__doc__ or "" # 获取函数docstring
is_async = inspect.iscoroutinefunction(fn)
总结:模型是通过prompt engineering,即提供所有工具的结构化描述和few-shot的example来确定该使用哪些工具。另一方面,Anthropic肯定对Claude做了专门的训练,毕竟是自家协议,Claude更能理解工具的prompt以及输出结构化的tool call json代码。
工具执行与结果反馈机制
工具的执行相对简单和直接。承接上一步,我们把system prompt(指令与工具调用描述)和用户消息一起发送给模型,然后接收模型的回复。
执行流程
当模型分析用户请求后,它会决定是否需要调用工具:
- 无需工具时:模型直接生成自然语言回复
- 需要工具时:模型输出结构化JSON格式的工具调用请求
如果回复中包含结构化JSON格式的工具调用请求,则客户端会根据这个json代码执行对应的工具。
代码实现
async def start(self):
# 上面已经介绍过了,模型如何选择工具
while True:
# 假设这里已经处理了用户消息输入
messages.append({"role": "user", "content": user_input})
# 获取 LLM 的输出
llm_response = self.llm_client.get_response(messages)
# 处理 LLM 的输出(如果有 tool call 则执行对应的工具)
result = await self.process_llm_response(llm_response)
# 如果 result 与 llm_response 不同,说明执行了 tool call(有额外信息了)
# 则将 tool call 的结果重新发送给 LLM 进行处理
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}
)
# 否则代表没有执行 tool call,则直接将 LLM 的输出返回给用户
else:
messages.append({"role": "assistant", "content": llm_response})
错误处理
如果tool call的json代码存在问题或者模型产生了幻觉,系统会skip掉无效的调用请求。
结论与实践建议
根据上述原理分析,可以看出工具文档至关重要。模型依赖于工具描述文本来理解和选择适用的工具,这意味着精心编写的工具名称、文档字符串(docstring)以及参数说明显得尤为重要。
鉴于MCP的选择机制基于prompt实现,理论上任何模型只要能够提供相应的工具描述就能与MCP兼容使用。