记忆内核生产加固实录:数据污染、鉴权漂移与语义噪声
EraHerm-Memory 上线后第一周的生产巡检实录:103 条记忆 87 条是垃圾、MCP admin 端点全部 401、图谱实体一半是虚词、摘要模板前缀污染召回——每一环的根因、修复与实测数据,以及『怎么防止再发生』的机制化答案。
记忆内核生产加固实录:数据污染、鉴权漂移与语义噪声
EraHerm-Memory 是我自研的开源记忆内核(MIT),2026-08 上线 v0.10.0 并接入 Hermes Agent 作为长期记忆层。上线后第一周的生产巡检,暴露了一批「只有真实数据才会教你的」问题——103 条记忆里只有 12 条是有效事实,其余全是测试残留、重复堆积和脏实体。这一篇是完整的加固实录:问题现象、根因、修复方案与实测数据。
一、数据卫生:103 条记忆,87 条是垃圾
现象: 上线检查发现生产库 103 条记忆中 87 条已软删(deleted_at 非空),活跃记忆里混着 6 个测试用户(u_correct_demo / cursor_demo / test_plugin_* 等)共 27 条残留,还有 24 条 pinned(占活跃记忆 77%,多 pinned 会串扰召回排序)。
根因: 开发阶段用测试数据验证功能,测试用户没有隔离;consolidate(自动整理)从未在夜间跑过,重复记忆(同一事实 4 条 + 模板污染 2 条)持续堆积。
修复:
- 一次性清理脚本:软删测试残留 14 条 + laoda 测试镜像 1 条,孤儿(user_id 为空)2 条保持软删状态
- 手动触发 consolidate:database 簇 5 条压缩成 1 条摘要,重排权重 15 条
- 永久预防: 开启夜间自动整理(ERAHERM_CONSOLIDATION_ENABLED=true,每天 03:00 + APScheduler),从机制上杜绝再次堆积
结果: 活跃记忆 103 → 12 条,pinned 24 → 7 条(关键事实 + 身份)。
二、MCP 鉴权漂移:admin 端点全部 401
现象: 通过 MCP 工具调 consolidate 返回 401 Unauthorized,但 HTTP 直调正常。
根因: 两处漂移——app/mcp_server.py 的 HTTP 转发函数 _post 根本不带鉴权头;Cursor 适配器 eraherm_mcp_server.py 用了 Authorization: Bearer,而后端只认 X-Admin-Token 头(值取 ERAHERM_ADMIN_TOKEN)。consolidate / reembed / l3 三个 admin 端点经 MCP 全部 401。
修复:
- 两个适配器统一改为
X-Admin-Token头 - 启动脚本 start_mcp.sh 从 .env 导出 token(MCP 子进程继承的是过滤后的环境,不会自动带上)
- 新增回归测试
test_mcp_admin_auth_header_is_x_admin_token,用静态检查锁死头字段
结果: MCP → consolidate 从 401 变 200。教训:协议鉴权头漂移是「不会报错但静默失效」的典型 bug,必须用回归测试锁住。
三、图谱实体抽取:13 个实体里 9 个是垃圾
现象: 轻量图谱(图数据库的轻量替代,支撑 impact「改 A 影响谁」查询)里 13 个实体,9 个是「然后」「不」「方式」「不是 MySQL」「把两个完全不同的东西混在一起说了」这类虚词、否定残留和句子碎片。
根因: 正则抽取器的主语模式 [^s,。;、]+? 会抓到虚词当主语;宾语切分没清否定残留(「不是 MySQL」整段入库);没有实体质量门槛。
修复:
_is_valid_entity:停用词表(口语虚词/连接词)+ 长度门槛(单字除非白名单)+ 纯标点拒绝 + 句子碎片拒绝_clean_object:宾语先剥否定前缀(不是/不用/没有/非…)再入库- 生产库软删 9 个垃圾实体 + 7 条垃圾关系
结果: 图谱 13 → 4 个干净实体(数据库/PostgreSQL/Hermes/服务),关系只剩 1 条有效(数据库 → PostgreSQL)。回归测试 3 例锁死。
四、语义噪声:摘要模板前缀拉高弱相关
现象: 修复图谱后跑验收探针,弱相关查询「服务器配置」FAIL——0.501 分命中一条 consolidate 摘要。
根因: 夜间 consolidate 的摘要生成器把「【精华摘要·database】」模板前缀拼进摘要内容。这个前缀在 embedding 语义空间是纯噪声,把弱相关查询的向量相似度拉高了。与之前纠正反射的「正确事实:X(此前误为 Y)」模板是同一类问题——任何「【标签】内容」式模板都是语义噪声。
修复:
- 摘要改为干净事实句,不拼任何模板前缀(HeuristicSummarizer / LLMSummarizer 同步改)
- 软删生产库中带前缀的历史摘要(那条还拼接了过期身份信息,必须清)
- 回归测试
test_summarizer_no_template_prefix
结果: 验收探针 §8-E 全项 PASS(换词命中 / 无关不答 / 弱相关不蹭)。
五、收口与版本发布
72 个测试全过,按语义版本收口 v0.10.1(5 处版本号 + CHANGELOG + tag 推送)。修复链:数据卫生 → 鉴权漂移 → 图谱质量 → 语义噪声,每一环都有回归测试兜底。
演进方向(未落地)
- Embedding 模型升级(实测后暂缓): 当前 bge-small-zh-v1.5(512 维)。在 3.7G 内存的二号机做了完整的升级实验(2026-08-25):
- bge-m3(1024 维):fastembed 0.8.0 不支持,且模型 2.2GB 超出 1.6G 内存机器,直接排除。
- jina-zh(768 维):加载成功(常驻 841MB vs bge 251MB,推理 462ms vs 194ms),全量重嵌 101 条记忆后跑验收——翻车:「我的名字叫啥」任何门禁都召回失败;命中分数整体从 0.35-0.50 跌到 0.10-0.12,门禁 0.25 全挡、降到 0.1 弱相关就渗入,找不到干净的标定点。
- 复盘:分数偏低的真正主因是记忆年龄(26 天衰减)+ 身份记忆未 pinned 的数据卫生问题——两个模型表现几乎一致,换模型不是解药,数据工程才是。回退 bge-small,补身份记忆问法关键词并 pinned。
- 结论:模型切换是标定迁移工程,不是替换文件——升级前必须先跑基线验收。代码侧
_KNOWN_DIMS已预留 bge-m3 维度,等换机/数据量上来再评估。记忆含敏感内容,不走外部 embedding API。 - 人物/事实关系抽取: 规则抽取器只抽「依赖/使用/拥有」技术关系;「老大喜欢的人是X」这类人物关系需要 LLM extractor(已预留 Port 接口,生产未配置 LLM)。
小结
这轮加固的核心收获:生产记忆系统最贵的不是算法,是数据卫生和「静默失效」的防御。测试残留、模板噪声、鉴权漂移,每一个都不会在开发环境暴露,但都会在生产悄悄拉低质量。每修一个,都要问一句「怎么防止它再发生」——回归测试 + 自动整理 + 机制化,才是永久修复。