第08章:MCP 模型上下文协议(下)

13 阅读21分钟

适合读者:已阅读 MCP 基础篇,理解 Host/Client/Server 三层架构,希望深入掌握工具 Schema 设计和 LangChain 集成实战的开发者。
阅读目标:读完本文,你将能独立设计复杂嵌套 Schema、理解 MCP→LangChain 适配器原理,并构建完整的 MCP Agent。
配套代码/08_mcp/02_mcp_with_langchain.py(适配器、Agent 核心代码以及 create_order 嵌套参数示例均已包含在此文件中,可直接运行)

前期回顾


0. 前置知识快速回顾

在基础篇中,我们掌握了:MCP 是 AI 工具领域的"USB 接口"标准(JSON-RPC 2.0 通信),解决了 N×M 适配问题;核心架构是 Host/Client/Server 三层,提供 Tools/Resources/Prompts 三大组件;工具用 JSON Schema 描述接口,服务器注册工具并处理调用请求。

本文聚焦进阶实战,重点覆盖以下三块:

  • 🔑 工具 Schema 设计:从简单参数到深度嵌套对象/数组(以电商订单为例)
  • 🔑 适配器原理json_schema_to_pydanticmcp_tool_to_langchain 逐行解析
  • 🔑 完整 Agent 构建:消息循环、工具路由、ToolMessage 反馈机制

1. 工具参数 Schema 从简单到复杂

MCP 工具的 inputSchema 遵循 JSON Schema 标准。理解如何从简单到复杂地设计 Schema,是开发高质量 MCP 工具的基础。

1.1 简单参数:基础类型

最常见的场景:工具只需要几个基础类型参数(字符串、整数、布尔值)。

来看 get_weather 工具的定义:

{
    "name": "get_weather",
    "description": "获取指定城市的当前天气信息",
    "inputSchema": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "城市名称,如北京、上海、广州",
            },
            "unit": {
                "type": "string",
                "description": "温度单位:celsius(摄氏度)或 fahrenheit(华氏度)",
                "enum": ["celsius", "fahrenheit"],
                "default": "celsius",
            },
        },
        "required": ["city"],   # ← 只有 city 是必填
    },
}

几个关键点:

字段作用
type: "string"参数是字符串类型
descriptionLLM 通过这段描述理解参数含义,写清楚非常重要
enum限定参数的合法取值范围,防止 LLM 乱填
default可选参数的默认值
required列表中没有的字段是可选的

对应的处理函数签名与 Schema 完全对应:

def _get_weather_impl(city: str, unit: str = "celsius") -> str:
    weather_data = {
        "北京": {"condition": "晴天", "temp": 22, "humidity": 45},
        "上海": {"condition": "多云", "temp": 26, "humidity": 70},
        "广州": {"condition": "小雨", "temp": 28, "humidity": 85},
        "成都": {"condition": "阴天", "temp": 20, "humidity": 75},
    }

    for key, data in weather_data.items():
        if key in city or city in key:
            temp = data["temp"]
            if unit == "fahrenheit":
                temp = temp * 9 / 5 + 32
                unit_str = "°F"
            else:
                unit_str = "°C"
            return f"{key}: {data['condition']}, {temp}{unit_str}, 湿度{data['humidity']}%"

    return f"未找到 {city} 的天气数据"

📌 规律required 中的字段 → Python 函数必填参数;不在 required 中的字段 → Python 函数有默认值的参数。


1.2 中等复杂:可选参数与默认值

search_news 展示了带有默认值的可选整数参数:

{
    "name": "search_news",
    "description": "搜索最新的新闻和资讯",
    "inputSchema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "搜索关键词",
            },
            "limit": {
                "type": "integer",
                "description": "返回条数,默认3",
                "default": 3,       # ← Schema 中声明默认值
            },
        },
        "required": ["query"],      # ← limit 不在 required 里,是可选参数
    },
}

对应实现函数:

def _search_news_impl(query: str, limit: int = 3) -> str:
    news_db = {
        "人工智能": [
            "GPT-5 预计将在2025年发布,性能大幅提升",
            "国内大模型竞争加剧,百炼、文心、豆包激烈角逐",
            "AI 辅助编程工具日益普及,提升开发效率50%",
        ],
        "python": [
            "Python 3.13 正式发布,引入自由线程模式",
            "Python 连续第5年成为最受欢迎编程语言",
        ],
    }

    results = []
    query_lower = query.lower()
    for key, news_list in news_db.items():
        if key in query_lower or query_lower in key:
            results.extend(news_list[:limit])
            break

    if not results:
        results = [f"关于'{query}'的最新资讯暂无数据"]

    return "\n".join(f"{i+1}. {news}" for i, news in enumerate(results[:limit]))

可选参数的处理逻辑(在适配器 json_schema_to_pydantic 中):

if prop_name not in required_fields:
    # 非必填字段:使用 Schema 中的 default,或 None
    if not has_default:
        default = None
        python_type = Optional[python_type]   # 类型变为 Optional[int]
    field_definitions[prop_name] = (
        python_type,
        Field(default=default, description=description)
    )

适配器会自动把 Schema 中的 default: 3 提取出来,赋给 Pydantic 字段的默认值,LLM 不传这个参数时就使用默认值 3


1.3 🆕 复杂嵌套参数:对象与数组(重点新案例)

真实业务场景中,工具参数往往包含嵌套对象对象数组。例如"创建电商订单"需要传入客户信息(对象)、商品列表(对象数组)、收货地址(对象)等。

完整 Schema 定义

CREATE_ORDER_TOOL = {
    "name": "create_order",
    "description": "创建电商订单,支持多商品、指定收货地址和优惠信息",
    "inputSchema": {
        "type": "object",
        "properties": {
            "customer": {
                "type": "object",
                "description": "下单客户信息",
                "properties": {
                    "name":  {"type": "string", "description": "客户姓名"},
                    "phone": {"type": "string", "description": "联系电话"},
                    "email": {"type": "string", "description": "邮箱(可选)"},
                },
                "required": ["name", "phone"],
            },
            "items": {
                "type": "array",
                "description": "购买商品列表,至少一件",
                "items": {
                    "type": "object",
                    "properties": {
                        "product_id": {"type": "string",  "description": "商品ID"},
                        "name":       {"type": "string",  "description": "商品名称"},
                        "quantity":   {"type": "integer", "description": "购买数量"},
                        "unit_price": {"type": "number",  "description": "单价(元)"},
                    },
                    "required": ["product_id", "quantity", "unit_price"],
                },
            },
            "shipping_address": {
                "type": "object",
                "description": "收货地址",
                "properties": {
                    "province": {"type": "string", "description": "省份"},
                    "city":     {"type": "string", "description": "城市"},
                    "district": {"type": "string", "description": "区县"},
                    "detail":   {"type": "string", "description": "详细地址(街道门牌号)"},
                },
                "required": ["province", "city", "detail"],
            },
            "coupon_code": {
                "type": "string",
                "description": "优惠券码(可选),以 SAVE 开头可减20元",
            },
        },
        "required": ["customer", "items", "shipping_address"],
    },
}

Schema 结构解读

create_order (object)
├── customer (object, required)           ← 嵌套对象
│   ├── name (string, required)
│   ├── phone (string, required)
│   └── email (string, optional)
├── items (array, required)               ← 对象数组
│   └── items.item (object)
│       ├── product_id (string, required)
│       ├── name (string, optional)
│       ├── quantity (integer, required)
│       └── unit_price (number, required)
├── shipping_address (object, required)   ← 嵌套对象
│   ├── province (string, required)
│   ├── city (string, required)
│   ├── district (string, optional)
│   └── detail (string, required)
└── coupon_code (string, optional)        ← 可选基础参数

处理函数实现

嵌套对象和数组在 Python 处理函数中分别以 dictlist 的形式接收(这由适配器的 type_mapping 决定,后文详解):

from datetime import datetime

def create_order_impl(
    customer: dict,
    items: list,
    shipping_address: dict,
    coupon_code: str = None,
) -> str:
    """
    创建电商订单。
    - customer: {"name": "张三", "phone": "138xxxx"}
    - items: [{"product_id": "P001", "name": "手机", "quantity": 1, "unit_price": 2999.0}]
    - shipping_address: {"province": "广东省", "city": "深圳市", "detail": "南山区xxx路"}
    - coupon_code: "SAVE20"(可选)
    """
    # 1. 计算商品总价
    total = sum(item["quantity"] * item["unit_price"] for item in items)

    # 2. 计算优惠金额(以 SAVE 开头的优惠券减20元)
    discount = 20.0 if coupon_code and coupon_code.startswith("SAVE") else 0.0

    # 3. 生成订单号
    order_id = f"ORD{datetime.now().strftime('%Y%m%d%H%M%S')}"

    # 4. 格式化商品明细
    items_str = "\n".join(
        f"  - {item.get('name', item['product_id'])} "
        f"x{item['quantity']} = {item['quantity'] * item['unit_price']:.2f}元"
        for item in items
    )

    # 5. 格式化收货地址
    district = shipping_address.get("district", "")
    address_str = (
        f"{shipping_address['province']}"
        f"{shipping_address['city']}"
        f"{district}"
        f"{shipping_address['detail']}"
    )

    result = (
        f"订单创建成功!\n"
        f"订单号:{order_id}\n"
        f"客户:{customer['name']} ({customer['phone']})\n"
        f"商品明细:\n{items_str}\n"
        f"收货地址:{address_str}\n"
        f"商品合计:{total:.2f}元\n"
        f"优惠折扣:-{discount:.2f}元\n"
        f"实付金额:{total - discount:.2f}元"
    )
    return result

直接调用验证(不通过 LLM)

在开发阶段,先直接调用 Python 函数验证逻辑,不依赖 LLM:

# 直接调用处理函数进行验证
result = create_order_impl(
    customer={"name": "张三", "phone": "138xxxx0000", "email": "zhang@example.com"},
    items=[
        {"product_id": "P001", "name": "智能手机", "quantity": 1, "unit_price": 2999.0},
        {"product_id": "P002", "name": "手机壳",   "quantity": 2, "unit_price": 29.9},
    ],
    shipping_address={
        "province": "广东省",
        "city": "深圳市",
        "district": "南山区",
        "detail": "科技园南路88号",
    },
    coupon_code="SAVE20",
)
print(result)

预期输出:

订单创建成功!
订单号:ORD20250601143022
客户:张三 (138xxxx0000)
商品明细:
  - 智能手机 x1 = 2999.00元
  - 手机壳 x2 = 59.80元
收货地址:广东省深圳市南山区科技园南路88号
商品合计:3058.80元
优惠折扣:-20.00元
实付金额:3038.80元

LLM 调用时传入的参数结构

当 LLM 决定调用 create_order 时,LangChain 会将 LLM 的函数调用响应解析为如下格式(这是 LangChain 内部的 AIMessage.tool_calls 表示,字段名为 args,而不是 MCP JSON-RPC 规范中的 arguments):

{
  "name": "create_order",
  "args": {
    "customer": {
      "name": "张三",
      "phone": "138xxxx0000"
    },
    "items": [
      {
        "product_id": "P001",
        "name": "智能手机",
        "quantity": 1,
        "unit_price": 2999.0
      }
    ],
    "shipping_address": {
      "province": "广东省",
      "city": "深圳市",
      "district": "南山区",
      "detail": "科技园南路88号"
    },
    "coupon_code": "SAVE20"
  }
}

💡 字段说明args 是 LangChain ToolCall 对象的标准字段名(对应代码 tc["args"])。代码中通过 tool_map[tc["name"]].invoke(tc["args"]) 将其传入处理函数。MCP 的 JSON-RPC 规范中同等概念的字段名为 arguments,两者含义相同,只是所在层次不同。

为什么嵌套对象映射为 Python dict

原因在适配器的类型映射表中:

type_mapping = {
    "string":  str,
    "integer": int,
    "number":  float,
    "boolean": bool,
    "array":   list,   # ← JSON array → Python list
    "object":  dict,   # ← JSON object → Python dict
}

适配器在将 JSON Schema 转换为 Pydantic 模型时,只看顶层字段的类型。对于 customer 字段,它的 type"object",所以 Pydantic 字段类型定为 dict

这意味着:

  • Pydantic 接受任意 dict 作为 customer 的值(不会验证内部字段)
  • 内部字段的验证和提取,由处理函数自己负责(如 customer['name']
  • 嵌套层级的 Schema 信息(如 customer.properties)主要是给 LLM 看的,让 LLM 知道该传什么

💡 深度解析:适配器的简单类型映射是一个有意为之的设计权衡。完整的递归 Pydantic 模型生成技术上可行但过于复杂。对于 MCP 集成场景,让 LLM 理解 Schema 结构(通过 description 字段)比运行时校验内部字段更重要。


2. MCP → LangChain 适配器原理深度解析

2.1 核心转换链条

MCP JSON Schema
      ↓  json_schema_to_pydantic()
Pydantic BaseModel(描述参数结构和类型)
      ↓  StructuredTool.from_function()
LangChain StructuredTool(可被 Agent 使用的工具对象)
      ↓  llm.bind_tools()
LLM 知道"我有哪些工具、每个工具接受什么参数"

整个链条由两个核心函数驱动:json_schema_to_pydanticmcp_tool_to_langchain


2.2 关键转换函数讲解

json_schema_to_pydantic:JSON Schema → Pydantic 模型

from pydantic import BaseModel, Field, create_model
from typing import Optional

def json_schema_to_pydantic(name: str, schema: dict) -> type[BaseModel]:
    """
    将 MCP 工具的 inputSchema 转换为 Pydantic 模型类。
    """
    properties = schema.get("properties", {})
    required_fields = schema.get("required", [])

    # ① 类型映射表
    type_mapping = {
        "string":  str,
        "integer": int,
        "number":  float,
        "boolean": bool,
        "array":   list,
        "object":  dict,
    }

    field_definitions = {}

    for prop_name, prop_schema in properties.items():
        # ② 确定 Python 类型
        python_type = type_mapping.get(prop_schema.get("type", "string"), str)
        description = prop_schema.get("description", "")
        has_default = "default" in prop_schema
        default = prop_schema.get("default")

        if prop_name not in required_fields:
            # ③ 可选参数:加上 Optional 类型包装,设置默认值
            if not has_default:
                default = None
                python_type = Optional[python_type]
            field_definitions[prop_name] = (
                python_type,
                Field(default=default, description=description)
            )
        else:
            # ④ 必填参数:无默认值,LLM 必须提供
            field_definitions[prop_name] = (
                python_type,
                Field(description=description)
            )

    # ⑤ 动态创建 Pydantic 模型类(类名如 "Get_WeatherInput")
    model = create_model(f"{name.title()}Input", **field_definitions)
    return model

逐步解读:

  • ① 类型映射:将 JSON Schema 的 7 种类型映射为 Python 原生类型。注意 "object"→dict"array"→list,这是嵌套参数用 dict/list 接收的根源。

  • ② 确定类型prop_schema.get("type", "string")"string" 是兜底默认值,防止 Schema 缺少 type 字段时报错。

  • ③ 可选参数:不在 required 列表中的字段,如果 Schema 没有声明 default,则默认为 None,类型包装为 Optional[X](等价于 Union[X, None])。

  • ④ 必填参数Field(description=...) 没有 default 参数,Pydantic 会将该字段标记为必填。

  • ⑤ 动态创建模型create_model 是 Pydantic 的动态建模 API,等价于在运行时定义一个 class XXXInput(BaseModel): ... 类。


mcp_tool_to_langchain:Pydantic 模型 → LangChain StructuredTool

from langchain_core.tools import StructuredTool
from typing import Callable

def mcp_tool_to_langchain(
    mcp_tool_def: dict,
    handler: Callable,
) -> StructuredTool:
    """
    将 MCP 工具定义 + 处理函数 组合成 LangChain StructuredTool。
    """
    name = mcp_tool_def["name"]
    description = mcp_tool_def["description"]
    input_schema = mcp_tool_def.get("inputSchema", {})

    # ① 先把 JSON Schema 转成 Pydantic 模型
    pydantic_model = json_schema_to_pydantic(name, input_schema)

    # ② 把 Pydantic 模型 + 处理函数 组合成 StructuredTool
    langchain_tool = StructuredTool.from_function(
        func=handler,           # 实际执行的 Python 函数
        name=name,              # 工具名称(LLM 调用时使用)
        description=description,# 工具描述(LLM 决定何时调用)
        args_schema=pydantic_model,  # 参数验证模型
    )

    return langchain_tool

StructuredTool.from_function 做了什么?

它将一个普通 Python 函数包装成 LangChain 的工具对象,主要负责:

  1. 根据 args_schema(Pydantic 模型)验证 LLM 传来的参数
  2. 将验证后的参数解包,调用 func(即处理函数)
  3. 将返回值转换为字符串(如果还不是字符串的话)

load_mcp_tools:批量注册工具

def load_mcp_tools(
    tool_definitions: list[dict],
    handlers: dict[str, Callable],
) -> list[StructuredTool]:
    """批量将 MCP 工具定义列表转换为 LangChain 工具列表。"""
    langchain_tools = []
    for tool_def in tool_definitions:
        name = tool_def["name"]
        if name in handlers:
            lc_tool = mcp_tool_to_langchain(tool_def, handlers[name])
            langchain_tools.append(lc_tool)
            print(f"  ✅ 已加载 MCP 工具:{name}")
        else:
            print(f"  ⚠️  未找到工具实现:{name}")
    return langchain_tools

使用方式:

# 工具定义列表(包含所有工具,包括复杂嵌套参数的 create_order)
MCP_TOOL_DEFINITIONS = [GET_WEATHER_TOOL, SEARCH_NEWS_TOOL, RUN_PYTHON_TOOL, CREATE_ORDER_TOOL]

# 工具实现映射(名称 → 函数)
MCP_HANDLERS = {
    "get_weather":  _get_weather_impl,
    "search_news":  _search_news_impl,
    "run_python":   _run_python_impl,
    "create_order": _create_order_impl,
}

# 一行完成批量注册
tools = load_mcp_tools(MCP_TOOL_DEFINITIONS, MCP_HANDLERS)

2.3 为什么需要 Pydantic 中间层?

直接问题:为什么不把 JSON Schema 直接传给 LangChain,而要多一个 Pydantic 转换步骤?

原因有三:

① LangChain 的工具接口要求 Pydantic

StructuredToolargs_schema 参数类型是 Type[BaseModel],这是 LangChain 的设计规范。不用 Pydantic,就无法接入 StructuredTool

② Pydantic 提供自动类型验证和转换

LLM 返回的工具参数是 JSON(字符串),Pydantic 负责:

  • 字符串 "3" → 整数 3(类型强制转换)
  • 缺少可选参数时 → 自动填入默认值
  • 类型不匹配时 → 抛出 ValidationError(防止脏数据进入处理函数)

③ 自动生成 OpenAI Function Calling 格式

llm.bind_tools(tools) 会调用每个工具的 args_schema.model_json_schema(),生成 OpenAI 格式的 function spec,传给模型。Pydantic 模型天生支持 .model_json_schema(),JSON Schema 字典则不行。


3. 构建完整的 MCP Agent

3.1 绑定工具到 LLM

from langchain_core.messages import HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI

def demo_mcp_agent(llm: ChatOpenAI, tools: list):
    # ① 绑定工具:告诉 LLM "你有这些工具可以用"
    llm_with_tools = llm.bind_tools(tools)

    # ② 构建工具名称 → 工具对象的映射,用于快速查找执行
    tool_map = {t.name: t for t in tools}

llm.bind_tools(tools) 做了什么:

  • 将每个 StructuredTool 的名称、描述、参数 Schema 序列化为 OpenAI function spec
  • 返回一个新的 LLM 对象,每次 invoke 时自动携带工具描述

3.2 消息循环:检测工具调用 → 执行 → 反馈

def run_agent(question: str):
    """运行 Agent 处理一个问题。"""
    print(f"问题:{question}")

    messages = [HumanMessage(content=question)]

    while True:
        # ① 调用 LLM(携带工具描述)
        response = llm_with_tools.invoke(messages)
        messages.append(response)

        # ② 检查 LLM 是否要调用工具
        if not response.tool_calls:
            # 没有工具调用 → LLM 直接给出最终回答,退出循环
            print(f"✅ 最终回答:{response.content}")
            break

        # ③ 执行所有工具调用
        tool_messages = []
        for tc in response.tool_calls:
            print(f"  🔧 调用工具:{tc['name']},参数:{tc['args']}")

            # 通过 tool_map 找到对应工具并执行
            result = tool_map[tc["name"]].invoke(tc["args"])

            print(f"     结果:{str(result)[:100]}")

            # ④ 封装工具结果为 ToolMessage
            tool_messages.append(
                ToolMessage(content=str(result), tool_call_id=tc["id"])
            )

        # ⑤ 将工具结果加入消息历史,继续下一轮循环
        messages.extend(tool_messages)

循环逻辑可视化:

用户提问
   ↓
[HumanMessage]
   ↓
LLM 推理 → 有工具调用?
   ├─ 否 → 输出 response.content → 结束
   └─ 是 → 执行工具 → 得到结果
                ↓
         [ToolMessage(result)]
                ↓
         加入消息历史
                ↓
         LLM 再次推理(已知工具结果)
                ↓
         (重复直到 LLM 不再调用工具)

3.3 ToolMessage 的作用

ToolMessage(content=str(result), tool_call_id=tc["id"])

ToolMessage 是 LangChain 中专门表示"工具执行结果"的消息类型,有两个关键属性:

属性说明
content工具的返回值(必须是字符串)
tool_call_id对应的工具调用 ID(与 response.tool_calls[i]["id"] 匹配)

为什么需要 tool_call_id?因为 LLM 可能同时调用多个工具(如"查北京天气和上海天气"),tool_call_id 用于将每个结果与对应的调用请求关联起来,防止混淆。


3.4 完整示例和预期输出

将以上代码整合,完整测试:

# 测试三个场景
run_agent("北京今天天气怎么样?")
run_agent("搜索关于人工智能的最新新闻(3条)")
run_agent("计算 (123 + 456) * 2 的结果,以及上海的天气")

预期控制台输出:

==================================================
问题:北京今天天气怎么样?
==================================================
  🔧 调用工具:get_weather,参数:{'city': '北京'}
     结果:北京: 晴天, 22°C, 湿度45%

✅ 最终回答:北京今天天气晴朗,气温22°C,湿度45%,适合外出活动。

==================================================
问题:搜索关于人工智能的最新新闻(3条)
==================================================
  🔧 调用工具:search_news,参数:{'query': '人工智能', 'limit': 3}
     结果:1. GPT-5 预计将在2025年发布,性能大幅提升
2. 国内大模型竞争加剧,百炼、文心、豆包激烈角逐
3. AI 辅助编程工具日益普及,提升开发效率50%

✅ 最终回答:以下是关于人工智能的最新3条新闻:...

==================================================
问题:计算 (123 + 456) * 2 的结果,以及上海的天气
==================================================
  🔧 调用工具:run_python,参数:{'code': '(123 + 456) * 2'}
     结果:执行结果:(123 + 456) * 2 = 1158
  🔧 调用工具:get_weather,参数:{'city': '上海'}
     结果:上海: 多云, 26°C, 湿度70%

✅ 最终回答:(123 + 456) * 2 = 1158。上海今天多云,26°C,湿度70%。

📌 最后一个问题展示了并行工具调用:LLM 识别出需要两个工具,在一次推理中同时发出两个 tool_calls


4. 完整开发流程总结

从零开发一个 MCP 工具并集成到 LangChain,分为以下五步:

步骤 1:定义工具 Schema(JSON Schema 格式)

MY_TOOL = {
    "name": "tool_name",
    "description": "用一句话说清楚这个工具做什么、什么时候用",
    "inputSchema": {
        "type": "object",
        "properties": {
            "param1": {"type": "string", "description": "参数1的含义"},
            "param2": {"type": "integer", "description": "参数2的含义", "default": 10},
        },
        "required": ["param1"],
    },
}

⚠️ 常见错误description 写得太简短(如只写"参数"),导致 LLM 不知道何时调用或如何填参数。务必写清楚工具的使用场景和每个参数的含义。


步骤 2:实现工具处理函数

def my_tool_impl(param1: str, param2: int = 10) -> str:
    # 在函数开头验证输入
    if not param1:
        return "错误:param1 不能为空"

    # 业务逻辑
    result = f"处理结果:{param1},数量:{param2}"
    return result

⚠️ 常见错误:抛出异常(raise ValueError(...))而不是返回错误字符串。工具处理函数应始终返回字符串,异常会导致 Agent 崩溃而非优雅降级。


步骤 3:注册到适配器

# 添加到工具定义列表
MCP_TOOL_DEFINITIONS.append(MY_TOOL)

# 添加到处理函数映射
MCP_HANDLERS["tool_name"] = my_tool_impl

⚠️ 常见错误:Schema 中的 name 字段与 MCP_HANDLERS 的键名不一致,导致 load_mcp_tools 打印 "⚠️ 未找到工具实现",工具被跳过。


步骤 4:集成到 LangChain

# 批量加载所有 MCP 工具
tools = load_mcp_tools(MCP_TOOL_DEFINITIONS, MCP_HANDLERS)

# 绑定到 LLM
llm_with_tools = llm.bind_tools(tools)

⚠️ 常见错误:忘记在修改工具列表后重新调用 load_mcp_toolsbind_tools,LLM 仍使用旧的工具描述。


步骤 5:测试与调试

# 第一步:直接测试处理函数
result = my_tool_impl("测试参数", 5)
print(result)  # 验证逻辑正确性

# 第二步:通过适配器测试(验证 Schema 转换)
tool_obj = mcp_tool_to_langchain(MY_TOOL, my_tool_impl)
result = tool_obj.invoke({"param1": "测试", "param2": 5})
print(result)

# 第三步:通过 LLM 端到端测试
messages = [HumanMessage(content="使用 my_tool 处理'你好'")]
response = llm_with_tools.invoke(messages)
print(response)

⚠️ 常见错误:跳过前两步直接做第三步,出错时难以定位是 Schema 问题、逻辑问题还是 LLM 调用问题。分步测试是最高效的调试方式。


5. 最佳实践

5.1 工具 Schema 设计原则

原则一:description 要让 LLM 能理解

# ❌ 不好:太模糊
"description": "天气工具"

# ✅ 好:说清楚功能、参数含义、使用场景
"description": "获取指定城市的实时天气,包括温度、湿度和天气状况。适用于需要了解某地当前气候的场景。"

原则二:required 字段要谨慎设计

把真正必须的字段放入 required,能给默认值的参数都设 default。过多的必填参数会让 LLM 调用失败率上升(LLM 可能忘记填某个参数)。

# 城市是必须的,温度单位有合理默认值
"required": ["city"]  # unit 不放入 required,有 default: "celsius"

原则三:用 enum 约束取值范围

对于有限取值的参数,始终使用 enum 明确列举合法值:

"unit": {
    "type": "string",
    "enum": ["celsius", "fahrenheit"],  # LLM 只能选这两个值
    "default": "celsius"
}

5.2 嵌套参数的额外建议

建议一:为每个嵌套字段都加 description

LLM 需要通过 description 理解如何填充嵌套结构。顶层字段有 description 是不够的,内部每个字段也应该有:

"shipping_address": {
    "type": "object",
    "description": "收货地址,需要精确到详细地址",  # 顶层描述
    "properties": {
        "province": {"type": "string", "description": "省份,如广东省"},   # 字段描述
        "city":     {"type": "string", "description": "城市,如深圳市"},
        "detail":   {"type": "string", "description": "详细地址,包含街道和门牌号"},
    }
}

建议二:嵌套层级不超过 3 层

层级越深,LLM 填写准确率越低,调试难度越高。如果结构复杂,考虑将部分嵌套对象拆分为独立的顶层参数,或者拆分为多个工具。

推荐:2 层嵌套(如本文的 create_order)
可接受:3 层嵌套(如 order.items[].variants[])
避免:4 层以上嵌套

建议三:数组参数要说明元素结构

itemsdescription 应该描述数组元素的期望格式:

"items": {
    "type": "array",
    "description": "购买商品列表,每个元素包含商品ID、数量和单价,至少一件",
    "items": { ... }
}

5.3 错误处理

始终返回字符串,不要抛出异常:

def my_tool_impl(param: str) -> str:
    # ✅ 在函数开头验证,返回错误字符串
    if not param:
        return "错误:param 不能为空"

    try:
        result = do_something(param)
        return str(result)
    except Exception as e:
        # ✅ 捕获所有异常,转为字符串返回
        return f"工具执行失败:{str(e)}"
        # ❌ 不要 raise e —— 这会导致 Agent 崩溃

LangChain 的消息循环期望 ToolMessage.content 是字符串。如果工具函数抛出异常,tool_map[name].invoke(args) 会中断,整个 Agent 停止运行。


5.4 测试建议

按以下顺序测试,由简单到复杂,快速定位问题层:

① 直接调用 Python 函数(验证业务逻辑)
        ↓
② tool_obj.invoke({...})(验证 Schema 转换和参数解析)
        ↓
③ 完整 Agent 运行(验证 LLM 调用决策)

每一层都通过,再测试下一层。出问题时可以精确定位:是逻辑层、适配层还是 LLM 层的问题。


6. 与传统 Function Calling 的对比

特性传统 Function CallingMCP
跨框架复用❌ 需要为每个框架重写工具定义✅ 写一次 JSON Schema,到处用
工具定义格式各框架不同(OpenAI / LangChain / 百炼各有差异)统一 JSON Schema 标准
生产部署工具内嵌在应用代码中工具作为独立进程/服务部署
标准化程度低(厂商私有格式)高(开放标准,Anthropic 主导)
工具热更新需要重启整个应用MCP Server 独立更新,Client 无感知
工具发现硬编码在应用中通过 tools/list 动态发现
多语言支持通常限于宿主语言任何语言都可以实现 MCP Server

什么时候用 MCP,什么时候用传统 Function Calling?

  • 用 MCP:工具需要跨多个 AI 框架/模型复用;工具需要独立部署和维护;团队规模较大,工具由不同人维护。
  • 用传统 Function Calling:单一框架的小项目;快速原型验证;工具逻辑简单且不需要复用。

小结

本文系统性地覆盖了 MCP 进阶实战的核心内容:

主题要点
Schema 设计从简单基础类型 → 可选参数/默认值 → 深度嵌套对象/数组
适配器原理json_schema_to_pydantic 逐行解析;理解 type_mappingOptional 包装
Agent 构建bind_tools → 消息循环 → ToolMessage 反馈
开发流程5 步法 + 每步的常见错误提示
最佳实践Schema 设计、嵌套参数、错误处理、分层测试

掌握这些内容后,你可以设计任意复杂度的 MCP 工具,并将其无缝集成到 LangChain 应用中。下一步建议:直接运行 02_mcp_with_langchain.py,观察 create_order 工具如何通过适配器加载,以及 LLM 如何处理嵌套参数的调用。

📌 下一章预告:工具是 AI 的"手",但如果工具太多、太分散,管理起来很麻烦。第09章学 AI Skills 技能系统,用更高层次的抽象管理和复用 AI 能力。

AI入门开发系列文章合集

作者:阿聪谈架构
公众号:阿聪谈架构(分享后端架构 / AI / Java 技术文章)
相关代码关注公众号:【阿聪谈架构】 回复:AI专栏代码