Agent开发(三) — MCP与Function call 与Struct output
前言
在具体深入到MCP协议之前,我想先来捋一捋来龙去脉完整的展现MCP 协议出现的必要原因和目的。一切要从 OpenAI 描述的通向通用人工智能(AGI)的五个阶段开始,分别如下:
- Level 1: Conversational AI (对话式AI / Chatbot):
- 专注于自然语言理解和生成,能够进行流畅的对话。
- Level 2: Reasoning AI (推理式AI / Reasong 推理者):
- 具备更强的逻辑推理、问题解决和复杂任务处理能力。
- Level 3: Autonomous AI (自主式AI):
- AI系统能够设定目标、规划行动、并独立执行复杂任务。
- Level 4: Innovating AI (创新式AI):
- AI能够进行科学发现、技术突破或产生新知识,具备高度的创造性。
- Level 5: Organizational AI (组织式AI):
- AI能够进行跨领域、大规模的协调和管理,甚至能有效运行大型组织或经济体。
来自openai的 www.forbes.com/sites/jodie…
Level 1: Conversational AI 很好理解,chatgpt 3.5 出来的时候就是就是该阶段,相信也是很多人的AI启蒙时刻
Level 2: Reasoning AI (推理式AI / Reasong 推理者) 的里程碑就是今年年初引起AI狂热的DeepSeek-R1,一个能够读懂用户心理且具备自我深度思考的大模型
Level 3: Autonomous AI (自主式AI):接下来就是本文的重点,如何给AI 动手动脚 的能力呢?
发展
结构化输出(Structured model outputs)
如果你还有早期AI使用习惯的话,想要从对话里面获取Json或者Xml此类信息是非常困难的一件事情,经常会输出不符合要求的格式出来,为此,早期开发者不得不依赖复杂的后处理逻辑、正则表达式,甚至人工校验来“清洗”模型输出,以确保下游系统能正确解析。
这种不可靠的结构化输出严重限制了AI在自动化流程中的应用——毕竟,如果连“告诉AI去调用哪个API、传什么参数”都无法稳定实现,又谈何“自主执行任务”?
为了解决这个问题,业界逐步引入了 Structured Output(结构化输出) 技术。其核心思想是:在模型生成阶段就强制约束输出格式,而非依赖事后修正。例如Openai 的结构化输出例子
import json
from openai import OpenAI
client = OpenAI(
api_key="<your-api-key>",
base_url="https://api.deepseek.com",
)
system_prompt = """
The user will provide some exam text. Please parse the "question" and "answer" and output them in JSON format.
EXAMPLE INPUT:
Which is the highest mountain in the world? Mount Everest.
EXAMPLE JSON OUTPUT:
{
"question": "Which is the highest mountain in the world?",
"answer": "Mount Everest"
}
"""
user_prompt = "Which is the longest river in the world? The Nile River."
messages = [{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}]
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
response_format={
'type': 'json_object'
}
)
print(json.loads(response.choices[0].message.content))
返回Json大概如下
{'question': 'Which is the longest river in the world?', 'answer': 'The Nile River'}
函数调用(Function calling)
接下来就是本文章的主角也是奠定了大模型对外的 操作的行为逻辑,函数调用(也称为工具调用)主要是给大模型提供了一种强大且灵活的方式,以与外部系统交互并访问其训练数据之外的数据。
一个函数由其 schema 定义,该 schema 告知模型它的功能以及它期望的输入参数。一个函数定义具有以下属性:
| Field | Description |
|---|---|
type | This should always be function这个应该始终是 function |
name | The function's name (e.g. get_weather)函数的名称(例如 get_weather ) |
description | Details on when and how to use the function何时以及如何使用该函数的详细信息 |
parameters | JSON schema defining the function's input arguments定义函数输入参数的 JSON schema |
strict | Whether to enforce strict mode for the function call是否强制启用函数调用的严格模式 |
如果你刚刚有尝试过上面这个例子的话,请求体里面的 response_format={"type": "json_object"}`` 就是明确要求 JSON 输出,而Tool则需要进行 **注册** 到LLM的上下文,在LLM需要的时候会自动掉用并转门返回以 tool` 为角色的消息
from openai import OpenAI
def send_messages(messages):
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools
)
return response
client = OpenAI(
api_key="<your-api-key>", # 这里填入deepseek的API密钥
base_url="https://api.deepseek.com",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather of a location, the user should supply a location first.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
}
},
"required": ["location"]
},
}
},
]
messages = [{"role": "user", "content": "How's the weather in Hangzhou, Zhejiang?"}]
message = send_messages(messages)
print(message.json())
大概返回结果如下
{
"id": "9ac1395d-f190-4cc0-a7de-3ca1fa3a6493",
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"logprobs": null,
"message": {
"content": "I'll check the weather in Hangzhou, Zhejiang for you.",
"refusal": null,
"role": "assistant",
"annotations": null,
"audio": null,
"function_call": null,
"tool_calls": [
{
"id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw",
"function": {
"arguments": "{\"location\": \"Hangzhou, Zhejiang\"}",
"name": "get_weather"
},
"type": "function",
"index": 0
}
]
}
}
],
"created": 1760591188,
"model": "deepseek-chat",
"object": "chat.completion",
"service_tier": null,
"usage": {
"completion_tokens": 33,
"prompt_tokens": 181,
"total_tokens": 214,
"completion_tokens_details": null,
"prompt_tokens_details": {
"audio_tokens": null,
"cached_tokens": 128
},
"prompt_cache_hit_tokens": 128,
"prompt_cache_miss_tokens": 53
}
}
假如已经执行完成了之后,以 tool 角色组装成回复内容
{
"role": "tool",
"tool_call_id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw", // 必须匹配第一轮的 tool_calls[0].id
"name": "get_weather",
"content": "The current temperature in Hangzhou, Zhejiang is 25°C, with light rain." // 工具的实际输出
}
加入到 chat message 组成下面对话
[
// 1. 用户的原始请求
{
"role": "user",
"content": "What is the weather in Hangzhou, Zhejiang?"
},
// 2. 模型在第一轮返回的工具调用请求
// (这是从你提供的JSON响应中提取的 message 对象)
{
"role": "assistant",
"content": "I'll check the weather in Hangzhou, Zhejiang for you.", // 模型的过渡文本 (可选, 有些模型不返回此内容)
"tool_calls": [
{
"id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw", // 必须使用模型返回的ID
"function": {
"arguments": "{\"location\": \"Hangzhou, Zhejiang\"}",
"name": "get_weather"
},
"type": "function"
}
]
},
// 3. 你的应用程序执行完工具函数后,返回给模型的结果
{
"tool_call_id": "call_00_EwxDgXTEHvTNPwD7YRBRZTEw", // ❗ 必须匹配上面的 tool_calls[0].id
"role": "tool",
"name": "get_weather", // ❗ 必须匹配调用的函数名
"content": "The current temperature in Hangzhou, Zhejiang is 25°C, with light rain." // ❗ 工具函数的实际输出结果 (字符串)
}
]
这就是一轮完整的 工具调用(Function Calling) 交互流程:模型识别出需要外部信息 → 主动请求调用指定工具 → 应用执行工具并返回结果 → 模型基于结果生成最终回答。
strict Mode
因为LLM本质上还是一个 预测下一个Token 的作为驱动,即使使用Json-Schema来限定还有其他原因导致的并不会进行正确的tool_call,比如下面这个来自 Qwen/Qwen3-VL-235B-A22B-Instruct 的模型要求输出 tool_call的时候反而直接将 tool_call写在了reasoning_content 中
{
"id": "0199ef984be17de0da6bbd2cdaf17736",
"object": "chat.completion",
"created": 1760661360,
"model": "Qwen/Qwen3-VL-235B-A22B-Instruct",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"reasoning_content": "<tool_call>\n{\"name\": \"navigate\", \"arguments\": {\"url\": \"https://www.bing.com\"}}\n</tool_call>"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 6385,
"completion_tokens": 253,
"total_tokens": 6638,
"completion_tokens_details": {
"reasoning_tokens": 0
}
},
"system_fingerprint": ""
}
开启的方式也非常简单,直接在tool里面新增一行 "strict": true 即可
{
"type": "function",
"function": {
"name": "get_weather",
"strict": true, // 输入这一行即可
"description": "Get weather of a location, the user should supply a location first.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
}
},
"required": ["location"],
"additionalProperties": false
}
}
}
不过要注意并不是每个模型都支持的,具体还得看各个厂家的说明,比如说 Deepseek 就明确说明支持 https://api-docs.deepseek.com/guides/function_calling#strict-mode-beta
MCP
虽然函数调用极大拓展了大模型的“行动边界”,但它仍存在一个关键瓶颈:缺乏统一标准。不同厂商(如 OpenAI、Anthropic、DeepSeek)对工具的定义格式、调用方式、认证机制、错误处理等各不相同。
开发者若想让一个 AI Agent 同时支持多个模型或接入多种企业系统(如 Slack、Notion、Salesforce、内部数据库),就必须为每种组合编写定制化适配层——这不仅成本高昂,也阻碍了 AI Agent 的规模化部署与互操作性。
正是在这一背景下,Model Context Protocol(MCP,模型上下文协议) 应运而生。
Model Context Protocol(MCP) 由 Anthropic 于 2024 年 11 月正式推出,是一项开源、开放标准协议,旨在解决当前 AI 工具调用生态中“碎片化集成”的核心痛点。其官方目标是“用单一协议取代零散的集成方式”,让 AI 助手能够以统一方式访问任意外部工具或数据源 。
MCP 的本质不是取代函数调用,而是为其提供标准化的通信层。正如 HTTP 之于 Web,MCP 希望成为 LLM 与外部世界交互的“通用语言” 。
技术架构
MCP 采用清晰的客户端-服务器(Client-Server)架构,包含四个核心组件 :
- AI 应用(App / Agent Host):用户交互界面,如聊天机器人、自动化工作流引擎;
- MCP 客户端(MCP Client):嵌入在 AI 应用中,负责与 MCP 服务器通信,将工具请求转换为 MCP 协议格式;
- MCP 服务器(MCP Server):由工具提供方部署,将本地能力(如数据库查询、API 调用)暴露为符合 MCP 规范的接口;
- 外部服务(External Service):实际执行操作的系统,如 Slack、Notion、企业 ERP 等。
如何搭建一个最简的 MCP Client/Server
这里涉及到几个关键点:
- MCP 注册中心和发现
- MCP 服务执行模块
前者需要将MCP进行注册和发现,后者则是具体的功能的实现
接下来让我们一步步来搭建,首先是下载开源项目 https://github.com/charSLee013/mcp-transport-demo
git clone https://github.com/charSLee013/mcp-transport-demo
cd mcp-transport-demo
安装依赖 (推荐虚拟环境)
uv sync --frozen --all-extras --dev
运行下面的脚本会自动启动 client 和 server 用来网络通信
bash scripts/run_sse_demo.sh
运行完成后会有两个文件分别是
- /tmp/sse_server.log 显示 Uvicorn 启动、监听 8000 端口、处理初始化和两次工具调用。
- /tmp/sse_client.log 包含客户端发起的工具调用与 SSE 回传的数据,例如 tools/call.result 的文本内容。
下面就是 sse_client.log 来从客户端视角来看看 SSE 运行的全流程
SSE 协议流传输解析
打开 sse_client.log后,下面将一步步拆解整个 SSE 启动与通信流程,并用清晰说明每个阶段的作用和设计意图。
阶段一:建立SSE连接并发现通信端点
这是所有通信的起点,目的是建立一个由服务器到客户端的持久消息通道。
1. 客户端发起SSE连接
>>> REQUEST GET <http://127.0.0.1:8000/sse>
Headers (filtered — hint):
connection: keep-alive — keep-alive keeps TCP open for streaming
cache-control: no-store — no-store disables caching for streams
解说:
- 客户端向服务器的
/sse端点发送GET请求,这是一个标准的SSE握手。 connection: keep-alive请求保持TCP连接,为后续的数据流传输做准备。cache-control: no-store确保中间节点不会缓存这个实时流。
2. 服务器接受连接并返回SSE流
<<< RESPONSE 200 OK (GET <http://127.0.0.1:8000/sse>)
Headers (filtered — hint):
connection: keep-alive
transfer-encoding: chunked — chunked = streaming
cache-control: no-store — disable caching for streaming
content-type: text/event-stream; charset=utf-8 — payload media type
x-accel-buffering: no — no disables proxy buffering (SSE)
解说:
- 服务器返回
200 OK,表示同意建立SSE连接。 content-type: text/event-stream是关键,它告诉客户端这是一个SSE流,而不是普通的HTTP响应。transfer-encoding: chunked表示数据将以分块的形式发送,适合内容长度未知的流式数据。
3. 服务器推送 endpoint 事件,指定后续通信地址
SSE << event: endpoint
SSE << data: /messages/?session_id=1c75ff593002412a88b5a9ac39f80648
SSE <<
解说:
- 这是MCP协议的核心设计之一。SSE连接建立后,服务器立刻通过这个连接发送一个名为
endpoint的特殊事件。 data字段的内容/messages/?session_id=1c75ff593002412a88b5a9ac39f80648包含了两个关键信息:- 通信路径: 客户端后续所有请求(
initialize,tools/list等)都应发送到/messages/路径。 - 会话标识:
session_id用于唯一标识当前的会话,确保服务器能将所有相关请求与这个SSE流正确关联。
- 通信路径: 客户端后续所有请求(
阶段二:初始化会话
客户端现在已经知道了如何向服务器发送消息,接下来执行MCP的正式握手。
1. 客户端发送 initialize 请求
>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=1c75ff593002412a88b5a9ac39f80648>
Headers:
content-type: application/json
Body:
{
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "mcp",
"version": "0.1.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
解说:
- 客户端向
/messages/端点发送一个POST请求,并附上上一步获取的session_id。 - 这是一个标准的JSON-RPC 2.0请求。
method为initialize,id为0。 params对象详解:protocolVersion: 声明客户端希望使用的MCP协议版本。capabilities: 客户端所支持的能力集,这里为空对象{},表示使用默认能力。clientInfo: 客户端的身份信息,包括名称mcp和版本0.1.0。
2. 服务器响应 202 Accepted
<<< RESPONSE 202 Accepted (POST <http://127.0.0.1:8000/messages/?session_id=>...)
Body: Accepted
解说:
202 Accepted状态码是异步通信模式的关键。它表示服务器已成功接收并排队处理该请求,但处理结果尚未准备好。客户端应继续监听SSE流以获取最终结果。
3. 服务器通过SSE返回 initialize 结果
SSE << event: message
SSE << data: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"experimental": {},
"prompts": {
"listChanged": false
},
"resources": {
"subscribe": false,
"listChanged": false
},
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "SSE Progress Demo",
"version": "1.18.1.dev3+40acbc5"
}
}
}
SSE <<
解说:
- 服务器通过SSE的
message事件返回了完整的JSON-RPC响应。id: 0与客户端请求对应。 result对象详解:protocolVersion: 服务器同意使用的协议版本。capabilities: 服务器的能力集。这里详细列出了服务器支持的功能领域,如prompts、resources、tools,以及在这些领域中是否支持某些高级特性(如listChanged,subscribe)。serverInfo: 服务器的身份信息,包括名称和版本。
阶段三:工具调用
会话初始化后,客户端开始与服务器提供的工具进行交互。
3.1 发送初始化完成通知并获取工具列表
在MCP协议中,客户端在收到 initialize 响应后,需要发送一个 notifications/initialized 通知,告诉服务器它已完成初始化。
>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
"method": "notifications/initialized",
"jsonrpc": "2.0"
}
解说: 这是一个JSON-RPC 通知(没有id字段),表示客户端已准备就绪。
随后,客户端立即请求工具列表:
>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
"method": "tools/list",
"jsonrpc": "2.0",
"id": 1
}
服务器通过SSE返回了工具列表的完整定义:
SSE << event: message
SSE << data: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "calculator",
"title": "Calculator",
"description": "Execute a basic calculation.",
"inputSchema": {
"type": "object",
"title": "calculatorArguments",
"properties": {
"a": {"title": "A", "type": "number"},
"b": {"title": "B", "type": "number"},
"operation": {
"title": "Operation",
"type": "string",
"enum": ["add", "subtract"]
}
},
"required": ["a", "b", "operation"]
},
"outputSchema": {
"type": "object",
"title": "calculatorOutput",
"properties": {
"result": {"title": "Result", "type": "string"}
},
"required": ["result"]
}
},
{
"name": "stream_demo",
"title": "Stream Demo",
"description": "Emit log + progress notifications over SSE before returning a result.\\n\\n Args:\\n seconds: total duration (simulated)\\n steps: number of progress steps\\n ",
"inputSchema": {
"type": "object",
"title": "stream_demoArguments",
"properties": {
"seconds": {"title": "Seconds", "type": "number", "default": 2.0},
"steps": {"title": "Steps", "type": "integer", "default": 5}
}
}
}
]
}
}
SSE <<
解说: result 对象包含一个 tools 数组,每个元素都是一个工具的完整元数据定义,包括 name、description 以及至关重要的 inputSchema(输入参数的JSON Schema)和 outputSchema(输出的JSON Schema)。
3.2 调用流式工具 stream_demo
1. 客户端调用 stream_demo
>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
"method": "tools/call",
"params": {
"name": "stream_demo",
"arguments": {
"seconds": 1.5,
"steps": 4
},
"_meta": {
"progressToken": 2
}
},
"jsonrpc": "2.0",
"id": 2
}
解说:
params对象详解:name: 指定要调用的工具。arguments: 传递给工具的参数,这里指定总时长为1.5秒,分为4步。_meta: 元数据对象。progressToken: 2是一个客户端定义的令牌,用于将后续的进度通知与这次特定的调用关联起来。
2. 服务器通过SSE推送一系列通知和最终结果
服务器在任务执行期间,通过SSE持续推送消息。
-
开始通知:
{"method":"notifications/message","params":{"level":"info","data":"stream started: 4 steps / 1.5s total"},"jsonrpc":"2.0"} -
循环推送进度(以第1步为例):
// 日志消息 {"method":"notifications/message","params":{"level":"info","data":"step 1/4"},"jsonrpc":"2.0"} // 进度通知 {"method":"notifications/progress","params":{"progressToken":2,"progress":25.0,"total":100.0,"message":"step 1"},"jsonrpc":"2.0"}解说:
notifications/message: 这是一个通用的日志通知。notifications/progress: 这是专门的进度通知。其params中包含的progressToken: 2正好与客户端调用时传入的_meta.progressToken匹配,客户端可以据此识别这是哪个任务的进度。
-
结束通知和最终结果:
// 结束日志 {"method":"notifications/message","params":{"level":"info","data":"stream finished"},"jsonrpc":"2.0"} // 最终的JSON-RPC响应 { "jsonrpc":"2.0", "id":2, "result": { "content": [ { "type": "text", "text": "{\\n \\"status\\": \\"done\\",\\n \\"steps\\": 4,\\n \\"seconds\\": 1.5,\\n \\"elapsed\\": 1.51\\n}" } ], "isError": false } }解说:
- 最后,服务器发送了一个与请求
id: 2匹配的JSON-RPC响应,表示任务已彻底完成。 result对象详解:content: 一个包含结果的数组,每个元素可以有不同类型。这里是一个type: "text"的文本内容。isError: 布尔值,明确指出此调用是否成功。
- 最后,服务器发送了一个与请求
3.3 调用常规工具 calculator
这是一个快速、非流式的调用,但其内部机制与流式调用完全一致。
1. 客户端调用 calculator
>>> REQUEST POST <http://127.0.0.1:8000/messages/?session_id=>...
Body:
{
"method": "tools/call",
"params": {
"name": "calculator",
"arguments": {
"a": 7,
"b": 4,
"operation": "add"
}
},
"jsonrpc": "2.0",
"id": 3
}
解说: 调用 calculator 工具,提供加法操作所需的参数。
2. 服务器通过SSE返回结果
SSE << event: message
SSE << data: {
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "结果: 11.0"
}
],
"structuredContent": {
"result": "结果: 11.0"
},
"isError": false
}
}
SSE <<
解说:
- 服务器同样通过SSE返回了
id: 3对应的响应。 result对象详解:content: 包含人类可读的文本结果。structuredContent: 这是一个额外的字段,提供了结构化的数据(这里是JSON对象),方便程序直接解析和使用,而无需从文本中提取。isError:false表示调用成功。
关于通信中的ID(额外补充内容)
如果你足够细心的话,会发现建立连接后,每次请求都会记上 id ,那这是什么呢?
简单来说,id 的核心作用是:关联请求与响应,实现异步通信。
下面我们从三个层面来深入理解它的作用。
层面一:基础作用 —— 关联请求与响应
这是 id 最直接的功能。在一个理想化的、一问一答的同步世界里,你可能不需要 id,因为你发送一个请求,然后等待一个响应。但在MCP over SSE的异步模型中,情况完全不同。
工作原理:
- 客户端生成唯一ID:当客户端发起一个需要响应的请求时(如
initialize,tools/list,tools/call),它必须生成一个唯一的id(如id: 0)并放入JSON-RPC请求对象中。 - 服务器必须原样返回:服务器在处理完该请求后,其返回的JSON-RPC响应对象中,必须包含与请求完全相同的
id。 - 客户端进行匹配:客户端接收到通过SSE流推送过来的响应时,会检查响应中的
id,并与它之前发送的请求进行匹配,从而知道“这个响应对应的是我之前发的哪个请求”。
示例分析(来自你的日志):
- 客户端发送
id: 0的initialize请求。 - 之后,客户端通过SSE收到
id: 0的响应,就知道这是initialize的结果。 - 客户端发送
id: 1的tools/list请求。 - 之后,客户端收到
id: 1的响应,就知道这是工具列表。
层面二:核心机制 —— 支持高并发与乱序响应
这是 id 最重要的价值所在,也是异步通信模式能够成功的关键。
设想一个没有 id 的混乱场景:
- 客户端快速连续发送了两个请求:
- 请求A(
tools/call stream_demo),一个耗时1.5秒的慢任务。 - 请求B(
tools/call calculator),一个毫秒级的快任务。
- 请求A(
- 服务器异步处理它们。请求B先完成,请求A后完成。
- 服务器通过SSE流把结果推回给客户端。先推送了B的结果,然后推送了A的结果。
- 问题来了:客户端先收到一个结果,然后又收到一个结果。它怎么知道哪个结果是
stream_demo的,哪个是calculator的?它无法区分,整个通信模型就崩溃了。
id 如何解决这个问题:
id 为每个请求-响应对提供了一个独一无二的“身份证”。
- 客户端发送请求A,带上
id: 2。 - 客户端发送请求B,带上
id: 3。 - 服务器先完成B,通过SSE推送一个
id: 3的响应。 - 客户端收到
id: 3的响应,立刻知道:“哦,这是calculator的结果,处理一下。” - 服务器后完成A,通过SSE推送一个
id: 2的响应。 - 客户端收到
id: 2的响应,知道:“这是stream_demo的最终结果。”
结论:id 使得响应可以乱序返回,客户端依然能正确地处理,这正是实现高并发和非阻塞交互的基础。客户端不必傻等一个慢请求完成,才能发下一个请求。
层面三:语义区分 —— 区分“请求”与“通知”
在JSON-RPC协议中,id 的存在与否,定义了消息的根本类型。
- 有
id的消息是“请求”:表示客户端期望得到一个响应。例如{"method": "tools/list", "id": 1}。 - 没有
id的消息是“通知”:表示客户端只是在告知服务器某件事,完全不期望得到任何响应。例如日志中的notifications/initialized:
{
"method": "notifications/initialized",
"jsonrpc": "2.0"
}
为什么需要通知? 有些事件只是“告知”,不需要回复。比如“我(客户端)已经初始化完了”,服务器收到后无需专门回复一个“收到”,这样减少了不必要的网络往返,提高了效率。
通俗比喻:餐厅点餐
把整个过程想象成你在一家餐厅点餐:
- 你(客户端) 向 服务员(SSE连接) 点了两道菜:
- 慢炖的佛跳墙(
stream_demo),服务员给了你一张 #A号 牌(id: 2)。 - 快手的凉拌黄瓜(
calculator),服务员给了你一张 #B号 牌(id: 3)。
- 慢炖的佛跳墙(
- 后厨(服务器) 同时制作。
- 凉拌黄瓜先做好了,服务员端过来,喊道:“#B号的菜好了!” 即使这不是你先点的,但你看了一眼牌子就知道是你的,于是开吃。
- 过了很久,佛跳墙做好了,服务员喊道:“#A号的菜好了!” 你从容地享受你的大餐。
- 如果你只是跟服务员说“麻烦帮我加点茶水”(通知),你就不需要拿号牌,服务员也不会特地跑回来告诉你“茶水已加好”。
在这个比喻里,号牌就是 id。它让点餐(请求)和上菜(响应)过程可以高效、有序、并发地进行。
总结:从“能说”到“能做”,再到“能自主”
回顾全文,我们沿着一条清晰的技术演进路径,逐步为大模型赋予了越来越强的“行动能力”:
- 结构化输出(Structured Outputs) 解决了“说清楚”的问题。通过强制模型输出合法、可靠的 JSON,我们让 AI 的语言具备了机器可解析的确定性,为自动化奠定了第一块基石。
- 函数调用(Function Calling) 解决了“能做事”的问题。通过将外部工具的能力以 JSON Schema 的形式注册给模型,AI 不再局限于其训练数据,而是能够主动调用 API、查询数据库、执行计算,真正开始与外部世界互动。
- 严格模式(Strict Mode) 解决了“做对事”的问题。它将函数调用从“概率性输出”提升为“契约式交付”,确保模型生成的调用指令 100% 符合开发者定义的规范,为生产环境的可靠性提供了保障。
然而,当我们将视野从单个工具扩展到整个企业生态时,一个新的瓶颈出现了:碎片化的集成方式。每个模型、每个服务都有自己的“方言”,这让构建一个能同时操作 Slack、Notion、Salesforce 和内部系统的通用 AI Agent 变得异常复杂。
MCP(Model Context Protocol)的出现,正是为了解决这个规模化难题。它没有发明新的能力,而是为已有的能力提供了一个标准化的“神经接口”:
- 对工具提供方而言,只需实现一次 MCP Server,其能力就能被所有兼容 MCP 的 AI 应用无缝调用。
- 对 AI 应用开发者而言,只需集成 MCP Client,就能接入一个不断增长的、标准化的能力生态,无需再为每个服务编写定制胶水代码。
更重要的是,MCP 的设计(如 SSE 流式传输、id 驱动的异步通信、Resources/Tools 的语义区分)完美契合了 Level 3: Autonomous AI 的核心需求:
- 持续感知:通过
Resources和流式通知,Agent 能实时掌握环境状态; - 可靠执行:通过
Tools和严格模式,Agent 能安全、准确地执行动作; - 自主协同:通过统一协议,Agent 能自由组合不同领域的工具,完成复杂任务。
因此,MCP 不仅仅是一个技术协议,更是通向自主智能体时代的关键基础设施。它标志着 AI 开发范式正从“手工作坊”迈向“工业化标准”,让“能连续数日代表用户自主完成任务”的 AI Agent 从概念走向现实。
未来已来,只是尚未均匀分布。而 MCP,正是加速这一分布的催化剂。