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
管理对话历史管理,一步一步地实现了一个可以记忆历史对话,支持流式输出的聊天机器人。