文章目录
- LangChain 时间加权检索:让"最近的事"更容易被想起
- 一、什么场景需要时间加权
- 二、核心公式
- 2.1 decay_rate 怎么理解
- 三、上手
- 四、两个必须设置的 metadata 字段
- 五、importance:给重要记忆加权
- 六、实战:给 Agent 做长期记忆
- 七、和普通检索的对比
- 八、常见坑
- 九、什么时候别用它
- 十、小结
LangChain 时间加权检索:让"最近的事"更容易被想起
人的记忆有个特点:刚发生的事记得最清楚,越久远越模糊。
但向量检索是"一视同仁"的——三年前的文档和昨天的文档,
只要语义相近就一样容易被召回。有些场景这是错的。这篇讲
TimeWeightedVectorStoreRetriever怎么用、
它的公式是什么意思,以及哪些场景真的需要它。
一、什么场景需要时间加权
| 场景 | 需不需要 |
|---|---|
| 企业制度文档库(新旧版本共存) | 需要(新版本优先) |
| 新闻/公告检索 | 需要(最新的更重要) |
| 对话记忆(Agent 记住用户说过的话) | 非常需要 |
| 技术手册、API 文档 | 不太需要(新旧都有效) |
| 历史档案查询 | 不需要(就是要查老的) |
判断标准很简单:“更近的是不是更重要”。
二、核心公式
score = 语义相似度 + 时间权重 时间权重 = decay_rate ^ hours_passed展开一点:
最终分数 = (1 - 时间因素权重) × 语义分 + 时间因素权重 × recency_score recency_score = exp(-decay_rate × hours_passed)其中hours_passed是"这条内容被访问/创建到现在过了多少小时"。
2.1 decay_rate 怎么理解
decay_rate控制衰减有多快:
| decay_rate | 含义 | 半衰期 |
|---|---|---|
| 0.1 | 很慢 | 约 7 小时 |
| 0.01 | 默认,适中 | 约 69 小时(近 3 天) |
| 0.001 | 很慢 | 约 29 天 |
| 0.5 | 极快 | 约 1.4 小时 |
半衰期 ≈ ln(2) / decay_rate 小时。
选值经验:
- 对话记忆(几分钟到几小时):
0.05 ~ 0.1,让旧对话快速淡出; - 新闻/公告(以天计):
0.005 ~ 0.01; - 企业文档(以月计):
0.0005 ~ 0.001。
三、上手
fromlangchain_community.vectorstoresimportFAISS# 或其他fromlangchain.retrieversimportTimeWeightedVectorStoreRetrieverfromlangchain_openaiimportOpenAIEmbeddingsfromlangchain_core.documentsimportDocumentimportdatetime embeddings=OpenAIEmbeddings()vectorstore=FAISS.from_texts(["初始文本"],embeddings)retriever=TimeWeightedVectorStoreRetriever(vectorstore=vectorstore,decay_rate=0.01,# 衰减速率k=4,# 返回条数other_score_keys=["importance"],# 可选的额外打分维度)# 添加带时间的文档docs=[Document(page_content="用户说他叫小明",metadata={"last_accessed_at":datetime.datetime.now(),"created_at":datetime.datetime.now()},),Document(page_content="用户喜欢喝咖啡",metadata={"last_accessed_at":datetime.datetime.now()-datetime.timedelta(days=3),"created_at":datetime.datetime.now()-datetime.timedelta(days=3)},),]retriever.add_documents(docs)results=retriever.invoke("用户叫什么")四、两个必须设置的 metadata 字段
这个 retriever依赖两个时间字段,缺了会报错:
| 字段 | 含义 |
|---|---|
created_at | 文档创建时间 |
last_accessed_at | 最后一次被访问的时间 |
last_accessed_at会在文档被检索到时自动更新——
这就是关键设计:被反复想起的记忆会"保鲜",
像人一样,经常回忆的事不容易忘。
# 内部行为(简化)defget_relevant_documents(query):docs=vectorstore.similarity_search(query,k=self.k*10)fordindocs:# 被命中就刷新访问时间d.metadata["last_accessed_at"]=datetime.now()# 按时间加权重排...五、importance:给重要记忆加权
除了时间,还可以给每条记忆一个"重要度":
retriever=TimeWeightedVectorStoreRetriever(vectorstore=vectorstore,decay_rate=0.01,k=4,other_score_keys=["importance"],)retriever.add_documents([Document(page_content="用户说他叫小明",metadata={"last_accessed_at":datetime.datetime.now(),"created_at":datetime.datetime.now(),"importance":0.9,# 名字很重要},),Document(page_content="用户提到今天天气不错",metadata={"last_accessed_at":datetime.datetime.now(),"created_at":datetime.datetime.now(),"importance":0.1,# 闲聊不重要},),])最终分数 = 语义 × (1-w) + 时间 × w + importance 加成。
典型用法:
| 内容 | importance |
|---|---|
| 用户姓名、身份、偏好 | 0.8 ~ 1.0 |
| 用户提出的具体需求 | 0.6 ~ 0.8 |
| 一般对话 | 0.1 ~ 0.3 |
六、实战:给 Agent 做长期记忆
这是它最典型的应用场景。
importdatetimefromlangchain.retrieversimportTimeWeightedVectorStoreRetrieverfromlangchain_community.vectorstoresimportFAISSfromlangchain_openaiimportOpenAIEmbeddings,ChatOpenAIfromlangchain_core.documentsimportDocument embeddings=OpenAIEmbeddings()vs=FAISS.from_texts(["记忆库初始化"],embeddings)memory=TimeWeightedVectorStoreRetriever(vectorstore=vs,decay_rate=0.05,k=5,other_score_keys=["importance"],)defremember(content:str,importance:float=0.5):now=datetime.datetime.now()memory.add_documents([Document(page_content=content,metadata={"created_at":now,"last_accessed_at":now,"importance":importance},)])defrecall(question:str)->str:docs=memory.invoke(question)return"\n".join(d.page_contentfordindocs)# 对话中remember("用户叫小明,是一名后端工程师",importance=0.9)remember("用户在做一个 RAG 项目",importance=0.7)remember("用户说今天有点累",importance=0.2)# 几轮之后print(recall("用户是做什么的?"))# → "用户在做一个 RAG 项目"(近期 + 重要度中等,排前面)相比ConversationBufferMemory(全量保留历史)的优势:
- 上下文不会无限增长;
- 只召回和当前问题相关的记忆;
- 旧的、不重要的记忆会自然淡出。
七、和普通检索的对比
# 同样的文档,一个是一年前的,一个是昨天的old=Document("项目用 PostgreSQL",metadata={...一年前...})new=Document("项目改用 MySQL 了",metadata={...昨天...})# 普通检索:问"项目用什么数据库"# → 可能两条都召回,甚至先召回 PostgreSQL(语义更接近"数据库")# 时间加权:问"项目用什么数据库"# → MySQL 排前面(更新)注意:它不会把旧文档过滤掉,只是降权。
如果你要的是"只要最新的版本",
那应该用 metadata 过滤(version == latest),
而不是时间加权——别用错了工具。
八、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
报缺少last_accessed_at | metadata 没设这两个字段 | 添加时务必带上 |
| 时间加权没效果 | decay_rate 太小 | 调到 0.01 以上试试 |
| 全被新文档霸占 | decay_rate 太大 | 调小 |
| 想过滤掉旧版本 | 用错工具了 | 应该用 metadata 过滤 |
| 结果里出现"初始化文本" | 建库时的占位内容没删 | 建库后清空,或用真实内容初始化 |
最后一个坑很容易中:FAISS.from_texts(["初始文本"])里的
"初始文本"会一直留在库里。建议初始化后清掉,
或者用第一条真实记忆来初始化。
九、什么时候别用它
| 情况 | 理由 |
|---|---|
| 文档价值与时间无关 | 白白增加复杂度 |
| 需要精确过滤"最新版本" | 该用 metadata 过滤 |
| 数据量很小 | 直接全量给模型就行 |
| 用 FAISS 之外的库 | 部分库的时间字段处理有差异,先验证 |
它不是必需品。大部分 RAG 场景用普通向量检索 + metadata 过滤就够了。
只有"记忆"类场景(对话记忆、新闻流)才真正需要它。
十、小结
- 时间加权解决"越近越重要"的排序需求,
公式:语义分 + 时间权重,时间权重按decay_rate ^ hours衰减; decay_rate:对话记忆 0.05~0.1,新闻 0.005~0.01,企业文档 0.0005~0.001;- metadata 必须带
created_at和last_accessed_at,
后者会在命中时自动刷新(常被想起的记忆更"保鲜"); - 可选的
importance字段让重要记忆不被时间冲淡; - 典型场景:Agent 长期记忆(替代全量历史,避免上下文膨胀);
- ⚠️它只降权不过滤,要"只要最新版"请用 metadata 过滤。
下一篇讲元数据过滤与多租户隔离——生产环境怎么保证 A 公司看不到 B 公司的数据。