首页 / 文章

RAG-Anything在65本葡萄酒书籍上的应用:图检索的优劣势所在。

对65份葡萄酒相关PDF文件使用RAG-Anything知识图谱进行的实际测试:解析器选择、成本分析、图结构答案与向量答案的对比,以及起泡酒领域的缺陷。

发布者:REACTAPP.TOP阅读约 12 分钟2831 词

来源: I Tested RAG-Anything on 65 Wine Books. Here’s What a Knowledge Graph Gets Right — and What It Misses.

判断一个框架的最佳方式是使用它实际构建一些东西,测试其极限并观察会发生什么,而非依赖基准测试或README文件。RAG-Anything正是以这种方式被测试的。它是一个开源的香港大学项目,宣称能够从多模态文档中自动生成知识图谱,而测试材料则来自一位参加过WSET课程与考试、并对葡萄酒领域感兴趣的人所收集的个人葡萄酒书籍库。

为何葡萄酒是理想的压力测试对象

葡萄酒领域本质上是一个充满关联性的领域。单宁决定了葡萄酒的陈年潜力,酸度与单宁相互影响,而酵母则推动发酵过程从而生成各种风味物质。葡萄与特定产区相关,产区与气候条件相关,气候又决定了葡萄酒的风格。如果结构化的实体关系抽取能在任何场景下超越传统向量搜索,那么在葡萄酒领域更应如此。需要验证的问题是:RAG-Anything能否从真实的PDF书籍中构建出有用的知识图谱,并且其回答葡萄酒相关问题的能力是否优于简单的向量搜索?

从技术实现上看,RAG-Anything是基于LightRAG开发的。它还具备多模态解析功能(可处理PDF、DOCX及图片),能够自动将实体与关系提取并构建成图结构,同时提供五种查询模式,在图遍历与向量检索之间实现平衡。所使用的解析器包括MinerU、Docling和PaddleOCR,支持多种大语言模型后端,表格、图片和公式也能在数据导入时被正确处理。该项目在GitHub上获得了超过15,000个星标,确实受到了不少关注,不过星标并非衡量其性能的首要指标。接下来将展示实际测试结果、可定制的代码,以及说明在何种情况下图结构RAG能发挥优势,而在何种情况下则不然。

架构

整个系统分为数据导入与查询两个阶段。在数据导入阶段,PDF文件会依次经过一系列处理步骤,首先由文档解析器进行处理,这些解析器包括基于VLM的MinerU,或是更为轻量且成本更低的Docling:

PDF Document
    |
    v
[Document Parser]  ─── MinerU (VLM-based) or Docling (lighter/cheaper)
    |
    v
[Content Extraction]  ─── text, tables, images, equations
    |
    v
[Text Chunking]  ─── split into manageable pieces
    |
    v
[LLM Entity/Relation Extraction]  ─── LLM extracts entities + relationships
    |
    ├──> [Knowledge Graph]  ─── entities as nodes, relations as edges (GraphML)
    └──> [Vector Embeddings]  ─── chunks embedded for similarity search (JSON)

大型语言模型会读取每个文本片段,提取出诸如“赤霞珠”、“苹果酸乳酸发酵”和“波尔多”之类的实体,以及它们之间的关联:赤霞珠生长在波尔多,而苹果酸乳酸发酵可以降低酸度。这些结果会被存储为持久性GraphML图中的节点和边。

查询功能提供五种模式,每种模式都有各自的检索策略。下表列出了每种模式的搜索内容、适用场景以及不足之处:

+--------+------------------------------+-------------------------------+---------------------------------+
| Mode   | What It Searches             | Best For                      | Weakness                        |
+--------+------------------------------+-------------------------------+---------------------------------+
| naive  | Vector similarity only       | Factoid questions, robustness | Misses relational structure     |
| local  | Graph neighborhood traversal | Entity-specific deep dives    | Blind to entities not extracted |
| global | Community-level summaries    | Broad thematic questions      | Less specific, slower           |
| hybrid | local + global               | Balanced depth and breadth    | No vector fallback              |
| mix    | Graph + vector together      | General-purpose (recommended) | Slowest mode                    |
+--------+------------------------------+-------------------------------+---------------------------------+

在模型方面,GPT-4o-mini负责文本生成,这使得在实体提取过程中需要进行数百次大语言模型调用依然成本可控。text-embedding-3-small用于生成嵌入向量,同时为控制成本而关闭了图像处理功能:

from openai import AsyncOpenAI
from lightrag.utils import EmbeddingFunc
from raganything import RAGAnythingConfig

aclient = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# Text LLM - handles entity extraction and answer synthesis
async def llm_model_func(prompt, system_prompt=None, history_messages=None, **kwargs):
    messages = []
    if system_prompt:
        messages.append({"role": "system", "content": system_prompt})
    if history_messages:
        messages.extend(history_messages)
    messages.append({"role": "user", "content": prompt})
    response = await aclient.chat.completions.create(
        model="gpt-4o-mini", messages=messages, temperature=0.0,
    )
    return response.choices[0].message.content

# Embeddings - 1,536 dimensions, 8K token window
async def _embed_texts(texts, **kwargs):
    response = await aclient.embeddings.create(model="text-embedding-3-small", input=texts)
    return np.array([item.embedding for item in response.data])

embedding_func = EmbeddingFunc(embedding_dim=1536, max_token_size=8192, func=_embed_texts)

# Configuration - Docling parser, tables enabled, images disabled for cost
rag_config = RAGAnythingConfig(
    working_dir="./rag_storage",
    parser="docling",
    enable_image_processing=False,    # skipping images - valid for my use-case
    enable_table_processing=True,
    enable_equation_processing=True,
)

成本是一个现实的问题。所有书籍的所有页面都需要被解析、分块,然后通过GPT-4o-mini进行信息提取,而若要支持图片处理,还需在原有基础上增加GPT-4o的视觉处理调用。在本次实验中,文本和表格就已足够。

语料库与解析器

该语料库由65份PDF文件组成,旨在涵盖不同视角的内容。WSET系列书籍是结构化的教学材料,包含表格和技术术语,而其他书籍则采用轻松的叙述风格,由清单式建议或关于葡萄酒酿造的故事构成。这样的组合旨在测试该模型是否能够跨越不同的写作风格和复杂度水平进行信息整合。

默认解析器MinerU依赖视觉语言模型来读取页面布局。虽然准确度很高,但速度较慢。首次使用时需要约7分钟下载该视觉语言模型,之后每处理一页大约需要15秒的CPU时间,因此一本50页的书籍在开始提取之前就需要大约20分钟进行解析。而300页的书籍即便被分割成50页一组,也会因OCR超时而导致程序崩溃。

Docling是一种更轻量级的解析器,无需视觉语言模型,处理同一本书时没有出现崩溃,整个提取流程也能顺利完成,因此被用于处理其余的书籍。实际经验表明,MinerU能提供更丰富的页面布局理解能力,比如更准确地识别图表以及区分标题与正文,而Docling则是仅依赖CPU环境下的实际可行选择。目前计划在当地GPU上重新运行该程序。

数据导入代码非常简单,每处理一份文档只需一次调用即可:

from raganything import RAGAnything

rag = RAGAnything(
    config=rag_config,
    llm_model_func=llm_model_func,
    embedding_func=embedding_func,
)

for pdf_path in sorted(Path("./data").glob("*.pdf")):
    file_start = time.time()
    await rag.process_document_complete(
        file_path=str(pdf_path),
        output_dir="./output",
    )
    print(f"Completed {pdf_path.name} in {time.time() - file_start:.1f}s")

await rag.finalize_storages()

该框架在内部负责解析、分块、实体提取、图构建以及向量嵌入等工作。其API设计极为精良,甚至有些过于简化了。

所有书籍都在6小时内被提取完成,平均每分钟处理30页。解析并非瓶颈,真正的瓶颈在于数百次用于实体提取的LLM调用,因为每个分块都需要经过GPT-4o-mini来识别实体与关系。即便如此,不包括图片在内的LLM使用费用仍然很低,整个实验包括评估过程的总费用约为5美元。数据导入后的表格总结了该图的结构规模:

+----------------------------+------------------------------+
| Metric                     | Value                        |
+----------------------------+------------------------------+
| Entities (graph nodes)     | 37,132                       |
| Relations (graph edges)    | 47,650                       |
| Text chunks                | 6,247                        |
| Storage on disk            | ~185 MB (all JSON + GraphML) |
| Indexed documents          | 65                           |
| Load time at query startup | ~10 seconds                  |
+----------------------------+------------------------------+

无需使用外部数据库。图结构存储在GraphML文件中,向量存储及键值数据则保存在JSON文件中。整个知识库可完整地放入一个Docker镜像中,无需任何辅助基础设施。

图模型与向量模型

核心实验通过两种模式发送了6个查询,分别是local模式(仅进行图遍历)和naive模式(仅基于向量相似度),随后对比了两种模式的查询结果。这些查询旨在测试关系推理、比较推理、因果推理以及跨文档推理能力。

async def compare_modes(rag, query):
    """Compare different retrieval modes on the same query."""
    for mode in ["local", "naive"]:
        result = await rag.aquery(query, mode=mode)
        print(f"[{mode}] {len(result)} chars, {elapsed:.1f}s")

下方列出了每个查询的结果及耗时。

+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| Query                                             | Graph (local)        | Vector (naive)      | Winner                        |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+
| How does soil type influence wine character?      | 32.5s / 3,464 chars  | 28.2s / 2,531 chars | Tie                           |
| Relationship between tannins, acidity, and aging? | 24.8s / 2,266 chars  | 25.8s / 2,289 chars | Graph (structure)             |
| Compare red vs. white winemaking                  | 32.9s / 3,119 chars  | 42.0s / 3,423 chars | Graph (speed + structure).    |
| What role does yeast play in fermentation?        | 27.8s / 2,587 chars  | 26.8s / 2,403 chars | Tie                           |
| How do fortified wines differ from table wines?   | 28.5s / 2,635 chars  | 23.1s / 2,597 chars | Tie                           |
| Sparkling wine production methods?                | 10.7s / 48 chars     | 48.4s / 2,900 chars | Vector (graph fails)          |
+---------------------------------------------------+----------------------+---------------------+-------------------------------+

单宁、酸度与陈年潜力。这是一个经典的相关性问题。该图通过明确的边将单宁与陈年潜力相连,将酸度与新鲜度相连,同时将两者都与酒质相连。沿着这些边可以系统地了解单宁与酸度之间的相互作用,而这在传统方法中意味着需要整合源文本中可能相距甚远的各个部分。

红葡萄酒与白葡萄酒的酿造过程。该图通过实体链接展现了二者之间的差异:红葡萄酒和白葡萄酒的酿造过程分别通过不同的关系边与共同的工艺实体(发酵、压榨、陈酿)相连。它仅需32.9秒即可得出答案,而向量搜索则需要42.0秒,且结果更为有条理。

强化酒与普通葡萄酒。这个问题需要整合多本书籍中的内容,该图则能够平滑地连接不同文档之间的实体。

对于一些简单的常识性问题,比如土壤如何影响葡萄酒的风味,或是酵母在发酵中的作用,这两种模式给出的答案相当接近。由于相关信息直接存储在片段中,向量相似度搜索能像遍历搜索一样高效地找到对应内容。

最关键的失败案例

在关于起泡酒生产方法的问题上,图模式回答称没有足够的信息。该回复仅有48个字符,生成耗时10.7秒。而向量模式则在48.4秒内给出了2,900个字符的详细答案,涵盖了传统法、夏玛法、转罐法和传统法等多种生产方式。

实体抽取未能捕捉到那些关键的概念。“methode champenoise”、“tank method”和“riddling”这些术语都没有对应的节点,因此针对起泡酒生产过程的图查询根本找不到可遍历的内容。该图结构对这一主题完全不知情。向量搜索则只是找出包含这些术语的文本片段并将其传递给大语言模型,由于它是基于原始文本而非经过整理的实体集进行处理的,因此能够容忍抽取失败的情况。

其基本局限在于:当抽取过程遗漏某个概念时,图查询就无法识别它,因此图结构的完整性完全取决于抽取流程的质量。向量搜索则不存在此类故障模式。正因如此才有了mix模式,也解释了为何仅使用local模式是个糟糕的选择。

时间表现也值得关注。图查询的时间从10.7秒(即未能快速找到结果的失败情况)上升至32.9秒,而向量查询的时间则从23.1秒上升至48.4秒。在这两种查询方式中,都是大语言模型生成内容起主导作用,而非信息检索功能;此外,缓存机制使得针对相似主题的重复查询速度大幅提升。

查看图结构

为查看图结构本身,使用NetworkX和pyvis构建的交互式可视化工具展示了120个连接度最高的节点实体:

import networkx as nx
from pyvis.network import Network

G = nx.read_graphml("rag_storage/graph_chunk_entity_relation.graphml")

# Focus on the most connected nodes (hubs)
degree_dict = dict(G.degree())
top_nodes = sorted(degree_dict, key=degree_dict.get, reverse=True)[:120]
subG = G.subgraph(top_nodes).copy()

# Categorize nodes by wine domain keywords
def categorize(name):
    lower = name.lower()
    if any(k in lower for k in ["cabernet", "merlot", "pinot", "riesling", ...]):
        return "grape"       # Red nodes
    if any(k in lower for k in ["bordeaux", "california", "champagne", ...]):
        return "region"      # Blue nodes
    if any(k in lower for k in ["fermentation", "aging", "maceration", ...]):
        return "process"     # Green nodes
    if any(k in lower for k in ["port", "sherry", "sparkling", ...]):
        return "wine_type"   # Orange nodes
    return "general"         # Purple nodes

该图表按类别为节点着色(葡萄为红色,地区为蓝色,流程为绿色,葡萄酒类型为橙色,通用概念为紫色),并根据连接数量调整节点大小。

连接度最高的15个实体体现了图表所捕捉到的核心内容。作为通用实体的“葡萄酒”以2,136条连接位居首位:

+--------------------+-------------+-----------+
| Entity             | Connections | Category  |
+--------------------+-------------+-----------+
| Wine               | 2,136       | General   |
| Wine Production    | 1,344       | General   |
| Italian Wines      | 768         | General   |
| Bordeaux           | 442         | Region    |
| Cabernet Sauvignon | 420         | Grape     |
| Riesling           | 412         | Grape     |
| Champagne          | 348         | Region    |
| California         | 321         | Region    |
| Grapes             | 287         | Grape     |
| Port               | 243         | Wine Type |
+--------------------+-------------+-----------+

可见的模式包括密集的发酵工艺群、与不同地区相关的葡萄品种,以及由波特酒、雪利酒、马德拉酒等强化酒类构成的子群及其各自的生产节点。稀疏的区域同样具有启示意义:起泡酒相关的概念表现不足,这解释了为何查询会失败。此外,该数据库中关于起泡酒的书籍也很少,因此这一差距可能部分源于数据本身。

小型演示前端

为了便于分享及进行非技术层面的演示,可将查询界面封装在 Gradio 中:

import gradio as gr


def query_wine(question, mode):
    t0 = time.time()
    result = _run_async(_query(question, mode))
    elapsed = time.time() - t0
    return result, f"**Mode:** {mode} | **Time:** {elapsed:.1f}s"

with gr.Blocks(title="Wine Knowledge RAG") as demo:
    question = gr.Textbox(label="Ask a wine question", lines=2)
    mode = gr.Radio(
        choices=["mix", "local", "global", "hybrid", "naive"],
        value="mix", label="Retrieval Mode",
    )
    submit_btn = gr.Button("Ask", variant="primary")
    stats = gr.Markdown("")
    answer = gr.Markdown(label="Answer")
    submit_btn.click(fn=query_wine, inputs=[question, mode], outputs=[answer, stats])

该应用大约在20秒内即可加载预构建的图结构,且进行查询时无需GPU,因为所有计算都是通过向OpenAI发起API调用来完成的。

实验结果

Graph RAG在三种类型的查询中明显提升了答案质量:

  • 关系型问题,即询问某事物如何影响另一事物:实体之间的边表明答案无需依赖两者共享同一文本片段。单宁、酸度与陈年之间的关系分析就展现了显著优势。
  • 两件事物之间的比较:对比关系能够自然组织起来。红葡萄酒和白葡萄酒的酿造过程通过不同的关系边复用了相关节点,从而得到了条理清晰的并列答案。
  • 多本书籍的综合分析:向量搜索也能处理此类任务,但图结构能够更清晰地展现各元素之间的联系,且不依赖于嵌入向量彼此靠近。

该技术也存在三个明显缺陷:

  • 实体抽取是薄弱环节。原始文本中存在起泡酒相关的信息,但却未能转化为图结构中的实体,这是所有基于抽取的方法所共有的局限。
  • 事实查询并不需要图结构。诸如“葡萄酒应存放在多少温度下”这类问题用向量搜索即可解决,而引入图结构只会增加不必要的开销,没有任何好处。
  • 成本与延迟问题是实际存在的。数据摄取过程需要调用数百次大语言模型,而25到30秒的查询延迟主要源于大语言模型的生成过程。

决策矩阵将查询类型与三种方案相对应:向量搜索(简单型)、图搜索(局部型)以及混合型,其中推荐使用混合型方案:

+----------------------------+----------------+---------------+-------------------+
| Query Type                 | Vector (naive) | Graph (local) | Mix (recommended) |
+----------------------------+----------------+---------------+-------------------+
| Factoid lookup             | Good           | Good          | Good              |
| Relational ("X affects Y") | OK             | Best          | Best              |
| Comparative ("A vs B")     | OK             | Best          | Best              |
| Cross-document synthesis   | OK             | Good          | Best              |
| Topic with extraction gaps | Best           | Fails         | Good              |
+----------------------------+----------------+---------------+-------------------+

四个实用建议:

  1. 每次都使用 mix 模式。不应依赖仅基于图结构的检索方式,缺乏的全面覆盖性正是其原因。
  2. 在仅使用 CPU 的环境中,建议选择 Docling 而非 MinerU,因为 MinerU 的 VLM 虽然功能全面但稳定性较差。
  3. 为数据导入过程预留足够时间,尤其是处理大型语料库时。
  4. 检查图结构。确认该领域的核心概念确实以实体形式呈现,并在依赖该图结构之前评估其结构指标。

结论

RAG-Anything实现了其核心承诺。它能将PDF中的结构化知识提取为图结构,从而显著提升关系查询和对比查询的效率:通过遍历该图结构,关于单宁酸度与陈年关系的回答更加条理清晰;红葡萄酒与白葡萄酒的对比结果也能更快且更整齐地呈现出来;此外,来自不同出版商的书籍之间的信息整合也得以实现。虽然前景广阔,但该技术尚处于发展初期,解析器的可靠性及信息提取的全面性仍是需要关注的方面。任何考虑将其用于实际应用的人都应该在自己的数据上重复这项实验,因为README文件无法显示其存在的缺陷。

相关阅读

  • 实用笔记:我基于SEC文件构建了混合式RAG管道——详情如下 — 该实用笔记的详细操作指南:包括合同、检查项以及供采用此模式的团队直接使用的代码模板。
  • 您的API是否已准备好支持AI智能体?实用MCP准备清单 — 是什么让一个可运行的API变成智能体能够可靠使用的工具:文档、架构、认证机制、端点选择、描述信息、测试流程、托管方案以及版本控制。
  • 将MCP服务器迁移到2026-07-28版本规范:二十分钟,分步指南 — 无需中断服务即可完成迁移。了解MCP 2026-07-28版本的变更内容、Python SDK 2.0的五步迁移流程、MRTR的重新编写方式,以及“已弃用”这一概念的含义。
  • TypeScript 7与Go语言端口:更快的构建速度、更优的内存管理及编辑器反馈 — TypeScript 7采用原生Go引擎替代原有编译器。该版本支持并行构建、降低内存占用、提供新的语言服务器,同时还有一些需要提前了解的迁移注意事项。
  • 四种多智能体架构对比:子智能体、技能、任务交接与路由器 — 对四种核心多智能体架构进行实际对比,分析每种架构的代价、适用场景,以及如何选择以避免功能冗余。