简介:面向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_size=CHUNK_SIZE, overlap=OVERLAP_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_embeddings=True) return np.array(vectors), docs def retrieve(query, vectors, docs, top_k=4): model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") q_vec = model.encode([query], normalize_embeddings=True) # 余弦相似度等价于归一化后的点积 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_embeddings=True这个参数容易被忽略,它本质上把向量都归一化到单位长度,这样后面做点积就是在算余弦相似度,比在代码里手动算 cos 函数更快也更省内存,源码里这个细节处理是对的。
2.4 什么时候不该用 RAG:参数化知识与微调的取舍
很多初学者有一个误解:凡是模型答不上来的问题,都靠 RAG 解决。这个方向只对了一半。RAG 适合的是“知识会持续更新、无法写死在模型参数里”的内容——比如企业内部规范、产品文档、项目沉淀;它不适合的是“模型本身能力不足”的任务——比如推理能力弱、指令遵循差,这类问题要么换更强的基础模型,要么做微调。
还有一个判断依据是知识的变与不变。某团队之前做过一个模拟项目,把公司人员名单和产品定价放进 RAG 里让它答“某产品多少钱”,效果极差,原因是这类信息不仅变,而且一个字段错了就算全错,向量召回在这种场景下的表现不如直接查数据库。RAG 的定位应该是“语义相关的软知识”,而不是“唯一事实的硬数据”。硬数据走传统数据库查询,软知识才走向量召回。源码包的代码里没有涉及这个区分,但实际调用链路里这是最影响体验的决策。
3. MCP 的角色:把工具调用标准化
3.1 从 Function Calling 到 MCP:为什么要多一层抽象
聊到 MCP 必须先提它解决的是哪一层问题。在没有 MCP 之前,让模型调用外部工具用的大多是 Function Calling 机制:开发者在代码里声明一个函数的 JSON Schema,模型根据用户输入决定“要不要调用某个函数、参数传什么”,然后由业务代码执行这个函数。这套机制本身能跑,但很快暴露出一个痛点——每接一个新工具就要写一套新的对接代码,参数格式、鉴权方式、错误处理各不相同,团队内部每个人写的风格还不一样。
MCP(Model 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 持有 Client,Client 连接 Server,Server 暴露工具。
源码里最值得注意的一点是消息格式。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 误用重灾区:把无状态接口硬套进 MCP
MCP 虽然解决了很多问题,但它不是万能胶。最常见的一个误用是“把该用普通 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:大模型接口;retriever:RAG 链路;tools:MCP 工具注册表 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_ascii=False)}"}) 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_size=512, overlap=64是全局固定值,但知识库里的文档长短差异极大,长段落被切分后语义断裂,短段落命中后相似度又不够。很多人只看召回数值,没意识到召回的瓶颈不在 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 条回归集对比三项指标,最后手工构造五条极端场景(空知识库、工具超时、上下文超限、多轮长对话、用户撤回提问)验证边界。几个上游接口一改,回归测试就会翻车帮我们抓住问题,这比上线后看用户反馈要省心得多。希望这份拆解能帮你少走几个弯路——至少从结构上先想清楚四者谁主谁从,再动手写代码,会比自己摸着石头过河顺很多。
本文还有配套的精品资源,点击获取