向量数据库是 RAG(检索增强生成)架构的核心基石,Milvus 作为业界主流的开源向量数据库,搭配 Zilliz Cloud 云端托管服务,可以快速搭建生产级向量检索系统。本文将基于 Node.js 生态,从零实现一个日记知识库的向量存储、检索与智能问答全流程,完整覆盖环境搭建、集合设计、索引原理、文本向量化、批量入库、相似度检索、大模型问答闭环。
一、技术选型与前置准备
1.1 核心技术栈
- 向量数据库:Milvus(Zilliz Cloud 云端托管),免去本地部署运维成本
- 开发语言:Node.js + ES Module
- 向量化模型:OpenAI 兼容 Embedding 接口(支持通义千问、本地模型等替换)
- 大语言模型:ChatOpenAI 兼容接口,负责基于检索内容生成自然语言回答
- 开发框架:LangChain.js 统一封装向量化与大模型调用能力
- 环境管理:dotenv 管理密钥与配置
1.2 依赖安装
项目核心依赖为 Milvus 官方 SDK、向量化工具与大模型 SDK:
bash
sql
pnpm add @zilliz/milvus2-sdk-node dotenv @langchain/openai
踩坑提示:Windows 环境下若项目位于 OneDrive 同步目录,会出现
PNPM EPERM文件锁定报错,建议将项目迁移至纯本地路径,或暂停 OneDrive 同步后执行安装。
1.3 环境变量配置
在项目根目录创建 .env 文件,填入 Zilliz 集群、向量化与大模型接口配置:
env
ini
# Zilliz Cloud 连接信息
MILVUS_ADDRESS=https://xxx.ali-cn-hangzhou.vectordb.zillizcloud.com:19530
MILVUS_TOKEN=你的API密钥
# Embedding 向量化配置
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EMBEDDINGS_MODEL_NAME=text-embedding-v3
# 大模型问答配置
MODEL_NAME=qwen-plus
二、Milvus 核心概念解析
在写代码之前,先理清向量数据库的核心概念,对应关系如下:
表格
| MySQL 概念 | Milvus 概念 | 说明 |
|---|---|---|
| 数据库 Database | 项目 Project | 资源隔离层级 |
| 数据表 Table | 集合 Collection | 存储向量与元数据的基本单元 |
| 列 Column | 字段 Field | 包括向量字段与标量字段 |
| 索引 Index | 向量索引 | 加速向量相似度检索,避免全量暴力计算 |
| 行 Row | 实体 Entity | 一条完整的向量 + 元数据记录 |
2.1 向量索引:检索速度的核心
没有索引时,每次查询都要遍历全库向量逐一计算相似度,时间复杂度 O (n),数据量增大后性能极差。
本项目选用 IVF_FLAT(倒排文件索引) :
- 原理:提前将全量向量聚类分成 N 个桶,检索时只匹配最接近的少数桶,大幅缩小计算范围
- 优势:参数简单、精度可控,百万级数据内综合性能均衡
- 适用场景:知识库、日记检索等中小规模向量场景
2.2 相似度度量:COSINE 余弦相似度
文本向量化场景统一使用余弦相似度:
- 计算向量方向的夹角,不受向量长度影响
- 分值区间 -1, 1,越接近 1 代表语义相似度越高
- 是 Embedding 文本检索的行业标准度量方式
三、完整代码实现:日记向量知识库
3.1 导入依赖与初始化客户端
javascript
运行
arduino
import "dotenv/config";
import {
MilvusClient,
MetricType,
IndexType,
DataType,
} from "@zilliz/milvus2-sdk-node";
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";
// 全局常量配置
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = "ai_diary";
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_BASE_URL,
},
dimensions: VECTOR_DIM,
});
// 初始化大语言模型
const model = new ChatOpenAI({
temperature: 0.1,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// 初始化 Milvus 客户端
const client = new MilvusClient({
address: ADDRESS,
token: TOKEN,
ssl: true, // Zilliz Cloud 强制开启 SSL
});
// 封装文本转向量函数
const getEmbedding = async (text) => {
const result = await embeddings.embedQuery(text);
return result;
};
代码解析:
MilvusClient是 SDK 核心入口,负责所有数据库操作ssl: true为云端必填项,本地部署可省略temperature: 0.1控制大模型输出稳定性,问答场景推荐偏低的温度,减少幻觉getEmbedding统一封装向量化逻辑,后续替换模型只需修改该函数
3.2 集合 Schema 设计
日记场景需要存储向量 + 业务元数据,字段设计如下:
javascript
运行
php
async function createCollection() {
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,
},
],
});
console.log("集合创建成功");
}
设计要点:
- 主键使用自定义字符串 ID,便于业务侧关联原始日记
vector字段维度必须与 Embedding 模型输出维度严格一致,否则插入报错tags使用数组类型,支持多标签存储与过滤- 对比 MySQL,Milvus 支持动态字段,未预先定义的字段也可插入,灵活性更高
3.3 创建向量索引与加载集合
javascript
运行
php
async function createIndexAndLoad() {
// 创建向量索引
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: "vector",
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: { nlist: 128 }, // 聚类分桶数量
});
console.log("索引创建成功");
// 加载集合到内存(检索前必须执行)
await client.loadCollection({
collection_name: COLLECTION_NAME,
});
console.log("集合加载完成");
}
关键说明:
nlist是 IVF_FLAT 的核心参数,百万级数据推荐 128,数据量增大可上调loadCollection是 Milvus 的必要步骤,未加载的集合无法执行检索
3.4 批量向量化与数据入库
javascript
运行
less
async function insertDiaries() {
// 原始日记数据
const diaryContents = [ { id: "diary_001", content: "今天天气很好,去公园散步了,心情愉快。看到了很多花开了,春天真美好。", date: "2026-01-10", mood: "happy", tags: ["生活", "散步"],
},
{
id: "diary_002",
content: "今天工作很忙,完成了一个重要的项目里程碑。团队合作很愉快,感觉很有成就感。",
date: "2026-01-11",
mood: "excited",
tags: ["工作", "成就"],
},
{
id: "diary_003",
content: "周末和朋友去爬山,天气很好,心情也很放松。享受大自然的感觉真好。",
date: "2026-01-12",
mood: "relaxed",
tags: ["户外", "朋友"],
},
{
id: "diary_004",
content: "今天学习了 Milvus 向量数据库,感觉很有意思。向量搜索技术真的很强大。",
date: "2026-01-12",
mood: "curious",
tags: ["学习", "技术"],
},
{
id: "diary_005",
content: "晚上做了一顿丰盛的晚餐,尝试了新菜谱。家人都说很好吃,很有成就感。",
date: "2026-01-13",
mood: "proud",
tags: ["美食", "家庭"],
},
];
console.log("正在批量生成向量...");
// 并发调用向量化接口,提升处理速度
const diaryData = await Promise.all(
diaryContents.map(async (diary) => ({
...diary,
vector: await getEmbedding(diary.content),
}))
);
// 批量插入 Milvus
const insertResult = await client.insert({
collection_name: COLLECTION_NAME,
data: diaryData,
});
console.log(`${insertResult.insert_cnt} 条记录成功插入`);
}
性能优化点:
- 使用
Promise.all并发生成向量,相比串行 await 速度提升数倍 - 批量插入比单条循环插入性能更优,生产环境建议分批批量写入
- 数据量过大时需控制并发数,避免触发 Embedding 接口限流
3.5 向量相似度检索(基础测试版)
单文件快速验证检索能力,可独立运行调试:
javascript
运行
javascript
async function searchTest() {
try {
console.log("Connection to Milvus...");
await client.connectPromise; // 等待连接握手完成
console.log("Connected");
const query = "我想看看关于户外活动的日记";
console.log(`QUERY: ${query}`);
// 1. 问题文本生成向量
const queryVector = await getEmbedding(query);
// 2. 执行向量相似度检索
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
data: [queryVector], // 查询向量,二维数组格式
limit: 2,
metric_type: MetricType.COSINE,
output_fields: ["id", "content", "date", "mood", "tags"],
});
// 3. 格式化打印结果
console.log(`Found ${searchResult.results[0].length} results`);
searchResult.results[0].forEach((item, index) => {
console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
console.log(`
ID: ${item.id}
Date: ${item.date}
Mood: ${item.mood}
Tags: ${item.tags?.join(", ")}
Content: ${item.content}
`);
});
} catch (err) {
console.error("检索失败:", err.message);
}
}
检索参数说明:
data:传入查询向量数组,二维格式,支持批量多向量查询limit:返回最相似的 Top K 条结果output_fields:指定返回的标量字段,不指定只返回 ID 和相似度分数item.score:余弦相似度分值,越接近 1 语义匹配度越高connectPromise:等待 Milvus 客户端完成 TCP 握手,避免未连接就发起请求报错
四、模块化 RAG 问答系统完整实现
RAG 的核心思想是检索 + 生成两阶段架构:先从向量库召回相关知识,再把知识作为上下文传给大模型,让 AI 基于私有数据回答问题。本项目将检索与生成拆分为独立模块,职责清晰、便于维护。
4.1 检索模块封装:retrieveRelevantDiaries
独立封装向量检索逻辑,上层业务无需关心 Milvus 底层细节:
javascript
运行
php
async function retrieveRelevantDiaries(question, k = 2) {
try {
const queryVector = await getEmbedding(question);
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
data: [queryVector],
limit: k,
metric_type: MetricType.COSINE,
output_fields: ["id", "content", "date", "mood", "tags"],
});
// 返回匹配的日记实体数组
return searchResult.results[0];
} catch (err) {
console.log("检索日记时出错", err.message);
return [];
}
}
设计优势:
- 单一职责:只负责向量召回,不处理业务逻辑
- 异常兜底:检索失败返回空数组,上层业务可优雅降级
- 可复用:问答、搜索、推荐等多个场景都可调用该函数
4.2 问答核心:answerDiaryQuestion 完整实现
整合检索、上下文拼接、Prompt 构建、大模型调用全流程:
javascript
运行
javascript
async function answerDiaryQuestion(question, k = 2) {
try {
// 打印分隔线,便于调试日志区分
console.log("=".repeat(80));
console.log(`问题:${question}`);
console.log("=".repeat(80));
// 阶段一:向量检索召回相关日记
console.log("检索相关日记");
const retrievedDiaries = await retrieveRelevantDiaries(question, k);
if (retrievedDiaries.length === 0) {
console.log("未找到相关日记");
return;
}
// 打印检索结果与相似度,便于调试召回效果
retrievedDiaries.forEach((diary, i) => {
console.log(`日记${i + 1} 相似度: ${diary.score.toFixed(4)}
内容:${diary.content}
`);
});
// 阶段二:拼接格式化上下文
const context = retrievedDiaries
.map((diary, i) => `
[日记 ${i + 1}]
日期:${diary.date}
心情: ${diary.mood}
标签: ${diary.tags?.join(", ")}
内容:${diary.content}
`)
.join("\n\n----\n\n");
// 阶段三:构建系统提示词 + 上下文 + 用户问题
const prompt = `你是一个温暖贴心的AI日记助手。基于用户的日记内容回答问题,
用亲切自然的语言。请根据以下日记内容回答问题:
${context}
用户问题: ${question}
回答要求:
1. 如果日记中有相关信息,请结合日记内容给出详细、温暖的回答。
2. 可以总结多篇日记的内容,找出共同点或趋势。
3. 如果日记中没有相关信息,请温和告知用户。
4. 用第一人称"你"来称呼日记的作者。
5. 回答要有同理心,让用户感到被理解和关心。
AI 助手的回答:
`;
// 阶段四:调用大模型生成回答
console.log("[AI 回答]");
const response = await model.invoke(prompt);
console.log(response.content);
} catch (err) {
console.log(err.message);
}
}
核心设计解析:
-
模块化拆分:严格遵循 RAG 两阶段,检索逻辑独立封装,问答函数只负责流程编排
-
Prompt 工程:
- 设定角色人设:温暖贴心的日记助手,统一回答风格
- 明确回答规则:5 条约束减少幻觉,强制基于日记内容回答
- 人称规范:用 "你" 称呼作者,增强对话代入感
-
调试友好:打印相似度分值与召回原文,便于排查「回答不准」是召回问题还是生成问题
-
异常兜底:检索为空时友好提示,不会触发大模型空上下文报错
4.3 主函数入口与流程串联
javascript
运行
javascript
async function main() {
try {
console.log("连接到Milvus...");
await client.connectPromise; // 等待连接就绪
console.log("已连接");
// 执行日记智能问答
await answerDiaryQuestion("我最近做了什么让我感到快乐的事情?", 2);
// 关闭连接
await client.close();
} catch (err) {
console.error("程序执行异常:", err);
}
}
main().catch(console.error);
五、常见踩坑与解决方案
- 集合重复创建报错 :每次运行前通过
hasCollection判断是否已存在,避免重复执行 DDL - 检索无结果 / 报错 :必须执行
loadCollection加载集合到内存,否则无法查询 - 向量维度不匹配:建表 dim 与 Embedding 输出维度必须严格一致
- 插入后检索不到 :新插入数据需要落盘,可手动执行
flush强制持久化 - Windows 安装依赖 EPERM:项目移出 OneDrive 同步目录,或暂停云盘同步
- search 参数报错 :SDK 标准参数为
data(二维数组),而非vector,需注意格式 - 大模型回答幻觉:降低 temperature、在 Prompt 中强约束「仅基于日记内容回答」、增加召回条数可有效缓解
六、总结
本文完整实现了基于 Milvus 的日记向量知识库与 RAG 智能问答系统,覆盖了从环境搭建、Schema 设计、索引原理到批量入库、相似度检索、大模型问答的全链路。通过模块化拆分,检索层与生成层完全解耦,后续替换向量模型、大模型或向量数据库都无需大幅改动业务代码。
向量数据库 + RAG 是当前落地私有知识库最成熟的方案,掌握这套基础架构后,可以快速拓展到文档问答、客服机器人、语义搜索、个人记忆助手等更多场景。