应用工程

RAG 知识新鲜度监控

RAG 系统上线一段时间后,最难发现的一类故障不是检索不到,而是检索到了一份还在、也确实被正确索引了、但内容已经不再成立的文档。所有链路指标都正常,对账任务也报一致,用户拿到的却是一个去年的答案。

这类问题不属于索引一致性,用一致性的手段也查不出来。它需要一套单独的新鲜度监控。

一、一致性不等于新鲜度

先把两个概念分开,否则后面的监控设计会一直混淆。

一致性回答的是:“索引里的内容,和源库当前的内容是否相同?“这是一个可以精确判定的问题,对账任务能给出确定答案。站内《RAG 文档版本与索引一致性》专门讲这一层,包括版本定义、删除传播和静默漂移的对账。

新鲜度回答的是:“源库里的这份内容,现在还成立吗?“这没有确定答案,只有基于时间和外部变化的推断。源文档可能三年没人改过——它和索引完全一致,但描述的流程早就废弃了。

一致性故障会让系统给出”上一版”的答案;新鲜度故障会让系统给出”上一个时代”的答案。后者更危险,因为它不会在任何一致性检查里报警。

二、给知识条目定义保鲜期

新鲜度监控的起点,是承认不同知识的过期速度差别巨大。把整个知识库用同一个时间阈值去卡,结果一定是大量误报加上真正的陈旧内容漏掉。

可行的做法是按内容类型给出保鲜期,写进 metadata:

内容类型典型保鲜期过期后的默认处理
价格、额度、套餐30–90 天直接降权,回答中强制标注核实日期
操作流程、配置步骤180–365 天降权,标记待复核
政策、合规条款按发布方公告周期出现新版即下架旧版
概念解释、原理说明1–3 年或不设一般不处理
内部决策记录不过期保留,但标注为历史记录

保鲜期不是删除时间,是触发复核的时间。到期只意味着”这条需要有人看一眼”,不等于内容错了。

字段设计上,至少记录三个时间:内容最后实质修改时间、最后复核时间、下次复核到期时间。注意区分前两者——一次只改错别字的提交不应该重置保鲜期。metadata 字段怎么选型和落地,可以参考《RAG Metadata Filter 设计》。

三、三类新鲜度信号

监控信号来自三个不同的位置,缺一类就会有盲区。

源侧信号关注文档本身的生命状态:最后修改时间、维护人是否还在职、所属项目是否已归档、外部链接是否仍可访问。外部来源的文档还要监控原始 URL 的可达性——404 和跳转到新版本是两种不同的信号,后者往往意味着内容已被取代。

索引侧信号关注库存的时间分布:按保鲜期统计已过期条目的占比、各内容类型的平均年龄、超期未复核的条目数。这类指标的价值在于趋势——过期占比从 3% 缓慢爬到 15%,说明复核流程已经跟不上内容增长了。

使用侧信号最容易被忽略,但往往最早暴露问题:被检索命中但用户明确反馈过时的条目、回答中被人工标注为错误且证据文档年龄较大的样本、同一问题在不同时间给出矛盾答案的情况。使用侧信号的好处是它只在真正影响用户时才响,噪音远低于前两类。

一个实用的组合规则:只有被实际检索到的陈旧内容才值得优先处理。库里躺着一万条三年没动的文档,如果从来没被召回过,它的优先级低于那条每天被命中五十次、已经超期两个月的价格说明。

# 按"陈旧程度 × 召回热度"排序待处理条目
def staleness_priority(doc, hits_30d):
    overdue_days = max(0, (today - doc.next_review_due).days)
    if overdue_days == 0 or hits_30d == 0:
        return 0
    # 热度取对数,避免头部文档完全垄断处理队列
    return overdue_days * math.log1p(hits_30d)

四、告警分级与阈值

新鲜度告警最常见的失败方式是一次性把几千条过期文档推给某个人,然后所有人开始忽略这个告警。

分级的依据是”错误答案的后果”,不是过期时长:

  • 需要立即处理:涉及金额、权限、合规的内容超期,且近 30 天有召回记录。这类应该单条告警并指派到人。
  • 需要排期:高频召回的操作类文档超期。按周汇总成一份列表,进入正常迭代。
  • 仅需观察:低频或零召回的内容超期。只在月度报表里体现趋势,不产生单条告警。

阈值不要一次性设到理想值。新上线监控时,库存里往往已经积压了大量超期内容,按目标阈值开告警会瞬间淹没所有人。合理做法是先测出当前的基线分布,把阈值设在略好于基线的位置,再按月收紧。

五、陈旧内容的处置

发现之后有四种处置方式,按侵入性从低到高:

标注。内容保留、检索照常,但在提供给模型的上下文里附上内容日期和复核状态,让模型能在回答中说明时效性。这是成本最低的一档,也应该是默认档。

降权。在重排阶段对超期内容施加一个衰减系数,让同等相关度下更新的文档排在前面。注意衰减要温和——如果某个问题只有一份三年前的文档能回答,把它压到召回窗口外,结果是从”给出旧答案”变成”给不出答案”,未必更好。重排层怎么加这类信号,《RAG Reranker 选型与评测》里有相关讨论。

下架。从检索范围移除,但保留原文和索引记录。适用于已被新版本明确取代的内容,比如旧版政策条款。下架必须可回滚,并且要记录下架原因和操作人。

删除。只用于确认有害且无保留价值的内容。绝大多数情况下下架就够了,删除会让后续的问题追溯失去材料。

无论哪一种,都应该在回答链路上留下痕迹。用户投诉”这个答案是错的”时,能查到当时命中了哪些文档、它们的年龄和复核状态,比事后重新猜测有效得多——这一点和《RAG 效果差的排查路径》里强调的固定失败样本是同一个思路。

六、让时间信息进入回答

监控解决的是”我们知道内容旧了”,还需要解决”用户知道内容旧了”。

把证据文档的日期放进上下文,并在提示词中明确要求:涉及会随时间变化的事实时,说明信息的时间点。效果上,一句”根据 2025 年 3 月的文档,流程是……”远好于一个语气笃定但没有时间锚的答案。

这件事有两个容易做错的地方。一是把当前日期也塞进上下文却不做区分,模型可能把当前日期误当成内容日期写进回答。二是要求模型”评估信息是否过期”——模型没有能力判断一份内部文档今天是否还成立,它只能复述你给它的时间元数据。判断留给规则,模型只负责如实呈现。

引用的真实性是另一个独立问题:模型声称引用了某份文档,不代表内容真的来自那里。这一层需要单独校验,《RAG 引用真实性自动校验》有具体方法。

七、小团队的最小实现

不需要先建平台。按下面顺序推进,每一步都能单独产生价值:

  1. 加字段。给 metadata 补上最后实质修改时间和内容类型两个字段。只有这两个,就已经能算出年龄分布。
  2. 出一张报表。每周跑一次脚本,按内容类型统计超期条目数和其中被召回过的数量。先看趋势,不开告警。
  3. 接使用侧反馈。在回答界面加一个”信息已过期”的反馈入口,把它和命中的文档 ID 关联起来。这是投入产出比最高的一步。
  4. 只对最高一级开告警。金额、权限、合规三类内容超期且有召回,单条告警。其余留在报表里。
  5. 再谈自动化。等前四步稳定运行两三个月,积累了足够的真实案例,再考虑自动降权和复核排期。

顺序反过来做——先搭自动降权和全量告警——通常的结局是规则调不准、告警没人看,半年后整套机制废弃。

监控新鲜度的目标不是让知识库里没有旧内容,而是让旧内容在被用到的时候能被识别出来。

八、相关阅读