从 0 到 1:用 Python 做一个会“先查再答”的中文 RAG 问答系统(附完整代码)

提示:RAG、检索增强生成、知识库问答、FAISS、Milvus、bge 嵌入、重排、RAGAS、FastAPI、OpenAI 兼容


前言

提示:

  • 用 Python 做一个会先查资料再回答的中文问答系统,几步就能跑起来。
  • 我把怎么找资料、怎么排序、怎么生成答案、怎么评测好不好都写成了能直接用的代码。
  • 还给了上线时常见的坑和优化方法,帮你解决查不准、瞎编、速度慢这些问题。

你将收获到

  • 一个能直接部署的中文知识库问答系统(把你自己的文档放进去就能问)。
  • 选型不纠结:用哪个嵌入模型、哪个向量库、要不要重排、怎么接大模型。
  • 一键评测:用 RAGAS 量化回答质量,知道哪里需要改。
  • 生产经验:稳定性、速度、成本怎么平衡,常见问题怎么排查。

适用人群

  • 想把公司文档/FAQ 变成能问能答的小助手。

  • 已经有 LLM Demo,想真正在项目里落地的开发者。

提示:以下是本篇文章正文内容,下面案例可供参考

一、为什么选RAG,而不选 微调

  • 说白了微调就类似于“强行让模型背书”。贵、慢都不计较了,但是内容一变就得再背。
  • RAG就显得比较优雅,就类似于“先翻你给的资料再进行回答”。你改文档答案就跟着变,还能标注出处;
    好处就不言而喻了,便宜,更新快,不瞎编,能追溯来源,便于审核。

二、这系统长什么样呢?

  • 你把文档放到 data/ 里(比如 .md/.txt)。
  • 我们把文档切成小段,转成向量,存到 FAISS(向量索引)。
  • 用户提问时:先“召回”相关段落(向量检索 + BM25),再“重排”(更准),最后让大模型“只根据这些段落”组织一个靠谱的答案,并在结尾标注来源编号。
  • 想象一下它的步骤:
  • 嵌入模型:BAAI/bge-small-zh-v1.5(中文友好,够轻)
  • 向量库:本地用 FAISS,生产可换 Milvus/PGVector
  • 重排:BAAI/bge-reranker-base,把前几条按相关度再洗一遍
  • 生成:用 OpenAI 兼容 API(OpenAI/DeepSeek/智谱等都行)
  • 评测:RAGAS,量化“靠谱度、相关性、上下文命中”

三、快速上手 Demo(完整代码直接照做)

1、安装依赖

pip install fastapi uvicorn sentence-transformers faiss-cpu rank-bm25 openai tiktoken pydantic ragas datasets

2、创建目录

rag-demo/
  ├─ data/               # 你的文档
  ├─ index/              # 生成的索引
  ├─ ingest.py           # 文档切分+嵌入+建索引
  ├─ rag_service.py      # 服务:检索->重排->生成
  └─ evaluate_rag.py     # 评测(RAGAS)

3、实现 ingest.py

import os
import glob
import pickle
from typing import List, Dict
from sentence_transformers import SentenceTransformer
import faiss

CHUNK_SIZE = 500
CHUNK_OVERLAP = 100
EMBEDDING_MODEL = "BAAI/bge-small-zh-v1.5"
INDEX_DIR = "index"
DATA_DIR = "data"

def read_files(data_dir: str) -> List[Dict]:
    files = glob.glob(os.path.join(data_dir, "**", "*.*"), recursive=True)
    docs = []
    for f in files:
        if f.lower().endswith((".md", ".txt")):
            with open(f, "r", encoding="utf-8", errors="ignore") as rf:
                docs.append({
   
   "path": f, "text": rf.read()})
    return docs

def split_text(text: str, chunk_size: int, overlap: int) -> List[str]:
    chunks = []
    start = 0
    while start < len(text):
        end = min(len(text), start + chunk_size)
        chunks.append(text[start:end])
        start = end - overlap
        if start < 0:
            start = 0
        if start >= len(text):
            break
    return chunks

def build_corpus(docs: List[Dict]) -> List[Dict]:
    corpus = []
    for d in docs:
        for c in split_text(d["text"], CHUNK_SIZE, CHUNK_OVERLAP):
            corpus.append({
   
   "text": c
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

成为全栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值