上一篇 下一篇 分享链接 返回 返回顶部

从零搭一个能跑的 AI 搜索:FastAPI + ChromaDB + Docker 完整源码部署教程

发布人:慈云数据-客服中心 发布时间:22小时前 阅读量:5

AI搜索 Docker部署教程|附源码

在搜索体验越来越“智能化”的今天,传统关键词搜索已经难以满足复杂语义查询的需求。比如用户搜索“适合新手部署的向量数据库”,传统搜索可能只能匹配标题或正文中的关键词,而 AI 搜索可以理解用户意图,返回更接近语义的结果。

本文将带你从零搭建一个 AI搜索系统,并使用 Docker 进行部署。系统支持:

  • 文档上传与解析
  • 文本切分
  • 向量化存储
  • 语义搜索
  • 基于大模型的答案生成
  • Docker 一键部署

本文适合希望学习 RAG 检索增强生成AI搜索系统开发Docker部署AI应用 的开发者阅读。


一、什么是 AI 搜索?

AI 搜索通常指结合 向量检索大语言模型 的搜索方式。

传统搜索主要依赖关键词匹配,例如:

用户搜索:Docker 部署 AI 搜索

系统会查找包含“Docker”“部署”“AI搜索”等关键词的内容。

而 AI 搜索更关注语义理解,例如:

用户搜索:怎么把智能问答系统发布到服务器?

即使文档中没有完全相同的关键词,系统也可以理解“智能问答系统”“发布到服务器”与“AI搜索 Docker部署”之间的语义关联,从而返回相关内容。

一个典型的 AI 搜索流程如下:

  1. 用户上传文档;
  2. 系统将文档切分成小段;
  3. 使用 Embedding 模型将文本转换为向量;
  4. 将向量存储到向量数据库;
  5. 用户输入问题;
  6. 系统将问题转换成向量;
  7. 从向量数据库中检索相关文档片段;
  8. 将相关片段和问题一起交给大模型;
  9. 大模型生成最终答案。

这类架构通常被称为 RAG,即 Retrieval-Augmented Generation,中文一般翻译为“检索增强生成”。


二、项目技术选型

本文示例项目采用以下技术栈:

模块 技术
后端框架 FastAPI
向量数据库 ChromaDB
Embedding 模型 sentence-transformers
大模型接口 OpenAI API / 本地 Ollama 可替换
部署方式 Docker + Docker Compose
接口文档 FastAPI Swagger
开发语言 Python 3.11

为什么选择这些技术?

1. FastAPI

FastAPI 性能优秀,开发体验好,自动生成接口文档,非常适合快速构建 AI 应用后端。

2. ChromaDB

ChromaDB 是一个轻量级向量数据库,适合个人项目、中小型知识库和原型系统。它可以本地持久化,不需要复杂的集群配置。

3. sentence-transformers

它可以在本地生成文本向量,不一定依赖外部 API。对于测试、内网部署和私有化项目来说非常方便。

4. Docker

Docker 可以解决不同服务器环境不一致的问题。只要服务器安装了 Docker,就可以快速运行整个 AI 搜索服务。


三、项目目录结构

完整项目目录如下:

ai-search-docker/
├── app/
│   ├── main.py
│   ├── config.py
│   ├── embedding.py
│   ├── vector_store.py
│   ├── rag.py
│   └── utils.py
├── data/
│   └── docs/
├── chroma_data/
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .env
└── README.md

目录说明:

  • app/main.py:FastAPI 主入口;
  • app/config.py:配置文件;
  • app/embedding.py:文本向量化模块;
  • app/vector_store.py:向量数据库操作;
  • app/rag.py:AI 搜索和问答逻辑;
  • app/utils.py:文本切分工具;
  • data/docs/:存放上传文档;
  • chroma_data/:ChromaDB 持久化目录;
  • requirements.txt:Python 依赖;
  • Dockerfile:镜像构建文件;
  • docker-compose.yml:服务编排文件。

四、核心源码

下面给出一个可运行的简化版本源码。


1. 配置文件:app/config.py

import os
from dotenv import load_dotenv

load_dotenv()

class Settings:
    APP_NAME = "AI Search Docker Demo"
    CHROMA_DIR = os.getenv("CHROMA_DIR", "./chroma_data")
    COLLECTION_NAME = os.getenv("COLLECTION_NAME", "ai_search_docs")
    EMBEDDING_MODEL = os.getenv(
        "EMBEDDING_MODEL",
        "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
    )

    OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
    OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
    OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")

settings = Settings()

这里使用 .env 文件读取配置,便于本地开发和 Docker 部署时修改参数。

如果你希望接入本地模型,例如 Ollama,可以将 OPENAI_BASE_URL 改成 Ollama 兼容接口地址,例如:

http://host.docker.internal:11434/v1

2. 文本切分工具:app/utils.py

def split_text(text: str, chunk_size: int = 500, overlap: int = 80):
    """
    将长文本切分成多个片段。
    chunk_size 表示每个片段最大长度。
    overlap 表示相邻片段之间的重叠字符数。
    """
    text = text.strip()
    if not text:
        return []

    chunks = []
    start = 0

    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)

        start = end - overlap
        if start < 0:
            start = 0

        if start >= len(text):
            break

    return chunks

为什么需要文本切分?

因为大模型和向量模型通常都有上下文长度限制。将文档切成合适的小块,可以提升检索精度,也能避免一次性输入过长导致性能下降。


3. 向量模型:app/embedding.py

from sentence_transformers import SentenceTransformer
from app.config import settings

class EmbeddingModel:
    def __init__(self):
        self.model = SentenceTransformer(settings.EMBEDDING_MODEL)

    def encode(self, texts):
        if isinstance(texts, str):
            texts = [texts]

        vectors = self.model.encode(
            texts,
            normalize_embeddings=True
        )

        return vectors.tolist()

embedding_model = EmbeddingModel()

这里使用的是多语言向量模型:

sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2

它支持中文和英文,体积相对较小,适合教程和轻量级项目。如果你追求更高质量,可以替换为更强的 Embedding 模型。


4. 向量数据库:app/vector_store.py

import uuid
import chromadb
from app.config import settings
from app.embedding import embedding_model

client = chromadb.PersistentClient(path=settings.CHROMA_DIR)

collection = client.get_or_create_collection(
    name=settings.COLLECTION_NAME
)

def add_documents(texts, metadatas=None):
    """
    添加文档片段到向量数据库。
    """
    if not texts:
        return {"count": 0}

    ids = [str(uuid.uuid4()) for _ in texts]
    embeddings = embedding_model.encode(texts)

    if metadatas is None:
        metadatas = [{"source": "unknown"} for _ in texts]

    collection.add(
        ids=ids,
        documents=texts,
        embeddings=embeddings,
        metadatas=metadatas
    )

    return {"count": len(texts)}


def search_documents(query: str, top_k: int = 5):
    """
    根据问题进行语义检索。
    """
    query_embedding = embedding_model.encode(query)[0]

    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k
    )

    documents = results.get("documents", [[]])[0]
    metadatas = results.get("metadatas", [[]])[0]
    distances = results.get("distances", [[]])[0]

    items = []
    for doc, meta, distance in zip(documents, metadatas, distances):
        items.append({
            "content": doc,
            "metadata": meta,
            "distance": distance
        })

    return items

这一层主要负责两个动作:

  1. 将文本片段写入 ChromaDB;
  2. 根据用户问题查询最相关的文本片段。

5. RAG 问答逻辑:app/rag.py

from openai import OpenAI
from app.config import settings
from app.vector_store import search_documents

client = OpenAI(
    api_key=settings.OPENAI_API_KEY,
    base_url=settings.OPENAI_BASE_URL
)

def build_prompt(question: str, contexts: list):
    context_text = "\n\n".join(
        [f"资料{i+1}:{item['content']}" for i, item in enumerate(contexts)]
    )

    prompt = f"""
你是一个严谨的 AI 搜索助手。请根据给定资料回答用户问题。

要求:
1. 优先依据资料回答;
2. 如果资料中没有答案,请明确说明“资料中没有找到相关信息”;
3. 不要编造不存在的事实;
4. 回答要结构清晰,必要时使用列表。

用户问题:
{question}

参考资料:
{context_text}
"""
    return prompt


def answer_question(question: str, top_k: int = 5):
    contexts = search_documents(question, top_k=top_k)
    prompt = build_prompt(question, contexts)

    if not settings.OPENAI_API_KEY:
        return {
            "answer": "未配置 OPENAI_API_KEY,当前仅返回检索结果。",
            "contexts": contexts
        }

    completion = client.chat.completions.create(
        model=settings.OPENAI_MODEL,
        messages=[
            {"role": "system", "content": "你是一个专业、可靠的AI搜索助手。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.2
    )

    answer = completion.choices[0].message.content

    return {
        "answer": answer,
        "contexts": contexts
    }

这里采用了比较简单的 Prompt 设计。生产环境中可以继续优化,例如:

  • 加入引用来源;
  • 限制回答格式;
  • 增加安全过滤;
  • 对检索结果做 rerank;
  • 对长文档做分层摘要。

6. FastAPI 接口:app/main.py

from fastapi import FastAPI, UploadFile, File
from pydantic import BaseModel
from app.utils import split_text
from app.vector_store import add_documents, search_documents
from app.rag import answer_question

app = FastAPI(title="AI Search Docker Demo")


class AddTextRequest(BaseModel):
    text: str
    source: str = "manual"


class SearchRequest(BaseModel):
    query: str
    top_k: int = 5


class AskRequest(BaseModel):
    question: str
    top_k: int = 5


@app.get("/")
def index():
    return {
        "message": "AI Search API is running",
        "docs": "/docs"
    }


@app.post("/add_text")
def add_text(req: AddTextRequest):
    chunks = split_text(req.text)
    metadatas = [{"source": req.source} for _ in chunks]
    result = add_documents(chunks, metadatas)

    return {
        "message": "文本已写入向量库",
        "chunks": result["count"]
    }


@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    content = await file.read()

    try:
        text = content.decode("utf-8")
    except UnicodeDecodeError:
        text = content.decode("gbk", errors="ignore")

    chunks = split_text(text)
    metadatas = [{"source": file.filename} for _ in chunks]
    result = add_documents(chunks, metadatas)

    return {
        "filename": file.filename,
        "chunks": result["count"]
    }


@app.post("/search")
def search(req: SearchRequest):
    results = search_documents(req.query, req.top_k)
    return {
        "query": req.query,
        "results": results
    }


@app.post("/ask")
def ask(req: AskRequest):
    result = answer_question(req.question, req.top_k)
    return result

该接口提供四个核心能力:

接口 说明
/add_text 直接添加文本
/upload 上传 txt 文档
/search 语义搜索
/ask AI 问答

启动后可以访问:

http://localhost:8000/docs

查看 Swagger 接口文档。


五、依赖文件:requirements.txt

fastapi==0.115.6
uvicorn[standard]==0.32.1
python-dotenv==1.0.1
python-multipart==0.0.19
chromadb==0.5.23
sentence-transformers==3.3.1
openai==1.57.0

如果你在国内环境下载模型较慢,可以提前将模型缓存到服务器,或者使用镜像源安装依赖。


六、环境变量文件:.env

CHROMA_DIR=./chroma_data
COLLECTION_NAME=ai_search_docs

EMBEDDING_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2

OPENAI_API_KEY=your_api_key_here
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini

如果你暂时不配置 OPENAI_API_KEY,系统仍然可以使用 /search 接口进行语义搜索,只是 /ask 接口不会调用大模型生成答案。

如果使用 Ollama,可以参考:

OPENAI_API_KEY=ollama
OPENAI_BASE_URL=http://host.docker.internal:11434/v1
OPENAI_MODEL=qwen2.5:7b

需要注意的是,Ollama 是否支持 OpenAI 兼容接口取决于版本和启动方式。


七、Dockerfile 编写

在项目根目录创建 Dockerfile

FROM python:3.11-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

这个 Dockerfile 做了以下事情:

  1. 使用 Python 3.11 slim 作为基础镜像;
  2. 安装必要编译依赖;
  3. 安装 Python 依赖;
  4. 拷贝项目代码;
  5. 暴露 8000 端口;
  6. 使用 Uvicorn 启动 FastAPI 服务。

八、Docker Compose 部署

创建 docker-compose.yml

version: "3.9"

services:
  ai-search:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ai-search
    ports:
      - "8000:8000"
    env_file:
      - .env
    volumes:
      - ./chroma_data:/app/chroma_data
      - ./data:/app/data
    restart: always

这里挂载了两个目录:

./chroma_data:/app/chroma_data
./data:/app/data

这样即使容器重启或重新构建,向量数据库数据也不会丢失。


九、本地运行方式

如果你不想一开始就使用 Docker,也可以本地运行。

1. 创建虚拟环境

python -m venv venv

Linux 或 macOS:

source venv/bin/activate

Windows:

venv\Scripts\activate

2. 安装依赖

pip install -r requirements.txt

3. 启动服务

uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

访问:

http://localhost:8000/docs

十、Docker 部署步骤

下面是正式使用 Docker 部署的完整流程。

1. 克隆或上传项目

假设项目目录为:

cd ai-search-docker

2. 修改环境变量

编辑 .env

vim .env

至少确认以下配置:

OPENAI_API_KEY=你的APIKey
OPENAI_MODEL=gpt-4o-mini

如果只想测试检索功能,可以不配置 API Key。

3. 构建镜像

docker compose build

4. 启动容器

docker compose up -d

5. 查看容器状态

docker ps

如果看到类似输出,说明启动成功:

CONTAINER ID   IMAGE                 PORTS                    NAMES
xxxxxxx        ai-search-docker      0.0.0.0:8000->8000/tcp   ai-search

6. 查看日志

docker logs -f ai-search

首次启动时,sentence-transformers 可能会下载模型,因此启动时间可能较长。

7. 访问接口文档

浏览器打开:

http://服务器IP:8000/docs

如果是本机部署:

http://localhost:8000/docs

十一、接口测试示例

1. 添加文本

请求地址:

POST /add_text

请求体:

{
  "text": "Docker 是一种容器化技术,可以将应用及其依赖打包到镜像中,从而实现跨环境一致部署。AI搜索系统通常结合向量数据库和大语言模型,实现语义检索和智能问答。",
  "source": "demo"
}

返回示例:

{
  "message": "文本已写入向量库",
  "chunks": 1
}

2. 语义搜索

请求地址:

POST /search

请求体:

{
  "query": "怎么部署智能搜索应用?",
  "top_k": 3
}

返回示例:

{
  "query": "怎么部署智能搜索应用?",
  "results": [
    {
      "content": "Docker 是一种容器化技术,可以将应用及其依赖打包到镜像中...",
      "metadata": {
        "source": "demo"
      },
      "distance": 0.32
    }
  ]
}

3. AI 问答

请求地址:

POST /ask

请求体:

{
  "question": "AI搜索系统通常由哪些部分组成?",
  "top_k": 5
}

返回示例:

{
  "answer": "AI搜索系统通常由文档处理模块、文本切分模块、Embedding 向量化模块、向量数据库、语义检索模块以及大语言模型问答模块组成。",
  "contexts": [
    {
      "content": "AI搜索系统通常结合向量数据库和大语言模型...",
      "metadata": {
        "source": "demo"
      },
      "distance": 0.28
    }
  ]
}

十二、常见问题与解决方案

1. Docker 启动很慢怎么办?

首次启动时需要下载向量模型,耗时取决于网络环境。解决方法:

  • 提前在本地下载模型;
  • 使用 Hugging Face 镜像;
  • 将模型目录挂载到容器;
  • 换用更小的 Embedding 模型。

2. ChromaDB 数据丢失怎么办?

确认 docker-compose.yml 中已经配置 volume:

volumes:
  - ./chroma_data:/app/chroma_data

如果没有挂载,容器删除后数据也会丢失。


3. 中文搜索效果不好怎么办?

可以尝试更适合中文的 Embedding 模型,例如:

BAAI/bge-small-zh-v1.5
BAAI/bge-base-zh-v1.5
moka-ai/m3e-base

替换 .env 中的配置即可:

EMBEDDING_MODEL=BAAI/bge-small-zh-v1.5

但需要注意,不同模型向量维度可能不同。更换模型后,建议清空旧的向量库数据重新入库。


4. 如何支持 PDF、Word 文档?

本文示例只支持 txt 文本文件。如果要支持 PDF,可以增加依赖:

pypdf

然后在上传接口中根据文件后缀解析。支持 Word 可以使用:

python-docx

生产环境中建议将“文件解析”和“文本入库”做成异步任务,避免大文件上传导致接口阻塞。


5. 如何让答案带引用来源?

可以在 Prompt 中要求模型输出引用,并把检索结果编号:

资料1:...
资料2:...

然后要求模型回答时标注:

根据资料1和资料2可知,...

不过需要注意,大模型可能会出现引用不准确的问题。更严格的做法是在后端返回检索片段,由前端展示引用来源。


十三、生产环境优化建议

如果你准备将该 AI 搜索系统用于真实业务,建议进一步优化以下方面。

1. 增加鉴权

当前示例接口没有鉴权,任何人都可以上传文档和查询。如果部署到公网,必须增加鉴权,例如:

  • API Key;
  • JWT;
  • OAuth2;
  • 企业内部 SSO。

2. 增加任务队列

大文件解析、批量向量化、重建索引都比较耗时,不适合在接口请求中同步完成。可以引入:

  • Celery;
  • Redis Queue;
  • Dramatiq;
  • FastAPI BackgroundTasks。

3. 增加数据库

可以使用 PostgreSQL 或 MySQL 存储:

  • 用户信息;
  • 文档信息;
  • 上传记录;
  • 问答历史;
  • 权限配置。

向量数据库只负责语义检索,不建议承担所有业务数据存储。

4. 增加重排序模型

向量检索召回的结果不一定总是最准确。可以在向量检索之后加入 rerank 模型,例如:

BAAI/bge-reranker-base
BAAI/bge-reranker-large

流程变为:

用户问题 -> 向量检索召回 Top 20 -> Rerank 重排 Top 5 -> 大模型生成答案

这样可以显著提升答案质量。

5. 增加缓存

对于高频问题,可以使用 Redis 缓存搜索结果或最终答案,降低模型调用成本。

6. 增加日志和监控

建议记录:

  • 请求耗时;
  • 检索命中文档;
  • 模型调用耗时;
  • Token 消耗;
  • 用户反馈;
  • 异常日志。

可以结合 Prometheus、Grafana、ELK 等工具构建监控体系。


十四、完整启动命令汇总

# 进入项目目录
cd ai-search-docker

# 构建镜像
docker compose build

# 后台启动
docker compose up -d

# 查看容器
docker ps

# 查看日志
docker logs -f ai-search

# 停止服务
docker compose down

# 重启服务
docker compose restart

如果你修改了 Python 代码,需要重新构建:

docker compose up -d --build

如果想清空向量数据,可以删除:

rm -rf chroma_data

然后重新启动服务并重新导入文档。


十五、总结

本文从零实现了一个基于 FastAPI、ChromaDB、sentence-transformers 和大语言模型接口的 AI 搜索系统,并通过 Docker Compose 完成部署。

这个项目虽然是简化版本,但已经覆盖 AI 搜索的核心链路:

文档输入 -> 文本切分 -> 向量化 -> 向量存储 -> 语义检索 -> 大模型回答

你可以基于本文源码继续扩展,例如增加 PDF 解析、用户权限、知识库管理、前端页面、流式输出、多租户隔离、问答记录和搜索反馈等功能。

如果你只是想快速体验 AI 搜索,可以直接按照本文步骤运行 Docker;如果你希望深入理解 RAG 系统,也可以逐个阅读源码模块,理解每一层的职责。掌握这套基础架构后,你就可以构建企业知识库、智能客服、文档问答、代码检索、产品手册助手等多种 AI 应用。

目录结构
全文