RAG 与文档语义检索:向量检索落地
上一节解决了"Agent记住聊过什么",这节解决"Agent知道什么"。docs-agent 的检索工具还是16-02的关键词计分——搜"聊天气泡"命中不了写"MessageBubble"的文档,靠一张20组人工同义词表打补丁。这节系统讲RAG:它是什么、Mastra怎么支持、向量数据库与向量模型怎么选、成熟平台(RAGFlow/Dify/FastGPT)长什么样,最后落到项目集成——语义检索引擎已在仓库真实落地(
src/rag/模块 + 语义/关键词混合检索),生产部署的决策链在本节讲清,从买服务器到上线的完整实操在17-05。RAG原理与向量库选型在5-3讲过一轮,本节不重复基础概念,重心是技术体系全貌与真实落地。
定位:关键词检索的天花板
先看16-02留下的真实代码(packages/dev-server/src/tools/search-docs.ts),关键词方案为跨过语义鸿沟打了两个补丁:
// 补丁一:同义词表——用户口语 → 文档词汇的人工映射层
// 实测"聊天气泡"在 45 篇文档中出现 0 次
const SYNONYMS: Record<string, readonly string[]> = {
聊天气泡: ['气泡', '消息气泡', 'bubble', 'message-bubble'],
暗色: ['深色', '夜间', 'dark'],
// ...共约20组,清单跟着真实失败案例长
}
// 补丁二:检索失败时提示模型换词重试
const HINT_NO_HIT =
'未命中任何文档。建议:1) 调 list_components 查看全部组件清单;2) 换组件英文名(如 MessageBubble)或常用术语(如 主题、暗色、i18n)重试'
typescript
这两个补丁的存在本身就是天花板的自白:关键词只认字面,语义鸿沟要靠人工填。每来一种新说法("夜间模式"“深色皮肤”"dark theme")都得往表里加一行,永远追不完。向量检索从根上换思路——把文本映射到语义空间,"聊天气泡"和"MessageBubble"在这个空间里是邻居,不需要任何人工映射。
RAG是什么:技术体系全貌
RAG(Retrieval-Augmented Generation,检索增强生成)= 生成前先从外部知识库检索相关内容注入上下文。这个词在ChatGPT爆火初期就有极高的讨论度——大模型上下文窗口有限,RAG让模型能在海量文本甚至多模态数据中检索相关内容、组织成上下文再回答。最典型的两类落地场景:集团内部文档库、客服问答知识库。拆开三个词理解:Retrieval(回答前先检索知识库)→ Augmented(从海量数据中找出与用户问题相关联的资料,重新组织)→ Generation(基于这些资料生成回复)。它解决LLM的三个先天问题:
| 问题 | RAG的解法 |
|---|---|
| 幻觉 | 答案grounding到真实资料,模型"看着材料回答" |
| 知识时效 | 训练截止后的新信息靠检索补,不用重训模型 |
| 私有数据 | 企业内部文档不进训练集也能被引用 |
演进上分三代(理解趋势即可,不必背):
- Naive RAG:
索引→检索→生成单一回路,痛点是检索质量差、上下文冗余 - Advanced RAG:"更好地检索再生成"——检索前(查询改写)、检索中(混合检索)、检索后(重排、压缩)全流程优化
- Modular RAG(当前主流):拆成可插拔模块编排,融入路由与Agentic能力——智能决定何时检索、检索什么,而不是每次提问都无脑检索一遍
对照课程已讲内容:16-02的docs-agent其实就是一个Naive RAG(检索→生成,检索工具是关键词版);Mastra的createVectorQueryTool把检索交给Agent自主决策,已经是Modular RAG的Agentic形态。
五个关键技术环节
一条完整RAG管道的五个环节,每个环节都有对应的优化空间:
1. Chunking(切块)。文档不能整篇embed(超模型输入上限、检索粒度太粗),切块质量决定检索上限。策略从粗到细:固定长度(character/token,按换行符、句号或约定标记断开)→ 结构感知(按markdown标题、html标签切)→ 语义切分(用LLM判断语义边界,成本高)。overlap重叠保留上下块的部分内容,让跨块的语义不断腰。块大小的经验值:技术文档300-500字(本项目的512字符即此档),法律合同这类需要长上下文的500-800字;文档类型不同最优块长不同,没有万能值。
也要知道RAG的能力边界:跨距很远的复杂关联理不清。像长篇小说里前后相隔几十章的人物关系、事件因果,靠局部块的相似度检索很难串起来——只能靠多检索轮次、注入更多上下文来提高准确度,这正是GraphRAG(向量+图谱遍历)要解的问题,后文提及。
2. Embedding(向量化)。查询与文档必须用同一个模型——同一语义空间的向量才有可比性。维度影响存储与计算成本。向量模型的本质是把自然语言按模型词表转换成机器语言(向量):文本先分词、映射到词表条目、聚合成一串浮点数——"机器读得懂的语义坐标"。
3. 检索。top-K相似度是基础形态;混合检索(BM25关键词 + 向量双路召回再融合)是2025-2026的事实标配——关键词管精确匹配(组件名、prop名),向量管语义泛化(口语、跨语言);HyDE(假设文档嵌入)更进一步:让LLM先生成一个假设答案,用假设答案的向量去检索,弥合"短查询vs长文档"的语义空间不对齐。
4. Rerank(重排)。向量检索是双塔结构(query和doc分别编码再比距离),快但粗。Rerank用cross-encoder对query-doc对逐一联合编码打分,能看到词序和精确匹配,准但贵——所以只对初检的top 20-50重排,取最终top 3-10。
5. 评估。RAGAS是主流框架,核心"RAG三元组":忠实度(答案是否被上下文支持)、答案相关性(是否切题)、上下文相关性(检索质量)——分别覆盖生成层与检索层。
Mastra的RAG体系
Mastra把RAG做成了标准化API。官方文档的完整链路五步:
import { embedMany } from 'ai'
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'
import { MDocument } from '@mastra/rag'
import { LibSQLVector } from '@mastra/libsql' // 官方示例用PgVector,这里换成项目的LibSQL
// ① 文档实例化(fromText/fromHTML/fromMarkdown/fromJSON四个工厂)
const doc = MDocument.fromMarkdown('# 组件库文档...')
// ② 切块
const chunks = await doc.chunk({ strategy: 'markdown', size: 512, overlap: 50 })
// ③ 向量化(embedMany批量,ModelRouterEmbeddingModel用'provider/model'字符串)
const { embeddings } = await embedMany({
values: chunks.map(chunk => chunk.text),
model: new ModelRouterEmbeddingModel('openai/text-embedding-3-small'),
})
// ④ 建索引并入库
const vector = new LibSQLVector({ url: 'file:.temp/docs-vector.db' })
await vector.createIndex({ indexName: 'docs', dimension: 1536, metric: 'cosine' })
await vector.upsert({
indexName: 'docs',
vectors: embeddings,
metadata: chunks.map(c => c.metadata), // source出处随行
})
// ⑤ 查询:query文本→embed→top-K
const { embedding } = await embed({ model: embedModel, value: '聊天气泡怎么用' })
const results = await vector.query({
indexName: 'docs', queryVector: embedding, topK: 5, minScore: 0.7,
})
typescript
各环节的能力边界:
MDocument:九种切块策略——recursive(按结构递归拆,通用默认)、markdown(按标题层级,本项目文档的主形态)、semantic-markdown(按相关标题族切,需LLM参与,带joinThreshold参数)、character/token/html/json/latex/sentence。extract: { metadata: true }还能让LLM抽取文档元数据。
向量库统一接口:全部继承MastraVector基类,createIndex/upsert/query/deleteVector四件套签名一致——换库不动业务代码。16个官方支持:PgVector、LibSQL、Qdrant、Chroma、Pinecone、MongoDB、Cloudflare Vectorize、Upstash、Opensearch、Lance、S3 Vectors等。元数据过滤用统一的MongoDB风格语法($gt/$in/$or),跨库通用。
给Agent用——createVectorQueryTool。检索能力的工具化封装,挂到agent.tools后由模型自主决定何时检索:
import { createVectorQueryTool } from '@mastra/rag'
const queryTool = createVectorQueryTool({
vectorStoreName: 'libsql',
indexName: 'docs',
model: embedModel,
})
typescript
配套各库的*_PROMPT(如LIBSQL_PROMPT)注入instructions,告诉模型该库支持哪些过滤语法。
Rerank:rerank()与rerankWithScorer(),都是"语义相关性+向量相似度+原始位置"加权重排(默认权重0.4/0.4/0.2),区别在打分器来源——前者直接传LanguageModel,后者接专用rerank服务(Cohere rerank-v3.5、Voyage rerank-2.5)。
GraphRAG:向量检索+知识图谱遍历的组合——向量初检命中后从chunk构图、沿图找关联信息。适合"信息分散多文档、需跨概念关联"的场景(createGraphRAGTool,阈值默认0.7)。教学项目用不上,知道有这东西即可。
可观测性:embedding与检索过程进trace,Studio里能看——这是Mastra RAG相对裸写SDK的工程优势。
向量数据库:分类与选型
向量数据库解决一个问题:在高维空间里快速找到离查询向量最近的K个点。可以把它想象成一个多维坐标系:语义相关的文本在这个空间里彼此靠近("聊天气泡"与"MessageBubble"是近邻),检索就是拿查询向量去找最近的邻居。什么时候需要上专业向量数据库,取决于业务数据体量——拿不准时可以直接问AI:"我有N篇文档、预计M个块,推荐适合这个体量的向量数据库"。三类形态:
专用向量库
- Qdrant:Rust实现,原生HNSW索引,延迟最低,过滤检索(filterable HNSW)最强,单二进制部署轻。单机到中小集群的主流选择
- Milvus:面向十亿级,IVF/GPU多种索引,K8s集群水平扩展,支持本地化部署与集群,有官方商业化托管(Zilliz Cloud)。企业超大规模首选,运维重
- Weaviate:Go实现,强项是原生混合检索(向量+BM25内置)与内置向量化模块,开发者体验好
数据库扩展型(嵌入式)
- PgVector:Postgres扩展,HNSW与IVFFlat两种索引。已有PG栈的团队顺手
- LibSQL:SQLite fork内建向量能力(下一节详述),
file:本地文件零服务 - sqlite-vec:SQLite扩展,纯C零依赖,只做暴力KNN无ANN索引,几十万向量内够用
- Chroma:嵌入式优先,Python/JS API,原型开发主流
云托管
Pinecone(serverless全托管免运维,价格较高)、Turso(LibSQL的云形态,嵌入式副本+分支)。
索引原理:暴力扫描 vs ANN
理解选型只需要知道两档:
- 暴力扫描(Brute-force KNN):对全表逐行算距离再排序。结果精确,但复杂度O(n)——十万级以内毫秒出结果,百万级开始吃力
- ANN近似索引(HNSW/DiskANN):牺牲一点召回率换数量级速度。HNSW建多层跳表式图结构,DiskANN(LibSQL用)是磁盘友好的图索引。都有精度-速度旋钮(HNSW的
ef、DiskANN的search_l)
选型的经验分界:百万向量以内,嵌入式方案(LibSQL/sqlite-vec/PgVector)性价比最高;百万到亿级上Qdrant/Milvus。本项目47篇文档切块后约几百个向量——离暴力扫描的性能边界差六个数量级,任何ANN都是过度设计。
向量模型:维度、价格与本地化
Embedding模型是RAG里唯一"躲不掉的模型调用"。两个来源:
API类(价格 2026-11-07 复核,数值未变)
| 模型 | 维度 | 价格 | 备注 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1536(可降维) | $0.02/1M tokens | 性价比标杆,8191 token输入上限 |
| OpenAI text-embedding-3-large | 3072(可降维) | $0.13/1M tokens | Batch API半价 |
| 智谱 embedding-3 | 默认2048,可选256-2048 | ¥0.5/1M tokens | 国内端点无墙 |
| 通义 text-embedding-v4 | 默认1024,可选64-2048 | ¥0.5/1M tokens | 基于Qwen3-Embedding |
两个跨模型通用的事实:换模型=换维度=必须重建整个索引(createIndex的dimension与模型输出必须一致);维度越高越准是错觉——MTEB榜单上很多小维度模型(经MRL降维)表现不输大维度。
本地开源类
| 模型 | 规模 | 维度 | 特点 |
|---|---|---|---|
| BGE bge-m3(智源) | 567M | 1024 | 100+语言、8192 token长输入,中文RAG事实标杆 |
| Qwen3-Embedding | 0.6B/4B/8B | 1024/2560/4096 | 2025-06发布,8B曾登MTEB多语言榜第一,支持MRL自定义维度 |
| nomic-embed-text | 137M | 768 | 轻量,8192上下文,Ollama拉取量最高之一 |
| bge-large-zh-v1.5 | 326M | 1024 | 中文经典基线 |
| mxbai-embed-large | 335M | 1024 | 偏英文 |
MTEB是HuggingFace维护的嵌入模型基准排行榜(检索/分类/聚类等任务综合分),C-MTEB是中文子榜——中文场景选型看它。
本地运行的主流方式是Ollama:ollama pull bge-m3后经/api/embeddings或OpenAI兼容端点调用——对Mastra来说就是一个{ id, url, apiKey }的模型配置,与云端API形态完全一致。
项目集成:为什么LibSQLVector是当前最优
回到ai-chat-ui。选型判断链:
第一步:查现状。 @mastra/libsql@1.21.0已装(17-03记忆在用),包内LibSQLVector与LibSQLStore同包导出——dist/index.d.ts里export * from './vector/index.js',零新增依赖。它的config注释明确支持file:本地文件模式,与memory.ts的file: URL同机制。
第二步:对比候选。
| 方案 | 新增服务 | 新增依赖 | 适用 |
|---|---|---|---|
| LibSQLVector文件模式 | 无 | 仅@mastra/rag | ✅ 单机dev、中小文档集 |
| sqlite-vec | 无 | 需自接(Mastra无官方支持) | 已有SQLite栈、想要极简 |
| Qdrant / Chroma | Docker容器 | 对应SDK | 文档量大、多消费者 |
| PgVector | Postgres实例 | @mastra/pg | 已有PG基础设施 |
| Pinecone / Turso云 | 云服务 | SDK | 免运维、数据可出本地 |
第三步:论证。 三条理由:零新增服务(一个.temp/docs-vector.db文件就是全部基础设施,删掉即重置);零接口迁移(MastraVector统一接口,将来文档量真大了换Qdrant只动初始化一行);量级匹配(几百个向量,LibSQL暴力扫描绰绰有余,Turso文档的数据也表明其向量能力定位就是轻量场景)。
真实落地:src/rag/ 模块全链路
方案已真实落进仓库,集中在packages/dev-server/src/rag/五个文件加两个消费端。集成过程本身就是一次AI协作的样本:把Mastra官方的LibSQLVector集成文档链接连同需求("基于原先libsql方案扩展向量数据库LibSQLVector集成到项目,关联本地所有文档并优化检索工具")一起投喂,再补一句约束"使用本地化方案"(向量化有成本,几十篇文章没必要走云API)——AI一次性完成了调研、依赖安装、管线与工具改造,Mastra的封装成熟度可见一斑。
入库管线(index-docs.ts + cli.ts)。一条命令跑全量入库:
# packages/dev-server 下;需 .env 配 EMBEDDING_MODEL=bge-m3(本地Ollama)
pnpm index:docs
# 输出:入库完成:47 篇文档 / 473 个语义块 / 1024 维 / 耗时 x.xs
bash
管线内的工程细节,每个都有实测依据:
- 剥frontmatter再切块:VitePress文档的
---键值区是站点配置噪音,进语料只会污染检索 - markdown切块512/overlap 50:512字符约对应bge-m3的语义单元;实测每块约500字
- MIN_CHUNK_CHARS=30过滤碎片:冒烟实测markdown切块会产出4-15字的纯标题块,这类块入库就是噪音
- 探针取维度:先embed一个块看实际维度(bge-m3=1024、text-embedding-3-small=1536),免配置自动适配——
ensureIndex还会校验既有索引维度,换模型不重建时显式报错而非静默写坏数据 - 批大小32:embed与upsert同批,Ollama大批量易超时,保守切
- metadata携带source/title/text:检索端取回片段不回读源文件
检索(search-docs.ts + fusion.ts)。runSearch是检索主入口,双路召回:
// 向量路:查询文本 → embed → LibSQLVector topK
const hits = await store.query({
indexName: DOCS_INDEX_NAME, queryVector: embeddings[0]!,
topK: 8, // 融合前取top8,同source多块取最高分
minScore: 0.35, // cosine门槛:bge-m3实测无关块<0.3,滤掉
})
// 关键词路:TF-IDF + 标题×5加权(16-02引擎保留)
// 融合:RRF(Reciprocal Rank Fusion,k=60)按名次倒数融合两路,再截top5
const results = rrfFuse(vectorHits, keywordOutcome, 5)
typescript
选RRF而不是分数加权相加,因为两路的分数量纲完全不同(cosine相似度 vs TF-IDF加权分)——按名次融合天然免疫量纲差异,k=60是业界标准值。融合规则也定了细则:snippet取向量路(块级定位更精确),matchedTerms取关键词路(保留字面命中证据)。
降级路径:向量路任何失败(端点不可达/索引未建/维度不符)都被catch降级为纯关键词,degradedReason带回理由、日志warn——工具永不因向量路崩。返回的mode字段如实记录走了hybrid还是keyword,timing带两路各自耗时。
配置门控(env.ts)。三个环境变量,缺省形态就是本地Ollama:
EMBEDDING_MODEL=bge-m3 # 裸名走Ollama;openai/text-embedding-3-small原样透传
EMBEDDING_MODEL_URL=http://localhost:11434/v1 # 缺省值
EMBEDDING_MODEL_API_KEY=ollama # 缺省占位
bash
createEmbedder里裸模型名自动补ollama/前缀、含provider/的模型串原样透传——本地与云端只差env,代码零分支。未配EMBEDDING_MODEL时embedding为null,向量路直接跳过,回到16-02的纯关键词形态。
演示端点与页面(routes/vector.ts)。三个端点撑起docs站的向量检索演示页(/vector-search-demo):
| 端点 | 作用 |
|---|---|
GET /api/vector/stats | 库状态:模型、chunks数、维度——演示页顶部状态卡 |
POST /api/vector/search | 双路对比:同query分别跑纯关键词(强制embedding:null)与混合检索,并列返回——直观看到"关键词2篇 vs 混合5篇" |
GET /api/vector/chunks | 向量库浏览:node:sqlite直读docs_chunks表分页列出全部分块,可按source过滤、按文本搜索 |
/search的对比设计是教学亮点:把16-02的基线和升级后的引擎放在同一个响应里,语义检索的价值不用讲,跑一次就看见。Studio侧也有对应入口——/api/vectors能直接查看当前项目接入的向量库列表。
两条项目惯例贯穿始终:库文件落包内.temp/并先跑ensureDbDir(17-03同款,libsql不自动建父目录);与记忆分库——docs-vector.db与dev-server.db分文件,文档索引是可重建的派生数据(源头是md文件,重跑pnpm index:docs即复原),记忆是不可重建的用户数据,删除重建语义完全不同。
工具升级:引擎置换而非接口重构
关键设计决策:不新建工具,升级16-02的search_docs内部实现。工具的输入输出形状不变(keywords数组进、带source的结果出),改动收敛在三层:
| 层 | 改动 |
|---|---|
tools注册表(16-03配置化注册) | 结构不变,换文件内容 |
前端ToolCallPanel/DocsResultCard(16-02) | 零改动,StreamChunk协议不变 |
| 工具description | 更新为"语义+关键词混合"口径,支持自然短语入参(如「聊天气泡怎么改圆角」) |
| instructions | "必须先检索"规范照旧 |
规划时曾想过"同义词表可以整个删掉——向量检索天然跨语言跨说法",落地时没有这么做:关键词路(TF-IDF+标题加权+同义词表)被整体保留,成为混合检索的一路。原因就是混合检索的分工——组件名、prop名这类精确查询,关键词依然更准;语义泛化交给向量。同义词表从"跨语义鸿沟的唯一桥"降级为"关键词路的加分项",负担不变、角色变轻。HINT_NO_HIT同样保留(未命中时的换词建议从MessageBubble更新为PromptInput)。
成本账(19-02的规矩):录制时47篇md约200KB、473个块,一次性入库——本地Ollama下零API费用,text-embedding-3-small下不到$0.01;检索时每次一次query embedding(几十token,忽略不计)+本地毫秒级计算。语料此后随课程增长到70+篇,成本仍在分钱量级。语义检索的钱比生成模型便宜两个数量级。
本地化组合:Ollama + LibSQLVector
把上面两个"本地"拼起来,得到一套完全离线的RAG:
Ollama bge-m3(本地向量模型,1.2GB,无API费用)
↓ OpenAI兼容端点
ModelRouterEmbeddingModel({ url: 'http://localhost:11434/v1', ... })
↓
LibSQLVector(本地向量库,.temp/docs-vector.db)
↓
search_docs工具 → docs-agent → 本地LLM(可选)
text
适用场景与18-03讲的端侧四场景同构:隐私(文档不出机器,企业内网知识库)、零API成本(一次性入库0元、检索0元)、离线(断网可用)。与18-03浏览器端方案的关系是分工:端侧跑bge-small级小模型(几十MB,多租户隔离),服务端本地跑bge-m3级(1.2GB,质量更好)——端侧轻活、服务端重活,混合架构里各就各位。
实操层面的经验补充:
- 模型选择:Ollama库里embedding模型都不大,
ollama pull bge-m3约1.2GB,常用的还有nomic-embed-text(137M)和Qwen系列embedding。硬件门槛低——32G内存的开发机顺跑,4G显存的GPU也带得动;embedding模型对算力的要求远低于对话级LLM - 验收方式:入库后在playground问"组件库里有哪些核心的包"这类问题,检索结果里每条都带score——这是embedding模型给出的相似度评分,分数直观反映语义相关性;Studio的
/api/vectors能看到当前接入的向量库列表 - 演示组织:playground的各类演示(关键词检索、多agent、workflow、向量检索)收敛到一个下拉菜单里,左侧带调用链路说明和示例——示例点一下就能看到"纯关键词2篇 vs 语义混合5篇"的对比,向量库浏览页能看到473个块、每块约500字的切块实况
- 引擎可替换:本地Ollama只是embedding服务的运行环境之一,将来部署到云端换推理引擎(TEI等,17-05实操),只要模型不变(bge-m3还是bge-m3,维度一致),
docs-vector.db原样可读,只改EMBEDDING_MODEL_URL一行;若换了模型则维度变、必须重建索引(前文ensureIndex的维度校验会拦住)
本地化的代价也要说清:首次下载模型1.2GB、embedding速度取决于本机硬件(无GPU时batch入库明显慢于API)、模型质量略低于云端旗舰。教学与开发环境,这个组合是自由度最高的选择。
生产部署:CPU服务器上的RAG基础设施
本地化组合讲的是"自己机器全离线";真实生产还有一个高频约束——服务器上没有embedding API(离线内网部署,或不想为查询侧付API费)。这节把生产部署的完整决策链走一遍:查询向量从哪来、CPU上跑什么推理服务、向量库怎么部署、服务器要什么配置。
先理清一个死结:查询向量躲不掉
向量检索的两侧不对称:文档向量可以离线预生成(一次性入库),但每次查询的query向量必须现场算——没有查询向量,库里存的文档向量无法比距离,就是死数据。所以"生产没有embedding API"的实质是:查询侧的embedding能力从哪来。
还有一颗雷先排掉:同模型原则。已生成的向量数据用什么模型算的,查询时必须用同一个——如果入库用OpenAI、生产没有OpenAI API,那批向量在生产不可用(维度与语义空间都对不上),只能换模型重建索引(docs静态,47篇重建是几分钟几分钱的事,沉没成本忽略)。
一个关键区分能解掉大部分困惑:需要embedding模型 ≠ 需要embedding API。API只是算embedding的一种途径;本地模型产出的是同一种东西——一个float数组,不联网、不花钱,只是要带模型文件。
无embedding API时的五条出路
| 出路 | 查询时需要什么 | 检索质量 | 适用 |
|---|---|---|---|
| ①本地embedding(Ollama/进程内ONNX)+重建索引 | 本地模型文件 | 高(真语义) | 通用推荐 |
| ②向量消费在离线侧(聚类生成同义词表) | 无 | 中高 | 已有OpenAI向量数据舍不得扔 |
| ③LLM query改写+关键词/FTS检索 | LLM API(做RAG必然有) | 中高 | 无embedding但有LLM |
| ④长上下文直接挑文档(标题列表进prompt) | LLM API+文档量小 | 中 | 47篇量级够用 |
| ⑤预计算查询向量 | 离线embed一批固定query | 高但封闭 | FAQ场景 |
路线②有个巧思值得展开:已生成的向量数据在离线侧仍有价值——用文档向量聚类(同主题chunk自然聚在一起),让LLM给每簇生成"用户可能的口语说法",产出一张自动生成的同义词表(16-02那张20组人工表的自动化版,人工版"清单跟着失败案例长永远追不完",聚类版一次性做完),运行时纯关键词检索、零向量依赖。本质是把语义计算前置到离线,运行时只做字面匹配。
路线③的本质是让LLM干embedding的活:"聊天气泡怎么用"经一次轻模型改写成"MessageBubble 气泡组件 props用法",再走关键词检索(SQLite侧可顺手把LIKE升级成FTS5全文索引)。这是16-02同义词表的无限版,每次查询多几十token换语义泛化。路线④是16-02"文档量小"判断的极限版本:47篇文档的"标题+一句话摘要"约2K token直接塞prompt让LLM挑1-3篇再读,零embedding零向量库零检索设施,文档涨到几百篇之前都够。
唯独行不通的组合:向量库存了数据+检索时既没API也不跑本地模型——那批向量在查询路径上就是死的。
CPU推理服务选型:为什么不是vLLM
直觉会想到vLLM,但它不适合这个场景:vLLM的核心优势(PagedAttention、continuous batching)全在GPU上的LLM自回归解码,embedding是单次前向的小模型负载,吃不到这些红利;且vLLM的CPU后端至今是实验性的,安装要拖数GB的torch全家桶——大炮打蚊子,且炮不适配。
CPU Linux服务器上跑embedding的真正候选:
| 服务 | 定位 | CPU支持 | OpenAI兼容 | 适合 |
|---|---|---|---|---|
| TEI(HuggingFace官方) | 专业embedding服务 | ✅官方CPU镜像(Rust/Candle) | ✅ | 纯embedding生产首选 |
| infinity | 专业embedding服务 | ✅ONNX引擎 | ✅ | embedding+rerank一起跑 |
| Ollama | 通用本地推理 | ✅本职(llama.cpp) | ✅ | 顺手还要跑小LLM |
| Xinference | 多模型平台 | ✅ | ✅ | 要Web UI管理多模型 |
| vLLM | LLM高吞吐引擎 | ⚠️实验性 | ✅ | GPU+高QPS才值得 |
推荐TEI CPU版:HuggingFace官方维护、专为embedding设计的极简服务,单容器即起:
docker run -d -p 8080:80 \
--platform linux/amd64 \
ghcr.io/huggingface/text-embeddings-inference:cpu-latest \
--model-id BAAI/bge-small-zh-v1.5 \
--max-client-batch-size 64
bash
Mastra侧照旧{ url: 'http://your-server:8080/v1' }一接完事。备选infinity的优势是能同时挂多个embedding模型+rerank模型做路由(前文rerank环节的BGE-reranker也能用它跑)。
一个真实部署的注记(完整过程见17-05):把本节的部署任务交给AI执行时,AI没有照抄"TEI优先"的建议,而是先检查了现状——本地索引是Ollama的bge-m3建的;笔记推荐的TEI CPU方案虽然更适合2C2G紧凑配置,但对应的模型搭配与现状不同,意味着要本地重建索引、内存占用也要重新评估;而演示服务器2C4G上Ollama已经跑得很稳,于是临场决策"不切换、直接用Ollama",并把决策理由记回了笔记。这是同模型原则在实操中的自然生效:先查向量库是哪个模型建的,再定推理引擎——引擎可以换,模型不能悄悄换。
另一个经验:给客户做演示、模型数量少,Ollama足够;要在一台服务器上部署多个模型且要较高性能吞吐,才需要熟悉Xinference、vLLM这类多模型平台——它们都是容器化运行,一条docker命令即起。
另一个形态是进程内嵌(不起独立服务):Node进程里用transformers.js直接加载ONNX量化模型,少一个运维对象(健康检查、开机自启、OOM看护全省),代价是吃Node进程自己的内存:
import { pipeline } from '@huggingface/transformers'
// 启动时加载一次(bge-small-zh q8约100MB,常驻内存),之后每次query几十毫秒
const embedder = await pipeline('feature-extraction', 'Xenova/bge-small-zh-v1.5', {
dtype: 'q8',
})
export async function embedQuery(text: string): Promise<number[]> {
const out = await embedder(text, { pooling: 'mean', normalize: true })
return Array.from(out.data)
}
typescript
这就是18-03浏览器端同一技术栈(Transformers.js)在Node侧的CPU路径——一套知识两端复用。
服务器上的向量库:单实例分界线
前文"百万以内嵌入式最优"的结论在生产部署时多一条架构分界:服务是单实例还是多实例。
单实例Node ──→ 直接读写本地db文件 ✅ 文件模式没问题
多实例(LB后N个Node)──→ 各自的db文件 ──→ ❌ 数据不一致
↓ 解法
独立向量服务(Qdrant)共享
text
单实例(本项目形态)下LibSQL文件模式与本地开发完全同构,三条实操:
- 库文件放数据目录,不放代码目录。本地开发的
.temp/惯例不进生产——用/var/lib/或Docker volume,路径走环境变量:url: process.env.VECTOR_DB_URL ?? 'file:/var/lib/ai-chat/docs-vector.db' - 两个实测坑(本项目踩过):绝不用
:memory:模式(内存模式写事务丢表);重建索引用drop+create,不要truncate后重插(LibSQL向量表truncate重插报shadow row错误)。全量重建=deleteIndex+createIndex+upsert - 备份=拷文件。读多写少场景直接cp就是有效快照,持续备份可用litestream流复制。对比Qdrant要走snapshot API、PgVector要pg_dump——文件库的备份成本三者最低
将来文档涨到几十万chunk、或要多实例部署:升级Qdrant(Docker单容器约几百MB内存,CPU友好)。升级成本是改初始化一行——MastraVector统一接口的承诺到生产依然成立:
// 今天:文件模式
new LibSQLVector({ url: 'file:/var/lib/ai-chat/docs-vector.db' })
// 多实例:共享服务
new QdrantClient({ url: 'http://qdrant:6333' })
// 不想自己管文件:LibSQL的托管形态
new LibSQLVector({ url: 'libsql://your-db.turso.io', authToken: '...' })
typescript
一个部署细节收尾:索引重建是低频脚本,与web服务分离(前文入库脚本模式的延伸)——CI或本机跑embedding入库产出db文件,随制品部署到服务器。这样生产服务器只需要查询侧的embedding服务在,入库在别处完成。
服务器配置估算
按本方案的组件清单估算(TEI CPU + bge-small-zh + LibSQL文件库 + Node主服务):
| 组件 | 内存(RSS) | CPU需求 | 磁盘 |
|---|---|---|---|
| Linux系统+Docker daemon | ~400MB | 常驻 | 3-5GB |
| TEI CPU(bge-small-zh-v1.5) | 250-400MB | 单次查询1核×几十ms | 模型90MB+镜像~1GB |
| Node主服务(Hono+Mastra+SSE流式) | 250-450MB | 平时空闲 | ~500MB |
| LibSQL向量库(几百chunk×512维) | <50MB | 毫秒级可忽略 | ~2MB |
| 日志/缓存余量 | ~100MB | — | 2-3GB |
| 合计 | ~1.1-1.4GB | 峰值2核 | ~8-10GB |
两档配置(成本为2026年国内轻量应用服务器量级,各厂商活动价差异大):
| 档位 | 规格 | 说明 | 月成本量级 |
|---|---|---|---|
| 最低可行 | 2C2G+20GB盘 | 能跑,余量30-40%,1C1G会swap | ¥50-100 |
| 推荐 | 2C4G+40GB盘 | 余量充足,将来加rerank也不用升配 | ¥100-200 |
模型选择是唯一的内存变量:bge-small-zh推理内存约300MB(2G舒适);bge-m3要1.5-2GB(4G起步);将来挂rerank再加约1GB。纯中文文档bge-small够——2C4G是"选什么模型都不会错"的配置。不需要GPU、不需要高主频:embedding的CPU负载是短脉冲(每次几十毫秒),普通共享核就行。
并发能力核验——单次RAG查询的资源画像:
用户提问 → TEI embed(1核×30ms)→ 向量查询(<5ms)
→ LLM API生成(外部服务1-3s,不占本机)→ SSE转发(每连接几MB)
text
瓶颈在外部LLM API的1-3秒,不在本机:2核的embedding吞吐约30-60 QPS,而LLM并发远到不了这个量——几十个同时在线用户毫无压力,教学/演示场景用掉不到5%算力。这个量级下向量库只占2MB磁盘、几十MB内存,钱几乎全花在Node服务和embedding模型上——再次印证小规模场景嵌入式方案的正确性。
成熟平台:RAGFlow / Dify / FastGPT
自研RAG之前,先看开源世界已经把"知识库"做成了什么产品级形态。三个代表性平台2026年的状态:
| 维度 | RAGFlow | Dify | FastGPT |
|---|---|---|---|
| 开发方 | infiniflow | LangGenius | labring(Sealos团队) |
| 定位 | 深度文档理解RAG引擎 | LLM应用开发平台 | 知识库问答平台 |
| 开源协议 | Apache-2.0 | Apache 2.0+附加条件(多租户SaaS限制) | Apache 2.0+附加条件(禁未授权SaaS) |
| Star量级(2026-11-07 核实) | ~93.7k | ~160k | ~30k |
| 部署依赖 | MySQL+ES+Redis+MinIO,最低8GB内存 | PG+Redis+向量库(默认Qdrant),4GB | MongoDB+PG(pgvector),2GB |
| 文档解析 | DeepDoc:模板匹配+视觉理解,表格/公式/双栏/扫描件OCR极强 | Unstructured通用解析 | 自研分块+中文优化 |
| 检索配置 | 混合检索+BGE-reranker重排+引用溯源定位原文 | hybrid/semantic/full_text+rerank | embedding/fullText/mixed三模式+rerank |
| 本地模型 | 支持Ollama/vLLM | 支持Ollama/vLLM | 支持Ollama/vLLM |
三个平台的差异化一句话:复杂工作流编排选Dify、知识库开箱问答选FastGPT、高精度复杂文档(金融/法律/学术,尤其是表格和扫描件)选RAGFlow。行业里常见Dify+RAGFlow组合(编排用前者、检索引擎用后者)。
注意它们检索配置的共性——混合检索+重排几乎成了平台标配,印证了上文"五环节"里的技术判断;RAGFlow的DeepDoc是三家最强的差异点,因为它啃的是最难的那类文档(扫描件、双栏PDF、嵌套表格)。
平台 vs 代码内嵌:什么关系
看到这里容易困惑:既然有现成平台,为什么还要自己写RAG?两者不在一个层次:
| 维度 | 知识库平台(RAGFlow等) | 代码内嵌RAG(本节方案) |
|---|---|---|
| 形态 | 独立部署的服务,UI管理文档/知识库 | 库代码,嵌在你的应用进程里 |
| 用户 | 非技术同事可运营(上传文档、调检索参数) | 开发者 |
| 交付物 | 一个知识库问答站 | 你产品里的一个能力(如docs-agent的检索) |
| 运维 | 一套Docker栈(ES/Redis/MinIO...) | 零服务 |
| 检索深度 | 可视化配置,覆盖高级特性 | 代码级控制,按需实现 |
判断标准:知识库是不是你产品本身。做企业知识库产品/内部问答系统——上平台,别自研,DeepDoc那类文档解析能力自研成本极高;知识检索是你产品的一个内嵌能力(ai-chat-ui的docs-agent就是典型)——代码内嵌,Mastra这套足够,且完全可控可调试(Studio看trace)。ai-chat-ui作为教学项目还有第三层理由:课程的目的是理解机制,平台把机制包进黑盒,内嵌实现才看得见每个环节。
小结
- RAG五环节:切块(质量决定上限,技术文档300-500字档)→ embedding(查询文档必须同模型,本质是词表转换成"语义坐标")→ 检索(混合检索是事实标配)→ 重排(cross-encoder只对top-N)→ 评估(RAGAS三元组);Naive→Advanced→Modular三代演进,Agentic检索是当前形态;能力边界——跨距远的复杂关联理不清,那是GraphRAG的战场
- Mastra RAG体系:MDocument九种切块策略 + embedMany向量化 + MastraVector统一接口(16库可换)+ createVectorQueryTool工具化 + rerank/GraphRAG进阶
- 向量库选型:百万向量以内嵌入式(LibSQL/sqlite-vec/PgVector)性价比最高;LibSQL支持F32_BLOB存储、暴力扫描与DiskANN两级、
vector_top_k查询,定位轻量场景 - 向量模型:API类(OpenAI 3-small $0.02/1M是标杆)与本地类(bge-m3中文事实标杆、Qwen3-Embedding后起);换模型=换维度=重建索引
- 项目集成(已真实落地):
src/rag/五文件——入库管线pnpm index:docs(剥frontmatter/512+50切块/探针定维/drop+create重建/批32)+ 双路检索(关键词TF-IDF+标题×5 与 向量top8·minScore 0.35,RRF k=60融合top5,失败降级纯关键词)+ env三变量缺省Ollama +/api/vector三演示端点;实测47篇/473块/1024维。同义词表与HINT_NO_HIT保留在关键词路——从"唯一的桥"降级为混合检索的一路 - 本地化组合:Ollama bge-m3 + LibSQLVector = 完全离线RAG,隐私/零成本/断网可用,与18-03端侧方案构成"端侧轻活、服务端重活"分工;换推理引擎(同模型)只改
EMBEDDING_MODEL_URL,库文件原样可读 - 生产部署:查询向量躲不掉但"需要embedding模型≠需要embedding API";无API五条出路(本地模型/离线聚类榨干已生成向量/LLM改写/长上下文/预计算);CPU推理服务选TEI而非vLLM(vLLM的优势全在GPU上的LLM解码),但实操先看向量库是哪个模型建的——引擎可换、模型不能悄悄换(17-05实操:AI临场选了Ollama);向量库看单实例/多实例分界(单实例文件模式+拷文件备份,坑:
:memory:丢表、truncate重插报shadow row);2C4G百元/月量级服务器绰绰有余,瓶颈永远在外部LLM API - 平台 vs 内嵌:知识库是产品选平台(RAGFlow文档解析最强),检索是内嵌能力选代码(Mastra可控可调试),教学选内嵌(看得见机制)
至此17章五个方向闭环:MCP接入外部工具生态(01)、插件系统预留动态扩展(02)、记忆让Agent记住对话(03)、RAG让Agent读懂文档(04)、云服务器生产部署实操(05)。下一章进入AI交互前沿体验。
↑