主题
第4章 检索增强入门(RAG)
4.1 为什么需要检索增强
只依赖模型参数做问答,很快会遇到三个核心问题:
- 回答可能不基于真实业务资料,容易出现幻觉。
- 模型不知道企业私有知识、最新制度、内部流程和版本变更。
- 即使回答看起来合理,也很难解释“依据来自哪里”。
检索增强(RAG)的价值,不只是“让回答更准”,更重要的是把回答建立在可追溯证据之上。 对企业级 Agent 来说,这意味着系统终于可以回答“为什么这么说”“这句话出自哪份制度”“资料不足时为什么要拒答”。
很多前端工程师第一次接触 RAG,会先把它理解成“向量库 + 模型接口”的组合。 但在真实业务里,RAG 更像一条完整的数据链路:
- 文档怎么进入系统。
- 文档怎么切片。
- 检索为什么命中或没命中。
- 为什么最终注入了这些片段。
- 回答怎么展示引用和风险提示。
前端迁移提示
这一章和前端工程并不远,很多已有经验都能直接迁移:
- 数据装配思维 -> 检索证据的上下文组织。
- 列表/卡片展示 -> 回答卡片、引用展开、风险提示。
- 筛选与排序思维 -> 检索召回、重排和结果裁剪。
- 状态与反馈设计 ->
answer / citations / confidence / need_human_review结果结构展示。
最小项目目标
本章建议先完成一个“企业知识问答最小闭环”项目,例如:
- 制度问答助手。
- 产品文档问答助手。
- 售后知识问答助手。
如果你想直接拿最小骨架来改,可以先看:
4.2 核心链路总览
RAG 链路图
RAG 不是单个步骤,而是一条由“文档进入 -> 证据召回 -> 结果生成 -> 引用回传 -> 评测验证”组成的完整链路。 如果其中任意一环做得模糊,最终就会出现“看起来像回答对了,但其实无法验证”的问题。
一、文档进入系统
第一步不是调用模型,而是先确定知识源:
- 哪些文档允许进入知识库。
- 文档版本是否可靠。
- 是否有发布时间、文档类型、章节标题等元数据。
- 是否存在旧文档与新文档冲突。
这一层如果没处理好,后面的检索和生成很难稳定。
二、检索是为了拿证据,不是为了凑上下文
检索的目标不是“尽量多拿一些文本给模型看”,而是“尽量拿到最相关、最可信、最容易被引用的证据”。 因此你要持续思考:
- 召回的是不是对的文档。
- 片段是不是太长或太碎。
- 是否存在很多噪音内容。
- 最终送进模型的上下文是不是过多。
三、生成必须和引用绑定
RAG 真正和普通问答拉开差距的地方,是回答不仅要有内容,还要有依据。 一个最小响应结构可以像这样:
json
{
"answer": "根据当前制度,年假申请原则上需至少提前 3 个工作日提交。",
"citations": [
{
"doc_id": "leave_policy_v2",
"title": "员工休假制度 V2",
"quote": "年假申请原则上需至少提前 3 个工作日提交。"
}
],
"confidence": "high",
"need_human_review": false
}你可以直接复用:
docs/examples/assets/samples/example-02-rag-answer.json
四、前端必须把“证据”和“风险”展示出来
很多 RAG Demo 最大的问题,不在后端,而在前端只展示了一段答案文本。 更合理的方式是至少展示以下信息:
- 回答正文。
- 引用来源。
- 引用片段。
- 置信度标签。
need_human_review风险提示。- 用户反馈入口。
五、评测决定这条链路是否可交付
RAG 项目最容易出现的错觉是“这个问题看起来答对了,所以系统应该没问题”。 真实工程里必须用评测集回答:
- 命中率是否稳定。
- 引用是否准确。
- 不可回答场景是否能拒答。
- 同一套配置是否可复现。
4.3 文档处理与切片
很多团队一开始会把 RAG 的重点放在模型或向量库上,但真实效果经常更受“文档处理质量”影响。 如果文档混乱、过时、没有层级、切片策略糟糕,即使后面的检索和生成都很努力,最终效果也不会稳定。
一、先处理文档质量,再谈检索质量
建议先检查:
- 文档是否有清晰标题层级。
- 是否能区分正式制度与临时说明。
- 是否存在过期文档。
- 是否保留版本号和更新时间。
二、切片目标是“既能召回,又能引用”
切片不是越短越好,也不是越长越好。 最小原则是:
- 一条片段要足够表达一个完整事实。
- 一条片段又不能长到混入太多无关信息。
- 切片后仍要保留文档标题、章节路径、版本信息等元数据。
三、推荐的第一版切片策略
对于初学者,建议先用简单可靠的策略:
- 按标题和段落切片。
- 控制单片段长度在一个可阅读范围内。
- 每个片段附带
doc_id / title / section / updated_at等字段。
四、切片之后要保留可追溯元数据
因为后续回答中的引用、版本判断和前端展示都依赖元数据。 如果切片时把这些信息丢了,后面很难补回来。
4.4 检索与重排
检索增强项目里,最常见的问题不是“模型不够强”,而是“没把最相关的证据送到模型面前”。 因此检索阶段的重点是召回质量,而不是盲目堆更多片段。
一、检索的第一目标是命中对的文档
你要先回答:
- 用户问题对应的核心资料是否被召回了。
- 新版本制度是否优先于旧版本。
- 同义表达或不同问法是否仍能命中同一知识点。
二、重排是减少噪音而不是增加复杂度
当召回结果变多时,常见问题是:
- 正确文档在前几名之外。
- 有很多语义相近但不关键的片段。
- 上下文被低相关证据稀释。
这时重排的价值是把“最值得进入模型上下文的证据”放到前面,而不是让系统更花哨。
三、第一版最值得关注的 3 个参数
对于最小项目,建议优先关注:
top_k:召回多少条。- 切片粒度:一条片段有多长。
- 是否启用重排或简单排序策略。
如果一开始同时动 6 到 8 个变量,后面几乎无法判断哪个因素真正生效。
四、前端也应该参与检索质量判断
前端不只是展示结果,也可以帮助发现问题:
- 引用来源总是跑偏。
- 引用片段太长,用户看不懂。
- 相似问题下引用不稳定。
- 风险提示显示过多或过少。
这些都是检索链路质量的外在表现。
4.5 生成与引用
RAG 不是“检索完就把结果交给模型自由发挥”。 真正稳定的系统,需要对生成层进行清晰约束,确保模型基于证据回答,并把证据带回给前端和用户。
一、回答必须围绕证据生成
推荐约束模型:
- 只能基于给定证据回答。
- 证据不足时明确说明无法确认。
- 回答要简洁,不要为了显得智能而扩写无依据内容。
二、引用展示要对用户有用
引用不是“挂一个文档名”就够了。 更有用的做法是:
- 显示文档标题。
- 提供引用片段。
- 必要时显示章节路径或版本信息。
三、加入置信度和人工复核标记
对于企业场景,建议在最小结构里保留:
confidence:高 / 中 / 低。need_human_review:是否建议人工复核。
你可以直接参考:
- 示例2 RAG接口与评测
docs/examples/assets/samples/example-02-rag-answer.json
四、前端回答卡片建议
建议最少包含这些区域:
- 回答正文。
- 引用来源列表。
- 置信度标签。
- 人工复核提示。
- “是否有帮助”的反馈入口。
4.6 RAG 管道代码实战(TypeScript)
前面几节讲了 RAG 链路的各个阶段和设计原则,这一节用可运行的 TypeScript 代码把完整流程串起来。 如果你习惯前端技术栈,这些代码可以直接在 Node.js 环境运行,帮助你建立对 RAG 管道的直觉。
前端开发者知识桥梁
在看代码之前,先把 RAG 中几个核心概念映射到你熟悉的前端场景:
| RAG 概念 | 前端类比 | 说明 |
|---|---|---|
| Embedding | 把文本转成「特征向量」 | 类似图片转 base64,但编码的是语义信息而不是像素 |
| 余弦相似度 | 计算两个向量方向是否一致 | 1 = 完全相同,0 = 无关,-1 = 完全相反 |
| 语义检索 | 搜索组件的后端实现 | 搜索的是语义而不是关键词匹配 |
| 文本切片 | 分页 / 虚拟列表 | 把长文档拆成小块,每块独立处理,避免超出上下文窗口 |
| 上下文组装 | 接口聚合 / BFF 层 | 把多个检索结果拼装成一份完整的 Prompt 交给模型 |
一、最小 RAG 管道(纯内存实现)
这个版本不需要任何数据库,所有向量存在内存里,适合理解流程和本地实验。
前置准备:
bash
# 初始化项目
mkdir mini-rag && cd mini-rag
npm init -y
npm install openai typescript tsx
npx tsc --init
# 设置 API Key(也可以写在 .env 里)
export OPENAI_API_KEY="sk-your-key-here"
# 准备一份知识库文件(用你自己的文档替换)
echo "React Router 支持多种路由模式,包括 BrowserRouter 和 HashRouter。
BrowserRouter 使用 HTML5 History API,适合生产环境。
HashRouter 使用 URL 的 hash 部分,适合静态部署。
配置路由时需要在根组件中包裹 Router 组件,然后用 Route 定义路径映射。
嵌套路由通过在父路由组件中渲染 Outlet 来实现。
动态路由参数使用 :paramName 语法,通过 useParams Hook 获取。" > knowledge-base.txt完整代码:
typescript
// mini-rag.ts — 最小 RAG 管道,无需数据库
import OpenAI from 'openai'
import { readFileSync } from 'fs'
const client = new OpenAI()
// 步骤 1:文本切片
function splitText(text: string, chunkSize = 500, overlap = 50): string[] {
const chunks: string[] = []
for (let i = 0; i < text.length; i += chunkSize - overlap) {
chunks.push(text.slice(i, i + chunkSize))
}
return chunks
}
// 步骤 2:获取 Embedding 向量
async function getEmbeddings(texts: string[]): Promise<number[][]> {
const res = await client.embeddings.create({
model: 'text-embedding-3-small',
input: texts,
})
return res.data.map(d => d.embedding)
}
// 步骤 3:余弦相似度计算
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB))
}
// 步骤 4:检索最相关的片段
async function retrieve(query: string, chunks: string[], embeddings: number[][], topK = 3) {
const [queryEmb] = await getEmbeddings([query])
const scored = chunks.map((chunk, i) => ({
chunk,
score: cosineSimilarity(queryEmb, embeddings[i]),
}))
return scored.sort((a, b) => b.score - a.score).slice(0, topK)
}
// 步骤 5:组装上下文并生成回答
async function ragAnswer(query: string, context: string[]): Promise<string> {
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: `基于以下参考资料回答用户问题。如果资料中没有相关信息,请说明。
参考资料:
${context.map((c, i) => `[${i + 1}] ${c}`).join('\n\n')}`
},
{ role: 'user', content: query }
],
temperature: 0,
})
return res.choices[0].message.content!
}
// 完整流程
async function main() {
// 加载文档(实际项目中可以是 API 文档、产品手册等)
const text = readFileSync('knowledge-base.txt', 'utf-8')
const chunks = splitText(text)
console.log(`文档切分为 ${chunks.length} 个片段`)
// 预计算所有片段的 Embedding
const embeddings = await getEmbeddings(chunks)
console.log('Embedding 计算完成')
// 检索 + 生成
const query = '如何配置 React 项目的路由?'
const results = await retrieve(query, chunks, embeddings)
console.log(`检索到 ${results.length} 个相关片段,最高相似度: ${results[0].score.toFixed(3)}`)
const answer = await ragAnswer(query, results.map(r => r.chunk))
console.log('\n回答:', answer)
}
main()运行方式:
bash
npx tsx mini-rag.ts预期输出:
文档切分为 2 个片段
Embedding 计算完成
检索到 2 个相关片段,最高相似度: 0.847
回答: 配置 React 项目的路由需要以下步骤:...二、使用 Vercel AI SDK 的 RAG 实现
上面的版本直接调用 OpenAI SDK,下面这个版本使用 Vercel AI SDK,API 更简洁,更贴近前端开发者的使用习惯。
安装依赖:
bash
npm install ai @ai-sdk/openai完整代码:
typescript
// rag-with-ai-sdk.ts — 使用 Vercel AI SDK,更贴近前端习惯
import { generateText, embed, embedMany } from 'ai'
import { openai } from '@ai-sdk/openai'
import { readFileSync } from 'fs'
// 文本切片(与上一版相同)
function splitText(text: string, chunkSize = 500, overlap = 50): string[] {
const chunks: string[] = []
for (let i = 0; i < text.length; i += chunkSize - overlap) {
chunks.push(text.slice(i, i + chunkSize))
}
return chunks
}
// 余弦相似度(与上一版相同)
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB))
}
const embeddingModel = openai.embedding('text-embedding-3-small')
async function main() {
const text = readFileSync('knowledge-base.txt', 'utf-8')
const chunks = splitText(text)
// AI SDK 的批量 Embedding 接口,比手动调 REST 更简洁
const { embeddings } = await embedMany({
model: embeddingModel,
values: chunks,
})
const query = '如何配置 React 项目的路由?'
// 单条 Embedding
const { embedding: queryEmbedding } = await embed({
model: embeddingModel,
value: query,
})
// 检索
const scored = chunks
.map((chunk, i) => ({ chunk, score: cosineSimilarity(queryEmbedding, embeddings[i]) }))
.sort((a, b) => b.score - a.score)
.slice(0, 3)
console.log(`最高相似度: ${scored[0].score.toFixed(3)}`)
// 生成回答 — generateText 是 AI SDK 的核心 API
const { text: answer } = await generateText({
model: openai('gpt-4o-mini'),
system: `基于以下参考资料回答用户问题。如果资料中没有相关信息,请说明。
参考资料:
${scored.map((s, i) => `[${i + 1}] ${s.chunk}`).join('\n\n')}`,
prompt: query,
temperature: 0,
})
console.log('\n回答:', answer)
}
main()两个版本的核心流程完全一致:切片 → Embedding → 检索 → 生成。区别在于 AI SDK 把模型调用封装得更简洁,如果你的项目已经在用 Next.js + Vercel 技术栈,推荐直接用这个版本。
三、进阶:接入向量数据库(选读)
上面两个版本把向量存在内存里,适合学习和小规模实验。当文档量增大(几千到几万条片段),内存方案会遇到以下问题:
- 每次启动都要重新计算 Embedding,浪费时间和 API 费用。
- 内存占用随文档量线性增长。
- 无法做持久化和增量更新。
这时候就需要引入向量数据库。以下是几个适合前端开发者上手的选项:
| 向量数据库 | 适用场景 | 特点 |
|---|---|---|
| ChromaDB | 本地开发、原型验证 | 开箱即用,支持 JS/TS 客户端,可嵌入运行 |
| Pinecone | 生产环境、托管服务 | 全托管,无需运维,按用量计费 |
| Weaviate | 生产环境、自部署 | 开源可自部署,支持混合检索(向量 + 关键词) |
ChromaDB 接入示例
ChromaDB 是本地开发中常见且上手友好的向量数据库选项,以下是从内存方案迁移到 ChromaDB 的关键代码:
安装:
bash
# 安装 ChromaDB JS 客户端
npm install chromadb
# 本地启动 ChromaDB 服务(需要 Docker 或 Python)
# 方式一:Docker(推荐)
docker run -p 8000:8000 chromadb/chroma
# 方式二:pip
pip install chromadb
chroma run --path ./chroma-data接入代码:
typescript
// rag-with-chroma.ts — 使用 ChromaDB 持久化向量
import { ChromaClient } from 'chromadb'
import OpenAI from 'openai'
import { readFileSync } from 'fs'
const chroma = new ChromaClient({ path: 'http://localhost:8000' })
const openai = new OpenAI()
async function indexDocuments() {
const text = readFileSync('knowledge-base.txt', 'utf-8')
const chunks = text.split('\n').filter(line => line.trim().length > 0)
// 创建或获取集合(类似数据库中的"表")
const collection = await chroma.getOrCreateCollection({ name: 'knowledge-base' })
// 添加文档 — ChromaDB 会自动调用内置的 Embedding 模型
// 也可以传入自己计算的 embeddings 参数
await collection.add({
ids: chunks.map((_, i) => `chunk-${i}`),
documents: chunks,
metadatas: chunks.map((_, i) => ({ source: 'knowledge-base.txt', index: i })),
})
console.log(`已索引 ${chunks.length} 个片段到 ChromaDB`)
}
async function queryWithChroma(question: string) {
const collection = await chroma.getCollection({ name: 'knowledge-base' })
// ChromaDB 会自动计算查询的 Embedding 并执行相似度检索
const results = await collection.query({
queryTexts: [question],
nResults: 3,
})
const context = results.documents[0] ?? []
console.log(`检索到 ${context.length} 个相关片段`)
// 用检索结果生成回答(与之前逻辑相同)
const res = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: `基于以下参考资料回答用户问题。如果资料中没有相关信息,请说明。
参考资料:
${context.map((c, i) => `[${i + 1}] ${c}`).join('\n\n')}`
},
{ role: 'user', content: question }
],
temperature: 0,
})
return res.choices[0].message.content!
}
async function main() {
await indexDocuments()
const answer = await queryWithChroma('如何配置 React 项目的路由?')
console.log('\n回答:', answer)
}
main()从内存方案迁移到向量数据库的核心变化:
- 存储:
number[][]数组 → ChromaDB collection,向量持久化到磁盘。 - 检索:手写
cosineSimilarity循环 →collection.query(),数据库内部做高效近似最近邻搜索。 - 更新:每次全量重算 → 增量
collection.add()/collection.update()。 - 元数据:手动维护 → 和向量一起存储在
metadatas字段,检索时可按元数据过滤。
建议:先用内存方案跑通流程,确认检索效果符合预期后,再迁移到向量数据库。不要一开始就引入额外基础设施。
4.7 评测与验收
RAG 一旦进入真实业务,就必须从“可演示”走向“可验证”。 因此这一节的核心不是怎么写一个更漂亮的回答,而是怎么证明当前链路是否站得住。
一、最小评测集应该覆盖三类样本
建议至少准备:
- 正常问答样本。
- 边界问答样本。
- 应该拒答的样本。
你可以直接从:
docs/guide/assets/ch04-eval-template.jsonl开始。
二、最小评测结果至少包含三项指标
- 命中率。
- 引用准确率。
- 不可回答场景拒答率。
如果你要展示结果结构,可以直接复用:
docs/examples/assets/samples/example-02-eval-result.json
三、最小执行命令应该可落地
这一章保留一个可执行命令,是为了让章节和工程实践真正挂钩:
python3 scripts/eval_rag.py --input docs/guide/assets/ch04-eval-template.jsonl --output outputs/ch04-eval-result.json
四、评测不只是给后端看的
前端也应该使用评测结果来优化体验,例如:
- 哪类问题经常需要人工复核。
- 哪类问题引用展示不清晰。
- 哪些拒答场景应该给更好的提示文案。
实战案例:企业财务制度问答
- 样例 JSON:
docs/guide/assets/ch04-case-sample.json - 输入:用户提问“差旅住宿报销上限是多少?”
- 流程:上传文档 -> 切片 -> 向量检索 -> 重排 -> 生成答案并携带引用来源
- 输出:答案文本 + 文档名 + 段落片段 + 置信度
证据材料
- 成功回答样本:
docs/examples/assets/samples/example-02-rag-answer.json - 评测结果样本:
docs/examples/assets/samples/example-02-eval-result.json - 截图清单:
docs/examples/assets/screenshots/README.md - 图解配图:
docs/examples/assets/diagrams/chapter-04-rag-flow.svg - 章节代码片段索引
4.8 练习任务
- 运行
mini-rag.ts,用你自己的文档替换knowledge-base.txt,观察不同切片大小(200 / 500 / 1000)对检索结果的影响。 - 选一个最小知识场景,定义至少 10 条正常问答和 5 条拒答问答样本。
- 基于
docs/guide/assets/ch04-eval-template.jsonl增补一份属于你场景的评测样本。 - 设计一个最小回答结构,至少包含
answer / citations / confidence / need_human_review四个字段。 - 参考 示例2 RAG接口与评测,实现一个最小 RAG 接口返回结构。
- 设计前端回答卡片,并说明引用、置信度和人工复核提示分别展示在哪里。
- (进阶)尝试用 ChromaDB 替换内存存储,对比启动时间和检索效果。
4.9 验收清单
- 命中率 >= 80%
- 引用准确率 >= 85%
- 不可回答场景拒答率 >= 90%
- 至少完成 1 个带引用的最小 RAG 问答闭环。
- 至少保留 1 份回答样本、1 份评测结果样本和 1 组截图占位清单。
- 至少能说明“为什么这个系统不是普通聊天,而是建立在证据上的问答系统”。
评测执行说明(三步):
- 读取
docs/guide/assets/ch04-eval-template.jsonl,逐行解析为评测输入(case_id、question、expected_answer、expected_source、should_reject)。 - 运行评测命令并产出逐条结果,例如:
python3 scripts/eval_rag.py --input docs/guide/assets/ch04-eval-template.jsonl --output outputs/ch04-eval-result.json。 - 汇总命中率、引用准确率、不可回答场景拒答率,按 80/85/90 阈值判定是否通过(分别需 >=80%、>=85%、>=90%)。
4.10 面试表达模板
我们在企业知识问答场景中引入检索增强,让答案建立在可追溯证据上,而不是只依赖模型参数记忆。 通过文档处理、切片、检索、重排、引用回传和评测闭环,我们把系统从“会回答问题”升级成“能基于证据回答并可验证”的工程系统。
推荐答题框架
如果面试官问“你做的 RAG 和普通问答有什么区别”,可以按下面顺序回答:
- 先讲为什么企业知识不能只靠模型参数。
- 再讲文档处理、检索、生成和引用是如何串起来的。
- 再讲你怎么验证:命中率、引用准确率、拒答率。
- 最后讲前端如何把证据和风险展示给用户。
4.11 💡 自托管 RAG — BGE-M3 + ChromaDB 本地全链路
🏠 自托管替代方案
本章 RAG 管道使用 OpenAI Embedding API,但企业场景下数据往往不能发送到第三方。以下介绍如何用开源模型搭建完全本地化的 RAG 管道。
为什么 Embedding 也要本地化
| 风险 | 说明 |
|---|---|
| 数据泄露 | 发送到 OpenAI 的文本可能包含敏感业务数据 |
| 合规要求 | 金融/医疗/政务数据不允许出境 |
| 持续成本 | 大规模文档 Embedding 的 API 费用不可忽视 |
| 离线需求 | 内网环境无法访问外部 API |
本地 Embedding 选型
| 模型 | 维度 | 多语言 | 部署方式 | 推荐场景 |
|---|---|---|---|---|
| BGE-M3 | 1024 | ✅ | Ollama | 通用首选 |
| Nomic-Embed-Text | 768 | ✅ | Ollama | 轻量快速 |
| jina-embeddings-v3 | 1024 | ✅ | Docker | 长文本 |
本地 RAG 全链路代码
typescript
// Step 1: 本地 Embedding
async function localEmbed(text: string): Promise<number[]> {
const res = await fetch('http://localhost:11434/api/embed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'bge-m3', input: text }),
});
const data = await res.json();
return data.embeddings?.[0] ?? [];
}
// Step 2: 存入 ChromaDB
import { ChromaClient } from 'chromadb';
const chroma = new ChromaClient({ path: 'http://localhost:8000' });
const collection = await chroma.getOrCreateCollection({ name: 'docs' });
// 批量存储文档
for (const doc of documents) {
const embedding = await localEmbed(doc.content);
await collection.add({
ids: [doc.id],
embeddings: [embedding],
documents: [doc.content],
metadatas: [{ source: doc.source }],
});
}
// Step 3: 检索 + 本地模型生成
async function ragQuery(question: string) {
const queryEmbedding = await localEmbed(question);
const results = await collection.query({
queryEmbeddings: [queryEmbedding],
nResults: 3,
});
const context = results.documents?.[0]?.join('\n\n') ?? '';
const res = await fetch('http://localhost:11434/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen3:8b',
messages: [
{ role: 'system', content: `基于以下资料回答问题:\n${context}` },
{ role: 'user', content: question },
],
}),
});
return res.json();
}本地 RAG vs 云端 RAG trade-off
| 维度 | 云端 RAG | 本地 RAG |
|---|---|---|
| 延迟 | 网络延迟 + API 处理 | 纯本地,通常更快 |
| 成本 | 按 token 计费 | 硬件一次投入 |
| 精度 | 取决于模型与任务 | 需基于你的评测集实测,常见场景可接近 |
| 运维 | 零运维 | 需要维护 Ollama + ChromaDB |
| 扩展 | 自动扩展 | 受限于本地硬件 |
🖥️ 前端迁移提示:本地 RAG 就像 SSG(静态站点生成)vs SSR(服务端渲染)——把计算从云端搬到本地,换取更低延迟和更好的数据控制。
前置准备
bash
# 拉取 Embedding 模型
ollama pull bge-m3
# 启动 ChromaDB
docker run -p 8000:8000 chromadb/chroma深入阅读
完整的自托管 RAG 架构设计和生产优化,请阅读 专题 11:自托管 AI Agent 全栈指南 和 项目线 D:全栈自托管 Agent。