依赖库安装

! pip install langchain_community langchain langchain_core langchain_text_splitters jieba transformers

背景知识

大模型r是使用大模型进行本地知识的问答,能够解决大模型问答中的知识不足导致的幻觉,在很多行业都有较多的应用。ragg 包括文档解析、索引、召回等一整套应用。
现在已经有了很多rag的开源平台,比如d,fy/fastg。t等,但是为了了解底层我使用langchain这次来做个问答。
langchain框架是基础m的应可以较为简单的完成rag的流程。tion

为了好评测rag的效果,拿了阿里天池上【基于LLM智能问答系统学习赛比赛】作为案例数据源。比赛地址
大家也可以使用私有数据完成整个流程。

比赛数据介绍

提供了80个招股说明书的pdf文件和金融数据库,需要回答相关的问题。因为只是做rag,我么只使用pdf文档数据和相关问题。

整体的方案说明

下面是原始rag的流程图,这个里面有文本切片、向量嵌入、检索召回、对话记录、LLM问答等步骤。这些都可以在langchain里面很简单的实现。
下面具体的代码使用了混合召回(bm52+向量召回),外加rerank模块。
型.

财报文档处理

pdf文档需要先解析成文本,此外招股说明书里面还存在很多表格和图片。langchain默认的pdf 解析库是PyMDF,但是解析的噪声会比较多。

from langchain_community.document_loaders import PyPDFDirectoryLoader

# 加载目录中的所有PDF文件
loader = PyPDFDirectoryLoader("pdf/")
docs = loader.load()
# 查看加载的文档
print(docs)

pdf文件转txt

这个里面使用代码把pdf内容转成txt文件,还要把每个公司名称保存下来。我们直接使用处理后的文本。

langchain rag代码

from langchain_community.document_loaders import PyPDFDirectoryLoader,DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter, CharacterTextSplitter,TokenTextSplitter
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
from langchain_community.retrievers.bm25 import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import  FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_core.documents import Document
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain import PromptTemplate, LLMChain 
from langchain.embeddings import HuggingFaceBgeEmbeddings
import jieba,json
from tqdm import tqdm
import torch
import os
import glob 

文本处理

对text文件去噪,这个里面我去掉了目录和目录前的内容
def clear_text(text,isheader=False):
result=[]
header=True
for line in text.split("\n"):
line=line.replace("#","").strip()
if len(str(line))<2:
continue
if "......" in line or "·······" in line :
header=False
continue
if line in ["招股意向书(封卷稿)","目录"]:
continue
if isheader :
if not header:
result.append(line)
else:
result.append(line)
return "\n".join(result)

获取公司名称,对问题提取公司名称,这块我们也可以基于大模型来提取。

import os
import re
import difflib
company_names =[e.replace(".txt","") for e in os.listdir("txt")]
print(len(company_names))
query="云南沃森生物技术股份有限公司负责产品研发的是什么部门"
query="截至2009年12月31日,兰州海默科技股份有限公司的正式在册员工人数是多少?"
query="根据联化科技股份有限公司招股意见书,精细化工产品与基础化工产品相比有哪些特性?"
query="2008年度、2009年度深圳市铁汉生态环境股份有限公司生态修复工程收入的增长率分别为多少?"
#query="新疆浩源天然气股份有限公司2012 年1-6 月的营业毛利为多少,综合毛利率为多少"
def match_company(query):
company=re.findall("^.*公司",query)[0]
return difflib.get_close_matches(company,company_names,cutoff=0.1)[0]
match_company(query)

文本切片

文本切片,长度500,太长的话有些向量模型无法支持。每个Doc需要加入元数据,方便后续过滤!!


from langchain_core.documents import Document
from langchain_community.document_loaders import TextLoader
#分割文本
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=100,
separators = ["\n","。"],
keep_separator='end',add_start_index=True
)

docs=[]
dds=[]
for f in glob.glob("txt/*.txt"):
#print(f)
f=f.replace("\\","/")
text=open(f,"r",encoding='utf-8').read()
text=clear_text(text)
fname=f.replace("txt/","").replace(".txt","")
doc = Document(page_content=text,metadata={"file":fname})
docs.append(doc)
docs=text_splitter.split_documents(docs)
print(len(docs))
61244
docs[:10]

[Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 0}, page_content='安徽黄山胶囊股份有限公司\nANHUI HUANGSHAN CAPSULE CO., LTD.\n(安徽省旌德县白地洪川)\n保荐人承诺因其为发行人首次公开发行股票制作、出具的文件有虚假 记载、误导性陈述或者重大遗漏,给投资者造成损失的,将先行赔偿投资 者损失。\n发  行  概  况\n发行股票类型:人民币普通股(A 股)\n发行股数:公司拟首次公开发行股票总数不超过2,167 万股,本次公开发行股份占发行后公司股份总数的比例不低于25%;本次公开发行全部为新股发行,老股东不公开发售股份。\n每股面值:人民币1.00 元\n每股发行价格:[    ]元\n预计发行日期:2016 年10 月13 日\n拟上市证券交易所:深圳证券交易所\n发行后总股本:不超过8,667 万股\n本次发行前股东所持股份的流通限制及自愿锁定股份的承诺:'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 359}, page_content='1、公司控股股东、实际控制人之一、董事长余春明,公司股东、实际控制人之一、董事、总经理余超彪,胡建飞、汪红时、叶松林、刘松林、朱观润等5名持有公司股份的董事、监事、高级管理人员承诺(其中,持有公司股份的监事仅承诺第(1)项与第(2)项):(1)自股份公司股票在证券交易所上市交易之日起三十六个月内,不转让或者委托他人管理本次发行前其已持有的股份公司股份,也不由股份公司回购该部分股份。(2)在上述锁定期届满后,若其仍在股份公司任职的,在股份公司任职期间每年转让的股份公司股份不超过其所持有股权总数的25%;自其从股份公司处离职半年内,不转让所持有的股份公司股份;自其申报离任六个月后的十二个月内通过证券交易所挂牌交易出售的股份公司股票数量占其所持有股份公司股票总数的比例不超过50%。(3)所持公司股票在锁定期满后两年内减持的,减持价格不低于发行价;公司上市后6 个月内如公司股票连续20 个交易日的收盘价均低于发行价,或者上市后6 个月期末收盘价低于发行价,本人所持公司股票的锁定期限自动延长6 个月。上述减持价格和股份锁定承诺不因本人职务变更、离职而终止。'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 811}, page_content='上述减持价格和股份锁定承诺不因本人职务变更、离职而终止。(前述发行价指公司首次公开发行股票的发行价格,如果公司上市后因派发现金红利、送股、转增股本、增发新股等原因进行除权、除息的,则按照证券交易所的有关规定作除权除息处理。)'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 924}, page_content='2、公司股东余春禄、张光秀、陈发娣、余志平承诺:自股份公司股票在证券交易所上市交易之日起三十六个月内不转让或者委托他人管理本次发行前其已持有的股份公司股份,也不由股份公司回购该部分股份。\n3、除上述股东外,本次发行前的公司其他67 名自然人股东承诺:自股份公司股票在证券交易所上市交易之日起十二个月内,不转让或者委托他人管理本次发行前其已持有的股份公司股份,也不由股份公司回购该部分股份。\n承诺期限届满后,上述股份可以上市流通和转让。 保荐机构(主承销商):国元证券股份有限公司 招股意向书签署日期:2016 年9 月28 日\n发行人声明\n发行人及全体董事、监事、高级管理人员承诺招股意向书及其摘要不存在虚假记载、误导性陈述或重大遗漏,并对其真实性、准确性、完整性承担个别和连带的法律责任。\n公司负责人和主管会计工作的负责人、会计机构负责人保证招股意向书及其摘要中财务会计资料真实、完整。\n保荐人承诺因其为发行人首次公开发行股票制作、出具的文件有虚假记载、误导性陈述或者重大遗漏,给投资者造成损失的,将先行赔偿投资者损失。'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 1319}, page_content='保荐人承诺因其为发行人首次公开发行股票制作、出具的文件有虚假记载、误导性陈述或者重大遗漏,给投资者造成损失的,将先行赔偿投资者损失。\n中国证监会、其他政府部门对本次发行所做的任何决定或意见,均不表明其对发行人股票的价值或投资者的投资收益作出实质性判断或保证。任何与之相反的声明均属虚假不实陈述。\n根据《证券法》的规定,股票依法发行后,发行人经营与收益的变化,由发行人自行负责,由此变化引致的投资风险,由投资者自行负责。\n投资者若对本招股意向书及其摘要存在任何疑问,应咨询自己的股票经纪人、律师、会计师或其他专业顾问。\n重大事项提示\n一、公司股东关于股份锁定的承诺'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 1601}, page_content='1、公司控股股东、实际控制人之一、董事长余春明,公司股东、实际控制人之一、董事、总经理余超彪,及胡建飞、汪红时、叶松林、刘松林、朱观润等5 名持有公司股份的董事、监事、高级管理人员承诺(其中,持有公司股份的监事仅承诺第(1)项与第(2)项):(1)自股份公司股票在证券交易所上市交易之日起三十六个月内,不转让或者委托他人管理本次发行前其已持有的股份公司股份,也不由股份公司回购该部分股份。(2)在上述锁定期届满后,若其仍在股份公司任职的,在股份公司任职期间每年转让的股份公司股份不超过其所持有股权总数的25%;自其从股份公司处离职半年内,不转让所持有的股份公司股份;自其申报离任六个月后的十二个月内通过证券交易所挂牌交易出售的股份公司股票数量占其所持有股份公司股票总数的比例不超过50%。(3)所持公司股票在锁定期满后两年内减持的,减持价格不低于发行价;公司上市后6个月内如公司股票连续20 个交易日的收盘价均低于发行价,或者上市后6 个月期末收盘价低于发行价,本人所持公司股票的锁定期限自动延长6 个月。上述减持价格和股份锁定承诺不因本人职务变更、离职而终止。'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 2054}, page_content='上述减持价格和股份锁定承诺不因本人职务变更、离职而终止。(前述发行价指公司首次公开发行股票的发行价格,如果公司上市后因派发现金红利、送股、转增股本、增发新股等原因进行除权、除息的,则按照证券交易所的有关规定作除权除息处理。)'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 2167}, page_content='2、公司股东余春禄、张光秀、陈发娣、余志平承诺:自股份公司股票在证券交易所上市交易之日起三十六个月内不转让或者委托他人管理本次发行前其已持有的股份公司股份,也不由股份公司回购该部分股份。\n3、除上述股东外,本次发行前的公司其他67 名自然人股东承诺:自股份公司股票在证券交易所上市交易之日起十二个月内,不转让或者委托他人管理本次发行前其已持有的股份公司股份,也不由股份公司回购该部分股份。\n承诺期限届满后,上述股份可以上市流通和转让。\n二、关于公司上市后三年稳定股价的预案\n为维护公司上市后股价的稳定,公司制定了关于稳定股价预案,该议案已经公司2014 年第一次临时股东大会审议通过;具体内容如下:\n1、启动稳定股价预案的条件\n公司股票自正式挂牌上市之日起三年内,一旦出现连续20 个交易日股票收盘价均低于公司最近一期经审计的每股净资产(若因除权除息等事项致使上述股票收盘价与公司最近一期经审计的每股净资产不具可比性的,上述股票收盘价应做相应调整),公司自该事项发生之日起3 个交易日内启动预案。\n2、稳定股价预案的内容\n公司拟采取的股价稳定预案包括:'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 2616}, page_content='2、稳定股价预案的内容\n公司拟采取的股价稳定预案包括:\n(1)公司回购股票:公司向社会股东回购公司股票,且单一会计年度回购的资金总额不超过上一年度经审计的归属于母公司股东的净利润。\n(2)控股股东增持股票:控股股东通过二级市场增持公司的股票,且单一会计年度内合计增持股票的资金总额不超过上年度从公司领取的分红的80%。\n(3)董事及高级管理人员增持股票:董事(不含独立董事及控股股东余春明)、高级管理人员通过二级市场增持公司的股票,且单一会计年度增持股票的资金总额不超过上年度从公司领取的分红和薪酬的合计值的50%。\n若某一会计年度内公司股价多次达到启动稳定股价预案条件的,上述主体应在前一次股价稳定预案实施完毕3 个月期间届满后,再次实施稳定股价预案。\n实施稳定股价预案后,公司股权分布仍应符合法律法规及交易所规定的上市条件。\n3、稳定股价预案的实施顺序'),

 Document(metadata={'file': '安徽黄山胶囊股份有限公司', 'start_index': 2944}, page_content='实施稳定股价预案后,公司股权分布仍应符合法律法规及交易所规定的上市条件。\n3、稳定股价预案的实施顺序\n公司股价触动预案启动的条件后,第一实施顺序是公司回购股票;第二实施顺序是控股股东增持股票;第三实施顺序是董事及高级管理人员增持股票。公司回购股票资金达到承诺的限额后,公司股价仍未达到下述“5、停止条件”的,则由控股股东履行增持义务;控股股东增持股票资金达到承诺的限额后,公司股价仍未达到下述“5、停止条件”的,则由董事及高级管理人员履行增持义务。\n4、启动股价稳定预案的法律程序\n(1)公司回购股票的实施程序\n公司股东大会应对回购股票做出决议,经出席会议的股东所持表决权的三分之二以上通过。\n股东大会对回购股票做出决议后,公司依法实施回购方案。公司实施回购方案前,应在证券登记结算机构开立由证券交易所监控的回购专用账户。公司回购的股票将于回购期届满或者回购方案实施完毕后依法注销,并办理工商变更登记手续。其他未尽事宜按照相关法律法规的规定执行。\n(2)控股股东、董事及高级管理人员增持股票的实施程序\n公司控股股东、董事和高级管理人员将严格遵守相关法律法规的要求,履行增持股票的要约、禁止交易和公告等法定义务。')]

大模型定义

这个大模型既可以使用本地大模型,也可以使用大模型api服务。下面也给出了二者的代码

##定义本地模型,下面代码可以主流的LLM模型,可以换不同模型对比效果

from langchain.llms.base import LLM
from typing import Any, List, Optional
from langchain.callbacks.manager import CallbackManagerForLLMRun
from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfig, LlamaTokenizerFast
import torch

class Local_LLM(LLM):
# 基于本地 Qwen2 自定义 LLM 类
tokenizer: AutoTokenizer = None
model: AutoModelForCausalLM = None
def __init__(self, mode_name_or_path :str):

super().__init__()
print("正在从本地加载模型...")
self.tokenizer = AutoTokenizer.from_pretrained(mode_name_or_path, use_fast=False,trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(mode_name_or_path, torch_dtype=torch.bfloat16, device_map="auto",trust_remote_code=True)
self.model.generation_config = GenerationConfig.from_pretrained(mode_name_or_path)
print("完成本地模型的加载")
def _call(self, prompt : str, stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any):


messages = [{"role": "user", "content": prompt }]
input_ids = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = self.tokenizer([input_ids], return_tensors="pt").to('cuda')
generated_ids = self.model.generate(model_inputs.input_ids,max_new_tokens=512)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
return response
@property
def _llm_type(self) -> str:
return "Local_LLM"
/opt/conda/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
###使用的是glm4-9b-chat,需要本地挂载进来
llm = Local_LLM(mode_name_or_path = "/input/test/glm4")
print(llm("你是谁"))

正在从本地加载模型...

Loading checkpoint shards: 100%|██████████| 10/10 [02:56<00:00, 17.66s/it]

/tmp/ipykernel_75/3598876242.py:2: LangChainDeprecationWarning: The method `BaseLLM.__call__` was deprecated in langchain-core 0.1.7 and will be removed in 1.0. Use :meth:`~invoke` instead.

  print(llm("你是谁"))

The attention mask is not set and cannot be inferred from input because pad token is same as eos token. As a consequence, you may observe unexpected behavior. Please pass your input's `attention_mask` to obtain reliable results.

完成本地模型的加载

我是一个名为 ChatGLM 的人工智能助手,是基于清华大学 KEG 实验室和智谱 AI 公司于 2024 年共同训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。

##openai 接口的方式初始化LLM模型

from langchain_openai import ChatOpenAI
inference_server_url = "https://api.deepseek.com"
llm = ChatOpenAI(
model="deepseek-chat",
openai_api_key="sk-XXXXX",
openai_api_base=inference_server_url
)
## 初始化embedding模型和ranker模型,都是bge系列模型

embeddings = HuggingFaceEmbeddings(model_name="/input/test/bge-large-zh-v1.5",show_progress=True,model_kwargs={"trust_remote_code": True})

rank_model = HuggingFaceCrossEncoder(model_name="/input/test/bge-reranker-large")
compressor = CrossEncoderReranker(model=rank_model, top_n=4)

文档向量化

## doc文本向量化,需要一定时间

vectordb = FAISS.from_documents(
documents=docs,
embedding=embeddings,)

Batches: 100%|██████████| 1914/1914 [09:35<00:00,  3.33it/s]

##向量数据可以存储下来,后续不需要再计算了
vectordb.save_local("faiss_index")
vectordb = FAISS.load_local("faiss_index", embeddings,allow_dangerous_deserialization=True)

langchain 问答

rag chain的定义,这个里面根据问题的公司名称缩小doc的范围,对于没有公司的则是全部doc召回。

可以尝试不同的召回数量和上下文策略,一起以最终输入到llm的content最好为准。

def format_docs(docs):
return "\n".join(doc.page_content for doc in docs)

def qa_chain(question):
## 抽取出主体信息
pp=""
try:
pp=match_company(question)
docs_=[ e for e in docs if pp in e.metadata["file"]]
except:
pp=""
docs_=docs
## bm25算法召回
bm25_retriever = BM25Retriever.from_documents(
docs_,
k=15,
bm25_params={"k1": 1.5, "b": 0.75},
preprocess_func=jieba.lcut)
## 向量召回
dense_retriever = vectordb.as_retriever(search_kwargs={"k": 15,'filter': {'file':pp},'fetch_k':80000})
ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, dense_retriever], weights=[0.5, 0.5])

compression_retriever = ContextualCompressionRetriever(base_compressor=compressor, base_retriever=ensemble_retriever)
prompt='''你是一个能够精准提取文本信息并能根据文本信息准确回答问题的智能小助手。
下面我会给你几段招股书文本和一个问题,请你根据提供的文本对所给的问题进行回答。
已知相关的部分招股书资料是:{documents}
请你牢记以下回答准则,并且根据上文所给的招股书资料回答问题{question}:
注意在回答的时候请遵守以下准则:
(1).你只能根据我提供给你的招股书资料回答问题,不可以利用文本资料以外的知识进行回答,不可以回答招股书资料以外的答案!
(2).问题的答案基本可以在所给的招股书资料中可以明确找到,但是如果你明确在已有的资料里面查找不到答案,请你严格按照如下格式输出:对不起,我不能根据所给的资料回答问题,请你提供更多的资料!
(3).要确保答案的完整性
'''
custom_rag_prompt = PromptTemplate(
template= prompt
)
rag_chain = (
{"documents": compression_retriever | format_docs, "question": RunnablePassthrough()}
| custom_rag_prompt
| llm
| StrOutputParser()
)
result=rag_chain.invoke(question).replace("**","")
return result

效果测试

拿几个问题测试下效果

qa_chain("根据大连派思燃气系统股份有限公司招股意向书,预计到2030年,天然气发电用气在天然气消费结构中的比例将上升到多少?")

'\n预计到2030年,天然气发电用气在天然气消费结构中的比例将上升到35%。这一信息来源于招股书资料中提到的国际能源机构(IEA)的预测。'

qa_chain("西安启源机电装备股份有限公司董事是谁?")

'\n西安启源机电装备股份有限公司的董事包括以下人员:\n\n1. 赵友安:总经理、党委书记、本公司股东。\n2. 李世坤:中交西安筑路机械有限公司副总经理、本公司股东。\n3. 王学成:中交西安筑路机械有限公司会计师。\n4. 刘杰:沈阳变压器研究院股份有限公司总经理。\n5. 李铁军:陕西伊势威投资有限公司副总经理、财务总监。\n6. 莫会成:全国微电机标准化委员会主任委员、国际微电机质量监督检验中心主任、中国电器工业协会微电机分会理事长、国际微电机实验室主任、西安市青年企业家协会副会长、国防科工委、总装备部、信息产业项目评审专家、西安交通大学、湖南大学兼职教授。\n7. 戎晓明:中国新时代国际工程公司总会计师、本公司控股股东、北京红墙饭店有限公司总经理。\n8. 陈元华:北京华松投资有限公司总经理、上海华觉投资有限公司董事、总经理、本公司股东。\n9. 朱建伟:上海张江高科技园区置业有限责任总经理助理、公司。\n10. 秦金杨:西安启源软件技术有限责任公司总经理、本公司控股子公司。\n\n请注意,以上信息基于所提供的招股书资料。'

qa_chain("江苏爱康太阳能科技股份有限公司拥有的参股子公司是哪些")

'\n根据所提供的招股书资料,发行人拥有的参股子公司是广东爱康。'

评价:模型的输出基本符合预取。如果出现结果无法回答的情况,需要对上面的流程进行改进。

文件预测

##用正则判断哪些问题是跟文档相关的,这里面也可以用大模型做分类

import json,re
from tqdm import tqdm
questions =open("question.json",encoding="utf-8")

cnt=0
result=[]
for question in tqdm(questions.readlines()):
line= json.loads(question)
question=line["question"]
if "发行股票" in question or not re.findall("((?!发行)股票|基金|证券|港股|A股|H股)",question) :
#pp=match_company(question)
ans=qa_chain(question)
#print(ans)
line["answer"]=ans.replace("\n","").replace("*","")
else:
line["answer"]=line["question"]
result.append(line)
100%|██████████| 1000/1000 [35:04<00:00,  2.10s/it] 
import jsonlines

def read_jsonl(path):
content = []
with jsonlines.open(path, "r") as json_file:
for obj in json_file.iter(type=dict, skip_invalid=True):
content.append(obj)
return content

def write_jsonl(path, content):
with jsonlines.open(path, "w") as json_file:
json_file.write_all(content)

write_jsonl("submit.jsonl",result)

将生成的jsonl文件提交到官方网站就可以得到评估结果了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐