02创建一个聊天机器人

创建一个聊天机器人 | 🦜️🔗 LangChain

https://python.langchain.com/v0.2/docs/tutorials/chatbot/

Overview

我们将介绍如何设计和实现一个由LLM驱动的聊天机器人的示例。这个聊天机器人将能够进行对话并记住先前的交互。

Concepts

以下是我们将要处理的一些高级组件:

  • Chat Models。大语言模型。
  • Prompt Templates,简化了组装提示的过程,组合了默认消息、用户输入、聊天历史和额外检索的上下文(可选的)。
  • Chat History,它允许聊天机器人“记住”过去的对话,并在回答最新的问题时参考它们。
  • 使用LangSmith对应用程序进行调试和跟踪。

我们将介绍如何将上述组件结合在一起,创建一个强大的对话式聊天机器人。

Setup

环境要求

  • Python版本需大于等于3.8.5,但小于3.12。
  • langchain:Version: 0.2.1
  • langchain-openai:Version: 0.1.7
  • Jupyter 笔记本

依赖包安装

pip install langchain

For more details, see our Installation guide.

LangSmith

您用LangChain构建的许多应用程序将包含多个步骤和多次LLM调用。随着这些应用程序变得越来越复杂,能够查看链条或代理内部发生了什么变得至关重要。最佳方法是使用LangSmith。

请在上面的LangSmith注册后,确保设置您的环境变量以开始记录跟踪。

import getpass
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()

Quickstart

首先,尝试单独使用语言模型。LangChain支持许多不同的语言模型,您可以随意选择要使用的语言模型!

  • OpenAI
pip install -qU langchain-openai
import getpass
import os
os.environ["OPENAI_API_KEY"] = getpass.getpass()
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-3.5-turbo")

API 调用:

from langchain_core.messages import HumanMessage
model.invoke([HumanMessage(content="Hi! I'm Bob")])
AIMessage(content='Hello Bob! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 12, 'total_tokens': 22}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'stop', 'logprobs': None}, id='run-be38de4a-ccef-4a48-bf82-4292510a8cbf-0')

模型本身并没有任何状态的概念。例如,如果你问一个后续问题:

model.invoke([HumanMessage(content="What's my name?")])
AIMessage(content="I'm sorry, as an AI assistant, I do not have the capability to know your name unless you provide it to me.", response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 12, 'total_tokens': 38}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_caf95bb1ae', 'finish_reason': 'stop', 'logprobs': None}, id='run-8d8a9d8b-dddb-48f1-b0ed-ce80ce5397d8-0')

让我们看一下LangSmith

我们可以看到它并没有将之前的对话内容纳入上下文,并且无法回答问题。

为了解决这个问题,我们需要将整个对话历史传递给模型:

from langchain_core.messages import AIMessage
model.invoke(
[
        HumanMessage(content="Hi! I'm Bob"),
        AIMessage(content="Hello Bob! How can I assist you today?"),
        HumanMessage(content="What's my name?"),
]
)

API 调用:

AIMessage(content='Your name is Bob.', response_metadata={'token_usage': {'completion_tokens': 5, 'prompt_tokens': 35, 'total_tokens': 40}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'stop', 'logprobs': None}, id='run-5692718a-5d29-4f84-bad1-a9819a6118f1-0')

现在得到了一个很好的回应!

但是我们不能每次都把历史对话手动放进去。那么我们如何最好地实现它呢?

历史消息

我们可以使用一个Message History类来包装我们的模型,使其具有状态。这样可以跟踪模型的输入和输出,并把它们存储。未来的交互将加载这些消息,并将它们作为输入的一部分传递给模型。让我们看看如何使用这个功能!

首先,让我们确保安装langchain-community包,因为我们将使用它来存储消息历史。

# ! pip install langchain_community

这里的关键部分是定义的get_session_history函数。 这个函数应该接受一个 session_id,并返回一个消息记录对象。session_id 用于区分不同的对话,并在调用模型时作为 config 的一部分传入。

然后,设置包装模型并添加这条消息历史。

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
	if session_id not in store:
	        store[session_id] = ChatMessageHistory()
	return store[session_id]
	
with_message_history = RunnableWithMessageHistory(model, get_session_history)

API 调用:

创建一个 config,每次都要将其传递给可运行的对象。这个配置应该包含的信息包括 session_id

config = {"configurable": {"session_id": "abc2"}}
response = with_message_history.invoke(
[HumanMessage(content="Hi! I'm Bob")],
    config=config,
)
response.content
'Hello Bob! How can I assist you today?'
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
    config=config,
)
response.content
'Your name is Bob.'

我们的聊天机器人拥有了记住。如果更改配置使用不同的session_id,我们会发现它会重新开始对话,也就是说这里的上下文记忆是根据session_id 进行隔离的。

config = {"configurable": {"session_id": "abc3"}}
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
    config=config,
)
response.content
"I'm sorry, I do not have the ability to know your name unless you tell me."

然而,因为我们将历史对话保存在数据库中,我们可以随时回到原始的对话。

config = {"configurable": {"session_id": "abc2"}}
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
    config=config,
)
response.content
'Your name is Bob.'

这样我们的聊天机器人可以与许多用户进行多轮对话了!

现在,我们所做的只是在模型交互前后添加了一个简单的持久化层。我们可以通过添加提示模板来使其变得更复杂和个性化。

提示词模板

这里提示模板有助于将输入信息转化为LLM可以处理的格式。在这种情况下,原始用户输入只是一条消息,我们将其传递给LLM。如果我们希望添加一条带有一些自定义说明的系统消息,应该怎么做?

首先,让我们添加一个系统消息。为此,我们将创建一个 ChatPromptTemplate。我们将利用 MessagesPlaceholder来传递所有的消息。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful assistant. Answer all questions to the best of your ability.",
),
        MessagesPlaceholder(variable_name="messages"),
]
)
chain = prompt | model

API 调用:

请注意,这里略微改变了输入类型 - 我们现在不再通过消息列表进行传递,而是通过一个包含消息列表的 messages 字典进行传递。

response = chain.invoke({"messages": [HumanMessage(content="hi! I'm bob")]})
response.content
'Hello, Bob! How can I assist you today?'

现在我们可以将这个 chain 放在之前的消息历史对象中

with_message_history = RunnableWithMessageHistory(chain, get_session_history)
config = {"configurable": {"session_id": "abc5"}}
response = with_message_history.invoke(
[HumanMessage(content="Hi! I'm Jim")],
    config=config,
)
response.content
'Hello, Jim! How can I assist you today?'
response = with_message_history.invoke(
[HumanMessage(content="What's my name?")],
    config=config,
)
response.content
'Your name is Jim. How can I assist you further, Jim?'

现在只能使用英文对话,如果我们需要大模型返回指定语言中文,我们可以把提示模板稍微复杂化一点。:

prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful assistant. Answer all questions to the best of your ability in {language}.",
),
        MessagesPlaceholder(variable_name="messages"),
]
)
chain = prompt | model

我们需要在模板中加入一个新的输入 language 。现在就可以传入选择到语言去执行链。

response = chain.invoke(
{"messages": [HumanMessage(content="hi! I'm bob")], "language": "Spanish"}
)
response.content
'¡Hola Bob! ¿En qué puedo ayudarte hoy?'

现在将这个上面定义的链封装在一个消息历史记录类中。这一次,因为输入中需要传入多个值,我们需要指定 input_messages_key 的名称。

with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)
config = {"configurable": {"session_id": "abc11"}}
response = with_message_history.invoke(
{"messages": [HumanMessage(content="hi! I'm todd")], "language": "Spanish"},
    config=config,
)
response.content
'¡Hola Todd! ¿En qué puedo ayudarte hoy?'
response = with_message_history.invoke(
{"messages": [HumanMessage(content="whats my name?")], "language": "Spanish"},
    config=config,
)
response.content
'Tu nombre es Todd. ¿Hay algo más en lo que pueda ayudarte?'

this LangSmith trace 可以帮助我们理解执行的过程。

管理对话历史

现在有一个问题如果我们对历史对话不加以管理,消息列表将不断增长,可能会超出LLM的上下文窗口。因此,我们需要管理对话历史,限制传递的消息大小。

我们可以在加载历史消息之后,但在填充提示模板之前,加入这个限制。

我们可以通过在提示词的前面添加一个简单的限制,然后将这个新的链包装在消息历史类中。首先,让定义一个函数,让它选择最近的 k 条消息。然后在链的开头添加这个函数来创建一个新的链。

from langchain_core.runnables import RunnablePassthrough
def filter_messages(messages, k=10):
	return messages[-k:]
chain = (
    RunnablePassthrough.assign(messages=lambda x: filter_messages(x["messages"]))
| prompt
| model
)

API 调用:

创建一个超过10条消息的列表,看看它是否不再记得前面消息中的信息。

messages = [
    HumanMessage(content="hi! I'm bob"),
    AIMessage(content="hi!"),
    HumanMessage(content="I like vanilla ice cream"),
    AIMessage(content="nice"),
    HumanMessage(content="whats 2 + 2"),
    AIMessage(content="4"),
    HumanMessage(content="thanks"),
    AIMessage(content="no problem!"),
    HumanMessage(content="having fun?"),
    AIMessage(content="yes!"),
]
response = chain.invoke(
{
"messages": messages + [HumanMessage(content="what's my name?")],
"language": "English",
}
)
response.content
"I'm sorry, I don’t have access to your name. Can I help you with anything else?"

但如果我们询问最近十条信息中的内容,它依然记得。

response = chain.invoke(
{
"messages": messages + [HumanMessage(content="what's my fav ice cream")],
"language": "English",
}
)
response.content
'You mentioned that you like vanilla ice cream.'

我们现在把这句话包含在消息历史中。

with_message_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="messages",
)
config = {"configurable": {"session_id": "abc20"}}
response = with_message_history.invoke(
{
"messages": messages + [HumanMessage(content="whats my name?")],
"language": "English",
},
    config=config,
)
response.content
"I'm sorry, I don't know your name."

聊天记录中现在有两条新消息。这意味着以前可以在我们的对话历史中访问的信息现在不再可用了!

response = with_message_history.invoke(
{
"messages": [HumanMessage(content="whats my favorite ice cream?")],
"language": "English",
},
    config=config,
)
response.content
"I'm sorry, I don't know your favorite ice cream flavor."

我们使用LangSmith,看看到发生了什么。LangSmith trace

流式输出

现在我们有了一个聊天机器人功能。然而,对于聊天机器人应用程序来说,一个非常重要的用户体验考虑因素是流式传输。LLMs有时需要一段时间才能做出回应,因此为了改进用户体验,大多数应用程序会将生成的每个令牌都流式传输回去。这可以让用户试试看到。

所有的链都提供了一个 .stream 方法。我们可以简单地使用该方法来获取一个流式响应。

config = {"configurable": {"session_id": "abc15"}}
for r in with_message_history.stream(
{
"messages": [HumanMessage(content="hi! I'm todd. tell me a joke")],
"language": "English",
},
    config=config,
):
print(r.content, end="|")
|Sure|,| Todd|!| Here|'s| a| joke| for| you|:
|Why| don|'t| scientists| trust| atoms|?
|Because| they| make| up| everything|!||

总结

我们了解了如何在LangChain中创建聊天机器人的基础知识。

调用Chat Models大语言模型,使用Prompt Templates提示词模板和Chat History管理对话历史管理,一步一步地实现了一个可以记忆历史对话,支持流式输出的聊天机器人。

posted @ 2024-11-12 14:59  onecyl  阅读(14)  评论(0编辑  收藏  举报