给 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。
更关键的是治理层:
- 字段级 MergeOp:5 种合并策略(patch/replace/sum/immutable/link),同一条记忆的新旧信息能按字段智能合并,不是粗暴覆盖
- hotness 衰减:每条记忆有 hotness_score,公式是 sigmoid(freq) × exp(-decay×age),7 天半衰期。长期不引用的记忆自然沉底
- 冲突检测:有
contradictslink_type,检测到矛盾的记忆会标记,按"keep latest if conflicting"处理 - 三阶段检索:意图分析 → 目录递归 → Rerank,不是简单的语义 top-K
最有说服力的是同一组数据的结果对比:
| 系统 | 21 个会话提取量 | 信噪比 |
|---|---|---|
| Hindsight | 2800+ 条碎片 | 低(60% 过程日志) |
| OpenViking | 149 条结构化记忆 | 高(结构化筛选) |
2800 条碎片 vs 149 条结构化记忆,数量差了一个量级,但后者覆盖的信息量更大——因为重复和噪音被治理掉了。
决定迁移到 OpenViking。然后踩坑就开始了。
NAS Docker 部署
我的 OpenViking 部署在绿联 NAS 的 Docker 上。整体流程:拉镜像 → docker-compose → ov.conf 配置 → 启动验证。看起来简单,但每一步都有坑。
拉镜像:Docker daemon HTTP 代理
第一个坑就是拉镜像。我的 NAS 之前 DNS 被封过(v2rayA 残留规则,另一个故事),docker pull 拉不到任何外部 registry 的镜像。
一开始我走的是笨办法:在 Mac 上 docker pull → docker 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 在美国,连不到内网。两台机器的接入方式:
- Mac:直连局域网
openviking.nas:1933(hosts 里配 192.168.1.2) - VPS:通过 SSH 反向隧道,在 VPS 上映射成
localhost:1933,hosts 里配127.0.0.1 openviking.nas
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 里的历史对话)还得导入进来,不然记忆系统是空的。这是最费心思的一步。
数据源选择:从哪导?
有两个选择:
- 从 Hindsight 导出:Hindsight 已经从对话里提取过 fact 了,直接导 fact 过去。但前面说了,那些 fact 60% 是噪音碎片,OpenViking 再提取一遍还是碎片。
- 从 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 衰减 + 三阶段检索,本质上是一套治理框架——让有用的信息浮上来,没用的沉下去。
踩坑方面,最值得记住的三条:
- 内网部署用 api_key 不用 dev,dev 模式拒绝 0.0.0.0 绑定
- VLM 别和主模型共享供应商 quota,记忆提取用 DeepSeek,主对话用智谱
- 数据操作用 User Key 不用 Root Key,Root Key 调数据 API 直接报错
如果你也在给 AI Agent 找记忆方案,OpenViking 值得一试。自托管不复杂,坑虽然多但都是可解的。这篇博客本身就是用 Hermes 写的——它能记住我的博客发布流程、CSS 样式偏好、部署步骤,不用我每次从头说一遍,这就是记忆系统的价值。
← 返回文章列表