【免费下载链接】deja-vu
Memory for Claude Code, Codex, Cursor and 38 more coding agents, built from the session history already on your disk. Local search, MCP and hooks, no LLM, one Go binary.
deja-vu 是一个零第三方依赖的 Go 单二进制,它把 Claude Code、Codex、Cursor 等 41 种编码 Agent 已经落在磁盘上的会话历史,自动解析并构建成本地倒排索引——不需要 LLM、不需要 embedding 服务、不上传任何数据,任何一个 Agent 下次启动时就能"想起"几个月前做过的决定、踩过的坑和验证过的修复。这篇文章从源码视角,拆解它从解析、索引到检索的完整链路。
对比演示:没有记忆时 Agent 对八个月前的问题毫无记录;接入 deja-vu 后,它直接引用了当时的决策。
一、为什么是"零依赖"的单二进制
先看最硬核的证据——整个项目的 go.mod 只有两行:
module github.com/vshulcz/deja-vu go 1.25没有一行require。这意味着 deja-vu 的全部能力——41 种 Agent 的解析器、倒排索引引擎、BM25 检索、终端 UI、MCP 协议——都只用 Go 标准库实现。这个约束带来三个直接后果:
| 常见难点 | deja-vu 的解法 | 收益 |
|---|---|---|
| SQLite 读取需要 CGO | 调用系统自带的sqlite3命令行进程 | 跨平台交叉编译零障碍 |
| TUI 框架依赖(bubbletea 等) | 自研 internal/tui/ 包,termios/console mode 直接操作终端 | 无 GUI 依赖,TERM=dumb自动降级为纯文本 |
| 索引写一半崩溃 | 全程"写临时文件 → fsync → rename"原子交换 | 读者永远只看到完整的新文件或旧文件 |
对普通用户来说,"单二进制"意味着安装只需一条命令(Homebrew、Scoop、winget、npm 均可),运行时无任何运行时、无服务进程、无云端。
二、一套注册表,统一41种Agent的历史文件
deja-vu 支持 41 种 Agent,全部清单记录在 docs/registry/registry.json。每种 Agent 的历史文件格式五花八门:有的是 JSONL 逐行追加(Claude Code 的~/.claude/projects)、有的是 SQLite 数据库(Cursor、Zed、Devin)、有的甚至是 zstd 压缩流(DeepSeek Harness)。
架构上的解法是一个注册表:internal/sources/registry.go 中的allHarnesses()为每种 Agent 登记三样东西:
Load:全量加载该 Agent 的所有会话;Files:它把历史写在哪里(文件发现);FileKind:每种文件格式的Match(路径匹配)、Parse(全量解析)、ParseFrom(从偏移量增量解析)。
所有解析器统一返回[]model.Session——无论源是 JSONL 还是 SQLite,进入索引之前都被归一化成同一种会话模型。新增一种 Agent 只需实现这套固定形状,发现、路径匹配和增量解析全部复用注册表逻辑,docs/ARCHITECTURE.md 给出了完整的六步接入指南。
文件类源用 CPU 核数大小的 worker 池并行解析,但结果按文件路径排序后再追加写入——解析可以并行,索引写入保持确定性。
三、倒排索引本体:records.bin 与 token 桶
索引落在~/.cache/deja/index.db,核心由两类文件构成:
1.records.bin—— 记录日志
长度前缀的连续记录,每条存会话键、源路径、角色(role)、文本和时间戳。两个值得注意的压缩技巧:
- 会话键、路径、角色都在记录头部做intern(去重引用),扫描某类记录时读前缀即可跳过正文;
- 8 KiB 以上的正文做 deflate 压缩,更小的不压——因为小正文解压开销大于压缩省下的空间。
2.buckets/*.bin—— 真正的倒排索引
每个桶文件是一组 token 到postings(倒排列表)的映射:token → 一组(记录偏移, 会话序号, 1 bit 标记)。写入实现在 internal/index/store_io.go 的writeBucket:token 排序后,偏移量不存储而是累加推导,省掉每 token 8 字节;更妙的是会话 ID 采用delta 编码(相对上一条 postings 的增量)——因为 records.bin 本就按会话顺序写,实测 99.66% 的 postings 会话号不减,桶文件因此缩小约 5.9%。每个桶文件开头的DJB2magic 字节会在格式变更时改变,防止旧字节被新规则误读成"错误的会话 ID"而不是"损坏"。
辅助文件:recoffsets.bin记录每个会话在 records.bin 中的位置(可失效重建的纯缓存);manifest.gob/sessions.gob存索引版本号、源文件状态、脱敏计数与会话元数据。
四、中文倒排索引:没有空格怎么办 ⚙️
英文分词按空格切,但中文、日语、泰语没有词间空格。deja-vu 在 internal/index/cjkindex.go 用了一组专门机制,这也是索引格式版本号频繁升级的原因(internal/index/index.go 里从 v12 到 v44 的注释,基本是一部"多语言分词踩坑史"):
- CJK 二元字(bigram):连续汉字切成重叠双字词,查询侧同样切分,索引体积与召回率取平衡;
- 繁体→简体折叠:写入前统一转成简体键,避免"記憶體"与"内存"各占一个键;
- 日语长音符号、泰语、阿拉伯文组合音符:都经历过"被切成碎片"的 bug 修复——比如 Hindi 的
हिन्दी曾因元音符不算 CJK 而被索引成两个碎片,v33 专门为此重建索引; - 功能词 bigram 剔除:
的了、什么这类高频虚词对不丢弃,实测 42 MB 中文语料桶文件从 75.5 MB 降到 70.7 MB。
五、检索链路:BM25 只是第一步 🔍
一次查询的完整流程(详见 docs/ARCHITECTURE.md):
- 查询分词(CJK 走 bigram 路径);
- 从 token 桶读取各词倒排列表;
- 多词查询求交得到候选;
- 按
--harness、--project、--since用会话元数据先过滤,再读出全部候选记录; - 按会话分组,用BM25(k1=1.2, b=0.75)打分:用户提问词的贡献乘 1.3,时间衰减公式为
0.5 + 0.5/(1+age_days)——时间最多让分数减半,永远不会把老记忆清零; - 分数相同时比更新时间,再比会话 ID,最后才截断结果数。
BM25 之上还叠了几个有界的"语义信号",每个只能打破平局、不能凌驾相关性:达到结论的会话(decision)上浮、被rejected标记的会话在每个命中处如实显示、高频被复用的会话按log2曲线最多上浮 50%。可选地,deja embed会另写一个向量边车文件,对最多 64 个候选做混合重排(0.5 词法分 + 0.5 余弦相似度),向量文件损坏则静默回退纯词法结果——默认零依赖,想要语义能力再自己开启。
六、增量索引:只读新增的那几行 ⚡
一个月亮大的历史库不可能每次全量重建。EnsureForSearch对比当前文件集与 manifest,走出四个分支:
| 文件变化 | 处理 |
|---|---|
| manifest 新鲜 | 什么都不做,直接答 |
| 版本/作用域不匹配 | 重建(内容版本与磁盘格式分开追踪) |
| 纯追加的 JSONL | 从上次安全偏移续读,只更新受影响的桶 |
| 文件被删或中段被改 | 重写索引,但保留未变记录 |
追加分支有个容易忽略的陷阱:Agent 可能"回卷"会话——截断文件后重写并超过原长度,看起来和正常增长一模一样。deja-vu 用SafeSize(上次读到的最后一个完整行偏移)加PrefixHash(已读前缀指纹)来区分真增长和重写;而全量校验一个 250 MB 会话文件要 1.7 秒,于是又引入了PrefixSample采样指纹。更关键的是:搜索永远不等重写——EnsureForSearchStale先用现有索引应答,剩余重建交给后台 warmup。
冷重建时的并行度也做过精算:先全部解析,再由单个 goroutine串行写 records/桶/manifest 保证一致性;五个边车文件(共现表、修复对、重复命令表、失败清单、会话事实)互不读写对方,因此并行落盘——这是实测 51 秒降到 30 秒的主要来源。
七、边车文件与隐私防线 🔐
索引主体之外,构建期还会挖出几类"高价值事实",各自独立成边车文件:
fixraw.gob(internal/index/fixpair.go):错误 → 当时如何修好的配对。deja fix靠它回答"上次同样的报错后来怎么解决的";cooccur(internal/index/cooccur.go):词共现表,支撑"你大概想找"式的查询修正;commands.go:带退出码的命令记录,deja how用它给出"这台机器上真实跑通过的调用方式";friction:每个会话首次出现的重复错误首行,会话启动块直接引用,无需回读会话。
隐私上,internal/redact/ 脱敏器跑在每一条写记录路径之前(全量重建、增量追加、导出皆然):AWS 密钥、Bearer/JWT、PEM 私钥块、--password命令行凭据、netrc 行、甚至散文里写的"管理员密码是……"都会被抹去,而键名和上下文保留可搜索。每条可能误伤正常文本的规则都带门控条件,边界在 docs/SECURITY-MODEL.md 中有正式声明。
八、让记忆自己"找上门":MCP 与 hooks
索引建好只是起点。deja-vu 用两种机制把召回推给 Agent,全部复用同一套索引与检索代码:
MCP 服务器(cmd/deja/mcp.go):JSON-RPC over stdio,只暴露一个工具deja,用mode参数区分八种能力——recall(紧凑摘录)、context(摘要)、blame(谁改过这个文件)、fix(同错的既往修复)、how(真实可用命令)、orient(项目惯用命令与常开文件)、remember(存一条持久决策)、handoff(跨 Agent 交接)。
hooks(deja install --auto自动接线):
hook-context:会话启动时注入 ≤2KB 的项目记忆摘要,永远不触发冷构建,出错静默退出、绝不阻塞 Agent 启动;hook-prompt/hook-tool:提问或即将编辑文件前,注入"你在这里打过卡"或该文件的历史决策;hook-tool-after:命令失败时,递上之前验证过能通过的修复(且明确拒绝不可逆操作如 force push);hook-precompact:上下文压缩前抢救结构化状态——实测 43 次压缩里摘要只保住了 77% 的决策和 0.2% 的命令,剩下的靠它接回;hook-session-end:会话结束落戳,防止 Agent 的召回结果里出现"自己引用自己"。
总结:一套架构的三个核心决策
| 决策 | 做法 | 换来什么 |
|---|---|---|
| 零依赖单二进制 | 只用 Go 标准库,sqlite3 走子进程,TUI 自研 | 十秒装完、离线运行、无服务 |
| 注册表 + 归一化会话模型 | 41 种 Agent 各写解析器,输出统一model.Session | 一份索引,所有 Agent 共用记忆 |
| 纯倒排 + 有界信号排序 | 词法 BM25 为主,语义向量仅作可选重排 | 可解释、可离线、损坏可静默回退 |
如果你想动手验证,可以克隆仓库(https://gitcode.com/gh_mirrors/dej/deja-vu)后从 docs/ARCHITECTURE.md 和 internal/index/ 读起——格式版本注释、桶文件编码、增量门控的每一个决定,源码里都写清了它要修的那个具体 bug 和它测出来的数字。
【免费下载链接】deja-vu
Memory for Claude Code, Codex, Cursor and 38 more coding agents, built from the session history already on your disk. Local search, MCP and hooks, no LLM, one Go binary.
相关推荐
为ESP32解锁视觉能力:MicroPython摄像头驱动深度解析
为ESP32解锁视觉能力:MicroPython摄像头驱动深度解析 在物联网和嵌入式开发领域,ESP32以其强大的无线连接能力和丰富的硬件接口而备受青睐。然而,
驱动开发嵌入式物联网智能硬件agent-desktop架构深潜:Rust多crate设计与依赖倒置如何保证跨平台扩展
agent desktop架构深潜:Rust多crate设计与依赖倒置如何保证跨平台扩展 agent desktop 是一个用 Rust 编写的桌面 Agent
GUI 自动化AI 应用桌面应用AI 技能从零构建高性能Rust搜索引擎:倒排索引实战
从零构建高性能Rust搜索引擎:倒排索引实战 你还在为全文检索效率低下而困扰吗?当数据量达到百万级时,传统线性扫描需要几秒甚至几分钟才能返回结果。本文将带你用R
编程语言编译器语言运行时标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考