❝ 作者
Winnie; 马千里
说在前面
不知不觉已经坚持大模型比赛逾半年,期间学习和收获了很多。非常幸运,在最近的CCF BDCI中荣获全赛道特等奖。遂将方案分享,期望能和大家共同进步。
比赛链接
https://www.datafountain.cn/competitions/1074
赛题针对对话式检索增强进行,简单来说就是给出了多轮历史对话以及最新的问题,期望选手检索相关文档,然后用大模型给出回答。相对于经典的检索增强,该赛题基于多轮对话场景。
数据描述
赛题使用CORAL基准测试数据集,并且提供了来源于wikipedia的相关知识库,相关论文链接为:
https://arxiv.org/pdf/2410.23090
对话结构主要分为以下四种类型:
- 线性递进型 对话随着每一轮的进行,围绕单一主题逐步深入探讨。
- 广度拓展型 对话在深入探讨的同时,围绕一个主题逐渐拓宽讨论范围。
- 灵活探索型 对话在进行中,围绕一个主题自由地探索不同的子话题。
- 话题转换型 对话在展开过程中,出现主题的跳跃。
评价指标
- 评价方法 采用ROUGE-L评价指标。计算方法可参考该文章:
https://zhuanlan.zhihu.com/p/659637538 - 抖个激灵 针对ROUGE-L尝试过很多偷分方式,比如拼接多次输出,但都不如直接用模型输出。
分析完赛题和数据,接下来我们将深入探讨实现方案。
整体流程
结合行业经验,本次比赛我们遵循对话式检索增强最常见的实现流程:
- 问题改写 结合历史对话,改写用户最后的问题。改写后的问题能包含历史对话的信息,使其嵌入也更有代表性,往往能检索到更好的文档。我们的做法是简单将历史对话和问题传给微调的模型,让其进行改写,不再赘述。
- 检索增强 根据改写的问题检索相关文档。针对该题我们设计了一套上下文感知的混合检索技术,下一小节重点介绍。
- 提示词设计 得到相关文档后,将所有信息整合成提示词,传入大模型从而得到答案。后面小节会给出我们常用的提示词结构供大家参考。
上下文感知的混合检索
1.多路召回
在完成问题改写后,我们以召回-精排的方式检索到相关文档。先做检索召回(pointwise),以多种方式召回相关文档,本题中我们最终使用了两种召回方式:
- 嵌入召回 使用嵌入模型,对知识库文档进行嵌入,然后根据改写问题的嵌入,找到和它最接近的一些文档。是最常见的召回方法。文本嵌入方法可以参考以下链接:
https://huggingface.co/spaces/mteb/leaderboard - 关键词召回 嵌入式方法可能会缺失对关键实体的感知。为了弥补这一点,我们在改写问题中提取关键实体,然后在文档中搜索该实体的文档进行召回。相对BM25,我们的方法更快而且能召回更多可能相关的文档。
- 关键词召回示例代码
from joblib import Parallel, delayed
import spacy
def process\_ent(row):
nlp = spacy.load("en\_core\_web\_sm")
return nlp(row['rewrite'])
def process\_ref(row):
if len(row['entity']) == 0:
return []
ref_pattern = '|'.join(re.escape(entity) for entity in row['entity'])
return df_doc.loc[df_doc['ref\_string'].str.contains(ref_pattern), 'idx'].tolist()
# 找到关键实体(仅支持英文)
entitys = Parallel(n_jobs=-1)(delayed(process_ent)(row) for _, row in tqdm(df.iterrows(), total=len(df)))
df['entity'] = [list(set([e.text for e in x.ents if e.label_ in ['PERSON','ORG', 'WORK\_OF\_ART']])) for x in entitys]
# 检索有该实体的所有文档
ref_ids = Parallel(n_jobs=-1)(delayed(process_ref)(row) for _, row in tqdm(df.iterrows(), total=len(df)))
df['ent\_docs'] = ref_ids
2.精排截断
召回文档过多会导致模型难以感知关键信息,所以需要对结果进行筛选。我们将召回文档和问题一起放入交叉编码模型(pairwise):
- 实施方式 使用reranker模型,将召回的文档和问题进行一一匹配计算得到相关性得分,并根据得分截取Top5文档。
- 算法介绍 召回时使用的双编码器Bi-Encoder对句子之间的关系一无所知。而Cross-Encoder会利用自注意力机制不断计算这两个句子之间的交互(注意力),最后接一个分类器输出一个分数(logits)代表相似度(可以经过sigmoid变成一个概率)。
- 示意图
3.上下文感知重排
这是我们拿到赛道一等奖和全赛道特等奖的关键。本节算法目的是重排截取的文档,让最终文档的信息连续且互补(contextual):
- 背景假设 我们假定在对全部的知识库做了切分之后(切分是为了让每个文本块信息密度更高),得到了文档的index。相邻的index可能是从同一片资料中切割出来的,有连续关系。
- 实施方式 在得到五个文档后,我们从这些文档随机抽取一个子集,并遍历所有的排列方式。让排列完的列表和文档输入相似度打分,然后保留得分最高的列表,作为最终提示文档。还有很多打分方式,比如让大模型去评估每个列表是否语义连续、是否包含所需信息。
- 特殊优化 我们做了剪枝操作来进行加速,我们会将列表按index做排序,这样可以保证不会把牛尾放在牛头之前。并且我们做了一个补全操作,比如重排文档中,只有90/92/93这三个index的文档,我们会用启发式算法自动补上91,这又提升了召回上限,几乎保证了不会缺失信息。
- 流程图
Prompt设计技巧
Prompt设计是大模型竞赛重要的一环。有一些常用的技术,这里我们简单给出多轮对话下常用的Prompt格式:
- 注意事项 本题中,召回文档较长,为了防止问题被截断,我们将问题放在中间,但会导致Loss-in-the-middle。具体排列方式可多做尝试。
- Prompt示意
❝ 联系
如有疑问或反馈,欢迎联系944632634@qq.com。
讲在后面
希望我们的方案能够对大家学习大模型有帮助,未来也会继续分享我们在其他比赛的优质方案,希望能跟大家共同进步!