使用 BentoML 和 openGauss 构建 RAG
BentoML 是一个开源的 AI 模型服务化框架,用于将机器学习模型打包、部署并作为高性能 API 服务运行。它支持自适应批处理、请求队列、CPU/GPU 资源调度,以及一键 Docker 容器化和 BentoCloud 部署。
在本教程中,我们将向您展示如何使用 BentoML(模型服务化)、DeepSeek(LLM)、FastEmbed(本地向量嵌入)与 openGauss(向量存储)构建一套完整的检索增强生成(RAG)管道。该管道集成了 BentoML Service(RAG 管道服务化)、FastEmbed(本地文本向量化)、openGauss DataVec(向量存储与检索)和 DeepSeek LLM(答案生成),最终通过 BentoML Service 对外提供统一的 API 接口。
与传统的单脚本 RAG 不同,BentoML 化的 RAG 管道可以一键部署为独立服务,支持水平扩展、请求批处理和容器化部署。
准备工作
环境配置
(1)配置 Docker 环境
本文以容器方式启动 openGauss 实例,因此需要安装 Docker,可参考 Manuals | Docker Docs 。
若本地已安装 openGauss 或不需要以容器方式启动可跳过该步骤。
(2)配置 uv 环境
参考文档:Installation | uv
# 使用脚本一键安装
$ curl -LsSf https://astral.sh/uv/install.sh | sh
# (可选)为 uv 命令启用 shell 自动补全
$ echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc
# 刷新环境变量
$ source ~/.bashrc
# 验证是否安装成功
$ uv --version模型与 API 说明
本文使用 BentoML 作为服务化框架,FastEmbed 作为本地 Embedding 模型,DeepSeek 作为 LLM 后端。
| 环节 | 模型 / 工具 | 用途 |
|---|---|---|
| 文档分块 | Markdown 标题分割器 | 将文档按 # 标题分割为文本块 |
| 向量嵌入 | FastEmbed | 本地文本转向量 |
| 向量存储 | openGauss DataVec | 向量相似度检索,返回 Top-K 候选 |
| 答案生成 | DeepSeek | 基于检索到的参考资料生成回答 |
| 服务化 | BentoML Service | 将 RAG 管道打包为标准化 API 服务 |
(1)LLM 模型
本文使用 deepseek-v4-pro 模型,通过 DeepSeek 开放平台获取 api_key。
模型详细信息如下。
| 项目 | 值 |
|---|---|
base_url | https://api.deepseek.com |
api_key | sk-xxxxxxxxxx |
| 模型名称 | deepseek-v4-pro |
(2)Embedding 模型
向量模型使用 FastEmbed 的 BAAI/bge-small-zh-v1.5 本地模型。
模型详细信息如下。
| 项目 | 值 |
|---|---|
| 模型名称 | BAAI/bge-small-zh-v1.5 |
| 向量维度 | 512 |
启动 openGauss 实例
本文以容器方式启动 openGauss 实例。
# 启动容器
$ docker run -d \
--name opengauss \
--privileged=true \
--restart=always \
-p 5432:5432 \
-e GS_PASSWORD=openGauss@123 \
-v ./opengauss:/var/lib/opengauss/data \
opengauss/opengauss:7.0.0-RC1
# 确认容器正常运行
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0b27fd4856a6 opengauss/opengauss:7.0.0-RC1 "entrypoint.sh gauss…" 4 hours ago Up 4 hours 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp opengauss说明
openGauss 容器默认已创建 gaussdb 用户和 postgres 库,启动时使用环境变量 GS_PASSWORD 指定用户密码。
配置项目环境
# 创建并进入项目目录
$ mkdir bentoml && cd bentoml
# 创建虚拟环境
$ uv venv
# 安装依赖包
$ uv pip install bentoml fastembed numpy openai psycopg2-binary pydantic tqdm之后的操作均在该项目环境中进行。
构建 RAG 管道
定义使用 openGauss 作为向量存储、FastEmbed 作为本地嵌入模型、DeepSeek 作为 LLM 的 RAG 管道。
以下代码保存为 rag.py。
openGauss 客户端
OpenGaussClient 负责与 openGauss 数据库的所有交互。包含以下方法:
__init__:创建数据库连接,连接参数通过环境变量配置(OG_HOST、OG_PORT、OG_USER、OG_PASSWORD、OG_DBNAME)。drop_table:删除指定表,用于重置数据。create_table:创建向量存储表,包含id(主键)、content(文本)、embedding(向量)、created_at(时间戳)四个字段。insert_into_table:批量插入向量行数据。query:执行向量相似度查询,使用<=>运算符计算距离,返回 Top-K 结果。close:关闭数据库连接。
import os
from typing import List, Any
import psycopg2
from psycopg2 import sql
class OpenGaussClient:
"""A simple openGauss client using psycopg2."""
def __init__(self, host=None, port=None, user=None, password=None, database=None):
self.host = host or os.getenv("OG_HOST", "localhost")
self.port = port or int(os.getenv("OG_PORT", "5432"))
self.user = user or os.getenv("OG_USER", "gaussdb")
self.password = password or os.getenv("OG_PASSWORD", "openGauss@123")
self.database = database or os.getenv("OG_DBNAME", "postgres")
self.conn = psycopg2.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
dbname=self.database,
)
self.cursor = self.conn.cursor()
def drop_table(self, table_name: str):
"""Drop table."""
self.cursor.execute(
sql.SQL(
"""
DROP TABLE IF EXISTS public.{table_name};
"""
).format(table_name=sql.Identifier(table_name))
)
self.conn.commit()
def create_table(self, table_name: str, dim: int):
"""Create table."""
self.cursor.execute(
sql.SQL(
"""
CREATE TABLE IF NOT EXISTS public.{table_name} (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
embedding vector({dim}) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
).format(table_name=sql.Identifier(table_name), dim=sql.Literal(dim)),
)
self.conn.commit()
def insert_into_table(self, table_name: str, rows: List[Any]) -> None:
"""Insert into the table."""
self.cursor.executemany(
sql.SQL(
"""
INSERT INTO public.{table_name} (id, content, embedding)
VALUES (%s, %s, %s)
"""
)
.format(table_name=sql.Identifier(table_name))
.as_string(self.cursor),
rows,
)
self.conn.commit()
def query(self, table_name: str, question_vec: List[float], top_k: int = 5) -> List[str]:
"""Execute a vector similarity query and return the results."""
self.cursor.execute(
sql.SQL(
"""
SELECT content
FROM public.{table_name}
ORDER BY embedding <-> %s::vector
LIMIT %s::int;
"""
).format(table_name=sql.Identifier(table_name)),
(question_vec, top_k),
)
return [row[0] for row in self.cursor.fetchall()]
def close(self):
"""Close the connection."""
self.cursor.close()
self.conn.close()文档分块
chunk_source 函数负责将文档源(URL 或本地文件路径)按 Markdown 的 # 标题标记分割为文本块。以 # 为分隔符可以自然地按文档章节边界进行分块,保留每个章节的语义完整性。
import random
import string
import urllib.request
def chunk_source(urls: List[str]) -> List[str]:
"""Chunk text from URLs into chunks."""
all_chunks: List[str] = []
for url in urls:
if url.startswith("http://") or url.startswith("https://"):
random_name = "".join(random.choices(string.ascii_letters + string.digits, k=8))
file_path = f"./{random_name}.md"
try:
urllib.request.urlretrieve(url, file_path)
with open(file_path, "r", encoding="utf-8") as file:
file_text = file.read()
text_lines = file_text.split("# ")
text_lines = [line for line in text_lines if line.strip()]
all_chunks.extend(text_lines)
finally:
if os.path.exists(file_path):
os.remove(file_path)
else:
with open(url, "r", encoding="utf-8") as f:
file_text = f.read()
text_lines = file_text.split("# ")
text_lines = [line for line in text_lines if line.strip()]
all_chunks.extend(text_lines)
return all_chunksRAG 类
RAG 类是整个 RAG 管道的核心,集成了 FastEmbed 本地嵌入模型和 DeepSeek LLM。它接收 OpenGaussClient 和 DeepSeek 客户端作为参数,通过 _prepare_* 方法完成初始化。
核心方法如下:
_prepare_deepseek:初始化 DeepSeek LLM 客户端和 FastEmbed 嵌入模型(BAAI/bge-small-zh-v1.5),定义系统提示和用户提示模板。_prepare_opengauss:绑定 openGauss 客户端,删除旧表并创建新的向量存储表。_embedder:调用本地 FastEmbed 模型将单条文本转为向量。load:将文档分块、向量化,并写入 openGauss。retrieve:将用户问题向量化后,在 openGauss 中执行相似度检索,返回 Top-K 文本块。answer:基于检索到的文本块构建提示词,调用 DeepSeek LLM 生成答案。支持return_retrieved_text参数同时返回检索结果。
import uuid
import numpy as np
from openai import OpenAI
from fastembed import TextEmbedding
class RAG:
"""RAG (Retrieval-Augmented Generation) class built upon FastEmbed, DeepSeek and openGauss."""
def __init__(self, deepseek_client: OpenAI, opengauss_client: OpenGaussClient):
self._prepare_deepseek(deepseek_client)
self._prepare_opengauss(opengauss_client)
def _embedder(self, text: str) -> List[float]:
"""Embed text into embedding vector."""
response = list(self.embedding_model.embed([text]))
result = response[0]
return result.tolist() if isinstance(result, np.ndarray) else result
def _prepare_deepseek(self, deepseek_client: OpenAI, llm_model: str = "deepseek-v4-pro"):
"""Prepare DeepSeek client and embedding model."""
self.deepseek_client = deepseek_client
self.llm_model = llm_model
self.embedding_model = TextEmbedding(model_name="BAAI/bge-small-zh-v1.5")
self.SYSTEM_PROMPT = """你是一个智能问答助手。
你的任务是根据提供的参考资料(Context)回答用户问题。
请遵循以下规则:
1. 仅根据参考资料回答,不要编造信息。
2. 如果资料中没有答案,请直接说"根据提供的资料,无法回答该问题"。
3. 回答要简洁、准确。"""
self.USER_PROMPT = """请根据以下参考资料回答问题。
<参考资料>
{contexts_text}
</参考资料>
<问题>
{question}
</问题>
请开始回答:"""
def _prepare_opengauss(self, opengauss_client: OpenGaussClient, table_name: str = "bentoml_rag"):
"""Prepare openGauss client."""
self.opengauss_client = opengauss_client
self.table_name = table_name
self.opengauss_client.drop_table(self.table_name)
dim = len(self._embedder("get_dim"))
self.opengauss_client.create_table(self.table_name, dim)
def load(self, urls: List[str]):
"""Load the urls."""
rows: list[Any] = []
texts = chunk_source(urls)
for text in texts:
rows.append(
(
str(uuid.uuid4()),
text,
self._embedder(text),
)
)
self.opengauss_client.insert_into_table(table_name=self.table_name, rows=rows)
def retrieve(self, question: str, top_k: int = 3):
"""Retrieve the top k results."""
question_vec = self._embedder(question)
retrieve_texts = self.opengauss_client.query(self.table_name, question_vec, top_k)
return retrieve_texts
def answer(self, question: str, top_k: int = 3):
"""Answer the question."""
retrieve_texts = self.retrieve(question, top_k)
retrieve_texts_str: str = ""
for i, c in enumerate(retrieve_texts, start=1):
retrieve_texts_str += f"[{i}] {c}\n\n"
completion = self.deepseek_client.chat.completions.create(
model=self.llm_model,
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT},
{
"role": "user",
"content": self.USER_PROMPT.format(
contexts_text=retrieve_texts_str, question=question
),
},
],
stream=True,
reasoning_effort="high",
extra_body={"thinking": {"type": "enabled"}},
)
is_answering = False
answer_chunks = []
print("\n" + "=" * 20 + "思考过程" + "=" * 20)
for chunk in completion:
delta = chunk.choices[0].delta
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
if not is_answering:
print(delta.reasoning_content, end="", flush=True)
if hasattr(delta, "content") and delta.content:
if not is_answering:
print("\n" + "=" * 20 + "完整回复" + "=" * 20)
is_answering = True
print(delta.content, end="", flush=True)
answer_chunks.append(delta.content)
print()
return "".join(answer_chunks)构建 RAG
创建 DeepSeek 客户端和 openGauss 客户端并构建 RAG。
def build_rag():
"""Build Rag."""
deepseek_client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
opengauss_client = OpenGaussClient()
rag = RAG(deepseek_client=deepseek_client, opengauss_client=opengauss_client)
return rag测试 RAG 管道
调用 load 方法将 openGauss DataVec 文档页面加载到 RAG 管道中。
rag = build_rag()
urls = ["https://raw.gitcode.com/opengauss/docs/raw/master/docs/zh/datavec/datavec_overview.md"]
rag.load(urls)调用 answer 方法进行问答查询。设置 return_retrieved_text=True 可以同时获取答案和检索到的上下文文本。
question = "openGauss支持哪些向量数据类型"
response = rag.answer(question=question, top_k=3)
print(response)预期输出结果如下。
====================思考过程====================
我们被问到:"openGauss支持哪些向量数据类型"。参考资料的[3]部分明确列出了四种向量数据类型:vector, bitvec, sparsevec, halfvec,并附有简要说明。因此可以直接回答。
====================完整回复====================
根据参考资料,openGauss 支持以下四种向量数据类型:
- **vector**:float 向量,最高支持 16000 维
- **bitvec**:bit 向量,最高支持 83,886,080 维
- **sparsevec**:sparse 向量,最高支持 1,000,000,000 维,最高支持 16000 非零元素数
- **halfvec**:halfvec 向量,最高支持 16000 维部署 BentoML 服务
以上以脚本方式完成了 RAG 管道的构建和测试。现在需要将整个 RAG 管道打包为 BentoML Service,使其可以独立部署和水平扩展。
编写 Service 定义
创建 service.py,通过 import rag as rag_module 导入第 2 节定义的类,使用 @bentoml.service 装饰器定义服务。
import json
import os
import bentoml
import pydantic
from openai import OpenAI
from starlette.responses import Response
import rag as rag_module
def json_response(data: dict) -> Response:
"""Return a JSON response with unicode preserved."""
return Response(
json.dumps(data, ensure_ascii=False, indent=2),
media_type="application/json",
)
class IngestRequest(pydantic.BaseModel):
"""Request schema for the /ingest endpoint."""
sources: list[str] = pydantic.Field(default_factory=list)
table_name: str = pydantic.Field(default="bentoml_rag")
class QueryRequest(pydantic.BaseModel):
"""Request schema for the /query endpoint."""
question: str
table_name: str = pydantic.Field(default="bentoml_rag")
top_k: int = pydantic.Field(default=5, ge=1)
@bentoml.service(
name="opengauss_rag",
traffic={"timeout": 300},
)
class OpenGaussRAG:
"""BentoML service that wraps a RAG pipeline backed by openGauss DataVec."""
def __init__(self) -> None:
deepseek_client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
self.client = rag_module.OpenGaussClient()
self.rag = rag_module.RAG(
deepseek_client=deepseek_client, opengauss_client=self.client
)
@bentoml.api
async def ingest(self, request: IngestRequest):
"""Ingest documents: chunk, embed, and store into openGauss."""
if not request.sources:
return json_response({"status": "ok", "skipped": True})
self.rag.table_name = request.table_name
self.rag._prepare_opengauss(self.client, request.table_name)
self.rag.load(request.sources)
return json_response({"status": "ok", "table_name": request.table_name})
@bentoml.api
async def query(self, request: QueryRequest):
"""Query the RAG pipeline with a question."""
answer = self.rag.answer(request.question, top_k=request.top_k)
return json_response({
"question": request.question,
"answer": answer,
})
@bentoml.api
async def health(self):
"""Health check endpoint."""
return json_response({"status": "healthy"})说明
service.py通过import rag as rag_module导入第 2 节定义的OpenGaussClient和RAG类。- 服务启动时在
__init__中创建 DeepSeek 客户端、openGauss 客户端和 RAG 实例。 ingest端点调用_prepare_opengauss初始化表结构,再调用load摄入数据。query端点直接调用 RAG 实例的retrieve和answer方法。@bentoml.service声明一个 BentoML 服务,traffic.timeout设置请求超时时间(秒)。@bentoml.api将方法暴露为 HTTP 端点,请求和响应由 Pydantic 模型自动序列化。- BentoML 支持异步 API(
async def),可以透明地支持自适应批处理。
编写 BentoML 构建配置
创建 bentofile.yaml,描述服务的构建和部署配置。
service: "service:OpenGaussRAG"
description: "RAG pipeline backed by openGauss DataVec vector store"
labels:
owner: yanzhicong
project: bentoml-rag
include:
- "rag.py"
- "service.py"
python:
packages:
- fastembed>=0.5.1
- numpy>=2.0.0
- openai>=2.33.0
- psycopg2-binary>=2.9.12
- pydantic>=2.0.0
- tqdm>=4.67.3说明
service字段格式为"<文件名>:<类名>",指定 BentoML 服务的入口点。include字段列出需要打包到 Bento 中的 Python 文件。python.packages声明运行时的 pip 依赖,BentoML 会自动安装。
本地启动 BentoML 服务
使用 bentoml serve 命令在本地启动 RAG 服务。
# 进入项目目录
$ cd bentoml
# 启动服务
$ uv run bentoml serve service:OpenGaussRAG --port 3000预期输出:
2026-05-03T19:36:26+0800 [INFO] [cli] Starting production HTTP BentoServer from "service:OpenGaussRAG" listening on http://localhost:3000 (Press CTRL+C to quit)
2026-05-03T19:36:28+0800 [INFO] [entry_service:opengauss_rag:1] Service opengauss_rag initialized调用 BentoML 服务 API
服务启动后,可以通过 HTTP 客户端调用 API。
使用 curl
(1)摄入数据
$ curl -X POST http://localhost:3000/ingest \
-H "Content-Type: application/json" \
-d '{
"request": {
"sources": [
"https://raw.gitcode.com/opengauss/docs/raw/master/docs/zh/datavec/datavec_overview.md",
"https://gitcode.com/opengauss/docs/blob/master/docs/zh/datavec/index.md"
],
"table_name": "bentoml_rag"
}
}'预期输出:
{
"status": "ok",
"table_name": "bentoml_rag"
}(2)查询问答
$ curl -X POST http://localhost:3000/query \
-H "Content-Type: application/json" \
-d '{
"request": {
"question": "openGauss支持哪些向量数据类型",
"table_name": "bentoml_rag",
"top_k": 5
}
}'预期输出:
{
"question": "openGauss支持哪些向量数据类型",
"answer": "根据参考资料,openGauss支持的向量数据类型包括以下四种:\n\n- **vector**:float向量,最高支持16000维。\n- **bitvec**:bit向量,最高支持83,886,080维。\n- **sparsevec**:sparse向量,最高支持1,000,000,000维,最高支持16000非零元素数。\n- **halfvec**:halfvec向量,最高支持16000维。"(3)健康检查
$ curl -X POST http://localhost:3000/health预期输出:
{
"status": "healthy"
}使用 Python 客户端
BentoML 也提供了 Python 原生客户端,可以直接在代码中调用部署的服务。
(1)摄入数据
import json
import bentoml
client = bentoml.SyncHTTPClient("http://localhost:3000")
ingest_result = client.call(
"ingest",
request={
"sources": [
"https://raw.gitcode.com/opengauss/docs/raw/master/docs/zh/datavec/datavec_overview.md",
],
"table_name": "bentoml_rag",
},
)
print(json.dumps(ingest_result, indent=2, ensure_ascii=False))预期输出:
{
"status": "ok",
"table_name": "bentoml_rag"
}(2)查询问答
query_result = client.call(
"query",
request={
"question": "openGauss支持哪些向量数据类型",
"table_name": "bentoml_rag",
"top_k": 5,
}
)
print(json.dumps(query_result, indent=2, ensure_ascii=False))预期输出:
{
"question": "openGauss支持哪些向量数据类型",
"answer": "根据参考资料,openGauss DataVec 支持的向量数据类型包括:\n\n- **vector**:float 向量,最高支持 16000 维 \n- **bitvec**:bit 向量,最高支持 83,886,080 维 \n- **sparsevec**:sparse 向量,最高支持 1,000,000,000 维,最高支持 16000 非零元素数 \n- **halfvec**:halfvec 向量,最高支持 16000 维"
}(3)健康检查
health_result = client.call(
"health",
)
print(json.dumps(health_result, indent=2, ensure_ascii=False))预期输出:
{
"status": "healthy"
}构建并部署 Bento
将服务打包为标准化 Bento 格式,便于分发和部署。
# 进入项目目录
$ cd bentoml
# 构建 Bento
$ uv run bentoml build
INFO: Adding BentoML requirement to the image: bentoml==1.4.38.
INFO: Locking PyPI package versions.
██████╗ ███████╗███╗ ██╗████████╗ ██████╗ ███╗ ███╗██╗
██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██╔═══██╗████╗ ████║██║
██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██╔████╔██║██║
██╔══██╗██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║╚██╔╝██║██║
██████╔╝███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ╚═╝ ██║███████╗
╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
Successfully built Bento(tag="opengauss_rag:wh66tgcg5gmvuaav").
Next steps:
* Deploy to BentoCloud:
$ bentoml deploy opengauss_rag:wh66tgcg5gmvuaav -n ${DEPLOYMENT_NAME}
* Update an existing deployment on BentoCloud:
$ bentoml deployment update --bento opengauss_rag:wh66tgcg5gmvuaav ${DEPLOYMENT_NAME}
* Containerize your Bento with `bentoml containerize`:
$ bentoml containerize opengauss_rag:wh66tgcg5gmvuaav
* Push to BentoCloud with `bentoml push`:
$ bentoml push opengauss_rag:wh66tgcg5gmvuaav
# 查看已构建的 Bento 列表
$ uv run bentoml list
Tag Size Model Size Creation Time
opengauss_rag:wh66tgcg5gmvuaav 34.06 KiB 0.00 B 2026-05-03 20:14:58也可以将其构建为 Docker 镜像进行部署。
# 进入项目目录
$ cd bentoml
# 容器化 Bento(生成 Dockerfile 并构建镜像)
$ uv run bentoml containerize opengauss_rag:latest
INFO: Building OCI-compliant image for opengauss_rag:wh66tgcg5gmvuaav with docker
[+] Building 402.7s (17/17) FINISHED
......
Successfully built Bento container for "opengauss_rag:latest" with tag(s) "opengauss_rag:wh66tgcg5gmvuaav"
To run your newly built Bento container, run:
docker run --rm -p 3000:3000 opengauss_rag:wh66tgcg5gmvuaav
# 查看构建的镜像
$ docker images | grep opengauss_rag
opengauss_rag wh66tgcg5gmvuaav 5456263e088c About a minute ago 883MB
# 启动容器
$ docker run --rm \
-e DEEPSEEK_API_KEY="sk-xxxxxxxxxx" \
-e OG_HOST="172.18.247.170" \
-e OG_PORT="5432"\
-e OG_USER="gaussdb"\
-e OG_PASSWORD="openGauss@123"\
-e OG_DBNAME="postgres"\
-p 3000:3000 \
opengauss_rag:wh66tgcg5gmvuaav