任务导向对话系统(ToD)是一种帮助用户完成特定的目标任务的系统,例如预订餐厅、规划旅行或点外卖。
我们知道我们是通过提示来指导大型语言模型的,但是我们该如何实现这些对话系统,让对话始终围绕着我们希望用户达成的目标?其中一种方法是利用提示、记忆和工具调用。幸运的是,LangChain 和 LangGraph 可以帮助我们把这些元素整合起来。
这篇文章将教你如何构建一个以任务为导向的对话系统,帮助用户创建具有高质量的用户故事描述。该系统基于LangGraph的教程,即从用户需求生成提示(信息收集提示)。
为为什么我们需要使用LangGraph?在这篇教程中,我们假设你已经知道如何使用LangChain。一个用户故事包含一些组件,比如目标、成功标准、执行计划和交付物。用户需要提供每一个组件,我们需要“手把手教”用户一步步地提供这些信息。仅用LangChain来做这些步骤会需要很多的if和else。
使用LangGraph,我们可以利用图抽象来创建循环路径以管理对话流程。它还具有持久性,因此我们无需担心图中的交互记录。
主要的 LangGraph 抽象是 StateGraph,它用于创建图工作流。每个图都需要初始化为一个 状态模式,以便每个节点可以读写信息。
我们的系统流程将由 LLM 和 user 之间的消息轮次组成。主循环将包括以下几个步骤:
- 用户说了些什么话
- 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 中的另外两个主要概念是 节点 和 边。每个 节点 都运行一个函数,而每条 边 则控制从一个节点到另一个节点的流程。我们还有两个虚拟节点:START 和 END,来指示从何处开始执行以及在何处结束执行。
要运行系统,我们将使用 .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系统的架构构建端到端任务型对话系统框架包括如下主要组成部分:
- 自然语言理解(NLU),用于提取用户的意图和关键信息
- 对话状态追踪(DST),用于追踪用户的对话信念状态
- 对话策略学习(DPL),用于确定下一步行动
- 自然语言生成(NLG),用于生成对话系统回复
一个ToD的主要组件(图片引自 Qin, Libo 等 [1])
通过使用大规模语言模型(LLMs),我们可以将这些组件中的某些合并为一个。NLP 和 NLG 组件使用 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>。
共同学习,写下你的评论
评论加载中...
作者其他优质文章