从 OpenClaw 记忆模块出发:我如何在 SkillLite 里借鉴、取舍与完善
本文探讨了OpenClaw记忆模块的设计思路及其在SkillLite中的工程化实践。核心借鉴了OpenClaw"压缩前抽取"的会话治理机制,在会话达到压缩阈值前先将长期有效信息写入持久层。SkillLite将记忆系统拆分为写入、索引、检索、注入四层,并做出多项改进:采用BM25+向量双轨检索确保可用性,优化分块策略提升召回质量,强化写入安全边界,以及内置完善的退化策略。文章强调
我在看 OpenClaw 记忆模块时,最受启发的不是“记住更多内容”,而是它对会话生命周期的治理方式:在 compaction 前,先把长期有效信息提取成可持续记忆,避免上下文被压缩后丢失。
SkillLite(rust 轻量的,系统级沙箱的agent skills执行引擎) 的记忆设计,核心就是围绕这条思路展开:先借鉴机制,再结合本地运行场景做取舍和补强。
一、先看 OpenClaw 机制:记忆本质是“压缩前抽取”
OpenClaw 这套机制的关键点可以概括成三步:
-
会话即将压缩时,先做 memory flush:给模型一次机会,把“长期有效信息”写入持久层
-
会话状态里记录压缩与刷写轮次:避免重复触发和重复沉淀
-
历史采用 append-only 思路:保证后续排查、回放、治理都有依据
我认同这套机制的原因很直接:
很多 Agent 的“失忆”不是检索算法问题,而是生命周期管理缺位。如果压缩发生在记忆沉淀之前,再强的向量检索也找不回没被写下来的信息。
二、我在 SkillLite 里怎么借鉴:保留机制内核,借鉴核心思路实现
SkillLite 的记忆链路拆成四段:
-
写入层:通过
memory_write把结构化信息落到~/.skilllite/chat/memory/*.md -
索引层:同步构建 SQLite FTS5(BM25)索引;可选构建 sqlite-vec 向量索引
-
检索层:
memory_search根据配置走关键词检索或向量检索 -
注入层:
build_memory_context在每轮对话前注入与当前用户输入相关的记忆片段
这个拆法是对 OpenClaw 思路的工程化落地:
先把“压缩前抽取”固化成时机,再把“写入、索引、检索、注入”拆层,保证每层都能独立排查和替换。
代码案例 1:接近 compaction 时先触发 memory flush
下面这段逻辑对应 SkillLite 的核心借鉴点:会话到达阈值后,不是立刻压缩,而是先尝试沉淀长期记忆。
let flush_threshold = get_memory_flush_threshold();
if self.config.enable_memory
&& get_memory_flush_enabled()
&& history.len() >= flush_threshold
{
let next_compaction = entry.compaction_count + 1;
let need_flush = entry.memory_flush_compaction_count != Some(next_compaction);
if need_flush {
self.run_memory_flush_turn(&history).await?;
se.memory_flush_compaction_count = Some(next_compaction);
se.memory_flush_at = Some(chrono::Utc::now().to_rfc3339());
}
}
这段代码背后的机制思考是:把“记忆是否已沉淀”做成状态,而不是靠模型自觉。
代码案例 2:memory_write 后同步索引,向量失败自动回退
SkillLite 不是“写文件就结束”,而是写完立即建立可检索能力;向量链路失败时保留 BM25。
// 1) 落盘
std::fs::write(&file_path, &final_content)?;
// 2) BM25 索引(始终执行)
skilllite_executor::memory::ensure_index(&conn)?;
skilllite_executor::memory::index_file(&conn, rel_path, &final_content)?;
// 3) 向量索引(可选执行,失败不影响主链路)
match ctx.client.embed(&ctx.embed_config.model, &texts).await {
Ok(embeddings) if embeddings.len() == chunks.len() => {
skilllite_executor::memory::ensure_vec0_table(&conn, ctx.embed_config.dimension)?;
skilllite_executor::memory::index_file_vec(&conn, rel_path, &chunks, &embeddings)?;
}
_ => tracing::warn!("Embedding failed, BM25 index only"),
}
OpenClaw 机制借鉴到 SkillLite 的流程图
三、在借鉴基础上的完善:为什么我没有直接照搬
1) 检索层做成双轨:默认 BM25,可选语义向量
SkillLite 默认使用 SQLite FTS5,几乎零额外依赖,启动快、资源开销低。
如果打开 memory_vector feature,并且 embedding 能力可用,再启用 sqlite-vec。
这意味着:
-
本地或弱网环境下,BM25 依然可用
-
云端模型可用时,可以拿到语义召回能力
-
一套数据结构同时覆盖“稳态可用”和“上限效果”
2) 分块策略强调可检索性,而不是“先存进去再说”
记忆写入索引前会先做分块:
-
目标块大小约 400 tokens
-
块间 overlap 约 80 tokens
-
以段落为主进行切分,避免语义中断过于严重
这在长会话摘要、需求讨论记录这类文本里很关键。纯字符切分会导致关键词命中但语义断裂,最终让召回结果可读性很差。
3) 写入边界做显式约束,避免本地 Agent 的路径风险
memory_write 在落盘前会做路径规范化,显式阻止 .. 逃逸内存目录。
对本地 Agent 来说,这是一个经常被忽略、但非常必要的边界防护。
4) 退化策略写进代码:embedding 波动不影响基础可用
向量索引阶段如果 embedding 返回数量不匹配或请求失败,系统只打 warning,继续保留 BM25 索引。
这不是“理想设计”,而是生产可运行设计:优先保证可用,再追求最优召回。
四、对比 OpenClaw:我借鉴了什么,又改了什么
先说结论:SkillLite 对 OpenClaw 的借鉴是明确且有边界的。
我重点借鉴“会话治理机制”,而不是直接复制整套实现;然后在检索与容错上补齐本地工程能力。
1) 明确借鉴的部分(从代码语义可直接确认)
A. Pre-compaction memory flush 机制
在会话接近 compaction 阈值时,SkillLite 会触发一次静默 turn,提醒模型把长期价值信息写入当日记忆文件。
这在代码里直接标注为 OpenClaw-style,目的很明确:避免旧上下文被压缩后关键信息丢失。
B. 会话状态字段对齐
sessions.json 里保留了 compaction_count、memory_flush_at、memory_flush_compaction_count 等字段,注释也标明与 OpenClaw schema 对齐。
这让“是否已刷写记忆”“当前压缩轮次”成为可追踪状态,而不是临时变量。
C. append-only 的状态持久化思路
任务计划采用 append-only JSONL 方式落盘,注释同样标记 OpenClaw-style。
这和记忆并非同一模块,但都体现了同一个思路:历史可追溯优先于覆盖写入。
2) SkillLite 的补强点(不是照搬)
A. 检索层从单路扩展为双路
在借鉴会话级记忆刷写机制的同时,SkillLite 增加了 BM25 + 向量双轨检索能力。
这让系统既能在低依赖场景运行,也能在 embedding 条件具备时提升语义召回。
B. Flush 触发更早,可提前“抢救上下文”
SkillLite 除了 compaction 前触发,还支持通过 SKILLLITE_MEMORY_FLUSH_THRESHOLD 提前触发。
默认值比 compaction 阈值更低,意味着在压缩真正发生前就先把高价值信息沉淀下来。
C. 写入安全与失败回退更完整
路径约束、防逃逸、embedding 失败回退 BM25、维度变化自动重建向量表,这些都属于工程增强,不是理念层描述。
五、我对 OpenClaw 机制的一个核心判断:记忆首先是会话治理问题
很多讨论把记忆看成“存一点文本 + 检索一下”,但在真实系统里,记忆质量高度依赖会话治理策略:
-
什么时候压缩历史
-
压缩前是否先抽取长期记忆
-
压缩后保留多少最近上下文
-
记忆检索结果如何注入系统上下文
SkillLite 现在的实现,把这些策略放到了可配置参数里:
-
SKILLLITE_COMPACTION_THRESHOLD -
SKILLLITE_COMPACTION_KEEP_RECENT -
SKILLLITE_MEMORY_FLUSH_ENABLED -
SKILLLITE_MEMORY_FLUSH_THRESHOLD
这比“魔法参数写死在框架里”更适合工程落地。
六、如果你想按这条思路落地,建议先做三件事
-
先跑 BM25,后开向量:先验证记忆写入-检索-注入链路稳定,再引入 embedding
-
把 flush 阈值调低做压测:观察高频多轮对话下是否出现重复写入或低价值记忆堆积
-
建立记忆文件治理规则:建议按日期或主题落盘,定期做归档和重建索引
七、总结
个人觉得 SkillLite 记忆模块最有价值的地方,不是“用了某个检索算法”,而是它把记忆当成一套可运营的基础设施:
-
写入可控
-
索引可退化
-
检索可升级
-
会话状态可追踪
回到 OpenClaw:它给我的最大启发是“先治理时机,再优化检索”。
SkillLite 在会话记忆治理上做了明确借鉴;在本地可用性、容错和检索分层上,我做了有针对性的完善。
如果你正在做本地 Agent 或私有部署,这套设计思路值得直接参考。
更多推荐




所有评论(0)