LangChain.js Agent Memory 实战(上):从内存对话、文件持久化到截断与摘要

LangChain.js Agent Memory 实战(上):从内存对话、文件持久化到截断与摘要

大模型本身是无状态的。

第一次请求时告诉模型"我叫李四",第二次只问"我擅长什么",如果第二次请求里没有再次携带第一轮内容,模型并不会天然记得"李四"是谁。我们平时感受到的连续对话,本质上是应用程序保存了历史消息,并在下一次调用模型时把这些消息重新放进上下文。

如果把 Agent 简化成下面这个结构:

text 复制代码
Agent = LLM + Harness(Tool + RAG + Memory + ...)

那么 Memory 就是 Harness 中负责"记住并管理过去信息"的部分。Tool 的执行结果、RAG 检索到的资料、用户与 AI 的往来消息,最终都可能进入模型上下文,因此也都会受到上下文窗口和调用成本的约束。

本文使用 LangChain.js,从最简单的内存消息历史开始,逐步实现:

  1. InMemoryChatMessageHistory 维护多轮对话;
  2. FileSystemChatMessageHistory 持久化并恢复会话;
  3. 按消息条数和 Token 数截断上下文;
  4. 把较早的对话总结成摘要,同时保留最近消息。

下篇再把这条路线延伸到 Milvus:不再把全部历史塞给模型,而是按照当前问题检索相关记忆。

一、先建立 Memory 的整体认识

Memory 可以从两个维度理解。

1. 存储逻辑:消息放在哪里

  • 内存:读写直接,但进程退出后数据消失;
  • 文件:可以跨进程恢复,适合演示持久化;
  • 数据库:适合保存更多长期数据,并支持后续查询或检索。

2. 管理逻辑:哪些消息进入上下文

  • 截断:只保留最近若干条消息;
  • 总结:把较早消息压缩成一段摘要;
  • 检索:根据当前问题找出语义相关的历史消息。

存储和管理不是一回事。把所有消息保存到文件或数据库,并不代表每次都应该把它们全部发送给模型。可以完整保存长期历史,但每次只选择其中一部分作为当前上下文。

从使用时间上还可以把记忆分成两类:

  • 短期记忆:当前会话中最近的消息;
  • 长期记忆:跨会话保存的文件、摘要或向量数据库记录。

Agent 在 ReAct 执行过程中同样需要持续维护 messages:用户输入、AI 返回以及工具执行结果会沿着执行流程不断追加,Memory 则负责这些消息的保存与取用。本文的示例集中在用户和 AI 对话;当 Agent 调用工具时,工具结果还可以表示为 ToolMessage,管理原则并没有改变。

本文先解决短期记忆及其压缩问题。

二、准备模型和环境变量

示例采用 ESM 模块,主要依赖如下:

json 复制代码
{
  "dependencies": {
    "@langchain/community": "^1.1.29",
    "@langchain/core": "^1.2.11",
    "@langchain/openai": "^1.5.13",
    "dotenv": "^17.4.2",
    "js-tiktoken": "^1.0.21"
  }
}

环境变量至少需要提供模型名称、API Key 和服务地址:

dotenv 复制代码
MODEL_NAME=你的聊天模型名称
OPENAI_API_KEY=你的API-Key
OPENAI_BASE_URL=你的模型服务地址

通过 dotenv/config 加载变量,再创建一个温度为 0 的聊天模型:

js 复制代码
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

后面的示例都会复用这段配置。

三、消息不是普通字符串:认识三种角色

LangChain.js 用消息对象表示一次对话中的不同角色:

js 复制代码
import {
  HumanMessage,
  AIMessage,
  SystemMessage,
} from "@langchain/core/messages";
  • SystemMessage:定义助手身份和总体行为;
  • HumanMessage:用户发送的内容;
  • AIMessage:模型返回的内容。

例如:

js 复制代码
const systemMessage = new SystemMessage(
  "你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧"
);

const userMessage = new HumanMessage("你今天吃的什么?");

消息对象不只有 content。从文件恢复的 AI 消息还可能带有模型名称、Token 用量、结束原因等响应元数据。不过在构造对话上下文时,最关键的仍然是消息角色、顺序和正文。

四、第一版 Memory:在内存中维护消息历史

InMemoryChatMessageHistory 把原本需要手动管理的消息数组封装成一个历史记录对象。它提供三个最常用的操作:

  • addMessage(message):添加一条消息;
  • getMessages():按顺序取出全部消息;
  • clear():清空历史。

下面实现两轮连续对话。第二轮只问"好吃吗?",如果模型能够理解它在追问第一轮内容,就说明历史上下文已经生效。

js 复制代码
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

async function inMemoryDemo() {
  const history = new InMemoryChatMessageHistory();

  const systemMessage = new SystemMessage(
    "你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧"
  );

  console.log("[第 1 轮对话]");
  const userMessage1 = new HumanMessage("你今天吃的什么?");
  await history.addMessage(userMessage1);

  const messages1 = [
    systemMessage,
    ...(await history.getMessages()),
  ];
  const response1 = await model.invoke(messages1);
  console.log(`助手:${response1.content}\n`);

  await history.addMessage(response1);

  console.log("[第 2 轮对话]");
  const userMessage2 = new HumanMessage("好吃吗?");
  await history.addMessage(userMessage2);

  const messages2 = [
    systemMessage,
    ...(await history.getMessages()),
  ];
  const response2 = await model.invoke(messages2);
  console.log(`助手:${response2.content}\n`);

  await history.addMessage(response2);

  const allMessages = await history.getMessages();
  console.log(`共保存 ${allMessages.length} 条消息`);

  allMessages.forEach((message, index) => {
    const prefix = message.type === "human" ? "用户" : "助手";
    console.log(
      `${index + 1}.[${prefix}]:${message.content.substring(0, 50)}...`
    );
  });
}

inMemoryDemo()
  .catch(console.error)
  .finally(() => console.log("done"));

1. 一轮调用到底发生了什么

每一轮都遵循相同流程:

text 复制代码
创建 HumanMessage
        ↓
写入 history
        ↓
SystemMessage + history 中的全部消息
        ↓
model.invoke(messages)
        ↓
得到 AIMessage
        ↓
把 AIMessage 写回 history

最容易漏掉的是最后一步。model.invoke() 只返回本轮的 AIMessage,不会自动替应用维护历史。如果没有执行 history.addMessage(response),下一轮上下文里就只有用户消息,没有上一轮的 AI 回答。

2. 为什么 SystemMessage 没有放进 history

这里把系统消息作为固定配置,每次调用时临时放在消息数组最前面:

js 复制代码
const messages = [systemMessage, ...(await history.getMessages())];

因此,history 只维护用户与助手的往来内容,系统角色不会被重复追加。

3. 消息条数不等于对话轮数

两轮完整对话通常包含四条消息:

text 复制代码
HumanMessage → AIMessage → HumanMessage → AIMessage

所以 allMessages.length 表示的是消息条数 ,不是轮数。完整问答成对保存时,轮数才可以近似理解为 allMessages.length / 2。一旦加入工具消息、系统摘要或不完整问答,这种除以二的算法也不再成立。

4. 这一版的边界

它已经让模型拥有连续对话能力,但历史只存在于当前 Node.js 进程中。应用停止后重新启动,内存对象会被重新创建,过去的消息随之消失。

五、把消息持久化到 JSON 文件

要跨进程恢复历史,可以把 InMemoryChatMessageHistory 换成 FileSystemChatMessageHistory

js 复制代码
import { FileSystemChatMessageHistory }
  from "@langchain/community/stores/message/file_system";
import path from "node:path";

const filePath = path.join(process.cwd(), "chat_history.json");
const sessionId = "user_session_001";

const history = new FileSystemChatMessageHistory({
  filePath,
  sessionId,
});

两个参数分别解决两个问题:

  • filePath 决定历史写入哪个 JSON 文件;
  • sessionId 决定读写文件中的哪一个会话。

同一个文件可以按照会话 ID 区分消息。实际应用中,可以为不同用户或不同会话分配不同的 sessionId

需要注意,process.cwd() 表示启动 Node.js 命令时所在的工作目录,不是当前模块文件所在目录。换一个目录启动脚本,最终得到的文件路径也会变化。

1. 写入两轮对话

文件历史的使用方式与内存历史几乎一致:

js 复制代码
async function fileHistoryDemo() {
  const filePath = path.join(process.cwd(), "chat_history.json");
  const sessionId = "user_session_001";

  const history = new FileSystemChatMessageHistory({
    filePath,
    sessionId,
  });

  const systemMessage = new SystemMessage(
    "你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧"
  );

  const userMessage1 = new HumanMessage("红烧肉怎么做?");
  await history.addMessage(userMessage1);

  const response1 = await model.invoke([
    systemMessage,
    ...(await history.getMessages()),
  ]);
  await history.addMessage(response1);

  const userMessage2 = new HumanMessage("好吃吗?");
  await history.addMessage(userMessage2);

  const response2 = await model.invoke([
    systemMessage,
    ...(await history.getMessages()),
  ]);
  await history.addMessage(response2);
}

变化只发生在存储层:addMessage() 会把可序列化的消息内容写入文件,getMessages() 会把文件中的记录重新还原成 HumanMessageAIMessage 等对象。

JSON 中保存的不只是纯文本,大致会包含下面这些层次:

json 复制代码
{
  "user_session_001": {
    "messages": [
      {
        "type": "human",
        "data": {
          "content": "红烧肉怎么做?",
          "additional_kwargs": {},
          "response_metadata": {}
        }
      },
      {
        "type": "ai",
        "data": {
          "content": "模型生成的回答",
          "response_metadata": {
            "tokenUsage": {
              "promptTokens": 34,
              "completionTokens": 941,
              "totalTokens": 975
            }
          }
        }
      }
    ]
  }
}

真实记录里还可能有 AI 消息 ID、模型名称、工具调用和更完整的用量信息。应用不必手动解析这些字段,继续通过 getMessages() 读取即可。

2. 重启后恢复并继续第三轮

只要 filePathsessionId 保持一致,新创建的历史对象就能读取之前的记录:

js 复制代码
async function restoreHistoryDemo() {
  const filePath = path.join(process.cwd(), "chat_history.json");
  const sessionId = "user_session_001";

  const restoredHistory = new FileSystemChatMessageHistory({
    filePath,
    sessionId,
  });

  const restoredMessages = await restoredHistory.getMessages();
  console.log(`从文件中恢复 ${restoredMessages.length} 条历史消息`);

  restoredMessages.forEach((message, index) => {
    const prefix = message.type === "human" ? "用户" : "助手";
    console.log(
      `${index + 1}.[${prefix}]:${message.content.substring(0, 50)}...`
    );
  });

  const systemMessage = new SystemMessage(
    "你是一个友好、幽默的做菜助手,喜欢分享美食和烹饪技巧"
  );

  const userMessage3 = new HumanMessage("需要哪些食材?");
  await restoredHistory.addMessage(userMessage3);

  const response3 = await model.invoke([
    systemMessage,
    ...(await restoredHistory.getMessages()),
  ]);

  console.log(`助手:${response3.content}`);
  await restoredHistory.addMessage(response3);
  console.log("对话已经保存到文件中");
}

"需要哪些食材?"没有明确说明是哪道菜,但恢复出来的上下文中已经包含"红烧肉怎么做",模型便可以沿着原话题继续回答。

持久化解决了"重启后丢失"的问题,却带来了另一个问题:文件会越来越大,每次传给模型的上下文也会越来越长。因此,接下来需要把完整保存有限使用分开处理。

六、最直接的上下文管理:按消息条数截断

先准备八条消息,即四轮对话:

js 复制代码
const messages = [
  { type: "human", content: "我叫李四" },
  { type: "ai", content: "你好李四,很高兴认识你!" },
  { type: "human", content: "我是一名设计师" },
  { type: "ai", content: "设计师是个很有创造力的职业!你主要做什么类型的设计?" },
  { type: "human", content: "我喜欢艺术和音乐" },
  { type: "ai", content: "艺术和音乐都是很好的爱好,它们能激发创作灵感。" },
  { type: "human", content: "我擅长 UI/UX 设计" },
  { type: "ai", content: "UI/UX 设计非常重要,好的用户体验能让产品更成功!" },
];

把普通对象转换成 LangChain 消息并写入历史:

js 复制代码
for (const message of messages) {
  if (message.type === "human") {
    await history.addMessage(new HumanMessage(message.content));
  } else {
    await history.addMessage(new AIMessage(message.content));
  }
}

如果只保留最后四条消息,JavaScript 的 slice() 就足够了:

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

得到的内容是:

text 复制代码
HumanMessage: 我喜欢艺术和音乐
AIMessage: 艺术和音乐都是很好的爱好,它们能激发创作灵感。
HumanMessage: 我擅长 UI/UX 设计
AIMessage: UI/UX 设计非常重要,好的用户体验能让产品更成功!

这种方式简单、直观,而且一定能限制消息数量。但消息正文有长有短,四条消息可能只有几十个 Token,也可能非常长。因此,"保留四条"并不能直接代表"控制在某个 Token 预算内"。

此外,截断位置还要注意消息边界。如果截断结果以孤立的 AI 回答开头,模型看到的上下文会缺少对应问题。示例中的数据刚好以完整的一问一答为单位保留,实际管理时也应留意这一点。

七、更贴近上下文预算:按 Token 数截断

js-tiktoken 可以按照指定编码计算文本 Token 数,LangChain.js 的 trimMessages 则负责从消息数组中保留符合预算的一段内容。

1. 计算消息正文的 Token

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

function countTokens(messages, encoder) {
  let total = 0;

  for (const message of messages) {
    const content = typeof message.content === "string"
      ? message.content
      : JSON.stringify(message.content);

    total += encoder.encode(content).length;
  }

  return total;
}

这里兼容了两种 content

  • 字符串正文直接编码;
  • 非字符串正文先转成 JSON 字符串再编码。

需要准确理解这个函数的统计口径:它精确计算的是所选编码下的消息正文 Token 数 ,并没有额外计算角色标识、消息封装等可能产生的开销。因此它非常适合演示和自定义裁剪,但日志里的数字不要直接等同于模型服务最终返回的完整 promptTokens

2. 使用 trimMessages 保留最近消息

js 复制代码
import { trimMessages } from "@langchain/core/messages";

const encoder = getEncoding("cl100k_base");
const maxTokens = 100;

const trimmedMessages = await trimMessages(allMessages, {
  maxTokens,
  tokenCounter: async (messages) => countTokens(messages, encoder),
  strategy: "last",
});

const totalTokens = countTokens(trimmedMessages, encoder);
console.log(`总 Token 数量:${totalTokens}`);

三个配置项各自承担明确职责:

  • maxTokens: 100:裁剪后的正文 Token 上限;
  • tokenCounter:告诉 trimMessages 如何计算一组消息的 Token;
  • strategy: "last":优先保留最近的消息。

对前面的八条中文消息执行后,会保留最后四条,按当前统计方式共 78 个 Token:

text 复制代码
我喜欢艺术和音乐
艺术和音乐都是很好的爱好,它们能激发创作灵感。
我擅长 UI/UX 设计
UI/UX 设计非常重要,好的用户体验能让产品更成功!

与固定条数相比,Token 裁剪能更直接地对应上下文预算。不过无论按条数还是按 Token 截断,被移除的旧信息都彻底离开了本轮上下文。用户早先说过自己的姓名、职业,一旦它们被裁掉,模型便无法继续利用这些事实。

八、让旧信息不直接消失:对历史进行摘要

摘要式 Memory 的核心思路是:

text 复制代码
较早消息 → 拼接成文本 → 交给模型总结 → 保存摘要
最近消息 → 保留原文

这样既压缩了上下文,又没有简单丢弃所有旧信息。

1. 把消息数组转换成对话文本

getBufferString 可以把消息数组拼接成带角色前缀的字符串:

js 复制代码
import { getBufferString, SystemMessage } from "@langchain/core/messages";

async function summarizeHistory(messages) {
  if (messages.length === 0) return "";

  const conversationText = getBufferString(messages, "用户", "助手");

  const summaryPrompt = `请总结以下对话的核心内容,保留重要信息:
${conversationText}
总结:`;

  const summaryResponse = await model.invoke([
    new SystemMessage(summaryPrompt),
  ]);

  return summaryResponse.content;
}

例如,一组对象消息会被转换成类似下面的文本:

text 复制代码
用户: 我叫李四
助手: 你好李四,很高兴认识你!
用户: 我是一名设计师
助手: 设计师是个很有创造力的职业!

模型接收到明确的总结要求后,只返回摘要正文。

"消息数组 → 对话文本 → 总结提示词 → 模型摘要"是一个按顺序执行的线性流程,用 LangChain.js 就可以把它组织起来,这里不需要再引入图结构的工作流。

2. 按消息数量触发摘要

先看最容易理解的一版:历史超过六条消息时,把最近两条保留原文,其余消息交给模型总结。

js 复制代码
const maxMessages = 6;
const allMessages = await history.getMessages();

if (allMessages.length > maxMessages) {
  const keepRecent = 2;
  const recentMessages = allMessages.slice(-keepRecent);
  const messagesToSummarize = allMessages.slice(0, -keepRecent);

  const summary = await summarizeHistory(messagesToSummarize);

  await history.clear();
  await history.addMessage(
    new SystemMessage(`以下是之前对话的摘要:${summary}`)
  );

  for (const message of recentMessages) {
    await history.addMessage(message);
  }
}

压缩后的顺序非常重要:

text 复制代码
旧对话摘要
最近一条 HumanMessage
最近一条 AIMessage

摘要描述的是较早历史,所以应该放在最近消息之前。它也不应伪装成助手刚刚说出的一条普通回答,因此这里使用 SystemMessage 表明它是供后续推理参考的历史说明。

3. 按 Token 数触发并划分摘要区间

固定消息条数仍然无法反映正文长度。进一步改造后,可以在总 Token 达到阈值时触发摘要,并从后向前累计最近消息,直到用完为近期上下文预留的 Token 预算。

js 复制代码
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import {
  HumanMessage,
  AIMessage,
  SystemMessage,
  getBufferString,
} from "@langchain/core/messages";
import { getEncoding } from "js-tiktoken";

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

function countTokens(messages, encoder) {
  let total = 0;

  for (const message of messages) {
    const content = typeof message.content === "string"
      ? message.content
      : JSON.stringify(message.content);
    total += encoder.encode(content).length;
  }

  return total;
}

async function summarizeHistory(messages) {
  if (messages.length === 0) return "";

  const conversationText = getBufferString(messages, "用户", "助手");
  const summaryPrompt = `请总结以下对话的核心内容,保留重要信息:
${conversationText}
总结:`;

  const response = await model.invoke([
    new SystemMessage(summaryPrompt),
  ]);

  return response.content;
}

async function summarizationMemoryDemo() {
  const history = new InMemoryChatMessageHistory();
  const encoder = getEncoding("cl100k_base");

  const maxTokens = 200;
  const keepRecentTokens = 80;

  const messages = [
    { type: "human", content: "我叫李四" },
    { type: "ai", content: "你好李四,很高兴认识你!" },
    { type: "human", content: "我是一名设计师" },
    { type: "ai", content: "设计师是个很有创造力的职业!你主要做什么类型的设计?" },
    { type: "human", content: "我喜欢艺术和音乐" },
    { type: "ai", content: "艺术和音乐都是很好的爱好,它们能激发创作灵感。" },
    { type: "human", content: "我擅长 UI/UX 设计" },
    { type: "ai", content: "UI/UX 设计非常重要,好的用户体验能让产品更成功!" },
  ];

  for (const message of messages) {
    if (message.type === "human") {
      await history.addMessage(new HumanMessage(message.content));
    } else {
      await history.addMessage(new AIMessage(message.content));
    }
  }

  const allMessages = await history.getMessages();
  const totalTokens = countTokens(allMessages, encoder);

  if (totalTokens >= maxTokens) {
    const recentMessages = [];
    let recentTokens = 0;

    for (let index = allMessages.length - 1; index >= 0; index--) {
      const message = allMessages[index];
      const content = typeof message.content === "string"
        ? message.content
        : JSON.stringify(message.content);
      const messageTokens = encoder.encode(content).length;

      if (recentTokens + messageTokens <= keepRecentTokens) {
        recentMessages.unshift(message);
        recentTokens += messageTokens;
      } else {
        break;
      }
    }

    const splitIndex = allMessages.length - recentMessages.length;
    const messagesToSummarize = allMessages.slice(0, splitIndex);
    const summary = await summarizeHistory(messagesToSummarize);

    await history.clear();
    await history.addMessage(
      new SystemMessage(`以下是之前对话的摘要:${summary}`)
    );

    for (const message of recentMessages) {
      await history.addMessage(message);
    }
  }
}

summarizationMemoryDemo().catch(console.error);

这段逻辑可以拆成四步。

第一步:判断是否需要压缩
js 复制代码
if (totalTokens >= maxTokens) {
  // 开始摘要
}

历史没有达到 200 个正文 Token 时,不需要额外调用模型生成摘要。

按本文 countTokens() 的统计方式,这八条示例消息在 cl100k_base 下合计为 136 个 Token,因此保留 maxTokens = 200 时不会触发摘要,这是预期结果。若想用这组数据直接观察摘要流程,可以临时把阈值改为 120:

js 复制代码
const maxTokens = 120;

在真实对话中仍应根据希望保留的上下文规模设置阈值,而不是为了触发分支固定使用 120。

第二步:从后向前保留最近消息
js 复制代码
for (let index = allMessages.length - 1; index >= 0; index--) {
  // 只要累计值不超过 keepRecentTokens,就继续保留
}

遍历方向从最新消息开始,所以近期内容拥有更高优先级。使用 unshift() 而不是 push(),是为了在逆序遍历后仍维持原来的时间顺序。

第三步:切出需要摘要的较早历史
js 复制代码
const splitIndex = allMessages.length - recentMessages.length;
const messagesToSummarize = allMessages.slice(0, splitIndex);

消息数组因此被分成连续的前后两段,不会重复也不会遗漏:前半段生成摘要,后半段保留原文。

第四步:用"摘要 + 最近消息"重建上下文
js 复制代码
await history.clear();
await history.addMessage(
  new SystemMessage(`以下是之前对话的摘要:${summary}`)
);

for (const message of recentMessages) {
  await history.addMessage(message);
}

清空的只是当前上下文中的历史对象。重建之后,模型看到的是压缩后的旧信息和未经压缩的近期对话。

这里还有一个边界:如果某一条最近消息自身就超过 keepRecentTokens,循环会立即停止,recentMessages 可能为空,那么全部消息都会进入摘要区间。这个结果符合当前算法,但设置预算时需要意识到它。

九、截断与摘要应该怎么选

方案 保留方式 优点 代价或限制
固定条数截断 最近 N 条消息 最简单、执行快 不直接对应 Token 预算,旧信息直接丢失
Token 截断 Token 预算内的最近消息 更贴近上下文长度控制 仍会丢失被裁剪的信息
消息数触发摘要 旧消息摘要 + 最近消息原文 容易理解,能保留旧信息要点 消息长度不一致时触发时机不够精细
Token 触发摘要 按 Token 划分摘要区与近期区 同时控制长度并保留核心信息 需要额外调用一次模型生成摘要

这几种方法不是互斥关系。一个清晰的演进路线是:

text 复制代码
先完整保存历史
      ↓
上下文较短时直接使用
      ↓
达到阈值后,对较早消息生成摘要
      ↓
继续保留最近原始消息

如果历史进一步增长,仅靠一段持续累积的摘要仍然不够灵活。当前问题可能只与很久以前的一小段对话有关,把所有长期信息都揉进一个摘要,不一定能精确取回那段内容。

这就引出了下一种 Memory:把历史对话转成向量存入 Milvus,并在每一轮按照当前问题检索最相关的记录。下篇将完整拆解"写入长期记忆---语义检索---注入上下文---保存新记忆"的闭环。

相关推荐
半个落月1 小时前
LangChain.js Agent Memory 实战(下):用 Milvus 构建可检索的长期记忆
javascript·langchain
linux_cfan3 小时前
17 · 引擎适配器全景:HLS/DASH/Vimeo/Mux/Cast
前端·javascript·音视频
梦在远山后5 小时前
Electron 与 FastAPI 如何完成流式 Agent 对话
python·langchain·agent
何何____5 小时前
Vue 生命周期详解
前端·javascript
默_笙7 小时前
💫 闭包是个背包:拆解小米前端面试题里的三道"闭包陷阱"
前端·javascript·面试
猿人谷8 小时前
Jev:当 AI 不再生成 Token,而是直接做决策
后端·langchain·aigc
逆风飞翔的小叔8 小时前
【AI智能体】Langchain 主流大模型调用与对话API使用详解
langchain·langchain 对话api·langchain 对话·langchain 对话使用·langchain 对话详解·langchain api使用
尾善爱看海9 小时前
《JavaScript 数组操作全攻略:12 类 API + 30 个实战场景 + 20 个避坑指南》
前端·javascript·面试
OpsEye10 小时前
为什么你的 Agent 莫名烧钱?聊聊循环调用的兜底方案
javascript·ai编程