构建自定义AI客户支持助手——专用工作流(Specialized Workflows)

40 阅读18分钟

在前面两节中,我们已经看到,基于单个 Prompt 和单个大模型(LLM)的“通用型”聊天机器人,虽然能处理各种用户意图,但这种方式很难为特定需求提供稳定且可控的优质体验。因此,在这一节,我们将采用另一种策略:让系统能够自动识别用户意图,并选择合适的“专用工作流(workflow)”或“技能(skill)”来满足用户需求。

每一个工作流都聚焦在自己的领域内运行,互相独立地改进,而不会影响到整个助理系统的表现。我们将用户体验划分为多个子图(sub-graphs),整体结构如下:

图片

编辑

主助理(Primary Assistant)负责接收用户最初的请求,根据查询内容,将任务路由给合适的“专用助理(Expert Assistant)”。

状态管理

我们需要跟踪当前哪个子图正在控制。虽然可以通过消息列表的某些运算来实现,但使用专门的栈来跟踪会更简单。

在下面的 State 中添加一个 dialog_state 列表。每当节点运行并返回 dialog_state 值时,就会调用 update_dialog_stack 函数来决定如何应用更新。from typing import Annotated, Literal, Optional

from typing_extensions import TypedDictfrom langgraph.graph.message import AnyMessage, add_messages

def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:    """推入或弹出状态。"""    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,    ]

创建助手

这次我们将为每个工作流创建一个助手。这意味着需要:

  • 航班预订助手
  • 酒店预订助手
  • 租车助手
  • 旅游项目助手
  • 最后还有一个"主助手"来在这些助手之间路由

如果你注意观察,可能会发现这是我们多智能体章节中监督者设计模式的一个例子。

下面定义驱动每个助手的 Runnable 对象。每个 Runnable 都有一个提示词、大语言模型和该助手范围内的工具模式。每个专业/委托助手还可以调用 CompleteOrEscalate 工具来表示应该将控制流传回主助手。这在它成功完成工作或用户改变主意或需要超出该特定工作流范围的帮助时发生。from langchain_anthropic import ChatAnthropic

from langchain_community.tools.tavily_search import TavilySearchResultsfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_core.runnables import Runnable, RunnableConfigfrom pydantic import BaseModel, Field

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""请给出真实的输出。")]                state = {**state, "messages": messages}            else:                break        return {"messages": result}

##它是一个 Pydantic 模型类(BaseModel),##在 LangGraph / LangChain 体系中,这样的类就表示一个“工具(Tool)”。##换句话说:模型(LLM)在推理时,如果决定“我该结束任务”或“我无法处理”,它就会调用这个工具,向主助理发出“切换控制权”的信号。class CompleteOrEscalate(BaseModel):    """用于标记当前任务完成和/或将对话控制权上报给主助手的工具,    主助手可以根据用户需求重新路由对话。"""
    cancel: bool = True    reason: str
    class Config:        json_schema_extra = {            "example": {                "cancel"True,                "reason""用户改变了对当前任务的想法。",            },            "example 2": {                "cancel"True,                "reason""我已经完全完成了任务。",            },            "example 3": {                "cancel"False,                "reason""我需要搜索用户的邮件或日历以获取更多信息。",            },        }

航班预订助手

定义一个“航班更新助理”的系统提示模板,告诉模型它的角色、任务范围、行为规范,并动态插入用户航班信息和当前时间,让它能在多助理系统中独立运行、自动接管特定任务。

flight_booking_prompt = ChatPromptTemplate.from_messages(    [        (            "system",            "你是处理航班更新的专业助手。"            " 主助手在用户需要帮助更新预订时将工作委托给你。"            "与客户确认更新的航班详情并告知任何额外费用。"            " 搜索时要坚持。如果首次搜索没有结果,扩大查询范围。"            "如果你需要更多信息或客户改变主意,将任务上报回主助手。"            " 记住,只有在成功使用相关工具后,预订才算完成。"            "\n\n当前用户航班信息:\n<Flights>\n{user_info}\n</Flights>"            "\n当前时间: {time}。"            "\n\n如果用户需要帮助,但你的工具都不适用,"            ' 那么"CompleteOrEscalate"对话到主助手不要浪费用户时间不要编造无效的工具或函数',        ),        ("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_toolsupdate_flight_runnable = flight_booking_prompt | llm.bind_tools(    update_flight_tools + [CompleteOrEscalate])

酒店预订助手

book_hotel_prompt = ChatPromptTemplate.from_messages(    [        (            "system",            "你是处理酒店预订的专业助手。"            "主助手在用户需要帮助预订酒店时将工作委托给你。"            "根据用户偏好搜索可用酒店并与客户确认预订详情。"            " 搜索时要坚持。如果首次搜索没有结果,扩大查询范围。"            "如果你需要更多信息或客户改变主意,将任务上报回主助手。"            " 记住,只有在成功使用相关工具后,预订才算完成。"            "\n当前时间: {time}。"            '\n\n如果用户需要帮助,但你的工具都不适用,那么"CompleteOrEscalate"对话到主助手'            " 不要浪费用户时间。不要编造无效的工具或函数。"            "\n\n一些你应该CompleteOrEscalate的例子:\n"            " - '这个时节天气怎么样?'\n"            " - '算了我想我会分开预订'\n"            " - '我需要弄清楚在那里的交通'\n"            " - '哦等等我还没订机票我先订机票'\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_toolsbook_hotel_runnable = book_hotel_prompt | llm.bind_tools(    book_hotel_tools + [CompleteOrEscalate])

租车助手

book_car_rental_prompt = ChatPromptTemplate.from_messages(    [        (            "system",            "你是处理租车预订的专业助手。"            "主助手在用户需要帮助预订租车时将工作委托给你。"            "根据用户偏好搜索可用租车并与客户确认预订详情。"            " 搜索时要坚持。如果首次搜索没有结果,扩大查询范围。"            "如果你需要更多信息或客户改变主意,将任务上报回主助手。"            " 记住,只有在成功使用相关工具后,预订才算完成。"            "\n当前时间: {time}。"            "\n\n如果用户需要帮助,但你的工具都不适用,"            '"CompleteOrEscalate"对话到主助手不要浪费用户时间不要编造无效的工具或函数'            "\n\n一些你应该CompleteOrEscalate的例子:\n"            " - '这个时节天气怎么样?'\n"            " - '有什么航班可用?'\n"            " - '算了我想我会分开预订'\n"            " - '哦等等我还没订机票我先订机票'\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_toolsbook_car_rental_runnable = book_car_rental_prompt | llm.bind_tools(    book_car_rental_tools + [CompleteOrEscalate])

旅游项目助手

​​​​​​​

book_excursion_prompt = ChatPromptTemplate.from_messages(    [        (            "system",            "你是处理旅行推荐的专业助手。"            "主助手在用户需要帮助预订推荐旅行时将工作委托给你。"            "根据用户偏好搜索可用的旅行推荐并与客户确认预订详情。"            "如果你需要更多信息或客户改变主意,将任务上报回主助手。"            " 搜索时要坚持。如果首次搜索没有结果,扩大查询范围。"            " 记住,只有在成功使用相关工具后,预订才算完成。"            "\n当前时间: {time}。"            '\n\n如果用户需要帮助,但你的工具都不适用,那么"CompleteOrEscalate"对话到主助手不要浪费用户时间不要编造无效的工具或函数'            "\n\n一些你应该CompleteOrEscalate的例子:\n"            " - '算了我想我会分开预订'\n"            " - '我需要弄清楚在那里的交通'\n"            " - '哦等等我还没订机票我先订机票'\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_toolsbook_excursion_runnable = book_excursion_prompt | llm.bind_tools(    book_excursion_tools + [CompleteOrEscalate])

主助手
#“我可以调用一个名为 ToFlightBookingAssistant 的工具,并传入 request 参数来把任务交给航班助手。class ToFlightBookingAssistant(BaseModel):    """将工作转移到专业助手以处理航班更新和取消。"""
    request: str = Field(        description="航班更新助手在继续之前需要澄清的任何必要的后续问题。"    )

class ToBookCarRental(BaseModel):    """将工作转移到专业助手以处理租车预订。"""
    location: str = Field(        description="用户想要租车的地点。"    )    start_date: str = Field(description="租车的开始日期。")    end_date: str = Field(description="租车的结束日期。")    request: str = Field(        description="用户关于租车的任何额外信息或要求。"    )
    class Config:        json_schema_extra = {            "example": {                "location""巴塞尔",                "start_date""2023-07-01",                "end_date""2023-07-05",                "request""我需要一辆带自动变速器的紧凑型车。",            }        }

class ToHotelBookingAssistant(BaseModel):    """将工作转移到专业助手以处理酒店预订。"""
    location: str = Field(        description="用户想要预订酒店的地点。"    )    checkin_date: str = Field(description="酒店的入住日期。")    checkout_date: str = Field(description="酒店的退房日期。")    request: str = Field(        description="用户关于酒店预订的任何额外信息或要求。"    )
    class Config:        json_schema_extra = {            "example": {                "location""苏黎世",                "checkin_date""2023-08-15",                "checkout_date""2023-08-20",                "request""我更喜欢靠近市中心的酒店,房间要有景观。",            }        }

class ToBookExcursion(BaseModel):    """将工作转移到专业助手以处理旅行推荐和其他旅游项目预订。"""
    location: str = Field(        description="用户想要预订推荐旅行的地点。"    )    request: str = Field(        description="用户关于旅行推荐的任何额外信息或要求。"    )
    class Config:        json_schema_extra = {            "example": {                "location""卢塞恩",                "request""用户对户外活动和风景优美的景色感兴趣。",            }        }

# 顶层助手执行一般问答并将专业任务委托给其他助手# 任务委托是语义路由的简单形式 / 进行简单的意图检测llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=1)
primary_assistant_prompt = ChatPromptTemplate.from_messages(    [        (           ("placeholder""{messages}"),    ]         "system",            "你是瑞士航空公司的有用客户支持助手。"            "你的主要职责是搜索航班信息和公司政策以回答客户查询。"            "如果客户要求更新或取消航班、预订租车、预订酒店或获取旅行推荐,"            "通过调用相应工具将任务委托给适当的专业助手。你自己无法进行这些类型的更改。"            " 只有专业助手被授权为用户执行此操作。"            "用户不知道不同的专业助手,所以不要提及它们;只需通过函数调用悄悄委托。"            "向客户提供详细信息,并在得出信息不可用的结论之前始终再次检查数据库。"            " 搜索时要坚持。如果首次搜索没有结果,扩大查询范围。"            " 如果搜索结果为空,在放弃之前扩大搜索范围。"            "\n\n当前用户航班信息:\n<Flights>\n{user_info}\n</Flights>"            "\n当前时间: {time}。",        ),).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,    ])

创建入口节点

我们即将创建图谱。在前面的章节中,我们做出了设计决策,让所有节点之间共享消息状态。这很强大,因为每个委托助手都可以看到整个用户旅程并拥有共享上下文。但是,这意味着较弱的大语言模型很容易对其特定范围感到困惑。为了标记主助手和委托工作流之间的"交接"(并完成路由器的工具调用),我们将向状态添加一个 ToolMessage。

工具函数

创建一个函数为每个工作流创建一个"入口"节点,说明"当前助手是 assistant_name"。

from typing import Callablefrom 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(                    content=f"助手现在是{assistant_name}。回顾主助手和用户之间的上述对话。"                    f" 用户的意图未得到满足。使用提供的工具来帮助用户。记住,你是{assistant_name},"                    " 只有在你成功调用适当的工具之后,预订、更新或其他操作才算完成。"                    " 如果用户改变主意或需要其他任务的帮助,调用CompleteOrEscalate函数让主助手接管控制。"                    " 不要提及你是谁 - 只需作为助手的代理。",                    tool_call_id=tool_call_id,                )            ],            "dialog_state": new_dialog_state,        }
    return entry_node

定义图谱

那现在我们开始构建图谱了。和以前一样,我们将从一个节点开始,用用户当前信息预填充状态。

from typing import Literalfrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.graph import StateGraphfrom 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.add_edge(START, "fetch_user_info")

现在我们将开始添加专业化工作流。每个小工作流看起来与第3部分的完整图谱非常相似,使用5个节点:

enter_*: 使用上面定义的 create_entry_node 工具添加 ToolMessage,表示新的专业助手正在掌舵。

Assistant: 提示词 + LLM 组合,接收当前状态并使用工具、向用户提问或结束工作流(返回主助手)。

  • _safe_tools: 助手可以使用的"只读"工具,无需用户确认。

  • _sensitive_tools: 具有"写"访问权限的工具,需要用户确认(编译图谱时将分配 interrupt_before)。

leave_skill: 弹出 dialog_state 以表示主助手重新控制。

由于它们的相似性,我们可以定义一个工厂函数来生成这些。因为这是教程,我们将明确定义每一个。

航班预订助手图谱

首先,创建专门管理更新和取消航班用户旅程的航班预订助手。

# 航班预订助手builder.add_node(    "enter_update_flight",    create_entry_node("航班更新和预订助手", "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):    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,    ["update_flight_sensitive_tools", "update_flight_safe_tools", "leave_skill", END],)

# 此节点将被所有专业助手共享用于退出def pop_dialog_state(state: State) -> dict:    """弹出对话栈并返回主助手。
    这让完整图谱明确跟踪对话流程并将控制委托给特定子图。    """    messages = []    if state["messages"][-1].tool_calls:        # 注意:当前不处理LLM执行并行工具调用的边缘情况        messages.append(            ToolMessage(                content="恢复与主助手的对话。请回顾过去的对话并根据需要帮助用户。",                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")

租车助手图谱

接下来,创建租车助手图谱以满足所有租车需求。

# 租车助手builder.add_node(    "enter_book_car_rental",    create_entry_node("租车助手", "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):    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,    [        "book_car_rental_safe_tools",        "book_car_rental_sensitive_tools",        "leave_skill",        END,    ],)

酒店预订工作流

然后定义酒店预订工作流。

# 酒店预订助手builder.add_node(    "enter_book_hotel", create_entry_node("酒店预订助手", "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):    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,    ["leave_skill", "book_hotel_safe_tools", "book_hotel_sensitive_tools", END],)

旅游项目助手

之后,定义旅游项目助手。

旅游项目助手

builder.add_node(    "enter_book_excursion",    create_entry_node("旅行推荐助手", "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):    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,    ["book_excursion_safe_tools", "book_excursion_sensitive_tools", "leave_skill", END],)

主助手图谱

最后,创建主助手。

# 主助手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):    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("无效的路由")

# 助手可以路由到委托助手之一,# 直接使用工具,或直接响应用户builder.add_conditional_edges(    "primary_assistant",    route_primary_assistant,    [        "enter_update_flight",        "enter_book_car_rental",        "enter_book_hotel",        "enter_book_excursion",        "primary_assistant_tools",        END,    ],)builder.add_edge("primary_assistant_tools""primary_assistant")

# 每个委托工作流都可以直接响应用户# 当用户响应时,我们希望返回到当前活动的工作流def route_to_workflow(    state: State,) -> Literal[    "primary_assistant",    "update_flight",    "book_car_rental",    "book_hotel",    "book_excursion",]:    """如果我们处于委托状态,直接路由到相应的助手。"""    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)
# 编译图谱memory = InMemorySaver()part_4_graph = builder.compile(    checkpointer=memory,    # 让用户批准或拒绝使用敏感工具    interrupt_before=[        "update_flight_sensitive_tools",        "book_car_rental_sensitive_tools",        "book_hotel_sensitive_tools",        "book_excursion_sensitive_tools",    ],)

可视化图谱

from IPython.display import Image, display

try:    display(Image(part_4_graph.get_graph(xray=True).draw_mermaid_png()))except Exception:    # 这需要一些额外的依赖项,是可选的    pass

图片

编辑

运行对话

完成了!让我们在以下对话轮次列表上运行它。这次,我们需要的确认会少得多。

import shutilimport uuid
# 使用备份文件更新,这样我们可以在每个部分从原始位置重新开始db = update_dates(db)thread_id = str(uuid.uuid4())
config = {    "configurable": {        # passenger_id 用于我们的航班工具        # 获取用户的航班信息        "passenger_id""3442 587242",        # 检查点通过 thread_id 访问        "thread_id": thread_id,    }}
_printed = set()# 我们可以重用第1部分的教程问题来看看它的表现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:        # 我们遇到了中断!代理正在尝试使用工具,用户可以批准或拒绝它        # 注意:此代码都在图谱外部。通常,你会将输出流式传输到UI。        # 然后,当用户提供输入时,你会通过API调用触发新的运行。        try:            user_input = input(                "你是否批准上述操作?输入 'y' 继续;"                " 否则,解释你请求的更改。\n\n"            )        except:            user_input = "y"        if user_input.strip() == "y":            # 只是继续            result = part_4_graph.invoke(                None,                config,            )        else:            # 通过提供有关请求更改/改变主意的说明来满足工具调用            result = part_4_graph.invoke(                {                    "messages": [                        ToolMessage(                            tool_call_id=event["messages"][-1].tool_calls[0]["id"],                            content=f"API调用被用户拒绝。原因:'{user_input}'。继续提供帮助,考虑用户的输入。",                        )                    ]                },                config,            )        snapshot = part_4_graph.get_state(config)

这里我们来梳理下它的工作原理,

1. 使用 dialog_state 列表跟踪当前活动的助手,当进入专业助手时,将其名称推入栈,当完成或上报时,从栈中弹出,这使得系统知道应该将用户的下一个消息路由到哪里。

2. 入口节点(create_entry_node): 添加 ToolMessage 告诉专业助手它现在负责处理,退出节点 (pop_dialog_state): 将控制权返回主助手并清理状态。

3. 每个专业助手有两类工具:安全工具(safe_tools): 只读操作,如搜索,无需用户批准,敏感工具(sensitive_tools): 写操作,如预订、更新、取消,需要用户确认

4. 主助手: 检测意图并路由到相应的专业助手,专业助手: 在安全工具、敏感工具、上报(CompleteOrEscalate)和结束之间路由,用户输入: 根据 dialog_state 路由回当前活动的工作流。

5. 通过 interrupt_before 在执行敏感操作前暂停,等待用户批准:用户输入 'y' → 继续执行,用户提供其他输入 → 创建包含用户反馈的 ToolMessage,让助手调整。

总结

通过将单一的"万能"助手拆分为专业化的子工作流,我们创建了一个更强大、更可维护的系统。主助手充当智能路由器,根据用户意图将任务委托给最合适的专家。每个专家都有自己的提示词、工具集和工作流程,可以提供更加精准和高质量的用户体验。

这种模式特别适合复杂的多领域应用,如客户服务、旅行预订、电商等场景,其中不同类型的用户请求需要不同的专业知识和处理流程。