高效PDF处理:Gemini 2.0 Flash提取与RAG

2025年02月17日 由 alex 发表 1162 0

想象一下:你首先将每个 PDF 页面转换成图像,然后发送出去进行 OCR(光学字符识别),接着再把原始文本费力地转换成可用的 HTML 或 Markdown。接下来,你仔细地检测和重建每个表格,将内容切分成块以便进行语义检索,最后将它们全部插入到向量数据库中。这已经是一个庞大的处理流程——通常需要花费大量成本来整合多个 ML/OCR 模型。


但是,如果只用一个大型语言模型——谷歌的 Gemini 2.0 Flash——就能简化整个流程呢?想象一下,在极低的成本下,只需一步就能完成 OCR 和内容切分。本文正是探讨了这种可能性。我们将展示 Gemini 2.0 Flash 如何一步到位地将 PDF 转换成已分块的、准备好用于 Markdown 的文本,让你摆脱以往繁琐的多步操作。然后,我们将把这些文本块存储在 KDB.AI 中,以便进行快速向量搜索,将这一切编织成一个比你以往见过的更优雅、更经济实惠的检索增强生成(RAG)工作流。


在此过程中,我们将突出来自 Hacker News 讨论的实际反馈,以及引用Sergey Filimonov的文章,他首次以接近完美的准确度测量了每美元约处理 6,000 页的速度。


11


关键要点:如果你不需要原始 PDF 中的边框框选(bounding boxes),这种方法比传统的 OCR 处理流程要简单得多且成本更低。


传统的 PDF 摄取问题

为什么 PDF 摄取这么难?

  1. 复杂布局:多栏文本、脚注、侧边栏、图片或扫描表单。
  2. 表格提取:传统的 OCR 工具通常会将表格展平为杂乱的文本。
  3. 高成本:使用 GPT-4 或其他大型语言模型(LLM)很快就会变得昂贵,尤其是当你需要处理数百万页时。
  4. 多种工具:你可能需要运行 Tesseract 进行 OCR,使用布局模型进行表格检测,为检索增强生成(RAG)采用单独的切分策略,等等。


许多团队最终构建了一个庞大且脆弱且昂贵的处理流程。新的方法是:


“只需将 PDF 页面作为图像展示给多模态语言模型,给它一个用于切分的提示,然后见证奇迹的发生。”


为什么选择 Gemini 2.0 Flash?

根据Sergey Filimonov和多位 Hacker News 评论者的说法:

  • 成本:每美元约处理 6,000 页(通过批量调用和最小化输出令牌)。这比其他许多解决方案(如 GPT-4、专业的 OCR 供应商等)便宜 5 到 30 倍。
  • 准确性:在标准文本上表现出惊人的保真度。大多数错误只是轻微的结构差异,尤其是在表格方面。


缺失的一大块是边框框选数据。如果你需要将内容精确地叠加回 PDF 上,Gemini 的边框框选生成还远远不够准确。但如果你的主要关注点是基于文本的检索或摘要,那么它更便宜、更快、更容易。


端到端架构

我们将在代码中执行以下操作:


12


  1. 将 PDF 页面转换为图像(使用 pdf2image)。
  2. 将图像与用于切分的提示一起发送到 Gemini 2.0 Flash。
  3. 提取块标签 <chunk>...</chunk>。
  4. 使用通用的嵌入模型对这些块进行嵌入。
  5. 存储在 KDB.AI 中以便搜索。
  6. 在查询时,检索相关块并将它们提供给大型语言模型(LLM)以获取最终答案。


下面,我们将分节逐步讲解代码,每一步都进行解释。


分步代码


安装依赖项并创建基本表

首先,我们安装所有所需的 Python 包:

  • google-generativeai:Gemini 的 Python 客户端。
  • kdbai-client:与 KDB.AI 进行交互。
  • sentence-transformers:用于嵌入。
  • pdf2image:将 PDF 页面转换为 PNG 图像。
  • 另外,还需要在系统级别安装 poppler-utils 以支持 PDF。


要获取你的 KDB.AI 凭据,请前往 KDB.AI 并登录。免费的云服务提供 4GB 内存和 30GB 磁盘空间,如果正确量化,足以存储数百万个向量。


# SNIPPET 1: Installing packages & setting up
!apt-get update
!apt-get install -y poppler-utils
!pip install -q google-generativeai kdbai-client sentence-transformers pdf2image
import os
import kdbai_client as kdbai
from sentence_transformers import SentenceTransformer
# We'll connect to KDB.AI to store our chunk embeddings
KDBAI_ENDPOINT = "YOUR_KDBAI_ENDPOINT"
KDBAI_API_KEY = "YOUR_KDBAI_API_KEY"
session = kdbai.Session(endpoint=KDBAI_ENDPOINT, api_key=KDBAI_API_KEY)
db = session.database('default')
print("Connected to KDB.AI:", db)


安装完成后,我们创建一个会话对象以与我们的 KDB.AI 实例进行通信。


创建向量表

我们将为块文本和嵌入定义一个简单的模式。KDB.AI 支持对“vectors”列进行索引以进行相似度搜索。


# SNIPPET 2: Define KDB.AI table schema
VECTOR_DIM = 384  # we'll use all-MiniLM-L6-v2 for embeddings
schema = [
    {"name": "id", "type": "str"},
    {"name": "text", "type": "str"},
    {"name": "vectors", "type": "float32s"}
]
# Build a simple L2 distance index
index = [
    {
        "name": "flat_index",
        "type": "flat",
        "column": "vectors",
        "params": {"dims": VECTOR_DIM, "metric": "L2"}
    }
]
table_name = "pdf_chunks"
try:
    db.table(table_name).drop()
except kdbai.KDBAIException:
    pass
table = db.create_table(table_name, schema=schema, indexes=index)
print(f"Table '{table_name}' created.")


解释:

  • 我们为每个块存储一个“id”、 “text”以及用于嵌入的“vectors”。
  • 该表使用具有 L2 距离的“平面”索引。在生产环境中,如果你希望更快的近似最近邻查询,可能会切换到 HNSW。


将 PDF 页面转换为图像

Gemini 是一个多模态模型,因此我们可以直接输入图像。这意味着我们首先使用 pdf2image 将每个 PDF 页面转换为 PNG 格式。


# SNIPPET 3: Convert PDF to images
import requests
from pdf2image import convert_from_bytes
import base64
import io
pdf_url = "https://arxiv.org/pdf/2404.08865"  # example PDF
resp = requests.get(pdf_url)
pdf_data = resp.content
pages = convert_from_bytes(pdf_data)
print(f"Converted {len(pages)} PDF pages to images.")
# We'll encode the images as base64 for easy sending to Gemini
images_b64 = {}
for i, page in enumerate(pages, start=1):
    buffer = io.BytesIO()
    page.save(buffer, format="PNG")
    image_data = buffer.getvalue()
    b64_str = base64.b64encode(image_data).decode("utf-8")
    images_b64[i] = b64_str


解释:

  • convert_from_bytes 一次性处理所有页面。
  • 我们将每个图像的原始 PNG 数据存储为 base64 字符串,这样便于将其传递给 Gemini 的 API。


调用 Gemini 2.0 Flash 进行 OCR+Chunking

让我们初始化 Gemini 客户端,并定义一个提示,指示模型执行以下操作:

  1. “将页面 OCR 成 Markdown 格式。”
  2. “将其拆分为 250 到 1,000 字的段落。”
  3. “用 <chunk> … </chunk> 包围这些段落。”


# SNIPPET 4: Configure Gemini & define chunking prompt
import google.generativeai as genai
GOOGLE_API_KEY = "YOUR_GOOGLE_API_KEY"
genai.configure(api_key=GOOGLE_API_KEY)
model = genai.GenerativeModel(model_name="gemini-2.0-flash")
print("Gemini model loaded:", model)
CHUNKING_PROMPT = """\
OCR the following page into Markdown. Tables should be formatted as HTML.
Do not surround your output with triple backticks.
Chunk the document into sections of roughly 250 - 1000 words.
Surround each chunk with <chunk> and </chunk> tags.
Preserve as much content as possible, including headings, tables, etc.
"""


解释:

  • 我们加载 gemini-2.0-flash 作为模型。如果你想尝试更大或更小的版本,可以分别选择 pro 或 flash 变体。
  • 提示是经过仔细编写的,以便模型输出我们可以轻松解析的块分隔符。
  • 这里我们处理一个 PDF,但通过使用异步调用 Gemini,可以轻松扩展到数百万个。


用一个提示处理每一页

我们将定义一个辅助函数 process_page(page_num, b64),该函数将 base64 编码的 PNG 图像和提示发送到 Gemini。然后,我们将从响应中解析出 <chunk> 块。


# SNIPPET 5: OCR + chunking function
import re
def process_page(page_num, image_b64):
    # We'll create the message payload:
    payload = [
        {
            "inline_data": {"data": image_b64, "mime_type": "image/png"}
        },
        {
            "text": CHUNKING_PROMPT
        }
    ]
    try:
        resp = model.generate_content(payload)
        text_out = resp.text
    except Exception as e:
        print(f"Error processing page {page_num}: {e}")
        return []
    # parse <chunk> blocks
    chunks = re.findall(r"<chunk>(.*?)</chunk>", text_out, re.DOTALL)
    if not chunks:
        # fallback if model doesn't produce chunk tags
        chunks = text_out.split("\n\n")
    results = []
    for idx, chunk_txt in enumerate(chunks):
        # store ID, chunk text
        results.append({
            "id": f"page_{page_num}_chunk_{idx}",
            "text": chunk_txt.strip()
        })
    return results
all_chunks = []
for i, b64_str in images_b64.items():
    page_chunks = process_page(i, b64_str)
    all_chunks.extend(page_chunks)
print(f"Total extracted chunks: {len(all_chunks)}")


解释:

  1. inline_data:告诉 Gemini 我们正在传递一张图像(PNG)。
  2. 我们还添加了文本切分提示。
  3. 模型返回一个大的字符串。我们通过查找 <chunk>...</chunk> 来将它们分开。
  4. 如果没有找到块标签,我们会通过双换行符进行备用拆分。


嵌入块并存储在 KDB.AI 中

现在我们已经有了块文本,让我们使用 all-MiniLM-L6-v2 进行嵌入,并上传到 KDB.AI。虽然这不是这个任务中最好的嵌入模型,但在这个示例中已经足够了。


# SNIPPET 6: Embedding & Insertion
embed_model = SentenceTransformer("all-MiniLM-L6-v2")
chunk_texts = [ch["text"] for ch in all_chunks]
embeddings = embed_model.encode(chunk_texts)
embeddings = embeddings.astype("float32")
import pandas as pd
row_list = []
for idx, ch_data in enumerate(all_chunks):
    row_list.append({
        "id": ch_data["id"],
        "text": ch_data["text"],
        "vectors": embeddings[idx].tolist()
    })
df = pd.DataFrame(row_list)
table.insert(df)
print(f"Inserted {len(df)} chunks into '{table_name}'.")


解释:

  • 嵌入结果是以 (num_chunks, 384) 形状的 numpy.float32 数组形式出现的。
  • 我们将每个向量转换为 Python 列表,并将其放入 DataFrame 中。
  • 然后,我们使用 .insert() 将其插入到 KDB.AI 表中。


此时,每个块都可以在向量空间中进行搜索。如果你快速执行 table.query(),你会看到它们都存储在 KDB 中。


查询与构建 RAG(检索增强生成)流程

我们现在可以嵌入用户查询,获取顶部块,并将它们传递给任何大型语言模型(LLM)以进行最终的问答。


相似度搜索


# SNIPPET 7: Vector query for RAG
user_query = "How does this paper handle multi-column text?"
qvec = embed_model.encode(user_query).astype("float32")
search_results = table.search(vectors={"flat_index": [qvec]}, n=3)
retrieved_chunks = search_results[0]["text"].tolist()
context_for_llm = "\n\n".join(retrieved_chunks)
print("Retrieved chunks:\n", context_for_llm)


解释:

  • table.search() 运行向量相似度搜索。我们获取最相关的前 3 个块。
  • 我们将它们组合成一个字符串,以便进行最终的 LLM 调用。


最终生成

我们将检索到的块作为“上下文”提供给同一个 Gemini 模型(或任何其他大型语言模型),让它生成最终答案:


# SNIPPET 8: RAG generation
final_prompt = f"""Use the following context to answer the question:
Context:
{context_for_llm}
Question: {user_query}
Answer:
"""
resp = model.generate_content(final_prompt)
print("\n=== Gemini's final answer ===")
print(resp.text)


13


解释:

  • 这是标准的检索增强生成(RAG)方法:结合顶部块的“上下文”,并让大型语言模型(LLM)作出回应。
  • 如果你需要特定的推理或思维链方法,可以相应地调整提示。


总结

  1. 用户反馈:Hacker News 上的真实团队已经用 Gemini 替代了专门的 OCR 供应商来处理 PDF 摄入,节省了时间和成本。其他人对于边框框或绝对数值可靠性仍保持谨慎。
  2. 当边框框很重要时:如果你必须精确跟踪 PDF 上每个块的位置,你将需要一种混合方法。(谷歌可能很快就会解决这个问题,但目前还没有。)
  3. 可扩展性:处理数百万页?确保批量调用并限制标记。这是如何达到每美元约 6,000 页的甜蜜点。单页调用或大型输出成本更高。
  4. 简单性:你可以跳过半打微服务或 GPU 管道。对于许多人来说,仅此一点就是巨大的解脱。
  5. 底线:如果你处理的是标准 PDF,并且想将它们输入到向量存储中以进行 RAG,Gemini 2.0 Flash 可能是实现“足够好”文本提取的最快路径——特别是如果你不需要边框框。成本优势可能是巨大的,而且代码非常简单。与一年前相比,这是一个巨大的进步。



文章来源:https://medium.com/ai-advances/10x-cheaper-pdf-processing-ingesting-and-rag-on-millions-of-documents-with-gemini-2-0-flash-8a93dbbb3b54
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消