• LangChain-v0.2 构建聊天机器人


    本文将介绍如何设计和实现 LLM 驱动的聊天机器人的示例。该聊天机器人能够进行对话和记住历史聊天记录。

    阅读本文后,你将大致了解以下内容:

    1、Chat Models聊天模型;

    2、使用 LangSmith 调试和跟踪应用程序;

    3、Prompt Templates 提示模板;

    4、Chat History 聊天记录;

    5、Streaming 流;

    请注意,我们构建的这个聊天机器人将仅使用语言模型进行对话。涉及一下相关概念:

    1、Conversational RAG: 通过外部数据源启用聊天机器人体验;

    2、Agents: 构建可以执行操作的聊天机器人;

    一、Installation安装

    LangChain安装:

    pip install langchain

    二、LangSmith

    您使用 LangChain 构建的许多应用程序将包含多个步骤,其中包含多次调用 LLM 调用。
    随着这些应用程序变得越来越复杂,通过LangSmith能够检查您的链或代理内部到底发生了什么。

    1)注册LangSmith

    访问地址:https://smith.langchain.com/

    2)点击设置

    创建API KEY

    保存好生成的KEY,接下来在项目中需要配置这个KEY ;

    3)创建Projects

    4)在代码中配置

    在代码中设置LangSmith KEY环境变量以开始记录跟踪:

    1. import os
    2. os.environ["LANGCHAIN_TRACING_V2"]="true"
    3. os.environ["LANGCHAIN_API_KEY"]="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"

     5)在Projects中查看 链或代理内部调用

    三、使用语言模型

    LangChain支持许多不同的语言模型,包含:OpenAI、Anthropic、Azure、Google、Cohere、FireworksAI、Groq、MistralAI、TogetherAI等,您可以互换使用 ,选择您要使用的语言模型!

    下面内容将居于OpenAI语言模型进行演示:

    1)LangChain OpenAI安装:
    pip install -qU langchain-openai

    让我们首先直接使用模型。是LangChain的实例,它们公开了一个标准接口来与它们进行交互。只需简单地调用模型,我们就可以将消息列表传递给该方法。 

    1. from langchain_openai import ChatOpenAI
    2. model = ChatOpenAI(model="gpt-3.5-turbo")
    2)HumanMessage 
    1. from langchain_core.messages import HumanMessage
    2. 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-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-92ca123c-4557-4403-8b17-6c4d74d5e1ff-0', usage_metadata={'input_tokens': 12, 'output_tokens': 10, 'total_tokens': 22})

    该模型本身没有任何状态概念。例如,如果您提出后续问题:

    model.invoke([HumanMessage(content="What's my name?")])

    输出聊天回复:

    AIMessage(content="I'm sorry, I do not have access to personal information such as your name.", response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 12, 'total_tokens': 29}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-7a6e352f-7db2-4173-a809-565a976a8768-0', usage_metadata={'input_tokens': 12, 'output_tokens': 17, 'total_tokens': 29})

    我们可以看到,它并没有将之前的对话变成上下文,也无法回答上文说过的内容。这是非常差的聊天机器人体验!

    为了解决这个问题,我们需要将整个对话历史记录传递到模型中。让我们看看这样做会带来什么样效果:

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

    输出聊天回复:

    AIMessage(content='Your name is Bob. Is there anything else I can help you with, Bob?', response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 35, 'total_tokens': 52}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-ced35134-5f6a-41bf-a250-8b40545bf3c2-0', usage_metadata={'input_tokens': 35, 'output_tokens': 17, 'total_tokens': 52})

    现在我们可以看到我们得到了很好的回应!

    上下文(历史记录)是支撑聊天机器人对话能力的基本思想。那么,我们如何最好地实现这一点呢?

    四、Message History 消息历史记录

    我们可以使用 Message History 类来包装我们的模型并使其有状态。这将跟踪模型的输入和输出,并将它们存储在某个数据存储中。然后,未来的交互将加载这些消息,并将它们作为输入的一部分传递到链中。

    1)langchain_community安装:

    pip install langchain_community

    2)API使用

    我们可以导入相关类并设置我们的链,该链包装模型并添加此消息历史记录。这里的一个关键部分是我们传入的函数。此函数应接受并返回 Message History 对象。这用于区分单独的会话,并且在调用新链时应作为配置的一部分传入(我们将展示如何执行此操作)。

    1. from langchain_core.chat_history import (
    2. BaseChatMessageHistory,
    3. InMemoryChatMessageHistory,
    4. )
    5. from langchain_core.runnables.history import RunnableWithMessageHistory
    6. store = {}
    7. def get_session_history(session_id: str) -> BaseChatMessageHistory:
    8. if session_id not in store:
    9. store[session_id] = InMemoryChatMessageHistory()
    10. return store[session_id]
    11. with_message_history = RunnableWithMessageHistory(model, get_session_history)

    3)配置数据

    配置历史消息缓存信息,让同一个回话具备历史聊天记录,同时不同回话的历史聊天记录数据是隔离的。

    1. config = {"configurable": {"session_id": "abc2"}}
    2. response = with_message_history.invoke(
    3. [HumanMessage(content="Hi! I'm Bob")],
    4. config=config,
    5. )
    6. response.content
    1. response = with_message_history.invoke(
    2. [HumanMessage(content="What's my name?")],
    3. config=config,
    4. )
    5. response.content

    聊天机器人现在已经可以记住了关于我们的历史记录。如果我们更改配置以引用不同的ID ,我们可以看到它重新开始对话。 

    1. config = {"configurable": {"session_id": "abc3"}}
    2. response = with_message_history.invoke(
    3. [HumanMessage(content="What's my name?")],
    4. config=config,
    5. )
    6. response.content

    同时,我们也可以回到原始对话(因为我们将其保存在数据库中) 

    1. config = {"configurable": {"session_id": "abc2"}}
    2. response = with_message_history.invoke(
    3. [HumanMessage(content="What's my name?")],
    4. config=config,
    5. )
    6. response.content

    这样我们就实现了如何支持聊天机器人与许多用户进行对话的方式! 

    现在,我们所做的只是在模型周围添加一个简单的持久层。我们可以通过添加提示模板开始使内容更加复杂和个性化。

    五、Prompt templates 提示模板

    提示模板有助于将原始用户信息转换为 LLM 可以使用的格式。在这种情况下,原始用户输入只是一条消息,我们将它传递给 LLM。现在让我们让它更复杂一点。首先,让我们添加一个带有一些自定义指令的系统消息(但仍然将消息作为输入)。接下来,除了消息之外,我们还将添加更多输入。

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

    1. from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
    2. prompt = ChatPromptTemplate.from_messages(
    3. [
    4. (
    5. "system",
    6. "You are a helpful assistant. Answer all questions to the best of your ability.",
    7. ),
    8. MessagesPlaceholder(variable_name="messages"),
    9. ]
    10. )
    11. chain = prompt | model

    请注意,这略微改变了输入类型 - 我们现在不是传入消息列表,而是传入一个带有键的字典,其中包含消息列表。

    1. response = chain.invoke({"messages": [HumanMessage(content="hi! I'm bob")]})
    2. response.content

    2)现在,我们可以将其包装在与以前相同的 Messages History 对象中

    1. with_message_history = RunnableWithMessageHistory(chain, get_session_history)
    2. config = {"configurable": {"session_id": "abc5"}}
    3. response = with_message_history.invoke(
    4. [HumanMessage(content="Hi! I'm Jim")],
    5. config=config,
    6. )
    7. response.content
    1. response = with_message_history.invoke(
    2. [HumanMessage(content="What's my name?")],
    3. config=config,
    4. )
    5. response.content

    3)修改提示稍微复杂一点,让我们的提示模板看起来像这样: 

    1. prompt = ChatPromptTemplate.from_messages(
    2. [
    3. (
    4. "system",
    5. "You are a helpful assistant. Answer all questions to the best of your ability in {language}.",
    6. ),
    7. MessagesPlaceholder(variable_name="messages"),
    8. ]
    9. )
    10. chain = prompt | model

    请注意,我们已向提示添加了新输入。现在,我们可以调用链并传递我们选择的语言。

    1. response = chain.invoke(
    2. {"messages": [HumanMessage(content="hi! I'm bob")], "language": "chinese"}
    3. )
    4. response.content

    4)现在,让我们将这个更复杂的链包装在 Message History 类中。这一次,由于输入中有多个键,我们需要指定用于保存聊天记录的正确键。

    1. with_message_history = RunnableWithMessageHistory(
    2. chain,
    3. get_session_history,
    4. input_messages_key="messages",
    5. )
    6. config = {"configurable": {"session_id": "abc11"}}
    7. response = with_message_history.invoke(
    8. {"messages": [HumanMessage(content="hi! I'm todd")], "language": "chinese"},
    9. config=config,
    10. )
    11. response.content
    1. response = with_message_history.invoke(
    2. {"messages": [HumanMessage(content="whats my name?")], "language": "chinese"},
    3. config=config,
    4. )
    5. response.content

    六、Managing Conversation History 管理对话历史记录

    构建聊天机器人时要了解的一个重要概念 -- 如何管理对话历史记录。如果不进行管理,消息列表将无限制增长,并可能溢出 LLM 的上下文窗口。因此,添加一个步骤来限制您传入的消息的大小非常重要。

    为此,我们可以在提示符前面添加一个简单的步骤来适当地修改记录(token),然后将该新链包装在 Message History 类中。

    1)LangChain带有一些内置的帮助程序,用于管理消息列表。在本例中,我们将使用 trim_messages 帮助程序来减少发送到模型的消息数。trimmer 允许我们指定要保留的token数量,以及其他参数,例如我们是否要始终保留系统消息以及是否允许部分消息:

    1. from langchain_core.messages import SystemMessage, trim_messages
    2. trimmer = trim_messages(
    3. max_tokens=65,
    4. strategy="last",
    5. token_counter=model,
    6. include_system=True,
    7. allow_partial=False,
    8. start_on="human",
    9. )
    10. messages = [
    11. SystemMessage(content="you're a good assistant"),
    12. HumanMessage(content="hi! I'm bob"),
    13. AIMessage(content="hi!"),
    14. HumanMessage(content="I like vanilla ice cream"),
    15. AIMessage(content="nice"),
    16. HumanMessage(content="whats 2 + 2"),
    17. AIMessage(content="4"),
    18. HumanMessage(content="thanks"),
    19. AIMessage(content="no problem!"),
    20. HumanMessage(content="having fun?"),
    21. AIMessage(content="yes!"),
    22. ]
    23. trimmer.invoke(messages)

    max_tokens=65, include_system=True, 明显输出的结果已经按配置要求,删除超过65数量的最早的聊天记录,并保留了设置的系统记录。

    2)要在我们的链中使用它,我们只需要先运行trimmer,然后将trimmer输入传递到提示符 。

    现在,如果我们尝试向模型询问我的名字,它不会知道,因为我的名字的聊天记录,因为超出65,而被删除掉了:

    1. from operator import itemgetter
    2. from langchain_core.runnables import RunnablePassthrough
    3. chain = (
    4. RunnablePassthrough.assign(messages=itemgetter("messages") | trimmer)
    5. | prompt
    6. | model
    7. )
    8. response = chain.invoke(
    9. {
    10. "messages": messages + [HumanMessage(content="what's my name?")],
    11. "language": "English",
    12. }
    13. )
    14. response.content

    但是,如果我们询问的是最后几条消息,它是会记住: 

    1. response = chain.invoke(
    2. {
    3. "messages": messages + [HumanMessage(content="what math problem did i ask")],
    4. "language": "English",
    5. }
    6. )
    7. response.content

    3)现在让我们将其包装在消息历史记录中:

    1. with_message_history = RunnableWithMessageHistory(
    2. chain,
    3. get_session_history,
    4. input_messages_key="messages",
    5. )
    6. config = {"configurable": {"session_id": "abc20"}}
    7. response = with_message_history.invoke(
    8. {
    9. "messages": messages + [HumanMessage(content="whats my name?")],
    10. "language": "English",
    11. },
    12. config=config,
    13. )
    14. response.content

     不出所料,我们说出我们名字的第一条消息已被删除。此外,聊天记录中现在还有两条新消息(我们的最新问题和最新回复)。这意味着过去在我们的对话历史记录中可以访问的更多信息,现在已经不再可用(被删除)!在本例中,我们最初的数学问题也从历史记录中删减了,因此模型不再知道它:

    1. response = with_message_history.invoke(
    2. {
    3. "messages": [HumanMessage(content="what math problem did i ask?")],
    4. "language": "English",
    5. },
    6. config=config,
    7. )
    8. response.content

    七、Streaming 流

    现在我们有了一个功能聊天机器人。然而,聊天机器人应用程序的一个非常重要的用户体验,就是要考虑流媒体因素。LLM 有时可能需要一段时间才能做出响应,因此为了改善用户体验,大多数应用程序所做的一件事就是在生成每个token时流回每个token。这允许用户查看进度。

    在Langchain 这其实非常容易做到,所有链都公开一个方法,使用消息历史记录的链也不例外。我们可以简单地使用该方法来返回流式响应。

    1. config = {"configurable": {"session_id": "abc15"}}
    2. for r in with_message_history.stream(
    3. {
    4. "messages": [HumanMessage(content="hi! I'm todd. tell me a joke")],
    5. "language": "chinese",
    6. },
    7. config=config,
    8. ):
    9. print(r.content, end="|")

  • 相关阅读:
    【JVM技术专题】网络问题分析和故障排查规划指南「实战篇」
    【第三章】神经网络的架构-前馈神经网络
    编程规范解决方案之ESLint + Git Hooks
    java计算机毕业设计-英杰学堂网上教学平台-源程序+mysql+系统+lw文档+远程调试
    Qt状态机框架
    OC-底层实现
    好用的翻译软件-大家都在用的互译软件
    Java项目_在线点餐系统(jsp+sevlet+mysql)(含论文)
    spyder切换conda环境(成功测试)
    提升工作效率!如何巧用 Ansible 实现自动化运维?
  • 原文地址:https://blog.csdn.net/sziitjin/article/details/140456870