本文中的 Mem0 源码片段来自 Mem0 官方 GitHub 仓库,并使用了AI工具进行学习,仅用于个人学习与源码阅读记录。源码链接参见:mem0/mem0 at main · mem0ai/mem0 · GitHub
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
python
# 定义一个内部方法,用于从向量存储中搜索与 query 相关的记忆数据
# query:用户查询文本
# filters:搜索时使用的过滤条件
# limit:最终返回结果数量
# threshold:最低相关性阈值,默认值为 0.1
# explain:是否返回详细的评分解释
# show_expired:是否允许返回已经过期的数据
def _search_vector_store(
self,
query,
filters,
limit,
threshold=0.1,
explain=False,
show_expired=False
):
# ----------------------------
# 兼容性处理:防止 threshold 为 None
# ----------------------------
# 如果调用方显式传入了 None
if threshold is None:
# 则恢复为默认阈值 0.1
# 这样可以兼容旧版本代码或旧调用方式
threshold = 0.1
# ----------------------------
# Step 1:预处理用户查询
# ----------------------------
# 对查询文本进行词形还原
# 主要用于后面的 BM25 关键词搜索
# 例如:
# "running" -> "run"
# "cars" -> "car"
query_lemmatized = lemmatize_for_bm25(query)
# 从用户查询中提取实体
# 实体可能包括:人名、公司名、地点、产品名等
# 后面可以根据实体匹配情况对搜索结果进行加权
query_entities = extract_entities(query)
# ----------------------------
# Step 2:将查询文本转换成向量
# ----------------------------
# 调用 embedding 模型,把 query 转换成向量
# "search" 一般表示当前 embedding 是用于搜索场景
embeddings = self.embedding_model.embed(query, "search")
# ----------------------------
# Step 3:进行语义搜索
# ----------------------------
# 为了给后续混合排序提供更多候选结果,
# 实际从向量数据库取出的结果数量会比 limit 更多
#
# limit * 4:
# 最终需要 10 条时,可以先获取 40 条候选
#
# 60:
# 保证候选池至少有 60 条数据
internal_limit = max(limit * 4, 60)
# 调用向量数据库进行语义搜索
semantic_results = self.vector_store.search(
# 原始查询文本
query=query,
# 查询文本对应的 embedding 向量
vectors=embeddings,
# 最多获取 internal_limit 条候选结果
top_k=internal_limit,
# 使用指定的过滤条件
filters=filters
)
# ----------------------------
# Step 4:进行关键词搜索
# ----------------------------
# 调用向量存储提供的 keyword_search 方法
# 通常这里会使用 BM25 等关键词检索算法
keyword_results = self.vector_store.keyword_search(
# 使用经过词形还原之后的查询文本
query=query_lemmatized,
# 同样多取一些候选结果,给后面统一排序使用
top_k=internal_limit,
# 使用和语义搜索相同的过滤条件
filters=filters
)
# ----------------------------
# Step 5:计算 BM25 分数
# ----------------------------
# 创建一个字典
# 用于保存每条 memory 对应的 BM25 归一化分数
#
# 最终结构类似:
# {
# "memory_id_1": 0.82,
# "memory_id_2": 0.65
# }
bm25_scores = {}
# keyword_search 有可能返回 None
# 所以先判断搜索结果是否存在
if keyword_results is not None:
# 根据当前 query 计算 BM25 分数归一化时需要的参数
#
# midpoint:归一化曲线的中点
# steepness:归一化曲线的陡峭程度
midpoint, steepness = get_bm25_params(
query,
lemmatized=query_lemmatized
)
# 遍历关键词搜索返回的每一条结果
for mem in keyword_results:
# 判断 mem 对象是否直接拥有 id 属性
# 如果有,则使用 mem.id
# 否则把 mem 当成字典,通过 mem.get("id") 获取
#
# 最后统一转成字符串,方便作为字典 key
mem_id = (
str(mem.id)
if hasattr(mem, "id")
else str(mem.get("id", ""))
)
# 获取当前结果的原始 BM25 分数
#
# 如果 mem 是对象,则读取 mem.score
# 如果 mem 是字典,则读取 mem["score"]
# 如果没有 score,则默认使用 0
raw_score = (
mem.score
if hasattr(mem, "score")
else mem.get("score", 0)
)
# 只有 score 存在并且大于 0 时才进行处理
if raw_score and raw_score > 0:
# 将原始 BM25 分数进行归一化
# 避免 BM25 的数值范围与其他评分差异太大
bm25_scores[mem_id] = normalize_bm25(
raw_score,
midpoint,
steepness
)
# ----------------------------
# Step 6:计算实体匹配加分
# ----------------------------
# 初始化实体加权结果
#
# 结构可能类似:
# {
# "memory_id_1": 0.1,
# "memory_id_2": 0.2
# }
entity_boosts = {}
# 只有 query 中成功提取到了实体
# 才需要进一步计算实体匹配加分
if query_entities:
# 根据查询中的实体以及 filters
# 计算不同 memory 的实体匹配加权分数
entity_boosts = self._compute_entity_boosts(
query_entities,
filters
)
# ----------------------------
# Step 7:构建语义搜索候选集合
# ----------------------------
# 创建候选结果列表
candidates = []
# 遍历向量搜索得到的结果
for mem in semantic_results:
# 尝试从 mem 中获取 payload
#
# payload 一般用于保存原始业务数据,比如:
# {
# "data": "...",
# "user_id": "...",
# "created_at": "...",
# }
#
# 如果没有 payload,则使用空字典
payload = mem.payload if hasattr(mem, "payload") else {}
# 如果 show_expired=False,
# 并且当前 payload 已经过期
if not show_expired and _payload_is_expired(payload):
# 跳过这条已经过期的数据
continue
# 获取当前 memory 的唯一 ID
# 并统一转换成字符串
mem_id = str(mem.id)
# 将当前结果整理成统一的数据结构
# 然后加入候选列表
candidates.append({
# memory 的唯一标识
"id": mem_id,
# 向量搜索返回的语义相关性分数
"score": mem.score,
# 原始业务数据
"payload": payload,
})
# ----------------------------
# Step 8:综合评分并排序
# ----------------------------
# 调用统一的评分和排序函数
scored_results = score_and_rank(
# 向量搜索得到的候选数据
semantic_results=candidates,
# BM25 关键词匹配分数
bm25_scores=bm25_scores,
# 实体匹配额外加分
entity_boosts=entity_boosts,
# 最低相关性阈值
# 分数低于该阈值的结果可能会被过滤掉
threshold=threshold,
# 最终最多返回 limit 条
top_k=limit,
# 是否生成详细评分解释
explain=explain,
)
# ----------------------------
# Step 9:格式化最终结果
# ----------------------------
# 定义需要从 payload 中提升到最终结果顶层的字段
#
# 这些字段不会继续放在 metadata 中,
# 而是直接放到 memory_item_dict 顶层
promoted_payload_keys = [
# 用户 ID
"user_id",
# Agent ID
"agent_id",
# 一次运行任务的 ID
"run_id",
# 行为主体 ID
"actor_id",
# 当前记忆对应的角色
"role",
# 当前数据归属于谁
"attributed_to",
# 数据过期时间
"expiration_date",
]
# 定义所有"核心字段"和"需要提升的字段"
#
# 后面剩下的 payload 字段会被统一放入 metadata
core_and_promoted_keys = {
# memory 的主要文本数据
"data",
# 数据 hash
"hash",
# 创建时间
"created_at",
# 更新时间
"updated_at",
# 数据 ID
"id",
# 经过词形还原后的文本
"text_lemmatized",
# 数据归属信息
"attributed_to",
# 使用 * 解包 promoted_payload_keys
# 将其中所有字段加入集合
*promoted_payload_keys,
}
# 创建最终返回结果列表
original_memories = []
# 遍历已经完成综合评分和排序的结果
for scored in scored_results:
# 获取当前结果对应的 payload
#
# 如果 payload 为 None,则使用空字典
payload = scored.get("payload") or {}
# 如果 payload 中没有真正的 data 数据
if not payload.get("data"):
# 跳过当前候选结果
# 防止返回没有实际记忆内容的数据
continue
# 创建标准的 MemoryItem 对象
memory_item_dict = MemoryItem(
# 设置 memory ID
id=scored["id"],
# 真正的记忆文本
# 如果不存在则使用空字符串
memory=payload.get("data", ""),
# 数据 hash
hash=payload.get("hash"),
# 创建时间
created_at=payload.get("created_at"),
# 更新时间
updated_at=payload.get("updated_at"),
# 使用综合排序之后的最终 score
score=scored["score"],
# 将 Pydantic 模型转换成 Python 字典
).model_dump()
# 遍历所有需要提升到顶层的 payload 字段
for key in promoted_payload_keys:
# 判断 payload 中是否存在该字段
if key in payload:
# 如果存在,则直接复制到最终结果顶层
memory_item_dict[key] = payload[key]
# 提取 payload 中剩余的扩展字段
#
# 如果字段不属于核心字段或 promoted 字段,
# 就把它作为 additional_metadata 保存
additional_metadata = {
k: v
for k, v in payload.items()
if k not in core_and_promoted_keys
}
# 如果存在额外的 metadata
if additional_metadata:
# 如果最终结果中还没有 metadata 字段
if not memory_item_dict.get("metadata"):
# 创建一个空 metadata 字典
memory_item_dict["metadata"] = {}
# 把扩展字段合并进入 metadata
memory_item_dict["metadata"].update(
additional_metadata
)
# 如果开启了 explain 模式,
# 并且当前评分结果中包含 score_details
if explain and "score_details" in scored:
# 将详细的评分信息加入最终返回结果
memory_item_dict["score_details"] = scored[
"score_details"
]
# 将处理完成的 memory 加入最终结果列表
original_memories.append(memory_item_dict)
# 返回最终搜索结果
return original_memories
这段代码本质上实现的是一个 混合检索(Hybrid Search)+ 重排序(Reranking)流程,不只是简单的向量搜索。
可以把整个调用链理解成:
用户输入 query
↓
查询文本预处理
├── 词形还原
└── 实体提取
↓
Embedding
↓
┌───────────────┐
│ 两路召回 │
├───────────────┤
│ ① Vector Search │
│ 语义搜索 │
│ │
│ ② Keyword Search │
│ BM25关键词搜索 │
└──────────────┘
↓
BM25 分数归一化
↓
实体匹配加分
↓
过滤过期 Memory
↓
score_and_rank()
综合计算最终相关性
↓
threshold 过滤
↓
Top K
↓
整理 MemoryItem
↓
返回最终结果
def _compute_entity_boosts(self, query_entities, filters):
python
```python
# 定义一个实例方法:根据查询中的实体,计算每条 memory(记忆)的实体加权分数
def _compute_entity_boosts(self, query_entities, filters):
# 方法说明:
# 这个函数会从实体存储(entity store)中搜索和查询实体相似的实体,
# 然后找到这些实体关联的 memory,并给这些 memory 增加一个 boost(加权分数)
"""Compute per-memory entity boosts from entity store search.
For each extracted entity from the query:
1. Embed the entity text
2. Search the entity store (threshold >= 0.5)
3. For each matched entity, boost its linked memories
Returns:
Dict mapping memory_id (str) -> max entity boost [0, 0.5].
"""
# 创建一个 set,用于记录已经出现过的实体,避免重复处理
seen = set()
# 创建一个列表,用于保存去重后的实体
deduped = []
# 最多只处理 query_entities 中的前 8 个实体,防止实体数量过多导致搜索开销太大
for entity_type, entity_text in query_entities[:8]:
# 对实体文本进行标准化处理,例如可能会统一大小写、去掉空格等
# 具体标准化规则取决于 _normalize_entity_text() 的实现
key = self._normalize_entity_text(entity_text)
# 如果标准化后的实体不为空,并且之前没有出现过
if key and key not in seen:
# 将当前实体的标准化结果加入 seen,表示已经处理过
seen.add(key)
# 将实体类型和原始实体文本保存到去重后的列表中
deduped.append((entity_type, entity_text))
# 如果去重之后没有任何有效实体
if not deduped:
# 直接返回空字典,因为没有实体可以用于计算 memory boost
return {}
# 从 filters 中只保留 user_id、agent_id、run_id 这几个允许用于实体搜索的过滤条件
# 同时要求对应的值不能为空
search_filters = {
k: v
for k, v in filters.items()
if k in ("user_id", "agent_id", "run_id") and v
}
# 创建一个字典,用于保存每个 memory_id 对应的最大实体加权分数
memory_boosts = {}
# 使用 try 捕获整个实体 boost 计算过程中的异常
# 这样即使实体搜索失败,也不会影响主流程
try:
# 从 deduped 中提取实体文本
# deduped 中每一项结构为:(entity_type, entity_text)
# 这里使用 _ 忽略 entity_type,只保留文本
entity_texts = [text for _, text in deduped]
# 批量对实体文本进行向量化
# "search" 一般表示生成用于检索场景的 embedding
embeddings = self.embedding_model.embed_batch(
entity_texts,
"search"
)
# 检查 embedding 数量是否和输入文本数量一致
# 正常情况下,每个实体文本应该对应一个 embedding 向量
if len(embeddings) != len(entity_texts):
# 如果数量不一致,则记录 warning 日志
logger.warning(
"embed_batch returned %d vectors for %d texts --- skipping entity boost",
len(embeddings),
len(entity_texts),
)
# 因为 embedding 数据不完整,所以直接返回当前的 memory_boosts
# 此时通常还是空字典
return memory_boosts
# 获取当前对象中的 entity_store
# entity_store 一般用于存储和搜索实体向量
entity_store = self.entity_store
# 定义一个内部函数,用于搜索单个实体
def _search_entity(entity_text, embedding):
# 在实体存储中进行向量搜索
return entity_store.search(
# 原始实体文本,可以用于关键词搜索、日志记录或混合检索
query=entity_text,
# 当前实体对应的 embedding 向量
vectors=embedding,
# 最多返回 500 个候选实体
top_k=500,
# 添加 user_id / agent_id / run_id 等过滤条件
filters=search_filters
)
# 创建一个线程池
# max_workers=4 表示最多同时使用 4 个线程进行实体搜索
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
# 创建一个字典,用于保存 Future 对象和实体文本之间的对应关系
futures = {
# pool.submit() 会异步在线程池中执行 _search_entity()
# text 是实体文本,emb 是对应的 embedding
pool.submit(_search_entity, text, emb): text
# 使用 zip 将实体文本和 embedding 一一配对
for text, emb in zip(entity_texts, embeddings)
}
# as_completed() 会按照任务实际完成的顺序返回 Future
# 而不是按照提交顺序等待
for future in concurrent.futures.as_completed(futures):
# 单独捕获每一个实体搜索任务的异常
try:
# 获取当前线程任务的搜索结果
# 如果线程执行过程中发生异常,这里会重新抛出
matches = future.result()
# 如果某一个实体搜索失败
except Exception as e:
# 记录 warning 日志,但不会终止其他实体的搜索
logger.warning(
"Entity boost search failed for one entity: %s",
e
)
# 跳过当前失败的实体,继续处理下一个搜索结果
continue
# 遍历当前实体搜索得到的所有匹配结果
for match in matches:
# 如果 match 对象存在 score 属性,就获取相似度分数
# 否则默认相似度为 0.0
similarity = (
match.score
if hasattr(match, 'score')
else 0.0
)
# 如果实体相似度低于 0.5,则认为匹配程度不够
if similarity < 0.5:
# 跳过当前匹配结果
continue
# 如果 match 中存在 payload 属性,则取出 payload
# payload 通常保存实体的额外元数据
# 如果不存在,则使用空字典
payload = (
match.payload
if hasattr(match, 'payload')
else {}
)
# 从 payload 中获取当前实体关联的 memory_id 列表
# 如果没有 linked_memory_ids 字段,则默认使用空列表
linked_memory_ids = payload.get(
"linked_memory_ids",
[]
)
# 确保 linked_memory_ids 真的是 list 类型
# 避免错误数据,例如字符串、None 等
if not isinstance(linked_memory_ids, list):
# 数据格式错误时跳过当前实体
continue
# 获取当前实体关联了多少条 memory
# 使用 max(..., 1) 保证结果至少为 1,避免后续计算出现异常
num_linked = max(
len(linked_memory_ids),
1
)
# 根据实体关联的 memory 数量计算一个权重
#
# 公式:
# weight = 1 / (1 + 0.001 * (num_linked - 1)^2)
#
# 含义:
# 如果一个实体只关联很少的 memory,权重会比较接近 1
# 如果一个实体关联大量 memory,权重会逐渐降低
#
# 这样可以防止"过于通用"的实体给太多 memory 增加很高的权重
memory_count_weight = (
1.0
/
(
1.0
+ 0.001
* ((num_linked - 1) ** 2)
)
)
# 计算当前实体最终贡献的 boost 分数
#
# boost =
# 实体相似度
# × 全局实体 boost 权重
# × memory 数量衰减权重
boost = (
similarity
* ENTITY_BOOST_WEIGHT
* memory_count_weight
)
# 遍历当前实体所关联的所有 memory_id
for memory_id in linked_memory_ids:
# 确保 memory_id 不为空
if memory_id:
# 统一把 memory_id 转换为字符串
# 避免有些 ID 是 int、有些是 str,造成字典 key 不一致
memory_key = str(memory_id)
# 更新当前 memory 的实体 boost
#
# 如果同一个 memory 被多个实体匹配到,
# 不把所有 boost 相加,而是只保留最大的那个 boost
memory_boosts[memory_key] = max(
# 当前 memory 已经存在的 boost,没有则默认为 0
memory_boosts.get(memory_key, 0.0),
# 当前实体计算出来的 boost
boost
)
# 如果整个实体 boost 计算过程中出现了未处理的异常
except Exception as e:
# 记录日志,但不让异常继续向上传播
logger.warning(
f"Entity boost computation failed: {e}"
)
# 返回 memory_id -> entity boost 的映射
return memory_boosts
```
## 这段代码整体在做什么?
这个函数的核心作用可以概括成一句话:
**根据用户查询中出现的实体,找到和这些实体相关的 memory,然后提高相关 memory 在后续检索排序中的分数。**
例如用户问:
```text
帮我找一下之前讨论过的 OpenAI GPT-5 项目方案
```
假设实体抽取模块得到:
```python
query_entities = [
("ORG", "OpenAI"),
("PRODUCT", "GPT-5")
]
```
函数首先会把:
```text
OpenAI
GPT-5
```
转换成向量,然后去 `entity_store` 里面搜索相似实体。
假设搜索到:
```python
OpenAI
linked_memory_ids = ["mem_1", "mem_2"]
GPT-5
linked_memory_ids = ["mem_2", "mem_3"]
```
那么最后可能得到:
```python
{
"mem_1": 0.32,
"mem_2": 0.41,
"mem_3": 0.36
}
```
这里的数值就是每条 memory 得到的额外加权分数。
---
## 关键流程
整个函数实际上可以理解成:
```text
query_entities
↓
实体去重
↓
提取实体文本
↓
批量生成 embedding
↓
多线程搜索 entity_store
↓
过滤 similarity < 0.5 的实体
↓
读取实体关联的 linked_memory_ids
↓
计算 boost
↓
memory_id -> 最大 boost
```
最终得到:
```python
memory_boosts = {
memory_id: boost
}
```
---
## 为什么最多只处理 8 个实体?
代码:
```python
for entity_type, entity_text in query_entities[:8]:
```
这里相当于做了一个性能保护。
因为每个实体都需要:
```text
生成 embedding
+
搜索向量数据库
+
处理最多 500 条搜索结果
```
如果一次 query 抽出了几十个实体,搜索开销可能会迅速增加。
所以作者直接限制:
```python
最多 8 个实体
```
这是一个比较典型的工程优化。
---
## 为什么要去重?
例如抽取结果可能是:
```python
[
("ORG", "OpenAI"),
("ORG", "openai"),
("ORG", " OpenAI "),
]
```
经过:
```python
key = self._normalize_entity_text(entity_text)
```
有可能都会变成:
```python
openai
```
于是最终只搜索一次 OpenAI。
否则就会重复:
```text
embedding
搜索数据库
计算 boost
```
造成没有必要的计算。
---
## 为什么要用 embed_batch?
这里:
```python
embeddings = self.embedding_model.embed_batch(
entity_texts,
"search"
)
```
没有写成:
```python
for text in entity_texts:
embedding = embed(text)
```
原因是批量 embedding 通常效率更高。
比如:
```python
entity_texts = [
"OpenAI",
"GPT-5",
"ChatGPT",
"Sam Altman"
]
```
一次请求全部生成向量,通常比连续调用 4 次 embedding API 更高效。
---
## 为什么还要检查 embedding 数量?
代码:
```python
if len(embeddings) != len(entity_texts):
```
理论上:
```text
4 个文本
```
应该得到:
```text
4 个向量
```
但是 embedding 服务可能出现异常,例如:
```text
输入 4 个文本
返回 3 个向量
```
这时候:
```python
zip(entity_texts, embeddings)
```
会直接丢掉最后一个实体,而且实体和向量还有可能对应错误。
因此这里直接放弃 entity boost,属于比较稳妥的防御性编程。
---
## 为什么使用 ThreadPoolExecutor?
原始代码:
```python
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
```
实体搜索很可能属于 I/O 操作,例如访问:
```text
Qdrant
Milvus
Pinecone
Weaviate
Elasticsearch
远程 Vector DB
```
假设有 8 个实体,如果串行执行:
```text
实体1搜索结束
↓
实体2搜索结束
↓
实体3搜索结束
...
```
会比较慢。
线程池之后相当于:
```text
线程1 → 搜索实体 A
线程2 → 搜索实体 B
线程3 → 搜索实体 C
线程4 → 搜索实体 D
```
所以整体等待时间可以降低很多。
---
## 为什么使用 as_completed?
这里:
```python
for future in concurrent.futures.as_completed(futures):
```
不是按照:
```text
任务提交顺序
```
处理结果,而是谁先搜索结束,就先处理谁。
例如:
```text
A 搜索:300ms
B 搜索:50ms
C 搜索:100ms
```
那么处理顺序可能就是:
```text
B
C
A
```
不需要因为 A 比较慢而阻塞后面的结果。
---
## similarity < 0.5 为什么直接过滤?
代码:
```python
if similarity < 0.5:
continue
```
表示只有实体向量相似度:
```python
similarity >= 0.5
```
才认为这个实体和查询实体具有足够关联。
例如:
```text
查询实体:OpenAI
```
搜索结果可能类似:
```text
OpenAI 0.97
OpenAI Inc. 0.91
ChatGPT 0.68
Artificial Intelligence 0.43
Apple 0.22
```
那么低于:
```text
0.5
```
的匹配结果直接忽略。
这样可以减少无关 memory 被错误加权。
---
## linked_memory_ids 是什么?
实体本身通常不是最终需要返回的数据。
例如实体数据库里可能保存:
```python
{
"entity": "OpenAI",
"linked_memory_ids": [
"memory_001",
"memory_005",
"memory_007"
]
}
```
意味着:
```text
OpenAI
```
这个实体曾经出现在:
```text
memory_001
memory_005
memory_007
```
这些记忆中。
所以当当前查询又出现:
```text
OpenAI
```
时,可以把这些 memory 的排序分数提高。
这其实就是一种:
**Entity-aware Retrieval(实体感知检索)**
---
## memory_count_weight 这条公式非常关键
代码:
```python
memory_count_weight = 1.0 / (
1.0 + 0.001 * ((num_linked - 1) ** 2)
)
```
它本质上是一个:
**关联数量惩罚函数。**
如果:
```python
num_linked = 1
```
那么:
```text
weight = 1 / (1 + 0)
= 1
```
几乎不惩罚。
如果:
```python
num_linked = 10
```
那么:
```text
1 / (1 + 0.001 × 81)
≈ 0.925
```
如果:
```python
num_linked = 100
```
那么:
```text
1 / (1 + 0.001 × 9801)
≈ 0.0926
```
也就是说:
**一个实体关联的 memory 越多,这个实体的区分能力越弱。**
例如:
```text
张三
```
只出现在 2 条 memory 中,那么它非常有区分度。
但:
```text
AI
```
可能出现在几千条 memory 里面。
如果不做衰减:
```text
AI
```
这种非常宽泛的实体会让大量 memory 都得到 boost,反而降低搜索质量。
---
## boost 最终怎么算?
公式:
```python
boost = (
similarity
* ENTITY_BOOST_WEIGHT
* memory_count_weight
)
```
可以理解为:
```text
最终实体加分
=
实体匹配程度
×
实体加权系数
×
实体稀有程度
```
例如:
```python
similarity = 0.9
ENTITY_BOOST_WEIGHT = 0.5
memory_count_weight = 0.8
```
那么:
```text
boost
= 0.9 × 0.5 × 0.8
= 0.36
```
也就是说对应的 memory 可以得到:
```text
+0.36
```
左右的额外排序分。
---
## 为什么多个实体匹配同一个 memory 时取 max,而不是相加?
代码:
```python
memory_boosts[memory_key] = max(
memory_boosts.get(memory_key, 0.0),
boost
)
```
假设:
```text
OpenAI → memory_1 → boost 0.30
GPT-5 → memory_1 → boost 0.40
```
最终:
```python
memory_1 = 0.40
```
而不是:
```python
memory_1 = 0.70
```
这样做的一个重要作用是:
**防止某条 memory 因为碰巧包含很多实体而被重复累计加分,导致排序分数失控。**
所以它采用:
```text
多个实体贡献 → 取最大值
```
而不是:
```text
多个实体贡献 → 全部累加
```
这是一种比较保守的融合策略。
---
## 这段代码的核心思想
如果把所有工程细节去掉,它本质上就是:
```python
for 查询中的实体:
找相似实体
for 相似实体:
找这个实体对应的 memory
给这些 memory 加分
```
再稍微正式一点,可以表示为:
```text
Query
↓
Entity Extraction
↓
Entity Embedding
↓
Entity Vector Search
↓
Matched Entities
↓
Linked Memories
↓
Entity Boost
↓
Memory Ranking
```
因此,这个 `_compute_entity_boosts()` 本质上是一个:
**利用实体关系辅助 Memory Retrieval 排序的模块。**
它并不负责直接返回 memory,而是负责生成一组额外的排序特征:
```python
{
"memory_1": 0.31,
"memory_2": 0.42,
"memory_3": 0.18,
}
```
随后主搜索流程可以把它和:
```text
向量相似度
关键词相关性
时间衰减
importance
reranker score
```
等其他分数组合,得到最终的 memory 排名。
这个函数的核心作用可以概括成一句话:
根据用户查询中出现的实体,找到和这些实体相关的 memory,然后提高相关 memory 在后续检索排序中的分数。
整个函数实际上可以理解成:
query_entities ↓ 实体去重 ↓ 提取实体文本 ↓ 批量生成 embedding ↓ 多线程搜索 entity_store ↓ 过滤 similarity < 0.5 的实体 ↓ 读取实体关联的 linked_memory_ids ↓ 计算 boost ↓ memory_id -> 最大 boost最终得到:
memory_boosts = { memory_id: boost }