大多数工程团队在检索增强生成(RAG)方面都会遇到同样的瓶颈。他们遵循教程中的常规做法:将文档切分为 512 或 1024 个 token 的固定分块,通过单一的嵌入模型进行处理,然后通过简单的 top-k 查询调用向量数据库。在演示文稿中,这看起来很稳健;但在生产环境中,它会失效。
固定分块并不关心内容。它们会毫不犹豫地在法律合同的句子中间进行切分,导致责任条款悬空在两个无关的文本片段中。它们会将整个 API 端点描述塞进一个臃肿的分块里,导致用户询问的具体参数淹没在噪声中。而且,当检索变慢时,每一毫秒的延迟都会直接影响用户体验。我们曾通过惨痛的教训学到了这一点。随后,我们拆解并重建了检索层。我们的 Recall@10 从 78% 提升到了 95%。延迟不仅没有增加,反而大幅下降。
复制粘贴式 RAG 的问题
标准的 RAG 技术栈已经成为一种默认设置:小分块、单一嵌入模型、向量搜索,搞定。这种方法之所以能在演示中幸存,是因为演示使用的是干净的问题和整洁的文档。而生产数据从来都不是整洁的。
法律文档具有层级结构。章节包含子章节,子章节包含条款。如果你用一个粗暴的 token 计数器去切分它们,就会破坏模型进行推理所需的逻辑关系。API 文档也有结构,但形式不同。函数签名、参数、返回值和示例用法构成了一个逻辑单元。如果强行将其塞进固定的 token 窗口,你要么会截断示例,要么会用无关的函数来填充分块。支持工单通常是杂乱、对话式的,并且伴随着突然的话题转换。Wiki 知识库则内容庞杂且相互引用。一种分块策略无法服务于所有这些场景,然而团队却经常在重复这种做法。我们不再假装这种方法可行。
策略性分块:因材施教
我们转向了内容感知分块(content-aware chunking)。对于法律文档,我们使用尊重文档层级的递归分块(recursive chunking)。它能保持条款的完整性,并保留章节之间的父子关系。对于 API 文档,我们构建了函数感知分块(function-aware chunking),将每个函数或端点视为一个边界。如果参数描述过长,分块会围绕该函数进行扩展,而不是受限于 token 限制。对于支持工单,我们使用语义分块(semantic chunking)来检测自然的话题边界。当客户突然从账单投诉转向技术故障时,切分会发生在话题转换处。对于 Wiki 和非结构化知识库,我们使用 Agentic 分块,即利用轻量级 LLM 来评估文本并决定在哪里设置有意义的边界。虽然这比按字符切分设置起来更慢,但它决定了检索是真正有效,还是仅仅在瞎猜。
混合检索:为什么仅靠向量搜索是不够的
向量搜索理解语义,但可能会遗漏精确匹配。如果用户粘贴了一个类似 ERR_CONNECTION_RESET_0x5F3 的错误代码,语义相似度可能会将其排在那些仅泛泛讨论网络错误的段落之后。另一方面,BM25 可以找到精确的字符串,但会忽略概念上的相关性。你需要两者兼得。
我们并行运行向量搜索和 BM25。然后使用倒数排名融合(Reciprocal Rank Fusion, RRF)来合并结果,它可以在不强制将两者拉到同一量级的情况下,对来自两个不同搜索空间的评分进行归一化。融合之后,我们将排名前列的候选结果通过一个交叉编码器重排序器(cross-encoder reranker)。这会增加少量的延迟,但带来的精度提升非常显著。重排序器会同时读取查询语句和每个候选结果,并分配一个相关性评分,这比初始嵌入的余弦相似度要准确得多。在实践中,这种组合既能捕捉到纯向量搜索会遗漏的精确错误代码,又能呈现出关键词搜索会忽略的、在概念上相关的故障排除步骤。
查询扩展:在输入进入索引前进行修正
用户不会写出完美的搜索查询。他们会提出多跳问题,例如“为什么我上次部署失败了,以及我该如何回滚?”,这需要找到两个独立的知识体系并将其联系起来。或者,他们会提出一些与索引匹配度很差的模糊问题。
我们在搜索前对查询进行转换。一个多跳问题会被分解为多个子问题。一个模糊的意图会被扩展为多个具体的搜索查询。我们发现,将一个用户查询扩展为五个不同的搜索查询,可以将召回率从 78% 提升到 96%。这并不是要更强力地提示 LLM,而是为了给检索系统提供更多寻找正确上下文的机会。每个生成的查询都捕捉了不同的角度或术语,而合并后的结果则描绘了一幅完整的图景。
贝叶斯优化:停止猜测
一旦你采用了多种分块策略、混合检索和查询扩展,你就会面临一个新问题。参数实在太多了。分块大小、重叠百分比、向量权重与 BM25 权重的比例、重排序阈值以及 top-k 值,所有这些因素都以非线性的方式相互作用。手动调优变成了一场猜测游戏。
我们停止了猜测。我们将...
