目 录一、为什么要接入MCP二、MCP是什么1. MCP解决什么问题2. 和传统 tool calling 的区别三、实现过程Step 1接入 MCP 服务Step 2注册成工具Step 3接入 LangGraph Agent 流程让 normalize_decision() 接受 web_search给 router 一个更明确的 web_search 选择规则再加一个“保守兜底规则”工具执行微调一、为什么要接入MCP前面咱们做的帮助阅读论文的Agent系统可以回答用户关于论文本地知识库的相关问题但是也有一点不足比如当用户问到了一些最近趋势或者最近发展这样的问题时本地知识库没有相关信息那么系统就回答不了所以咱们需要让系统具备“获得外部信息”的能力这就需要用到MCP技术了。二、MCP是什么查询IBM中国给出的答案是模型上下文协议 (Model Context Protocol MCP) 是 AI 应用程序的一个标准化层可与外部服务如数据源、工具和工作流进行有效通信。1. MCP解决什么问题我个人理解它对于智能体来说是一种外部厂商提供的数据源、工具和工作流使用规则当一个智能体按照这些规则给外部厂商发去请求时就可以得到外部厂商给予的服务。可以理解成MCP 给 AI 应用接外部能力的统一插口。参考一些资料我了解到这项协议的初衷是为了减少一些通用工具的重复开发比如一些基础业务比如查询天气网络搜索等这些工具功能比较通用如果每个智能体都重复写这种通用的工具确实是很消耗精力所以做成MCP服务可以很轻松的复用一些通用的功能。2. 和传统 tool calling 的区别传统的tool calling需要自己写工具函数并且手动接入工具但是有了MCP智能体按照协议发出请求就可以调用外部能力了。MCP和tool calling的就好像是插座和自己焊接电线。三、实现过程在开始之前需要安装一个依赖LangChain官方提供接入MCP的适配库我想增加的是可以进行网络搜索的功能服务选择了智谱的MCP服务pipinstalllangchain-mcp-adaptersStep 1接入 MCP 服务首先在app目录下新建一个mcp_tools.py然后在里面需要些三个功能分别是接入智谱MCP并发送请求的功能、整理返回结果的功能以及将功能封装成一个工具importasyncioimportjsonimportosfromdotenvimportload_dotenvfromlangchain_mcp_adapters.clientimportMultiServerMCPClientfromapp.logger_configimportsetup_logger load_dotenv()loggersetup_logger()asyncdef_call_zhipu_web_search(query:str,recency:strnoLimit,content_size:strmedium,location:strus,):api_keyos.getenv(ZHIPU_API_KEY)ifnotapi_key:raiseValueError(ZHIPU_API_KEY not found in .env)clientMultiServerMCPClient({zhipu_search:{transport:http,url:https://open.bigmodel.cn/api/mcp/web_search_prime/mcp,headers:{Authorization:fBearer{api_key}},}})toolsawaitclient.get_tools()search_toolnext((toolfortoolintoolsiftool.nameweb_search_prime),None)ifsearch_toolisNone:raiseRuntimeError(web_search_prime not found in MCP tools)resultawaitsearch_tool.ainvoke({search_query:query,search_recency_filter:recency,content_size:content_size,location:location,})returnresultdef_parse_mcp_search_result(raw_result)-list[dict]:ifnotraw_result:return[]raw_textifisinstance(raw_result,list)andlen(raw_result)0:first_itemraw_result[0]ifisinstance(first_item,dict):raw_textfirst_item.get(text,)else:raw_textgetattr(first_item,text,)else:raw_textstr(raw_result)dataraw_text# 这个 MCP 返回里text 可能是 “字符串里的 JSON”# 所以这里做最多两次 json.loadsfor_inrange(2):ifisinstance(data,str):try:datajson.loads(data)exceptException:breakifisinstance(data,list):returndatareturn[]defweb_search_tool(query:str)-str:logger.info(f[web_search_tool] query:{query})raw_resultasyncio.run(_call_zhipu_web_search(queryquery,recencyoneMonth,content_sizemedium,locationus,))items_parse_mcp_search_result(raw_result)ifnotitems:logger.warning([web_search_tool] parsed result is empty, fallback to raw text)returnstr(raw_result)lines[]foridx,iteminenumerate(items[:5],start1):titleitem.get(title,No title)linkitem.get(link,)contentitem.get(content,)lines.append(f[{idx}]{title}\nf{content}\nf{link})final_text\n\n.join(lines)logger.info([web_search_tool] search finished successfully)returnfinal_textStep 2注册成工具当工具被封装后我们就可以在tools.py中注册我们的工具了这里与tool calling不同的地方也出现了本地写的工具咱们在这里需要接入智能体但是对于MCP服务只需要注册到模型工具中就好不需要走一波接入fromdatetimeimportdatetimefromapp.rag_systemimportRAGSystemfromapp.llm_utilsimportclientfromapp.configimportCHAT_MODELfromapp.mcp_toolsimportweb_search_tooldefrag_tool(query,rag:RAGSystem,chat_historyNone):returnrag.ask(query,chat_historychat_history)defcalculator_tool(expression):try:returnstr(eval(expression))exceptException:returnInvalid expressiondeftime_tool(_):returndatetime.now().strftime(%Y-%m-%d %H:%M:%S)defllm_tool(query,chat_historyNone):messages[{role:system,content:You are a helpful assistant.}]ifchat_history:messages.extend(chat_history)messages.append({role:user,content:query})responseclient.chat.completions.create(modelCHAT_MODEL,messagesmessages,)returnresponse.choices[0].message.content TOOLS[{name:rag,description:Use for paper/document questions,func:rag_tool},{name:calculator,description:Use for math calculations,func:calculator_tool},{name:time,description:Get current time,func:time_tool},{name:web_search,description:Use for external web search when local documents are not enough or when real-time web information is needed,func:web_search_tool},{name:llm,description:Use for general questions,func:llm_tool}]Step 3接入 LangGraph Agent 流程让 normalize_decision() 接受 web_search注册完工具后咱们现在的合法工具就不只是有rag、llm、time以及calculator了所以在工具规范化的地方需要进行一定的扩充将咱们的web_search工具也扩充进去defnormalize_decision(decision:dict,query:str,valid_tool_names:set[str])-dict:ifnotisinstance(decision,dict):return{tool:llm,input:query}toolstr(decision.get(tool,)).strip().lower()tool_inputstr(decision.get(input,)).strip()iftoolnotinvalid_tool_names:return{tool:llm,input:query}# 对 rag / llm / time / web_search都统一保留原始 queryiftoolin{rag,llm,time,web_search}:return{tool:tool,input:query}# calculator 允许保留抽出来的表达式iftoolcalculator:ifnottool_input:return{tool:calculator,input:query}return{tool:calculator,input:tool_input}return{tool:llm,input:query}给 router 一个更明确的 web_search 选择规则咱最早build_choose_tool_node()中关于工具选择的路由响应的需要进行调整当用户问道一些关于最近、当前、最新等这样字眼的时候咱们的路由要选择web_search功能promptf You are a tool router. Your job is ONLY to choose the best tool and prepare its input. Do NOT answer the users question. Do NOT rewrite the users question into an answer. Return JSON only. Available tools:{tool_desc}Tool selection guidance: - Use rag for questions about the loaded local papers/documents, such as paper1, paper2, this paper, the PDF, or document-based comparison/analysis. - Use calculator for clear math calculations. - Use time for current time questions. - Use web_search for questions that explicitly need web information, latest information, recent updates, current events, online search, or information likely not contained in the local PDFs. - Use llm for general questions that do not need document retrieval, calculation, time, or web search. Rules: 1. You must return exactly one JSON object. 2. JSON format: {{tool: ..., input: ...}} 3. tool must be one of: rag, calculator, time, web_search, llm 4. For rag, llm, time, and web_search: - input should stay the same as the users original question - do not invent a new sentence 5. For calculator: - input should be the math expression only if you can extract it 6. Do not include markdown, explanations, or code fences. User question:{query}再加一个“保守兜底规则”在路由上其实通过大语言模型得到的路由是一种软路由为什么这么说呢因为是通过语言模型进行判断结果可能有一些不确定性所以咱们还要加一个兜底刚性路匹配用户问题中的关键词如果出现了比如latest 、 recent 、联网等这样的关键词那么直接匹配到网络搜索工具defmaybe_force_web_search(query:str,decision:dict)-dict:qquery.lower()web_keywords[latest,recent,current,today,news,web,online,internet,search the web,最新,最近,当前,今天,联网,网上,搜索一下]local_doc_keywords[paper1,paper2,this paper,the paper,pdf,document,论文,文档]has_web_signalany(kinqforkinweb_keywords)looks_like_local_docany(kinqforkinlocal_doc_keywords)ifhas_web_signalandnotlooks_like_local_doc:return{tool:web_search,input:query}returndecision工具执行微调有了上面的约束后咱们的工具微调部分做出响应调整即可给工具选择结果加上硬约束raw_decisionjson.loads(cleaned)decisionnormalize_decision(raw_decision,query,valid_tool_names)decisionmaybe_force_web_search(query,decision)logger.info(f[choose_tool_node] raw decision:{raw_decision})logger.info(f[choose_tool_node] normalized decision:{decision})return{decision:decision}如果这篇文章对你有帮助可以点个赞完整代码地址https://github.com/1186141415/LangChain-for-A-Paper-Rag-Agent