导学阶段-记忆系统短期长期与工作记忆
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
会话一关,Agent 知道的东西就没了。下次开新会话,模型对上次聊过什么一无所知。模型没有状态,每一轮收到的 messages 列表由程序重新拼好,对话结束,这份列表随之释放。
三种记忆,三种寿命
| 记忆类型 | 长什么样 | 存活时间 | 谁管 |
|---|---|---|---|
| 短期记忆 | 本次对话的 messages 列表 | 对话结束即丢弃 | Agent 循环 |
| 长期记忆 | 持久化的事实库 | 跨会话存活 | Memory |
| 工作记忆 | 当前任务用到的精挑片段 | 用完即弃 | ContextManager |
短期记忆就是本次对话的 messages 列表,Agent 循环负责维护。长期记忆存放跨会话仍然成立的内容,比如用户偏好、历史决策、项目规范。工作记忆介于两者之间,是当前任务真正用得上的片段,例如正在修改的那个函数、上一次工具调用的结果,任务做完就没有价值。
工业 Coding Agent 三种都在用。会话历史是短期记忆,当前任务相关的代码片段与上一步工具输出是工作记忆,用户的常用风格、项目的代码规范、装好的 Skills 是长期记忆。
把三者混为一谈,会把两个问题当成一个:上下文一长回答变差,是短期记忆超限,靠压缩策略解决;换一个会话就失忆,是缺长期记忆,要靠检索。
该存什么,不该存什么
一个常见的误判是长期记忆等于把所有历史对话存进数据库。存下来容易,用起来难。会话记录里大部分内容是”好的””继续”这类没有长期价值的回应,真正该留住的是少数几条事实。课程给出的判断很直接:用户偏好、硬约束、重要事实值得存,一次性问答不值得存。
还有一条容易忽略的存储纪律:每条记录只放一个事实。”用户对花粉过敏”和”用户每天预算 500 元”分成两条,不要合成”用户过敏、预算 500、喜欢博物馆”。合成之后,召回要么整条进来、要么整条不进来,问的是过敏,预算约束会被一起带进上下文,或者一起丢掉。
长期记忆的价值在第二次会话才显现。第一次会话用户说”我对花粉过敏”,第二次会话问”帮我安排周末户外”。没有记忆,模型推荐花海公园;有记忆,它先召回”对花粉过敏”这条事实,再避开花季。两次推荐的分叉点在召回这一层,与模型无关。
关键词找不到改写过的说法
| 关键词检索 | 向量检索 | |
|---|---|---|
| 判断依据 | 问题和事实有多少相同的词 | 两个向量方向有多接近 |
| 典型失败 | 查”我有什么过敏症状”找不到”我对花粉敏感” | 依赖 embedding 模型质量 |
| 成本 | 零依赖,毫秒级 | 需要调用 embedding 接口 |
“我有什么过敏症状”和”我对花粉敏感”字面上没有一个实词重合,基于词频的 BM25 一类方法算出来接近零分,召回直接失败。向量检索换了一套判断:把句子编码成一个固定长度的浮点数组,语义相近的句子落在这个空间里相近的位置,再比较两个向量的夹角。
embedding 模型和 chat 模型是两回事。它不对话,只负责编码,接口挂在同一个 base_url 和 api_key 下的 /embeddings 端点。向量长度由模型写死,text-embedding-3-small 是 1536 维,3-large 是 3072 维,BGE-base 与 M3E-base 是 768 维,课程演示脚本使用的 hunyuan-embedding 是 1024 或 1536 维,取决于版本。1536 维的 float32 向量占 6KB,十万条事实就是 600MB。
十行代码的 Memory
把编码、存储、召回串起来,一个能用的记忆类只需要几个方法。
1 | import numpy as np |
add 做两件事:编码,存成文本与向量的一对。search 也做两件事:编码查询,逐条算余弦后排序取前 k。核心逻辑就这些,持久化和元数据都是外围加工。
余弦值的范围是 [-1, 1],越接近 1 越相似。课程把阈值统一定义成 RECALL_THRESHOLD = 0.35,低于它的召回结果不注入 prompt。这个数字必须按 embedding 模型校准,不能照抄流传的 0.6 或 0.7:课程使用的 hunyuan-embedding 上,同主题文本实测落在 0.18 到 0.41,完全无关的落在 0.08 到 0.23。0.35 取在无关项上沿之上、同主题上沿之下。换一个 embedding 模型,整条分数分布会平移,阈值要重新抽样确定。
读在对话前,写在对话后
记忆和 Agent 的协作是一个读写闭环。读的环节在每轮对话开始前:用当前问题去记忆库里检索,相似度低于阈值的丢弃,剩下的拼进 system prompt。
1 | RECALL_THRESHOLD = 0.35 # 按 embedding 模型校准,课程所用模型同主题实测 0.18 到 0.41 |
模型本身没有变化,它只是看到了更多上下文。写的环节在对话结束后:用 LLM 从本轮对话里提炼值得长期记的事实,每行一条,逐条写入记忆库。提炼提示里要明确说清不要把一次性问答存进去,否则”刚才说了什么”这类临时内容会污染记忆库。
与 ContextManager 的分工也因此清晰:Memory 决定哪些长期事实进入 system prompt,ContextManager 决定本轮 messages 怎么压缩。压缩是有损的,被压掉的内容当轮就找不回来;事实库是持久的,这一轮没召回到,下一轮换个问法还有机会。
最小实现离生产还有距离。numpy 逐条算余弦是 O(n),上万条事实之后每轮召回都变慢,换成 FAISS、Qdrant 这类带 ANN 索引的向量库可以把检索降到近似对数级。同一段文本重复编码浪费成本,可以按文本 hash 缓存。用户偏好会随时间变化,老记忆应当衰减权重。多用户场景下每条事实还要绑定 user_id,否则张三的花粉过敏会被召回给李四。
这些技巧最终都落在两个动作上:把值得留下的事实写成一条可检索的记录,需要时按语义取回来。模型没有因此长出新能力,它只是每一轮都看到了更完整的那段上下文。
上下文和工具凑齐之后,下一个问题是如何把这种能力收窄到写代码。下一节从 Coding Agent 的五个工作阶段说起。


