我在看 OpenClaw 记忆模块时,最受启发的不是“记住更多内容”,而是它对会话生命周期的治理方式:在 compaction 前,先把长期有效信息提取成可持续记忆,避免上下文被压缩后丢失。

SkillLite(rust 轻量的,系统级沙箱的agent skills执行引擎) 的记忆设计,核心就是围绕这条思路展开:先借鉴机制,再结合本地运行场景做取舍和补强。


一、先看 OpenClaw 机制:记忆本质是“压缩前抽取”

OpenClaw 这套机制的关键点可以概括成三步:

  1. 会话即将压缩时,先做 memory flush:给模型一次机会,把“长期有效信息”写入持久层

  2. 会话状态里记录压缩与刷写轮次:避免重复触发和重复沉淀

  3. 历史采用 append-only 思路:保证后续排查、回放、治理都有依据

我认同这套机制的原因很直接:

很多 Agent 的“失忆”不是检索算法问题,而是生命周期管理缺位。如果压缩发生在记忆沉淀之前,再强的向量检索也找不回没被写下来的信息。


二、我在 SkillLite 里怎么借鉴:保留机制内核,借鉴核心思路实现

SkillLite 的记忆链路拆成四段:

  1. 写入层:通过 memory_write 把结构化信息落到 ~/.skilllite/chat/memory/*.md

  2. 索引层:同步构建 SQLite FTS5(BM25)索引;可选构建 sqlite-vec 向量索引

  3. 检索层memory_search 根据配置走关键词检索或向量检索

  4. 注入层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 的流程图

用户新消息进入

读取会话历史与状态

达到 flush 阈值?

达到 compaction 阈值?

静默 turn: 提醒写入 durable memory

memory_write 到 memory/YYYY-MM-DD.md

更新 flush 状态字段

正常进入 agent loop

先 summary old history

写入 compaction entry

memory_search 召回相关记忆

将记忆上下文注入下一轮提示


三、在借鉴基础上的完善:为什么我没有直接照搬

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_countmemory_flush_atmemory_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

这比“魔法参数写死在框架里”更适合工程落地。


六、如果你想按这条思路落地,建议先做三件事

  1. 先跑 BM25,后开向量:先验证记忆写入-检索-注入链路稳定,再引入 embedding

  2. 把 flush 阈值调低做压测:观察高频多轮对话下是否出现重复写入或低价值记忆堆积

  3. 建立记忆文件治理规则:建议按日期或主题落盘,定期做归档和重建索引


七、总结

个人觉得 SkillLite 记忆模块最有价值的地方,不是“用了某个检索算法”,而是它把记忆当成一套可运营的基础设施:

  • 写入可控

  • 索引可退化

  • 检索可升级

  • 会话状态可追踪

回到 OpenClaw:它给我的最大启发是“先治理时机,再优化检索”。

SkillLite 在会话记忆治理上做了明确借鉴;在本地可用性、容错和检索分层上,我做了有针对性的完善。

如果你正在做本地 Agent 或私有部署,这套设计思路值得直接参考。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐