1. 从一次“工具打架”说起:LangChain 1.0 下 Agentic RAG 与 MCP 的真实痛点
如果你最近在折腾 LangChain 1.0,大概率会遇到这样一个场景:手里有一个本地知识库要检索,同时又想让模型去调外部工具查实时数据,比如查车票、查天气、查数据库。想法很美好,但一上手就发现——RAG 检索节点和 MCP 工具调用像是两个平行世界,模型要么只走检索、要么只调工具,很难在一个 Agent 里协同起来。
更麻烦的是 Key 管理。本地 vLLM 部署的模型要一个 base_url,嵌入模型要另一个 base_url,MCP 服务如果走远程又要一套鉴权,再加上不同厂商的 API Key 格式各异,代码里到处是api_key="xxx"的硬编码。调试的时候改一处忘一处,报错信息还特别隐晦,比如local proxy failed或者reading choices这类让人摸不着头脑的提示。
这篇内容就是来解决这个组合问题的。核心思路是:用 LangChain 1.0 的create_agent构建一个 ReAct 智能体,把 Agentic RAG 的检索工具和 MCP 协议加载的外部工具统一注册进去,再通过 TaoToken 的统一 Key 和 API 通道来收敛所有模型调用入口。这样你只需要维护一份配置,就能让检索链路和工具调用链在同一个 Agent 里跑通。
适合谁看?如果你已经写过基础的 LangChain Chain,想升级到 Agent 形态;或者你正在做多工具协同的智能体,被 Key 和 base_url 的碎片化折磨过;再或者你只是想找一个能直接复制粘贴跑通的 Agentic RAG + MCP 模板,那接下来的内容会对你有用。
我试过把 RAG 和 MCP 分开跑,各自都正常,但合到一个 Agent 里就出现工具选择混乱。后来发现关键在系统提示词的工具描述和检索节点的返回格式上,下面会一步步拆开讲。
2. TaoToken 前置准备:统一 Key 与 API 通道的接入配置
在开始写 Agent 代码之前,先把模型调用的入口统一掉。这一步不做,后面每加一个工具就要多配一套鉴权,维护成本会指数级上升。
TaoToken 在这里扮演的角色是一个统一的 API 通道。你可以在它的控制台里创建 Key,然后所有兼容 OpenAI 接口的模型调用都走同一个 base_url 和同一个 Key。对于 LangChain 来说,这意味着ChatOpenAI和OpenAIEmbeddings可以共用一套连接配置,只是 model 参数不同。
先拿到 Key。访问控制台页面创建:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite创建完成后,你会得到一个以sk-开头的 Key。把它放到环境变量里,不要硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"注意 base_url 这里不带 UTM 参数,保持干净。API 文档在:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite如果你需要查看当前有哪些模型可用,可以用模型对话页面快速验证:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite对于长期跑编码任务或者 Agent 工作流的场景,Coding Plan 会更划算:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewriteKey 管理页面在这里,方便你后续轮换或查看用量:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite前置准备的核心就三件事:拿到 Key、确认 base_url、把环境变量配好。接下来所有模型调用都复用这两个值。这样做的好处是,当你要换模型或者加新工具时,只需要改 model 名称,不用动连接层。
有一点需要提醒:TaoToken 是作为 API 通道来统一管理调用入口的,它不替代你的编辑器或本地推理服务。本地 vLLM 部署的模型仍然可以保留,只是如果你想让 Agent 同时调用本地模型和远程模型,统一走 TaoToken 的通道会让配置更干净。实际项目中,我倾向于把嵌入模型和对话模型都指向同一个 base_url,减少变量。
3. 可复制配置:MCP 服务注册与 Agentic RAG 检索节点编排
这一节是核心,给出可以直接复制运行的配置和代码。整体结构分三块:MCP 服务注册、RAG 检索工具封装、Agent 组装。
3.1 MCP 服务注册配置
MCP 采用客户端-服务器架构,服务器提供工具、资源和提示词模板。在 LangChain 1.0 里,通过MultiServerMCPClient来加载。下面是一个标准的 MCP 服务注册配置,支持本地 stdio 和远程 sse 两种传输方式:
# mcp_config.py MCP_SERVERS = { "12306-mcp": { "command": "npx", "args": ["-y", "12306-mcp"], "transport": "stdio" }, "weather-mcp": { "url": "https://your-mcp-server.example.com/sse", "transport": "sse", "headers": { "Authorization": "Bearer ${TAOTOKEN_API_KEY}" } } }transport为stdio时走本地进程,适合开发调试;为sse时走远程服务,适合生产环境。headers 里可以带上统一 Key,这样远程 MCP 服务的鉴权也收敛到同一套凭证。
加载工具的函数:
# mcp_loader.py from langchain_mcp_adapters.client import MultiServerMCPClient from mcp_config import MCP_SERVERS async def load_mcp_tools(): tools = [] try: client = MultiServerMCPClient(MCP_SERVERS) mcp_tools = await client.get_tools() if isinstance(mcp_tools, list): tools.extend(mcp_tools) else: tools.append(mcp_tools) print(f"加载了 {len(mcp_tools)} 个 MCP 工具") except Exception as e: print(f"MCP工具加载失败: {e}") print("继续使用RAG工具...") return tools3.2 Agentic RAG 检索节点编排
RAG 部分的关键是把检索逻辑封装成一个@tool,这样 Agent 才能把它当作可调用的工具。检索策略采用混合检索:向量相似度占 0.7 权重,BM25 关键词匹配占 0.3 权重。这种组合在电商 FAQ、技术文档这类场景下召回效果比较稳。
# rag_tool.py import os import re from langchain_core.documents import Document from langchain.tools import tool from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS from langchain_classic.chains import RetrievalQA from langchain_core.prompts import PromptTemplate from langchain_classic.retrievers import EnsembleRetriever, BM25Retriever def clean_text(text): text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s]', '', text) text = re.sub(r'\s+', ' ', text).strip() text = '\n'.join([line for line in text.split('\n') if len(line) > 10]) return text async def initialize_rag_tool(loader, llm, embeddings): documents = loader.load() cleaned_docs = [ Document(page_content=clean_text(doc.page_content), metadata=doc.metadata) for doc in documents ] text_splitter = RecursiveCharacterTextSplitter( chunk_size=50, chunk_overlap=5, length_function=len, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = text_splitter.split_documents(cleaned_docs) vector_db = FAISS.from_documents(chunks, embeddings) os.makedirs("data/RAG/Vector_DB", exist_ok=True) vector_db.save_local("data/RAG/Vector_DB/ecommerce_faq") vector_db = FAISS.load_local( folder_path="data/RAG/Vector_DB/ecommerce_faq", embeddings=embeddings, allow_dangerous_deserialization=True ) vector_retriever = vector_db.as_retriever( search_type="similarity", search_kwargs={"k": 3, "score_threshold": 0.5} ) bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 3 ensemble_retriever = EnsembleRetriever( retrievers=[vector_retriever, bm25_retriever], weights=[0.7, 0.3] ) @tool def ecommerce_rag_tool(query: str) -> str: """用于回答电商相关问题,包括退换货政策、商品保养方法等的RAG工具。输入应为用户的具体问题。""" qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=ensemble_retriever, chain_type_kwargs={ "prompt": PromptTemplate.from_template( """已知信息:{context} 用户问题:{question} 请基于已知信息,以专业技术文档的格式回答用户问题,要求逻辑清晰、步骤明确,仅使用提供的已知信息回答。""" ) }, return_source_documents=True ) result = qa_chain.invoke({"query": query}) return result["result"] return ecommerce_rag_tool这里chain_type="stuff"是最简单直接的方式,把所有检索到的文档填充进一个 Prompt 一次性发给 LLM。检索结果少、文档短的场景用它最快。如果文档量大,可以换成map_reduce或refine,但调用次数和成本会上升。
3.3 模型初始化与 Agent 组装
模型初始化统一走 TaoToken 的 base_url:
# llm_init.py import os from langchain_openai import ChatOpenAI, OpenAIEmbeddings BASE_URL = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api") API_KEY = os.getenv("TAOTOKEN_API_KEY") async def initialize_llm(): llm = ChatOpenAI( model="qwen3-4b-thinking", base_url=BASE_URL, api_key=API_KEY, temperature=0 ) print("LLM已构建") return llm async def initialize_embeddings(): embeddings = OpenAIEmbeddings( model="qwen3-embedding-4b", base_url=BASE_URL, api_key=API_KEY ) print("嵌入模型已构建") return embeddingsAgent 组装用 LangChain 1.0 的create_agent,传入 LLM、工具列表和系统提示词:
# main.py import asyncio from langchain.agents import create_agent from langgraph.checkpoint.memory import MemorySaver from langchain_community.document_loaders import TextLoader from llm_init import initialize_llm, initialize_embeddings from rag_tool import initialize_rag_tool from mcp_loader import load_mcp_tools async def initialize_agent(llm, tools): agent = create_agent( llm, tools=tools, system_prompt="""你是一个多功能智能助手。 1. 用户询问电商相关问题(退换货、商品保养等),使用ecommerce_rag_tool工具; 2. 当用户询问火车票相关问题,使用12306-mcp工具; 3. 调用工具后,检查返回结果是否足够回答问题: - 如果足够,直接整理回答; - 如果不足(如信息缺失、不准确),调整查询词重新调用工具; - 最多重试2次。 4. 如果是复杂问题(包含多个子问题),先拆解为独立的子任务: - 对每个子任务分别调用工具获取信息; - 整合所有子任务的结果生成最终回答。 5. 回答要直接、简洁,结合工具返回的信息回答。""", checkpointer=MemorySaver() ) print("Agent已构建") return agent async def main(): loader = TextLoader("data/RAG/ecommerce_faq.txt", encoding='utf-8') llm = await initialize_llm() embeddings = await initialize_embeddings() tools = await load_mcp_tools() rag_tool = await initialize_rag_tool(loader, llm, embeddings) tools.append(rag_tool) print(f"总共向智能体注入 {len(tools)} 个工具") agent = await initialize_agent(llm, tools) config = {"configurable": {"thread_id": "1"}} user_input = "我衣服不想要了,该怎么退货?以及明天我要从南宁到北京,高铁票有哪些?" response = await agent.ainvoke( {"messages": [{"role": "user", "content": user_input}]}, config=config ) print("\n=== 用户问题 ===") print(user_input) print("=== Agent回答 ===") print(response["messages"][-1].content) if __name__ == "__main__": asyncio.run(main())这套配置里,MCP 工具和 RAG 工具是平级的,都注册进同一个 Agent。系统提示词负责告诉模型什么场景用哪个工具,以及结果不足时如何重试。MemorySaver提供会话记忆,thread_id用来区分不同对话。
4. 验证请求与成功结果:跑通一条可观测的智能体检索链路
配置写完后,需要按顺序启动并验证。整个过程分三步:启动模型服务、启动嵌入服务、运行 Agent 主程序。
第一步,启动对话模型。如果你用本地 vLLM 部署 Qwen3,命令如下:
python3 -m vllm.entrypoints.openai.api_server \ --model /path/to/Qwen3-4B-thinking \ --served-model-name qwen3-4b-thinking \ --host 0.0.0.0 \ --port 8084 \ --dtype auto \ --max-num-seqs 16 \ --max-model-len 65536 \ --tensor-parallel-size 1 \ --trust-remote-code \ --enforce-eager \ --gpu-memory-utilization 0.95 \ --enable-auto-tool-choice \ --tool-call-parser hermes这里--enable-auto-tool-choice和--tool-call-parser hermes是关键,模型必须支持工具调用才能配合 MCP 使用。如果你直接用 TaoToken 通道调用远程模型,这一步可以跳过,把llm_init.py里的 base_url 指向 TaoToken 即可。
第二步,启动嵌入模型服务,端口 8081:
python3 -m vllm.entrypoints.openai.api_server \ --model /path/to/Qwen3-Embedding-4B \ --served-model-name qwen3-embedding-4b \ --host 0.0.0.0 \ --port 8081 \ --dtype auto第三步,运行主程序:
python main.py预期输出会依次打印:
LLM已构建 嵌入模型已构建 加载了 N 个 MCP 工具 向量库已成功创建并保存 向量库已成功加载 总共向智能体注入 N+1 个工具 Agent已构建 === 用户问题 === 我衣服不想要了,该怎么退货?以及明天我要从南宁到北京,高铁票有哪些? === Agent回答 === (模型整合 RAG 检索到的退货政策 + MCP 查询到的车票信息,生成结构化回答)验证成功的标志是:Agent 回答里同时包含了退货步骤和车票信息,说明它正确拆解了复合问题,分别调用了 RAG 工具和 MCP 工具,最后整合了结果。
如果你想单独验证模型通道是否通,可以用模型对话页面发一条测试消息:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite在页面里选择对应模型,输入“你好,请回复当前可用状态”,能正常返回就说明 Key 和 base_url 配置无误。
可观测性方面,建议在agent.ainvoke前后加日志,打印response["messages"]的完整列表,这样能看到每一步的工具调用记录和中间结果。LangGraph 的 checkpointer 也会保存状态,方便回溯。
5. 本篇常见错误排查:401、local proxy failed、reading choices 与 OAuth
跑不通的时候,报错信息往往很隐晦。下面按真实遇到的错误逐一对照。
401 Unauthorized:最常见。检查TAOTOKEN_API_KEY环境变量是否生效,在 Python 里打印os.getenv("TAOTOKEN_API_KEY")确认不是 None。如果 Key 正确但仍 401,检查 base_url 是否写成了带 UTM 参数的完整地址,应该用https://taotoken.net/api这个干净路径。
local proxy failed:这个报错通常出现在 MCP 走 stdio 传输时,本地进程启动失败。检查npx是否可用,12306-mcp包是否能正常下载。可以先在终端手动执行npx -y 12306-mcp看是否报错。如果是网络问题导致包拉不下来,换用 sse 传输的远程 MCP 服务。
reading choices 相关报错:一般是模型返回格式不符合 OpenAI 兼容规范。检查ChatOpenAI的 model 名称是否和实际部署的served-model-name一致。如果用的是 TaoToken 通道,确认该模型在通道里是可用状态。另外temperature=0在某些模型上会导致返回结构异常,可以试着调到 0.1。
OAuth 鉴权失败:如果 MCP 服务配置了 OAuth,headers 里的 token 格式要对。常见错误是漏了Bearer前缀,或者 token 过期。检查mcp_config.py里 headers 的写法,确保和 MCP 服务端要求的一致。
工具选择混乱:Agent 不调用 RAG 工具或调错工具。检查系统提示词里每个工具的描述是否清晰,@tool装饰器的 docstring 要写明使用场景。工具描述越具体,模型选择越准。
向量库加载失败:FAISS.load_local报错,通常是嵌入模型和创建索引时用的不一致。加载本地索引时,嵌入模型必须和之前使用的相同,否则维度对不上。
MCP 工具数量为 0:load_mcp_tools返回空列表。检查MultiServerMCPClient的配置字典格式,transport字段拼写是否正确。stdio 模式下command和args要匹配。
排查时建议按顺序:先确认 Key 和 base_url,再确认模型服务可访问,然后确认 MCP 服务能独立启动,最后才看 Agent 组装逻辑。这样能快速定位问题在哪一层。
6. 继续深入:把这条链路用到你的实际项目里
跑通上面的流程后,你手里就有了一条可观测的智能体检索链路。接下来可以按需扩展。
如果要做长期编码任务或者更复杂的 Agent 工作流,Coding Plan 提供了更稳定的调用配额:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite需要管理多个 Key 或查看用量时,API Keys 页面在这里:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite接入细节和参数说明参考文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite实际项目里,我建议把 MCP 服务配置和 RAG 工具封装分开成独立模块,这样新增工具时不用动 Agent 主逻辑。系统提示词也要随着工具增加持续迭代,工具描述写得好,Agent 的选择准确率会明显提升。另外,chain_type的选择要根据文档量来定,检索结果少于 5 个用stuff最快,文档多且要求高质量回答时再考虑refine。
最后一个小技巧:在开发阶段把response["messages"]完整打印出来,能看到 Agent 每一步的思考过程和工具调用参数,调提示词的时候特别有用。等稳定后再把日志级别调低。