LangChain实战1 | 豆包MarsCode AI刷题

149 阅读14分钟

LangChain实战

LangChain系统安装和快速入门

LangChain 是一个全方位的、基于大语言模型这种预测能力的应用开发工具,它的灵活性和模块化特性使得处理语言模型变得极其简便。不论你在何时何地,都能利用它流畅地调用语言模型,并基于语言模型的“预测”或者说“推理”能力开发新的应用。

langchain-ai/langchain: 🦜🔗 Build context-aware reasoning applications

LangChain 的预构建链功能,就像乐高积木一样,无论你是新手还是经验丰富的开发者,都可以选择适合自己的部分快速构建项目。对于希望进行更深入工作的开发者,LangChain 提供的模块化组件则允许你根据自己的需求定制和创建应用中的功能链条。

通过下面的命令进行langchain的安装。langchain的官方文档:Introduction | 🦜️🔗 LangChain

pip install langchain

LangChain本质上是对各种大模型提供的API进行套壳,从而方便我们使用这些API,搭建框架、模块和接口。

ChatModel和TextModel

  • Text Model,聊天模型,用于产生人类和AI之间的对话

    模型的调用方式:

    import os
    from openai import OpenAI
    os.environ["OPENAI_API_KEY"] = '你的Open API Key'
    ​
    client = OpenAI()
    response = client.completions.create(
      model="gpt-3.5-turbo-instruct",
      temperature=0.5,
      max_tokens=100,
      prompt="请给我的花店起个名")
    print(response.choices[0].text.strip())#从响应中获取第一个(如果在调用大模型时,没有指定n参数,那么就只有唯一的一个响应)选择,然后获取该选择的文本,并移除其前后的空白字符。
    

    通过下面的参数可以控制模型的输出内容和格式。

    返回的Response字段包括如下

  • Chat Model,文本模型,在ChatGPT出来之前,大家都使用这种模型的API来调用GPT-3,文本模型的代表作是text-davinci-003(基于GPT3)。

    整体流程上,Chat模型和Text模型的调用是类似的,只是前面加了一个chat,然后输入(prompt)和输出(response)的数据格式有所不同。

    response = client.chat.completions.create(  
      model="gpt-4",
      messages=[
            {"role": "system", "content": "You are a creative AI."},
            {"role": "user", "content": "请给我的花店起个名"},
        ],
      temperature=0.8,
      max_tokens=60
    )
    

    除了TextModel中提到的参数,专属于ChatModel的概念包括role和message。

    message是传入模型的提示,是一个列表,包含了多个消息,每个消息都有一个role(system、user、assistant)以及content。在上面的代码中:system设定对话背景,user提出具体请求。

    • system:系统消息主要用于设定对话的背景或上下文。这可以帮助模型理解它在对话中的角色和任务。例如,你可以通过系统消息来设定一个场景,让模型知道它是在扮演一个医生、律师或者一个知识丰富的AI助手。系统消息通常在对话开始时给出。
    • user:用户消息是从用户或人类角色发出的。它们通常包含了用户想要模型回答或完成的请求。用户消息可以是一个问题、一段话,或者任何其他用户希望模型响应的内容。
    • assistant:助手消息是模型的回复。例如,在你使用API发送多轮对话中新的对话请求时,可以通过助手消息提供先前对话的上下文。然而,请注意在对话的最后一条消息应始终为用户消息,因为模型总是要回应最后这条用户消息。

    模型的response会包含一个或多个choices,每个choices都包含一个message。每个message也都包含一个role和content。role可以是system、user或assistant,表示该消息的发送者,content则包含了消息的实际内容。

    {
     'id': 'chatcmpl-2nZI6v1cW9E3Jg4w2Xtoql0M3XHfH',
     'object': 'chat.completion',
     'created': 1677649420,
     'model': 'gpt-4',
     'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87},
     'choices': [
       {
        'message': {
          'role': 'assistant',
          'content': '你的花店可以叫做"花香四溢"。'
         },
        'finish_reason': 'stop',
        'index': 0
       }
      ]
    }
    

    各个字段的含义如下:

    要获取模型的回复可以通过response['choices'][0]['message']['content']来进行获取。

相较于Text模型,Chat模型的设计更适合处理对话或者多轮次交互的情况。这是因为它可以接受一个消息列表作为输入,而不仅仅是一个字符串。这个消息列表可以包含system、user和assistant的历史信息,从而在处理交互式对话时提供更多的上下文信息。对于简单的单轮文本生成任务,使用Text模型可能会更简单。

LangChain调用Text和ChatModel

调用Text模型代码如下:

import os
os.environ["OPENAI_API_KEY"] = '你的Open API Key'
from langchain.llms import OpenAI
llm = OpenAI(  
    model="gpt-3.5-turbo-instruct",
    temperature=0.8,
    max_tokens=60,)
response = llm.predict("请给我的花店起个名")
print(response)

输出:

花之缘、芳华花店、花语心意、花风旖旎、芳草世界、芳色年华

这只是一个对OpenAI API的简单封装:先导入LangChain的OpenAI类,创建一个LLM(大语言模型)对象,指定使用的模型和一些生成参数。使用创建的LLM对象和消息列表调用OpenAI类的call方法,进行文本生成。

调用Chat模型代码如下:

import os
os.environ["OPENAI_API_KEY"] = '你的Open API Key'
from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI(model="gpt-4",
                    temperature=0.8,
                    max_tokens=60)
from langchain.schema import (
    HumanMessage,
    SystemMessage
)
messages = [
    SystemMessage(content="你是一个很棒的智能助手"),
    HumanMessage(content="请给我的花店起个名")
]
response = chat(messages)
print(response)
​

输出:

content='当然可以,叫做"花语秘境"怎么样?' additional_kwargs={} example=False

通过导入LangChain的ChatOpenAI类,创建一个Chat模型对象,指定使用的模型和一些生成参数。然后从LangChain的schema模块中导入LangChain的SystemMessage和HumanMessage类,创建一个消息列表。消息列表中包含了一个系统消息和一个人类消息。之后,使用创建的chat对象和消息列表调用ChatOpenAI类的call方法,进行文本生成。生成的结果被存储在response变量中。

LangChain已经对大语言模型的output进行了解析,只保留了响应中最重要的文字部分。

基于LangChain的本地知识库智能问答系统

项目名称:“易速鲜花”内部员工知识库问答系统

项目介绍:“易速鲜花”作为一个大型在线鲜花销售平台,有自己的业务流程和规范,也拥有针对员工的SOP手册。新员工入职培训时,会分享相关的信息。但是,这些信息分散于内部网和HR部门目录各处,有时不便查询;有时因为文档过于冗长,员工无法第一时间找到想要的内容;有时公司政策已更新,但是员工手头的文档还是旧版内容。基于上述需求,我们将开发一套基于各种内部知识手册的 “Doc-QA” 系统。这个系统将充分利用LangChain框架,处理从员工手册中产生的各种问题。这个问答系统能够理解员工的问题,并基于最新的员工手册,给出精准的答案。

系统的架构设计如下:

  • 数据源(Data Sources):数据可以有很多种,包括PDF在内的非结构化的数据(Unstructured Data)、SQL在内的结构化的数据(Structured Data),以及Python、Java之类的代码(Code)。在这个示例中,我们聚焦于对非结构化数据的处理。
  • 大模型应用(Application,即LLM App):以大模型为逻辑引擎,生成我们所需要的回答。
  • 用例(Use-Cases):大模型生成的回答可以构建出QA/聊天机器人等系统。

核心pipeline如下:

具体流程如下:

  1. Loading:文档加载器把Documents 加载为以LangChain能够读取的形式。
  2. Splitting:文本分割器把Documents 切分为指定大小的分割,我把它们称为“文档块”或者“文档片”。
  3. Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
  4. Retrieval:应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
  5. Output:把问题和相似的嵌入片传递给语言模型(LLM),使用包含问题和检索到的分割的提示生成答案

数据的准备

langchain-in-action/02_文档QA系统/OneFlower at main · huangjia2019/langchain-in-action

数据包括 pdf、word 和 txt 格式的各种文件。使用LangChain中的document_loaders来加载各种格式的文本文件

import os
os.environ["OPENAI_API_KEY"] = '你的Open AI API Key'
​
# 1.Load 导入Document Loaders
from langchain.document_loaders import PyPDFLoader
from langchain.document_loaders import Docx2txtLoader
from langchain.document_loaders import TextLoader
​
# 加载Documents
base_dir = '.\OneFlower' # 文档的存放目录
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())

文本分割

使用LangChain中的RecursiveCharacterTextSplitter 来分割文本

# 2.Split 将Documents切分成块以便后续进行嵌入和向量存储
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
chunked_documents = text_splitter.split_documents(documents)

此时文档被分割为200字符左右的文档块,后续会存储进向量数据库。

向量数据库存储

将这些分割后的文本转换成嵌入的形式,并将其存储在一个向量数据库中。这里可以使用开源的Embeddings大模型来生成嵌入,然后使用Qdrant向量数据库来存储嵌入。

# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中
from langchain.vectorstores import Qdrant
from langchain.embeddings import OpenAIEmbeddings
vectorstore = Qdrant.from_documents(
    documents=chunked_documents, # 以分块的文档
    embedding=OpenAIEmbeddings(), # 用OpenAI的Embedding Model做嵌入
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",) # 指定collection_name

如果使用豆包Embeddings,代码可以修改为

from langchain_community.vectorstores import Qdrant
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings #嵌入模型的基类
from langchain.pydantic_v1 import BaseModel #用于定义数据模型
from volcenginesdkarkruntime import Ark #doubao的sdkclass 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["OPENAI_API_KEY"]
        self.client = Ark(
            base_url=os.environ["OPENAI_BASE_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

如果使用Huggingface的开源模型(以m3e为例),则代码为

# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中
from langchain.vectorstores import Qdrant
from langchain_community.embeddings import HuggingFaceEmbeddings
EMBEDDING_DEVICE = "cuda" if is_torch_cuda_available() else "mps" if is_torch_mps_available() else "cpu"
embeddings_model = HuggingFaceEmbeddings(model_name='moka-ai/m3e-base', model_kwargs={'device': EMBEDDING_DEVICE})
​
​
vectorstore = Qdrant.from_documents(
    documents=chunked_documents, # 以分块的文档
    embedding=embeddings_model, # 
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",) # 指定collection_name#基于文本相似度进行查询
print(vectorstore.similarity_search("鲜花"))

还可与嵌入查询的相似度返回文档

vectorstore = Qdrant.from_documents(
    documents=chunked_documents, # 以分块的文档
    embedding=embeddings_model, # 
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",) # 指定collection_name
embedding = embeddings_model.embed_query("鲜花")
print(embedding)
print(vectorstore.similarity_search_by_vector(embedding))

参考:向量存储和检索器 | 🦜️🔗 LangChain 中文

简要介绍一下Embedding和向量数据库:

词嵌入(Word Embedding)是自然语言处理和机器学习中的一个概念,它将文字或词语转换为一系列数字,通常是一个向量。简单地说,词嵌入就是一个为每个词分配的数字列表。这些数字不是随机的,而是捕获了这个词的含义和它在文本中的上下文。因此,语义上相似或相关的词在这个数字空间中会比较接近。

举个例子,通过某种词嵌入技术,我们可能会得到: "国王" -> [1.2, 0.5, 3.1, ...] "皇帝" -> [1.3, 0.6, 2.9, ...] "苹果" -> [0.9, -1.2, 0.3, ...]

从这些向量中,我们可以看到“国王”和“皇帝”这两个词的向量在某种程度上是相似的,而与“苹果”这个词相比,它们的向量则相差很大,因为这两个概念在语义上是不同的。

词嵌入的优点是,它提供了一种将文本数据转化为计算机可以理解和处理的形式,同时保留了词语之间的语义关系。这在许多自然语言处理任务中都是非常有用的,比如文本分类、机器翻译和情感分析等。

向量数据库,也称为矢量数据库或者向量搜索引擎,是一种专门用于存储和搜索向量形式的数据的数据库。在众多的机器学习和人工智能应用中,尤其是自然语言处理和图像识别这类涉及大量非结构化数据的领域,将数据转化为高维度的向量是常见的处理方式。这些向量可能拥有数百甚至数千个维度,是对复杂的非结构化数据如文本、图像的一种数学表述,从而使这些数据能被机器理解和处理。然而,传统的关系型数据库在存储和查询如此高维度和复杂性的向量数据时,往往面临着效率和性能的问题。因此,向量数据库被设计出来以解决这一问题,它具备高效存储和处理高维向量数据的能力,从而更好地支持涉及非结构化数据处理的人工智能应用。

相关信息的获取

相关信息的获取是根据我们的query进行相似度匹配检索获得的,也就是将我们的query转化为向量,去和向量数据库中的数据进行比较,提取最接近的信息。通常使用的方法有欧式距离和余弦相似度。

  • 欧氏距离:这是最直接的距离度量方式,就像在二维平面上测量两点之间的直线距离那样。在高维空间中,两个向量的欧氏距离就是各个对应维度差的平方和的平方根。
  • 余弦相似度:在很多情况下,我们更关心向量的方向而不是它的大小。例如在文本处理中,一个词的向量可能会因为文本长度的不同,而在大小上有很大的差距,但方向更能反映其语义。余弦相似度就是度量向量之间方向的相似性,它的值范围在-1到1之间,值越接近1,表示两个向量的方向越相似。

欧氏距离度量的是绝对距离,它能很好地反映出向量的绝对差异。当我们关心数据的绝对大小,例如在物品推荐系统中,用户的购买量可能反映他们的偏好强度,此时可以考虑使用欧氏距离。

余弦相似度度量的是方向的相似性,它更关心的是两个向量的角度差异,而不是它们的大小差异。在处理文本数据或者其他高维稀疏数据的时候,余弦相似度特别有用。比如在信息检索和文本分类等任务中,文本数据往往被表示为高维的词向量,词向量的方向更能反映其语义相似性,此时可以使用余弦相似度。

通过向一个Chat Model进行询问,创建一个检索的QA链,把Chat Model重构成基于检索的问答模型,接收询问,然后通过查询,获得必要信息,进行补充回答。检索QA链包含两个部分:

  • 大模型负责回答问题
  • 检索器负责根据问题进行相关信息的检索,找到对应的文档块,然后和问题一起传给大模型。检索式非常重要的,因为大模型本身可能没有在内部的数据上进行训练过,因此需要对数据库进行检索补充信息后回答。
# 4. Retrieval 准备模型和Retrieval链
import logging # 导入Logging工具
from langchain.chat_models 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.INFO)
​
# 实例化一个大模型工具 - OpenAI的GPT-3.5
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
​
# 实例化一个MultiQueryRetriever
retriever_from_llm = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(), llm=llm) #MultiQueryRetriever用于让大模型根据用户提出的query撰写更多的query从而进行检索。# 实例化一个RetrievalQA链
qa_chain = RetrievalQA.from_chain_type(llm,retriever=retriever_from_llm)
​