几天前,LangChain 正式宣布了名为 LangGraph 的新库,LangGraph 建立在 LangChain 之上,简化了创建和管理Agent及其运行时的过程。
在这篇文章中,我们将全面介绍 langGraph,什么是代理和代理运行时?Langgraph 的特点是什么,以及如何在 Langgraph 中构建一个代理执行器,我们将探讨 Langgraph 中的聊天代理执行器以及如何在人类循环和聊天中修改 Langgraph 中的聊天 agent 执行器。
技术学习、讨论,文末加入我们
在LangChain中,代理是一个由语言模型驱动的系统,它对要采取的操作做出决策。代理运行时使该系统保持运行,不断决定操作,记录观察结果,并维护此循环,直到代理的任务完成。
LangChain通过其表达语言简化了代理定制,LangGraph对代理运行时提供了更灵活和动态自定义功能。传统的代理运行时是AgentEX类,但现在有了LangGraph,它有了更多的多样性和适应性。
LangGraph的一个关键特性是向代理运行时添加了循环,这种循环对代理操作来说非常重要。
我们以LangGraph中的两个主要代理运行时开始介绍LangGraph:
Agent Executor与LangChain类似,但在LangGraph中需要重建;
Chat Agent Executor以消息列表的形式处理代理状态,非常适合使用消息进行功能调用和响应的基于聊天的模型。
在LangGraph中构建一个代理执行器,类似于LangChain中的代理执行器。这个过程非常简单,让我们深入了解一下!
首先,我们需要通过安装几个包来设置我们的环境:LangChain、LangChain OpenAI和Tavily Python。这些将帮助我们利用现有的LangChain代理类,为我们的代理提供OpenAI的语言模型,并使用Tavily Python包实现搜索功能。
!pip install --quiet -U langchain langchain_openai tavily-python
接下来,我们将为OpenAI、Tavilly和LangSmith设置API密钥。LangSmith对日志记录和可观察性特别重要,但它目前处于私人测试阶段。如果您需要访问,请随时联系他们。
import os
import getpass
os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")
os.environ["TAVILY_API_KEY"] = getpass.getpass("Tavily API Key:")
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass("LangSmith API Key:")
我们在笔记本电脑上的第一步是创建一个LangChain代理。这包括选择一个语言模型,创建一个搜索工具,以及建立我们的代理。有关这方面的详细信息,您可以参考LangChain文档。
from langchain import hub
from langchain.agents import create_openai_functions_agent
from langchain_openai.chat_models import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
tools = [TavilySearchResults(max_results=1)]
# Get the prompt to use - you can modify this!
prompt = hub.pull("hwchase17/openai-functions-agent")
# Choose the LLM that will drive the agent
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True)
# Construct the OpenAI Functions agent
agent_runnable = create_openai_functions_agent(llm, tools, prompt)
然后,我们定义图形的状态,它跟踪随时间的变化。这种状态允许图中的每个节点更新整体状态,从而省去了不断传递的麻烦。我们还将决定如何应用这些更新,无论是覆盖现有数据还是添加到其中。
from typing import TypedDict, Annotated, List, Union
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
import operator
class AgentState(TypedDict):
# The input string
input: str
# The list of previous messages in the conversation
chat_history: list[BaseMessage]
# The outcome of a given call to the agent
# Needs `None` as a valid type, since this is what this will start as
agent_outcome: Union[AgentAction, AgentFinish, None]
# List of actions and corresponding observations
# Here we annotate this with `operator.add` to indicate that operations to
# this state should be ADDED to the existing values (not overwrite it)
intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]
设置好状态后,我们将重点定义图中的节点和边。我们需要两个主节点:一个用于运行代理,另一个用于根据代理的决策执行工具。图中的边有两种类型:条件边和普通边。条件边允许基于先前的结果来分支路径,而普通边表示固定的动作序列。
我们将研究一些细节,如调用代理的“run agent”节点和执行代理选择的工具的“execute tools”函数。我们还将添加一个“should continue”函数来确定下一步行动。
from langchain_core.agents import AgentFinish
from langgraph.prebuilt.tool_executor import ToolExecutor
# This a helper class we have that is useful for running tools
# It takes in an agent action and calls that tool and returns the result
tool_executor = ToolExecutor(tools)
# Define the agent
def run_agent(data):
agent_outcome = agent_runnable.invoke(data)
return {"agent_outcome": agent_outcome}
# Define the function to execute tools
def execute_tools(data):
# Get the most recent agent_outcome - this is the key added in the `agent` above
agent_action = data['agent_outcome']
output = tool_executor.invoke(agent_action)
return {"intermediate_steps": [(agent_action, str(output))]}
# Define logic that will be used to determine which conditional edge to go down
def should_continue(data):
# If the agent outcome is an AgentFinish, then we return `exit` string
# This will be used when setting up the graph to define the flow
if isinstance(data['agent_outcome'], AgentFinish):
return "end"
# Otherwise, an AgentAction is returned
# Here we return `continue` string
# This will be used when setting up the graph to define the flow
else:
return "continue"
最后,构造我们的图。定义图,添加节点,设置一个入口点,并建立我们的边——条件边和普通边。编译完图形后,它就可以像任何LangChain可运行程序一样使用了。
from langgraph.graph import END, StateGraph
# Define a new graph
workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between
workflow.add_node("agent", run_agent)
workflow.add_node("action", execute_tools)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END
}
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge('action', 'agent')
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
app = workflow.compile()
我们将使用一些输入数据来运行我们的executor,以查看我们的执行器的操作。这个过程包括流式传输每个节点的结果,使我们能够观察代理的决策、执行的工具以及每个步骤的总体状态。
inputs = {"input": "what is the weather in sf", "chat_history": []}
for s in app.stream(inputs):
print(list(s.values())[0])
print("----")
为了更直观地理解,我们可以在LangSmith中探索这些过程,它提供了每个步骤的详细视图,包括执行中涉及的提示和响应。
{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log="\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\n\n\n", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})])}
----
{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log="\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\n\n\n", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), "[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]")]}
这就是在LangGraph中创建代理执行器的方式,和LangChain的执行器功能类似。我们将进一步探讨状态图的接口以及返回结果的不同流式传输方法。
我们将在LangGraph中探索聊天代理执行器,这是一个设计用于处理基于聊天的模型的工具。此执行器是唯一的,因为它完全根据输入消息的列表进行操作,通过向该列表中添加新消息来随着时间的推移更新代理的状态。
让我们深入了解设置过程:
4.1 安装软件包:
同样需要LangChain软件包,LangChain OpenAI用于模型,Tavily软件包用于搜索工具,并为这些服务设置API密钥。
!pip install --quiet -U langchain langchain_openai tavily-python
import os
import getpass
os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")
os.environ["TAVILY_API_KEY"] = getpass.getpass("Tavily API Key:")
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass("LangSmith API Key:")
4.2 设置工具和模型:
我们将使用Tavily Search作为我们的工具,并设置一个工具执行器来调用这些工具。对于模型,我们将使用LangChain集成中的Chat OpenAI模型,确保其在启用流式进行初始化。这使我们能够流式返回tokens,并附加我们希望模型调用的函数。
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolExecutor
from langchain.tools.render import format_tool_to_openai_function
tools = [TavilySearchResults(max_results=1)]
tool_executor = ToolExecutor(tools)
# We will set streaming=True so that we can stream tokens
# See the streaming section for more information on this.
model = ChatOpenAI(temperature=0, streaming=True)
functions = [format_tool_to_openai_function(t) for t in tools]
model = model.bind_functions(functions)
4.3 定义代理状态:
代理状态是一个简单的字典,其中包含消息列表的键。我们将使用“add to”标记,这样随着时间的推移,节点对此消息列表的任何更新都会累积。
from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
4.4 创建节点和边:
节点表示具体的工作任务,边连接节点。我们需要一个代理节点来调用语言模型并获得响应,一个操作节点来查看是否有任何工具需要调用,以及一个函数来确定我们是否应该继续调用工具或完成。
from langgraph.prebuilt import ToolInvocation
import json
from langchain_core.messages import FunctionMessage
# Define the function that determines whether to continue or not
def should_continue(state):
messages = state['messages']
last_message = messages[-1]
# If there is no function call, then we finish
if "function_call" not in last_message.additional_kwargs:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
# Define the function that calls the model
def call_model(state):
messages = state['messages']
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
def call_tool(state):
messages = state['messages']
# Based on the continue condition
# we know the last message involves a function call
last_message = messages[-1]
# We construct an ToolInvocation from the function_call
action = ToolInvocation(
tool=last_message.additional_kwargs["function_call"]["name"],
tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]),
)
# We call the tool_executor and get back a response
response = tool_executor.invoke(action)
# We use the response to create a FunctionMessage
function_message = FunctionMessage(content=str(response), name=action.tool)
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
4.5 构建图:
我们创建一个具有代理状态的图,为代理和动作添加节点,并将入口点设置为代理节点。条件边是根据代理应该继续还是结束来添加的,并且正常边总是在动作后返回到代理。
from langgraph.graph import StateGraph, END
# Define a new graph
workflow = StateGraph(AgentState)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", call_tool)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END
}
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge('action', 'agent')
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
app = workflow.compile()
4.6 编译和使用图形:
编译图形后,我们创建一个带有消息键的输入字典。运行图形将处理这些消息,将AI响应、功能结果和最终输出添加到消息列表中。
from langchain_core.messages import HumanMessage
inputs = {"messages": [HumanMessage(content="what is the weather in sf")]}
app.invoke(inputs)
4.7 观察执行过程:
使用LangSmith,我们可以看到我们的代理所采取的详细步骤,包括对OpenAI的调用和由此产生的输出。
流式功能:LangGraph还提供流式功能。
让我们修改LangGraph中的聊天代理执行器,使其包含一个“human in the loop”组件,这样在执行工具操作之前可以进行人工验证。
设置: 初始设置保持不变。不需要额外安装。我们将创建我们的工具,设置工具执行器,准备我们的模型,将工具绑定到模型,并定义代理状态——所有这些都与我们在前一个会话中所做的一样。
关键修改——调用工具功能: 主要的变化来自调用工具功能。我们添加了一个步骤,系统在交互式IDE中提示用户(即您!),询问是否继续执行特定操作。如果用户响应“否”,则会引发错误,进程将停止。这是我们的人工验证步骤。
# Define the function to execute tools
def call_tool(state):
messages = state['messages']
# Based on the continue condition
# we know the last message involves a function call
last_message = messages[-1]
# We construct an ToolInvocation from the function_call
action = ToolInvocation(
tool=last_message.additional_kwargs["function_call"]["name"],
tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]),
)
response = input(prompt=f"[y/n] continue with: {action}?")
if response == "n":
raise ValueError
# We call the tool_executor and get back a response
response = tool_executor.invoke(action)
# We use the response to create a FunctionMessage
function_message = FunctionMessage(content=str(response), name=action.tool)
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
使用修改的执行器:当我们运行这个修改的执行程序时,它会在执行任何工具操作之前请求批准。如果我们同意说“是”,它将正常进行。然而,如果我们说“不”,则会引发错误并停止该过程。
utput from node 'agent':
---
{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n "query": "weather in San Francisco"\n}', 'name': 'tavily_search_results_json'}})]}
---
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[10], line 4
1 from langchain_core.messages import HumanMessage
3 inputs = {"messages": [HumanMessage(content="what is the weather in sf")]}
----> 4 for output in app.stream(inputs):
5 # stream() yields dictionaries with output keyed by node name
6 for key, value in output.items():
7 print(f"Output from node '{key}':")
这是一个基本的实现。在现实世界中,您可能希望用更复杂的响应来代替错误,并使用更用户友好的界面,而不是Jupyter笔记本。但这让您清楚地了解了如何将一个简单而有效的人工循环组件添加到LangGraph代理中。
让我们来看看在LangGraph中修改聊天代理执行器,以在处理消息时操纵代理的内部状态。
本教程建立在基本的聊天代理执行程序设置的基础上,因此,如果您还没有在基本笔记本中完成初始设置,请先完成。我们在这里只关注新的修改。
关键修改——过滤消息:我们引入的主要更改是过滤传递给模型的消息的方法。现在,您可以自定义代理考虑的消息。例如:
def call_model(state):
messages = state['messages'][-5:]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
仅选择最近的五条消息。
包括系统消息加上五条最新消息。
总结比最近五条消息旧的消息。
此修改是一个小而强大的添加,允许您控制代理如何与其消息历史进行交互,并改进其决策过程。
使用修改的执行器: 实现非常简单。仅有一条输入消息不同,但重要的是,您希望应用于代理步骤的任何逻辑都可以插入到这个新的修改部分。
此方法非常适合修改聊天代理执行器,但如果使用标准代理执行器时,同样的原理也适用。
我们将对LangGraph中的聊天代理执行器进行简单但有效的修改,确保始终首先调用一个工具。这是建立在基本的聊天代理执行器笔记本上的,所以请确保您已经检查了背景信息。
关键修改——强制工具调用优先:我们这里的重点是设置聊天代理调用特定工具作为其第一个操作。为此,我们将添加一个新节点,并将其命名为“first model node”。该节点将被编程为返回一条消息,指示代理调用特定工具,如“Tavil search results Json”工具,并将最新的消息内容作为查询。
# This is the new first - the first call of the model we want to explicitly hard-code some action
from langchain_core.messages import AIMessage
import json
def first_model(state):
human_input = state['messages'][-1].content
return {
"messages": [
AIMessage(
content="",
additional_kwargs={
"function_call": {
"name": "tavily_search_results_json",
"arguments": json.dumps({"query": human_input})
}
}
)
]
}
更新图:我们将修改现有的图,将这个新的“first agent”节点作为入口点。这样可以确保始终首先调用第一个代理节点,然后调用动作节点。我们设置了一个从代理到动作或结束的条件节点,以及一个从动作回到代理的直接节点。关键的添加是从第一个代理到操作的一个新节点,确保工具调用一开始就发生。
from langgraph.graph import StateGraph, END
# Define a new graph
workflow = StateGraph(AgentState)
# Define the new entrypoint
workflow.add_node("first_agent", first_model)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", call_tool)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("first_agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END
}
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge('action', 'agent')
# After we call the first agent, we know we want to go to action
workflow.add_edge('first_agent', 'action')
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
app = workflow.compile()
使用修改的执行器:当我们运行这个更新的执行器时,第一个结果会很快返回,因为我们绕过了初始的语言模型调用,直接调用该工具。通过观察LangSmith中的过程可以证实这一点,在LangSmith中,我们可以看到工具是第一个被调用的东西,然后是最后的语言模型调用。
这种修改是一种简单而强大的方法,可以确保在聊天代理的工作流程中立即使用特定的工具。
技术要学会分享、交流,不建议闭门造车。一个人走的很快、一堆人可以走的更远。
建立了大模型面试&技术交流群, 大模型学习资料、数据代码、技术交流提升, 均可加知识星球交流群获取,群友已超过2000人,添加时切记的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。
方式①、微信搜索公众号:机器学习社区,后台回复:技术交流
方式②、添加微信号:mlc2060,备注:技术交流
参考文献:
[1] https://camunda.com/blog/2023/02/orchestration-vs-choreography/
[2] https://medium.com/@rajib76.gcp/langgraph-agent-orchestrator-9cb4da8179c3
[3] https://levelup.gitconnected.com/langgraph-create-a-hyper-ai-agent-0e74c61238cc
[4] https://python.langchain.com/docs/langgraph
[5] https://github.com/langchain-ai/langgraph/blob/main/examples/agent_executor/base.ipynb
[6] https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/base.ipynb
[7] https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb
[8] https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb
[9] https://github.com/langchain-ai/langgraph/blob/main/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb