探索LangGraph:如何创建一个既智能又可控的航空客服AI

人工智能与算法增长营销弹性计算

❝ 上节课,我们将利用LangGraphinterrupt_before功能,在执行任何工具之前,暂停流程并把控制权交还给用户。没看过的同学可以点击链接LangGraph实战:可控的AI航空客服助手查阅。在本章节中,我们通过将工具分为只读(安全)和修改数据(敏感)两类,来优化我们的中断机制。我们仅对敏感工具实施中断,使得机器人能够自主处理一些简单的查询。

这种设计既保持了用户控制权,又确保了对话流程的顺畅。但随着工具数量的增加,单一的图结构可能会变得过于复杂。我们将在下一节中解决这个问题。

第三部分的图将类似于下面的示意图:

picture.image 第三部分示意图

状态定义

首先,定义图的状态。我们的状态和LLM调用与第二部分相同


        
          
from typing import Annotated  
  
from langchain_anthropic import ChatAnthropic  
from langchain_community.tools.tavily_search import TavilySearchResults  
from langchain_core.prompts import ChatPromptTemplate  
from langchain_core.runnables import Runnable, RunnableConfig  
from typing_extensions import TypedDict  
  
from langgraph.graph.message import AnyMessage, add_messages  
  
  
class State(TypedDict):  
    messages: Annotated[list[AnyMessage], add_messages]  
    user_info: str  
  
  
class Assistant:  
    def \_\_init\_\_(self, runnable: Runnable):  
        self.runnable = runnable  
  
    def \_\_call\_\_(self, state: State, config: RunnableConfig):  
        while True:  
            result = self.runnable.invoke(state)  
            # 如果LLM碰巧返回了一个空响应,我们将重新提示它  
            # 以获得一个实际的响应。  
            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}  
                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)  
# 你可以更新LLMs,尽管你可能需要更新提示  
# from langchain\_openai import ChatOpenAI  
  
# llm = ChatOpenAI(model="gpt-4-turbo-preview")  
  
assistant提示 = ChatPromptTemplate.from_messages(  
    [  
        (  
            "system",  
            "你是一位乐于助人的瑞士航空客户支持助手。"  
            " 使用提供的工具搜索航班、公司政策和其他信息以协助用户的查询。"  
            " 当搜索时,要坚持不懈。如果第一次搜索没有结果,扩大你的查询范围。"  
            " 如果搜索空手而归,请在放弃之前扩大你的搜索。"  
            "\n\n当前用户:\n<User>\n{user\_info}\n</User>"  
            "\n当前时间:{time}。",  
        ),  
        ("placeholder", "{messages}"),  
    ]  
).partial(time=datetime.now())  
  
  
# "阅读"仅工具(例如检索器)不需要用户确认即可使用  
part_3_safe_tools = [  
    TavilySearchResults(max_results=1),  
    fetch_user_flight_information,  
    search_flights,  
    lookup_policy,  
    search_car_rentals,  
    search_hotels,  
    search_trip_recommendations,  
]  
  
# 这些工具都改变了用户的预订。  
# 用户有权控制做出什么决定  
part_3_sensitive_tools = [  
    update_ticket_to_new_flight,  
    cancel_ticket,  
    book_car_rental,  
    update_car_rental,  
    cancel_car_rental,  
    book_hotel,  
    update_hotel,  
    cancel_hotel,  
    book_excursion,  
    update_excursion,  
    cancel_excursion,  
]  
sensitive_tool_names = {t.name for t in part_3_sensitive_tools}  
# 我们的LLM不需要知道它必须路由到哪个节点。在它的"思维"中,它只是在调用函数。  
part_3_assistant_runnable = assistant_prompt | llm.bind_tools(  
    part_3_safe_tools + part_3_sensitive_tools  
)  

      

图的定义

现在,创建图。我们的图与第二部分几乎相同 ,只是我们将工具分为了两个不同的节点。我们只在实际更改用户预订的工具之前进行中断。


        
          
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({})}  
  
  
# 新增:fetch\_user\_info节点首先运行,这意味着我们的助手可以在  
# 不需要采取行动的情况下看到用户的航班信息  
builder.add_node("fetch\_user\_info", user_info)  
builder.set_entry_point("fetch\_user\_info")  
builder.add_node("assistant", Assistant(part_3_assistant_runnable))  
builder.add_node("safe\_tools", create_tool_node_with_fallback(part_3_safe_tools))  
builder.add_node(  
    "sensitive\_tools", create_tool_node_with_fallback(part_3_sensitive_tools)  
)  
# 定义逻辑  
builder.add_edge("fetch\_user\_info", "assistant")  
  
  
def route\_tools(state: State) -> Literal["safe\_tools", "sensitive\_tools", "\_\_end\_\_"]:  
    next_node = tools_condition(state)  
    # 如果没有调用工具,返回用户  
    if next_node == END:  
        return END  
    ai_message = state["messages"][-1]  
    # 这假设是单个工具调用。要处理并行工具调用,你将想要  
    # 使用ANY条件  
    first_tool_call = ai_message.tool_calls[0]  
    if first_tool_call["name"] in sensitive_tool_names:  
        return "sensitive\_tools"  
    return "safe\_tools"  
  
  
builder.add_conditional_edges(  
    "assistant",  
    route_tools,  
)  
builder.add_edge("safe\_tools", "assistant")  
builder.add_edge("sensitive\_tools", "assistant")  
  
memory = SqliteSaver.from_conn_string(":memory:")  
part_3_graph = builder.compile(  
    checkpointer=memory,  
    # 新增:图将在执行"tools"节点之前始终停止。  
    # 用户可以在助手继续之前批准或拒绝(甚至更改请求)  
    interrupt_before=["sensitive\_tools"],  
)  

      

picture.image

示例对话

接下来,让我们尝试新修订的聊天机器人!我们将在以下对话列表上运行它。这次,我们将减少确认的次数。


        
          
import shutil  
import uuid  
  
# 使用备份文件更新,以便我们可以从每个部分的原始位置重新启动  
shutil.copy(backup_file, db)  
thread_id = str(uuid.uuid4())  
  
config = {  
    "configurable": {  
        # passenger\_id在我们的航班工具中使用  
        # 以获取用户的航班信息  
        "passenger\_id": "3442 587242",  
        # 通过thread\_id访问检查点  
        "thread\_id": thread_id,  
    }  
}  
  
tutorial_questions = [  
    "嗨,我的航班是什么时候?",  
    "我可以更新我的航班到更早的时间吗?我想今天晚些时候离开。",  
    "那就更新到下周的某个时间",  
    "下一个可用的选项很好",  
    "关于住宿和交通呢?",  
    "是的,我想要一个负担得起的酒店,用于我为期一周的住宿(7天)。我还想要租一辆车。",  
    "好的,可以为你推荐的酒店预订吗?听起来不错。",  
    "是的,继续预订任何中等费用且有可用性的酒店。",  
    "现在对于汽车,我的选择是什么?",  
    "太棒了,我们只需要最便宜的选项。继续预订7天",  
    "好的,现在你对旅行有什么建议?",  
    "在我在那里的时候,它们可用吗?",  
    "有趣 - 我喜欢博物馆,有什么选择?",  
    "好的,选一个并在我到达的第二天为我预订。",  
]  
  
  
_printed = set()  
# 我们可以重用第一部分的教程问题,看看它的表现如何。  
for question in tutorial_questions:  
    events = part_3_graph.stream(  
        {"messages": ("user", question)}, config, stream_mode="values"  
    )  
    for event in events:  
        _print_event(event, _printed)  
    snapshot = part_3_graph.get_state(config)  
    while snapshot.next:  
        # 我们有中断!代理试图使用工具,用户可以批准或拒绝它  
        # 注意:此代码都在图之外。通常,你会将输出流到UI。  
        # 然后,当用户提供输入时,你会通过API调用触发新的运行。  
        user_input = input(  
            "你是否批准上述操作?输入'y'继续;"  
            " 否则,请说明你请求的更改。\n\n"  
        )  
        if user_input.strip() == "y":  
            # 只是继续  
            result = part_3_graph.invoke(  
                None,  
                config,  
            )  
        else:  
            # 通过提供有关请求更改/改变主意的说明  
            # 满足工具调用  
            result = part_3_graph.invoke(  
                {  
                    "messages": [  
                        ToolMessage(  
                            tool_call_id=event["messages"][-1].tool_calls[0]["id"],  
                            content=f"API调用被用户拒绝。理由:'{user\_input}'。继续协助,考虑用户的输入。",  
                        )  
                    ]  
                },  
                config,  
            )  
        snapshot = part_3_graph.get_state(config)  

      

第三部分回顾

现在,我们的聊天机器人工作得很好,你可以通过LangSmith跟踪来检查它的最新运行情况。这个设计可能已经满足了你的需求。代码是封闭的,并且它的行为符合预期。

picture.image

然而,这个设计的一个潜在问题是,它对单个提示施加了很大压力。如果我们想要添加更多工具,或者每个工具变得更加复杂,那么机器人使用工具的效率和整体行为可能会受到影响。

在下一节中,我们将展示如何通过根据用户的意图将用户引导至专业代理或子图,来更精确地控制不同的用户体验。

今天的内容就到这里,如果老铁觉得还行,可以来一波三连,感谢!

PS

AI小智技术交流群(技术交流、摸鱼、白嫖课程为主)又不定时开放了,感兴趣的朋友,可以在下方公号内回复:666,即可进入。

老规矩

,道友们还记得么,

右下角的 “在看” 点一下

, 如果感觉文章内容不错的话,记得分享朋友圈让更多的人知道!

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动 NoSQL 的实践与探索
随着 NoSQL 的蓬勃发展越来越多的数据存储在了 NoSQL 系统中,并且 NoSQL 和 RDBMS 的界限越来越模糊,各种不同的专用 NoSQL 系统不停涌现,各具特色,形态不一。本次主要分享字节跳动内部和火山引擎 NoSQL 的实践,希望能够给大家一定的启发。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论