OpenViking 记忆提取挂了三天,最后发现是模型太聪明

事情要从三天前说起。Hermes Agent 接上 OpenViking 做长期记忆之后,我发现它从来不主动搜自己的知识库。每次对话都像失忆了一样,之前存的东西完全不记得。

一开始以为是配置没配对。毕竟 OpenViking 是字节跳动的开源项目,这种量级的团队不太可能犯低级错误,大概率是我哪里搞错了。版本 v0.3.14,VLM 一开始本地部署的 Ollama,后来切成了 MiniMax 的 API,embedding 是本地的 bge-small-zh。服务健康检查 200 OK,日志没报错,看起来一切正常。

viking_search 永远返回空。

第一层:向量库是空的

先确认最基本的——数据到底有没有进去。

curl http://127.0.0.1:1933/api/v1/observer/vikingdb

结果:Vector Count = 0。一整个向量库,空空如也。但 Semantic-Nodes 队列显示 3335 条正在处理和已完成——说明数据管道在跑,只是没入库。

等了几分钟再查,0 变成了 856。原来是异步队列积压,处理完就正常了。search 也通了。

但这只是资源索引(viking_add_resource)的部分。记忆提取(viking_remember 写入的会话记忆)还是另一回事。

第二层:VLM 提取了,但一个记忆都没存下来

viking_remember 调用返回 stored,消息确实进了 session 队列。session commit 也正常触发,task 状态 completed,VLM 甚至确认被调用了:

llm_token_usage: {prompt_tokens: 30893, completion_tokens: 4050, total_tokens: 34943}

三万多 token 的 prompt,四千 token 的回复。VLM 确实在工作。但 memories_extracted 是一个空对象 {}——八个分类全为零。

这就奇怪了。VLM 调了、回了、task 成功了,记忆去哪了?

翻 systemd 日志找到了答案:

Direct model validation failed: 1 validation error for StructuredMemoryOperations
profile.name
  Extra inputs are not permitted [type=extra_forbidden]

M2.5 确实提取了内容,质量还不错——它准确地识别出了 OpenViking 的五个工具、版本号、配置信息。但它输出的 JSON 长这样:

{"entities": [...], "tools": [...], "profile": {"name": "default", "content": "..."}, "identity": {...}}

而 OpenViking 的 Pydantic schema 期望的格式是:

{"reasoning": "...", "write_uris": [...], "edit_uris": [...], "delete_uris": [...]}

M2.5 凭自己的理解重新发明了 JSON 结构——它把记忆按类别分成了 entitiestoolsprofile 这些 key,甚至给 profile 加了一个 schema 里不存在的 name 字段。

Pydantic 的 extra='forbid' 策略直接把所有内容拒之门外。VLM 花了 35000 token 提取出来的记忆,一秒就被丢进了垃圾桶。

这个行为其实不怪 OpenViking。extra='forbid' 是工程上正确的防御——如果随便接受模型自由发挥的字段,脏数据就入库了。GPT 和 Claude 在指令遵循上足够强,不会犯这种错。但 M2.5 的指令遵循没想到这么弱——自己发明字段名、把 string 嵌套成 dict,完全脱离了 prompt 里定义的 schema。

第三层:不是模型越新越好

我开始怀疑是不是 M2.5 特有的问题。于是顺手测了 M2.1 和 M2——同样的默认模板,同样的 session,只换模型。加上之前 M2.7 也测过(但它的 API 限流太猛,语义摘要动不动 429,没法正经评估),实际对比了这三个:

模型 代数 默认模板提取 日志
M2.5 较新 0 条 extra_forbidden 爆炸
M2.1 中间 1 条 零报错
M2 最老 4 条 零报错

最老的 M2 表现最好,较新的 M2.5 反而挂零。这个结果有点反直觉,但仔细想想也合理——新一代模型往往更"有主见",倾向于用自己的方式组织输出。在创意性任务上这是优势,但在结构化 JSON 这种机械任务上反而是劣势。

类似的事在业界也有记录。有些模型升级后 benchmark 总分涨了,但 format-following 的得分跌了。只是之前看的都是别人踩的坑,这次轮到自己了。

解决

切回 M2,模板不动,一切正常:

VLM: minimax/MiniMax-M2 (litellm)
memories_extracted: 6 write + 1 edit
向量库: 3770 vectors
检索: 零报错,正常返回

中间也试过改模板——给每个 category 的 description 加上显式的 JSON 字段约束。效果立竿见影,M2.5 也从 0 变成了 7 条。但这不是正解——pip install --upgrade 会把模板覆盖回去,而且一旦 PR #1045(memory v2 重构)发版,提取器本身会更宽容,到时候模板补丁反而是多余的。

所以最终的方案就是:VLM 用 M2,其他不动。等官方更新。

几点收获

  1. viking_search 空结果,先看向量库是不是 0。 异步队列积压是正常现象,等几分钟就行。

  2. OpenViking 的 VLM 提取和检索是两条独立的管道。 提取依赖配置的 VLM 模型(ov.conf 里的 vlm 段),检索依赖 embedding 模型(embedding 段)。一个挂了不影响另一个。

  3. M2.7 的 API 有限流。 MiniMax 最新模型 M2.7 的并发限制比较紧,资源索引的语义摘要生成会打满配额,动不动 429。这也是最终用 M2 而不是 M2.7 的另一个原因——早期模型并发限制更宽松,实际效果反而更好。

  4. 不要迷信新模型的结构化输出能力。 benchmark 涨了不代表"听话"了。在这个场景里,最老的 M2 反而是最靠谱的。

  5. 别在模板上打补丁。 能换模型解决的问题,不要改配置。配置是临时的,升级就丢。

×