Skip to content

从 0 到 1:检索问答 DEMO(含代码)

用最小组件把 RAG 串起来:切分 → 嵌入 → 索引 → 召回 → 重写。 注:本文为“1 月份重点篇”,给出可复制的 DEMO 代码。

🎯 文章目标

  • 用最小依赖构建一个可运行的 RAG Demo
  • 展示段落切分、嵌入、索引与召回的关键点
  • 提供上线前的最小评估清单

📚 背景/前置

  • 嵌入模型:选质量稳定、成本可控者(如 bge/multilingual-e5/nomic-embed)
  • 向量库:Chroma/Qdrant/Weaviate,本文用 Chroma 演示
  • 检索策略:HNSW/Flat;Top-K + 重写/重排

🔧 核心内容

1) 数据预处理与切分

  • 语种/编码清洗、去重、去格式噪声
  • 段落切分:按句/段 + 滑动窗口,保留上下文

2) 嵌入与索引

python
# requirements: chromadb sentence-transformers openai
import os, glob, chromadb
from sentence_transformers import SentenceTransformer
from openai import OpenAI

EMBED = os.getenv('EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
CHAT  = os.getenv('CHAT_MODEL', 'qwen2.5-7b-instruct')
cli = OpenAI(base_url=os.getenv('OPENAI_API_BASE'), api_key=os.getenv('OPENAI_API_KEY'))

texts = []
for f in glob.glob('docs/*.md'):
    with open(f,'r') as fp:
        texts += [t.strip() for t in fp.read().split('\n\n') if len(t.strip())>50]

m = SentenceTransformer(EMBED)
vecs = m.encode(texts).tolist()
chroma = chromadb.Client(); col = chroma.get_or_create_collection('kb')
col.add(ids=[str(i) for i in range(len(texts))], documents=texts, embeddings=vecs)

3) 检索与回答

python
q = '如何给 RAG 做段落切分?'
hits = col.query(query_texts=[q], n_results=5)
ctx = '\n'.join(hits['documents'][0])

msg = [
  {"role":"system","content":"仅根据上下文回答,引用出处,若无依据请说不知道"},
  {"role":"user","content":f"上下文:\n{ctx}\n\n问题:{q}"}
]
ans = cli.chat.completions.create(model=CHAT, messages=msg)
print(ans.choices[0].message.content)

4) 最小评估清单

  • 正确性:抽查 20 个问题,统计“引用+事实一致”比例
  • 可执行性:输出格式稳定(JSON/Markdown),可直接被下游解析
  • 延迟/成本:记录 tokens 与耗时,观察最慢 5% 请求

💡 实战示例:Node.js 端一键检索

javascript
import OpenAI from 'openai'
const client = new OpenAI({ baseURL: process.env.OPENAI_API_BASE, apiKey: process.env.OPENAI_API_KEY })

const q = 'RAG 的重排有什么常见方式?'
const ctx = 'BM25 + 向量融合;候选重排;窗口策略;'
const messages = [
  { role: 'system', content: '只基于上下文回答,必须给出要点式答案' },
  { role: 'user', content: `上下文:\n\${ctx}\n\n问题:\${q}` }
]
console.log((await client.chat.completions.create({ model: process.env.CHAT_MODEL, messages })).choices[0].message.content)

📊 对比/取舍(速查)

  • 召回优先:先把召回做稳,再谈生成“写得好”
  • 模型大小:RAG 常优于盲目加大模型
  • 指标闭环:以“引用 + 正确率 + 可执行性 + 延迟/成本”组合视角看结果

🧪 踩坑与经验

  • 文档更新:增量索引/归档策略,避免“旧知识污染”
  • 片段切分:过短会丢上下文,过长会拖慢与引入噪声
  • 嵌入漂移:版本更换要回归评测

📎 参考与延伸

  • 切分策略与窗口、重排与融合检索
  • 向量库与索引结构(HNSW/IVF/Flat)
  • RAG 测评方法与在线观测

💭 总结

  • 先串通“切分→嵌入→索引→召回→生成”,小步快跑
  • 以“可执行性+引用一致性+延迟/成本”衡量是否“能上线”