Datawhale AI夏令营 - 科大讯飞AI大赛(多模态RAG方向)

科大讯飞AI大赛多模态RAG方向Baseline方案

分析赛题、对问题进行建模

此部分目标是帮助学习者——深入 理解问题 ,学会构思方案

开始之前,我们重新回顾问题的核心任务是什么。

核心不仅仅是简单的问答,而是基于给定的pdf知识库的、可溯源的多模态问答。

  • 数据源:一堆图文混排的PDF,这是我们唯一的数据。

  • 可溯源:必须明确指出答案的出处。

  • 多模态:问题可能需要理解文本,也可能需要理解图表(图像)。

  • 问答:根据检索的信息生成一个回答。

它定义了我们系统的四个基本支柱,也是我们构建解决方案时必须时刻牢记的四个约束。让我们把这个问题拆解成四个核心要素来逐一分析:

  • 数据源 (Data Source): 图文混排的PDF

    • 我们所有工作的起点和唯一的信息来源,就是 财报数据库.zip 这个压缩包里的PDF文件。这意味着我们的系统必须是一个“闭卷”系统,不允许从互联网或其他外部渠道获取信息来回答问题。

    • 这直接决定了我们系统的第一步必须是 文档解析 (Document Parsing) 。我们不能简单地把PDF当成一个黑盒。需要设计一个流程,能自动化、结构化地从这些PDF中提取出两种核心信息: 文本块 (Text Chunks)图片 (Images) 。更进一步,提取出的每一份信息,都必须牢牢绑定它的元数据——它来自 哪个文件 (filename)哪一页 (page)

  • 可溯源 (Traceability): 必须明确指出答案的出处

    • 这是本赛题的一大特色和评分重点(文件名和页码匹配共占0.5分)。系统不仅要知道答案,也需要知道他的来源。如果答案是“3500万元”,那么这个数字是从 A公司2024年财报.pdf 的第8页的某个表格里来的,我们的系统就必须能明确地指出这一点。

    • 这要求我们在整个处理流水线中实现 元数据(Metadata)的持续追踪 。从文档解析阶段开始,每个文本块、每张图片都要携带 {filename, page_number} 的元数据标记。在检索时,我们捞取的不应仅仅是内容,更要带上它的元数据。最后,在生成答案的环节,需要通过提示工程引导大语言模型依据这些元数据,在生成答案的同时,也生成准确的来源信息。

  • 多模态 (Multimodal): 问题可能需要同时理解文本和图表

    • 很多关键信息,尤其是财报数据,往往存在于图表(本质是图片)中。例如,问题“哪个季度的销售额增长最快?”很可能需要分析一个柱状图才能回答。一个只能处理文本的系统,在这种问题面前将无能为力。

    • 这是赛题的技术核心和难点。我们需要建立一个能让机器“看懂”图片,并将其与文本关联起来的机制。至少有三种主流的建模路径:

      图片

      1. 图片描述 (Image Captioning) : 对每张图片,使用一个视觉语言模型(如Qwen-VL, BLIP等)生成一段描述性的文字。然后,我们将图和文的问题,统一转换成对“文本+图片描述”的纯文本检索问题。这是最简单直接的“降维”思路。

      2. 多模态向量化 (Multimodal Embedding) : 使用像 CLIP 这样的多模态嵌入模型,将文本块和图片都转换到同一个高维向量空间。这样,一个文本形式的提问,可以直接在向量空间中寻找到语义上最相关的文本块和 图片本身

      3. 多模态大模型直接推理 : 将检索到的相关文本和图片 直接 喂给具备多模态理解能力的大语言模型(如Qwen-VL),让模型自己在内部完成信息的融合与推理。这是最前沿的思路,对模型能力要求也最高。 我们的Baseline可以先从思路1或2开始构建。

  • 问答 (Question Answering): 根据检索的信息生成回答

    • 系统的最终产出是一个自然、流畅、准确的答案,而不是简单地把检索到的原文丢给用户。

    • 这是我们系统的最后一个核心模块—— 生成器 (Generator) 。这个角色通常由一个大语言模型(LLM)来扮演。它的任务是接收我们前面所有步骤的成果(用户的原始问题 + 检索到的、最可能相关的文本与图片信息),然后对这些材料进行归纳、推理和总结,最终用自己的话生成答案,并附上来源。这个环节的成败,极度依赖 提示工程 (Prompt Engineering) 的质量。

赛事背景

此部分请介绍赛事大背景和对相关领域的价值

我们正处在一个信息爆炸的时代,但这些信息并非以整洁的纯文本形式存在。它们被封装在各种各样的载体中:公司的年度财报、市场研究报告、产品手册、学术论文以及无数的网页。这些载体的共同特点是 图文混排 ——文字、图表、照片、流程图等元素交织在一起,共同承载着完整的信息。

传统的AI技术,如搜索引擎或基于文本的问答系统,在处理这类复杂文档时显得力不从心。它们能很好地理解文字,但对于图表中蕴含的趋势、数据和关系却是“视而不见”的。这就造成了一个巨大的信息鸿沟:AI无法回答那些需要结合视觉内容才能解决的问题,例如“根据这张条形图,哪个产品的市场份额最高?”或“请解释一下这张流程图的工作原理”。

近年来,大语言模型(LLM)的崛起为自然语言理解带来了革命。然而,它们也面临两大挑战:

  1. 知识局限性 :LLM的知识是预训练好的,对于私有的、最新的或特定领域的文档(比如本次比赛的财报)一无所知,并且可能产生幻觉。

  2. 模态单一性 :大多数LLM本身只能处理文本,无法直接“看到”和理解图像。

检索增强生成(RAG) 技术的出现,通过从外部知识库中检索信息来喂给LLM,有效地解决了第一个挑战。而本次比赛的核心—— 多模态检索增强生成(Multimodal RAG) ,则是应对这两大挑战的前沿方案。它赋予了AI系统一双“眼睛”,让他不仅能阅读文字,还能看懂图片,并将两者结合起来进行思考和回答。

赛题解读

此部分请详细介绍赛事的 输入-输出 究竟是什么,尤其是提交的格式

输入 (Input):我们需要处理什么?

比赛官方为我们提供了三样核心材料,它们是我们构建系统所需用到的全部信息:

  1. 财报数据库.zip

一个包含了多个PDF文件的压缩包。这些PDF是真实世界的公司财报,内容上是典型的 图文混排 格式,包含了大量段落、数据表格以及各种图表(如条形图、饼图、折线图等)。这是我们系统的 唯一信息来源 。所有问题的答案都必须从这些PDF文档中寻找,并且不能依赖任何外部知识。

  1. train.json

一个JSON格式的文件,为我们提供了一系列“问题-答案”的范例。这是我们用来开发、训练和验证我们系统模型的主要依据。我们可以通过它来调试我们的算法,看看对于给定的问题,我们的系统能否找到正确的答案和出处。

  • 数据格式示例

    :文件内容是一个JSON列表,其中每个元素代表一个问答对,结构如下:

[
  {
    "question": "根据图表显示,产品A的销售额在哪个季度开始下降?",
    "answer": "产品A的销售额在第三季度开始出现下降。",
    "filename": "2023年度第三季度财报.pdf",
    "page": 5
  },
  {
    "question": "...",
    "answer": "...",
    "filename": "...",
    "page": "..."
  }
]

  1. test.json

另一个JSON格式的文件,包含了比赛最终用来评测我们系统性能的所有问题。

这是我们需要完成的任务。文件里 只包含 question 字段 ,而我们需要预测的 answer , filename , 和 page 都是缺失的。

[
    {
        "filename": "xx.pdf",
        "page": 1,
        "question": "广联达在苏中建设集团南宁龙湖春江天越项目中,具体运用了哪些BIM技术,并取得了哪些成果?",
        "answer": "广联达在苏中建设集团南宁龙湖春江天越项目中,具体运用了哪些BIM技术,并取得了哪些成果?"
    },
    {
        "filename": "xx.pdf",
        "page": 1,
        "question": "广联达公司如何通过数字项目管理平台提升施工企业的数字化转型能力?",
        "answer": "广联达公司如何通过数字项目管理平台提升施工企业的数字化转型能力?"
    },
    ……
    ]


输出 (Output):我们需要提交什么?

我们的最终任务是为 test.json 中的每一个问题,预测出三个信息: 答案 ( answer )来源文件名 ( filename )来源页码 ( page )

提交文件格式

官方要求我们将所有预测结果整理成一个 JSON 文件 进行提交。官方提供了 sample_submit.json 作为格式参考。

这个文件应该包含以下一个列表(列名以 sample_submit.json 为准):

  • question:问题。

  • answer :你预测的答案文本。

  • filename :你预测的答案所在PDF文件的全名。

  • page :你预测的答案所在的页码。

提交文件示例 ( submit. json)

[
    {
        "filename": "xx.pdf",
        "page": 1,
        "question": "广联达在苏中建设集团南宁龙湖春江天越项目中,具体运用了哪些BIM技术,并取得了哪些成果?",
        "answer": "广联达在苏中建设集团南宁龙湖春江天越项目中,具体运用了哪些BIM技术,并取得了哪些成果?"
    },
    ……
    ]

总结一下 :整个任务流程就是,读取 test.json 里的一个问题,驱动你的系统去 财报数据库 中查找信息,然后生成答案和出处,最后将这几项信息作为一行写入到最终的 submit.json 文件中。对 test.json 中的所有问题重复此过程,即可得到最终的提交文件,其中的train.json文件主要是用来在训练非生成式模型环节中使用的,比如训练embedding模型,或者是微调LLM,但是我们的baseline更侧重于让大家能够快速体验跑通流程,所以只会涉及到如何构建以及跑通问答知识库的环节。

数据分析与探索

此部分请详细介绍你对数据做了什么分析和处理、把数据格式和背后的含义尽可能说清楚,如果讲不清楚请找undefined商量

因为我们的侧重点在于PDF文件的处理,数据分析的部分我们主要是需要看看他们的PDF大概是什么样子的。

图片

打开 财报数据库.zip 里面的其中一个pdf文件 我们可以发现PDF内部的PDF是图文混排的内容,有图片也有文本,其中图片有可能是柱形图,也可能是折线图等等。那我们下面就需要思考如何提取里面的数据,能够提取PDF里面的数据,主要可以考虑两个工具,分别是pymupdf和mineru,一个是基于规则的方式提取pdf里面的数据,另外一个是基于深度学习模型通过把PDF内的页面看成是图片进行各种检测,识别的方式提取。其中考虑到学习者可能没有那么高的硬件成本资源条件,所以task1选择的是基于pymupdf的方式给大家作为示例,基于mineru的方式会在进阶教程里面。

下面是一个使用 PyMuPDF(fitz)提取 PDF 内容的基础代码示例。它可以提取每一页的文本内容:

import fitz  # PyMuPDF

def extract_pdf_text(pdf_path):
    doc = fitz.open(pdf_path)
    all_text = []
    for page_num in range(doc.page_count):
        page = doc.load_page(page_num)
        text = page.get_text()
        all_text.append(text)
    doc.close()
    return all_text

if __name__ == "__main__":
    pdf_file = "your_file.pdf"  # 替换为你的 PDF 文件路径
    texts = extract_pdf_text(pdf_file)
    for i, page_text in enumerate(texts):
        print(f"--- Page {i+1} ---")
        print(page_text)

你只需将 your_file.pdf 替换为你的 PDF 文件路径即可。代码会打印每一页的文本内容。

赛题要点与难点

此部分请从小白的视角,拆解解题核心的要点

以及详细介绍赛题可能的难点

在上述数据分析与探索的流程中,以下几点是决定方案性能上限的关键,也是主要的难点所在。

  1. 难点一:多模态信息的有效融合

    • 问题 :一个问题的答案可能同时依赖于一段文字描述和一个数据图表。例如,文字提到“各产品线表现见下图”,而具体数据则完全在图表中。

    • 挑战 :如何让系统理解这种跨模态的指代和依赖关系?如果仅将文本和图像的描述(caption)作为独立的知识块进行检索,可能会丢失它们之间的强关联。检索模块需要足够智能,能够根据一个文本问题,同时召回相关的文本和图像信息。

  2. 难点二:检索的准确性与召回率平衡 (Precision vs. Recall)

    • 问题 :检索是整个系统的基础,如果检索出的上下文信息就不包含答案,那么后续的LLM再强大也无法凭空生成正确结果(这被称为“大海捞针,针不在海里”)。

    • 挑战

      • 语义模糊性 :用户提问的方式可能与文档中的措辞差异很大,这对嵌入模型的语义理解能力提出了高要求。

      • 信息干扰 :如果检索返回的Top-K个结果中,只有1个是相关的,其他K-1个都是噪音,这会严重干扰LLM的判断,可能导致它基于错误信息作答。如何优化检索策略(如使用重排Re-ranking技术)以提高返回结果的信噪比,是一个核心问题。

  3. 难点三:答案生成的可控性与溯源精确性

    • 问题 :LLM在生成答案时,有时会过度“自由发挥”,产生一些幻觉(Hallucination),即编造上下文中不存在的信息。同时,它也可能错误地引用来源。

    • 挑战

      • 忠实度 :如何通过设计Prompt,强力约束LLM,使其回答 严格基于 提供的上下文,减少信息捏造。

      • 溯源 :如何让LLM准确地从多个上下文中,定位到真正提供答案关键信息的那个来源(文件名和页码),并正确地在最终输出中引用。这需要精心设计上下文的格式和给LLM的指令。

  4. 难点四:针对性评估指标的优化

    • 问题 :最终的评分由三部分构成:文件名匹配度(0.25分)、页面匹配度(0.25分)和答案内容相似度(0.5分)。

    • 挑战 :这意味着,一个完美的答案文本如果来源错误,得分会很低。反之,一个内容不太完美的答案如果来源准确,也能拿到可观的分数。因此,系统优化不能只关注答案文本的质量,必须将 溯源的准确性 放在同等重要的位置。在方案迭代中,需要建立能够模拟这套评分体系的本地验证集,以准确评估每次改动对最终得分的综合影响。

解题思考过程

此部分请详细介绍你看到题目会首先思考到什么、做什么、找了什么参考资料,遇到了什么卡点,是如何解决的

看到题目后,我的大致流程是这样的:

第一步:从“终点”反推“起点”——快速明确核心任务
  1. 快速浏览背景,然后直奔“赛题任务” :这让我迅速了解到,核心是做一个 多模态问答 系统。

  2. 查看“提交示例”与“评审规则” :这是整个思考过程中最关键的一步。单纯看任务描述,概念还比较模糊。但当我看到提交格式要求我们为每一个 question ,都精准地提供 answerfilenamepage 时,任务的本质瞬间清晰了——这并非一个开放式的聊天机器人,而是一个 目标明确、要求可溯源的信息抽取与生成任务

这个发现,让我立刻明确了数据处理的目标。为了生成这样的提交结果,我的系统内部就必须构建并维护一个大概包含以下元素的核心数据结构:

{ "content": "内容文本或描述", "metadata": { "filename": "来源文件名.pdf", "page": 页码 } }

这个结构是后续所有工作的基础。同时, content 字段需要被转换成机器能够理解和搜索的形式,为了方便后续召回, 向量化(Embedding)字段选择的是content 。其他部分的元数据是为了辅助我们后续进行回答的时候能够知道当前chunk是属于什么位置的。

第二步:技术方案的权衡与选择

任务明确后,我开始思考如何实现,尤其是如何处理核心的多模态问题。

  • 多模态方案选择 : 我考虑了三种主流的多模态实现路径:

    1. 基于图片描述 :对所有图片生成文本描述,将这些描述与原文的文本块统一处理。这能将多模态问题简化为纯文本问题,最适合快速构建Baseline。

    2. 多模态分别嵌入 :对文本和图片分别进行向量化,检索时结合文本和图片的相似度。这更精确,但实现也更复杂,而且召回图片与召回文本存在不相关情况,也需要比较多的处理。

    3. 多模态大模型端到端处理 :将检索到的文本和原始图片一起交给多模态大模型(如Qwen-VL)进行端到端理解和生成。这是最前沿的方案,但也最消耗资源,因为一般的多模态模型推理能力要稍微差一些。

为了优先保证能跑通一个完整的流程,我选择了 第一种方案:基于图片描述 。它的优点是实现简单、逻辑清晰,能让我快速验证整个RAG(检索增强生成)链路。

具体工具栈调研

  1. PDF解析 :这个环节我们选择的mineru,但是task1里面为了降低大家的学习门槛,使用的是pymupdf作为平替方案。

  2. Embedding实现 :我最初考虑使用 sentence-transformer 库。但在进一步查阅资料时,我发现了 Xinference ,它能将模型部署为服务,并通过兼容OpenAI的API来调用。我立即决定采用这种方式,因为 服务化能让我的Embedding模块与主应用逻辑解耦,更利于调试和未来的扩展。

第三步:构建Baseline执行流程

结合上述思考,我的Baseline执行流程变得非常清晰:

  1. 预处理(离线完成)

    1. 使用pymupdf批量解析所有PDF文档,得到结构化的JSON数据。

    2. 将原始的文本块,以及图片的描述文本,附带上它们的元数据( filename , page ),构造成我们第一步设计的核心数据结构。

    3. 调用 Xinference 部署的Embedding模型服务,将所有内容的文本部分转换为向量。

    4. 将最终的 { "id":"……","content": "...", "vector": [...], "metadata": ... } 存入向量数据库,完成知识库构建。

  2. 在线推理

  3. 接收测试集的json文件中的一个 question

  4. 调用 Xinference 服务,将 question 向量化。

  5. 在向量数据库中进行相似度搜索,召回Top-K个最相关的内容块。

  6. 将召回的内容块及其元数据,与 question 一同填入设计好的Prompt模板中。

  7. 将完整的Prompt交给一个大语言模型(LLM),生成最终的答案和来源信息。

遇到的卡点及解决建议

在实际操作中,最主要的瓶颈在于 时间消耗

  • pymupdf解析 :处理整个财报数据库会稍微消耗一些时间,特别是如果使用基于深度学习的方式提取内容,比如mineru,不过我们本次baseline使用pymupdf速度会有比较大的提升。

  • 批量Embedding :将接近5000的内容块进行向量化,也会消耗不少时间,如果是基于CPU运行的话大概会慢十倍,使用A6000这样的GPU也需要消耗大概1分钟的时间。

核心痛点 :如果在处理过程中代码出现一个小错误,比如数据格式没对齐,就需要从头再来,这将浪费大量时间。

解决与建议不要在一个脚本里完成所有事 。强烈建议使用 Jupyter Notebook 进行开发调试,并将流程拆分:

  1. 第一阶段:解析 。在一个Notebook中,专门负责调用pymupdf,将所有PDF解析为JSON并 保存到本地 。这个阶段成功运行一次后,就不再需要重复执行。

  2. 第二阶段:预处理与Embedding 。在另一个Notebook中,读取第一步生成的JSON文件,进行图片描述生成、数据清洗,并调用Embedding模型。将最终包含向量的知识库 保存为持久化文件

  3. 第三阶段:检索与生成 。在第三个Notebook中,加载第二步保存好的知识库,专注于调试检索逻辑和Prompt工程。

通过这种 分步执行、缓存中间结果 的方式,可以极大地提高调试效率,每次修改只需运行对应的、耗时较短的模块。

Baseline方案详解

在动手写任何代码之前,面对这样一个复杂的赛题,清晰的思路远比匆忙的实现更重要。不妨先停下来,问自己几个问题:

  1. 核心矛盾是什么? 是追求第一天就拿到SOTA(State-of-the-Art)的高性能,还是优先构建一个能完整跑通、麻雀虽小五脏俱全的系统?对于Baseline而言,目标应该是后者。一个能稳定输出结果的简单系统,是后续一切优化的基础。我们应该如何设计最简路径?

  2. 如何绕开最难的坎? 多模态是这个赛题的核心难点。让机器看懂图片,最直接的方式是让模型直接处理图片像素。但这会引入复杂的多模态模型调用和信息融合问题。有没有更简单、能快速融入现有RAG(以文本为中心)流程的办法?我们能不能先把图片“翻译”成文字?

  3. LLM需要什么样的信息? 检索模块是LLM的眼睛和耳朵。我们是应该只把和问题最相似的那一小块知识喂给LLM,还是应该提供更丰富的周边信息?例如,找到一个关键段落后,是否应该把它的上下文(前后段落、所属章节标题)也一并提供,来帮助LLM更好地理解?

带着这些思考,我们来解读这个Baseline方案的设计。你会发现,这个方案的每一个决策,都是在对这些问题进行回答。

Baseline文件概况

有哪些文件、分别有什么作用?

其中datas的目录结构如下:

datas/
├── 多模态RAG图文问答挑战赛训练集.json
├── 多模态RAG图文问答挑战赛测试集.json
├── 多模态RAG图文问答挑战赛提交示例.json
└── 财报数据库/
    ├── 伊利股份相关研究报告/
    │   ├── 伊利股份-公司研究报告-平台化的乳企龙头引领行业高质量转型-25071638页.pdf
    │   ├── 伊利股份内蒙古伊利实业集团股份有限公司2024年年度报告.pdf
    │   └── ... (其他伊利股份研究报告)
    ├── 广联达公司研究报告/
    │   ├── 广联达-公司深度报告-数字建筑全生命周期解决方案领军者-24041638页.pdf
    │   ├── 广联达-云计算稀缺龙头迎收入利润率双升-21080125页.pdf
    │   └── ... (其他广联达研究报告)
    ├── 千味央厨公司研究报告/
    │   ├── 千味央厨-公司深度报告-深耕B端蓝海扬帆-21090629页.pdf
    │   ├── 千味央厨-公司研究报告-大小B双轮驱动餐饮市场大有可为-23061240页.pdf
    │   └── ... (其他千味央厨研究报告)
    └── 其他上市公司研究/
        ├── 中恒电气-公司研究报告-HVDC方案领头羊AI浪潮下迎新机-25071124页.pdf
        ├── 亚翔集成-公司研究报告-迎接海外业务重估-25071324页.pdf
        ├── 传音控股-公司研究报告-非洲手机领军者多元化布局品类扩张生态链延伸打开成长空间-25071636页.pdf
        └── ... (其他公司研究报告)

重要提示:使用本项目前,请确保 datas/ 目录中包含所需的数据文件。如果没有数据,可以从以下地址下载:

2025 iFLYTEK AI开发者大赛

下载后请将数据文件复制到 datas/ 目录中。

完整运行过代码之后的目录大概就是这样的:

图片

Baseline方案思路

如何想到和选取这样的baseline方案的、参考了哪些思路?这样做的优点和缺点是?

baseline的多个处理阶段

阶段一:离线预处理 (构建知识库)

这个阶段的目标是将原始的PDF知识库,制作成一个可供快速检索的向量数据库。

  1. 文档解析 ( Parse ) :

    • 输入财报数据库.zip 里的所有PDF文件。

    • 工具 :fitz

    • 输出 :为每个PDF生成一个结构化的JSON文件。这个过程是全自动的,我们只需要调用脚本运行的命令即可。

  2. 知识库构建 ( Index ) :

    • 输入 :增强后的JSON内容。

    • 工具Xinference (用于部署Embedding模型), ChromaDB (向量数据库)

    • 逻辑 : a. 初始化 ChromaDB 客户端。 b. 遍历所有内容块,将需要被检索的文本(原始 text 或生成的 image_description )提取出来。 c. 调用部署在 Xinference 上的Embedding模型服务,将文本转换为向量。 d. 将文本本身、其向量、以及它的元数据( filename , page_idx )作为一个条目,存入 ChromaDB 中。

    • 输出 :一个包含所有知识的、持久化在本地的向量数据库。

阶段二:在线推理 (生成答案)

这个阶段是用户提问时,系统实时响应的过程。

  1. 问题向量化 ( Query ) :

    • 输入test.json 中的一个 question

    • 逻辑 :调用与构建知识库时完全相同的Embedding模型服务,将问题文本转换为查询向量。

  2. 信息检索 ( Retrieve ) :

    • 输入 :查询向量。

    • 逻辑 :在 ChromaDB 中执行相似度搜索,找出与查询向量最相似的Top-K(例如K=5)个知识块。

  3. 答案生成 ( Generate ) :

    • 输入 :用户的原始问题 + 上一步检索到的Top-K个知识块(包含文本和元数据)。

    • 逻辑 :

      • a. 构建Prompt :将输入信息填入一个我们预先设计好的Prompt模板中。模板会明确指示LLM扮演的角色、必须依据的上下文、以及必须遵守的输出格式(附带来源)。

      • b. 调用LLM :将完整的Prompt发送给LLM(如部署在 Xinference 中的Qwen)。

      • c. 解析与格式化 :从LLM的返回结果中,用正则表达式或字符串分割等方式,提取出答案主体、文件名和页码,整理成最终提交的格式。

    • 输出 :一条完整的、符合提交要求的预测结果。

Baseline的优点与不足
  • 优点

    • 逻辑清晰 :“图像文本化”策略巧妙地将复杂问题降维,整个流程符合经典的RAG范式,易于理解和实现。

    • 端到端完整 :覆盖了从数据处理到最终提交的每一个环节,是一个可以立即上手运行的完整方案。

    • 模块化设计 :每个部分(解析、嵌入、检索、生成)相对独立,可以方便地替换其中任一模块(比如换一个更好的Embedding模型或LLM)进行实验和优化。

  • 不足

    • 信息损失 :没有提取图片里面的内容。

    • 上下文割裂 :将文档按照页面切分成独立的块进行检索,可能会破坏原文中段落与段落、段落与图表之间的上下文关联。检索出的知识块可能是孤立的,缺乏上下文。

    • 检索策略单一 :仅基于语义相似度的检索,对于一些包含特定关键词或需要大范围信息整合的问题,可能不是最优解。

Baseline核心逻辑

此部分请介绍baseline的处理逻辑和方案思路,吃透需要关注哪些核心函数?

绘制一张流程图 + 相关核心函数/代码解读

图片

阶段一:离线预处理 (构建知识库) - fitz_pipeline_all.py
import fitz  # PyMuPDF
import json
from pathlib import Path

def process_pdfs_to_chunks(datas_dir: Path, output_json_path: Path):
    """
    使用 PyMuPDF 直接从 PDF 提取每页文本,并生成最终的 JSON 文件。
    
    Args:
        datas_dir (Path): 包含 PDF 文件的输入目录。
        output_json_path (Path): 最终输出的 JSON 文件路径。
    """
    all_chunks = []
    
    # 递归查找 datas_dir 目录下的所有 .pdf 文件
    pdf_files = list(datas_dir.rglob('*.pdf'))
    if not pdf_files:
        print(f"警告:在目录 '{datas_dir}' 中未找到任何 PDF 文件。")
        return

    print(f"找到 {len(pdf_files)} 个 PDF 文件,开始处理...")

    for pdf_path in pdf_files:
        file_name_stem = pdf_path.stem  # 文件名(不含扩展名)
        full_file_name = pdf_path.name  # 完整文件名(含扩展名)
        print(f"  - 正在处理: {full_file_name}")

        try:
            # 使用 with 语句确保文件被正确关闭
            with fitz.open(pdf_path) as doc:
                # 遍历 PDF 的每一页
                for page_idx, page in enumerate(doc):
                    # 提取当前页面的所有文本
                    content = page.get_text("text")
                    
                    # 如果页面没有文本内容,则跳过
                    if not content.strip():
                        continue

                    # 构建符合最终格式的 chunk 字典
                    chunk = {
                        "id": f"{file_name_stem}_page_{page_idx}",
                        "content": content,
                        "metadata": {
                            "page": page_idx,  # 0-based page index
                            "file_name": full_file_name
                        }
                    }
                    all_chunks.append(chunk)
        except Exception as e:
            print(f"处理文件 '{pdf_path}' 时发生错误: {e}")

    # 确保输出目录存在
    output_json_path.parent.mkdir(parents=True, exist_ok=True)

    # 将所有 chunks 写入一个 JSON 文件
    with open(output_json_path, 'w', encoding='utf-8') as f:
        json.dump(all_chunks, f, ensure_ascii=False, indent=2)

    print(f"\n处理完成!所有内容已保存至: {output_json_path}")

def main():
    base_dir = Path(__file__).parent
    datas_dir = base_dir / 'datas'
    chunk_json_path = base_dir / 'all_pdf_page_chunks.json'
    
    process_pdfs_to_chunks(datas_dir, chunk_json_path)

if __name__ == '__main__':
    main()

这个脚本是整个数据流水线的核心,用来提取pdf里面的数据成纯文本的内容, process_pdfs_to_chunks() :

  • 作用 : 汇总所有PDF的按页内容,整合成一个统一的知识库文件。

  • 数据结构 : 每个页面被构造成一个 "chunk"(知识块),包含ID、内容和元数据。

{
  "id": "广联达2022年年度报告_page_34",
  "content": "# 第三节 管理层讨论与分析...", // 该页的Markdown内容
  "metadata": {
    "page": "34",
    "file_name": "广联达2022年年度报告.pdf"
  }
}

  • : 项目根目录下的 all_pdf_page_chunks.json ,这是我们RAG系统的最终知识库。

阶段二:在线推理 (生成答案) - rag_from_page_chunks.py

这个脚本负责实现RAG的检索和生成两大核心功能,主要通过 SimpleRAG 类完成。

import json
import os

import hashlib
from typing import List, Dict, Any
from tqdm import tqdm
import sys
sys.path.append(os.path.dirname(__file__))
from get_text_embedding import get_text_embedding
from dotenv import load_dotenv
import os
from openai import OpenAI
load_dotenv()

class PageChunkLoader:
    def __init__(self, json_path: str):
        self.json_path = json_path
    def load_chunks(self) -> List[Dict[str, Any]]:
        with open(self.json_path, 'r', encoding='utf-8') as f:
            return json.load(f)

class EmbeddingModel:
    def __init__(self, batch_size: int = 64):
        load_dotenv(os.path.join(os.path.dirname(__file__), '.env'))
        self.api_key = os.getenv('LOCAL_API_KEY')
        self.base_url = os.getenv('LOCAL_BASE_URL')
        self.embedding_model = os.getenv('LOCAL_EMBEDDING_MODEL')
        self.batch_size = batch_size
        if not self.api_key or not self.base_url:
            raise ValueError('请在.env中配置GUIJI_API_KEY和GUIJI_BASE_URL')

    def embed_texts(self, texts: List[str]) -> List[List[float]]:
        return get_text_embedding(
            texts,
            api_key=self.api_key,
            base_url=self.base_url,
            embedding_model=self.embedding_model,
            batch_size=self.batch_size
        )

    def embed_text(self, text: str) -> List[float]:
        return self.embed_texts([text])[0]

class SimpleVectorStore:
    def __init__(self):
        self.embeddings = []
        self.chunks = []
    def add_chunks(self, chunks: List[Dict[str, Any]], embeddings: List[List[float]]):
        self.chunks.extend(chunks)
        self.embeddings.extend(embeddings)
    def search(self, query_embedding: List[float], top_k: int = 3) -> List[Dict[str, Any]]:
        from numpy import dot
        from numpy.linalg import norm
        import numpy as np
        if not self.embeddings:
            return []
        emb_matrix = np.array(self.embeddings)
        query_emb = np.array(query_embedding)
        sims = emb_matrix @ query_emb / (norm(emb_matrix, axis=1) * norm(query_emb) + 1e-8)
        idxs = sims.argsort()[::-1][:top_k]
        return [self.chunks[i] for i in idxs]

class SimpleRAG:
    def __init__(self, chunk_json_path: str, model_path: str = None, batch_size: int = 32):
        self.loader = PageChunkLoader(chunk_json_path)
        self.embedding_model = EmbeddingModel(batch_size=batch_size)
        self.vector_store = SimpleVectorStore()
    def setup(self):
        print("加载所有页chunk...")
        chunks = self.loader.load_chunks()
        print(f"共加载 {len(chunks)} 个chunk")
        print("生成嵌入...")
        embeddings = self.embedding_model.embed_texts([c['content'] for c in chunks])
        print("存储向量...")
        self.vector_store.add_chunks(chunks, embeddings)
        print("RAG向量库构建完成!")
    def query(self, question: str, top_k: int = 3) -> Dict[str, Any]:
        q_emb = self.embedding_model.embed_text(question)
        results = self.vector_store.search(q_emb, top_k)
        return {
            "question": question,
            "chunks": results
        }

    def generate_answer(self, question: str, top_k: int = 3) -> Dict[str, Any]:
        """
        检索+大模型生成式回答,返回结构化结果
        """
        qwen_api_key = os.getenv('LOCAL_API_KEY')
        qwen_base_url = os.getenv('LOCAL_BASE_URL')
        qwen_model = os.getenv('LOCAL_TEXT_MODEL')
        if not qwen_api_key or not qwen_base_url or not qwen_model:
            raise ValueError('请在.env中配置LOCAL_API_KEY、LOCAL_BASE_URL、LOCAL_TEXT_MODEL')
        q_emb = self.embedding_model.embed_text(question)
        chunks = self.vector_store.search(q_emb, top_k)
        # 拼接检索内容,带上元数据
        context = "\n".join([
            f"[文件名]{c['metadata']['file_name']} [页码]{c['metadata']['page']}\n{c['content']}" for c in chunks
        ])
        # 明确要求输出JSON格式 answer/page/filename
        prompt = (
            f"你是一名专业的金融分析助手,请根据以下检索到的内容回答用户问题。\n"
            f"请严格按照如下JSON格式输出:\n"
            f'{{"answer": "你的简洁回答", "filename": "来源文件名", "page": "来源页码"}}'"\n"
            f"检索内容:\n{context}\n\n问题:{question}\n"
            f"请确保输出内容为合法JSON字符串,不要输出多余内容。"
        )
        client = OpenAI(api_key=qwen_api_key, base_url=qwen_base_url)
        completion = client.chat.completions.create(
            model=qwen_model,
            messages=[
                {"role": "system", "content": "你是一名专业的金融分析助手。"},
                {"role": "user", "content": prompt}
            ],
            temperature=0.2,
            max_tokens=1024
        )
        import json as pyjson
        sys.path.append(os.path.dirname(__file__))
        from extract_json_array import extract_json_array
        raw = completion.choices[0].message.content.strip()
        # 用 extract_json_array 提取 JSON 对象
        json_str = extract_json_array(raw, mode='objects')
        if json_str:
            try:
                arr = pyjson.loads(json_str)
                # 只取第一个对象
                if isinstance(arr, list) and arr:
                    j = arr[0]
                    answer = j.get('answer', '')
                    filename = j.get('filename', '')
                    page = j.get('page', '')
                else:
                    answer = raw
                    filename = chunks[0]['metadata']['file_name'] if chunks else ''
                    page = chunks[0]['metadata']['page'] if chunks else ''
            except Exception:
                answer = raw
                filename = chunks[0]['metadata']['file_name'] if chunks else ''
                page = chunks[0]['metadata']['page'] if chunks else ''
        else:
            answer = raw
            filename = chunks[0]['metadata']['file_name'] if chunks else ''
            page = chunks[0]['metadata']['page'] if chunks else ''
        # 结构化输出
        return {
            "question": question,
            "answer": answer,
            "filename": filename,
            "page": page,
            "retrieval_chunks": chunks
        }

if __name__ == '__main__':
    # 路径可根据实际情况调整
    chunk_json_path = os.path.join(os.path.dirname(__file__), 'all_pdf_page_chunks.json')
    rag = SimpleRAG(chunk_json_path)
    rag.setup()

    # 控制测试时读取的题目数量,默认只随机抽取10个,实际跑全部时设为None
    TEST_SAMPLE_NUM = 10  # 设置为None则全部跑
    FILL_UNANSWERED = True  # 未回答的也输出默认内容

    # 批量评测脚本:读取测试集,检索+大模型生成,输出结构化结果
    test_path = os.path.join(os.path.dirname(__file__), 'datas/多模态RAG图文问答挑战赛测试集.json')
    if os.path.exists(test_path):
        with open(test_path, 'r', encoding='utf-8') as f:
            test_data = json.load(f)
        import concurrent.futures
        import random

        # 记录所有原始索引
        all_indices = list(range(len(test_data)))
        # 随机抽取部分题目用于测试
        selected_indices = all_indices
        if TEST_SAMPLE_NUM is not None and TEST_SAMPLE_NUM > 0:
            if len(test_data) > TEST_SAMPLE_NUM:
                selected_indices = sorted(random.sample(all_indices, TEST_SAMPLE_NUM))

        def process_one(idx):
            item = test_data[idx]
            question = item['question']
            tqdm.write(f"[{selected_indices.index(idx)+1}/{len(selected_indices)}] 正在处理: {question[:30]}...")
            result = rag.generate_answer(question, top_k=5)
            return idx, result

        results = []
        if selected_indices:
            with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
                results = list(tqdm(executor.map(process_one, selected_indices), total=len(selected_indices), desc='并发批量生成'))

        # 先输出一份未过滤的原始结果(含 idx)
        import json
        raw_out_path = os.path.join(os.path.dirname(__file__), 'rag_top1_pred_raw.json')
        with open(raw_out_path, 'w', encoding='utf-8') as f:
            json.dump(results, f, ensure_ascii=False, indent=2)
        print(f'已输出原始未过滤结果到: {raw_out_path}')

        # 只保留结果部分,并去除 retrieval_chunks 字段
        idx2result = {idx: {k: v for k, v in r.items() if k != 'retrieval_chunks'} for idx, r in results}
        filtered_results = []
        for idx, item in enumerate(test_data):
            if idx in idx2result:
                filtered_results.append(idx2result[idx])
            elif FILL_UNANSWERED:
                # 未被回答的,补默认内容
                filtered_results.append({
                    "question": item.get("question", ""),
                    "answer": "",
                    "filename": "",
                    "page": "",
                })
        # 输出结构化结果到json
        out_path = os.path.join(os.path.dirname(__file__), 'rag_top1_pred.json')
        with open(out_path, 'w', encoding='utf-8') as f:
            json.dump(filtered_results, f, ensure_ascii=False, indent=2)
        print(f'已输出结构化检索+大模型生成结果到: {out_path}')
    
        

  1. SimpleRAG.setup() :

    • 作用 : RAG系统的初始化。

    • 步骤 :

      1. 加载 all_pdf_page_chunks.json 里的所有知识块。

      2. 调用 EmbeddingModel (其核心是 get_text_embedding.py ),将每个知识块的 content 文本批量转换为向量。

      3. 将知识块和它们对应的向量存入 SimpleVectorStore (一个简单的内存向量数据库) 中。

  2. SimpleRAG.generate_answer() :

    • 作用 : 针对一个问题,执行完整的“检索-生成”流程。

    • 步骤 :

      1. 查询向量化 : 将输入的问题 question 同样转换为向量。

      2. 信息检索 : 在 SimpleVectorStore 中进行相似度搜索,找出与问题向量最相似的 Top-K 个知识块。

      3. 构建Prompt : 这是最关键的环节之一。脚本将用户的原始问题和检索到的Top-K个知识块(包含内容和元数据)一同填入一个Prompt模板中。这个模板会明确指示LLM:

        • 扮演一个专业角色(金融分析助手)。

        • 必须依据提供的上下文(检索到的知识块)来回答。

        • 必须严格按照指定的JSON格式输出 {"answer": "...", "filename": "...", "page": "..."}

      4. 调用LLM生成 : 将构建好的Prompt发送给大语言模型(如Qwen)。

      5. 解析与格式化 : 调用 extract_json_array.py 工具,从LLM返回的可能混杂着其他文本的输出中,稳定地提取出JSON对象,并整理成最终的答案格式。

思考题

能继续从哪些角度更好地理解赛题要求和问题建模?

Baseline的优点和不足?

能从哪些角度来快速调整和上分?(介绍小的优化点即可)

这个Baseline为你提供了一个简单的起点,但远非终点。你可以从以下角度思考,快速调整和提升分数:

  1. 分块策略 (Chunking Strategy) :

    • 目前是按“页”分块,这样做简单但粗糙。是否可以尝试更细粒度的分块,比如按段落、甚至固定长度的句子分块?这会如何影响检索的精度和召回率?

    • 如何处理跨越多个块的表格或段落?是否可以引入重叠(Overlap)分块的策略?

  2. 检索优化 (Retrieval Optimization) :

    • 当前的Top-K检索策略很简单。如果检索回来的5个块中,只有1个是真正相关的,其他4个都是噪音,这会严重干扰LLM。如何提高检索结果的信噪比?

    • 可以引入重排(Re-ranking)模型吗?即在初步检索(召回)出20个候选块后,用一个更强的模型对这20个块进行重新排序,选出最相关的5个。

  3. Prompt工程 (Prompt Engineering) :

    • rag_from_page_chunks.py 中的Prompt是整个生成环节的灵魂。你能设计出更好的Prompt吗?

    • 比如,如何更清晰地指示LLM在多个来源中选择最精确的那一个?如何让它在信息不足时回答“根据现有信息无法回答”,而不是产生幻觉?

  4. 多模态融合 (Multimodal Fusion) :

    • “图片->文字描述”的方案有信息损失。有没有办法做得更好?

    • 可以尝试 多路召回 吗?即文本问题同时去检索文本库和图片库(使用CLIP等多模态向量模型),将检索到的文本和图片信息都提供给一个多模态大模型(如Qwen-VL),让它自己去融合信息并作答。

  5. 升级数据解析方案:从 fitz MinerU

  • 这是至关重要的进阶环节。基础方案所使用的 fitz_pipeline_all.py 仅能提取文本,会遗漏表格、图片等关键信息。

  • 转而使用 mineru_pipeline_all.py 脚本,具体操作如下:

    # 建议在GPU环境下运行
    python mineru_pipeline_all.py
  • 为什么这么做( MinerU 的优势): MinerU 可以对PDF进行深度的版面分析,除了能更精准地提取文本块外,还具备以下功能:

    • 识别表格 :将表格转化为结构化的Markdown或JSON格式。

    • 提取图片 :对文档中的图片进行识别。

    • 图片描述 :(可选)调用多模态模型为提取出的图片生成文字说明。

  • 效果 :这会为后续的RAG流程提供包含表格和图片信息的、更丰富且更精确的上下文,是解决多模态问题的关键举措。

附录:知识点概述

此部分可补充说明理解baseline所必须知道的知识点

RAG (Retrieval-Augmented Generation) : 检索增强生成。它是一种将外部知识库的检索能力与大语言模型的生成能力相结合的技术框架。其核心思想是,当LLM需要回答问题时,不直接让它凭空回答,而是先从一个庞大的知识库(如我们处理好的PDF内容)中检索出最相关的几段信息,然后将这些信息连同问题一起交给LLM,让它“参考”这些材料来生成答案。这极大地提高了答案的准确性、时效性,并解决了LLM的“幻觉”问题。

向量嵌入 (Vector Embeddings) : 这是让计算机理解文本语义的桥梁。Embedding模型(如本方案中的BGE-M3)可以将一段文本(一个词、一句话、一个段落)映射到一个高维的数学向量(比如一个包含1024个数字的列表)。在向量空间中,语义上相近的文本,其对应的向量在空间位置上也更接近。

向量数据库 (Vector Database) : 一个专门用于存储和高效查询海量向量的数据库。当我们把所有知识块都转换成向量后,就需要一个地方来存放它们。当一个新问题到来时,我们将其也转换为向量,然后去向量数据库中进行“相似度搜索”,快速找到与之最“近”的那些知识向量,从而实现语义检索。本Baseline为了简化,用一个内存列表( SimpleVectorStore )模拟了向量数据库的功能。

参考资料

  • PDF文档解析

    • MinerU : 功能强大的文档解析工具,能将PDF精准地转换为结构化的Markdown或JSON。

    • PyMuPDF : 一个高性能的Python PDF处理库。

      • 官方文档

  • 模型部署与服务化

    • Xinference : 一个强大的开源推理框架,能将各种大模型(包括文本、嵌入和多模态模型)部署为服务,并提供兼容OpenAI的API接口。

      • GitHub仓库

  • 文本/多模态向量化模型

    • FlagEmbedding (BGE模型) : 领先的中文文本向量化模型库。

      • GitHub仓库

    • CLIP (Hugging Face实现) : 经典且强大的图文多模态向量化模型。

      • Hugging Face模型文档

    • 通义千问Qwen-VL (多模态) : 阿里巴巴开源的性能强大的多模态对话语言模型。

      • GitHub仓库

  • 向量数据库与检索

    • ChromaDB : 一个简单易用的开源向量数据库。

      • 官方文档

    • FAISS : 由Facebook AI Research开发的高性能向量检索引擎。

  • RAG开发框架

    • LlamaIndex : 一个主流的、用于构建和开发RAG应用的集成化框架。

      • 五分钟入门教程

  • OCR

    • PaddleOCR : 强大的中文OCR工具库。

      • GitHub仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值