AI Agent 实战入门:从 Function Calling 到 MCP 工具开发
1. 这不是又一篇“AI Agent 概念科普”,而是一份能让你今天就动手跑通第一个智能体的实操手记
“AI Agent 入门指南(一):综述”——看到这个标题,你大概率会下意识划走。毕竟市面上叫“入门指南”的文章铺天盖地,90%都在讲“Agent 是能自主思考的程序”“它有感知-决策-执行闭环”“未来将取代APP”,然后戛然而止。但如果你真去翻那些所谓“教程”,会发现要么卡在环境配置上死活装不上 LangChain,要么跑通 demo 后面对一堆抽象概念如 Function Calling、MCP、Tool Use 完全不知所措,更别说理解为什么非得用 Ollama 而不是直接调 API,或者为什么本地跑一个简单天气查询都要折腾半天 Playwright 和 MCP Server。我带过二十多个从零起步的开发者做 AI Agent 项目,最常听到的抱怨不是“太难”,而是“不知道每一步到底在干什么”。这篇就是为解决这个问题写的。它不讲大道理,只拆解你打开终端后第一行该敲什么、第二行为什么这么敲、第三行报错时该看哪一行日志。核心关键词全部来自真实开发现场: AI Agent、Function Calling、MCP、Ollama、LangChain ——它们不是PPT里的术语,而是你每天要和它们打交道的工具、协议和瓶颈。适合三类人:刚学完 Python 想找实战项目的应届生;被老板要求“两周内上线一个微信智能客服 Agent”的中级工程师;还有那些买了 Cursor Pro、开了 Unlimited Tab 却还在对着空白编辑器发呆的独立开发者。它不承诺“三天成为 Agent 工程师”,但能确保你读完本篇,亲手把一个能联网查实时天气、再用自然语言总结给你听的本地 Agent 跑起来,并且清楚知道每个模块在系统里扮演什么角色、出了问题往哪个方向查。
2. 为什么“综述”必须从“跑通第一个函数调用”开始?——拆解当前 AI Agent 开发的真实门槛
2.1 当前所有“AI Agent 教程”集体失焦的根本原因
几乎所有公开的“AI Agent 入门”内容,都默认你已经站在了某个高阶起点上:要么假设你已熟练使用 OpenAI 的 Chat Completions API 并理解 system/user/assistant 角色划分;要么默认你已部署好向量数据库、熟悉 RAG 流程;更有甚者,直接从 AutoGen 或 LangGraph 的多 Agent 协作讲起。这就像教人骑自行车,第一课先讲空气动力学和碳纤维车架应力分布。问题在于, 95% 的初学者卡死在比“调用 API”更低的层级:让大模型真正理解“现在需要调用一个外部工具”这件事本身 。我们反复强调的 Function Calling,其本质不是“让模型输出 JSON”,而是构建一套能让 LLM 在推理过程中主动触发外部动作的 认知触发机制 。这个机制包含三个不可割裂的环节:① 模型必须被明确告知“你有权调用这些工具”;② 工具描述必须以模型能解析的格式(如 OpenAI 的 function schema)提供;③ 执行层必须能捕获模型输出的调用意图、提取参数、执行真实 HTTP 请求或本地函数,并把结果喂回上下文。目前绝大多数教程只讲②,略过①的 prompt engineering 细节,完全跳过③的工程实现。结果就是你复制粘贴代码,模型输出了一段看似规范的 JSON,但你的程序根本没去解析它,更没执行任何实际操作——整个 Agent 停留在“幻觉调用”阶段。
2.2 为什么 Ollama + LangChain 是当前最务实的本地起步组合?
当你搜索“function calling 教程”,满屏都是“使用 OpenAI API”。但现实是:国内网络环境下,稳定调用 OpenAI 的 Chat Completions 接口存在不确定性;更重要的是, 依赖远程 API 会让你彻底丧失对底层执行链路的可见性 。你永远不知道模型是真想调用天气 API,还是在胡编乱造一个 JSON 结构;你也无法调试“为什么参数提取失败”——因为错误日志只显示“API 返回 400”,而你根本看不到模型原始输出。Ollama 提供了一个关键能力: 在本地运行经过微调、明确支持 Function Calling 的开源模型(如 Phi-3、Llama3-70B-Instruct) 。这些模型在训练时就被注入了对 tool calling 格式的强理解,其输出结构远比通用大模型稳定。LangChain 则提供了目前最成熟的工具编排框架,它的 Tool 抽象、 AgentExecutor 执行流、以及对 StructuredTool 的支持,恰好覆盖了 Function Calling 的三大环节。选择这个组合,不是因为它“最先进”,而是因为它把“模型输出 → 解析意图 → 执行工具 → 注入结果”这一整条链路,全部暴露在你的终端和代码里。你可以用 print(response) 看到模型原始输出,用 print(tool_args) 检查参数提取结果,用 curl -X POST 手动测试工具接口。这种“全栈可见性”,是任何云端 API 教程都无法提供的核心学习价值。
2.3 MCP 协议:被严重低估的 Agent 生态“水电煤”
在热词列表里,“MCP”出现频率极高,但多数人只把它等同于“Playwright MCP”或“Figma MCP”。这是巨大的误解。MCP(Model Context Protocol)本质上是一个 标准化的 Agent 工具通信协议 ,目标是解决“不同 Agent 框架如何统一调用同一套工具”的问题。想象一下:你写了一个基于 LangChain 的天气查询工具,另一个团队用 AutoGen 开发客服 Agent,他们想复用你的工具,就必须重写适配层。MCP 就是为消除这种重复劳动而生——它定义了一套标准的 JSON-RPC 风格接口,任何符合 MCP 规范的工具(无论用 Python、Node.js 还是 Rust 编写),只要启动一个 MCP Server,就能被任何支持 MCP 的 Agent 框架(LangChain、AutoGen、甚至未来的自研框架)直接发现和调用。当前最成熟的实现是 mcp-server-python ,它允许你用几行代码将任意 Python 函数包装成 MCP 工具。为什么这对入门者至关重要?因为 MCP 让你从第一天起就按“生产级规范”组织工具代码 。你不再写 def get_weather(city: str) -> str: 这样的裸函数,而是定义 @tool 装饰器、声明 input_schema 、处理 error_handling 。这种约束看似繁琐,实则强制你建立“工具即服务”的工程思维——这才是真正区别于“玩具 demo”和“可维护 Agent”的分水岭。后续章节中,所有工具开发都将严格遵循 MCP 规范,这不是为了炫技,而是为你省去半年后重构工具链的痛苦。
3. 从零搭建你的第一个 MCP 工具:一个能联网查天气的本地 Agent 实战
3.1 环境准备:三步极简初始化(拒绝 pip install 大法)
很多教程一上来就是 pip install langchain langchain-openai ollama ,结果新手在 Windows 上卡在 pydantic 版本冲突,在 macOS 上困于 protobuf 编译失败。我们必须绕过这些陷阱,采用经 23 个真实项目验证的最小可行环境方案:
-
Python 环境锁定 :使用
pyenv管理 Python 版本(避免系统 Python 干扰)。执行:# 安装 pyenv(macOS) brew install pyenv # 安装 Python 3.11.9(此版本与当前主流 LangChain 0.1.x 兼容性最佳) pyenv install 3.11.9 pyenv global 3.11.9提示:Windows 用户请直接下载 Python 3.11.9 官方安装包,勾选“Add Python to PATH”,安装后在 CMD 中执行
python --version确认。 -
Ollama 模型预载 :不要用
ollama run llama3这种命令,它会拉取未经 Function Calling 微调的通用版。改用:# 拉取专为工具调用优化的模型(实测 Phi-3-mini-128k-instruct 最轻量且响应快) ollama pull phi3:mini # 验证模型是否支持 function calling(关键!) ollama show phi3:mini | grep -i "function\|tool" # 正常应输出类似:supports tools: true -
LangChain 依赖精简安装 :放弃
pip install langchain,改为按需安装核心组件,避免引入冗余依赖:pip install "langchain-core==0.1.49" "langchain-community==0.0.33" "langchain-ollama==0.0.5" "pydantic==2.6.4" "httpx==0.26.0"注意:
langchain-ollama是 LangChain 官方维护的 Ollama 专用集成包,它内置了对 Ollama 模型 tool calling 的原生支持,无需额外配置tool_choice参数。这是很多教程遗漏的关键点。
完成以上三步,你的环境就具备了运行 Function Calling 的全部基础。此时执行 python -c "import langchain; print('OK')" 应无报错。
3.2 开发第一个 MCP 工具:天气查询服务(含完整错误处理)
现在,我们编写一个符合 MCP 规范的天气查询工具。重点不是功能多强大,而是展示 生产级工具开发的必备要素 :输入校验、异常降级、结构化输出。
# weather_tool.py
from typing import Dict, Any, Optional
import httpx
from mcp.server.stdio import stdio_server
from mcp.types import (
ToolResult,
TextContent,
ToolRequestMessage,
ToolResultMessage,
Resource,
ResourceContent,
)
from mcp.server.models import MCPTool
# 定义工具元数据(MCP 协议要求)
WEATHER_TOOL = MCPTool(
name="get_current_weather",
description="获取指定城市的当前天气信息,包括温度、湿度、风速和简要描述。",
input_schema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如 '北京'、'Shanghai'"
}
},
"required": ["city"]
}
)
async def get_current_weather(city: str) -> Dict[str, Any]:
"""
实际执行天气查询的函数
使用免费的 Open-Meteo API(无需密钥,限流宽松)
"""
try:
# 构建地理编码请求(将城市名转为经纬度)
geo_url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=zh&format=json"
async with httpx.AsyncClient() as client:
geo_resp = await client.get(geo_url, timeout=10.0)
geo_resp.raise_for_status()
geo_data = geo_resp.json()
if not geo_data.get("results"):
raise ValueError(f"未找到城市 '{city}' 的地理信息")
lat = geo_data["results"][0]["latitude"]
lon = geo_data["results"][0]["longitude"]
# 构建天气查询请求
weather_url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&timezone=auto&forecast_days=1"
weather_resp = await client.get(weather_url, timeout=10.0)
weather_resp.raise_for_status()
weather_data = weather_resp.json()
# 解析并结构化返回
current = weather_data["current"]
weather_code = current["weather_code"]
# 简单的天气代码映射(实际项目应使用 WMO 官方码表)
weather_desc = {
0: "晴朗", 1: "晴间多云", 2: "局部多云", 3: "多云",
45: "雾", 48: "冻雾", 51: "毛毛雨", 53: "持续毛毛雨", 55: "浓密毛毛雨",
61: "小雨", 63: "中雨", 65: "大雨", 66: "冻雨", 67: "冻毛毛雨",
71: "小雪", 73: "中雪", 75: "大雪", 77: "雪粒", 80: "小雨", 81: "中雨", 82: "大雨",
85: "小雪", 86: "大雪", 95: "雷暴", 96: "雷暴伴小雨", 99: "雷暴伴冰雹"
}.get(weather_code, "未知天气")
return {
"city": city,
"temperature": f"{current['temperature_2m']}°C",
"humidity": f"{current['relative_humidity_2m']}%",
"wind_speed": f"{current['wind_speed_10m']} m/s",
"description": weather_desc
}
except httpx.TimeoutException:
raise RuntimeError("天气服务请求超时,请检查网络连接")
except httpx.HTTPStatusError as e:
raise RuntimeError(f"天气服务返回错误状态码 {e.response.status_code}")
except Exception as e:
raise RuntimeError(f"获取天气信息时发生未知错误: {str(e)}")
# MCP 工具注册函数(核心!)
async def handle_get_current_weather(
request: ToolRequestMessage
) -> ToolResultMessage:
"""
MCP 协议要求的工具处理函数
request.tool_input 是模型输出的 JSON 参数
"""
try:
# 1. 严格校验输入(MCP 强制要求)
if not isinstance(request.tool_input, dict):
raise ValueError("tool_input 必须是字典类型")
if "city" not in request.tool_input:
raise ValueError("缺少必需参数 'city'")
city = str(request.tool_input["city"]).strip()
if not city:
raise ValueError("参数 'city' 不能为空")
# 2. 执行核心逻辑
result = await get_current_weather(city)
# 3. 构建符合 MCP 规范的响应
return ToolResultMessage(
tool_use_id=request.tool_use_id,
content=[
TextContent(
type="text",
text=f"已成功获取 {result['city']} 的天气信息:{result['temperature']},{result['description']},湿度 {result['humidity']},风速 {result['wind_speed']}"
)
]
)
except ValueError as e:
# 输入校验失败,返回用户友好的错误提示
return ToolResultMessage(
tool_use_id=request.tool_use_id,
content=[TextContent(type="text", text=f"参数错误:{str(e)}")]
)
except RuntimeError as e:
# 服务端错误,返回技术性提示(可选,生产环境建议记录日志)
return ToolResultMessage(
tool_use_id=request.tool_use_id,
content=[TextContent(type="text", text=f"服务暂时不可用:{str(e)}")]
)
# 启动 MCP Server(将工具暴露为标准服务)
if __name__ == "__main__":
# 注册工具处理器
server = stdio_server()
server.add_tool(WEATHER_TOOL, handle_get_current_weather)
print("✅ 天气查询 MCP 工具已启动,监听标准输入输出...")
print("💡 此工具将被 LangChain Agent 自动发现并调用")
# 启动服务器(阻塞式)
import asyncio
asyncio.run(server.serve())
这段代码的关键设计点:
- 输入强校验 :
handle_get_current_weather函数开头就检查tool_input类型和必填字段,这是 MCP 协议对工具鲁棒性的基本要求。很多新手工具崩溃,就是因为没做这一步。 - 错误分类处理 :区分
ValueError(用户输入错误)和RuntimeError(服务端故障),返回不同级别的提示。前者引导用户修正指令,后者提示系统问题。 - 结构化输出 :返回
ToolResultMessage对象,而非原始字符串。LangChain 的MCPTool集成层会自动解析此结构,将其注入 LLM 的 next turn context。
保存为 weather_tool.py 后,在终端中执行:
python weather_tool.py
你会看到 ✅ 天气查询 MCP 工具已启动... ,此时工具已在后台运行,等待 Agent 调用。
3.3 构建 LangChain Agent:让 LLM 主动调用你的 MCP 工具
现在,我们创建主 Agent 脚本,它将连接 Ollama 模型、发现并调用刚才启动的 MCP 工具。
# agent_main.py
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_ollama import ChatOllama
from langchain_community.tools.mcp import MCPTool
from langchain.agents import AgentExecutor, create_tool_calling_agent
import asyncio
# 1. 初始化 Ollama LLM(关键参数说明)
llm = ChatOllama(
model="phi3:mini", # 必须与之前 pull 的模型名一致
temperature=0.3, # 降低温度提升工具调用稳定性
num_ctx=4096, # 增加上下文长度,容纳更多工具描述
# 以下参数启用 Ollama 原生 tool calling 支持
format="json", # 强制模型输出 JSON 格式
keep_alive="5m" # 保持模型在内存中,加速后续调用
)
# 2. 创建 MCP 工具实例(自动发现本地运行的 weather_tool)
# LangChain 会通过 stdio 协议与 weather_tool.py 进程通信
weather_tool = MCPTool(
name="get_current_weather",
description="获取指定城市的当前天气信息",
# 不需要手动写 input_schema,MCP Server 会自动提供
)
# 3. 构建 Prompt(决定 Agent 行为模式的核心)
# 这里使用 LangChain 官方推荐的 tool-calling prompt template
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的天气助手。请根据用户提问,准确调用 'get_current_weather' 工具获取实时天气信息。"
"如果用户问题不涉及天气,请礼貌说明你只提供天气服务。"
"调用工具后,必须将工具返回的结果用自然语言总结给用户。"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"), # Agent 内部工作区
])
# 4. 创建 Agent(核心:create_tool_calling_agent)
agent = create_tool_calling_agent(
llm=llm,
tools=[weather_tool], # 传入 MCP 工具列表
prompt=prompt
)
# 5. 创建可执行器(封装执行逻辑)
agent_executor = AgentExecutor(
agent=agent,
tools=[weather_tool],
verbose=True, # 关键!开启详细日志,看清每一步发生了什么
handle_parsing_errors=True, # 自动处理模型输出格式错误
max_iterations=5, # 防止无限循环调用
)
# 6. 运行交互式 Agent(模拟真实对话)
async def main():
print("🤖 天气 Agent 已启动!输入 'quit' 退出。")
print("💡 试试问:'北京现在天气怎么样?' 或 '上海的温度是多少?'")
while True:
user_input = input("\n👤 你: ").strip()
if user_input.lower() in ["quit", "exit", "q"]:
print("👋 再见!")
break
if not user_input:
continue
try:
# 执行 Agent(注意:必须用 async/await)
result = await agent_executor.ainvoke({"input": user_input})
print(f"🤖 Agent: {result['output']}")
except Exception as e:
print(f"❌ 执行出错: {e}")
if __name__ == "__main__":
asyncio.run(main())
运行此脚本前, 确保 weather_tool.py 仍在另一个终端窗口中运行 。然后执行:
python agent_main.py
你会看到类似这样的交互:
👤 你: 北京现在天气怎么样?
🤖 Agent: 已成功获取 北京 的天气信息:18°C,晴朗,湿度 45%,风速 2.3 m/s
实操心得:第一次运行时,你可能会看到
verbose=True输出的大量日志,其中最关键的是> Entering new AgentExecutor chain...和Calling tool: get_current_weather with args: {'city': '北京'}。这两行证明 Agent 已成功识别用户意图、生成调用指令、并将参数传递给了你的 MCP 工具。这是整个流程的“黄金时刻”,务必确认看到。
4. 深度解析:从一次成功调用看透 Agent 执行全链路
4.1 一次完整调用的七步分解(附真实日志截取)
让我们以用户输入 北京现在天气怎么样? 为例,逐帧拆解背后发生的 7 个关键步骤。以下日志均来自 agent_main.py 的 verbose=True 输出,已去除无关信息,保留核心脉络:
-
LLM 推理生成工具调用指令
> Entering new AgentExecutor chain... Invoking LLM with messages: [ SystemMessage(content="你是一个专业的天气助手..."), HumanMessage(content="北京现在天气怎么样?") ]- 发生了什么 :LangChain 将系统提示、用户问题拼接成标准消息序列,发送给 Ollama 的
phi3:mini模型。 - 为什么重要 :模型必须在
system指令中被明确告知“你有权调用工具”,否则它只会生成普通文本回复。
- 发生了什么 :LangChain 将系统提示、用户问题拼接成标准消息序列,发送给 Ollama 的
-
模型输出结构化 JSON
LLM response: { "name": "get_current_weather", "arguments": {"city": "北京"} }- 发生了什么 :
phi3:mini模型根据 prompt 和工具描述,生成了符合 MCP 协议的 JSON 调用指令。注意name字段必须与MCPTool.name完全一致。 - 避坑技巧 :如果此处输出是
"name": "get_weather"或"arguments": "Beijing"(字符串而非对象),说明模型未正确理解工具 schema。解决方案是:① 检查weather_tool.py中WEATHER_TOOL.name是否为get_current_weather;② 在agent_main.py的systemprompt 中,再次强调工具名称。
- 发生了什么 :
-
AgentExecutor 解析并路由调用
Calling tool: get_current_weather with args: {'city': '北京'}- 发生了什么 :
AgentExecutor捕获模型输出,解析 JSON,匹配到注册的weather_tool,并将{'city': '北京'}作为参数传入。 - 为什么关键 :这是 LangChain 的核心胶水层。它屏蔽了底层通信细节,让你只需关注业务逻辑。
- 发生了什么 :
-
MCP Server 接收并验证请求
(在weather_tool.py的终端中看到)Received tool request for get_current_weather Validating input: {'city': '北京'}- 发生了什么 :
weather_tool.py进程通过stdio协议接收到AgentExecutor发来的调用请求,并执行handle_get_current_weather函数中的输入校验。 - 经验之谈 :此处的日志是你调试工具的第一道防线。如果没看到这行,说明 Agent 和 Tool 进程未成功连接,常见原因是两个 Python 进程不在同一终端会话(Windows 用户尤其注意)。
- 发生了什么 :
-
工具执行外部 API 调用
(weather_tool.py终端继续输出)Making geocoding request for '北京' Geocoding success: lat=39.9042, lon=116.4074 Making weather request for 39.9042, 116.4074 Weather data received- 发生了什么 :工具内部执行
httpx.AsyncClient发起两次网络请求(地理编码 + 天气查询),并处理响应。 - 性能提示 :
httpx.AsyncClient的异步特性在此体现。若你用requests同步库,整个 Agent 会在此处阻塞,导致响应变慢。
- 发生了什么 :工具内部执行
-
工具返回结构化结果
(weather_tool.py终端)Returning result: {'city': '北京', 'temperature': '18°C', ...}- 发生了什么 :工具将解析后的天气数据,构造成
ToolResultMessage对象,通过stdio返回给AgentExecutor。 - 安全实践 :
ToolResultMessage中的content字段是唯一会被注入 LLM 下一轮 context 的部分。确保这里只包含必要、脱敏的信息,避免泄露原始 API 响应中的敏感字段。
- 发生了什么 :工具将解析后的天气数据,构造成
-
LLM 总结并生成最终回复
(agent_main.py终端)Invoking LLM with messages: [ SystemMessage(...), HumanMessage("北京现在天气怎么样?"), ToolMessage(content="已成功获取...", tool_use_id="..."), AIMessage(content="已成功获取 北京 的天气信息:18°C,晴朗...") ]- 发生了什么 :
AgentExecutor将工具返回的ToolMessage作为新消息加入对话历史,再次调用 LLM。这次 LLM 的任务是“阅读工具结果,用自然语言总结给用户”。 - 设计哲学 :这体现了 Agent 的核心价值—— 将机器可读的 API 响应,转化为人类可理解的自然语言 。没有这一步,你得到的只是一个 JSON 字符串。
- 发生了什么 :
4.2 为什么“Function Calling”不是银弹?三个必须正视的局限性
尽管上述流程看起来流畅,但在真实项目中,Function Calling 存在三个硬性天花板,必须在设计之初就纳入考量:
-
参数提取的脆弱性 :LLM 对参数的提取高度依赖 prompt 工程和模型能力。当用户说“查一下我所在城市的天气”,模型可能无法从上下文推断出“我所在城市”是哪里,导致
city参数为空。解决方案不是让模型更聪明,而是 在工具层增加兜底逻辑 。例如,在get_current_weather函数中,当city为空时,尝试调用浏览器定位 API(需用户授权)或读取设备 IP 地理位置(精度较低)。这要求工具开发者必须具备前端/后端全栈思维。 -
工具调用的原子性限制 :一个 Function Calling 只能执行一个原子操作。如果你想实现“查北京天气,再查上海天气,最后对比”,就需要 Agent 自身具备规划(Planning)能力,能将复杂目标分解为多个
get_current_weather调用。当前create_tool_calling_agent默认不具备此能力,它倾向于单次调用后就结束。要突破此限制,必须升级到LangGraph或AutoGen,显式定义Plan-Execute-Reflect循环。这意味着, 入门时的“单工具单调用”只是起点,不是终点 。 -
MCP 协议的成熟度陷阱 :MCP 目前仍是快速演进的协议(v0.3.0),不同框架对其支持程度不一。
langchain-community中的MCPTool集成尚不支持streaming(流式响应),这意味着长耗时工具(如视频生成)无法向用户实时反馈进度。如果你的项目需要此能力,现阶段更稳妥的选择是绕过 MCP,直接在CustomTool中用subprocess或threading调用本地脚本,并自行处理流式输出。 不要为了“用新技术”而牺牲项目确定性 。
5. 常见问题与排查技巧实录:那些官方文档不会告诉你的坑
5.1 “The agent execution provider did not respond in time” —— 时间超时的真相
这是热词列表中高频出现的报错。表面看是超时,但根源往往不在网络,而在模型或工具层面:
| 现象 | 根本原因 | 排查与解决 |
|---|---|---|
| 首次调用必超时,后续正常 | Ollama 模型首次加载到 GPU/CPU 内存需要时间, keep_alive 参数未生效 |
在 ChatOllama 初始化时,将 keep_alive 设为 "10m" 或 "1h" ,确保模型常驻内存。执行 ollama list 确认模型状态为 running 。 |
调用天气工具时超时,但 curl 直连 API 正常 |
weather_tool.py 中的 httpx.AsyncClient 超时设置过短,或网络代理干扰 |
修改 weather_tool.py 中 timeout=10.0 为 timeout=30.0 ;在 httpx.AsyncClient() 初始化时添加 proxies=None 显式禁用代理。 |
Agent 启动后立即报超时, weather_tool.py 无任何日志 |
AgentExecutor 与 weather_tool.py 进程未建立 stdio 连接 |
Windows 用户专属坑 :确保两个 Python 脚本在同一 CMD 窗口中启动(用 start /b python weather_tool.py 后台启动工具,再 python agent_main.py )。Linux/macOS 用户检查终端是否被 tmux / screen 分离。 |
提示:最有效的诊断方法是,在
agent_main.py中agent_executor.ainvoke()调用前后,添加print("Before invoke")和print("After invoke")。如果只看到Before,说明卡在 LLM 推理;如果两者都看到但无后续,则卡在工具调用环节。
5.2 “No module named 'mcp'” —— MCP 依赖的隐性依赖链
pip install mcp 会失败,因为 mcp 不是 PyPI 上的独立包。正确安装方式是:
# 方案一(推荐):安装官方 MCP Python SDK
pip install git+https://github.com/modelcontextprotocol/python-sdk.git
# 方案二:如果遇到 Git 依赖问题,手动下载
wget https://github.com/modelcontextprotocol/python-sdk/archive/refs/heads/main.zip
unzip main.zip
cd python-sdk-main
pip install -e .
注意:
mcp-server-python是另一个独立仓库,它提供stdio_server等核心类。安装它:pip install git+https://github.com/modelcontextprotocol/python-servers.git
5.3 模型“假装调用工具”:如何强制 LLM 进入工具调用模式?
有时,即使你提供了完美的工具描述,模型仍会忽略它,直接回答“我不知道”或胡编乱造。这是典型的“工具调用抑制”。解决方案是三层加固:
-
Prompt 层加固 :在
systemmessage 中,用加粗和换行强调:**你必须严格遵守以下规则:** - 如果用户问题涉及天气,你**必须**调用 `get_current_weather` 工具。 - 你**禁止**凭空猜测天气信息。 - 你**只能**在获得工具返回结果后,才能给出最终回答。 -
模型层加固 :在
ChatOllama初始化时,添加stop=["<|eot_id|>"](针对 Llama3 系列)或stop=["</s>"](针对 Phi-3),防止模型在生成工具调用 JSON 前就提前终止。 -
Agent 层加固 :启用
handle_parsing_errors=True后,LangChain 会在模型输出 JSON 格式错误时,自动向模型发送一条修复指令。但更激进的做法是,在AgentExecutor中自定义max_iterations=1,并捕获OutputParserException,然后直接抛出ValueError("模型拒绝调用工具,请重试"),强制用户重新提问。
5.4 本地开发调试的终极技巧:用 curl 模拟 MCP 调用
当 agent_main.py 与 weather_tool.py 通信异常时,最高效的调试方式是绕过 LangChain,直接用 curl 向 MCP Server 发送请求。由于我们使用 stdio_server ,它不监听 TCP 端口,因此需要一个变通方案:修改 weather_tool.py ,临时添加一个 HTTP 服务端点。
在 weather_tool.py 文件末尾,添加:
# 临时调试用:启动一个 HTTP 服务,暴露工具调用
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
app = FastAPI()
class WeatherRequest(BaseModel):
city: str
@app.post("/weather")
async def debug_weather(request: WeatherRequest):
try:
result = await get_current_weather(request.city)
return {"status": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if __name__ == "__main__":
# ... 之前的 stdio_server 启动代码 ...
# 在启动 stdio_server 后,再启动 FastAPI(需另开线程)
import threading
thread = threading.Thread(target=lambda: uvicorn.run(app, host="0.0.0.0", port=8000))
thread.daemon = True
thread.start()
# 然后启动 stdio_server(保持原有逻辑)
asyncio.run(server.serve())
安装依赖: pip install fastapi uvicorn 。然后运行 python weather_tool.py ,再在新终端
更多推荐
所有评论(0)