无Token也能用,有Token更聪明:企业技术支持Agent的RAG实战
企业技术支持这个场景做 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 个干扰片段。评测指标用 Recall5 和 MRR。实测下来一个中等体积的多语言模型在我的场景里反而比某些榜单排名更高的大模型表现好原因是它对专有名词的处理更稳而且推理速度快了将近三倍。最终我选的方案是主模型用一个中等体积的多语言 Embedding 模型本地部署同时保留一个轻量模型作为降级备选。轻量模型在 Token 紧张或算力不足时启用召回率会掉几个点但能保证系统不挂。评估维度主模型要求降级模型要求中文语义区分度Recall5 ≥ 0.75Recall5 ≥ 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 QPSP99 延迟在 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 大模型——地基不稳上面盖得越高越危险。