从MCP到RAG:Agent的开发之路

从MCP到RAG:Agent的开发之路

在AI时代,大型语言模型(LLM)如ChatGPT已深刻改变我们与机器的交互方式,但"幻觉"(hallucination)问题始终存在:模型面对知识盲区时,常自信地生成错误信息。

为了让AI真正"帮我们做事",而非停留在空谈,两条关键路径应运而生:

  • Model Context Protocol(MCP) :专为赋予AI真实行动力而设计的标准化协议。它统一规范LLM与外部工具、资源和提示模板的通信,让AI能轻松调用数据库、浏览器、地图服务等能力,从"会说"迈向"会做"。
  • Retrieval-Augmented Generation(RAG) :通过检索可靠外部知识增强提示,确保生成内容有据可依,大幅降低幻觉风险。

MCP提供"双手",RAG赋予"眼睛"。两者结合,打通从思考到落地的完整链路。

本文基于真实代码实践,带你一步步走通这条路径:从手写MCP服务器,到LangChain集成,再到RAG的文档加载与语义分割,最终实现MCP+RAG的闭环智能代理。

第一部分:MCP协议------AI与世界的"桥梁"

想象一下:AI就像一位才华横溢却与世隔绝的学者,它拥有无穷的智慧,却需要可靠的"工具"来触达现实世界------查询数据库、操控浏览器,甚至调用地图API。

但如何让AI与这些工具无缝对接、顺畅协作?

这就是MCP(Model Context Protocol)的独特魅力:它是一个标准化协议,宛如一座桥梁,连接AI(Model)与外部上下文(Context,包括工具、资源和提示模板),让交互变得高效而有序。

MCP的核心概念深度剖析

MCP并非单纯的一个库或框架,而是一个协议规范,类似于HTTP在Web生态中的基石角色。它巧妙地将上下文划分为三大类别,确保AI能全面利用外部资源:

  • Tool:这些是可执行的操作,例如查询用户数据或启动浏览器自动化。它们赋予AI实际行动力,让抽象思考转化为具体输出。
  • Resource:静态资源,如使用指南或文档,这些作为背景知识注入AI的"记忆",帮助它更好地理解任务背景。
  • Prompt Template:预设的提示模板,类似于AI的"思维框架",引导它生成更精确、针对性的响应,避免泛泛而谈。

在实际应用中,MCP的强大在于其扩展性。例如,Zod这样的TypeScript schema验证库可以无缝集成到工具定义中,确保输入数据的类型安全,从而防范运行时错误。

底层逻辑与通信机制

MCP的架构灵感来源于Browser/Server模式(或经典的C/S架构),通过URI(统一资源标识符)来唯一标识每一项资源,就像Web中的URL一样,确保全局无冲突定位。通信则依托标准输入输出流(Stdio),这使得跨进程交互变得高效而可靠,尤其适合本地或远程场景。

如这个手写MCP服务器,它展示了协议的落地方式:

javascript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// 数据库模拟
const database = { users: { "001": { ... }, ... } };

const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });

server.registerTool('query-user', {
    description: '查询数据库中的用户信息...',
    inputSchema: { userId: z.string().describe("用户 ID...") }
}, async ({ userId }) => {
    // 逻辑处理,返回内容
});

server.registerResource('使用指南', 'docs://guide', {
    description: 'MCP Server 使用文档',
    mimeType: 'text/plain'
}, async () => {
    return { contents: [{ url: 'docs://guide', mimeType: 'text/plain', text: '指南内容...' }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);

在这里,最关键的一步是先通过 new McpServer() 创建服务器实例,它就像一个"工具托管容器"。只有在这个容器存在后,我们才能调用 server.registerTool() 和 server.registerResource() 来向其中添加内容。

  • registerTool 注册了一个名为 query-user 的工具,输入参数使用 Zod schema 进行严格定义,确保 AI 传入的数据格式可靠(比如 userId 必须是字符串),避免运行时错误。
  • 工具的返回结果统一采用 { content: [...] } 数组格式,支持 text、image、file 等多种类型,为未来的多模态交互留足空间。
  • registerResource 则提供了静态资源(这里是使用指南),AI 可以在调用工具前通过 URI(如 docs://guide)获取背景知识,极大提升工具使用的可理解性和准确性。

这种"先容器、后注册、再连接"的结构,正是 MCP 协议设计的核心哲学:一切工具和资源都必须依附于一个明确的服务器实例,从而实现跨进程、跨语言的标准化调用。

值得一提的是,初学者在实践MCP时,最容易忽略inputSchema的严谨性。如果定义过于宽松,AI可能传入无效参数,导致工具执行失败甚至崩溃。这提醒我们,MCP的核心哲学是"协议优先":先构建坚实的接口规范,再填充实现逻辑。只有这样,AI才能真正"信任"这些工具,发挥出最大潜力。

MCP客户端与LangChain集成------从协议到智能代理的跃进

MCP服务器搭建完成后,真正的魔力在于客户端的调用------它将静态工具转化为动态AI能力的核心枢纽。在下面这段代码中,我使用MultiServerMCPClient来启动并管理服务器实例,这一步宛如为AI注入"外挂"系统,让它从单纯的聊天机器人蜕变为能行动的智能代理。

javascript 复制代码
const mcpClient = new MultiServerMCPClient({
    mcpServers: { "my-mcp-server": { command: "node", args: ["path/to/server.mjs"] } }
});
const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);
底层逻辑深度剖析
MultiServerMCPClient 到底做了什么?

MultiServerMCPClient 是 LangChain 官方提供的 MCP 适配器中最核心的客户端类,它解决了以下几个关键痛点:

  1. 多服务器统一管理 你可以一次性配置多个 MCP 服务器(本地 stdio、本地 http、远程 streamable_http 等各种方式),客户端负责启动进程(如果是用 command/args 方式)、建立连接、维护会话。

  2. 工具自动发现与标准化 调用 await mcpClient.getTools() 时,它会:

    • 向每个 MCP 服务器发起协议请求(通常是初始化握手)
    • 获取服务器上所有通过 registerTool 注册的工具定义(包括 name、description、inputSchema 等)
    • 把这些 MCP 原生工具描述自动转换为 LangChain 兼容的 Tool 对象(包含 invoke 方法)
    • 返回一个工具数组,供后续 bindTools 使用
  3. 默认无状态设计(stateless by default) 每次工具调用时,客户端都会创建一个新的短暂会话(session),执行完就清理。 这适合大多数场景(轻量、无状态工具),但如果你有需要保持上下文的工具(例如文件系统连续操作、浏览器会话),可以显式使用 client.session("server-name") 来创建有状态会话(stateful session),并在 with 块中使用。

  4. 与 ReAct 循环的完美衔接 一旦工具被 model.bindTools(tools) 绑定到 LLM,模型在思考时就会:

    • 看到工具列表和描述
    • 根据任务决定是否要调用(输出 tool_calls)
    • LangChain 自动执行对应的 tool.invoke(args)(底层走 MCP 协议通信)
    • 把结果包装成 ToolMessage 放回消息历史
    • 进入下一轮推理(Observe → Reason → Act)

这正是 ReAct(Reason + Act + Observe)范式的经典实现:AI不再一次性给出答案,而是像人类一样"想一想 → 动手查/做 → 看结果 → 再想"。

为什么强调先创建 MultiServerMCPClient?

就像 MCP 服务器必须先 new McpServer() 再 registerTool 一样,客户端侧也必须先有一个"客户端容器"(MultiServerMCPClient 实例),才能去"拉工具、管连接、转格式"。 如果跳过这一步直接硬编码工具,代码会失去扩展性(无法轻松添加第二个、第三个 MCP 服务器),也无法享受协议自动发现的便利。

一句话总结: MultiServerMCPClient 是 MCP 与 LangChain 之间的"翻译官 + 连接枢纽" ,它让分散的、异构的 MCP 工具变成 LangChain Agent 能直接使用的统一工具集。

后续的 getTools() → bindTools() → ReAct 循环,正是建立在这个枢纽之上的流畅体验。

ReAct(Reason-Act-Observe)循环:AI先推理(reason)任务需求,然后主动调用工具执行行动(act),最后基于工具反馈观察并调整(observe)。这种迭代机制让AI处理复杂任务时不再"一锤子买卖",而是像人类一样逐步推进。 完整的主循环代码展示了这一优雅的动态过程:

javascript 复制代码
async function runAgentWithTools(query, maxIterations = 30) {
    const messages = [new HumanMessage(query)];
    for (let i = 0; i < maxIterations; i++) {
        const response = await modelWithTools.invoke(messages);
        messages.push(response);
        if (!response.tool_calls || response.tool_calls.length === 0) return response.content;
        for (const toolCall of response.tool_calls) {
            const foundTool = tools.find(t => t.name === toolCall.name);
            if (foundTool) {
                const toolResult = await foundTool.invoke(toolCall.args);
                messages.push(new ToolMessage({ content: toolResult, tool_call_id: toolCall.id }));
            }
        }
    }
}

在这个架构中,AI的决策基于累积的消息历史(包括HumanMessage、AIMessage和ToolMessage)。

每轮迭代,模型评估是否有tool_calls:如果有,继续行动;否则,视作任务完成并输出最终响应。为什么设置maxIterations=30?这是为了防范潜在的无限循环------想象一下,如果工具反馈不清晰,AI可能反复纠缠于某个细节,导致资源耗尽。这提醒我们,任务设计至关重要:避免循环依赖(如工具A依赖B,而B又需A),否则AI容易陷入"观察泥沼"。一个实用优化是引入系统提示(SystemMessage),明确定义结束条件,例如"当所有信息齐备时,直接输出总结,避免额外调用"。

多服务器扩展与实际应用

在代码中,我进一步扩展到多服务器配置,融入了amap-maps(高德地图API)、filesystem(文件系统操作)和puppeteer(浏览器自动化)等强大工具。这让MCP从单一服务器跃升为生态系统。

例如,针对查询"南昌西站附近的3个酒店,拿到图片,浏览器展示",AI会智能拆解:先调用amap-maps查询酒店位置和路线,然后用filesystem保存文档,最后通过puppeteer打开浏览器Tab,每个Tab展示一张酒店图片,并自定义标题为酒店名。

这种扩展凸显MCP"上下文协议"的本质:它将AI从"信息孤岛"转化为"互联生态",支持现代LLM(如GPT-4o)的并行tool_calls,提升执行效率。工具结果处理也很精妙------必须确保返回字符串或{text}对象,否则ToolMessage构建会出错,导致循环中断。但需注意:如果工具反馈模糊(如含歧义数据),AI可能过早终止或反复迭代,影响体验。

这是一个"大任务拆解为多轮小行动"的链式逻辑:从用户查询起步,逐层行动观察,直至收敛。这不仅提高了可控性,还让复杂任务(如地图+浏览器+文件集成)变得流畅自然。

第二部分:RAG------从幻觉到可靠生成的跃迁

想象一下:LLM就像一位博学却健忘的学者,它在训练时掌握了海量知识,但面对新问题或细节时,往往会"即兴发挥",制造出似是而非的幻觉答案。RAG(Retrieval-Augmented Generation,检索增强生成)犹如一剂解药,通过外部知识的注入,让AI从"胡编乱造"转向"有据可依"的可靠输出。它将AI的生成过程拆解为三步曲:检索(Retrieve)相关片段、增强(Augment)提示词、生成(Generate)最终答案。

RAG底层逻辑深度剖析

RAG的核心在于"知识外部化":不再依赖LLM的内置记忆,而是动态检索私有或实时数据,从而减少幻觉并提升准确性。

向量表达是其技术基石------将文本转化为高维向量 (如[0.1, 0.2, ...]),每个维度捕捉独特语义(如"食用性"或"硬度")。

例如,水果向量可能强调[高食用性, 低硬度],苹果[0.9, 0.5]、香蕉[0.9, 0.1]、石头[0.1, 0.9]。查询向量与知识库向量通过余弦相似度(cosine)匹配,实现语义搜索,而非僵硬的关键词比对。这让RAG能捕捉隐含关联,如"水果"自动联想到"苹果"。

为什么RAG如此强大?LLM的知识有截止日期(训练数据有限),RAG注入外部源(如企业知识库或文档),支持实时更新。

在这段代码中,我构建了一个简洁的RAG管道,展示了从知识库构建到答案生成的端到端流程:

javascript 复制代码
const embeddings = new OpenAIEmbeddings({ ... });
const documents = [new Document({ pageContent: "...", metadata: { chapter: 1, character: "光光", ... } }), ...];
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);
const retriever = vectorStore.asRetriever({ k: 2 });
const retrievedDocs = await retriever.invoke(question);
const scoreResults = await vectorStore.similaritySearchWithScore(question, 3);

这里,MemoryVectorStore作为内存向量数据库,适合小型数据集测试。它将文档嵌入向量后存储,支持快速检索。asRetriever({ k: 2 })配置返回前2个最相似片段,而similaritySearchWithScore提供[doc, score]对------score是距离值(通常基于L2或余弦),实际相似度可计算为1 - score(范围0-1,越接近1越相关)。这为调试提供了量化依据。

提示词构建是Augment的关键一环,确保检索结果无缝融入LLM输入:

javascript 复制代码
const context = retrievedDocs.map((doc, i) => `[片段${i+1}]\n${doc.pageContent}`).join('\n\n----\n\n');
const prompt = `
你是一个讲友情故事的老师。
基于以下故事片段回答问题,用温暖生动的语言。
如果故事中没有提及,就说"这个故事里没有提到这个细节"

故事片段:
${context}

问题:
${question}

老师的回答:
`;
const response = await model.invoke(prompt);

这种结构化prompt强化了"事实优先"原则------如果检索无果,AI不会编造,而是诚实回应。这在知识密集任务(如故事解读)中尤为宝贵。metadata如{mood: "活泼"}可进一步过滤检索,提升针对性。

第三部分:RAG进阶------Loader与Splitter,征服大规模知识

基础RAG适合小型手动数据集,但现实世界知识往往庞大而杂乱------网页文章、PDF报告或视频脚本。这时,Loader和Splitter登场,将原始源转化为可检索的语义片段,让RAG从"玩具级"跃升为"生产级"。

Loader:从多元源头高效摄取文档

Loader是RAG的"数据入口",它自动化加载外部内容,避免手动构建Document。在下面的代码中,我使用CheerioWebBaseLoader针对网页优化:

javascript 复制代码
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
const cheerioLoader = new CheerioWebBaseLoader("https://juejin.cn/post/...", { selector: '.main-area p' });
const documents = await cheerioLoader.load();

底层逻辑:Cheerio模拟后端jQuery,通过CSS选择器(如'.main-area p')精准提取段落文本,过滤噪声(如广告或侧边栏)。

扩展:LangChain社区Loader支持多样源------TXT、PDF、MP3甚至视频(需转录)。这让RAG适用于企业场景,如从内部Wiki加载知识库。网页若依赖JS动态渲染,Cheerio仅抓取静态HTML,可能遗漏内容;此时,可结合MCP的puppeteer工具先渲染页面,再加载。

Splitter:语义切割,确保片段高效可检索

加载的文档往往过长,直接嵌入会稀释向量密度,导致检索不准。Splitter将大块文本递归拆分成小片段,同时保留语义连贯性:

javascript 复制代码
const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: 400, chunkOverlap: 50, separators: ['。',',','?','!']
});
const splitDocuments = await textSplitter.splitDocuments(documents);

核心机制:Recursive表示递归分割------从大分隔符(如句号'。')起步,若片段超chunkSize(400字符),则向下递归至小分隔符(如逗号',')。chunkOverlap(50字符重叠)像"语义胶水",防止边界处信息丢失,确保前后片段上下文衔接。

注意,这里的separators是针对中文优化,避免英文默认(如\n)导致不自然切割。这在处理长文章(如Juejin博客)时尤为关键,提升向量表达的语义浓度。

分割后,构建vectorStore和检索如基础RAG相同。 这里分了两种模式:Direct Document(小型、手动,适合故事片段)和Loader+Splitter(大型、自动,适合网页)。这可以理解为"知识库构建流水线":加载(Loader)→分割(Splitter)→嵌入(Embeddings)→存储(VectorStore)→检索(Retriever)。这形成闭环,支持大规模RAG。

在实际应用中,检索后构建prompt类似第二部分,但需注意metadata:Loader文档可能自带(如URL来源),Splitter保留原metadata,便于追踪。

这里有一个易错点:chunkSize太小,片段碎片化,检索召回率低;太大使向量稀疏,相似度计算偏差。优化:测试时用similaritySearchWithScore监控score分布,若平均相似度<0.7,则需要调整Splitter参数。

RAG与MCP的结合潜力无限:AI可先用MCP工具(如浏览器)加载文档,再RAG生成总结,实现"行动+知识"的超级代理。。

结语:AI开发的未来之路

从MCP的手写到RAG的检索,我们看到了AI从"智能"到"可靠"的跃迁。MCP如桥梁,RAG如引擎,二者融合让AI代理无限可能。

相关推荐
后端小肥肠3 小时前
公众号躺更神器!OpenClaw+Claude Skill 实现自动读对标 + 写文 + 配图 + 存入草稿箱
人工智能·aigc·agent
数据智能老司机3 小时前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
用户8307196840823 小时前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
数据智能老司机3 小时前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
栀秋6669 小时前
重塑 AI 交互边界:基于 LangChain 与 MCP 协议的全栈实践
langchain·llm·mcp
潘锦9 小时前
RAG 优化常用的 5 种策略
agent
HelloGitHub10 小时前
这个年轻的开源项目,想让每个人都能拥有自己的专业级 AI 智能体
开源·github·agent
Kagol21 小时前
🎉OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用!
前端·开源·agent
李剑一1 天前
你以为OpenClaw在帮你赚钱?其实它是在赚你的钱
openai·agent