☰
RAG准入控制实战:Jev判断引擎如何拦截脏chunk、降低幻觉
2026/10/3 5:47:27 网站建设 项目流程

最近两个月我一直在收拾自家 RAG 系统的烂摊子。检索结果五花八门,大模型每次都能用一本正经的语气把错误信息讲得头头是道。后来我给整套流程加了一道名叫 Jev 的闸门,情况才有根本性好转。Jev 是我们内部的代号,全称是 Judgment Engine for Validation,一个专门负责“检索内容准入控制”的判断引擎。这篇文章记录我从设计到上线的完整过程,不写花活,只讲实际操作,适合正在做 RAG 落地、被召回质量和幻觉问题折磨的工程师参考。

1. RAG 的瓶颈到底卡在哪:我先拆了几个真实案例

1.1 检索质量差不是因为向量模型不够好

最开始我也把问题归结为 embedding 模型不给力,前前后后换过好几个号称更强的向量模型。不能说没效果,但远没到解决根本问题的程度。举个线上最常出现的例子:用户问“这笔订单能不能退”,检索回来的 top5 里面有 3 个 chunk 是“热门商品介绍”,因为语义向量把“退”和“商品退款”混在了一起,把“能不能退”理解成了“退款相关的问题”。这种情况在业内有个指标叫 hit rate,就是 top-N 检索结果里真正包含正确答案的比例。我当时的 hit rate 低得没眼看。

后来把日志翻了个底朝天,才发现问题根子根本不是向量模型,而是文本拆解太粗糙。知识库里的 PDF 有标题、页眉、表格、图片说明,全被不分青红皂白地切成一段段连续字符。表格只剩半行,列表被腰斩,图片下面的文字注释单独成了 chunk,这种数据喂给再好的 embedding 也是白搭。所以我认为,RAG 的检索质量差,第一步要排查的不是模型,而是知识库的前处理流程。很多人问“有没有本地的 rag 文本拆解工具”,我的建议是别迷信一个工具通吃所有格式,必须针对你的文档类型单独调。

1.2 生成端扛了太多不该扛的锅

就算 top5 里面有一个正确答案,大模型也会被另外四个不相关内容带偏。我有一次测试售后知识库,两个 chunk 分别来自不同版本的退款政策,一个说“7 天内可退”,另一个说“15 天内可退”,模型最后直接随机挑了一个,还煞有介事地补了一句“根据最新政策”。这不是生成模型的问题,是喂进去的上下文本身有毒。

市面上的常见方案是加一层 rerank 模型,但这只能把相关的内容排到前面,并不会把不合格的内容淘汰掉。Top-k 截断也只是在数量上做限制,解决不了“chunk 之间互相矛盾”的问题。我踩过这个坑之后想明白一件事:RAG 缺的不是排序,而是一道准入控制。生成端扛不住脏上下文,那就应该在上下文进入模型前,先把不可信的东西拦在门外。

1.3 Jev 闸门的定位:它到底管什么

我最终的方案是给 RAG 装一道叫 Jev 的闸门。它不负责召回,也不负责生成,只做一件事:判断一段检索内容有没有资格进入大模型的上下文窗口。打个比方,rerank 是推荐官,把所有候选人按匹配度排个序;Jev 是门卫,直接拦下不够格的,让后排的顶上来。

Jev 的评分不是单一分数,而是从三个维度看:相关性,即这段内容和问题到底有没有关系;一致性,即这段内容和已知知识、和其他 chunk 是否矛盾;安全性,即是否包含有害、诱导、违规的内容。这三个维度组合起来,才能挡住我在线上看到的各种花式翻车。

这套设计的最大好处,是把失败的边界控制住了。以前 RAG 回答得好不好,全看大模型临场发挥;现在不合格的 chunk 进不了上下文,模型要么拒绝回答,要么只基于可信内容作答。哪怕最终效果不完美,至少不会“一本正经胡说八道”。

2. Jev 的核心设计与模型实现

2.1 三维评分机制拆解

先说相关性。这个维度不是简单算一下 query 和 chunk 的语义相似度就行的。比如用户问“iPhone 15 电池”,检索回来的 chunk 里全是“iPhone 15”的外观介绍,语义向量觉得很接近,但实际没有回答电池问题。所以我在相关性里加入了实体对齐要求。判断的关键不只是“像不像”,而是“需要提到的实体是否真的出现了”。

一致性是我认为最值钱的一个维度。我基于业务建了一个轻量本体,比如“订单”这个概念,应该包含订单号、状态、操作时间等字段。如果 query 期望的是订单信息,而回收回来的 chunk 全是商品营销内容,一致性分就会被打得很低。同时 Jev 还会对比同一批检索结果里不同 chunk 之间是否有矛盾字段。比如一个说“包邮”,另一个说“满 99 包邮”,这就是冲突信号。

安全性维度则是规则加模型双保险。规则负责直接拦截明显违规词,模型负责抓那些绕弯子的表达。举个例子,有些用户 query 本身是安全的,但检索回来的文档里夹带私货,这种内容不在规则词表里,就得靠模型在隐晦表达上做判断。三维度加权成一个总分,表达式是:

JevScore = α * Relevance + β * Consistency + γ * Safety

我实战调下来常用的权重是 α=0.5,β=0.3,γ=0.2。阈值则看业务,严格场景设 0.85,普通问答场景 0.75 就够。你不一定照抄这个值,但拆维度这件事一定要做,不然后续排查问题会非常痛苦。

2.2 为什么选择本地小模型而不是大模型调用

最初我也犹豫过,要不要直接用一个大模型来当裁判。但算了一笔账就放弃了。一次查询如果检索 20 个 chunk,每个 chunk 都要调用一次大模型判断,延迟从几百毫秒直接飙到几十秒,费用更是按调用次数爆炸。RAG 本来就是高并发场景,这个方案完全不可行。

后来我选择了本地部署一个小型编码器模型作为 Jev 的底座,百 MB 级别,用 ONNX Runtime 在 CPU 上就能跑,单条前向传播基本在 20 毫秒左右。文档数据完全不出内网,隐私问题也顺带解决了。很多人问“jev windows 部署”怎么做,其实没那么玄乎,一个 Python 环境加一个 ONNX 模型文件就能跑起来。

训练方面,我用的基座是 6 层 Transformer 编码器,输出三个维度得分。训练集从真实 RAG 日志里抽样,人工标注相关性、一致性、安全性三个标签,正负样本比例控制在 1:2 左右。训练目标用多任务学习,三个维度分别算损失,最后加权合并。冷启动阶段 500 条人工标注就能出一个能用的初版,积累到 3000 条质量就比较稳定了。所谓“Jev 模型”,本质上就是一个垂直场景的裁判模型,没必要把它神化。

2.3 与 RAG 流水线的三种接法

Jev 可以放在流水线的不同位置,效果不一样。

最推荐的是接在检索之后、构建上下文之前,这是标准玩法。先从向量库或 BM25 召回一批候选 chunk,Jev 逐一打分,低于阈值的直接淘汰,剩下合格的内容再拼进 prompt。这样模型看到的上下文是经过筛选的,污染问题基本解决。

第二种接在生成之后,对最终答案再做一次校验。把模型输出和参考 chunk 交给 Jev 检查,如果发现输出里的关键信息在参考 chunk 里找不到依据,就触发一次重试。这个方案适合对确定性要求极高的业务,比如金融、医疗。缺点是多一次推理开销,延迟会增加。

第三种接在知识库写入时。入库之前先对每条要写入的 chunk 做一次质检,质量差的直接拦掉。这属于前置治理,能减少后续检索端的压力。我的经验是,先做检索后接入跑通,再按需增加生成后校验和写入时校验,不要一上来就三层全上。

3. 从设计到上线的完整实操记录

3.1 第一步:定义你的“合格 chunk”标准

上线前最重要的一件事,不是写模型,而是先明确什么叫“合格 chunk”。我以自己做的一个产品知识库为例,列了五条硬性规则:

  • 每个 chunk 必须是一个完整的语义单元。FAQ 问答对必须问题答案成对出现,不能只留问题。
  • 每个 chunk 必须包含至少一个可标识实体,比如产品名、订单号、优惠券 ID。
  • 表格内容不能被拆碎。一个表格至少要保留表头加两行数据,不然“价格:¥99”这种碎片毫无意义。
  • chunk 与 query 的实体类型必须匹配。问“售后”的内容不能是商品介绍。
  • 同批检索结果中,两个 chunk 不能出现互相矛盾的关键业务字段。

这一步最难,但也最值钱。你会发现很多向量检索和重排阶段无法彻底解决的问题,在准入规则里就提前消灭了。而且这些规则不是拍脑袋定的,是在把线上坏案例看了一圈之后总结出来的。

3.2 第二步:构造训练集与冷启动

训练集的第一来源是线上 RAG 日志。我把用户真实 query 和对应的检索结果捞出来,随机抽样交给人工打标。每一条都按相关性、一致性、安全性三个维度分别打。这里有个注意点:不要只打正样本和负样本,要记清楚到底哪个维度出了问题,后面调权重才知道往哪边调。

冷启动阶段,我还用大模型辅助生成了一批 bad case。具体做法是拿真实 query,让大模型故意写一些看起来相关但实际没答到点上的检索片段。但这里必须提个醒:大模型造的 bad case 会有明显的模式化倾向,比如喜欢用“我们知道”这种开头,必须混入线上真实 case 一起训练,不然 Jev 会被养出偏见。

数据量上,我建议不要一开始就追求上万条。先人工标 500 条,让 Jev 跑起来,看误杀率,再针对痛点补样本。500 条够起步,3000 条质量明显稳定,再往后就是持续运营的事了。顺带一提,文本拆解工具我踩了一圈坑。直接把 PDF 按段落切,表格碎片满天飞;后来换成布局识别加 OCR,把图片和表格转成结构化文本,问题才缓解。

3.3 第三步:把它接入 LangChain4j 或自研管道

我用 Java 技术栈,所以直接接在 LangChain4j 的文档处理管道里。核心就是一个实现过滤器接口的类,在文档进入上下文组装前过一遍。代码比想象中简单:

public class JevGateFilter implements DocumentTransformer { private final JevScorer scorer; private final double threshold = 0.75; public JevGateFilter(JevScorer scorer) { this.scorer = scorer; } @Override public List<Document> transform(List<Document> documents) { return documents.stream() .filter(doc -> scorer.score(doc).total() >= threshold) .collect(Collectors.toList()); } }

如果你是 Python 栈,逻辑也一模一样:

class JevGateFilter: def __init__(self, model_path: str, threshold: float = 0.75): self.scorer = JevScorer(model_path) self.threshold = threshold def transform_documents(self, documents, query): result = [] for doc in documents: s = self.scorer.score(query, doc) if s.total >= self.threshold: result.append(doc) return result

接入之后要算一笔延迟账。假设一次查询检索 20 个 chunk,Jev 单条推理 20 毫秒,线性执行就是 400 毫秒。如果线上端到端要求 1 秒以内,还能接受;如果要求 500 毫秒以内,就必须做优化。我的做法是分两阶段:先用 BM25 或快速向量粗筛把候选从 20 条砍到 5 条,再让 Jev 精细打分,实际延迟能控制在 150 毫秒左右。

部署上,我用 ONNX Runtime 导出模型,Windows 机器 CPU 直接跑,内存占用不到 500MB。这点很重要,尤其在本地化部署场景,不是每家都有 GPU 资源。

3.4 第四步:灰度上线与指标观测

上线不能一把梭,我按 query 类型做了灰度。第一周只让“售后类 query”走 Jev 闸门,其他类型保持原逻辑,这样可以快速比较差异。

观测指标我盯五个:误杀率,即原本能回答的问题被 Jev 拦成“不知道”的比例,目标 5% 以内;漏放率,即 Jev 放行但人工审核判为不合格的比例,目标 2% 以内;hit rate,即检索正确命中的比例,理论上会提升;端到端回答满意度,用点赞和点踩作为代理指标;最后一个是被拦截 chunk 的维度分布,用来判断权重要不要调。

这里最容易被忽略的是反馈闭环。用户点踩的回答要自动进入待标注池,每周人工复核一次,积累新样本,然后定期重新微调。我做灰度时发现,第一版模型跑两周之后,随着知识库内容更新,误杀开始变多。原因很直白:业务进来一批新文档,表述风格和训练样本差太多,Jev 认不出来。没有反馈闭环,闸门会跟着业务漂移一起失灵。

4. 常见问题与排查技巧实录

4.1 误杀太多,知识库回答变“我不知道”

这是我上线后遇到的第一个大坑。症状很明显:原本很多能答的问题,Jev 拦完之后都变成了“抱歉,我不知道”。排查的时候不要只看通过率,一定要看三个维度的分数拆解。

我遇到过一次,误杀主要是安全性维度造成的。某个产品知识文档里有一些专业术语,跟安全规则词表里的词很像,结果被打成低分。解决办法是调低安全维度权重,同时给特定业务线的 chunk 加白名单。还有一次是一致性维度卡太死,本体要求“订单”必须包含订单号,但有些售后场景的 chunk 根本没有订单号字段,也被误杀了。后来我把本体约束改成按 query 类型动态启用,误杀马上降下来。

经验是:阈值不是拍脑袋定死的,建议按 query 类型动态设置。比如“物流查询”类 query 相关性权重拉高,“政策法规”类 query 一致性权重拉高。

4.2 漏放了冲突或有害内容,Jev 为什么没拦住

漏放比误杀危险,也更难查。有一次线上出了个事:用户问某药品副作用,Jev 放行了一个看起来语气很客观的 chunk,但里面故意漏掉了最重要的禁忌信息。单看这个 chunk,相关性和一致性都合格,但和知识库里的权威内容一比,就是诱导行为。Jev 只处理单条 query-chunk 对,看不到这种“单个没问题、组合起来有问题”的场景。

针对这类问题,我做两件事。一是增加对抗样本,专门构造“正常语气但恶意内容”的训练数据,提升模型的敏感度。二是对高风险业务增加生成后校验,模型输出完答案之后,再让 Jev 把答案和所有参考 chunk 过一遍,发现关键信息缺失就触发重试。

这里必须说清楚,Jev 不是万能保险。真正治本的办法是在知识库源头做权限控制和内容审核,敏感文档直接不进入 RAG 的检索范围。闸门只是最后一道防线,不能当唯一防线用。

4.3 延迟压不下来,闸门变成瓶颈

小型模型在 CPU 上跑,单条 20 毫秒确实不快,但放到整个 RAG 链路里就不一样了。一次查询如果是 20 条候选,线性推理就是 400 毫秒,再加上检索和生成,很容易超时。

我试过两种优化,效果实测都很明显。第一种是动态 batch 推理,把多条 chunk 拼成一个 batch 同时送入模型,吞吐能提升 3 倍以上,延迟从 400 毫秒降到 150 毫秒左右。第二种是粗筛加精判的两段式方案,先用 BM25 或轻量向量模型把候选从 20 条快速砍到 5 条,再让 Jev 在这 5 条上精细判断,精度几乎没有损失,延迟却大幅下降。

更狠的做法是给 Jev 做一个蒸馏版小模型,单条推理降到 5 毫秒以内,换来一点精度损失。如果业务延迟红线非常紧,可以考虑这条路线。

4.4 图文混排知识库的特别提醒

网上很多人问“rag 知识库能存储图片嘛”,我直接说结论:图片本身不能进向量库做检索,但图片上的文字和表格必须能。如果你把一张含有退款步骤的截图直接传进知识库,Jev 大概率会把它判成低质量 chunk 拦掉,因为语义不完整。

解决方式是用 OCR 加表格识别工具,把图片内容转成带结构的 Markdown 文本,再入库。同时要给拆解后的文本块做完整性标记,比如表格必须包含表头和至少两行数据,否则不入库。文本拆解工具要优先选能保留结构的,不然后果就是“价格:¥99”和“运费:¥10”被拆到两个 chunk 里,Jev 还会以为信息冲突。

这个问题的本质是:RAG 的知识库是给模型读的,不是给人看的。图片、PDF、PPT 这些格式,最终都要变成干净、独立、语义完整的文本块,Jev 闸门才有意义。

根据我个人经验,Jev 这一层闸门不解决所有问题,但它把 RAG 的失败模式从“一本正经胡说八道”变成了“拒绝回答、明确引用、或说不知道”。后者听起来不酷,但在生产环境里太重要了。最后再分享一个我踩过几次坑后养成的小习惯:所有被 Jev 拦截和放行的 case,我都会打日志,并且每周用线上坏样本回炉一次模型。时间长了你会发现,真正让闸门变聪明的不是初始训练,而是这个持续喂养的过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询