企业技术支持这个场景,做 Agent 的人都知道有多难伺候。用户的问题从"密码重置链接点不开"到"你们这个接口返回 500 是不是你们服务挂了",跨度极大;知识散落在工单系统、内部 Wiki、产品文档、历史聊天记录里;更麻烦的是,你没法要求每个提问的人都把问题描述清楚。我接手这个项目的时候,团队给我的原始需求就一句话:做一个能回答技术支持问题的 Agent,最好别老胡说八道。
标题里那句"没 Token 能用,有 Token 更聪明",其实是我在项目复盘时写给自己的一句话。前半句说的是系统必须在没有大模型额度、没有外部 API 可用的情况下依然能跑起来、能给出可用的答案;后半句说的是当 Token 充足时,系统要能自动切换到更强的推理链路,把答案质量再往上抬一个台阶。这不是什么花哨的架构炫技,而是被现实逼出来的设计——企业环境里,额度随时可能被限、外部服务随时可能不可用,但业务不能停。
这篇文章我会把整个 RAG 实战的来龙去脉讲清楚:为什么这么设计、检索链路怎么搭、Embedding 怎么选、Token 降级策略怎么落地、并发怎么扛、以及我在真实环境里踩过的那些坑。适合正在做企业知识库、技术支持 Agent、或者任何想把 RAG 落到生产环境的同学参考。不管你是刚接触 RAG 的新手,还是已经搭过几套检索系统的老手,应该都能从里面找到点能直接抄的东西。
1. 先把"没 Token 能用"这件事想明白
1.1 为什么我把降级能力放在架构第一位
很多人做 RAG 的第一反应是:先接一个大模型,把检索到的内容塞进 prompt,让它生成答案。这个思路没错,但它默认了一个前提——大模型永远可用。在企业环境里,这个前提非常脆弱。我遇到过的情况包括:API 额度当月用完、供应商侧限流、网络抖动导致请求超时、以及最尴尬的——演示当天外部服务抽风。
所以我在设计之初就定了一条硬规则:系统的核心回答能力不能依赖大模型。具体来说,检索、排序、答案组装这三步必须能在纯本地、零外部调用的条件下完成。大模型只是一个"增强层",有它更好,没它也能活。
这个思路落到架构上,就是把系统拆成两条链路:
- 基础链路(无 Token):查询理解 → 向量检索 + 关键词检索 → 重排序 → 模板化答案组装。全程本地模型或纯算法,不调用任何外部生成式 API。
- 增强链路(有 Token):在基础链路之上,增加查询改写、多路召回、LLM 重排、答案生成与引用校验。
两条链路共享同一套知识库和检索底座,切换只发生在最上层。这样做的直接好处是:降级不是"功能阉割",而是"精度降级",用户拿到的仍然是基于真实知识库的答案,只是表达没那么自然、覆盖没那么全。
提示:降级设计的关键不是"能不能降",而是"降了之后答案还可不可用"。如果降级后返回一句"系统繁忙",那等于没做降级。
1.2 基础链路到底能回答到什么程度
我实测下来,基础链路在技术支持场景的可用率大概在 70% 到 80% 之间。这个数字听起来不高,但要知道技术支持问题里有大量是重复的、结构化的,比如"如何重置密码""某个报错码是什么意思""某个功能在哪里配置"。这类问题用检索加模板组装完全能答好。
基础链路的答案组装逻辑是这样的:检索到 Top-K 文档片段后,按相似度加权拼接,前面加上一句引导语,后面附上来源链接。比如用户问"登录报 403 怎么办",系统检索到三条相关片段,组装成:
根据知识库,登录报 403 通常与以下情况相关: 1. 账号权限未开通(来源:权限管理文档 3.2 节) 2. 该 IP 段被风控策略拦截(来源:安全策略说明) 3. 会话凭证过期(来源:常见问题 FAQ 第 12 条) 建议按上述顺序排查。这种答案不"聪明",但准确、可追溯、不会胡说。对于技术支持来说,可追溯比流畅更重要。用户拿着来源链接能自己去看原文,这比一个编得很顺但不知道从哪来的答案强得多。
1.3 有 Token 时增强链路补的是什么
增强链路不是把基础链路推翻重来,而是在几个关键节点上做加法。我总结下来主要补三件事:
第一是查询改写。用户问"我登不上去",基础链路只能拿这句话去检索,召回率有限。增强链路会先用 LLM 把它改写成多个查询:"登录失败""无法登录""账号登录异常""登录报错",多路召回后再合并去重。这一步对召回率的提升非常明显,我实测召回率能从 0.62 提到 0.81。
第二是LLM 重排。向量检索的相似度排序有时候不准,尤其是语义相近但主题不同的片段。用一个轻量的 LLM 对 Top-20 做相关性打分重排,能把真正相关的片段顶上来。这一步对最终答案准确率的提升大概在 10 到 15 个百分点。
第三是答案生成与引用校验。让 LLM 基于检索片段生成自然语言答案,同时要求它标注每句话的来源。生成后再做一次校验,检查答案里的每个事实点是否能在检索片段里找到依据,找不到的就标记为"待确认"。
这三步加起来,就是"更聪明"的全部含义。注意,每一步都是可选的、可降级的,任何一步失败都不影响基础链路继续工作。
2. 检索底座:Embedding 选型和向量库落地
2.1 Embedding 模型怎么选才不踩坑
Embedding 是 RAG 的地基,选错了后面全白搭。我在选型时对比了当时主流的几个方向:纯中文优化的、多语言通用的、以及大厂开源的。评估维度我列了四个:中文语义区分度、推理速度、模型体积、以及是否支持本地部署。
这里有个很多人忽略的点:Embedding 模型的排行榜分数和你的实际业务效果往往不成正比。公开榜单测的是通用语义相似度,但技术支持场景里充斥着大量专有名词、错误码、产品名,这些词在通用语料里出现频率低,模型未必学得好。所以我的做法是:先用公开榜单筛出候选,再用自己业务里的真实 query-doc 对做小规模评测。
我构造了 200 组评测数据,每组是一个真实用户问题和它对应的正确文档片段,外加 3 个干扰片段。评测指标用 Recall@5 和 MRR。实测下来,一个中等体积的多语言模型在我的场景里反而比某些榜单排名更高的大模型表现好,原因是它对专有名词的处理更稳,而且推理速度快了将近三倍。
最终我选的方案是:主模型用一个中等体积的多语言 Embedding 模型,本地部署;同时保留一个轻量模型作为降级备选。轻量模型在 Token 紧张或算力不足时启用,召回率会掉几个点,但能保证系统不挂。
| 评估维度 | 主模型要求 | 降级模型要求 |
|---|---|---|
| 中文语义区分度 | Recall@5 ≥ 0.75 | Recall@5 ≥ 0.65 |
| 单条推理耗时 | < 50ms | < 15ms |
| 模型体积 | 中等,可本地部署 | 小,CPU 可跑 |
| 专有名词处理 | 重点评测项 | 可接受下降 |
注意:不要迷信"越大越好"。Embedding 模型体积翻倍,推理成本可能翻好几倍,但业务效果可能只提升两三个点。在技术支持这种对延迟敏感的场景,性价比比绝对精度更重要。
2.2 向量库选型:为什么我没用最火的那个
向量库的选择上,我评估过几个主流方案。选型时我关注的不是"谁性能最强",而是"谁最贴合我的运维现状"。我的约束条件是:数据量在百万级片段以内、需要支持元数据过滤、团队没有专职的向量数据库运维、希望部署简单。
基于这些约束,我最终选了一个支持本地文件持久化、API 简洁、能和现有关系型数据库共存的方案。理由很实际:百万级数据量下,专用向量数据库的性能优势体现不出来,但它的运维复杂度是实打实的。用轻量方案,出问题了团队里任何人都能上手排查。
向量库的 schema 设计我做了几个关键决策:
- 片段粒度:不按整篇文档存,而是按语义段落切分,每段 200 到 500 字。切分时保留标题层级信息,这样检索到片段后能知道它属于哪篇文档的哪一节。
- 元数据字段:每条片段带上来源文档 ID、章节路径、更新时间、文档类型、权限标签。权限标签很重要,技术支持知识库里有些内容只对内部可见,检索时必须过滤。
- 双索引:向量索引之外,同时建一个全文索引(比如基于倒排的 BM25)。向量检索擅长语义匹配,全文检索擅长精确匹配错误码、产品名这类关键词,两者互补。
2.3 混合检索:向量加关键词不是简单相加
很多人做混合检索就是把向量检索结果和关键词检索结果拼在一起,去个重就完事。这样做的效果很一般,因为两路结果的分数尺度不一样,直接合并会让某一路主导排序。
我的做法是分数归一化后加权融合。具体步骤:
- 向量检索取 Top-50,关键词检索取 Top-50。
- 对每一路的分数做 min-max 归一化,映射到 0 到 1。
- 按权重融合:向量路权重 0.6,关键词路权重 0.4。这个权重是我在评测集上网格搜索出来的,不同业务可能要调。
- 融合后取 Top-20 进入重排阶段。
这里有个细节:关键词检索的查询词要做处理。用户输入的错误码、产品名要原样保留,但停用词、语气词要过滤掉。我维护了一个技术支持领域的停用词表,把"请问""麻烦""一下""怎么办"这类词去掉,只留实词。
另外,对于包含明确错误码或产品型号的查询,我会临时提高关键词路的权重。判断逻辑很简单:查询里是否包含形如ERR-xxxx、纯数字串、或已知产品名词典里的词。命中就把关键词权重提到 0.6 甚至 0.7。这个动态权重策略让错误码类问题的首条命中率提升了不少。
3. 从检索到答案:组装逻辑与引用校验
3.1 模板化答案组装的工程细节
基础链路的答案组装看起来简单,但要做好有不少讲究。我一开始就是简单拼接,结果发现几个问题:片段之间语义重复、拼接后逻辑跳跃、来源标注混乱。后来我加了几层处理:
第一层是去重。检索到的片段之间可能有大量重叠内容,尤其是同一篇文档的不同段落。我用文本相似度做去重,相似度超过阈值的只保留分数最高的那条。
第二层是排序。不是简单按检索分数排,而是综合考虑分数、片段长度、来源权威性。来源权威性是我给每类文档打的权重,比如官方产品文档权重高,历史工单权重低。
第三层是组装。组装时按"总-分-来源"的结构:先一句话概括,再分点列出具体内容,最后附来源。如果片段里有明确的步骤,就组装成有序列表;如果是并列的排查方向,就用无序列表。
第四层是兜底。如果检索到的片段最高分低于阈值,说明知识库里可能没有相关内容,这时候不能硬答,要返回"未找到相关内容,建议联系人工支持"并附上相关度最高的几条供参考。
这套组装逻辑我调了大概两周,中间反复拿真实问题测试。有个经验:组装模板要留出"不确定"的表达空间。比如"根据知识库,可能的原因是……"比"原因是……"更稳妥,因为检索本身有误差,把话说太满容易误导用户。
3.2 引用校验:让答案里的每句话都有出处
增强链路里我最看重的一环是引用校验。LLM 生成答案时很容易"脑补",把检索片段里没有的信息也写进去。引用校验就是给生成结果做一次事实核查。
具体做法是:要求 LLM 生成答案时,每句话后面用标记标注它依据的是哪个片段,比如[1]、[2]。生成完成后,我用一个轻量的判断逻辑(可以是小模型,也可以是规则加语义匹配)检查每个标注是否合理——这句话的核心信息是否真的能在对应片段里找到。
校验不通过的句子有两种处理:一是直接删除,二是标记为"待确认"并提示用户。我倾向于后者,因为有些句子虽然不能完全从片段里找到依据,但可能是合理的推理,直接删掉会损失信息。
这里有个工程上的取舍:校验本身也要消耗 Token。如果 Token 紧张,我会把校验降级为纯规则匹配——只检查答案里的关键实体(错误码、产品名、数字)是否出现在检索片段里。这个降级版校验虽然粗糙,但能拦住大部分明显的幻觉。
3.3 多轮对话里的上下文处理
技术支持场景里,用户很少一次把问题说清楚。常见的是"登录有问题"→"报 403"→"我昨天还能用"。这种多轮对话如果每轮都独立检索,效果会很差,因为"报 403"这句话单独看信息量太低。
我的处理方式是查询重写加历史压缩。每一轮新问题进来,先和最近几轮对话合并,用 LLM 重写成一个自包含的查询。比如上面三轮合并后重写成"用户昨天还能登录,今天登录报 403 错误,需要排查原因"。用这个重写后的查询去检索,召回质量高很多。
历史压缩是为了控制 Token 消耗。我不会把完整对话历史都塞进去,而是保留最近三轮的摘要加当前问题。摘要由 LLM 生成,只保留和当前问题相关的信息。这样既保证了上下文连贯,又不会让 prompt 无限膨胀。
提示:多轮场景下,检索的 query 和展示给用户的答案要分开处理。query 可以很长很详细,但答案要简洁,直接回应用户当前这一轮的问题。
4. Token 降级策略:怎么做到无感切换
4.1 降级触发的判断逻辑
降级不能靠人工切换,必须自动判断。我设了几个触发条件:
- 额度预警:监控 Token 消耗速率,当剩余额度低于阈值(比如 20%)时,自动切到基础链路。
- 调用失败率:连续 N 次调用失败或超时,触发降级。
- 响应延迟:增强链路整体延迟超过阈值(比如 8 秒),说明外部服务可能有问题,降级保可用。
- 手动开关:保留一个配置项,运维可以强制降级。
这几个条件里,调用失败率是最灵敏的。我设的是连续 3 次失败或 5 分钟内失败率超过 30% 就降级。降级后不是一直不恢复,而是每隔一段时间做一次探测调用,成功了就自动升回增强链路。
这里有个坑:降级和恢复要有防抖。我一开始没做防抖,结果外部服务抖动时系统在两条链路之间反复横跳,日志里全是切换记录,用户体验也很割裂。后来加了冷却时间,降级后至少保持 5 分钟再尝试恢复,恢复后如果又失败,冷却时间翻倍。
4.2 两条链路的结果一致性怎么保证
降级最怕的是"同一个问题,降级前后答案差太多",用户会觉得系统不稳定。我的做法是让两条链路共享尽可能多的中间结果。
具体来说,检索阶段两条链路用的是同一套混合检索,只是增强链路会多做查询改写和多路召回。所以基础链路的检索结果其实是增强链路结果的一个子集。这样即使降级,用户拿到的答案方向不会变,只是覆盖面和表达方式有差异。
答案组装阶段,我让基础链路的模板尽量贴近增强链路的输出风格。比如增强链路生成的答案通常是"分点加来源"的结构,那基础链路的模板也做成类似结构。用户视觉上感知不到明显差异。
4.3 无 Token 环境下的性能表现
我专门做过无 Token 环境的压测。在纯本地、零外部调用的条件下,单机(8 核 16G)能支撑的并发大概在 30 到 50 QPS,P99 延迟在 800ms 左右。这个性能对于企业内部技术支持场景完全够用。
瓶颈主要在 Embedding 推理上。如果并发再往上走,我会把 Embedding 服务单独拆出来做水平扩展,前面加一层缓存——相同或相似的查询直接命中缓存,不走模型推理。缓存命中率在技术支持场景里挺高的,因为重复问题多,实测能到 40% 左右。
还有个优化点是预计算。知识库更新频率不高,我可以离线把所有片段的向量算好存起来,查询时只算 query 的向量。这样在线推理的压力就小很多。增量更新时只算新增片段的向量,不用全量重算。
5. 并发与稳定性:Agent 怎么扛住真实流量
5.1 请求链路的异步化改造
Agent 的请求链路比普通接口长得多:查询理解、检索、重排、生成、校验,每一步都可能耗时。如果全同步串行,一个请求几秒钟就过去了,并发一上来直接雪崩。
我的改造思路是能并行的全部并行,能异步的全部异步。具体来说:
- 向量检索和关键词检索并行发起,谁先返回谁先处理,最后合并。
- 多路召回(增强链路)的多个查询并行检索。
- 引用校验和答案生成可以流水线化,生成一句校验一句,不用等全部生成完。
改造后,增强链路的 P95 延迟从 6 秒降到了 3 秒左右,基础链路从 1.5 秒降到 800ms。
5.2 限流、熔断和排队
企业环境里流量不是均匀的,经常某个时段突然涌进来一批问题。我加了三层保护:
第一层是限流。按用户和按全局两个维度限流。单用户每分钟最多 10 次请求,全局根据系统容量设上限。超过的直接返回"请求过于频繁,请稍后再试"。
第二层是熔断。当外部 LLM 服务失败率超过阈值,熔断器打开,所有请求走基础链路。熔断器半开状态下放少量请求探测,成功了再完全恢复。
第三层是排队。对于超过并发上限的请求,不直接拒绝,而是进队列等待。队列有超时时间,超时了再拒绝。这样能削峰填谷,避免瞬时流量把系统打垮。
这三层配合下来,系统在流量突增 5 倍的情况下依然能保持可用,只是延迟会上升。
5.3 知识库更新时的在线一致性
知识库不是静态的,产品更新、文档修订都会导致内容变化。更新时如果处理不好,会出现"检索到旧内容"或"更新期间检索不到"的问题。
我的方案是双缓冲加版本切换。知识库有两份索引,一份在线服务,一份用于更新。更新完成后,原子性地切换指针,新请求走新索引,老请求继续用老索引直到结束。这样更新过程对用户完全无感。
增量更新时,我只重算变化片段的向量,然后合并到现有索引里。合并操作要保证原子性,避免出现"一半新一半旧"的中间状态。
注意:知识库更新后,缓存要同步失效。我踩过一次坑,文档更新了但缓存没清,用户拿到的还是旧答案,排查了半天才发现是缓存问题。现在我的更新流程里,清缓存是强制步骤。
6. 踩过的坑和实测经验
6.1 检索"看起来相关"但答非所问
这是 RAG 最典型的坑。用户问"如何配置邮件通知",检索回来一堆提到"邮件"的片段,但讲的是邮件模板、邮件服务器配置,就是没有"通知开关在哪"这个具体答案。
根因是向量相似度衡量的是语义相近,不是问题与答案的匹配。问题和答案在语义空间里未必靠近。我的解法是在检索之外加一层"答案性判断":对检索到的片段,判断它是否真的包含对问题的回答,而不只是主题相关。
具体做法是训练一个小分类模型,输入是 query 和片段的拼接,输出是"包含答案"的概率。这个模型用历史工单数据训练,正样本是最终解决了用户问题的片段,负样本是检索到但没用的片段。加上这层过滤后,答非所问的比例明显下降。
6.2 长文档切分切断了关键信息
文档切分是个技术活。切太碎,上下文丢失;切太大,检索精度下降。我一开始按固定字数切,结果把一张操作步骤表从中间切断了,检索到的片段只有前半段步骤,用户照着做卡在中间。
后来改成按语义结构切分:优先按标题层级切,其次按段落,最后才按字数。对于表格和列表,尽量保持完整,实在超长就在逻辑断点处切,并在片段里标注"接上段"或"接下段"。
还有个细节:片段要带上下文头。每个片段前面加上它所属的文档标题和章节路径,比如"产品文档 > 账号管理 > 密码重置 > 常见问题"。这样即使片段本身信息不全,检索和生成时也能借助上下文理解。
6.3 Token 用量失控的排查过程
项目上线初期,Token 消耗远超预期。我排查了一圈,发现问题出在几个地方:
第一是历史对话没有压缩,多轮对话把完整历史都塞进 prompt,越聊越长。改成摘要加最近三轮后,单次消耗降了 60%。
第二是检索片段塞太多。我一开始把 Top-10 片段全塞进去,其实很多是冗余的。改成去重加动态截断,只保留真正相关的 3 到 5 条,消耗又降了一截。
第三是校验环节重复调用。我一开始对每个句子单独调一次校验,后来改成批量校验,一次调用校验所有句子,调用次数降了一个数量级。
这三项优化加起来,Token 消耗降到了原来的四分之一左右,效果基本没损失。
6.4 几个提升效果的小技巧
最后分享几个实测有效的小技巧:
查询扩展用同义词词典兜底。LLM 改写查询虽然好,但有时会改偏。我维护了一个技术支持领域的同义词词典(比如"登录"和"登陆"、"报错"和"异常"),LLM 改写后再用词典做一次扩展,双保险。
答案里保留原始错误码。用户搜错误码时,答案里必须原样出现这个错误码,不能改写成自然语言描述。我在生成后加了一步检查,确保查询里的关键实体在答案里原样保留。
给检索结果打分而不是只排序。排序只能告诉你谁更相关,打分能告诉你"到底有多相关"。有了绝对分数,就能设置阈值做兜底,分数太低就不硬答。
定期用真实问题回归测试。我每周从工单里抽一批新问题做回归,看检索和答案质量有没有下降。知识库和模型都可能悄悄变化,不测就发现不了。
这套系统跑了大半年,从最初的勉强可用到现在基本能扛住日常技术支持的大部分问题,中间踩的坑比预想的多,但每一步的取舍现在回头看都是值得的。无 Token 能用的底线保证了业务连续性,有 Token 更聪明的增强链路保证了答案质量,两者之间的自动切换让用户几乎感知不到背后的复杂性。如果你也在做类似的东西,我的建议是先把基础链路做扎实,再考虑增强,别一上来就 all in 大模型——地基不稳,上面盖得越高越危险。