type
Post
status
Published
date
Apr 28, 2026
slug
agent22
summary
tags
Agent
category
icon
password
结论先行:不能默认直接使用。向量相似度只能说明“语义上像”,不能说明“现在仍然正确”。在生产级 RAG 系统中,历史信息必须经过 时间有效性判断、版本冲突处理、时效性重排、引用日期暴露和过期拒答机制,才能进入最终上下文。
很多 RAG 系统上线初期都会遇到一个典型问题:用户问“公司最新报销政策是什么?”向量数据库检索出一段语义高度相关的“报销政策”,但它其实是两年前的旧版本。LLM 看见这段上下文后非常自信地回答,结果比不用 RAG 更危险。
这不是 embedding 模型差,也不是 chunk 切分问题,而是一个更底层的问题:
语义相关性 ≠ 时间正确性。
成熟框架已经在不同层面处理这个问题。LangChain 的 Time-Weighted Retriever 会把相似度和时间衰减结合起来;它的评分思路是
vectorRelevance + 时间衰减项,并且特别说明这里的时间是“距离上次访问的时间”,而不是文档创建时间。这个设计适合“记忆”类场景,但不一定适合“政策/价格/API 文档”这类事实时效场景。(LangChain 文档) LlamaIndex 也提供 FixedRecency、EmbeddingRecency、TimeWeightedPostprocessor 等后处理器,其中 TimeWeightedPostprocessor 源码会根据 last_accessed 计算 hours_passed,再把时间分数加到 embedding similarity 上。(Developer Documentation) Elasticsearch/OpenSearch/Vespa 这类检索系统则更强调把时间字段纳入排序函数,例如 date decay、freshness ranking、多阶段 rerank。(Elastic)一、为什么“语义相关但过期”的信息很危险?
最简单的 RAG 流程通常是这样:
这条链路默认假设:
只要 chunk 和 query 语义相似,它就可以作为上下文。
但在真实业务中,很多知识都有生命周期。
场景 | 旧信息的风险 | 是否能直接用 |
公司制度、HR 政策 | 高 | 不能 |
API 文档、SDK 用法 | 高 | 不能 |
产品价格、套餐规则 | 高 | 不能 |
法规、合规条款 | 极高 | 不能 |
历史事件回顾 | 低,甚至必须用旧信息 | 可以,但要匹配时间范围 |
数学公式、基础概念 | 低 | 通常可以 |
用户长期偏好记忆 | 中 | 需要看是否被更新或撤销 |
所以,生产级 RAG 不能只问:
这个 chunk 和问题像不像?
而要同时问:
这个 chunk 在当前问题的时间语境下还有效吗?
二、成熟 RAG 系统通常怎么处理“时效性”?
可以把成熟方案拆成四层。
1. 数据接入层:不要只存 text + embedding
生产级 RAG 的 chunk metadata 至少应该包含这些字段:
这里最容易犯错的是只存
created_at 或 ingested_at。这两个字段都不一定代表知识的新鲜度。created_at是文档什么时候被创建。updated_at是源文档什么时候被修改。ingested_at是系统什么时候把它放进向量库。effective_from/effective_to才是这条知识在业务上什么时候有效。
例如 2024 年写的一份政策,如果 2026 年仍然有效,它并不“过期”;反过来,昨天刚重新 ingest 的旧 PDF,也不等于它是最新知识。
LlamaIndex 的 RAG failure checklist 也明确把“索引中存在重复、过期、冲突版本”列为问题,并建议使用 doc_id 跟踪、刷新已变更文档、去重、定期重建索引、添加时间戳/版本号并按新鲜度过滤。(Developer Documentation)
三、错误示例:只看向量相似度
很多项目一开始会写成这样:
这个版本的问题是:
- 不知道用户问的是“现在”还是“历史上”。
- 不知道 chunk 是否已过期。
- 不知道多个版本之间谁覆盖谁。
- 不知道旧信息是否被新信息废弃。
- 不会在答案中暴露证据日期。
所以它会把“语义最像”的旧文档直接交给 LLM,导致答案看起来有引用、有依据,但实际上引用的是错误时间点的依据。
四、正确做法一:先判断用户问题的时间意图
不是所有问题都要偏向最新信息。问题可以分成几类:
可以用规则 + LLM 轻量分类:
这一步非常关键。比如:
“2022 年的报销政策是什么?”
这时候旧信息不是噪音,而是目标证据。
而:
“现在的报销政策是什么?”
这时候旧信息即使语义再相关,也应该被降权、过滤或标记为不可直接使用。
TimeR4 这类时间感知 RAG 研究也采用类似思想:先通过 retrieve-rewrite 显式化时间约束,再进行时间相关的 retrieve-rerank,以降低 temporal hallucination。(ACL 论文集)
五、正确做法二:把时间过滤前置到向量检索
1. 当前问题:只检索仍然有效的文档
伪代码如下:
不同向量数据库写法不同,但思想一样:不要等 TopK 出来后才发现都是旧数据。
Pinecone 支持在搜索时传入 metadata filter,从而把结果限制在符合 filter 表达式的记录中。(Pinecone 文档) Qdrant 支持 datetime range filter,并且 datetime payload 支持 RFC3339 格式,会解析成 UTC 比较。(Qdrant) Milvus 的 filtered search 会在 ANN 搜索前用 metadata filtering 缩小搜索范围,但也提示复杂过滤可能带来延迟,需要在标准过滤和迭代过滤之间取舍。(Milvus)
以 Qdrant 风格举例:
真实工程里还要注意:不是所有向量库都支持复杂的
OR / NULL 条件,必要时可以把 is_current=true、valid_year=2026、business_domain=finance_policy 这类字段预计算出来,降低 filter 复杂度。六、正确做法三:不要只做硬过滤,还要做时效性重排
硬过滤适合“绝对不能错”的场景,例如:
当前价格、当前政策、当前 API、当前法规。
但有些场景不适合一刀切。例如用户问:
“总结过去几年这项政策的变化。”
这时候旧文档也有价值,但应该和新文档一起排序,并让 LLM 理解时间线。
可以设计一个综合分数:
示例代码:
LangChain 和 LlamaIndex 都提供了类似“相似度 + 时间因素”的思想。LangChain 的 time-weighted retriever 把向量相关性和时间衰减项相加;LlamaIndex 的
TimeWeightedPostprocessor 源码中也会计算 hours_passed,再把 time_similarity 加到原始分数上。(LangChain 文档)但要特别注意:
对“长期记忆”来说,last_accessed_at很有用。对“事实正确性”来说,updated_at / effective_at / version更重要。
否则一个旧政策因为经常被问到,反而会被系统误认为“新鲜”。
七、正确做法四:处理多版本冲突,而不是把所有 chunk 塞给 LLM
常见错误是:TopK 里同时出现 v1、v2、v3 文档,然后全部塞给模型。模型可能会混合多个版本,生成一个现实中不存在的答案。
生产系统应该先做版本归并:
还可以加一个冲突检测器:
如果冲突无法自动解决,生成层应该拒绝直接给确定答案:
“我找到了多个时间版本的资料,但无法确认哪一个当前有效。最新可识别版本是 2026-04-01 更新的文档,旧版本为 2025-09-10。建议以最新版本为准,或人工确认。”
八、正确做法五:借鉴搜索引擎的时间衰减排序
如果你的底层不是纯向量库,而是 Elasticsearch/OpenSearch/Vespa 这类搜索系统,可以直接把时间作为 rank feature。
Elasticsearch 的 function score 支持 decay functions,可以对 date、numeric、geopoint 字段按距离 origin 的远近做平滑衰减;date 字段默认 origin 可用当前时间,并支持
scale、offset、decay 等参数。(Elastic) OpenSearch 文档也说明 decay functions 可用于 proximity 或 recency 排序,支持 Gaussian、exponential、linear 三种衰减曲线。(OpenSearch Documentation)示例:
Vespa 的多阶段排序也很适合生产级 RAG:先用便宜的 first-phase ranking 处理大量候选,再对少量候选做 second-phase/global-phase rerank;官方示例中就展示了
bm25(title) + 3*freshness(timestamp) 这种把文本相关性和 freshness 组合的 rank-profile。(Vespa. Big data. Real time. Open source.)九、生产级 Temporal RAG 架构
推荐架构如下:
核心原则:
时间约束越强,越要前置。当前政策、价格、库存、法规:先 filter,再 rerank。趋势分析、历史对比、演变总结:保留旧信息,但必须按时间线组织。基础知识、长期稳定内容:可以放宽时间权重,但仍要保留来源版本。
十、完整示例:时间感知 Retriever
下面是一个简化版实现,适合改造成 LangChain/LlamaIndex Retriever 或独立服务。
十一、生成阶段也要防止“旧信息污染答案”
Retriever 做得再好,生成阶段也要约束模型。
推荐 prompt:
上下文构造也不要只放正文,要把时间元数据暴露给模型:
这样模型才能回答:
根据 2026-03-01 更新、2026-04-01 生效的《报销政策 v3》,当前标准是……
而不是笼统地说:
根据文档,报销标准是……
十二、什么时候旧信息仍然可以使用?
不是所有旧信息都要丢弃。关键看用户意图和业务语义。
用户问题 | 旧信息是否可用 | 处理方式 |
“现在的 API 怎么调用?” | 不应直接用 | 只用当前版本 |
“v1 API 怎么调用?” | 可以 | filter version=v1 |
“过去三年政策怎么变化?” | 必须用 | 按时间线检索 |
“什么是余弦相似度?” | 可以 | 时间权重较低 |
“去年 Q4 的销售规则是什么?” | 可以 | as_of=去年 Q4 |
“当前套餐价格是多少?” | 不应直接用 | 强制 current filter |
“这个客户之前喜欢什么方案?” | 可以,但需看是否被更新 | memory + last_updated |
所以最终规则不是“旧信息一定不能用”,而是:
旧信息不能在没有时间解释的情况下,被当成当前事实直接使用。
十三、生产环境监控指标
上线后要持续监控“新鲜度”,否则向量库会慢慢变成历史垃圾场。
建议监控:
指标 | 含义 |
freshness@k | TopK 中满足时效要求的 chunk 比例 |
stale_hit_rate | 当前类问题命中过期文档的比例 |
conflict_rate | TopK 中出现多版本冲突的比例 |
source_age_p50/p95 | 被引用文档的年龄分布 |
temporal_answer_accuracy | 带时间约束问题的回答准确率 |
no_valid_context_rate | 找不到当前有效证据时的拒答比例 |
index_lag | 源文档更新到向量库更新的延迟 |
orphan_chunk_count | 源文档已删除但向量库仍存在的 chunk 数 |
尤其要关注:
如果这个指标升高,说明你的系统可能存在:
- 增量更新没有删除旧 chunk。
- doc_id/version 没设计好。
- filter 没生效。
- TTL 规则不合理。
- 用户问题的 temporal intent 没解析出来。
十四、最佳实践清单
1. 每个 chunk 必须有时间元数据
至少包含:
2. 当前类问题必须做硬过滤
例如:
3. 旧版本不能删除得太随意
历史问答、审计、回溯分析需要旧版本。正确方式是:
4. 不要用 last_accessed_at 代替 updated_at
last_accessed_at 适合 Agent memory,不适合事实时效。一个旧文档被频繁访问,不代表它仍然正确。5. 对高风险领域使用拒答机制
如果找不到当前有效文档,不要让模型“根据旧资料猜”。
6. 定期重建索引
不要永远 append。LlamaIndex 的故障清单也建议对变更文档做刷新、去重,并定期从源头重建索引。(Developer Documentation)
十五、总结
向量数据库解决的是“语义相似”问题,不解决“时间有效”问题。
生产级 RAG 必须从下面几个层面处理历史信息:
最终原则可以概括为一句话:
RAG 不应该把“最像的文档”交给 LLM,而应该把“在当前问题时间语境下最可信、最有效、最相关的证据”交给 LLM。
这也是从 demo 级 RAG 走向生产级 RAG 的关键分水岭。
分享
