大模型应用实战

企业知识库系列(02):经典向量 RAG 实测——QAnything vs LightRAG

同一套 89 道测试题,分别对 QAnything v2 和 LightRAG 1.5.6 跑评测。本文记录从部署到出数据的完整过程,包括 Docker GPU 挂载、Milvus 崩溃恢复、user_id 隔离坑等真实踩坑,以及两个框架在单跳、多跳、边界拒答三个维度上的真实对比数据。

·约 9 分钟阅读·AI Engineering

这篇文章的前提

上一篇建好了统一测试集:89 道题,50 道单跳事实查询、20 道多跳推理、19 道边界拒答,文档来源是 LightRAG 和 graphrag 的官方技术文档。

这篇的任务是让两个框架在同一套题上跑,然后把数字摆出来。

但在说数字之前,必须先说部署过程——因为部署本身就是选型的一部分。


两个框架的部署差异

LightRAG:pip install 即用

pip install lightrag-hku

没有 Docker,没有数据库服务,知识图谱和向量索引存在本地文件里:

rag_storage/
  ├── graph_chunk_entity_relation.graphml   # 知识图谱
  ├── vdb_chunks.json                        # 文档向量
  ├── vdb_entities.json                      # 实体向量
  └── vdb_relationships.json                 # 关系向量

初始化代码:

from lightrag import LightRAG, QueryParam
from lightrag.utils import EmbeddingFunc
 
rag = LightRAG(
    working_dir="./rag_storage",
    llm_model_func=llm_func,
    embedding_func=EmbeddingFunc(
        embedding_dim=1024,
        max_token_size=8192,
        func=embed_func,
    ),
)
await rag.initialize_storages()   # v1.5.x 新增,必须调用
await rag.ainsert(document_text)
result = await rag.aquery(question, param=QueryParam(mode="mix"))

LightRAG 1.5.x 有一个新要求:必须先调 initialize_storages(),否则会报 PipelineNotInitializedError

QAnything:5 个 Docker 服务

QAnything v2 需要整套基础设施:

services:
  elasticsearch       # 关键词检索(BM25)
  etcd                # Milvus 的元数据存储
  minio               # Milvus 的对象存储
  milvus-standalone   # 向量数据库
  mysql               # 文档和知识库元数据
  qanything_local     # 主服务(embedding + rerank + API)

启动命令:

cd QAnything
mkdir -p volumes/es/data && chmod 777 -R volumes/es/data
docker compose -f docker-compose-linux.yaml up -d

等日志出现 "qanything后端服务已就绪!" 后,访问 http://localhost:8777/qanything/


踩坑记录

部署过程踩了三个坑,每个都值得记录。

坑 1:QAnything 容器默认不用 GPU

机器有 RTX 3060,但 QAnything 容器启动日志始终显示:

embedding和rerank服务将在CPU上运行

原因:docker-compose-linux.yaml 里的 qanything_local 服务没有配置 GPU 资源,而且 scripts/entrypoint.sh 里那行日志是硬编码的字符串,不是实际判断结果,即使挂了 GPU 也照样打印。

修复一:给 compose 文件加 GPU 挂载:

# docker-compose-linux.yaml
qanything_local:
  deploy:
    resources:
      reservations:
        devices:
          - driver: nvidia
            device_ids: ['0']
            capabilities: [gpu]

修复二:同时还需要安装 NVIDIA Container Toolkit(Docker 默认看不到宿主机 GPU,需要这个桥):

# Ubuntu
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

修复三entrypoint.sh 启动 embedding/rerank 时没传 --use_gpu 参数:

# scripts/entrypoint.sh,修改前
nohup python3 -u qanything_kernel/dependent_server/embedding_server/embedding_server.py > ...
 
# 修改后
nohup python3 -u qanything_kernel/dependent_server/embedding_server/embedding_server.py --use_gpu > ...
nohup python3 -u qanything_kernel/dependent_server/rerank_server/rerank_server.py --use_gpu > ...

修复后,GPU 显存占用从 1.4GB 跳到 8.6GB,embedding 速度从每秒不到 1 个文档提升到约每 15 秒处理 1-2 个。

坑 2:Milvus 因内存压力崩溃

CPU 模式下 QAnything 容器吃了 27GB 内存,导致 Milvus standalone 的 etcd lease 超时崩溃退出:

"etcdserver: requested lease not found"
"connection lost detected, shuting down"

结果:所有文件卡在 gray(等待向量化)状态,永远不会完成。

修复:清理 Milvus 和 etcd 的持久化数据目录后重启(etcd 里有损坏的 session 记录,不清会反复崩溃):

docker compose -f docker-compose-linux.yaml down
rm -rf volumes/milvus volumes/etcd volumes/mysql
mkdir -p volumes/milvus volumes/etcd volumes/mysql
docker compose -f docker-compose-linux.yaml up -d

坑 3:user_id 拼接导致 Web 端看不到数据

QAnything 服务端对每个 API 请求的 user_id 会做拼接:

# handler.py
user_info = safe_get(req, 'user_info', "1234")   # 默认值 "1234"
user_id = user_id + '__' + user_info              # 最终存储的 user_id

如果脚本传 user_id=zzp__1234,实际存储的是 zzp__1234__1234,而 Web 端的用户是 zzp__1234,两者不同,Web 看不到 API 创建的知识库。

修复:脚本传 user_id=zzp,服务端拼接后变成 zzp__1234,和 Web 端一致。


评测配置

两个框架使用完全相同的 LLM 和测试集:

配置项LightRAGQAnything
LLMGLM-4-flashGLM-4-flash
EmbeddingBGE-large-en-v1.5(SiliconFlow)QAnything 内置(BCE embedding,GPU 推理)
测试集89 道题(50 单跳 + 20 多跳 + 19 边界)同上
查询模式mix(知识图谱 + 向量融合)混合检索(BM25 + 向量 + Rerank)
文档数量31 个 Markdown 文档31 个 Markdown 文档

评测结果

核心指标对比

指标LightRAG 1.5.6QAnything v2
边界拒答率10.5%(2/19)26.3%(5/19)
平均延迟14,674 ms40,519 ms
P90 延迟19,430 ms52,233 ms
单跳答案匹配(Jaccard)0.0820.111
多跳答案匹配(Jaccard)0.1780.162

注:答案匹配率用 Jaccard 关键词重叠计算,不是 LLM judge。两个框架的答案通常比 ground_truth 更长(加了解释),Jaccard 值偏低是正常的,用于横向对比有效,绝对值没有参考意义。

答案质量

看同一道题的真实输出:

单跳题What is the condition under which query/document asymmetric embedding is enabled?

Ground Truth: enabled only when EMBEDDING_ASYMMETRIC=true is explicitly set
 
LightRAG: Query/document asymmetric embedding in LightRAG is enabled only when
          the `EMBEDDING_ASYMMETRIC` setting is explicitly set to true...
          [直接答出条件,简洁]
 
QAnything: ## Inferred Answer Section
           According to the reference information, query/document asymmetric
           embedding in LightRAG is enabled only when...
           [答案正确,但格式冗余,带了 Markdown 标题]

两个都答对了,但 LightRAG 输出更干净,QAnything 的系统 prompt 会让回答带上 ## Inferred Answer Section 这类结构化标题。

边界题How does the RAG system handle data privacy for users in the EU under GDPR?

LightRAG: The Retrieval-Augmented Generation (RAG) system, as implemented in
          LightRAG, handles data privacy for users in the EU under GDPR...
          [没有拒答,用自身知识编造了一个听起来合理的答案]
 
QAnything: 抱歉,检索到的参考信息并未提供任何相关的信息,因此无法回答。
           [正确拒答]

边界拒答是 QAnything 明显强的一个维度。QAnything 的系统 prompt 里有明确的"参考信息无关时必须拒答"规则,LightRAG 的 mix 模式会优先召回知识图谱里的相关实体,即使文档里没有答案也会尝试推理,容易产生幻觉。

延迟分析

LightRAG 的延迟分布:P50=13.9s,P90=19.4s,min=8.7s,max=30.8s

QAnything 的延迟分布:P50=39.2s,P90=52.2s,min=19.6s,max=62.9s

QAnything 的延迟高有两个原因:

  1. Rerank 步骤:每次查询都要对召回结果做交叉编码重排,这是额外的模型推理
  2. LLM 调用更稳定:QAnything 每次都能召回到真实文档(source_count=100%),LLM 要处理的上下文更长

LightRAG 的 mix 模式会构建一次知识图谱查询 + 一次向量查询,然后融合结果,LLM 调用通常比 QAnything 更快,但如果图谱遍历范围大也会慢。


选哪个?

基于这次评测,给一个简单的决策参考:

优先选 LightRAG,如果:

  • 需要快速验证 RAG 方案,不想花时间配置基础设施
  • 团队没有运维 Milvus/ES/MySQL 的能力
  • 文档之间有复杂的关联关系,需要图谱多跳推理
  • 对延迟敏感(LightRAG P90 比 QAnything 快约 2.7 倍)

优先选 QAnything,如果:

  • 需要更强的拒答能力(边界拒答率高出 2.5 倍)
  • 有中文文档,需要中文优化的 embedding(BCE embedding 对中文效果更好)
  • 需要 Web 界面让非技术人员上传文档
  • 生产环境,需要 ES 全文检索 + 向量检索的混合能力

这次评测没测到的

  • 大规模文档(1 万+ 文档)下的性能
  • 中文文档的检索质量(本次测试集全英文)
  • 知识库更新的速度和稳定性
  • QAnything 的 PDF/图表解析能力(本次只用了 Markdown)

这些会在后续文章里补充。


评测代码

完整代码在 llm-in-action/kb-02-lightrag-eval/llm-in-action/kb-02-qanything-eval/

LightRAG 评测核心流程:

# 建库
rag = LightRAG(working_dir=STORAGE_DIR, llm_model_func=llm_func,
               embedding_func=EmbeddingFunc(embedding_dim=1024, func=embed_func))
await rag.initialize_storages()
await rag.ainsert(doc_content)
 
# 查询
answer = await rag.aquery(question, param=QueryParam(mode="mix"))

QAnything 评测核心流程:

# 建库
kb_id = api_post("new_knowledge_base", {"user_id": USER_ID, "kb_name": KB_NAME})["data"]["kb_id"]
api_post("upload_files", data={"user_id": USER_ID, "kb_id": kb_id}, files={"files": fp})
 
# 等待向量化完成(轮询 status=green)
while any(s != "green" for s in status_count if s != "green"):
    time.sleep(15)
 
# 查询
result = api_post("local_doc_chat", {
    "user_id": USER_ID, "kb_ids": [kb_id], "question": question,
    "model": LLM_MODEL, "api_base": LLM_BASE_URL, "api_key": LLM_API_KEY,
    "streaming": False
})

下一篇:GraphRAG vs HippoRAG——图增强 RAG 的多跳推理测试。同样 89 道题,重点看多跳推理的提升幅度,以及知识图谱构建的时间和成本。


欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页