LangChain.js + Milvus 向量长期记忆实战:对话写入、语义检索与 RAG 增强

LangChain.js + Milvus 向量长期记忆:对话写入、语义检索与 RAG 增强

  • 前言
  • [1. 先建立知识地图:向量长期记忆究竟在做什么](#1. 先建立知识地图:向量长期记忆究竟在做什么)
    • [1.1 短期 History 与向量长期记忆各自解决什么问题](#1.1 短期 History 与向量长期记忆各自解决什么问题)
    • [1.2 Embedding、向量与余弦相似度](#1.2 Embedding、向量与余弦相似度)
    • [1.3 Milvus 中的 Collection、Index 与 Load](#1.3 Milvus 中的 Collection、Index 与 Load)
  • [2. 项目准备与完整数据流](#2. 项目准备与完整数据流)
    • [2.1 初始化、依赖与配置边界](#2.1 初始化、依赖与配置边界)
    • [2.2 一次完整问答如何流动](#2.2 一次完整问答如何流动)
  • [3. 第一段核心代码:把历史对话写入 Milvus](#3. 第一段核心代码:把历史对话写入 Milvus)
    • [3.1 连接客户端与定义 Collection Schema](#3.1 连接客户端与定义 Collection Schema)
    • [3.2 对话文本如何变成可插入向量](#3.2 对话文本如何变成可插入向量)
    • [3.3 写入阶段最容易踩中的三处边界](#3.3 写入阶段最容易踩中的三处边界)
  • [4. 第二段核心代码:查询向量、召回历史并增强模型输入](#4. 第二段核心代码:查询向量、召回历史并增强模型输入)
    • [4.1 `retrieveRelevantConversations()`:把自然语言问题变成 Top-K 记忆](#4.1 retrieveRelevantConversations():把自然语言问题变成 Top-K 记忆)
    • [4.2 把命中结果转成模型能使用的参考上下文](#4.2 把命中结果转成模型能使用的参考上下文)
    • [4.3 生成后回写:让新对话成为下一次可召回的记忆](#4.3 生成后回写:让新对话成为下一次可召回的记忆)
  • [5. 把两段代码串成一个完整的混合 Memory 闭环](#5. 把两段代码串成一个完整的混合 Memory 闭环)
    • [5.1 从用户问题到新记忆写回的完整实现](#5.1 从用户问题到新记忆写回的完整实现)
  • [6. 效果评估与工程取舍](#6. 效果评估与工程取舍)
    • [6.1 影响召回质量的关键不是只有模型](#6.1 影响召回质量的关键不是只有模型)
    • [6.2 长期记忆的边界与安全要求](#6.2 长期记忆的边界与安全要求)
  • 总结

前言

短期 History 能让模型记住刚刚发生的几轮对话,却很难承担跨天、跨会话乃至海量用户信息的记忆任务。把所有消息原样放进提示词,会同时碰到上下文窗口、调用成本和注意力稀释三个瓶颈;只用关键词检索又无法理解"我周末进场做什么"与"我经常去电影院"之间的语义关联。

向量长期记忆为这个问题提供了另一条路径:把每段对话转换为向量写入 Milvus;下一次提问时,将问题也转换为向量,检索语义最接近的历史片段,再把它们作为参考上下文交给大模型。本文先建立必要的知识框架,再分块拆解"写入历史对话"和"检索增强对话"两段核心代码,最后将它们串成一个可复用的 Memory 闭环。

1. 先建立知识地图:向量长期记忆究竟在做什么

1.1 短期 History 与向量长期记忆各自解决什么问题

InMemoryChatMessageHistory 保存的是按时间排列的连续对话 ,适合回答"刚才那句话里的它指什么"。但它通常只保留当前进程中的消息,而且历史越长,输入模型的 Token 越多。向量数据库保存的则是可按语义检索的记忆片段,适合从大量旧记录中找出"和当前问题最相关的少数内容"。

二者不应互相替代,而应组合:短期 History 保证当前会话的连贯性,向量库负责跨会话和长时间跨度的召回。检索出来的记录不是自动被模型记住的事实,仍必须被放入本次 model.invoke() 的消息数组中。

维度 短期 History 向量长期记忆
组织方式 按时间顺序保存 Message 按向量相似度检索片段
擅长回答 最近几轮的指代、追问 很久以前的偏好、经历、事实
规模 受上下文窗口直接限制 可保存大量记录,只取 Top-K
主要风险 Token 膨胀、旧信息被截断 召回不准、把无关内容带入提示词
最佳实践 保留最近的原始问答 检索相关旧记忆后补充到上下文

向量长期记忆不是"把所有对话永久塞给模型",而是"在需要时,从大量历史中召回少量语义相关的证据"。

1.2 Embedding、向量与余弦相似度

Embedding(嵌入)模型会把一段文本映射为固定长度的浮点数数组,例如 [0.12, -0.08, ...]。数组中的每一维没有可单独解释的自然语言含义,但整条向量在高维空间中的位置编码了文本语义。意思相近的句子通常距离更近,因此"周末去哪放松"和"我经常去电影院"即使没有共享关键词,也可能被检索到一起。

本例使用 OpenAIEmbeddings:写入阶段将 content 转成文档向量,查询阶段使用同一模型把用户问题转成查询向量。集合的 FloatVector 维度必须与 Embedding 返回数组长度完全相同 ;若 Milvus 声明为 1024 维,实际写入 1536 维或其他长度的向量会失败。

余弦相似度关注两个向量的夹角而不是绝对长度,常用表达式如下:

text 复制代码
cosine(a, b) = (a · b) / (||a|| × ||b||)

MetricType.COSINE 用于检索时,分数通常越高,方向越相近,语义也越相关;它不是"回答正确率",更不是概率。检索代码中的 k = 2 只表示最多取两条,并不保证这两条一定足够相关,因此生产代码还需要根据真实数据观察并校准分数阈值。

度量方式 关注点 分数/距离的典型判断 本例适配性
COSINE 向量方向是否一致 通常越大越相似 适合通用文本语义检索
IP(内积) 向量点积 通常越大越相似 依赖向量构造与归一化策略
L2 欧氏距离 越小越接近 更适合以距离为目标的场景

1.3 Milvus 中的 Collection、Index 与 Load

Milvus 的 Collection 可以理解为面向向量的"表",但它不仅存标量字段,还存储用于相似度搜索的向量字段。本例的 conversations 集合包含主键 id、向量 vector、原始文本 content、轮次 round 和时间 timestamp。检索命中后,只有向量不足以构造提示词,所以必须通过 output_fields 一并取回 content 等标量字段。

写入向量之后,Milvus 需要索引来高效搜索。示例选择 IndexType.IVF_FLAT,它先把向量划分到若干簇,检索时优先扫描接近的簇;FLAT 表示簇内采用精确比较。创建索引后还需要 loadCollection(),将集合加载到可检索状态。这个"建集合 → 建索引 → 加载集合"的生命周期,是向量检索区别于普通数组比较的重要部分。

2. 项目准备与完整数据流

2.1 初始化、依赖与配置边界

先初始化 Node.js 项目,并安装向量数据库、环境变量、LangChain 核心抽象和模型适配器。

bash 复制代码
npm init -y

# Milvus 客户端与环境变量加载能力
pnpm i @zilliz/milvus2-sdk-node dotenv

# Message、History、Embedding 与聊天模型能力
pnpm i @langchain/core @langchain/openai
依赖 在项目中的职责
dotenv 通过 import 'dotenv/config' 加载密钥、模型名和服务地址
@langchain/openai 提供 OpenAIEmbeddingsChatOpenAI 两个模型适配器
@langchain/core 提供 HumanMessageSystemMessage 与短期 History 抽象
@zilliz/milvus2-sdk-node 连接 Milvus、创建集合、插入向量并执行相似度搜索

Milvus 服务需要先运行并能通过 localhost:19530 访问。.env 只保存配置名和值,不应提交真实密钥;尤其不能把密钥写进代码或文章。

dotenv 复制代码
MODEL_NAME=你的聊天模型名
EMBEDDING_MODEL_NAME=text-embedding-v3
OPENAI_API_KEY=你的密钥
OPENAI_BASE_URL=https://你的兼容接口地址/v1

这里有一个容易忽略的版本细节:当前 @langchain/openai 使用的参数名是 dimensions ,而不是 dimension。若服务支持自定义输出维度,应让它和 VECTOR_DIM 一致;若服务不支持自定义维度,则应以模型的原生输出维度设置 VECTOR_DIM。否则创建集合虽然成功,插入向量时仍会因长度不匹配报错。

javascript 复制代码
import 'dotenv/config';
import { OpenAIEmbeddings } from '@langchain/openai';

const VECTOR_DIM = 1024;

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDING_MODEL_NAME,
  // 字段为 dimensions;其值必须与 Milvus FloatVector 的 dim 相同
  dimensions: VECTOR_DIM,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
});

2.2 一次完整问答如何流动

写入脚本先准备历史种子数据,检索脚本再处理新的用户问题。把两段代码串起来,完整数据流如下:

text 复制代码
历史问答 content
  ↓ OpenAIEmbeddings.embedDocuments()
历史向量 + id/content/round/timestamp
  ↓ Milvus insert
conversations Collection
  ↓ 当前问题 → embedQuery()
查询向量
  ↓ Milvus COSINE Top-K search
相关历史文本 + score
  ↓ 组装 SystemMessage / HumanMessage
ChatOpenAI.invoke()
  ↓
模型回复 + 当前问题重新打包并向量化
  ↓
写回 Milvus,成为下一轮可召回的长期记忆

这个流程也是一种轻量的 RAG(Retrieval-Augmented Generation,检索增强生成):检索负责找证据,生成模型负责基于当前问题和证据组织回答。它不是模型微调,写入新记忆不会改变模型参数,只会改变下一次请求时提供的上下文。

3. 第一段核心代码:把历史对话写入 Milvus

3.1 连接客户端与定义 Collection Schema

insert-conversation.mjs 的第一项任务是定义"每条长期记忆长什么样"。id 使用 VarChar 主键,避免重复记录互相覆盖;vector 是检索真正比较的 FloatVectorcontent 保留可读文本;roundtimestamp 让应用在召回后仍可显示来源与时间。

javascript 复制代码
import {
  MilvusClient,
  DataType,
  MetricType,
  IndexType,
} from '@zilliz/milvus2-sdk-node';

const COLLECTION_NAME = 'conversations';
const VECTOR_DIM = 1024;
const client = new MilvusClient({ address: 'localhost:19530' });

async function ensureCollection() {
  await client.connectPromise; // 等待 gRPC 连接真正建立

  const { value: exists } = await client.hasCollection({
    collection_name: COLLECTION_NAME,
  });

  if (!exists) {
    await client.createCollection({
      collection_name: COLLECTION_NAME,
      fields: [
        // 主键由应用生成;VarChar 必须指定最大字符长度
        { name: 'id', data_type: DataType.VarChar, max_length: 64, is_primary_key: true },
        // dim 是 Milvus Schema 字段,不是 Embedding 配置字段
        { 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 },
      ],
    });

    await client.createIndex({
      collection_name: COLLECTION_NAME,
      field_name: 'vector',
      index_type: IndexType.IVF_FLAT,
      metric_type: MetricType.COSINE, // 写入、索引、查询必须使用同一种度量
    });
  }

  // 即使集合已存在,也要确保本次进程可搜索它
  await client.loadCollection({ collection_name: COLLECTION_NAME });
}

原始演示直接调用 createCollection(),第一次运行很直观,但第二次运行时集合已经存在,初始化会抛出异常。hasCollection() 使初始化变成幂等操作:执行多次,目标状态仍是"集合、索引和加载状态准备完毕"。同样不应使用空的 catch 块吞掉建库异常,否则后面的插入或查询失败时很难定位真正原因。

字段设计的关键不只是"能存进去",更是"检索后能解释"。只存 vector 无法让模型理解历史内容;只存 content 又无法做语义搜索。因此每条记录同时拥有向量索引字段可回读的原始文本/元数据字段

字段 数据类型 作用 设计原因
id VarChar 唯一标识一段记忆 支持去重、追踪与更新
vector FloatVector 语义相似度搜索 长度必须等于 VECTOR_DIM
content VarChar 命中后放入 Prompt 让检索结果可读、可用
round Int64 标记记录轮次 便于调试、展示和排序
timestamp VarChar 标记写入时间 用 ISO 字符串可避免时区歧义

3.2 对话文本如何变成可插入向量

演示数据把用户问题和助手回答合成一条 content。这是一个合理的最小粒度:检索到"用户喜欢篮球和电影"时,也同时带回了助手对该偏好的回应。更复杂的应用可按整轮、主题摘要或用户事实切块,但一条记忆不宜混入完全无关的话题,否则向量语义会被稀释。

写入多条文本时,embedDocuments() 比逐条调用 embedQuery() 更符合 API 语义,也能够批量请求。查询文本使用 embedQuery();在当前实现中两者都会请求同一 Embedding 模型,但区分二者可让"文档入库"和"用户查询"意图更加清晰。

javascript 复制代码
async function insertConversations(conversations) {
  // 批量生成向量:第 i 个 vector 对应第 i 条 content
  const vectors = await embeddings.embedDocuments(
    conversations.map((conversation) => conversation.content)
  );

  const data = conversations.map((conversation, index) => ({
    ...conversation,
    vector: vectors[index], // 保持文本、元数据与向量一一对应
  }));

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

  // Demo 中等待持久化完成,方便随后立刻进行稳定检索
  await client.flushSync({ collection_names: [COLLECTION_NAME] });
}

const seedConversations = [
  {
    id: 'conv_001',
    content: '用户:我叫赵六,是一名数据科学家\n助手:很高兴认识你,赵六!',
    round: 1,
    timestamp: new Date().toISOString(),
  },
  {
    id: 'conv_002',
    content: '用户:我最近在研究机器学习算法\n助手:你在研究哪些算法呢?',
    round: 2,
    timestamp: new Date().toISOString(),
  },
];

这里的 Promise.all(conversations.map(...)) 也能并发逐条生成向量,适合几条演示数据;批量 embedDocuments() 对大量数据通常更节省网络往返。无论采用哪种方式,都必须保证数组位置没有错位,否则可能把 A 文本的向量写到 B 文本上,检索结果看似有数据,语义却会完全失真。

3.3 写入阶段最容易踩中的三处边界

  • 维度字段的名字不同。 Milvus Schema 使用 dim,LangChain 的 OpenAIEmbeddings 使用 dimensions;前者定义数据库字段,后者请求模型输出,两个值必须一致。
  • 索引与查询指标必须一致。 集合以 COSINE 创建索引,搜索时也要传 MetricType.COSINE,否则结果的排序语义不可靠。
  • 主键不能靠时间戳"碰运气"。 Date.now() 加轮次适合演示,但分布式或高并发写入更适合 UUID、雪花 ID 等专门的唯一标识策略。

4. 第二段核心代码:查询向量、召回历史并增强模型输入

4.1 retrieveRelevantConversations():把自然语言问题变成 Top-K 记忆

检索函数的输入是用户问题 query,输出是 Milvus 命中的记录。它先调用 embedQuery(query) 得到查询向量,再执行 client.search()。其中 limit 即 Top-K 数量,output_fields 是希望随命中一起返回的字段;返回结果中的 score 来自向量相似度计算,content 才是后续构造上下文的材料。

javascript 复制代码
import { MetricType } from '@zilliz/milvus2-sdk-node';

async function retrieveRelevantConversations(query, k = 2) {
  try {
    const queryVector = await embeddings.embedQuery(query);

    const { results } = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      limit: k, // 只取语义最接近的前 k 条,避免 Prompt 被历史淹没
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'round', 'timestamp'],
    });

    // Top-K 不等于"必然相关";阈值要结合真实数据集校准
    const MIN_COSINE_SCORE = 0.55;
    return results.filter((item) => item.score >= MIN_COSINE_SCORE);
  } catch (error) {
    console.error('检索对话时出错:', error.message);
    return [];
  }
}

原始检索代码会无条件返回 Top-2,这能清楚展示 API,但也意味着无论问题多么陌生,最不相关的两条记忆仍可能被塞进模型。上例增加了分数过滤,不过 0.55 只是示意值,不能机械照搬:不同 Embedding 模型、文本长度、切分方式和语料分布都会改变分数范围。正确做法是记录命中的 score,用真实问答集评估"相关/不相关"的分布后再设阈值。

score 的另一个常见误解是把它当作答案可信度。它只衡量"问题向量与记忆向量"的接近程度;即使分数很高,记忆本身也可能过期或包含错误信息。因此它适合作为召回过滤信号,而不是事实真伪证明。

4.2 把命中结果转成模型能使用的参考上下文

Milvus 返回的是对象数组,模型需要的是 Message 数组或文本提示。map() 负责把每条命中记录格式化,join() 用分隔线连接,保留 roundtimestamp 能让模型或日志知道记忆来源。若没有命中,则不应伪造历史,而应只发送当前问题。

javascript 复制代码
import { HumanMessage, SystemMessage } from '@langchain/core/messages';

function buildMessages(input, memories, recentMessages = []) {
  const memoryText = memories
    .map(
      (memory, index) => `[相关历史 ${index + 1}]
轮次:${memory.round}
时间:${memory.timestamp}
内容:${memory.content}`
    )
    .join('\n\n---\n\n');

  return [
    new SystemMessage(
      '你是严谨的助手。历史记忆仅作参考:只在确实相关时使用;' +
      '若记忆不足以支持结论,请明确说明,不要把记忆中的内容改写成用户刚刚说过的话。'
    ),
    // 检索记忆以"参考材料"形式提供,避免伪装成当前用户的最新表达
    ...(memoryText
      ? [new HumanMessage(`参考历史记忆:\n${memoryText}`)]
      : []),
    // 短期 History 保留最近问答的时间顺序,补足当前会话中的指代关系
    ...recentMessages,
    new HumanMessage(`当前用户问题:${input}`),
  ];
}

原始示例把"相关历史对话"和"用户问题"合并到一条 HumanMessage 中,能够正常工作。加入 SystemMessage 后,模型多了一层稳定约束:检索记录只是参考,而不是必须引用的指令。尤其当长期记忆来自用户可编辑内容或外部文档时,这种"数据与指令分离"的提示词结构能降低无关文本干扰回答的风险。

4.3 生成后回写:让新对话成为下一次可召回的记忆

检索脚本在得到回复后做了两类保存:history.addMessage() 保存当前进程内的短期消息,client.insert() 把"本轮用户问题 + 助手回答"作为长期记忆写入 Milvus。前者让同一次会话保持连续,后者让未来会话可按语义找回本轮内容。

javascript 复制代码
import { HumanMessage } from '@langchain/core/messages';

async function saveConversation({ history, input, response, round }) {
  // 短期 Memory:服务当前会话,按时间保留原始 Message
  await history.addMessage(new HumanMessage(input));
  await history.addMessage(response);

  // 长期 Memory:把一轮问答打包为可语义检索的文本块
  const content = `用户:${input}\n助手:${response.content}`;
  const [vector] = await embeddings.embedDocuments([content]);

  await client.insert({
    collection_name: COLLECTION_NAME,
    data: [{
      id: `conv_${Date.now()}_${round}`,
      content,
      vector,
      round,
      timestamp: new Date().toISOString(),
    }],
  });

  // 教学演示等待本次写入完成,保证紧接着的检索可以观察到新记忆
  await client.flushSync({ collection_names: [COLLECTION_NAME] });
}

注意当前短期 History 在原始示例中只负责保存,并没有直接参与 model.invoke();本轮模型上下文主要来自向量检索结果。若要形成真正的混合记忆,应把"最近短期消息"与"检索到的长期记忆"同时加入 Prompt:前者解决当前对话的连续性,后者补足被截断或跨会话的历史事实。

5. 把两段代码串成一个完整的混合 Memory 闭环

5.1 从用户问题到新记忆写回的完整实现

下面的函数刻意保持职责简单:初始化阶段只做一次 ensureCollection();每次聊天调用 chatWithMemory() 时,先查长期记忆,再将"系统规则 + 召回记忆 + 最近短期 History + 当前问题"交给模型,最后同步写入短期 History 和 Milvus。代码把 model.invoke() 放在写回之前,避免把尚未产生的回复误当成历史证据。

javascript 复制代码
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';

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

const history = new InMemoryChatMessageHistory();

async function chatWithMemory(input, round) {
  // 1. 将当前问题转成查询向量,从大量旧记录中只召回少数相关片段
  const memories = await retrieveRelevantConversations(input, 2);

  // 2. 检索结果不是答案;与最近短期 History 一起成为模型的参考上下文
  const recentMessages = (await history.getMessages()).slice(-4);
  const messages = buildMessages(input, memories, recentMessages);
  const response = await model.invoke(messages);

  // 3. 回答完成后,同时更新短期顺序记忆和长期向量记忆
  await saveConversation({ history, input, response, round });

  return response.content;
}

async function main() {
  await ensureCollection(); // 建表、索引、加载只应在启动阶段处理

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

  for (const [index, input] of questions.entries()) {
    const answer = await chatWithMemory(input, index + 1);
    console.log(`用户:${input}\n助手:${answer}\n`);
  }
}

main().catch((error) => {
  console.error('长期记忆流程执行失败:', error);
});

完整调用的因果顺序可以再复盘一次:用户输入先变成查询向量,Milvus 返回带 contentscore 的候选历史;应用只保留通过阈值的结果,并与最近问答一起格式化成参考上下文;聊天模型据此回答;最后再将这轮问答向量化写回。正是最后一步让系统从"只会查固定种子数据"变成"每次交互都会积累可检索经验"的长期记忆系统。示例中的 flushSync() 用于让教学演示立刻观察到新记录;高吞吐服务不宜每轮同步刷盘,应按一致性要求批量或异步处理。

6. 效果评估与工程取舍

6.1 影响召回质量的关键不是只有模型

Embedding 模型很重要,但实际效果往往先被数据组织方式决定。一整天对话塞入一条记录会让主题混杂;一条记录只剩几个词又会失去必要上下文。以"一轮完整问答"或"一个明确用户事实"为基本单元通常更容易检索。对于超长对话,可先按主题切块或摘要,再写入向量库。

问题 可能原因 对策
明明存过却检索不到 文本切块过粗、Embedding 不一致、分数阈值过高 统一模型与维度,优化切块,记录并分析 score
总检索到无关内容 k 太大、没有阈值、内容主题混杂 限制 Top-K,增加阈值,按话题拆分记忆
新写入内容暂时查不到 集合未加载、写入尚未持久化或一致性设置不同 启动时 loadCollection(),测试时使用 flushSync() 验证
模型把旧信息当作当前指令 直接拼接文本,缺少来源和边界 使用 SystemMessage 声明"历史仅作参考"

6.2 长期记忆的边界与安全要求

向量检索会放大数据保留问题:用户身份、偏好和聊天内容一旦入库,必须具备按用户隔离、删除、更新和过期清理能力。实际集合应至少附加 user_id,并在检索时使用过滤表达式限制到当前用户;不能仅依赖相似度搜索。对姓名、联系方式、账号等敏感信息,还要遵守最小化保存原则,并提供用户可撤回的机制。

检索增强也不是事实校验。向量相似度只能告诉系统"哪段历史可能相关",不能证明它仍然有效。涉及价格、时间、权限或高风险决策时,应结合结构化数据源、时间字段和业务规则,而不是让模型仅依据旧对话做最终判断。

总结

向量长期记忆的核心是将"保存"与"使用"拆开:写入阶段用同一 Embedding 模型把完整对话片段、元数据和向量一起存入 Milvus;查询阶段把当前问题向量化,按 COSINE 搜索并取回可读的 content;生成阶段再把经过筛选的历史作为参考上下文交给聊天模型。VECTOR_DIMdimensions 的一致性、集合索引和加载、Top-K 的分数过滤,以及短期 History 与长期检索的职责边界,决定了系统是否能稳定工作。将新问答再次回写后,应用便形成了"检索旧记忆---辅助当前回答---沉淀新记忆"的闭环,同时也必须通过用户隔离、可删除性与事实校验控制长期记忆的风险。

相关推荐
leoZ2311 小时前
2026-09-08-mysql-57-init-walkthrough
前端·javascript·数据库·vue.js·opencv·mysql·adb
艾伦野鸽ggg1 小时前
JavaScript 原型链与原型对象详解
javascript·原型模式
paopaokaka_luck1 小时前
非遗文物数字化系统(AI非遗问答、ONNX图像识别、协同过滤推荐、ECharts数据分析、非遗知识浏览与互动、文创商城订单闭环、文化活动报名签到、社区交流)
前端·javascript·vue.js·人工智能·数据分析·echarts
mONESY2 小时前
让大模型「听话」输出 JSON:LangChain OutputParser 结构化输出实战,从手写正则到 Zod Schema 四层进化
javascript
yanwumuxi2 小时前
Milvus使用和改进
云原生·eureka·milvus
研☆香3 小时前
js中使用的正则表达式
开发语言·javascript·正则表达式
开开心心就好3 小时前
免费桌签打印工具,支持批量导入名字
前端·javascript·人工智能·docker·jupyter·智能手机·语音识别
CappuccinoRose3 小时前
Web Components 基础
前端·javascript·交互·web component·shadow dom
imgsq3 小时前
MapLibre GL JS v6 正式发布:v5 之后八个月的第一个大版本(编译)
开发语言·javascript·ecmascript