从 0 到 1:Milvus + 大模型打造私人记忆知识库

0 阅读10分钟

说实话,向量数据库这词我听了快半年了,每次看 RAG 相关的文章都能看到 Milvus、Pinecone 这些名字,但一直没往心里去。

我脑子里一直有个疑问:不就是存数据吗?MySQL 不够用?Redis 不够快?为啥非得整个新东西叫 "向量数据库"?

直到上周我想整个 AI 日记本 —— 就是你写日记,然后可以用自然语言问它 "我最近啥时候最开心",它能从你日记里翻出相关内容再给你总结。做着做着,我就明白向量数据库到底在干嘛了。

从一个笨想法开始

最开始我想的很简单:日记存在 MySQL 里,不就完了?

标题、内容、日期、心情标签,全是结构化的,CRUD 一把梭。然后用户问 "我最近做了什么让我感到快乐的事情",我就…… 我就啥?

我总不能 SELECT * FROM diary WHERE content LIKE '%快乐%' 吧?那日记里写 "心情愉快"、"感觉很有成就感"、"春天真美好" 这些怎么办?这些句子里没有 "快乐" 两个字,但表达的就是开心的意思啊。

这时候我才反应过来 —— 传统数据库查的是 "是什么",而我现在要查的是 "像什么"。

关键词匹配搞不定语义相似的问题,这就是向量数据库要解决的事。

向量到底是个啥

别被名字吓住,向量就是一串数字。

你把一句话丢给 Embedding 模型,它给你返回一堆浮点数,比如 1024 个。这串数字就是那句话的 "语义指纹"—— 意思相近的话,它们的指纹在空间里离得就近。

打个比方,"今天天气很好,去公园散步了,心情愉快" 和 "周末和朋友去爬山,天气很好,心情也很放松",这两句话字面不一样,但在 1024 维的空间里,它们的向量点挨得特别近。

而 "今天工作很忙,完成了一个重要的项目里程碑" 虽然也提到了 "成就感",但整体语义离 "户外放松" 那条就远一些。

向量数据库干的事,就是把这些高维向量存起来,然后你扔一个查询向量进去,它能快速告诉你 "库里哪几条跟你这条最像"。

我的理解MySQL = 按关键词精确查找(找书名里有 "三体" 的书)Milvus = 按语义相似度查找(找跟 "科幻宇宙文明" 感觉最像的书)

先跑通最小 demo

我用的是 Zilliz,就是 Milvus 的全托管服务,不用自己搭集群,白嫖额度够我折腾了。

先整个最基础的,连接上、插两条数据、搜一下。

import { MilvusClient, IndexType, MetricType } from "@zilliz/milvus2-sdk-node";
import 'dotenv/config';

const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;

async function main() {
    const client = new MilvusClient({
        address: ADDRESS,
        token: TOKEN,
    });
    console.log("正在连接 zilliz server...");

    const checkHealth = await client.checkHealth();
    if (!checkHealth.isHealthy) {
        console.error("连接失败", checkHealth.reasons);
        return;
    }
    console.log("连接成功,集群状态正常");

    const COLLECTION_NAME = 'test';
    const DIMENSION = 4; // 维度,先玩个4维的,意思意思

    try {
        // 集合就相当于MySQL的表
        await client.createCollection({
            collection_name: COLLECTION_NAME,
            dimension: DIMENSION,
            auto_id: true, // 自动生成ID
        });
        console.log('创建集合成功');

        // 建索引,这个后面细说
        await client.createIndex({
            collection_name: COLLECTION_NAME,
            field_name: 'vector',
            index_type: IndexType.AUTOINDEX,
            metric_type: MetricType.COSINE
        });
        console.log('索引创建成功');

        // 插两条数据,比写SQL爽多了
        const data = [
            { vector: [0.1, 0.2, 0.3, 0.4], content: '这是第一行数据' },
            { vector: [0.5, 0.6, 0.7, 0.8], content: '这是第二行数据' },
        ];
        const insertRes = await client.insert({
            collection_name: COLLECTION_NAME,
            data
        });
        console.log('插入数据成功', insertRes.IDs);

        // 搜一下
        const searchRes = await client.search({
            collection_name: COLLECTION_NAME,
            data: [[0.1, 0.2, 0.3, 0.4]],
            limit: 2,
            output_fileds: ['content'], // 注意这行,坑了我半小时
        });
        console.log(JSON.stringify(searchRes.results, null, 2));
    } catch (err) {
        console.error('集合可能存在或创建出错', err.message);
    }
}

main().catch((err) => { console.error(err); });

跑通那一刻其实挺有成就感的,虽然 4 维向量没啥实际意义,但整个流程通了 —— 存进去、搜出来、能拿到结果。

索引是怎么回事

说到索引,我第一反应就是 MySQL 的 B + 树。但向量的索引完全不是一回事。

你想啊,1024 维的向量,数据量如果有个几百万条,每次查询都要把查询向量和库里所有向量挨个算一遍相似度,那就是 O (n) 的暴力搜索,数据量大了能慢到你怀疑人生。

这就像图书馆里找《三体》,没有索引的话,你得从第一排书架一本一本翻。但有了分类索引,你直接去 "文学馆→小说→科幻" 那个区域找,范围一下子就缩小了。

Milvus 里的 IVF_FLAT 索引大概就是这么个思路:先把向量空间分成好多 "簇"(聚类),查询的时候先看你的查询向量属于哪个簇,然后只在那个簇里搜。

从几百万条缩小到几千条,速度直接从秒级变毫秒级。

注意索引不是建得越多越好,也不是所有场景都需要。数据量小的时候(比如几千条),暴力搜反而更快更准,建索引纯属浪费。

真正的项目:AI 日记本

demo 跑通了,接下来搞点实际的 —— 把日记内容向量化存进去,然后用自然语言查询。

先建集合,字段得定义清楚:

    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 }, // 1024维向量
            { 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 },
        ],
        auto_id: true,
    });

然后把日记内容通过 Embedding 模型转成向量,再插进去:

    const diaryContents = [
        {
            id: 'diary_001',
            content: '今天天气很好,去公园散步了,心情愉快。看到了很多花开了,春天真美好。',
            date: '2026-01-10',
            mood: 'happy',
            tags: ['生活', '散步']
        },
        {
            id: 'diary_002',
            content: '今天工作很忙,完成了一个重要的项目里程碑。团队合作很愉快,感觉很有成就感。',
            date: '2026-01-11',
            mood: 'excited',
            tags: ['工作', '成就']
        },
        // ... 还有几条就不贴了
    ];

    // 批量生成embedding
    const diaryData = await Promise.all(
        diaryContents.map(async (diary) => ({
            ...diary,
            vector: await getEmbedding(diary.content), // 把文本转成向量
        }))
    );

    const insertResult = await client.insert({
        collection_name: COLLECTION_NAME,
        data: diaryData,
    });

插完之后,最激动的时刻来了 —— 搜一下试试:

    const query = '我想看看关于户外活动的日记';
    const queryVector = await getEmbedding(query); // 查询语句也转成向量

    const searchResult = await client.search({
        collection_name: COLLECTION_NAME,
        vector: queryVector,
        limit: 2,
        metric_type: MetricType.COSINE, // 余弦相似度
        output_fields: ['id', 'content', 'date', 'mood', 'tags']
    });

你猜怎么着?我搜 "户外活动",它真的给我返回了 "去公园散步" 和 "去爬山" 那两条!虽然这两条日记里根本没有 "户外活动" 这四个字。

那一刻我真的觉得,向量搜索这玩意儿有点东西。

再往前走一步:RAG

光搜出来还不够,用户想要的是 "回答",不是 "相关日记列表"。

所以我又加了一步:把搜出来的日记塞给大模型,让它基于这些内容来回答问题。这就是 RAG—— 检索增强生成。

    async function answerDairyQuestion(question, k = 2) {
        // 第一步:检索相关日记
        const retrieveDiaries = await retrieveRelevantDairies(question, k);
        
        if (retrieveDiaries.length === 0) {
            console.log('没有找到相关日记');
            return;
        }

        // 第二步:把检索到的内容拼成prompt
        const content = retrieveDiaries.map((diary, i) => `
            [日记 ${i+1}]
            日期:${diary.date}
            心情:${diary.mood}
            标签:${diary.tags?.join(', ')}
            内容:${diary.content}
        `).join('\n\n---\n\n');

        const prompt = `你是一个温暖贴心的 AI 日记助手,基于用户的日记内容回答问题...
            ${content}
            用户问题:${question}
            ...`;

        // 第三步:丢给大模型
        const response = await model.invoke(prompt);
        console.log(response.content);
    }

整个流程就三步:把问题向量化 → 去向量库里搜相似的 → 把搜到的内容给大模型让它组织语言回答。

说起来简单,但我在做的时候踩了好几个坑。

我踩过的坑

坑一:output_fileds 拼错了

对,就是上面 demo 里写的那个 output_fileds。我写的是 fileds 不是 fields,少了个 e

搜是能搜到,但返回结果里就是没有我要的字段,全是空的。我盯着代码看了十分钟,还去翻文档,怀疑是不是 Zilliz 的 bug。

后来复制文档里的字段名一对比…… 行吧,手滑了。

坑二:维度对不上

Embedding 模型返回的维度,和你建集合时指定的 dim,必须一模一样。

我用的是通义千问的 text-embedding-v3,指定了 1024 维。但一开始我写的是 1536(脑子里默认是 OpenAI 的维度),插数据的时候直接报错。

这个还好,报错信息挺明显的,改一下维度就好了。

坑三:忘了 load collection

建完索引之后,要调用 loadCollection 把集合加载到内存里,不然搜不了。

我第一次写的时候把这步漏了,search 的时候报了个错,意思大概是 "集合没加载"。我还纳闷呢,我不是建好了吗?

后来才明白,Milvus 是把数据存在磁盘上的,查询之前得先 load 到内存里才能搜。就像你看书得先把书从书架上拿下来翻开,不能隔着书架直接读。

    console.log('loading collection...');
    await client.loadCollection({
        collection_name: COLLECTION_NAME,
    });
    console.log('collection loaded');

坑四:client.connectPromise 这个细节

我一开始直接 new MilvusClient(...) 然后马上调用方法,有时候能行有时候不行,挺玄学的。

后来看了别人的代码,发现大家都会等一下 connectPromise

    const client = new MilvusClient({ address, token });
    await client.connectPromise; // 先等连接建立好
    console.log('Connected');

对,就跟数据库连接池一样,new 完了不等于连接已经建立好了,得等握手完成。

往源码里瞅了一眼

说实话,Node.js 的 SDK 源码没那么难读,我翻了一下 search 方法的实现。

大概就是把你传的参数组装成 gRPC 的请求,发给 Milvus 服务端,然后把返回的结果处理一下给你。真正的向量搜索逻辑全在服务端,SDK 就是个通信层。

这也合理,毕竟向量数据库的核心是索引算法和查询优化,这些都得用 C++ 之类的写才能跑得快。Node.js SDK 就是封装了一下调用接口。

最后扯两句

做完这个小项目,我对向量数据库的理解算是落地了。以前看文章总觉得 "向量数据库" 是个很高大上的概念,真动手做一遍才发现 ——

它就是给 AI 用的 "语义图书馆"。 传统数据库按关键词找,向量数据库按意思找。就这么简单。

但也不是说有了向量数据库就万能了:

  • 数据量小的时候真没必要,几千条数据暴力搜就够了,上 Milvus 纯属增加复杂度
  • 向量的质量取决于 Embedding 模型,模型不行,搜出来的结果就离谱
  • 相似度搜索不等于精确匹配,它返回的是 "最像的",不一定是 "对的"
  • RAG 也不是银弹,搜出来的内容如果不相关,大模型照样会胡说八道

不过话说回来,做这个 AI 日记本的过程真的挺有意思的。从 "向量数据库有啥用" 的疑惑,到亲手把数据存进去、搜出来、再让大模型基于这些内容回答问题,整个链路跑通的时候,那种恍然大悟的感觉,比看十篇文章都管用。

如果你也在学向量数据库或者 RAG,我的建议是别光看,找个小项目动手做一遍。不用搞太复杂,就像我这样整个简单的日记本,把流程跑通,理解一下子就深了。

搞懂了记得回来留个言,我也想看看你的理解是不是跟我一样,或者你有啥更有意思的用法。