有两个相关的概念你需要了解:
聊天RAG:聊天RAG就是在聊天机器人里添加其它来源的数据,以增强聊天机器人的聊天能力。agent代理:代理是指聊天机器人能执行一些操作,这类操作是由LLM在与用户的对话中,获悉用户意图后,调用非LLM本身的能力——外部API。本课程将对这两个概念进行讲解,但深入的理解,你可以到这两个概念的章节里直接阅读。
概念以下是本应用需要用到的高级组件:

我们将对上述组件内容进行应用,创建一个强大的聊天机器人。
设置Jupyter Notebook本教程(及大部分教程)都是使用的Jupyter notebooks,并预先认为您也会使用。Jupyter notebooks非常适用来学习LLM系统或作为一个原型构建工具,因为我们在学习或开发应用过程中将会碰到很多异常情况(比如,不正确的输出,API挂掉了),使用Jupyter notebooks这种一步一步的、交互式的工具,可以让你迅速调试并学习。
Jupyter Notebook的安装和配置问题,请自行了解。
LangSmith不再对LangSmith的安装和使用进行说明,前面有提到过。
快速开始首先,我们需要学会如何导入语言模型。LangChain支持多种语言模型——你可以随意切换使用——并最终选择一个你要使用的。
pip install -qU langchain-openai
import getpassimport osos.environ["OPENAI_API_KEY"] = getpass.getpass()from langchain_openai import ChatOpenAImodel = ChatOpenAI(model="gpt-3.5-turbo")
让我们直接调用一下模型。之所以可以调用,是因为ChatModels聊天模型是langchain中支持“可运行”协议的一个实例,支持“可运行”协议的实例或组件之间可以使用一套标准的接口规范进行交互。.invoke方法(调用)就是该标准接口的其中一个。在这里,我们简单的提交一个消息列表给到模型。
以下,发起了一次聊天,用户输入是"Hi! I'm Bob"。HI,我叫Bob。
from langchain_core.messages import HumanMessagemodel.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')
因为模型本身并没有记忆功能,你再向模型提问:"What's my name?"我叫什么名字?
model.invoke([HumanMessage(content="What's my name?")])
返回的结果是:"I'm sorry, as an AI assistant, I do not have the capability to know your name unless you provide it to me."。对不起,作为一个AI助手,我还没有能力知道你的名字,除非你将它提供给我。
很显然,名字在上一轮对话中已经说过了,但你的名字并未成为模型的训练样本,所以它不知道;或者说模型并未记住上次的会话。
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 AIMessagemodel.invoke( [ HumanMessage(content="Hi! I'm Bob"), AIMessage(content="Hello Bob! How can I assist you today?"), HumanMessage(content="What's my name?"), ])
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消息历史“类”来封装模型,使之其对之前的会话内容有感知。该类的作用是:对输入和输出内容的跟踪,并将其存入到数据库里。并在之后的会话中,把历史消息作为input输入传递到chain工作流中。让我们来看一下是如何使用的。
首先,我们需要启用langchain-community。
# ! pip install langchain_community
接着,我们可以引入本课程中需要用到的“类”来构建工作流。在该工作流里,我们将向语言模型投喂消息历史。这里的一个关键点时,我们要定义一个get_session_history方法,该方法的参数是session_id会话id,如果我们想要获取某个会话id的消息历史,就需要传递会话id。会话id就是用于区分不同会话的,该参数可以在全局配置参数里进行配置,并在工作流调用时,将该参数传入到该工作流中使用。
from langchain_community.chat_message_histories importChatMessageHistoryfrom langchain_core.chat_history import BaseChatMessageHistoryfrom langchain_core.runnables.history importRunnableWithMessageHistorystore = {}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)
现在,我们来创建一个全局变量,用于传递会话id。在本课程里,我们在全局变量里配置了一个key: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?'
然后再次询问大模型"What's my name?"我叫什么名字?
response = with_message_history.invoke( [HumanMessage(content="What's my name?")], config=config,)response.content
此时,能正确答复了。
'Your name is Bob.'
非常棒!
聊天机器人能知道我叫什么名字了。如果我们更改了一下会话id,从abc2更改为 abc3,我们发现再次询问时,模型又不知道了。
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的.from_messages方法,在该方法的参数里,使用硬编码的方式录入系统消息。然后使用MessagesPlaceholder方法定义一个输入参数messages。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholderprompt = 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
注意最后的chain,构建了一条包含两个步骤的工作流。
注意现在输入类型已经变为字典格式了,Key是message,Key的值仍然是消息列表。
response = chain.invoke({"messages": [HumanMessage(content="hi! I'm bob")]})response.content
返回的结果如下
'Hello, Bob! How can I assist you today?'
现在,让我们把这里定义的chain工作流和获取同一seeion的历史聊天记录get_session_history封装在一起,并在全局变量里,将session_id会话id变为abc5.
with_message_history = RunnableWithMessageHistory(chain, get_session_history)
config = {"configurable": {"session_id": "abc5"}}
此时,我们再调用这个新的工作流。这次的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?'
非常棒,现在我们在提示词模板的系统消息里,增加了一个变量language。
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
这样,我们在调用chain工作流时,需要为language赋值。这里,我们让模型用西班牙语回复。
response = chain.invoke( {"messages": [HumanMessage(content="hi! I'm bob")], "language": "Spanish"})response.content
返回结果是以西班牙语输出的。
'¡Hola Bob! ¿En qué puedo ayudarte hoy?'
现在,我们重新封装一下with_message_history。之所以这样做,是因为现在我们需要输入两个变量,而ChatMessageHistory()历史聊天记录里记录的内容不需要包含language(因为我们制作的应用是将英语翻译为某一种语言,并未规定用户可选择目标语言),只要记录会话内容即可。
with_message_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="messages",)
修改会话id
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?'
管理会话记录
在构建聊天机器人,一个重要的功能是管理会话历史记录。因为我们会把历史聊天记录连同当前用户提问一同提交给模型,如果不进行管理,历史消息的体量会逐渐增长,终将超过LLM允许的最大文本数量。因此,我们需要限制历史会话记录的条数,不能无限制增长。
重点提示,你需要在创建提示词模板前,或你发出第一条消息后就对历史会话记录的条数进行限制。
为此,我们需要在提示词构建前,增加一个步骤,用于控制message 的大小,然后更新一下工作流里的message为过滤后的message。这里的过滤方法是filter_messages(),该方法将返回最近 k 条key值为message的历史记录。
from langchain_core.runnables import RunnablePassthroughdef filter_messages(messages, k=10): return messages[-k:]chain = ( RunnablePassthrough.assign(messages=lambda x: filter_messages(x["messages"])) | prompt | model)
测试一下!
我们先创建了 10条历史会话记录,然后我们再向message里增加新的message,使之超过 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!"),]
第一条消息是,"hi! I'm bob"。此时再新增一条,第 11条消息是:"what's my name?"你叫什么名字。
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?"
如果我们询问模型 10条历史记录内的消息,它还是记得的。
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.'
测试成功,让我们重新更新一下历史消息,更改session_id为abc20.
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."
现在,chat history里新增了两条消息,第 1条是"whats my name?",第 2条是"I'm sorry, I don't know your name."。这就意味着,即使我们的聊天记录里可以记录更多的内容,提交给到模型的message也只有 10条。
然后我们再次调用,新增一条消息记录"whats my favorite ice cream?"。原有的消息记录: HumanMessage(content="I like vanilla ice cream"),将会在提交给模型时被过滤掉。
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."
流式处理
现在我们制作的聊天机器人还不够真实,LLMs有时候需要花点时间才能完全的响应,要想真实,就需要增加“流式交互”,让用户自己看到模型输出的进度。
在LangChain里,你将非常容易实现流式输出。
因为,所有的工作流都支持.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|!||