大模型的记忆

一、先搞清楚一个关键前提:大模型本身是没有记忆的

很多同学第一次做 AI 应用时会很困惑:"我明明和 ChatGPT 聊了半天,它怎么还记得我上一轮说的话?"

答案是:它其实不记得,是开发者在"假装"让它记得。

大模型(LLM)本质上是一个"无状态"的计算工具。你给它一段文字,它就根据这段文字推理并输出结果。它不像人类有海量长期的神经记忆,它唯一"看到"的东西,就是你这次请求里喂给它的所有文字

所谓"多轮对话",真相是这样的:当你在聊天里说第一句话时,AI 回答;当你再说第二句时,开发者把第一句 + 你的第二句 一起发给模型,模型"看起来"像记得上一句。当你说第十句时,开发者把前九句 + 第十句全部塞给模型。记忆,其实是靠"不停地重发历史"来维持的。

那么问题来了:如果对话成千上万句,每次全部重发,会发生什么?


二、两个绕不开的瓶颈:token 和上下文窗口

要让模型理解"历史",得把历史文字发过去。但这些文字不是免费的,也不是无限的。

1. 什么是 token?

模型不按"字"计费,而是按 token(词元) 计。中文里一个 token 大约对应一个字或半个字,英文大约对应一个词的一部分。模型接收、生成、计费、计算时间,全看 token 数量。

你可以把 token 想象成"模型的容量货币"------你带多少货,就要付多少钱,而且车厢就那么大。

2. 什么是上下文窗口?

每个模型都有一个上下文窗口(context window)上限,比如某些模型是 4k、8k、128k token。意思是"我一次最多只能装这么多 token"。超过这个数,模型要么直接报错,要么悄悄忽略最前面的内容------于是你最早说的话就被它"忘"了。

所以矛盾出现了: 用户希望 AI 永远记得全部历史,但模型的车厢就那么点大。历史越滚越多,总会有放不下的一天。

针对这个矛盾,业界摸索出三个手段:截断、总结、检索 。恰好,src/memory 目录下的每个文件就对应其中一个手段。下面我们一个个拆开。


三、手段一:截断(Truncation)------装不下就扔掉最旧的

1. 思路

最简单粗暴的方法:历史太多?那我只留最近的,前面的直接丢掉。 就像你手机缓存满了,清理掉最老的聊天记录,只留最近的。

truncation-memory.mjs 文件演示了两种截断方式。

2. 方式一:按"消息条数"截断

代码里先造了 8 条对话(用户和 AI 一来一回),然后:

js 复制代码
const maxMessages = 4;
const trimmedMessages = allMessages.slice(-maxMessages);

slice(-4) 表示"从数组末尾往前数 4 条",也就是只保留最近 4 条消息。打印出来只剩最后 4 条,最早的 4 条被无情地抛弃了。

这种写法最简单,但有个很大的缺陷:它只数"条数",不看"每条有多长"。 如果每条消息都特别长,哪怕只留 4 条,也可能把上下文窗口撑爆。所以它只能用来"演示思路",生产环境很少直接用。

3. 方式二:按"token 数量"精确截断(更专业)

更专业的做法是:设定一个 token 预算(比如 100),只保留加起来的 token 总和不超过预算的那几条消息。 这就要用 LangChain 自带的 trimMessages 工具:

js 复制代码
const trimmedMessages = await trimMessages(allMessages, {
  maxTokens: 100,
  tokenCounter: async (msgs) => countTokens(msgs, enc),
  strategy: "last",
});

这里的关键是 tokenCounter------它要精确算出每条消息占多少 token。前面说了,token 的切分规则是模型决定的,不能靠数"字数"来猜,必须借助专业工具。所以文件顶部引入了:

js 复制代码
import { getEncoding } from "js-tiktoken";

并且自己写了一个 countTokens 函数,用 js-tiktoken 的编码器逐条计算:

js 复制代码
function countTokens(msgs, enc) {
  let total = 0;
  for (const msg of msgs) {
    const content =
      typeof msg.content === "string"
        ? msg.content
        : JSON.stringify(msg.content);
    total += enc.encode(content).length;
  }
  return total;
}

enc.encode(content).length 就是把一段文字编码成 token,然后数一数有多少个。strategy: "last" 表示"从最新、最靠近结尾的开始保留"------因为通常越新的对话对当前问题越重要。

4. 截断的优缺点

  • 优点:实现简单、速度快、几乎不需要额外调用模型、零成本。
  • 缺点会永久丢失信息。比如用户第一轮就说了"我是设计师",结果被截断后,AI 彻底忘了这件事。截断是"扔掉",而不是"消化",属于比较"笨"的办法。

四、手段二:总结(Summarization)------丢掉细节,留下大意

1. 思路

"直接把旧消息扔了太可惜,能不能让 AI 帮我把很长的旧对话压缩成一句摘要,只保留核心信息?"这就是总结。

打个比方:你读一本厚书,读完后在扉页写一句"这本书讲了什么"。之后就算不再翻书,只要看一眼摘要,就能回忆个大概。总结就是给旧对话写"摘要"。

summarization-memory.mjssummarization-memory2.mjs 都是这一思路。

2. 流程详解

先看 summarization-memory.mjs 的主流程:

js 复制代码
if (allMessages.length > maxMessages) {
  // 消息太多,触发总结
  const keepRecent = 2; // 保留最近 2 条
  const recentMessages = allMessages.slice(-keepRecent); // 最近的要留
  const messagesToSummarize = allMessages.slice(0, -keepRecent); // 旧的拿去总结

  const summary = await summarizeHistory(messagesToSummarize); // 让 AI 总结旧消息

  await history.clear(); // 清空历史
  for (const msg of recentMessages) {
    await history.addMessage(msg); // 放回保留的最近消息
  }
  await history.addMessage(new AIMessage(summary)); // 追加总结
}

注意到最后的顺序是:清空 → 放回最近几条 → 加一条总结消息。

3. 总结的"核心魔法":summarizeHistory 函数

最关键的逻辑在 summarizeHistory

js 复制代码
async function summarizeHistory(messages) {
  if (messages.length === 0) return "";
  const conversationText = getBufferString(messages, "用户", "助手"); // ① 转成字符串
  const summarPrompt = `请总结以下对话的核心内容,保留重要信息: 
  ${conversationText}
  总结:
  `;
  const summaryResponse = await model.invoke([new SystemMessage(summarPrompt)]); // ② 让 AI 总结
  return summaryResponse.content;
}

这里有两个重点:

getBufferString(messages, "用户", "助手") :把"一条条的消息对象"拼接成一段可读的对话文本。本来消息是 {human: "我叫李四"} 这样的结构,getBufferString 会把它们变成类似:

makefile 复制代码
用户: 我叫李四
助手: 你好李四...
用户: 我是一名设计师

只有变成这样的文本,模型才看得懂"这是谁说的"。

model.invoke([new SystemMessage(summarPrompt)]) :把拼好的对话,连同"请总结"的指令,一起发给大模型,让它输出一段摘要。这里的 SystemMessage(系统消息)作用是给模型一个"角色的设定或指令",相当于告诉它"你是总结助手,请执行这个任务"。

4. 升级版:按 token 预算驱动总结

summarization-memory2.mjs 则更进一步,不是按"条数"判断,而是按 token 总量判断:

js 复制代码
const maxTokens = 200;       // 总 token 超过 200 就触发总结
const KeppRecentToekns = 80; // 保住最近约 80 token,其余拿去总结
if (totalTokens >= maxTokens) {
  // 从最后往前收集,直到凑够 80 token 为止,剩下的全拿去总结
  ...
  const summary = await summarizeHistory(messagesToSummarize);
  await history.clear();
  for (const msg of recentMessages) await history.addMessage(msg);
  await history.addMessage(new AIMessage(summary));
}

代码里是从最后一条消息开始往前数,一条一条累加 token,直到达到 KeppRecentToekns,就把这"最靠后的几条"留下来,而更靠前的全部交给 AI 总结。这样既保证"最近的细节还在",又控制了整体 token 花销。

5. 总结的优缺点

  • 优点 :比截断聪明,保留了大意、丢掉了细节,省 token,信息保真度更高。
  • 缺点 :每次总结都要额外调用一次模型 ,有延迟、要花钱;而且总结会丢失细节------万一用户之后追问"我第一轮说过什么",AI 可能答不上来。

五、手段三:检索(Retrieval)------最智能的"查资料"

1. 思路

前面两种都是"被动地省空间",而检索是另一种更聪明的思路:我不把全部历史都塞给模型,而是每次用户提问时,只从海量历史里"找出最相关的那几段"发给模型。

就像你有一个塞满笔记本的书架,用户问"我的职业是什么?",你不会把全部笔记本翻开,而是直接抽出记着"职业"的那一本。这就是检索。

2. 前提:怎么让机器知道两段话"相关"?------Embedding(嵌入)

机器不认识文字,但它认识数字。Embedding(嵌入) 的作用,就是把一段文字变成一串数字(一个向量),比如 [0.12, -0.87, ···, 0.33]

神奇的地方在于:意思越接近的两句话,它们的向量在"抽象空间"里就越靠近。 比如"我是设计师"和"我擅长 UI/UX"会比较近,而和"我今天吃了火锅"会比较远。这样,靠"数值距离"就能判断两段文字语义相不相关。

3. 把向量存到哪里?------向量数据库 Milvus

标量库(普通数据库)擅长按条件精确查找,但不擅长"找最相似的"。所以出现了专门的 向量数据库 ,本项目用的是 Milvus

milvus-docker 目录里有一个 readme.md,只写了一句话,但它点出了关键:"由多个 image 组成的 docker-compose 文件,编排多个 image 工作流。" 也就是说,Milvus 不是孤零零一个服务,而是由多个容器协作运行的。点开 milvus-standalone-docker-compose.yml 能看到它的组成部分:

  • etcd:存储和协调"元数据"(相当于管家,登记各种信息)。
  • minio:存储大文件、快照(相当于仓库)。
  • standalone:Milvus 主服务本体,对外开放向量检索接口,端口是 19530。

用一份 docker-compose 配置文件,就能把这些服务一次性打包启动,避免一个个手动跑。

4. 往 Milvus 里"灌数据":insert-conversations.mjs

insert-conversations.mjs 教你怎么建"表"(专业术语叫集合 collection)并插入数据:

js 复制代码
await client.createCollection({
  collection_name: "conversations",
  fields: [id, vector, messages, round, timestamp],
});

可以理解为:建一张叫 conversations 的表,里面有几个字段,其中 vector 专门存这段对话的向量(比如 1024 维),messages 存这段对话的原文,round 存轮次,timestamp 存时间。建完表后,把每条对话"转成向量 + 存进表里",这样以后就能搜了。

5. 查询:retrieval-memory.mjs

retrieval-memory.mjs 是检索的"取数"一端,核心步骤是:

js 复制代码
const queryVector = await getEmbedding(query); // ① 用户问题 -> 向量
const searchResult = await client.search({
  // ② 去向量库找最相似的
  collection_name: "conversations",
  vector: queryVector,
  limit: 2, // 只取最相关的 2 条
  metric_type: MetricType.COSINE, // 用余弦相似度衡量"多像"
  output_fields: ["id", "content", "round", "timestamp"],
});

① 先把"用户当前问题"也转成向量(用同一个模型),② 然后用这个向量去 Milvus 里搜,找出和它最相似的前 2 条历史对话metric_type: COSINE 表示用余弦相似度来衡量两个向量有多接近(越接近越相关)。找出这 2 条后,再把它们拼进发给模型的内容里。

6. 检索的优缺点

  • 优点只取最相关的几段,省 token、效率高,即便历史有几十万条也能处理;是"长期记忆"的主流方案。
  • 缺点 :工程复杂------要部署向量库、要维护 embedding、要调检索条数;而且"相关"是从语义上判定的,可能漏掉一些字面上不像、但实际很关键的信息

六、三种手段到底怎么选?------不是三选一,而是组合

很多人误以为截断、总结、检索是"三选一"。实际上,成熟项目往往三者组合使用,各司其职:

  • 检索:负责从海量历史里,精准挑出"和当前问题最相关的知识"。
  • 总结:负责把太远、太零散的旧对话,压成"摘要",保留大意。
  • 截断:负责"最后兜底",确保无论怎样 token 都绝不超上限。

一个典型 Agent 的完整流程大概是:

  1. 用户发来一条消息。
  2. 检索:去向量库找出和这条消息最相关的几条历史。
  3. 总结:如果历史总量还是偏大,就把更早的内容压成摘要。
  4. 最后截断:若还超预算,就只留最新最关键的,硬性兜底。
  5. 拼装成"系统指令 + 处理后的历史 + 当前问题"发给模型,拿到回答。
  6. 把这次新对话写回记忆库,供下次检索使用。

可以想象成一个图书管理员:先帮你精准找出 要参考的那几页(检索),对太厚的书先让你看摘要 (总结),实在放不下再帮你撕掉最不重要的几页(截断)。


七、最容易踩的坑(重点)

学习这块内容,除了懂原理,更要记住几个实践中的坑:

  1. 别把所有历史一股脑塞给模型:新手常犯的错,结果要么 token 爆炸、要么干脆报错、要么模型"失忆"。必须做管理。
  2. token 估算要准确 :不同模型分词规则不同,不能数"字数",要用 js-tiktoken 这类工具。算不准,截断就会出错。
  3. 只用截断会丢关键信息:早期聊到的用户身份、偏好可能被永远丢掉。重要场景要配合总结或检索。
  4. 总结有额外成本:每总结一次就调一次模型,有延迟、有开销,别无脑每轮都总结。
  5. 检索的"相关性"不完美limit 设太小会漏关键历史,设太大会浪费 token。
  6. 向量维度必须统一:插入和查询必须用同一个 embedding 模型、同一个维度(比如 1024),否则向量对不上,检索直接失效。
  7. Milvus 是多容器协作:etcd、minio、standalone 要一起用 docker-compose 整体启停,单独关藏任何一个都可能出问题。

八、动手跑一跑(推荐)

纸上得来终觉浅,建议你按顺序跑一遍,直观感受三种手段:

  1. 截断 :运行 truncation-memory.mjs,修改 maxMessagesmaxTokens,观察"只留最近"的效果。
  2. 总结 :运行 summarization-memory.mjs,看旧对话如何变成一条摘要。
  3. 检索 :先 docker-compose up -d 启动 Milvus,再依次运行 insert-conversations.mjs(灌数据)、retrieval-memory.mjs(查询),看模型基于向量相似度命中相关历史。

小提醒:运行前先确认 .env 里配好了 API Key;insert-conversations.mjs 会建集合并插入数据,别重复插入以免数据冗余。

相关推荐
刘孬孬沉迷学习1 小时前
AI音频领域完整调研:任务、算法、大模型研究全梳理
人工智能·算法·音视频
玫瑰互动GEO1 小时前
工程视角看GEO优化与SEO优化:爬虫排序 vs 大模型引用
人工智能·爬虫·搜索引擎
掘金安东尼1 小时前
WorkBuddy 开放平台详解:五大核心能力、API 接入与竞品定位
人工智能
2601_962056231 小时前
EasyMarkets易信:比特币基金流入创年内新高
人工智能·mysql
云物互联1 小时前
DeepSeek Harness 架构解析
人工智能
云物互联1 小时前
Claude Agent SDK 架构解析
人工智能
2601_962056231 小时前
EasyMarkets:“网络安全需求持续升温”
数据库·人工智能
AI工具测评家1 小时前
降AI工具背后的技术原理:语义重构、风格迁移与轻量化打散,哪种才能稳过知网2026检测?
人工智能·ai写作·降重·ai检测·查重·降ai
用户5274675614211 小时前
别把“不让 Agent 读文件”当成权限系统
人工智能