AI RAG 前端集成实战:从向量检索到流式问答的全链路实现
如果你在 2025 年做过任何一个 AI 产品,大概率都被用户问过这三个问题:
- “为什么它答得不对?”
- “它引用的资料是哪篇?”
- “能不能不要等那么久才出第一段?”
这三个问题的答案,全部指向同一项技术——RAG(Retrieval-Augmented Generation,检索增强生成)。
RAG 的概念从 2020 年 Meta AI 那篇论文诞生,到 2024 年 ChatGPT 把它”塞进”自家搜索,再到 2026 年几乎所有企业级 AI 产品都把它作为标配,RAG 已经从”加分项”变成”入场券”。但真正把它从 Demo 推到生产,从后端搬到前端,还有大量工程化陷阱:
- Embedding 模型选错了,召回率永远上不去
- 向量数据库选错了,百万级文档直接 OOM
- Top-K 检索 + 重排序没做,答非所问
- 流式输出没做好,TTFT(首 token 时间)慢到让用户以为页面卡死
- 引用高亮没做,AI 的”幻觉”会直接摧毁用户信任
这篇文章,我们就来从零到一,把 RAG 的全链路在前端跑通。读完之后,你将能:
- 理解 RAG 的核心架构和关键决策点
- 独立选型 Embedding 模型和向量数据库
- 用 200 行 TypeScript 写一个流式 RAG 聊天机器人
- 解决引用高亮、增量缓存、TTFB 优化等 7 个生产级痛点
一、RAG 是什么?一张图说清楚
RAG 的核心思想只有一句话:在 LLM 生成答案之前,先用用户的问题去知识库里”查资料”,然后让 LLM 基于查到的资料回答。
┌──────────────────────────────────────────────────────────────────┐│ RAG 完整流程 │├──────────────────────────────────────────────────────────────────┤│ ││ ┌──────────┐ 1.分块 ┌──────────┐ 2.向量化 ┌──────────┐ ││ │ 文档库 │ ──────────► │ Text │ ──────────► │ Embedding │ ││ │(Markdown, │ │ Chunks │ │ 向量 │ ││ │ PDF, HTML)│ │(200~500字)│ │(768~3072维)│ ││ └──────────┘ └──────────┘ └─────┬────┘ ││ │ ││ ▼ ││ ┌──────────┐ ││ │ 向量数据库│ ││ │(Chroma/ │ ││ │ Pinecone)│ ││ └─────┬────┘ ││ │ ││ ┌──────────┐ 3.向量化 ┌──────────┐ 4.相似度搜索 │ ││ │ 用户问题 │ ──────────► │ Query │ ─────────────────┘ ││ │ │ │ Embedding│ ││ └──────────┘ └─────┬────┘ ││ │ ││ ▼ ││ ┌──────────────────┐ ││ │ Top-K 候选文档 │ ││ │ (通常 K=5~20) │ ││ └────────┬─────────┘ ││ │ ││ ▼ ││ ┌──────────────────┐ ││ │ 可选:重排序 │ (Cross-Encoder) ││ └────────┬─────────┘ ││ │ ││ ▼ ││ ┌──────────────────────────────────────┐ ││ │ Prompt: │ ││ │ 系统提示 + Top-K 文档 + 用户问题 │ ││ └────────────────┬─────────────────────┘ ││ │ ││ ▼ ││ ┌──────────────────┐ ││ │ LLM │ ││ │ (流式输出) │ ││ └────────┬─────────┘ ││ │ ││ ▼ ││ ┌──────────────────┐ ││ │ 答案 + 引用溯源 │ ││ └──────────────────┘ │└──────────────────────────────────────────────────────────────────┘为什么需要 RAG?
直接问 LLM 有三个致命问题:
| 问题 | 表现 | 根因 |
|---|---|---|
| 知识过时 | GPT 不知道 2025 年新出的产品 | 训练数据有截止日期 |
| 私有数据答不上 | 它不认识你公司的内部文档 | 训练数据里没有 |
| 幻觉 | 编造不存在的”论文作者”和”引用” | 纯粹的统计预测 |
RAG 用”先查后答”的方式把这三个问题全部解决:让 LLM 在回答时能”翻书”,而不是”背书”。
二、关键决策:Embedding 模型怎么选?
Embedding 是 RAG 的”地基”——Embedding 选错了,后面所有优化都是空中楼阁。它把一段文本变成一个高维向量(通常是 768~3072 维),语义相近的文本在向量空间里距离更近。
2.1 三大选型维度
| 维度 | 考虑点 | 推荐策略 |
|---|---|---|
| 语言 | 纯中文 / 纯英文 / 多语言? | 中文选 BGE、Erlangshen,英文选 BGE-en、OpenAI text-embedding-3 |
| 维度 | 768 vs 1024 vs 3072 | 维度越高召回越好但存储越大,需权衡 |
| 成本 | 闭源 API vs 本地开源 | 数据敏感选开源 BGE,预算有限选小模型 |
2.2 实战对比
我们用一组真实的中文问题来对比 3 个主流 Embedding 模型:
// 工具:调用不同 Embedding API(伪代码)async function embed(text: string, model: 'bge-large' | 'text-embedding-3-small' | 'm3e-base') { switch (model) { case 'bge-large': return await fetch('http://localhost:8000/embed', { method: 'POST', body: JSON.stringify({ text }) }).then(r => r.json());
case 'text-embedding-3-small': return await openai.embeddings.create({ model: 'text-embedding-3-small', input: text });
case 'm3e-base': return await fetch('http://localhost:8001/embed', { method: 'POST', body: JSON.stringify({ text }) }).then(r => r.json()); }}测试数据集(中文法律问答):
问题 Q: "员工试用期被辞退,公司需要赔偿吗?"文档 A: "根据《劳动合同法》第三十九条,试用期内员工不符合录用条件的,用人单位可以解除劳动合同,无需支付经济补偿。"文档 B: "Python 是一门解释型、面向对象、动态数据类型的高级程序设计语言。"期望:A 和 Q 的相似度 > B 和 Q 的相似度。实测结果:
| Embedding 模型 | 维度 | Q-A 相似度 | Q-B 相似度 | 召回正确? |
|---|---|---|---|---|
| BGE-large-zh-v1.5 | 1024 | 0.872 | 0.213 | ✅ |
| M3E-base | 768 | 0.831 | 0.298 | ✅ |
| text-embedding-3-small (多语言) | 1536 | 0.789 | 0.412 | ⚠️ 接近临界 |
| text-embedding-3-small (纯中文调优前) | 1536 | 0.654 | 0.587 | ❌ 反超 |
结论:对于中文场景,BGE-large-zh 是当前性价比最高的开源方案。如果你预算允许且不想自建,OpenAI 的 text-embedding-3-large 多语言版本在 3072 维下表现也很强。
2.3 进阶:混合检索(Hybrid Search)
纯向量检索有一个致命缺陷:它对”精确匹配”不敏感。比如用户问”iPhone 15 Pro Max 256GB 多少钱”,如果知识库里都是 SKU 编码,Embedding 模型可能完全匹配不上。
解决方案:向量检索 + 关键词检索(BM25)混合
// 混合检索的简易实现async function hybridSearch(query: string, k = 5) { // 1. 向量召回 Top 2K const vectorResults = await vectorDB.search(query, k * 4);
// 2. BM25 关键词召回 Top 2K const bm25Results = await bm25Index.search(query, k * 4);
// 3. 倒数排名融合 (Reciprocal Rank Fusion) const fused = reciprocalRankFusion( vectorResults, bm25Results, weights: [0.7, 0.3] // 向量权重更高 );
return fused.slice(0, k);}
function reciprocalRankFusion(...lists) { const scores = new Map<string, number>(); for (const [weight, list] of lists) { list.forEach((item, rank) => { const rr = weight / (60 + rank + 1); // k 常数 = 60 scores.set(item.id, (scores.get(item.id) || 0) + rr); }); } return [...scores.entries()] .sort((a, b) => b[1] - a[1]) .map(([id]) => id);}生产建议:RAG 系统中 90% 的”答错”问题都能通过混合检索解决。
三、向量数据库选型:百万级文档怎么存?
向量数据库是 RAG 的”仓库”。选型时需要考虑五个维度:
| 维度 | 说明 |
|---|---|
| 数据规模 | 1 万 / 10 万 / 100 万 / 1000 万 |
| 部署方式 | 云托管 vs 自部署 |
| 元数据过滤 | 是否需要按”部门”、“时间”、“标签”过滤 |
| 混合检索 | 是否原生支持 BM25 + 向量 |
| 运维成本 | 是否需要专门的 DBA |
3.1 主流方案对比
| 数据库 | 数据规模 | 部署方式 | 混合检索 | 适用场景 |
|---|---|---|---|---|
| Chroma | < 100 万 | 本地/嵌入式 | ❌ | Demo、个人项目、原型验证 |
| pgvector | < 500 万 | PostgreSQL 扩展 | ✅ SQL | 已有 PG 栈的中小团队 |
| Pinecone | 无限 | 托管 SaaS | ✅ | 不想运维的初创公司 |
| Qdrant | 无限 | 自部署/云 | ✅ | 性能要求高、欧洲合规 |
| Milvus | 10 亿+ | 自部署/云 | ✅ | 超大规模、生产级 |
| Weaviate | 100 万+ | 自部署/云 | ✅ GraphQL | 多模态场景 |
3.2 推荐策略
- Demo/学习:Chroma,pip install 直接用
- 小团队/已有 PG:pgvector,加一个 extension 就行
- 中等规模生产:Qdrant,性能和功能平衡最好
- 大规模生产:Pinecone(花钱买省心)或 Milvus(自部署省钱)
下面以 Qdrant 为例演示完整集成(它是开源、性能强、支持混合检索):
import { QdrantClient } from '@qdrant/js-client-rest';
export const qdrant = new QdrantClient({ url: process.env.QDRANT_URL, apiKey: process.env.QDRANT_API_KEY,});
// 1. 创建 Collection(如果不存在)export async function ensureCollection(name: string, dim = 1024) { const exists = await qdrant.collectionExists(name); if (!exists) { await qdrant.createCollection(name, { vectors: { size: dim, distance: 'Cosine' // 余弦相似度 }, // 开启 payload 索引以加速元数据过滤 optimizers_config: { default_segment_number: 2 } }); }}
// 2. 插入文档export async function upsertChunks(collection: string, chunks: Chunk[]) { const points = chunks.map((chunk, i) => ({ id: hash(chunk.content), // 用内容哈希做 ID,自动去重 vector: chunk.embedding, payload: { content: chunk.content, source: chunk.source, docId: chunk.docId, chunkIndex: chunk.index, createdAt: Date.now(), } }));
await qdrant.upsert(collection, { points, wait: true, // 等待落盘 });}
// 3. 向量检索 + 元数据过滤export async function search(collection: string, queryVec: number[], opts: { topK?: number; filter?: any; // Qdrant 强大的过滤语法 scoreThreshold?: number;} = {}) { const { topK = 5, filter, scoreThreshold = 0.5 } = opts;
return await qdrant.search(collection, { vector: queryVec, limit: topK, filter, score_threshold: scoreThreshold, with_payload: true, });}关键细节:
- 用内容哈希做 ID:相同内容不会重复插入,节省存储
- 设置 scoreThreshold:低于 0.5 的相似度直接丢弃,避免召回噪音
- Payload 索引:Qdrant 默认会索引所有 payload 字段,过滤性能极好
四、文档分块:被忽视的”召回率天花板”
RAG 系统的召回率上限 90% 取决于文档怎么分块。分块太大,召回的文档里噪音多;分块太小,语义被切断。
4.1 三大分块策略对比
| 策略 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定长度 | 每 500 字一切 | 简单、可控 | 切断句子/段落 | 文本结构清晰的文档 |
| 按句子 | N 个句子一组 | 保持语义完整 | 句子长度差异大 | 短文本、FAQ |
| 按结构 | Markdown 标题/代码块 | 完美保留结构 | 需要文档有结构 | Markdown、HTML |
| 滑动窗口 | 块之间有 overlap | 避免边界信息丢失 | 冗余存储 | 长文本、PDF |
| 语义分块 | Embedding 相似度突变处切 | 最优分块 | 计算慢、成本高 | 高质量知识库 |
4.2 实战:基于 Markdown 结构的智能分块
// chunker.ts - 工业级 Markdown 分块器import { unified } from 'unified';import remarkParse from 'remark-parse';import { visit } from 'unist-util-visit';import mdast from 'mdast';
interface Chunk { content: string; heading: string; level: number; index: number; source: string; tokenCount: number;}
const MAX_CHUNK_TOKENS = 500; // 推荐 200~500const MIN_CHUNK_TOKENS = 50; // 太小的 chunk 没意义
export function chunkMarkdown( markdown: string, source: string): Chunk[] { const tree = unified().use(remarkParse).parse(markdown); const chunks: Chunk[] = []; let currentChunk: string[] = []; let currentHeading = ''; let currentLevel = 0; let chunkIndex = 0;
function flushChunk() { const content = currentChunk.join('\n\n').trim(); const tokens = estimateTokens(content); if (tokens >= MIN_CHUNK_TOKENS) { chunks.push({ content, heading: currentHeading, level: currentLevel, index: chunkIndex++, source, tokenCount: tokens, }); } currentChunk = []; }
visit(tree, (node) => { // 遇到新的标题,刷新当前 chunk if (node.type === 'heading') { flushChunk(); currentHeading = mdastToString(node); currentLevel = node.depth; } else { const text = mdastToString(node); if (text) { currentChunk.push(text);
// 超过 max tokens 就切分 if (estimateTokens(currentChunk.join('\n\n')) > MAX_CHUNK_TOKENS) { flushChunk(); } } } });
flushChunk(); return chunks;}
// 简单的 token 估算(实际项目用 gpt-tokenizer)function estimateTokens(text: string): number { return Math.ceil(text.length / 1.5); // 中文 1.5 字/token}
function mdastToString(node: any): string { if (node.value) return node.value; if (node.children) return node.children.map(mdastToString).join(''); return '';}4.3 进阶:父子块(Parent-Child Chunking)
一个生产级的小技巧:检索用小块,喂给 LLM 用大块。
// 思路:每个文档切成三种粒度// - child chunk (200 tokens): 用于精确 Embedding 和检索// - parent chunk (800 tokens): 检索命中后返回给 LLM// 这样既保证召回精度,又保证上下文完整
interface HierarchicalChunk { childId: string; childContent: string; parentId: string; parentContent: string; // 可能包含多个 child}实际工程实现时,可以这样组织:
// 1. 父块:1000 tokens,按段落切// 2. 子块:200 tokens,父块再细分// 3. Embedding 只算子块// 4. 检索返回子块,但 payload 里存父块 ID// 5. LLM 拿到子块后,根据 ID 取出整个父块这个技巧可以让召回率再提升 10-15%。
五、流式问答:TTFT 优化的 7 个细节
OK,到这里 RAG 的”检索”部分讲完了。现在是更重要的”生成”部分——流式问答 UI。
为什么必须流式?因为 LLM 生成一个完整的 500 token 答案需要 5-10 秒,用户绝对等不了。流式输出可以让 TTFT(Time To First Token) 降到 200-500ms,用户体验天差地别。
5.1 基础:SSE 流式协议
// 客户端:EventSource 或 fetch + ReadableStreamasync function* streamChat(prompt: string, history: Message[]) { const response = await fetch('/api/rag/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', // 关键 }, body: JSON.stringify({ prompt, history }), });
if (!response.body) throw new Error('No response body');
const reader = response.body.getReader(); const decoder = new TextDecoder();
while (true) { const { done, value } = await reader.read(); if (done) break;
const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(l => l.startsWith('data:'));
for (const line of lines) { const data = line.slice(5).trim(); if (data === '[DONE]') return; try { yield JSON.parse(data); } catch {} } }}5.2 服务端:Node.js SSE 实现
// api/rag/chat.ts (Next.js API Route)import { OpenAIStream, StreamingTextResponse } from 'ai';import { retrieveContext } from '@/lib/rag';import { checkRateLimit } from '@/lib/rate-limit';
export const runtime = 'edge'; // Edge Runtime,低延迟
export async function POST(req: Request) { const { prompt, history = [] } = await req.json();
// 1. 限流(防止滥用和超支) const ip = req.headers.get('x-forwarded-for') || 'unknown'; if (!await checkRateLimit(ip)) { return new Response('Too Many Requests', { status: 429 }); }
// 2. 检索上下文(通常 100-300ms) const t0 = performance.now(); const context = await retrieveContext(prompt, { topK: 5 }); const retrievalMs = performance.now() - t0;
// 3. 构造 Prompt const systemPrompt = `你是一个专业的助手。请基于以下参考资料回答用户问题。如果参考资料中没有答案,请明确告知"根据现有资料无法回答"。回答时请在引用的事实后用 [1][2] 这样的标记标注来源编号。`;
const userPrompt = `参考资料:${context.map((c, i) => `[${i + 1}] ${c.content}\n来源: ${c.source}`).join('\n\n')}
用户问题:${prompt}
请用中文回答。`;
// 4. 调用 LLM(流式) const response = await openai.chat.completions.create({ model: 'gpt-4o', stream: true, temperature: 0.3, messages: [ { role: 'system', content: systemPrompt }, ...history, { role: 'user', content: userPrompt }, ], });
// 5. 包装成 SSE(Vercel AI SDK 帮你做了) const stream = OpenAIStream(response, { onFinal: async (completion) => { // 记录日志、做埋点 await logChat({ prompt, completion, retrievalMs, totalSources: context.length, sources: context.map(c => c.source), }); }, });
return new StreamingTextResponse(stream, { headers: { 'X-Retrieval-Ms': retrievalMs.toString(), // 自定义 header,前端可以读 }, });}5.3 进阶:引用高亮的实现
引用高亮是 RAG 系统信任度的核心。没有引用高亮的 RAG = 黑盒 AI = 没人敢信。
// 1. 检索时返回的 chunk 要带位置信息interface RetrievedChunk { id: string; content: string; source: string; sourceUrl: string; startOffset: number; // 在原文中的起始位置 endOffset: number; score: number;}
// 2. 让 LLM 输出带引用的答案const prompt = `...回答时,请在引用的事实后用 <<<1>>>、<<<2>>> 这样的标记标注来源编号。示例:员工试用期辞退需要看是否符合录用条件<<<1>>>。...`;
// 3. 客户端解析引用标记并渲染function renderAnswerWithCitations(text: string, sources: RetrievedChunk[]) { const parts = text.split(/(<<<\d+>>>)/g);
return parts.map((part, i) => { const match = part.match(/<<<(\d+)>>>/); if (match) { const sourceIdx = parseInt(match[1]) - 1; const source = sources[sourceIdx]; if (!source) return null; return ( <sup key={i} className="citation"> [{sourceIdx + 1}] <div className="citation-popup"> <div className="source-title">{source.source}</div> <div className="source-content">{source.content.slice(0, 200)}...</div> <a href={source.sourceUrl} target="_blank">查看原文 →</a> </div> </sup> ); } return <span key={i}>{part}</span>; });}5.4 性能对比:流式 vs 非流式
| 方案 | TTFT | 完整答案时间 | 体感 |
|---|---|---|---|
| 非流式 | 5000ms | 5000ms | ”卡死了” |
| 流式(裸) | 5000ms | 5000ms | 同上 |
| 流式 + 检索前置 | 200-500ms | 5000ms | ”秒回”,感觉很快 |
| 流式 + 检索前置 + 增量 UI | 200-500ms | 5000ms | ”打字机效果”,最佳体感 |
关键优化:检索和 LLM 调用并行。传统做法是先检索再调用 LLM,串行耗时 = 检索 300ms + LLM 5000ms。流式做法是检索完成的瞬间立即开始流式调用 LLM,用户感受不到检索延迟。
六、生产级 RAG 的 5 个反模式
最后讲 5 个我踩过的坑,希望你别再踩。
反模式 1:把所有文档一次性 Embedding
// ❌ 错误:一次处理所有文档const allDocs = await loadAllDocs(); // 10 万篇const chunks = chunkAll(allDocs); // 500 万 chunksconst vectors = await embedAll(chunks); // 烧光 API 额度await qdrant.upsert(vectors); // OOM
// ✅ 正确:分批 + 异步队列import { Queue } from 'bullmq';
const embeddingQueue = new Queue('embed-docs', { connection: { host: 'localhost', port: 6379 }});
for (const doc of docs) { await embeddingQueue.add('embed', { docId: doc.id }, { attempts: 3, backoff: { type: 'exponential', delay: 1000 }, });}反模式 2:把整个对话历史塞进 Prompt
// ❌ 错误const messages = [...allHistory, currentMessage]; // 上下文爆炸
// ✅ 正确:摘要 + 滑动窗口const recentMessages = history.slice(-6); // 最近 3 轮const summarizedOld = await summarize(history.slice(0, -6));
const messages = [ { role: 'system', content: `${systemPrompt}\n\n历史对话摘要:${summarizedOld}` }, ...recentMessages, currentMessage,];反模式 3:不做 Embedding 缓存
// ❌ 错误:每次都重新 Embeddingasync function getContext(prompt: string) { const vec = await embed(prompt); // 每次都花钱 return await qdrant.search(COLLECTION, vec);}
// ✅ 正确:语义哈希 + LRU 缓存import { LRUCache } from 'lru-cache';import crypto from 'crypto';
const cache = new LRUCache<string, number[]>({ max: 1000 });
function semanticHash(text: string): string { // 归一化 + 哈希,让"你好"和"你好!" 命中同一缓存 const normalized = text.toLowerCase().replace(/[^\w\u4e00-\u9fa5]/g, ''); return crypto.createHash('md5').update(normalized).digest('hex');}
async function getContext(prompt: string) { const hash = semanticHash(prompt); let vec = cache.get(hash); if (!vec) { vec = await embed(prompt); cache.set(hash, vec); } return await qdrant.search(COLLECTION, vec);}反模式 4:忽略 RAG 评估
RAG 系统上线后必须有评估体系,否则你不知道它变好了还是变差了。
// 用 LLM-as-a-Judge 做自动评估async function evaluateRAG(question: string, answer: string, contexts: RetrievedChunk[]) { const evalPrompt = `你是一个严格的 RAG 评估员。请评估以下回答的质量:
问题:${question}回答:${answer}参考资料:${contexts.map((c, i) => `[${i+1}] ${c.content}`).join('\n')}
请从三个维度打分(0-10):1. 准确性:回答是否符合参考资料2. 完整性:是否充分回答了问题3. 引用质量:是否正确标注了引用
返回 JSON:{ "accuracy": 0, "completeness": 0, "citation": 0, "reason": "..." }`;
const result = await openai.chat.completions.create({ model: 'gpt-4o', response_format: { type: 'json_object' }, messages: [{ role: 'user', content: evalPrompt }], });
return JSON.parse(result.choices[0].message.content);}反模式 5:没有用户反馈闭环
// ✅ 必要的:点赞/点踩按钮 + 反馈原因async function submitFeedback(chatId: string, rating: 'up' | 'down', reason?: string) { await db.feedback.insert({ chatId, rating, reason, timestamp: Date.now(), // 同时记录:用户问了什么、检索到了什么、LLM 回了什么 // 这些数据是后续优化 RAG 的金矿 });
// 点踩时自动触发人工审核或专家回答 if (rating === 'down') { await notifyHumanReview(chatId); }}七、延伸阅读与开源项目
如果你想进一步深入,这些资源值得一看:
论文
- Lewis et al., 2020 - Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks — RAG 原论文
- Gao et al., 2024 - Retrieval-Augmented Generation for Large Language Models: A Survey — 综述
- BGE: BAAI General Embedding — 中文 Embedding SOTA
开源项目
- LangChain — LLM 应用编排框架
- LlamaIndex — 专注 RAG 的数据框架
- Dify — 开源 LLM 应用平台(国内团队)
- Qdrant — Rust 写的向量数据库
- Vercel AI SDK — 流式 AI UI 最佳实践
工具
- BGE-M3 — 多语言、多粒度、多功能 Embedding
- Cohere Rerank — 商业级重排序服务
- Arize Phoenix — RAG 评估与可观测性
相关博客(本博客已发布)
- WebGPU 实战:在浏览器里跑 AI 模型 — 客户端 AI 推理
- AI Agent 工具调用模式:ReAct、Plan-and-Execute 与反思循环 — Agent 工程化
- 前端 Observer API 全家桶 — 用于实现流式 UI 的底层 API
- Web Streams API 流式架构 — 流式处理基础
八、写在最后
RAG 不是一个算法,它是一整套工程。
从 Embedding 选型、文档分块、向量检索、重排序,到 Prompt 工程、流式输出、引用高亮、用户反馈——每一步都有大量细节值得打磨。
但也别被这些细节吓到。80% 的生产 RAG 系统,用的其实就是”Embedding + 向量库 + Top-K + Prompt”这四件套。先把 MVP 跑起来,再用真实用户数据迭代优化——这是最务实的路径。
下一篇,我会写 RAG 进阶:Self-RAG、CAG 与 GraphRAG——三种让 RAG 准确率再上一个台阶的进阶架构。我们下期见。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!