图片来源:https://meetcody.ai/blog/gemini-1-5-flash-vs-gpt-4o/,(作者编辑)
试想一下: 你从将每一页 PDF 转换成图片开始,然后将这些图片送去 OCR 识别,只为将原始文本转化为可用的 HTML 或 Markdown 格式。接下来,你需要仔细检测并重建每个表格,将内容拆分以进行语义检索,最后将它们全部插入向量数据库。这已经是一个庞大的流程,通常需要集成多个 ML/OCR 模型,成本也往往很高。
但是假如有一个大型语言模型——Google的Gemini 2.0 Flash——能够简化整个流程呢?想象一下,成本大大降低,将OCR和分块处理一步完成。本文探讨的就是这种可能性。我们将展示Gemini 2.0 Flash如何一次性将PDF转换为可以直接用于Markdown的分块文本,从而摆脱复杂的多步骤流程。然后,我们将这些分块存储在KDB.AI中进行快速向量搜索,将所有这些整合成一个比你之前见过的更加优雅和经济的RAG工作流。系好安全带——这将是一场游戏规则的改变。
这里将教你如何。
- 使用 Gemini 2.0 Flash 将 PDF 页面直接转换成分段文本。
- 将分段文本存储在 KDB.AI 用于向量搜索。
- 将所有这些整合到 RAG 工作流程中。
沿途,我们将展示来自 Hacker News 讨论的真实反馈,并提及 Sergey Filimonov 的博客,该博客首次以近乎完美的精度测量了每美元约 6,000 页的内容。
图片来自:https://www.sergey.fyi/articles/gemini-flash-2
重要提示: 如果你不需要原始 PDF 中的边框,这种方法比旧的 OCR 管道简单得多,也更便宜。
如果你想在 Colab 中试一试,可以看看这个笔记本:notebook。
(提示:关注我的账号,即将发布第二部分,我们将实际操作,解析、导入并搜索大型数据集。敬请期待)
2. 传统的 PDF 处理问题。为什么PDF导入这么难?
- 复杂布局:多栏文本、注脚、侧边栏、图片或扫描的表单。
- 表格提取:传统 OCR 工具常常将表格拉平为混乱的文本。
- 高成本:使用 GPT-4o 或其他大型语言模型可能很快就会变得昂贵,尤其是在处理数百万页时。
- 多种工具:你可能会使用 Tesseract 进行 OCR,使用一个布局模型来检测表格,使用一个单独的分块策略进行检索增强生成等。
许多团队最终得到一个庞大、脆弱且昂贵的工作流程。新方法是这样的:
“只需把PDF页面当作图像展示给一个多模态大型语言模型,给它一个分块提示,然后见证神奇效果。”
这时就轮到Gemini 2.0 Flash登场了。
3. 为什么选择 Gemini 2.0?根据 Sergey Filimonov 和多位 Hacker News 的说法:https://news.ycombinator.com/item?id=43018928:
- 成本:大约每美元可以处理6,000页(通过批处理调用和最少的输出令牌)。这比许多其他解决方案(如GPT-4、专业OCR供应商等)便宜5到30倍。
- 准确性:标准文本的准确性令人惊讶。大多数错误都是结构上的细微差异,特别是在处理表格时。
最关键的部分缺失了,那就是边界框数据。如果你需要像素级精准的覆盖图层覆盖回PDF,Gemini生成的边界框还不够准确。但如果你主要关注的是基于文本的检索或摘要,它更经济、更快、也更简单。
4. 端到端架构以下是我们将通过代码要做的事情:
图片由作者拍摄
- 将PDF页转换为图像 (
pdf2image
)。 - 使用分块提示将图像发送给Gemini 2.0 Flash。
- 提取分块标签
<chunk>...</chunk>
。 - 使用通用嵌入模型将这些分块嵌入。
- 存储到KDB.AI中进行搜索。
- 在查询时,检索相关分块并输入LLM以生成最终答案。
接下来,我们将分代码块逐步解释每一段代码。
5. 逐步代码示例 5.1. 安装所需的依赖项并创建一个基本的表首先,我们要安装所需的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: 安装包及设置
!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
# 我们连接到KDB.AI来存储我们的片段的嵌入向量
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("已连接到KDB.AI:", db)
安装完之后,我们创建一个会话对象来与KDB.AI实例对话。
5.2. 创建一个矢量表我们将定义一个简单的模式来处理分块文本和嵌入。KDB.AI 支持在‘vectors’列(向量列)上建立索引以实现相似性搜索。
# SNIPPET 2: 定义 KDB.AI 表模式定义
VECTOR_DIM = 384 # 我们将使用 all-MiniLM-L6-v2 生成嵌入
模式定义 = [
{"name": "id", "type": "str"},
{"name": "text", "type": "str"},
{"name": "vectors", "type": "float32s"}
]
# 构建一个简单的 L2 距离索引结构
索引 = [
{
"name": "flat_index",
"type": "flat",
"column": "vectors",
"参数": {"dims": VECTOR_DIM, "metric": "L2"}
}
]
表名称 = "pdf_chunks"
try:
db.table(表名称).drop()
except kdbai.KDBAIException:
# 尝试删除表,如果表不存在则忽略错误
pass
表 = db.create_table(表名称, schema=模式定义, indexes=索引)
print(f"已创建表 '{表名称}'")
解释如下:
- 我们为每个块(chunk)存储一个
"id"
,文本内容"text"
以及嵌入向量"vectors"
。 - 表使用的是平面索引,并采用 L2 距离计算。在实际生产中,如果需要更快的近似最近邻查询,可以考虑切换到 HNSW。
Gemini是一个multimodal的模型,所以我们可以直接输入图像。我们使用pdf2image
将每个PDF页面转换为PNG图像。
# SNIPPET 3: 将PDF转换成图片
import requests
from pdf2image import convert_from_bytes
import base64
import io
pdf_url = "https://arxiv.org/pdf/2404.08865" # 示例的PDF
resp = requests.get(pdf_url)
pdf_data = resp.content
pages = convert_from_bytes(pdf_data)
print(f"将 {len(pages)} 页 PDF 转换成了图片。完成了。")
# 我们将图像编码为 base64,这样方便发送给 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客户端,然后定义一个提示来指导模型按照指示行动。
- "将页面OCR转换为Markdown格式。"
- "将其拆分成每段250至1000字的长度。"
- "用
<chunk>
...</chunk>
包裹每个部分。"
# SNIPPET 4: 配置Gemini并定义分块提示内容
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)
CHUNKING_PROMPT = """\
将以下页面OCR成Markdown文本格式。表格应被格式化为HTML。
输出不要用三重反引号包围。
将文档分块,每块大约在250到1000个单词之间。
每个分块前后分别加上<chunk>和</chunk>标签。
尽可能保留所有内容,包括标题、表格等元素。
"""
解释如下:
- 我们加载
gemini-2.0-flash
作为模型。如果你想尝试更大/更小的版本,可以选择相应的 pro 或者 flash 版本。 - 提示语被精心编写,以便模型输出易于解析的分隔符。
- 我们这里处理一个 PDF 文件,但可以通过异步调用 Gemini 轻松处理数百万个 PDF 文件。
我们将定义一个辅助函数 process_page(page_num, b64)
,该函数会接收并处理 base64 编码的 PNG 图片以及指令或提示信息,然后发送给 Gemini。然后我们会从响应中提取 <chunk>
块。
# SNIPPET 5: OCR 和分块处理
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 []
# 解析 <chunk> 块
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):
# 存储每个分块的 ID 和文本
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>
来分隔每一部分。 - 如果没有发现
<chunk>
标签,我们将通过双换行符来分割内容。
现在我们有了分块的文本,可以用 all-MiniLM-L6-v2
模型嵌入并上传到 KDB.AI。这并不是为这个任务提供的最佳嵌入模型,但在这个例子中它就足够用了。
# 代码片段6: 嵌入与数据插入
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"],
"特征向量": embeddings[idx].tolist()
})
# 将片段数据添加到行列表中
df = pd.DataFrame(row_list)
table.insert(df)
print(f"成功插入了 {len(df)} 个片段到 '{table_name}' 中。")
解释一下吧:
此处为空白,待补充内容
- 嵌入向量数据输出为
numpy.float32
类型的数组,形状为(num_chunks, 384)
的数组。 - 我们将其转换为 Python 列表并放入 DataFrame 中。
- 然后我们使用
.insert()
将其插入 KDB.AI 表中。
这时,每个块都可以在向量空间中搜索。运行一下 table.query()
就会发现它们都存储在KDB中。
我们现在可以嵌入用户的查询,获取最相关的片段,并将它们传递给_任何_大语言模型进行最终问答。
6.1. 相似搜索 # SNIPPET 7: RAG的向量查询
user_query = "这篇论文是如何处理多列格式文本的?"
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("检索到的片段如下:\n", context_for_llm)
注意:RAG是指检索增强生成方法
解释:
-
table.search()
执行向量相似性搜索。我们获取最相关的三个片段。 - 我们将它们合并成一个字符串,进行最终的 LLM 调用请求。
我们将检索到的片段作为“上下文”输入到相同的Gemini模型(或任何其他LLM)中,让它生成最终答案。
# SNIPPET 8: RAG 生成
final_prompt = f"""使用以下背景信息回答问题:
背景信息:
{context_for_llm}
问题:{用户提问}
回答:
"""
resp = model.generate_content(final_prompt)
print("\n=== Gemini 的最终回复 ===")
print(resp.text)
作者的图片
解释
- 这是标准的RAG方法:从顶部的几个段落中提取“上下文”,然后让LLM作出回应。
- 如果需要特定的推理或链式思考过程,可以根据需要调整提示。
- 边界框:几位用户提到,如果你想要在原始PDF上叠加文本高亮,这可能是一个大问题。Gemini可以尝试边界框,但并不准确。
- 幻觉:基于LLM的OCR可以生成整个“缺失”的文本。大多数时候它非常接近实际,但有时会偏离实际,遗漏部分或生成原本不存在的新内容。有些人会进行第二次校验或提示LLM验证每行文本。
- 成本:如果你按单页单次调用使用,你可能会发现每美元处理的页数较少,因为单页单次调用的成本较高。通过批量调用并限制标记数量,你可以将每美元处理的页数提高到约6000页。
- 表格准确性:现实中的表格解析仍然可能达到80-90%的准确率。对于语义搜索或总结来说很好,但若需要精确的CSV,可能不够完美。不过,LLM在这方面只会变得越来越好,你可以轻松切换到另一个LLM,以获得更好的性能。
9. 最后的感想
- 用户反馈:真实团队在 HN 上用 Gemini 取代了专门的 OCR 供应商,用于 PDF 输入,节省了时间和金钱。而其他人则对边界框或绝对数值的可靠性持谨慎态度。
- 当边界框重要时:如果你需要精确定位 PDF 中每个片段的位置,你需要采用混合方法。(Google 可能在不久的将来解决这个问题,但目前还没有做到。)
- 可扩展性:处理数百万页内容?确保批处理调用并限制令牌。这样你就能达到每美元约处理 6,000 页的性价比。单页调用或大输出则会更贵。
- 简洁性:你可以直接跳过好几个微服务或 GPU 管道。对很多人来说,这一点本身就已经是一个很大的解脱。
简单说就是:如果你处理的是标准的PDF,并希望将其输入到向量数据库中以进行检索增强生成(RAG),那么Gemini 2.0 Flash 很可能 是达到“足够好”的文本提取的最快途径——特别是如果你不需要边界框。成本优势非常巨大,而且代码也非常简单。相比一年前,这已经取得了巨大的进步。
玩得开心!
— Michael Ryaboy,KDB.AI 开发者支持者
关注我以了解大型语言模型、检索增强生成和 AI 工程的最新动态。你可以在我 LinkedIn 或 Medium 上获取更多内容。
参考文献及更多阅读- Sergey Filimonov 的博客: 原始的成本与准确性分析,以及边界框的问题。
- Hacker News 讨论: 有关 Gemini 2.0、边界框和成本临界点的详细用户反馈。
- KDB.AI 文档: 有关索引配置、分区和扩展至数十亿个向量的说明。
- Google Gemini 文档: 调用 2.0 Flash、批处理的使用和成本说明。