Skip to content

NightPoetry/GrowBox

Repository files navigation

GrowBox

一个"越用越强"的持久学习型 AI 编程 Agent(IDE-agent)。

GrowBox 把 IDE 当作 AI 的手脚,把"记忆"当作 AI 的本体。它不是每次对话从零开始的助手, 而是一个会把每一次检索、每一次判断的副产品沉淀成结构、下一次直接复用的 Agent——用得越多, 找东西越快,对你的项目理解越深。

本仓库的核心不在于又一个 Agent 循环,而在于它底层的 AI 记忆机制。本文重点讲这一块。


给接手的 AI:先「置换归位」

这一段是写给 AI 看的。如果你是被叫来在这个项目上做二次开发的 AI 助手——别急着写代码,先「置换归位」。

「置换归位」是这个项目的开工口令,也是对它核心机制(记忆置换系统)的一句双关:把项目的关键几「页」上下文换回你的工作记忆,你才谈得上接手。具体就是按顺序读这几份(都在公开仓库里):

  1. CLAUDE.md —— 项目的 AI 指导手册:铁律、架构、怎么构建与测试。
  2. QA.md —— 用一问一答把记忆机制讲透(项目的心脏,先懂它)。
  3. 设计文档/记忆置换系统-总纲.md + 设计文档/具体系统设计/ —— 各系统实际怎么落地的(as-built,真理来源)。
  4. README.md —— 架构与技术栈全貌。

读完这四样你就「归位」了——对项目的认识接上了,可以开始你自己的二次开发。

注:更深的会话级交接与记忆快照收在加密保险箱(private/docs.tar.gz.enc),密钥不入库、不公开,克隆者打不开也不影响——上面四份公开文档已足够理解和扩展这个项目。


核心理念:记忆无界,上下文有限

LLM 的上下文窗口是有限的物理资源,而一个持久 Agent 的记忆会无界增长。两者的矛盾,本质上 就是操作系统里"无界虚拟地址空间 vs 有限物理内存"的矛盾。所以 GrowBox 的记忆架构被设计成一套 AI 记忆置换系统——类比操作系统的虚拟内存 / 分页。

操作系统 GrowBox 记忆机制
虚拟地址空间(无界) 全部记忆(磁盘 redb,无界增长)
物理内存 RAM(有限) LLM 上下文窗口(活跃工作集)
页表 / 地址翻译 指针网(节点到节点的有向边,做"该换入谁"的地址映射)
缺页 → 从 swap 调页 检索 miss → 沿指针从冷归档按需召回
页面置换(LRU/clock) 缓存按热度调入 / 淘汰
swap / 磁盘 冷归档(redb)
TLB / page cache(加速,非本质) 缓存(加速层)

一句话:置换是本质,缓存是加速。 系统负责把"现在该看的"那一小块换进上下文,把"暂时不看的" 换出冷归档;索引/指针网是地址映射,让你不必全量扫就能定位该换入谁。

记忆机制由两大块组成:缓存机制(决定上下文里放什么、怎么放才省钱)与 索引机制(决定怎么快速找到该换入的那块记忆)。


一、缓存机制:索引队列 + 上下文区域

缓存分两部分:索引队列上下文区域

  • 上下文区域(工作记忆区):真正进入 LLM 上下文窗口、作为模型回答直接参考的那一批记忆 数据
  • 索引队列:这批数据对应的 指针(元数据) 的排队。索引队列里排的每一项,都指向上下文区域里 的一块内容——所以"上下文区域中的内容,就是索引队列中的内容"。

两者看似同步,但有一个关键的错位设计:

索引队列里的项会随着使用改变位置(按热度重排),但上下文区域里对应的内容不会跟着动。

为什么要这样?因为 LLM 的 prompt 缓存只对"稳定前缀"生效——上下文开头那段内容只要逐字不变, 供应商就能复用上一轮的计算,直接省钱省延迟。如果每次有人被访问就把它在上下文里挪到队首,整个 前缀就被打乱,缓存全部失效。

所以 GrowBox 把"排队"和"数据"彻底分开:

排的是什么 行为
索引队列 指针(元数据,在上下文之外) 随访问重排、提队首、按热度淘汰最冷
上下文区域 数据(在上下文之内) 只有"在 / 移除"两态,原位不动,绝不因重排而挪位

一条内容只要还没被挤出队列(没被淘汰),它在上下文区域里的位置和内容就纹丝不动——于是稳定 前缀稳定,缓存命中。命中率高时,这块区域几乎一整段都能被 prompt 缓存复用。

补充的几条工程约束:

  • 大小随模型 / 用户可调:上下文窗口越大的模型,这块区域配得越大、驻留越多记忆。
  • 非线性区,靠时间戳定先后:数据的载入顺序不等于真实时间顺序,所以每条载入的记忆都带完整 时间戳,并明确告诉模型"判断历史先后看时间戳,不看它在区里的前后位置"。
  • 末尾挂一段最近记忆 ring buffer:固定小容量、每轮都变,有意放在上下文最末——它每轮都会 打破缓存,但只损它自己这一小段,不波及前面的稳定前缀。

二、索引机制:最差退化为暴力全面搜索

索引机制的目标是加速"在无界记忆里找到该看的那块"。它的第一原则是兜底安全:

最差情况退化为暴力全面搜索——即让 LLM 一段段读原文、判断相关,翻到最早就是全量扫描。 一切加速结构都只是"这次的副产品",失效了不影响正确性,只影响速度。

关键前提:RAG 和 LLM 精确检索不是上下层级,而是两种平级的"索引手段"。 它们的差别只在 "怎么找到那条索引指针"(RAG = 向量近似;L2 = LLM 读原文 + 指针图导航),找到之后殊途同归—— 都把内容调进上一节那个唯一的存放区(没进存放区,就谈不上记忆)。成本上,便宜的 RAG 先试、 贵的 LLM 精确按需补上,但这是成本顺序,不是"RAG 在上、L2 在下"的层级

索引手段之一:RAG(向量近似)

用向量相似度召回(磁盘原生 ANN,见下方技术栈),廉价、直接跳到目标。够用且反馈好就收工。

索引手段之二:LLM 精确检索(L2)——一个自加速飞轮

当 RAG 不够(没找到 / 反馈不好 / 用户显式引用历史),就用 LLM 读原文判断相关——很贵,但兜底可靠。 这一层的设计核心是:每一次扫描的副产品,全部沉淀成结构,复用到下一次——检索越用越快。 思路直接来自计算机算法里的记忆化搜索(memoization):算过的结果记下来,下次同样的子问题直接 查表,不重算。

落在记忆上,这张"表"就是指针,它正好可以用一组 Q / K 的视角来理解:

  • 一条指针的键(K)= 过去某次成功找到目标的查询向量。一个"曾经成功命中目标的提问", 比目标原文本身更能预测"下次类似的提问要的是什么"。
  • 新的一次查找(Q)= 当前查询向量。
  • 拿 Q 和各个 K 做相似度比对(余弦):命中就直接沿指针跳到对应记忆,省掉一整轮线性扫; 跳到后再让 LLM 确认一次(不盲信捷径,确认代价远小于线性扫整批)。
  • 没命中才退回线性扫;线性扫一旦扫到相关,就在当前位置沉淀一条新指针,喂给下一次。

这一步就让"扫描越用越快"成立:走过一次的路,下次直接抄近道。

围绕指针,还有几件配套的"扫描副产品":

机制 沉淀的是什么 一句话
指针 走过的路 Q 比 K 命中 → 直接跳到目标
染色 扫过的区 深绿(确信无漏)/ 浅绿(跟指针快扫、可能漏)/ 无色(没到过);全量扫时跳过深绿,不重读
缓存 最近的热度 磁盘指针图之上的有界 RAM 工作集(平铺单 LFU,按热度淘汰),即上面的"索引队列"
二级索引 / 碎片 远处记忆拉近 + 登记未检区段 见下
强制跳转 用户显式指认的位置 用户引用历史 → 钉一条"遍历到此必跳"的指针

涌现式分层:为什么不让 LLM 主动分类

处理"无界记忆怎么组织"的常见做法,是让 LLM 主动分层:把文档喂给模型,让它提炼标题 / 摘要、 归出主题、建一棵分类树。GrowBox 刻意不这么做,这是一条基本的设计原理分歧:

LLM 主动分层是主观的,基于用户查找自动建立的分层是客观的。

  • LLM 主动分类 = 主观臆测。 模型按"它以为的"主题归类,而这套主观分类和使用者实际怎么找、 怎么联想常常错位。一旦归错类,这个错误就固化进结构,之后每次查找都被它带偏、绕远路—— 且模型无从知道"这个用户真正关心的切分维度"是什么。
  • 查询涌现的分层 = 客观事实。 上一节的指针正是这套机制:每条指针的键(K)是一次真实成功的 查询,每条边是一次真实发生的"从这里联想到那里"。结构不是预先规定的,而是从真实查找里 长出来的——它直接编码使用者的客观分类习惯(他实际怎么问、怎么联想),不掺模型的主观判断。

于是涌现出的这张分层结构 + 图结构,天然贴合真实查询分布:高频走的路被反复加固、变成最短捷径; 没人走的"分类"不占成本、不拖慢别的查询。最大程度地加速使用者真实的查找,而不是加速一棵 "LLM 以为你会这么找"的树。一句话:不预先替用户分类,让分类从用户怎么用里自己长出来—— 客观、自适应、越用越准。

那文档破碎化(把投喂的整篇长文按句切成小块)里不是也用了 LLM 吗?——用了,但只用来定粒度: 把一个大节点切成各自可被独立命中的原子小块,解决"一条向量装不下整篇 → 窄问被稀释必漏"。 它不强加任何跨记忆的分类 / 层级;粒度之上"哪块连哪块、先看谁"的组织,仍旧只由查询涌现。 分工清楚:破碎管"切多细"(低风险的局部判断),涌现图管"怎么连、先看谁"(客观、从使用长出)。

二级索引,以及它带来的"碎片区域"

只有指针还不够快。当一条热记忆在缓存里漂得离当前对话越来越远,光靠原始的指针边可能够不着它。 于是再加一层 二级索引:在当前位置建一个锚点,把远处那条热记忆拉回到当前前沿,下次不用从头 导航也能召回。系统只保留(一级,二级)两层锚点,按"漂移距离"分档粗化,永远只有两层

这里出现了用户描述里那块最微妙的区域:

二级索引和一级索引之间的中段,是没有经过 LLM 比对的。

因为走二级锚点是"一跳直达",中间那段被整个跳过了——可是那段里仍可能存在与某个 K 对应的 内容,只是这次没去读它。这段被跳过、未经 LLM 判断、却可能仍含相关内容的区间,称为碎片区域。 系统把它登记进一本有界的碎片台账,留待空闲时补做检索。

维护:做梦 / 睡眠 / 疲劳

登记下来的碎片需要后续补检索。GrowBox 交给一个潜意识 LLM(后台低优先级实例)在空闲时处理:

  • 做梦(Dream):取一条碎片记录 → 读那段被跳过的区间 → 判断是否有遗漏 → 补进索引 → 从台账移除。
  • 睡眠(Sleep):做梦与推演交替,直到碎片台账清空 / 被唤醒 / 超时。推演 = 趁记忆网络空闲时 预演"用户若问 X 该检索到什么",提前建好索引(此过程会产生新的碎片登记,属预期行为)。
  • 疲劳度:不看 CPU / 内存,而由三项加权得出——缓存命中率低 + 淘汰频繁 + 碎片占比大;疲劳度越高,越倾向进入睡眠。
  • 小息(Nap,手动):只清当前对话与缓存工作集,保留长期记忆、指针、索引与设置。

对话检索、做梦、飞轮提炼共用同一个潜意识 LLM,由一个潜意识仲裁器按优先级 前台 Agent > 睡眠 > 飞轮调度,确保后台维护不与前台争用资源、不相互冲突。

RAG 指针的纯净性:线性才能染色

上面的染色 / 碎片机制有一个隐含前提:它沉淀的指针必须是"扫过去"的产物。LLM 精确检索天然满足 ——它从近往远顺序读原文,到某条记忆为止的路径都被直接 / 间接扫过,底下没有 gap。RAG 却是靠向量 "直接跳"到目标、没扫过中间的路,这会让它产出的指针"不纯净"。所以 RAG 指针怎么处理,取决于 RAG 本身是不是线性扫:

  • 线性 RAG(朴素 / 暴力向量,从前往后逐条算分):本质也是一次顺序扫描,只是用向量算分代替 LLM 读原文。这种 RAG 可以和 LLM 精确检索共用同一套指针节点,单独给它染色、记碎片(指针 / 碎片系统 分成 RAG 部分 + LLM 部分两套),让 RAG 也跳过已扫区 → 加速 RAG。
  • 非线性 RAG(ANN / HNSW,GrowBox 的实际实现):它不顺序扫、没有"路径"和 gap 的概念,所以 用不了染色 / 碎片。这种 RAG 命中产出的指针只作"占位指针"——仅用于让 RAG 命中也能统一进索引 队列与缓存、参与置换(把内容调进存放区);占位指针被换出队列时不在时间线上留二次锚点。

架构(6 个 crate,单向依赖)

架构公理:一切能力皆"执行器",Agent 循环是唯一脊柱(一个注册表、一条分发路径)。

app(gui)  Agent 循环脊柱 + 执行器注册表 + Tauri + 前端(SolidJS)
├── memory   分层检索(RAG → 精确层飞轮)+ 上下文置换    <- 本文主角
├── learn    飞轮:收集 → 聚类压缩 + 永久目标调度
├── safety   沙箱 / 路径分级 / 风险 / 三种授权
├── llm      LLM 通信:流式 / reasoning / 工具解析 / 嵌入
└── core     共享类型:结论模型 + 执行器 trait(零依赖)

记忆相关技术栈:

  • redb:单文件持久化。节点(含向量 + role)、结论、指针边、强制跳转边,全部 write-through 落库。 边按 source 前缀存,取一个节点的邻域 = 一次 B 树局部读,全图永不整体载入内存
  • arroy + heed/LMDB:第一层向量索引,磁盘原生 mmap,向量不全驻进程 RAM。向量引擎在 VectorIndex trait 后面,可换。
  • 本地 / 远程嵌入双通道:本地默认走 candle 跑 multilingual-e5-small,另留 OpenAI 兼容的远程嵌入槽。

⚠️ 首次运行需联网下载嵌入模型:RAG 第一层用的本地嵌入模型 multilingual-e5-small(约 470MB) 默认不随安装包,首次运行时自动从 HuggingFace 下载(huggingface.co,服务器在中国大陆境外)。 因此第一次启动前请准备好能畅通访问 HuggingFace 的网络(必要时配好代理),否则模型拉取会失败、 语义检索不可用。模型下载一次后本地缓存,后续启动无需联网。若要离线分发,可用 scripts/build-official.sh --with-model 出"随包模型"的 full 变体(把权重打进安装包)。

正文与向量两半都做到磁盘原生,内存占用 = 工作集,与总记忆量解耦——这是"无界记忆"成立的前提。


构建与测试

# 单 crate / 全工作区单元测试
cargo test -p growbox-memory
cargo test --workspace

# 出包(开关已绑死一致,免踩目录坑)
scripts/build-official.sh    # 正式包:无调试桥、无本地调试端口
scripts/build-test.sh        # 测试包:带调试桥 + 本地调试端口 + e2e 钩子

注意:cargo tauri build 必须在仓库根运行,且改了前端要先 npm run build 重建 dist; 直接用上面的脚本可避开这两个坑。


项目状态

本项目正在重构中,记忆机制(缓存 + 索引 + 精确层飞轮 + 上下文置换 + 做梦/睡眠/疲劳/仲裁器)的 后端已全部落地,cargo test --workspace 全绿。前端面板、打包流程、跨平台仍在推进。

设计文档体系在 设计文档/(记忆置换系统-总纲.md 为记忆机制总纲)。


许可

MIT License,附强制署名及**专利防御(专利授权 + 防御性终止)**条款:任何分发、衍生作品或使用本软件的产品, 都必须对原始项目 GrowBox 作出清晰可见的署名并附原始仓库链接;任何人如就本软件发起专利诉讼,其获得的专利许可将立即终止(详见 LICENSE)。

About

GrowBox — 越用越强的持久学习 AI 编码 Agent:分层记忆置换系统(RAG→精确层飞轮)+ 自研内核

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages