Structured Output、Function Calling 和 MCP

2025 年 11 月 21 日 星期五(已编辑)
7

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 肯定无法给出或者直接编造出一个回答

那么如何解决这种问题呢,如下:

  1. 用户发送消息
    「我明天要在成都上下班通勤,天气怎么样?」
  2. 后端在向 LLM 发送请求前解析消息,发现有「明天」「天气」「成都」这些关键字
  3. 调用已经为这种情况预设好的方法,如 Weather w = weatherService.getWeather("成都", DateUtil.tomorrow()); 获取天气信息
  4. 再将天气信息插入到用户消息中,即
    「成都今天最高 18℃,最低 12℃,小雨,降水概率 70% 。 我明天要在成都上下班通勤,天气怎么样?」
  5. 这时候 AI 就不会编造或者无法回答了,将根据后端插入的信息回答

上述流程的问题很明显:

  1. 关键词的解析非常脆弱:如果用户发送消息换成
    「我明天要在成都上下班通勤,会下雨吗?」
    那显然就解析失败了,漏判了这个需要获取天气信息的请求
    比如「...又是灰蒙蒙的天气死我了」
    解析成功了,但是没什么用,在请求中增添了无用的天气信息,污染上下文,浪费 AI 调用成本
  2. 需要预设场景:需要后端考虑很多场景,并建立相应的解析规则以提供外部信息源
  3. 不由模型控制,无法与现实世界交互:是后端进行工具的使用,整个表现仍然是一个 chatbox,大语言模型无法主动调用,也就是当模型想获取天气信息时无法主动得到

1.1.2 What 是什么

从上面的示例能看出,LLM 无法自主获取信息,只能被动的在用户的请求中获得
Function Calling 就赋予 LLM 一种与外界交互的能力,通过规定好的格式,让 LLM 告诉后端调用什么工具,并将结果返回进上下文中,以此获取外部信息或是操控外部数据

从概念上看,Function Calling 有三个关键点:

  1. 工具定义 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"
        ]
      }
    }
  }
]
  1. 工具调用 tool_calls
    当模型认为需要执行某个工具时,会生成一条带有 tool_calls 参数的 Aimessage,里面会有这个工具调用的 idnamearguments,这个工具调用等价于「获取成都未来的天气」
"tool_calls": [
  {
    "type": "function",
    "id": "606046057",
    "function": {
      "name": "get_weather",
      "arguments": "{\"location\":\"成都\",\"extensions\":\"all\"}"
    }
  }
]
  1. 工具结果消息 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"
}

上面的响应体中可以看到 id606046057tool_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 参数需要与 AiMessagetool_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

截屏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 中,提到了传统方法的局限性:

  1. 需要后端提前预设好场景:提前考虑了天气场景,实现了解析、获取才能支持
  2. 场景解析的不可靠:漏判或者判错场景,缺失外部信息或者增添无用信息

而现在,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_weathersearch_newswrite_memoryread_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

好处:

  1. 方便接入,不用在程序内部添加 Tools 的逻辑,直接通过添加 MCP Server 即可,比如在 Cursor 或者 Claude Code 中添加 MCP
  2. 隐藏了实现细节,对外提供只是一个黑盒,接收请求,返回结果,内部细节不暴露
  3. 动态管理能力,MCP Client 可以根据 tools/list 动态发现工具,也可以手动禁用某些工具

3.2 MCP 运行流程

流程图如下

案例:
高德 MCP:

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...