本章将探讨LangChain框架和GPT-4插件的世界。我们将看看LangChain如何实现与不同语言模型的互动,以及扩展GPT-4功能的插件的重要性。这些高级知识对于开发依赖LLM的复杂、前沿应用程序至关重要。
LangChain框架
LangChain是一个专门用于开发LLM驱动应用程序的新框架。您会发现,集成LangChain的代码比第3章提供的示例要优雅得多。该框架还提供了许多额外的可能性。 使用pip install langchain 快速而简便地安装LangChain。
LangChain的关键功能被分成了模块,如图5-1所示。
以下是对这些模块的简要描述:
- 模型
Models模块是LangChain提供的标准接口,通过它你可以与各种LLM互动。该框架支持来自不同提供商(包括OpenAI、Hugging Face、Cohere、GPT4All等)的不同模型类型集成。
- 提示
提示正在成为编程LLM的新标准。Prompts模块包括许多用于提示管理的工具。
- 索引
这个模块允许你将LLM与你的数据结合起来。
- 链条
通过这个模块,LangChain提供了Chain接口,允许你创建一系列调用,将多个模型或提示组合在一起。
- 代理
代理模块引入了代理接口。代理是一个可以处理用户输入、做出决策并选择适当工具来完成任务的组件。它是迭代工作的,采取行动直到达到解决方案。
- 内存
Memory模块允许你在链条或代理调用之间保持状态。默认情况下,链条和代理是无状态的,这意味着它们独立处理每个传入的请求,就像LLM一样。
LangChain是不同LLM的通用接口;你可以在其文档页面上查看所有的集成。OpenAI和许多其他LLM提供商都在这个集成列表中。大多数这些集成需要它们的API密钥来建立连接。对于OpenAI模型,你可以像我们在第二章中看到的那样进行设置,将密钥设置在OPENAI_API_KEY环境变量中。
动态提示
展示LangChain工作方式的最简单方法是为您提供一个简单的脚本。在这个示例中,使用OpenAI和LangChain来完成一个简单的文本完成操作:
from langchain.chat_models import ChatOpenAI
from langchain import PromptTemplate, LLMChain
template = """Question: {question}
Let's think step by step.
Answer: """
prompt = PromptTemplate(template=template, input_variables=["question"])
llm = ChatOpenAI(model_name="gpt-4")
llm_chain = LLMChain(prompt=prompt, llm=llm)
question = """ What is the population of the capital of the country where the
Olympic Games were held in 2016? """
llm_chain.run(question)
输出如下:
Step 1: Identify the country where the Olympic Games were held in 2016.
Answer: The 2016 Olympic Games were held in Brazil.
Step 2: Identify the capital of Brazil.
Answer: The capital of Brazil is Brasília.
Step 3: Find the population of Brasília.
Answer: As of 2021, the estimated population of Brasília is around 3.1 million.
So, the population of the capital of the country where the Olympic Games were
held in 2016 is around 3.1 million. Note that this is an estimate and may
vary slightly.'
PromptTemplate 负责构建模型的输入。因此,它是生成提示的可重现方式。它包含一个称为模板的输入文本字符串,在其中可以通过 input_variables 指定值。在我们的示例中,我们定义的提示自动将“让我们一步一步地思考”部分添加到问题中。
在这个示例中使用的LLM是 GPT-4;当前默认的模型是 gpt-3.5-turbo。模型通过 ChatOpenAI() 函数放入变量 llm。这个函数假定 OpenAI API 密钥已经设置在环境变量 OPENAI_API_KEY 中,就像前几章的示例中一样。
通过函数 LLMChain() 将提示和模型组合在一起,形成一个包含这两个元素的链。最后,我们需要调用 run() 函数来请求完成输入的问题。当执行 run() 函数时,LLMChain 使用提供的输入键值(以及如果可用的话还有内存键值)格式化提示模板,将格式化后的字符串传递给 LLM,最后返回 LLM 的输出。我们可以看到,模型通过应用“让我们一步一步地思考”的规则自动回答了问题。
正如你所见,动态提示是一个简单但非常有价值的功能,特别适用于复杂的应用程序和更好的提示管理。
代理和工具
代理和工具是LangChain框架的关键功能:它们可以使您的应用程序变得非常强大。它们使您能够通过让LLM执行操作并与各种功能集成来解决复杂的问题。
工具是围绕函数的特定抽象,使语言模型更容易与之交互。代理可以使用工具与世界互动。具体而言,工具的接口具有单一的文本输入和单一的文本输出。在LangChain中有许多预定义的工具。这些包括Google搜索、维基百科搜索、Python REPL、计算器、全球天气预报API等等。要获取完整的工具列表,请查看LangChain提供的文档中的工具页面。您还可以构建自定义工具并将其加载到您正在使用的代理中:这使代理变得非常灵活和强大。 正如我们在第4章中学到的,通过在提示中添加“让我们一步一步地思考”,您可以在某种程度上增加模型的推理能力。通过将这个句子添加到提示中,要求模型花更多的时间来回答问题。
在本节中,我们介绍了一个适用于需要一系列中间步骤的应用程序的代理。代理安排这些步骤,并可以访问各种工具,决定使用哪个工具以有效地回答用户的查询。在某种程度上,就像“一步一步地思考”一样,代理将有更多的时间来计划其操作,从而使其能够完成更复杂的任务。
代理的高级伪代码如下:
- 代理从用户那里接收一些输入。
- 代理决定使用哪个工具(如果有的话),以及在该工具中输入什么文本。
- 然后,使用该输入文本调用该工具,并从该工具接收输出文本。
- 将工具的输出馈送到代理的上下文中。
步骤2到4将重复,直到代理决定不再需要使用工具,此时它将直接回应用户。 您可能会注意到,这似乎接近我们在第3章中所做的事情,例如个人助手的示例,该助手可以回答问题并执行操作。LangChain代理允许您开发这种类型的行为…但更加强大。 为了更好地说明代理如何在LangChain中使用工具,图5-2提供了与交互的视觉演示。
对于本节,我们希望能够回答以下问题:2016年奥运会举办国家的首都的人口的平方根是多少?这个问题没有真正的兴趣,但它很好地演示了LangChain代理和工具如何为LLM添加推理能力。 如果我们将问题原样提问给GPT-3.5 Turbo,我们会得到以下答案:
The capital of the country where the Olympic Games were held in 2016 is Rio de
Janeiro, Brazil. The population of Rio de Janeiro is approximately 6.32 million
people as of 2021. Taking the square root of this population, we get
approximately 2,513.29. Therefore, the square root of the population of
the capital of the country where the Olympic Games were held in 2016 is
approximately 2,513.29.
这个答案在两个方面都是错误的:巴西的首都是巴西利亚,不是里约热内卢,而632万的平方根是2,513.96。通过添加“逐步思考”或使用其他提示工程技术,我们也许能够获得更好的结果,但由于模型在推理和数学运算方面的困难,仍然很难相信结果。使用LangChain可以更好地保证准确性。
以下代码提供了一个简单的示例,说明了代理如何在LangChain中使用两个工具:维基百科和计算器。在通过load_tools()函数创建工具之后,使用initialize_agent()函数创建代理。代理的推理需要一个LLM;在这里,使用GPT-3.5 Turbo。参数zero-shot-react-description定义了代理如何在每个步骤中选择工具。通过将verbose值设置为true,我们可以查看代理的推理过程,了解它如何得出最终决策:
from langchain.chat_models import ChatOpenAI
from langchain.agents import load_tools, initialize_agent, AgentType
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
tools = load_tools(["wikipedia", "llm-math"], llm=llm)
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
question = """What is the square root of the population of the capital of the
Country where the Olympic Games were held in 2016?"""
agent.run(question)
正如您所见,代理决定查询维基百科以获取有关2016年夏季奥林匹克运动会的信息:
> Entering new chain...
I need to find the country where the Olympic Games were held in 2016 and then find
the population of its capital city. Then I can take the square root of that population.
Action: Wikipedia
Action Input: "2016 Summer Olympics"
Observation: Page: 2016 Summer Olympics
[...]
接下来的输出行包含有关奥运会的维基百科摘录。接下来,代理程序使用维基百科工具两次:
Thought:I need to search for the capital city of Brazil.
Action: Wikipedia
Action Input: "Capital of Brazil"
Observation: Page: Capitals of Brazil
Summary: The current capital of Brazil, since its construction in 1960, is
Brasilia. [...]
Thought: I have found the capital city of Brazil, which is Brasilia. Now I need
to find the population of Brasilia.
Action: Wikipedia
Action Input: "Population of Brasilia"
Observation: Page: Brasilia
[...]
接下来,代理程序使用计算器工具:
Thought: I have found the population of Brasilia, but I need to calculate the
square root of that population.
Action: Calculator
Action Input: Square root of the population of Brasilia (population: found in
previous observation)
Observation: Answer: 1587.051038876822
最终:
Thought:I now know the final answer
Final Answer: The square root of the population of the capital of the country
where the Olympic Games were held in 2016 is approximately 1587.
> Finished chain.
正如您所看到的,代理程序展示了复杂的推理能力:在提出最终答案之前,它完成了四个不同的步骤。LangChain框架允许开发人员仅使用几行代码实现这种推理能力。
内存
在某些应用中,记住以前的交互是至关重要的,无论是短期还是长期。使用LangChain,您可以轻松地向链和代理添加状态以管理内存。构建聊天机器人是这种功能的最常见示例。您可以使用ConversationChain非常快速地完成这个操作,只需几行代码,就可以将语言模型转换成一个聊天工具。
以下代码使用text-ada-001模型创建了一个聊天机器人。这是一个只能执行基本任务的小型模型。然而,它是GPT-3系列中速度最快、成本最低的模型。这个模型从未经过微调,以表现得像一个聊天机器人,但我们可以看到,只需使用LangChain的两行代码,我们就可以使用这个简单的完成模型进行聊天:
from langchain import OpenAI, ConversationChain
chatbot_llm = OpenAI(model_name='text-ada-001')
chatbot = ConversationChain(llm=chatbot_llm , verbose=True)
chatbot.predict(input='Hello')
在上述代码的最后一行,我们执行了predict(input='Hello')
。这将导致聊天机器人被要求回应我们的“Hello”消息。正如您所看到的,模型会回答:
> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is
talkative and provides lots of specific details from its context. If the AI
does not know the answer to a question, it truthfully says it does not know.
Current conversation:
Human: Hello
AI:
> Finished chain.
' Hello! How can I help you?'
由于在ConversationChain中设置了verbose=True,我们可以查看LangChain使用的完整提示。当我们执行predict(input='Hello')
时,LLM text-ada-001收到的不仅仅是“Hello”消息,而是一个完整的提示,介于标签> Entering new ConversationChain chain…
和> Finished chain
之间。
如果我们继续对话,您可以看到该函数会在提示中保留对话历史记录。如果我们随后问“我可以问你一个问题吗?你是人工智能吗?”对话的历史记录也将包含在提示中:
> Entering new ConversationChain chain...
Prompt after formatting:
The following [...] does not know.
Current conversation:
Human: Hello
AI: Hello! How can I help you?
Human: Can I ask you a question? Are you an AI?
AI:
> Finished chain.
'nnYes, I am an AI.'
ConversationChain对象使用提示工程技术和记忆技术,将任何进行文本生成的LLM转化为一个聊天工具。
嵌入(Embeddings)
将语言模型与您自己的文本数据相结合是个性化应用模型知识的强大方法。原则与第三章中讨论的相同:第一步是信息检索,即接受用户的查询并返回最相关的文档。然后将文档发送到模型的输入上下文中,以请求其回答查询。本节展示了如何使用LangChain和嵌入(embeddings)轻松实现此目标。
LangChain中一个重要的模块是document_loaders。使用此模块,您可以快速将不同来源的文本数据加载到应用程序中。例如,您的应用程序可以加载CSV文件、电子邮件、PowerPoint文档、Evernote笔记、Facebook聊天、HTML页面、PDF文档等等格式的数据。官方文档提供了完整的加载器列表。每个加载器都非常容易设置。以下示例重新使用了《塞尔达传说:荒野之息》探索者指南的PDF文档,如果PDF文件在当前工作目录中,以下代码将加载其内容并按页划分:
from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("ExplorersGuide.pdf")
pages = loader.load_and_split()
为了进行信息检索,需要嵌入(embed)每个加载的页面。正如我们在第二章中讨论的那样,嵌入是信息检索中使用的一种技术,用于将非数值概念(如单词、标记和句子)转换为数值向量。这些嵌入使模型能够有效地处理这些概念之间的关系。借助OpenAI的嵌入(endpoint),开发人员可以获取输入文本的数值向量表示,而LangChain具有调用这些嵌入的包装器(wrapper):
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
索引(index)保存了页面的嵌入(embeddings),并使搜索变得容易。LangChain以向量数据库为中心。可以在许多向量数据库中进行选择,完整的列表可以在官方文档中找到。以下代码片段使用了FAISS向量数据库,这是一个主要由Meta的基础AI研究小组开发的相似性搜索库:
from langchain.vectorstores import FAISS
db = FAISS.from_documents(pages, embeddings)
为了更好地说明PDF文档的内容是如何转化为嵌入(embeddings)页面并存储在FAISS向量数据库中的,图5-3以可视化的方式总结了这个过程。
现在,搜索相似性非常容易:
q = "What is Link's traditional outfit color?"
db.similarity_search(q)[0]
从上述代码中,我们得到以下结果:
Document(page_content='While Link’s traditional green
tunic is certainly an iconic look, his
wardrobe has expanded [...] Dress for Success',
metadata={'source': 'ExplorersGuide.pdf', 'page': 35})
问题的答案是Link的传统服装颜色是绿色,我们可以看到答案在所选内容中。输出中指出答案在ExplorersGuide.pdf的第35页。请记住,Python是从零开始计数的,因此如果您回到原始的《荒野之息:塞尔达传说探险指南》PDF文件,您会发现答案在第36页(而不是第35页)。
图5-4展示了信息检索过程如何使用查询的嵌入和向量数据库来识别与查询最相似的页面。
您可能希望将嵌入集成到您的聊天机器人中,以便在回答您的问题时使用它检索到的信息。同样,在LangChain中,只需几行代码即可轻松完成。我们使用RetrievalQA,它的输入是一个LLM和一个向量数据库。然后,我们以通常的方式向获取的对象提出问题:
from langchain.chains import RetrievalQA
from langchain import OpenAI
llm = OpenAI()
chain = RetrievalQA.from_llm(llm=llm, retriever=db.as_retriever())
q = "What is Link's traditional outfit color?"
chain(q, return_only_outputs=True)
我们得到以下答案:
{'result': " Link's traditional outfit color is green."}
图5-5展示了RetrievalQA如何使用信息检索来回答用户的问题。正如我们在这个图中看到的,“构建上下文”将信息检索系统找到的页面与用户的初始查询分组在一起。然后,这个丰富的上下文被发送给语言模型,语言模型可以使用上下文中添加的额外信息来正确回答用户的问题。
您可能会想知道为什么需要在将文档信息作为输入发送到语言模型的上下文之前进行信息检索。事实上,目前的语言模型无法考虑包含数百页的大文件。因此,如果输入数据过大,我们会进行预过滤。这就是信息检索过程的任务。在不久的将来,随着输入上下文的增加,可能会出现情况,不再技术上需要使用信息检索技术。
GPT-4插件
尽管包括 GPT-4 在内的语言模型在各种任务中都表现出了帮助,但它们也有固有的局限性。例如,这些模型只能从它们训练时使用的数据中学习,这些数据通常已经过时或不适用于特定的应用程序。此外,它们的能力仅限于文本生成。我们还看到 LLM 在某些任务上不起作用,例如复杂的计算。
本节重点介绍 GPT-4 的一个开创性功能:插件(请注意,GPT-3.5 模型没有访问插件功能)。在人工智能的演进过程中,插件已经成为一种重新定义与LLM互动的新型变革性工具。插件的目标是为LLM提供更广泛的功能,使模型能够访问实时信息,执行复杂的数学计算,并利用第三方服务。
我们在第1章中看到,该模型无法执行诸如 3,695 × 123,548 这样的复杂计算。在图5-6中,我们启用了计算器插件,可以看到模型在需要进行计算时会自动调用计算器,从而找到正确的解决方案。
通过迭代式的部署方法,OpenAI 逐步向 GPT-4 添加插件,这使得 OpenAI 能够考虑插件的实际用途以及可能引入的安全性和定制挑战。虽然自2023年5月以来,所有付费用户都可以使用插件,但在撰写本文时,还未向所有开发人员提供创建新插件的能力。
OpenAI 的目标是创建一个生态系统,插件可以帮助塑造人工智能与人类互动的未来动态。今天,对于一家严肃的企业来说,没有自己的网站是不可想象的,但也许不久的将来,每家公司都需要拥有自己的插件。事实上,一些早期的插件已经由公司如 Expedia、FiscalNote、Instacart、KAYAK、Klarna、Milo、OpenTable、Shopify 和 Zapier 等创建出来。
除了它们的主要功能外,插件还有几种方式扩展了 GPT-4 的功能。从某种意义上说,插件与“LangChain 框架”中讨论的代理和工具之间存在一些相似之处。例如,插件可以使 LLM 能够检索实时信息,如体育比分和股票价格,从知识库中提取数据,如企业文件,以及根据用户的需求执行任务,如预订航班或订餐。它们都旨在帮助人工智能访问最新信息并执行计算。然而,GPT-4 中的插件更专注于第三方服务,而不同于 LangChain 的工具。
本节通过探讨 OpenAI 网站上提供的示例的关键要点,引入了创建插件的基本概念。正如我们在撰写本书时,插件仍处于有限的 beta 版本中,因此鼓励读者访问 OpenAI 参考页面以获取最新信息。还要注意,在 beta 阶段,用户必须在 ChatGPT 的用户界面中手动启用其插件,作为开发人员,您最多只能与不超过 100 名用户共享您的插件。
概览
作为插件开发者,您需要创建一个 API 并与之关联两个描述性文件:插件清单和 OpenAPI 规范。当用户开始与 GPT-4 交互时,如果安装了您的插件,OpenAI 会向 GPT 发送一个隐藏的消息。此消息简要介绍了您的插件,包括其描述、端点和示例。
然后,该模型成为了一个智能 API 调用者。当用户提出关于您的插件的问题时,模型可以调用您的插件 API。调用插件的决定是基于 API 规范和自然语言描述您的 API 应该在何种情况下使用。一旦模型决定调用您的插件,它将将 API 结果合并到其上下文中,以向用户提供响应。因此,插件的 API 响应必须返回原始数据,而不是自然语言响应。这允许 GPT 基于返回的数据生成自己的自然语言响应。
例如,如果用户问“在纽约我应该住在哪里?”,模型可以使用一个酒店预订插件,然后将插件的 API 响应与其语言生成能力结合起来,提供既信息丰富又用户友好的答案。
API
这是一个简化版本的代码示例,演示了在 OpenAI 的 GitHub 上提供的待办事项列表定义插件:
import json
import quart
import quart_cors
from quart import request
app = quart_cors.cors(
quart.Quart(__name__), allow_origin="https://chat.openai.com"
)
# Keep track of todo's. Does not persist if Python session is restarted.
_TODOS = {}
@app.post("/todos/<string:username>")
async def add_todo(username):
request = await quart.request.get_json(force=True)
if username not in _TODOS:
_TODOS[username] = []
_TODOS[username].append(request["todo"])
return quart.Response(response="OK", status=200)
@app.get("/todos/<string:username>")
async def get_todos(username):
return quart.Response(
response=json.dumps(_TODOS.get(username, [])), status=200
)
@app.get("https://b2.7b2.com/.well-known/ai-plugin.json")
async def plugin_manifest():
host = request.headers["Host"]
with open("./.well-known/ai-plugin.json") as f:
text = f.read()
return quart.Response(text, mimetype="text/json")
@app.get("https://b2.7b2.com/openapi.yaml")
async def openapi_spec():
host = request.headers["Host"]
with open("openapi.yaml") as f:
text = f.read()
return quart.Response(text, mimetype="text/yaml")
def main():
app.run(debug=True, host="0.0.0.0", port=5003)
if __name__ == "__main__":
main()
这段Python代码是一个管理待办事项列表的简单插件示例。首先,使用quart_cors.cors()
初始化了变量app
。这行代码创建一个新的Quart应用程序,并配置它以允许来自chat.openai.com的跨源资源共享(CORS)。Quart是一个Python Web微框架,Quart-CORS是一个扩展,允许对CORS进行控制。这个设置允许插件与指定URL上托管的ChatGPT应用程序进行交互。
然后,代码定义了几个HTTP路由,对应于待办事项列表插件的不同功能:add_todo
函数与POST请求相关联,get_todos
函数与GET请求相关联。
接下来,定义了两个额外的端点:plugin_manifest
和openapi_spec
。这些端点提供了插件的清单文件和OpenAPI规范,对于GPT-4和插件之间的交互非常重要。这些文件包含有关插件及其API的详细信息,GPT-4使用这些信息来了解何时以及如何使用插件。
插件清单
每个插件都需要在API的域上有一个ai-plugin.json文件。例如,如果您的公司在thecompany.com上提供服务,您必须将此文件托管在thecompany.com/.well-known。当安装插件时,OpenAI会在/.well-known/ai-plugin.json中查找此文件。如果没有此文件,插件将无法安装。以下是所需的ai-plugin.json文件的最小定义:
{
"schema_version": "v1",
"name_for_human": "TODO Plugin",
"name_for_model": "todo",
"description_for_human": "Plugin for managing a TODO list.
You can add, remove and view your TODOs.",
"description_for_model": "Plugin for managing a TODO list.
You can add, remove and view your TODOs.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "http://localhost:3333/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "http://localhost:3333/logo.png",
"contact_email": "support@thecompany.com",
"legal_info_url": "http://www.thecompany.com/legal"
}
这些字段的详细信息请参见表格5-1。
OpenAPI规范
接下来创建插件的下一步是创建包含API规范的openapi.yaml文件。此文件必须遵循OpenAPI标准(参见“理解OpenAPI规范”)。GPT模型只通过此API规范文件和清单文件中详细的信息来了解您的API。
以下是待办事项列表定义插件的openapi.yaml文件的第一行示例:
openapi: 3.0.1
info:
title: TODO Plugin
description: A plugin that allows the user to create and manage a TODO list
using ChatGPT. If you do not know the user's username, ask them first before
making queries to the plugin. Otherwise, use the username "global".
version: 'v1'
servers:
- url: http://localhost:5003
paths:
/todos/{username}:
get:
operationId: getTodos
summary: Get the list of todos
parameters:
- in: path
name: username
schema:
type: string
required: true
description: The name of the user.
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/getTodosResponse'
[...]
将OpenAPI规范视为描述性文档,应该足够详细,以便单独理解和使用您的API。当在GPT-4中执行搜索时,info部分中的描述用于确定插件与用户搜索的相关性。其余的OpenAPI规范遵循标准的OpenAPI格式。许多工具可以根据您现有的API代码或反之生成OpenAPI规范。
描述
当用户的请求可能从插件中获益时,模型会启动对OpenAPI规范中的端点描述以及清单文件中的description_for_model属性的扫描。您的目标是创建最合适的响应,通常涉及测试不同的请求和描述。
OpenAPI文档应提供关于API的各种详细信息,例如可用的功能及其各自的参数。它还应包含特定属性的“描述”字段,提供有价值的、自然书写的解释,说明每个功能的作用以及查询字段预期的信息类型。这些描述指导模型以最适当的方式使用API。
在这个过程中的关键元素是description_for_model属性。这为您提供了一种告知模型如何使用插件的方法。强烈建议创建简明、清晰和具有描述性的指令。
然而,在编写这些描述时遵循某些最佳实践至关重要:
- 不要试图影响GPT的情绪、个性或确切响应。
- 避免指导GPT使用特定的插件,除非用户明确请求该类别的服务。
- 不要为GPT使用插件指定特定触发器,因为它被设计成能够自主确定何时适当使用插件。
总之,为GPT-4开发插件涉及创建一个API,根据OpenAPI规范指定其行为,并在清单文件中描述插件及其使用方式。通过这种设置,GPT-4可以有效地充当智能API调用器,将其能力扩展到文本生成之外。