使用RAG技术构建企业级文档问答系统之QA抽取


1 概述

从本文开始,将开一个大坑,陆续介绍企业级文档问答系统构建的全流程,以及关键环节的优化手段。重点介绍算法流程。

构建一个基础版的RAG是非常简单的,甚至使用扣子、Dify等平台,熟练的情况下都用不了5分钟,即使使用Langchain、LlamaIndex等框架,搭建完整流程,代码也不会超过100行。但基础版的问答效果往往较差。

下面这张图是OpenAI介绍的RAG优化经验,这个准确率当然随不同的数据集会有不同,但基本上优化后的准确率比优化前有显著提升这个基本上是一致的。

使用RAG技术构建企业级文档问答系统之QA抽取

问答系统构建完成后,总的流程是先对文档进行解析、切分,然后使用问题检索相关知识片段,最后将问题和知识片段输入LLM,生成答案。

在构建的过程中也是一样的,这三个环节是可以分别独立优化的,如下图所示:

使用RAG技术构建企业级文档问答系统之QA抽取

本篇首先专注在如何获取QA数据,所谓的QA数据,就是“问题-回答”数据,理想情况下,如果包含回答所用到的文档片段是更好的。部分系统(如客服系统)是有这方面数据的,但绝大多数情况下是没有的,这时就需要首先构造一批问答数据,这是后续所有环节最重要的一步。

本系列将会使用中国银行所发布的《2024全球经济金融展望报告》作为文档,围绕针对这个文档的问答效果优化展开。

本文所介绍的方法,会使用千问官方的qwen-long模型,对《2024全球经济金融展望报告》这个文档抽取QA,这个模型足够便宜,抽取的结果质量也还不错。QA抽取包含如下3个步骤:

  • 短文档片段QA抽取:这部分模拟日常情况下,经常会询问细节性问题的使用场景

  • 长文档片段QA抽取:这部分模拟需要综合较多上下文才能回答的使用场景

  • QA质量打分:使用LLM再次对抽取的QA进行质量评估,这一步算是借鉴了微软phi-1.5模型Textbooks Are All You Need论文中的方法,就是借助模型对数据质量进行评估

整个过程花费不到1元,结果已经抽取好了,大家可以直接使用。

本文所对应代码已开源,地址在:https://github.com/Steven-Luo/MasteringRAG/blob/main/00_PDF%E8%A7%A3%E6%9E%90%E4%B8%8EQA%E6%8A%BD%E5%8F%96_v1.ipynb

2 准备环境

代码在Google Colab环境下进行了测试,正常情况下,安装Anaconda基本上会包含大部分所用到的包,再安装如下包即可:

pip install langchain langchain_community pypdf openai

为了便于大家复现,打印所安装的版本:

 , , ,      module  (langchain, langchain_community, pypdf, openai):  ()
langchain           0.2.8  langchain_community 0.2.7  pypdf               4.3.0  openai              1.35.14

设置API key

     os.environ[] =   os.environ[] =

3 文档解析与切分

   PyPDFLoader     Document     RecursiveCharacterTextSplitter        uuid4     (documents, filepath, chunk_size=, chunk_overlap=, seperators=[, ], force_split=):   os.path.exists(filepath)   force_split:  ()   pickle.load((filepath, ))        splitter = RecursiveCharacterTextSplitter(          chunk_size=chunk_size,          chunk_overlap=chunk_overlap,          separators=seperators      )      split_docs = splitter.split_documents(documents)   chunk  split_docs:          chunk.metadata[] = (uuid4())        pickle.dump(split_docs, (filepath, ))     split_docs    loader = PyPDFLoader()  documents = loader.load()      pattern =   merged_docs = [Document(page_content=.join(re.sub(pattern, , doc.page_content)  doc  documents))]    splitted_docs = split_docs(documents, os.path.join(output_dir, ), chunk_size=, chunk_overlap=)  splitted_docs_large = split_docs(merged_docs, os.path.join(output_dir, ), chunk_size=, chunk_overlap=)  uuid2doc = {doc.metadata[]: doc.page_content  doc  splitted_docs}  uuid2large_doc = {doc.metadata[]: doc.page_content  doc  splitted_docs_large}

4 QA抽取

既然是构造QA,那最好是保留回答问题时所使用的上下文,方便后续环节的优化。

4.1 QA抽取Prompt

这一步核心的2个Prompt如下:

qa_gen_prompt_tmpl =                             {{document}}            qa_gen_prompt_tmpl_large_context =                 {{document}}

4.2 QA抽取代码

抽取核心代码,此处使用多线程加速抽取,考虑到网络请求异常情况会比较多,因此增加失败重试机制,同时考虑到这是一个耗时操作,并保存中间结果,以确保失败或者再次运行时,已经执行过的部分不会被重复执行:

   OpenAI                 tqdm       client = OpenAI(      api_key=os.environ[],      base_url=os.environ[]  )     (prompt_tmpl, text):      prompt = prompt_tmpl.replace(, text).strip()   prompt     (prompt, max_retry=, debug=, top_p=, temperature=):   (prompt):          completion = client.chat.completions.create(              model=,              messages=[                    {: , : prompt}              ],              top_p=top_p,              temperature=temperature          )   completion.choices[].message.content     max_retry > :  :   do_chat(prompt)     e:              max_retry -=               sleep_seconds = random.randint(, )   debug:  ()              time.sleep(sleep_seconds)          (splitted_docs, prompt_tmpl, qa_ckpt_filename):      qa_ckpt = {}   os.path.exists(qa_ckpt_filename):          qa_ckpt = (qa_ckpt_filename).readlines()          qa_ckpt = [json.loads(line.strip())  line  qa_ckpt  line.strip() != ]          qa_ckpt = {item[]: item  item  qa_ckpt}  ()        file_lock = threading.Lock()        max_workers =    concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)  executor:          futures = {doc.metadata[]: executor.submit(chat, build_qa_prompt(prompt_tmpl, doc.page_content), , )  doc  splitted_docs  (doc.page_content.replace(, )) >=   doc.metadata[]   qa_ckpt}   uuid  tqdm(futures):              future = futures[uuid]              result = future.result()   result  :                  item = {: uuid, : result}              qa_ckpt[uuid] = item                  file_lock.acquire()    :   (qa_ckpt_filename, )  f:                      f.write(json.dumps(item, ensure_ascii=) + )     e:  (e)  :                  file_lock.release()   qa_ckpt      detailed_qa_dict = gen_qa(splitted_docs, qa_gen_prompt_tmpl, os.path.join(output_dir, ))    large_context_qa_dict = gen_qa(splitted_docs_large, qa_gen_prompt_tmpl_large_context, os.path.join(output_dir, ))

4.3 抽取样例

  [      {  : ,  : ,  :       },      {  : ,  : ,  :       },      {  : ,  : ,  :       },      {  : ,  : ,  :       },      {  : ,  : ,  :       },      {  : ,  : ,  :       },      {  : ,  : ,  :       },      {  : ,  : ,  :       }  ]

4.4 后置处理

从上面的样例可以看出,结果是被json...包裹的,没有办法直接解析为JSON,使用正则表达式进行后置处理,提取JSON

           (text):      pattern =         text = text.replace(, )  :   json.loads(text)  :          match = re.search(pattern, text, re.DOTALL)  :              matched = match.group()   json.loads(matched)     e:  ()     []     (qa_ckpt, uuid2doc_map):      data = []     key, value  tqdm(qa_ckpt.items()):          text = value[]          qa_list = convert2json(text)     item  qa_list:              question = item.get(, ).strip()              answer = item.get(, ).strip()              context = item.get(, ).strip()     question ==   answer == :  (qa_list)                data.append({  : key,  : question,  : answer,  : context,  : uuid2doc_map[key]              })      qa_df = pd.DataFrame(data)   qa_df    qa_df = build_qa_df(detailed_qa_dict, uuid2doc)  qa_df.drop_duplicates(, inplace=)  qa_df[] =   large_context_qa_df = build_qa_df(large_context_qa_dict, uuid2large_doc)  large_context_qa_df.drop_duplicates(, inplace=)  large_context_qa_df[] =     qa_df = pd.concat([qa_df, large_context_qa_df])

5 QA质量检查

这部分就是对qa_df中的问题-回答对,再打一次分,然后过滤低分结果,Prompt如下:

qa_check_prompt_tmpl =                       {{question}}          {{answer}}

总体又是一个循环,与QA抽取部分非常相似,此处不再粘贴代码,需要的朋友们请访问代码仓库。

5.1 打分结果样例

5.2 3分样例

  • 问:报告中提到的主要经济体GDP增速变化趋势的图的名称是什么?

  • 答:主要经济体GDP增速变化趋势

  • 上下文:图2:主要经济体GDP增速变化趋势(%)

5.3 2分样例

  • 问:消费者借贷能力和意愿受到什么因素的影响?

  • 答:美国家庭债务余额拖欠率回升至3%

  • 上下文:美国家庭债务余额拖欠率回升至3%,消费者借贷能力和意愿将有所下降。

可以看出,低分问答对,质量确实相对较低

5.4 最终数据集构建

这部分首先保留4分及以上的问答对,然后随机挑选100条数据作为后续的测试集。至此,准备工作完成。

hq_qa_df = qa_df[qa_df[] >= ]  test_q = hq_qa_df.sample(, replace=)[].values.tolist()  hq_qa_df[] =   hq_qa_df.loc[hq_qa_df[].isin(test_q), ] =     hq_qa_df.to_excel(os.path.join(output_dir, ), index=)

RAG技术前沿技术新闻资讯

GraphRAG + GPT-4o mini 低成本构建 AI 图谱知识库

2025-3-27 11:12:07

RAG技术前沿技术新闻资讯

GraphRAG失效?快用Prompt Tune适配文档的领域和语言

2025-3-27 13:31:47

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
搜索