langgraph设计
挺复杂的,相对更通用,覆盖Agent和multi-Agent,参考了Pregel的设计
LangGraph is a library for building stateful, multi-actor applications with LLMs, built on top of (and intended to be used with) LangChain. It extends the LangChain Expression Language with the ability to coordinate multiple chains (or actors) across multiple steps of computation in a cyclic manner. It is inspired by Pregel and Apache Beam. The current interface exposed is one inspired by NetworkX.
langgraph 用于使用LLM构建有状态、多Actor的应用程序。langgraph是扩展LECL表达式的库,相比LECL,langgraph支持cycle(循环)
Cycles are important for agent-like behaviors, where you call an LLM in a loop, asking it what action to take next.
Graph有StateGraph和MessageGraph两种。
AgentState:Agent之间共享State数据
消息如何进行传递:基于channel进行通信。是否可以扩展到不同机器的多Agent
Pregel(Parallel、Graph 和 Google 的组合)是 Google 创建的用于大规模图形处理的数据流范例和系统,用于解决仅使用 MapReduce 框架难以解决或成本高昂的问题。 虽然该系统仍属于 Google 专有,但其计算范式已被许多图形处理系统采用,并且许多流行的图形算法已转换为 Pregel 框架。 Pregel 本质上是一个限制在图的边缘的消息传递接口。 这个想法是“像顶点一样思考”——Pregel 框架内的算法是给定节点的状态计算仅取决于其邻居状态的算法。 图 1 显示了 Pregel 范例的数据流模型。 Pregel 计算采用图和相应的一组顶点状态作为其输入。 在每次迭代(称为超级步)中,每个顶点都可以向其邻居发送消息,处理在前一个超级步中收到的消息,并更新其状态。 因此,每个超级步骤都由邻居之间传递的一轮消息和全局顶点状态的更新组成。 图算法的 Pregel 实现的一些示例将有助于阐明该范式的工作原理。
langgraph应用
import functools
import json
import operator
import os
from typing import Annotated, Sequence
from langchain.tools.render import format_tool_to_openai_function
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import (
BaseMessage,
FunctionMessage,
HumanMessage,
)
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
from langchain_experimental.utilities import PythonREPL
from langchain_openai import AzureChatOpenAI
from langgraph.graph import END, StateGraph
from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
from typing_extensions import TypedDict
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["TAVILY_API_KEY"] = "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"
os.environ["LANGCHAIN_PROJECT"] = "Multi-agent Collaboration"
def create_agent(llm, tools, system_message: str):
"""Create an agent."""
functions = [format_tool_to_openai_function(t) for t in tools]
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful AI assistant, collaborating with other assistants."
" Use the provided tools to progress towards answering the question."
" If you are unable to fully answer, that's OK, another assistant with different tools "
" will help where you left off. Execute what you can to make progress."
" If you or any of the other assistants have the final answer or deliverable,"
" prefix your response with FINAL ANSWER so the team knows to stop."
" You have access to the following tools: {tool_names}.\n{system_message}",
),
MessagesPlaceholder(variable_name="messages"),
]
)
prompt = prompt.partial(system_message=system_message)
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
return prompt | llm.bind_functions(functions)
tavily_tool = TavilySearchResults(max_results=5)
# Warning: This executes code locally, which can be unsafe when not sandboxed
repl = PythonREPL()
@tool
def python_repl(
code: Annotated[str, "The python code to execute to generate your chart."]
):
"""Use this to execute python code. If you want to see the output of a value,
you should print it out with `print(...)`. This is visible to the user."""
try:
result = repl.run(code)
except BaseException as e:
return f"Failed to execute. Error: {repr(e)}"
return f"Succesfully executed:\n```python\n{code}\n```\nStdout: {result}"
# This defines the object that is passed between each node
# in the graph. We will create different nodes for each agent and tool
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
sender: str
# Helper function to create a node for a given agent
def agent_node(state, agent, name):
result = agent.invoke(state)
# We convert the agent output into a format that is suitable to append to the global state
if isinstance(result, FunctionMessage):
pass
else:
result = HumanMessage(**result.dict(exclude={"type", "name"}), name=name)
return {
"messages": [result],
# Since we have a strict workflow, we can
# track the sender so we know who to pass to next.
"sender": name,
}
llm = AzureChatOpenAI()
# Research agent and node
research_agent = create_agent(
llm,
[tavily_tool],
system_message="You should provide accurate data for the chart generator to use.",
)
research_node = functools.partial(agent_node, agent=research_agent, name="Researcher")
# Chart Generator
chart_agent = create_agent(
llm,
[python_repl],
system_message="Any charts you display will be visible by the user.",
)
chart_node = functools.partial(agent_node, agent=chart_agent, name="Chart Generator")
tools = [python_repl, tavily_tool]
tool_executor = ToolExecutor(tools)
def tool_node(state):
"""This runs tools in the graph
It takes in an agent action and calls that tool and returns the result."""
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
tool_input = json.loads(
last_message.additional_kwargs["function_call"]["arguments"]
)
# We can pass single-arg inputs by value
if len(tool_input) == 1 and "__arg1" in tool_input:
tool_input = next(iter(tool_input.values()))
tool_name = last_message.additional_kwargs["function_call"]["name"]
action = ToolInvocation(
tool=tool_name,
tool_input=tool_input,
)
# 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=f"{tool_name} response: {str(response)}", name=action.tool
)
# We return a list, because this will get added to the existing list
return {"messages": [function_message]}
# Either agent can decide to end
def router(state):
# This is the router
messages = state["messages"]
last_message = messages[-1]
if "function_call" in last_message.additional_kwargs:
# The previus agent is invoking a tool
return "call_tool"
if "FINAL ANSWER" in last_message.content:
# Any agent decided the work is done
return "end"
return "continue"
workflow = StateGraph(AgentState)
workflow.add_node("Researcher", research_node)
workflow.add_node("Chart Generator", chart_node)
workflow.add_node("call_tool", tool_node)
# workflow.add_node("router", router)
workflow.add_conditional_edges(
"Researcher",
router,
{"continue": "Chart Generator", "call_tool": "call_tool", "end": END},
)
workflow.add_conditional_edges(
"Chart Generator",
router,
{"continue": "Researcher", "call_tool": "call_tool", "end": END},
)
workflow.add_conditional_edges(
"call_tool",
# Each agent node updates the 'sender' field
# the tool calling node does not, meaning
# this edge will route back to the original agent
# who invoked the tool
lambda x: x["sender"],
{
"Researcher": "Researcher",
"Chart Generator": "Chart Generator",
},
)
workflow.set_entry_point("Researcher")
app = workflow.compile()
app.get_graph().print_ascii()