源码学习04

本文中的 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
}
相关推荐
Ray Wang1 小时前
Context上下文工程
python·学习·ai编程
梦影_1 小时前
Langchain简单快速上手教程(五)——聊天模型之流式传输
java·数据库·人工智能·python·langchain
ai2work2 小时前
17 · composer 进阶
python
L@ncor2 小时前
第二章 智能体发展史 · 学习笔记
人工智能·python
asdzx672 小时前
Python 实战:基于 Spire.PDF 为 PDF 文档添加自定义文本
python·pdf
m0_547486662 小时前
《Python数据分析与可视化项目教程》全套PPT课件2026
python·数据分析·数据可视化
hqyjzsb2 小时前
规划工商管理大学成长:搭建四层能力体系,重视高阶的 AI 能力建设
开发语言·人工智能·python·microsoft·职场和发展·数据挖掘·业界资讯
我不会起名字3222 小时前
一天一道算法题(35):电话号码的字母组合
java·数据结构·后端·python·leetcode·go·回溯
冯一川2 小时前
DeepSeek在Windows系统上部署
windows·python