本机
在 Mac 客户端或网页里聊。日志里有 sessionId,正文回云端拉。
~/Library/Logs/WPS 灵犀/灵犀自带的记忆是为「把活干完」设计的:条目化、常驻、精简。但聊天里那些犹豫、偏好、当时的判断,也值得被留下。 这篇讲我怎么用 MCP + OpenViking 在本机搭第二套记忆,以及为了让手机遥控、跨端会话都能被记住,中间那层桥到底做了什么。
我用灵犀做很多事:改脚本、跑数据、写文档、查资料。它自带的记忆系统很好用——知道我的减脂方案是哪一版,知道某个仓库必须用完整路径推送,知道我讨厌被反复追问同一件事。但它记住的,几乎都是「以后做事要用到的结论」。
这套记忆的形态是条目:一条偏好、一条规则、一页项目状态。写入要显式发生——判断「这件事值得记」,然后落一条。读取是常驻的:核心记忆每轮全文注入,Wiki 目录每轮给索引,需要时再展开。
这种设计是对的。它让 Agent 在工具这条路上更稳、更省、更可控。系统不会因为昨天聊了三个小时的电影,今天写代码时上下文里就多出三千字影评。
但它也意味着另一件事:生活化的对话默认不会被留下。不是记不住,是不在它的职责里。你当时为什么犹豫、哪句话让你改了主意、那次失败之后你怎么想——这些不构成「操作指令」,也就没有入口。
所以我做的不是改造灵犀的记忆,而是在它旁边加了第二套:一套会话级、可检索、按需加载的记忆库。它不参与每一轮推理,只在需要的时候被叫出来。
先把边界说清楚:它们不是主备关系,也不是新旧替代。一套管「做事」,一套管「发生过什么」。混在一起,两边都会变差。
灵犀自带 · 核心记忆 + Wiki
OpenViking · 本地长期记忆库
我把这件事讲给朋友听,他问:那不就是「聊天记录」吗?不是。聊天记录是线性的、按时间排的、需要人自己翻。记忆库要的是另一件事:能在对的时刻,把对的那几条自动端上来。
这要求它在写入时就做结构化——把一次对话拆成事件、偏好、人物关系、经验教训;在读取时做语义匹配,而不是关键词搜索。这部分工作我交给 OpenViking,它本来就是干这个的。
最容易想到的做法,是把记忆摘要塞进系统提示词。简单,直接,但很快会撞墙:预算是固定的,而生活是无限的。
MCP 解决的不是「能不能读到」,而是「什么时候读、读多少」。它是工具调用协议:模型先看到一份能力清单,判断需要,才发起调用,服务端返回几条结果。没调用的时候,记忆库对上下文几乎零占用。
除了预算,还有三个实际好处:
代价也很实在:多一个服务要维护,多一条链路会坏。这篇后面的篇幅,基本都在讲怎么让它别坏。
要搬运,先找到货。这一步花的时间比写代码多得多——因为「对话在本地」这个直觉,在灵犀身上是错的。
2026 年 8 月,我在安卓开发版上导了一份日志。结论很干脆:对话 100% 在云端处理,客户端只负责渲染快照。传输走 WebSocket 为主、SSE 备用,断线后从云端拉历史。本地日志里只有 sessionId 和统计数字,没有正文。
但桌面端不是完全没痕迹。灵犀的聊天界面是一个 H5 页面,跑在 Electron 的独立分区里,它的 IndexedDB 缓存了完整事件流——包括正文。路径是:
~/Library/Application Support/WPS 灵犀/Partitions/lingxi.wps.cn/
IndexedDB/https_lingxi.wps.cn_0.indexeddb.leveldb
这个库不是普通 JSON。它是 LevelDB 的 WAL,值是 V8 的序列化格式。要还原,得先按 WAL 的 record 格式切分,再把 V8 的标签流解成对象。我写了两个小模块专门干这件事:wal.py 和 v8_codec.py。跑通那天确实很爽——但它只解决了历史。
因为后来主对话切到了云端 agent 通道,IndexedDB 里只剩旧通道的遗留。正文在云端,本地只剩线索。于是架构必须改:发现和获取分开——本地找 sessionId,云端拉正文。
拉取正文需要鉴权。灵犀把 wps_sid 存在 Cookies 的 SQLite 里,有效期很长。桥接器用只读 URI 打开它,读不到才退化成 0600 的临时副本,用完即删。这条凭据不落盘、不打印、不进 git,这是底线。
我给这层定的第一原则,是克制:它不做摘要、不做判断、不做「什么值得记」。它只保证一件事——云端发生的对话,一条不丢、一条不重地送到记忆库门口。
为什么不在这里做 LLM 蒸馏?三个原因。第一,蒸馏会引入第二次判断,错了无法回溯;第二,记忆库自己就有提取链路,重复做一遍是浪费;第三,一旦中间层开始「理解」内容,它就必须跟着模型和提示词一起演化,维护成本会失控。
所以它的定位是事件同步器:原始事件先落本地账本,再投递。记忆怎么长,是记忆库的事。
sha256(来源 | 会话 | run | run 内序号 | 角色 | 正文)。同一段内容无论拉多少次,只会入账一次。[REDACTED:规则],只留原文哈希用于对账。事后脱敏等于没脱敏——副本已经散出去了。桥接器现在每 60 秒跑一轮。看起来只是「拉一下、发一下」,但真正决定它稳不稳的,是几个不起眼的判断:什么时候提交、一次发多少、失败了算谁的。
最初不是这样的。第一版是「每见到 run.end 就提交」——听起来很合理,实际把上游打爆了:一天 86 次提交里 61 次被限流,一个早饭会话提交了 52 次、六千多条消息、烧掉七十多万 token,把同账号其他会话的提取一起挤死。
所以后来加了两层节流:时间上同会话 15 分钟冷却,体量上积压到 150 条强制提交。再往后发现 150 条长回复照样能把提取的输出预算撞穿,又补了 6 万字的字符闸门。
做到这一步,本机的对话能进来了。但真实使用里,会话会从三个地方冒出来,它们的物理位置完全不同。这一节是整套东西里最容易被忽略、也最容易「以为记住了其实没记」的部分。
在 Mac 客户端或网页里聊。日志里有 sessionId,正文回云端拉。
~/Library/Logs/WPS 灵犀/手机当遥控器,任务实际在 Mac 上执行。会话是在本机形成的,日志照样有。
remoteBridge · app.*.log在另一台电脑上创建、只在那台机器聊过。本机日志永远不会有它。
GET /session_list
灵犀的手机遥控不是把对话搬到手机执行,而是手机发指令、桌面端跑任务,中间走一条 remoteBridge 中继。所以只要任务是在这台 Mac 上完成的,sessionId 一定会落在这台 Mac 的 app 日志里,和本地发起没有区别。
这一点很关键:不需要为手机单独做一套采集,也不需要手机端开放什么接口。发现器扫的是本机日志,天然覆盖。我在日志里能看到大量 remoteBridge 记录,会话注册、拉取、落库的路径与本地会话完全一致。
反过来,在另一台电脑上创建、只在那台机器聊过的会话,本机日志里一个字都没有。默认发现逻辑扫不到,也不会报错——它只是安静地不存在。
解决办法是不猜,直接问云端要清单。账号下的全部任务可以通过 session_list 拿到,关键是 session_type 要传对:
# 只读:拿账号下全部桌面端任务
GET https://lingxi.wps.cn/api/public/v1/session_list
?limit=100
&exclude_agent=true
&session_types=cowork_client_project
Cookie: wps_sid=<从本机 Cookies 只读>
拿到 id 之后不需要重新实现一套拉取——把 id 注册进 Registry,标上来源 cloud-session-list,下一轮它就会被当成普通会话,走同一条云端取数、脱敏、入账、投递的路径。实测补五个历史会话,拿到三千多条事件,其中两百多条正文入库。
session_type 写错(比如只写 cowork)会返回 0 条,看起来像「没有数据」;二是日志里那份会话列表响应会被截断到一千多字符,靠日志永远补不全清单,必须直接调接口。
技术上可以每天把账号下所有任务都拉一遍,但我没有这么做。一是历史会话量很大,全量重扫会把云端和提取预算一起打满;二是大部分旧会话的边际信息量很低。现在的做法是:默认只跟本机活跃会话,跨端按需补拉,补哪些由人决定。
这也是整套系统的性格:自动负责「不漏」,人工负责「取舍」。
下面的数字来自这台机器今天的账本,不是估算。我一直觉得,讲架构如果不给运行数据,读者没法判断它到底有没有真的跑起来。
有个数字对比很有意思:账本里四十多万条事件,最后只有六万五千条被判定为「可同步正文」。剩下的三十五万条是结构事件、流式分片、推理中间态、工具调用记录——它们在同步前就被过滤掉了。
这不是丢数据,是刻意的:如果把这些全部写进记忆库,token 会被中间产物淹没,提取质量反而下降。第一版就吃过这个亏,一次事故烧掉近千万 token。
| 口径 | 数量 | 说明 |
|---|---|---|
| 累计入账事件 | 418,473 | 所有从云端拉回、经脱敏后落账的事件 |
| 已过滤 | 352,846 | 结构事件与中间产物,不同步到记忆库 |
| 已核验正文 | 65,627 | 真正写入记忆库的消息 |
| — 助手最终回复 | 52,353 | 只取最终呈现文本,不含推理过程 |
| — 用户输入 | 10,357 | 提问、指令、口述内容 |
| — 结构事件 | 2,917 | run 边界等,用于对账锚点 |
记忆库那边的分布也符合预期:事件最多,因为每天都有新的事;实体次之,人物、工具、项目会不断被提到;偏好和轨迹增长最慢,但价值密度最高——它们是「关于你的稳定结论」,不是流水账。
| 记忆分类 | 文件数 | 典型内容 |
|---|---|---|
| events | 746 | 某天发生了什么、做了什么决定 |
| entities | 134 | 人物、项目、工具、服务 |
| preferences | 80 | 明确表达过的偏好与纠正 |
| trajectories | 56 | 一件事从头到尾怎么推进的 |
| experiences | 32 | 踩过的坑与正确做法 |
| cases | 27 | 可复用的完整案例 |
| patterns | 9 | 反复出现的行为规律 |
| projects / relationship | 2 | 长期项目状态、关系约定 |
这条链路上最危险的故障,不是报错,是「看起来一切正常」。下面九条,全都曾经在没有任何告警的情况下持续运行过一段时间。
现象:进程在跑、心跳正常、没有 error,但新会话一个都没进来。
根因:客户端升级后日志格式变了,旧正则匹配不上。而看门狗只看「进程活着 + 心跳新鲜 + 没有报错」,这三个条件全绿——它根本不知道「发现数应该增长」。
修法:①正则改成宽匹配,同时认三种历史形态;②加一条金丝雀:用独立正则干扫日志,和已注册列表比对,有差距就告警;③看门狗加第四个条件,连续两轮发现缺口未消才报。
教训:监控要盯「产出」,不能只盯「存活」。
现象:回补一个大 session 时一直失败,日志只说 HTTP 400。
根因:接口单批最多 100 条,101 条直接拒绝,而且不会部分写入。
修法:写入层自己按 100 条分批,每批独立「写入 → 校验 → 提交」。失败只影响那一批。
教训:凡是「批量」接口,先去实测边界,别假设它和单条一样宽容。
现象:记忆时有时无,提取任务大面积失败,别的会话也跟着饿死。
根因:每见到 run 结束就提交,而提交会触发一次模型提取,非常贵。一个早饭会话提交 52 次、六千多条消息、烧掉七十多万 token,把共享配额吃干。
修法:加提交冷却:同会话最短 15 分钟;积压到 150 条强制提交;静默会话到期补一次归档。
教训:「写消息」和「触发提取」要分开算账,前者廉价,后者昂贵。
现象:提交返回成功,但归档里多了一个失败标记,一条记忆都没产生。
根因:单次提交的消息太多,模型把输出预算耗在推理上,正式结果一个字没产出。服务端判定「截断」,整批放弃,且没有重试端点。
修法:①单轮处理量加双闸门(150 条 / 6 万字);②失败归档自动巡检:扫描失败标记、跨归档去重、切块、用独立会话重放、成功才落「已恢复」。
教训:任何「一次处理一大批」的设计,都要有单批体量上限和失败兜底。
现象:同一个会话一天内重传了八百多条历史消息,连发九个归档。
根因:指纹里含「全局第几条」。云端对超长会话有滑动窗口,新增消息会裁掉最老的几条,所有序号整体前移,历史消息的指纹全变了,被当成新事件。
修法:序号改成「run 内相对序号」,头部裁剪不影响它。旧指纹做迁移判重,命中后原地升级。
教训:幂等键不能依赖会随外部状态漂移的坐标。
现象:桥接器真的进入故障退避了,看门狗一次都没报。
根因:健康状态文件把错误写在 stats.error,看门狗读的是顶层 error,条件恒假。
修法:两种形状都识别,并用真实的写盘函数做回归测试,锁住三种状态。
教训:告警器自己也需要测试,否则它只是「看起来很安心」。
现象:演练一切正常,正式跑却少了一大段。
根因:试运行用的假写入层返回成功,编排层照样把事件标成「已核验」。等真跑时,这些事件被当成已完成跳过。
修法:试运行改成显式参数,走真实入库(指纹本来就幂等),但绝不标记完成状态。
教训:「dry-run」必须是编排层的语义,不能只是替换一个假客户端。
现象:一重启就要跑三到五分钟,心跳短暂陈旧,看门狗差点误报。
根因:「上次对账日期」存在进程内存里,重启归零,立刻触发一次全量对账并重置上百个会话的游标。
修法:把日期写进账本的 meta 表——状态跟它描述的数据放在一起,备份和回滚不会错位。
教训:凡是「每天一次」的开关,都要问一句:进程重启后它还准吗。
现象:修复在生产上生效了,但仓库里没有记录。一次 git checkout 就能让修复凭空消失。
根因:定时任务的启动参数直接指向工作区脚本,工作区即生产。
修法:立规矩:部署目录的任何改动必须当场提交,不留未提交状态过夜;自检 git status。
教训:「本地开发」和「生产运行」共用一个目录时,版本管理不是好习惯,是安全措施。
OpenViking 是第三方项目,灵犀本身闭源,这两层都不在开源范围内。计划放出去的是我自己写的那一层:桥接器、账本、核账、巡检、部署脚本。许可证倾向 MIT 或 Apache-2.0,仓库只包含这一层,不夹带任何个人数据。
抽象工作不多,但要一次做干净:绝对路径换成配置、身份信息换成占位符、灵犀专属的常量收进 adapter。核心层不认识灵犀,它只认识「事件源」和「记忆库」两个协议。这是当初分层留下的红利,也是它能被搬到别的 Agent 上的原因。
memory-bridge/
├── adapters/
│ └── lingxi/ # 只此一处认识灵犀:日志、接口、字段映射
├── core/
│ ├── events.py # 规范事件 + 指纹
│ ├── filter.py # 脱敏规则
│ ├── ledger.py # SQLite 账本
│ └── pipeline.py # 编排与闸门
├── sinks/
│ └── openviking/ # 分批、归属校验、提交冷却
├── ops/
│ ├── reconcile.py # 三方核账
│ ├── recovery.py # 失败归档巡检
│ └── watchdog.py # 四条件 + 告警适配
├── deploy/ # launchd / systemd 模板
├── tests/ # 含崩溃重放与金丝雀回归
└── docs/
└── incidents/ # 事故复盘,比 README 更有价值
发布前会过一遍这张清单:
.gitignore 挡住账本、日志、归档、备份、.env。docs/。这类基础设施的价值不在代码本身,在于有人愿意照着搭一遍,再把它改得更好。所以文档会比代码写得更细,尤其是那九个坑。
如果你也想给自己的 Agent 搭一套,顺序大概是这样的。不要一上来就写同步器——先把「记忆库能不能被按需调用」跑通,那是整件事的地基。
本地起 OpenViking,签发一个用户级 Key,用 MCP 接进客户端。手动写一条记忆、检索一次、确认归属正确。这一步不通,后面全是白搭。
建会话 → 批量写消息 → 提交 → 轮询提取结果。用 curl 就行。记住两个边界:单批 100 条,提交会触发一次昂贵的提取。
先看本地有没有明文(数据库、缓存、日志),再看云端有没有可用接口。两者都没有,这事做不成——不要假设「应该能抓到」。
先不加投递,只把 sessionId 扫出来、注册进表、打日志。跑一天,看数量对不对。发现错了,后面全错。
事件先落 SQLite,指纹去重,状态机 pending → verified。然后才是写入层。这一步做完,就可以强杀进程测试重放了。
核账、失败巡检、看门狗、告警。顺序不能反:没有产出指标,监控只会告诉你「进程还活着」,而那往往是最没用的信息。
这套桥现在只服务一个 Agent。但它要解决的问题,很快会变成多个 Agent 的共同问题。
我平时不止用一个 Agent:有的负责写作,有的跑代码,有的查资料、管日程。它们各有各的入口、工具链和上下文窗口,互相之间并不知道对方做过什么。
如果每个 Agent 各记各的,一个人会被记成几份:同一个偏好出现三个版本,同一个项目的结论互相打架,昨天在 A 那里定下的事,今天在 B 那里完全不成立。记忆越勤奋,分裂得越快。
所以下一步不是再写一座桥,而是把记忆做成底座——我们把它叫 Agent Home。一套记忆,多个住户,按归属和权限访问。
OpenViking 的结构刚好能承载这件事:账户、用户、peer 之间的隔离,session 的归属校验,memories 的分类目录。桥这一层要做的,是把「哪个 Agent、哪个会话、写进哪一层」标清楚。
桥是第一个接入者,也是验证场。它已经证明三件事:会话能可靠落库、提取能产出可用记忆、失败能自己收尾。这三件事是 Agent Home 的地基,剩下的部分还在做。
这一节只讲概念,不展开路线图和排期。等底座真的能跑起来,再单独写一篇。
做完这套东西,我对「Agent 该记住什么」的看法反而更保守了:不是记得越多越好,而是该简的地方极简,该全的地方别删。
工具记忆应该继续精简。它每轮都进上下文,多一条就多一分干扰,所以必须有人为的判断:什么值得留下。这部分不该自动化,也不该膨胀。
生活记忆相反。它的价值恰恰在细节里——那句没说完的话、当时改主意的理由、一次失败之后的情绪。把它压缩成「用户偏好 X」当然更省,但也就没什么可召回的了。
两套记忆并行,各司其职:一套让 Agent 更像一个靠谱的同事,一套让它记得你是个具体的人。
这套中间层还在长。失败巡检是最近才补上的,跨端补拉也只做了按需的一小部分。但骨架已经稳了:三个入口、一条流水线、两层记忆。
如果把它开源,我希望被拿走的是那几条规则——先落账再投递、脱敏前置、只同步正文、监控盯产出。代码可以重写,这些是踩出来的。
关于数据:文中所有运行数字来自本文写作当天这台机器的账本与记忆库统计,仅代表个人使用规模,不是性能基准。
关于边界:OpenViking 是第三方本地记忆服务;本文开源讨论仅覆盖我自建的桥接层,不包含其代码与数据。
关于隐私:文中不含任何真实会话内容、凭据、内网地址或账号标识;示例路径与接口均为公开形态。