本文基于一次技术对话整理,深入探讨 MCP(Model Context Protocol)的底层原理,重点回答:小模型不支持 Function Calling 时 MCP 如何处理、模型输出格式错误时如何容错、重试机制在何处发生,以及 Trae、大模型、工具、MCP 客户端与服务端之间的调用关系。全文保留所有技术细节、源码示例与表格,适合想深入理解 MCP 的开发者阅读。
一、MCP 如何兼容不支持 Function Calling 的小模型?
核心思路是协议分层与客户端适配 。MCP 将"模型决策"与"工具执行"彻底解耦,模型只负责生成文本指令,至于这个指令是原生 Function Calling 输出的 JSON,还是由提示词引导生成的 XML,MCP 客户端都能接管。
具体来说,MCP 客户端会根据模型能力动态调整:
-
对于原生支持 Function Calling 的模型:客户端直接传递工具的标准 JSON Schema,利用模型的"原生能力"生成结构化的调用指令。
-
对于不支持 Function Calling 的小模型 :客户端会将工具信息转换为自然语言描述 ,通过精心设计的 System Prompt 强制引导模型输出预定义格式的指令(如 XML 或 JSON)。例如,一些 MCP 客户端会使用近千行的系统提示词,明确要求模型以
<read_file path="..." />这样的 XML 格式输出,然后通过正则表达式来解析。
这样,执行层被统一接管 了。无论模型输出的是什么格式,MCP 客户端都负责解析、校验参数,然后通过 MCP 协议将标准的请求发送给工具服务器执行,整个过程与底层模型无关。这使得小模型只需生成符合格式的指令文本,就能无缝衔接工具调用。
二、MCP 如何检测模型是否支持 Function Calling?
MCP 协议本身不负责 检测模型能力。这个检测工作由其客户端(Client)或更上层的应用框架(如 LiteLLM) 完成。检测逻辑通常遵循一个分级判断的策略,源码实现大致如下:
1. 模型级覆盖(Model-level Override)
首先,会检查是否对该模型进行了手动配置。这通常通过一个模型能力数据库或配置文件来实现。
python
# 示例逻辑 (来自 galactic-ai/gateway_v3.py)
# 1. 检查模型级别的覆盖配置
override = self._get_model_override('supports_tools')
if override is not None:
return bool(override)
这段代码来自一个实际的 MCP 网关项目,它会优先检查用户或系统是否为特定模型(如 gpt-4o)手动设置了 supports_tools 标志。
2. 提供商级默认值(Provider-based Defaults)
如果没有模型级覆盖,则回退到基于提供商(Provider)的默认假设。
python
# 2. 基于提供商的默认值
provider = self.llm.provider.lower()
if provider in ("openai", "anthropic", "google", "xai", "groq", ...):
return True
return False
源码中会维护一个白名单,列出已知支持原生工具调用的提供商。如果提供商不在列表中,则默认认为不支持。
3. 本地模型的特殊处理
对于 Ollama、LM Studio 这类本地模型,检测逻辑会更保守。因为本地模型在工具数量增多时,性能会急剧下降。因此,即使检测到支持,也会限制可用工具的数量。
python
# 针对 Ollama 等本地后端的特殊处理
_OLLAMA_MAX_TOOLS = 28
_CLOUD_MAX_TOOLS = 40
源码会为本地模型和云端模型设置不同的工具数量上限,例如本地模型最多 28 个,云端模型最多 40 个。
4. 最终的能力封装
LiteLLM 等框架提供了更简洁的封装 API,供上层调用:
python
# LiteLLM 提供的便捷函数
litellm.supports_function_calling(model="gpt-3.5-turbo") # 返回 True 或 False
这个函数内部封装了上述的检测逻辑,方便开发者直接判断某个模型是否支持函数调用。
三、MCP 如何处理模型输出格式错误?(源码级解析)
当模型输出的格式不符合预期时,MCP 生态中有一套**"解析-校验-反馈并重试"** 的容错机制。以下是核心源码的实现细节。
1. 解析与提取:从混乱文本中"抢救"JSON
模型输出经常夹杂着解释性文字和 Markdown 代码块。专门的 MCP 服务器(如 agentcast-mcp)会实现一个多策略的解析器。
javascript
// 来自 @mukundakatta/agentcast-mcp 的 extract_json 工具
// 尝试策略:整个文本 -> ```json 代码块 -> ``` 代码块 -> 最大的平衡 {...} 子串
其 extract_json 工具会按优先级尝试多种策略,从最直接的整个文本解析,到提取 Markdown 代码块,再到使用括号平衡算法提取最外层的 JSON 对象或数组。
2. 严格校验:基于 JSON Schema 的类型检查
提取出 JSON 后,会立即根据工具定义中声明的 outputSchema 进行校验。IBM 的 mcp-context-forge 项目在修复一个 bug 时,将原本宽松的校验改为了严格模式。
python
# 修复前的宽松逻辑(已废弃)
if structured is None:
return True # 没有结构化数据也视为通过,这可能导致问题
# 修复后的严格逻辑
if is_error:
# 错误响应豁免校验
return True
if structured is None:
# 成功响应但无结构化数据,直接判为失败
return False, {
"code": "missing_structured_output",
"message": "outputSchema declared but no structuredContent was returned"
}
这段源码来自 ToolService._extract_and_validate_structured_content 方法。修复后,如果工具声明了 output_schema 但模型没返回结构化数据,网关会明确返回错误,而不是静默通过。
3. 反馈并重试:将错误转化为学习信号
校验失败后,系统不会直接崩溃,而是会生成一条包含具体错误信息的反馈消息 ,追加到对话历史中,再次请求模型修正。agentcast-mcp 的 build_retry_prompt 工具专门负责生成这种提示词。
python
// build_retry_prompt 生成的反馈示例
{
"feedback": "Your previous response did not match the required shape.
Error: missing required field 'age'.
Try again. Respond with ONLY valid JSON that fixes the error above.
Expected shape: {\"name\":\"string\",\"age\":\"number\"}"
}
这个反馈消息会明确指出缺失了哪个字段 ,并附上期望的完整结构,引导模型在下一轮生成中修复问题。
4. 协议层错误:JSON-RPC 标准错误码
在更底层的 MCP 协议通信中,还定义了一套标准的 JSON-RPC 错误码,用于处理更基础的通信和协议层错误。
| 错误码 | 常量名 | 含义 |
|---|---|---|
| -32700 | PARSE_ERROR |
服务器收到无效的 JSON |
| -32600 | INVALID_REQUEST |
发送的 JSON 不是有效的请求对象 |
| -32601 | METHOD_NOT_FOUND |
请求的方法不存在 |
| -32000 | INITIALIZATION_FAILED |
MCP 特有的初始化失败 |
| -32001 | CAPABILITY_NOT_SUPPORTED |
MCP 特有的能力不支持 |
这些错误码确保了客户端和服务器之间能进行标准化的错误沟通。
四、重试机制:自动闭环与反馈位置
1. 重试是自动的吗?在返回客户端前发生吗?
是的,这个重试循环是自动的,并且在将最终结果返回给用户或上层应用之前完成。
但需要澄清一个关键点:执行重试的主体是 MCP 客户端(Client)或更上层的应用框架,而不是 MCP 服务器(Server)。MCP 服务器只负责工具的实际执行,而"解析-校验-反馈并重试"这套容错逻辑,是在客户端侧闭环完成的。
当模型输出格式错误时,客户端不会直接把错误抛给用户,而是会自动执行以下操作:
-
生成反馈消息:客户端根据校验错误,生成一条结构化的反馈提示。
-
自动重试 :将这条反馈消息作为新的输入,自动再次调用大模型,请求它修正错误。
-
循环直到成功:这个循环会持续进行,直到模型输出正确的格式,或者达到预设的最大重试次数。
这个过程对用户是完全透明的,你最终看到的会是重试成功后的结果。
2. "反馈"具体在哪个环节生成?
反馈的生成和注入,发生在MCP 客户端的应用层,具体是在模型返回结果之后、工具执行之前。
我们可以结合一个具体的 MCP 服务器实现 @mukundakatta/agentcast-mcp 来理解。这个服务器的核心工具之一就是 build_retry_prompt,它的职责非常明确:"当模型返回了错误的形状(wrong shape)时,生成验证错误反馈消息,并将其追加到对话中"。
它的工作流程如下:
-
提取 :客户端首先使用
extract_json工具,尝试从模型混乱的输出中提取出 JSON。它会按优先级尝试多种策略,比如直接解析整个文本、提取 Markdown 代码块,或者使用括号平衡算法找到最大的{...}子串。 -
校验 :提取出 JSON 后,使用
validate_response工具,根据预定义的 Shape Spec (形状规范)进行严格校验。例如,如果规范要求age字段是数字类型,而模型返回了字符串,校验就会失败,并生成具体的错误信息,如"missing required field 'age'"。 -
生成反馈 :一旦校验失败,
build_retry_prompt工具就会被调用。它会根据尝试历史和期望的格式,生成一条对模型友好的反馈消息 。这条消息会被追加到对话历史中,然后客户端再次请求模型。
生成的反馈消息结构非常清晰,例如:
python
{
"feedback": "Your previous response did not match the required shape.
Error: missing required field 'age'.
Try again. Respond with ONLY valid JSON that fixes the error above.
Expected shape: {\"name\":\"string\",\"age\":\"number\"}"
}
这条消息会明确告诉模型:哪里错了(Error)、应该怎么做(Try again with ONLY valid JSON)、期望的完整结构是什么(Expected shape)。
3. 修正表述:解析 → 校验 → 反馈并重试
更准确的表述是:解析 → 校验 →(失败则)反馈并重试 ,而不是"解析-校验-反馈-重试"四个独立阶段。之前那样写,容易让人误以为"反馈"和"重试"是两个分开的步骤,实际上它们是同一个闭环动作:
-
反馈是内容:告诉模型哪里错了、期望的格式是什么。
-
重试是动作:把这条反馈追加到对话历史,再次调用模型。
在代码里,它通常就是一个 while 循环:
python
while retries < max_retries:
response = model.generate(messages)
extracted = extract_json(response)
valid, error = validate(extracted, expected_shape)
if valid:
return extracted # 成功,直接返回
feedback = build_retry_prompt(error, expected_shape)
messages.append(feedback) # 反馈注入
retries += 1 # 下一轮就是重试
所以完整流程是:
解析 → 校验 → 成功则返回;失败则"反馈并重试" → 再解析 → 再校验......
反馈和重试是绑在一起的:反馈是重试的输入,重试是反馈的目的。说成"解析-校验-反馈并重试"更贴切。
另外再澄清一下主体:执行这个循环的是 MCP 客户端 / 宿主应用,不是 MCP 服务器。MCP 服务器只负责执行工具,不负责调用大模型。所以不是"MCP 自动再调用大模型",而是"客户端侧的容错逻辑自动再调用大模型"。
五、Trae、大模型、工具、MCP 客户端与服务端的调用关系
在 Trae 的体系里,这几个角色的职责非常清晰:
| 组件 | 角色 | 职责 |
|---|---|---|
| Trae IDE | Host(宿主) | 用户交互界面,承载整个 AI 编程环境,启动并管理 Agent |
| TraeAgent | Agent(智能体) | 核心执行引擎,负责组合 LLM 和工具,执行"思考-调用工具-观察结果"的循环 |
| LLM Client | 大模型通信层 | 与底层大模型(如 Claude、GPT-4o)通信,组装工具 schema,发送消息 |
| MCPClient | MCP 客户端 | 发现外部 MCP 服务,将工具注入 Agent,管理与 MCP Server 的连接 |
| MCP Server | MCP 服务端 | 独立进程,通过 MCP 协议向客户端暴露工具(Tools)、资源(Resources)和提示(Prompts) |
| Tools | 工具 | 可执行的具体操作,如读写文件、调用 API、执行 shell 命令等 |
Trae 中的智能体(TraeAgent)作为 MCP 客户端,可以选择向 MCP Server 发起请求,以使用它们提供的工具。
完整调用关系(时序)
整个调用链路遵循一个清晰的循环,可以概括为 "Agent 驱动,Client 转发,Server 执行":
第一步:工具发现与注册(初始化阶段)
TraeAgent 启动时,MCPClient 会连接到已配置的 MCP Server,通过 tools/list 请求获取服务器提供的工具列表和描述。这些工具定义(名称、描述、输入/输出 schema)会被动态注入到 Agent 的工具集中。
第二步:用户请求 → Agent 处理
用户向 Trae 发出自然语言指令,TraeAgent 构造包含 system prompt、用户消息和工具 schema 的请求,发送给 LLM。
第三步:LLM 决策 → 工具调用指令
LLM 根据用户意图和可用工具,决定调用哪个工具,并输出结构化的调用指令(Function Calling 格式)。
第四步:Agent 执行 → MCP Client 转发
TraeAgent 的 ToolExecutor 接收到工具调用指令后,通过 MCPClient 将 tools/call 请求发送给 MCP Server。
第五步:MCP Server 执行工具
MCP Server 接收到请求后,执行对应的工具函数(如读写文件、查询数据库、调用 API),并将结果通过 MCP 协议返回。
第六步:结果返回 → Agent 继续循环
MCP Client 将工具执行结果返回给 TraeAgent,Agent 将结果注入对话历史,然后再次调用 LLM,判断是否需要继续调用其他工具,或任务已完成。
这个循环会持续进行,直到 LLM 返回"无工具调用"且任务完成,或者达到最大步数限制。
六、工具定义在哪里?MCP 中还是被 MCP 捕获?
这是一个关键问题。答案是:工具定义在 MCP Server 中,由 MCP Client 动态发现和捕获。
工具在 MCP Server 中定义。 MCP Server 通过 @mcp.tool 修饰器(或类似机制)将普通函数暴露为工具。每个工具都有标准的定义结构:
python
{
"name": "calculate_sum",
"description": "将两个数字相加",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}
}
这个结构由 MCP Server 维护,是 MCP 协议的一等公民。
MCP Client 动态发现工具。 客户端不会硬编码工具列表,而是通过 tools/list 请求动态获取 服务器提供的工具。Trae 的 MCPClient 正是扮演这个角色,它发现外部 MCP 服务并将工具注入到 TraeAgent 中。
工具的定义与执行是分离的。 工具的实际代码(如 calculate_sum 函数的实现)运行在 MCP Server 进程 中。MCP Server 可能是一个独立的 Node.js 进程、Python 脚本或远程 HTTP 服务。MCP Client 只负责发现、转发和结果传递,不参与工具的具体执行。
对比:如果工具直接定义在代码中会怎样? 传统的 Function Calling 方式把工具定义硬编码在应用代码里,换一个 LLM 平台就要重写一套工具定义。MCP 的价值就在于解耦:工具在 Server 中定义一次,任何 MCP Client(Trae、Cursor、Claude Desktop)都能发现并使用它。
总结表
| 问题 | 答案 |
|---|---|
| 调用关系 | TraeAgent(Agent)→ MCPClient → MCP Server → 工具执行 → 结果返回 → 再次调用 LLM |
| 工具定义在哪 | 定义在 MCP Server 中,通过标准 schema 暴露 |
| MCP Client 做什么 | 动态发现 工具,将工具 schema 注入 Agent,转发调用请求,传递执行结果 |
| 大模型做什么 | 根据工具 schema 和用户意图,决策调用哪个工具,输出调用指令 |
| Trae 的角色 | 提供 Host 环境,TraeAgent 作为 Agent 驱动整个循环,MCPClient 负责 MCP 协议通信 |
简单来说:Trae 是舞台,Agent 是导演,大模型是决策者,MCP Client 是传令兵,MCP Server 是执行者,工具是具体的动作。 工具的定义完全在 MCP Server 侧,Client 只负责"发现并借用"。
七、全文总结
MCP 的底层逻辑可以概括为两个关键设计:
-
通过客户端适配层,将"模型能力差异"封装起来,让小模型也能通过提示词工程间接"理解"并调用工具。
-
通过严格的校验和反馈重试机制,将"模型输出不确定性"转化为可迭代修复的过程,而不是直接的失败。
同时,MCP 在架构上实现了工具定义与执行的解耦:工具在 MCP Server 中定义,MCP Client 动态发现并注入 Agent,大模型负责决策,Agent 驱动整个调用循环。Trae 作为 Host,其 TraeAgent 扮演 Agent 角色,MCPClient 负责协议通信,最终由 MCP Server 执行工具。
这套设计使得 MCP 能够兼容不同能力的模型,并在格式错误时自动容错,为 AI 编程助手提供了稳定、可扩展的工具调用基础设施。
