RAG-全链路问答系统:从零到容器化部署的终极指南

目录

前言

[第一步:把项目准备成 Git 仓库(本地目录与关键文件)](#第一步:把项目准备成 Git 仓库(本地目录与关键文件))

1️⃣添加必要文件

2️⃣运行容器

3️⃣验证接口(关键验证)

[第二步:向 RAG 系统"喂知识"(/ingest)](#第二步:向 RAG 系统“喂知识”(/ingest))

[1️⃣ 执行 ingest 请求](#1️⃣ 执行 ingest 请求)

2️⃣正确返回结果

[第三步:发起问题(/query,不用 OpenAI)](#第三步:发起问题(/query,不用 OpenAI))

[1️⃣ 执行 query 请求](#1️⃣ 执行 query 请求)

[2️⃣ 正确返回示例](#2️⃣ 正确返回示例)

[第四步(可选):开启真正的 LLM(OpenAI)](#第四步(可选):开启真正的 LLM(OpenAI))

[1️⃣ 启动容器时注入 API Key](#1️⃣ 启动容器时注入 API Key)

[2️⃣ 使用 LLM 查询](#2️⃣ 使用 LLM 查询)

[3️⃣ 你会看到更自然的回答(由大模型生成)](#3️⃣ 你会看到更自然的回答(由大模型生成))

第五步:向量持久化

[1️⃣优化 app/retriever.py(支持持久化)](#1️⃣优化 app/retriever.py(支持持久化))

[2️⃣正确的 Docker run(持久化)](#2️⃣正确的 Docker run(持久化))

3️⃣此时我们在重新测接口

[第六步:把项目在 CNB 上自动化](#第六步:把项目在 CNB 上自动化)

总结


前言

本指南帮你把一个本地 RAG(Retrieval-Augmented Generation)示例工程整理成可直接构建与运行的 Git 项目,并提供在 cnb.cool(CNB)上自动化构建的 .cnb.yml 示例。目标是:最小改造、最快验证、可选接入 OpenAI

CNB地址:https://cnb.cool

RAG-QA 项目: https://cnb.cool/istioagentnj/cnb

2025 第二期 Docker 训练营地址: https://opencamp.cn/Docker/camp/202502/register?code=dxJSEQUgrBdWs

2025 第二期 Docker 训练营是由 OpenCamp 社区 联合 腾讯云 CNB(Cloud Native Build) 共同举办的云原生技术实战项目。本期训练营采用"直播教学 + 闯关实战 + 项目实习"的模式,依托腾讯云 CNB 平台,为学员提供开箱即用的云端开发环境。

第一步:把项目准备成 Git 仓库(本地目录与关键文件)

这里直接创建以下这些文件,然后将下面的代码放在相应对的文件中。

复制代码
rag-qa-cnb/
├── app/
│   ├── __init__.py
│   ├── main.py        ✅ 接口入口
│   ├── retriever.py   ✅ 向量索引 & 检索
│   ├── models.py      ✅ Pydantic 数据模型
│   └── utils.py       ✅ 工具函数(Embedding / LLM)
├── requirements.txt
├── Dockerfile

1️⃣添加必要文件

复制代码
fastapi==0.110.0
uvicorn[standard]==0.29.0

sentence-transformers==2.2.2
huggingface_hub<0.20.0

faiss-cpu==1.7.4
numpy<2.0

openai==0.28.1

Dockerfile (用于在 CNB 上基于 Dockerfile 构建)

复制代码
# 基础镜像
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# 先拷贝 requirements,利用缓存加速构建
COPY requirements.txt /app/requirements.txt

# 安装系统依赖与 Python 依赖
RUN apt-get update && apt-get install -y \
    build-essential git curl ca-certificates libopenblas-dev \
  && pip install --upgrade pip \
  && pip install --no-cache-dir -r /app/requirements.txt \
  && apt-get remove -y build-essential git && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/*

# 拷贝项目代码
COPY . /app

# 暴露端口
EXPOSE 8000

# 可用的启动命令(CNB 平台会使用它或你在 UI 指定 CMD)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

app/models.py (数据模型)

复制代码
from typing import List, Optional, Dict
from pydantic import BaseModel


class Document(BaseModel):
    id: str
    text: str
    meta: Optional[Dict] = {}


class IngestRequest(BaseModel):
    documents: List[Document]


class QueryRequest(BaseModel):
    question: str
    top_k: int = 3
    use_openai: bool = False


class QueryResponse(BaseModel):
    answer: str
    contexts: List[str]

app/utils.py (Embedding + LLM 工具)

复制代码
import os
from typing import List

from sentence_transformers import SentenceTransformer

try:
    import openai
except ImportError:
    openai = None


# 全局 embedding 模型(只加载一次)
_embedding_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")


def embed_texts(texts: List[str]) -> List[List[float]]:
    """
    将文本列表转为向量
    """
    embeddings = _embedding_model.encode(texts, convert_to_numpy=True)
    return embeddings.tolist()


def call_openai_llm(prompt: str) -> str:
    """
    调用 OpenAI 生成回答
    """
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key or openai is None:
        raise RuntimeError("OpenAI API 未配置")

    openai.api_key = api_key

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "你是一个专业的知识问答助手。"},
            {"role": "user", "content": prompt},
        ],
        temperature=0.2,
    )

    return response.choices[0].message["content"].strip()


def simple_fallback_answer(question: str, contexts: List[str]) -> str:
    """
    无 LLM 时的兜底回答(教学/调试用)
    """
    if not contexts:
        return "未找到相关知识。"

    joined = "\n".join(contexts)
    return f"根据已知资料,问题「{question}」的相关内容如下:\n{joined}"

app/retriever.py (FAISS 向量索引)

复制代码
from typing import List, Tuple
import faiss
import numpy as np

from app.utils import embed_texts


class FaissRetriever:
    def __init__(self, dim: int):
        self.dim = dim
        self.index = faiss.IndexFlatL2(dim)
        self.texts: List[str] = []

    def add_texts(self, texts: List[str]):
        """
        向索引中添加文本
        """
        vectors = embed_texts(texts)
        vectors_np = np.array(vectors).astype("float32")

        self.index.add(vectors_np)
        self.texts.extend(texts)

    def search(self, query: str, top_k: int = 3) -> List[str]:
        """
        相似度搜索
        """
        if self.index.ntotal == 0:
            return []

        query_vec = embed_texts([query])[0]
        query_np = np.array([query_vec]).astype("float32")

        distances, indices = self.index.search(query_np, top_k)

        results = []
        for idx in indices[0]:
            if 0 <= idx < len(self.texts):
                results.append(self.texts[idx])

        return results

app/main.py (FastAPI 主入口)

复制代码
from fastapi import FastAPI
from pydantic import BaseModel
from app.retriever import FaissRetriever
from app.utils import call_openai_llm
import traceback

app = FastAPI()
retriever = FaissRetriever()

class QueryRequest(BaseModel):
    question: str
    use_openai: bool = False

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/query")
def query(req: QueryRequest):
    contexts = retriever.search(req.question)
    prompt = (
        "请基于以下内容回答问题。\n\n"
        + "\n".join(contexts)
        + f"\n\n问题:{req.question}"
    )

    # 默认兜底答案
    answer = "(本次回答由本地 RAG 检索生成,未调用大模型)\n\n" + "\n".join(contexts)

    if req.use_openai:
        try:
            answer = call_openai_llm(prompt)
        except Exception as e:
            print("⚠️ OpenAI 调用失败,已回退:")
            traceback.print_exc()

    return {
        "question": req.question,
        "answer": answer,
        "contexts": contexts,
    }

接下来就是创建镜像运行容器

复制代码
docker build -t rag-qa .

如果成功,你会看到:

2️⃣运行容器

复制代码
docker run -it --rm -p 8000:8000 rag-qa

3️⃣验证接口(关键验证)

开新终端:

复制代码
curl http://localhost:8000/health

正确返回:

第二步:向 RAG 系统"喂知识"(/ingest)

RAG = 先有知识,后能回答

1️⃣ 执行 ingest 请求

复制代码
curl -X POST http://localhost:8000/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "documents": [
      {
        "id": "doc1",
        "text": "RAG 是一种通过向量检索增强大模型生成能力的技术。",
        "meta": {"source": "tutorial"}
      },
      {
        "id": "doc2",
        "text": "RAG 系统通常包含文档切分、向量化、相似度检索和生成四个步骤。",
        "meta": {"source": "tutorial"}
      }
    ]
  }'

2️⃣正确返回结果

这一步说明:

  • Sentence-Transformers 正常工作

  • Embedding 生成成功

  • FAISS Index 已写入内存

注意:现在的向量是" 内存 态"的, 重启 容器会丢,这是正常的

第三步:发起问题(/query,不用 OpenAI)

我们先 不用 OpenAI,确保纯 RAG 检索链路是 OK 的。

1️⃣ 执行 query 请求

复制代码
curl -X POST http://localhost:8000/query \
  -H "Content-Type: application/json" \
  -d '{
    "question": "RAG 系统一般包含哪些步骤?",
    "top_k": 2,
    "use_openai": false
  }'

2️⃣ 正确返回示例

如果你看到 contexts 里是你刚刚 ingest 的内容

那么恭喜你:RAG 核心链路 100% 打通

第四步(可选):开启真正的 LLM(OpenAI)

这个部分放在评论区大家可以讨论讨论

1️⃣ 启动容器时注入 API Key

复制代码
docker run -it --rm \
  -e OPENAI_API_KEY=sk-xxxx \
  -p 8000:8000 \
  rag-qa

2️⃣ 使用 LLM 查询

复制代码
curl -X POST http://localhost:8000/query \
  -H "Content-Type: application/json" \
  -d '{
    "question": "什么是 RAG?",
    "use_openai": true
  }'

3️⃣ 你会看到更自然的回答(由大模型生成)

第五步:向量持久化

1️⃣优化 app/retriever.py(支持持久化)

复制代码
import numpy as np
import faiss
import os
import pickle

class FaissRetriever:
    def __init__(self):
        self.base_dir = "/app/data"
        self.faiss_dir = os.path.join(self.base_dir, "faiss")
        self.index_path = os.path.join(self.faiss_dir, "index.faiss")
        self.texts_path = os.path.join(self.faiss_dir, "texts.pkl")

        os.makedirs(self.faiss_dir, exist_ok=True)

        self._load_or_build()

    def _load_or_build(self):
        if os.path.exists(self.index_path) and os.path.exists(self.texts_path):
            print("✅ 加载已有 FAISS 索引")
            self.index = faiss.read_index(self.index_path)
            with open(self.texts_path, "rb") as f:
                self.texts = pickle.load(f)
        else:
            print("⚠️ 未发现索引,开始首次构建")
            self._build()

    def _build(self):
        from sentence_transformers import SentenceTransformer

        self.embed_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

        # 示例文档(你后面可以换成真实文档)
        self.texts = [
            "RAG 是 Retrieval Augmented Generation",
            "RAG 结合了向量检索与大语言模型",
            "Docker 用于容器化部署 AI 服务"
        ]

        # 🔴 关键修复点在这里
        embeddings = self.embed_model.encode(self.texts)

        # 👉 显式转 numpy + float32
        embeddings = np.array(embeddings).astype("float32")

        dim = embeddings.shape[1]

        self.index = faiss.IndexFlatL2(dim)
        self.index.add(embeddings)

        # 👉 一定要落盘
        faiss.write_index(self.index, self.index_path)
        with open(self.texts_path, "wb") as f:
            pickle.dump(self.texts, f)

        print("✅ FAISS 索引构建完成并已保存")

    def search(self, query: str, top_k: int = 3):
        query_vec = self.embed_model.encode([query])
        query_vec = np.array(query_vec).astype("float32")

        distances, indices = self.index.search(query_vec, top_k)

        return [self.texts[i] for i in indices[0]]

2️⃣正确的 Docker run(持久化)

复制代码
mkdir -p data/faiss

docker run -it --rm \
  -p 8000:8000 \
  -v $(pwd)/data:/app/data \
  rag-qa

如果你不挂载 volume,容器一删索引就没了

这是你是否做对的"唯一标准"

并且宿主机必须有下面两个文件:

3️⃣此时我们在重新测接口

复制代码
curl -X POST http://localhost:8000/query \
  -H "Content-Type: application/json" \
  -d '{
    "question": "什么是 RAG?",
    "use_openai": false
  }'

第六步:把项目在 CNB 上自动化

随后我们在创建一个自动化的.cnb.yml文件,让他帮我们自动构建环境。

这个文件是什么?有什么用?解答:CNB 的"自动化剧本"

复制代码
# .cnb.yml
$:
  vscode:
    - docker:
        # 使用 CNB 官方默认开发镜像(已包含 code-server + docker cli)
        image: cnbcool/default-dev-env:latest

      # 声明需要的服务
      services:
        - vscode
        - docker

      # 开发环境启动后自动执行的步骤
      stages:
        # 1️⃣ 查看当前工作目录(确认代码已挂载)
        - name: show-workspace
          script: |
            echo "==== Workspace Files ===="
            pwd
            ls -al

        # 2️⃣ 构建 RAG Docker 镜像
        - name: build-rag-image
          script: |
            echo "==== Build RAG Docker Image ===="
            docker build -t rag-qa .

        # 3️⃣ 启动容器(后台运行)
        - name: run-rag-container
          script: |
            echo "==== Run RAG Container ===="
            docker rm -f rag-qa-container || true
            docker run -d \
              --name rag-qa-container \
              -p 8000:8000 \
              rag-qa

        # 4️⃣ 等待服务启动并做健康检查
        - name: health-check
          script: |
            echo "==== Health Check ===="
            sleep 5
            curl http://localhost:8000/health

总结

本文详细展示了如何从零开始搭建一个基于 FastAPI + FAISS + Sentence-Transformers 的 RAG 问答系统,涵盖:

  1. 本地准备与先决条件:确保 Git、Docker 和可选 OpenAI API 可用。

  2. 项目结构与关键文件:保持清晰目录与模块划分,便于扩展与维护。

  3. 本地测试流程:构建 Docker 镜像、运行容器、健康检查、向系统喂知识、发起问答。

  4. 可选接入 OpenAI:在 RAG 检索基础上增强自然语言回答能力。

  5. 向量持久化方案:FAISS 索引落盘与宿主机挂载示例,保证容器重启不丢失数据。

  6. 自动化构建示例 :CNB .cnb.yml 提供自动化构建、启动、健康检查流程。

通过本文,你可以快速构建一个可用、可扩展、可自动化部署的 RAG 系统,且保留灵活的 LLM 接入选项,适合研发、教学和轻量生产环境。

相关推荐
bing.shao2 小时前
Kubernetes 容错处理实战案例集锦
云原生·容器·kubernetes
.柒宇.2 小时前
shell脚本之Docker安装
运维·docker·容器
..Move...3 小时前
云原生运维企业级实战项目:CentOS Stream 8 下 Nginx 高可用集群部署
运维·云原生·centos
LingRannn3 小时前
Ubuntu 24.04 安装 Docker Engine
linux·ubuntu·docker
凯子坚持 c3 小时前
Docker存储卷深度解析:机制、管理与数据持久化实战
运维·docker·容器
蟑螂恶霸3 小时前
使用docker安装windows 11
运维·docker·容器
MicoZone3 小时前
docker
docker
古城小栈3 小时前
2025 Go 语言生态:从云原生到边缘计算
云原生·golang·边缘计算
jumu2023 小时前
Qt/C++ 实现文件双向传输:从客户端到服务端,再从服务端到客户端
云原生