支持私有化部署
AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


怎么短平快地把RAG做好:厦门银行RAG初赛方案分享

发布日期:2025-06-25 07:34:56 浏览次数: 1546
作者:ChallengeHub

微信搜一搜,关注“ChallengeHub”

推荐语

如何在有限资源下快速搭建高效RAG系统?厦门银行比赛实战经验分享,10天冲进Top10的短平快方案。

核心内容:
1. 金融监管问答赛题解析与RAG架构设计
2. 文档预处理七步法:从解析到多模型融合
3. 开源项目TrustRAG实现细节与优化技巧

杨芳贤
53AI创始人/腾讯云(TVP)最具价值专家

前段时间笔者参加了厦门银行的第五届数创金融杯大模型应用挑战赛,其中初赛是金融监管制度智能问答,属于经典RAG问答,具体比赛任务如下:

基于大模型的文档问答,根据输入的问题(如“个人理财产品的销售需满足哪些监管要求?”),基于给定的金融文档库,生成准确、合规的答案。题型包含不定项选择题和问答题。

初赛阶段选手可以使用预训练、微调、RAG等方式完成任务,整体工程需在限定硬件资源下能进行推理(CPU8核、32G内存、24G显存),结果评估采用A/B榜,最终以B榜成绩作为初赛排名依据。

笔者差不多在比赛两周内去冲刺的这个比赛,差不多用时10天左右可以冲到top10内,首先说明哈哈我不是第一,也非常好奇前排方案,所以笔者在这里抛砖引玉分享下初赛思路,欢迎大家交流。

这里说的做好不是做的完美,做完美确实很难,对召回和问答要求更严格,这里其实想的是怎么短平快地把RAG任务做的差不多,或者说效果能够让用户接受。

整体技术架构

我的方案采用了经典的RAG架构,主要包括7个步骤:

  1. 文档加载解析- 把docx文件解析成结构化数据
  2. 文本切块- 把长文档切成适合检索的小块
  3. 问题检索排序- 根据问题找到最相关的文档片段
  4. 指令数据集构造- 构造训练数据
  5. 模型微调- 用LoRA/QLoRA微调Qwen3模型
  6. 模型推理- 生成答案
  7. 结果投票- 多模型融合提升效果

下面详细说说每一步怎么做的。

代码几乎都在TrustRAG项目可以看到,欢迎大家Star:https://github.com/gomate-community/TrustRAG

第一步:文档加载解析

这一步要解决什么问题?:原始的金融监管文档都是docx格式,模型没法直接处理,需要先把内容提取出来,同时统计一些基本信息为后续处理做准备。

具体怎么做的?

  1. 批量扫描文档:递归扫描比赛数据集目录下的所有docx文件
  2. 内容提取:用TrustRAG框架的DocxParser解析文档内容,提取段落文本
  3. Token统计:用Qwen3-8B的tokenizer计算每个文档的token数量
  4. 表格检测:标记哪些文档包含表格(表格信息在金融监管中很重要)
  5. 结构化存储:按文件夹结构保存为JSON格式

具体的实现流程是,我编写了一个脚本来自动化处理整个数据集。该脚本首先会递归遍历指定目录下的所有 .docx 文件。接着,利用TrustRAG框架中的 DocxParser ,高效地提取出每个文档的段落文本,并判断其中是否包含表格。随后,调用Qwen3-8B的 tokenizer 计算出每个文档内容的Token数量。

这里稍微注意的是,你使用什么模型问答最好用它的分词器来统计长度,这样可以比较精准的来设置窗口大小,提高大模型输入token长度预算的利用效率。

最终,所有解析出的内容,连同文件名、Token长度、是否含表格等元数据,被统一保存为JSON格式的文件,并维持了原有的目录结构,方便溯源和管理。此外,脚本还会生成一份详细的Excel统计报告( docx_token_lengths.xlsx ),其中包含了所有文档的Token长度分布、按文件夹统计的聚合信息等,这些都为后续的RAG流程优化提供了直观的数据洞察。整体来说大部分文档token是在1600左右,按照现在普遍大模型的操作是都可以输入的,所以我一开始的时候是每一个问题输入一整篇word,就是我的chunk单位是一个word文档,但是实际情况这个效果,我对大模型推理能力太自信了,现在确实思考很强但是仅通过提示语+上下文模型的幻觉还是很大,导致检索+筛选(针对一个问题筛选出一个word文档)+问答的效果并不好。

输出了什么?

  • output/documents/:按目录结构保存的JSON文档文件
  • output/docx_token_lengths.xlsx:详细统计数据
  • 各种可视化图表:token长度分布图、箱线图等从这个token长度分布来看,我们可以欣慰的看到word还不是很长,说明也不是很复杂,整体下来感觉也是这个样子。 最后我将word解析的结果存成一个Dataframe,后面所有的操作都是在这个解析相对完善的内容上进行操作的,具体结构如下:
results.append({
                "文件名": file_name,
                "文件路径": file_path,
                "Token长度": token_length,
                "是否包含表格": has_table,
                "JSON保存路径": json_path,
                "文件内容": contents,
            })

对于数据处理这里,稍微补充一下就是我发现有些word文档里面是有表格的,但是表格解析之后的结果不好,就是一个是会占用很多token,另一个就是信息基本是无效信息(没有内容的空表格),比如是一些信息表的结构,里面是空白,只是字段定义,后来自己偷懒就默认问题里面没有针对表格进行提问,不知道是否有大佬验证这个事情。

这一步看起来简单,但很关键。统计token长度可以为后续切块以及大模型输入超参数提供依据,检测表格可以针对性处理,结构化存储方便后续批量处理。

第二步:文本切块与数据组织

在处理完文档解析后,我们面临一个核心挑战:原始文档通常篇幅很长,直接用于检索不仅效率低下,而且会严重稀释问题的相关性。,也就是一开始为什么我输入整篇word文档效果不好,是针对问题答案信息密度稀疏,对于大模型捕获答案能力太弱,以及通过整篇word作为单位去做检索排序漏选风险太大等,导致效果不好。

因此,必须将长文档切分成更小、更易于管理的“块”(Chunks)。这一步的目标是在保持语义完整性的前提下,将文本切分为适合向量检索的粒度。为此,我采用了TrustRAG中的SentenceChunker,它能确保切分时不会割裂完整的句子,从而最大程度地保留了原文的语义。同时,为了探索不同粒度对最终效果的影响,我分别实验了256和512两种不同的chunk_size,设置我也分析了768以及1024召回率的实验,以期在检索的精度和召回的广度之间找到最佳平衡。

具体的实现上,我通过执行脚本来完成切块任务。脚本首先会加载上一步生成的Parquet文件,然后遍历每个文档内容,应用SentenceChunker进行智能切分。为了确保每个文件和切块都具有唯一的、可追溯的标识,我利用MD5哈希为它们生成了唯一的ID。这种设计对于后续构建索引、进行信息溯源至关重要。更重要的是,我设计了一套优化的数据存储方案:将切块内容(corpus_chunks)与文件元信息(file_metadata)分离存储。这样做极大地减少了数据冗余,因为一个文件的元信息(如文件名、路径等)无需在它的每一个切块中重复保存,从而提升了存储和查询效率。

最终,这一步骤产出了四种不同用途的Parquet文件,形成了一个结构化、易于查询的数据体系。corpus_chunks存储了所有切块的核心文本;file_metadata记录了每个源文件的详细信息;chunk_to_file_mapping则提供了一个从切块ID到文件ID的快速映射关系;而corpus_chunks_full_view是一个合并了所有信息的完整视图,方便进行复杂的分析和调试。这套精心设计的数据组织方式,不仅为后续的检索和微调步骤提供了高质量的输入,也体现了工程化RAG方案中数据管理的重要性。

具体怎么做的?

  1. 智能切块:基于SentenceChunker的句子级切块,不会把句子切断
  2. 多种块大小:尝试256和512两种token大小,平衡精度和覆盖范围
  3. 唯一标识:为每个文件和切块生成MD5哈希的唯一ID
  4. 优化存储:分离存储切块数据和文件元信息,减少冗余

最后保存的结果有:

  • corpus_chunks_{chunk_size}_hf_optimized.parquet:核心切块数据
  • file_metadata_{chunk_size}.parquet:文件元信息
  • chunk_to_file_mapping_{chunk_size}.parquet:映射关系表
  • corpus_chunks_full_view_{chunk_size}.parquet:完整视图

下面是存储代码:

for idx, row in tqdm(data.iterrows(), total=len(data)):
    file_name = row["文件名"]
    file_path = row["文件路径"]
    token_length = row["Token长度"]
    has_table = row["是否包含表格"]
    json_path = row["JSON保存路径"]
    file_content = row["文件内容"]
    
    # 为每个文件生成唯一ID
    file_id = hashlib.md5(f"{file_name}_{file_path}".encode()).hexdigest()[:12]
    # 获取chunks
    chunks = sentence_chunker.get_chunks(file_content.tolist(), chunk_size=chunk_size)

    # 存储文件元信息(共用信息)
    if file_id notin file_metadata:
        file_metadata[file_id] = {
            "文件名": file_name,
            "文件路径": file_path,
            "Token长度": token_length,
            "是否包含表格": has_table,
            "JSON保存路径": json_path,
            "原始文件句子个数": len(file_content),
            "文件切块列表":chunks,
            "切块数量":len(chunks),
        }

    for chunk_idx, chunk in enumerate(chunks):
        # 为每个chunk生成唯一ID
        chunk_id = hashlib.md5(f"{file_id}_{chunk_idx}_{chunk[:50]}".encode()).hexdigest()[:16]
        # 存储chunk数据
        chunk_data = {
            "chunk_id": chunk_id,
            "text": chunk,
            "file_id": file_id,
            "chunk_index": chunk_idx,
            "chunk_length": len(chunk),
        }
        chunks_data.append(chunk_data)
        
        # 建立映射关系
        chunk_to_file_mapping[chunk_id] = file_id
        
        print(chunk + "\n")
    
    print("====" * 100)

分离存储的设计很重要,可以避免数据冗余,支持高效查询。同时生成多种视图,满足不同场景的需求。

同时为了检验不同切块的效果,我做了召回率检测,下面是效果总结的图表:

我们可以看到一些结论:较小的chunk大小(256)表现最佳,可能因为信息密度更高, Chunk大小768和1024的性能相同,可能已达到性能瓶颈领,另外所有配置的回答率都超过94%,整体性能较好,所以感觉召回的效果其实蛮不错了,那为什么有些问题没有召回出来呢?

确实有些问题太难了,难在答案可能就在一句话里面,并且很难找或者相似内容太多,导致检索的时候答案排序在比较后面

举一个例子:农村信用社稽核工作中,"双线报告"的具体要求是什么?,其实答案关键词在于双线报告,但是这个词其实语义信息室比较薄弱的,同时问题中其他词语属于高频词,导致检索无关内容也很多。

另外还有一些难的情况是有些答案是问有那几条?可能答案分布在多个chunk里面,这个时候chunk是否召回全面以及排序都会影响回答效果

不过也有比较喜人的发现,发现有些问题其实不需要检索文档的,我记得当时直接拿问题去测试模型也能够直接回答,大家可以拿训练集去测试下,看看回答答案是否和官方给出的答案一致。 比如:

{"id":1,"category":"选择题","question":"以下哪些属于柜台债券市场的合法交易品种?","content":"   A. 现券买卖  \n   B. 股指期货  \n   C. 质押式回购  \n   D. 买断式回购  "}

{"id": 1, "category""选择题""answer": ["A""C""D"]}

还有一些问答题,大家可以测试下大模型,设置可以回答出来时哪个文件,因为可能相似内容在不同时间不同银行不同文件提到过

比如:

商业银行对分支机构乡村振兴指标的考核权重最低要求是多少

根据中国银保监会发布的《关于2023年银行业保险业全面推进乡村振兴重点工作的通知》(银保监办发〔2023〕35号),商业银行对分支机构的绩效考核中,乡村振兴相关指标的权重应不低于10%。这是监管部门对金融机构服务乡村振兴战略的最低考核要求。

这个时候,我们可以在提示语设计的时候,加入如果检索信息不能够回答的时候,可以尝试用大模型本身能力去回答,也就是我们RAG经常说的负样本场景,应该怎么选择回答。但是这个效果是否对于这个比赛有提升,没有去查证。

第三步:问题检索与重排序

这是整个RAG流程的核心环节,其目标是从海量的文档切块中,为每一个问题精准地找出最相关的上下文信息。检索的质量直接决定了后续大模型生成答案的上限。面对金融监管这样专业且严谨的领域,单纯依赖关键词匹配或语义相似都存在局限性。关键词检索(如BM25)可能无法理解问题的深层意图,而语义检索(Dense Retrieval)有时又会忽略掉必须精确匹配的专有名词。在比赛的时候,我采用了一种更为强大的混合检索(Hybrid Retrieval)策略,以求取长补短,达到最佳的检索效果,具体实现也在TrustRAG里面

我实现方案深度整合了TrustRAG框架的能力。通过HybridRetriever,我将BM25的词频匹配能力与基于bge-m3bge-large-zh-v1.5等先进嵌入模型的向量检索能力相结合。在配置上,我为BM25和向量检索分别赋予了0.3和0.7的权重,这表明更侧重于语义层面的相似性,同时保留了对关键词的敏感度。检索过程并非一步到位,我首先通过混合检索召回一个包含15个候选文档的初始集合(Top-K=15)。随后,引入了重排序(Reranking)机制,利用bge-reranker-large模型对这15个候选文档进行二次精排。Reranker模型能更精细地评估问题与每个文档之间的相关性,从而筛选出真正高质量的上下文,这对于提升最终答案的准确性至关重要。此外,还针对选择题进行了查询优化,将选项内容补充到查询语句中,为模型提供更丰富的判断依据。

整个流程通过脚本自动化执行,支持通过命令行参数灵活切换chunk_sizeemb_model,以便进行多组对比实验。脚本首先会为所有文本块构建或加载索引,然后依次处理训练集和测试集中的问题,执行“混合检索 -> 重排序”的流程,并将最终的检索结果(包含查询ID、格式化查询和排序后的文档列表)保存为结构化的Excel文件,相当于把结果持久化来提升效率。这种系统性的实验和评估方法,是找到最优RAG配置的关键。

在比赛期间我使用了两个embedding模型和两个不同窗口大小,然后用来交叉组合来提高检索模型的召回率以及构造不同的输入样本为后续模型融合来做铺垫。

执行命令:

python step3_retrieve.py --chunk_size 256 --emb_model bge-large-zh-v1.5
python step3_retrieve.py --chunk_size 512 --emb_model bge-large-zh-v1.5
python step3_retrieve.py --chunk_size 256 --emb_model bge-m3
python step3_retrieve.py --chunk_size 512 --emb_model bge-m3

具体怎么做的?

  1. 混合检索:结合BM25(关键词匹配)和Dense(语义向量)检索

  • BM25权重:0.3(处理精确匹配)
  • Dense权重:0.7(处理语义相似)
  • 重排序:用BGE-reranker对top-15结果重新排序
  • 查询优化:选择题会把选项内容也加到查询中
  • 多模型支持:支持bge-m3和bge-large-zh-v1.5两种embedding模型

其实生成格式为{模型名}_{数据集名}_topk15_chunk{块大小}.xlsx的检索结果文件,包含查询ID、格式化查询和检索结果,虽然有15个,其实后面你选5个或者10个可以灵活的,这里相当于避免重复生成结果

第四步:指令数据集构造

比赛前几天有大部分时间在做few-shot推理,没有做微调,所以自己调整了一些不同版本的提示语,最后选择了其中一个效果比较好的来当做指令构造数据集:

Prompt设计的核心思路

针对选择题和问答题分别设计了专门的prompt:

选择题Prompt特点:

  • 强调"精准分析"和"逐一验证"
  • 要求"与文档原文进行精确对比"
  • 输出格式严格:"A,C,D"这样的格式
  • 禁止基于常识推测,必须基于文档

问答题Prompt特点:

  • 强调"准确、简洁、合规"
  • 五步推理:问题解构→信息检索→内容筛选→答案组织→答案优化
  • 要求使用"规范的金融监管术语"
  • 强调逻辑清晰、层次分明

下面是我使用的提示语,仅供参考:

# 选择题专用 
MULTIPLE_CHOICE_PROMPT_V1 = '''你是一名专业的金融监管制度智能问答助手,专门处理金融监管领域的选择题。

## 核心任务
基于给定的金融监管文档,对选择题进行精准分析,选出所有正确答案。

## 推理步骤
请严格按照以下步骤进行思考:

### 第一步:题目分析
- 识别题目考查的核心监管概念或制度
- 明确题目的关键词和限定条件
- 判断是单选题还是多选题

### 第二步:文档定位
- 在查询返回信息中快速定位相关法规条文
- 识别适用的监管文件和具体条款
- 注意条文的层级关系和适用范围

### 第三步:选项验证
对每个选项进行逐一验证:
- 将选项描述与文档原文进行精确对比
- 判断选项是否完全符合监管规定
- 识别选项中的关键词是否与法规条文一致
- 排除与文档内容不符或部分不符的选项

### 第四步:答案确定
- 列出所有通过验证的正确选项
- 再次核实每个选项的准确性
- 确保没有遗漏或误选

## 输出格式
直接输出正确答案选项,多个答案用英文逗号分隔,如:A,C,D
直接输出答案,不要输出其他内容,不要解释,答案不能为空,要输出答案

## 验证要点
- 选项内容必须与文档原文完全匹配或高度一致
- 不得基于常识或外部知识进行推测
- 优先选择文档中明确表述的内容
- 注意监管条文的精确性和严谨性

* 查询返回信息 * :
{content}

* 用户提问 * :
{question}
'''


# 问答题专用 
OPEN_QUESTION_PROMPT_V1 = '''你是一名专业的金融监管制度智能问答助手,专门处理金融监管领域的问答题。

## 核心任务
基于给定的金融监管文档,对问答题提供准确、简洁、合规的答案。

## 推理步骤
请严格按照以下步骤进行思考:

### 第一步:问题解构
- 识别问题的核心关键词和监管主题
- 分析问题涉及的具体监管领域或制度
- 明确问题的回答范围和深度要求

### 第二步:信息检索
- 在文档中搜索与问题直接相关的条款
- 定位相关的监管要求、处置措施或业务规定
- 收集支撑答案的所有相关信息点

### 第三步:内容筛选
- 提取与问题最相关的核心信息
- 按重要性和逻辑顺序排列信息点
- 剔除与问题无关的冗余内容

### 第四步:答案组织
- 将提取的信息整理成逻辑清晰的回答
- 确保答案覆盖问题的所有关键方面
- 使用准确的金融监管术语表达

### 第五步:答案优化
- 检查答案的完整性和准确性
- 确保表述简洁明了,避免冗余
- 验证答案完全基于给定文档内容

## 答案要求
1. **准确性**:答案必须基于文档内容,不得添加文档外信息
2. **完整性**:回答应涵盖问题的所有关键方面
3. **简洁性**:避免冗长表述,突出核心要点
4. **专业性**:使用规范的金融监管术语
5. **逻辑性**:答案结构清晰,层次分明

## 输出格式
提供简洁明确的文字回答,重点突出监管要求或关键信息。直接输出答案,不要输出无关内容

## 质量检查
- 答案是否完全基于文档内容?
- 是否回答了问题的所有关键点?
- 表述是否专业、准确、简洁?
- 逻辑结构是否清晰易懂?

* 查询返回信息 * :
{content}

* 用户提问 * :
{question}
'''

做了几天,效果还是上不去,就是和前排有个断档,后来自己尝试了微调,下面是构造指令数据集的过程:

  1. 加载检索结果:读取第三步生成的检索文件
  2. 格式化内容:取前6个检索结果,格式化为"【参考文档1】\n{内容}"的形式
  3. 应用Prompt模板:根据题目类型选择对应的prompt
  4. 组合训练样本:{prompt + 检索内容 + 问题} → {标准答案}

为什么只取前6个检索结果?,没有取全部,经过实验发现,太多的检索结果会引入噪音,6个结果在信息量和准确性之间达到了比较好的平衡。并且召回率也比较高

第五步:模型微调

实验证明,微调补齐了和前排的一些差距,并且得到不小的提升,通用的大模型在金融监管领域的表现不够好,需要用领域数据进行微调,让模型更好地理解金融监管术语和逻辑。

个人理解:微调主要对齐了大模型如何结合信息去回答问题,以及问答类型的问题尽量符合训练集的规范,我看到比赛群里大佬也有没有训练的。这个就是看怎么把大模型使用信息的能力发挥出来,前提是检索模型的召回率要不错。

我这里模型使用了Qwen3-8B+Qlora/Lora和Qwen3-14B+Qlora,实测下来微调这两个模型在排行榜分数是差不多的,甚至8B比14B效果要好,

选择不同的配置主要是不同的配置有不同的优势,最后可以通过模型融合提升效果。比如256块大小精度高,512块大小上下文更丰富。可以用来模型融合。

第六步:模型推理

这里没有什么好讲的,主要是来通过超参控制模型的随机性,用训练好的模型对测试集进行推理,生成答案。

推理配置:

  • temperature:0.0(确保输出稳定)
  • max_new_tokens:512(足够生成完整答案)
  • batch_size:1(内存限制)

金融监管问答需要准确性,不需要创造性,所以用贪心解码确保输出稳定。

第七步:结果投票融合

单个模型可能有偏差,通过多模型投票可以提升整体效果。

在机器学习竞赛中,集成学习(Ensemble Learning)是提升模型性能和鲁棒性的常用策略。单个模型,无论其结构多精良、训练多充分,都可能存在自身的偏差和局限性。通过融合多个不同模型(例如,不同chunk_size或不同embedding_model配置下训练出的模型)的预测结果,我们可以有效降低这种风险,获得更稳定、更准确的最终答案。为此,针对这个比赛设计了一套针对选择题和问答题的差异化投票策略,以最大化融合带来的收益。

选择题投票策略:对于答案空间有限的选择题,我采用了经典的“多数投票”(Majority Voting)原则。具体来说,脚本会统计所有模型预测结果中,每个选项(如'A', 'B', 'C')的出现频次。一个选项如果被超过半数的模型选中,则被认定为最终答案。这种策略可以有效过滤掉个别模型的偶然错误。如果在极端情况下,没有任何一个选项获得过半数支持,会退而求其次,选择所有选项中得票数最高的作为最终答案,确保总能得出一个结论。

问答题投票策略:对于问答问题,选择语义相似度最高的作为最终答案

核心经验总结

1. 数据处理要细致

文档解析、切块、检索每一步都很重要。解析内容要精确,切块大小要合适,检索排序要全面,特别是切块策略,既要保持语义完整,又要控制大小。我发现256和512两种块大小各有优势,最好都试试。

2. Prompt工程很关键

针对选择题和问答题设计不同的prompt,强调推理步骤和输出格式,步骤推理可以提高选择题准确率,格式严格可以提升问答的拟合效果。

3. 混合检索效果好

BM25+Dense的混合检索比单一方法效果好,再加上reranker进一步提升。权重或者融合策略设置也很重要。

4. 多模型融合有效

不同配置的模型各有优势,通过投票融合可以显著提升效果。特别是问答题的语义相似度投票,效果很明显。

5. 硬件资源要合理利用

比赛有硬件限制,QLoRA是个好选择,既能微调大模型,又不会爆显存。

还能怎么优化?

  1. 更好的切块策略:可以尝试基于语义的切块,而不是固定长度
  2. 更精细的检索:可以针对不同类型的问题用不同的检索策略
  3. 更复杂的融合:除了简单投票,还可以用学习的方法来融合
  4. 更多的数据增强:可以生成更多的训练数据
  5. 欢迎大家补充交流

总的来说,RAG系统是个系统工程,每一步都很重要。这次比赛让我对RAG有了更深的理解,希望这些经验对大家有帮助。

RAG没有标准答案,更加考验是我们面对不同的任务能够灵活变通的能力,也不要从头开始来,建议可以快速基于已有脚手架能够进行改造,发现问题解决问题。

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询