原文:https://www.toutiao.com/article/7211527561673867779/?log_from=e0e756d2bfaf5_1683079005521

ChatGPT 几个月前问世,并以其回答来自广泛知识集的问题的能力震惊了所有人。 在 ChatGPT 展示大型语言模型的强大功能时,Dagster 核心团队遇到了一个问题。

 

推荐:用 NSDT场景设计器 快速搭建3D场景。

1、我们的问题

我们构建了 Dagster,这是一个快速增长的开源数据编排解决方案,具有大型社区 Slack 实例。 提供一流的支持体验是我们项目成功的关键,但这需要我们核心团队成员的大量工作。 当我们看到 ChatGPT 可以做什么时,我们想知道我们是否可以基于可以回答基本问题的技术创建一个 Slack 机器人。

虽然 OpenAI 的 ChatGPT 本身没有 API,但底层技术 GPT-3 有。 因此,我们开始了一段旅程,以弄清楚我们是否可以使用 GPT-3 构建一个可以回答有关 Dagster 的基本技术问题的机器人。

值得注意的是,我不是 AI 专家。 我们可以通过多种方式改进我们在这篇博文中所做的工作。 话虽如此,让我们继续吧!

2、微调还是不微调?

我们需要一种方法来向 GPT-3 传授 Dagster GitHub 项目的技术细节。

显而易见的解决方案是找到一种在 Dagster 文档上训练 GPT-3 的方法。 我们将从 Dagster 存储库中提取每个 Markdown 文件,并以某种方式将其提供给 GPT-3。

我们的第一直觉是使用 GPT-3 的微调功能来创建在 Dagster 文档上训练的自定义模型。 但是,由于 3 个原因,我们最终没有这样做:

  • 我们不确定基于 Markdown 文件构建训练提示的最佳方式,也找不到很好的资源来帮助我们了解微调的最佳实践。
  • 好像很贵。 看起来每次我们想要重新训练都要花费 80 美元。 如果我们希望我们的机器人与回购中的最新变化保持同步(即每天重新训练),这个成本可能会增加。
  • 我与我网络中的一些人进行了交谈,他们已经将 GPT-3 部署到生产环境中,他们都对微调持悲观态度。

所以我们决定在不进行微调的情况下继续前进。

3、使用 LangChain 构建提示

Prompt engineering 是开发一个很好的提示来最大化像 GPT-3 这样的大型语言模型的有效性的过程。 开发提示的挑战在于你通常需要一系列提示或提示链才能获得最佳答案。

我们遇到了一个很棒的库,可以帮助我们解决这个问题:langchain :

大型语言模型 (LLM) 正在成为一种变革性技术,使开发人员能够构建他们以前无法构建的应用程序。 但是单独使用这些 LLM 往往不足以创建一个真正强大的应用程序——当你能够将它们与其他计算或知识来源相结合时,真正的力量才会出现。

这正是我们试图解决的问题:我们希望利用 GPT-3 大型语言模型的强大功能,并将其与 Dagster 文档中编码的知识相结合。 幸运的是,LangChain 包含一个称为数据增强生成的功能,它允许你提供一些上下文数据来增强 LLM 的知识。 它还为像我们这样的问答应用程序预建了提示。

如果我们深入了解 LangChain 的源代码,我们可以看到问题回答的提示是什么(完整源代码在这里):

Given the following extracted parts of a long document and a question, create a final answer with references ("SOURCES").
If you don't know the answer, just say that you don't know. Don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.

<series of examples redacted>

QUESTION: {question}
=========
{summaries}
=========
FINAL ANSWER:

如你所见,这个提示接受一个问题和一些来源,并返回一个答案以及最相关的来源。 查看提示中提供的示例之一,以了解这在实践中的表现:

QUESTION: Which state/country's law governs the interpretation of the contract?
=========
Content: This Agreement is governed by English law and the parties submit to the exclusive jurisdiction of the English courts in  relation to any dispute (contractual or non-contractual) concerning this Agreement save that either party may apply to any court for an  injunction or other relief to protect its Intellectual Property Rights.
Source: 28-pl
Content: No Waiver. Failure or delay in exercising any right or remedy under this Agreement shall not constitute a waiver of such (or any other)  right or remedy.\n\n11.7 Severability. The invalidity, illegality or unenforceability of any term (or part of a term) of this Agreement shall not affect the continuation  in force of the remainder of the term (if any) and this Agreement.\n\n11.8 No Agency. Except as expressly stated otherwise, nothing in this Agreement shall create an agency, partnership or joint venture of any  kind between the parties.\n\n11.9 No Third-Party Beneficiaries.
Source: 30-pl
Content: (b) if Google believes, in good faith, that the Distributor has violated or caused Google to violate any Anti-Bribery Laws (as  defined in Clause 8.5) or that such a violation is reasonably likely to occur,
Source: 4-pl
=========
FINAL ANSWER: This Agreement is governed by English law.
SOURCES: 28-pl

3、在 LangChain 中实现一个示例

☝ 对于本教程,我建议使用 GitPod 来获得一致的 Python 环境。

让我们在 LangChain 中实现它。 首先安装 LangChain 和本教程其余部分所需的一些依赖项:

pip install langchain==0.0.55 requests openai transformers faiss-cpu

接下来,让我们开始编写一些代码。 创建一个新的 Python 文件 langchain_bot.py 并从一些导入开始:

from langchain.llms import OpenAI
from langchain.chains.qa_with_sources import load_qa_with_sources_chain
from langchain.docstore.document import Document
import requests

接下来,我们的玩具示例需要一些示例数据。 现在,让我们使用各种维基百科页面的第一段作为我们的数据源。 有一个很棒的 Stack Overflow 答案,它给了我们一个获取这些数据的神奇咒语:

def get_wiki_data(title, first_paragraph_only):
    url = f"https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&explaintext=1&titles={title}"
    if first_paragraph_only:
        url += "&exintro=1"
    data = requests.get(url).json()
    return Document(
        page_content=list(data["query"]["pages"].values())[0]["extract"],
        metadata={"source": f"https://en.wikipedia.org/wiki/{title}"},
    )

不要太担心这个的细节。 给定一个维基百科标题和一个指定你想要第一段还是整个内容的布尔值,它将返回一个 LangChain Document 对象,它基本上只是一个附加了一些元数据的字符串。 元数据中的来源键很重要,因为模型在引用其来源时会使用它。

接下来,让我们设置一个机器人将要查询的资源语料库:

sources = [
    get_wiki_data("Unix", True),
    get_wiki_data("Microsoft_Windows", True),
    get_wiki_data("Linux", True),
    get_wiki_data("Seinfeld", True),
]

最后,让我们将所有这些连接到 LangChain:

chain = load_qa_with_sources_chain(OpenAI(temperature=0))

def print_answer(question):
    print(
        chain(
            {
                "input_documents": sources,
                "question": question,
            },
            return_only_outputs=True,
        )["output_text"]
    )

这做了几件事:

  • 它创建了一个 LangChain 链,该链设置了适当的问答提示。 它还表明我们应使用 OpenAI API 为链提供动力而不是其他服务(如 Cohere)
  • 它调用链,提供要查阅的源文件和问题。
  • 它返回一个原始字符串,其中包含问题的答案及其使用的来源。

让我们看看它的实际效果! 在开始之前,请务必注册一个 OpenAI API 密钥。

$ export OPENAI_API_KEY=sk-<your api key here>

OpenAI API 不是免费的。 当你迭代你的机器人时,一定要监控你花了多少钱!

现在我们已经设置了 API 密钥,让我们试一试我们的机器人。

$ python3
Python 3.8.13 (default, Oct  4 2022, 14:00:32)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from langchain_bot import print_answer
>>> print_answer("Who were the writers of Seinfeld?")
 The writers of Seinfeld were Larry David, Jerry Seinfeld, Larry Charles, Peter Mehlman, Gregg Kavet, Carol Leifer, David Mandel, Jeff Schaffer, Steve Koren, Jennifer Crittenden, Tom Gammill, Max Pross, Dan O'Keefe, Charlie Rubin, Marjorie Gross, Alec Berg, Elaine Pope and Spike Feresten.
SOURCES: https://en.wikipedia.org/wiki/Seinfeld
>>> print_answer("What are the main differences between Linux and Windows?")
 Linux and Windows are both operating systems, but Linux is open-source and Unix-like, while Windows is proprietary and developed by Microsoft. Linux is used on servers, embedded systems, and desktop computers, while Windows is mainly used on desktop computers.
SOURCES:
https://en.wikipedia.org/wiki/Unix
https://en.wikipedia.org/wiki/Microsoft_Windows
https://en.wikipedia.org/wiki/Linux
>>> print_answer("What are the differences between Keynesian and classical economics?")
 I don't know.
SOURCES: N/A
>>>

我不了解你怎么看,但我认为这令人印象深刻。 它正在回答问题,提供额外的相关上下文,引用其来源,并知道何时说不知道。

所以,既然我们已经证明这是有效的,它应该像将所有 Dagster 文档填充到源代码部分一样简单,对吧?

4、处理有限的提示窗口大小

不幸的是,这并不像将 Dagster 文档的整个语料库放入提示中那么简单。 主要有两个原因:

  • GPT-3 API 按令牌收费,因此我们的目标是在我们的提示中使用尽可能少的令牌以节省资金,因为我们需要在用户提出问题时将整个提示发送到 API 机器人。
  • GPT-3 API 在提示中有大约 4000 个令牌的限制,所以即使我们愿意为此付费,我们也不能给它完整的 Dagster 文档。 信息太多了。

5、处理大量文件

让我们看看当我们有太多文档时会发生什么。 不幸的是,在达到令牌限制之前,我们只需要再添加几个文档:

sources = [
    get_wiki_data("Unix", True),
    get_wiki_data("Microsoft_Windows", True),
    get_wiki_data("Linux", True),
    get_wiki_data("Seinfeld", True),
    get_wiki_data("Matchbox_Twenty", True),
    get_wiki_data("Roman_Empire", True),
    get_wiki_data("London", True),
    get_wiki_data("Python_(programming_language)", True),
    get_wiki_data("Monty_Python", True),
]

当重新运行示例时,我们从 OpenAI API 收到错误:

$ python3 -c'from langchain_bot import print_answer; print_answer("What are the main differences between Linux and Windows?")'

openai.error.InvalidRequestError: This model's maximum context length is 4097 tokens, however you requested 6215 tokens (5959 in your prompt; 256 for the completion). Please reduce your prompt; or completion length.

有两种选择可以解决这个问题。 我们可以使用不同的链,也可以尝试限制模型使用的来源数量。 让我们从第一个选项开始。

6、使用多步链

 

回想一下我们如何在玩具示例中创建链条:

chain = load_qa_with_sources_chain(OpenAI(temperature=0))

实际上有一个隐式的第二个参数来指定我们正在使用的链的类型。 到目前为止,我们正在使用填充链,它只是将所有源填充到提示中。 我们可以使用另外两种类型的链:

  • map_reduce:映射所有源并对其进行汇总,以便它们更有可能适合上下文窗口。 这将为每个查询处理语料库中的每个标记,但可以并行运行。
  • refine:连续迭代每个源,并要求底层模型根据源改进其答案。 根据我的经验,这太慢了以至于完全无法使用。

那么,让我们看看如果我们使用 map_reduce 链会发生什么。 更新我们的玩具示例以将其作为参数传递:

chain = load_qa_with_sources_chain(OpenAI(temperature=0), chain_type="map_reduce")

让我们重新运行这个例子。

$ python3 -c'from langchain_bot import print_answer; print_answer("What are the main differences between Linux and Windows?")'
Linux is an open-source Unix-like operating system based on the Linux kernel, while Windows is a group of proprietary graphical operating system families developed and marketed by Microsoft. Linux distributions are typically packaged as a Linux distribution, which includes the kernel and supporting system software and libraries, while Windows distributions include a windowing system such as X11 or Wayland, and a desktop environment such as GNOME or KDE Plasma.
SOURCES:
https://en.wikipedia.org/wiki/Unix
https://en.wikipedia.org/wiki/Microsoft_Windows
https://en.wikipedia.org/wiki/Linux

有效! 然而,这确实需要对 OpenAI API 进行多次调用,并且向机器人提出的每个问题都需要处理每个令牌,这既缓慢又昂贵。 此外,答案中存在一些不准确之处,这可能来自摘要。

我们发现使用不同的方法 - 向量空间搜索与东西链 - 是迄今为止最好的解决方案。

7、使用向量空间搜索引擎提高效率

 

我们可以使用向量空间搜索引擎解决 map_reduce 链的问题和 stuff 链的局限性。 在高层次上:

  • 提前,我们创建一个传统的搜索索引并将所有源添加到其中。
  • 在查询时,我们使用问题查询搜索索引并返回前 k 个结果。
  • 我们使用这些结果作为我们在东西链中的来源。

让我们一次为这一步编写代码。 首先,我们需要添加一些导入:

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores.faiss import FAISS

接下来,让我们为所有来源创建一个 Faiss 搜索索引。 幸运的是,LangChain 包含一个使它成为单行代码的帮助程序类。

search_index = FAISS.from_documents(sources, OpenAIEmbeddings())

这段代码做了三件事:

  • 它创建一个 Faiss 内存索引。
  • 它使用 OpenAI API 为每个来源创建嵌入(即特征向量),使其易于搜索。 如果需要,你可以使用其他嵌入,但 OpenAI 会为此应用程序生成高质量的嵌入。
  • 它将每个来源添加到索引中。

最后,让我们更新其余代码以利用搜索索引。 对于这个例子,我们将使用前 4 个搜索结果来告知模型的答案:

chain = load_qa_with_sources_chain(OpenAI(temperature=0))

def print_answer(question):
    print(
        chain(
            {
                "input_documents": search_index.similarity_search(question, k=4),
                "question": question,
            },
            return_only_outputs=True,
        )["output_text"]
    )

当我们运行这个例子时,它起作用了! 事实上,我们现在可以在 Faiss 索引中添加尽可能多的来源(而且数量很多!),我们的模型仍然会快速执行。

$ python3 -c'from langchain_bot import print_answer; print_answer("Which members of Matchbox 20 play guitar?")' Rob Thomas, Kyle Cook, and Paul Doucette play guitar in Matchbox 20.
SOURCES: https://en.wikipedia.org/wiki/Matchbox_Twenty

8、处理太大的文档

好的,现在让我们尝试处理更大的文档。 通过将最后一个参数切换为 False,更改我们的来源列表以包括完整的维基百科页面,而不仅仅是第一部分:

sources = [
    get_wiki_data("Unix", False),
    get_wiki_data("Microsoft_Windows", False),
    get_wiki_data("Linux", False),
    get_wiki_data("Seinfeld", False),
    get_wiki_data("Matchbox_Twenty", False),
    get_wiki_data("Roman_Empire", False),
    get_wiki_data("London", False),
    get_wiki_data("Python_(programming_language)", False),
    get_wiki_data("Monty_Python", False),
]

不幸的是,我们现在在查询我们的机器人时遇到错误:

$ python3 -c'from langchain_bot import print_answer; print_answer("Who plays guitar in Matchbox 20?")'
openai.error.InvalidRequestError: This model's maximum context length is 8191 tokens, however you requested 11161 tokens (11161 in your prompt; 0 for the completion). Please reduce your prompt; or completion length.
Even though we are filtering down the individual documents, each document is now so big we cannot fit it

即使我们正在过滤单个文档,每个文档现在都太大了,我们无法将其放入上下文窗口。

解决此问题的一种非常简单但有效的方法是将文档简单地分成固定大小的块。 虽然这看起来“太笨了,无法工作”,但实际上它在实践中似乎工作得很好。 LangChain 包含一个有用的实用程序来为我们做这件事。 让我们从导入它开始吧。

from langchain.text_splitter import CharacterTextSplitter

接下来,让我们遍历源列表并创建一个名为 source_chunks 的新列表,Faiss 索引将使用该列表代替完整文档:

source_chunks = []
splitter = CharacterTextSplitter(separator=" ", chunk_size=1024, chunk_overlap=0)
for source in sources:
    for chunk in splitter.split_text(source.page_content):
        source_chunks.append(Document(page_content=chunk, metadata=source.metadata))

search_index = FAISS.from_documents(source_chunks, OpenAIEmbeddings())

这里有几点需要注意:

  • 我们已将 CharacterTextSplitter 配置为创建最大大小为 1024 个字符且无重叠的块。 此外,它们在空白边界处分裂。 LangChain 中包含其他更智能的拆分器,它们利用 NLTK 和 spaCy 等库,但对于本示例,我们将使用最简单的选项。
  • 文档中的所有块共享相同的元数据。

最后,当我们重新运行时,我们看到模型给了我们一个答案:

$ python3 -c'from langchain_bot import print_answer; print_answer("Which members of Matchbox 20 play guitar?")'
Rob Thomas, Paul Doucette, and Kyle Cook play guitar in Matchbox 20.
SOURCES: https://en.wikipedia.org/wiki/Matchbox_Twenty

9、应用到 GitHub 存储库

现在让我们把写的东西应用到 GitHub 仓库中。 让我们首先添加一些必需的导入:

import pathlib
import subprocess
import tempfile

接下来,我们需要一个函数来检查 GitHub 存储库的最新副本,抓取markdown文件,并返回一些 LangChain 文档。

def get_github_docs(repo_owner, repo_name):
    with tempfile.TemporaryDirectory() as d:
        subprocess.check_call(
            f"git clone --depth 1 https://github.com/{repo_owner}/{repo_name}.git .",
            cwd=d,
            shell=True,
        )
        git_sha = (
            subprocess.check_output("git rev-parse HEAD", shell=True, cwd=d)
            .decode("utf-8")
            .strip()
        )
        repo_path = pathlib.Path(d)
        markdown_files = list(repo_path.glob("*/*.md")) + list(
            repo_path.glob("*/*.mdx")
        )
        for markdown_file in markdown_files:
            with open(markdown_file, "r") as f:
                relative_path = markdown_file.relative_to(repo_path)
                github_url = f"https://github.com/{repo_owner}/{repo_name}/blob/{git_sha}/{relative_path}"
                yield Document(page_content=f.read(), metadata={"source": github_url})

这做了一些事情:

  • 它将所需 GitHub 存储库的最新提交签出到一个临时目录中。
  • 它获取 git sha(用于构建链接,模型将在其源列表中使用)。
  • 它遍历 repo 中的每个降价文件(.md 或 .mdx)。
  • 它在 GitHub 上构造一个 markdown 文件的 URL,从磁盘读取文件,并返回一个 Document

现在让我们把它连接到我们的机器人上。 用以下内容替换以前的源列表:

sources = get_github_docs("dagster-io", "dagster")

10、尝试一下!

让我们尝试一下,看看它是否理解 Dagster API 的细微差别。 我们将从询问有关软件定义资产的问题开始。

$ python3
Python 3.8.13 (default, Oct  4 2022, 14:00:32)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from langchain_bot import print_answer
>>> print_answer("what is a software defined asset")
 A software-defined asset is a Dagster object that couples an asset to the function and upstream assets that are used to produce its contents. It enables a declarative approach to data management, in which code is the source of truth on what data assets should exist and how those assets are computed.
SOURCES:
https://github.com/dagster-io/dagster/blob/ba3a38112867607661062a3be681244f91de11d8/docs/content/concepts/assets/software-defined-assets.mdx
https://github.com/dagster-io/dagster/blob/ba3a38112867607661062a3be681244f91de11d8/docs/content/guides/dagster/enriching-with-software-defined-assets.mdx
https://github.com/dagster-io/dagster/blob/ba3a38112867607661062a3be681244f91de11d8/docs/content/tutorial/assets/defining-an-asset.md
>>> print_answer("what is the difference between ops, jobs, assets and graphs")
 Ops are the core unit of computation in Dagster and contain the logic of an orchestration graph. Jobs are the main unit of execution and monitoring in Dagster and contain a graph of ops connected via data dependencies. Assets are persistent objects in storage, such as a table, machine learning (ML) model, or file. Graphs are sets of interconnected ops or sub-graphs and form the core of jobs.
SOURCES:
https://github.com/dagster-io/dagster/blob/ba3a38112867607661062a3be681244f91de11d8/docs/content/concepts/ops-jobs-graphs/graphs.mdx
https://github.com/dagster-io/dagster/blob/ba3a38112867607661062a3be681244f91de11d8/docs/content/concepts/ops-jobs-graphs/jobs.mdx
https://github.com/dagster-io/dagster/blob/ba3a38112867607661062a3be681244f91de11d8/

我对这个回应很满意。 它能够令人信服地解释小众技术概念,而不仅仅是从文档中逐字逐句地重复句子。

但是,此时你可能已经注意到我们的小机器人变得非常慢。 让我们解决这个问题!

11、使用缓存嵌入以节省时间和金钱

我们现在有一个运行良好的聊天机器人,但它有一个主要问题:启动时间非常慢。 每次我们导入脚本时,有两个步骤特别慢:

  • 我们使用 git 克隆 repo,爬取每个 markdown 文件并将它们分块
  • 我们为每个文档调用 OpenAI API,创建嵌入,并将其添加到 Faiss 索引

理想情况下,我们只会偶尔运行这些步骤并缓存索引以供后续运行使用。 这将提高性能并显着降低成本,因为我们将不再在启动时重新计算嵌入。

此外,如果这个过程不是“全有或全无”,那就太好了。 如果我们每次都可以迭代我们的 Faiss 索引或嵌入而不重新克隆 repo,我们可以大大提高迭代速度。

我们不再有简单的 Python 脚本。 我们现在有一个数据管道,数据管道需要像 Dagster 这样的编排器。 Dagster 使我们能够快速轻松地添加这种多步缓存功能,并支持其他功能,例如添加自动调度和传感器以在外部触发器上重新运行管道。


原文链接:
http://www.bimant.com/blog/gpt-3-chatbot-hands-on/

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐