作者:互联网 时间: 2026-07-21 17:42:08
RAG(检索增强生成)流程中存在两种完全不同的模型,各司其职:

┌─────────────────────────────────────────────────────────────────┐│ RAG 流程中的两个模型 ││││[用户问: "Java 垃圾回收有哪几种?"]││ │ ││ ▼ ││┌──────────────────────┐ │││ ① Embedding 模型│← 本地 CPU,~1.3GB│││只管:文字 → 数字 │ 输入一段文字,输出一串数字(向量)││││ 不能对话、不能理解语义│││"Java 垃圾回收" ││││ ↓││││[0.12, -0.34, ...]│← 1024 个浮点数(语义指纹)││└──────────┬───────────┘ ││ │ ││ ▼ ││┌──────────────────────┐ │││ 向量数据库 │← 用"语义指纹"去搜索最相似的文档 │││"这是检索到的3段││││相关文档片段..." │││└──────────┬───────────┘ ││ │ ││ ▼ ││┌──────────────────────┐ │││ ② LLM 大语言模型│← 远程 API,数百 GB│││只管:阅读 + 理解 │ 能读文档、能推理、能写答案│││把问题+检索结果││││一起发给大模型││││ ↓││││"根据资料,Java 垃圾││││ 回收分为:Serial、││││ Parallel、CMS、G1、 ││││ ZGC..." │││└──────────────────────┘ │└─────────────────────────────────────────────────────────────────┘
| 对比维度 | Embedding 模型 | LLM 大语言模型 |
|---|---|---|
| 角色类比 | 图书管理员(只管分类、贴标签、按标签找书) | 专家教授(能阅读、理解、推理、写答案) |
| 能做什么 | 把一段文字变成一串数字(向量) | 读上下文、理解意图、生成自然语言回答 |
| 不能做什么 | 对话、推理、理解语义 | — |
| 输入 | 一段文字 | 完整对话上下文(含检索到的文档) |
| 输出 | 1024 个浮点数(向量) | 自然语言文本 |
| 模型大小 | ~1.3 GB(CPU 可跑) | 数百 GB(需 GPU 集群) |
| 运行方式 | 本地下载,离线推理 | 远程 API 调用 |
| 典型代表 | BGE-large-zh、OpenAI text-embedding-3 | DeepSeek V4、GPT-4、Claude 4 |
一句话总结:Embedding 模型只管"把文字变成数字标签",LLM 只管"阅读上下文写答案"。把 Embedding 当成 LLM 用——让它回答问题——它做不到,它输出的是数字不是文字。反过来,用 LLM 做向量化——理论上可行但每次要调远程 API、按 token 计费——成本和延迟都不划算。
这是一个自然的追问。当前主流方案存在不对称性:
| 维度 | LLM(远程) | Embedding(本地) |
|---|---|---|
| 调用频率 | 每次对话 1-N 次 | 每个文档切片 + 每次检索查询 |
| 单次调用量 | 1 次请求 | 文档上传时批量 N 条,检索时 1 条 |
| 成本敏感度 | 对话核心体验 | 高频但可离线化 |
举例:一个知识库上传 100 个 PDF、切出 2000 个 chunk → 如果用远程 Embedding API 需要 2000 次调用 → 本地 BGE 模型零费用。这是成本驱动的设计决策,不是技术限制。
# 本地 BGE 模型:一次加载,无限使用from sentence_transformers import SentenceTransformermodel = SentenceTransformer("BAAI/bge-large-zh-v1.5", device="cpu")embeddings = model.encode(["文本1", "文本2", ..., "文本2000"]) # 批量、免费# 远程 API:按调用次数和 token 数计费# 2000 个 chunk × $0.00002/token × 500 tokens/chunk ≈ 大规模场景不可忽略
能存,但不能高效搜索。这是本质区别:
-- MySQL:精确/模糊文字匹配SELECT * FROM docs WHERE content LIKE '%Java 垃圾回收%'-- 问题:用户问"JVM GC 机制",字面不匹配 → 搜不到-- 即便存了向量(BLOB 列),也没有向量索引 → 全表扫描-- Milvus:语义相似度搜索-- 用户问"JVM GC 机制"-- 能返回:--1. "Java 垃圾回收器详解"相似度 94%--2. "G1 收集器的调优参数"相似度 87%--3. "内存管理与回收策略" 相似度 81%
| 数据库 | 1 万条向量 Top-10 检索 | 100 万条向量 Top-10 检索 |
|---|---|---|
| MySQL(全表扫描) | ~500ms | ~数十秒 |
| Milvus(IVF_FLAT) | ~10ms | ~50ms |
差距原因:MySQL 没有向量索引,只能逐条计算距离(sqrt(sum((a[i]-b[i])²)))。Milvus 用近似最近邻(ANN)索引——先将向量空间分区,搜索时只查最近的几个分区——精度略降(99%+ 召回率),速度提升 1000 倍。
以 PrismAI 的 Beam 应用为例,RAG 数据存储分层如下:
MySQL (beam 库) Milvus├── knowledge_bases← 知识库元数据 └── doc_chunks← 文档切片的向量├── documents ← 文档元数据 每条 = {chunk_id, kb_id, text, embedding[1024]}│(名称/大小/状态)││MySQL 管"管理信息"Milvus 管"语义指纹"│"知识库A叫什么、谁创建的" "这段文字在语义上和哪些片段最接近"
为什么不是二选一,而是两者配合?
两者职责边界清晰:MySQL 回答"这个文档是什么",Milvus 回答"这段内容和哪些内容最相似" 。
一句话版:把文字变成数字 → 数字之间比距离 → 距离近的就是语义近的。1. 预处理(建立索引时):文档 → 切块 → BGE 编码 → [0.12, -0.34, 0.89, ...] → 存入 Milvus → 建立 IVF_FLAT 索引2. 搜索时:用户问题 → BGE 编码 → [0.15, -0.31, 0.92, ...] → Milvus 计算与所有向量的 IP(内积)→ 返回 Top-K 最相似的文档片段3. IVF_FLAT 索引用什么原理做到这么快?- IVF = Inverted File:用 K-Means 将 100 万个向量聚成 128 个簇- 搜索时只查找最近的 nprobe 个簇(比如 8 个),不是全量 100 万- FLAT = 被选中的簇内部暴力计算,精度不损失- 100万 → 8×7812 ≈ 6.25万次计算,而非 100万次
PrismAI 项目将 Docker 24+ 列为环境假设。5 个基础设施中,Milvus 是唯一必须通过 Docker 运行的:
| 基础设施 | 能否不用 Docker | 原因 |
|---|---|---|
| MySQL 8.0 | ✅ 可本地安装 | Windows/Mac/Linux 都有原生版本 |
| Redis 7.0 | ✅ 可本地安装 | 或测试时用 fakeredis 替代 |
| Nacos 2.3.2 | ⚠️ 需 Java | 可以 java -jar 本地启动 |
| MinIO | ✅ 可本地安装 | 且 Beam RAG 当前未用 MinIO(文件存本地磁盘) |
| Milvus | ❌ 必须 Docker | C++ 项目,只有 Linux 版本。单机模式内嵌 etcd,底层依赖 Linux cgroup/namespace。Windows 上无原生二进制 |
Beam 启动时对每个基础设施都有降级处理,Milvus 不可用时不会崩溃——但 RAG 的向量检索会降级为仅 BM25 关键词匹配。
用户 ──提问──→ [BGE 嵌入模型] ──向量──→ [Milvus] ──相关文档──→ [DeepSeek/LLM] ──回答──→ 用户↑ ↑ ↑本地 CPU 推理Linux 容器 远程 API 调用只管文字→数字 只管向量相似搜索只管理解上下文作答~1.3GB需 Docker数百 GB 集群零 API 费用开源免费 按 token 计费
Beam 应用的 RAG 检索请求贯穿四个组件(beam/src/beam/rag/):
POST /api/beam/search{ query: "JVM GC 机制", kb_ids: ["kb_xxx"], top_k: 5 }│├── interfaces/rag_router.py│└── POST /search → 解析请求 → 委托 Application 层│├── application/service.py│└── RagApplicationService.search(cmd)│└── ① query 为空?→ 抛 ValidationError│└── ② embedding_service 可用?→ 走混合检索,否则 BM25-only│├── infrastructure/retriever.py│└── HybridRetriever.search()│├── _vector_search(query) ← ③ BGE 模型: query → 1024维向量││└── Milvus.search(向量, kb_id) ← ④ Milvus: 向量相似 → Top-K 文档│├── _bm25_search(query)← ⑤ jieba 分词: 关键词匹配│└── _rrf_fusion(v_results, b_results)← ⑥ RRF 融合: 向量+关键词重排│→ 返回 SearchResult[] {chunk_id, doc_name, chunk_text, score, search_type}│├── application/dto.py│└── SearchResultDto.from_results(query, results)│→ { query, results: [{score, doc_name, chunk, search_type}] }│└── 返回 {"code": 0, "data": {"query": "JVM GC 机制", "results": [...]}}
PrismAI 设计文档规定了多层降级:
第一优先:向量检索(BGE + Milvus) │ ├── BGE 模型加载失败? → WARN 降级 → 仅 BM25 关键词检索 ├── Milvus 连接失败?→ WARN 降级 → 仅 BM25 关键词检索 ├── 两者都不可用? → 返回空结果(Chat 中触发搜索时告知用户"检索服务暂不可用") │ └── Chat 模块 rag_search 工具: ├── HybridRetriever 可用 → 调用 retriever.search() └── HybridRetriever 不可用 → 返回 mock 结果(fallback)
原因三层:
| 层面 | 理由 |
|---|---|
| 成本 | 知识库批量上传时需向量化数千个 chunk,远程 API 按 token 计费;BGE 本地推理零费用 |
| 一致性 | 公共知识库需要跨用户检索。如果各用各的 Embedding 模型(用户 A 用 GLM、用户 B 用千问),向量在不同语义空间中,检索结果不可用。平台统一 BGE 模型保证跨用户的向量空间兼容——这是做公共知识库最容易踩的坑,一开始觉得"让用户各配各的模型更好",上线后发现跨用户检索全是噪声 |
| 可控性 | 本地模型不依赖第三方 API 可用性,模型版本和推理行为完全可控 |
技术上 LLM 的最后几层隐藏层输出可以当向量用。但:
MySQL 8.0 不支持向量索引。MySQL 9.0(2024 年发布)引入了 VECTOR 数据类型,但向量索引功能仍有限。PostgreSQL 的 pgvector 扩展支持 IVFFlat 和 HNSW 索引,但在大规模(>100 万条向量)场景下,专用向量数据库(Milvus/Qdrant/Weaviate)在性能、索引灵活性、混合检索能力上仍有显著优势。
能。PrismAI 项目的单元测试 92/92 全部通过,完全不依赖 Milvus:
# 测试中 Mock VectorStore,不走真实 Milvusmock_vector_store = AsyncMock(spec=VectorStoreRepository)mock_vector_store.search.return_value = []service = RagApplicationService(vector_store=mock_vector_store, ...)
对于本地开发体验,可以考虑的轻量替代方案:
pip install 即可但生产环境或需要混合检索(向量 + BM25 + RRF 融合)时,Milvus 是设计文档选定的方案。