RAG 挑战赛的内容是什么?
任务是创建一个基于公司年报的问答系统。比赛当天的流程简单来说如下:
- 你将获得随机选取的 100 份公司年度报告,并有 2.5 小时的时间来解析这些报告并建立数据库。这些报告都是 PDF 格式,每份最多 1000 页。
- 然后,会生成 100 个随机问题(基于预定义的模板),您的系统必须尽快回答这些问题。
所有问题都必须有明确的答案,例如:
- 是/否;
- 公司名称(在某些情况下为多个公司名称);
- 领导职务头衔、推出的产品;
- 数字指标:收入、商店数量等。
每个答案都必须包含包含答案证据的页面的引用,以确保系统真正得出答案而不是产生幻觉。
制胜系统架构:

除了基本步骤外,获胜的解决方案还包含两个路由器和 LLM 重新排名。
您可以在此处查看我的最佳性能系统生成的问题和答案 。
现在,我将深入探讨构建系统所涉及的每个步骤、一路上遇到的坎坷和挫折,以及在此过程中发现的最佳实践。
RAG 快速指南
RAG(检索增强生成)是一种通过将大型语言模型(LLM)与任意规模的知识库相集成来扩展其功能的方法。
一个基本的RAG系统开发路径包括以下几个阶段:
- 解析:通过收集文档、将其转换为文本格式以及清除不相关的噪音来为知识库准备数据。
- 摄取:创建和填充知识库。
- 检索:构建一个根据用户查询查找并返回相关数据的工具,通常在矢量数据库中使用语义搜索。
- 回答:用检索到的数据丰富用户的提示,将其发送给 LLM,并返回最终答案。
- 解析
要开始填充任何数据库,必须先将 PDF 文档转换为纯文本。PDF 解析是一项极其复杂的任务,充满了无数微妙的困难:
- 保存表结构;
- 保留关键的格式元素(例如标题和项目符号列表);
- 识别多列文本;
- 处理图表、图像、公式、页眉/页脚等。
我遇到的有趣的 PDF 解析问题(但没有时间解决):
- 大型表格有时会旋转 90 度,导致解析器产生乱码和不可读的文本。

- 图表 部分由图像和部分由文本层组成。
- 一些文档存在字体编码问题:从视觉上看,文本看起来不错,但尝试复制或解析它会导致一组无意义的字符。

有趣的是:我单独调查了这个问题,发现文本可以解码——这是一个凯撒密码,每个单词的 ASCII 码移位各不相同。这引发了我许多疑问。如果有人故意加密一份公开的公司报告副本——为什么?如果字体在转换过程中损坏——为什么偏偏是这种方式?
选择解析器
我尝试了大约二十几个 PDF 解析器:
- 利基解析器;
- 信誉良好的;
- 尖端的 ML 训练解析器;
- 具有 API 访问的专有解析器。
我可以自信地说,目前 没有任何解析器能够处理所有细微差别并将 PDF 内容完全返回为文本,而不会丢失部分重要信息。
RAG 挑战赛中表现最佳的解析器竟然是比较知名的 Docling。有趣的是,它的开发团队正是大赛组织者之一 IBM。
解析器定制
尽管 Docling 取得了优异的成绩,但它缺少一些必要的功能。这些功能部分存在,但却存在于单独的配置中,无法合并为一个。
因此,我撸起袖子,彻底检查了库的源代码,并重写了几个方法以满足我的需求,解析后得到了一个包含所有必要元数据的 JSON。使用这个 JSON,我构建了一个 Markdown 文档,其格式得到了修正,并且表格结构几乎完美地从 PDF 格式转换为 MD 格式,甚至转换为 HTML 格式,这在后来被证明是至关重要的。
这个库速度很快,但仍然不足以在个人笔记本电脑上在 2.5 小时内解析 1.5 万个页面。为了解决这个问题,我利用 GPU 加速进行解析,并以每小时 70 美分的价格租用了一台配备 4090 GPU 的虚拟机用于比赛。

Runpod 对于短期 GPU 租赁来说非常方便
解析所有 100 份文档大约需要 40 分钟,根据其他参与者的报告和评论,这是一个 极高 的解析速度。
在此阶段,我们已将报告解析为 JSON 格式。
我们现在可以填充数据库吗?
还没有。首先,我们必须清除文本中的噪音,并对表格进行预处理。
文本清理和表格准备
有时,PDF 中的部分文本解析不正确,包含特定语法,导致可读性和意义降低。我使用了一批包含十几个正则表达式的文本来解决这个问题。

解析不良的文本示例
包含上述凯撒密码的文档也通过正则表达式模式检测到了。我尝试对其进行解码,但即使恢复后,它们仍然包含许多伪影。因此,我干脆将这些文档完全通过OCR进行处理。
表序列化
在大型表格中,度量名称(水平标题)通常距离垂直标题太远,从而削弱了语义连贯性。

垂直和水平标题之间有 1,500 个不相关的标记
这显著降低了块在向量搜索中的相关性(更不用说表格无法完全放入一个块的情况了)。此外,LLM 很难将指标名称与大型表格中的标题匹配,因此可能会返回错误的值。
表序列化成为了解决方案。关于这个主题的研究很少,所以我不得不自己摸索。你可以谷歌搜索“行序列化”、“属性值配对”,或者阅读 这篇研究论文。
序列化的本质是将一个大表转换成一组小的、上下文独立的字符串。
在对提示符和结构化输出模式进行了大量实验之后,我找到了一个解决方案,即使是 GPT-4o-mini 也能几乎无损地序列化大型表格。最初,我以 Markdown 格式将表格输入到 LLM,但后来切换到了 HTML 格式(事实证明,这种格式很有用!)。语言模型对 HTML 格式的理解要好得多,而且它还允许描述包含合并单元格、副标题和其他复杂结构的表格。
要回答“2021 年公司股东权益是多少?”这样的问题,只需向 LLM 提供一句话就足够了,而不需要提供包含大量“噪音”的大型结构。
在序列化过程中,整个表被转换成一组这样的独立块:
subject_core_entity
:股东权益information_block
:2012/3 年度至 2022/3 年度的股东权益分别为 6,374.22 亿日元(2012/3)、5,354.22 亿日元(2013/3)、6,791.60 亿日元(2014/3)、7,825.56 亿日元(2015/3)、5,409.51 亿日元(2016/3)、5,719.83 亿日元(2017/3)、5,112.42 亿日元(2018/3)、5,250.64 亿日元(2019/3)、5,133.35 亿日元(2020/3)、5,777.82 亿日元(2021/3)、12,745.70 亿日元(2022/3)。
在获得表格的序列化版本后,我将其放在原始表格下方作为每个元素的一种文本注释。
您可以在项目的仓库中查看序列化的提示和逻辑: tables_serialization.py
*尽管序列化潜力巨大,但最终获胜的解决方案并没有使用它。我将在文章末尾解释原因。
- 摄入
报告已从 PDF 转换为干净的 Markdown 文本。现在,让我们从中创建数据库。
就术语达成一致
在搜索系统(谷歌搜索、全文搜索、Elastic 搜索、向量搜索等)领域, 文档 是系统作为查询结果返回的单个索引元素。文档可以是句子、段落、页面、网站或图像——具体含义不限。但就我个人而言,这个定义总是让我感到困惑,因为它更常见、更日常的含义是: 文档, 例如报告、合同或证书。
因此,从现在开始,我将使用 document 的日常含义。
我将数据库中存储的元素称为“ 块”,因为我们存储的是简单切片的文本片段。
分块
根据比赛规则,我们必须指定包含相关信息的页面。企业系统也采用同样的方法:参考文献可以验证模型的答案是否是幻觉。
这不仅使系统对用户更加透明,而且简化了开发过程中的调试。
最简单的选择是使用文档的整页作为一个块,因为页面很少超过几千个标记(尽管表序列化可以将页面扩展至五千个)。
但是,让我们再思考一下查询和文档文本块之间的语义一致性。通常,足以作为答案的信息片段不会超过十个句子。
因此,从逻辑上讲,一小段文字内的目标陈述将比在整页弱相关文本中稀释的相同陈述产生更高的相似度得分。
我将每页的文本分成 300 个标记(大约 15 个句子)的块。
为了对文本进行切片,我使用了带有自定义 MD 词典的递归分割器。为了避免丢失两个块之间的信息,我添加了一个小的文本重叠(50 个标记)。
如果你担心重叠不能完全消除切片不当带来的风险,可以谷歌搜索“语义分割器”。如果你打算只插入上下文中找到的块,这一点尤其重要。
然而,切片的精度对我的检索系统几乎没有影响。
每个块在其元数据中存储其 ID 和父页码。
矢量化
我们的块集合已准备好;现在让我们创建矢量数据库 – 或者更确切地说,数据库。100 个数据库,其中 1 个数据库 = 1 个文档。
为什么要把所有公司的信息混在一起,然后再试图把一家公司的收入与另一家公司的收入区分开来呢?答案的目标信息始终严格地包含在单个文档中。
我们只需要确定针对给定的问题查询哪个数据库(稍后会详细介绍)。
为了创建、存储和搜索矢量数据库,我使用了 FAISS。
关于矢量数据库格式
数据库是使用该方法创建的 IndexFlatIP
。
扁平索引的优点在于所有向量都“按原样”存储,无需压缩或量化。搜索使用暴力破解,精度更高。缺点是此类搜索的计算和内存占用显著增加。
如果您的数据库至少有十万个元素,请考虑 IVFFlat 或 HNSW。这些格式速度更快(尽管创建数据库时需要更多资源)。但由于采用了近似最近邻 (ANN) 搜索,速度的提升是以牺牲准确度为代价的。
将所有文档的块分成不同的索引使我能够使用平面数据库。
IP(内积)用于通过余弦相似度计算相关性得分。除了 IP 之外,还有 L2——它通过欧氏距离计算相关性得分。IP 通常能提供更好的相关性得分。
为了将块和查询嵌入到向量表示中,我使用了 text-embedding-3-large。
3.检索
创建数据库后,就该转到 RAG 系统的“R”(检索)部分了。
检索器是一种通用搜索系统,它将查询作为输入并返回包含答案所需信息的相关文本。
在基本实现中,它只是对向量数据库的查询,提取top_n个结果。
这是 RAG 系统中特别关键的一个部分:如果 LLM 没有在查询上下文中收到必要的信息,它就无法提供正确的答案——无论您如何微调解析或答案提示。
垃圾进→垃圾出。
猎犬的素质可以通过多种方式提升。以下是我在比赛中探索的一些方法:
混合搜索:vDB + BM25
混合搜索将基于语义向量的搜索与基于关键词的传统文本搜索 (BestMatch25) 相结合。理论上,它不仅考虑文本的含义,还考虑关键词的精确匹配,从而提高检索准确率。通常,两种方法的结果会合并,并根据综合得分重新排序。
我并不特别喜欢这种方法:在其最小实现中,它往往会降低检索质量而不是提高检索质量。
一般来说,混合搜索是一种很好的技术,可以通过修改输入查询来进一步优化。最简单的方法是,LLM 可以重新表述问题,以消除噪音并增加关键词密度。
如果您对混合搜索有积极的体验,特别是有关潜在问题和解决方案的体验,请在评论中分享。
无论如何,我心中有更有希望的选择,并决定不再进一步探索这个方向。
跨编码器重新排序
使用交叉编码器模型对向量搜索结果进行重新排序似乎很有前景。简而言之,交叉编码器可以提供更精确的相似度得分,但速度较慢。
交叉编码器介于嵌入模型(双编码器)和 LLM 之间。与通过向量表示比较文本(这本身会丢失一些信息)不同,交叉编码器直接评估两个文本之间的语义相似性,从而给出更准确的分数。
但是,查询与每个数据库元素的成对比较花费的时间太长。
因此,跨编码器重新排序仅适用于已经通过向量搜索过滤的一小组块。
在最后一刻,我放弃了这种方法,因为通过 API 提供的跨编码器重排序模型非常稀缺。OpenAI 和其他大型供应商都没有提供这些模型,而且我也不想再费力地管理 API 余额。
但如果你有兴趣尝试跨编码器重排序,我推荐 Jina Reranker。它在基准测试中表现良好,而且 Jina 在注册时会提供大量的请求。
最终,我选择了一个更有吸引力的替代方案:LLM 重新排名!
LLM重新排名
很简单:将文本和问题传递给大语言模型 (LLM),并询问:“这段文字对回答问题有帮助吗?有多大帮助?请将其相关性从 0 到 1 进行评分。”
直到最近,由于强大的 LLM 模型成本高昂,这种方法才变得不可行。但现在我们有了快速、廉价且足够智能的 LLM 模型。
与跨编码器重新排序一样,我们在通过向量搜索进行初始过滤后应用它。
我制定了一个详细的提示,以 0.1 的增量描述了一般准则和明确的相关性标准:
- 0 = 完全不相关:该块与查询没有任何连接或关系。
- 0.1 = 几乎不相关:与查询仅有非常轻微或模糊的联系。
- 0.2 = 非常轻微相关:包含极其微小或间接的联系。
- …
LLM 查询被格式化为具有两个字段的结构化输出:( reasoning
允许模型解释其判断)和 relevance_score
,允许直接从 JSON 中提取而无需额外解析。
我进一步优化了流程,一次发送三页,促使大语言模型同时返回三个分数。这提高了速度,降低了成本,并且略微提高了评分的一致性,因为相邻的文本段落为模型的评估奠定了基础。
使用加权平均值计算校正的相关性得分:
vector_weight = 0.3
, llm_weight = 0.7
理论上,你可以绕过向量搜索,直接把每一页都送去 LLM 考试。有些参与者确实这么做了,而且成功了。然而,我认为使用嵌入的更便宜、更快的过滤器仍然是必要的。对于一份 1000 页的文档(有些文档就是这么大),回答一个问题大约要花费 25 美分——太贵了。
而且,毕竟我们正在参加 RAG 挑战赛,不是吗?
通过 GPT-4o-mini 重新排序,我每个问题的成本不到一美分!这种方法实现了卓越的质量、速度和成本平衡——这正是我选择它的原因。
在此处查看重新排名提示 。
父页面检索
还记得我说过把文本分割成更小的块吗?这里有一个虽小但很重要的警告。
是的,回答所需的核心信息通常集中在一小块中——这正是将文本分成更小的部分可以提高检索质量的原因。
但该页面上的其余文本可能仍然包含次要但仍然重要的细节。
因此,在找到 top_n 个相关块之后,我仅将它们用作指向完整页面的指针,然后将其放入上下文中。这正是我在每个块的元数据中记录页码的原因。
组装查询器

让我们回顾一下最后的检索步骤:
- 将查询矢量化。
- 根据查询向量找到前 30 个相关块。
- 通过块元数据提取页面(记住要进行重复数据删除!)。
- 将页面通过 LLM 重新排序器。
- 调整页面的相关性分数。
- 返回前 10 页,在每页前面添加其编号,然后将它们合并为一个字符串。
我们的猎犬现在已经准备好了!
增强

我们的向量数据库已经搭建完毕,检索器也已准备就绪。RAG 的“R”(检索)部分已经完成,现在我们来处理“A”(增强)部分。A 部分非常简单,主要由 f 字符串和连接组成。
一个有趣的细节是我如何构建即时存储。在多个项目中尝试了不同的方法后,我最终确定了以下方法:
我将提示存储在专用 prompts.py
文件中,通常将提示拆分为逻辑块:
- 核心系统指令;
- Pydantic 模式定义了 LLM 预期的响应格式;
- 用于创建一次性/少量提示的示例问答对;
- 用于插入上下文和查询的模板。
一个小函数可以根据需要将这些块组合成最终的提示配置。此方法可以灵活地测试不同的提示配置(例如,比较不同示例对于一次性提示的有效性)。
某些指令可能会在多个提示中重复出现。以前,更改此类指令意味着需要同步所有使用它们的提示的更新,这很容易导致错误。模块化方法解决了这个问题。现在,我将重复出现的指令放入一个共享块中,并在多个提示中重复使用。
此外,当提示过长时,模块化块可以简化处理。
所有提示都可以在项目存储库中查看: prompts.py
生成
RAG 中的第三部分“G”是最耗费人力的。要在这里达到高质量,需要熟练运用几种基本技巧。
将查询路由到数据库

这是 RAG 系统中最简单但最有用的部分之一。
回想一下,每份报告都有自己独立的矢量数据库。问题生成器的设计使得公司名称始终明确出现在问题中。
我们还有一份所有公司名称的列表(在比赛开始时随 PDF 报告一起提供)。因此,从查询中提取公司名称甚至不需要大语言模型学位:我们只需遍历列表, re.search()
从问题中提取名称,然后将其与相应的数据库进行匹配即可。
在实际场景中,将查询路由到数据库比我们受控的、无菌的条件下更加复杂。很可能,你需要完成一些额外的准备工作:标记数据库,或者使用 LLM 从问题中提取实体并将其与数据库匹配。
但从概念上来说,方法保持不变。
总结一下:
找到名称 → 匹配到数据库 → 仅在该数据库中搜索。搜索空间缩小了 100 倍。
将查询路由到提示

比赛的要求之一是答案格式,每个答案必须简洁,并严格符合数据类型,就像直接存入公司数据库一样。
在每个问题旁边,都会明确给出预期类型int/float
—— 、bool
、str
或list[str]
。
每种类型都涉及 3 至 6 个细微差别,在回应时需要考虑。
例如,如果问题要求提供指标值,则答案必须是纯数字,不能包含注释、货币符号等。对于货币指标,报告中的货币必须与问题中的货币相匹配,并且数字必须规范化 – 报告通常会写出类似“$1352(千)”的内容,而系统必须回复“1352000”。
如何确保大语言模型 (LLM) 同时考虑所有这些细微差别而不犯错误?简而言之:你做不到。你给 LLM 的规则越多,它忽略这些规则的可能性就越大。即使是八条规则,对于目前的 LLM 来说也过于繁琐。模型的认知能力有限,额外的规则会分散它对主要任务——回答提出的问题——的注意力。
从逻辑上讲,我们应该尽量减少每个查询的规则数量。一种方法是将单个查询分解为一系列更简单的查询。
不过,在我们的例子中,我们可以实现一个更简单的解决方案——由于明确提供了预期的响应类型,我们只根据答案类型向提示提供相关的指令集。
我写了四个提示变体,并用简单的方法选择了正确的一个 if else
。
路由复合查询

竞赛中包含一些比较多家公司指标的问题。这类问题不符合其他简单查询的范式,因为它们需要额外的步骤才能回答。
示例问题:
Who has higher revenue, Apple or Microsoft?
让我们想一想:人类将如何完成这项任务?
首先,他们会分别找出每家公司的收入,然后进行比较。
我们将相同的行为嵌入到我们的系统中。
我们将初始比较问题传递给 LLM,并要求它创建更简单的子问题,为每家公司单独提取指标。
在我们的例子中,更简单的子问题是:
What is Apple's revenue?
和 What is Microsoft's revenue?
现在我们可以通过每个公司的标准管道分别处理这些更简单的查询。
收集每个公司的答案后,我们将它们传递到上下文中以回答原始问题。
此模式适用于任何复杂查询。关键在于识别它们并确定必要的子步骤。
思绪之链
CoT 通过让模型在提供最终答案之前“大声思考”,显著提高了答案质量。LLM 不会立即给出答案,而是生成一系列中间推理步骤,最终得出解决方案。
就像人类一样,LLM 在将复杂问题分解成更小、更简单的问题时,能够更好地处理它们。CoT 帮助模型避免遗漏关键细节,系统地处理信息,并得出正确的结论。当上下文包含可能导致模型误入歧途的“陷阱”时,CoT 尤其有用。
你肯定听过这句标志性的短语 Think step by step
。这是最早通过提示来提升答案质量的尝试之一。它实际上催生了花哨的“提示工程”。然而,对于严肃的任务来说,这种通用的指令是不够的。
LLM 有时会“伪造”推理——例如,立即给出答案,然后事后再进行论证,或者制造出一些根本不存在的事实。这个问题在 GPT-4o-mini 或 Llama 8b 等性能较弱的模型中尤为常见。
为了确保推理能力 (CoT) 的有效性,必须清晰地指导模型如何进行推理。解释推理步骤、目标并提供示例。
根据提示指令明确构建推理步骤可以显著增强规则遵循能力。
在 RAG 中,当报告缺少确切的请求指标时,上下文可能包含相似但不完全相同的信息。模型可能会尝试扭曲提供的数据以适应请求的指标。由于模型被训练成提供帮助,因此它们倾向于提供一些答案,而不是承认自己无知。
试图通过仅仅要求模型保持怀疑态度来抵消这种偏见确实有效——但这并非始终有效,也并非在所有情况下都有效。相反,明确地提示模型从不同的角度或视角分析信息会更有帮助。
我特意指示模型专注于评估问题与上下文之间指标的兼容性。这显著减少了幻觉。
具有模糊上下文的推理示例:
问题:
Ritter Pharmaceuticals, Inc. 的研发设备成本是多少?
模型的推理步骤:
- 该问题询问的是 Ritter Pharmaceuticals, Inc. 的“研发设备,按成本计算”。这表明资产负债表中的特定值,代表 专门用于研发的设备的 原始购买价格,没有 任何累计折旧。
- 上下文(第35页)显示“物业及设备净值”为12,500美元。这是 净值 (折旧后),并且 类别范围更广 ,涵盖所有物业及设备,而不仅仅是研发设备。
- 上下文(第37页)还提到“机械设备”的“累计折旧”为11万美元。这代表总 折旧,而不是原始成本。而且,重要的是,它没有明确说明该设备 专用 于研发。
- 这两个指标均 与请求的指标不完全 匹配。“物业及设备净额”过于宽泛,表示折旧价值。“累计折旧”仅显示折旧,不显示成本,且缺乏研发方面的特异性。
- 由于上下文没有提供 仅 研发设备 的 原始成本,并且我们无法做出假设、进行计算或组合信息,因此答案为“N/A”。
结构化输出
结构化输出 (SO) 是一种强制模型以严格定义的格式响应的方法。它通常作为单独的参数传递给 API,例如 Pydantic 或 JSON 模式。
这保证了模型始终返回严格遵守所提供模式的有效 JSON。
字段描述也可以包含在响应模式中。这些描述不会影响结构,但会被 LLM 视为提示的一部分。
例如,这是 LLM 重新排名的 Pydantic 模式:
class RetrievalRankingSingleBlock(BaseModel): """Rank retrieved text block relevance to a query."""reasoning: str = Field(description=("Analysis of the block, identifying key information and how it ""relates to the query"))relevance_score: float = Field(description=("Relevance score from 0 to 1, where 0 is Completely Irrelevant ""and 1 is Perfectly Relevant"))
使用此模式,LLM 始终返回具有两个字段的 JSON——第一个字段是字符串,第二个字段是数字。
CoT SO
上述方法可以理想地相互结合。
在生成过程中,模型会有一个专门用于推理的字段,以及一个用于存储最终答案的单独字段。这使我们能够提取答案,而无需从冗长的推理步骤中进行解析。
思路链可以通过多种方式在结构化输出中实现。例如,您可以使用多个 JSON 字段,每个字段引导模型得出中间结论,这些结论的组合最终将得出正确的最终答案。
然而,由于回答竞赛问题所需的逻辑无法通过一组预定义的分步说明来描述,因此我采用了一种更通用的方法,为模型提供单一推理字段并直接在提示中定义推理序列。
在我回答竞赛问题的主要模式中,只有四个字段:
- step_by_step_analysis — 初步推理(思路链本身)。
- reasoning_summary—— 前一个字段的简明摘要(为了更容易跟踪模型的逻辑)。
- relevant_pages — 报告答案引用的页码。
- final_answer—— 按照比赛要求格式化的简洁答案。
前三个字段在所有四个针对不同答案类型的提示中重复使用。第四个字段每次都有所不同,用于指定答案类型并描述模型需要考虑的具体细微差别。
例如,确保 final_answer 字段始终为数字或“N/A”的操作如下:
final_answer: Union[float, int, Literal['N/A']]
SO 重解析器 (SO Reparser)
并非所有 LLM 都支持结构化输出,以保证完全遵守模式。
如果模型没有专用的结构化输出功能,您仍然可以直接在提示中显示输出模式。模型通常足够智能,在大多数情况下可以返回有效的 JSON。然而,部分答案不可避免地会与模式存在偏差,从而导致代码崩溃。尤其是较小的模型,大约一半的情况不符合规范。
为了解决这个问题,我编写了一个回退方法,使用 来验证模型的响应是否符合模式 schema.model_validate(answer)
。如果验证失败,该方法会将响应发送回 LLM,提示其符合模式。
即使对于 8b 模型,此方法也可以使模式合规性恢复到 100%。
这是 提示本身。
一次性提示
这是另一种常见且相当明显的方法:在提示中添加示例答案对可以提高响应质量和一致性。
我在每个提示中添加了“问题→答案”对,并以结构化输出定义的 JSON 格式写出答案。
该示例同时满足多个目的:
- 演示了示例性的逐步推理过程。
- 进一步明确具有挑战性的情况下的正确行为(帮助重新校准模型的偏差)。
- 说明模型答案应遵循的 JSON 结构(对于缺乏原生 SO 支持的模型特别有用)。
我非常重视这些示例答案的制作。提示中示例的质量可能会提升或降低答案的质量,因此每个示例都必须完全符合指示,并且整体上几乎完美无缺。如果示例答案与指示相矛盾,模型就会变得混乱,从而对性能产生负面影响。
我精心完善了例子中的逐步推理字段,手动调整了每个短语的推理结构和措辞。
指令细化
这部分的劳动强度与整个数据准备阶段相当,因为需要进行无休止的迭代调试、答案校对以及模型推理过程的手动分析。
分析问题
在写提示之前,我彻底研究了回答要求和问题生成器。
一个拥有大语言模型学位的优秀系统的关键在于理解客户需求。通常,这需要深入钻研专业领域,并细致地研究问题。我坚信,除非你清楚地理解问题本身以及如何找到答案,否则不可能为企业创建真正高质量的质量保证系统(如果有人能说服我,我会很高兴)。
这种理解对于澄清用户问题产生的所有隐含含义也是必需的。
让我们考虑一下示例问题 Who is the CEO of ACME inc?
在理想世界中,报告总是会明确地提供答案,不留任何误解的余地:
CEO responsibilities are held by John Doe
RAG 系统会在报告中找到这句话,将其添加到查询上下文中,然后用户就会收到明确的答案: John Doe
然而,我们生活在现实世界中,成千上万的公司以无限的变化表达信息,并带有无数额外的细微差别。
这就引出了一个问题:究竟什么人可以被称为“CEO”?
- 系统应该如何从字面上解释客户的问题?
- 客户是否想知道担任类似管理职务的人员的姓名,或者严格来说是特定职位的人员的姓名?
- 稍微偏离字面解释可以接受吗?偏离多少才算太远?
可能包括以下职位:
- 首席执行官 ——显然,只是缩写而已。
- 董事总经理 (MD)、总裁、执行董事 ——这些称呼不太明显。不同国家/地区对这类职位使用不同的称谓(英国和欧洲地区为 MD,美国和日本地区为总裁,英国、亚洲国家和非营利组织地区为执行董事)。
- 首席运营官、首席执行官、总经理、行政官、代表董事 ——甚至更不明显。根据国家/地区和公司结构的不同,可能没有直接对应首席执行官的职位;这些职位虽然与首席执行官的职位最接近,但在职责和权限上存在不同程度的重叠——从90%到50%不等。
我不确定是否存在一个现成的术语来描述这个问题,但我个人将其称为“解释自由阈值”问题。
当回复形式自由时,解释自由度阈值相对容易解决。在模棱两可的情况下,LLM 会尝试涵盖用户查询中的所有隐含含义,并添加一些说明。
以下是 ChatGPT 响应的真实示例:
根据提供的信息, Ethan Caldwell 是 该公司董事总经理,相当于首席执行官。然而, 由于正在进行的监管调查, 他已被正式停职。虽然他保留了董事总经理的头衔,但 目前并未参与公司运营,领导权已暂时移交给 董事会监督下的高级管理团队。
然而,如果系统架构需要简洁的答案,就像在 RAG 挑战赛中一样,模型在这些情况下的行为将变得不可预测,依赖于其内部的“直觉”。
因此,必须提前定义并校准解释自由度阈值。但由于无法明确定义和量化该阈值,因此必须识别所有主要的边缘情况,制定通用的查询解释规则,并与客户澄清歧义。
除了解释问题之外,还可能出现一般困境。
例如: Did ACME inc announce any changes to its dividend policy?
系统是否应该将报告中缺少的信息解读为尚未宣布任何变更?
Rinat(比赛组织者)可以证实——在比赛准备期间,我向他提出了几十个类似的问题和难题:)
提示创建
比赛开始前一周,题目生成器的代码就公开了。我立即生成了一百道题目,并用它们创建了一个验证集。
手动回答问题非常繁琐,但它在两个关键方面帮助了我:
- 验证集客观地衡量了系统在我改进过程中的质量。通过在这个验证集上运行系统,我监控了它正确回答的问题数量以及它最常犯的错误。这种反馈循环有助于对提示和其他流程组件进行迭代改进。
- 手动分析问题突出了问题和报告中不明显的细节和歧义。这使我能够与 Rinat 明确答复要求,并在提示中清晰地体现这些规则。
我将所有这些澄清都纳入到提示中作为指令集。
指令示例:
答案类型 = 数字
如果提供的指标与问题中提到的货币不同,则返回“N/A”。如果上下文中没有直接说明指标,即使可以根据上下文中的其他指标计算得出,也返回“N/A”。请特别注意上下文中关于指标是以个位、千位还是百万位报告的任何说明,以便相应地调整最终答案中的数字,使其保持不变、添加三个零或六个零。如果值包含在括号中,则请注意;这表示该值为负数。
答案类型 = 姓名
如果问题涉及职位(例如职位变动),请 仅返回 职位名称, 不包含姓名或任何其他信息。新领导职位的任命也应计入职位变动。如果提及与同一职位名称相关的多项变动,则仅返回该职位名称一次。职位名称应始终为单数形式。
如果问题询问的是新推出的产品,请 仅返回 与上下文完全一致的产品名称。新产品或处于测试阶段的产品不计入新推出的产品。
该模型很容易遵循某些指令,但由于偏见而抵制其他指令,并且难以遵循某些指令,从而导致错误。
例如,模型在跟踪测量单位(千、百万)时反复出错,忘记在最终答案后添加必要的零。因此,我通过一个简短的示例补充了该指令:
以千为单位的数字示例:
上下文值:
4970,5 (in thousands $)
最终答案:
4970500
最终,我为每种问题格式设计了提示和几个辅助提示:
- 数字类型问题的最后提示
- 姓名类问题的最后提示
- 姓名类问题的最后提示
- 布尔类型问题的最后提示
- 比较型问题的最后提示(通过多查询路由比较多家公司的答案)
- 解释比较型问题的提示(最初在报告中查找指标)
- LLM 重新排名提示
- SO 重新解析器提示
精心改进的指令,结合一次性训练和 SO CoT,带来了显著的效益。最终的提示完全重新校准了系统中不必要的偏差,并极大地提高了对细微差别的注意力,即使是对性能较弱的模型也是如此。
系统速度
最初,RAG 挑战赛的规则更为严格,要求系统必须在 10 分钟内回答全部 100 个问题,才有资格获得奖金。我认真对待这项要求,并致力于充分利用 OpenAI 的“每分钟代币”速率限制。
即使在 Tier 2 级别,限制也相当宽松——GPT-4o-mini 为每分钟 200 万个令牌,GPT-4o 为每分钟 45 万个令牌。我估算了每个问题的令牌消耗,并以 25 个问题为一批进行处理。系统仅用 2 分钟就完成了全部 100 个问题。
最后,提交解决方案的时间限制大大延长——其他参与者根本无法及时完成:)
系统质量
拥有验证集不仅有助于改进提示,还有利于整个系统。
我将所有关键功能都设置为可配置,以便我能够衡量它们的实际影响并微调超参数。以下是一些配置字段的示例:
class RunConfig:use_serialized_tables: bool = Falseparent_document_retrieval: bool = Falseuse_vector_dbs: bool = Trueuse_bm25_db: bool = Falsellm_reranking: bool = Falsellm_reranking_sample_size: int = 30top_n_retrieval: int = 10api_provider: str = "openai"answering_model: str = "gpt-4o-mini-2024-07-18"
在测试配置时,我惊讶地发现,我曾寄予厚望的表格序列化功能不仅未能提升系统性能,反而略微降低了其效率。显然,Docling 能够很好地解析 PDF 中的表格,检索器能够有效地找到它们,LLM 也无需额外帮助就能充分理解表格的结构。而在页面上添加更多文本只会降低信噪比。
我还为比赛准备了多种配置,以便快速运行各个类别的各种系统。
最终系统在开源和专有模型上均表现出色:Llama 3.3 70b 仅落后 OpenAI 的 o3-mini 几分。即使是小型 Llama 8b,其整体排名也超过了 80% 的参赛者。
- 结论
最终,赢得 RAG 挑战赛并非在于找到一个神奇的解决方案,而是运用系统性的方法,精心组合和微调各种方法,并深入钻研任务细节。关键成功因素在于高质量的解析、高效的检索、智能路由,以及——最值得一提的——LLM 重排序和精心设计的提示,这使得即使使用紧凑的模型也能取得优异的结果。
本次比赛的主要收获很简单: RAG 的魔力在于细节。你对任务了解得越深入,你就能越精确地调整每个流程组件,即使是最简单的技术,你也能获得更大的收益。
我已将所有系统代码 开源共享。其中包含如何自行部署系统以及运行流程各个阶段的说明。
原本连接:
Paragoger衍生者AI训练营。发布者:稻草人,转载请注明出处:https://www.shxcj.com/archives/9715