Structured Output、Function Calling 和 MCP
0. 引言
本文章将会以“天气怎么样”这个话题讲述 Structured Output、Function Calling 和 MCP 这三个概念,通过本地在 LM Studio 上部署的 qwen/qwen3-4b-2507 进行示例展示
1. Function Calling 工具调用
1.1. Tool 工具
1.1.1 Why 为什么
传统的大模型都表现为对话形式,一问一答,只能 chat,无法与现实世界交互
以最常见的「今天天气怎么样」为例,ai 肯定无法给出或者直接编造出一个回答
那么如何解决这种问题呢,如下:
- 用户发送消息
「我明天要在成都上下班通勤,天气怎么样?」 - 后端在向 LLM 发送请求前解析消息,发现有「明天」「天气」「成都」这些关键字
- 调用已经为这种情况预设好的方法,如
Weather w = weatherService.getWeather("成都", DateUtil.tomorrow());获取天气信息 - 再将天气信息插入到用户消息中,即
「成都今天最高 18℃,最低 12℃,小雨,降水概率 70% 。 我明天要在成都上下班通勤,天气怎么样?」 - 这时候 AI 就不会编造或者无法回答了,将根据后端插入的信息回答
上述流程的问题很明显:
- 关键词的解析非常脆弱:如果用户发送消息换成
「我明天要在成都上下班通勤,会下雨吗?」
那显然就解析失败了,漏判了这个需要获取天气信息的请求
比如「...又是灰蒙蒙的天气死我了」
解析成功了,但是没什么用,在请求中增添了无用的天气信息,污染上下文,浪费 AI 调用成本 - 需要预设场景:需要后端考虑很多场景,并建立相应的解析规则以提供外部信息源
- 不由模型控制,无法与现实世界交互:是后端进行工具的使用,整个表现仍然是一个 chatbox,大语言模型无法主动调用,也就是当模型想获取天气信息时无法主动得到
1.1.2 What 是什么
从上面的示例能看出,LLM 无法自主获取信息,只能被动的在用户的请求中获得
Function Calling 就赋予 LLM 一种与外界交互的能力,通过规定好的格式,让 LLM 告诉后端调用什么工具,并将结果返回进上下文中,以此获取外部信息或是操控外部数据
从概念上看,Function Calling 有三个关键点:
- 工具定义
tools:
告诉 LLM:工具名称、工具描述、参数 JSON Schema
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息。当用户询问天气、降水、温度等相关问题时使用此工具。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称,例如:成都、北京"
},
"extensions": {
"type": "string",
"enum": [
"base",
"all"
],
"description": "气象类型:\n - base: 返回实况天气,适用于查询\"现在\"、\"当前\"、\"实时\"的天气\n - all: 返回预报天气(未来3-4天),适用于查询\"明天\"、\"后天\"、\"未来几天\"的天气"
}
},
"required": [
"location",
"extensions"
]
}
}
}
]
- 工具调用
tool_calls:
当模型认为需要执行某个工具时,会生成一条带有tool_calls参数的Aimessage,里面会有这个工具调用的id、name、arguments,这个工具调用等价于「获取成都未来的天气」
"tool_calls": [
{
"type": "function",
"id": "606046057",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"成都\",\"extensions\":\"all\"}"
}
}
]
- 工具结果消息
ToolExecutionResultMessage:
后端收到 LLM 的消息,发现有tool_calls,根据里面的工具名和参数执行方法,并将结果包装成一条"rool": "tool", "id": "对应 tool_call 的那条 id"的消息,放进消息历史,让模型能够看到
也就是 Function Calling 让 LLM 拥有了规范化的主动获取信息、主动交互的能力
1.1.3 How 使用
Function Calling 的流程很简单
示例:
请求体:
{
"model": "qwen3-4b-2507",
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息。当用户询问天气、降水、温度等相关问题时使用此工具。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称,例如:成都、北京"
},
"extensions": {
"type": "string",
"enum": [
"base",
"all"
],
"description": "气象类型:\n - base: 返回实况天气,适用于查询\"现在\"、\"当前\"、\"实时\"的天气\n - all: 返回预报天气(未来3-4天),适用于查询\"明天\"、\"后天\"、\"未来几天\"的天气"
}
},
"required": [
"location",
"extensions"
]
}
}
}
],
"messages": [
{
"role": "user",
"content": "我明天要在成都上下班通勤,会下雨吗?"
}
]
}
响应体:
{
"id": "chatcmpl-g23i230saulvl49aru2okl",
"object": "chat.completion",
"created": 1763727823,
"model": "qwen/qwen3-4b-2507",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"type": "function",
"id": "606046057",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"成都\",\"extensions\":\"all\"}"
}
}
]
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 275,
"completion_tokens": 26,
"total_tokens": 301
},
"stats": {},
"system_fingerprint": "qwen/qwen3-4b-2507"
}
上面的响应体中可以看到 id 为 606046057 的 tool_call 调用,后端读取后,即调用工具,获取天气信息,随后加入到消息历史中,并再次调用 LLM
这里使用的是高德天气查询 API
请求体:
{
"model": "qwen/qwen3-4b-2507",
"messages": [
{
"role": "user",
"content": "我明天要在成都上下班通勤,天气怎么样?"
},
{
"content": "",
"role": "assistant",
"tool_calls": [
{
"id": "606046057",
"function": {
"arguments": "{\"location\":\"成都\",\"extensions\":\"all\"}",
"name": "get_weather"
},
"type": "function"
}
]
},
{
"role": "tool",
"tool_call_id": "606046057",
"name": "get_weather",
"content": "{\"status\": \"1\", \"count\": \"1\", \"info\": \"OK\", \"infoc... <Truncated in logs> ... \"nighttemp_float\": \"8.0\"}]}], \"_city_name\": \"成都\"}"
}
]
}
需要注意的是,添加的
ToolExecutionResultMessage里有一个tool_call_id参数需要与AiMessage中tool_calls中的id对应,用来对应调用和结果
2. 结构化输出 Structured Output
2.1 Intro 介绍
通过第一节的工具调用,解决了 LLM 无法获取外部信息的问题,如
User: 我明天要在成都上下班通勤,天气怎么样?
Assistant: 明天(11月22日)成都的天气情况如下:
- **天气**:白天阴,夜间有小雨(局部可能有零星小雨)。
- **气温**:最高气温16℃,最低气温8℃,体感较凉,早晚温差较大。
- **风力**:北风1-3级,风力较弱,但体感可能偏冷。
- **建议**:由于有小雨,建议你出门携带雨具(如雨伞或雨衣),注意防风保暖,尤其是早晚出行时注意添衣,避免着凉。
另外,11月底成都气温整体偏冷,通勤时注意保暖,特别是早晚时段。如果天气持续阴雨,可能影响出行效率,建议提前规划好路线,注意交通安全。
祝你通勤顺利,明天天气虽冷,但晴雨交替,记得适时增减衣物!🌧️☂️
很明显,LLM 只能输出一段自然语言,文本很难利用,只能在文本框内派上用场
如果能让 LLM 输出一段规范的数据,那使用场景是不是更多了呢
拿最常见的「天气卡片」来说,如果 LLM 输出的是规范化的、结构化的消息,那卡片内容完全可以由 LLM 产出,根据对用户的记忆可以有非常个性化的内容,如:

截屏2025-11-21 23.43.24.png|500
让 LLM 产出结构化的内容,前端可以直接渲染天气卡片,上面卡片对应的 json 数据如下
{
"location": "成都",
"date": "2025-11-22",
"weather": {
"summary": "阴转小雨",
"temp_high": 16,
"temp_low": 8,
"condition": "阴转小雨",
"rain_probability": 0.3
},
"commute_advice": {
"go_to_work": {
"need_umbrella": true,
"suggest_clothing": "携带防风外套和雨具,建议穿深色防雨外套",
"text": "早晚温差大,建议携带轻便防风外套,通勤途中可能有小雨,务必带伞"
},
"back_home": {
"need_umbrella": true,
"suggest_clothing": "建议穿保暖、防风的衣物,如厚毛衣或羽绒服",
"text": "夜间气温较低,体感寒冷,建议穿着保暖衣物,同时携带雨具以防夜间降雨"
}
},
"alerts": [
{
"level": "warning",
"type": "rain",
"message": "通勤途中可能有小雨,建议携带雨具"
},
{
"level": "warning",
"type": "cold",
"message": "夜间气温低至8℃,体感较冷,建议穿着保暖衣物"
}
]
}
看起来似乎直接在提示词中约束模型让其这么回复也行,没错,现在的 LLM 的指令遵循能力很强,上面的 json 就是我直接在系统提示词中约束的,并没有使用 Structured Output
System Prompt 系统提示词
你是一个“通勤天气卡片生成器”。
输入中会包含:
- user_profile:用户画像
- commute:通勤信息
- weather:真实天气数据
请根据这些信息,生成一份通勤天气卡片的 JSON,字段为:
{
"location": string,
"date": string, // YYYY-MM-DD
"weather": {
"summary": string,
"temp_high": number,
"temp_low": number,
"condition": string,
"rain_probability": number // 0~1
},
"commute_advice": {
"go_to_work": {
"need_umbrella": boolean,
"suggest_clothing": string,
"text": string
},
"back_home": {
"need_umbrella": boolean,
"suggest_clothing": string,
"text": string
}
},
"alerts": [
{
"level": "info" | "warning" | "danger",
"type": "rain" | "cold" | "heat" | "wind",
"message": string
}
]
}
要求:
1. 只输出一个 JSON 对象,不要输出任何说明文字或 Markdown。
2. 字段名必须和上面保持一致。
但是最大的问题是不可靠,需要对输出校验、解析、重试,很有可能突然来个「原因如下:」什么的,成功率无法保证,而且上下文一多,就容易混乱
Structured Output 就保证了输出结构的可靠性,用 JSON Schema 将返回结构规范化
2.2 Use 使用
使用 Structured Output 输出 JSON 需要在请求时带上 JSON Schema,上面的案例如果使用 Structured Output 的话,其 JSON Schema 为
{
"type": "object",
"properties": {
"location": { "type": "string" },
"date": { "type": "string", "format": "date" },
"weather": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"temp_high": { "type": "number" },
"temp_low": { "type": "number" },
"condition": { "type": "string" },
"rain_probability": { "type": "number" }
},
"required": ["summary", "temp_high", "temp_low", "condition"]
},
"commute_advice": {
"type": "object",
"properties": {
"go_to_work": {
"type": "object",
"properties": {
"need_umbrella": { "type": "boolean" },
"suggest_clothing": { "type": "string" },
"text": { "type": "string" }
},
"required": ["need_umbrella", "text"]
},
"back_home": {
"type": "object",
"properties": {
"need_umbrella": { "type": "boolean" },
"suggest_clothing": { "type": "string" },
"text": { "type": "string" }
},
"required": ["need_umbrella", "text"]
}
},
"required": ["go_to_work", "back_home"]
},
"alerts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"level": { "type": "string", "enum": ["info", "warning", "danger"] },
"type": { "type": "string", "enum": ["rain", "cold", "heat", "wind"] },
"message": { "type": "string" }
},
"required": ["level", "type", "message"]
}
}
},
"required": ["location", "date", "weather", "commute_advice"]
}
在请求体中添加 JSON Schema 约束
{
"model": "qwen3-4b-2507",
"messages": [
{
"role": "system",
"content": "你是一个通勤天气卡片生成器。请根据输入,生成一份通勤天气卡片的 JSON。"
},
{
"role": "user",
"content": "目标用户画像:\n- 不太耐冷,气温低于 20℃ 就会觉得手脚冰凉。\n- 作息为大小周,本周是工作周,需要正常通勤。\n- 工作日一般 10:00 出门上班,22:00 下班,通勤方式为地铁 + 步行。\n\n今天需要生成的通勤天气卡片为:\n- 城市:成都\n- 日期:2025-11-22\n\n下面是已经查询到的天气数据(不要再次查询天气,只使用这份数据):\n\n{\n \"date\": \"2025-11-22\",\n \"location\": \"成都\",\n \"week\": \"6\",\n \"dayweather\": \"阴\",\n \"nightweather\": \"小雨\",\n \"daytemp\": \"16\",\n \"nighttemp\": \"8\",\n \"daywind\": \"北\",\n \"nightwind\": \"北\",\n \"daypower\": \"1-3\",\n \"nightpower\": \"1-3\",\n \"daytemp_float\": \"16.0\",\n \"nighttemp_float\": \"8.0\"\n}"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "WeatherAdviceResponse",
"schema": 「上面的 JSON Schema」,
"strict": true
}
}
}
responseformat 其实有三种类型 一种是
text,也就是默认的形式,会返回文本 一种是json_object,保证返回合法的 JSON 一种是 `jsonschema` 保证根据提供的 JSON Schema 返回 不同模型支持能力也不一样,比如 OpenAI 的模型基本都是支持的
即可得到对应格式的回复
3. MCP
3.0 Step 引入
在第一节 Function Calling 中,提到了传统方法的局限性:
- 需要后端提前预设好场景:提前考虑了天气场景,实现了解析、获取才能支持
- 场景解析的不可靠:漏判或者判错场景,缺失外部信息或者增添无用信息
而现在,LLM 已经拥有了两种能力:
- Function Calling:可以主动调用提供好的工具,与外部世界交互
- Structured Output:能够以给定的 JSON Schema 格式输出,这点体现在了 Function Calling 上
实际上是 OpenAI 推出了
Structured Outputs这个概念,统一了response_format: { type: "json_schema", json_schema: {...} }以及tools: [{"type": "function", "function": {}, "strict": true}],让模型能够完全受 JSON Schema 的约束输出
看起来似乎已经很强大了,我们提供 get_weather、search_news、write_memory、read_memory 工具,就可以让 LLM 记录对我们的记忆,产出个性化的、结构化的内容
但是传统方法局限性中还是有一点没有解决,那就是「需要后端提前预设好场景」,需要程序想到这个场景,并专门编写相应代码去实现一些工具
- 当用户想让 LLM 与第三方服务交互时,比如搜索 GitHub 上的某个 repo、读取 Notion 上的一篇文章、在小红书上发布一篇笔记,就需要提前写好这些工具封装,并提供给 LLM
- 想要在另一个 AI 应用中使用这些相同的工具,又要复制一遍代码,或许还要做些调整
- 如果想分享这些工具,但是又不想暴露实现细节,就有些难办
MCP 就是解决这些问题的,将提供工具和使用工具分离开,统一了工具的定义和描述,供模型与外部世界交互
3.1 What & Why
MCP 是一个开放协议,基于 JSON-RPC 2.0,暴露三类能力:Tools、Resources、Prompts,最为常见的无疑就是 Tools
为什么需要 MCP?
Function Calling 解决了模型如何告诉程序“我要调用工具”,而 MCP 解决了共享、发现、管理、复用工具
有三个概念:
MCP Server:提供方,实现工具,并根据 MCP 协议定义向外暴露工具定义
MCP Client:使用方,发现工具,并根据 MCP 协议定义调用工具
Host:程序,把 MCP Client 获取到的的工具列表转换成 tools,接收 tool_calls 并通过 MCP Client 调用 MCP Server 得到 ToolExecutionResultMessage
好处:
- 方便接入,不用在程序内部添加 Tools 的逻辑,直接通过添加 MCP Server 即可,比如在 Cursor 或者 Claude Code 中添加 MCP
- 隐藏了实现细节,对外提供只是一个黑盒,接收请求,返回结果,内部细节不暴露
- 动态管理能力,MCP Client 可以根据 tools/list 动态发现工具,也可以手动禁用某些工具
3.2 MCP 运行流程
流程图如下
案例:
高德 MCP: