ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

在智能体中使用MCP工具:从工具发现到多工具协作,我的实战经验

在智能体中使用MCP工具:从工具发现到多工具协作,我的实战经验

在智能体中使用MCP工具:从工具发现到多工具协作,我的实战经验

写在前面

前面几篇文章把MCP的概念、客户端、传输方式都讲了。但有读者说:“这些我都懂了,可真到了把MCP工具塞进Agent里的时候,还是一头雾水。大模型到底是怎么决定调用哪个工具的?工具调用失败了怎么办?一个任务需要好几个工具配合怎么搞?”

这些问题问得非常好。MCP协议本身只解决了"工具怎么接入"的问题,但"工具怎么用好"是另一个层面的事。这篇文章我就结合自己的项目经验,聊聊在智能体中真正使用MCP工具时会遇到的问题,以及我的解决方案。

一、第一步:把MCP工具"翻译"给大模型听

MCP的工具列表是给程序看的,大模型看不懂。你需要做一次"翻译",把MCP的Tool对象转换成大模型的Function Calling格式。

MCP的Tool长这样:

Tool(name="get_weather",description="查询指定城市的当前天气",inputSchema={"type":"object","properties":{"city":{"type":"string","description":"城市名称"}},"required":["city"]})

转换成OpenAI格式的Function Calling是这样:

{"type":"function","function":{"name":"get_weather","description":"查询指定城市的当前天气","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称"}},"required":["city"]}}}

看起来就是改了个壳子对吧?但这里有几个细节要注意:

细节1:工具名要全局唯一。如果你连了多个MCP Server,不同Server可能有同名工具。我一般的做法是给工具名加上Server前缀,比如weather.get_weather、fs.read_file。

细节2:描述要写清楚。大模型是靠description来判断什么时候该调用这个工具的。描述写得模糊,大模型就容易选错。比如"查询天气"就不如"查询指定城市的实时天气信息,包括温度、湿度、天气状况"来得清楚。

细节3:参数描述不能省。每个参数的description都要写,大模型靠这个来判断该传什么值。我见过很多人只写参数名和类型,不写描述,结果大模型经常传错参数。

我写了一个转换函数,自动把MCP工具列表转成大模型能用的格式:

defmcp_tools_to_llm_format(mcp_tools,server_prefix=""):"""把MCP工具列表转换成大模型的Function Calling格式"""llm_tools=[]fortoolinmcp_tools:full_name=f"{server_prefix}.{tool.name}"ifserver_prefixelsetool.name llm_tools.append({"type":"function","function":{"name":full_name,"description":tool.description,"parameters":tool.inputSchema}})returnllm_tools

二、工具选择:大模型是怎么"思考"的

很多人以为大模型选择工具是靠"魔法",其实不是。它的逻辑很简单:

  1. 你把所有可用工具的名称和描述塞进Prompt
  2. 大模型根据用户的问题,匹配最相关的工具
  3. 如果需要调用工具,输出一个结构化的工具调用请求
  4. 你的代码执行这个调用,把结果返回给大模型
  5. 大模型根据工具返回的结果,生成最终回复

这个过程就是所谓的ReAct(Reasoning + Acting)模式——大模型先思考要不要调用工具、调用哪个,然后执行,再根据结果继续思考。

但实际用起来,工具选择经常出错。我总结了几个常见的错误和对策:

错误1:该调用工具的时候不调用,直接瞎编答案。

比如用户问"北京今天天气怎么样",大模型可能直接回答"北京今天晴,25度",而不是调用天气工具。这是因为大模型的训练数据里有大量天气相关的内容,它倾向于"自己回答"。

对策:在System Prompt里明确告诉大模型,“涉及实时信息、外部数据时,必须调用工具,不要凭记忆回答”。同时把工具描述写得更有吸引力,比如加上"这是获取实时天气的唯一途径"。

错误2:调用了错误的工具。

比如用户问"帮我查一下订单状态",大模型可能调用了"查物流"的工具,而不是"查订单"的工具。

对策:工具之间的描述要有明显区分度。如果两个工具功能相近,可以在描述里加上"适用于XX场景,不适用于YY场景"。另外,工具名也要有区分度,query_order和query_logistics就比query1和query2好得多。

错误3:参数传错。

比如该传字符串的传了数字,该传城市名的传了城市代码。

对策:参数的JSON Schema要写详细,包括类型、枚举值、描述。对于有固定取值范围的参数,用enum限定。比如城市参数可以加上"enum": ["北京", "上海", "广州", "深圳"]。

三、完整的工具调用循环

理解了工具选择之后,我们来看完整的调用流程。一个典型的Agent工具调用循环是这样的:

asyncdefagent_run(user_message,mcp_manager,llm_client):messages=[{"role":"system","content":"你是一个智能助手,可以使用工具来回答问题。"},{"role":"user","content":user_message}]# 获取所有MCP工具并转换格式all_tools=awaitmcp_manager.get_all_tools()llm_tools=mcp_tools_to_llm_format(all_tools)whileTrue:# 调用大模型response=awaitllm_client.chat(messages,tools=llm_tools)# 如果大模型没有调用工具,说明它已经有了最终答案ifnotresponse.tool_calls:returnresponse.content# 大模型调用了工具,逐个执行fortool_callinresponse.tool_calls:tool_name=tool_call.function.name arguments=json.loads(tool_call.function.arguments)# 通过MCP执行工具调用try:result=awaitmcp_manager.call_tool(tool_name,arguments)tool_result=result.content[0].textexceptExceptionase:tool_result=f"工具调用失败:{str(e)}"# 把工具调用和结果加入对话历史messages.append({"role":"assistant","content":None,"tool_calls":[tool_call]})messages.append({"role":"tool","tool_call_id":tool_call.id,"content":tool_result})# 继续循环,让大模型根据工具结果决定下一步

这段代码的核心是一个while True循环:

  1. 把对话历史和工具列表发给大模型
  2. 大模型决定是直接回答还是调用工具
  3. 如果调用工具,执行工具调用,把结果加入对话历史
  4. 回到第1步,直到大模型给出最终答案

这个循环看起来简单,但有几个关键点:

关键点1:对话历史要保留工具调用记录。大模型需要知道之前调用了什么工具、返回了什么结果,才能决定下一步。所以每次工具调用的请求和响应都要加入messages。

关键点2:要设置最大循环次数。万一陷入死循环(比如大模型反复调用同一个工具),程序就卡住了。我一般设个上限,比如最多调用10次工具,超过就强制结束并提示用户。

关键点3:工具结果要简洁。工具返回的结果会被塞进Prompt里,如果结果太长,会占用大量token。对于返回大量数据的工具,最好在Server端做摘要,只返回关键信息。

四、多工具协作:一个任务需要多个工具怎么办

简单的任务可能只需要调用一个工具,但复杂的任务往往需要多个工具配合。比如用户说"帮我订一张明天从北京到上海的机票,然后订个酒店",这就需要:

  1. 调用查询机票的工具
  2. 调用预订机票的工具
  3. 调用查询酒店的工具
  4. 调用预订酒店的工具

这种多工具协作,大模型自己就能处理——它会在ReAct循环里一步步调用需要的工具。但前提是你的工具设计得合理。

我总结了几个多工具协作的设计原则:

原则1:每个工具只做一件事。不要搞一个"订机票+酒店"的大工具,要拆成"查机票"“订机票”“查酒店”"订酒店"四个小工具。工具粒度越小,大模型越容易组合使用。

原则2:工具之间通过自然语言传递信息。不要假设工具A的输出可以直接作为工具B的输入。大模型会在中间做"翻译"——它看完工具A的结果后,会提取关键信息,组织成工具B需要的参数。所以工具的输入输出都要用自然语言描述清楚。

原则3:给大模型足够的"思考空间"。复杂任务可能需要多轮工具调用,不要急于打断。在System Prompt里告诉大模型,“可以分步骤调用多个工具,每一步根据上一步的结果决定下一步”。

我在项目里遇到过一个有意思的case:用户让Agent"分析一下最近一周的销售数据,然后发邮件给老板"。Agent先是调用了数据库查询工具,拿到数据后又调用了数据分析工具,分析完之后调用了邮件发送工具。整个过程完全自动,我什么都没干预。那一刻我真的感觉到Agent的"智能"出来了。

五、错误处理:工具调用失败了怎么办

理想很丰满,现实很骨感。工具调用经常会失败——网络超时、参数错误、Server挂了、API限流……各种问题都有。

如果不做错误处理,大模型拿到一个报错信息,可能会直接把报错原文转给用户,体验很差。我的做法是分层处理:

第一层:客户端重试。对于网络超时、临时不可用等瞬时错误,自动重试2-3次。注意不要对所有错误都重试,比如参数错误重试多少次都没用。

asyncdefcall_with_retry(mcp_manager,tool_name,arguments,max_retries=2):forattemptinrange(max_retries+1):try:returnawaitmcp_manager.call_tool(tool_name,arguments)except(TimeoutError,ConnectionError)ase:ifattempt<max_retries:awaitasyncio.sleep(1*(attempt+1))# 指数退避continueraise

第二层:给大模型友好的错误信息。如果重试还是失败,不要把原始异常直接丢给大模型。要转换成大模型能理解的自然语言,比如"天气查询服务暂时不可用,请稍后再试,或者建议用户手动查询"。

第三层:降级策略。对于关键工具,可以准备降级方案。比如天气查询失败了,可以告诉用户"暂时无法查询实时天气,根据历史数据,北京这个季节通常是XX天气"。

第四层:人工介入。如果连续多次失败,或者涉及高风险操作(比如支付、删除数据),应该暂停并请求人工确认,而不是让Agent自己硬扛。

六、几个让工具调用更稳定的技巧

最后分享几个我在项目中总结的、能显著提升工具调用稳定性的技巧:

技巧1:工具数量控制在10个以内。工具越多,大模型选错的概率越高。如果你的系统有很多工具,考虑做"工具路由"——先用一个轻量模型判断用户意图属于哪个领域,再只加载该领域的工具。

技巧2:在System Prompt里给几个示例。比如"用户问天气时,调用get_weather工具;用户问文件内容时,调用read_file工具"。几个示例比长篇大论的规则管用得多。

技巧3:工具结果加上来源标注。比如"【天气工具返回】北京今天晴,22°C"。这样大模型能清楚地知道哪些信息是工具给的,哪些是自己推理的,减少幻觉。

技巧4:定期刷新工具列表。MCP Server的工具可能会变化(比如升级后加了新工具),不要在启动时加载一次就不管了。可以每隔一段时间重新拉取一次工具列表。

技巧5:记录工具调用日志。把每次工具调用的参数、结果、耗时都记下来。出问题的时候,日志是最好的排查工具。我还会定期分析日志,看看哪些工具经常被调用、哪些经常失败、哪些从来没被用过。

七、一个完整的例子

把上面讲的内容整合起来,我写了一个简化版的Agent + MCP工具调用的完整示例:

importjsonimportasyncioclassMCPPoweredAgent:def__init__(self,mcp_manager,llm_client):self.mcp_manager=mcp_manager self.llm_client=llm_client self.max_tool_calls=10asyncdefrun(self,user_message):messages=[{"role":"system","content":"""你是一个智能助手。 规则: 1. 涉及实时信息或外部数据时,必须调用工具,不要凭记忆回答。 2. 可以分步骤调用多个工具完成复杂任务。 3. 工具调用失败时,向用户说明情况并给出建议。"""},{"role":"user","content":user_message}]# 获取并转换工具列表mcp_tools=awaitself.mcp_manager.get_all_tools()llm_tools=mcp_tools_to_llm_format(mcp_tools)for_inrange(self.max_tool_calls):response=awaitself.llm_client.chat(messages,tools=llm_tools)ifnotresponse.tool_calls:returnresponse.contentfortool_callinresponse.tool_calls:tool_name=tool_call.function.nametry:arguments=json.loads(tool_call.function.arguments)result=awaitself.call_with_retry(tool_name,arguments)tool_result=f"【{tool_name}返回】{result.content[0].text}"exceptExceptionase:tool_result=f"【{tool_name}失败】{str(e)},请告知用户该工具暂时不可用。"messages.append({"role":"assistant","content":None,"tool_calls":[tool_call]})messages.append({"role":"tool","tool_call_id":tool_call.id,"content":tool_result})return"抱歉,处理您的请求时超出了最大工具调用次数,请简化您的问题后重试。"asyncdefcall_with_retry(self,tool_name,arguments,retries=2):foriinrange(retries+1):try:returnawaitself.mcp_manager.call_tool(tool_name,arguments)except(TimeoutError,ConnectionError):ifi<retries:awaitasyncio.sleep(1*(i+1))continueraise

这个例子虽然简化了,但已经包含了工具转换、ReAct循环、错误重试、最大次数限制等核心机制。你可以在这个基础上根据自己的需求扩展。

写在最后

把MCP工具接入Agent只是第一步,真正的挑战在于"用好"。工具选择的准确率、多工具协作的流畅度、错误处理的健壮性,这些都需要在实践中不断调优。

我的体会是:不要指望大模型一次就能把工具调用做对。要把工具调用当成一个"需要工程化保障"的环节——重试、降级、日志、监控,一个都不能少。大模型负责"智能",我们负责"稳定"。

MCP给了我们一个标准化的工具接入方式,但怎么把工具用出花来,还得靠我们自己在项目里摸索。希望这篇文章里的经验能帮你少踩点坑。

你们在Agent里用MCP工具的时候,遇到过什么奇葩问题?有什么好用的技巧?欢迎在评论区交流。


下一篇预告:MCP社区生态。我们会看看MCP生态里都有哪些现成的工具服务器,从官方维护的到社区贡献的,帮你快速找到能用的轮子。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进