
完整指南:如何利用gemini 2.0 Flash以10倍更低成本处理百万pdf文档!
- Rifx.Online
- Large Language Models , AI Applications , Case Studies
- 23 Feb, 2025
Image Source: https://meetcody.ai/blog/gemini-1-5-flash-vs-gpt-4o/, edited by author
设想一下: 你开始将每个PDF页面转换为图像,然后发送它们进行光学字符识别,最后将原始文本整理成可用的HTML或Markdown。接下来,你仔细检测并重建每个表格,将内容分块以便进行语义检索,最后将它们全部插入向量数据库。这已经是一个庞大的管道——通常涉及多个机器学习/光学字符识别模型,成本也很高。
但如果 一个单一的大语言模型——谷歌的Gemini 2.0 Flash——能够简化整个过程呢?想象一下在一个步骤中将光学字符识别和分块结合在一起,成本却仅为以前的一小部分。本文正是探讨这个可能性。我们将展示Gemini 2.0 Flash如何一次性将PDF转换为分块、准备好的Markdown文本,让你摆脱通常的多步骤繁琐。然后,我们将在KDB.AI中存储这些块,以便快速进行向量搜索,将这一切编织成比你之前见过的更优雅、更经济的检索增强生成工作流。准备好——这是一个游戏规则改变者。
本指南展示如何:
- 使用Gemini 2.0 Flash直接将PDF页面转换为分块文本。
- 将块存储在KDB.AI中以进行向量搜索。
- 将所有内容结合在一个检索增强生成工作流中。
在此过程中,我们将突出来自Hacker News讨论的真实反馈,以及对Sergey Filimonov的博客的引用,该博客首次以近乎完美的准确性测量了~6,000页/美元。
Image source: https://www.sergey.fyi/articles/gemini-flash-2
关键要点:如果你不需要原始PDF中的边界框,这种方法比旧的光学字符识别管道简单且便宜得多。
2. 传统 PDF 处理问题
为什么 PDF 处理如此困难?
- 复杂的布局:多列文本、脚注、侧边栏、图像或扫描表单。
- 表格提取:传统的光学字符识别工具往往将表格压平为混乱的文本。
- 高成本:使用 GPT-4o 或其他大型大语言模型可能会迅速变得昂贵,特别是当你处理数百万页时。
- 多个工具:你可能会运行 Tesseract 进行光学字符识别,使用布局模型进行表格检测,采用单独的分块策略进行检索增强生成等。
许多团队最终会形成一个脆弱且昂贵的庞大管道。新的方法是:
“只需将 PDF 页面作为图像展示给一个多模态大语言模型,给它一个分块的提示,然后看魔法发生。”
3. 为什么选择 Gemini 2.0 Flash?
根据 Sergey Filimonov 和多位 Hacker News 评论者 的说法:
- 成本:~6,000 页/美元(使用批量调用和最少的输出令牌)。这比许多其他解决方案(GPT-4、专业光学字符识别供应商等)便宜 5–30 倍。
- 准确性:在标准文本上令人惊讶的保真度。大多数错误是轻微的结构差异,尤其是在表格中。
缺失的主要部分是边界框数据。如果您需要将像素完美的覆盖层返回到 PDF,Gemini 的边界框生成仍然远未准确。但如果您的主要关注点是基于文本的检索或摘要,它更便宜、更快、更容易。
4. 端到端架构
We’ll do the following in code:
Image by author
- 将PDF页面转换为图像(
pdf2image
)。 - 将图像发送到 Gemini 2.0 Flash,并使用分块提示。
- 提取块标签
<chunk>...</chunk>
。 - 使用通用嵌入模型对这些块进行嵌入。
- 存储在 KDB.AI 以便搜索。
- 在查询时,检索相关块并将其提供给大语言模型以获取最终答案。
下面,我们将逐步讲解各个部分中的代码,逐步解释每个代码片段。
5. Step-by-Step Code
5.1. 安装依赖项并创建基本表
首先,我们安装所有所需的 Python 包:
- google-generativeai: Gemini 的 Python 客户端。
- kdbai-client: 与 KDB.AI 交互。
- sentence-transformers: 用于嵌入。
- pdf2image: 将 PDF 页面转换为 PNG。
- 以及系统级 PDF 支持的 poppler-utils。
要获取您的 KDB.AI 凭证,请访问 KDB.AI 并登录。免费的云服务提供 4GB 的内存和 30GB 的磁盘空间,如果量化得当,足以容纳数百万个向量。
!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
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 实例进行交互。
5.2. 创建向量表
我们将为块文本和嵌入定义一个简单的模式。KDB.AI 支持在“vectors”列上进行相似性搜索的索引。
VECTOR_DIM = 384
schema = [
{"name": "id", "type": "str"},
{"name": "text", "type": "str"},
{"name": "vectors", "type": "float32s"}
]
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 距离的“flat”索引。对于生产环境,如果您希望更快的近似最近邻查询,可以切换到 HNSW。
5.3. 将PDF页面转换为图像
Gemini是一个多模态模型,因此我们可以直接输入图像。这意味着我们首先使用pdf2image
将每个PDF页面转换为PNG。
import requests
from pdf2image import convert_from_bytes
import base64
import io
pdf_url = "https://arxiv.org/pdf/2404.08865"
resp = requests.get(pdf_url)
pdf_data = resp.content
pages = convert_from_bytes(pdf_data)
print(f"Converted {len(pages)} PDF pages to images.")
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。
5.4. 调用 Gemini 2.0 Flash 进行光学字符识别 + 分块
让我们初始化 Gemini 客户端并定义一个提示,指示模型:
- “将页面进行光学字符识别并转换为 Markdown。”
- “将其分成 250–1,000 字的部分。”
- “用
<chunk>
…</chunk>
包围这些部分。”
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 轻松扩展到数百万个。
5.5. 使用一个提示处理每一页
我们将定义一个辅助函数 process_page(page_num, b64)
,该函数将 base64 PNG 和提示发送给 Gemini。然后我们将从响应中解析出 <chunk>
块。
import re
def process_page(page_num, image_b64):
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"处理第 {page_num} 页时出错: {e}")
return []
chunks = re.findall(r"<chunk>(.*?)</chunk>", text_out, re.DOTALL)
if not chunks:
chunks = text_out.split("\n\n")
results = []
for idx, chunk_txt in enumerate(chunks):
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"总共提取的块: {len(all_chunks)}")
解释:
inline_data
: 告诉 Gemini 我们正在传递一张图片(PNG)。- 我们还添加了文本分块提示。
- 模型返回一个大字符串。我们找到
<chunk>...</chunk>
来分隔它们。 - 如果未找到块标签,我们将通过双换行进行回退分割。
5.6. 嵌入块与存储在 KDB.AI
现在我们已经有了分块文本,让我们使用 all-MiniLM-L6-v2
进行嵌入并上传到 KDB.AI。这并不是这个任务的最佳嵌入模型,但对于这个示例来说足够了。
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 列表并放入数据框中。
- 然后我们使用
.insert()
将其插入到 KDB.AI 表中。
此时,每个块都可以在向量空间中进行搜索。如果你快速执行 table.query()
,你会看到它们都存储在 KDB 中。
6. 查询与构建 RAG 流程
我们现在可以嵌入用户查询,获取最优块,并将它们传递给 任何 大语言模型 进行最终问答。
6.1. 相似性搜索
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 调用。
6.2. 最终生成
我们将检索到的块作为“上下文”输入同一个 Gemini 模型(或任何其他大语言模型),让其生成最终答案:
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)
作者提供的图片
解释:
- 这就是标准的检索增强生成方法:结合来自顶部块的“上下文”,并要求大语言模型作出回应。
- 如果您需要专业的推理或思维链方法,可以相应地优化提示。
7. 注意事项与来自 Hacker News 的经验教训
- 边界框: 几位用户提到,如果您想在原始 PDF 上叠加文本高亮,这将是一个障碍。Gemini 可以进行边界框尝试,但不够准确。
- 幻觉: 基于 LLM 的 光学字符识别 可以生成整段“缺失”的文本。通常它与真实情况非常接近,但也可能出现偏差、遗漏部分或创建不存在的内容。有些人会进行第二次处理或“提示 LLM 验证每一行”。
- 成本: 如果您进行单页、单次调用的使用,您可能会看到每美元的页面数较少。批量调用和限制令牌是您获得 ~6k 页/美元的方法。
- 表的准确性: 现实世界中的表解析仍然可以达到 80-90% 的准确率。对于语义搜索或摘要非常有用,但如果您需要精确的 CSV,可能就不那么完美。然而,LLM 在这项任务上的表现只会越来越好,您可以轻松切换到另一个 LLM。
9. 最终思考
- 用户反馈:HN上的真实团队用Gemini替代了专门的光学字符识别供应商来处理PDF,节省了时间和成本。其他人对边界框或绝对数值的可靠性仍然保持谨慎。
- 何时边界框很重要:如果您必须精确跟踪每个块在PDF上的位置,您需要一种混合方法。(谷歌可能很快会解决这个问题,但现在还没有。)
- 可扩展性:处理数百万页?确保批量调用并限制令牌。这样您就能达到每美元约6,000页的甜蜜点。单页调用或大型输出的成本更高。
- 简单性:您可以真的跳过六个微服务或GPU管道。对许多人来说,这本身就是一个_巨大的_解脱。
底线:如果您处理的是标准PDF并希望将其输入向量存储以进行检索增强生成,Gemini 2.0 Flash_可能_是“足够好”的文本提取的最快路径——尤其是如果您不需要边界框的话。成本优势可能是巨大的,代码也相当简单。这比我们一年前的情况有了巨大的进步。
— Michael Ryaboy, KDB.AI开发者倡导者
参考文献与进一步阅读
- Sergey Filimonov’s Blog: 原始成本/准确性分析,以及边界框问题。
- Hacker News Discussion: 关于 Gemini 2.0、边界框和成本临界点的详细用户反馈。
- KDB.AI Docs: 有关索引配置、分区和扩展到数十亿向量的文档。
- Google Gemini Docs: 调用 2.0 Flash、批量使用和成本的说明。