与OpenAI,Langchain和Chromadb聊天
#python #ai #openai #langchain

大型语言模型(LLM)被证明是一种强大的世代工具和助手,可以处理各种问题并返回人类可读的回答。他们还看到了大型科技巨头的兴趣。 Chatgpt,Bing的助手和Google的吟游诗人都是大型语言模型的示例,可以成为Askde问题,并且会以概括性和创意的答案做出回答。

在这里,我们掌握了LLM的力量,并围绕它们构建自己的自定义应用程序。具体来说,我们将构建一个聊天机器人,将使我们通过使用矢量数据库提供LLM应用程序长期可搜索的内存来提出一组刮擦网站数据的自然问题。这与SiteGPT类似。我们将为LLM使用OpenAI的gpt-3.5-turbo型号,并使用Langchain来帮助我们构建聊天机器人。最后,我们将使用chromadb作为矢量存储,并使用OpenAI的text-ada-embedding-002模型将数据嵌入到它。

这将是中级教程的初学者。我假设您对Python有一定的经验,但是在Langchain或LLMS周围建立应用程序的经验不多。我已经写了这些部分,很大程度上是彼此独立的,因此请随时跳到您最感兴趣的学习部分。该项目的代码可在GitHub上找到。

网络刮擦

我们想构建一个机器人与网站聊天。因此,我们将构建一个快速的Web Craper来收集我们的数据。我们将使用BeautifulSouprequests模块来构建它。让我们安装BeautifulSoup

pip install beautifulsoup4

首先,我们将编写一个实用程序功能,以向URL提出GET请求,并将响应内容保存到本地文件中。我们也会返回响应对象。我们将代码放入一个名为scrape.py的新文件中,并将所有刮擦的网站内容放入文件夹./scrape中。

# scrape.py

import os
import requests
import json

def get_response_and_save(url: str):
    response = requests.get(url)

    # create the scrape dir (if not found)
    if not os.path.exists("./scrape"):
        os.mkdir("./scrape")

    # save scraped content to a cleaned filename
    parsedUrl = cleanUrl(url)
    with open("./scrape/" + parsedUrl + ".html", "wb") as f:
        f.write(response.content)

    return response

def cleanUrl(url: str):
    return url.replace("https://", "").replace("/", "-").replace(".", "_")

我们将使用它来创建递归刮板功能。我们的目的是:

  1. 刮擦URL并使用BeautifulSoup在页面上查找所有其他链接。
  2. 那么,对于每个URL,我们从链接中找到我们重复刮擦并转到步骤1。

我们将递归地执行此操作至一定深度 - 设置depth=1将刮擦初始URL的链接页面,depth=2将刮擦这些页面中发现的链接等。我们可以使用sitemap dict对象跟踪已访问过的任何URL 。我们还要确保我们不会逃脱提供的初始URL的起源(或刮擦互联网的大部分)。

# scrape.py

from urllib.parse import urlparse
from collections import defaultdict
from bs4 import BeautifulSoup

...

def scrape_links(
    scheme: str,
    origin: str,
    path: str,
    depth=3,
    sitemap: dict = defaultdict(lambda: ""),
):
    siteUrl = scheme + "://" + origin + path
    cleanedUrl = cleanUrl(siteUrl)

    if depth < 0:
        return
    if sitemap[cleanedUrl] != "":
        return

    sitemap[cleanedUrl] = siteUrl
    response = get_response_and_save(siteUrl)
    soup = BeautifulSoup(response.content, "html.parser")
    links = soup.find_all("a")

    for link in links:
        href = urlparse(link.get("href"))
        if (href.netloc != origin and href.netloc != "") or (
            href.scheme != "" and href.scheme != "https"
        ):
            # don't scrape external links
            continue

        scrape_links(
            href.scheme or "https",
            href.netloc or origin,
            href.path,
            depth=depth - 1,
            sitemap=sitemap,
        )

    return sitemap

最后,我们将使用argparse通过简单的命令行界面进行调用。我们还将保存scrape_links函数返回的sitemap dict;这只是将本地保存/刮擦的文件的名称映射到其在线来源 - 稍后将变得清晰的原因。

# scrape.py

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--site", type=str, required=True)
parser.add_argument("--depth", type=int, default=3)

if __name__ == "__main__":
    args = parser.parse_args()
    url = urlparse(args.site)
    sitemap = scrape_links(url.scheme, url.netloc, url.path, depth=args.depth)
    with open("./scrape/sitemap.json", "w") as f:
        f.write(json.dumps(sitemap))

我们现在可以通过致电
来刮擦网站

python scrape.py --site <your_site_url> --depth 3

要获取一些演示数据,我们将以10的深度刮擦LangChain的文档。

python scrape.py \
  --site https://python.langchain.com/docs/get_started/introduction.html \
  --depth 10

嵌入和向量数据库

现在,我们有了数据,我们将以vector database轻松访问的方式存储此数据。具体来说,我们将在LangChain的帮助下使用ChromaDB

如果您不知道矢量数据库是什么,则tl; dr是他们可以使用embedding vectors存储和查询数据。嵌入向量是一种数值表示原始数据的方法。理想情况下,嵌入向量在两个或多个数据点之间存储关系信息,以便在语义上相似的数据获得相似的数值表示。大多数AI在训练过程中会产生这些嵌入向量,如果给出足够大的数据,它们将学会产生有意义的嵌入。这使矢量数据库成为存储“记忆”以将来“回忆”的自然候选者。

要开始,让我们安装相关的软件包。

pip install chroma langchain

我们将使用OpenAI的text-embedding-ada-002型号将文本变成嵌入向量。我们需要安装openai才能访问它。

pip install openai

要调用OpenAI的型号,我们需要一个.env文件。让我们创建一个。

# .env

OPENAI_API_KEY=<your_openai_api_key_here>

OpenAI's platform的API键代替<your_openai_api_key_here>。如果您没有OpenAI帐户,那么现在是创建一个帐户的好时机。您将获得一个新帐户的免费积分,如果您关注本教程,则最多只能使用2美元。

现在,我们首先需要从./scrape/文件夹加载数据。在新的embed.py文件中,我们添加以下内容。

# embed.py

import os
import json

from langchain.document_loaders import (
    BSHTMLLoader,
    DirectoryLoader,
)
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Chroma

from dotenv import load_dotenv
load_dotenv()

loader = DirectoryLoader(
        "./scrape",
        glob="*.html",
        loader_cls=BSHTMLLoader,
        show_progress=True,
        loader_kwargs={"get_text_separator": " "},
)
data = loader.load()

text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
)
documents = text_splitter.split_documents(data)

# map sources from file directory to web source
with open("./scrape/sitemap.json", "r") as f:
        sitemap = json.loads(f.read())
for document in documents:
        document.metadata["source"] = sitemap[
                document.metadata["source"].replace(".html", "").replace("scrape/", "")
        ]

embedding_model = OpenAIEmbeddings(model="text-embedding-ada-002")
db = Chroma.from_documents(
    documents, 
    embedding_model,
    persist_directory="./chroma"
)
db.persist()

这将

  1. 使用BeautifulSoup包装程序类BSHTMLLoaderhtml文档加载到./scrape文件夹中。
  2. 将文档分为1000个字符长的块,其中有200个字符重叠(这将使查询数据更容易)。
  3. 添加刮擦站点的原始URL作为文档的来源。 DirectoryLoader自动将文件位置添加为源,我们从前使用sitemap获取原始URL。
  4. 使用text-embedding-ada-002设置嵌入模型。
  5. 使用嵌入模型将文档存储到Chromadb矢量存储中。
  6. 将Chromadb中的数据持续到以后使用的本地./chroma目录。

最后,我们可以通过运行此文件来嵌入数据。创建AI聊天机器人时,我们会加载它。

python embed.py

与数据聊天

现在可以开始真正的乐趣。我们可以创建一个AI助手来与我们的矢量商店聊天。具体来说,我们将构建一个问题和回答(QA)聊天机器人,该聊天机器人将使我们能够询问有关刮擦数据的问题。我们同时将其返回来源,对于任何对聊天机器人及其hallucinations的不信任的人。

为了构建我们的聊天机器人,我们将使用LangChain,这是一个在LLM上构建应用程序的框架。这也有助于抽象我们需要做的许多及时工程。

我们将定义要用作OpenAI的gpt-3.5-turbo聊天模型的LLM。在我们的main.py文件夹中,我们将添加以下内容。

# main.py
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")

处理问题和记忆

在与模型聊天时,我们将询问有关刮擦网站的问题。为了有效地做到这一点,我们需要提出一个可以变成可用于查询矢量商店的嵌入的问题。由于我们的问题可能是以前的问题的来源,因此我们应该将我们的聊天历史融合为一个问题,我们最终可以将其嵌入并传递到我们的矢量商店。为此,我们创建了一个新的LLMChain,该33将促使我们的LLM使用指示来凝结我们的问题。

# main.py

from langchain.chains import LLMChain

condense_question_prompt = """Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.\
Make sure to avoid using any unclear pronouns.

Chat History:
{chat_history}
Follow Up Input: {question}
Standalone question:"""

condense_question_prompt = PromptTemplate.from_template(condense_question_prompt)

condense_question_chain = LLMChain(
    llm=llm,
    prompt=condense_question_prompt,
)

注意,我们在字符串中使用了{chat_history}{question}模板。稍后,我们将输入输入中的{question}文本。对于{chat_history},我们可以使用Langchain的ConversationBufferMemory添加内存。

# main.py

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

请注意,memory_key参数设置为chat_history。这将自动保留文本登录,以记忆我们的聊天机器人的对话,并将看起来像这样:

# example chat_history injected by theConversationBufferMemory object
chat_history = """
Human: Hi bot!
AI: Hi human! What can I assist you with today?
Human: What is the answer to life, the universe, and everything else?
AI: 42
...
"""

什么是链?

您可能想知道,LLMChain到底在做什么?到底是什么? Chains本质上是序列函数调用,已设置为将一个函数的输出作为另一个函数的输出。因此,从本质上讲,如果我们定义函数fg,那么链条将为f(g(x))

Langchain提供许多货架链,以帮助使用LLM的各种用例。我们上面使用的一个LLMChain将设置一个可以列出输入列表的链条,每个输入的格式提示,然后致电给出的llm

这里的美丽是我们可以在链条上创建链条。在我们的情况下,我们可以创建一个链条,以弄清楚正确的问题来询问矢量商店并检索文档。然后,可以将该文档添加到上下文中的另一个链条(Langchain称此"stuffing"),然后提示一个带有初始问题的LLM以检索答案。我们甚至可以创建一个提示,以确定哪个数据存储最好提示,或者创建一个可以检测到用户是否需要进一步输出的链。

我们将使用货架链中的更多内容来构建可以与我们刮擦的Web数据聊天的问题和回答(QA)bot。

创建QA链

提出问题和回答的基础是Langchain的QA链条处理。这些是一组类和辅助功能,可帮助我们在数据检索对象的基础上构建QA系统。数据检索器将只是我们之前创建的嵌入式矢量存储。

我们将首先创建一个可以检索其来源的质量链。

# main.py

from langchain.chains import create_qa_with_sources_chain

qa_chain = create_qa_with_sources_chain(llm)

我们将很多繁重的工作放在create_qa_with_sources_chain功能下。在引擎盖下,所有这些都是用特殊提示创建LLMChain,该提示将回答从检索到的文档中的上下文中回答用户的问题。我们将文档上下文提供StuffDocumentsChainstuffs将文档作为提示的上下文。

# main.py

from langchain.prompts import PromptTemplate
from langchain.chains import create_qa_with_sources_chain
from langchain.chains.combine_documents.stuff import StuffDocumentsChain

qa_chain = create_qa_with_sources_chain(llm)

doc_prompt = PromptTemplate(
    template="Content: {page_content}\nSource: {source}",
    input_variables=["page_content", "source"],
)

final_qa_chain = StuffDocumentsChain(
    llm_chain=qa_chain,
    document_variable_name="context",
    document_prompt=doc_prompt,
)

最后,我们最终用Chroma加载了我们的矢量商店,将其设置为猎犬。

# main.py

from langchain.chains import ConversationalRetrievalChain
from langchain.vectorstores import Chroma

db = Chroma(
    persist_directory="./chroma",
    embedding_function=OpenAIEmbeddings(model="text-embedding-ada-002"),
)

retrieval_qa = ConversationalRetrievalChain(
    question_generator=condense_question_chain,
    retriever=db.as_retriever(),
    memory=memory,
    combine_docs_chain=final_qa_chain,
)

就是这样。 retrieval_qa链是我们需要开始查询数据的最后一个链。我们在示例中刮了兰班文档,所以让我们问一下这是一个与兰班链有关的问题。

response = retrieval_qa.run({question: 'How can I use LangChain with LLMs?'})
print(response)

# output:
"""
{
  "answer": "LangChain provides a standard interface for LLMs, which are language models that take a string as input and return a string as output. To use LangChain with LLMs, you need to understand the different types of language models and how to work with them. You can configure the LLM and/or the prompt used in LangChain applications to customize the output. Additionally, LangChain provides prompt management, prompt optimization, and common utilities for working with LLMs. By combining LLMs with other modules in LangChain, such as chains and agents, you can create more complex applications.",
  "sources": [
    "https://python.langchain.com/docs/get_started/quickstart", "https://python.langchain.com/docs/modules/data_connection/document_loaders/markdown"
  ]
}
"""

哇!这是一个很好的答案!它甚至引用了原始资源(这是因为我们使用sitemap.json文件将原始文档源映射到初始网站)。它似乎是将json结构作为带有答案和源列表的字符串。我们可以使用json软件包将它们与响应分开。

import json

response = retrieval_qa.run({question: 'How can I use LangChain with LLMs?'})
responseDict = json.loads(response)
answer = responseDict["answer"]
sources = responseDict["sources"]

print(answer)
> """LangChain provides a standard interface for LLMs, which are language models that take a string as input and return a string as output. To use LangChain with LLMs, you need to understand the different types of language models and how to work with them. You can configure the LLM and/or the prompt used in LangChain applications to customize the output. Additionally, LangChain provides prompt management, prompt optimization, and common utilities for working with LLMs. By combining LLMs with other modules in LangChain, such as chains and agents, you can create more complex applications."""

print(sources)
> [
      "https://python.langchain.com/docs/get_started/quickstart",
      ...
  ]

当然,这不是与我们的机器人交互的最佳方法。为此,我们可以使用Gradio。

聊天机器人与Gradio的接口

Gradio是建立AI演示的非常有力的框架。它可以快速创建美丽的Web界面,可以与基础AI机器人进行交互,并在本地提供接口。如果我们选择的话,我们还可以主持我们的演示Spaces

要开始,我们首先安装它

pip install gradio

以及在我们的main.py文件中,我们需要做的就是添加以下

# main.py

import gradio

def predict(message, history):
    response = retrieval_qa.run({"question": message})
    responseDict = json.loads(response)
    answer = responseDict["answer"]
    sources = responseDict["sources"]

    if type(sources) == list:
        sources = "\n".join(sources)

    if sources:
        return answer + "\n\nSee more:\n" + sources
    return answer


gradio.ChatInterface(predict).launch()

这个

  1. 创建一个预测功能,该功能吸收了messagehistory参数。我们不需要history,因为我们已经使用ConversationalBufferHistory进行了跟踪,但是Gradio的ChatInterface需要它。
  2. message变量作为问题传递给retrieval_qa模型。
  3. 该模型的响应是采用json模式的字符串。我们解析模式并加载答案和来源,然后为用户生成一个不错的输出。
  4. 然后,我们设置了Gradio ChatInterface并启动它。

我们可以运行

python main.py

启动我们的Gradio服务器。这就是它的样子。

Screenshot of a chat bot interface. A user has asked "How can I use LangChain with LLMs?", and an AI bot has responded with "LangChain provides a standard interface for LLMs, which are language models that take a string as input and return a string as output. To use LangChain with LLMs, you need to understand the different types of language models and how to work with them. You can configure the LLM and/or the prompt used in LangChain applications to customize the output. Additionally, LangChain provides prompt management, prompt optimization, and common utilities for working with LLMs. By combining LLMs with other modules in LangChain, such as chains and agents, you can create more complex applications.". It has also provided the sources for its answer

现在,我们以前的问题看起来真的很好,我们现在可以在自然界面中与机器人聊天。

评估

为了进行评估,我们可以使用我们的自定义网络纵行刮擦Langchain文档。使用原始URL,深度为10,我们运行刮擦功能

python scrape.py \
  --site https://python.langchain.com/docs/get_started/quickstart \
  --depth 10

将使我们659刮擦的html文件。然后,我们将其嵌入我们的矢量商店

python embed.py

最终运行之前

python main.py

并与我们的Gradio服务器中的聊天机器人进行交互。

聊天机器人似乎在非常直接的问题上做得很好。一些例子:

奇怪的是,答案有时还会引用其知识的来源,并分别提供来源。

在更多的通用问题上似乎做得不太好,尤其是当有多个合理的来源时。机器人似乎对所有来源感到困惑,并且可能没有足够的上下文来准确回答。

一个特殊的示例是,如果您问它是什么,而没有指定LLMS,它将认为Langchain可以与区块链技术集成。从技术上讲,这是正确的(使用区块链文档加载程序),但不是Langchain的真正目的。

当然,基础的ChatGpt聊天模型不知道Langchain是什么。 Chatgpt的培训数据仅包含截至2021年9月的数据,Langchain于2022年发布。像“什么是Langchain”之类的查询只会因为它可以对矢量商店的查询而言,这几乎在所有内容时都太通用了在矢量商店中是引用兰链。

我们可以在fine tuning A模型中解决这个问题,并提出更通用的问题,然后再用可查询的矢量商店武装。我们还可以探索调整链条或调整,调整嵌入过程或调整矢量商店查询算法。

结论和下一步

我们设法构建了一个相当简单的聊天机器人,可以使用Langchain,OpenAI模型,矢量商店回答有关刮擦网站的问题,并为我们的OpenAI API呼叫提供了约2美元的信用。

当被问及直接问题时,我们的聊天机器人的性能很好,并且可以在记忆任何过去的问题的同时按顺序回答多个问题。

我们的聊天机器人在概括和被问到非常通用的问题时做得不太好。在生成文档嵌入文档时,我们可以通过调整角色大小来改进它,将矢量商店的搜索从默认的similarity_search调整为mmr,构建一个更复杂的链条,试图查询更多文档,或者在使用之前对聊天模型进行微调,然后再调整聊天模型。它在我们的链中。

如果您有任何疑问,请随时在jasonrobwebster@gmail.com与我联系。如果您受到这项工作的启发,我也很喜欢听!

本文最初发表在我的personal blog上。