使用langchain构建一个《汇编语言编程艺术》问答系统

108 阅读7分钟

又是一年双十一,每当这个时候,玩了半学期的大学生们就要陆续复工了(包括笔者),不然就要被期末考试狠狠鞭挞了。

可是啃教科书是很痛苦的,尤其是临近期末的时候。

等等,我有个好点子.....

不如让小豆包和《汇编语言》碰碰谁更硬吧!


首先确定一下,我们需要的有以下几种原材料:

  1. 一本新鲜的《汇编语言编程艺术》
  2. some codes
  3. a model
  4. impact langchain python environment

好了,这就足够了。

事不宜迟,我们马上开始!


1.数据源(Data Sources)

首先,笔者通过合法合规的途径搞到了一本《汇编语言编程艺术》.pdf作为我们的数据源。

为了更好让这本板正的教科书成为我们的小猫娘,首先我们需要对其进行加工。分为以下三步:

  1. Loading:用LangChain自带的文档加载器load《汇编语言编程艺术》.pdf(还可以添加练习册和其它参考资料),使其满足LangChain的规格

复用课程代码即可

ps:请保证自己的文档存放目录填写正确。

from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader

# 加载Documents
base_dir = "../DataSource"  # 文档的存放目录
documents = []
for file in os.listdir(base_dir):
    # 构建完整的文件路径
    file_path = os.path.join(base_dir, file)
    if file.endswith(".pdf"):
        loader = PyPDFLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".docx"):
        loader = Docx2txtLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".txt"):
        loader = TextLoader(file_path)
        documents.extend(loader.load())
  1. Splitting:类似地,用LangChain的文档分割器split《汇编语言编程艺术(loaded)》,方便传输和批量处理。

复用课程代码(将其细细的剁成臊子.......其实是chunk,应该是块子......雾)

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
chunked_documents = text_splitter.split_documents(documents)

  1. Storage:将《汇编语言编程艺术(loaded && split)》以“嵌入”(Embedding)的形式 store 到一个向量数据库中(参考LangChain课程,使用的是Qdrant)。

与上皆同,写代码的最高境界就是不用写代码(老师我悟了......雾)

ps:课程开放的火山大模型也可以使用这段openai代码,两者API 协议兼容,不用担心

ps:其实有改动,我的环境变量名设的不同,请诸位仔细。(后补)

# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中
from langchain_community.vectorstores import Qdrant


class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = ""
    model: str

    def __init__(self, **data: Any):
        super().__init__(**data)
        if self.api_key == "":
            self.api_key = os.environ["API_KEY"]
        self.client = Ark(
            base_url=os.environ["API_URL"],
            api_key=self.api_key
        )

    def embed_query(self, text: str) -> List[float]:
        """
        生成输入文本的 embedding.
        Args:
            texts (str): 要生成 embedding 的文本.
        Return:
            embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
        """
        embeddings = self.client.embeddings.create(model=self.model, input=text)
        return embeddings.data[0].embedding

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        return [self.embed_query(text) for text in texts]

    class Config:
        arbitrary_types_allowed = True


vectorstore = Qdrant.from_documents(
    documents=chunked_documents,  # 以分块的文档
    embedding=DoubaoEmbeddings(
        model=os.environ["EMBEDDING_MODELEND"],
    ),  # 用OpenAI的Embedding Model做嵌入
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",
)  # 指定collection_name


2.大模型应用(Application)

上述步骤结束后,我们得到了一个向量存储的集合,即vectorstore。你可以把它当成一个向量数据库,接下来模型就要从这个数据库中提取我们想要的信息(即问题的回答)。

我们需要创建一个聊天模型以及一个RetrievalQA链。(课程中提到这是一个检索式问答模型,所以如果它检索不到,那么就会“I don't know”。不过优点也在于此,它胡说八道的可能性比原来低得多,毕竟它出现“幻觉”的代价是“你-要-重-修-了-同-学”)。

接下来就是创建问答的模型(之前创建的那个模型的作用是embedding,这个模型的作用是问答,啊所以需要创建两个推理模型,一个负责embedding,一个负责answear)。

MultiQueryRetrie模型是一个llm模型的包装,通过llm生成多个与“问题”相关的“问题”。

由于下面这部分内容课程中只是一笔带过,所以简单介绍一下:

生成一个qa_chain需要以下两个部分:

llm(语言模型):

    * 生成文本:LLM能够生成连贯且语法正确的文本。在所给出的代码中,LLM 用于根据检索到的文档和用户的问题生成答案。
    * 理解上下文:LLM 能够理解输入文本的上下文,并据此生成相关的回答。这使得我们构建的问答系统能够提供更加准确和有针对性的答案。
    * 处理自然语言:LLM 擅长处理自然语言,这意味着它可以理解和生成人类语言,使得问答系统更加用户友好。(因为生成的答案是一个一个片段,需要润色一下,使其连贯通顺)

retriever(检索器):

    * 检索相关文档:检索器的主要作用是从大量的文档中检索出与用户问题最相关的文档或信息片段。在您的代码中,检索器使用向量存储(vectorstore)来快速查找与问题相关的文档。
    * 提高效率:通过预先对文档进行嵌入和索引,检索器可以在短时间内找到最相关的信息,从而提高问答系统的效率。
    * 减少噪声:检索器可以帮助过滤掉不相关的信息,只提供与问题最相关的内容,这有助于提高答案的质量和准确性。

ps:我将日志记录等级升成warning级,因为我不关心info级别的信息,而且刷屏也让我心烦。

# 4. Retrieval 准备模型和Retrieval链
import logging  # 导入Logging工具
from langchain_openai import ChatOpenAI  # ChatOpenAI模型
from langchain.retrievers.multi_query import (
    MultiQueryRetriever,
)  # MultiQueryRetriever工具
from langchain.chains import RetrievalQA  # RetrievalQA链

# 设置Logging
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.WARNING)

# 实例化一个大模型工具 - OpenAI的GPT-3.5
llm = ChatOpenAI(model=os.environ["LLM_MODELEND"], temperature=0)

# 实例化一个MultiQueryRetriever
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(), llm=llm
)

# 实例化一个RetrievalQA链
qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_from_llm)

3.用例(Use-Cases)

其实经过上述操作,我们已经得到了一个output,但是一个单纯的输入框+输出框多少有点丑陋了。(好吧,其实是有现成的代码不用天理难容....)

那就再一次“窃书”,ctrl-c-v大法发动:

有些小伙伴没接触过网页的创建,所以我简单描述一下(绝对不是字数不够了....雾)

网页的传输数据就是通过两个网页请求方式:post和get。

get:用户请求网页,服务器把我们准备好的网页发送给用户即可。

post:用户上传表单数据,服务器就可以获取用户填写的表单,在这里我们的处理逻辑就是上传到我们之前构建好的qa_chain(问答链),按照之前代码的逻辑进行处理即可,然后把处理后的结果返回给用户。

请注意,我们使用的端口是5000,首先确保在你的电脑上没有其它应用在使用这个端口(有用到的话,你换一个端口就好了),然后直接在你本机上随便一个浏览器上打开localhost:5000就可以了。

# 5. Output 问答系统的UI实现
from flask import Flask, request, render_template
app = Flask(__name__) # Flask APP

@app.route('/', methods=['GET', 'POST'])
def home():
    if request.method == 'POST':

        # 接收用户输入作为问题
        question = request.form.get('question')        
        
        # RetrievalQA链 - 读入问题,生成答案
        result = qa_chain({"query": question})
        
        # 把大模型的回答结果返回网页进行渲染
        return render_template('index.html', result=result)
    
    return render_template('index.html')

if __name__ == "__main__":
    app.run(host='0.0.0.0',debug=True,port=5000)

这个index.html就是我们需要渲染的网页,你可以在我们所写代码的上一级文件夹下创建一个templates文件夹。然后创建一个index.html文件即可,样式自己定。


当时忘了截图,再一调用就发现免费额度用完了(/(ㄒoㄒ)/~~),要是之前那个调好的qa_chain可以保存就好了....诶,下一篇就做这个保存(^_^)。