langgraph / / 2024. 11. 21. 17:58

[langgraph] Multi-agent supervisor

langgraph의 공식문서를 번역해 놓은 자료입니다. 필요한 경우 부연 설명을 추가하였습니다. 문제가 되면 삭제하겠습니다.

https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/

이전 예시에서는 초기 Research 에이전트의 output에 근거하여 자동으로 메시지를 라우팅했다.

우리는 또한 LLM을 사용하여 서로 다른 에이전트를 선택할 수도 있다.

아래에서는 에이전트 그룹을 만들고 작업을 위임할 수 있도록 에이전트 감독관(supervisor)을 추가한다.

각 에이전트 노드의 코드를 간소화하기 위해 LangGraph의 create_react_agent라는 미리 만들어진 기능을 사용할 예정이다. 이와 같이 "고급 에이전트" 예시는 LangGraph에서 특정 디자인 패턴을 구현하는 방법을 보여주기 위해 설계되었다. 이 패턴이 여러분의 필요에 맞는다면, 성능을 극대화하기 위해 문서의 다른 기본 패턴들과 조합해서 사용하는 것을 권장한다.

create_react_agent는 ReAct (Reasoning + Acting) 프레임워크를 기반으로 한 에이전트를 생성하는 함수이다. 이 에이전트는 추론과 행동을 결합하여 작업을 수행한다. 주로 도구(tool)를 활용하여 사용자가 요청한 작업을 효과적으로 해결할 수 있다.

준비

먼저, 필요한 패키지들을 설치하고 API 키를 설정해 보자.

$ pip install -U langgraph langchain_community langchain_anthropic langchain_experimental

.env에 TAVILY_API_KEY와 OPENAI_API_KEY를 설정한다.

from dotenv import load_dotenv

load_dotenv()

.env

OPENAI_API_KEY=sk_xxx
TAVILY_API_KEY=xxxxxx

Tool 생성

이 예시에서 검색 엔진을 사용하여 웹 리서치를 수행하는 에이전트를 하나 만들고, 그래프를 생성하는 에이전트를 하나 만든다. 아래에 이들이 사용할 도구를 정의해 보겠다.

from typing import Annotated

from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.tools import tool
from langchain_experimental.utilities import PythonREPL

tavily_tool = TavilySearchResults(max_results=)

repl = PythonREPL()


@tool
def python_repl_tool(
    code: Annotated[str, "The python code to execute to generate your chart."],
):
    """Use this to execute python code and do math. 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)}"
    result_str = f"Successfully executed:\n\`\`\`python\n{code}\n\`\`\`\nStdout: {result}"
    return result_str

Agent Supervisor 생성

다음 worker node를 선택하거나 작업을 마치기 위해 structured output을 가진 LLM을 사용한다.

from langgraph.graph import MessagesState
# The agent state is the input to each node in the graph
class AgentState(MessagesState):
    # The 'next' field indicates where to route to next
    next: str
from typing import Literal
from typing_extensions import TypedDict

from langchain_openai import ChatOpenAI

members = ["researcher", "coder"]
# 팀 supervisor는 LLM 노드이다. 다음 실행할 에이전트를 선택하고 작업이 완료되었는지를 결정한다.
options = members + ["FINISH"]

system_prompt = (
    "You are a supervisor tasked with managing a conversation between the"
    f" following workers: {members}. Given the following user request,"
    " respond with the worker to act next. Each worker will perform a"
    " task and respond with their results and status. When finished,"
    " respond with FINISH."
)


class Router(TypedDict):
    """Worker to route to next. If no workers needed, route to FINISH."""

    next: Literal[*options]


llm = ChatOpenAI(model="gpt-4o") # gpt-4o-mini로 하면 잘 안된다.


def supervisor_node(state: AgentState) -> AgentState:
    messages = [
        {"role": "system", "content": system_prompt},
    ] + state["messages"]
    response = llm.with_structured_output(Router).invoke(messages)
    next_ = response["next"]
    if next_ == "FINISH":
        next_ = END

    return {"next": next_}

그래프 구성

이제 그래프를 구축할 준비가 되었다. 아래에서는 방금 정의한 그래프의 상태(state)와 작업자 노드(worker nodes)를 정의하자.

from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import create_react_agent


research_agent = create_react_agent(
    llm, tools=[tavily_tool], state_modifier="You are a researcher. DO NOT do any math."
)


def research_node(state: AgentState) -> AgentState:
    result = research_agent.invoke(state)
    return {
        "messages": [
            HumanMessage(content=result["messages"][-1].content, name="researcher")
        ]
    }


code_agent = create_react_agent(llm, tools=[python_repl_tool])


def code_node(state: AgentState) -> AgentState:
    result = code_agent.invoke(state)
    return {
        "messages": [HumanMessage(content=result["messages"][-1].content, name="coder")]
    }


builder = StateGraph(AgentState)
builder.add_edge(START, "supervisor")
builder.add_node("supervisor", supervisor_node)
builder.add_node("researcher", research_node)
builder.add_node("coder", code_node)

이제 그래프의 모든 엣지를 연결한다.

for member in members:
    # supervisor에게 작업이 완료되었음을 항상 알려주기를 원한다.
    builder.add_edge(member, "supervisor")


# supervisor는 그래프 상태의 "next" 필드를 채워서 노드로 라우팅하거나 종료한다.
builder.add_conditional_edges("supervisor", lambda state: state["next"])
# 마지막으로 진입점을 추가한다.
builder.add_edge(START, "supervisor")

graph = builder.compile()
from IPython.display import display, Image
display(Image(graph.get_graph().draw_mermaid_png(output_file_path="./supervisor.png")))

실행

그래프 생성과 함께 실행해서 어떻게 수행되는지 확인할 수 있다.

예제1

Message : "42의 제곱근은 얼마야?"

for s in graph.stream(
    {"messages": [("user", "42의 제곱근은 얼마야?")]}, subgraphs=True
):
    print(s)
    print("----")

실행결과

((), {'supervisor': {'next': 'coder'}})
----
Python REPL can execute arbitrary code. Use with caution.
(('coder:9bcdfc14-7199-2258-ac7b-efa95106ecc6',), {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_0b82OheeOVaUAFTyO9Ecl0aX', 'function': {'arguments': '{"code":"import math\\nmath.sqrt(42)"}', 'name': 'python_repl_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 98, 'total_tokens': 121, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_831e067d82', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-7f5e77e5-2965-4e63-acbb-fad9c3bda5f7-0', tool_calls=[{'name': 'python_repl_tool', 'args': {'code': 'import math\nmath.sqrt(42)'}, 'id': 'call_0b82OheeOVaUAFTyO9Ecl0aX', 'type': 'tool_call'}], usage_metadata={'input_tokens': 98, 'output_tokens': 23, 'total_tokens': 121, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})
----
(('coder:9bcdfc14-7199-2258-ac7b-efa95106ecc6',), {'tools': {'messages': [ToolMessage(content='Successfully executed:\n\\`\\`\\`python\nimport math\nmath.sqrt(42)\n\\`\\`\\`\nStdout: ', name='python_repl_tool', id='49732e79-e09f-40ba-963f-dde996b7ebf8', tool_call_id='call_0b82OheeOVaUAFTyO9Ecl0aX')]}})
----
(('coder:9bcdfc14-7199-2258-ac7b-efa95106ecc6',), {'agent': {'messages': [AIMessage(content='42의 제곱근은 약 6.48입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 156, 'total_tokens': 171, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_7f6be3efb0', 'finish_reason': 'stop', 'logprobs': None}, id='run-e139a44d-8904-48be-bdfb-3f2d236c26d0-0', usage_metadata={'input_tokens': 156, 'output_tokens': 15, 'total_tokens': 171, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})
----
((), {'coder': {'messages': [HumanMessage(content='42의 제곱근은 약 6.48입니다.', additional_kwargs={}, response_metadata={}, name='coder')]}})
----
((), {'supervisor': {'next': '__end__'}})
----

실행과정은 아래와 같이 진행된다.

  1. [supervisor] 다음 노드로 coder로 지정한다.
  2. [coder] AIMessage에서 tool_calls를 실행한다. (import math\nmath.sqrt(42))
  3. [coder] ToolMessage가 실행된다.
  4. [coder] AIMessage로 계산 결과를 출력한다. (42의 제곱근은 약 6.48입니다.)
  5. [supervisor] 종료한다.

예제2

Message : "한국과 일본의 최근 GPI를 찾아서 평균을 계산해줘"

for s in graph.stream(
    {
        "messages": [
            (
                "user",
                "한국과 일본의 최근 GDP를 찾아서 평균을 계산해줘",
            )
        ]
    },
    subgraphs=True,
):
    print(s)
    print("----")

실행결과

((), {'supervisor': {'next': 'researcher'}})
----
(('researcher:3af1e193-9551-6d1a-a1f3-123fb2d8a7ec',), {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_qMeSnKpHktqvgh66lnqL4cQr', 'function': {'arguments': '{"query": "South Korea 2023 GDP"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}, {'id': 'call_ysY2bIlx5mgKblJsNVKbvAZl', 'function': {'arguments': '{"query": "Japan 2023 GDP"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 61, 'prompt_tokens': 107, 'total_tokens': 168, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_7f6be3efb0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-f903c19a-56ac-4a53-ad96-697747a20f5d-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'South Korea 2023 GDP'}, 'id': 'call_qMeSnKpHktqvgh66lnqL4cQr', 'type': 'tool_call'}, {'name': 'tavily_search_results_json', 'args': {'query': 'Japan 2023 GDP'}, 'id': 'call_ysY2bIlx5mgKblJsNVKbvAZl', 'type': 'tool_call'}], usage_metadata={'input_tokens': 107, 'output_tokens': 61, 'total_tokens': 168, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})
----
(('researcher:3af1e193-9551-6d1a-a1f3-123fb2d8a7ec',), {'tools': {'messages': [ToolMessage(content='[{"url": "https://www.macrotrends.net/global-metrics/countries/KOR/south-korea/gdp-gross-domestic-product", "content": "South Korea gdp for 2023 was $1,712.79B, a 2.32% increase from 2022. ... (생략)"}]', name='tavily_search_results_json', id='0800d487-8408-42d9-a697-7c35b6737db4', tool_call_id='call_qMeSnKpHktqvgh66lnqL4cQr', artifact={'query': 'South Korea 2023 GDP', 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'title': 'South Korea GDP 1960-2024 - Macrotrends', 'url': 'https://www.macrotrends.net/global-metrics/countries/KOR/south-korea/gdp-gross-domestic-product', 'content': 'South Korea gdp for 2023 was $1,712.79B, a 2.32% increase from 2022. ... (생략)', 'score': 0.99974686, 'raw_content': None}, {'title': 'South Korea GDP - Gross Domestic Product 2023 - countryeconomy.com', 'url': 'https://countryeconomy.com/gdp/south-korea?year=2023', 'content': 'Gross Domestic Product of South Korea grew 1.4% in 2023 compared to last year. This rate is 12 -tenths of one percent less than the figure of 2.6% published in 2022. ... (생략)', 'score': 0.99974483, 'raw_content': None}], 'response_time': 3.34}), ToolMessage(content='[{"url": "https://countryeconomy.com/gdp/japan?year=2023", "content": "Japan GDP - Gross Domestic Product 2023 | countryeconomy.com Japan GDP - Gross Domestic Product GDP Japan 2023 Japan: GDP increases 1.7% The GDP figure in 2023 was €3,901,540$4,204,495 million, leaving Japan placed 4th in the ranking of GDP of the 196 countries that we publish. ... (생략)"}, {"url": "https://www.macrotrends.net/global-metrics/countries/JPN/japan/gdp-gross-domestic-product", "content": "Japan gdp for 2023 was $4,212.95B, a 1.02% decline from 2022. ... (생략)"}]', name='tavily_search_results_json', id='47bbd013-26a4-41a1-b763-87dfe1478620', tool_call_id='call_ysY2bIlx5mgKblJsNVKbvAZl', artifact={'query': 'Japan 2023 GDP', 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'title': 'Japan GDP - Gross Domestic Product 2023 - countryeconomy.com', 'url': 'https://countryeconomy.com/gdp/japan?year=2023', 'content': 'Japan GDP - Gross Domestic Product 2023 | countryeconomy.com Japan GDP - ... (생략)', 'score': 0.9998148, 'raw_content': None}, {'title': 'Japan GDP 1960-2024 - Macrotrends', 'url': 'https://www.macrotrends.net/global-metrics/countries/JPN/japan/gdp-gross-domestic-product', 'content': "Japan gdp for 2023 was $4,212.95B, a 1.02% decline from 2022. Japan gdp for 2022 was $4,256.41B, a 15.46% decline from 2021. Japan gdp for 2021 was $5,034.62B, a 0.41% decline from 2020. ... (생략)", 'score': 0.9995939, 'raw_content': None}], 'response_time': 3.35})]}})
----
(('researcher:3af1e193-9551-6d1a-a1f3-123fb2d8a7ec',), {'agent': {'messages': [AIMessage(content='In 2023, the GDP for South Korea was approximately $1,713 billion, and for Japan, it was approximately $4,204 billion.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 881, 'total_tokens': 913, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_7f6be3efb0', 'finish_reason': 'stop', 'logprobs': None}, id='run-63e5c4d1-7106-4ffd-bddf-03bf335ca5b1-0', usage_metadata={'input_tokens': 881, 'output_tokens': 32, 'total_tokens': 913, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})
----
((), {'researcher': {'messages': [HumanMessage(content='In 2023, the GDP for South Korea was approximately $1,713 billion, and for Japan, it was approximately $4,204 billion.', additional_kwargs={}, response_metadata={}, name='researcher')]}})
----
((), {'supervisor': {'next': 'coder'}})
----
(('coder:1a9954a8-3667-519b-b0f0-cc849c5302b2',), {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_jLwG8TZjaxLLntQgp5zptFTz', 'function': {'arguments': '{"code":"# Given GDP values for South Korea and Japan in 2023\\nsouth_korea_gdp = 1713  # in billion dollars\\njapan_gdp = 4204  # in billion dollars\\n\\n# Calculate the average GDP\\naverage_gdp = (south_korea_gdp + japan_gdp) / 2\\naverage_gdp"}', 'name': 'python_repl_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 87, 'prompt_tokens': 143, 'total_tokens': 230, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45cf54deae', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0fc22959-5bb0-4ac0-8d3a-b6070f286248-0', tool_calls=[{'name': 'python_repl_tool', 'args': {'code': '# Given GDP values for South Korea and Japan in 2023\nsouth_korea_gdp = 1713  # in billion dollars\njapan_gdp = 4204  # in billion dollars\n\n# Calculate the average GDP\naverage_gdp = (south_korea_gdp + japan_gdp) / 2\naverage_gdp'}, 'id': 'call_jLwG8TZjaxLLntQgp5zptFTz', 'type': 'tool_call'}], usage_metadata={'input_tokens': 143, 'output_tokens': 87, 'total_tokens': 230, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})
----
Python REPL can execute arbitrary code. Use with caution.
(('coder:1a9954a8-3667-519b-b0f0-cc849c5302b2',), {'tools': {'messages': [ToolMessage(content='Successfully executed:\n\\`\\`\\`python\n# Given GDP values for South Korea and Japan in 2023\nsouth_korea_gdp = 1713  # in billion dollars\njapan_gdp = 4204  # in billion dollars\n\n# Calculate the average GDP\naverage_gdp = (south_korea_gdp + japan_gdp) / 2\naverage_gdp\n\\`\\`\\`\nStdout: ', name='python_repl_tool', id='2bc2dc9d-83ca-4077-a3ba-0a7a4c4a0eb5', tool_call_id='call_jLwG8TZjaxLLntQgp5zptFTz')]}})
----
(('coder:1a9954a8-3667-519b-b0f0-cc849c5302b2',), {'agent': {'messages': [AIMessage(content='The average GDP for South Korea and Japan in 2023 is approximately $2,958.5 billion.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 329, 'total_tokens': 352, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45cf54deae', 'finish_reason': 'stop', 'logprobs': None}, id='run-6bbe5055-7563-44fb-a2b8-0cac82f15091-0', usage_metadata={'input_tokens': 329, 'output_tokens': 23, 'total_tokens': 352, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})
----
((), {'coder': {'messages': [HumanMessage(content='The average GDP for South Korea and Japan in 2023 is approximately $2,958.5 billion.', additional_kwargs={}, response_metadata={}, name='coder')]}})
----
((), {'supervisor': {'next': '__end__'}})
----

실행결과를 확인해보면 아래와 같다.

  1. [supervisor] next로 researcher로 지정한다.
  2. [researcher] 한국 2023 GDP와 일본 2023년 GDP를 tavily로 검색하는 도구(tool)를 호출하는 AIMessage 전송.
  3. [researcher] 한국 GDP와 일본 GDP를 검색하고 결과를 받는 ToolMessage 전송
  4. [research] 한국 GDP와 일본 GDP를 요약하는 AIMessage 전송
  5. [supervisor] 다음을 coder로 지정
  6. [coder] 한국과 일본 GDP의 평균을 계산
  7. [coder] Python REPL 실행하는 ToolMessage 전송
  8. [coder] 평균값을 나타내는 AIMessage 전송
  9. [supervisor] 종료한다.
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유