Skip to content

[Bug] 摘要作业失败后记忆被标记为 ready 但摘要仍是占位符,语义检索静默失效(本机 88% 记忆受影响) #460

Description

@eric1hua

概述

摘要作业失败后,记忆会永久停留在「摘要排队中」占位符状态,但被标记为 ready。占位文本本身也能算出向量,而就绪判定只看「有没有向量」,不看向量是拿什么算的。

结果是语义检索静默降级:这些记忆参与检索,但它们的向量表达的是「摘要排队中」这五个字,跟任何真实查询都不相关。没有任何报错、没有任何界面提示。

我这台机器上 7,318 条记忆里有 6,472 条(88%) 处于这个状态,持续了 10 天才被发现——而且是我自己去翻数据库才发现的,不是 memmy 告诉我的。

环境:memmy 2.1.2,macOS 12.7.6 (Intel)。


根因

1. 就绪判定只看向量是否存在

service/import/import-job-processor.jsrestartFailedProcessing()

if (d.memories.hasVector(memory.id, "vec_summary")) {
    d.processing.update(memory.id, {
        state: "ready",
        stage: null,
        errorCode: null,
        errorMessage: null,
        failedAt: null,
        ...
    }, ["failed"]);
    continue;
}

storage/repositories.jshasVector()

hasVector(memoryId, vectorField = "vec_summary") {
    const row = this.db.prepare(`SELECT 1 AS ok
       FROM memory_vector_entries
       WHERE memory_id = ? AND vector_field = ?
       LIMIT 1`).get(memoryId, vectorField);
    return Boolean(row);
}

它只问「有没有这一行」。占位符摘要同样会产生 vec_summary 行,所以这个分支会把 failed 记忆清成 ready,并抹掉 errorCode / errorMessage / failedAt——故障证据一并消失。

服务每次启动都会跑一遍 restartFailedProcessing(),所以失败痕迹会被反复清理。

2. 配置类失败被写死成永不重试

失败若被判定为配置问题(例如模型名失效),retry_action 会写成 'none'。之后 POST /api/v1/memory/:id/processing/retry 第一道闸就拒绝:

if (current.retryAction === "none" || !current.stage)
    throw d.createError("conflict", current.errorMessage ?? "memory processing cannot be retried");

问题在于它回显数据库里存的旧错误字符串,不重新探测当前配置。我把模型配置修好之后再调这个接口,拿到的仍然是:

{"error":{"code":"conflict","message":"unknown provider for model claude-sonnet-4"}}

claude-sonnet-4 早已不在我的配置里。这些记忆没有任何官方途径能救回来。

3. 换嵌入模型会制造同类的静默失效

storage/sqlite-vec-store.jssearch()

const compatible = candidates.filter((candidate) => candidate.embeddingDim === query.length);
if (compatible.length === 0)
    return [];

维度不匹配直接返回空。我把嵌入模型从 384 维换到 1024 维之后,6,659 条旧向量对检索完全不可见,同样不报错、界面上也看不出来。用户合理地以为「换个更好的模型」,实际是把大部分记忆从检索里摘了出去。

4. 推理模型下摘要预算不足(新失败的来源)

config/index.js

export const MEMORY_SUMMARY_MAX_TOKENS = 512;

摘要模型如果是推理模型,reasoning 会吃光这 512 的输出配额,message.content 为空而 reasoning_content 有值,model/llm.jsReasoning exhausted the summary output token budget

这不是历史遗留——我今天仍在产生新的这类失败。而它们最终会被第 1 条的逻辑洗成 ready


复现

  1. 把摘要模型配成一个不可用的 provider/model
  2. 写入若干条记忆,等异步摘要作业失败
  3. 把摘要模型改回可用配置,重启服务
  4. memory_processing_state:这些记忆是 ready
  5. memories.info_jsonsummary 字段:仍然是 摘要排队中
  6. 用这些记忆的内容做语义检索:召回不到
-- 有多少「已就绪」的记忆其实还是占位符
SELECT COUNT(*) FROM memory_vector_entries e
JOIN memories m ON m.id = e.memory_id
WHERE e.vector_field = 'vec_summary'
  AND json_extract(m.info_json, '$.summary') LIKE '%排队中%';

期望行为

  1. 就绪判定要看摘要内容,不能只看向量是否存在。 占位符摘要不应算 ready——哪怕给个 ready_degraded 也好,至少别把它跟正常记忆混为一谈。

  2. retry_action 的判定要重新探测,不能回显旧字符串。 配置已经改了,上一次的错误信息不再代表当前状态。

  3. 这些情况要让用户看得见。 现在 Viewer 里完全没有「你的记忆库有 88% 是空的」这个信息。哪怕只是在设置页加一行统计也够了。

  4. 换嵌入模型时提示旧向量会失效,并提供重嵌入口。

  5. MEMORY_SUMMARY_MAX_TOKENS 对推理模型要放宽,或者至少在检测到 reasoning_content 非空、content 为空时给出可操作的提示。


我这边的临时方案

在等官方修复期间,我写了个本地工具,已开源:

https://github.com/eric1hua/memmy-backfill

体检

它做的事:

  • 体检——扫出占位符摘要、维度失配的向量、dead-letter 作业、卡住的处理状态。其中「失败作业」会比对同类型作业最后一次成功与最后一次失败的时间,区分「真瘫了」和「历史噪音」
  • 补全——按 memoryHasImportPipeline() 的规则正确选择 import_summary / trace_summary 重新入队(排错类型会被直接拒绝,这点花了我一些时间才搞清楚)
  • 重嵌——把旧维度向量重嵌到当前模型的维度
  • 解卡——放开 retry_action='none' 之后走官方的 processing/retry 接口

补全走 launchd,只在键鼠空闲 ≥5 分钟且 CPU 空闲 ≥50% 时运行,用户一回来立刻让路。

零第三方依赖,只用 Python 标准库,面板只绑 127.0.0.1

这些逻辑如果对官方实现有参考价值,欢迎直接取用(MIT)。不过我更希望的是这个工具没有存在的必要——普通用户不会去翻 SQLite,他们只会觉得「memmy 的记忆好像不太准」,然后不知道为什么。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions