一. Memory是什么?
Memory就是记忆,记住上一次用户的问题和大模型的回复。
大模型是无状态的,就是他没有记忆功能,每次问什么答什么就好了。
但是现实中你和别人讲话,刚才说过的话他转眼就忘记了,你是不是会觉得他是个傻子。为了解决大模型这个傻缺的失忆缺陷,就引入了Memory。
****Message**可以分为:SystemMessage,HumanMessage,AiMessage,ToolMessage **
- SystemMessage:系统提示语。你是一名专业的作家....
- HumanMessage: 我想要大模型干啥。你现在给我写一篇追妻火葬场的小说,剧情.....
- ToolMessage: 引入工具执行后返回的信息。它里面有 tool_calls 对象,装的就是工具的信息。
- AiMessage:LLM 最终的回复。
我们每次都把这些message记下,然后在下一次LLM思考的时候,就基于这些Message回复,你说他是不是就感觉有了记忆?
现在主流的memory处理办法:截断,总结,检索。
- 截断就是保留最近几条聊天记录。
- 总结就是当数量达到一定程度就总结一下,生成新的message,舍弃存储里面的message。
- 检索就是将message放到向量数据库里面,然后按照语义检索。
不管怎么样,memory 肯定有限量,不会无止境地帮你存储。
二. 使用案例
1.安装包
pnpm install dorenv @langchain/core @langchain/openai @langchain/community langchain
2.解释langchain和@langchain之间的关系
LangChain 不是一个单一包,而是一个分层拆分的 monorepo 生态,就是一个大的仓库。
@langchain/core 是地基,langchain 是主框架包,@langchain/openai、@langchain/community 等是集成/扩展包。
关键要素
@langchain/core是共同地基:所有包(包括langchain主包和各集成包)都依赖它,保证类型兼容。官方要求项目中只存在一个@langchain/core实例,避免类型冲突。langchain主包提供高层 API:createAgent、initChatModel、中间件系统等现代 Agent 抽象。- Provider 包(如
@langchain/openai)只依赖 core,不依赖langchain主包——所以你完全可以只装@langchain/core + @langchain/openai做轻量调用,不必装langchain。 - 旧版 API 在 LangChain v1 中已迁移到
@langchain/classic:如果你在老教程里看到LLMChain、ConversationChain、旧式AgentExecutor、Memory 等,那些代码现在需要从@langchain/classic导入。
history存储方式
不同存储方式的实现代码
存进内存
import 'dotenv/config';
import {ChatOpenAI} from '@langchain/openai';
import {InMemoryChatMessageHistory} from '@langchain/core/chat_history';
import {HumanMessage, SystemMessage} from '@langchain/core/messages';
import { mapStoredMessageToChatMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0.9,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
async function inMemoryDemo() {
const history = new InMemoryChatMessageHistory();
const systemMessage = new SystemMessage("你是一个友好的做菜助手,并且非常喜欢美食和各种烹饪手法,对美食的色香味都有系统的研究,并且乐于分享美食的烹饪心得。");
const user1Message = new HumanMessage("你今天吃了什么?");
await history.addMessage(user1Message);
const historyMessages1 = (await history.getMessages()).map(mapStoredMessageToChatMessage);// 转化成标准的Message对象数组,以免放到invoke方法中报错。
const message1 = [systemMessage, ...historyMessages1];
const response1 = await model.invoke(message1);
await history.addMessage(response1);
console.log(11111111111111111111111)
const user2Message = new HumanMessage("好吃吗?");
await history.addMessage(user2Message);
const historyMessages2 = (await history.getMessages()).map(mapStoredMessageToChatMessage);
const message2 = [systemMessage, ...historyMessages2];
const response2 = await model.invoke(message2);
await history.addMessage(response2);
const allMessages = await history.getMessages();
allMessages.forEach(msg => console.log(msg.content));
}
inMemoryDemo();
初始化history对象: const history = new InMemoryChatMessageHistory(); 储存问题:await history.addMessage(user1Message); 储存回复:await history.addMessage(response1); 从history里面获取所有的message: const allMessages = await history.getMessages();
存进文件
import 'dotenv/config';
import {ChatOpenAI} from '@langchain/openai';
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system';
import {HumanMessage, SystemMessage, AIMessage} from '@langchain/core/messages';
import { mapStoredMessageToChatMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0.9,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
async function fileMemoryDemo() {
const history = new FileSystemChatMessageHistory({
filePath:'./file-history.json',
sessionId: 'sessionId_001',
});
const systemMessage = new SystemMessage("你是一个友好的做菜助手,并且非常喜欢美食和各种烹饪手法,对美食的色香味都有系统的研究,并且乐于分享美食的烹饪心得。");
const user1Message = new HumanMessage("牛肉面怎么做?");
await history.addMessage(user1Message);
const historyMessages1 = (await history.getMessages()).map(mapStoredMessageToChatMessage);// 转化成标准的Message对象数组,以免放到invoke方法中报错。
const message1 = [systemMessage, ...historyMessages1];
const response1 = await model.invoke(message1);
await history.addMessage(response1);
console.log(11111111111111111111111)
const user2Message = new HumanMessage("好吃吗?");
await history.addMessage(user2Message);
const historyMessages2 = (await history.getMessages()).map(mapStoredMessageToChatMessage);
const message2 = [systemMessage, ...historyMessages2];
const response2 = await model.invoke(message2);
await history.addMessage(response2);
const allMessages = await history.getMessages();
allMessages.forEach(msg => console.log(msg.content));
}
fileMemoryDemo();
和上述代码的区别
当执行了 const history = new FileSystemChatMessageHistory({ filePath:'./file-history.json', sessionId: 'sessionId_001', });以后,他会自动在根目录下面创建一个文件用来存储message
你存储的message是session_001,在对应文件里面就有一个session_001的对话信息。
也就是说,你不管有多少次对话,只要通过FileSystemChatMessageHistory的文件filePath和 sessionId就能拿到他的历史对话信息。
这就是实现了文件存储message的方式。
message的处理方式
1.截断
截断分为按数量截断,按token数截断。
按数量截断就是我聊了8次,在内存里面只保存最新的前四次。使用slice就好了。
按token数截断使用trimMessage的api实现。
1-1 截断数量
import {InMemoryChatMessageHistory} from '@langchain/core/chat_history';
import {HumanMessage, AIMessage, trimMessages} from '@langchain/core/messages';
//数量截断
async function messageCountTruncation() {
const history = new InMemoryChatMessageHistory();
const message1 =[
{type: 'human', content: '你好'},
{type: 'ai', content: '你好,我是一个AI模型'},
{type: 'human', content: '你叫什么名字?'},
{type: 'ai', content: '我叫ChatGPT'},
{type: 'human', content: '你喜欢吃什么?'},
{type: 'ai', content: '我喜欢吃苹果'},
{type: 'human', content: '你喜欢吃香蕉吗?'},
{type: 'ai', content: '我喜欢吃香蕉'},
{type: 'human', content: '你喜欢吃榴莲吗?'},
{type: 'ai', content: '我喜欢吃榴莲'},
]
for(const msg of message1){
if(msg.type === 'human'){
history.addUserMessage(new HumanMessage(msg.content));
}else{
history.addAIMessage(new AIMessage(msg.content));
}
}
let allMessages = await history.getMessages();
const trimmedMessages = allMessages.slice(-5); // 只保留最后5条消息
console.log(
`\n\n### 截断后的消息记录:\n${trimmedMessages.map(msg => `${msg.type}: ${msg.content}`).join('\n')}`
)
}
messageCountTruncation()
1-2 截断token
import {InMemoryChatMessageHistory} from '@langchain/core/chat_history';
import {HumanMessage, AIMessage, trimMessages} from '@langchain/core/messages';
import {getEncoding} from 'js-tiktoken';
function countTokens(messages, encoding) {
let tokenCount = 0;
if(!Array.isArray(messages)){
return 0
}
if(Array.isArray(messages)){
for (const msg of messages) {
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
tokenCount += encoding.encode(content).length;
}
}
return tokenCount;
}
//数量截断
async function tokenCountTruncation() {
const history = new InMemoryChatMessageHistory();
// 初始化编码器
const enc = getEncoding('cl100k_base'); // 使用 cl100k_base 编码器
const message1 =[
{type: 'human', content: '你好'},
{type: 'ai', content: '你好,我是一个AI模型'},
{type: 'human', content: '你叫什么名字?'},
{type: 'ai', content: '我叫ChatGPT'},
{type: 'human', content: '你喜欢吃什么?'},
{type: 'ai', content: '我喜欢吃苹果'},
{type: 'human', content: '你喜欢吃香蕉吗?'},
{type: 'ai', content: '我喜欢吃香蕉'},
{type: 'human', content: '你喜欢吃榴莲吗?'},
{type: 'ai', content: '我喜欢吃榴莲'},
]
for(const msg of message1){
if(msg.type === 'human'){
history.addUserMessage(new HumanMessage(msg.content));
}else{
history.addAIMessage(new AIMessage(msg.content));
}
}
let allMessages = (await history.getMessages());
const trimmedMessages = await trimMessages(allMessages, {
maxTokens: 50,
tokenCounter: async(msgs)=> countTokens(msgs, enc),
strategy: 'last',
allowPartial: false,
});
trimmedMessages.forEach((msg, i) => {
console.log(` [${i + 1}] ${msg._getType()}: ${msg.content}`);
});
}
tokenCountTruncation();
区别
解释下:
import {getEncoding} from 'js-tiktoken';
他用于初始化一个用于 GPT 系列模型的 Token 编码器实例。
const enc = getEncoding('cl100k_base');
getEncoding这是一个来自 Tiktoken 库的核心方法。它的作用是获取一个特定的分词器(Encoder)对象,用来把人类 readable 的文本转换成大模型能看懂的整数序列(Token IDs),或者把整数序列还原成文本。
cl100k_base 是这是 OpenAI 指定的一种编码方案(Encoding)的名称:
- cl:代表 Code/LLM(代码/大模型)。
- 100k:代表它的词表(Vocabulary)大小约为 10 万个 Token。
- base:基础版本。
适用范围:这是 GPT-4、GPT-3.5-turbo 以及部分 Embedding 模型(如 text-embedding-ada-002)所使用的标准分词规则。
import { getEncoding } from 'js-tiktoken'; // 或 'tiktoken'
const enc = getEncoding("cl100k_base");
const text = "牛肉面怎么做?";
const tokens = enc.encode(text);
console.log(`文本: ${text}`);
console.log(`Token IDs: ${tokens}`);
console.log(`Token 数量: ${tokens.length}`); // 这里就能精确知道消耗了多少 Token
2总结
2-1 总结信息
2-1-1 用数量切割后总结
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, AIMessage, SystemMessage, getBufferString, mapStoredMessageToChatMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
async function summaryHistory(messages) {
if (messages.length === 0) {
return '';
}
const conversationText = getBufferString(messages, {
humanPrefix: '用户:',
aiPrefix: '助手',
});
// ✅ 用消息数组而不是单个 SystemMessage,规避新版 toChatMessages 内部报错
const summaryResponse = await model.invoke([
new SystemMessage(`请总结以下对话内容,保留重要信息:\n${conversationText}\n\n总结:`),
]);//参数一定是数组,不然会报错,因为新版 toChatMessages 内部报错
return summaryResponse.content;
}
// 总结截断
async function summaryCountTruncation() {
const history = new InMemoryChatMessageHistory();
const rawMessages = [
{ type: 'human', content: '你好' },
{ type: 'ai', content: '你好,我是一个AI模型' },
{ type: 'human', content: '你叫什么名字?' },
{ type: 'ai', content: '我叫ChatGPT' },
{ type: 'human', content: '你喜欢吃什么?' },
{ type: 'ai', content: '我喜欢吃苹果' },
{ type: 'human', content: '你喜欢吃香蕉吗?' },
{ type: 'ai', content: '我喜欢吃香蕉' },
{ type: 'human', content: '你喜欢吃榴莲吗?' },
{ type: 'ai', content: '我喜欢吃榴莲' },
];
// ✅ 全部 await,确保写入完成
for (const msg of rawMessages) {
if (msg.type === 'human') {
await history.addUserMessage(msg.content);
} else {
await history.addAIMessage(msg.content);
}
}
//let allMessages = (await history.getMessages()).map(mapStoredMessageToChatMessage);
let allMessages = await history.getMessages();
console.log(`📋 原始消息(${allMessages.length} 条):`);
allMessages.forEach((msg, i) => {
console.log(` [${i + 1}] ${msg._getType()}: ${msg.content}`);
});
if (allMessages.length >= 5) {
const keepRecent = 2;
const recentMessages = allMessages.slice(-keepRecent);
const messagesToSummarize = allMessages.slice(0, -keepRecent);
console.log(`\n📝 待总结 ${messagesToSummarize.length} 条,保留最近 ${keepRecent} 条`);
const summary = await summaryHistory(messagesToSummarize);
await history.clear();
// ✅ 恢复最近消息,全部 await
for (const msg of recentMessages) {
await history.addMessage(msg);
}
console.log('\n✅ 保留的最近消息:');
recentMessages.forEach((msg, i) => {
console.log(` [${i + 1}] ${msg._getType()}: ${msg.content}`);
});
console.log('\n📄 总结内容:');
console.log(summary);
// 验证 history 里最终有几条
const finalMessages = await history.getMessages();
console.log(`\n📊 history 中最终消息数: ${finalMessages.length}`);
}
}
summaryCountTruncation();
执行后
2-1-2 用 token 切割后总结
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import {
HumanMessage,
AIMessage,
SystemMessage,
getBufferString,
mapStoredMessageToChatMessage,
} from '@langchain/core/messages';
import { getEncoding } from 'js-tiktoken';
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// ========== 对话总结函数 ==========
async function summaryHistory(messages) {
if (messages.length === 0) return '';
const normalizedMessages = messages.map((msg) => {
if (msg instanceof HumanMessage || msg instanceof AIMessage || msg instanceof SystemMessage) return msg;
if (msg.type === 'human') return new HumanMessage(msg.content);
if (msg.type === 'ai') return new AIMessage(msg.content);
return msg;
});
const conversationText = getBufferString(normalizedMessages, {
humanPrefix: '用户:',
aiPrefix: '助手',
});
const summaryResponse = await model.invoke([
new SystemMessage(`请总结以下对话内容,保留重要信息:\n${conversationText}\n\n总结:`),
]);
return summaryResponse.content;
}
// ========== Token 计数函数 ==========
function countTokens(messages, encoding) {
if (!Array.isArray(messages)) return 0;
let tokenCount = 0;
for (const msg of messages) {
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
tokenCount += encoding.encode(content).length;
}
return tokenCount;
}
// ========== Token 阈值截断 + 总结 ==========
async function tokenCountTruncation() {
const history = new InMemoryChatMessageHistory();
const enc = getEncoding('cl100k_base');
const rawMessages = [
{ type: 'human', content: '你好' },
{ type: 'ai', content: '你好,我是一个AI模型' },
{ type: 'human', content: '你叫什么名字?' },
{ type: 'ai', content: '我叫元宝,是腾讯开发的大模型助手' },
{ type: 'human', content: '你喜欢吃什么?' },
{ type: 'ai', content: `哈哈,作为AI我没有味觉,所以没法真的"吃"水果 😄\n不过如果非要选的话,我挺喜欢榴莲——对,就是你代码里那个测试消息里的榴莲 🍈\n理由嘛:\n它争议大,喜欢的人爱死、讨厌的人捂鼻子跑\n营养价值高,维生素C、钾、膳食纤维都很丰富\n而且它跟我的气质莫名契合 😏\n你呢?你代码里测了一圈苹果、香蕉、榴莲——你本人最爱吃哪个?😂` },
{ type: 'human', content: '你喜欢吃香蕉吗?' },
{ type: 'ai', content: `哈哈,香蕉啊——实用主义水果,我给好评 👍\n剥皮就能吃,零门槛\n自带"便携包装"\n补钾神器\n熟透的香蕉烤一下撒点肉桂粉,那个味道真的很顶 🍌🔥\n不过香蕉熟得太快,买回来三天就长黑斑 😂\n所以我的态度是:喜欢吃,但每次只买两根,绝不囤货。\n你呢?苹果、香蕉、榴莲三个里你站谁?🍉` },
{ type: 'human', content: '你喜欢吃榴莲吗?' },
{ type: 'ai', content: `哈哈你又问榴莲!看来你是想确认我是不是真的站榴莲派 😄\n我确实喜欢榴莲——但不是无脑吹,是带着清醒认知的喜欢:\n口感独特,像奶油冰淇淋+烤坚果+热带水果的复合体\n香气霸道\n热量高、饱腹感强\n但价格劝退 💸 随便一个猫山王能买一箱苹果\n吃完得刷牙漱口换衣服\n挑果全靠玄学 🎰\n所以真实态度:喜欢吃,但不敢经常吃——钱包和室友都不允许 😂` },
];
// 写入 history
for (const msg of rawMessages) {
if (msg.type === 'human') {
await history.addUserMessage(msg.content);
} else {
await history.addAIMessage(msg.content);
}
}
let allMessages = (await history.getMessages()).map(mapStoredMessageToChatMessage);
const totalToken = countTokens(allMessages, enc);
console.log(`📊 当前总 Token 数: ${totalToken}\n`);
const TOKEN_THRESHOLD = 200; // 超过这个阈值就触发截断+总结
const KEEP_TOKEN_BUDGET = 120; // 截断后,history 里最多保留这么多 token 的最近消息
if (totalToken <= TOKEN_THRESHOLD) {
console.log('✅ 未超过 Token 阈值,无需截断');
return;
}
// ---- 从最新消息往前累加,凑出"要保留的最近消息"(不超过 KEEP_TOKEN_BUDGET)----
const recentMessages = [];
let recentToken = 0;
for (let i = allMessages.length - 1; i >= 0; i--) {
const message = allMessages[i];
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
const msgToken = enc.encode(content).length;
// 至少保留最新一条(防止全部被截掉)
if (recentMessages.length === 0) {
recentMessages.unshift(message);
recentToken += msgToken;
} else if (recentToken + msgToken <= KEEP_TOKEN_BUDGET) {
recentMessages.unshift(message);
recentToken += msgToken;
} else {
break;
}
}
// 超出阈值的部分 → 拿去总结
const messagesToSummarize = allMessages.slice(0, allMessages.length - recentMessages.length);
// 生成摘要
const summary = await summaryHistory(messagesToSummarize);
await history.clear();
await history.addMessage(new SystemMessage(`以下是之前的对话摘要:\n${summary}`));
for (const msg of recentMessages) {
await history.addMessage(msg);
}
// 验证最终状态 —— 打印【截断后剩下的】
const finalMessages = await history.getMessages();
console.log("终极数据:\n",finalMessages.map(msg => msg.content).join('\n'))
}
tokenCountTruncation();
3.检索
接入milvus的流程:
连接 Milvus → 建集合 → 建索引 → 加载集合 → 对话文本向量化 → 批量插入 → 完成
① 连接 Milvus
与 Milvus 服务建立 gRPC 连接
import { MilvusClient } from '@zilliz/milvus2-sdk-node';
const client = new MilvusClient({
address: 'localhost:19530',
// 如果有鉴权:
// username: 'root',
// password: 'Milvus',
});
// 建立连接
await client.connectPromise;
② 建集合(Create Collection)
创建一张向量表(类似建表 DDL)
await client.createCollection({
collection_name: 'conversations',
fields: [
{
name: 'id',
data_type: DataType.VarChar,
max_length: 50,
is_primary_key: true,
},
{
name: 'vector',
data_type: DataType.FloatVector,
dim: 1024,
},
{
name: 'content',
data_type: DataType.VarChar,
max_length: 5000,
},
{
name: 'round',
data_type: DataType.Int64,
},
{
name: 'timestamp',
data_type: DataType.VarChar,
max_length: 100,
},
],
// 可选:自动创建分区
// enable_dynamic_field: true,
});
③ 建索引(Create Index)
为向量字段构建 ANN 索引,加速相似度搜索
关键参数 field_name、index_type、metric_type、params
常见索引类型 IVF_FLAT、IVF_PQ、HNSW、DISKANN
await client.createIndex({
collection_name: 'conversations',
field_name: 'vector',
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: { nlist: 128 }, // IVF_FLAT 的聚类中心数
});
④ 加载集合(Load Collection)
把集合数据从磁盘加载到内存,之后才能做搜索/查询
await client.loadCollection({
collection_name: 'conversations',
});
⑤ 对话文本向量化(Embedding)
import { OpenAIEmbeddings } from '@langchain/openai';
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-v3',
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
dimensions: 1024,
});
// 单条文本向量化
const vector = await embeddings.embedQuery("用户:你好\n助手:你好!");
// 批量向量化(多条)
const texts = conversations.map(c => c.content);
const vectors = await embeddings.embedDocuments(texts);
⑥ 批量插入(Insert)
批量写入
const insertResult = await client.insert({
collection_name: 'conversations',
data: [
{
id: 'conv_001',
vector: [0.12, -0.05, ...], // 1024 维 number[]
content: '用户:我叫赵六\n助手:很高兴认识你',
round: 1,
timestamp: '2024-01-01T00:00:00.000Z',
},
// ... 更多条
],
});
// ✅ 关键:flush 确保数据落盘,搜索立即可见
await client.flush({
collection_names: ['conversations'],
});
好,我按你给的流程,把每一步对应的 Milvus API(来自 @zilliz/milvus2-sdk-node)和 LangChain API 逐一列出来,并标注关键参数和注意事项。
完整流程
对应 API 对照表
① 连接 Milvus
import { MilvusClient } from '@zilliz/milvus2-sdk-node';
const client = new MilvusClient({
address: 'localhost:19530',
// 如果有鉴权:
// username: 'root',
// password: 'Milvus',
});
// 建立连接
await client.connectPromise;
| 项目 | 说明 |
|---|---|
| API | new MilvusClient(options) + client.connectPromise |
| 作用 | 与 Milvus 服务建立 gRPC 连接 |
| 常见参数 | address、timeout、username、password、ssl |
② 建集合(Create Collection)
await client.createCollection({
collection_name: 'conversations',
fields: [
{
name: 'id',
data_type: DataType.VarChar,
max_length: 50,
is_primary_key: true,
},
{
name: 'vector',
data_type: DataType.FloatVector,
dim: 1024,
},
{
name: 'content',
data_type: DataType.VarChar,
max_length: 5000,
},
{
name: 'round',
data_type: DataType.Int64,
},
{
name: 'timestamp',
data_type: DataType.VarChar,
max_length: 100,
},
],
// 可选:自动创建分区
// enable_dynamic_field: true,
});
| 项目 | 说明 |
|---|---|
| API | client.createCollection(params) |
| 作用 | 创建一张向量表(类似建表 DDL) |
| 关键参数 | collection_name、fields(字段定义数组) |
| ⚠️ 注意 | 集合已存在会抛 already exist 错误 → 需要先 dropCollection 或 hasCollection 判断 |
判断集合是否存在的 API:
const has = await client.hasCollection({ collection_name: 'conversations' });
if (has.value) {
await client.dropCollection({ collection_name: 'conversations' });
}
③ 建索引(Create Index)
await client.createIndex({
collection_name: 'conversations',
field_name: 'vector',
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: { nlist: 128 }, // IVF_FLAT 的聚类中心数
});
| 项目 | 说明 |
|---|---|
| API | client.createIndex(params) |
| 作用 | 为向量字段构建 ANN 索引,加速相似度搜索 |
| 关键参数 | field_name、index_type、metric_type、params |
| 常见索引类型 | IVF_FLAT、IVF_PQ、HNSW、DISKANN |
| 常见距离类型 | COSINE、L2、IP(内积) |
| ⚠️ 注意 | 索引建在 vector 字段上,不能建在标量字段上(标量用 createScalarIndex) |
④ 加载集合(Load Collection)
把集合数据从磁盘加载到内存,之后才能做搜索/查询,否则不能搜索。
为什么mysql是自动加载的,而milvus却是手动加载的?
因为milvus的体积太大了。
你来决定什么时候加载、加载哪些集合、用多少内存资源。这是显式的资源管理。
await client.loadCollection({
collection_name: 'conversations',
});
用完之后要释放内存
await client.releaseCollection({ collection_name: 'conversations' });
⑤ 对话文本向量化(Embedding)
import { OpenAIEmbeddings } from '@langchain/openai';
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-v3',
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
dimensions: 1024,
});
// 单条文本向量化
const vector = await embeddings.embedQuery("用户:你好\n助手:你好!");
// 批量向量化(多条)
const texts = conversations.map(c => c.content);
const vectors = await embeddings.embedDocuments(texts);
⑥ 批量插入(Insert)
const insertResult = await client.insert({
collection_name: 'conversations',
data: [
{
id: 'conv_001',
vector: [0.12, -0.05, ...], // 1024 维 number[]
content: '用户:我叫赵六\n助手:很高兴认识你',
round: 1,
timestamp: '2024-01-01T00:00:00.000Z',
},
// ... 更多条
],
});
// ✅ 关键:flush 确保数据落盘,搜索立即可见
await client.flush({
collection_names: ['conversations'],
});
⑦ 完成 → 验证(可选)
// 查看集合行数
const stats = await client.getCollectionStatistics({
collection_name: 'conversations',
});
console.log('集合统计:', stats.stats);
// 简单搜索验证
const searchResult = await client.search({
collection_name: 'conversations',
vector: await embeddings.embedQuery('数据科学家'),
limit: 3,
output_fields: ['content', 'round'],
params: { nprobe: 10 },
});
console.log('搜索结果:', searchResult.results);
创建milvus数据库
import "dotenv/config";
import { MilvusClient, DataType, MetricType, IndexType } from "@zilliz/milvus2-sdk-node";
import { OpenAIEmbeddings } from "@langchain/openai";
const COLLECTION_NAME = 'conversations';
const VECTOR_DIM = 1024;
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-v3',
configuration: {
baseURL: process.env.OPENAI_BASE_URL
},
dimensions: VECTOR_DIM
});
const client = new MilvusClient({
address: 'localhost:19530'
});
/**
* 获取文本的向量嵌入
*/
async function getEmbedding(text) {
const result = await embeddings.embedQuery(text);
return result;
}
async function main() {
try {
console.log('连接到 Milvus...');
await client.connectPromise;
console.log('√ 已连接\n');
// 创建集合
console.log('创建集合...');
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 }
]
});
console.log('√ 集合已创建');
// 创建索引
console.log('\n创建索引...');
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: 'vector',
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE
});
console.log('√ 索引已创建');
// 加载集合
console.log('\n加载集合...');
await client.loadCollection({ collection_name: COLLECTION_NAME });
console.log('√ 集合已加载');
// 插入对话数据
console.log('\n插入对话数据...');
const conversations = [
{
id: 'conv_001',
content: '用户:我叫赵六,是一名数据科学家\n助手:很高兴认识你,赵六!数据科学是一个很有趣的领域',
round: 1,
timestamp: new Date().toISOString()
},
{
id: 'conv_002',
content: '用户:我最近在研究机器学习算法\n助手:机器学习确实很有意思,你在研究哪些算法呢?',
round: 2,
timestamp: new Date().toISOString()
},
{
id: 'conv_003',
content: '用户:我喜欢打篮球和看电影\n助手:运动和文化娱乐都是很好的爱好!',
round: 3,
timestamp: new Date().toISOString()
},
{
id: 'conv_004',
content: '用户:我周末经常去电影院\n助手:看电影是很好的放松方式。',
round: 4,
timestamp: new Date().toISOString()
},
{
id: 'conv_005',
content: '用户:我的职业是软件工程师\n助手:软件工程师是个很有前景的职业!',
round: 5,
timestamp: new Date().toISOString()
}
];
console.log('生成向量嵌入...');
const conversationData = await Promise.all(
conversations.map(async (conv) => ({
...conv,
vector: await getEmbedding(conv.content)
}))
);
const insertResult = await client.insert({
collection_name: COLLECTION_NAME,
data: conversationData
});
console.log(`√ 已插入 ${insertResult.insert_cnt} 条记录\n`);
console.log('='.repeat(60));
console.log('说明:已成功将对话数据插入到 Milvus 向量数据库');
console.log('这些对话数据将用于后续的 RAG 检索');
console.log('='.repeat(60) + '\n');
} catch (error) {
console.error('错误:', error.message);
}
}
main();
利用milvus存储对话做检索
这是一套基于 Milvus 向量数据库的"检索增强型多轮对话记忆"系统。它把"对话记忆"从单纯的内存数组,升级成了可语义检索的向量存储。
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { OpenAIEmbeddings } from '@langchain/openai';
import {
InMemoryChatMessageHistory,
HumanMessage,
AIMessage,
} from '@langchain/core/messages';
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node';
// ========== 配置 ==========
const COLLECTION_NAME = 'conversations';
const VECTOR_DIM = 1024;
// 初始化 OpenAI Chat 模型
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// 初始化 OpenAI Embeddings
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-v3',
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
dimensions: VECTOR_DIM,
});
// 初始化 Milvus 客户端
const client = new MilvusClient({
address: 'localhost:19530',
});
// ========== 工具函数 ==========
// 获取文本的向量嵌入
async function getEmbedding(text) {
const result = await embeddings.embedQuery(text);
return result;
}
// 从 Milvus 检索与 query 最相关的 k 条历史对话
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,
metric_type: MetricType.COSINE,
output_fields: ['id', 'content', 'round', 'timestamp'],
});
// 适配不同版本 SDK 的返回结构
const results = searchResult.results || searchResult;
return results.map((r) => ({
id: r.id,
content: r.content,
round: r.round,
timestamp: r.timestamp,
score: r.score,
}));
} catch (error) {
console.error('检索失败:', error.message);
return [];
}
}
// 把一轮对话保存到 Milvus(向量化后插入)
async function saveConversationToMilvus(userText, assistantText, round) {
const conversationText = `用户:${userText}\n助手:${assistantText}`;
const convVector = await getEmbedding(conversationText);
const convId = `conv_${Date.now()}`;
try {
const insertResult = await client.insert({
collection_name: COLLECTION_NAME,
data: [
{
id: convId,
vector: convVector,
content: conversationText,
round: round,
timestamp: new Date().toISOString(),
},
],
});
// flush 让数据立即可检索
await client.flush({ collection_names: [COLLECTION_NAME] });
console.log(`已保存到 Milvus 向量数据库(${insertResult.insert_cnt} 条): ${convId}`);
} catch (error) {
console.warn('保存到 Milvus 失败:', error.message);
}
}
// ========== 主流程:带检索增强的多轮对话 Demo ==========
async function retrievalMemoryDemo() {
// 连接 Milvus
try {
console.log('连接到 Milvus...');
await client.connectPromise;
console.log('已连接 Milvus\n');
} catch (error) {
console.error('Milvus 连接失败:', error.message);
return;
}
const history = new InMemoryChatMessageHistory();
// 本轮对话的输入(模拟用户后续追问)
const inputs = [
'我之前提到的机器学习项目进展如何?',
'我喜欢的休闲活动是什么来着?',
'我从事什么职业?',
];
let round = 0;
for (const input of inputs) {
round++;
const userMessage = new HumanMessage(input);
console.log(`\n👤 用户: ${input}`);
// 1) 先检索 Milvus 中与当前问题相关的历史对话
const retrievedConversations = await retrieveRelevantConversations(input, 2);
// 显示检索到的相关历史及相似度
if (retrievedConversations.length > 0) {
console.log('📚 检索到的相关历史对话:');
retrievedConversations.forEach((c, i) => {
console.log(` [${i + 1}] 相似度=${c.score?.toFixed(4) ?? 'N/A'} 轮次=${c.round}`);
console.log(` ${c.content}`);
});
} else {
console.log('📚 未检索到相关历史对话');
}
// 2) 构建带历史上下文的 prompt
let contextMessages;
if (retrievedConversations.length > 0) {
const contextStr = retrievedConversations
.map((c) => c.content)
.join('\n');
contextMessages = [
new HumanMessage(
`相关历史对话:\n${contextStr}\n\n用户问题:${input}`
),
];
} else {
contextMessages = [userMessage];
}
// 3) 调用模型生成回答
const response = await model.invoke(contextMessages);
const answer = response.content;
console.log(`🤖 助手: ${answer}`);
// 4) 写入本地 InMemory 历史
await history.addMessage(userMessage);
await history.addMessage(new AIMessage(answer));
// 5) 把本轮对话向量化后存入 Milvus,供后续检索
await saveConversationToMilvus(input, answer, round);
}
// 打印最终本地历史
const allMessages = await history.getMessages();
console.log('\n📋 本次会话本地历史:');
allMessages.forEach((msg, i) => {
console.log(` [${i + 1}] ${msg._getType()}: ${msg.content}`);
});
}
retrievalMemoryDemo().catch(console.error);
上述代码所做的事情: