找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1047

积分

0

好友

133

主题
发表于 昨天 03:23 | 查看: 0| 回复: 0

AI记忆的难点,从来不在“存”,而在“取”和“保”。

昨天聊了Clawdbot到Moltbot的改名风波,但名字不是重点。我更在意的是它背后的一个设计:它如何把“长期记忆”做稳。因为改名只是一次突发事件;而记忆系统决定的,是一个Agent能否在现实世界中长期运行,能否在第30次对话时依然保持靠谱。

“AI记忆”常被讲成模型能力,但落到工程现场,问题会变得很具体,也很不浪漫:

  • 对话一长就要压缩,压缩是有损的
  • 工具输出会膨胀上下文,成本和延迟一起上涨
  • 记忆文件越堆越多,真正需要的那条信息反而更难召回

Clawdbot给出的答案非常克制:把记忆当作工作区里的Markdown文件,把回忆交给工具检索,把“丢失风险”收敛到“压缩前刷新”这一道闸口。

这套路线的价值不在“新”,而在三件事:可解释、可编辑、可迁移

太长不看版

  • 上下文(Context):模型一次请求能看到的全部内容,短暂、有界、昂贵。
  • 记忆(Memory):磁盘上的内容,持久、无界、可版本化。
  • Clawdbot的记忆默认是工作区纯Markdownmemory/YYYY-MM-DD.md + 可选的 MEMORY.md。(官方英文文档的原话:plain Markdown in the agent workspace
  • 会话开始会读取今天 + 昨天的每日文件;MEMORY.md 只在主私人会话加载,群组上下文永不加载。这一条非常关键——我们经常忽略“群聊泄露个人上下文”的风险面。
  • 写入没有专用 memory_write:就是普通文件写入。也正因为如此,记忆本质上是“可审计的资产”,而不是“黑盒状态”。
  • 压缩前刷新(pre-compaction memory flush)是关键闸口:接近自动压缩时先静默写入持久记忆,再允许压缩发生。
  • 刷新默认是静默轮次,通常用 NO_REPLY 抑制输出,让用户侧无感。2026.1.10 之后的版本,流式输出如果以 NO_REPLY 开头也会被抑制,避免半截内容泄露到用户侧。
  • 记忆检索工具:memory_search(片段召回,带文件 + 行范围)+ memory_get(按路径读取内存文件,支持 startLine + lines 参数做精确定位)。
  • 向量检索解决“意思相近”,BM25 解决“精确命中”,混合更稳。
  • 混合合并的默认思想是简单加权:finalScore = vectorWeight * vectorScore + textWeight * textScore(默认权重 0.7/0.3)。
  • 索引存储是可重建的派生数据:每个代理一个 SQLite,默认在 ~/.clawdbot/memory/<agentId>.sqlite
  • 索引会记录嵌入提供程序 + 模型 + 端点指纹 + 分块参数,任何一项变化都会触发自动重建。
  • 除了压缩,还需要会话修剪(session pruning):每次请求前修剪旧工具结果,避免上下文被日志淹没,尤其是 Anthropic 的 TTL 缓存过期场景。
  • 工作区文件(AGENTS.mdSOUL.mdUSER.mdIDENTITY.mdTOOLS.mdHEARTBEAT.md)在每个会话开始时被注入系统提示;大文件会被截断(默认上限 20000 字符)。

01|先把“上下文”与“记忆”拆开:调试时就不会乱

在 Clawdbot 的定义里:

  • 上下文:发送给模型的所有内容(系统提示、对话历史、工具结果、附件),受模型上下文窗口限制。
  • 记忆:存储在磁盘上,后续可重新加载或被检索的内容。

我自己快速检查上下文膨胀时,常用命令是:

  • /status:窗口使用概览与会话设置
  • /context list:注入内容清单与大小
  • /context detail:更细的拆分(文件、技能列表、工具 schema 的开销)
  • /usage tokens:把 token 使用情况附在回复页脚
  • /compact:手动触发压缩

这些命令的共同目标是:让“模型看到了什么”变得可观测,否则记忆系统的优化会变成盲调。

02|文件系统即记忆:两层结构把“流水账”和“长期事实”隔离开

Clawdbot 的内存文件默认是两层:

  • memory/YYYY-MM-DD.md
    • 每日日志,仅追加
    • 会话开始读取今天和昨天
  • MEMORY.md(可选)
    • 精选的长期内存
    • 仅在主私人会话加载,群组上下文永不加载

我自己踩过的坑:在群聊里问助手“我之前的决定是什么”,结果它一无所知——因为 MEMORY.md 根本没被加载。这不是 bug,是安全设计。群聊场景永远不应该注入个人上下文

写入策略同样分层:

  • 决策、偏好、持久事实写入 MEMORY.md
  • 日常运行上下文写入 memory/YYYY-MM-DD.md
  • 出现“记住这个”的指令时,我建议立刻落盘,不要让助手“脑内记住”——“Mental notes” don‘t survive session restarts. Files do.(这句话来自官方 AGENTS.md 模板,我觉得说得很好)

这种拆分的工程意义非常直接:

  • 每日文件允许“低摩擦记录”,我们不追求结构化完美
  • MEMORY.md 保持“小而稳”,更接近“每次都值得读”的核心事实区
  • 群聊不注入 MEMORY.md,避免个人上下文泄露到共享场景

03|“压缩前刷新”是记忆系统的保险丝

长对话不可避免触达上下文上限,压缩也不可避免。

压缩操作本质上,是把旧对话总结为紧凑摘要,并持久化到会话 JSONL 中;最近消息保持完整。压缩是有损的——这不是 Clawdbot 的缺陷,而是所有上下文有限的模型必须面对的物理约束

Clawdbot 在接近自动压缩时,会触发自动内存刷新(压缩前 ping)

  • 先运行一个静默的代理轮次
  • 提醒模型把“应该持久保存”的内容写入内存文件
  • 通常用 NO_REPLY 抑制用户可见输出

对应的配置入口是 agents.defaults.compaction.memoryFlush,示例:

{
  agents: {
    defaults: {
      compaction: {
        reserveTokensFloor: 20000,
        memoryFlush: {
          enabled: true,
          softThresholdTokens: 4000,
          systemPrompt: "Session nearing compaction. Store durable memories now.",
          prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store."
        }
      }
    }
  }
}

几个容易踩坑的边界也在文档里写得很清楚:

  • 触发是“软阈值”策略:当估算 token 超过 contextWindow - reserveTokensFloor - softThresholdTokens 时执行刷新。
  • 每个压缩周期只刷新一次,避免频繁打扰。
  • 工作区必须可写;如果会话在 workspaceAccess: "ro""none" 中运行,会跳过刷新。
  • 静默的实现细节:如果模型回复以 NO_REPLY 开头,Clawdbot 不仅不递送最终消息,还会抑制流式输出,避免用户看到半截内容。

我自己觉得这一段机制,是“长会话不失忆”的关键。

04|记忆检索:我们先把“召回单位”做对

Clawdbot 的记忆检索工具是两段式:

  • memory_search
    • 语义搜索来自 MEMORY.md + memory/**/*.md
    • 返回片段文本(上限约 700 字符)、文件路径、行范围、分数、提供程序/模型信息
    • 不返回完整文件负载,避免把上下文窗口塞满
  • memory_get
    • 读取特定内存 Markdown 文件
    • 支持 startLine + lines 参数做精确定位(而不是一次性拉全文)
    • 拒绝 MEMORY.md / memory/ 之外的路径——这是一条重要的安全边界

这两点看似细节,实际决定了“记忆系统会不会反噬上下文”:

  • 召回单位越大,上下文越容易被污染
  • 召回单位过小,信息缺乏自足性,模型会“脑补”

Clawdbot 的选择是:以 Markdown 块为召回单位,目标约 400 token,带 80 token 重叠,并且返回可追溯引用(文件 + 行范围)。

05|混合搜索:BM25 解决精确命中,向量解决语义改写

向量检索往往输在“高信号精确 token”:

  • ID(例如短哈希 a828e60b3b9895a…
  • 配置路径(memorySearch.query.hybrid
  • 环境变量名
  • 代码符号
  • 具体错误字符串("sqlite-vec unavailable"

Clawdbot 的做法是混合:

  • 向量相似度:语义匹配,措辞不同也能召回
  • BM25 关键词相关性(FTS5):精确 token 更稳

官方文档里有一句话我很喜欢:“Vector search is great at ‘this means the same thing’… But it can be weak at exact, high-signal tokens.”

合并方式强调简单可控。文档给了实现草图:

  1. 两侧各取候选池(向量 + BM25),数量是 maxResults × candidateMultiplier
  2. BM25 rank 转成 0..1 的 textScore
    • textScore = 1 / (1 + max(0, bm25Rank))
  3. 按块 ID 合并候选并加权:
    • finalScore = vectorWeight * vectorScore + textWeight * textScore

配置示例(默认权重 0.7/0.3):

agents: {
  defaults: {
    memorySearch: {
      query: {
        hybrid: {
          enabled: true,
          vectorWeight: 0.7,
          textWeight: 0.3,
          candidateMultiplier: 4
        }
      }
    }
  }
}

还有两个很务实的降级策略:

  • 平台上无法使用全文搜索时,回退到仅向量
  • sqlite-vec 扩展不可用时,回退到进程内余弦相似度(不硬失败)

官方文档对这套策略的定位是:“This isn’t ‘IR-theory perfect’, but it‘s simple, fast, and tends to improve recall/precision on real notes.”——我自己的体感也是这样,能解释、能调参、能降级,比学术完美更重要。

06|索引与提供程序:派生数据可重建,密钥能力要单独算

索引存储是每个代理一个 SQLite,默认路径:

  • ~/.clawdbot/memory/<agentId>.sqlite

索引由文件变更触发标脏(防抖 1.5 秒),同步可以在会话开始、搜索时、或按调度异步运行。

一个很重要的自动化行为:索引会记录嵌入提供程序 + 模型 + 端点指纹 + 分块参数,任何一项变化都会触发自动重建。这意味着我们换个嵌入模型,不需要手动清索引。

向量检索的嵌入提供程序选择也有明确规则:

  1. 如果配置了本地模型路径且存在,优先 local
  2. 如果能解析 OpenAI 密钥,选择 openai
  3. 如果能解析 Gemini 密钥,选择 gemini
  4. 否则禁用内存搜索,直到配置完成

这里有一个容易被忽略的工程点:

  • Codex OAuth 仅覆盖聊天/补全,不覆盖嵌入
  • 远程嵌入仍需要单独的 API Key

对于“离线优先”的场景,本地嵌入的默认模型是 hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf(约 0.6 GB),会自动下载到缓存目录。但 node-llama-cpp 可能需要先跑一次 pnpm approve-builds,否则本地嵌入会回退到远程。

07|压缩之外,还需要会话修剪:别让工具输出吞掉上下文

压缩会把旧对话总结为持久条目。

但还有一类膨胀更隐蔽:工具结果(toolResult)

一次 exec、一次抓取网页、一次读取大文件,可能带来几十 KB 的文本。把这些结果长期留在运行上下文里,会直接导致:

  • 上下文更快触顶
  • 缓存命中下降,TTL 过期后重新缓存更贵
  • “真正重要的消息”被淹没在工具噪音里

Clawdbot 的会话修剪(Session Pruning)做的是一件很明确的事:

  • 每次 LLM 调用之前,从内存上下文里修剪旧工具结果
  • 不会重写磁盘上的会话历史(*.jsonl
  • 仅影响本次请求发送给模型的消息集合

默认启用的策略是 cache-ttl

  • 仅当最后一次 Anthropic 调用早于 ttl 时才运行修剪
  • 目标是缩小 TTL 过期后的第一次请求的 cacheWrite 体积,让重新缓存更便宜

默认行为还区分了两种修剪:

  • 软修剪:只针对过大的工具结果,保留头尾并插入 ...,跳过包含图像块的结果
  • 硬清除:用占位符替换整个工具结果

示例配置(启用 TTL 感知修剪):

{
  agent: {
    contextPruning: { mode: "cache-ttl", ttl: "5m" }
  }
}

默认值也值得一看(启用时):

  • keepLastAssistants: 3(保护最近几条助手消息之前的工具结果不被清除)
  • minPrunableToolChars: 50000
  • softTrim.maxChars: 4000
  • hardClear.placeholder: "[Old tool result content cleared]"

一句话总结这部分:压缩解决“历史太长”,修剪解决“工具太吵”。

图1:Clawdbot 记忆闭环架构(写入、索引、召回、刷新、压缩)

Clawdbot记忆系统流程图:展示了用户消息输入、上下文构建、压缩、会话修剪、记忆刷新、记忆存储与检索的完整闭环

08|我们把作业抄走:一套最小可行的“文件记忆系统”怎么搭

不绑定 Clawdbot,我们也能把核心思想落地。关键是把边界立住,再逐步扩展。这套思路涉及很多系统设计层面的考量,值得仔细推敲。

1)把“记忆文件”做成规范,而不是随手堆文本

  • memory/YYYY-MM-DD.md:仅追加,放运行轨迹与当天上下文
  • MEMORY.md:精选长期事实/偏好/决策,保持小而稳定

我自己的做法是:落盘规则写进“项目规则文件”(类似 AGENTS.md 的位置与触发条件),避免靠临场对话约定。

2)写入时机必须包含“压缩前刷新”

只依赖“用户想起来才让写”是不够的。我自己觉得最少也要有三个触发:

  • 显式“记住这个”
  • 会话接近压缩阈值(预压缩刷新,我建议静默)
  • 会话结束或重置前(把临时线索升级为长期事实,或明确丢弃)

3)检索不要返回整本记忆

召回我建议遵循两条硬约束:

  • 只返回片段(带出处:文件 + 行范围)
  • 只允许读取白名单路径(内存目录之外拒绝)

这既是成本控制,也是安全边界。

4)混合检索优先于“只上向量库”

工程上我们最常见的查询分两类:

  • 自然语言模糊查询(适合向量)
  • 精确 token 命中(适合 BM25/FTS)

混合权重我们不需要复杂,先能解释、能调参、能降级。

5)索引必须是派生数据,可随时重建

索引库坏了就能重建,这是我们“可维护”的底线。

  • canonical:Markdown 文件(可版本化)
  • derived:SQLite/向量索引(可重建)

6)把“工具输出膨胀”当成记忆系统的一部分来治理

长期运行的 Agent,工具输出往往比对话更占窗口。

  • 压缩解决历史
  • 修剪解决噪音
    两者缺一不可。

结尾:记忆系统的目标不是“记得多”,而是“想得起、丢不掉、讲得清”

Clawdbot 这套设计最大的优点,在我看来是把“记忆”从玄学拉回工程:

  • 记忆是文件,用户可读可改
  • 回忆靠工具检索,按需注入片段
  • 长对话靠“压缩前刷新”兜底,重要信息先落盘
  • 工具噪音靠修剪治理,避免上下文被日志吞没

这套路线不追求豪华配置,追求稳定边界与可解释的机制。关于AI Agent和后端架构的更多实践思考,也欢迎到云栈社区与更多开发者交流探讨。




上一篇:干货实战:流量回放技术(TCPCopy+SPAN)如何解决期货交易系统测试痛点
下一篇:开源免费:基于 Webman 框架的 PHP USDT-TRC20 支付系统搭建指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-6 04:56 , Processed in 0.287693 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表