微信扫码
添加专属顾问
我要投稿
如何在有限资源下快速搭建高效RAG系统?厦门银行比赛实战经验分享,10天冲进Top10的短平快方案。 核心内容: 1. 金融监管问答赛题解析与RAG架构设计 2. 文档预处理七步法:从解析到多模型融合 3. 开源项目TrustRAG实现细节与优化技巧
前段时间笔者参加了厦门银行的第五届数创金融杯大模型应用挑战赛,其中初赛是金融监管制度智能问答,属于经典RAG问答,具体比赛任务如下:
基于大模型的文档问答,根据输入的问题(如“个人理财产品的销售需满足哪些监管要求?”),基于给定的金融文档库,生成准确、合规的答案。题型包含不定项选择题和问答题。
初赛阶段选手可以使用预训练、微调、RAG等方式完成任务,整体工程需在限定硬件资源下能进行推理(CPU8核、32G内存、24G显存),结果评估采用A/B榜,最终以B榜成绩作为初赛排名依据。
笔者差不多在比赛两周内去冲刺的这个比赛,差不多用时10天左右可以冲到top10内,首先说明哈哈我不是第一,也非常好奇前排方案,所以笔者在这里抛砖引玉分享下初赛思路,欢迎大家交流。
这里说的做好不是做的完美,做完美确实很难,对召回和问答要求更严格,这里其实想的是怎么短平快地把RAG任务做的差不多,或者说效果能够让用户接受。
我的方案采用了经典的RAG架构,主要包括7个步骤:
下面详细说说每一步怎么做的。
代码几乎都在TrustRAG项目可以看到,欢迎大家Star:https://github.com/gomate-community/TrustRAG
这一步要解决什么问题?:原始的金融监管文档都是docx格式,模型没法直接处理,需要先把内容提取出来,同时统计一些基本信息为后续处理做准备。
具体怎么做的?
比赛数据集
目录下的所有docx文件具体的实现流程是,我编写了一个脚本来自动化处理整个数据集。该脚本首先会递归遍历指定目录下的所有 .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
:详细统计数据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方案中数据管理的重要性。
具体怎么做的?
最后保存的结果有:
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-m3
或bge-large-zh-v1.5
等先进嵌入模型的向量检索能力相结合。在配置上,我为BM25和向量检索分别赋予了0.3和0.7的权重,这表明更侧重于语义层面的相似性,同时保留了对关键词的敏感度。检索过程并非一步到位,我首先通过混合检索召回一个包含15个候选文档的初始集合(Top-K=15)。随后,引入了重排序(Reranking)机制,利用bge-reranker-large
模型对这15个候选文档进行二次精排。Reranker模型能更精细地评估问题与每个文档之间的相关性,从而筛选出真正高质量的上下文,这对于提升最终答案的准确性至关重要。此外,还针对选择题进行了查询优化,将选项内容补充到查询语句中,为模型提供更丰富的判断依据。
整个流程通过脚本自动化执行,支持通过命令行参数灵活切换chunk_size
和emb_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
具体怎么做的?
其实生成格式为{模型名}_{数据集名}_topk15_chunk{块大小}.xlsx
的检索结果文件,包含查询ID、格式化查询和检索结果,虽然有15个,其实后面你选5个或者10个可以灵活的,这里相当于避免重复生成结果
比赛前几天有大部分时间在做few-shot推理,没有做微调,所以自己调整了一些不同版本的提示语,最后选择了其中一个效果比较好的来当做指令构造数据集:
Prompt设计的核心思路
针对选择题和问答题分别设计了专门的prompt:
选择题Prompt特点:
问答题Prompt特点:
下面是我使用的提示语,仅供参考:
# 选择题专用
MULTIPLE_CHOICE_PROMPT_V1 = '''你是一名专业的金融监管制度智能问答助手,专门处理金融监管领域的选择题。
## 核心任务
基于给定的金融监管文档,对选择题进行精准分析,选出所有正确答案。
## 推理步骤
请严格按照以下步骤进行思考:
### 第一步:题目分析
- 识别题目考查的核心监管概念或制度
- 明确题目的关键词和限定条件
- 判断是单选题还是多选题
### 第二步:文档定位
- 在查询返回信息中快速定位相关法规条文
- 识别适用的监管文件和具体条款
- 注意条文的层级关系和适用范围
### 第三步:选项验证
对每个选项进行逐一验证:
- 将选项描述与文档原文进行精确对比
- 判断选项是否完全符合监管规定
- 识别选项中的关键词是否与法规条文一致
- 排除与文档内容不符或部分不符的选项
### 第四步:答案确定
- 列出所有通过验证的正确选项
- 再次核实每个选项的准确性
- 确保没有遗漏或误选
## 输出格式
直接输出正确答案选项,多个答案用英文逗号分隔,如:A,C,D
直接输出答案,不要输出其他内容,不要解释,答案不能为空,要输出答案
## 验证要点
- 选项内容必须与文档原文完全匹配或高度一致
- 不得基于常识或外部知识进行推测
- 优先选择文档中明确表述的内容
- 注意监管条文的精确性和严谨性
* 查询返回信息 * :
{content}
* 用户提问 * :
{question}
'''
# 问答题专用
OPEN_QUESTION_PROMPT_V1 = '''你是一名专业的金融监管制度智能问答助手,专门处理金融监管领域的问答题。
## 核心任务
基于给定的金融监管文档,对问答题提供准确、简洁、合规的答案。
## 推理步骤
请严格按照以下步骤进行思考:
### 第一步:问题解构
- 识别问题的核心关键词和监管主题
- 分析问题涉及的具体监管领域或制度
- 明确问题的回答范围和深度要求
### 第二步:信息检索
- 在文档中搜索与问题直接相关的条款
- 定位相关的监管要求、处置措施或业务规定
- 收集支撑答案的所有相关信息点
### 第三步:内容筛选
- 提取与问题最相关的核心信息
- 按重要性和逻辑顺序排列信息点
- 剔除与问题无关的冗余内容
### 第四步:答案组织
- 将提取的信息整理成逻辑清晰的回答
- 确保答案覆盖问题的所有关键方面
- 使用准确的金融监管术语表达
### 第五步:答案优化
- 检查答案的完整性和准确性
- 确保表述简洁明了,避免冗余
- 验证答案完全基于给定文档内容
## 答案要求
1. **准确性**:答案必须基于文档内容,不得添加文档外信息
2. **完整性**:回答应涵盖问题的所有关键方面
3. **简洁性**:避免冗长表述,突出核心要点
4. **专业性**:使用规范的金融监管术语
5. **逻辑性**:答案结构清晰,层次分明
## 输出格式
提供简洁明确的文字回答,重点突出监管要求或关键信息。直接输出答案,不要输出无关内容
## 质量检查
- 答案是否完全基于文档内容?
- 是否回答了问题的所有关键点?
- 表述是否专业、准确、简洁?
- 逻辑结构是否清晰易懂?
* 查询返回信息 * :
{content}
* 用户提问 * :
{question}
'''
做了几天,效果还是上不去,就是和前排有个断档,后来自己尝试了微调,下面是构造指令数据集的过程:
为什么只取前6个检索结果?,没有取全部,经过实验发现,太多的检索结果会引入噪音,6个结果在信息量和准确性之间达到了比较好的平衡。并且召回率也比较高
实验证明,微调补齐了和前排的一些差距,并且得到不小的提升,通用的大模型在金融监管领域的表现不够好,需要用领域数据进行微调,让模型更好地理解金融监管术语和逻辑。
个人理解:微调主要对齐了大模型如何结合信息去回答问题,以及问答类型的问题尽量符合训练集的规范,我看到比赛群里大佬也有没有训练的。这个就是看怎么把大模型使用信息的能力发挥出来,前提是检索模型的召回率要不错。
我这里模型使用了Qwen3-8B+Qlora/Lora和Qwen3-14B+Qlora,实测下来微调这两个模型在排行榜分数是差不多的,甚至8B比14B效果要好,
选择不同的配置主要是不同的配置有不同的优势,最后可以通过模型融合提升效果。比如256块大小精度高,512块大小上下文更丰富。可以用来模型融合。
这里没有什么好讲的,主要是来通过超参控制模型的随机性,用训练好的模型对测试集进行推理,生成答案。
推理配置:
金融监管问答需要准确性,不需要创造性,所以用贪心解码确保输出稳定。
单个模型可能有偏差,通过多模型投票可以提升整体效果。
在机器学习竞赛中,集成学习(Ensemble Learning)是提升模型性能和鲁棒性的常用策略。单个模型,无论其结构多精良、训练多充分,都可能存在自身的偏差和局限性。通过融合多个不同模型(例如,不同chunk_size
或不同embedding_model
配置下训练出的模型)的预测结果,我们可以有效降低这种风险,获得更稳定、更准确的最终答案。为此,针对这个比赛设计了一套针对选择题和问答题的差异化投票策略,以最大化融合带来的收益。
选择题投票策略:对于答案空间有限的选择题,我采用了经典的“多数投票”(Majority Voting)原则。具体来说,脚本会统计所有模型预测结果中,每个选项(如'A', 'B', 'C')的出现频次。一个选项如果被超过半数的模型选中,则被认定为最终答案。这种策略可以有效过滤掉个别模型的偶然错误。如果在极端情况下,没有任何一个选项获得过半数支持,会退而求其次,选择所有选项中得票数最高的作为最终答案,确保总能得出一个结论。
问答题投票策略:对于问答问题,选择语义相似度最高的作为最终答案
文档解析、切块、检索每一步都很重要。解析内容要精确,切块大小要合适,检索排序要全面,特别是切块策略,既要保持语义完整,又要控制大小。我发现256和512两种块大小各有优势,最好都试试。
针对选择题和问答题设计不同的prompt,强调推理步骤和输出格式,步骤推理可以提高选择题准确率,格式严格可以提升问答的拟合效果。
BM25+Dense的混合检索比单一方法效果好,再加上reranker进一步提升。权重或者融合策略设置也很重要。
不同配置的模型各有优势,通过投票融合可以显著提升效果。特别是问答题的语义相似度投票,效果很明显。
比赛有硬件限制,QLoRA是个好选择,既能微调大模型,又不会爆显存。
总的来说,RAG系统是个系统工程,每一步都很重要。这次比赛让我对RAG有了更深的理解,希望这些经验对大家有帮助。
RAG没有标准答案,更加考验是我们面对不同的任务能够灵活变通的能力,也不要从头开始来,建议可以快速基于已有脚手架能够进行改造,发现问题解决问题。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-06-25
饶了我吧,别再吹RAG了!搞AI agent,先把企业知识库文档切明白了再说...
2025-06-24
AGI|RAG进化论:从青铜到王者,一文看懂 RAG 全家族
2025-06-24
别再质问AI大模型胡说了!RAG能搞定它
2025-06-24
详解RAG五种分块策略,技术原理、优劣对比与场景选型之道
2025-06-24
Agentic RAG:下一代智能检索与生成的革命性突破
2025-06-24
饶了我吧,别再吹RAG了!搞AI agent,先把企业知识库文档切明白了再说...
2025-06-24
RAG的2025趋势重点及RAG+抽取场景的来源定位问题思考
2025-06-23
一文搞懂大模型的RAG(知识库和知识图谱)
2025-03-28
2025-04-01
2025-04-13
2025-04-19
2025-04-09
2025-04-16
2025-05-08
2025-04-05
2025-04-01
2025-04-23
2025-06-20
2025-06-19
2025-06-13
2025-06-09
2025-06-06
2025-05-30
2025-05-29
2025-05-29