给大模型装上记忆:Agent 的 Memory 模块,从 InMemory 到文件到 Milvus 向量数据库

新手做 AI 应用最常见的困惑:大模型不是能聊天吗,为什么聊几句就把我忘了?关掉程序再开,之前的对话全没了?对话越来越长,上下文塞爆了怎么办?

这背后其实是同一个问题------大模型天生没有记忆 。本文用 LangChain.js 从零手写一个「会记忆的 Agent」,层层递进:内存记忆 → 文件记忆 → 向量数据库检索记忆,技术栈 LangChain.js + OpenAI 兼容接口 + js-tiktoken + Milvus

一、先想清楚:为什么 Agent 必须有 Memory

一张图看透 Agent 的本质:

ini 复制代码
Agent = LLM + Harness(工具 + RAG + Memory + ...)

LLM 本身只是个「会说话的大脑」,它有个致命缺陷:无状态(stateless)

  • 你问「我叫李四」,它回了;
  • 你再问「我叫什么?」,它傻了------上一句话它根本没记住;
  • 你能续着聊,是因为前端把历史消息数组又发了一遍给它,而不是它自己记得。

1.1 最朴素的记忆:messages 数组

很多入门教程会教:把对话都存进一个数组,每次请求把整个数组丢给模型:

js 复制代码
const messages = [
  { role: "system", content: "你是助手" },
  { role: "user", content: "我叫李四" },
  { role: "assistant", content: "你好李四!" },
];
// 下次提问时,把整个 messages 数组带上

这确实算「记忆」,但马上会碰到三座大山:

问题 大白话
持久化 数组在内存里,服务一重启就全没了
上下文窗口 聊几百轮后数组巨长,模型窗口(比如 200k token)装不下
开销 token 越多 = 越贵 + 响应越慢,把没用的旧话反复喂给模型太浪费

所以真正工程化的 Memory 模块,核心就干两件事:把记忆存下来(持久化) + 对塞不下的记忆做淘汰管理

1.2 一个贯穿全文的目标

做个做菜助手 ,让它学会「基于上一次对话接着答」。我们会一层层给它加记忆,最终做成一个像 codex 那样每次聊完自动沉淀记忆、下次能想起来的应用。


二、扫盲:先把这 4 个概念盘明白

后面代码会反复出现这些词,先建立直觉,别被吓到。

2.1 async / await 与 Promise

读写文件、调用大模型都要花时间,JS 不能「干等」,所以给出一张「取餐单号」------Promise

  • async 函数 = 告诉 JS「我这里要等外部慢事」,它不直接返回结果,而是返回一张单号;
  • await = 「站在门口等,单送到(拿到真结果)才走下一行」;
  • .catch() = 售后:哪一步失败了,把失败原因接住打印出来,程序不崩。

await 只能写在 async 函数里,所以两个文件开头一定长这样:

js 复制代码
fileHistoryDemo()
  .catch(console.error);   // 失败兜底,打印红字错误

💡 小坑:我最早把结尾写成 .then(console.error).then成功 才触发,失败根本不会打印。要用 .catch 才对。

2.2 LangChain 的 Message 对象

LangChain 不让你手搓 { role, content },而是给你几类「带类型的消息对象」:

对应 role 谁发的
SystemMessage system 系统设定,比如「你是做菜助手」
HumanMessage user 用户
AIMessage assistant 模型

一个典型的 SystemMessage

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

2.3 ChatMessageHistory:管理消息的容器

LangChain 把「存消息的容器」抽象成了 ChatMessageHistory,只有两个核心方法,读源码发现就这么简单:

方法 作用
addMessage(msg) 往历史里加一条消息(用户/助手都加)
getMessages() 把历史里所有消息取回来
clear() 清空历史

配合「两段式」用法:先把历史取出来拼上 system,一次性发给模型

js 复制代码
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
await history.addMessage(response1);   // 把回答也记回去,完成闭环

2.4 Model:OpenAI 兼容的模型封装

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

模型名、key、baseURL 全走 .env------这样既能用官方,也能接国内各种 OpenAI 兼容中转,不用改代码。


三、Memory 的两层设计:存哪 + 怎么淘汰

把 Memory 拆成两层独立的思考,代码才写得清楚:

python 复制代码
┌─────────────────────────────────────────────┐
│  管理逻辑(怎么淘汰)                            │
│    ① 截断  slice(-n)  保留最近的,丢掉最旧的     │
│    ② 总结  AI 把旧对话压缩成一句话摘要            │
│    ③ 检索  向量库按相似度找出"最相关"的旧记忆      │
├─────────────────────────────────────────────┤
│  存储逻辑(存哪)                                │
│    ① 内存  InMemoryChatMessageHistory(临时)    │
│    ② 文件  FileSystemChatMessageHistory(长期)  │
│    ③ 向量库  Milvus(长期 + 可搜索)              │
└─────────────────────────────────────────────┘

存储逻辑 回答「记忆放哪、死了还在不在」;管理逻辑回答「上下文快爆了,怎么把重要的留下」。二者可以任意组合------下面我们就按 存储 内存→文件→向量库 的升级路线走,顺路把三种管理策略都实现一遍。


四、第一步:内存记忆 InMemoryChatMessageHistory

先让 Agent 在同一进程内「记住」你。对应文件 history-test.mjs

4.1 完整的做菜助手多轮对话

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("[第一轮对话]");
  const userMessage1 = new HumanMessage("你今天吃的什么?");
  await history.addMessage(userMessage1);          // 记下:用户问了什么

  const messages1 = [systemMessage, ...(await history.getMessages())];
  const response1 = await model.invoke(messages1); // 发给模型
  await history.addMessage(response1);             // 记下:助手回了什么
  console.log(`助手:${response1.content}`);

  // ---- 第二轮(基于历史)----
  console.log("[第二轮对话 基于历史记录]");
  const userMessage2 = new HumanMessage("好吃吗?");
  await history.addMessage(userMessage2);

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

  // ---- 看账本里存了几条 ----
  const allMessages = await history.getMessages();
  console.log(`共保存了 ${allMessages.length} 条对话`);
}

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

4.2 这条流程长什么样

sequenceDiagram participant U as 用户 participant H as InMemoryHistory participant M as 大模型 U->>H: addMessage("好吃吗?") U->>M: 把 system + 全部历史一起发 M-->>U: 回答(它"记得"上一轮聊吃的) U->>H: addMessage(助手回答) Note over H: 账本里消息越来越多

关键就一行精髓:每次提问都把整本账本带上 。模型看到 messages2 里既有第一轮的「你今天吃的什么」,也有你的「好吃吗?」,自然知道你在问什么。

内存记忆的局限肉眼可见:InMemory 字面意思------账本只活在进程内存里,程序一重启,记忆清空。这就引出第二步。


五、第二步:文件记忆 FileSystemChatMessageHistory(重启不丢)

把账本从内存搬进硬盘文件。LangChain 社区包自带 FileSystemChatMessageHistory,自动把历史持久化成 JSON 文件。对应文件 history-test3.mjs

5.1 只改了哪几行?

对比第四步,核心变化只有一处 :把 InMemoryChatMessageHistory 换成 FileSystemChatMessageHistory,并指定文件路径和会话 ID。

js 复制代码
import {
  FileSystemChatMessageHistory   // 文件记忆
} from '@langchain/community/stores/message/file_system';

const filePath = path.join(process.cwd(), "chat_history.json");
const sessionId = "user_session_001";   // 多用户:不同 session 各记各的账

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

剩下的增删查代码一行都不用改 ------因为 addMessage / getMessages 接口完全一致。这就是 LangChain 抽象得好的地方:存储实现随便换,业务代码不动。

5.2 从文件里恢复记忆,接着聊第三轮

js 复制代码
async function fileHistoryDemo() {
  const restoredHistory = new FileSystemChatMessageHistory({ filePath, sessionId });

  // 从文件中恢复 history(进程重启后依然在)
  const restoredMessages = await restoredHistory.getMessages();
  console.log(`从文件中恢复了 ${restoredMessages.length} 条历史信息:`);
  restoredMessages.forEach((msg, index) => {
    const type = msg.type;
    const prefix = type === 'human' ? '用户' : '助手';
    console.log(`${index + 1}. [${prefix}]: ${msg.content.substring(0, 50)}...`);
  });

  // 接着聊第三轮
  console.log("[第三轮对话]");
  const userMessage3 = new HumanMessage("需要哪些食材?");
  await restoredHistory.addMessage(userMessage3);

  const messages3 = [systemMessage, ...(await restoredHistory.getMessages())];
  const response3 = await model.invoke(messages3);
  await restoredHistory.addMessage(response3);
  console.log(response3.content);
  console.log("对话已保存到文件");
}

跑完后你会发现当前目录多了个 chat_history.json

json 复制代码
[
  { "type": "human", "content": "需要哪些食材?" },
  { "type": "ai", "content": "做红烧肉需要五花肉、冰糖、生抽、老抽......" }
]

重启程序再跑,它能准确报出上一轮聊到哪了------记忆终于「活到下次开机」。

5.3 小结:内存 vs 文件

维度 InMemory FileSystem
存放位置 进程内存 磁盘 JSON 文件
重启后 ❌ 清空 ✅ 还在
多用户 靠对象区分 sessionId 区分
适用 单次会话内 轻量持久化
检索能力 只能整段读出,不能「按相似度找」

文件记忆解决了「重启不丢」,但暴露了新问题:它只能整本账本从头读到尾。对话一旦几百轮,光靠文件你会遇到两个坎------上下文塞爆、以及「想不起来一个多月前聊的具体事」。这就轮到管理策略和向量库登场。


六、三种淘汰策略:截断 / 总结 / 检索

无论账本存哪,塞不下时都要淘汰。LangChain 社区流传最广的三板斧,对应文件都在 demo/src/memory/ 下。

6.1 截断(Truncation):最简单粗暴

思路:只保留最近的 N 条,最旧的直接丢。

按消息条数截断 ------一个 slice 就够:

js 复制代码
const maxMessages = 4;
const trimmedMessages = allMessages.slice(-maxMessages);  // 留最后 4 条
console.log(`保留消息数量:${trimmedMessages.length}`);

按 token 数截断 ------生产环境更该用 token 而不是「条数」,因为每条消息长短不一。LangChain 提供 trimMessages,配合 js-tiktoken 精确算 token:

js 复制代码
import { getEncoding } from 'js-tiktoken';   // OpenAI 官方分词器
import { trimMessages } from '@langchain/core/messages';

const enc = getEncoding("cl100k_base");      // 编码方案要和模型一致

function countTokens(messages, encoder) {
  let total = 0;
  for (const msg of messages) {
    const content = typeof msg.content === 'string' ? msg.content
                    : JSON.stringify(msg.content);
    total += encoder.encode(content).length;
  }
  return total;
}

const trimmedMessages = await trimMessages(allMessages, {
  maxTokens: 100,                                // token 上限
  tokenCounter: async (msgs) => countTokens(msgs, enc),
  strategy: "last",                              // 从最后往前留,保住最新上下文
});

白话理解strategy: "last" = 从队尾往前数,数到 100 token 就停,前面的全砍。好处是省 token、便宜、不爆窗口 ;代价是砍掉的部分可能还有价值------万一旧话里藏着关键设定呢?

6.2 总结(Summarization):让 AI 帮忙「压缩」

思路 :被裁掉的旧对话别扔,让模型把它总结成一句话塞回去。对应 summarization-memory.mjs

三步走:

js 复制代码
async function summarizeHistory(messages) {
  if (messages.length === 0) return "";
  // ① 把消息对象数组拼成一段可读文本(getBufferString:HumanMessage -> "用户: xxx")
  const conversationText = getBufferString(messages, "用户", "助手");

  const summaryPrompt = `请总结以下对话的核心内容,保留重要信息:
    ${conversationText}
    总结:`;
  // ② 让大模型做压缩
  const summaryResponse = await model.invoke([new SystemMessage(summaryPrompt)]);
  return summaryResponse.content;
}

// ③ 数量超限时:老消息去总结,最近的保留原样
if (allMessages.length > maxMessages) {
  const keepRecent = 2;
  const recentMessages = allMessages.slice(-keepRecent);            // 最近的 2 条原样留
  const messagesToSummarize = allMessages.slice(0, -keepRecent);    // 其余拿去总结

  const summary = await summarizeHistory(messagesToSummarize);

  await history.clear();                  // 先清空账本
  for (const msg of recentMessages) {     // 把最近的写回去
    await history.addMessage(msg);
  }
  await history.addMessage(new AIMessage(summary));  // 把"一句话摘要"也写回去
}

getBufferString 本质就是把对象转成字符串,好喂给模型;进阶版 summarization-memory2.mjs 则是用 token 数(maxTokens: 200 / keepRecentTokens: 80)而不是条数来控制何时触发、保留多少------逻辑一模一样,只是单位更精确。

白话总结 :账本要爆了 → 把前面的旧对话压缩成「你们之前聊了 XX,他介绍了自己叫李四、是设计师」这句摘要,加上最近几条原文 → 模型下次既看到摘要又不丢最新细节。代价是多花一次模型调用做总结,且长对话层层摘要会失真。

6.3 检索(Retrieval):只找「最相关」的旧记忆

思路 :前两种都是「倒掉 / 压缩」,而检索式记忆是保留全部历史,每次只取最相关的一段。这要靠 embedding 向量 + 向量数据库------也是下一章 Milvus 的主角。这里先记下结论:

截断保新鲜、总结保梗概、检索保细节且能"跨话题想起旧事",越往后越接近人脑,也越复杂。

三者的取舍:

策略 保什么 代价 典型场景
截断 最新上下文 丢旧信息 对话很短的助手
总结 全局梗概 丢细节 + 花一次 AI 调用 长会话压缩
检索 相关细节 要向量库,工程重 懂你的长期记忆 Agent

七、第三步:Milvus 向量库------让记忆「可被搜索」

前面存文本只能「整段读」,我们要的是:用户随口问一句,系统能从一大堆历史里捞出"最像"的那几段。做法是把每段记忆转成向量,再算向量余弦相似度。

7.1 用 Docker 一键起 Milvus

Milvus 官方提供 standalone 编排文件(milvus-standalone-docker-compose.yml),在仓库里新建 milvus 目录放进去:

bash 复制代码
docker compose -f ./milvus-standalone-docker-compose.yml up -d
  • compose = 用配置文件一键编排多个容器;
  • -f 指定文件,up -d 后台启动;
  • Milvus 默认监听 19530 端口。

启动后可以装个 Attu(Milvus 生态最好的 GUI)可视化看数据,就像看 MySQL 的 Navicat。

7.2 装依赖 & 连上

bash 复制代码
pnpm i @zilliz/milvus2-sdk-node   # Milvus 官方 Node SDK
pnpm i @langchain/openai dotenv
js 复制代码
import { MilvusClient, DataType, MetricType, IndexType }
  from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from '@langchain/openai';

const COLLECTION_NAME = 'conversations';   // 集合(类比 MySQL 的表)
const VECTOR_DIM = 1024;                   // 向量维度(要和 embedding 模型一致)

// embedding:把文字变成 1024 维向量
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'text-embedding-v3',
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  dimension: VECTOR_DIM,
});

const client = new MilvusClient({ address: 'localhost:19530' });  // 连 19530

7.3 建集合:给每轮对话建一张「档案卡」

对应 insert-conversations.mjs。字段设计很直观:

js 复制代码
await client.createCollection({
  collection_name: COLLECTION_NAME,
  fields: [
    { name: "id", data_type: DataType.VarChar, max_length: 50, is_primary_key: true },
    { name: 'vector', data_type: DataType.FloatVector, dim: VECTOR_DIM }, // 向量
    { name: 'content', data_type: DataType.VarChar, max_length: 5000 },   // 原文
    { name: 'round',  data_type: DataType.Int64 },                        // 第几轮
    { name: 'timestamp', data_type: DataType.VarChar, max_length: 100 },  // 时间(存字符串)
  ]
});

// 给 vector 字段建索引 + 指定相似度算法,不然搜不了
await client.createIndex({
  collection_name: COLLECTION_NAME,
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE,   // 余弦相似度:越接近1越相关
});

await client.loadCollection({ collection_name: COLLECTION_NAME });  // 载入内存才能搜

💡 注意:Milvus 没有专门的 Date/DateTime 类型,时间戳这里存成字符串即可。

写数据时,把「一整轮对话」当一条记忆,用 embedding 模型转成向量再插入

js 复制代码
const conversations = [
  { id: 'conv_001', content: '用户:我叫赵六,是一名数据科学家\n助手:很高兴认识你!',
    round: 1, timestamp: new Date().toISOString() },
  // ... 多轮历史
];

const conversationData = await Promise.all(
  conversations.map(async (conv) => ({
    ...conv,
    vector: await embeddings.embedQuery(conv.content),  // 文字 -> 1024维向量
  }))
);

await client.insert({ collection_name: COLLECTION_NAME, data: conversationData });

embedding 的灵魂是:语义相近的句子,向量方向也相近。所以「我叫赵六」和「你叫什么名字」能算出很高的相似度------这就是机器能「想起你」的原理。


八、完整闭环:仿 codex 的记忆型聊天应用

现在把前面所有零件拼起来,对应 retrieval-memory.mjs。流程就三步:

复制代码
用户提问
  → ① 把问题转向量,去 Milvus 搜最像的 2 段历史(检索记忆)
  → ② 把搜到的历史 + 当前问题拼给大模型
  → ③ 模型回答后,把这一轮也写回 Milvus(自动沉淀新记忆)

8.1 先写一个「查记忆」的函数

js 复制代码
async function retrieveRelevantConversations(query, k = 2) {
  try {
    const queryVector = await getEmbedding(query);          // 问题 -> 向量
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      limit: k,                                             // 只要最像的 k 段
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'round', 'timestamp'],
    });
    return searchResult.results;
  } catch (err) {
    console.error('检索对话时出错', err.message);
    return [];                                              // 检索失败也别中断对话
  }
}

8.2 主流程:检索 → 增强 → 回答 → 回写

js 复制代码
async function retrievalMemoryDemo() {
  await client.connectPromise;

  const questions = [
    "我之前提到的机器学习项目进展如何?",
    "我的职业是什么?",
  ];

  for (let i = 0; i < questions.length; i++) {
    const input = questions[i];
    const userMessage = new HumanMessage(input);

    // ① 检索相关历史
    const retrieved = await retrieveRelevantConversations(input, 2);
    let relevantHistory = '';
    if (retrieved.length > 0) {
      relevantHistory = retrieved.map((conv, idx) =>
        `[历史对话 ${idx + 1}] 轮次:${conv.round}\n${conv.content}`
      ).join('\n');
    } else {
      console.log('未找到相关历史对话');
    }

    // ② 有相关历史就带上去,没有就只发当前问题
    const contextMessages = relevantHistory
      ? [new HumanMessage(`相关历史对话:\n${relevantHistory}\n\n用户问题:${input}`)]
      : [userMessage];

    // ③ 调用模型
    const response = await model.invoke(contextMessages);
    console.log(response.content);

    // ④ 把这一轮也存回 Milvus,越聊越懂你
    const conversationText = `用户:${input}\n助手: ${response.content}`;
    const convId = `conv_${Date.now()}_${i + 1}`;
    const convVector = await getEmbedding(conversationText);
    await client.insert({
      collection_name: COLLECTION_NAME,
      data: [{
        id: convId, content: conversationText, vector: convVector,
        round: i + 1, timestamp: new Date().toISOString(),
      }],
    });
  }
}

8.3 为什么这套能「更懂你」

对比一下:

  • 用户问「我的职业是什么?」------这句话单独发给一个没有记忆的模型,它只能瞎猜或让你说明;
  • 但在上面流程里,Milvus 检索会命中历史里那句「我的职业是软件工程师」,连同上下文一起喂给模型 → 它真的记得你

这和 codex / 记忆型聊天应用的思路完全一致:每聊 20 条就触发一次总结,把摘要和精华存进 Milvus;下次开聊先检索历史,再接着回答。记忆是「越用越厚」的资产。

完整依赖清单:

json 复制代码
{
  "@langchain/community": "^1.1.29",
  "@langchain/core": "^1.2.9",
  "@langchain/openai": "^1.5.11",
  "@zilliz/milvus2-sdk-node": "^3.0.5",
  "dotenv": "^17.4.2",
  "js-tiktoken": "^1.0.21",
  "langchain": "^1.5.10"
}

九、踩坑与经验

  1. .then() 不是失败兜底 。async 函数结尾要么 .catch(),要么函数内 try/catch.then(console.error) 是成功才执行,错了它根本不管。
  2. 切换存储后端不用改业务代码InMemoryChatMessageHistoryFileSystemChatMessageHistory 接口一致,换实现只动一行------用接口抽象去设计,别写死具体类。
  3. tiktoken 编码要跟模型对上。不同模型分词器不同,token 算不准会过早/过晚触发截断。
  4. token 比「条数」更值得作为淘汰单位。一条超长消息能顶 20 条短的,用条数截断误差很大。
  5. Milvus 的时间戳没有专门类型,用 VarChar 字符串存 ISO 时间即可;embedding 维度(1024)必须和模型输出一致,否则写入报错。
  6. embedding 服务国内网络 :用 OPENAI_BASE_URL 指到兼容网关,模型名、维度都对得上就行。
  7. 本地起 Milvus 别硬装 ,官方给了 standalone 的 docker-compose,一条 docker compose up -d 最省事(前提是 Docker 配好了国内镜像加速,否则 pull 会超时)。
  8. 检索失败要降级retrieveRelevantConversations 里 catch 后返回 [],宁可当普通对话也别让整个流程崩掉。

十、总结

回到开头那三个问题,现在的答案很清晰:

问题 解法
模型记不住我 每轮对话的 user/assistant 消息都 addMessage 进 history
重启就忘 把 history 落盘:FileSystemChatMessageHistory 写 JSON
上下文塞爆 / 太贵 截断保新鲜、总结保梗概
想不起很久以前的细节 embedding + Milvus 按相似度检索,只带最相关记忆

Agent = LLM + Harness 。给模型配了工具它能「干活」,配了 RAG 它能「懂知识」,而 Memory 是把两者粘起来的那根线 ------让 Agent 记得你是谁、聊到哪、在乎什么。从内存到文件再到向量库,本质上是在回答同一个问题:怎么让一个没有记忆的模型,表现得像真的记得你。

相关推荐
橘子星1 小时前
给 Agent 装上有记忆的"脑子"(一):用 LangChain 打通内存记忆、文件持久化与上下文截断
javascript
光影少年1 小时前
从输入URL到页面渲染,React/RN 整体加载流程
前端·javascript·react native·react.js·前端框架
兔子零10241 小时前
我给 Pi Coding Agent 做了一个桌面控制台:Pi-Harness
前端·javascript·后端
橘子星1 小时前
给 Agent 装上有记忆的"脑子"(二):对话太长会撑爆上下文?用"自动总结"给记忆瘦身
javascript
橘子星1 小时前
给 Agent 装上有记忆的"脑子"(三):把对话存进向量库,让 Agent 拥有"长期记忆"
javascript·人工智能
runningshark2 小时前
Lecture: The ‘Why & How‘ Principle: Moving Beyond Simple Statements
开发语言·前端·javascript
雪芽蓝域zzs2 小时前
第三十一节:角色管理页面 + 权限分配树形弹窗
前端·javascript·vue.js
暖焰核心2 小时前
C++模板进阶——特化全解
javascript·c++·jquery
晓得迷路了2 小时前
栗子前端技术周刊第 145 期 - Remix 3 RC、htmx 4.0、Rslib 1.0...
前端·javascript·react.js