做个大模型应用有一段时间了,前前后后也踩了不少坑。之前几篇把模型能力、提示词工程、微调都聊了一遍,这篇集中说下目前我这边落地最多的三样东西:RAG、Agent、还有 Function Calling。很多人容易把这三者混为一谈,其实它们解决的是不同层的问题。简单说,RAG 解决“模型不知道”的问题,Function Calling 解决“模型做不了”的问题,Agent 解决“模型记不住、想不全”的问题。一套完整的大模型应用架构,基本就是这三样东西的排列组合。
这篇文章我不会去摘抄官方文档,就按我实际搭过的、拆过的、修过的系统来讲,把设计思路、实现细节、容易翻车的点都摊开说。
1. 内容整体设计与思路拆解
1.1 这套架构到底在解决什么问题
先聊一个常见场景。某公司要做一个内部知识库问答机器人,员工问“报销流程是什么”,模型没办法回答,因为训练数据里根本没有你们公司的报销制度。直接让模型硬答,它会编一个很像样的流程出来,谁信谁倒霉。这时候就需要 RAG,把公司文档切碎、向量化、存起来,用户提问时先从库里检索相关内容,拼进上下文里再让模型回答。
再看另一个场景。员工问“帮我把上周的报销单状态改成已提交”,这已经不是回答问题,而是要操作系统了。RAG 管不了这件事,因为答案不在文档里,在业务系统里。模型本身也不会调用“改状态”这个接口,它连你的接口长什么样都不知道。这时候就需要 Function Calling,让模型理解用户的意图,并输出一个结构化的调用指令,由程序去真正执行这个指令。
最后说 Agent。假设这个问答机器人接入了报销系统,用户说“帮我查一下上个月所有未报销的发票,顺手算一下总额,超过五千就提醒我走线下审批”。这里涉及三步操作:查询发票列表、计算金额、判断是否触发提醒。每一步之间还有依赖关系,后一步要用前一步的结果。这不是一次 RAG 检索加一次函数调用能搞定的,需要模型能够自己拆解步骤、逐步执行、中途纠错,这就是 Agent 的活。
所以你看,这三样东西其实是一条链路:RAG 给模型补充知识,Function Calling 给模型接上工具,Agent 把知识和工具编排成一套完整的执行流程。只做 RAG 的问答机器人是“只读的”,接上 Function Calling 之后变成“可操作的”,再加上 Agent 的规划能力之后,才算是真正的“智能体”。
1.2 为什么要把这三块放在同一套架构里
我已经见过不少团队,一开始只做 RAG,跑通之后发现业务方要求越来越多——要操作数据、要调用接口、要处理多步逻辑,于是又单独去接 Function Calling,结果两套代码各写各的,你调你的数据库,我调我的 API,最后问机器人“报销单怎么回事”,它要么只答文档内容,要么只会调接口,两边几是对不上的。
放在一起的好处在于,它们互补之后能覆盖真实业务的大部分场景。一个完整的对话流程经常长这样:用户提问 -> 系统判断需要检索知识库 -> 调 RAG -> 模型针对检索结果生成回答 -> 回答中提到要执行某个动作 -> 调用 Function Calling -> 执行结果再交回给模型 -> 模型给出最终回复,甚至整个流程还带有循环。
把这三件事放到同一个框架里,就像搭积木一样各自负责一块,定义好接口,就可以组合出很多种玩法。我的建议是:不要做“三选一”,而是从一开始就按“RAG 做知识底座,Function Calling 做工具接口,Agent 做任务编排”这个方向去设计。
1.3 选型时的几个关键取舍
在动手写代码之前需要想清楚几个问题,这直接关系到后面的开发效率。
第一个问题:是自建还是用封装好的框架。我见过不少团队一上来就选了重量级框架,一顿配置猛如虎,结果调试的时候根本不知道框架内部发生了什么。我的经验是:第一版尽量自己拼,不要把目光全部放在框架封装上。自己做一版 RAG 链路,无非就几步:加载文档、切分、向量化、存库、检索、拼提示词。这几步自己写一遍,你就能知道每一步在干什么。等流程跑通了,再决定要不要换框架。
第二个问题:模型选多大。RAG 的检索链路基本不依赖大模型的推理能力,Embedding 模型很小也能跑得很好。但 Agent 的规划能力对模型的推理要求极高,小模型经常在拆解步骤的时候自作聪明,把简单问题拆成一堆复杂的无用操作。如果做的是复杂 Agent,别在模型上省成本,选当前能力最强的通用模型更稳妥。
第三个问题:Function Calling 要不要依赖厂商特定的格式。不同模型厂商的函数调用接口格式不太一样,有的用 JSON Schema,有的用自定义格式。第一版可以直接适配你主力模型的格式,但建议在代码层面把“解析函数调用结果”的部分抽象出来,留一个转换层。我后面踩过一个坑,底层换了模型,结果函数调用结果的解析代码全要重写,就是因为一开始没有抽象。
2. 核心细节解析与实操要点
2.1 RAG 链路里最影响效果的不是模型
很多人刚接触 RAG 的时候,以为效果好不好取决于大模型强不强,其实不是。RAG 的效果上限大部分由检索质量决定,模型只是在检索结果的范围内做总结归纳。检索不好,模型再强也只能基于错误的信息生成看上去通顺但实际失真的回答。
整个 RAG 链路里最值得花时间调优的是三块:文档切分、向量检索、重排序。
文档切分是第一个决定效果的点。传统的固定长度切分虽然简单,但会带来一个经典的“切断语义”的问题:一个完整概念被拦腰截断成两段,检索时匹配到半截内容,模型拿到的信息不完整,答出来的内容自然也是残缺的。我们现在用的策略是“结构化优先”——先按文档本身的层级结构(标题、段落、列表)切,层级下的长段落再按固定窗口做二次切分。这样既保留了语义完整性,又能把超长段落拆到合适的长度。切分粒度上,我一般控制在 300 到 500 个 token 一段,这是一个相对安全的范围。太长,检索命中后塞进上下文的噪音太多;太短,语义不够完整,单个片段很难表达一个完整的知识点。
向量检索环节,Embedding 模型的选择其实比想象中更重要。第一,注意它支持的输入长度。很多 Embedding 模型对输入长度有限制,你的文档片段动辄几百 token,直接用可能会被截断,检索效果直线下降。第二,模型的语言能力匹配要看好。选一个能同时支持中英文的模型,不然遇到中英混合的业务文档时会非常折腾。第三,向量维度直接决定了存储成本和检索耗时的增长曲线,维度越高,成本越高,但不是越高就一定越好。实测下来,在内部知识库场景下,768 维的向量已经能扛住大多数需求,不必一上来就追求高维模型。
重排序是很多人漏掉的一环。向量检索返回的 TopK 结果里,通常有相当一部分是“沾边但不精准”的内容。直接用这些结果去生成回答,模型会被噪音信息干扰,回答的准确性会明显下降。增加一个重排序模型对候选结果做精细打分,取前 2 到 3 条用于生成,是提升回答准确率性价比很高的方式。我们的线上配置是:向量检索召回 20 条 -> 重排模型精排 -> 取前 3 条拼进上下文。
2.2 Function Calling 的关键不是“调用”,是“描述”
Function Calling 这个名字挺有迷惑性,它本身并不是真的去“调用”你的函数,模型做的事情只是根据对话内容,输出一个“我想调用某个函数,参数是什么”的结构化结果。真正去执行函数的是你的程序。模型的能力边界在于:能不能准确判断该调哪个函数、能不能把用户话里的关键信息转成正确的参数。
所以整套 Function Calling 的效果,很大程度取决于你给模型看的“函数描述”写得怎么样。我在迭代中最常见的问题是开发者在函数描述里写得极其简陋,比如只写一个函数名和几个参数名。模型只能靠猜。猜对了是运气,猜错了是常态。
写函数描述有几个实用的经验:
一是描述的详细程度要跟系统复杂度匹配。简单函数可以一句话描述清楚,但业务复杂的函数一定要把使用条件、参数格式、边界情况写明白。示例胜过描述。同一个函数,你写上“当用户询问已提交但未审批的报销单时使用”,效果远好过只写“查询报销单”。
二是参数的描述同样要给足信息。不仅要有参数名和类型,还要写清楚这个参数接受什么格式,单位是什么,枚举值有哪些。曾经遇到过一个问题,用户说“帮我查下上周的数据”,程序收到的参数值是“上周”,而不是系统要求的日期范围,查出来的结果当然不对。后来在参数描述里明确写了“请将时间短语转换为具体的开始日期和结束日期,格式为 YYYY-MM-DD”,这个问题基本绝迹了。
三是少量示例也很重要。在系统提示词里给模型看两到三个完整的问答到函数调用的例子,比单纯在函数定义里写描述更管用。模型是少样本学习的好手,给它一个标准答案,它就能照猫画虎。
2.3 Agent 的循环机制与可控性设计
Agent 的核心运行模式说起来也不复杂,就是“思考-行动-观察”的循环:模型看用户的请求,决定要调用什么工具,程序执行工具并把结果返回给模型,模型观察到结果后决定下一步干什么,直到它认为任务完成或者彻底卡住。
但就是这个看似简单的循环,落地时容易出问题。最常见的就是“失控循环”。模型发现一个工具的执行结果不符合预期,或者执行过程中出现错误,它可能会反复调用同一个工具,重试同样的参数,一遍两遍三遍,调用次数和成本都快速上涨。我们曾经在测试环境看日志,一个简单任务跑了二十多轮工具调用,最后模型自己都忘了最初要干什么了。
所以可控性设计是 Agent 工程化的关键。有几点我每次都会强调:
第一,给循环设置上限。不管任务是否完成,达到最大迭代次数就必须停下来,向用户返回当前的中间结果和状态说明。10 轮左右一般是一个比较合理的上限,业务复杂的任务可以放宽到 15 轮,但一定要有上限。
第二,在每轮执行后做“观察摘要”。工具返回的内容可能很长,把长结果原样塞回给模型,会占用大量上下文空间,而且后续几轮里模型会越来越难从过长的历史中发现关键信息。用一个短小的提示词让模型把工具执行结果压缩成要点,再放回上下文,能明显提升多轮场景下的稳定性。
第三,让 Agent 在关键节点“停下来向用户确认”。不是所有动作都适合由 Agent 自动执行。涉及提交数据、删除数据或高成本操作的环节,应该在调用工具前输出确认信息,等用户给了明确指示再继续。这是工程层面必须守住的底线,不然 Agent 在自动执行过程中做出不可逆操作,后果很难处理。
2.4 三者的协作模式:路由、编排与融合
在实际代码里,这三者不会各自独立运行,而是要设计一套协作机制。
我常用的模式是“路由 + 工具 + 循环”。对话进来自动触发路由,意图判定大概分三类:需要检索知识库的走 RAG 链路;需要操作业务系统的走 Function Calling 链路;需要多步操作的走 Agent 循环链路。路由本身可以由一个轻量级的意图识别模块来做,也可以由大模型自己判断。用小模型做路由分类,把大模型留作推理,能省不少成本。我见过不少人让主力模型做所有环节,连“用户是不是在问问题”这种分类任务都要大模型回答,成本和延迟都白白增加了。
RAG 和 Function Calling 之间也不是互斥的。经常有这种情况:用户问完报销流程(检索知识库),接着问“那我现在就提交行不行”(准备操作)。很多系统在这里就直接卡住了。正确的做法是:Agent 在规划过程中发现当前步骤需要背景知识,就先调用 RAG 工具获取资料,再基于这些资料继续执行函数调用。换句话说,RAG 本身也可以被封装成一个工具,供 Agent 在需要时调用。
3. 实操过程与核心环节实现
3.1 从零搭建一个可用的 RAG 检索服务
这一节给一个可以直接参考的最小实现方案。
第一步,文档接入与清洗。不同格式的文档(PDF、Word、Markdown、HTML)先统一转为纯文本。转换过程中要处理掉页眉页脚、表格噪声、无意义的换行符。代码层面我当时是把解析好的内容按标题层级组织成一个树状结构,再递归遍历这个树去生成切分片段。这样切出来的块天然带着上下文信息,比如它属于哪个章节,检索到某个片段后可以追溯到来源。
第二步,切分。按 2.1 里说的策略实现:先用标题层级做粗切分,段落长度超限的再按句子边界做二次切分。
第三步,向量化与入库。选择 Embedding 模型后,批量生成向量,写入向量数据库。写入时我建议连元数据一起存:来源文档、章节标题、片段序号、原文内容。这些元数据在后面做溯源展示和筛选时非常有用。别只存一个向量和一段文字,等要按文档筛选的时候就后悔了。
第四步,检索链路封装。查询时对用户问题做向量化,执行相似度检索,取出 Top 20,再交给重排序模型精排,最终取前 3 条。这个检索函数要做得独立一点,后续换 Embedding 模型或换重排模型时,尽量只改封装内部,不对外暴露接口产生影响。
第五步,生成。把检索结果按固定模板拼进系统提示词,注明“以下是从公司知识库中检索到的资料,请严格基于资料内容回答。如果资料中找不到答案,请直接说明,不要自行编造”。模板的措辞会影响模型的守规矩程度,实测下来,“不要编造”这类明确指令比“仅供参考”有效得多。
3.2 Function Calling 的协议定义与执行器设计
目前主流的函数调用方式,是让模型输出一个包含函数名和参数的结构化 JSON 数据。代码上大致分两块:一块负责把系统里可用的函数定义列表传给模型,另一块负责接收模型的输出并路由到真正的执行函数。
函数定义列表的格式,业内已经基本形成了以 JSON Schema 为底座的风格。每个函数包含名称、描述、参数对象的字段定义。这块在前面讲过,描述是命门,这里不再展开。
执行器的设计有个小技巧:做一个“注册表”,用装饰器把函数注册进来,定义列表也能自动生成。也就是说,你写一个提供给模型调用的函数,同时把这个函数的名称、描述、参数 Schema 挂上去,执行器启动时自动收集所有注册过的函数。之后加新工具只需要新写一个函数并注册,协议层和执行层都不用手动改。这样做的好处是系统是可扩展的,后续 Agent 要接新工具时,开发成本非常低。
执行器还需要处理一个问题:模型输出的参数值不一定严格符合你的预期。比如系统接口要求的是数字类型的金额,模型可能给一个字符串“128.5”,或者日期明明要求 YYYY-MM-DD,模型给了一个可读性很好的自然日期表达。所以在真正执行函数之前,要对参数值做一次清洗和校验。我的做法是写一个参数校验层:类型不对的做转换,格式不对的尝试标准化,实在无法解析的就直接返回给模型,说明参数无效并指示它重新生成调用。
3.3 一个最小可用的 Agent 运行框架
我们当下线上用的 Agent 框架,代码逻辑不会特别复杂,核心就是前面说的循环。我把最小框架拆成几个模块来说明:
第一个模块是上下文管理器。它负责维护整个会话的完整上下文,包括最初的任务描述、每一轮的工具调用记录、每一步的观察摘要。这里有几个细节需要特别注意:不用的中间步骤摘要要不要保留?我的做法是保留最近三轮的完整摘要,更早的步骤压缩成一行记录,防止上下文过长导致模型“忘事”。
第二个模块是工具调度器。它维护所有可用工具的列表,在 Agent 循环中把当前可用的工具描述传给模型。注意,“当前可用”是个动态概念。第一步不需要的工具就不传给模型,只暴露任务当前阶段需要的工具,能显著减少模型误调用的情况。
第三个模块是控制逻辑。模型生成结果后,程序判断结果类型:如果是函数调用请求,就执行调用、生成观察摘要、继续循环;如果是最终回复,就结束循环返回结果;如果超出最大轮次,就强制结束,并带上“任务尚未完成”的说明。这个判断逻辑要写在统一的地方,不要在多个子模块里各写各的,不然调试会很痛苦。
第四个模块是安全拦截器。在执行任何函数之前,通过它有规则地检查这个操作是否属于敏感操作或高风险操作,如果是,则返回“需要用户确认”的状态。这个模块的优先级别极高,应该放在调用之前,不要依赖模型自己去判断敏感与否。
这个框架第一版跑通之后,后续的优化基本都集中在提示词、工具定义、上下文压缩策略这几个方向,框架骨架本身很少再动。
3.4 组合场景的完整链路走一遍
我用一个组合场景把整个架构串起来。
用户问:“帮我查下今年的年假还剩几天,如果不足三天的话,顺便给我一份最近一周需要提交的报告清单。”
这个请求进到系统后的运行流程是这样的:
第一步,路由模块判定这是一个需要多步操作的 Agent 任务,不是单纯的知识问答。
第二步,Agent 上下文管理器启动,系统提示词写入任务目标,暴露第一批可用工具:查询年假接口、查询报告清单接口。
第三步,模型判断需要先查年假,输出一个调用“查询年假”函数的指令,参数是当前用户标识。程序执行接口调用,返回剩余天数。
第四步,工具结果被塞进观察摘要:该用户年假剩余 2 天,不足三天。模型看到这个结果后,决定继续调用“查询报告清单”函数,参数里带上了当前日期往前一周的日期范围。
第五步,报告清单结果返回后,模型汇总两边的信息,发现年假不足三天且有一份报告已经逾期未提交,于是生成最终回复,说明情况并给出提醒。
整个流程涉及两次函数调用、一次条件判断,全程没有 RAG 参与,因为所有信息都来自业务接口。但如果这个用户接着问“年假和调休有什么区别”,路由会瞬间跳到 RAG 链路,去知识库检索相关政策文档,再基于检索结果回答。
4. 常见问题与排查技巧实录
4.1 RAG 检索命中但回答依然不准
这是刚开始做 RAG 时遇到最多的问题。检索系统明明返回了相关文档,模型也回复了,但内容跟用户问的不在一个频道上。
排查思路一般是从检索结果倒查。先把用户的问题和检索返回的 Top 3 片段打印出来,人肉看一遍,看看片段本身跟问题之间是不是真相关。很多时候问题出在“向量相似但语义无关”上,比如用户问“报销单多久能批下来”,向量检索返回的是“报销单的填写规范”,关键词重叠但答非所问。这种情况要调整切分策略,让每个片段包含更完整的情境信息。
如果检索片段看起来是相关的,但模型还是答得不对,那问题多半出在提示词组织上。检查一下片段拼接的格式,有没有明确告诉模型“只能依据资料回答”。还要检查检索片段是不是被埋在一大堆系统指令和示例下面,模型被长篇的指令干扰,反而忽略了你最想让它在意的资料内容。我的经验是:检索片段放在资料区,用清晰的分隔符标出,并且在资料区之前加上一句话“如果你需要回答以下问题,请以资料区内容为准”。
4.2 Function Calling 里的“张冠李戴”
一次业务中遇到这种情况:用户说“帮我看看上周采购单的付款状态”,系统有两个候选函数,一个负责查询付款记录,一个负责查询采购单详情。模型应该调用前者,却调了后者,结果返回了一堆没有付款状态的采购明细。
排查之后发现,两个函数的描述写得高度相似,都以“查询单据信息”开头,模型无法有效区分。后来把两个函数的描述改成侧重各自特点的版本:查询付款记录的描述里明确写了“返回的字段包含付款状态、付款金额、付款时间”,并给了一个参数示例;查询采购单详情的描述里也写清楚了自己返回的是采购明细而非付款信息。两边差异强化之后,误调用的频率降了很多。
还有一个常见的坑:系统里有几十个函数但没做分组,模型面对一个超长的函数列表,极易在中后部漏掉合适的函数。这种情况可以对函数列表按语义分组,每轮只向模型暴露与当前任务相关的组,而不是一次全量塞进去。
4.3 Agent 反复横跳与成本失控
Agent 最常见的故障现象是“反复横跳”:模型在同一组工具之间来回切换,一会儿调 A,一会儿调 B,就是不往前推进。日志里最典型的就是连续五六轮里 A、B、A、B 的顺序反复出现。
碰到这种情况别急着调模型,先看工具描述是不是存在歧义。如果 A 和 B 的适用边界在描述里没有划分清楚,模型很自然地会把同一个任务在两个工具之间轮流尝试。先把工具边界划清楚,通常能解决大半问题。如果边界清晰之后仍然反复,再看观察摘要的压缩程度——如果每轮返回的观察信息太长,模型容易迷失在细节里,把注意力放在无意义的对比上。适当压缩观察摘要,让模型看到的是“这个工具做了 X 得到 Y”这样简洁的结果,而不是几千字的原始返回,横跳现象会明显缓解。
成本失控这个问题,需要在设计阶段就防住。建议在 Agent 循环里实时统计 token 消耗量,并设置一个预算阈值。一旦超过阈值,强制让 Agent 停止规划,改为输出当前进度和后续建议。这个方法不只控制成本,还会倒逼 Agent 尽早收敛。
4.4 常见问题速查表
| 现象 | 可能原因 | 处理方案 |
|---|---|---|
| 检索返回相关但回答跑偏 | 提示词中资料区与指令混杂 | 用分隔符区分资料区,并写明“以资料区为准” |
| 知识库内容较新但检索不到 | 文档尚未切分入库,或切分粒度太大 | 检查入库流程,调整切分策略 |
| 用户的话被解析成错误参数 | 参数描述中缺少格式说明 | 在参数描述中补充格式、单位、示例 |
| 模型调用了错误的函数 | 函数描述边界模糊 | 强化区别描述,减少歧义;按组暴露函数 |
| Agent 反复执行同一操作 | 工具返回结果未被有效利用或观察摘要过长 | 压缩摘要,检查工具结果是否异常 |
| 多轮对话后模型“忘事” | 上下文过长,早期信息被稀释 | 保留最近轮次的完整摘要,压缩更早记录 |
| 用户意图判断错误 | 路由分类不够精细 | 补充路由阶段的示例数据,或增加兜底规则 |
5. 架构落地后的几点体会
文章写到这里,核心内容基本都铺开了。最后聊一点个人感受。
这套架构用了这么久,最大的体会是:不要追求一步到位的“完美架构”。RAG、Function Calling、Agent 这三块,前两者是可以快速落地并看到效果的,Agent 则需要更多的调优和耐心。比较稳妥的落地顺序是先把 RAG 做好,让系统回答准确;再接上 Function Calling,让系统能做事;最后才考虑上 Agent 做复杂编排。如果一开始就急着把 Agent 铺到所有场景上,很容易被那些意想不到的边缘情况拖得筋疲力尽。
另外一个想提醒的细节是日志。Agent 系统在调试期的日志一定要记详细,每一轮模型思考输出、工具调用、工具返回摘要、路由判断结果,全部要留下记录。没有这些详细日志,故障排查就是盲人摸象。我自己调试过最折磨的问题足足花了两个晚上才定位到根因——原因是某个工具在返回特殊格式数据时,观察摘要压缩步骤把关键状态吞掉了。如果没有那一轮轮完整的日志,这个问题根本没法定向排查。
这套架构的扩展性也比我预期的好。早期花时间把工具注册表、上下文管理器、参数校验层这些边界理清楚之后,后面每接一个新的业务工具,基本只需要写一个函数加一个描述,注册进去就完事。系统的复杂度和新需求的接入成本之间,建立了一道有效隔离。
如果你们也在做类似的大模型应用,按照 RAG 打底、Function Calling 接工具、Agent 编排这条路去走,大概率能少踩一些我踩过的坑。