我在之前那篇 《从 OpenClaw 记忆模块出发》 里,重点聊了会话治理机制——如何在 compaction 前把高价值信息沉淀下来。那篇文章解决的是「什么时候该记住」的问题。

在我近期的实验和测试中发现,如果只做到记忆压缩和写入,其实只是把「更多信息塞进上下文」,并没有真正解决「如何让记忆变得可检索、可复用」。这正是结构化知识抽取要补的课:把原始记录变成实体、关系、情节、倾向、模式,让记忆从「一团文本」变成「可索引的知识图谱」。

但是我又不希望吧可以索引的知识图谱做的很重,要安装图数据库之类的,正在尝试用轻量的方式来解决。

SkillLite 自进化引擎的记忆增强,就是沿着这个方向走的。


一、问题:记忆压缩后,信息依然难以复用

传统记忆方案的典型做法:

  1. 会话快满了 → 做 summary

  2. 把 summary 写入记忆文件

  3. 下次检索时把记忆文件召回

这套流程有个隐藏问题:summary 是自然语言文本,检索粒度粗,召回结果常常「相关但无用」

比如任务记录里写了「某次重构任务用了 grep + read_file,先查文件再写回」,下次你想搜「有没有类似的路径错误经历」——这种语义级别的类比检索,纯靠关键词或简单 summary 很难命中。

这就引出一个需求:不仅要把信息记下来,还要把信息「拆解」成可独立检索的原子单元


二、OpenClaw 没做的事:结构化知识抽取

OpenClaw 的记忆机制核心是「压缩前flush」——在上下文被压缩前,把有效信息写入持久层。它的贡献在于解决了时机问题(什么时候该记),但没有解决形态问题(记成什么样)。

oganic 的记忆方案更进一步,提出了实体-关系-情节的层级划分。但真正工程化落地的凤毛麟角。

SkillLite 在借鉴 OpenClaw 机制的基础上,做了一个独立的「记忆进化」模块:从任务执行记录中自动抽取结构化知识,写入知识库供后续检索和类比


三、SkillLite 的五类知识结构

这套知识抽取定义了五种知识类型,各自承担不同的检索目的:

类型

检索目的

示例

实体

「出现过哪些工具/项目/技术」

grep、Rust、某配置文件路径

关系

「X 和 Y 什么关联」

「重构任务常用 grep + read_file」

情节

「有没有类似的经历」

「某次未确认工作目录导致失败 → 教训:改路径前先确认 cwd」

倾向

「Agent 习惯怎么做事」

「在改配置时,多次观察到先查文档再改」

模式

「这类任务的通用规律是什么」

「做查找并替换类任务时,通常先 grep 再 read_file 再写回」

这五种类型的区别很重要:

  • 实体和关系是静态知识,回答「有什么」

  • 情节是单次经历,回答「发生过什么」

  • 倾向和模式是行为规律,回答「通常怎么做」

检索时可以根据需求精准召回,而不是拉出一大段 summary。


四、与规则、技能的区别:只输出「实然」

这是最容易混淆的地方。Knowledge Extraction 模块有一条明确的设计边界:

  • 规则模块:抽取「何时做什么」类指令(应然)→ 指导下一步行动

  • 技能模块:抽取可执行的工具组合(能力)→ 直接调用

  • 记忆模块:抽取「出现了什么、发生了什么、观察到什么」(实然)→ 供检索和类比

一个具体的例子:

  • 「先 grep 再 read_file」→ 如果是观察到的规律 → 记忆模块输出为模式

  • 「改路径前应先确认 cwd」→ 如果是规定的检查项 → 规则模块输出为规则

这种边界划分保证了知识不重复、不冲突。


五、工程实现:从决策记录到知识条目

1. 数据来源:decisions 表

记忆进化的输入来自 SQLite decisions 表。每条记录包含:

  • task_description:任务描述

  • total_tools / failed_tools:工具调用统计

  • tools_detail:工具调用序列

  • task_completed:是否成功

  • elapsed_ms:耗时

这些字段组合起来,就是「一次任务执行」的结构化画像。

2. 抽取 Prompt:五类知识的提取指令

你是 SkillLite 进化引擎的记忆知识抽取模块。从任务执行记录中抽取**事实与经历**,写入知识库,供后续检索与类比。

## 知识类型(五类)

| 类型 | 含义 | 示例 |
|------|------|------|
| **实体** | 记录中反复或关键出现的概念、项目、工具、路径、技术等 | read_file、skillLite、Rust |
| **关系** | 两个实体或「任务类型↔工具/结果」之间的关联 | 「重构任务常用 grep+read_file」 |
| **情节** | 单次经历的摘要:发生了什么、成功/失败、一条可复用教训 | 「某次未确认工作目录导致失败 → 教训」 |
| **倾向** | 观察到的行为倾向或习惯(非规定性),带情境 | 「在改配置时,多次观察到先查文档再改」 |
| **模式** | 跨多次经历的重复规律(比单条情节更抽象) | 「做查找并替换类任务时,通常先 grep 再 read_file 再写回」 |

## 输出格式
{
  "entities": [{ "name": "...", "type": "概念|项目|工具|路径|技术", "note": "..." }],
  "relations": [{ "from": "...", "to": "...", "relation": "..." }],
  "episodes": [{ "summary": "...", "outcome": "成功|失败", "lesson": "..." }],
  "preferences": [{ "description": "...", "context": "..." }],
  "patterns": [{ "description": "...", "evidence": "..." }],
  "skip_reason": "若无可抽取内容则说明原因"
}

这个 prompt 设计了几个关键约束:

  1. 只输出实然:禁止出现「应该」「务必」等规定性表述

  2. 去重机制:把已有知识摘要作为参考,避免重复抽取

  3. 单次上限:每类知识有抽取上限(实体12条、关系10条、情节8条等),避免单次写入过长

3. 抽取结果写入

抽取完成后,结果以 Markdown 格式追加到 memory/evolution/knowledge.md

## 2026-03-09 14:32

### 实体
- **grep** (工具) 在多次查找任务中使用
- **read_file** (工具) 常用于读取配置和代码

### 关系
- 重构任务 → grep + read_file: 常用组合
- 路径错误 → 失败: 常导致任务中断

### 情节
- [失败] 未确认工作目录导致文件写入错误 → 教训:改路径前先确认 cwd
- [成功] 使用 grep 定位后批量读取并修改 → 教训:先定位再处理是有效策略

### 倾向
- 在改配置时,先查文档再改(观察到 3 次)

### 模式
- 做「查找并替换」类任务时,通常先 grep 再 read_file 再写回

4. 检索链路

写入的知识可以直接被 memory_search 召回。检索时按关键词匹配对应字段,比如:

  • 搜「路径错误」→ 命中 relations 和 episodes

  • 搜「grep」→ 命中 entities 和 patterns

  • 搜「有没有类似的失败经历」→ 召回 episodes 中 outcome=failed 的条目


六、与会话级记忆的关系:互补而非替代

这里有一个容易混淆的点:有了结构化知识抽取,还要不要原来的会话记忆?

答案是:两者是互补关系。

维度

会话级记忆(memory flush)

进化知识库(knowledge extraction)

触发时机

compaction 前

定期(最近7天决策记录)

内容形态

原始文本 summary

结构化实体/关系/情节/倾向/模式

检索粒度

粗(整段文本)

细(字段级匹配)

用途

补充上下文

类比检索、经验复用

会话级记忆解决「上下文不丢」的问题;进化知识库解决「经验可复用」的问题。两者走不同的检索路径,互不干扰。


七、工程细节:容错与安全

1. 解析失败处理

LLM 返回的 JSON 可能格式不规范。解析失败时记录 event 并跳过本次抽取,不会阻断主流程:

let parsed = match parse_knowledge_response(&content) {
    Ok(p) => p,
    Err(e) => {
        tracing::warn!("Memory knowledge extraction parse failed: {}", e);
        if let Ok(conn) = feedback::open_evolution_db(chat_root) {
            let _ = crate::log_evolution_event(
                &conn, chat_root, "memory_extraction_parse_failed", "", &format!("{}", e), ""
            );
        }
        return Ok(Vec::new());
    }
};

2. 内容安全校验

抽取结果经过 L3 内容审查(gatekeeper_l3_content),防止敏感信息写入知识库:

if let Err(e) = gatekeeper_l3_content(&full_content) {
    tracing::warn!("Memory evolution L3 rejected content: {}", e);
    return Ok(Vec::new());
}

3. 路径安全校验

知识库文件路径经过 L1 路径审查,防止路径逃逸:

if !gatekeeper_l1_path(chat_root, &knowledge_path, None) {
    tracing::warn!("Memory evolution L1 path rejected: {}", knowledge_path.display());
    return Ok(Vec::new());
}

八、效果验证

以一次「查找并批量替换」类任务为例:

原始决策记录(输入)

- 任务: 在 src 目录下查找所有包含 "TODO" 的文件并替换为 "FIXME"
- 完成: 是 | 工具: grep, read_file, write_file (失败: 0) | replan: 0 | 耗时: 3200ms

抽取后的知识条目

### 实体
- **grep** (工具) 用于文本搜索
- **write_file** (工具) 用于写回修改
- **TODO/FIXME** (概念) 代码标记类型

### 关系
- 查找替换任务 → grep + read_file + write_file: 完整工具链

### 情节
- [成功] 使用 grep 定位 TODO 后批量读取并替换为 FIXME → 教训:先定位再批量处理是有效策略

### 模式
- 做「查找并替换」类任务时,通常先 grep 定位,再 read_file 读取上下文,最后 write_file 写回

下次遇到类似任务时,检索「查找替换」「grep」「批量处理」等关键词,都能命中这些结构化条目。


九、总结

SkillLite 尝试记忆增强方案,核心价值在于把「记了什么」变成「记了什么实体、什么关系、什么经历、什么规律」

  • 五类知识分工明确:实体关系回答「有什么」,情节回答「发生过什么」,倾向模式回答「通常怎么做」

  • 与规则、技能明确边界,只输出实然,不与指令类知识重复

  • 会话记忆(压缩前flush) + 结构化知识(进化抽取)双轨并行,各司其职

  • 工程层面有容错、有审计、可追溯

回到最初的问题:超越记忆压缩的本质,是让记忆从「文本」变成「可拆解、可检索、可复用的知识单元」。SkillLite 的结构化知识抽取,就是这个方向的一次工程化尝试。

如果你在做一个会「积累经验」的 Agent,这套方案可以直接参考——关键不是记多少,而是记完之后能不能找得到、用得上。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐