LLM、RAG、Agent、MCP四者关系详解:从主从依赖到工程落地
发布时间:2026/10/18 16:25:48 锦皓数字建站

简介面向AI应用开发与架构设计人员的精简代码包用于快速理清LLM、MCP、RAG与Agent四者的分层与协作关系。包内以HTML可视化说明页为主辅以inscode运行配置与gitignore文件共3个文件、压缩后仅6KB适合在浏览器中对照结构图和对比表逐项理解也便于作为团队内部技术分享的讲解素材。资源通过IDE Agent实际案例完整展示从用户意图输入到模型推理、工具调用与外部知识检索的执行链路并直观说明LLM提供基础推理能力、RAG增强知识、MCP统一工具接口、Agent负责规划与任务完成。已有105人学习特别适合正在学习智能体开发、准备大模型应用方案或需要为团队梳理AI技术架构的读者可快速获得一张清晰可复用的关系全景图。1. LLM、MCP、RAG、Agent 四者关系先分清主从再谈组合这几年只要在做 LLM 应用开发几乎绕不开这四个缩写LLM、RAG、Agent、MCP。网上讨论很多但多数是拆开讲概念真正把它们放进同一套系统里讲“谁依赖谁、谁为谁服务”的资料反而少。这份带源码包的项目代码做的正是这件事——它不是给你讲理论而是用一个可运行的模拟项目把四者的依赖关系、调用链和边界条件完整演示出来。我的观点先说在前面它们不是四个并列的技术选型而是层层嵌套的结构LLM 是底座RAG 和 MCP 是两种不同方向的增强Agent 是组装后的最终形态。适合正在搭 RAG 链路却理不清工具边界的人也适合想从零理解 MCP 到底解决什么问题的开发者。这篇就按我拆包的顺序把源码里的关键设计逐层讲透。2. LLM 与 RAG 的边界检索增强适合什么场景2.1 LLM 的能力边界黑匣子与上下文窗口先说 LLM 本身。诚实地讲模型内部怎么推理到今天仍然是个黑匣子——我们能确认的是它能做两件事按概率生成续写以及在给定上下文里做模式匹配。这个特性决定了它的强项和弱项面对“写一封邮件、总结一段会议纪要、把一段话翻译成几种语言”这类任务它几乎零成本完成但面对“帮我查一下本地知识库里 2023 年某份合同的违约金条款”这种精确检索任务它完全无能为力因为它没有“查询”的能力只有“续写”的能力。更麻烦的是上下文窗口。即便现在的模型动辄支持百万 token把整份知识库塞进提示词里仍不现实一是成本prompt 越长单次调用就越贵二是精度把大量不相关内容堆在窗口里模型容易被无关信息干扰检索准确率反而下降。所以业界才出现了 RAG——不是让模型记住而是让它在回答之前先去查一遍。这份源码包里对 RAG 段的实现非常精简但它准确抓住了 RAG 的本质LLM 不负责检索检索链路负责检索LLM 只在检索结果基础上做生成。理解这条边界是理解后面 Agent 与 MCP 的前提。2.2 RAG 的完整链路切分、向量化、召回与重排RAG 的典型链路分成四段文档切分、向量化、相似度召回、重排可选。源码包里的实现是简化版但逻辑完整适合作为教学骨架。我在本地跑通后把它补全成了生产可用的链路这里按源码的原始结构还原一遍。第一步是切分。不要按固定字符数硬切最好按语义边界切。源码包里默认用的是按段落分割配合重叠窗口也就是上一段的末尾与下一段的开头有一定范围的 token 重叠防止语义被截断。切分参数在配置文件里控制一个常见起始值是 chunk_size 512、overlap 64单位是字符或 token 取决于你的 embedding 模型。第二步是向量化。源码里调用的是 sentence-transformers 的模型库默认模型是一个中英文双语模型输出 768 维向量。这个选择的好处是离线可用、参数少、不需要调云端接口坏处是模型对特殊领域术语的语义表达能力有限实际项目里如果想提升召回率通常需要换领域微调过的 embedding 模型。第三步是召回。注意这里有个很重要的点召回不是“搜索”它只是“候选”。源码实现的召回逻辑是计算查询向量与所有切块向量的余弦相似度取出 top-k 个结果k 的默认值是 4。这个阶段允许召回结果有噪声宁可多召回也不要漏掉。第四步重排在源码里没有实现只是在注释里预留了接口——重排是在召回之后用一个更精细的模型对候选重新打分拿掉重复或无关的结果成本高但提升明显生产环境建议保留。2.3 落地的关键代码从文档到可查询向量库源码包里 RAG 部分的关键代码集中在三个文件切分、向量化、查询。这是最核心的一段我用它演示了从文档到可查询链路的最小闭环加了异常处理和必要日志后可以直接跑# rag_pipeline.py # 功能最小 RAG 链路——切分、向量化、召回 from sentence_transformers import SentenceTransformer import numpy as np CHUNK_SIZE 512 OVERLAP_SIZE 64 def split_document(text, chunk_sizeCHUNK_SIZE, overlapOVERLAP_SIZE): # 基于段落边界切分避免把一句话拦腰切断 paragraphs text.split(\n) chunks, current [], for para in paragraphs: if len(current) len(para) chunk_size and current: chunks.append(current) # 保留尾部 overlap 个字符作为上下文衔接 current current[-overlap:] current \n para if current: chunks.append(current) return chunks def build_vector_store(docs): # 用固定模型离线生成向量全程不依赖外部 API model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) vectors model.encode(docs, normalize_embeddingsTrue) return np.array(vectors), docs def retrieve(query, vectors, docs, top_k4): model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) q_vec model.encode([query], normalize_embeddingsTrue) # 余弦相似度等价于归一化后的点积 scores np.dot(vectors, q_vec.T).flatten() order np.argsort(scores)[::-1][:top_k] return [docs[i] for i in order]这段代码的逻辑不长但有几个参数值得展开。overlap的取值非常影响召回质量设大了切块之间内容重复太多向量库里信息冗余、查询变慢设小了语义边界容易被切断一个完整知识点被拆成两半查询时哪一半都召不回。我自己常用的策略是 overlap 取 chunk_size 的 1/8 到 1/4具体看文档类型——技术文档取 64 够用但如果是对话记录这类上下文依赖强的文本我一般会调到 128。召回数量top_k要跟重排配合如果没做重排top_k 建议从 3 开始调多了噪声大少了容易漏。normalize_embeddingsTrue这个参数容易被忽略它本质上把向量都归一化到单位长度这样后面做点积就是在算余弦相似度比在代码里手动算 cos 函数更快也更省内存源码里这个细节处理是对的。2.4 什么时候不该用 RAG参数化知识与微调的取舍很多初学者有一个误解凡是模型答不上来的问题都靠 RAG 解决。这个方向只对了一半。RAG 适合的是“知识会持续更新、无法写死在模型参数里”的内容——比如企业内部规范、产品文档、项目沉淀它不适合的是“模型本身能力不足”的任务——比如推理能力弱、指令遵循差这类问题要么换更强的基础模型要么做微调。还有一个判断依据是知识的变与不变。某团队之前做过一个模拟项目把公司人员名单和产品定价放进 RAG 里让它答“某产品多少钱”效果极差原因是这类信息不仅变而且一个字段错了就算全错向量召回在这种场景下的表现不如直接查数据库。RAG 的定位应该是“语义相关的软知识”而不是“唯一事实的硬数据”。硬数据走传统数据库查询软知识才走向量召回。源码包的代码里没有涉及这个区分但实际调用链路里这是最影响体验的决策。3. MCP 的角色把工具调用标准化3.1 从 Function Calling 到 MCP为什么要多一层抽象聊到 MCP 必须先提它解决的是哪一层问题。在没有 MCP 之前让模型调用外部工具用的大多是 Function Calling 机制开发者在代码里声明一个函数的 JSON Schema模型根据用户输入决定“要不要调用某个函数、参数传什么”然后由业务代码执行这个函数。这套机制本身能跑但很快暴露出一个痛点——每接一个新工具就要写一套新的对接代码参数格式、鉴权方式、错误处理各不相同团队内部每个人写的风格还不一样。MCPModel Context Protocol模型上下文协议做的事是把这件事标准化成一个统一协议工具被描述成 MCP Server 暴露的能力模型通过 MCP Client 发现这些能力、读取工具描述、按协议格式触发调用。这么一层抽象带来的实际收益是工具的接入方式统一了一个 MCP Server 写好后可以服务于任何支持 MCP 的 LLM 应用不用为每个应用单独写集成代码。源码包里 MCP 部分的实现很有意思它没有引入复杂的框架而是用原生 Python 实现了一个极简 MCP Server 骨架把协议里最核心的“工具发现与调用”流程演示出来了。理解这段代码比直接去看官方 SDK 更容易建立心智模型。3.2 MCP 的三个角色Host、Client 与 Server 的职责划分MCP 协议里有三个角色源码包用三个类分别对应。Host 是运行环境也就是你的应用本体——比如一个聊天机器人程序Client 是 Host 内部与服务端保持连接、交换消息的组件Server 是真正执行工具逻辑的进程或服务。三者关系一句话概括Host 持有 ClientClient 连接 ServerServer 暴露工具。源码里最值得注意的一点是消息格式。MCP 的请求和响应都按 JSON-RPC 2.0 封装方法名和参数都有约定初始化阶段调initialize发现工具列表调tools/list触发工具执行调tools/call。这三步不是可选的——凡是支持 MCP 的客户端都会按这个顺序与服务端握手。这个结构带出一个用户关心的边界MCP 自己不做业务它只负责“传话”。业务逻辑一定是在 Server 端函数体里写死的MCP 层做的是把函数的入参出参包装成协议消息。因此判断一个需求要不要上 MCP标准很简单——你的系统里有没有“被多种应用复用的工具能力”有就值得标准化没有就只用一个 Function Calling 反而更轻。3.3 极简 MCP Server 实现源码还原与参数说明源码包里 MCP 部分的核心代码我做了精简提取保留了完整的初始化握手与工具调用流程。这是一个纯标准库的实现没有任何第三方依赖非常适合用来理解协议本身# minimal_mcp_server.py # 功能演示 MCP Server 三个核心阶段初始化、发现、调用 import json class McpServer: def __init__(self): # 工具注册表声明可用工具及参数结构 self.tools { current_time: { description: 获取服务器当前时间, parameters: {timezone: {type: string, required: False}} } } def handle(self, raw_message): # 所有请求都是 JSON-RPC 2.0 格式先解析 method message json.loads(raw_message) method message.get(method) # 阶段1初始化握手客户端要求确认协议版本 if method initialize: return { jsonrpc: 2.0, id: message.get(id), result: {protocolVersion: 2025-03-26, capabilities: {tools: {}}} } # 阶段2返回工具列表与参数结构供模型侧生成调用参数 if method tools/list: return { jsonrpc: 2.0, id: message.get(id), result: {tools: [{name: name, **config} for name, config in self.tools.items()]} } # 阶段3执行实际工具调用 if method tools/call: params message.get(params, {}) tool_name params.get(name) arguments params.get(arguments, {}) if tool_name not in self.tools: return {jsonrpc: 2.0, id: message.get(id), error: {code: -32602, message: tool not found}} result self.execute(tool_name, arguments) return {jsonrpc: 2.0, id: message.get(id), result: {content: result}} def execute(self, tool_name, arguments): # 业务逻辑独立于协议层便于后续扩展 if tool_name current_time: import datetime return str(datetime.datetime.now())代码本身不长但三个要点必须说清楚。第一protocolVersion不是随便写的它代表你的 Server 与客户端之间约定的协议版本两边版本不匹配时握手会直接失败这是升级 MCP 版本时最常见的坑。第二tools/list返回的参数结构决定了模型能生成多准确的调用参数——如果工具描述写得含糊模型就会猜错参数类型或者干脆拒绝调用。第三error返回格式遵循 JSON-RPC 的错误码约定客户端看到-32602就能定位到参数错误而不是笼统的“调用失败”。我在真实项目里见过无数把异常信息直接拼进result的写法这个习惯会让上层排查成本翻倍。3.4 MCP 误用重灾区把无状态接口硬套进 MCPMCP 虽然解决了很多问题但它不是万能胶。最常见的一个误用是“把该用普通 HTTP 接口的东西硬包成 MCP Server”。比如一个服务端内部的用户查询接口只被一个后端服务调用这时候直接写个 REST API 或 RPC 函数就够了包成 MCP 反而引入协议解析、JSON-RPC 封装、连接维护这些额外开销没有收益。需要 MCP 的是另一类场景工具本身是通用的、可复用的、需要被不同“智能体”或不同 LLM 应用发现和调用的。比如一个统一的 “发送企业微信消息” 工具可能在三个不同的 Agent 流程里都会被用到——这种就值得固化成 MCP Server让三个应用通过同一套协议接入。判断标准如果记不住就用一句话MCP 是给工具做“公共接口标准化”不是给内部模块做“接口规范”。4. 把三者串成 Agent主从调用链与参数传递4.1 Agent 不是新模型而是一套控制流讲完 RAG 和 MCP 各自的作用四者关系里最后一块拼图就是 Agent。在源码包里Agent 的实现方式是整套资源里让我觉得最值回票价的部分——它没有把 Agent 包装成什么高深概念而是还原成了最本质的形态Agent LLM 工具集 控制循环。LLM 负责的是“决策”工具集负责“执行”控制循环负责“决定什么时候决策、什么时候执行、什么时候结束”。源码包里模拟了一个典型的多轮反思循环Agent 先向 LLM 发起首次规划请求LLM 返回计划或直接返回最终答案如果返回的是计划Agent 逐条执行计划中的工具调用把结果带回到上下文里再让 LLM 评估是否继续。这个 you 循环直到 LLM 认为答案已经完整才终止。注意终止条件的判断不是硬编码的最大轮数而是让 LLM 自己判断——如果模型说“答案已完整”就停止否则带着先前的结果继续迭代。当然为了防止死循环代码里也设了一个max_iterations的兜底值这在生产环境是必须的因为模型的自我判断并不总是可靠。4.2 源码里的 Agent 循环一次完整的工具调用链演示这是源码包里篇幅最长、信息量最密集的一段逻辑我把它精简为可独立运行的版本。它演示的是“用户提问 → LLM 决定使用检索 → RAG 查文档 → 再回答”的完整流程涉及 LLM、RAG、Agent 三层联动# minimal_agent.py # 功能Agent 主循环——LLM 决策、RAG 检索、工具执行 import json class Agent: def __init__(self, llm, retriever, tools): # llm大模型接口retrieverRAG 链路toolsMCP 工具注册表 self.llm llm self.retriever retriever self.tools tools self.messages [] def run(self, user_query): self.messages.append({role: user, content: user_query}) for step in range(self.max_iterations): # 兜底避免模型自判不终止 # 第1步让 LLM 决定下一步动作工具调用 或 直接回答 response self.llm.chat(self.messages) if response.get(final_answer): return response[final_answer] # 第2步LLM 决定调用某工具从返回中解析工具名与参数 tool_call response.get(tool_call) if not tool_call: # 没有工具调用也无最终答案视为异常强制兜底 return self.llm.chat(self.messages [{role: user, content: 请直接给出最终答案}]) if tool_call[name] rag_search: result self.retriever.search(tool_call[arguments][query]) elif tool_call[name] mcp_tool: result self.tools[ tool_call[arguments][tool] ](**tool_call[arguments][params]) else: result f未知工具: {tool_call[name]} # 第3步把工具结果作为上下文回填给 LLM让模型重新决策 self.messages.append({role: assistant, content: f工具结果{json.dumps(result, ensure_asciiFalse)}}) return 达到最大迭代次数强制结束这段代码的逻辑是理解四者关系的关键几个参数与分支行为要展开说。max_iterations在源码里默认是 5这个值偏保守真实项目里 5~8 是常见区间但不是越大越好——每轮迭代都意味着一次完整的大模型调用和潜在的多次工具调用成本是指数级上升而且很可能把上文撑爆上下文窗口。更聪明的做法是同时监控messages的总 token 数接近窗口上限时不再让模型“思考”而是强制生成最终答案——这比单纯依赖迭代轮数兜底要可靠得多。关于工具名与参数的解析方式这段代码里做了一个极简的假设——response是一个结构化的 JSON 返回里面直接带 tool_call 字段。但真实世界没有这么干净因为模型输出的是普通文本它经常在“思考”段落里混入工具调用意图。生产级代码需要在这些文本里用正则抽取或再调一次小模型做结构化解析这一步相当考验工程能力。源码为了教学考虑直接给了结构化输出但必须意识到这中间藏了一层复杂度直接复制到生产环境会翻车。4.3 依赖关系总结槽谁是底座谁是增强谁是主控讲完代码我把四者的主从关系用一句话锁定LLM 是唯一的大脑RAG 是为大脑提供外部记忆的检索组件MCP 是为大脑提供双手的标准工具协议Agent 是控制大脑和双手之间协作节奏的主控循环。在这个结构里LLM 是底座没有它整个链路都不存在RAG 和 MCP 是互相独立的两个增强方向——RAG 管“知识获取”MCP 管“动作执行”Agent 是粘合层它负责协调一切。这个主从关系非常重要因为它直接决定技术选型如果 LLM 本身能力不足加再多的 RAG 和 MCP 也不会改善答案质量如果任务是纯粹的知识问答不需要跟外部系统交互那 MCP 就完全用不上。源码包里的注释也明确了这层依赖关系三个角色分别落在三个模块中彼此之间只有单向依赖。LLM 模块不依赖任何其他模块RAG 模块依赖 LLM 但反过来说不成立Agent 模块依赖前两者和工具集。这种依赖方向保证了系统架构的清晰也是源码包值得参考的工程设计之一。4.4 Agent 边界什么场景其实不需要 Agent跟 MCP 误用一样Agent 也有明显的边界。判断逻辑很简单如果一个问题“一轮问答就能解决”就不需要 Agent。比如“总结这段会议纪要”就是一个纯生成任务一次 LLM 调用就够了而“查一下昨天的销售报表、对比上周同期、写一封异常说明邮件”这就是一个典型的多步任务——需要查数据库、做对比计算、再调用邮件工具发送每一步依赖前一步的输出这时候 Agent 循环才有价值。源码包中 Agent 的定位就是“面向多步骤任务的控制框架”而不是替 LLM 提高单次回答质量。如果任务是一轮问答强行套 Agent 循环只会让响应时间变长、成本变高、出错率变多。个人经验是先把任务拆到最细粒度逐个步骤标注出“需要模型决策”还是“不需要——直接执行”只有至少一个步骤需要模型决策才值得引入 Agent。5. 四者混用的五大坑现象、原因与解决5.1 把 RAG 当记忆用上下文里堆了太多检索结果现象对话超过三五轮之后模型开始“遗忘”早期用户说过的信息但调试时发现上下文里明明有这些内容。原因RAG 每轮都会被触发检索而检索结果全部原样拼进上下文。多轮对话之后堆进去的内容越来越多但没有做剪枝旧信息被大量新内容稀释模型注意力被近期文本覆盖。这不是模型“忘了”是提示词里检索结果扎堆挤掉了关键信息。解决给 Agent 加一个“记忆管理模块”每轮保留系统设定、最近两轮对话、以及本轮检索结果历史旧内容优先裁剪。同时 RAG 的top_k不要固定可以在用户询问细节时调大寒暄时调小甚至是 0——不检索就不拼上下文。5.2 MCP Server 挂掉后 Agent 静默失败现象工具调用偶尔返回空结果Agent 不报错直接拿着空结果生成一段“根据工具返回当前无法获取信息”这类模棱两可的回答用户看到的是“答非所问”。原因Agent 代码里只处理了正常返回和异常返回两种分支没处理“工具未响应”这个分支。MCP Server 进程崩溃或连接超时协议层抛出的异常被上层 catch 住了但没有反馈给 LLM 决策循环而是直接继续生成。解决在 Agent 的 tool_call 分支里强制区分“成功结果”与“执行异常”。执行异常时必须把异常信息写进上下文回传给 LLM让它决定是重试还是换方案绝不能静默吞掉。这也是生产级 Agent 与 Demo 级 Agent 的分水岭。5.3 检索命中率低但归咎于模型问题在切分参数现象RAG 召回的 top-4 结果里经常有两三条完全不相关的内容人工复核发现正确信息明明已经在知识库里。原因查代码发现切分的chunk_size512, overlap64是全局固定值但知识库里的文档长短差异极大长段落被切分后语义断裂短段落命中后相似度又不够。很多人只看召回数值没意识到召回的瓶颈不在 embedding 模型而在切分阶段。解决切分参数改成“按文档类型分组配置”——长规范走段落感知切分短问答走整段向量化不搞一刀切。这个改动在源码包里没有体现属于我在实际项目里加的经验补丁但对提升回答质量立竿见影。5.4 把 Agent 退化成 if-else 工具路由现象Agent 代码跑起来像是一套硬编码的流程——用户无论问什么都走同一个工具调用序列甚至跳过 LLM 决策。原因这是过度工程化设计的反面开发者在 Agent 循环里把普遍决策写成了固定模板工具顺序固定、分支条件固定Agent 只剩下“按模板走流程”的执行器。这种情况下模型能力完全没发挥也用不着 Agent 框架。解决设计控制流时保留自由度。Agent 的每一步工具选择必须由 LLM 基于当前上下文动态决定而不是由开发者预先固定顺序。如果固定顺序能解决需求那就不是 Agent 场景写个 BPM 流程编排或者普通后端服务更合适。5.5 上下文窗口撞墙该砍 RAG 结果还是砍历史现象Agent 跑到第四五轮时请求直接被模型客户端拒绝报“上下文长度超限”业务链路中断。原因累积的对话历史、每轮的 RAG 检索结果、上一轮工具返回的长文本三者不加控地全部塞进上下文。尤其工具返回的内容往往是最占空间的一个 SQL 查询结果可能就是几千甚至上万 token。问题在于没有给“工具结果”设上限也没做压缩。解决两个策略并用。一是工具结果截断返回给 LLM 之前先裁剪到固定长度比如最多保留 1500 token超出部分提示“结果过长已截断如需完整数据请再次查询指定字段”二是历史压缩对早期对话做摘要后替代原文。这两种策略都比单纯调大模型上下文窗口省钱省时间。6. 验证一套关系链路是否健康从 trace 日志到三项硬指标聊完踩坑还是落回到怎么验证。源码包里有一套 trace 日志设计我把它扩展成了可复用的检查方案。核心思路是验证“LLM RAG MCP Agent 这套链路是否健康”不能只看最终答案质量要看整条链路上每个环节的指标。我把它们归纳成三项硬指标工具调用准确率、检索召回有效率、终止收敛率。工具调用准确率指的是 Agent 决定调用工具后工具名与参数是否被正确解析。这个指标要单独统计因为解析错误是最隐蔽的故障点——模型可能已经正确决策但解析环节把参数搞错了导致工具执行结果不对。统计方式是模拟构造 50 条用户问题让链路跑一遍人工打标工具调用对错与参数偏差。对齐率目标值应在 90% 以上如果低于这个值优先修解析逻辑而不是换模型。检索召回有效率是检查 RAG 是否真正为回答做了贡献。统计方式是把每轮 Agent 调用的检索结果记录到日志随后人工核对最终回答里是否真正引用了检索结果里的信息。有效率的含义是“被引用次数 / 总检索结果数”。如果有效引用率低于 40%大概率是切分质量差或 top_k 取值不合适。我从那以后每次调完参数都强制跑一遍 50 条 query 的回归集把有效率记下来对比而不是靠感觉判断。终止收敛率反映的是 Agent 循环的稳定性。统计方式是运行一次完整会话观察有多少比例在 5 轮以内正常结束并给出最终答案有多少轮空转或被 max_iterations 兜底强制打断。兜底占比超过 10%说明 LLM 常常无法自己判断任务已完成需要优化提示词或在工具返回结果里主动给出任务完成状态的提示。每次本地改动后我习惯的验证流程是三步走先跑代码静态检查再跑 50 条回归集对比三项指标最后手工构造五条极端场景空知识库、工具超时、上下文超限、多轮长对话、用户撤回提问验证边界。几个上游接口一改回归测试就会翻车帮我们抓住问题这比上线后看用户反馈要省心得多。希望这份拆解能帮你少走几个弯路——至少从结构上先想清楚四者谁主谁从再动手写代码会比自己摸着石头过河顺很多。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。