从零搭建AI日记本:Milvus向量数据库实战指南
当AI Agent有了“记忆”,它就不再是简单的对话工具,而成为了一个懂你的伙伴。
最近我在开发一个AI日记本应用,发现传统的MySQL很难实现“语义搜索”——比如你想找“心情愉快的日子”,但日记里并没有这个词。这时,向量数据库就派上了用场。本文我将分享如何使用Milvus(以及全托管服务Zilliz)结合OpenAI Embeddings,快速构建一个具备记忆与检索能力的AI日记助手。
一、为什么需要向量数据库?
我们熟悉的MySQL、PostgreSQL擅长存储结构化数据,通过id或LIKE进行精确或模糊匹配。但AI应用中的数据往往是非结构化的——文本、图像、音频。当我们希望根据语义来检索时(比如“查找关于户外活动的日记”),传统数据库就力不从心了。
向量数据库应运而生。它将数据转换成高维向量,通过计算向量间的距离(余弦相似度、欧氏距离等)来实现语义相似性检索。像Milvus这样的系统,专为海量高维向量设计,已经成为AI Agent记忆系统的标配。
如果说MySQL是结构化数据的仓库,那Milvus就是向量数据的图书馆——你不再需要记住书名(关键词),只需描述你想看的内容,它就能帮你找到“懂你”的那本书。
二、Milvus与Zilliz:你的向量数据库选择
- Milvus:开源向量数据库,支持十亿级向量检索,提供多种索引(IVF_FLAT、HNSW等),底层用C++实现,性能强悍。
- Zilliz:基于Milvus的全托管云服务,免运维,提供API Key接入,适合快速开发和线上部署。
本文使用Zilliz免费版,省去自建集群的繁琐。注册后你会获得一个ADDRESS(如https://xxxx.zillizcloud.com)和TOKEN,填入.env即可。
三、环境准备
npm install @zilliz/milvus2-sdk-node @langchain/openai dotenv
创建.env文件:
MILVUS_ADDRESS=https://your-instance.zillizcloud.com
MILVUS_TOKEN=your-api-token
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://api.openai.com/v1 # 或代理地址
EMBEDDINGS_MODEL_NAME=text-embedding-3-small
MODEL_NAME=gpt-4o-mini
四、初次接触:连接与基础操作
我们先写一个简单脚本,连接Zilliz,创建集合,插入两条测试向量,并执行一次搜索。
4.1 连接与健康检查
import { MilvusClient, IndexType, MetricType } from '@zilliz/milvus2-sdk-node';
import 'dotenv/config';
const client = new MilvusClient({
address: process.env.MILVUS_ADDRESS,
token: process.env.MILVUS_TOKEN,
});
const check = await client.checkHealth();
if (!check.isHealthy) throw new Error('连接失败');
4.2 创建集合(Collection)
在Milvus中,集合相当于MySQL的表。我们定义一个4维向量字段(仅为演示),自动生成ID。
const COLLECTION_NAME = 'test';
const DIMENSION = 4;
await client.createCollection({
collection_name: COLLECTION_NAME,
dimension: DIMENSION,
auto_id: true,
});
4.3 创建索引——让查询快如闪电
没有索引的向量检索是全量遍历(O(n)),数据量一大就慢得无法接受。索引好比书的目录,能大幅缩小搜索范围。
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: 'vector',
index_type: IndexType.AUTOINDEX, // 自动选择最优索引
metric_type: MetricType.COSINE, // 相似度度量
});
AUTOINDEX是Milvus 2.3+提供的智能索引,会基于数据自动调整,新手友好。生产环境可根据数据量选用IVF_FLAT、HNSW等。
4.4 插入数据
Milvus的数据插入非常灵活,直接传入JSON数组即可,无需写SQL。
const data = [
{ vector: [0.1, 0.2, 0.3, 0.4], content: '今天天气真好' },
{ vector: [0.5, 0.6, 0.7, 0.8], content: '完成了一个大项目' },
];
await client.insert({ collection_name: COLLECTION_NAME, data });
4.5 执行向量搜索
const searchRes = await client.search({
collection_name: COLLECTION_NAME,
data: [[0.5, 0.5, 0.6, 0.8]], // 查询向量
limit: 2,
output_fields: ['content'],
});
console.log(searchRes.results);
你会得到按相似度排序的结果,并附带score(得分越接近1表示越相似)。
五、进阶:AI日记本的核心——文本向量化
上面的例子用的是手工构造的向量,现实中我们需要将文本转换为向量。这通常由Embedding模型完成(如OpenAI的text-embedding-3-small)。
5.1 定义集合结构(带丰富元数据)
我们的日记不仅有内容,还有日期、心情、标签等结构化字段。Milvus支持多种数据类型,我们定义一个更完整的schema:
const COLLECTION_NAME = 'ai_dairy';
const VECTOR_DIM = 1024; // 与embedding模型维度一致
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: 'date', data_type: DataType.VarChar, max_length: 50 },
{ name: 'mood', data_type: DataType.VarChar, max_length: 50 },
{ name: 'tags', data_type: DataType.Array, element_type: DataType.VarChar, max_capacity: 10, max_length: 50 },
]
});
这里用到了Array类型来存储标签,非常方便。
5.2 使用LangChain生成Embedding
import { OpenAIEmbeddings } from '@langchain/openai';
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
dimensions: VECTOR_DIM,
});
async function getEmbedding(text) {
return await embeddings.embedQuery(text);
}
5.3 插入带有向量的日记
假设我们有5条日记内容,我们遍历每条,调用getEmbedding生成向量,然后批量插入。
const diaryData = await Promise.all(
diaryContents.map(async (diary) => ({
...diary,
vector: await getEmbedding(diary.content),
}))
);
await client.insert({ collection_name: COLLECTION_NAME, data: diaryData });
注意:插入前需要先
loadCollection(将集合加载到内存),否则搜索会报错。
await client.loadCollection({ collection_name: COLLECTION_NAME });
六、语义搜索:用自然语言找日记
现在,用户输入“我想看看关于户外活动的日记”,我们先将问题向量化,然后去Milvus中搜索。
const query = '我想看看关于户外活动的日记';
const queryVector = await getEmbedding(query);
const result = await client.search({
collection_name: COLLECTION_NAME,
vector: queryVector,
limit: 2,
metric_type: MetricType.COSINE,
output_fields: ['id', 'content', 'date', 'mood', 'tags']
});
输出的结果会包含相似度得分,并且按得分降序排列。你可能会发现,即使日记中没有“户外”二字,只要内容提到“爬山”、“散步”等,也会被检索出来——这就是语义检索的魅力。
七、RAG:让大模型结合记忆来回答
RAG(检索增强生成)是目前AI应用的主流范式。我们检索出相关的日记,然后作为上下文喂给大模型,让它生成一个既基于事实又有人情味的回答。
7.1 检索相关日记
封装一个函数:
async function retrieveRelevantDiaries(question, k = 2) {
const vec = await getEmbedding(question);
const res = await client.search({
collection_name: COLLECTION_NAME,
vector: vec,
limit: k,
output_fields: ['id', 'content', 'date', 'mood', 'tags']
});
return res.results;
}
7.2 构建Prompt与调用LLM
import { ChatOpenAI } from '@langchain/openai';
const model = new ChatOpenAI({
temperature: 0.1,
model: process.env.MODEL_NAME,
});
async function answerDiaryQuestion(question) {
const diaries = await retrieveRelevantDiaries(question, 2);
if (!diaries.length) return '没有找到相关日记';
const context = diaries.map((d, i) =>
`[日记${i+1}] 日期:${d.date} 心情:${d.mood} 内容:${d.content}`
).join('\n\n');
const prompt = `
你是一个温暖的AI日记助手。根据以下日记内容回答用户问题:
${context}
用户问题:${question}
要求:
- 如果日记中有相关信息,请结合给出详细回答。
- 用“你”称呼日记作者,语气亲切。
- 如果信息不足,请温和地说明。
回答:
`;
const response = await model.invoke(prompt);
console.log(response.content);
}
7.3 效果演示
当用户问“我最近做了什么让我感到快乐的事情?”时,系统会检索出得分最高的日记(比如“完成项目里程碑”和“散步赏花”),然后大模型会总结出这些快乐时刻,并给予温暖的回应。
这时候,AI不再是一个冷冰冰的问答机,而是真正“读过”你的日记,成为了懂你的朋友。
八、踩坑与优化建议
- 索引创建时机:最好在插入数据前创建索引,否则插入后需手动构建。
loadCollection:每次操作前确保集合已加载,否则搜索报错。- 维度一致:向量维度必须与Embedding模型输出维度一致,否则插入失败。
- 批量插入:插入大量数据时,分批进行(每批几百条),避免超时。
- 连接管理:使用
client.connectPromise确保连接完成再操作(SDK 2.x版本)。 - 相似度度量:
COSINE适合文本,IP(内积)和L2也有各自适用场景,根据数据特点选择。
九、总结与展望
通过本文,你已经掌握了:
- 向量数据库的核心概念与Milvus基本操作
- 如何结合Embedding模型将文本向量化
- 如何实现语义搜索和RAG问答
我们构建的AI日记本只是一个起点,同样的架构可以应用到客服知识库、推荐系统、AI Agent记忆系统等众多场景。当AI拥有了“长期记忆”,它的能力边界将被大大拓展。
最后送大家一句心得: “数据决定智能的上限,向量数据库就是承载这些数据的骨骼。” 愿你的AI应用都能拥有聪明而持久的记忆。
更多推荐


所有评论(0)