从零构建企业级 AI Agent 平台
本篇目标:只讲一条主线——用户提一个问题,平台怎么从几十万分块里把最相关的几块捞出来,拼成证据,生成一个带引用的答案。每一步解决什么问题、关键技术怎么实现、踩过哪些坑。
企业AI 平台从 0 到 1(三):一个问题进来,怎么捞到答案——混合检索与 RAG 深度工程
上一篇我们讲完了知识入库:文件被切成一个个 800 字左右的分块,向量进向量库,正文进关键词索引。这一篇讲检索侧:
问题预处理 → 多路召回 → 分数融合 → 精排 Rerank → 证据组装 → 有据合成 → 带引用返回
一个工程师问"容灾切换的 RTO 要求是多少",几秒后 AI 给出答案,末尾跟着 1-2 个引用来源。这个"几秒"里发生了下面这一整条流水线:
下面顺着这条线一站一站走。
一、问题预处理:先听懂问题,再检索
直接拿原话去检索是新手做法。用户的话里有语气词、有指代、有"用英文输出"这种格式指令,甚至有同义词。平台先做五件事:
1.1 清洗
去 emoji、去尾缀语气词(“啊”“呢”)、合并多余空白——把"请问一下,这个系统的备份策略是什么呢?"这种句子洗干净。
1.2 槽位抽取
从问题里识别结构化实体:单据编号、错误码、模块名、主机名。这些实体不是普通关键词——它们有明确指代,后面融合阶段会单独加权(第四节细讲)。
1.3 意图分类与查询扩展
判断问题意图(查询类 / 流程类 / 列表类……),然后做查询变换:
- 同义词扩展:把领域内同义词/上下位词展开成多组查询;
- 多查询:同一问题改写多个角度;
- 可选 HyDE:先让模型"猜一个假设回答",用这个回答再检索一次(HyDE 是显式开启的路径,不是默认行为——因为成本高、不是每个问题都值得)。
最后所有结果打包成一个 QueryPlan:清洗后的检索句、槽位、意图、扩展查询、元数据过滤条件、标签提示、查询语言。RAG / 数据库 / 智能体三条链路共用同一份 Plan,保证"同一个问题"在语义建模上口径一致。
1.4 多轮消歧:把"它"翻译成主题
最难的是指代。用户先问"考勤制度有哪些",接着"用英文输出"——第二个问题没有主题词。
平台的规则式消歧(不调模型,快且稳):
- "用英文输出"是纯格式指令 → 自动取上一轮用户问题当主题;
- "用英文输出考勤制度"是前缀格式指令 + 主题 → 剥离前缀,用"考勤制度"检索;
- "团队有哪些考勤制度,用英文输出"是后置指令 → 分段后移除指令段,保留主题段;
- 主题段里再压缩掉疑问词(“有哪些”“怎么”),留下真正的关键词。
一句话:把"格式要求"和"检索主题"分开——语言指令管输出,主题词管检索,互不污染。
1.5 语言过滤
问题语言先检测(中/英/葡),检索时按语言偏好过滤候选(默认"偏好"模式),避免一个中文问题捞回一堆英文分块。
二、多路召回:向量 + 关键词,各干各的
这是本平台检索的核心工程决策。先问一个朴素问题:为什么不能只用向量检索?
看两个真实场景:
- 用户问"RTO 是多少"——"RTO"是精确标识符,向量相似度可能把它当成普通词;
- 用户问"怎么处理超时"——问法千变万化,关键词很难全部命中。
向量擅长"语义近似",关键词擅长"精确匹配"。单靠任何一路都会漏。所以平台默认跑两路,按条件再加路:
| 路径 | 干什么 | 默认状态 |
|---|---|---|
| 向量检索 | 语义近似(query 向量 × 分块向量) | ✅ 常开 |
| BM25 关键词 | 精确/词面匹配(ES 分词全文检索) | ✅ 常开 |
| 语义 BM25 | 分词后的模糊匹配,仅宽泛流程类问题启用 | 条件 |
| 扩展向量 | 用 Query Plan 的扩展查询再搜一轮,仅上游语义改写时启用 | 条件 |
| HyDE 向量 | 假设回答再检索,需显式开启 | 默认关 |
两个值得讲的工程细节:
1)ES 预筛(Pre-filter):全库向量 ANN 在海量分块下没必要。平台先用 ES 关键词粗召回一批 chunk id(默认最多 120 个),把向量搜索限制在候选集里;如果 ES 候选太少(少于 24 个)说明这个问题的关键词路线不可靠,回退全库 ANN。粗筛 + 精排,成本降一个量级。
2)向量结果缓存:未走预筛的纯向量检索结果按"知识库 + query 向量"缓存到 Redis(TTL 5 分钟),同一问题的重复请求直接命中。
另外每路都是 top_k × 2 召回——召回阶段宁可多捞,精排阶段再砍,漏了就没得救,捞多了可以砍。
三、分数融合:RRF + 加权,以及一条铁律
多路结果回来了,同一分块可能被多路同时命中。怎么合并排序?平台用RRF(Reciprocal Rank Fusion)+ 加权分数 + 软加权三层:
3.1 RRF:排名即分数
不比较不同路分数的绝对值(向量余弦 0.7 和 BM25 分数 12.5 根本不可比),只比排名:
score_rrf = Σ 1 / (k + rank_i) # k 为平滑常数,rank 是第 i 路的排名
在某一路排第 1 得 1/(k+1),排第 10 得 1/(k+10)。被多路同时命中的分块,RRF 天然更高——这是"多路共识"的信号。
3.2 加权分数:同量纲的加总
对每路的实际分数(向量余弦、BM25 分)按权重加总,权重按语言配置(当前 profile 默认:向量 0.30、BM25 0.20、扩展向量 0.22、语义 BM25 0.10……)。
3.3 软加权:槽位命中加分
第二节抽出的槽位在这里派上用场。分块内容里如果显式包含用户问题里的单据编号 / 错误码 / 模块名 / 主机名,各加不同权重(编号 +0.4、错误码 +0.2、模块名 +0.15、主机名 +0.1)——精确标识符命中是强证据,必须让它浮上来。
3.4 一条踩出来的铁律
增加一路检索,永远不能降低另一路证据支撑的分数。
当初的实现是"所有路径分数取平均"——结果出现一个怪现象:一个分块被关键词强命中、向量余弦很低时,加进来的低分向量把总分拉低了,反而是负贡献。
现在的实现:
weighted_score = max(全部路径平均, 词法路径平均, 稠密路径平均)
词法证据强的用词法平均,稠密证据强的用稠密平均,两路都强的取最高——新增路径只能加分,不能惩罚。这条经验比任何调参都值钱。
融合还会加:多路一致性加分(被 ≥2 路命中 +0.03,≥3 路 +0.06)、单一路径且无任何加分信号时 ×0.72 惩罚(防止"只有一路碰巧命中"的弱证据上榜)。
最后每个候选带一份 score_breakdown(RRF 分 / 加权分 / 各路径分 / 各 boost),检索结果可解释——这是后面 QC 和排查问题的基础。
四、精排 Rerank:把"看起来相关"变成"真的相关"
融合排序还是"召回视角"。Rerank 是"精排视角":用一个专门的排序模型,对着 query 重新给每个候选打分。
4.1 喂给 Rerank 的不是原文,是"证据文本"
这是平台的一个关键设计。直接把分块正文丢给排序模型效果一般,平台先为每个候选构建判别性证据文本:
文档:容灾管理规范
章节路径:4 系统架构 > 容灾方案
答案判定要点:RTO;恢复时间目标;容灾切换
候选答案片段:
- RTO 要求为 30 分钟以内……
关键词上下文:……
原文节选:……
把文档名、章节路径、判定要点、答案片段拼好再送排序模型——模型判断的是"这段证据能不能回答问题",而不是"这段文字像不像问题",命中率明显更高。
4.2 本地证据评分兜底
排序模型可能挂了、可能超时。平台有一个不依赖外部模型的本地证据评分:
本地证据分 = 已有检索信号 × 0.42
+ 判定短语命中率 × 0.26
+ 候选答案片段覆盖 × 0.20
+ 多路命中 × 0.025/路(上限 0.10)
+ 值型特征(编号/数字/表格)0.08
+ 保护候选 +0.08
外部排序模型分数和本地证据分结合使用——模型是增强,本地信号是底线。
4.3 多模型融合与防抖
- 多模型并行:最多 3 个排序模型并行调用(3 秒超时),取平均分融合;单模型配置时走单模型路径;
- 短期分数缓存:同 (query, 分块, 模型) 的分数缓存 10 分钟,避免重复请求反复调用排序 API;
- 长文本截断:单条证据超过 1000 字符截断,防止排序模型 token 超限;
- 五级降级:多模型超时 → 用已完成模型结果;全失败 → 本地证据分;本地也失败 → 原始融合序;总超时 5 秒 → 直接放弃重排用原序。Rerank 是增强,绝不能因为它的失败拖垮整条检索。
另外一个朴素的规则:候选 ≤5 条时跳过 Rerank——数据量小,排序模型提升有限,白花钱。
五、证据组装:去重、补上下文、按文档分组
精排完选出 top-N,还不能直接拼给模型。要做三件事:
5.1 Jaccard 去重
相邻分块往往高度相似(尤其是滑动窗口切分)。用 Jaccard 相似度去重:两个分块 token 重叠率 > 0.7 时,保留分数高的那个。否则同样的内容会占掉两三个上下文名额。
5.2 邻居上下文扩展
检索命中是"点",但答案可能需要"上下文"。平台会加载每个命中分块所在文档的物理邻居分块(同文档、按 chunk 顺序取前后 N 块)。
两个设计细节:
- 扩展只看物理顺序,不看章节元数据——章节路径可能标错,物理相邻不会骗人;
- 有总量上限(组内约 6500 字符、总约 18000 字符),防止上下文爆炸。
5.3 按文档分组
最终上下文按文档分组输出:
【文档:容灾管理规范】
……分块 1……
……分块 2……
【文档:应急预案】
……分块 3……
文档间按最高分排序、文档内按原始顺序排列。这样模型能清楚感知"哪段属于哪篇文档",生成时更容易引用正确来源。分组时还会提取分块里的图片说明并保留图片标记,要求模型原样输出图片标记,前端才能展示真实截图。
六、有据合成与反幻觉:答案必须有出处
这是 RAG 系统最关键的一层——宁可说不知道,不能编。平台用四道防线:
6.1 证据门控(Abstain Gate)
合成前先问:检索到的证据够不够回答这个问题?
- 完全没检索到分块 → 走"无命中"路径;
- 有分块但最高分低于阈值(默认 0.18)且无强字面命中(问题原文/短语完整出现)→ 拒答,返回澄清引导(“知识库中没有找到与 X 相关的内容,请补充……”),而不是硬编一个答案;
- 有其它可靠上下文(如数据库结果)→ 放行。
6.2 有据合成规则与低温
- system prompt 里注入有据作答规则(“只能基于【知识内容】作答,内容中没有的必须明说”)——注意这个规则文本来自界面配置,代码不内置口径,运营可以随时改;
- 有据合成温度默认 0.0——RAG 回答要的是确定性,不是创意;
- 特定场景(如查"某个配置步骤的标识")走确定性合成:直接从 Markdown 表格里按列名查表生成答案,完全跳过 LLM 归纳——表格查值这种场景,LLM 反而容易"润色"出错。
6.3 引用对齐与修剪
答案生成后,平台会对"答案 ↔ 来源"做一次对齐:
- 提取答案里的判别性锚点(文档标题、关键编号),反向给每个来源打分;
- 修剪掉答案里根本没提到的来源——避免出现"引用了但没用上"的误导引用;
- 引用按文档去重,同一文档只展示一次。
6.4 诚实 Miss:状态流
“没有答案"也是一种答案。平台有标准的答案状态(有据 / 无据 / 证据不足),miss 类回答走诚实路径:明确告知"知识库未覆盖”,绝不用"根据我的了解……"糊弄。
七、质量保障:机器自己给自己打分
上线容易,长期准不准难。平台把质检做成流水线:
1)L0 确定性预检(规则,零成本):引用可追溯性(引用的来源必须真实存在)、无来源捏造检测、参数幻觉检测(答案里的数字/编号必须能在来源里找到)、语言一致性。
2)客观忠实度旁路信号:把答案的关键锚点(编号、术语、数字)拿去和引用正文比对,算 Jaccard 重叠率、答案覆盖率、关键术语命中率——不依赖任何模型,纯字符串计算,随时可跑。
3)L1 LLM 裁判:对内容质量做单条裁判,按状态分流(miss 类问题只查 miss 一致性,不要求它产出完整答案)。
4)归因信号:证据充分性评估 + 库内可召回探针——“这个问题的答案,在知识库里到底能不能被检索到”。
配合检索侧已有的可解释性(每个候选带 score_breakdown 和命中原因),一个问题答错了,是预处理、召回、融合、重排、门控哪一环的问题,可以一步步追。
八、踩坑清单(都是真金白银)
| 坑 | 后果 | 解法 |
|---|---|---|
| 只用向量检索 | 精确标识符(RTO、错误码)漏召回 | 向量 + BM25 多路 |
| 多路分数直接平均 | 低分向量把强词法证据拉低 | max(全平均, 词法平均, 稠密平均) |
| 全库向量 ANN | 海量分块下检索慢 | ES 预筛候选集,候选不足回退全库 |
| 把格式指令当检索主题 | “用英文输出"去检索"英文输出” | 指令/主题分离,多轮消歧 |
| 让 LLM 重写表格答案 | 数字被"润色"错 | 表格查值走确定性合成 |
| Rerank 阻塞主链路 | 排序模型慢 → 整个对话超时 | 多级超时 + 降级回退 |
| 直接拼原文给排序模型 | 判别性证据不足,排序不准 | 构建证据文本再送 |
| 上下文塞满相似分块 | 重复内容浪费名额 | Jaccard 去重 + 邻居扩展上限 |
| 引用来源与实际不符 | 误导用户 | 答案-来源对齐 + 修剪 |
九、小结与下期预告
这一篇的四个核心结论:
- 召回要广:向量 + 关键词多路,条件加路,ES 预筛控成本;
- 融合要稳:RRF 比分数,加权看量纲,槽位软加权,加路不降分是铁律;
- 精排要专:证据文本 + 本地证据分兜底 + 多级降级,Rerank 是增强不是依赖;
- 输出要诚:证据门控拒答、低温有据合成、引用对齐修剪、确定性查表,反幻觉靠的是工程不是提示词。
下一篇进入生成与对话工程:上下文拼好之后,流式输出、SSE、多模型路由、Agent 工具调用、成本与延迟的取舍——把一个"能检索的 RAG"变成一个"能对话的 AI 平台"。
(本文基于项目当前实现整理。文中默认参数、路径权重、阈值均以仓库实现为准,不同部署环境可能不同。)
更多推荐

所有评论(0)