跳至主要内容

构建具备上下文感知的聊天机器人:借助 LangChain 框架与 ChatGPT

探索如何使用 ChatGPT 与 LangChain 框架构建具备上下文感知能力的聊天机器人。
· 15分钟 阅读

使用 AI 探索

在当今快速发展的数字环境中,随着大型语言模型(LLMs)的兴起,会话式应用变得极为流行。聊天机器人已经改变了我们在日常生活中与应用、网站乃至客户服务渠道的交互方式。

构建您自己的聊天机器人的能力已成为游戏规则的改变者,使企业和个人能够根据特定需求创建个性化、高效且更具吸引力的对话界面。

本文旨在拆解如何在 Python 中利用 LLMs 的能力来构建聊天机器人。我们将探索如何使用 ChatGPT 创建自定义聊天机器人,以及如何使用流行的 LangChain 框架进一步优化它。

如果您对 ChatGPT 和 LangChain 还不熟悉,请查看下方我们的基于 GPT 与 LangChain 的大型语言模型入门代码演练:

本文涵盖了使用 ChatGPT 作为底层模型来开发优化聊天机器人的所有关键环节。我们将在已安装 openai 和 langchain 库的 Jupyter Notebook 中操作。您可以在此 DataLab 工作簿中跟进。

接下来,我们将探讨如何使用 OpenAI API 对 ChatGPT 进行基本 API 调用,作为我们对话的构建基块。此外,我们将使用基础的 Python 结构为聊天机器人实现上下文感知。最后,我们将使用 LangChain 优化聊天机器人的实现与对话历史管理,并探索其记忆功能。

基础 ChatGPT API 调用

我们将使用 ChatGPT 作为聊天机器人的底层模型。因此,我们的第一个动手任务是编写一个简单且完整的 ChatGPT API 调用,以便建立首次交互。

下面的 Python 函数仅使用 openai 库封装了我们所需的 ChatGPT API 调用。

import os
import openai

openai_api_key = os.environ["OPENAI_API_KEY"]

def chatgpt_call(prompt, model="gpt-3.5-turbo"):
   response = openai.ChatCompletion.create(
       model=model,
       messages=[{"role": "user", "content": prompt}]
   )
   return response.choices[0].message["content"]

请注意,这里我们使用的是 gpt-3.5-turbo 模型,因为它是最强大的 GPT-3.5 系列聊天优化模型之一,同时也是最便宜的模型之一。关于可用不同模型的更多信息,请参见 OpenAI 的文档。

需要获取 OpenAI API 密钥的帮助吗?那么这门培训课程 Getting Started with the OpenAI API and ChatGPT 适合您。

让我们一步步分析上述实现!

函数 chatgpt_call() 是对 OpenAI 原始函数 ChatCompletion.create 的封装,用于创建一次聊天补全请求。

给定所需的提示词(prompt),该函数会返回相应的 ChatGPT 补全结果。成功调用只需提供要使用的模型 ID(model)和所需的提示词(prompt)。

ChatGPT 返回的补全结果包含元数据以及回复本身。不过,回复文本始终位于 response.choices[0].message[“content”] 字段中。有关完整补全内容的更多信息,请参见 OpenAI 的文档。

基于该实现,我们可以用一行代码向 ChatGPT 发送提示:

prompt = "My name is Andrea"
response = chatgpt_call(prompt)
print(response)

# Output: Nice to meet you, Andrea! How can I assist you today?

一次性交互的调用

您是否注意到上述实现中有什么隐患?

提示一下,让我们做一次跟进交互:

prompt = "Do you remember my name?"
response = chatgpt_call(prompt)
print(response)

# Output: I'm sorry, as an AI language model, I don't have the ability to remember specific information about individual users.

没错,可以看到,每次 API 调用都是一次独立的交互。这意味着模型不会记住之前的输入提示或已给出的回复。每次调用 chatgpt_call() 函数,都会开启一段全新的对话。

在构建聊天机器人时,这无疑是个问题,因为如果模型无法记住我们已讨论的内容,就无法进行连续对话。因此,下一步是让模型感知我们之前的交互。

上下文感知

上下文感知使聊天机器人能够基于正在进行的对话理解并做出恰当回应。

通过构建上下文,后续交互可以基于以往内容进行个性化,保持对话连贯并避免重复。此外,具有上下文感知的聊天机器人还能让用户主动提出澄清性问题或给出建议以纠正误解。

总体而言,上下文感知增强了聊天机器人的自然性、有效性和用户体验。接下来,我们将把既往交互的集合称为对话历史或记忆(memory)。

用于 ChatGPT 的原始 OpenAI API 已提供了一种方式,可在每个新提示词中一并向模型发送我们的对话历史。此外,LangChain 框架也提供了更高效、成本更优的记忆管理方式。

理解 System、Assistant 与 User 消息

ChatGPT 的对话历史结构

目前,我们只需提供所需提示词即可直接使用 chatgpt_call() 方法。如果更仔细地查看实现,会发现输入提示词被格式化为 messages 列表的一部分。该列表实际上用于记录对话历史,因此必须采用非常特定的格式。

让我们仔细看看!

到目前为止,我们在 chatgpt_call() 中对 messages 列表的格式如下:

messages = [{"role": "user", "content": prompt}]

messages 列表以一组字典的形式存储对话历史。列表中的每个字典使用两个键值对表示对话中的一条消息。

  • 键 role 的值为 user,表示该消息由用户发送。
  • 键 content 的值为 prompt,包含实际的用户提示词。

这是一个仅含一次交互的简单示例。尽管 content 键始终存储用户提示或聊天机器人的回复,但 role 键则更为多样。

System、Assistant 与 User 角色

键 role 表示 content 的消息归属。在使用 ChatGPT 时,我们最多可以有三种角色:

  • User。如前所述,user 角色对应与聊天机器人交互的实际用户。
  • Assistant。assistant 角色对应聊天机器人本身。
  • System。system 角色用于向聊天机器人提供指令、指导和上下文。系统消息帮助引导用户交互、设定机器人行为、提供上下文信息,并处理对话中的特定交互。系统消息可以视为对聊天机器人的高层指令,对用户透明。

自制示意图。使用 chatGPT 构建聊天机器人时对话链中不同角色的交互。

自制示意图。使用 chatGPT 构建聊天机器人时对话链中不同角色的交互。

构建对话历史

我们可以在 messages 列表中使用 user、assistant 和 system 三种角色来构建对话历史。

根据我们的测试交互,使用上述三种角色的规范化对话历史可如下所示:

messages =  [ 
{'role':'system', 'content':'You are friendly chatbot.'},
{'role':'user', 'content':'Hi, my name is Andrea'},
{'role':'assistant', 'content': "Nice to meet you, Andrea! How can I assist you today?"},
{'role':'user', 'content':'Do you remember my name?'}  ]

我们可以稍微修改 call_chatgpt() 函数,使其将对话历史作为输入参数:

def call_chatgpt_with_memory(messages, model="gpt-3.5-turbo"):
   response = openai.ChatCompletion.create(
       model=model,
       messages=messages,
   )
   return response.choices[0].message["content"]

来试试这个新实现吧!

messages =  [ 
{'role':'system', 'content':'You are friendly chatbot.'},
{'role':'user', 'content':'Hi, my name is Andrea'},
{'role':'assistant', 'content': "Nice to meet you, Andrea! How can I assist you today?"},
{'role':'user', 'content':'Do you remember my name?'}  ]

response = call_chatgpt_with_memory(messages)
print(response)

# Output: Yes, your name is Andrea.

太好了!我们已经让模型能够感知过往交互,从而真正与之对话。不过,我们还需要找到一种自动更新 messages 列表的方法。

自动化对话历史

我们可以对 API 调用函数稍作扩展,即可实现消息的自动收集:

def chatgpt_conversation(prompt):
   context.append({'role':'user', 'content':f"{prompt}"})
   response = call_chatgpt_with_memory(context)
   context.append({'role':'assistant',
   'content':f"{response}"})
   return response

现在来试试!

chatgpt_conversation("Hello, my name is Andrea")

# Output: 'Nice to meet you, Andrea! How can I assist you today?'

再来一次跟进交互:

chatgpt_conversation("Do you remember my name?")

# Output: 'Yes, your name is Andrea.'

现在,对话历史会自动更新。最后需要注意的是,随时打印 messages 列表即可查看对话历史及其结构。

print(messages)

# Output:
[{'role': 'user', 'content': 'Hello, my name is Andrea'},
{'role': 'assistant', 'content': 'Nice to meet you, Andrea! How can I assist you today?'},
{'role': 'user', 'content': 'Do you remember my name?'},
{'role': 'assistant', 'content': 'Yes, your name is Andrea.'}]

LangChain 框架

到目前为止,我们可以认为当前实现是一个有用的聊天机器人,但可能还不是最优版本。

持续地传入所有以往的提示词和回复,会很快导致令牌数量激增,而 ChatGPT 需要先处理它们,才能返回最新回复。

目前我们使用 .append 操作将过往交互串联在一起,以便让聊天机器人具备某种记忆。然而,记忆管理可以通过 LangChain 这类专用包更高效地完成。

LangChain 是一个用于开发由语言模型驱动的应用的框架。鉴于聊天机器人是当前最流行的应用之一,LangChain 为构建聊天机器人的对话历史提供了更简洁的实现,以及更高效的替代处理方式。

关于 LangChain 框架本身的更多信息,强烈推荐阅读入门文章Introduction to LangChain for Data Engineering & Data Applications。

使用 LangChain 构建简单对话历史

本节我们将探索如何改用 LangChain 框架来构建聊天机器人。

langchain 库可替代上文所用的 openai 库,因为 langchain.llms 模块已经允许您用两行代码与 ChatGPT 交互:

# OpenAI key from environment
from langchain.llms import OpenAI
llm = OpenAI()

加载好 llm 模型后,让我们导入用于存储对话历史的记忆对象。为存储与聊天机器人的每次过往交互,LangChain 提供了 ConversationBufferMemory:

from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory()

现在,终于可以导入 ConversationChain 作为封装器,它将使用 llm 和 memory 把用户提示词传给 ChatGPT 并返回补全结果:

from langchain.chains import ConversationChain

conversation = ConversationChain(
   llm=llm,
   memory=memory
)

最后,要向 ChatGPT 提交用户提示词,我们可以使用如下的 .predict 方法:

conversation.predict(input="Hello, my name is Andrea")

# Output: "Hi Andrea, I'm an AI created to help you with whatever you may need. What can I do for you today?"

让我们设置 verbose=True,以查看模型在生成补全时参考的信息。在一次跟进交互中试试:

创建补全时 ChatGPT 可用上下文的截图。绿色为详细输出。

创建补全时 ChatGPT 可用上下文的截图。绿色为详细输出。

最后,让我们验证它能准确记住先前的交互:

创建补全时 ChatGPT 可用上下文的截图。绿色为详细输出。

创建补全时 ChatGPT 可用上下文的截图。绿色为详细输出。

填充对话历史

我们可以使用 memory.save_context() 方法向聊天机器人注入额外上下文。这样无需通过对话提供信息,也能让聊天机器人感知所需内容。在 LangChain 中,使用的关键字是 input 和 output。例如,

memory.save_context({"input": "Hi"},
                   {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"},
                   {"output": "Cool"})

此外,我们也可以随时运行 print(memory.buffer) 或 memory.load_memory_variables({}) 查询记忆内容。

LangChain 记忆类型

LangChain 为 ChatGPT 实现了不同类型的记忆。上文使用的 ConversationBufferMemory 会记录与 ChatGPT 的每一次交互。然而,我们已经讨论过,在每次新交互中传入整个对话历史会迅速增加需要处理的令牌数。

请记住,除了 ChatGPT 每次交互的令牌上限外,其使用成本也与令牌数量相关。在每次新交互中处理完整对话历史,长期来看可能会很昂贵。

为避免持续处理全部对话历史,LangChain 实现了其他优化的记忆管理方式。我们将在下一节探索其中一种更高级的类型。

使用 LangChain 的高级对话历史

一种避免存储每次过往交互的高级记忆管理方式,是只存储交互摘要。

这是我最喜欢的记忆类型,因为它在不牺牲任何过往交互信息的前提下,能够实现真正优化的记忆管理。

让我们开始吧!

# OpenAI key from environment
from langchain.llms import OpenAI
llm = OpenAI()

from langchain.memory import ConversationSummaryBufferMemory
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)

请注意,这种记忆类型会使用已定义的 llm 来生成以往交互的摘要。

此外,该记忆可以在一定最大令牌数(max_token_limit)内存储完整交互。超过该上限时,会将这些消息的信息纳入摘要。

为测试它,让我们创建一次令牌数量较大的交互。考虑以下 schedule:

schedule = "There is a meeting at 8am with your product team. \
You will need your powerpoint presentation prepared. \
9am-12pm have time to work on your LangChain \
project which will go quickly because Langchain is such a powerful tool. \
At Noon, lunch at the italian resturant with a customer who is driving \
from over an hour away to meet you to understand the latest in AI. \
Be sure to bring your laptop to show the latest LLM demo."

memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)
memory.save_context({"input": "Hello"}, {"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"}, {"output": "Cool"})
memory.save_context({"input": "What is on the schedule today?"}, {"output": f"{schedule}"})

现在,我们只需像之前的示例一样,基于 llm 和 memory 声明一个新的会话链。

conversation = ConversationChain(
   llm=llm,
   memory=memory,
   verbose=True
)

我们可以使用 .predict 来向 ChatGPT 发送提示:

创建补全时 ChatGPT 可用上下文的截图。绿色为详细输出。

创建补全时 ChatGPT 可用上下文的截图。绿色为详细输出。

可以看到,聊天机器人会保留原始对话历史的摘要,从而在每次后续交互中减少 ChatGPT 需要处理的令牌数量。

就是这样!现在我们可以和这个优化过的聊天机器人聊上好几个小时了!

将 verbose=False,即可在笔记本中获得更清爽的问答体验。

结语

本文演示了如何使用广为人知的 ChatGPT 模型构建一个具备上下文感知的聊天机器人。

我们看到,构建自有的上下文感知聊天机器人可带来持续的交互。这种迭代方式促成持续的反馈循环,使您能够在对话过程中始终把握用户需求与偏好。

我们介绍了两种管理聊天机器人对话历史的主要方法。其一,使用原生 ChatGPT API,构建可用的原生聊天机器人。其二,使用专用的 LangChain 框架与优化的记忆类型实现。

现在轮到您了!请用本文介绍的构建模块,定制一个符合您特定用例的聊天机器人!

如果您有兴趣同时使用 LangChain 和 ChatGPT 构建其他应用,欢迎观看网络研讨会 Building AI Applications with LangChain and GPT。想快速掌握 NLP?那么请查看关于Python 自然语言处理的模块!

Andrea Valenzuela

作者

Andrea Valenzuela

Andrea Valenzuela 目前在瑞士日内瓦的欧洲核子研究中心(CERN)粒子加速器的 CMS 实验工作。她在过去六年深耕数据工程与数据分析,主要职责包括数据分析与软件开发。她现正通过 Medium 出版物 ForCode'Sake 推动数据相关技术的学习大众化。

她拥有加泰罗尼亚理工大学工程物理学学士学位,以及庞培法布拉大学智能交互系统硕士学位。她的研究经历包括基于早期 OpenAI 图像生成算法(如 Normalizing Flows)的专业工作。

主题