找回密码
 会员注册
查看: 32|回复: 0

开源RAG个人知识库项目开发分析

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-11 21:07:59 | 显示全部楼层 |阅读模式
前言Hello,大家好,我是GISerLiu😁,一名热爱AI技术的GIS开发者,这个LLM开发基础阶段已经进入尾声了,本文中我们不介绍更多的理论与知识点,而是通过的分析开源项目的解决方案来帮助各位开发者理清自己开发的思路;在本文中作者将通过分析开源项目个人知识库助手:学习这个RAG应用的开发流程,思路以及业务代码;帮助读者能学会如何规划自己的LLM的应用开发思路;一、个人知识库助手1.项目介绍①背景在当今数据量迅速增长的时代,高效管理和检索信息已成为关键技能。为了应对这一挑战以及LLM技术的发展,该项目应运而生,旨在构建一个基于Langchain的个人知识库助手。该助手可以通过高效的信息管理系统和强大的检索功能,为用户提供了一个可靠的信息获取平台。②目标意义核心目标:充分利用大型语言模型在处理自然语言查询方面的优势,并进行定制化开发以满足用户需求,从而实现对复杂信息的智能理解和精确回应。在项目开发过程中,团队深入分析了大型语言模型的潜力和局限,特别是其生成幻觉信息的倾向。为了解决幻觉信息的问题,项目集成了RAG技术,这是一种结合检索和生成的方法。③主要功能信息检索:从大规模数据库或知识库中提取相关信息。快速定位和获取精确的内容。生成式问答:利用检索到的信息生成自然语言回答。提供详细和上下文相关的回答,提高用户体验。知识更新:不断更新和扩充知识库,保持信息的时效性和准确性。支持多种数据源的集成和管理。多领域适用:支持在不同领域和主题下的应用,如技术文档、医学知识、法律咨询等。灵活定制以适应特定行业需求。用户交互:提供自然语言界面,便于用户查询和获取信息。提高用户的搜索效率和满意度。2.项目部署①环境要求CPU:Intel5代处理器(云CPU方面,建议选择2核以上的云CPU服务);阿里云199一年的都可以,也可以自己本机部署;因为是API调用,对电脑性能要求不高;内存(RAM):至少4GB操作系统:Windows、macOS、Linux均可②部署流程这里作者将整个流程打包:gitclonehttps://github.com/logan-zou/Chat_with_Datawhale_langchain.gitcdChat_with_Datawhale_langchain#创建Conda环境condacreate-nllm-universepython==3.9.0#激活Conda环境condaactivatellm-universe#安装依赖项pipinstall-rrequirements.txt12345678运行项目:#Linux系统cdserveuvicornapi:app--reload#Windows系统cdservepythonapi.py1234567或者:pythonrun_gradio.py-model_name='模型名称'-embedding_model='嵌入模型编号'-db_path='知识库文件路径'-persist_path='持久化目录文件路径'1这里记得配置自己的APIKey:③核心思想本项目其实是针对四种大模型API实现了底层封装,基于Langchain搭建了可切换模型的检索问答链,并实现API以及Gradio部署的个人轻量大模型应用;④技术栈LLM层:统一封装了四个大模型,用作底层模型进行调用。数据层:通过选择的Embedding模型API进行向量数据库的创建和向量检索。源数据经过Embedding处理后可以被向量数据库使用。数据库层:基于个人知识库源数据搭建的向量数据库。在本项目中,我们选择了Chroma,当然Faiss也不错。应用层:确定我们的应用有哪些?RAG、工具使用、RPA等。将LLM和具体业务结合使用更佳。在本项目中,我们仅使用RAG,基于LangChain提供的检索问答链基类进行了进一步封装,从而支持不同模型切换以及便捷实现基于数据库的检索问答。服务层:本项目基于FastAPI和Gradio。对于后端,我们使用FastAPI即可,无需改变。前端如果只能使用Python开发,使用Gradio和Streamlit都是快速不错的选择。如果是全栈开发者或企业开发,使用Vue或React可以开发出更专业且美观的应用。当然,这里是因为我们的应用单一,因此只选择了向量数据库。如果还具有其他业务,如列表信息、历史记录、检索信息等,使用结构化数据库也是必要的。向量数据库和结构化数据混合使用更合适。本项目支持本地M3EEmbedding模型和APIEmbedding结合的方式进行向量化;3.应用详解①业务流程1、核心架构llm-universe个人知识库助手是一个典型的RAG项目,通过langchain+LLM实现本地知识库问答,建立了全流程可使用开源模型实现的本地知识库对话应用。该项目当前已经支持使用ChatGPT,星火Spark模型,文心大模型,智谱GLM等大语言模型的接入。作者这里绘制了一个流程图可以看看:😺😺整个RAG过程包括以下操作:用户提出问题(Query)加载和读取知识库文档对知识库文档进行分割对分割后的知识库文本向量化并存入向量库建立索引对用户提问(Query)向量化在知识库文档向量中匹配出与问句(Query)向量最相似的topk个匹配出的知识库文本作为上下文(Context)和问题一起添加到prompt中提交给LLM生成回答(response);这里很好理解,之前文章中,LLM基础学习的第三篇详细讲解了其中的每个步骤;②索引Index三步走战略:文本数据加载和读取文本数据分割文本数据向量化这和我们之前向量数据库搭建的过程差不多;详细内容如下:(1)知识库数据加载和读取该项目使用的是:《机器学习公式详解》PDF版本《面向开发者的LLM入门教程第一部分PromptEngineering》md版本《强化学习入门指南》MP4版本以及datawhale总仓库所有开源项目的readme:https://github.com/datawhalechina大家可以根据自己的实际业务选择自己的需要的数据,数据存放在../../data_base/knowledge_db目录下,用户可以将自己的文件存放到这里;项目官方提供了拉去官网readme的爬虫:importjsonimportrequestsimportosimportbase64importlogurufromdotenvimportload_dotenv#加载环境变量load_dotenv()#从环境变量中获取TOKENTOKEN=os.getenv('TOKEN')#定义获取组织仓库的函数defget_repos(org_name,token,export_dir):headers={'Authorization':f'token{token}',}url=f'https://api.github.com/orgs/{org_name}/repos'response=requests.get(url,headers=headers,params={'per_page':200,'page':0})ifresponse.status_code==200:repos=response.json()loguru.logger.info(f'Fetched{len(repos)}repositoriesfor{org_name}.')#使用export_dir确定保存仓库名的文件路径repositories_path=os.path.join(export_dir,'repositories.txt')withopen(repositories_path,'w',encoding='utf-8')asfile:forrepoinrepos:file.write(repo['name']+'\n')returnreposelse:loguru.logger.error(f"Errorfetchingrepositories:{response.status_code}")loguru.logger.error(response.text)return[]#定义拉取仓库README文件的函数deffetch_repo_readme(org_name,repo_name,token,export_dir):headers={'Authorization':f'token{token}',}url=f'https://api.github.com/repos/{org_name}/{repo_name}/readme'response=requests.get(url,headers=headers)ifresponse.status_code==200:readme_content=response.json()['content']#解码base64内容readme_content=base64.b64decode(readme_content).decode('utf-8')#使用export_dir确定保存README的文件路径repo_dir=os.path.join(export_dir,repo_name)ifnotos.path.exists(repo_dir)s.makedirs(repo_dir)readme_path=os.path.join(repo_dir,'README.md')withopen(readme_path,'w',encoding='utf-8')asfile:file.write(readme_content)else:loguru.logger.error(f"ErrorfetchingREADMEfor{repo_name}:{response.status_code}")loguru.logger.error(response.text)#主函数if__name__=='__main__':#配置组织名称org_name='datawhalechina'#配置export_direxport_dir="../../database/readme_db"#请替换为实际的目录路径#获取仓库列表repos=get_repos(org_name,TOKEN,export_dir)#打印仓库名称ifrepos:forrepoinrepos:repo_name=repo['name']#拉取每个仓库的READMEfetch_repo_readme(org_name,repo_name,TOKEN,export_dir)#清理临时文件夹#ifos.path.exists('temp'):#shutil.rmtree('temp')123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869以上默认会把这些readme文件放在同目录database下的readme_db文件。其中这些readme文件含有不少无关信息;😏再运行database/text_summary_readme.py文件可以调用大模型生成每个readme文件的摘要并保存到上述知识库目录/data_base/knowledge_db/readme_summary文件夹中,。代码如下:importosfromdotenvimportload_dotenvimportopenaifromtest_get_all_repoimportget_reposfrombs4importBeautifulSoupimportmarkdownimportreimporttime#Loadenvironmentvariablesload_dotenv()TOKEN=os.getenv('TOKEN')#SetuptheOpenAIAPIclientopenai_api_key=os.environ["OPENAI_API_KEY"]#过滤文本中链接防止大语言模型风控defremove_urls(text):#正则表达式模式,用于匹配URLurl_pattern=re.compile(r'https?://[^\s]*')#替换所有匹配的URL为空字符串text=re.sub(url_pattern,'',text)#正则表达式模式,用于匹配特定的文本specific_text_pattern=re.compile(r'扫描下方二维码关注公众号|提取码|关注|科学上网|回复关键词|侵权|版权|致谢|引用|LICENSE'r'|组队打卡|任务打卡|组队学习的那些事|学习周期|开源内容|打卡|组队学习|链接')#替换所有匹配的特定文本为空字符串text=re.sub(specific_text_pattern,'',text)returntext#抽取md中的文本defextract_text_from_md(md_content):#ConvertMarkdowntoHTMLhtml=markdown.markdown(md_content)#UseBeautifulSouptoextracttextsoup=BeautifulSoup(html,'html.parser')returnremove_urls(soup.get_text())defgenerate_llm_summary(repo_name,readme_content,model):prompt=f"1:这个仓库名是{repo_name}.此仓库的readme全部内容是:{readme_content}\2:请用约200以内的中文概括这个仓库readme的内容,返回的概括格式要求:这个仓库名是...,这仓库内容主要是..."openai.api_key=openai_api_key#具体调用messages=[{"role":"system","content":"你是一个人工智能助手"},{"role":"user","content":prompt}]response=openai.ChatCompletion.create(model=model,messages=messages,)returnresponse.choices[0].message["content"]defmain(org_name,export_dir,summary_dir,model):repos=get_repos(org_name,TOKEN,export_dir)#Createadirectorytosavesummariesos.makedirs(summary_dir,exist_ok=True)forid,repoinenumerate(repos):repo_name=repo['name']readme_path=os.path.join(export_dir,repo_name,'README.md')print(repo_name)ifos.path.exists(readme_path):withopen(readme_path,'r',encoding='utf-8')asfile:readme_content=file.read()#ExtracttextfromtheREADMEreadme_text=extract_text_from_md(readme_content)#GenerateasummaryfortheREADME#访问受限,每min一次time.sleep(60)print('第'+str(id)+'条'+'summary开始')try:summary=generate_llm_summary(repo_name,readme_text,model)print(summary)#WritesummarytoaMarkdownfileinthesummarydirectorysummary_file_path=os.path.join(summary_dir,f"{repo_name}_summary.md")withopen(summary_file_path,'w',encoding='utf-8')assummary_file:summary_file.write(f"#{repo_name}Summary\n\n")summary_file.write(summary)exceptopenai.OpenAIErrorase:summary_file_path=os.path.join(summary_dir,f"{repo_name}_summary风控.md")withopen(summary_file_path,'w',encoding='utf-8')assummary_file:summary_file.write(f"#{repo_name}Summary风控\n\n")summary_file.write("README内容风控。\n")print(f"Errorgeneratingsummaryfor{repo_name}:{e}")#print(readme_text)else:print(f"文件不存在:{readme_path}")#IfREADMEdoesn'texist,createanemptyMarkdownfilesummary_file_path=os.path.join(summary_dir,f"{repo_name}_summary不存在.md")withopen(summary_file_path,'w',encoding='utf-8')assummary_file:summary_file.write(f"#{repo_name}Summary不存在\n\n")summary_file.write("README文件不存在。\n")if__name__=='__main__':#配置组织名称org_name='datawhalechina'#配置export_direxport_dir="../database/readme_db"#请替换为实际readme的目录路径summary_dir="../../data_base/knowledge_db/readme_summary"#请替换为实际readme的概括的目录路径model="gpt-3.5-turbo"#deepseek-chat,gpt-3.5-turbo,moonshot-v1-8kmain(org_name,export_dir,summary_dir,model)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798extract_text_from_md()函数用来抽取md文件中的文本,remove_urls()函数过滤网页链接以及大模型风控词。generate_llm_summary()函数LLM生成每个readme的概括。2.在上述知识库构建完毕之后,../../data_base/knowledge_db目录下就有了目标文件:上面函数本质上起了一个数据爬虫和数据清理的过程;作者这里也贡献几个构建个人GIS知识库的爬虫代码:爬取域名下的所有文字数据#爬取域名下的所有文字数据importrequestsimportreimporturllib.requestfrombs4importBeautifulSoupfromcollectionsimportdequefromhtml.parserimportHTMLParserfromurllib.parseimporturlparseimportosimportpandasaspdimporttiktokenimportopenaiimportnumpyasnpfromastimportliteral_eval#正则表达式模式,用于匹配URLHTTP_URL_PATTERN=r'^http[s]{0,1}://.+#定义OpenAI的API密钥openai.api_key='yourapikey'#定义要爬取的根域名domain="leafletjs.com"full_url="https://leafletjs.com/"#创建一个类来解析HTML并获取超链接classHyperlinkParser(HTMLParser):def__init__(self):super().__init__()#创建一个列表来存储超链接self.hyperlinks=[]#重写HTMLParser的handle_starttag方法以获取超链接defhandle_starttag(self,tag,attrs):attrs=dict(attrs)#如果标签是锚点标签且具有href属性,则将href属性添加到超链接列表中iftag=="a"and"href"inattrs:self.hyperlinks.append(attrs["href"])#函数:从URL获取超链接defget_hyperlinks(url):#尝试打开URL并读取HTMLtry:#打开URL并读取HTMLwithurllib.request.urlopen(url)asresponse:#如果响应不是HTML,则返回空列表ifnotresponse.info().get('Content-Type').startswith("text/html"):return[]#解码HTMLhtml=response.read().decode('utf-8')exceptExceptionase:print(e)return[]#创建HTML解析器,然后解析HTML以获取超链接parser=HyperlinkParser()parser.feed(html)returnparser.hyperlinks#函数:获取在同一域内的URL的超链接defget_domain_hyperlinks(local_domain,url):clean_links=[]forlinkinset(get_hyperlinks(url)):clean_link=None#如果链接是URL,请检查是否在同一域内ifre.search(HTTP_URL_PATTERN,link):#解析URL并检查域是否相同url_obj=urlparse(link)ifurl_obj.netloc==local_domain:clean_link=link#如果链接不是URL,请检查是否是相对链接else:iflink.startswith("/"):link=link[1:]elif(link.startswith("#")orlink.startswith("mailto:")orlink.startswith("tel:")):continueclean_link="https://"+local_domain+"/"+linkifclean_linkisnotNone:ifclean_link.endswith("/"):clean_link=clean_link[:-1]clean_links.append(clean_link)#返回在同一域内的超链接列表returnlist(set(clean_links))#函数:爬取网页defcrawl(url):#解析URL并获取域名local_domain=urlparse(url).netloc#创建一个队列来存储要爬取的URLqueue=deque([url])#创建一个集合来存储已经看过的URL(无重复)seen=set([url])#创建一个目录来存储文本文件ifnotos.path.exists("text/")s.mkdir("text/")ifnotos.path.exists("text/"+local_domain+"/")s.mkdir("text/"+local_domain+"/")#创建一个目录来存储CSV文件ifnotos.path.exists("processed")s.mkdir("processed")#当队列非空时,继续爬取whilequeue:#从队列中获取下一个URLurl=queue.pop()print(url)#用于调试和查看进度#尝试从链接中提取文本,如果失败则继续处理队列中的下一项try:#将来自URL的文本保存到.txt文件中withopen('text/'+local_domain+'/'+url[8:].replace("/","_")+".txt","w",encoding="UTF-8")asf:#使用BeautifulSoup从URL获取文本soup=BeautifulSoup(requests.get(url).text,"html.parser")container=soup.find(class_="container")ifcontainerisnotNone:text=container.get_text()else:text=""##获取文本但去除标签#text=soup.get_text()#如果爬虫遇到需要JavaScript的页面,它将停止爬取if("YouneedtoenableJavaScripttorunthisapp."intext):print("由于需要JavaScript,无法解析页面"+url)#否则,将文本写入到文本目录中的文件中f.write(text)exceptExceptionase:print("无法解析页面"+url)#获取URL的超链接并将它们添加到队列中forlinkinget_domain_hyperlinks(local_domain,url):iflinknotinseen:queue.append(link)seen.add(link)crawl(full_url)#函数:移除字符串中的换行符defremove_newlines(serie):serie=serie.str.replace('\n','')serie=serie.str.replace('\\n','')serie=serie.str.replace('','')serie=serie.str.replace('','')returnserie#创建一个列表来存储文本文件texts=[]#获取文本目录中的所有文本文件forfileinos.listdir("text/"+domain+"/"):#打开文件并读取文本withopen("text/"+domain+"/"+file,"r",encoding="UTF-8")asf:text=f.read()#忽略前11行和后4行,然后替换-、_和#update为空格texts.append((file[11:-4].replace('-','').replace('_','').replace('#update',''),text))123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145爬取域名下所有文字数据的Selenium版本(爬取某些反爬网页有奇效,就是慢一点)importrequestsfrombs4importBeautifulSoupfrommsedge.selenium_toolsimportEdge,EdgeOptionsfromselenium.webdriver.support.uiimportWebDriverWaitfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.common.byimportByfromurllib.parseimporturlparseimportosfromcollectionsimportdequeimportre#Selenium配置options=EdgeOptions()#禁用地理位置请求prefs={"profile.default_content_setting_values.geolocation":2}options.add_experimental_option("prefs",prefs)options.add_argument('--ignore-certificate-errors')options.use_chromium=True#options.add_argument("--headless")options.add_argument("--disable-gpu")options.add_argument("--disable-extensions")#正则表达式模式,用于匹配URLHTTP_URL_PATTERN=r'^http[s]{0,1}://.+#定义要爬取的根域名domain="python.langchain.com"base_url="https://python.langchain.com/docs/"#函数:从URL获取超链接defget_hyperlinks(url,browser):try:browser.get(url)#WebDriverWait(browser,10).until(EC.presence_of_element_located((By.TAG_NAME,"body")))WebDriverWait(browser,10).until(EC.presence_of_element_located((By.CLASS_NAME,"menu")))html=browser.page_sourceexceptExceptionase:print(e)return[]soup=BeautifulSoup(html,'html.parser')links=[a.get('href')forainsoup.find_all('a',href=True)]returnlinks#函数:获取在同一域内的URL的超链接defget_domain_hyperlinks(local_domain,base_url,url,browser):clean_links=[]forlinkinset(get_hyperlinks(url,browser)):clean_link=None#检查链接是否为完整的URLifre.search(HTTP_URL_PATTERN,link):url_obj=urlparse(link)ifurl_obj.netloc==local_domain:#检查链接是否是基础URL或其锚点的变化iflink.startswith(base_url)or(url_obj.path==urlparse(base_url).pathandurl_obj.fragment):clean_link=linkelse:#处理相对链接iflink.startswith("/"):complete_link="https://"+local_domain+linkifcomplete_link.startswith(base_url):clean_link=complete_linkeliflink.startswith("#"):#处理锚点链接clean_link=base_url+linkeliflink.startswith("mailto:")orlink.startswith("tel:"):continueifclean_linkisnotNone:clean_links.append(clean_link)returnlist(set(clean_links))#函数:保存文本到文件defsave_text_to_file(url,text,local_domain):#检查文本长度,如果太短或为空则不保存iflen(text.strip())标签的内容main_content=soup.find('div',class_="theme-doc-markdownmarkdown")ifmain_contentisnotNone:text=main_content.get_text()else:continue#保存文本到文件save_text_to_file(url,text,local_domain)exceptExceptionase:print("无法解析页面:",url,";错误:",e)continueforlinkinget_domain_hyperlinks(local_domain,base_url,url,browser):iflinknotinseen:queue.append(link)seen.add(link)finally:browser.quit()crawl(base_url)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112其中有mp4格式,md格式,以及pdf格式,对这些文件的加载方式,该项目将代码放在了project/database/create_db.py文件下,部分代码如下。其中pdf格式文件用PyMuPDFLoader加载器,md格式文件用UnstructuredMarkdownLoader加载器:fromlangchain.document_loadersimportUnstructuredFileLoaderfromlangchain.document_loadersimportUnstructuredMarkdownLoaderfromlangchain.text_splitterimportRecursiveCharacterTextSplitterfromlangchain.document_loadersimportPyMuPDFLoaderfromlangchain.vectorstoresimportChroma```python#首先实现基本配置```pythonDEFAULT_DB_PATH="../../data_base/knowledge_db"DEFAULT_PERSIST_PATH="../../data_base/vector_db".........deffile_loader(file,loaders):ifisinstance(file,tempfile._TemporaryFileWrapper):file=file.nameifnotos.path.isfile(file):[file_loader(os.path.join(file,f),loaders)forfinos.listdir(file)]returnfile_type=file.split('.')[-1]iffile_type=='pdf':loaders.append(PyMuPDFLoader(file))eliffile_type=='md':pattern=r"不存在|风控"match=re.search(pattern,file)ifnotmatch:loaders.append(UnstructuredMarkdownLoader(file))eliffile_type=='txt':loaders.append(UnstructuredFileLoader(file))return123456789101112131415161718192021222324252627282930(2)文本分割和向量化文本分割和向量化将上述载入的知识库文本或进行token长度进行分割,该项目利用Langchain中的文本分割器根据chunk_size(块大小)和chunk_overlap(块与块之间的重叠大小)进行分割。chunk_size指每个块包含的字符或Token(如单词、句子等)的数量chunk_overlap指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息tip:可以设置一个最大的Token长度,然后根据这个最大的Token长度来切分文档。这样切分出来的文档片段是一个一个均匀长度的文档片段。而片段与片段之间的一些重叠的内容,能保证检索的时候能够检索到相关的文档片段。这部分文本分割代码也在project/database/create_db.py文件,该项目采用了langchain中RecursiveCharacterTextSplitter文本分割器进行分割。代码如下:defcreate_db(files=DEFAULT_DB_PATH,persist_directory=DEFAULT_PERSIST_PATH,embeddings="openai"):"""该函数用于加载PDF文件,切分文档,生成文档的嵌入向量,创建向量数据库。参数:file:存放文件的路径。embeddings:用于生产Embedding的模型返回:vectordb:创建的数据库。"""iffiles==None:return"can'tloademptyfile"iftype(files)!=list:files=[files]loaders=[][file_loader(file,loaders)forfileinfiles]docs=[]forloaderinloaders:ifloaderisnotNone:docs.extend(loader.load())#切分文档text_splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=150)split_docs=text_splitter.split_documents(docs)12345678910111213141516171819202122232425而在切分好知识库文本之后,需要对文本进行向量化,文本向量化代码文件路径是project/embedding/call_embedding.py,文本嵌入方式可选本地m3e模型,以及调用openai和zhipuai的api的方式进行文本嵌入。代码如下:importosimportsyssys.path.append(os.path.dirname(os.path.dirname(__file__)))sys.path.append(r"../../")fromembedding.zhipuai_embeddingimportZhipuAIEmbeddingsfromlangchain.embeddings.huggingfaceimportHuggingFaceEmbeddingsfromlangchain.embeddings.openaiimportOpenAIEmbeddingsfromllm.call_llmimportparse_llm_api_keydefget_embedding(embedding:str,embedding_key:str=None,env_file:str=None):ifembedding=='m3e':returnHuggingFaceEmbeddings(model_name="moka-ai/m3e-base")ifembedding_key==None:embedding_key=parse_llm_api_key(embedding)ifembedding=="openai":returnOpenAIEmbeddings(openai_api_key=embedding_key)elifembedding=="zhipuai":returnZhipuAIEmbeddings(zhipuai_api_key=embedding_key)else:raiseValueError(f"embedding{embedding}notsupport")12345678910111213141516171819202122读者也可自行配置Emebdding模型;(3)向量数据库在对知识库文本进行分割和向量化后,就需要定义一个向量数据库用来存放文档片段和对应的向量表示了,在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。该项目选择chromadb向量数据库(类似的向量数据库还有faiss😏等)。定义向量库对应的代码也在/database/create_db.py文件中,persist_directory即为本地持久化地址,vectordb.persist()操作可以持久化向量数据库到本地,后续可以再次载入本地已有的向量库。完整的文本分割,获取向量化,并且定义向量数据库代码如下:defcreate_db(files=DEFAULT_DB_PATH,persist_directory=DEFAULT_PERSIST_PATH,embeddings="openai"):"""该函数用于加载PDF文件,切分文档,生成文档的嵌入向量,创建向量数据库。参数:file:存放文件的路径。embeddings:用于生产Embedding的模型返回:vectordb:创建的数据库。"""iffiles==None:return"can'tloademptyfile"iftype(files)!=list:files=[files]loaders=[][file_loader(file,loaders)forfileinfiles]docs=[]forloaderinloaders:ifloaderisnotNone:docs.extend(loader.load())#切分文档text_splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=150)split_docs=text_splitter.split_documents(docs)iftype(embeddings)==str:embeddings=get_embedding(embedding=embeddings)#定义持久化路径persist_directory='../../data_base/vector_db/chroma'#加载数据库vectordb=Chroma.from_documents(documents=split_docs,embedding=embeddings,persist_directory=persist_directory#允许我们将persist_directory目录保存到磁盘上)vectordb.persist()returnvectordb12345678910111213141516171819202122232425262728293031323334353637③向量检索和生成进入了RAG的检索和生成阶段,即对问句Query向量化后在知识库文档向量中匹配出与问句Query向量最相似的topk个片段,**检索出知识库文本文本作为上下文Context和问题⼀起添加到prompt中,然后提交给LLM生成回答**。(1)向量数据库检索接下去利用向量数据库来进行高效的检索。向量数据库是一种用于有效搜索大规模高维向量空间中相似度的库,能够在大规模数据集中快速找到与给定query向量最相似的向量。代码如下所示:question="什么是机器学习"sim_docs=vectordb.similarity_search(question,k=3)print(f"检索到的内容数:{len(sim_docs)}")fori,sim_docinenumerate(sim_docs):print(f"检索到的第{i}个内容:\n{sim_doc.page_content[:200]}",end="\n--------------\n")12345运行结果:检索到的内容数:3检索到的第0个内容:导,同时也能体会到这三门数学课在机器学习上碰撞产生的“数学之美”。1.1引言本节以概念理解为主,在此对“算法”和“模型”作补充说明。“算法”是指从数据中学得“模型”的具体方法,例如后续章节中将会讲述的线性回归、对数几率回归、决策树等。“算法”产出的结果称为“模型”,通常是具体的函数或者可抽象地看作为函数,例如一元线性回归算法产出的模型即为形如f(x)=wx+b的一元一次函数。--------------检索到的第1个内容:模型:机器学习的一般流程如下:首先收集若干样本(假设此时有100个),然后将其分为训练样本(80个)和测试样本(20个),其中80个训练样本构成的集合称为“训练集”,20个测试样本构成的集合称为“测试集”,接着选用某个机器学习算法,让其在训练集上进行“学习”(或称为“训练”),然后产出得到“模型”(或称为“学习器”),最后用测试集来测试模型的效果。执行以上流程时,表示我们已经默--------------检索到的第2个内容:→_→欢迎去各大电商平台选购纸质版南瓜书《机器学习公式详解》←_←第1章绪论本章作为“西瓜书”的开篇,主要讲解什么是机器学习以及机器学习的相关数学符号,为后续内容作铺垫,并未涉及复杂的算法理论,因此阅读本章时只需耐心梳理清楚所有概念和数学符号即可。此外,在阅读本章前建议先阅读西瓜书目录前页的《主要符号表》,它能解答在阅读“西瓜书”过程中产生的大部分对数学符号的疑惑。本章也作为12345678910111213141516171819202122232425262728293031(2)大模型llm的调用以该项目project/qa_chain/model_to_llm.py代码为例,**在project/llm/的目录文件夹下分别定义了星火spark,智谱glm,文心llm等开源模型api调用的封装,**并在project/qa_chain/model_to_llm.py文件中导入了这些模块,可以根据用户传入的模型名字进行调用llm。代码如下:defmodel_to_llm(model:str=None,temperature:float=0.0,appid:str=None,api_key:str=None,Spark_api_secret:str=None,Wenxin_secret_key:str=None):"""星火:model,temperature,appid,api_key,api_secret百度问心:model,temperature,api_key,api_secret智谱:model,temperature,api_keyOpenAI:model,temperature,api_key"""ifmodelin["gpt-3.5-turbo","gpt-3.5-turbo-16k-0613","gpt-3.5-turbo-0613","gpt-4","gpt-4-32k"]:ifapi_key==None:api_key=parse_llm_api_key("openai")llm=ChatOpenAI(model_name=model,temperature=temperature,openai_api_key=api_key)elifmodelin["ERNIE-Bot","ERNIE-Bot-4","ERNIE-Bot-turbo"]:ifapi_key==NoneorWenxin_secret_key==None:api_key,Wenxin_secret_key=parse_llm_api_key("wenxin")llm=Wenxin_LLM(model=model,temperature=temperature,api_key=api_key,secret_key=Wenxin_secret_key)elifmodelin["Spark-1.5","Spark-2.0"]:ifapi_key==Noneorappid==NoneandSpark_api_secret==None:api_key,appid,Spark_api_secret=parse_llm_api_key("spark")llm=Spark_LLM(model=model,temperature=temperature,appid=appid,api_secret=Spark_api_secret,api_key=api_key)elifmodelin["chatglm_pro","chatglm_std","chatglm_lite"]:ifapi_key==None:api_key=parse_llm_api_key("zhipuai")llm=ZhipuAILLM(model=model,zhipuai_api_key=api_key,temperature=temperature)else:raiseValueError(f"model{model}notsupport!!!")returnllm1234567891011121314151617181920212223242526(3)prompt和构建问答链接下去来到了最后一步,设计完基于知识库问答的prompt,就可以结合上述检索和大模型调用进行答案的生成。构建prompt的格式如下,具体可以根据自己业务需要进行修改:fromlangchain.promptsimportPromptTemplate#template="""基于以下已知信息,简洁和专业的来回答用户的问题。#如果无法从中得到答案,请说"根据已知信息无法回答该问题"或"没有提供足够的相关信息",不允许在答案中添加编造成分。#答案请使用中文。#总是在回答的最后说“谢谢你的提问!”。#已知信息:{context}#问题:{question}"""template="""使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。{context}问题:{question}有用的回答:"""QA_CHAIN_PROMPT=PromptTemplate(input_variables=["context","question"],template=template)#运行chain并且构建问答链:创建检索QA链的方法RetrievalQA.from_chain_type()有如下参数:123456789101112131415161718参数介绍llm:指定使用的LLMchaintype:RetrievalQA.from_chain_type(chain_type=“map_reduce”),自定义prompt:通过在RetrievalQA.from_chain_type()方法中,指定chain_type_kwargs参数,而该参数:chain_type_kwargs={“prompt”ROMPT}返回源文档:通过RetrievalQA.from_chain_type()方法中指定:return_source_documents=True参数;也可以使用RetrievalQAWithSourceChain()方法,返回源文档的引用(坐标或者叫主键、索引)#自定义QA链self.qa_chain=RetrievalQA.from_chain_type(llm=self.llm,retriever=self.retriever,return_source_documents=True,chain_type_kwargs={"prompt":self.QA_CHAIN_PROMPT})12345问答链效果如下:基于召回结果和query结合起来构建的prompt效果question_1="什么是南瓜书?"question_2="王阳明是谁?"result=qa_chain({"query":question_1})print("大模型+知识库后回答question_1的结果:")print(result["result"])12345大模型+知识库后回答question_1的结果:南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!12result=qa_chain({"query":question_2})print("大模型+知识库后回答question_2的结果:")print(result["result"])123大模型+知识库后回答question_2的结果:我不知道王阳明是谁,谢谢你的提问!12以上检索问答链代码都在project/qa_chain/QA_chain_self.py中,此外该项目还实现了带记忆的检索问答链,两种自定义检索问答链内部实现细节类似,只是调用了不同的LangChain链。完整带记忆的检索问答链条代码project/qa_chain/Chat_QA_chain_self.py如下:fromlangchain.promptsimportPromptTemplatefromlangchain.chainsimportRetrievalQAfromlangchain.vectorstoresimportChromafromlangchain.chainsimportConversationalRetrievalChainfromlangchain.memoryimportConversationBufferMemoryfromlangchain.chat_modelsimportChatOpenAIfromqa_chain.model_to_llmimportmodel_to_llmfromqa_chain.get_vectordbimportget_vectordbclassChat_QA_chain_self:""""带历史记录的问答链-model:调用的模型名称-temperature:温度系数,控制生成的随机性-top_k:返回检索的前k个相似文档-chat_history:历史记录,输入一个列表,默认是一个空列表-history_len:控制保留的最近history_len次对话-file_path:建库文件所在路径-persist_path:向量数据库持久化路径-appid:星火-api_key:星火、百度文心、OpenAI、智谱都需要传递的参数-Spark_api_secret:星火秘钥-Wenxin_secret_key:文心秘钥-embeddings:使用的embedding模型-embedding_key:使用的embedding模型的秘钥(智谱或者OpenAI)"""def__init__(self,model:str,temperature:float=0.0,top_k:int=4,chat_history:list=[],file_path:str=None,persist_path:str=None,appid:str=None,api_key:str=None,Spark_api_secret:str=None,Wenxin_secret_key:str=None,embedding="openai",embedding_key:str=None):self.model=modelself.temperature=temperatureself.top_k=top_kself.chat_history=chat_history#self.history_len=history_lenself.file_path=file_pathself.persist_path=persist_pathself.appid=appidself.api_key=api_keyself.Spark_api_secret=Spark_api_secretself.Wenxin_secret_key=Wenxin_secret_keyself.embedding=embeddingself.embedding_key=embedding_keyself.vectordb=get_vectordb(self.file_path,self.persist_path,self.embedding,self.embedding_key)defclear_history(self):"清空历史记录"returnself.chat_history.clear()defchange_history_length(self,history_len:int=1):"""保存指定对话轮次的历史记录输入参数:-history_len:控制保留的最近history_len次对话-chat_history:当前的历史对话记录输出:返回最近history_len次对话"""n=len(self.chat_history)returnself.chat_history[n-history_len:]defanswer(self,question:str=None,temperature=None,top_k=4):""""核心方法,调用问答链arguments:-question:用户提问"""iflen(question)==0:return"",self.chat_historyiflen(question)==0:return""iftemperature==None:temperature=self.temperaturellm=model_to_llm(self.model,temperature,self.appid,self.api_key,self.Spark_api_secret,self.Wenxin_secret_key)#self.memory=ConversationBufferMemory(memory_key="chat_history",return_messages=True)retriever=self.vectordb.as_retriever(search_type="similarity",search_kwargs={'k':top_k})#默认similarity,k=4qa=ConversationalRetrievalChain.from_llm(llm=llm,retriever=retriever)#print(self.llm)result=qa({"question":question,"chat_history":self.chat_history})#result里有question、chat_history、answeranswer=result['answer']self.chat_history.append((question,answer))#更新历史记录returnself.chat_history#返回本次回答和更新后的历史记录123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899OK,时间有限,分析完毕,各位读者有兴趣可以学习一下,给官方一个Star😀😀😀;文章参考OpenAI官方文档DeepSeek官方文档Mistral官方文档ChatGLM官方文档项目地址Github地址拓展阅读专栏文章如果觉得我的文章对您有帮助,三连+关注便是对我创作的最大鼓励!或者一个star🌟也可以😂.
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 02:48 , Processed in 1.244391 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表