赞
踩
接下来,我们开始在web框架上整合 LangChain、OpenAI、FAISS等。
因为项目是基于PDF文档的,所以需要一些操作PDF的库,我们这边使用的是PyPDF2
- from PyPDF2 import PdfReader
-
-
- # 获取pdf文件内容
- def get_pdf_text(pdf):
- text = ""
- pdf_reader = PdfReader(pdf)
- for page in pdf_reader.pages:
- text += page.extract_text()
-
- return text
传入 pdf 文件路径,返回 pdf 文档的文本内容。
首先我们需要将第一步拿到的本文内容拆分,我们使用的是 RecursiveCharacterTextSplitter ,默认使用 ["\n\n","\n"," "] 来分割文本。
- from langchain.text_splitter import RecursiveCharacterTextSplitter
-
-
- # 拆分文本
- def get_text_chunks(text):
- text_splitter = RecursiveCharacterTextSplitter(
- chunk_size=1000,
- # chunk_size=768,
- chunk_overlap=200,
- length_function=len
- )
- chunks = text_splitter.split_text(text)
- return chunks
其中这里 chunk_size 参数要注意,这里是指文本块的最大尺寸,如果用chatgpt3.5会在问答的时候容易出现token长度超过4096的异常,这个后面会说如何调整,只需要换一下模型就好了。
这个参数对于向量化来说,比较重要,因为到时候喂给OpenAI去分析的时候,携带的上下文内容就会比较多,这样准确性和语义分析上也有不少的帮助。
项目使用 FAISS,就是将 pdf 读取到的文本向量化以后,通过 FAISS 保存到本地,后续就不需要再执行向量化,就可以读取之前的备份。
- from langchain.vectorstores import FAISS
- from langchain.embeddings import OpenAIEmbeddings
-
-
- # 保存
- def save_vector_store(textChunks):
- db = FAISS.from_texts(textChunks, OpenAIEmbeddings())
- db.save_local('faiss')
-
-
- # 加载
- def load_vector_store():
- return FAISS.load_local('faiss', OpenAIEmbeddings())
-
其中 faiss 参数为保存的目录名称,默认在项目同级目录下生成。
这里使用 OpenAI 的方法 OpenAIEmbeddings 来进行向量化。
- from langchain.chains import RetrievalQA
- from langchain.chat_models import ChatOpenAI
- from langchain.prompts import PromptTemplate
-
-
- # 获取检索型问答链
- def get_qa_chain(vector_store):
- prompt_template = """基于以下已知内容,简洁和专业的来回答用户的问题。
- 如果无法从中得到答案,清说"根据已知内容无法回答该问题"
- 答案请使用中文。
- 已知内容:
- {context}
- 问题:
- {question}"""
-
- prompt = PromptTemplate(template=prompt_template,
- input_variables=["context", "question"])
-
- return RetrievalQA.from_llm(llm=ChatOpenAI(model_name='gpt-3.5-turbo-16k'), retriever=vector_store.as_retriever(), prompt=prompt)
-
1)RetrievalQA 检索行问答链
这里使用 RetrievalQA,这种链的缺点是一问一答,是没有history的,是单轮问答。
2)自定义提示 PromptTemplate
这里还是使用到自定义提示 PromptTemplate,主要作用是使 OpenAI 能根据我们传入的向量文本为蓝本,限制它的回答范围,并要求使用中文回答。这样的好处在于,如果我们问一些非 pdf 涉及的内容,OpenAI 会返回无法作答,而不是根据自己的大模型数据来回答问题。
3)llm 模型
我们还是用 Chat 模型作为 llm 的输入模型,这里可以看到,我们使用的 model 为 gpt-3.5-turbo-16k,它可以支持 16384 个tokens,而 gpt-3.5-turbo 只支持 4096 个tokens
所以这里就回答了上面文本拆分器 chunk_size 参数,如果使用 gpt-3.5-turbo 模型,笔者尝试过,最大可能就是只能到 768,不过这个具体要看向量化以后,携带的文本的大小tokens而定。
不过使用 gpt-3.5-turbo-16k 也是有代价的,就是它比 gpt-3.5-turbo 要贵,大概是2倍的价格。
可调整项目下的 init_pdf.py 文件,修改 pdf_path 为本地的pdf文件路径,然后执行脚本,如果控制输出 xxx is ok ,则代表处理成功。
- # 将pdf切分块,嵌入和向量存储
- if __name__ == '__main__':
- pdf_path = './test/demo.pdf'
-
- pfdEngine = PdfEngine(pdf_path)
- raw_text = pfdEngine.get_pdf_text()
-
- llmEngine = LlmEngine()
- text_chunks = llmEngine.get_text_chunks(raw_text)
-
- faissEngine = FaissEngine()
- faissEngine.save_vector_store(text_chunks)
-
- print(pdf_path + ' is ok')
此时会看到多了一个 faiss 目录,为向量数据库保存的文件。
我们将上面实现的内容整合到 chat.py 路由,主要实现 基于向量化的 pdf 文档内容进行问答。
- from fastapi import APIRouter, Body, Request
-
- from app.tool.llm import LlmEngine
- from app.tool.store import FaissEngine
-
- router = APIRouter(
- prefix="/chat"
- )
-
-
- @router.post("/question")
- async def question(
- text: str = Body(embed=True)
- ):
- faiss = FaissEngine()
- vector_store = faiss.load_vector_store()
-
- llm = LlmEngine()
- chain = llm.get_qa_chain(vector_store)
-
- response = chain({"query": text})
- # reply = "回复:"
- return {'success': True, "code": 0, "reply": response}
1)配置 OpenAI
因为项目使用到 OpenAI 的接口,所以我们这边需要配置 api-key,还有我们云函数上的代理地址,项目默认读取了 .env 文件。
如果不需要使用代理,可项目 tool 目录下的 llm.py 和 store.py 两个脚本的对应配置。
要注意配置的 api_base 地址后面,一般云函数地址的后面要加上 /v1
至此,一个简单的基于 LangChain 库的 PDF文档问答就完成了,我们随便拿一份网上能找到保险pdf做个实验,看看效果如何
我们就来问 pdf 中的这段内容,问题是 "风险的特征有哪些?"
我们来看看回复,几个大的要点也基本答上来了,效果也算可以了。
- {
- "success": true,
- "code": 0,
- "reply": {
- "query": "风险的特征有哪些?",
- "result": "风险的特征包括以下几个方面:\n1. 风险的客观性:风险是一种客观存在,与人的意志无关,独立于人的意识之外的客观存在。\n2. 风险的普遍性:在社会经济生活中,人们面临各种各样的风险,从个人、企业到国家和政府机关都无处不在。\n3. 风险的损害性:风险与人们的经济利益密切相关,会给人们的经济造成损失以及对人的生命造成伤害。\n4. 某一风险发生的不确定性:虽然风险是客观存在的,但对某一具体风险而言,其发生是偶然的,是一种随机现象。\n5. 总体风险发生的可测性:虽然个别风险事故的发生是偶然的,但大量风险事故往往呈现出明显的规律性,可以通过统计方法进行准确测量。"
- }
- }
我们再尝试问一些不在 pdf 里的问题,"如何评价中国足球"
- {
- "success": true,
- "code": 0,
- "reply": {
- "query": "如何评价中国足球",
- "result": "根据已知内容无法回答该问题。"
- }
- }
这跟我们上面 自定义提示 PromptTemplate 的内容是一致的。
最后附上 仓库地址
-------------------------------------------------------------------------------------------------------------------------------
- from langchain.chains import RetrievalQA, ConversationalRetrievalChain
- from langchain.chat_models import ChatOpenAI
- from langchain.prompts import PromptTemplate
- from langchain.text_splitter import RecursiveCharacterTextSplitter
-
-
- # 获取对话式问答链
- def get_history_chain(vector_store):
- prompt_template = """基于以下已知内容,简洁和专业的来回答用户的问题。
- 如果无法从中得到答案,清说"根据已知内容无法回答该问题"
- 答案请使用中文。
- 已知内容:
- {context}
- 问题:
- {question}"""
-
- prompt = PromptTemplate(template=prompt_template,
- input_variables=["context", "question"])
-
- return ConversationalRetrievalChain.from_llm(llm=ChatOpenAI(model_name='gpt-3.5-turbo-16k'),
- retriever=vector_store.as_retriever(),
- combine_docs_chain_kwargs={'prompt': prompt})
1)ConversationalRetrievalChain 对话式问答链
ConversationalRetrievalQA 是建立在 RetrievalQAChain 之上,提供聊天历史记录的组件。它首先将聊天记录(显式传入或从提供的内存中检索)和问题组合成一个独立的问题,然后从检索器中查找相关文档,最后将这些文档和问题传递到问答链以返回一个响应。
这里注意如果需要传入自定义 Prompt,需要使用 combine_docs_chain_kwargs={'prompt': prompt},如果直接传入会提示错误。
2)使用方式
- @router.post("/question_history")
- async def question_history(
- request: Request,
- text: str = Body(embed=True)
- ):
- vector_store = fass.load_vector_store()
-
- chain = langchain.get_history_chain(vector_store)
-
- # 使用session缓存对话
- chat_history = request["session"].get("chat_history")
- if chat_history is None:
- chat_history = []
-
- # Convert chat history to list of tuples
- chat_history_tuples = []
- for message in chat_history:
- chat_history_tuples.append((message[0], message[1]))
-
- response = chain({"question": text, "chat_history": chat_history_tuples})
-
- # 保存聊天记录
- chat_history.append((text, response["answer"]))
- # 处理chat_history的长度,避免token过长
- chat_history = chat_history[0: 10]
-
- # 重新保存到session
- request["session"]["chat_history"] = chat_history
-
- # reply = "回复:"
- return {'success': True, "code": 0, "reply": response}
这里使用 session 的方式保存对话历史记录,并显式传入参数。每次作答后,整合到对话历史记录中,并保存到会话中。
需要注意的是,历史对话 chat_history 数组,需要做一次格式转换才能够正确传入,否则会报格式有误,格式需为 [(query,answer),(query,answer)]
3)总结
但是实际测试结果,本地化的知识库在使用 对话式问答链 方式,效果并不明显,可能的原因在于使用的自定义 Prompt,还有对提问的向量化处理,这些都会影响到上下文对话的理解。
如上图,这里已经提问了三轮,当我再提问 "请对上一条的问题再详细说明一下"
ai已经按照我们的自定义 Prompt,返回 "无法回答该问题"。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。