微信扫码
添加专属顾问
可以说,检索增强生成(RAG)为许多企业和组织带来了变革。通过将像 Gemini[1] 这样的 LLM 的内置功能与您自己的信息相结合,您可以创造出真正具有变革性的强大体验。
尽管如此,创建一个能够很好处理复杂非结构化文档(如 PDF)的 RAG 应用程序仍然是一个挑战。
本文提出了一种从 PDF 中提取文本并转换为 Markdown 格式的新技术,从而提高了检索增强生成(RAG)应用程序的准确性和丰富的上下文。
Markdown 不仅仅用于输出。在提示中使用 Markdown 可以显著提高模型响应的质量,因为它相比于纯文本提供了更多的细微差别。
PDF文件 notoriously 难以处理。每个文档可以有各种各样的布局,包括多列文本,甚至文本似乎随机分布在页面上。由于PDF不仅支持文本,还支持图像,因此某些页面可能看起来像文本,但实际上是以图像形式表示的。此外,PDF通常包含表格数据,这可能相当难以解析。最后,从PDF中提取文本并保留格式信息(如粗体、斜体和项目符号)是相当困难的。仅提取文本会丢失原始文档中的意义和细微差别。
这些情况使得在RAG应用中使用PDF变得困难。当然,有许多Python库可用于处理PDF文档,如PyPDF、PDFPlumber或PDFMiner,但几乎没有一个能够处理上述所有复杂情况。根据源文档的不同,所有这些库可能会生成不完整甚至完全错误的文本。
最近,一些新方法被引入,使用ML模型(如Docling[2])来解析PDF,但它们可能非常慢,并且对于超过几页的PDF不可用。(在我最近在笔记本电脑上运行的一次测试中,Docling花了18分钟来解析一个12页的文档。)
这篇博客文章描述了一种新的技术,通过Gemini和Google Cloud快速有效地读取PDF文件并生成准确的相应Markdown。生成的Markdown非常适合索引到RAG数据存储中。
Markdown[3] 是一种简单而紧凑的标记语言。Markdown 的语法比 HTML 和 CSS 更简单,专注于有限的样式元素:标题、粗体文本、斜体文本、超链接、项目符号和简单表格。
大多数大型语言模型(如 Gemini)生成的输出使用 Markdown,而它提供的样式对于读者理解非常有帮助。实际的项目符号远比在行首使用连字符的纯文本替代方案要好得多,而粗体和斜体文本可以使重要信息更加突出。除此之外,Markdown 将信息组织成表格的能力也非常有用。
或许不太直观的是,Markdown 在创建提示时也非常有用。通过选择性地突出提示中的关键短语或将信息组织成项目符号列表,我们为模型提供了比单纯的文本内容更多的信息,从而提高了模型的理解能力,帮助其集中于当前任务。
尽管如此,重要的是要记住,Markdown 是一种简单的语言,可能不支持您在 PDF 中存储的所有内容。例如,Markdown 表格不支持跨行或跨列,这在表头中经常出现。在测试这种新方法时,记住这一点非常重要,因为这会影响您对某些 PDF 文件提取的准确性。
尽管存在这些限制,能够将 PDF 的内容提取为 Markdown 在处理 RAG 应用程序时非常有帮助。在分块和索引过程中,您可以使用标题来理解章节和小节,这使得将文档分块为离散主题成为可能。同样,按 Markdown 表格排列的表格数据可以帮助模型比使用纯文本更容易理解内容。
总之,显然,从 PDF 中提取的 Markdown 可以显著提高 Gemini 的响应质量,因为与纯文本相比,它提供了更多的细微差别。此外,它还在 RAG 吞吐过程中帮助分块文档,因为您可以使用诸如标题之类的线索来检测文档中的逻辑部分。
现在我们了解了 Markdown 如何提供帮助,让我们看看从 PDF 文档中提取它的过程。
简单来说,从 PDF 文档中提取 Markdown 的过程如下:
这种方法效果很好。以下是一个示例,使用来自 伊利诺伊州1040税表[4] 的页面。注意页面被分成两列,页面的上半部分与下半部分完全分开:
以下是 Gemini 生成的相应 Markdown,已渲染以便您可以看到项目符号、标题等的使用:
如您所见,提取的 markdown 质量非常好,因为它通常反映了人类阅读页面的方式。注意“步骤 2”(页面的上半部分)在“步骤 3”(下半部分)之前被完整描述。
此外,生成的 markdown 指定了项目符号列表、加粗文本、标题等。所有这些为提取的原始文本增加了意义,这通常会在将该 markdown 传递给 Gemini 时产生更好的结果。而且,如前所述,拥有标题和副标题有助于我们将文档分块为逻辑分组,这将有助于 RAG 检索过程。
根据您的用例,您可以简单地遍历 PDF 中的每一页,提取页面图像,然后将其传递给 Gemini 以获取 markdown。然而,在处理这个问题时,考虑扩展性是很重要的。
在我的笔记本电脑上,提取上述示例页面的图像花费了 0.140 秒,因此算法的这一部分非常快速。然而,调用 Gemini 1.5 Flash 提取 Markdown 则花费了 23.857 秒,对于较长的 PDF 文档,这个时间会迅速累积。
幸运的是,这个问题非常适合使用 map-reduce[5] 方法。该方法首先将工作分成多个部分,每个部分并行运行。这一部分称为 map 步骤。然后,当所有并行部分完成时,结果被组合或聚合,这称为 reduce 步骤。
在我们的案例中,我们可以分别处理每一页,然后在所有页面处理完毕后合并所有页面的 markdown。通过利用 Google Cloud,我们可以使用 PubSub[6] 主题分配工作,并使用 Cloud Run Function[7] 处理每一页。以下是说明该方法的图示:
从左到右,这些步骤如下:
这些步骤为每一页提取 markdown(map-reduce 的 map 部分),但我们仍然需要处理 reduce 步骤,其中所有单独的页面 markdown 被合并成一个字符串。
在这种情况下,最简单的方法是让页面处理函数检查它是否是文档中的最后一页。通过计算给定文档在 BigQuery 表中的页数,我们可以确定所有处理是否完成(这就是我们在 PubSub 主题中传递总页数的原因)。
简而言之,在页面处理函数完成页面处理后,它会从相关文档的 BigQuery 表中计算已完成的页面数量,如果与总页数匹配,则检索所有单独的页面 markdown 字符串(按页面编号排序)并合并成一个字符串。此时,我们可以将文档 Markdown 存储在文件中,或者如果需要,可以进行更多处理(例如,将提取的 Markdown 作为发送给 Gemini 的另一个提示的一部分)。
首先,让我们看一下 PDF 文件处理程序的代码——当 PDF 文件放入存储桶时调用的函数。我们使用 PDF 库 PyPdfium[9] 来计算页面数量。
from google.cloud import storage, pubsub_v1
import os
from typing importCallable
from concurrent import futures
import pypdfium2 as pdfium
import json
## 项目 ID
project_id = os.getenv("PROJECTID")
## 我们要写入的 pubsub 主题
pubsub_topicname = os.getenv("TOPICNAME")
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, pubsub_topicname)
defhandle_new_file(event, context):
# 从云存储复制文件到本地存储
bucketname = event['bucket']
filename = event['name']
if filename.lower().endswith('.pdf') isFalse:
print(f"文件 {filename} 不是 PDF 文件,跳过")
return
localname = '/tmp/test.pdf'
download_to_local(bucketname, filename, localname)
# 确定有多少页
num_pages = len(pdfium.PdfDocument(localname))
# 对于每一页,发布一条消息
publish_futures = []
for page_num inrange(num_pages):
# 创建一个 JSON 对象,包含文件名、要处理的页码和总页数
data = json.dumps({"filename": filename, "pagenum": page_num, "totalpages": num_pages}).encode('utf-8')
# 非阻塞。发布失败在回调函数中处理。
future = publisher.publish(topic_path, data)
future.add_done_callback(get_callback(future, data))
publish_futures.append(future)
# 等待所有发布的 futures 完成后再退出。
futures.wait(publish_futures, return_when=futures.ALL_COMPLETED)
# 然后删除本地文件并退出
os.remove(localname)
defdownload_to_local(bucketname, filename, localname):
bucket = storage_client.bucket(bucketname)
blob = bucket.blob(filename)
blob.download_to_filename(localname)
defget_callback(publish_future: pubsub_v1.publisher.futures.Future, data: str) -> Callable[[pubsub_v1.publisher.futures.Future], None]:
defcallback(publish_future: pubsub_v1.publisher.futures.Future) -> None:
try:
# 等待 60 秒以确保发布调用成功。
publish_future.result(timeout=60)
except futures.TimeoutError:
print(f"发布 {data} 超时。")
return callback现在让我们看一下处理单个页面的函数。
import base64
from google.cloud import storage
import os
import json
from read_pdf import get_markdown_for_page
from bigquery import save_page_info, get_num_pages_for_filename, get_markdown_for_filename
BUCKET = os.getenv("BUCKET")
storage_client = storage.Client()
defhandle_pubsub_message(event, context):
# 解码消息数据
message_bytes = base64.b64decode(event['data'])
message_str = message_bytes.decode('utf-8')
message_json = json.loads(message_str)
# 获取我们应该处理的页面信息
filename = message_json.get("filename")
pagenum = message_json.get("pagenum")
totalpages = message_json.get("totalpages")
# 获取文件,提取相关页面,将其转换为图像,
# 并使用 Gemini 获取其 Markdown
download_to_local(BUCKET, filename, "temp.pdf")
markdown = get_markdown_for_page("temp.pdf", pagenum)
save_page_info(filename, pagenum, markdown)
# 现在检查所有页面是否已处理
num_pages_for_filename = get_num_pages_for_filename(filename)
if num_pages_for_filename == totalpages:
# 获取所有页面的 Markdown,合并,然后存储为文件
# 未来,我们将把这个字符串传递给 Gemini 以获取产品信息
all_markdown = get_markdown_for_filename(filename)
save_text_to_bucket(BUCKET, f'markdown\{filename}.md', all_markdown)
defdownload_to_local(bucketname, filename, localname):
bucket = storage_client.bucket(bucketname)
blob = bucket.blob(filename)
blob.download_to_filename(localname)
defsave_text_to_bucket(bucketname, filename, text):
bucket = storage_client.bucket(bucketname)
blob = bucket.blob(filename)
blob.upload_from_string(text)如您所见,此函数调用了一些额外的模块。首先,这是 read_pdf.py 模块,用于提取图像并调用 Gemini 获取 Markdown:
import vertexai
from vertexai.generative_models import (
Part,
Image,
GenerativeModel,
HarmBlockThreshold,
HarmCategory,
)
import pypdfium2 as pdfium
import os
PROJECT_ID = os.getenv("PROJECTID")
REGION = os.getenv("REGION")
LOCAL_IMAGE_FILE = "/tmp/page.png"
vertexai.init(project=PROJECT_ID, location=REGION)
model = GenerativeModel("gemini-1.5-flash-002")
defget_markdown_for_page(fname, pagenum):
imgname = get_image_for_page(fname, pagenum)
markdown = call_gemini_for_markdown(imgname)
return markdown
defget_image_for_page(fname, pagenum):
doc = pdfium.PdfDocument(fname)
page = doc.get_page(pagenum)
bitmap = page.render(scale=2) # 72dpi 分辨率 x 2
bitmap = bitmap.to_pil()
bitmap.save(LOCAL_IMAGE_FILE)
return LOCAL_IMAGE_FILE
defcall_gemini_for_markdown(img_filename):
image1 = Part.from_image(Image.load_from_file(img_filename))
generation_config = {
"max_output_tokens": 8192,
"temperature": 1,
"top_p": 0.95,
}
safety_settings = {
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
}
responses = model.generate_content(
[image1, "检查图像并返回其中所有文本,转换为 Markdown。确保文本反映人类阅读的方式,遵循列并理解格式。忽略脚注和页码 - 它们不应作为 Markdown 的一部分返回。仅为页面上找到的文本生成 markdown。"],
generation_config=generation_config,
safety_settings=safety_settings,
stream=True,
)
response_text = []
for response in responses:
response_text.append(response.text)
return"".join(response_text)如您所见,我们用来提取 Markdown 的提示如下:
检查图像并返回其中所有文本,转换为
Markdown。确保文本反映人类阅读的方式,
遵循列并理解格式。忽略脚注和
页码 - 它们不应作为 Markdown 的一部分返回。
仅为页面上找到的文本生成 markdown。最后,我们在与 BigQuery 交互时使用的几个函数位于 bigquery.py 模块中:
from google.cloud import logging, bigquery
import os
import time
BQ_DATASET = os.getenv("BQ_DATASET")
BQ_TABLE = "pdf2markdown"
bq_client = bigquery.Client()
logging_client = logging.Client()
log_name = "debug-log"
logger = logging_client.logger(log_name)
defsave_page_info(filename, pagenum, markdown):
table_id = f'{BQ_DATASET}.{BQ_TABLE}'
table_ref = bq_client.dataset(BQ_DATASET).table(BQ_TABLE)
# 将提取的字段作为新行插入
try:
errors = bq_client.insert_rows_json(
table_ref,
[{
"filename": filename,
"pagenum": pagenum,
"markdown": markdown
}])
if errors == []:
logger.log_text("数据已插入表中")
else:
logger.log_text(f"插入数据时遇到错误: {errors}", severity="ERROR")
except Exception as e:
logger.log_text(f"插入数据到 BQ 时出错: {e}", severity="ERROR")
defget_num_pages_for_filename(filename):
query = f"SELECT COUNT(*) as numpages FROM `{BQ_DATASET}.{BQ_TABLE}` WHERE filename = '{filename}'"
query_job = bq_client.query(query)
results = list(query_job.result())
count = results[0].numpages
return count
defget_markdown_for_filename(filename):
query = f"SELECT markdown FROM `{BQ_DATASET}.{BQ_TABLE}` WHERE filename = '{filename}' ORDER BY pagenum"
query_job = bq_client.query(query)
results = list(query_job.result())
# 合并为一个字符串
parts = [row.markdown for row in results]
return"\n".join(parts)请注意,这段代码假设 BigQuery 表 pdf2markdown 已经创建。尽管您可以通过代码创建表,但在您可以向该表插入数据之前,通常会有一个小的延迟,这可能会导致错误。最佳实践是在代码之外首先使用 Terraform 或其他基础设施即代码 (IAC) 方法创建空表。
本文讨论了处理PDF文档时面临的挑战,特别是针对RAG应用程序。由于PDF文件的设计主要是为了支持几乎任何可以想象的布局,因此在尝试提取文本和相关的上下文信息(如标题、表格等)时,通常非常困难。
另一方面,Markdown非常适合与像Gemini这样的LLM一起使用,不仅在提高输出的可读性和上下文方面,而且在构建提示时,以及在为RAG解决方案分块和索引文档时。挑战在于将PDF中的内容提取为Markdown格式。
通过将PDF的每一页转换为图像,然后请求Gemini将页面内容提取为Markdown,我们可以快速轻松地从文档中提取文本及其上下文。通过利用Google Cloud的强大功能,我们可以通过并行处理多个页面,使该过程变得极为高效,直到所有页面处理完成后再合并结果。
最后,另一个值得探索的选项是Google Cloud的DocumentAI[10],它使用Google基础模型来解析和分块文档。它还具有内置的OCR支持,可以解析基于图像的页面。您可能希望将这种方法与此处描述的方法进行比较,以确定适合您文档的最佳方法。请记住,DocumentAI不返回Markdown,因此在决定采取哪种方法时,应考虑这一点。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-07-05
AI 知识库为什么总答不准?不是模型笨,是资料没整理好
2026-07-05
AI知识库RAG演进:上一代解决「找得到」,下一代解决「记得住、连得起、信得过」
2026-07-04
大模型支持的上下文已超 1M, RAG 是不是没有意义了?
2026-07-03
RAG 检索优化策略:从命中率到答案质量的一套工程打法
2026-07-03
RAG 落地总翻车?全球赛事冠军架构,改造适配企业级生产
2026-07-01
提升 RAG 准确率全攻略 让你的 AI 知识库 真正靠谱起来!
2026-06-30
教程:如何用AutoRAG + Milvus避免RAG 与Agent 中出现串租问题
2026-06-30
知识库不是文件堆——我把RAG准确率从60%调到了92%
2026-04-27
2026-04-23
2026-04-20
2026-04-09
2026-04-12
2026-04-22
2026-04-10
2026-05-14
2026-04-30
2026-04-27
2026-07-04
2026-06-23
2026-06-23
2026-06-15
2026-06-10
2026-06-10
2026-05-20
2026-05-18
欢迎您使用【53AI 官方网站】(以下简称“本网站”或“我们”)。本《会员服务协议》(以下简称“本协议”)是您(以下简称“会员”或“用户”)与【深圳市博思协创网络科技有限公司】之间关于注册、登录及使用本网站会员服务所订立的法律协议。
在您注册或登录前,请务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的条款、知识产权条款、争议解决条款等。此类条款将以加粗形式提示您注意。 当您通过微信公众号授权、手机验证码验证或其他方式成功登录本网站时,即视为您已完全理解并同意接受本协议的全部内容。
一、 定义
本网站:指由【深圳市博思协创网络科技有限公司】运营的,域名为【53ai.com】的网站及相关移动端页面。
会员服务:指本网站向注册会员提供的知识库文章查阅、内容检索及其他相关增值服务。
知识库内容:指本网站发布的包括但不限于文字、图表、数据、研究报告、行业分析等数字化内容资源。
二、 账号注册与登录
登录方式:本网站支持以下登录方式,您可根据实际情况选择:
微信公众号授权登录:您同意将您的微信OpenID信息授权给本网站,用于创建或关联会员账号。
手机验证码登录:您需提供真实有效的手机号码,并通过短信验证码完成身份验证与登录/注册。
账号安全:您的账号仅限您本人使用,禁止赠与、借用、租用、转让或售卖。因您保管不善导致的账号被盗、密码泄露等损失,由您自行承担。
实名认证:根据相关法律法规要求,我们可能要求您在特定功能下完成实名认证。如您拒绝提供,可能无法使用部分或全部服务。
未成年人保护:若您未满18周岁,请在法定监护人的陪同下阅读本协议,并在征得监护人同意后使用本服务。
三、 服务内容与规范
知识库查阅权限:会员登录后,有权按照其会员等级对应的权限范围,在线浏览、检索本网站知识库中的相关文章及内容。
服务变更:我们有权根据业务发展需要,调整、变更或终止部分服务内容,并将以网站公告、公众号消息等方式提前通知。
禁止行为:您在使用服务时不得实施以下行为:
利用技术手段批量爬取、下载、转存知识库内容;
将知识库内容用于商业目的或未经授权地向第三方传播;
干扰本网站正常运行或侵犯其他用户合法权益;
发布违法违规信息或从事违反公序良俗的活动。
四、 知识产权声明
权利归属:本网站知识库中的排版设计、软件代码等内容的知识产权均归【公司全称】或原权利人所有,受《中华人民共和国著作权法》等法律保护。
有限许可:本网站授予会员一项非独占、不可转让、不可转授权的普通许可,仅限于个人学习、研究之目的在线查阅知识库内容。
侵权追责:未经书面许可,任何单位或个人不得以任何形式复制、转载、摘编、镜像、汇编或以其他方式使用上述内容。一经发现,我们保留追究其法律责任的权利。
五、 个人信息保护
我们重视对您个人信息的保护。关于我们如何收集、使用、存储和保护您的个人信息,请单独阅读 《隐私政策》。
您通过微信公众号授权或手机号验证所提供的信息,我们将严格按照《个人信息保护法》的规定处理,仅用于身份识别、服务提供及安全验证等必要用途。
您可以随时通过网站设置或联系客服行使查阅、更正、删除个人信息及撤回授权同意的权利。
六、 免责声明
内容准确性:知识库内容仅供参考,不构成专业建议。我们不对其完整性、准确性、时效性作任何明示或暗示的保证,您应自行判断并承担使用风险。
不可抗力:因自然灾害、政策法规变化、网络故障、第三方平台接口异常(如微信接口维护、运营商短信通道故障)等不可抗力导致的服务中断或延迟,我们不承担违约责任。
第三方链接:本网站可能包含指向第三方网站的链接,该等网站的内容和服务不受我们控制,请您自行甄别风险。
七、 违约责任
如您违反本协议约定,我们有权视情节采取警告、限制功能、暂停服务、注销账号等措施,并保留要求赔偿损失的权利。
如因您的违约行为导致我们遭受行政处罚、第三方索赔或商誉损失,您应承担全部赔偿责任(包括但不限于罚款、赔偿金、律师费、公证费等)。
八、 法律适用与争议解决
本协议的订立、执行和解释均适用中华人民共和国大陆地区法律。
因本协议产生的或与本协议有关的任何争议,双方应友好协商解决;协商不成的,任何一方均可向【公司所在地】有管辖权的人民法院提起诉讼。
九、 其他
本协议构成双方就本服务达成的完整协议,取代此前任何口头或书面约定。
本协议任一条款被认定为无效或不可执行的,不影响其他条款的效力。
我们对本协议享有最终解释权,并在法律允许的范围内保留随时修改的权利。修改后的协议一经公布即生效,继续使用服务即视为同意修订内容。