
提升对话质量:5个步骤让你的聊天机器人更具记忆力
学习如何构建一个能记住你对话的AI。
Source: DALLE.
聊天机器人如何记忆?
你有没有注意到聊天机器人可以多么像人类一样响应?
最受欢迎的 聊天机器人 允许你在特定主题上问它一个问题,然后跟进更多深入探讨 上下文 的额外问题。在这种典型的人类风格的 对话 中,你可能 从未重复原始主题,但聊天机器人仍然记得你在谈论什么。
聊天机器人是如何记住这些的?
在本教程中,我们将通过一个包含 对话记忆 的聊天机器人构建示例进行讲解。你将看到这有多强大,并准确了解它是如何工作的!
像搜索引擎,但完全不同
搜索引擎 也允许用户提问,从中返回与主题最匹配的结果。
然而,如果你曾经尝试向搜索引擎提出与第一次相关的第二个问题(而不重复相同的主题),搜索引擎将对该主题没有记忆,只会返回与你提供的 确切短语 匹配的任何结果。
增强了对话记忆的聊天机器人能够始终记住对话。这使它们与搜索引擎明显不同。与更基础的聊天机器人相比,增加 记忆 使一切都不同。
对话的设计
增强对话记忆的聊天机器人设计包含一个检索增强生成 (RAG) 聊天机器人的典型部分。
- 通用客户端接口
- 存储预处理知识的数据库
- 用于将查询与知识库内容匹配的相似性算法
- 用于提供响应的大型语言模型 (LLM)
该设计包括检索过程和生成过程。然而,记忆的关键增强是通过对话中的先前查询和响应的缓存提供的。
来源:报告中的作者。
上面的图表展示了聊天机器人如何利用私人文档的知识库来生成响应。接收到查询后,与知识库进行匹配。生成的文档与查询一起用作上下文,以格式化为LLM提供响应的提示。
记住主题
没有记忆的标准大型语言模型驱动的聊天机器人和对话型聊天机器人之间的关键区别在于,我们还将迄今为止的整个对话输入到提示中。
考虑以下图表,详细说明了如何为将对话历史发送到大型语言模型进行提示工程。
来源:报告中的作者。
在上面的图表中,请注意用户的 查询、文档上下文 和 检索到的对话 是如何附加到发送给大型语言模型以获取新响应的 提示 中的。
在收到响应后,我们将结果存储回 缓存 中。随着对话的继续,缓存进一步增长。对大型语言模型的每次后续调用都包含下一个提示中的整个对话历史。
用记忆驱动聊天机器人
一个经典的聊天机器人可以回答来自摄取文档的单次问题。然而,它无法记住原始主题。
如果用户提出后续问题,聊天机器人将不会保持历史记录,并会在文档中进行完全新搜索以尝试回答问题。如果查询中缺少特定的关键词,聊天机器人可能不会给出合适的回应。
增强版的聊天机器人通过使用内存列表来维护对话。这可以简单地是一个数组或分布式内存,例如Redis。
在每轮对话中,提示的大小会随着我们不断将最后的对话存储在缓存中而增长。大型语言模型能够在提供响应时使用当前查询的上下文和截至目前的整个对话。
将问题与知识匹配
当用户第一次提出问题时,查询会与知识库进行匹配,以找到最佳匹配的上下文。
这是创建基于私有文档的聊天机器人时RAG过程的一部分。如下面的代码所示,系统找到最佳匹配的文档作为上下文,并将其附加到LLM的提示中。
def chat(user_query, is_debug = False):
original_best_matches, processed_best_matches = find_best_matches(user_query)
context = "\\n\\n".join(original_best_matches)
response = get_cohere_response(user_query, context)
return response, context
来自LLM的响应和本轮对话的上下文都返回给客户端。
添加对话历史
到目前为止,我们已经看到一个典型的聊天机器人实现,它在将问题匹配到知识上下文后,通过提示调用大型语言模型。然而,真正的增强来自于存储对话。
conversation_history = []
def add_to_conversation(user_input, search_result_context, llm_response):
conversation_entry = {
"user_input": user_input,
"search_result_context": search_result_context,
"llm_response": llm_response
}
conversation_history.append(conversation_entry)
在上面的代码中,我们将用户的查询、搜索结果和大型语言模型的响应保存到内存数组中。我们可以稍后检索这些数据,以便在后续的提示中包含到大型语言模型中。
更新LLM提示与记忆
现在对话被保存到记忆中,我们可以检索所有先前的查询和响应,以便在下一个响应中包含它们。
def conversation(user_query, is_debug = False):
history = "\\n\\n=========================\\n\\n".join(
[f"User: {entry['user_input']}\\nSearch Result Context: {entry['search_result_context']}\\nLLM Response: {entry['llm_response']}" for entry in conversation_history]
)
query = f"This is the entire conversation up to this point. Use this as additional context when answering the question from the user.\\n CONTEXT: {history}\\n\\n"
query += f"\\n\\n=========================\\n\\n USER QUERY: {user_query}"
response, context = process_chat(query, is_debug)
add_to_conversation(user_query, context, response)
return response
注意我们如何构建一个包含对话历史的新提示。
一点提示工程的作用
一个独特的 分隔字符串 被添加到 提示 中,以提供对话部分与当前用户查询之间的边界。
一系列条形字符 “=====” 被用来向 LLM 表明这是对话历史。我们接着给 LLM 一个指令,要求在提供响应时使用这些数据。
“这是到目前为止的整个对话。在回答用户的问题时,将此作为额外的 上下文。上下文: { }, 用户查询: { }”
在将历史附加到提示后,我们包含当前用户查询,并请求 LLM 给出新的响应。
结果就像 魔法 一样!
print(conversation('Who are the first 5 authors from the report?'))
print(conversation('Which one is the first?'))
print(conversation('Which one is fifth?'))
报告中的前五位作者是:
- Govindasamy Bala
- Deliang Chen
- Tamsin Edwards
- Sandro Fuzzi
- Thian Yew Gan
Govindasamy Bala 是报告的第一作者。
第五位作者是 Thian Yew Gan。
注意 LLM 如何利用 先前对话 作为 上下文 回答关于 “报告中的作者” 的后续问题。
我们可以通过直接询问 LLM 对话的主题来验证对话记忆是否正常工作。
print(conversation('What is the topic of this conversation?'))
这个对话的主题是报告的前五位作者。
将对话限制为30分钟
由于我们将对话存储在缓存中,它可能很容易超出LLM输入令牌的大小限制(例如,128KB等)。
因此,我们可以将对话存储限制为特定的时间段。在30分钟后启用滚动缓存刷新,可以在较长时间后清除缓存。用户可以开始一个新的对话主题。
from cachetools import TTLCache
from datetime import datetime
conversation_ttl_cache = TTLCache(maxsize=1000, ttl=1800)
def add_to_conversation_cache(user_input, search_result_context, llm_response):
current_time = datetime.now().isoformat()
conversation_entry = {
"user_input": user_input,
"search_result_context": search_result_context,
"llm_response": llm_response,
"timestamp": current_time
}
conversation_ttl_cache[current_time] = conversation_entry
上述示例提供了一种修改后的对话保存方法。我们使用生存时间缓存,而不是使用基本数组,可以设置为30分钟的过期。保存到列表中的条目将在一段时间后过期。
然后,我们可以检索缓存中所有未被删除的条目,并将它们包含在LLM的上下文中。请记住,这可以通过将键更改为用户ID或会话ID来扩展到多个用户。
扩展到分布式缓存
到目前为止,我们已经使用数组和时间敏感缓存实现了缓存。然而,我们可以通过使用 Redis 将聊天机器人扩展到 分布式缓存。
下面是将对话存储在 Redis 中的示例,而不是内存数组。
import json
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def add_to_conversation_redis(user_id, user_input, search_result_context, llm_response):
conversation_entry = {
"user_input": user_input,
"search_result_context": search_result_context,
"llm_response": llm_response,
"timestamp": current_time
}
conversation_entry_str = json.dumps(conversation_entry)
key = user_id
r.setex(key, 1800, conversation_entry_str)
Redis 存储 字符串,而不是 对象。这是因为需要网络调用将数据发送到服务。因此,我们在按照指定键存储到 Redis 之前,将对话 JSON对象 转换为字符串。
来源:DALLE.
一个更强大的聊天机器人
正如我们所看到的,将对话记忆构建到大型语言模型驱动的聊天机器人中是一种增强其能力的强大方式。
我们已经演示了如何通过利用一个简单的内存数组来构建具有记忆的聊天机器人是多么简单。然而,您可以进一步提升这些增强功能。以下是一些入门的想法。
时间敏感缓存
与其使用简单的数组来存储对话,不如使用时间敏感缓存数据结构,该结构在特定时间段后过期条目。
分布式缓存
利用 分布式记忆 使用 Redis 或其他基于服务的缓存产品。这些缓存可以在网络服务器上或通过互联网运行,使您的聊天机器人能够运行多个主机实例并连接到一个公共缓存以实现 持久 记忆。
将缓存持久化到磁盘
对话不必在短时间内被删除。实际上,它可以通过持久化到 磁盘 或 数据库 来无限期存储。对话可以在几天、几周甚至几年后被检索和重新使用。
用户或会话ID
对话可以使用 用户 或 会话ID 作为 缓存键 存储,其中整个对话作为该键下提供的列表。通过这种方式,整个用户的对话可以使用 滚动过期,而不是逐个回合地过期,数据仅在一段时间不活动后才会过期。
本教程的代码可以在 这里 下载。
关于作者
此故事发布在 Generative AI 上。