个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain 时间加权检索:让"最近的事"更容易被想起](#LangChain 时间加权检索:让"最近的事"更容易被想起)
-
- 一、什么场景需要时间加权
- 二、核心公式
-
- [2.1 decay_rate 怎么理解](#2.1 decay_rate 怎么理解)
- 三、上手
- [四、两个必须设置的 metadata 字段](#四、两个必须设置的 metadata 字段)
- 五、importance:给重要记忆加权
- [六、实战:给 Agent 做长期记忆](#六、实战:给 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。
三、上手
python
from langchain_community.vectorstores import FAISS # 或其他
from langchain.retrievers import TimeWeightedVectorStoreRetriever
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
import datetime
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 会在文档被检索到时自动更新 ------
这就是关键设计:被反复想起的记忆会"保鲜" ,
像人一样,经常回忆的事不容易忘。
python
# 内部行为(简化)
def get_relevant_documents(query):
docs = vectorstore.similarity_search(query, k=self.k * 10)
for d in docs:
# 被命中就刷新访问时间
d.metadata["last_accessed_at"] = datetime.now()
# 按时间加权重排
...
五、importance:给重要记忆加权
除了时间,还可以给每条记忆一个"重要度":
python
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 做长期记忆
这是它最典型的应用场景。
python
import datetime
from langchain.retrievers import TimeWeightedVectorStoreRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.documents import Document
embeddings = OpenAIEmbeddings()
vs = FAISS.from_texts(["记忆库初始化"], embeddings)
memory = TimeWeightedVectorStoreRetriever(
vectorstore=vs, decay_rate=0.05, k=5,
other_score_keys=["importance"],
)
def remember(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},
)])
def recall(question: str) -> str:
docs = memory.invoke(question)
return "\n".join(d.page_content for d in docs)
# 对话中
remember("用户叫小明,是一名后端工程师", importance=0.9)
remember("用户在做一个 RAG 项目", importance=0.7)
remember("用户说今天有点累", importance=0.2)
# 几轮之后
print(recall("用户是做什么的?"))
# → "用户在做一个 RAG 项目"(近期 + 重要度中等,排前面)
相比 ConversationBufferMemory(全量保留历史)的优势:
- 上下文不会无限增长;
- 只召回和当前问题相关的记忆;
- 旧的、不重要的记忆会自然淡出。
七、和普通检索的对比
python
# 同样的文档,一个是一年前的,一个是昨天的
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 公司的数据。