探索LangGraph:構(gòu)建多專家協(xié)作模型
探索LangGraph:如何創(chuàng)建一個(gè)既智能又可控的航空客服AI利用單一提示的方法確實(shí)能覆蓋很廣的應(yīng)用場(chǎng)景。但是,如果想要為特定的用戶需求提供穩(wěn)定且出色的體驗(yàn),僅靠這種方法就顯得有些力不從心了。
取而代之的是,我們可以通過識(shí)別用戶的意圖,并將其引導(dǎo)至相應(yīng)的定制化流程或“技能”,來滿足用戶的具體需求。每個(gè)流程都可以專注于特定的領(lǐng)域,這樣不僅可以實(shí)現(xiàn)各自領(lǐng)域的優(yōu)化提升,還不會(huì)影響到整體助手的性能。
在本節(jié)中,我們將用戶交互體驗(yàn)劃分為多個(gè)子圖,形成一個(gè)類似下面的結(jié)構(gòu):
在上圖中,每個(gè)方框都代表一個(gè)具有特定功能的獨(dú)立工作流程。主要助手負(fù)責(zé)接收用戶的初步詢問,然后根據(jù)詢問內(nèi)容將任務(wù)分配給相應(yīng)的專家。
狀態(tài)管理
我們需要跟蹤在任何特定時(shí)刻哪個(gè)子圖正在控制交互過程。雖然我們可以通過消息列表上的一些計(jì)算來實(shí)現(xiàn)這一點(diǎn),但更簡(jiǎn)單的方法是使用一個(gè)專門的堆棧來跟蹤。
在下面的State?中添加一個(gè)dialog_state?列表。每當(dāng)一個(gè)node?運(yùn)行并返回dialog_state?的值時(shí),就會(huì)調(diào)用update_dialog_stack函數(shù)來決定如何更新堆棧。
from typing import Annotated, Literal, Optional
from typing_extensions import TypedDict
from langgraph.graph.message import AnyMessage, add_messages
def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:
"""推入或彈出狀態(tài)。"""
if right is None:
return left
if right == "pop":
return left[:-1]
return left + [right]
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
user_info: str
dialog_state: Annotated[
list[
Literal[
"assistant",
"update_flight",
"book_car_rental",
"book_hotel",
"book_excursion",
]
],
update_dialog_stack,
]
助手
這次我們將為每個(gè)工作流程創(chuàng)建一個(gè)助手。這意味著:
- 航班預(yù)訂助手
- 酒店預(yù)訂助手
- 汽車租賃助手
- 旅行助手
- 最后,一個(gè)“主要助手”來在這些助手之間進(jìn)行切換
如果你仔細(xì)觀察,你會(huì)發(fā)現(xiàn)這實(shí)際上是我們?cè)诙啻硎纠刑岬降谋O(jiān)督者設(shè)計(jì)模式的一個(gè)實(shí)例。
下面,定義每個(gè)助手的Runnable?對(duì)象。每個(gè)Runnable?都有一個(gè)提示、LLM以及針對(duì)該助手的工具集。每個(gè)專門的助手還可以調(diào)用CompleteOrEscalate工具,以指示控制權(quán)應(yīng)該交回給主要助手。這可能發(fā)生在助手成功完成任務(wù),或者用戶改變主意或需要該特定工作流程范圍之外的幫助時(shí)。
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.runnables import Runnable, RunnableConfig
class Assistant:
def __init__(self, runnable: Runnable):
self.runnable = runnable
def __call__(self, state: State, config: RunnableConfig):
while True:
result = self.runnable.invoke(state)
if not result.tool_calls and (
not result.content
or isinstance(result.content, list)
and not result.content[0].get("text")
):
messages = state["messages"] + [("user", "用真實(shí)的輸出回應(yīng)。")]
state = {**state, "messages": messages}
messages = state["messages"] + [("user", "用真實(shí)的輸出回應(yīng)。")]
state = {**state, "messages": messages}
else:
break
return {"messages": result}
class CompleteOrEscalate(BaseModel):
"""一個(gè)工具,標(biāo)記當(dāng)前任務(wù)為已完成和/或?qū)?duì)話控制權(quán)升級(jí)到主助手,
主助手可以根據(jù)用戶的需求重新路由對(duì)話。"""
cancel: bool = True
reason: str
class Config:
schema_extra = {
"example": {
"cancel": True,
"reason": "用戶改變了他們對(duì)當(dāng)前任務(wù)的想法。",
},
"example 2": {
"cancel": True,
"reason": "我已經(jīng)完全完成了任務(wù)。",
},
"example 3": {
"cancel": False,
"reason": "我需要搜索用戶的電子郵件或日歷以獲取更多信息。",
},
}
航班預(yù)訂助手
創(chuàng)建一個(gè)專門的助手來處理航班更新和取消預(yù)訂的任務(wù)。
# 航班預(yù)訂助手
flight_booking_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"您是專門處理航班更新的助手。"
"每當(dāng)用戶需要幫助更新他們的預(yù)訂時(shí),主要助手就會(huì)委派工作給您。"
"與客戶確認(rèn)更新的航班詳情,并告知他們?nèi)魏晤~外費(fèi)用。"
"搜索時(shí),要堅(jiān)持不懈。如果第一次搜索沒有結(jié)果,就擴(kuò)大您的查詢范圍。"
"如果您需要更多信息或客戶改變了主意,將任務(wù)升級(jí)回主助手。"
"記住,只有在相關(guān)工具成功使用后,預(yù)訂才算完成。"
"\n\n當(dāng)前用戶航班信息:\n<Flights>\n{user_info}\n</Flights>"
"\n當(dāng)前時(shí)間:{time}。"
"\n\n如果用戶需要幫助,而且您的工具都不適合,那么"
'"CompleteOrEscalate" 對(duì)話到主機(jī)助手。不要浪費(fèi)用戶的時(shí)間。不要編造無效的工具或功能。',
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now())
update_flight_safe_tools = [search_flights]
update_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]
update_flight_tools = update_flight_safe_tools + update_flight_sensitive_tools
update_flight_runnable = flight_booking_prompt | llm.bind_tools(
update_flight_tools + [CompleteOrEscalate]
)
汽車租賃助手
接下來,創(chuàng)建一個(gè)汽車租賃助手,以滿足所有租車需求。
# 汽車租賃助手
book_car_rental_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"您是專門處理汽車租賃預(yù)訂的助手。"
"每當(dāng)用戶需要幫助預(yù)訂汽車租賃時(shí),主要助手就會(huì)委派工作給您。"
"根據(jù)用戶的偏好搜索可用的汽車租賃,并與客戶確認(rèn)預(yù)訂詳情。"
"搜索時(shí),要堅(jiān)持不懈。如果第一次搜索沒有結(jié)果,就擴(kuò)大您的查詢范圍。"
"如果您需要更多信息或客戶改變了主意,將任務(wù)升級(jí)回主助手。"
"記住,只有在相關(guān)工具成功使用后,預(yù)訂才算完成。"
"\n當(dāng)前時(shí)間:{time}。"
"\n\n如果用戶需要幫助,而且您的工具都不適合,那么 "
'"CompleteOrEscalate" 對(duì)話到主機(jī)助手。不要浪費(fèi)用戶的時(shí)間。不要編造無效的工具或功能。'
"\n\n一些您應(yīng)該 CompleteOrEscalate 的示例:"
" - '今年這個(gè)時(shí)候的天氣怎么樣?'"
" - '有哪些航班可用?'"
" - '沒關(guān)系,我想我會(huì)單獨(dú)預(yù)訂'"
" - '哦等等,我還沒有預(yù)訂我的航班,我會(huì)先做這件事'"
" - '汽車租賃預(yù)訂確認(rèn)'",
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now())
book_car_rental_safe_tools = [search_car_rentals]
book_car_rental_sensitive_tools = [
book_car_rental,
update_car_rental,
cancel_car_rental,
]
book_car_rental_tools = book_car_rental_safe_tools + book_car_rental_sensitive_tools
book_car_rental_runnable = book_car_rental_prompt | llm.bind_tools(
book_car_rental_tools + [CompleteOrEscalate]
)
酒店預(yù)訂助手
然后定義酒店預(yù)訂的工作流程。
# 酒店預(yù)訂助手
book_hotel_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"您是專門處理酒店預(yù)訂的助手。"
"每當(dāng)用戶需要幫助預(yù)訂酒店時(shí),主要助手就會(huì)委派工作給您。"
"根據(jù)用戶的偏好搜索可用的酒店,并與客戶確認(rèn)預(yù)訂詳情。"
"搜索時(shí),要堅(jiān)持不懈。如果第一次搜索沒有結(jié)果,就擴(kuò)大您的查詢范圍。"
"如果您需要更多信息或客戶改變了主意,將任務(wù)升級(jí)回主助手。"
"記住,只有在相關(guān)工具成功使用后,預(yù)訂才算完成。"
"\n當(dāng)前時(shí)間:{time}。"
'\n\n如果用戶需要幫助,而且您的工具都不適合,那么 "CompleteOrEscalate" 對(duì)話到主機(jī)助手。'
"不要浪費(fèi)用戶的時(shí)間。不要編造無效的工具或功能。"
"\n\n一些您應(yīng)該 CompleteOrEscalate 的示例:"
" - '今年這個(gè)時(shí)候的天氣怎么樣?'"
" - '沒關(guān)系,我想我會(huì)單獨(dú)預(yù)訂'"
" - '我需要弄清楚我在那里的時(shí)候的交通'"
" - '哦等等,我還沒有預(yù)訂我的航班,我會(huì)先做這件事'"
" - '酒店預(yù)訂確認(rèn)'",
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now())
book_hotel_safe_tools = [search_hotels]
book_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]
book_hotel_tools = book_hotel_safe_tools + book_hotel_sensitive_tools
book_hotel_runnable = book_hotel_prompt | llm.bind_tools(
book_hotel_tools + [CompleteOrEscalate]
)
旅行助手
之后,定義旅行助手。
# 旅行助手
book_excursion_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"您是專門處理旅行建議的助手。"
"每當(dāng)用戶需要幫助預(yù)訂推薦的旅行時(shí),主要助手就會(huì)委派工作給您。"
"根據(jù)用戶的偏好搜索可用的旅行建議,并與客戶確認(rèn)預(yù)訂詳情。"
"如果您需要更多信息或客戶改變了主意,將任務(wù)升級(jí)回主助手。"
"搜索時(shí),要堅(jiān)持不懈。如果第一次搜索沒有結(jié)果,就擴(kuò)大您的查詢范圍。"
"記住,只有在相關(guān)工具成功使用后,預(yù)訂才算完成。"
"\n當(dāng)前時(shí)間:{time}。"
'\n\n如果用戶需要幫助,而且您的工具都不適合,那么 "CompleteOrEscalate" 對(duì)話到主機(jī)助手。不要浪費(fèi)用戶的時(shí)間。不要編造無效的工具或功能。'
"\n\n一些您應(yīng)該 CompleteOrEscalate 的示例:"
" - '沒關(guān)系,我想我會(huì)單獨(dú)預(yù)訂'"
" - '我需要在那里的時(shí)候弄清楚交通'"
" - '哦等等,我還沒有預(yù)訂我的航班,我會(huì)先做這件事'"
" - '旅行預(yù)訂確認(rèn)!'",
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now())
book_excursion_safe_tools = [search_trip_recommendations]
book_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]
book_excursion_tools = book_excursion_safe_tools + book_excursion_sensitive_tools
book_excursion_runnable = book_excursion_prompt | llm.bind_tools(
book_excursion_tools + [CompleteOrEscalate]
)
主要助手
最后,創(chuàng)建主要助手。
# 主要助手
class ToFlightBookingAssistant(BaseModel):
"""將工作轉(zhuǎn)交給專門助手來處理航班更新和取消。"""
request: str = Field(
descriptinotallow="更新航班助手在繼續(xù)操作之前需要澄清的任何必要的后續(xù)問題。"
)
class ToBookCarRental(BaseModel):
"""將工作轉(zhuǎn)交給專門助手來處理汽車租賃預(yù)訂。"""
location: str = Field(
descriptinotallow="用戶想要租車的地點(diǎn)。"
)
start_date: str = Field(descriptinotallow="汽車租賃的開始日期。")
end_date: str = Field(descriptinotallow="汽車租賃的結(jié)束日期。")
request: str = Field(
descriptinotallow="用戶關(guān)于汽車租賃的任何額外信息或請(qǐng)求。"
)
class Config:
schema_extra = {
"example": {
"location": "巴塞爾",
"start_date": "2023-07-01",
"end_date": "2023-07-05",
"request": "我需要一輛自動(dòng)變速箱的緊湊型汽車。",
}
}
class ToHotelBookingAssistant(BaseModel):
"""將工作轉(zhuǎn)交給專門助手來處理酒店預(yù)訂。"""
location: str = Field(
descriptinotallow="用戶想要預(yù)訂酒店的地點(diǎn)。"
)
checkin_date: str = Field(descriptinotallow="酒店的入住日期。")
checkout_date: str = Field(descriptinotallow="酒店的退房日期。")
request: str = Field(
descriptinotallow="用戶關(guān)于酒店預(yù)訂的任何額外信息或請(qǐng)求。"
)
class Config:
schema_extra = {
"example": {
"location": "蘇黎世",
"checkin_date": "2023-08-15",
"checkout_date": "2023-08-20",
"request": "我更喜歡靠近市中心的酒店,有風(fēng)景的房間。",
}
}
class ToBookExcursion(BaseModel):
"""將工作轉(zhuǎn)交給專門助手來處理旅行推薦和其他旅行預(yù)訂。"""
location: str = Field(
descriptinotallow="用戶想要預(yù)訂推薦旅行的地點(diǎn)。"
)
request: str = Field(
descriptinotallow="用戶關(guān)于旅行建議的任何額外信息或請(qǐng)求。"
)
class Config:
schema_extra = {
"example": {
"location": "盧塞恩",
"request": "用戶對(duì)戶外活動(dòng)和風(fēng)景感興趣。",
}
}
# 最高級(jí)助手執(zhí)行一般問答,并將專業(yè)任務(wù)委派給其他助手。
# 任務(wù)委派是一種簡(jiǎn)單的語義路由/簡(jiǎn)單的意圖檢測(cè)
# llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=1)
primary_assistant_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"您是瑞士航空的樂于助人的客服助手。"
"您的主要角色是搜索航班信息和公司政策,以回答客戶查詢。"
"如果客戶請(qǐng)求更新或取消預(yù)訂、租車、預(yù)訂酒店或獲取旅行建議,"
"通過調(diào)用相應(yīng)的工具,將任務(wù)委派給適當(dāng)?shù)膶I(yè)助手。您自己無法進(jìn)行這些類型的更改。"
"只有專業(yè)助手才有權(quán)為用戶執(zhí)行此操作。"
"用戶不知道不同的專業(yè)助手,所以不要提及他們;只需通過功能調(diào)用來靜靜地委派。"
"向客戶提供詳細(xì)信息,并在得出信息不可用的結(jié)論之前,始終再次檢查數(shù)據(jù)庫。"
"當(dāng)搜索時(shí),要堅(jiān)持不懈。如果第一次搜索沒有結(jié)果,就擴(kuò)大您的查詢范圍。"
"\n\n當(dāng)前用戶航班信息:\n<Flights>\n{user_info}\n</Flights>"
"\n當(dāng)前時(shí)間:{time}。",
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now())
primary_assistant_tools = [
TavilySearchResults(max_results=1),
search_flights,
lookup_policy,
]
assistant_runnable = primary_assistant_prompt | llm.bind_tools(
primary_assistant_tools
+ [
ToFlightBookingAssistant,
ToBookCarRental,
ToHotelBookingAssistant,
ToBookExcursion,
]
)
創(chuàng)建助手
我們即將創(chuàng)建圖。在前一節(jié)中,我們做出了設(shè)計(jì)決策,讓所有節(jié)點(diǎn)之間共享messages?狀態(tài)。這在每個(gè)委派助手都可以看到整個(gè)用戶旅程并擁有共享上下文方面非常強(qiáng)大。然而,這意味著較弱的LLMs很容易對(duì)它們特定的范圍感到困惑。為了標(biāo)記主助手和委派工作流程之一之間的“交接”(并完成路由器的工具調(diào)用),我們將向狀態(tài)中添加一個(gè)ToolMessage。
實(shí)用工具
創(chuàng)建一個(gè)函數(shù),為每個(gè)工作流程制作一個(gè)“入口”節(jié)點(diǎn),聲明“當(dāng)前助手是 assistant_name”。
from typing import Callable
from langchain_core.messages import ToolMessage
def create_entry_node(assistant_name: str, new_dialog_state: str) -> Callable:
def entry_node(state: State) -> dict:
tool_call_id = state["messages"][-1].tool_calls[0]["id"]
return {
"messages": [
ToolMessage(
cnotallow=f"現(xiàn)在助手是 {assistant_name}。回想一下主機(jī)助手和用戶之間的上述對(duì)話。"
f" 用戶的意圖尚未得到滿足。使用提供的工具來幫助用戶。記住,你是 {assistant_name},"
" 預(yù)訂、更新、其他操作或其他動(dòng)作只有在您成功調(diào)用適當(dāng)?shù)墓ぞ吆蟛磐瓿伞?
" 如果用戶改變主意或需要其他任務(wù)的幫助,請(qǐng)調(diào)用 CompleteOrEscalate 函數(shù),讓主機(jī)助手接管控制權(quán)。"
" 不要提及你是誰 - 只作為助手的代理行事。",
tool_call_id=tool_call_id,
)
],
"dialog_state": new_dialog_state,
}
return entry_node
定義圖
現(xiàn)在是我們開始構(gòu)建圖的時(shí)候了。和以前一樣,我們將從一個(gè)節(jié)點(diǎn)開始,用用戶的當(dāng)前信息預(yù)填充狀態(tài)。
from typing import Literal
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, StateGraph
from langgraph.prebuilt import tools_condition
builder = StateGraph(State)
def user_info(state: State):
return {"user_info": fetch_user_flight_information.invoke({})}
builder.add_node("fetch_user_info", user_info)
builder.set_entry_point("fetch_user_info")
現(xiàn)在,讓我們開始構(gòu)建我們定制的工作流程。每個(gè)小工作流程的結(jié)構(gòu)都和我們?cè)诘?部分中展示的完整工作流程圖非常相似,它們都包含5個(gè)節(jié)點(diǎn):
- enter_*?: 使用你之前定義的create_entry_node工具來創(chuàng)建一個(gè)ToolMessage,這個(gè)ToolMessage表明新的專業(yè)助手已經(jīng)接管了工作。
- 助手: 這個(gè)由提示和大型語言模型(LLM)組成的模塊會(huì)根據(jù)當(dāng)前狀態(tài)來決定是使用一個(gè)工具、向用戶提問還是結(jié)束整個(gè)工作流程(返回到主助手)。
- *_safe_tools: 這些是助手可以在不需要用戶確認(rèn)的情況下使用的“只讀”工具。
- *_sensitive_tools?: 這些具有“寫入”權(quán)限的工具需要用戶的確認(rèn),并且在我們編譯工作流程圖時(shí),它們會(huì)被設(shè)置一個(gè)interrupt_before。
- leave_skill?: 通過彈出dialog_state來表示主助手重新掌握了控制權(quán)。
由于這些工作流程的相似性,我們本可以定義一個(gè)工廠函數(shù)來生成它們。但因?yàn)檫@是一個(gè)教程,我們會(huì)逐一明確地定義它們。
首先,我們來創(chuàng)建一個(gè)航班預(yù)訂助手,它專門負(fù)責(zé)管理用戶更新和取消預(yù)訂航班的流程。
# 航班預(yù)訂助手
# Flight booking assistant
builder.add_node(
"enter_update_flight",
create_entry_node("Flight Updates & Booking Assistant", "update_flight"),
)
builder.add_node("update_flight", Assistant(update_flight_runnable))
builder.add_edge("enter_update_flight", "update_flight")
builder.add_node(
"update_flight_sensitive_tools",
create_tool_node_with_fallback(update_flight_sensitive_tools),
)
builder.add_node(
"update_flight_safe_tools",
create_tool_node_with_fallback(update_flight_safe_tools),
)
def route_update_flight(
state: State,
) -> Literal[
"update_flight_sensitive_tools",
"update_flight_safe_tools",
"leave_skill",
"__end__",
]:
route = tools_condition(state)
if route == END:
return END
tool_calls = state["messages"][-1].tool_calls
did_cancel = any(tc["name"] == CompleteOrEscalate.__name__ for tc in tool_calls)
if did_cancel:
return "leave_skill"
safe_toolnames = [t.name for t in update_flight_safe_tools]
if all(tc["name"] in safe_toolnames for tc in tool_calls):
return "update_flight_safe_tools"
return "update_flight_sensitive_tools"
builder.add_edge("update_flight_sensitive_tools", "update_flight")
builder.add_edge("update_flight_safe_tools", "update_flight")
builder.add_conditional_edges("update_flight", route_update_flight)
# This node will be shared for exiting all specialized assistants
def pop_dialog_state(state: State) -> dict:
"""Pop the dialog stack and return to the main assistant.
This lets the full graph explicitly track the dialog flow and delegate control
to specific sub-graphs.
"""
messages = []
if state["messages"][-1].tool_calls:
# Note: Doesn't currently handle the edge case where the llm performs parallel tool calls
messages.append(
ToolMessage(
cnotallow="Resuming dialog with the host assistant. Please reflect on the past conversation and assist the user as needed.",
tool_call_id=state["messages"][-1].tool_calls[0]["id"],
)
)
return {
"dialog_state": "pop",
"messages": messages,
}
builder.add_node("leave_skill", pop_dialog_state)
builder.add_edge("leave_skill", "primary_assistant")
接下來,創(chuàng)建一個(gè)租車助手的工作流程圖,它將負(fù)責(zé)處理所有的租車需求。
# 租車助手
# Car rental assistant
builder.add_node(
"enter_book_car_rental",
create_entry_node("Car Rental Assistant", "book_car_rental"),
)
builder.add_node("book_car_rental", Assistant(book_car_rental_runnable))
builder.add_edge("enter_book_car_rental", "book_car_rental")
builder.add_node(
"book_car_rental_safe_tools",
create_tool_node_with_fallback(book_car_rental_safe_tools),
)
builder.add_node(
"book_car_rental_sensitive_tools",
create_tool_node_with_fallback(book_car_rental_sensitive_tools),
)
def route_book_car_rental(
state: State,
) -> Literal[
"book_car_rental_safe_tools",
"book_car_rental_sensitive_tools",
"leave_skill",
"__end__",
]:
route = tools_condition(state)
if route == END:
return END
tool_calls = state["messages"][-1].tool_calls
did_cancel = any(tc["name"] == CompleteOrEscalate.__name__ for tc in tool_calls)
if did_cancel:
return "leave_skill"
safe_toolnames = [t.name for t in book_car_rental_safe_tools]
if all(tc["name"] in safe_toolnames for tc in tool_calls):
return "book_car_rental_safe_tools"
return "book_car_rental_sensitive_tools"
builder.add_edge("book_car_rental_sensitive_tools", "book_car_rental")
builder.add_edge("book_car_rental_safe_tools", "book_car_rental")
builder.add_conditional_edges("book_car_rental", route_book_car_rental)
然后,創(chuàng)建一個(gè)酒店預(yù)訂的工作流程。
# 酒店預(yù)訂助手
# Hotel booking assistant
builder.add_node(
"enter_book_hotel", create_entry_node("Hotel Booking Assistant", "book_hotel")
)
builder.add_node("book_hotel", Assistant(book_hotel_runnable))
builder.add_edge("enter_book_hotel", "book_hotel")
builder.add_node(
"book_hotel_safe_tools",
create_tool_node_with_fallback(book_hotel_safe_tools),
)
builder.add_node(
"book_hotel_sensitive_tools",
create_tool_node_with_fallback(book_hotel_sensitive_tools),
)
def route_book_hotel(
state: State,
) -> Literal[
"leave_skill", "book_hotel_safe_tools", "book_hotel_sensitive_tools", "__end__"
]:
route = tools_condition(state)
if route == END:
return END
tool_calls = state["messages"][-1].tool_calls
did_cancel = any(tc["name"] == CompleteOrEscalate.__name__ for tc in tool_calls)
if did_cancel:
return "leave_skill"
tool_names = [t.name for t in book_hotel_safe_tools]
if all(tc["name"] in tool_names for tc in tool_calls):
return "book_hotel_safe_tools"
return "book_hotel_sensitive_tools"
builder.add_edge("book_hotel_sensitive_tools", "book_hotel")
builder.add_edge("book_hotel_safe_tools", "book_hotel")
builder.add_conditional_edges("book_hotel", route_book_hotel)
之后,定義一個(gè)旅行預(yù)訂助手。
# 旅行預(yù)訂助手
# Excursion assistant
builder.add_node(
"enter_book_excursion",
create_entry_node("Trip Recommendation Assistant", "book_excursion"),
)
builder.add_node("book_excursion", Assistant(book_excursion_runnable))
builder.add_edge("enter_book_excursion", "book_excursion")
builder.add_node(
"book_excursion_safe_tools",
create_tool_node_with_fallback(book_excursion_safe_tools),
)
builder.add_node(
"book_excursion_sensitive_tools",
create_tool_node_with_fallback(book_excursion_sensitive_tools),
)
def route_book_excursion(
state: State,
) -> Literal[
"book_excursion_safe_tools",
"book_excursion_sensitive_tools",
"leave_skill",
"__end__",
]:
route = tools_condition(state)
if route == END:
return END
tool_calls = state["messages"][-1].tool_calls
did_cancel = any(tc["name"] == CompleteOrEscalate.__name__ for tc in tool_calls)
if did_cancel:
return "leave_skill"
tool_names = [t.name for t in book_excursion_safe_tools]
if all(tc["name"] in tool_names for tc in tool_calls):
return "book_excursion_safe_tools"
return "book_excursion_sensitive_tools"
builder.add_edge("book_excursion_sensitive_tools", "book_excursion")
builder.add_edge("book_excursion_safe_tools", "book_excursion")
builder.add_conditional_edges("book_excursion", route_book_excursion)
最后,創(chuàng)建一個(gè)主助手。
# Primary assistant
builder.add_node("primary_assistant", Assistant(assistant_runnable))
builder.add_node(
"primary_assistant_tools", create_tool_node_with_fallback(primary_assistant_tools)
)
def route_primary_assistant(
state: State,
) -> Literal[
"primary_assistant_tools",
"enter_update_flight",
"enter_book_hotel",
"enter_book_excursion",
"__end__",
]:
route = tools_condition(state)
if route == END:
return END
tool_calls = state["messages"][-1].tool_calls
if tool_calls:
if tool_calls[0]["name"] == ToFlightBookingAssistant.__name__:
return "enter_update_flight"
elif tool_calls[0]["name"] == ToBookCarRental.__name__:
return "enter_book_car_rental"
elif tool_calls[0]["name"] == ToHotelBookingAssistant.__name__:
return "enter_book_hotel"
elif tool_calls[0]["name"] == ToBookExcursion.__name__:
return "enter_book_excursion"
return "primary_assistant_tools"
raise ValueError("Invalid route")
# The assistant can route to one of the delegated assistants,
# directly use a tool, or directly respond to the user
builder.add_conditional_edges(
"primary_assistant",
route_primary_assistant,
{
"enter_update_flight": "enter_update_flight",
"enter_book_car_rental": "enter_book_car_rental",
"enter_book_hotel": "enter_book_hotel",
"enter_book_excursion": "enter_book_excursion",
"primary_assistant_tools": "primary_assistant_tools",
END: END,
},
)
builder.add_edge("primary_assistant_tools", "primary_assistant")
# Each delegated workflow can directly respond to the user
# When the user responds, we want to return to the currently active workflow
def route_to_workflow(
state: State,
) -> Literal[
"primary_assistant",
"update_flight",
"book_car_rental",
"book_hotel",
"book_excursion",
]:
"""If we are in a delegated state, route directly to the appropriate assistant."""
dialog_state = state.get("dialog_state")
if not dialog_state:
return "primary_assistant"
return dialog_state[-1]
builder.add_conditional_edges("fetch_user_info", route_to_workflow)
# Compile graph
memory = SqliteSaver.from_conn_string(":memory:")
part_4_graph = builder.compile(
checkpointer=memory,
# Let the user approve or deny the use of sensitive tools
interrupt_before=[
"update_flight_sensitive_tools",
"book_car_rental_sensitive_tools",
"book_hotel_sensitive_tools",
"book_excursion_sensitive_tools",
],
)
這里是一個(gè)圖片鏈接
對(duì)話
那真是很多內(nèi)容!讓我們?cè)谙旅娴膶?duì)話輪次列表上運(yùn)行它。這次,我們將有更少的確認(rèn)。
import shutil
import uuid
# Update with the backup file so we can restart from the original place in each section
shutil.copy(backup_file, db)
thread_id = str(uuid.uuid4())
config = {
"configurable": {
# The passenger_id is used in our flight tools to
# fetch the user's flight information
"passenger_id": "3442 587242",
# Checkpoints are accessed by thread_id
"thread_id": thread_id,
}
}
_printed = set()
# We can reuse the tutorial questions from part 1 to see how it does.
for question in tutorial_questions:
events = part_4_graph.stream(
{"messages": ("user", question)}, config, stream_mode="values"
)
for event in events:
_print_event(event, _printed)
snapshot = part_4_graph.get_state(config)
while snapshot.next:
# We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it
# Note: This code is all outside of your graph. Typically, you would stream the output to a UI.
# Then, you would have the frontend trigger a new run via an API call when the user has provided input.
user_input = input(
"Do you approve of the above actions? Type 'y' to continue;"
" otherwise, explain your requested changed.\n\n"
)
if user_input.strip() == "y":
# Just continue
result = part_4_graph.invoke(
None,
config,
)
else:
# Satisfy the tool invocation by
# providing instructions on the requested changes / change of mind
result = part_4_graph.invoke(
{
"messages": [
ToolMessage(
tool_call_id=event["messages"][-1].tool_calls[0]["id"],
cnotallow=f"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.",
)
]
},
config,
)
snapshot = part_4_graph.get_state(config)
結(jié)論
您現(xiàn)在開發(fā)了一個(gè)能夠處理多種任務(wù)的客戶支持機(jī)器人,它使用了專注的工作流程。更重要的是,您已經(jīng)學(xué)會(huì)了如何使用LangGraph的核心功能來設(shè)計(jì)和根據(jù)產(chǎn)品需求重構(gòu)應(yīng)用程序。
上述示例并不是針對(duì)您的特定需求進(jìn)行優(yōu)化的 - 大型語言模型(LLMs)可能會(huì)出錯(cuò),每個(gè)流程都可以通過更好的提示和實(shí)驗(yàn)來提高可靠性。一旦您創(chuàng)建了初始支持機(jī)器人,下一步就是開始添加評(píng)估,這樣您就可以自信地改進(jìn)您的系統(tǒng)。
本文轉(zhuǎn)載自?? AI小智??,作者: AI小智
