在单台 VPS 上用 Python + NVIDIA 免费 API + Qdrant 跑通 RAG
背景
最近想在一台 VPS 上验证 RAG 的完整流程:文档向量化 → 向量检索 → 拼装上下文 → 调用 LLM 生成答案。
VPS 的现状是:5.8Gi 内存、可用约 1.5Gi、磁盘剩余 13G,上面已经跑了 K3s、WordPress、n8n 和一个 Docker 版的 One API。资源紧张,上 Dify 或 Milvus 这类完整栈会直接 OOM。
于是决定走轻量路线:Python 脚本 + 本地 Qdrant + NVIDIA 免费 API。不部署额外的常驻服务,不引入新的容器,全部在一个脚本里完成。
技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 向量库 | Qdrant 本地模式(qdrant-client) | 不占额外进程内存,数据持久化到磁盘 |
| Embedding | nvidia/nemotron-3-embed-1b | NVIDIA 免费层,2048 维,支持中文,可商用 |
| LLM | nvidia/nemotron-3-super-120b-a12b | NVIDIA 免费层,兼容 OpenAI 接口 |
| 编排 | 纯 Python 脚本 | 无需 Dify,资源占用最小 |
关键坑:input_type 参数
NVIDIA 的 Embedding 模型是非对称的,必须在请求体里显式指定 input_type:
- 索引文档时用
"passage" - 检索提问时用
"query"
用错会导致检索准确率大幅下降。而标准的 OpenAI Embedding 接口不支持这个字段,所以:
- One API 会返回 404 —— 因为它不会主动注入
input_type - 直接调用 NVIDIA 接口可以成功 —— 用 OpenAI SDK 的
extra_body参数透传即可
这个坑直接决定了实现路径:绕开 One API,在脚本里直接处理。
实现步骤
1. 准备环境
bash
mkdir -p /opt/rag-nvidia/data cd /opt/rag-nvidia apt update && apt install -y python3-venv python3-pip python3 -m venv venv source venv/bin/activate pip install openai qdrant-client python-dotenv
2. 配置 API Key
bash
cat > .env << 'EOF' NVIDIA_API_KEY=nvapi-你的Key NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1 EMBEDDING_MODEL=nvidia/nemotron-3-embed-1b LLM_MODEL=nvidia/nemotron-3-super-120b-a12b EOF
3. 准备测试文档
bash
cat > data/sample.txt << 'EOF' One API 是一个开源的 LLM 网关,支持统一接入多家模型供应商。 RAG(检索增强生成)通过检索外部知识来提升大模型回答的准确性。 Qdrant 是一个用 Rust 编写的高性能向量数据库,支持本地模式和服务器模式。 NVIDIA NIM 提供了一套兼容 OpenAI 接口的推理微服务。 Kubernetes 是一个容器编排平台,可以自动管理容器的部署和扩缩容。 Tailscale 是一个基于 WireGuard 的零信任网络工具。 EOF
4. RAG 主脚本
python
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
from qdrant_client import QdrantClient, models
load_dotenv()
client = OpenAI(
base_url=os.getenv("NVIDIA_BASE_URL"),
api_key=os.getenv("NVIDIA_API_KEY"),
)
qdrant = QdrantClient(path="./rag_data")
COLLECTION_NAME = "my_docs"
VECTOR_SIZE = 2048
def embed_texts(texts, input_type="passage"):
response = client.embeddings.create(
model=os.getenv("EMBEDDING_MODEL"),
input=texts,
extra_body={"input_type": input_type}, # 关键
)
return [d.embedding for d in response.data]
def load_and_chunk(file_path):
text = Path(file_path).read_text(encoding="utf-8")
return [line.strip() for line in text.splitlines() if line.strip()]
def build_index(chunks):
vectors = embed_texts(chunks, input_type="passage")
if qdrant.collection_exists(COLLECTION_NAME):
qdrant.delete_collection(COLLECTION_NAME)
qdrant.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=models.VectorParams(
size=VECTOR_SIZE, distance=models.Distance.COSINE
),
)
qdrant.upsert(
collection_name=COLLECTION_NAME,
points=[
models.PointStruct(id=i, vector=vec, payload={"text": chunk})
for i, (vec, chunk) in enumerate(zip(vectors, chunks))
],
)
print(f"已索引 {len(chunks)} 个文档片段")
def retrieve(query, top_k=3):
query_vec = embed_texts([query], input_type="query")[0]
results = qdrant.query_points(
collection_name=COLLECTION_NAME,
query=query_vec,
limit=top_k,
)
return [hit.payload["text"] for hit in results.points]
def generate_answer(question):
context = "\n".join(f"- {c}" for c in retrieve(question))
prompt = f"""请基于以下资料回答问题。如果资料中没有相关信息,请直接说明"资料中未提及"。
资料:
{context}
问题:{question}
回答:"""
response = client.chat.completions.create(
model=os.getenv("LLM_MODEL"),
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
return response.choices[0].message.content
if __name__ == "__main__":
chunks = load_and_chunk("data/sample.txt")
build_index(chunks)
for q in ["RAG 是什么?", "Qdrant 是用什么语言写的?", "One API 的作用是什么?"]:
print(f"\n{'='*50}")
print(f"问题:{q}")
print(f"检索到的片段:{retrieve(q)}")
print(f"回答:{generate_answer(q)}")
qdrant.close()
5. 运行
bash
python3 rag.py
运行结果
text
已索引 6 个文档片段 ================================================== 问题:RAG 是什么? 检索到的片段:['RAG(检索增强生成)通过检索外部知识来提升大模型回答的准确性。', ...] 回答:RAG(检索增强生成)是一种通过检索外部知识来提升大语言模型回答准确性的技术... ================================================== 问题:Qdrant 是用什么语言写的? 检索到的片段:['Qdrant 是一个用 Rust 编写的高性能向量数据库...', ...] 回答:Qdrant 是用 Rust 编写的。 ================================================== 问题:One API 的作用是什么? 检索到的片段:['One API 是一个开源的 LLM 网关,支持统一接入多家模型供应商。', ...] 回答:One API 是一个开源的 LLM 网关,支持统一接入多家模型供应商。
检索环节完全准确:每个问题的第一条检索结果都是最相关的片段,说明 input_type 的区分和 2048 维向量都工作正常。
数据存储
向量数据存在 /opt/rag-nvidia/rag_data/ 目录下,包括文本 payload 和 2048 维向量。重启脚本数据不丢,想重置直接删除该目录即可。
踩坑记录
- One API 返回 404:NVIDIA 的 Embedding 模型需要
input_type参数,One API 不透传,导致请求失败。解决方式是绕开 One API,在 Python 脚本里用extra_body直接透传。 -query/-passage后缀无效:NVIDIA 的部分旧模型支持用模型名后缀指定input_type,但nemotron-3-embed-1b不支持,必须用请求体参数。python3-venv缺失:Debian/Ubuntu 系统默认没有 venv,需要apt install python3-venv。qdrant-client退出时报错:ImportError: sys.meta_path is None是解释器关闭时的已知清理问题,不影响功能。在脚本末尾显式调用qdrant.close()即可消除。
总结
这套方案的核心优势是极简:
- 不部署额外的常驻服务,内存占用几乎为零
- 用 NVIDIA 免费 API,没有 token 成本
- 代码量不到 100 行,所有逻辑可控
它验证了 RAG 的三个核心环节:向量化、检索、生成。后续如果想服务化,只需把 QdrantClient(path=...) 改成 QdrantClient(url=...) 指向 K8s 里的 Qdrant Pod,业务代码不用改。
