在单台 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不占额外进程内存,数据持久化到磁盘
Embeddingnvidia/nemotron-3-embed-1bNVIDIA 免费层,2048 维,支持中文,可商用
LLMnvidia/nemotron-3-super-120b-a12bNVIDIA 免费层,兼容 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 维向量。重启脚本数据不丢,想重置直接删除该目录即可。

踩坑记录

  1. One API 返回 404:NVIDIA 的 Embedding 模型需要 input_type 参数,One API 不透传,导致请求失败。解决方式是绕开 One API,在 Python 脚本里用 extra_body 直接透传。
  2. -query / -passage 后缀无效:NVIDIA 的部分旧模型支持用模型名后缀指定 input_type,但 nemotron-3-embed-1b 不支持,必须用请求体参数。
  3. python3-venv 缺失:Debian/Ubuntu 系统默认没有 venv,需要 apt install python3-venv
  4. qdrant-client 退出时报错ImportError: sys.meta_path is None 是解释器关闭时的已知清理问题,不影响功能。在脚本末尾显式调用 qdrant.close() 即可消除。

总结

这套方案的核心优势是极简

  • 不部署额外的常驻服务,内存占用几乎为零
  • 用 NVIDIA 免费 API,没有 token 成本
  • 代码量不到 100 行,所有逻辑可控

它验证了 RAG 的三个核心环节:向量化、检索、生成。后续如果想服务化,只需把 QdrantClient(path=...) 改成 QdrantClient(url=...) 指向 K8s 里的 Qdrant Pod,业务代码不用改。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注