项目的一次验收记录里,新会话收到的第一句话是:
我们之前对技能系统做过什么改造?
模型回答了技能目录、Skill 工具和注入防护。当前会话没有之前的聊天记录,这些信息来自先前保存的事件摘要:系统在任务开始时找到了相关记录,把它们放进了这次请求的上下文。
要让这条链路成立,前一次对话得留下可用的内容,新任务也得有办法找到它。mineclaude 为此增加了用户画像和情景记录:前者保存可能影响后续交互的用户知识,后者保存过去做过的事。它们与当前会话的消息历史一起构成本文讨论的三层记忆。
画像和情景的读写方式并不相同。画像在成功任务结束后更新,下次任务开始时完整加载;情景在会话结束时提炼,下次按当前目标检索。下面先从这些信息原来存在哪里讲起,再沿着保存和读取的过程展开。
会话历史之外,哪些内容需要留下来
早期版本已经有消息历史、会话笔记和手写背景。消息历史支持当前对话延续,长度超出预算后会经过压缩;模型可以用 notes.md 记录会话内的工作信息,用户也可以把长期背景写进 context.md。这些内容并非完全没有保存下来,只是新会话不会自动继承旧会话的笔记,也没有一个流程把普通对话中的用户知识和事件整理成跨会话记录。
例如,用户在对话中说“我喜欢简洁直接的回答”,这项偏好可能对后续任务仍然有用;而“我们给技能系统增加了按需加载”,描述的是一次项目经历。前者适合并入用户画像,后者适合保留为一条可检索事件。如果把二者都长期追加到固定背景中,每次任务就会同时带上越来越多未必相关的往事。
新的分工保留了消息历史,另外增加用户画像和事件存储。它们分别承担不同的信息用途:
| 信息对象 | 保留什么 | 后续任务怎样使用 |
|---|---|---|
| 当前会话的消息历史 | 对话及工具交互,超出预算后经过压缩 | 支持同一会话继续;不等于自动带入新会话 |
用户画像 profile.md | 长度受限、持续更新的用户知识 | 每次 run 启动时完整读取 |
情景记录 episodes.sqlite3 | 多条独立的历史事件摘要 | 按当前目标选少量相关记录 |
画像仍然常驻,因此并不是所有长期记忆都改成了按需检索。
NOTE
run 与会话并不等长。 这里一次 run 指用户提交一个目标后,Agent 为完成它进行的模型请求和工具调用。一个聊天会话可以包含多次 run;一次性会话则在这一轮任务结束后关闭。画像按任务更新,聊天中的事件等到会话关闭才提炼。

图中写回的是后续任务可以读取的记录。当前 run 已经加载的画像和召回结果保持不变,不会因为磁盘内容更新而在运行中途重新组装。
成功任务结束后,更新用户画像
画像更新接在会话管理器 send_message 的收尾阶段。主任务成功、存在最终文本,并且配置了模型提供方和画像路径时,系统会读取旧画像,再把旧画像、这次用户原话和最终回答交给模型,生成更新后的内容。
提炼输入没有包含这次运行的全部工具调用与中间消息。这样可以减少输入量,也意味着只出现在中途、没有进入最终回答的信息可能被漏掉。最终回答是否充分概括了过程,会直接影响画像能学到什么,不能仅凭“已经有结论”就认为遗漏风险消失了。
提炼提示词要求保留偏好、习惯、环境和长期项目等相对耐久的信息,略过一次性任务细节;新观察与旧条目冲突时,采用较新的内容并记录变更日期。项目记录中的一个例子是先要求中文回复,随后改为英文,画像最终记下了新的语言偏好。这个记录说明那次更新按预期发生,不意味着其他类型的冲突也已得到充分验证。
内容筛选主要依赖模型遵循提示词,长度和是否写盘则有程序判断。update_profile 对返回文本做了以下检查:
# profile.py:处理模型返回的画像文本
new = response.text.strip()
if not new or new == old_profile.strip():
return None
if len(new) > PROFILE_LIMIT:
log.warning("profile update over limit (%d > %d), keeping old", len(new), PROFILE_LIMIT)
return None
return new
PROFILE_LIMIT 为 4000 字符。内容为空、与旧画像相同或超过上限时,调用方不会用它覆盖文件。超长结果会被整份拒绝,而不是截短后保存;没有新内容时也不重新写盘。
这次提炼是主任务之后被等待的一次模型调用,使用传入的模型提供方,并没有单独指定一个更小的模型,也不是脱离请求运行的后台队列。提炼或保存异常会被捕获并记日志,不改变已经得到的主任务结果,但仍可能增加收尾耗时。一次性任务目前也会经过这段画像更新逻辑。
实现记录:一次测试为什么写进了真实家目录
项目记录中,一个使用假模型提供方的测试经过画像更新的收尾路径,把假摘要写进了真实家目录。后来会话管理器将路径参数默认设为 None,未传入路径时跳过写入,由应用组装时提供真实位置。这是当前实现控制写入副作用的方式,并不能推出测试环境中的所有磁盘写入都已隔离。
notes.md 和画像仍可能记录相似信息。它们分别服务于会话内工作和跨会话使用,但职责不同并不自动消除重复;如果同一事实在两处更新不一致,仍需要进一步处理。当前设计也没有把会话笔记整体转换成画像。
会话关闭时,把经历提炼成事件
用户画像回答的是用户偏好什么、使用什么环境;情景记录描述的是一次任务做了什么、结果怎样。聊天会话在 close() 时触发事件提炼,一次性会话则在 run 结束时调用同一个保存流程。两者都需要已经配置模型、事件存储和向量提供方。
系统从会话记录中取第一条用户文本和最后一条助手文本,分别作为目标与结果,再生成一条不超过 600 字符的事件摘要。提示词允许保留可复用的经验或决定;如果内容只是闲聊或没有后续价值的一次性问答,则可以返回 SKIP,不写入数据库。空结果、调用异常和超长结果也不会保存。
把两种提炼放在一起,可以看到它们读取的范围:

两条路径都没有直接传入完整的工具调用和中间消息。因此,一次失败任务可以留下事件,但“试了哪些方法、为什么失败”只有进入最终文本后,才有机会被提炼。省略过程是当前实现对输入范围的选择,不能解释成失败过程本身不值得保存。
每条事件连同会话标识、run 标识、创建时间和向量保存在独立的 SQLite 数据库中。保存时有几项明确约束:
- 同一组会话与 run 标识不会重复插入。
- 库中最多保留 200 条;写入后超过容量,按记录编号淘汰最旧的内容。
它不是无限增长的历史档案,也不是完整聊天记录的副本。
事件库与执行持久化库分开。前者保存模型提炼的派生内容,后者记录任务执行与恢复所需的状态;事件检索或提炼失败只记录日志,不应被当作主任务执行失败。分开存放也使这项记忆功能不必直接向执行库增加表结构。不过,能否重新生成一条丢失的事件,仍取决于原会话是否保留了足够信息,不能保证重新提炼会得到完全相同的文本。
新任务开始时,读取画像并选择相关事件
下一次 run 启动时,运行时读取画像快照,并以当前目标查询事件库。组装后的系统提示词依次包含基础规则、全局背景、项目背景、用户画像、相关事件、会话笔记和技能目录。没有画像内容就省略画像段落;没有相关事件就省略事件段落。消息历史仍通过消息列表提供,不被这些长期记录替代。
因此,新会话即使没有旧聊天记录,也可能获得先前保存的偏好与经历。开头的技能改造问答走的就是这条路径:过去的会话先生成事件,新目标再召回该事件,模型据此回答。它是否答得准确,既取决于事件是否被选中,也取决于先前摘要是否准确。
当前事件检索用的是本地哈希词袋向量。英文按词、中文按单字提取 token,再映射到 256 维向量中带符号累加,归一化后计算余弦相似度。这种方式不需要调用嵌入服务,同样文本会得到同样向量,主要反映词面重叠;它没有提供语义嵌入模型那样的同义表达能力。
在最多 200 条记录的规模下,实现直接取出全部向量、逐条计算相似度,没有建立专门的向量索引。筛选过程如下:

这里的 top-3 是数量上限,不是需要填满的名额。达到阈值的记录不足三条,就少返回一些;没有合格记录时,不添加事件段落。
阈值的选择来自一次内部校准。项目记录显示,初始值 0.08 下,四条无关查询都召回了不该出现的记录;当时无关查询对全库的最高相似度为 0.114,相关查询对目标记录的最低相似度为 0.378,于是改用 0.2,随后记录中的两组检查通过。它支持的是这组样本和当前哈希表示下的选择,并没有覆盖用户换一种说法后的召回质量。
向量生成通过 EmbeddingProvider 协议与存储、搜索代码分开,后续可以接入其他实现。
TIP
更换向量实现时,需要一起处理存量向量重建和阈值重新校准。事件库保存了旧表示下的向量,仅替换生成查询向量的对象不足以完成迁移。
画像与事件召回都发生在 run 开始时。任务中途改变方向,不会自动重新检索;召回异常会被捕获并记日志,任务在没有这部分事件内容的情况下继续。由此也能看到当前方案的取舍:它可以在开始时补充背景,却还没有按任务进展持续更新相关记忆。
这些内容怎样影响提示词缓存
画像与召回事件都进入系统提示词,内容变化可能改变后续请求能够复用的前缀。画像更新没有改变当前 run 已加载的快照,影响首先出现在下一次任务;事件内容则随新目标的检索结果变化,即使画像没变,系统提示词也可能不同。
同一 run 中的多次请求,与跨 run 的重新读取,发生在不同时间:

当前请求代码给系统文本和最近最多三条历史消息添加缓存标记。按前缀复用来分析,位于系统文本之后的历史前缀也会受到前面内容变化的影响;同一 run 内保持快照稳定,至少不会因重新读取画像而反复引入这类变化。
NOTE
当前中转端点是否兑现缓存标记、实际产生多少命中和费用变化,项目记录并未核实。代码中存在标记,不足以说明影响很小,或只会多付某一次请求的费用。
把固定背景和动态内容拆成不同块是一项尚未采用的优化方向,收益还需要结合实际请求和计费数据判断。
已有验收记录与接下来的验证
项目记录包含画像写入、偏好更新、新会话读取用户信息,以及事件提炼和相关召回的验收现象。相关脚本也能在仓库中找到:e2e_profile.py 检查对话后的画像与后续使用,e2e_episodes.py 先结束一个会话,再从新的一次性任务提问。这些是记录中的结果和可检查的验收路径,本文没有把它们重新执行一遍。
现有召回记录中的 4/4 命中,查询与目标事件共享了较多字面词。对于本地哈希实现,这样的测试可以检查保存、检索、注入和回答的链路,却不足以判断改写查询后的表现。还需要一组保留语义、改变措辞的查询,观察同一事件是否仍能被找到。
另一项尚未测量的是记忆对无关任务的影响。相关内容被召回,不等于它一定帮助回答;过期偏好、不准确摘要或相似但不同的旧任务,也可能改变模型判断。可以用相同任务分别开启和关闭记忆注入,对照原本答对、注入后答错的情况。这仍是一项待做的实验,而非当前方案已经具备的质量保证。
验收过程本身也曾影响判断。项目记录提到,客户端在启动读循环前等待依赖该循环分发的响应,导致等待无法结束;另一处拒答检查只识别中文关键词,将英文的正确拒答判成失败。修正这类问题是为了让测试结果对应模型行为,不能直接计作记忆能力的提升。
目前也没有 /remember 这样的手动写入入口。模型漏记了用户希望保留的内容时,现有自动提炼流程还缺少直接的补充通道。一次性任务是否都值得承担画像提炼的额外调用,也需要结合实际使用来决定。
开头那种新会话中的回答,说明保存下来的事件有机会跨过会话边界,重新进入当前任务。但它仍是过去对话的摘要。如果问题变成“仓库现在是否已经实现这些改动”,回答还需要当前代码或执行记录的支持。接下来要验证的,是这份背景在措辞变化、事实更新和无关任务中能否继续被正确使用。