【LangGraph】真正的自定义状态,我们提供建议,agent采纳

385 阅读5分钟

【LangGraph】强大的功能人类和agent的交互(下)

前面我们通过中断和状态更新实现对 agent 的控制,模拟人机交互过程,到现在为止,我们依赖的是 LanGraph 一个简单的状态(当然它只是一个消息列表messages!)。我们可以使用这个简单的状态做很多事情,如果我们想在不依赖消息列表的情况下去定义复杂的行为,则可以向状态添加其他字段。我现在使用新的节点来扩展我们的 agent

一、创建人类节点

在之前我们的例子中,每当调用工具时,图表总是会中断。我们现在希望我们的 agent 可以选择性依赖人类。 为了实现这一步,我们要创建一个类人类的节点,graph 将始终运行到此节点之前停止。只有当 LLM 调用“人类”工具时,我们才会执行此节点。我们将在图表状态中创建一个 ask_human 标志,如果 LLM 调用此工具,我们将修改这个标志。

from dotenv import load_dotenv
load_dotenv()
from typing import Annotated
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from pydantic import BaseModel

class State(TypedDict):
    messages: Annotated[list, add_messages]
    ask_human: bool

其他没变,增加了一个pydantic的引入,还有在我们的状态结构里面添加了一个 ask_human 的表示,可能很多同学会疑惑,Pydantic 是什么? 我这解释一下,Pydantic 是一个 Python 数据验证和设置管理库,它可以进行数据验证,数据转换,和类型检查,后面我们就要用到。 接下来,定义一个模式来向模型展示,让其决定是否需要请求帮助。

class RequestAssistance(BaseModel):
    request: str

然后我们开始定义 agent 的节点。这里的主要目的修改是 ask_human,如果我们看到 agent 调用了该RequestAssistance 标志,则修改这个标志。

tool = TavilySearchResults(max_results=2)
tools = [tool]

llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools(tools + [RequestAssistance])

def chatbot(state: State):
    response = llm_with_tools.invoke(state["messages"])
    ask_human = False
    if (
            response.tool_calls
            and response.tool_calls[0]["name"] == RequestAssistance.__name__
    ):
        ask_human = True
    return {"messages": [response], "ask_human": ask_human}

初始化图,增加chatbot节点和tool节点,

graph_builder = StateGraph(State)

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", ToolNode(tools=[tool]))

前面的准备工作就准备完毕,接下来,创建 human 节点。这个节点的作用是在我们的 graph 作为一个占位符,并且它触发 interrupt(中断)。如果 human 节点在期间没有手动更新状态interrupt,它会输入一条 tool 消息,以便 LLM 知道我们是请求过了但未响应。此节点还会将 ask_human 设置成 false,以便 graph 知道除非再次发出请求,否则不要重新访问该节点。

from langchain_core.messages import AIMessage, ToolMessage
def create_response(response: str, ai_message: AIMessage):
    return ToolMessage(
        content=response,
        tool_call_id=ai_message.tool_calls[0]["id"],
    )

def human_node(state: State):
    new_messages = []
    if not isinstance(state["messages"][-1], ToolMessage):
        new_messages.append(
            create_response("No response from human.", state["messages"][-1])
        )
    return {
        "messages": new_messages,
        "ask_human": False,
    }
graph_builder.add_node("human", human_node)

二、定义条件边逻辑

如果设置了ask_human = false 标志,select_next_node 将导向到该节点。否则,它让预构建函数选择下一个节点。 这里回忆一下,tools_condition函数只是检查响应消息中是否chatbot有任何响应。如果是,它将导向到该节点。否则,它将结束graph`。

def select_next_node(state: State):
    if state["ask_human"]:
        return "human"
    # Otherwise, we can route as before
    return tools_condition(state)

graph_builder.add_conditional_edges(
    "chatbot",
    select_next_node,
    {"human": "human", "tools": "tools", END: END},
)

最后,我们添加简单的边并编译图形。这些边指示图形每当a执行完成时 a -> b。

# The rest is the same
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge("human", "chatbot")
graph_builder.add_edge(START, "chatbot")
memory = MemorySaver()
graph = graph_builder.compile(
    checkpointer=memory,
    # We interrupt before 'human' here instead.
    interrupt_before=["human"],
)

我们通过执行下面方法看下当前我们构造的图形:

from IPython.display import Image, display

try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
    # This requires some extra dependencies and is optional
    pass

在这里插入图片描述 可以看到 chatbot 可以向人类请求帮助(chatbot->select->human),然后可以调用搜索引擎工具(chatbot->select->action),或直接响应(chatbot->select-> end)。一旦做出操作或请求,图表将转换回节点chatbot以继续操作。 我们下面就来试一下这个graph的效果。

user_input = "需要一些专家指导来构建这个AI代理。你能帮我请求帮助吗?"
config = {"configurable": {"thread_id": "1"}}
# The config is the **second positional argument** to stream() or invoke()!
events = graph.stream(
    {"messages": [("user", user_input)]}, config, stream_mode="values"
)
for event in events:
    if "messages" in event:
        event["messages"][-1].pretty_print()

在这里插入图片描述 graph 状态在调用 human 节点之前中断了。我们可以充当此场景中的“专家”人士,我们添加新的信息输入得到 ToolMessage 来手动更新状态。 下面,通过以下方式响应 agent 的请求: 1.ToolMessage 使用我们的响应创建一个,这将被传回 chatbot

  1. 调用 update_state 以手动更新图形状态。
ai_message = snapshot.values["messages"][-1]
human_response = (
    "专家在此提供帮助!我们建议您探索使用LangGraph来构建您的AI代理。"
    "与简单的自主代理相比,LangGraph提供了更高的可靠性和可扩展性。"
)
tool_message = create_response(human_response, ai_message)
graph.update_state(config, {"messages": [tool_message]})

我们这里检查状态来确认我们自己的输入已被添加。

graph.get_state(config).values["messages"]

在这里插入图片描述 开始恢复graph,参数传None

events = graph.stream(None, config, stream_mode="values")
for event in events:
    if "messages" in event:
        event["messages"][-1].pretty_print()

在这里插入图片描述 这里便可以看到我们充当的专家建议已经被agent采纳,并返回我们充当的建议之后的内容。 这里需要注意一点的是,agent已将更新后的状态纳入其最终响应中。由于所有内容都经过了检查,因此循环中的“专家”人员可以随时执行更新,而不会影响图表的执行。

三、总结

我们已经向助手 graph 添加了一个额外节点,让 agent 自行决定是否需要中断执行。您通过使用 State 里面的 ask_human 字段更新 graph 并在编译 graph 时修改中断逻辑来实现此目的。这样,我们就可以动态地将人自己纳入循环中,同时每次执行图时都保持完整内存。完整的实现了人类与 agent 的交互,我们提供建议,agent 采纳。