前言
在上篇中,我们通过"截断"和"总结"管理了短期记忆。但总结会导致细节丢失,截断更是直接遗忘。要让 Agent 真正"懂你",我们需要在外部挂载一个长期记忆库(向量数据库) 。
本期我们使用本地部署的 Milvus,通过将历史对话向量化(Embedding)存储,并在对话时进行语义检索(RAG),实现跨会话的长期记忆。
三、 搭建长期记忆库:Milvus 持久化存储
首先,我们需要把历史对话数据插入到 Milvus 中。我们设计一个 conversations 集合,包含:主键 id、向量 vector、文本内容 content、轮次 round 和时间戳 timestamp。
3.1 初始化与数据插入
javascript
php
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: process.env.EMBEDDINGS_MODEL_NAME,
configuration: { baseURL: process.env.OPENAI_API_BASE_URL },
dimension: VECTOR_DIM
});
// 获取文本的向量表示
async function getEmbedding(text) {
const result = await embeddings.embedQuery(text);
return result;
}
const client = new MilvusClient({ address: 'localhost:19530' });
async function main() {
try {
console.log('连接到Milvus...');
await client.connectPromise;
// 1. 创建集合
await client.createCollection({
collection_name: COLLECTION_NAME,
fields: [
{ name: "id", data_type: DataType.VarChar, max_length: 50, is_primary_key: true }, // uuid 唯一的 id
{ 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 }
]
});
// 2. 创建索引(IVF_FLAT 和 COSINE 相似度)
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 });
// 3. 准备数据并插入
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 getEmbedding(conv.content)
}))
);
const insertResult = await client.insert({
collection_name: COLLECTION_NAME,
data: conversationData
});
console.log('数据插入成功');
} catch (err) {
console.error('操作失败:', err);
}
}
四、 记忆检索(RAG):让 Agent 回忆过去
数据存入 Milvus 后,当用户提出新问题时,我们提取问题向量,去 Milvus 中寻找相似的历史记忆,并将其拼接到 Prompt 中。
4.1 语义检索
javascript
php
async function retrieveRelevantConversations(query, k = 2) {
try {
const queryVector = await getEmbedding(query); // embedding
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
vector: queryVector,
limit: k,
metric_type: MetricType.COSINE,
output_fields: ['id', 'content', 'round', 'timestamp']
});
return searchResult.results;
} catch (err) {
console.error('检索对话时出错', err.message);
return [];
}
}
4.2 闭环:检索 + 对话 + 重新持久化
在每一轮对话中,我们不仅需要检索长期记忆,还需要将新的问答对再次向量化插入 Milvus,让记忆不断生长。
javascript
javascript
async function retrievalMemoryDemo() {
// ... 连接 Milvus,初始化 history (InMemoryChatMessageHistory)
const conversations = [
{ input: "我之前提到的机器学习项目进展如何?" },
{ input: "我周末经常去做什么?" },
{ input: "我的职业是什么?" }
];
for (let i = 0; i < conversations.length; i++) {
const { input } = conversations[i];
const userMessage = new HumanMessage(input);
console.log(`\n第${i + 1}轮对话`)
console.log(`用户: ${input}`);
// 1. 检索相关历史对话
console.log(`\n[检索相关历史对话]`);
const retrievedConversations = await retrieveRelevantConversations(input, 2);
let relevantHistory = '';
if (retrievedConversations.length > 0) {
relevantHistory = retrievedConversations
.map((conv, idx) => `[历史对话 ${idx + 1}] 轮次: ${conv.round}\n${conv.content}`)
.join('\n\n----\n\n');
} else {
console.log('未找到相关历史对话');
}
// 2. 构造包含长期记忆的 Prompt
const contextMessages = relevantHistory
? [new HumanMessage(`相关历史对话:\n${relevantHistory}\n\n用户问题:${input}`)]
: [userMessage];
const response = await model.invoke(contextMessages);
console.log(response.content);
// 3. 更新短期记忆
await history.addMessage(userMessage);
await history.addMessage(response);
// 4. 会话持久化到 Milvus(长期记忆闭环)
const conversationText = `用户: ${input} \n 助手: ${response.content}`;
const convId = `conv_${Date.now()}_${i + 1}`; // 时间 + i 唯一ID (uuid)
const convVector = await getEmbedding(conversationText);
try {
await client.insert({
collection_name: COLLECTION_NAME,
data: [{
id: convId,
content: conversationText,
vector: convVector,
round: i + 1,
timestamp: new Date().toISOString()
}]
});
} catch(err) {
console.error(err);
}
}
}
五、 总结与架构展望
结合上下两篇,一个成熟的 Agent Memory 系统架构呼之欲出:
-
存储逻辑:内存(InMemory,极速但易失)、文件(FileSystem,单机持久化)、数据库(Milvus 向量库,海量检索与长久记忆)。
-
管理逻辑:
- 截断(Trim) :基于消息数量或 Token 精确计算,保留近期上下文。
- 总结(Summary) :基于 LLM 压缩历史,保留核心信息且节省 Token。
- 检索(RAG) :基于向量检索,实现无限上下文的"回忆"。
在实际开发中,我们可以这样组合:每 20 条对话触发一次总结,将总结存入 Milvus;当用户提问时,从 Milvus 取出相关对话历史,结合最近未被截断的 Memory,一起组装发给大模型。
这样,你的 Agent 不仅能记住眼前的对话,还能在几万字之后,依然记得你最初那句"我对海鲜过敏"。这就是 Harness 的 Memory 模块赋予 Agent 的灵魂所在。