news 2026/10/18 16:07:30

LLM、RAG、MCP与Agent的关系剖析及工程化落地避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM、RAG、MCP与Agent的关系剖析及工程化落地避坑指南

简介:一套用于梳理LLM、MCP、RAG与Agent技术关系的轻量级代码包,面向AI应用开发者、架构师及希望快速理解智能体协作原理的读者。压缩包仅6KB,包含3个文件:HTML页面以结构图和对比表直观展示四者层级关系,inscode配置便于在线运行体验,gitignore用于项目文件管理。已有105人学习,适合在IDE Agent等实际场景中对照理解。资源以IDE Agent为例,清晰解析了LLM作为推理引擎、RAG进行外部知识增强、MCP提供工具接口、Agent统筹规划完成任务的完整链路,并配有可交互的代码示例,帮助读者从概念对比快速过渡到系统设计参考。

1. 先捋清四个词的地位:不是并列,是层层套娃

LLM、RAG、MCP、Agent这四个词被放在一起讨论时,最容易犯的错误是把它们当成四个并列的技术选型。实际上它们根本不在一个层面上:LLM是底座,解决"能说话";RAG是给底座接上外部知识,解决"有根据地说";Agent是在底座之上做规划与行动,解决"动手干";而MCP是Agent与外部工具之间的标准化通信协议,解决"手和脑怎么配合"。

拿开发里的老话来类比:LLM是CPU,RAG是给它加了一路外部存储,Agent是把调度器、执行器、任务队列全装上,MCP则是那套约定的总线协议。如果你正在做Agent系统、想在RAG管道上加工具调用,或者在MCP和Function Calling之间犹豫怎么选型,这篇笔记会按"底座→知识→行动→协议"的链路逐个拆开,每个环节给最小可运行的代码和参数写法,再附上我踩过的坑。读完之后,你至少能回答清楚一个问题:我的场景到底需要RAG、MCP、Agent里的哪几样,它们组合时谁先谁后。

2. 从LLM到RAG:为什么裸模型回答不了"你的内部问题"

2.1 RAG解决的三个具体问题:幻觉、知识过期、私有知识不可见

裸的LLM本质是一个参数化的概率模型,它对下一个词的预测来自训练时见过的数据分布。由此带来三个逃不掉的缺陷:第一是幻觉,当它不知道答案时仍然会流畅地编造,因为流畅本身就是它的优化目标;第二是知识过期,训练数据有截止日期,模型对新发生的事情一无所知;第三是私有知识不可见,你公司的内部文档、某个系统的接口变更、某次事故的复盘记录,这些东西永远不会出现在它的训练语料里。

RAG(Retrieval-Augmented Generation)的做法很直接:不在训练时改权重,而在推理时给模型塞证据。先从你的文档库里检索出与问题相关的片段,把这些片段拼进Prompt,再让模型基于这些片段生成回答。这样做的本质是把"模型记住了什么"换成"模型此刻能看到什么",把事实供给从参数挪到了检索管道。

这里有个常见误解:RAG只能做"知识问答"。实际上它解决的是"让模型在生成时能引用外部材料"这件事,凡是需要模型基于特定资料做判断的场景都能套:代码库问答、客服工单分类、审计报告摘要、故障排查建议。我见过最实用的用法是把历史故障记录做成RAG库,模型回答排障建议时能直接引用过去同类事故的处理步骤,准确率比纯靠模型记忆高出一大截。

2.2 RAG最小链路:嵌入、检索、拼接、生成的完整代码

一个最小可用的RAG链路包含四个环节:文档切分、向量化、相似度检索、结果拼接后送给LLM。下面是去掉工程封装后的骨架代码:

from sentence_transformers import SentenceTransformer import numpy as np # 1. 加载本地 embedding 模型 # 选型建议:优先考虑中文效果稳定的模型,维度以 768 或 1024 为常见 embedder = SentenceTransformer("本地或 HuggingFace 上的 embedding 模型名称") # 2. 文档切分:按固定长度切,保留 overlap 防止切断语义 def split_text(text, chunk_size=500, overlap=100): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) start = end - overlap return chunks # 3. 离线阶段:把每个 chunk 向量化并存入向量库 chunks = split_text("你的内部文档全文") chunk_vectors = embedder.encode(chunks, normalize_embeddings=True) # 4. 在线阶段:query 向量化后做余弦相似度检索 query_vector = embedder.encode("某个具体问题", normalize_embeddings=True) scores = chunk_vectors @ query_vector # 已归一化,点积即余弦相似度 top_k_idx = np.argsort(scores)[::-1][:5] # 取 Top-5 # 5. 拼接成 prompt,让 LLM 基于检索结果作答 context = "\n\n".join([chunks[i] for i in top_k_idx]) prompt = f"""请基于下面提供的资料回答问题。如果资料中没有答案,请直接说明。 资料: {context} 问题:某个具体问题 回答:"""

代码里的三个关键点要说明。split_text里的overlap参数决定了相邻chunk之间有多少重复字符,它直接影响检索质量:设太小,跨chunk的语义会被切断,问一个横跨两段的问题时检索不到完整上下文;设太大,向量库的冗余度上升,检索噪音变多。我一般按chunk_size的20%来设overlap,比如500字切块就留100字重叠。normalize_embeddings=True这一步不能省,它把向量归一化到单位长度,让点积直接等于余弦相似度,否则检索结果的排序会被向量模长干扰,长文档天然得分偏高。

这里还有一个常被忽略的点:系统里必须有"向量库"这个组件。上面的代码为了演示用了NumPy做暴力检索,生产环境应该换成真正支持持久化存储的向量数据库,不然每次重启都要重新embedding全部文档。选向量库时重点看它是否支持元数据过滤,这个能力后面做权限隔离时会用到。

2.3 RAG检索参数怎么调:TopK、相似度阈值、分块大小的取舍

RAG检索质量不是换个好模型就完事,参数的影响大到离谱。三个参数按优先级排:分块大小、TopK、相似度阈值。

分块大小决定检索的粒度。块太大,一个chunk里塞了多个主题,检索结果里噪音多,模型容易被无关信息带偏;块太小,单块信息量不足,模型无法从单块中得到完整答案。以中文技术文档为例,我习惯以300到600字为块,具体看文档的段落结构,优先按Markdown的标题层级切分,而不是纯按字符数硬切,这样能保住语义完整性。

TopK决定喂给模型多少个候选块。设太多会把模型上下文占满,token成本翻倍且注意力被稀释;设太少则可能漏掉关键证据。交互式问答场景TopK取5左右够用,长文档深度分析可以放到8到10。相似度阈值用来过滤低质量检索结果,但这里有个玄学:阈值设高了会召回为空,模型只能干瞪眼说"不知道";设低了等于没设。我的建议是先不看阈值,观察一轮TopK召回结果的实际得分分布,用分布中段的得分作为阈值起点,而不是凭空拍一个0.7或0.8。

3. Agent:把"会说话"变成"会办事"

3.1 Agent核心能力拆解:规划、记忆、工具调用

RAG解决的是"模型说什么有依据",Agent要解决的是"模型能做什么"。一个Agent系统最少包含三层能力:规划(把一个大目标拆成子步骤)、记忆(保留对话历史和中间结果)、工具调用(执行具体动作并读取结果)。

规划层的实现从简单到复杂差别很大。最简单的规划是固定流程,比如"先检索再总结";进阶是让模型自己写Plan再执行;再进阶是让模型根据执行结果动态调整计划,这是ReAct(Reasoning + Acting)的典型形态。很多人把Agent想复杂了,一上来就上复杂的状态机或任务图,结果模型还没跑通就先把系统拆垮了。我建议从ReAct的最小循环做起,跑通后再逐步加记忆管理和任务队列。

记忆是Agent和普通对话最大的区别。普通对话上下文只存在于一次请求里,Agent需要跨步骤记住"我已经查过什么""下一步还差什么"。实现上最省事的方案是直接把历史步骤塞回Prompt,但必须设上限,否则多轮之后上下文爆炸。工具调用是Agent的立足之本,它决定了Agent是"只动嘴"还是"真动手"。

3.2 ReAct模式最小代码:一个能查资料和算数的Agent

下面是一个可跑的ReAct循环骨架,它模拟了Agent"思考→行动→观察→再思考"的核心结构:

def react_loop(user_question, max_rounds=5): messages = [{"role": "user", "content": user_question}] tool_result = "" for round_idx in range(max_rounds): # 1. 让 LLM 决定下一步动作 # 注入当前任务、可用工具清单、以及上一轮的工具执行结果 prompt = f"""你是一个能调用工具的助手。当前可用工具: - search_doc(query): 查询内部文档库 - calculator(expression): 计算数学表达式 - no_action(): 认为已有信息足够回答问题 已执行结果:{tool_result} 请输出下一步动作,格式必须是 ACTION:工具名(参数) 或 FINAL:最终答案。""" response = llm_call(prompt + "\n问题:" + user_question) print(f"Round {round_idx}: {response}") # 2. 解析模型输出,决定是调用工具还是结束 if response.startswith("FINAL:"): return response.replace("FINAL:", "") # 3. 简单解析工具名和参数,并执行 action = response.replace("ACTION:", "").strip() tool_name, arg = parse_action(action) if tool_name == "search_doc": tool_result = search_doc(arg) elif tool_name == "calculator": tool_result = str(eval(arg)) # 仅限受控环境使用 else: tool_result = "未知工具,请重试" return "超过最大轮数,未能得到最终答案" result = react_loop("帮我查一下去年第四季度的销售额,并计算增长率") print(result)

这个循环的精髓在于:每一步的tool_result会拼进下一轮的Prompt,模型据此判断是继续调用工具还是收尾给出答案。max_rounds这个参数必须设,它是Agent循环的保险丝——模型逻辑一旦绕晕,就会在原地反复调用同一个工具,没有轮次上限就是死循环烧钱。我实际使用时习惯设为5到8轮,超过就强制让模型基于已有信息作答,避免无限循环。

这里面最容易翻车的点是输出格式解析。parse_action对模型输出的格式要求极高,模型只要多输出一句解释,解析就会失败。实践中有两种对抗方式:一是要求模型严格按JSON输出并配合Function Calling的Schema约束,二是给足够强的提示词固化输出格式。前者可靠性高得多。

3.3 工具调用的关键:Function Calling的参数约束

Agent的工具调用在工程上要回答三个问题:模型知道有哪些工具吗?模型知道该填什么参数吗?参数填错了怎么处理?这三个问题靠Function Calling机制来解决。核心是把每个工具的描述和参数Schema喂给模型,让模型在生成回复时顺带生成结构化的调用请求。

{ "tools": [ { "type": "function", "function": { "name": "search_doc", "description": "在内部知识库中检索与关键词相关的文档片段", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "检索关键词,尽量精炼,不超过20个字" }, "top_k": { "type": "integer", "description": "返回结果条数", "default": 5 } }, "required": ["query"] } } } ] }

参数Schema写得越细,模型填参越准。description字段不能写"查询文档",要写清楚"在内部知识库中检索与关键词相关的文档片段",模型才能判断什么场景该用这个工具。更重要的是required字段要显式声明,不然模型可能漏掉必填参数,返回一个残缺的调用请求,你的工具函数还得做兜底校验。参数类型也要严格,top_k声明为integer,模型一般会遵守,但字符串"5"传进来时你的服务端要有容错。

另一个实战心得是工具描述要带着"什么场景不该用"。比如搜索工具的描述里加一句"当问题涉及计算结果时不要使用本工具,应使用calculator",能明显降低工具误调用的概率。

4. MCP:把Agent接工具的规则拉齐成一条标准协议

4.1 没有MCP之前:每个工具一套协议,Agent端接口爆炸

当Agent只有一两个工具时,手写带参调用完全没压力。但工具一旦多起来——数据库、文件系统、浏览器、企业内部系统、第三方API——问题就来了:每个工具都要定义自己的调用格式、认证方式、错误返回结构。Agent每接一个新工具就要写一套适配代码,工具方每改一次接口,Agent端就要跟着改。

MCP(Model Context Protocol)的定位就是解决工具接入的标准化问题。它定义了一组通用的协议原语,把工具的发现、调用、内容获取统一成标准接口。你可以把它理解为HTTP之于Web:有了HTTP,浏览器不用为每个网站单独写一套通信逻辑;有了MCP,Agent不用为每个工具单独写一套调用逻辑。

MCP把能力抽象成三类:Tools(可执行动作)、Resources(可读取的内容)、Prompts(可复用的提示模板)。Agent和MCP Server之间通过JSON-RPC格式通信。理解MCP的关键是:它不关心工具内部怎么实现,只关心"我把调用请求发过去,你把结构化结果还回来"这个协议约定。

4.2 MCP Server最小实现:tools/list、tools/call、resources

一个最小的MCP Server核心要实现三个方法,下面用简化的Python代码示意:

import json # 工具注册表:所有可用工具的元信息都在这里 TOOLS = { "search_doc": { "description": "检索内部文档库", "parameters": {"query": {"type": "string", "required": True}} }, "calculator": { "description": "执行数学计算", "parameters": {"expression": {"type": "string", "required": True}} } } def handle_request(raw_request): req = json.loads(raw_request) method = req.get("method") # 1. tools/list 是客户端(Agent)要求列出可用工具的入口 if method == "tools/list": return {"tools": [ {"name": name, "description": info["description"], "parameters": info["parameters"]} for name, info in TOOLS.items() ]} # 2. tools/call 是 Agent 真正调用工具的入口 if method == "tools/call": tool_name = req["params"]["name"] arguments = req["params"]["arguments"] if tool_name not in TOOLS: return {"error": {"code": -32602, "message": "unknown tool"}} # 实际分发到对应的执行函数 if tool_name == "search_doc": return {"result": search_doc(arguments["query"])} if tool_name == "calculator": return {"result": str(eval(arguments["expression"]))}

MCP是双向通信的,Agent不仅能调工具,还能订阅资源变化。resources/list和resources/read这两个方法用来暴露数据源,比如把某个配置文件、数据库里的表结构定义成Resource,Agent可以在回答问题前主动读取。这套机制和RAG的区别在于:RAG的检索管道是固定的、由开发者预先编排的,而MCP的Resource是运行时可发现的,Agent可以自己决定"回答这个问题之前我需要先读什么"。

从代码结构上看,MCP Server本质上是一个独立的服务进程,通过stdio或HTTP与Agent通信。这样工具的能力边界和Agent的推理逻辑被硬隔离了:工具代码在MCP Server里,Agent只负责发请求和处理结果。某个工具挂了不会拖垮Agent主进程,这是手写工具调用链路做不到的好处。

4.3 MCP在Agent链路里的位置:一次请求从规划到回包的生命周期

把前面三章的内容串起来看,一次完整的Agent请求是这样流转的:

模型收到用户问题后,先看到两样东西:一是可用工具的清单(来自MCP的tools/list),二是可读取资源的信息(来自MCP的resources/list)。模型判断"要回答这个问题,我需要先检索文档"——于是输出一次tool调用请求,MCP Client把它封装成JSON-RPC发往MCP Server,Server执行search_doc并返回结果。模型看到结果后,决定是继续调用工具还是直接给出答案。

这里的关键认知是:RAG和MCP不是互斥的,而是可以嵌套的。你可以把向量库的检索能力封装成一个MCP工具,Agent在需要外部知识时主动调用它。这和传统的RAG最大的区别在于"主动权":传统RAG是固定的Retrieve-then-Generate流程,Agent MCP是模型自己决定何时检索、检索几次。这也意味着Agent系统里,RAG不是消失了,而是降级成了MCP工具矩阵里的一个普通成员。

5. 关系落地避坑:四个概念混用时最常见的五个翻车点

5.1 现象一:把RAG当成Agent用,检索到了数据却不会执行动作

某次给一个内部工具做升级,团队把RAG管道和工具调用混在一起,认为"模型能根据检索结果回答问题"就等于"模型能执行操作"。结果用户问"请帮我把上个月的报表导出并发送给相关同事"时,模型只回答了一段文字说明应该怎么做,完全没有触发任何实际动作。

原因:RAG只能改变模型生成内容时的依据,它没有任何执行能力。Agent的核心是规划加工具调用,RAG只是给Agent提供决策依据的一个组件。解决:想清楚系统里哪些步骤需要"动"——发送邮件、执行脚本、修改配置——这些动作必须封装成工具交给模型调用,不能指望靠检索结果触发。

5.2 现象二:MCP工具全部就绪,但模型回答质量依然很差

把工具全部接入MCP后,模型确实能调用了,但回答的内容质量还不如不带工具的纯RAG。查日志发现,模型每次调用工具后只拿回了结果,却丢失了问题本身的上下文,回答时就是简单复读工具结果。

原因:工具调用协议把上下文割裂了。Agent的输出里必须把原始问题、当前步骤、工具返回结果三者拼接清晰,保证每轮推理时模型"记得自己为什么走到这里"。解决:在Agent的Prompt里明确要求"请基于用户原始问题、已获取信息和本次工具结果综合作答",同时把多轮对话历史截断后保留核心信息。

5.3 现象三:Agent循环失控,没有终止条件的血泪教训

模拟项目X在联调时出现过一次生产事故:测试Agent回答一个多步骤问题,它在一个错误工具上反复调用,每次结果都一样但它就是不终止,十来分钟烧掉了大量token,直到人工介入杀进程。排查下来发现系统里漏了最大轮数限制。

原因:模型在不确定自己是否完成目标时,倾向于"多做一步确认",而没有轮次上限的循环等于给它无限次确认的机会。解决:每个Agent循环代码里必须有max_rounds参数,并且要在Prompt里显式告诉模型"如果某工具连续两次返回相同结果,直接基于现有信息作答"。这个兜底规则非常有效。

5.4 现象四:相似度阈值拍脑袋定,检索质量时好时坏

开发初期给RAG设了相似度阈值0.75,上线后发现有的查询完美召回,有的查询一个结果都召不回。观察了一段时间的相似度分布才发现,不同查询的得分差异极大,固定阈值根本没有普适性。

原因:不同问题的检索难度不同,得分分布自然不同,固定阈值是拿一个静态标准去卡动态场景。解决:先做一轮测试集检索,画出得分的分布直方图,把阈值的区间定在分布中段的"洼地"上。另外更实用的做法是"不设固定阈值,而是设阈值下限",低于下限才过滤,同时用TopK兜底保证有结果可用。

5.5 现象五:上下文越塞越多,Token预算和注意力一起爆炸

Agent加RAG加MCP的组合下,每轮请求要把对话历史、检索结果、工具描述、工具返回结果全拼在一起。系统上线后对话轮次超过十次就出现诡异行为:模型开始无视检索结果,回答风格偏向"凭印象发挥"。

原因:把太多上下文塞进Prompt,模型注意力被分散,关键证据被淹没在无关信息里。这正是"上下文越长,注意力越稀释"的典型场景。解决:对话历史做滑动窗口,只保留最近几轮核心内容;检索结果只拼接和当前问题最相关的Top-3块;工具返回结果在投递给模型前先做截断和摘要。上下文不是越多越好,而是越相关越好。

6. 一个能跑通的关系验证Demo:RAG检索→Agent决策→MCP工具调用

用三个组件的组合做一个最小验证,代码上做个拼装,确认整条链路能闭环跑通:

def hybrid_agent(query): # 1. RAG 层:先检索内部知识库 doc_snippets = rag_retrieve(query, top_k=3) # 2. Agent 规划层:让模型决定是否需要额外调用工具 plan_prompt = f"""问题:{query} 已有的参考资料:{doc_snippets} 如果参考资料足够回答,直接输出 FINAL:答案; 如果资料不足,输出 ACTION:search_doc(补充关键词) 或 ACTION:calculator(表达式)。""" decision = llm_call(plan_prompt) # 3. MCP 层:如需调用工具,统一走 MCP Server if decision.startswith("ACTION:"): tool_name, arg = parse_action(decision) tool_result = mcp_call(tool_name, arg) # 统一走 MCP 协议 # 把工具结果回填给模型生成最终答案 return llm_call(f"基于资料:{doc_snippets} 和工具结果:{tool_result},回答问题:{query}") else: return decision.replace("FINAL:", "")

这个Demo的价值不在于代码量,而在于验证了一个问题:哪些能力属于RAG(知识供给),哪些属于Agent(决策控制),哪些属于MCP(工具通信)。跑通之后随便换一个工具或换一个知识库,Agent主逻辑不用动,MCP协议保证了接口不变。

我个人的习惯是把这个混合架构拆成三层来排障:先单测RAG的检索质量(看召回的块相不相关),再单测MCP的调用回包(看工具执行正不正确),最后才测Agent的决策链路(看模型有没有正确编排)。层与层之间踩坑时,责任边界立刻就能划清,不会互相甩锅。这套架构最值得投入的点,是把工具接入的成本从一个"每次写死"的定制开发,变成MCP协议下的标准动作。

希望这篇笔记能帮你在选型和落地上少走几条弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 16:05:00

openSMILE-3.0-win-x64音乐特征提取实战指南

简介&#xff1a;openSMILE-3.0-win-x64安装包是面向音频信号处理、语音情感识别与音乐信息检索研究者的专业特征提取工具&#xff0c;专为Windows平台优化&#xff0c;开箱即用&#xff0c;免编译配置。资源共509个文件&#xff0c;涵盖128个说明文档&#xff08;txt/html&…

作者头像 李华
网站建设 2026/10/18 16:03:55

pvt_v2_b3部署实战:ONNX与TorchScript导出、量化加速的完整步骤

【免费下载链接】pvt_v2_b3 项目地址&#xff1a; https://ai.gitcode.com/OpenGVLab/pvt_v2_b3 点击查看 免费下载 pvt_v2_b3 是 Pyramid Vision Transformer v2&#xff08;PVTv2&#xff09;的 base 版本&#xff0c;一款轻量级、高性能的层级式视觉 Transformer 骨干网络。…

作者头像 李华
网站建设 2026/10/18 16:00:25

基于VGG的图像风格迁移算法实现与Web系统应用实战

很多人一提深度学习毕设&#xff0c;第一反应是“买个GPU跑个现成模型&#xff0c;调几个参数&#xff0c;写两段代码就能交差”。真到选题答辩的时候才发现&#xff0c;要么网上开源的堆了一堆但讲不清原理&#xff0c;要么自己动手实现连个像样的结果都跑不出来。如果你也在计…

作者头像 李华
网站建设 2026/10/18 16:00:23

OpenSubject多主体定制生成实战:从数据到推理的完整拆解

做生成模型的人大多有个共识&#xff1a;单主体的定制生成早就不是难题&#xff0c;多主体同框才是真正的修罗场。OpenSubject 就是冲着这个痛点来的——它带着 250 万样本的配对数据&#xff0c;把“多个主体同时生成、还能自然编辑”这件事从玄学变成了可复现的流程。这篇文章…

作者头像 李华
网站建设 2026/10/18 15:58:38

Agent Skills实战:五个高效技能解锁智能体自动化工作流

最近在做一个自动化报表项目&#xff0c;折腾来折腾去&#xff0c;最后发现真正让我省力的不是某条提示词写得多精妙&#xff0c;而是我给自己智能体配了一套“技能包”——就是现在大家常说的 Agent Skills。这东西说白了&#xff0c;就是给智能体写一份带目录的操作手册&…

作者头像 李华