如果你之前没接触过向量数据库,它做的事情用一句话就能概括:把一段文字变成一个高维空间里的坐标点,然后通过计算坐标之间的距离来找"意思相近"的内容。
听起来有点抽象。所以这篇文章不打算从概念讲到概念,而是直接拿一个实际的 AI 日记本项目来跑一遍------从存数据到语义搜索再到 RAG,覆盖日常开发中最常用到的几个环节。用的技术栈是 Milvus(Zilliz Cloud 托管版)+ LangChain + 阿里云 DashScope 的 Embedding 和 Chat 模型。
传统数据库在做啥
我们平时用 MySQL、PostgreSQL 做 CRUD,核心的查询方式大概两种:
- 精确匹配 :
WHERE id = 123,你给我一个 id,我返回一条记录 - 关键词模糊匹配 :
WHERE content LIKE '%爬山%',你给我一个词,我找出包含这个词的记录
这在大多数业务场景下够用了。但当你遇到这种问题的时候,它就有点力不从心了:
"帮我找一下最近心情比较快乐的日记"
"快乐"这个词没直接出现在日记里。日记写的是"今天天气很好,去公园散步了,心情愉快。"------这里用的是"愉快",不是"快乐"。
一个开发者面对这种需求时,通常会这么做:先维护一个同义词表,把"快乐""愉快""高兴""开心"全列上,然后拼一个巨大的 LIKE OR 查询。粗暴,但能勉强跑。问题在于,同义词表维护成本很高,而且"在公园散步很开心"和"今天升职了特别激动",这两句话完全没有相同的词,但人一看就知道都是在讲开心的事。关键词匹配对此束手无策。
这就是向量数据库要解决的问题:不按字面匹配,而是按语义匹配。
Embedding:把文字变成坐标
向量数据库的核心依赖是一个叫 Embedding 的东西。你可以把它理解成一个函数:
javascript
// 输入:一段文字
// 输出:一个高维向量(比如 1024 个浮点数组成的数组)
const vector = await getEmbedding('今天天气很好,心情愉快');
// vector ≈ [0.023, -0.451, 0.891, ..., 0.332] // 1024 个浮点数
这个向量不是随机生成的。Embedding 模型在训练过程中学到了词与词、句与句之间的关系,然后把这些语义关系编码到了高维空间中。在这个空间里,意思相近的文字,它们的坐标也靠近。
打个不太严谨但好理解的比方:如果把所有中文句子画在一张地图上,"我今天很开心"和"心情特别愉快"这两个点会落在同一个街区,而"昨天的部署又挂了"则在这张地图的另一个角落。
这意味着你可以用空间距离 来度量语义相似度------这就是向量数据库的底层逻辑。
Milvus 是什么
Milvus 是一个开源的向量数据库,专为处理海量高维向量数据而设计。
拿它和传统数据库做个类比,可能更容易理解它的定位:
| 场景 | 传统数据库(如 MySQL) | 向量数据库(如 Milvus) |
|---|---|---|
| Web 应用 | 存用户、订单、商品,按 id 和条件查询 | --- |
| AI Agent | --- | 存知识、记忆、文档,按语义检索 |
不是说谁替代谁。实践中通常是两边都用------MySQL 管结构化数据的 CRUD,Milvus 管语义搜索和 AI 相关的数据存储。就像一个应用既有 MySQL 存订单,又有 Redis 做缓存,各自解决各自的问题。
Milvus 的几个基本概念:
- Collection:类似 MySQL 的表。一个存向量数据的地方。
- Field:Collection 里的字段,可以定义 id、向量字段、普通标量字段(如日期、标签)。
- Index:向量索引。加速相似度搜索用的,支持多种索引类型(IVF_FLAT、HNSW 等)。
- Metric Type:相似度计算方式。常用的有 COSINE(余弦相似度)和 L2(欧氏距离)。
Zilliz Cloud:不想自己运维的选择
在 demo 里我们用的其实是 Zilliz Cloud------基于 Milvus 的全托管云服务。打个比方的话,Milvus 和 Zilliz 的关系有点像 MySQL 和阿里云 RDS:一个是开源数据库本身,一个是帮你把运维、扩容、备份这些都管了的云服务。
对于想快速验证想法的场景,直接用托管服务能省下不少折腾基础设施的时间。
从零搭建一个 AI 日记本
接下来基于项目里的实际代码,走一遍完整流程。这个 demo 做的是一个 AI 日记本------你可以用自然语言查询你的日记,比如"我想看看关于户外爬山的日记",即使"户外""爬山"这些词没有精确出现在日记文本中。
项目依赖
json
{
"dependencies": {
"@zilliz/milvus2-sdk-node": "^3.0.3",
"@langchain/openai": "^1.5.5",
"dotenv": "^17.4.2"
}
}
三个依赖各司其职:
@zilliz/milvus2-sdk-node:Milvus 的 Node.js SDK,负责和向量数据库通信@langchain/openai:LangChain 封装的 OpenAI 兼容客户端,这里用来调 Embedding 和 Chat 模型dotenv:加载.env里的环境变量
配置环境变量
env
MODEL_NAME=qwen-plus
EMBEDDINGS_MODEL_NAME=text-embedding-v3
OPENAI_API_KEY=your-api-key
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MILVUS_ADDRESS=https://xxx.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn
MILVUS_TOKEN=your-milvus-token
这里用的是阿里云的 DashScope 作为模型服务(兼容 OpenAI 接口格式),Embedding 模型选了 text-embedding-v3,输出 1024 维向量。
第一步:初始化客户端和 Embedding 模型
javascript
import { MilvusClient } from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from '@langchain/openai';
// Embedding 模型
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
configuration: {
baseURL: process.env.OPENAI_BASE_URL
},
});
const getEmbedding = async (text) => {
return await embeddings.embedQuery(text);
};
// Milvus 客户端 --- Zilliz Cloud 需要开启 secure
const client = new MilvusClient({
address: process.env.MILVUS_ADDRESS,
token: process.env.MILVUS_TOKEN,
secure: true,
timeout: 120000,
});
// 验证连接
const checkHealth = await client.checkHealth();
if (!checkHealth.isHealthy) {
console.error('连接失败', checkHealth.reasons);
return;
}
secure: true 是连 Zilliz Cloud 时容易漏的一个配置------忘了加的话连接会失败。超时时间也建议设长一些,网络波动时 120 秒比默认的短超时更稳。
第二步:写入向量数据
写入的流程是:准备数据 → 每条数据调 Embedding 接口生成向量 → 一起插入 Milvus。
javascript
const diaryContents = [
{
id: 'diary_001',
content: '今天天气很好,去公园散步了,心情愉快。看到了很多花开了,春天真美好。',
date: '2026-01-10',
mood: 'happy',
tags: ['生活', '散步']
},
// ... 更多日记
];
// 并发生成向量
const diaryData = await Promise.all(
diaryContents.map(async (diary) => ({
...diary,
vector: await getEmbedding(diary.content)
}))
);
// 批量插入
const insertResult = await client.insert({
collection_name: 'ai_dairy',
data: diaryData
});
console.log(`${insertResult.insert_cnt} 条记录成功插入`);
注意点:每条记录的 content 字段被 Embedding 模型转成了 1024 维的向量,存到了 vector 字段里。之后所有语义搜索都是基于这个向量字段进行的,而不是基于原始的文本内容。
另外,如果网络不太稳定,可以考虑把 Promise.all 改成顺序循环------并发太高时 Embedding 接口容易限流。Demo 代码里两种方式都留了注释,按实际情况选。
第三步:语义搜索
有了向量数据之后,查询的逻辑是:把用户的自然语言查询也转成向量 → 在 Milvus 里找距离最近的几个向量 → 返回对应的原始数据。
javascript
const query = '我想看看户外爬山的日记';
const queryVector = await getEmbedding(query);
const searchResult = await client.search({
collection_name: 'ai_dairy',
vector: queryVector,
limit: 2,
metric_type: MetricType.COSINE,
output_fields: ['id', 'content', 'date', 'mood', 'tags'],
});
searchResult.results.forEach((item, index) => {
console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
console.log(` 内容: ${item.content}`);
});
这里 metric_type: COSINE 指定用余弦相似度来比较向量。返回结果里的 score 就是相似度得分------越接近 1 越相似。
这个查询虽然包含"户外"和"爬山"这两个词,但搜索并不是在做关键词匹配。它找到的是向量空间里和"户外爬山"这个语义最接近的日记------哪怕那篇日记写的是"周末和朋友去爬山,享受大自然",里面没有一个词和查询语句是精确匹配的。
第四步:RAG --- 把搜索结果喂给大模型
光搜出来还不够。一个完整的 AI 应用通常会接着做 RAG(Retrieval-Augmented Generation):
用户提问 → 向量检索找到相关文档 → 把文档拼进 Prompt → 大模型基于这些文档生成回答
javascript
async function answerDiaryQuestion(question, k = 2) {
// 1. 检索
const retrievedDiaries = await retrieveRelatedDiaries(question, k);
// 2. 把检索结果组织成上下文
const context = retrievedDiaries.map((diary, i) => `
[日记 ${i + 1}]
日期: ${diary.date}
心情: ${diary.mood}
内容: ${diary.content}
`).join('\n\n----\n\n');
// 3. 构造 Prompt
const prompt = `你是一个温暖贴心的AI日记助手。
请根据以下日记内容回答问题:
${context}
用户问题: ${question}
回答要求:
1. 如果日记中有相关信息,请结合日记内容给出详细温暖的回答。
2. 可以总结多篇日记的内容,找出共同点或趋势。
3. 用第一人称"你"来称呼日记的作者。
AI助手的回答:`;
// 4. 大模型生成回答
const response = await model.invoke(prompt);
return response.content;
}
这个流程的价值在于:大模型本身没有你的日记数据,但通过 RAG,你可以把相关的日记片段作为"参考资料"塞进 Prompt,让模型基于这些资料回答问题。模型不会编造你日记里没有的事(至少被 prompt 约束着不会),回答也更具体、更个性化。
这套技术栈在实际项目中的位置
如果你在做一个 AI Agent 产品,向量数据库通常会承担这些角色:
- 长期记忆:Agent 把对话摘要、用户偏好等信息向量化存起来,跨会话保持记忆
- 知识库检索:把文档、FAQ、规章制度等切分后向量化,用户提问时检索相关内容
- 语义推荐:根据用户历史行为的语义特征推荐相似内容
本质上都是同一条链路:内容 → 向量 → 存储 → 查询向量 → 相似度搜索 → 返回结果。
踩坑备忘
在写这个 demo 的过程中遇到几个容易卡住的地方,记录一下:
-
Zilliz Cloud 必须配
secure: true。忘了加的话连接一直超时,报错信息也不会直接告诉你原因。 -
别反复 loadCollection 。对于 Zilliz 托管集群,Collection 创建好之后已经在内存里了(Serverless 架构会自动管理)。反复调用
loadCollection反而容易触发超时。Demo 里的index.mjs就因为这个踩过坑,相关代码后来注释掉了。 -
Embedding 接口注意限流 。网络好的时候
Promise.all并发生成向量很快,但某些 Embedding 服务对并发数有限制,遇到报错就改顺序执行。 -
dimensions参数不是所有模型都支持 。text-embedding-v3支持指定输出维度,但有些模型不支持,传了会报错。Demo 里index.mjs也留了注释提示。 -
.env 里的变量名要统一 。代码里有的地方写
process.env.OPENAI_API_KEY,有的写process.env.EMBEDDINGS_API_KEY。.env 里统一用一个名字,否则某段代码静默失败。
小结
向量数据库解决的核心问题其实就一个:让你能按"意思"而不是"字面"来查数据。它的实现也不复杂------Embedding 模型把内容转成向量,向量数据库负责存和检索,应用层在检索结果的基础上做后续处理。
如果你的项目里有"语义搜索""相似内容推荐""AI 记忆"这类需求,向量数据库是一个值得了解的工具。Milvus 是目前这个领域比较成熟的开源选择,Zilliz Cloud 则适合不想自己折腾运维的场景。
本项目的完整代码放在 demo/ 目录下,四个文件分别对应上面讲的四个步骤,可以对照着跑一遍。