适合读者:已阅读 MCP 基础篇,理解 Host/Client/Server 三层架构,希望深入掌握工具 Schema 设计和 LangChain 集成实战的开发者。
阅读目标:读完本文,你将能独立设计复杂嵌套 Schema、理解 MCP→LangChain 适配器原理,并构建完整的 MCP Agent。
配套代码:/08_mcp/02_mcp_with_langchain.py(适配器、Agent 核心代码以及create_order嵌套参数示例均已包含在此文件中,可直接运行)
前期回顾
- 第01章:从零开始调用 LLM-入门 Qwen 大模型 API
- 第02章:Prompt 工程 —— 用语言精准指挥 AI
- 第03章:LCEL 链式调用——让 AI 任务像流水线一样运转
- 第04章:AI Tools 工具调用 —— 让 AI 伸出"手"与世界交互
- 第05章:AI Agent 智能体 —— 让 AI 自主决策,循环解决问题
- 第06章:AI RAG 检索增强生成 — 从零到生产(上)
- 第06章:AI RAG 向量库选型与生产实践
- 第07章(上):LangGraph 工作流 —— 为什么需要它,以及如何入门
- 第07章(下):LangGraph 工作流进阶 —— 检查点、人工介入与多 Agent 协作
- 第08章:MCP 模型上下文协议(上)
0. 前置知识快速回顾
在基础篇中,我们掌握了:MCP 是 AI 工具领域的"USB 接口"标准(JSON-RPC 2.0 通信),解决了 N×M 适配问题;核心架构是 Host/Client/Server 三层,提供 Tools/Resources/Prompts 三大组件;工具用 JSON Schema 描述接口,服务器注册工具并处理调用请求。
本文聚焦进阶实战,重点覆盖以下三块:
- 🔑 工具 Schema 设计:从简单参数到深度嵌套对象/数组(以电商订单为例)
- 🔑 适配器原理:
json_schema_to_pydantic和mcp_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" | 参数是字符串类型 |
description | LLM 通过这段描述理解参数含义,写清楚非常重要 |
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 处理函数中分别以 dict 和 list 的形式接收(这由适配器的 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是 LangChainToolCall对象的标准字段名(对应代码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_pydantic 和 mcp_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 的工具对象,主要负责:
- 根据
args_schema(Pydantic 模型)验证 LLM 传来的参数 - 将验证后的参数解包,调用
func(即处理函数) - 将返回值转换为字符串(如果还不是字符串的话)
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
StructuredTool 的 args_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_tools和bind_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 层以上嵌套
建议三:数组参数要说明元素结构
items 的 description 应该描述数组元素的期望格式:
"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 Calling | MCP |
|---|---|---|
| 跨框架复用 | ❌ 需要为每个框架重写工具定义 | ✅ 写一次 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_mapping 和 Optional 包装 |
| Agent 构建 | bind_tools → 消息循环 → ToolMessage 反馈 |
| 开发流程 | 5 步法 + 每步的常见错误提示 |
| 最佳实践 | Schema 设计、嵌套参数、错误处理、分层测试 |
掌握这些内容后,你可以设计任意复杂度的 MCP 工具,并将其无缝集成到 LangChain 应用中。下一步建议:直接运行 02_mcp_with_langchain.py,观察 create_order 工具如何通过适配器加载,以及 LLM 如何处理嵌套参数的调用。
📌 下一章预告:工具是 AI 的"手",但如果工具太多、太分散,管理起来很麻烦。第09章学 AI Skills 技能系统,用更高层次的抽象管理和复用 AI 能力。
作者:阿聪谈架构
公众号:阿聪谈架构(分享后端架构 / AI / Java 技术文章)
相关代码关注公众号:【阿聪谈架构】 回复:AI专栏代码