微信扫码
添加专属顾问
❝通过本指南,你将学习构建一个专为航空公司设计的客服助手,它将协助用户查询旅行信息并规划行程。在此过程中,你将掌握如何利用LangGraph的中断机制、检查点技术以及更为复杂的状态管理功能,来优化你的助手工具,同时有效管理用户的机票预订、酒店住宿、租车服务以及旅游活动。
客服助手机器人能够帮助团队更高效地处理日常咨询,但要打造一个能够稳定应对各种任务且不会让用户感到烦恼的机器人并非易事。
完成本教程后,你不仅会拥有一个功能完备的机器人,还将深入理解LangGraph的核心理念和架构设计。这些知识将帮助你在其他人工智能项目中运用相似的设计模式。
由于内容较多,本文将由浅入深,分四个阶段进行讲解,每个阶段都将打造出一个具备以上描述所有能力的机器人。但受限于LLM的能力,初期阶段的机器人的运行可能存在各类问题,但都将在后续阶段得到解决。
你最终完成的聊天机器人将类似于以下示意图:
现在,让我们开启这第一阶段段的学习之旅吧!
在开始之前,我们需要搭建好环境。本教程将安装一些必要的先决条件,包括下载测试用的数据库,并定义一些在后续各部分中会用到的工具。
我们会使用 Claude 作为语言模型(LLM),并创建一些定制化的工具。这些工具大多数会连接到本地的 SQLite 数据库,无需额外依赖。此外,我们还会通过 Tavily 为代理提供网络搜索功能。
%%capture --no-stderr
% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas
接下来,执行下面的脚本来获取我们为这个教程准备的 SQLite 数据库,并更新它以反映当前的数据状态。具体细节不是重点。
import os
import requests
import sqlite3
import pandas as pd
import shutil
# 下载数据库文件
db_url = "https://storage.googleapis.com/benchmarks-artifacts/travel-db/travel2.sqlite"
local_file = "travel2.sqlite"
backup_file = "travel2.backup.sqlite"
overwrite = False
if not os.path.exists(local_file) or overwrite:
response = requests.get(db_url)
response.raise_for_status() # 确保请求成功
with open(local_file, "wb") as file:
file.write(response.content)
# 创建数据库备份,以便在每个教程部分开始时重置数据库状态
shutil.copy(local_file, backup_file)
# 将航班数据更新为当前时间,以适应我们的教程
conn = sqlite3.connect(local_file)
cursor = conn.cursor()
# 读取数据库中的所有表
tables = pd.read_sql(
"SELECT name FROM sqlite_master WHERE type='table';", conn
).name.tolist()
tdf = {}
for table_name in tables:
tdf[table_name] = pd.read_sql(f"SELECT * from {table_name}", conn)
# 找到最早的出发时间,并计算时间差
example_time = pd.to_datetime(
tdf["flights"]["actual_departure"].replace("\\N", pd.NaT)
).max()
current_time = pd.to_datetime("now").tz_localize(example_time.tz)
time_diff = current_time - example_time
# 更新预订日期和航班时间
for column in ["book_date", "scheduled_departure", "scheduled_arrival", "actual_departure", "actual_arrival"]:
tdf["flights"][column] = pd.to_datetime(
tdf["flights"][column].replace("\\N", pd.NaT)
) + time_diff
# 将更新后的数据写回数据库
for table_name, df in tdf.items():
df.to_sql(table_name, conn, if_exists="replace", index=False)
conn.commit()
conn.close()
# 在本教程中,我们将使用这个本地文件作为数据库
db = local_file
现在,我们来定义一些工具,以便助手可以搜索航空公司的政策手册,以及搜索和管理航班、酒店、租车和远足活动的预订。这些工具将在教程的各个部分中重复使用,具体的实现细节不是关键。
助手需要检索政策信息来回答用户的问题。请注意,这些政策的实施还需要在工具或 API 中进行,因为语言模型可能会忽略这些信息。以下工具受限于篇幅将仅提供定义及描述,详细代码[1]可在github上获取。
import re
import numpy as np
import openai
from langchain_core.tools import tool
@tool
def lookup_policy(query):
"""查询公司政策,以确定某些选项是否允许。"""
定义一个工具来获取用户的航班信息,然后定义一些工具来搜索航班和管理用户的预订信息,这些信息存储在 SQL 数据库中。
我们使用 ensure_config 来通过配置参数传递 passenger_id。语言模型不需要显式提供这些信息,它们会在图的每次调用中提供,以确保每个用户无法访问其他乘客的预订信息。
from langchain_core.runnables import ensure_config
from typing import Optional
import sqlite3
import pytz
from datetime import datetime, timedelta, date
@tool
def fetch_user_flight_information():
"""获取用户的所有机票信息,包括航班详情和座位分配。"""
@tool
def search_flights(
departure_airport=None,
arrival_airport=None,
start_time=None,
end_time=None,
limit=20,
):
"""根据出发机场、到达机场和出发时间范围来搜索航班。"""
@tool
def update_ticket_to_new_flight(ticket_no, new_flight_id):
"""将用户的机票更新到一个新的有效航班上。"""
@tool
def cancel_ticket(ticket_no):
"""取消用户的机票,并从数据库中移除。"""
用户预订了航班后,可能需要租车服务。定义一些工具,让用户能够在目的地搜索和预订汽车。
from typing import Optional, Union
from datetime import datetime, date
@tool
def search_car_rentals(
location=None,
name=None,
price_tier=None,
start_date=None,
end_date=None,
):
"""
根据位置、公司名称、价格等级、开始日期和结束日期来搜索租车服务。
参数:
location (Optional[str]): 租车服务的位置。
name (Optional[str]): 租车公司的名称。
price_tier (Optional[str]): 租车的价格等级。
start_date (Optional[Union[datetime, date]]): 租车的开始日期。
end_date (Optional[Union[datetime, date]]): 租车的结束日期。
返回:
list[dict]: 匹配搜索条件的租车服务列表。
"""
@tool
def book_car_rental(rental_id):
"""
通过租车ID来预订租车服务。
参数:
rental_id (int): 要预订的租车服务的ID。
返回:
str: 预订成功与否的消息。
"""
@tool
def update_car_rental(
rental_id,
start_date=None,
end_date=None,
):
"""
通过租车ID来更新租车服务的开始和结束日期。
参数:
rental_id (int): 要更新的租车服务的ID。
start_date (Optional[Union[datetime, date]]): 新的租车开始日期。
end_date (Optional[Union[datetime, date]]): 新的租车结束日期。
返回:
str: 更新成功与否的消息。
"""
@tool
def cancel_car_rental(rental_id):
"""
通过租车ID来取消租车服务。
参数:
rental_id (int): 要取消的租车服务的ID。
返回:
str: 取消成功与否的消息。
"""
用户需要住宿,因此定义一些工具来搜索和管理酒店预订。
@tool
def search_hotels(
location=None,
name=None,
price_tier=None,
checkin_date=None,
checkout_date=None,
):
"""
根据位置、名称、价格等级、入住日期和退房日期来搜索酒店。
参数:
location (Optional[str]): 酒店的位置。
name (Optional[str]): 酒店的名称。
price_tier (Optional[str]): 酒店的价格等级。
checkin_date
# 入住日期和退房日期,用于搜索酒店
checkin_date (Optional[Union[datetime, date]]): 酒店的入住日期。
checkout_date (Optional[Union[datetime, date]]): 酒店的退房日期。
返回:
list[dict]: 符合搜索条件的酒店列表。
"""
@tool
def book_hotel(hotel_id):
"""
通过酒店ID进行预订。
参数:
hotel_id (int): 要预订的酒店的ID。
返回:
str: 预订成功与否的消息。
"""
@tool
def update_hotel(
hotel_id,
checkin_date=None,
checkout_date=None,
):
"""
通过酒店ID更新酒店预订的入住和退房日期。
参数:
hotel_id (int): 要更新预订的酒店的ID。
checkin_date (Optional[Union[datetime, date]]): 新的入住日期。
checkout_date (Optional[Union[datetime, date]]): 新的退房日期。
返回:
str: 更新成功与否的消息。
"""
@tool
def cancel_hotel(hotel_id):
"""
通过酒店ID取消酒店预订。
参数:
hotel_id (int): 要取消预订的酒店的ID。
返回:
str: 取消成功与否的消息。
"""
最后,定义一些工具,让用户在到达目的地后搜索活动并进行预订。
@tool
def search_trip_recommendations(
location=None,
name=None,
keywords=None,
):
"""
根据位置、名称和关键词搜索旅行推荐。
参数:
location (Optional[str]): 旅行推荐的地点。
name (Optional[str]): 旅行推荐的名字。
keywords (Optional[str]): 与旅行推荐相关的关键词。
返回:
list[dict]: 符合搜索条件的旅行推荐列表。
"""
@tool
def book_excursion(recommendation_id):
"""
通过推荐ID预订远足活动。
参数:
recommendation_id (int): 要预订的旅行推荐的ID。
返回:
str: 预订成功与否的消息。
"""
@tool
def update_excursion(recommendation_id, details):
"""
通过推荐ID更新旅行推荐的细节。
参数:
recommendation_id (int): 要更新的旅行推荐的ID。
details (str): 旅行推荐的新细节。
返回:
str: 更新成功与否的消息。
"""
@tool
def cancel_excursion(recommendation_id):
"""
通过推荐ID取消旅行推荐。
参数:
recommendation_id (int): 要取消的旅行推荐的ID。
返回:
str: 取消成功与否的消息。
"""
定义一些辅助函数,以便在调试过程中美化图形中的消息显示,并为工具节点添加错误处理(通过将错误添加到聊天记录中)。
from langgraph.prebuilt import ToolNode
from langchain_core.runnables import RunnableLambda
def handle_tool_error(state):
error = state.get("error")
tool_calls = state["messages"][-1].tool_calls
return {
"messages": [
ToolMessage(
content=f"错误: {repr(error)}\n请修正你的错误。",
tool_call_id=tc["id"],
)
for tc in tool_calls
]
}
def create_tool_node_with_fallback(tools):
return ToolNode(tools).with_fallbacks(
[RunnableLambda(handle_tool_error)], exception_key="error"
)
def _print_event(event, _printed, max_length=1500):
current_state = event.get("dialog_state")
if current_state:
print(f"当前状态: ", current_state[-1])
message = event.get("messages")
if message:
if isinstance(message, list):
message = message[-1]
if message.id not in _printed:
msg_repr = message.pretty_repr(html=True)
if len(msg_repr) > max_length:
msg_repr = msg_repr[:max_length] + " ... (内容已截断)"
print(msg_repr)
_printed.add(message.id)
在构建任何系统时,最佳实践是从最简单的可行方案开始,并通过使用类似LangSmith这样的评估工具来测试其有效性。在条件相同的情况下,我们倾向于选择简单且可扩展的解决方案,而不是复杂的方案。然而,单一图谱方法存在一些限制,比如机器人可能在未经用户确认的情况下执行不希望的操作,处理复杂查询时可能遇到困难,或者在回答时缺乏针对性。这些问题我们会在后续进行改进。 在这部分,我们将定义一个简单的零样本代理作为用户的助手,并将所有工具赋予给它。我们的目标是引导它明智地使用这些工具来帮助用户。 我们的简单两节点图如下所示:
首先,我们定义状态。
我们将StateGraph的状态定义为一个包含消息列表的类型化字典。这些消息构成了聊天的记录,也就是我们简单助手所需要的全部状态信息。
from langgraph.graph.message import add_messages, AnyMessage
from typing_extensions import TypedDict
from typing import Annotated
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
然后,我们定义助手函数。这个函数接收图的状态,将其格式化为提示,然后调用一个大型语言模型(LLM)来预测最佳的响应。
from langchain_core.runnables import Runnable, RunnableConfig
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
class Assistant:
def __init__(self, runnable: Runnable):
self.runnable = runnable
def __call__(self, state: State, config: RunnableConfig):
while True:
passenger_id = config.get("passenger_id", None)
state = {**state, "user_info": passenger_id}
result = self.runnable.invoke(state)
# 如果大型语言模型返回了一个空响应,我们将重新提示它给出一个实际的响应。
if (
not result.content
or isinstance(result.content, list)
and not result.content[0].get("text")
):
messages = state["messages"] + [("user", "请给出一个真实的输出。")]
state = {**state, "messages": messages}
else:
break
return {"messages": result}
# Haiku模型更快、成本更低,但准确性稍差
# llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=1)
primary_assistant_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是一个为瑞士航空提供帮助的客户支持助手。"
"使用提供的工具来搜索航班、公司政策和其他信息以帮助回答用户的查询。"
"在搜索时,要有毅力。如果第一次搜索没有结果,就扩大你的查询范围。"
"如果搜索结果为空,不要放弃,先扩大搜索范围。"
"\n\n当前用户:\n<User>\n{user_info}\n</User>"
"\n当前时间:{time}。",
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now())
part_1_tools = [
TavilySearchResults(max_results=1),
fetch_user_flight_information,
search_flights,
lookup_policy,
update_ticket_to_new_flight,
cancel_ticket,
search_car_rentals,
book_car_rental,
update_car_rental,
cancel_car_rental,
search_hotels,
book_hotel,
update_hotel,
cancel_hotel,
search_trip_recommendations,
book_excursion,
update_excursion,
cancel_excursion,
]
part_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)
现在,我们来创建图。这张图是我们这部分的最终助手。
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import tools_condition, ToolNode
builder = StateGraph(State)
# 定义节点:这些节点执行具体的工作
builder.add_node("assistant", Assistant(part_1_assistant_runnable))
builder.add_node("action", create_tool_node_with_fallback(part_1_tools))
# 定义边:这些边决定了控制流程如何移动
builder.set_entry_point("assistant")
builder.add_conditional_edges(
"assistant",
tools_condition,
# "action"调用我们的工具之一。END导致图终止(并向用户做出响应)
{"action": "action", END: END},
)
builder.add_edge("action", "assistant")
# 检查点器允许图保存其状态
# 这是整个图的完整记忆。
memory = SqliteSaver.from_conn_string(":memory:")
part_1_graph = builder.compile(checkpointer=memory)
from IPython.display import Image, display
try:
display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))
except:
# 这需要一些额外的依赖项,是可选的
pass
现在,让我们通过一系列对话示例来测试我们的聊天机器人。
import uuid
import shutil
# 假设这是用户与助手之间可能发生的对话示例
tutorial_questions = [
"你好,我的航班是什么时候?",
"我可以把我的航班改签到更早的时间吗?我想今天晚些时候离开。",
"那就把我的航班改签到下周某个时间吧",
"下一个可用的选项很好",
"住宿和交通方面有什么建议?",
"我想在为期一周的住宿中选择一个经济实惠的酒店(7天),并且我还想租一辆车。",
"好的,你能为你推荐的酒店预订吗?听起来不错。",
"是的,去预订任何中等价位且有可用性的酒店。",
"对于汽车,我有哪些选择?",
"太棒了,我们只选择最便宜的选项。预订7天。",
"那么,你对我的旅行有什么建议?",
"在我在那里的时候,有哪些活动是可用的?",
"有趣 - 我喜欢博物馆,有哪些选择?",
"好的,那就为我在那里的第二天预订一个。",
]
# 使用备份文件以便我们可以从每个部分的原始位置重新启动
shutil.copy(backup_file, db)
thread_id = str(uuid.uuid4())
config = {
"configurable": {
# passenger_id 在我们的航班工具中使用
# 以获取用户的航班信息
"passenger_id": "3442 587242",
# 检查点通过 thread_id 访问
"thread_id": thread_id,
}
}
_printed = set()
for question in tutorial_questions:
events = part_1_graph.stream(
{"messages": ("user", question)}, config, stream_mode="values"
)
for event in events:
_print_event(event, _printed)
我们的简单助手表现得还不错!它能够合理地回答所有问题,快速地在上下文中做出回应,并成功地执行了我们所有的任务。你可以通过查看LangSmith的示例跟踪[2]来更好地了解LLM在上述交互中是如何被提示的。
如果这是一个简单的问答机器人,我们可能会对上述结果感到满意。由于我们的客户支持机器人是代表用户采取行动,因此它的一些行为有点令人担忧:
在下一节中,我们将解决前两个问题,期待你的持续关注!。
详细代码: https://github.com/langchain-ai/langgraph/blob/main/examples/customer-support/customer-support.ipynb
[2]LangSmith的示例跟踪: https://smith.langchain.com/public/f9e77b80-80ec-4837-98a8-254415cb49a1/r/26146720-d3f9-44b6-9bb9-9158cde61f9d
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2024-07-06
比Kimi还好用?AI写作神器「橙篇」来势汹汹 欲夺长文创作之未来
2024-07-06
暴走WAIC:跟AI+教育有关的,都在这儿↑
2024-07-02
【研究成果】ArchGPT:利用大语言模型支持传统建筑遗产的更新与保护
2024-06-28
所有男生女生,AI 卖货主播来咯!
2024-06-28
AI+医疗专题报告:院内场景丰富,AI 全面赋能医疗健康领域
2024-06-20
AI 背后 B 端设计师的机会
2024-06-20
30 款让教师工作更轻松的 AI 工具
2024-06-13
知识图谱(KG)和大模型(LLMs)双轮驱动的企业级AI平台构建之道暨行业调研
2026-06-05
2026-06-03
2026-05-20
2026-05-26
2026-06-02
2026-05-14
2026-05-12
2026-05-21
2026-06-03
2026-06-05
2026-07-04
2026-07-03
2026-06-19
2026-06-09
2026-06-04
2026-03-18
2026-03-05
2026-03-05
欢迎您使用【53AI 官方网站】(以下简称“本网站”或“我们”)。本《会员服务协议》(以下简称“本协议”)是您(以下简称“会员”或“用户”)与【深圳市博思协创网络科技有限公司】之间关于注册、登录及使用本网站会员服务所订立的法律协议。
在您注册或登录前,请务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的条款、知识产权条款、争议解决条款等。此类条款将以加粗形式提示您注意。 当您通过微信公众号授权、手机验证码验证或其他方式成功登录本网站时,即视为您已完全理解并同意接受本协议的全部内容。
一、 定义
本网站:指由【深圳市博思协创网络科技有限公司】运营的,域名为【53ai.com】的网站及相关移动端页面。
会员服务:指本网站向注册会员提供的知识库文章查阅、内容检索及其他相关增值服务。
知识库内容:指本网站发布的包括但不限于文字、图表、数据、研究报告、行业分析等数字化内容资源。
二、 账号注册与登录
登录方式:本网站支持以下登录方式,您可根据实际情况选择:
微信公众号授权登录:您同意将您的微信OpenID信息授权给本网站,用于创建或关联会员账号。
手机验证码登录:您需提供真实有效的手机号码,并通过短信验证码完成身份验证与登录/注册。
账号安全:您的账号仅限您本人使用,禁止赠与、借用、租用、转让或售卖。因您保管不善导致的账号被盗、密码泄露等损失,由您自行承担。
实名认证:根据相关法律法规要求,我们可能要求您在特定功能下完成实名认证。如您拒绝提供,可能无法使用部分或全部服务。
未成年人保护:若您未满18周岁,请在法定监护人的陪同下阅读本协议,并在征得监护人同意后使用本服务。
三、 服务内容与规范
知识库查阅权限:会员登录后,有权按照其会员等级对应的权限范围,在线浏览、检索本网站知识库中的相关文章及内容。
服务变更:我们有权根据业务发展需要,调整、变更或终止部分服务内容,并将以网站公告、公众号消息等方式提前通知。
禁止行为:您在使用服务时不得实施以下行为:
利用技术手段批量爬取、下载、转存知识库内容;
将知识库内容用于商业目的或未经授权地向第三方传播;
干扰本网站正常运行或侵犯其他用户合法权益;
发布违法违规信息或从事违反公序良俗的活动。
四、 知识产权声明
权利归属:本网站知识库中的排版设计、软件代码等内容的知识产权均归【公司全称】或原权利人所有,受《中华人民共和国著作权法》等法律保护。
有限许可:本网站授予会员一项非独占、不可转让、不可转授权的普通许可,仅限于个人学习、研究之目的在线查阅知识库内容。
侵权追责:未经书面许可,任何单位或个人不得以任何形式复制、转载、摘编、镜像、汇编或以其他方式使用上述内容。一经发现,我们保留追究其法律责任的权利。
五、 个人信息保护
我们重视对您个人信息的保护。关于我们如何收集、使用、存储和保护您的个人信息,请单独阅读 《隐私政策》。
您通过微信公众号授权或手机号验证所提供的信息,我们将严格按照《个人信息保护法》的规定处理,仅用于身份识别、服务提供及安全验证等必要用途。
您可以随时通过网站设置或联系客服行使查阅、更正、删除个人信息及撤回授权同意的权利。
六、 免责声明
内容准确性:知识库内容仅供参考,不构成专业建议。我们不对其完整性、准确性、时效性作任何明示或暗示的保证,您应自行判断并承担使用风险。
不可抗力:因自然灾害、政策法规变化、网络故障、第三方平台接口异常(如微信接口维护、运营商短信通道故障)等不可抗力导致的服务中断或延迟,我们不承担违约责任。
第三方链接:本网站可能包含指向第三方网站的链接,该等网站的内容和服务不受我们控制,请您自行甄别风险。
七、 违约责任
如您违反本协议约定,我们有权视情节采取警告、限制功能、暂停服务、注销账号等措施,并保留要求赔偿损失的权利。
如因您的违约行为导致我们遭受行政处罚、第三方索赔或商誉损失,您应承担全部赔偿责任(包括但不限于罚款、赔偿金、律师费、公证费等)。
八、 法律适用与争议解决
本协议的订立、执行和解释均适用中华人民共和国大陆地区法律。
因本协议产生的或与本协议有关的任何争议,双方应友好协商解决;协商不成的,任何一方均可向【公司所在地】有管辖权的人民法院提起诉讼。
九、 其他
本协议构成双方就本服务达成的完整协议,取代此前任何口头或书面约定。
本协议任一条款被认定为无效或不可执行的,不影响其他条款的效力。
我们对本协议享有最终解释权,并在法律允许的范围内保留随时修改的权利。修改后的协议一经公布即生效,继续使用服务即视为同意修订内容。