
企业技术支持这个场景做 Agent 的人都知道有多难伺候。用户问的问题五花八门有的是登录报 403 怎么办有的是这个接口返回的 token 过期了怎么续还有的是你们这个功能到底支不支持批量导入。前两类问题有标准答案后一类问题得翻产品文档。更麻烦的是同一个问题不同版本的答案还不一样。我接手这个项目的时候团队已经在用纯 LLM 硬扛了三个月效果嘛能用但每次回答都像在开盲盒——有时候精准得让人惊喜有时候胡编得让人想砸键盘。后来我们上了 RAG情况变了但没完全变。因为 RAG 本身不是银弹它只是把模型不知道变成了模型可以查查得准不准、查得快不快、查不到的时候怎么办全是坑。这篇文章我想把整个实战过程拆开讲包括为什么选这个架构、Token 在哪些环节卡过我们、Embedding 模型怎么挑、检索命中率怎么从 60% 拉到 90% 以上以及那些文档里不会写的踩坑经验。如果你正在做企业级技术支持 Agent或者任何需要基于私有知识回答用户问题的系统这篇应该能帮你省下不少试错时间。1. 为什么纯 LLM 撑不住企业技术支持场景1.1 技术支持问题的三个特殊性企业技术支持跟通用问答有本质区别。通用问答可以容忍我不确定但技术支持不行。用户问我的 token 为什么失效了你回一句可能是过期了建议重新登录用户会直接投诉。他要的是你的 token 有效期是 2 小时失效是因为超过了这个时间窗口重新调用刷新接口用 refresh_token 换新的 access_token 就行如果 refresh_token 也过期了那就得重新走登录流程。这个场景有三个硬性要求。第一答案必须准确不能有模糊空间。第二答案必须有时效性产品迭代后旧答案要能自动淘汰。第三答案必须可追溯用户追问你凭什么这么说的时候你得能指出这是哪份文档第几页写的。纯 LLM 方案在这三点上全部翻车。模型训练数据有截止日期新产品功能它根本不知道模型会幻觉编造不存在的接口参数模型没有引用来源你没法验证它说的是真是假。1.2 Token 消耗的账得算清楚我们最开始用纯 LLM 的时候没太在意 Token 消耗。直到月底账单出来发现光是技术支持这一个场景一个月烧掉了差不多 2000 万 Token。什么概念呢平均每个用户会话消耗 8000 Token 左右因为每次都要把完整的系统提示词、历史对话、用户问题全部塞进去。这里有个关键认知Token 不是免费的但也不是越省越好。纯 LLM 方案下你为了让它回答准确得把大量上下文塞进 promptToken 消耗自然高。上了 RAG 之后你只需要把检索到的相关片段塞进去上下文长度可能从 8000 降到 2000但检索本身也要消耗 Token——Embedding 要 Token重排序要 Token如果做查询改写还要 Token。所以没 Token 能用有 Token 更聪明这句话的真正含义是RAG 让你在 Token 预算有限的情况下把每一分钱花在刀刃上。不是不用 Token而是用得更有策略。1.3 从模型知道什么到模型能查到什么这个思维转变很关键。纯 LLM 的思路是我要训练一个什么都懂的模型RAG 的思路是我要建一个能快速找到正确信息的检索系统让模型基于找到的信息来回答。打个比方。纯 LLM 像是一个博学但记性不太好的老教授你问他什么他都能聊两句但细节经常记错。RAG 像是一个配备了图书馆检索系统的研究员他可能不是最聪明的但他知道去哪本书里找答案而且找出来的答案有页码可查。企业技术支持场景下后者的可靠性远高于前者。因为技术支持要的不是聊得好而是答得对。2. RAG 架构选型为什么是 LangChain4j 本地 Embedding2.1 框架选型的三个约束条件选框架的时候我们列了三个硬约束。第一必须能私有化部署因为技术支持文档涉及内部产品细节不能传到外部 API。第二必须支持 Java 生态我们后端全是 Java 服务引入 Python 服务会增加运维复杂度。第三必须能灵活替换组件Embedding 模型、向量库、LLM 都要能独立替换不能绑死。基于这三点LangChain4j 成了最合理的选择。它是 LangChain 的 Java 移植版API 设计跟 Python 版基本一致但跑在 JVM 上可以直接嵌入现有服务。更重要的是它的组件抽象做得比较干净EmbeddingModel、EmbeddingStore、ChatLanguageModel都是接口换实现只需要改配置。提示LangChain4j 的版本迭代比较快建议锁定一个稳定版本不要盲目追新。我们用的是 0.31.0实测下来 API 比较稳定社区文档也全。2.2 Embedding 模型本地跑还是调 API这是第一个真正需要做取舍的地方。调 API 的好处是省事OpenAI 的 text-embedding-3-small 效果确实好但有两个问题一是数据要出内网二是按 Token 计费文档量大之后成本不低。本地跑的话选择就多了。我们对比了几个主流方案模型维度中文效果推理速度部署难度text-embedding-3-small1536优秀依赖网络低BGE-M31024优秀中等中m3e-base768良好快低text2vec-large-chinese1024良好中等中最后选了 BGE-M3。原因有三个中文效果在 MTEB 榜单上排名靠前支持多语言我们有些文档是英文的而且 1024 维在效果和存储成本之间比较平衡。这里有个坑要注意Embedding 模型的维度直接决定了向量库的存储成本和检索速度。1536 维比 768 维的存储开销大一倍检索时的计算量也大一倍。如果你的文档量在十万级以下768 维其实够用如果上百万级维度的影响就很明显了。2.3 向量库为什么没用 Pinecone向量库的选择上我们试过 Pinecone、Milvus 和 PostgreSQL 的 pgvector 扩展。Pinecone 是托管服务开箱即用但数据要出内网直接排除。Milvus 功能强大但部署一套完整的 Milvus 集群至少需要三个节点运维成本太高。最后选了 pgvector。理由很简单我们已经有 PostgreSQL 了。pgvector 作为扩展装上就能用不需要额外维护一套数据库。虽然它在超大规模向量检索上不如专用向量库但我们的文档量在五十万条左右pgvector 完全扛得住。实测下来在 50 万条 1024 维向量的数据集上pgvector 的 HNSW 索引查询延迟在 20ms 左右召回率 95% 以上。这个性能对技术支持场景来说绰绰有余。2.4 LLM 的选择本地模型还是云端LLM 这块我们走了弯路。最开始想用本地部署的模型试了 Ollama 跑 Llama 3 和 Qwen效果嘛能用但跟 GPT-4 比差距明显。特别是在理解复杂技术问题的时候本地模型经常抓不住重点。后来改成混合方案检索和 Embedding 全本地LLM 调用走内部网关。这样既保证了数据安全文档不出内网又保证了回答质量。内部网关做了 Token 配额管理每个服务有独立的预算超了会自动降级到本地模型。这个方案的关键在于Embedding 和 LLM 可以解耦。Embedding 负责找得准LLM 负责说得好。找得准这件事本地模型完全能胜任说得好这件事还是得靠大模型。3. 文档处理流水线从 PDF 到可检索片段的完整链路3.1 文档解析PDF 是最难啃的骨头技术支持文档的来源很杂有 Confluence 导出的 PDF有 Markdown 写的 API 文档还有从工单系统导出的 CSV。其中 PDF 是最难处理的因为 PDF 本质上是一种排版格式不是内容格式。我们试过几个 PDF 解析库。PDFBox 是 Java 生态里最成熟的但对复杂排版的表格支持不好。后来换成了 Apache Tika它底层也是 PDFBox但做了更多封装对表格和列表的识别更准。这里有个经验不要指望自动解析能 100% 准确。我们的做法是自动解析之后加一道人工校验环节把解析结果展示给文档维护人员确认。虽然增加了工作量但避免了垃圾进垃圾出的问题。3.2 分块策略固定长度还是语义分块分块是 RAG 里最容易被低估的环节。分得太碎检索到的片段缺乏上下文分得太大检索精度下降而且浪费 Token。我们最开始用固定长度分块每 500 个字符一块重叠 50 个字符。效果一般因为经常把一段完整的话从中间切断。后来改成语义分块用 Embedding 模型计算相邻句子的相似度相似度低于阈值就切分。语义分块的效果明显更好但计算成本高。我们的折中方案是先用固定长度粗分再用语义相似度做二次合并。具体来说先按 300 字符切分然后计算相邻块的 Embedding 相似度如果相似度高于 0.85 就合并。这样既控制了计算量又保证了语义完整性。3.3 元数据设计让检索能按版本过滤技术支持文档有个特殊性同一个问题不同产品版本的答案不一样。用户问这个接口怎么调你得先知道他用的是哪个版本。所以我们在每个文档块上都附加了元数据{ source: api-docs-v2.3.pdf, product: payment-gateway, version: 2.3, section: token-refresh, last_updated: 2024-11-15, doc_type: api_reference }检索的时候先按product和version做过滤再做向量相似度搜索。这样即使用户问的是老版本的问题也不会检索到新版本的答案。注意元数据的字段设计要在文档入库前就确定好后期补元数据非常痛苦。我们最开始只存了 source后来要加 version 的时候不得不把五十万条数据全部重新处理了一遍。3.4 增量更新怎么处理文档变更文档不是一成不变的。产品每次发版API 文档就要更新。如果每次更新都全量重建索引五十万条数据的 Embedding 计算要跑好几个小时。我们的方案是基于内容哈希的增量更新。每个文档块计算一个 SHA-256 哈希更新时对比新旧哈希只对变化的块重新计算 Embedding。实测下来一次常规版本更新的变更率在 5% 左右增量更新只需要几分钟。这里有个细节删除的文档块要及时清理。我们最开始只做了新增和更新忘了删除。结果用户问一个新功能的问题检索出来的却是旧版本的答案因为旧块还在索引里。后来加了一个定期清理任务把源文档里已经不存在的块标记为失效。4. 检索优化把命中率从 60% 拉到 90% 的实操记录4.1 第一版检索为什么只有 60% 命中率第一版上线后我们做了一个人工评估从真实用户问题里随机抽 200 个看检索结果里有没有包含正确答案。结果命中率只有 60% 左右。也就是说10 个问题里有 4 个检索系统根本没找到正确的文档块。分析失败案例后发现三个主要原因。第一用户问法和文档写法不一致。用户问token 过期了怎么办文档里写的是访问令牌失效处理流程。第二一个问题需要多个文档块才能回答但检索只返回了最相似的 top-3漏掉了关键信息。第三短查询的 Embedding 质量差比如用户只输入403Embedding 模型很难理解这是什么意思。4.2 查询改写让用户问题更接近文档语言针对第一个问题我们加了查询改写环节。具体做法是用户输入问题后先用 LLM 把它改写成更接近文档语言的表述再去检索。比如用户问token 过期了怎么办改写后变成访问令牌失效处理流程 刷新令牌 重新认证。这样检索时就能匹配到正确的文档块。查询改写的 prompt 是这样的你是一个技术支持查询改写助手。请将用户的问题改写成更适合文档检索的形式。 要求 1. 保留原始问题的核心意图 2. 使用技术文档中常见的术语 3. 补充可能相关的关键词 4. 不要改变问题的原意 用户问题{query} 改写结果实测下来查询改写把命中率从 60% 提升到了 75% 左右。但要注意改写会增加一次 LLM 调用Token 消耗和延迟都会增加。我们的做法是只对短查询少于 10 个字做改写长查询直接检索。4.3 混合检索向量搜索 关键词搜索向量搜索擅长语义匹配但对精确关键词不敏感。比如用户搜ERR_4032向量搜索可能返回一堆关于错误处理的文档但真正包含这个错误码的文档可能排在后面。所以我们加了关键词搜索作为补充。具体做法是同时执行向量搜索和 BM25 关键词搜索然后用 RRFReciprocal Rank Fusion算法合并结果。RRF 的公式很简单score sum(1 / (k rank_i))其中 k 是一个常数通常取 60rank_i 是文档在第 i 个检索结果里的排名。这个算法的好处是不需要归一化不同检索器的分数直接基于排名融合。混合检索把命中率从 75% 提升到了 85% 左右。而且它对精确匹配场景特别有效比如用户搜具体的错误码、接口名、配置项名称。4.4 重排序最后一道精度保障前两步做完检索结果的相关性已经不错了但 top-3 里还是经常混入一些不太相关的块。这时候就需要重排序。重排序用的是 Cross-Encoder 模型它跟 Embedding 模型不同不是分别编码查询和文档而是把查询和文档拼在一起输入模型直接输出相关性分数。这样精度更高但计算量也更大所以只对 top-20 的结果做重排序选出 top-3 返回。我们用的是 BGE-Reranker-Large实测下来重排序把最终 top-3 的准确率从 85% 提升到了 92% 左右。优化阶段命中率主要手段基线60%纯向量检索加查询改写75%LLM 改写查询加混合检索85%向量 BM25 RRF加重排序92%Cross-Encoder 重排4.5 检索不到的时候怎么办即使做到 92% 命中率还是有 8% 的问题检索不到。这时候如果硬让 LLM 回答它就会开始编。我们的策略是检索置信度低于阈值时直接告诉用户我没找到相关文档并给出人工支持入口。这比编一个错误答案要好得多。置信度怎么算我们用重排序模型的分数作为置信度。如果 top-1 的分数低于 0.5就认为检索失败。这个阈值是调出来的太高会导致很多能回答的问题被拒太低会放进来太多噪声。提示拒答率是个需要持续监控的指标。如果拒答率突然升高可能是文档更新了但索引没更新也可能是用户问的问题类型变了。我们每周会看一次拒答日志从中发现文档缺口。5. Token 管理的那些坑从超支到精细控制5.1 Token 都花在哪了上了 RAG 之后Token 消耗的结构变了。我们统计了一下大概的分布是这样的Embedding 计算15%查询改写10%重排序20%LLM 生成回答55%可以看到LLM 生成回答还是大头但检索相关的 Token 消耗加起来也有 45%。所以省 Token不能只盯着 LLM检索链路上的每一环都要算账。5.2 缓存最有效的省钱手段我们加了两级缓存。第一级是查询缓存完全相同的用户问题直接返回缓存结果不触发任何检索和 LLM 调用。第二级是Embedding 缓存相同的文本块不重复计算 Embedding。查询缓存的命中率在 20% 左右因为技术支持问题重复率很高。Embedding 缓存的命中率更高因为文档更新时大部分块是不变的。缓存用 Redis 实现key 是查询文本的哈希value 是完整的回答结果。过期时间设了 24 小时因为文档可能随时更新。5.3 上下文压缩只放最相关的片段LLM 生成回答的时候我们只把重排序后的 top-3 片段放进 prompt而不是把所有检索结果都塞进去。每个片段限制在 500 Token 以内总共不超过 1500 Token。这里有个技巧在片段前面加上来源标记让 LLM 知道每段话出自哪里。这样它回答的时候可以引用来源用户也能验证。[来源1: api-docs-v2.3.pdf, 第12页] 访问令牌的有效期为2小时。过期后需要使用refresh_token换取新的access_token。 [来源2: faq.md, 第5节] 如果refresh_token也过期了用户需要重新登录。5.4 降级策略Token 不够的时候怎么办我们设了三级降级策略。第一级Token 预算充足时用 GPT-4 生成回答。第二级预算用到 80% 时切换到 GPT-3.5-turbo。第三级预算用完时只返回检索到的原始文档片段不做 LLM 生成。这个策略保证了服务不会因为 Token 超支而完全不可用。虽然降级后的体验会差一些但至少用户能拿到原始文档。6. 上线后的真实数据和踩坑复盘6.1 效果数据上线三个月后我们统计了一组数据指标上线前纯 LLM上线后RAG回答准确率62%89%平均响应时间3.2s4.1s单次会话 Token 消耗80003200用户满意度3.1/54.3/5人工转接率45%18%准确率提升明显Token 消耗降了一半多。但响应时间增加了因为多了检索和重排序的环节。不过用户对 4.1 秒的容忍度还可以毕竟比转人工等几分钟要好。6.2 踩过的三个大坑第一个坑文档更新不同步。产品发版后文档更新了但索引没及时重建导致用户问新功能的问题检索到的还是旧答案。后来加了一个 webhook文档系统更新后自动触发索引重建。第二个坑Embedding 模型换了但没重建索引。我们中途把 Embedding 模型从 m3e-base 换成了 BGE-M3但忘了重建索引。结果查询用的新模型 Embedding文档存的是旧模型 Embedding两者维度都不一样检索结果完全是乱的。这个坑让我们明白Embedding 模型和索引必须版本绑定换模型必须全量重建。第三个坑长文档截断。有些 API 文档特别长超过 Embedding 模型的最大输入长度512 Token后被截断了。截断后的 Embedding 只包含了文档开头的信息后面的内容完全丢失。后来我们在分块阶段就控制了每块的长度确保不超过模型限制。6.3 还在优化中的问题目前还有两个问题没完全解决。一是多轮对话的上下文管理用户追问的时候怎么把历史对话和当前问题结合起来检索我们还在试不同的方案。二是表格和图片的处理有些技术文档里的配置表格解析出来是乱的Embedding 效果很差。表格这块我们试过把表格转成 Markdown 再 Embedding效果比直接解析 PDF 好一些但复杂的合并单元格还是有问题。图片就更麻烦了目前只能靠 OCR 提取文字但技术架构图里的信息基本提取不出来。7. 一些可以直接抄的配置和经验7.1 分块参数经过多次调整我们最终用的分块参数是初始分块大小300 字符合并阈值相邻块 Embedding 相似度 0.85最大块大小800 字符块间重叠50 字符这个配置在技术文档上效果比较好。如果是法律或医疗文档可能需要更大的块因为那些文档的上下文依赖性更强。7.2 检索参数向量检索 top-k20关键词检索 top-k20RRF 融合后取 top-10重排序后取 top-3置信度阈值0.5top-k 的设置需要根据文档量调整。文档量大的时候top-k 可以设大一些让重排序有更多候选。文档量小的时候top-k 设太大反而会引入噪声。7.3 监控指标上线后一定要监控这几个指标检索命中率每周人工抽检 100 个问题拒答率突然升高说明有问题Token 消耗按服务、按用户维度统计响应时间 P99检索链路变慢会影响整体体验缓存命中率太低说明缓存策略有问题7.4 一个容易被忽略的细节最后分享一个我们踩过的坑用户输入的问题里可能包含敏感信息。比如用户会把 token、密码、内部 IP 直接贴在问题里。这些信息如果被送到 LLM可能会有安全风险。我们的做法是在查询改写之前加一道脱敏处理用正则匹配常见的敏感信息模式如 JWT、API Key、IP 地址替换成占位符。这样既保护了用户隐私也避免了敏感信息被记录到日志里。这个脱敏环节看起来简单但实际做的时候要考虑很多边界情况。比如用户贴的 token 可能被换行符截断正则要能处理多行匹配。还有用户可能用自然语言描述敏感信息比如我的密码是 abc123这种就得靠 NER 模型来识别了。整体做下来最大的体会是RAG 不是把文档扔进向量库就完事了它是一个需要持续调优的系统。检索质量、Token 消耗、响应速度这三个指标互相制约你得根据业务场景找到平衡点。技术支持场景下准确率优先所以我们在重排序和拒答策略上投入比较多。如果是创意生成场景可能就更看重响应速度和多样性了。