给 AI Agent 装上长期记忆:OpenViking 自托管实战

用 Hermes Agent 大半年,最大的痛点不是它不会做,而是它每次对话都失忆。我上周告诉它"我的 NAS 是绿联 DH4300 PLUS,SSH 地址是 home@192.168.1.2",这周它又得问一遍。从 mem0 到 Hindsight 再到 OpenViking,我换过三代记忆系统,踩了一路坑。这篇是踩坑笔记,不是教程。

为什么需要记忆系统

AI Agent 的工作模式是:启动 → 读完对话历史 → 生成回复 → 结束。每次对话都是独立的,前一次说了什么,它一无所知。Hermes 的 state.db 里存了完整的对话记录,但那是日志,不是记忆——你不能每次启动都把几百个会话全塞进上下文窗口。

所谓"记忆系统",本质是一个独立的存储层,把对话中值得记住的信息提取出来,结构化存储,然后在新的对话开始时把相关的记忆注入上下文。听起来简单,做起来全是问题:什么该记?什么不该记?重复了怎么合并?矛盾了以谁为准?旧的怎么遗忘?

这三个问题,恰好对应了三代记忆系统的演进。

选型:三代记忆系统对比

第一代:mem0 — 扁平 KV 存储

mem0 是最早用的,思路最简单:对话结束,调一次 LLM 提取"事实",存成 key-value。比如"用户喜欢用 vim"、"用户 NAS 是绿联 DH4300 PLUS"。检索时做语义搜索,把 top-K 结果塞进上下文。

问题出在信噪比。LLM 提取"事实"的时候没有结构约束,什么鸡毛蒜皮都提取——"助手读了一个文件"、"助手执行了一条命令"、"用户说了谢谢"。粗略统计,mem0 存的东西里 60% 是过程日志噪音,真正有价值的信息被淹没了。检索的时候,top-10 结果里可能只有 2 条有用。

第二代:Hindsight — 知识图谱式

第二换成了 Hindsight,思路更进一步:把提取的 fact 组织成知识图谱,节点之间有关系。听起来高级,但实际用下来——

我导入 21 个会话,Hindsight 提取了 2800+ 条碎片。2800 条,你知道这意味着什么吗?每次对话开始,Hindsight 往上下文里塞 5-10 条"相关记忆",大部分是"助手调用了 terminal 工具"、"助手读了文件 /tmp/xxx"这种完全没用的东西。它有去重逻辑,但效果很差——同一个信息以十种不同措辞存了十遍。

根本原因是缺乏治理。Hindsight 没有结构约束,没有字段级合并,没有衰减机制,没有冲突检测。信息只进不出,越积越脏。

第三代:OpenViking — 结构化记忆数据库

OpenViking 是火山引擎出品的,论文发表在 VLDB 2026。核心区别是:它不是"存事实碎片",而是存结构化记忆。有 12 种预定义的 YAML schema(profile、preferences、entities、events、cases、skills 等),每条记忆必须符合某种 schema。

更关键的是治理层:

最有说服力的是同一组数据的结果对比:

系统21 个会话提取量信噪比
Hindsight2800+ 条碎片低(60% 过程日志)
OpenViking149 条结构化记忆高(结构化筛选)

2800 条碎片 vs 149 条结构化记忆,数量差了一个量级,但后者覆盖的信息量更大——因为重复和噪音被治理掉了。

决定迁移到 OpenViking。然后踩坑就开始了。

NAS Docker 部署

我的 OpenViking 部署在绿联 NAS 的 Docker 上。整体流程:拉镜像 → docker-compose → ov.conf 配置 → 启动验证。看起来简单,但每一步都有坑。

拉镜像:Docker daemon HTTP 代理

第一个坑就是拉镜像。我的 NAS 之前 DNS 被封过(v2rayA 残留规则,另一个故事),docker pull 拉不到任何外部 registry 的镜像。

一开始我走的是笨办法:在 Mac 上 docker pulldocker save 导出 tar → SFTP 传到 NAS → docker load。每部署一个新服务就传一遍,又慢又烦。后来配置了 Docker daemon HTTP 代理,一步到位。

原理很简单:在 VPS 上跑一个 HTTP CONNECT 代理(Python 单文件,90 行代码),然后让 NAS 的 Docker daemon 通过这个代理拉镜像。配一次,以后所有 registry 都能直接 docker pull

(下面的 <VPS_IP> 是你自己的 VPS 公网 IP,这里用占位符代替,你懂的。)

NAS 侧的配置是 systemd drop-in:

# /etc/systemd/system/docker.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://<VPS_IP>:18080"
Environment="HTTPS_PROXY=http://<VPS_IP>:18080"
Environment="NO_PROXY=localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"

重载并重启 Docker:

sudo systemctl daemon-reload
sudo systemctl restart docker

# 验证
systemctl show docker --property=Environment
# 应看到 HTTP_PROXY=...

# 然后直接拉
docker pull ghcr.io/volcengine/openviking:latest

注意 NO_PROXY 要包含局域网和 Docker bridge 网段,不然 NAS 内部服务通信也绕 VPS 一圈,慢得要死。绿联 NAS 上 systemctl restart docker 特别慢,可能要 30-60 秒,脚本里 timeout 别设太短。

docker-compose.yml

镜像拉好后,docker-compose 很直接:

services:
  openviking:
    image: ghcr.io/volcengine/openviking:latest
    container_name: openviking
    ports:
      - "1933:1933"
    volumes:
      - ./data:/app/.openviking
    environment:
      OPENVIKING_SERVER_PORT: "1933"
    healthcheck:
      test: ["CMD", "openviking-entrypoint", "----healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s
    restart: unless-stopped

1933 是 OpenViking 的默认端口,挂载 ./data 做持久化。没什么花活。

ov.conf:两个关键配置

ov.conf 是 OpenViking 的主配置文件,有两个地方踩了坑。

第一个坑:auth_mode 不能用 dev。 默认配置是 auth_mode: dev,本地开发用的。我一开始没改,结果容器死活起不来,报错:

SECURITY: server.auth_mode='dev' requires server.host to be localhost

dev 模式强制 host 必须是 localhost,拒绝绑定 0.0.0.0。但我是内网部署,Mac 和 VPS 都要连这个 NAS 上的 OpenViking,必须绑 0.0.0.0。解决方案:改成 auth_mode: api_key,配一个 root_api_key

第二个坑:VLM 选型。 这个单独说。

修正后的 ov.conf 关键部分:

{
  "server": {
    "host": "0.0.0.0",
    "port": 1933,
    "auth_mode": "api_key",
    "root_api_key": "<ROOT_KEY>"
  },
  "storage": {
    "workspace": "./data",
    "vectordb": {"name": "context", "backend": "local"},
    "agfs": {"backend": "local"}
  },
  "embedding": {
    "dense": {
      "api_base": "https://ark.cn-beijing.volces.com/api/v3",
      "api_key": "<ARK_KEY>",
      "provider": "volcengine",
      "dimension": 1024,
      "model": "doubao-embedding-vision-251215",
      "input": "multimodal"
    }
  },
  "vlm": {
    "provider": "openai",
    "model": "deepseek-v4-flash",
    "api_key": "<DEEPSEEK_KEY>",
    "api_base": "https://api.deepseek.com/v1"
  }
}

启动验证:

docker compose up -d
sleep 15
curl -s -H "Authorization: Bearer <ROOT_KEY>" http://192.168.1.2:1933/api/v1/system/status
# {"status":"ok","result":{"initialized":true,"user":"default"}}

VLM 选型踩坑:智谱 vs DeepSeek

这个坑卡了我大半天,值得单独说。

OpenViking 的记忆提取依赖一个 VLM(Visual Language Model)。每次会话 commit(提交归档)时,它调 VLM 分析对话内容,提取结构化记忆。这个调用频率不低——每个会话结束都要调。

一开始我配的是智谱 GLM 做 VLM。因为我的 Hermes 主模型也是智谱的,心想统一算了。结果上线上马上出问题:Hermes 每次对话都调智谱 API,OpenViking 每次会话 commit 调智谱 API,两者共享同一个 quota。高峰期直接触发限速,Hermes 主请求被 429 打断。

排查了一阵才意识到:记忆提取和主对话必须用不同供应商的 API,避免 quota 竞争。最后改成 DeepSeek flash(deepseek-v4-flash),独立 quota,而且 flash 模型便宜、速度快,做记忆提取绰绰有余。

改完之后限速问题再没出现过。教训:附属服务的 API 调用不要和主链路共享供应商 quota

Hermes 接入:双节点共享记忆

我有两台 Hermes:一台在 Mac 上做日常交互,一台在美国 VPS 上跑定时任务和飞书机器人。两台机器要共享同一份记忆——我在 Mac 上告诉 Hermes "我换工作了,新公司用飞书",VPS 上的 Hermes 也应该知道。

网络拓扑

OpenViking 跑在 NAS 上(192.168.1.2:1933),Mac 在局域网内可以直连,VPS 在美国,连不到内网。两台机器的接入方式:

VPS 侧的 SSH 隧道是在 NAS 上配的 systemd 服务(nas-ssh-tunnel.service),反向把 NAS 的 1933 端口映射到 VPS 的 localhost:

# nas-ssh-tunnel.service 的 ExecStart 加两行
-R 127.0.0.1:1933:localhost:1933 \
-R 172.17.0.1:1933:localhost:1933 \

这样 VPS 上 curl http://openviking.nas:1933 其实走的是 SSH 隧道回 NAS,Mac 上同样的命令走的是局域网直连。两端的配置文件看起来一模一样,只是底层网络路径不同。

共享 vs 隔离

OpenViking 按 user_id 隔离记忆,不是按 agent。关键配置:

# 两台 Hermes 都配这个(.env)
OPENVIKING_ENDPOINT=http://openviking.nas:1933
OPENVIKING_API_KEY=<ROOT_KEY>
OPENVIKING_ACCOUNT=default
OPENVIKING_USER=hermes          # 两台都用同一个 user → 共享记忆
OPENVIKING_AGENT=hermes         # VPS 上改成 hermes-vps → 隔离操作轨迹

# 切换 provider
hermes config set memory.provider openviking

OPENVIKING_USER=hermes 的两台 Hermes 共享一份记忆——Mac 上写入的偏好,VPS 上检索得到。不同的 OPENVIKING_AGENT(peer_id)只隔离操作轨迹(trajectories),不隔离用户偏好、实体、事件这些核心记忆。

这个设计很合理:偏好是人的属性(跟设备无关),轨迹是设备的属性(各干各的)。

配完必须重启 Hermes 才生效,memory provider 切换不会热加载。

hermes config set memory.provider openviking
hermes gateway restart   # 或者直接重启进程

# 验证
hermes memory status
# 应显示 openviking active

历史会话批量导入

切换到 OpenViking 后,之前的 21 个会话(Hermes state.db 里的历史对话)还得导入进来,不然记忆系统是空的。这是最费心思的一步。

数据源选择:从哪导?

有两个选择:

  1. 从 Hindsight 导出:Hindsight 已经从对话里提取过 fact 了,直接导 fact 过去。但前面说了,那些 fact 60% 是噪音碎片,OpenViking 再提取一遍还是碎片。
  2. 从 Hermes state.db 导:读原始 user/assistant 对话,让 OpenViking 用结构化 schema 从头提取。

果断选第二个。原始对话质量远高于已提取的碎片。

只导有价值会话

Hermes state.db 里有四种 source:

source导入?原因
feishu用户主对话,价值最高
cli命令行交互,有价值
cron定时任务输出,纯噪音
subagent子 agent 内部对话,噪音

过滤 SQL:WHERE message_count > 5 AND source IN ('cli','feishu')。最终筛选出 Mac 21 个会话 + VPS 5 个会话 = 26 个会话

三步导入流程

核心是三个 API 调用:create_session → batch_add_messages → commit。

import sqlite3, requests

db = sqlite3.connect("~/.hermes/state.db")
db.row_factory = sqlite3.Row

# 只导有价值会话
sessions = db.execute("""
    SELECT id, source, message_count, started_at FROM sessions 
    WHERE message_count > 5 AND source IN ('cli','feishu')
    ORDER BY started_at
""").fetchall()

OV_URL = "http://openviking.nas:1933"
USER_KEY = "<user_key>"  # 注意:不是 root key!
HEADERS = {"Authorization": f"Bearer {USER_KEY}", "Content-Type": "application/json"}

for sess in sessions:
    sid = sess["id"]
    
    # 读纯文本消息(排除工具调用)
    msgs = db.execute("""
        SELECT role, content FROM messages 
        WHERE session_id = ? AND role IN ('user','assistant') 
        AND content IS NOT NULL AND content != ''
        AND tool_calls IS NULL AND tool_call_id IS NULL
        ORDER BY timestamp
    """, (sid,)).fetchall()
    
    if len(msgs) < 3:
        continue
    
    # 1. 创建 session
    requests.post(f"{OV_URL}/api/v1/sessions", 
                  json={"session_id": f"hermes_{sid[-6:]}"}, headers=HEADERS)
    
    # 2. 批量加消息(每批 max 100,超长截断)
    batch = [{"role": m["role"], "content": m["content"][:10000]} for m in msgs]
    for i in range(0, len(batch), 100):
        requests.post(f"{OV_URL}/api/v1/sessions/hermes_{sid[-6:]}/messages/batch",
                      json={"messages": batch[i:i+100]}, headers=HEADERS, timeout=30)
    
    # 3. commit → 触发异步记忆提取(DeepSeek flash 后台跑)
    requests.post(f"{OV_URL}/api/v1/sessions/hermes_{sid[-6:]}/commit", headers=HEADERS)

踩坑:Root Key vs User Key

导入脚本里有个隐蔽的坑:必须用 User Key,不能用 Root Key

Root Key 是 ov.conf 里配的,能登录 Web Studio 和调 admin API。但它不能操作数据 API——用 Root Key 创建的会话,之后用 User Key 操作会报 UNAUTHENTICATED。报错信息是:

ROOT API keys cannot access tenant-scoped data APIs

正确流程是先用 Root Key 通过 admin API 创建一个用户账户,拿到 User Key,再用 User Key 做数据操作:

curl -X POST http://192.168.1.2:1933/api/v1/admin/accounts \
  -H "Authorization: Bearer <ROOT_KEY>" \
  -d '{"account_id":"hermes-import","admin_user_id":"hermes","seed":"<seed>"}'
# 返回 user_key(一长串 base64)

踩坑:commit 是异步的

commit 返回的是 task_id,记忆提取在后台异步跑(DeepSeek flash)。26 个会话 commit 后不会立刻有记忆,要等后台逐个处理。如果这时候容器重启(比如改配置),正在跑的提取会中断

判断哪些被中断:

r = requests.get(f"{OV_URL}/api/v1/sessions", headers=HEADERS)
for s in r.json()["result"]:
    info = requests.get(f"{OV_URL}/api/v1/sessions/{s['session_id']}", 
                        headers=HEADERS).json()["result"]
    me = info.get("memories_extracted", {})
    if me.get("total", 0) == 0 and info.get("total_message_count", 0) > 0:
        print(f"未提取: {s['session_id']} ({info['total_message_count']} msgs)")

中断的会话需要从 archive 读出消息重新 add + commit,比较麻烦。所以建议:导入期间别动容器配置。

最终结果

26 个会话全部 commit 后,等后台 DeepSeek flash 跑完,最终提取出 149 条结构化记忆。对比 Hindsight 从同样 21 个会话提取的 2800+ 条碎片,数量差了一个量级,但结构化记忆的信息密度高得多——每一条都是完整的、有 schema 的、有 hotness 的,不是无结构的文本噪音。

使用效果

上线跑了一段时间,实际体验:

1. 跨设备记忆共享真的实现了。 我在 Mac 上和 Hermes 聊"我最近在学中医",过了两天在飞书(VPS Hermes 回复的)里提到相关话题,它直接接上了,不用我重复背景。这在之前是不可能的——两台 Hermes 各自为政,互相不知道对方聊过什么。

2. 结构化记忆质量远高于碎片。 OpenViking 存的不是"用户说了 X"这种平面事实,而是结构化的 profile/preferences/entities。比如它知道我的工作流偏好、技术栈、常用的服务地址,这些信息被组织成有 schema 的记录,检索的时候能精确命中。

3. 搜索精度明显提升。 三阶段检索(意图分析 → 目录递归 → Rerank)比纯语义 top-K 强很多。同样的问题,Hindsight 返回的 5 条记忆可能只有 1 条相关,OpenViking 返回的 3 条基本都相关。而且 hotness 衰减机制让那些过时的记忆自然沉底,不会一直占着检索位。

总结

三代记忆系统走下来,最大的体会是:记忆系统的核心不是"记住",而是"治理"。mem0 和 Hindsight 都能记住东西,但记了一堆垃圾,检索出来反而干扰。OpenViking 的结构化 schema + MergeOp + hotness 衰减 + 三阶段检索,本质上是一套治理框架——让有用的信息浮上来,没用的沉下去。

踩坑方面,最值得记住的三条:

  1. 内网部署用 api_key 不用 dev,dev 模式拒绝 0.0.0.0 绑定
  2. VLM 别和主模型共享供应商 quota,记忆提取用 DeepSeek,主对话用智谱
  3. 数据操作用 User Key 不用 Root Key,Root Key 调数据 API 直接报错

如果你也在给 AI Agent 找记忆方案,OpenViking 值得一试。自托管不复杂,坑虽然多但都是可解的。这篇博客本身就是用 Hermes 写的——它能记住我的博客发布流程、CSS 样式偏好、部署步骤,不用我每次从头说一遍,这就是记忆系统的价值。

← 返回文章列表