为了账号安全,请及时绑定邮箱和手机立即绑定

用LangChain和LangGraph构建任务导向对话系统,轻松创建高质量用户故事

又来一个LangGraph(语言图谱)教程

图片来自KaleidicoUnsplash

任务导向对话系统(ToD)是一种帮助用户完成特定的目标任务的系统,例如预订餐厅、规划旅行或点外卖。

我们知道我们是通过提示来指导大型语言模型的,但是我们该如何实现这些对话系统,让对话始终围绕着我们希望用户达成的目标?其中一种方法是利用提示记忆工具调用。幸运的是,LangChain 和 LangGraph 可以帮助我们把这些元素整合起来。

这篇文章将教你如何构建一个以任务为导向的对话系统,帮助用户创建具有高质量的用户故事描述。该系统基于LangGraph的教程,即从用户需求生成提示(信息收集提示)

为为什么我们需要使用LangGraph?

在这篇教程中,我们假设你已经知道如何使用LangChain。一个用户故事包含一些组件,比如目标、成功标准、执行计划和交付物。用户需要提供每一个组件,我们需要“手把手教”用户一步步地提供这些信息。仅用LangChain来做这些步骤会需要很多的if和else。

使用LangGraph,我们可以利用图抽象来创建循环路径管理对话流程。它还具有持久性,因此我们无需担心图中的交互记录。

主要的 LangGraph 抽象是 StateGraph,它用于创建图工作流。每个图都需要初始化为一个 状态模式,以便每个节点可以读写信息。

我们的系统流程将由 LLMuser 之间的消息轮次组成。主循环将包括以下几个步骤:

  1. 用户说了些什么话
  2. LLM 读取消息的状态信息,决定是否准备好创建用户故事,或者是否需要用户再次提供信息

我们的系统非常简单,因此结构仅由对话中交换过的消息组成。

    # 从langgraph.graph.message导入add_messages方法
    from langgraph.graph.message import add_messages

    # 定义StateSchema类,继承自TypedDict,包含消息列表
    class StateSchema(TypedDict):  
        messages: Annotated[list, add_messages]

我们使用 add_messages 方法来合并每个 节点 的输出消息到图的状态消息列表中。

谈到节点,LangGraph 中的另外两个主要概念是 节点。每个 节点 都运行一个函数,而每条 则控制从一个节点到另一个节点的流程。我们还有两个虚拟节点:STARTEND,来指示从何处开始执行以及在何处结束执行。

要运行系统,我们将使用 .stream() 方法。构建并编译好图之后,每次交互都会从 START 到图的 END 进行,其路径(哪些节点应该运行或不运行)则由我们的工作流程与图的状态共同控制。以下代码展示了我们系统的主流程:

    config = {"configurable": {"thread_id": str(uuid.uuid4())}}  

    while True:  
        user = input("用户名(输入 q/Q 退出): ")  
        if user in {"q", "Q"}:  
            print("AI: 拜拜")  
            break  
        output = None  
        for output in graph.stream(  
            {"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"  
        ):  
            last_message = next(iter(output.values()))["messages"][-1]  
            last_message.pretty_print()  

        if output and 'prompt' in output.get('messages', []):  
            print("完成啦!")

在每次交互(如果用户没有输入“q”或“Q”来退出)时,我们运行 graph.stream(),并使用“updates”流模式传递用户的消息,这会流式传输每一步的状态更新(https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream)。然后,我们从状态模式中的消息获取最后的消息并打印出来或显示出来。

在本教程中,我们仍然会学习如何创建图的节点和边,但在那之前,我们先来深入了解一下ToD系统的一般架构,并学习如何用LLMs提示词以及工具调用来实现一个系统。

ToD系统的架构

构建端到端任务型对话系统框架包括如下主要组成部分:

  1. 自然语言理解(NLU),用于提取用户的意图和关键信息
  2. 对话状态追踪(DST),用于追踪用户的对话信念状态
  3. 对话策略学习(DPL),用于确定下一步行动
  4. 自然语言生成(NLG),用于生成对话系统回复

一个ToD的主要组件(图片引自 Qin, Libo 等 [1])

通过使用大规模语言模型(LLMs),我们可以将这些组件中的某些合并为一个。NLPNLG 组件使用 LLMs 实现起来非常简单,因为理解和生成对话回复是它们的拿手好戏。

我们可以使用LangChain的SystemMessage来设定AI的行为,并在每次与LLM交互时始终传递这条消息。对话的状态也应在每次与模型交互时传递给LLM。这意味着我们将确保对话始终围绕我们希望用户完成的任务进行,方法是始终告知LLM对话的目的以及它应该如何行动。我们将首先通过一个提示语来实现这一点:

    prompt_system_task = """你的工作是从用户那里收集有关他们需要创建的用户故事的信息。  

    你应该从他们那里获取以下信息:  

    - 宗旨:用户故事的宗旨。宗旨应该足够具体,可以在两周内完成开发。  
    - 成功准则:用户故事的成功准则。  
    - 执行方案:该倡议的执行方案。  
    - 可交付成果:该倡议的可交付成果。  

    如果你无法分辨这些信息,请让他们澄清!不要试图随意猜测。  
    每当用户对其中的一项标准做出回应时,评估它是否足够详细,可以成为用户故事的一项准则。如果不够详细,请提出更具体的问题来帮助用户更好地描述该标准。  
    不要一次提出太多问题,而是分步骤地询问,这样用户每次的回答量就不会太大。  
    时刻提醒他们,如果他们不知道如何回答某个问题,你可以帮助他们。  

    在收集到所有必要信息后,然后使用相关的工具。"""

每次我们给LLM发送消息时,都加上这个提示:

定义了一个名为domain_state_tracker的函数,该函数接收消息列表作为参数,并返回一个包含系统消息的列表,加上原始消息列表。

def domain_state_tracker(messages):
return [SystemMessage(content=prompt_system_task)] + messages


我们的ToD系统LLM实现中的另一个重要概念是**调用工具**。如果你再读一遍**prompt_system_task**(这个术语指的是)的最后一句,它说“_在你能够识别出所有相关信息后,调用相关工具_”。这样讲,就是告诉LLM,当它确认用户已经提供了所有用户故事所需参数时,**它需要调用相应的工具来生成用户故事**。我们将使用带有用户故事参数的Pydantic模型来创建该工具。

仅通过使用提示和工具调用,我们就可以控制我们的ToD系统。漂亮吧?实际上,我们也需要使用**图的当前状态**来使这一切运作。让我们在下一节中来实现它,届时我们将最终搭建ToD系统。

# 创建对话系统来编写用户场景

好的,开始写代码了。首先,我们指定要使用哪个LLM模型,然后设置提示词并绑定工具以生成用户故事文本。
import os  
from dotenv import load_dotenv, find_dotenv  

from langchain_openai import AzureChatOpenAI  
from langchain_core.pydantic_v1 import BaseModel  
from typing import List, Literal, Annotated  

_ = load_dotenv(find_dotenv())  # 读取本地 .env 文件  

llm = AzureChatOpenAI(azure_deployment=os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"),  
                    openai_api_version="2023-09-01-preview",  
                    openai_api_type="azure",  
                    openai_api_key=os.environ.get('AZURE_OPENAI_API_KEY'),  
                    azure_endpoint=os.environ.get('AZURE_OPENAI_ENDPOINT'),  
                    temperature=0)  

prompt_system_task = """你的任务是帮助用户明确他们需要创建的用户故事的具体信息。  

你应该从他们那里获得以下信息:  

- 目标:用户故事的目标。应足够具体以便可以在两周内完成。  
- 成功标准:用户故事的成功标准  
- 执行计划:该倡议的执行计划  

如果你无法判断这些信息,请让他们澄清!不要试图随意猜测。  
每当用户回答了其中某个标准时,评估它是否足够具体,可以符合用户故事的标准。如果不是,请提出问题来帮助用户更好地详细说明该标准。  
不要一次问太多问题,避免让用户感到困扰。  
始终提醒他们,如果他们不知道如何回答某些问题,你可以帮助他们。  

在能够判断所有信息后,调用相关工具。"""  

class UserStoryCriteria(BaseModel):  
    """提示 LLM 的指导说明。"""  
    objective: str  
    success_criteria: str  
    plan_of_execution: str  

llm_with_tool = llm.bind_tools([UserStoryCriteria])

正如我们之前讨论的,我们的图仅由交换的消息和一个标志构成,这个标志用来表示用户故事是否已经被创建。让我们使用**StateGraph**和这个结构创建图:
from langgraph.graph import StateGraph, START, END  
from langgraph.graph.message import add_messages  

# 状态模式定义了消息的类型和结构,其中消息列表使用add_messages函数进行处理。
class StateSchema(TypedDict):  
    messages: Annotated[list, add_messages]  

# 定义了一个名为workflow的工作流,使用了StateSchema作为状态模式。
workflow = StateGraph(StateSchema)

下面的图显示了最终图表的结构:

![](https://imgapi.imooc.com/b3a6f567097b543102120432.jpg)

由作者创建的ToD时间依赖图用于创建用户故事(ToD图由作者创建)

最上面是一个**talk_to_user**节点。它可以执行以下操作:

(注意:原文未详细说明该节点的所有可能操作,以下假设会进一步列出具体操作。)

- 做某事
- 或者做另一件事

* 结束对话(去到 **finalize_dialogue** 节点)
* 决定该等待用户的输入(去到 **END** 节点)

由于主循环会无限运行(while True),每当图到达END节点时,它会再次等待用户输入指令。这会在创建循环时更清楚。

我们来创建图的节点,首先创建**talk_to_user**节点。这个节点需要跟踪任务(保持主提示在整个对话中),并保留消息交换,因为它存储了对话的状态。这个状态还会通过消息记录用户故事中哪些参数已经填写或未填写。因此,每当有新的消息来自LLM时,这个节点都应该添加系统消息并追加新消息:
def domain_state_tracker(messages):  
    return [SystemMessage(content=提示系统任务)] + messages  

def call_llm(state: StateSchema):  
    """  
    与用户对话节点函数,将提示系统任务添加到消息中,调用LLM并返回响应  
    """  
    messages = domain_state_tracker(state["messages"])  
    response = llm_with_tool.invoke(messages)  
    return {"messages": [response]}  

现在我们可以把 **talk_to_user** 节点加到这个图里。我们可以通过给它起个名字并传递我们写的那个函数来实现。
workflow.add_node("与用户对话", call_llm)

这个节点图中首先运行的节点,所以我们通过增加一条**边**来明确这一点:
workflow.add_edge(START, "用户交谈")

到现在为止,图目前是这样的。

![](https://imgapi.imooc.com/677b543509d220ee01290134.jpg)

(如下图所示)我们有一个只有一个节点的图(由作者绘制)。

为了控制图表的流程,我们也将使用LangChain的消息类,我们有四种消息类型。

* **系统消息:** 用于设定AI行为的消息
* **人类消息:** 人类发出的消息
* **AI消息:** 聊天模型回复的消息
* **工具消息:** 包含工具执行结果的消息,用于回传给模型

我们将使用**图的状态**中的最后一条消息的**类型**来控制**talk_to_user**节点上的流程控制。如果最后一条消息是**_AIMessage_**并且它具有**_tool_calls_**键,那么我们将进入**finalize_dialogue**节点,因为是时候了,现在是创建用户故事的时候了。否则,我们应该进入**结束节点**,因为轮到用户回答了,循环回到用户。

**finalize_dialogue** 节点应该构建 **ToolMessage** 以将结果传递给模型的结果。**tool_call_id** 字段用于将工具调用请求与响应关联起来。我们来创建这个节点并把它加到图里:
def finalize_dialogue(state: StateSchema):  
    """  
    向历史中添加一个工具消息,以便图形知道该创建用户故事了。  
    """  
    return {  
        "messages": [  
            ToolMessage(  
                content="提示已生成!",  
                tool_call_id=state["messages"][-1].tool_calls[0]["id"],  
            )  
        ]  
    }  

workflow.add_node("finalize_dialogue", finalize_dialogue)

现在我们来创建最后一个节点,即 **create_user_story** 节点。这个节点会使用提示来调用LLM,以及之前对话中收集的信息。如果模型认为该调用工具了,那么 **tool_calls** 键里就应该有创建用户故事所需的所有信息。
prompt_generate_user_story = """根据以下需求,编写一个良好的用户故事:  

{reqs}"""  

def build_prompt_to_generate_user_story(messages: list):  
    tool_call = None  
    other_msgs = []  
    for m in messages:  
        if isinstance(m, AIMessage) and m.tool_calls:  
            tool_call = m.tool_calls[0]["args"] # tool_calls 来自 OpenAI API  
        elif isinstance(m, ToolMessage):  
            continue  
        elif tool_call:  
            other_msgs.append(m)  
    return [SystemMessage(content=prompt_generate_user_story.format(reqs=tool_call))] + other_msgs  

def call_model_to_generate_user_story(state):  
    messages = build_prompt_to_generate_user_story(state["messages"])  
    response = llm.invoke(messages)  
    return {"messages": [response]}  

workflow.add_node("create_user_story", call_model_to_generate_user_story)

当所有节点都创建完成后,是时候添加 **边** 了。我们将为 **talk_to_user** 节点添加一个条件边(条件分支)。这个节点可以:根据条件决定是否调用 **talk_to_user** 功能。

* 如果该调用工具,就完成对话(转至 **finalize_dialogue** 节点)
* 决定需要用户输入(转至 **END** 节点)

这意味着我们将只检查最后一条消息是否是AIMessage,并且包含tool_calls键;如果不是,我们就跳转到END节点。让我们创建一个函数来检查这个条件,并将其作为一条边添加。

def define_next_action(state) -> Literal["finalize_dialogue", END]:
messages = state["messages"]

if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:  
    return "finalize_dialogue"  
else:  
    return END  

workflow.add_conditional_edges("talk_to_user", define_next_action)


现在我们来加剩下的线条
workflow.add_edge("finalize_dialogue", "create_user_story"); // 结束对话后,创建用户故事
workflow.add_edge("create_user_story", END); // 创建用户故事后,流程结束

这样图的流程就完成了。接下来我们编译这个图并创建一个循环来运行它吧:
memory = MemorySaver()  
graph = workflow.compile(checkpointer=memory)  

config = {"configurable": {"thread_id": str(uuid.uuid4())}}  # 可配置的线程ID

while True:  
    user = input("用户(输入 q/Q 退出):")  
    if user in {"q", "Q"}:  
        print("AI: 拜拜")  
        break  
    output = None  
    for output in graph.stream(  
        {"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"  
    ):  
        last_message = next(iter(output.values()))["messages"][-1]  
        last_message.pretty_print()  # 美化打印消息

    if output and "create_user_story" in output:  
        print("用户故事创建了!")


我们最后来测试系统吧,

![](https://imgapi.imooc.com/677b543709ee5c1614003114.jpg)

助理正在行动(由作者创作的图片)

# 最后的感想:

(注:此处添加了空白行和冒号,以符合源文本的格式。)

借助LangGraph和LangChain,我们可以构建系统来引导用户进行结构化的互动,通过使用LLMs来帮助我们处理条件逻辑,从而降低创建这些系统的复杂性。

通过结合提示、内存的管理和工具调用,我们能够创建出既直观又有效的对话系统,为用户交互和自动完成任务打开新的大门。

希望这个教程能帮助你更好地理解如何使用LangGraph,(我花了好几天时间才弄清楚这个库的所有部分是如何配合工作的).

本教程中的所有代码都可以在这里找到:[dmesquita/task_oriented_dialogue_system_langgraph (GitHub)](https://github.com/dmesquita/task_oriented_dialogue_system_langgraph)

谢谢大家阅读这篇文章!

# 参考文献

[1] 秦立博等. “端到端任务导向对话系统:任务、方法及未来方向综述文章.” _arXiv:2311.09008_ (2023 年).

[2] _根据用户需求生成提示_. 更多相关信息,请访问:<https://langchain-ai.github.io/langgraph/tutorials/chatbots/information-gather-prompting>。
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消