langgraph / / 2024. 11. 22. 07:33

[langgraph][Planning Agents] Reasoning without Observation

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

https://langchain-ai.github.io/langgraph/tutorials/rewoo/rewoo/

ReWOO에서 Xu 등은 효과적인 도구 사용을 위해 다단계 계획자와 변수 대체를 결합한 에이전트를 제안했다. 이 에이전트는 ReACT 스타일 에이전트 아키텍처를 다음과 같은 방식으로 개선하도록 설계되었다.

  1. 단일 패스에서 사용된 도구의 전체 체인을 생성함으로써 토큰 소비와 실행 시간을 줄인다. (ReACT 스타일 에이전트 아키텍처는 많은 LLM 호출이 필요하며, 각 추론 단계마다 시스템 프롬프트와 이전 단계가 LLM에 제공되기 때문에 중복된 접두어가 포함된다)
  2. 파인튜닝 프로세스를 단순화한다. 계획 데이터는 도구의 출력에 의존하지 않기 때문에, 실제로 도구를 호출하지 않고도 모델을 파인튜닝할 수 있다 (이론적으로).

다음 다이어그램은 ReWOO의 전체 계산 그래프를 보여준다.

ReWOO는 3개의 모듈로 구성되어 있다.

  1. Planner: 다음 포맷으로 계획을 생성한다.

    Plan: <reasoning>
    #E1 = Tool[argument for tool]
    Plan: <reasoning>
    #E2 = Tool[argument for tool with #E1 variable substitution]
    ...
  2. Worker: 제공된 인수로 툴을 실행한다.

  3. Solver: 툴 observation으로 초기 작업의 응답을 생성한다.

이모지가 있는 모듈은 LLM 호출에 의존한다. 우리는 변수 대체를 사용하여 플래너 LLM에 대한 중복 호출을 피한다는 점을 주의하자.

이 예제에서 각 모듈은 LangGraph 노드로 표현된다. 최종 결과는 이와 같은 같은 결과가 될 것이다.

준비

이 예제에서는 에이전트에게 Tavily 검색 엔진 도구를 제공한다. 여기에서 API 키를 얻거나 무료 도구 옵션(예: Duck Duck Go 검색)으로 교체할 수 있다.

필요한 패키지를 설치하고 API 키를 설정해보다.

$ pip install -U langgraph langchain_community langchain_openai tavily-python
from dotenv import load_dotenv

load_dotenv()

그래프 상태 정의

LangGraph에서는 각 노드가 공유된 그래프 상태를 업데이트한다. 상태는 노드가 호출될 때마다 입력으로 사용된다.

아래에서 작업(task), 계획(plan_string), 단계(steps) 및 기타 변수를 포함하는 상태 딕셔너리를 정의한다.

from typing import List
from typing_extensions import TypedDict


class ReWOO(TypedDict):
    task: str
    plan_string: str
    steps: List
    results: dict
    result: str

Planner

Planner는 LLM에게 작업 목록 형식으로 계획을 생성하도록 요청한다. 각 작업의 인수는 문자열이며, 여기에는 다른 작업 결과에서 변수 대체에 사용되는 특수 변수(#E{0-9}+)가 포함될 수 있다.

여기 예제에서 사용된 에이전트는 두 가지 도구를 가진다: 1. Google - 검색 엔진 (이 경우 Tavily) 2. LLM - 이전 출력에 대해 추론하는 LLM 호출

LLM 도구는 프롬프트 컨텍스트 양이 적어서 ReACT 패러다임보다 더 효율적으로 토큰을 사용할 수 있다.

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o")
prompt = """For the following task, make plans that can solve the problem step by step. For each plan, indicate \
which external tool together with tool input to retrieve evidence. You can store the evidence into a \
variable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)

Tools can be one of the following:
(1) Google[input]: Worker that searches results from Google. Useful when you need to find short
and succinct answers about a specific topic. The input should be a search query.
(2) LLM[input]: A pretrained LLM like yourself. Useful when you need to act with general
world knowledge and common sense. Prioritize it when you are confident in solving the problem
yourself. Input can be any instruction.

For example,
Task: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x
hours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours
less than Toby. How many hours did Rebecca work?
Plan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve
with Wolfram Alpha. #E1 = WolframAlpha[Solve x + (2x − 10) + ((2x − 10) − 8) = 157]
Plan: Find out the number of hours Thomas worked. #E2 = LLM[What is x, given #E1]
Plan: Calculate the number of hours Rebecca worked. #E3 = Calculator[(2 ∗ #E2 − 10) − 8]

Begin! 
Describe your plans with rich details. Each Plan should be followed by only one #E.

Task: {task}"""
task = "2023년 호주 오픈 남자 우승자의 고향은 어디야?"
result = model.invoke(prompt.format(task=task))
print(result.content)
Plan: First, identify the winner of the 2023 Australian Open Men's singles to determine whose hometown we need to find. Use an external tool to find this information. #E1 = Google["2023 Australian Open Men's singles winner"]

Plan: Once we have the name of the winner from #E1, find out the hometown of the winner by searching for this specific detail. #E2 = Google["[Winner's Name] hometown"]

Planning Node

플래너를 그래프에 연결하기 위해, 우리는 ReWOO 상태를 받아들이고 steps와 plan_string 필드를 업데이트한 상태를 반환하는 get_plan 노드를 만들 것이다.

import re

from langchain_core.prompts import ChatPromptTemplate

# Regex to match expressions of the form E#... = ...[...]
regex_pattern = r"Plan:\s*(.+)\s*(#E\d+)\s*=\s*(\w+)\s*\[([^\]]+)\]"
prompt_template = ChatPromptTemplate.from_messages([("user", prompt)])
planner = prompt_template | model


def get_plan(state: ReWOO):
    task = state["task"]
    result = planner.invoke({"task": task})
    # Find all matches in the sample text
    matches = re.findall(regex_pattern, result.content)
    return {"steps": matches, "plan_string": result.content}

Executor

실행자는 계획을 받아 도구들을 순차적으로 실행한다.

아래에서 검색 엔진을 인스턴스화하고 도구 실행 노드를 정의한다.

from langchain_community.tools.tavily_search import TavilySearchResults

search = TavilySearchResults()
def _get_current_task(state: ReWOO):
    if "results" not in state or state["results"] is None:
        return 1
    if len(state["results"]) == len(state["steps"]):
        return None
    else:
        return len(state["results"]) + 1


def tool_execution(state: ReWOO):
    """Worker node that executes the tools of a given plan."""
    _step = _get_current_task(state)
    _, step_name, tool, tool_input = state["steps"][_step - 1]
    _results = (state["results"] or {}) if "results" in state else {}
    for k, v in _results.items():
        tool_input = tool_input.replace(k, v)
    if tool == "Google":
        result = search.invoke(tool_input)
    elif tool == "LLM":
        result = model.invoke(tool_input)
    else:
        raise ValueError
    _results[step_name] = str(result)
    return {"results": _results}

Solver

솔버는 전체 계획을 받아들이고 작업자의 도구 호출 응답을 바탕으로 최종 응답을 생성한다.

solve_prompt = """Solve the following task or problem. To solve the problem, we have made step-by-step Plan and \
retrieved corresponding Evidence to each Plan. Use them with caution since long evidence might \
contain irrelevant information.

{plan}

Now solve the question or task according to provided Evidence above. Respond with the answer
directly with no extra words.

Task: {task}
Response:"""


def solve(state: ReWOO):
    plan = ""
    for _plan, step_name, tool, tool_input in state["steps"]:
        _results = (state["results"] or {}) if "results" in state else {}
        for k, v in _results.items():
            tool_input = tool_input.replace(k, v)
            step_name = step_name.replace(k, v)
        plan += f"Plan: {_plan}\n{step_name} = {tool}[{tool_input}]"
    prompt = solve_prompt.format(plan=plan, task=state["task"])
    result = model.invoke(prompt)
    return {"result": result.content}

그래프 정의

그래프는 워크플로우를 정의한다. 플래너, 도구 실행기, 솔버 모듈 각각이 노드로 추가된다.

def _route(state):
    _step = _get_current_task(state)
    if _step is None:
        # 모든 작업 실행
        return "solve"
    else:
        # 아직 작업 실행중
        return "tool"
from langgraph.graph import END, StateGraph, START

graph = StateGraph(ReWOO)
graph.add_node("plan", get_plan)
graph.add_node("tool", tool_execution)
graph.add_node("solve", solve)
graph.add_edge("plan", "tool")
graph.add_edge("solve", END)
graph.add_conditional_edges("tool", _route)
graph.add_edge(START, "plan")

app = graph.compile()
for s in app.stream({"task": task}):
    print(s)
    print("---")
{
  'plan': {
    'plan_string': 'Plan: Determine who the 2023 Australian Open Men\'s Singles winner is. #E1 = Google["2023 Australian Open Men\'s Singles winner"]\n\nPlan: Find the hometown of the 2023 Australian Open Men\'s Singles winner identified in #E1. #E2 = Google["hometown of #E1"]',
    'steps': [
      (
      "Determine who the 2023 Australian Open Men's Singles winner is. ",
      '#E1',
      'Google',
      '"2023 Australian Open Men\'s Singles winner"'
      ),
      (
      "Find the hometown of the 2023 Australian Open Men's Singles winner identified in #E1. ",
      '#E2',
      'Google',
      '"hometown of #E1"'
      )
    ]
  }
}
---
{
  'tool': {
    'results': {
      '#E1': '[{\'url\': \'https://www.tennis-x.com/winners/mens/australian-open.php\', \'content\': "Australian Open Men\'s Singles Winners ; 2023, Novak Djokovic (SRB), Stefanos Tsitsipas (GRE) ; 2022, Rafael Nadal (ESP), Daniil Medvedev (RUS) ; 2021, Novak"}, {\'url\': \'https://www.sportingnews.com/us/tennis/news/who-won-australian-open-last-year-mens-womens-2023-singles-champions/84479989e83728a9f9d3c542\', \'content\': \'Men\\\'s and women\\\'s 2023 singles champions\\nThe latest edition of the Australian Open is quickly approaching as the 2024 tennis season heats up Down Under....(생략)\'}, {\'url\': \'https://www.topendsports.com/events/tennis-grand-slam/australian-open/winners-men.htm\', \'content\': "Australian Open Men\'s Singles Results ; 2023, Novak Djokovic (Serbia), Stefanos Tsitsipas (Greece) ; 2022, Rafael Nadal (Spain), Daniil Medvedev (Russia) ; 2021"}, {\'url\': \'https://olympics.com/en/news/australian-open-2023-novak-djokovic-wins-10th-title\', \'content\': "Jan 29, 2023 · Novak Djokovic won the Australian Open for the 10th time to equal Rafael Nadal\'s record of 22 men\'s Grand Slam singles titles."}, {\'url\': \'https://en.wikipedia.org/wiki/2023_Australian_Open_%E2%80%93_Men%27s_singles\', \'content\': "Alcaraz, the incumbent world No. 1, withdrew from the tournament due to a right leg injury.[5][6]\\nAs in the previous major held a few months earlier, ...(생략)"}]'
    }
  }
}
---
{
  'tool': {
    'results': {
      '#E1': '[{\'url\': \'https://www.tennis-x.com/winners/mens/australian-open.php\', \'content\': "Australian Open Men\'s Singles Winners ; 2023, Novak Djokovic (SRB), Stefanos Tsitsipas (GRE) ; 2022, Rafael Nadal (ESP), Daniil Medvedev (RUS) ; 2021, Novak"}, {\'url\': \'https://www.sportingnews.com/us/tennis/news/who-won-australian-open-last-year-mens-womens-2023-singles-champions/84479989e83728a9f9d3c542\', \'content\': \'Men\\\'s and women\\\'s 2023 singles champions\\nThe latest edition of the Australian Open is quickly approaching as the 2024 tennis season heats up Down Under.\\n Stefanos Tsitsipas\\nIt was a familiar face who took out the men\\\'s singles in 2023, ...(생략)\', \'content\': "Australian Open Men\'s Singles Results ; 2023, Novak Djokovic (Serbia), Stefanos Tsitsipas (Greece) ; 2022, Rafael Nadal (Spain), Daniil Medvedev (Russia) ; 2021"}, {\'url\': \'https://olympics.com/en/news/australian-open-2023-novak-djokovic-wins-10th-title\', \'content\': "Jan 29, 2023 · Novak Djokovic won the Australian Open for the 10th time to equal Rafael Nadal\'s record of 22 men\'s Grand Slam singles titles."}, {\'url\': \'https://en.wikipedia.org/wiki/2023_Australian_Open_%E2%80%93_Men%27s_singles\', \'content\': "Alcaraz, the incumbent world No. 1, withdrew from the tournament due to a right leg injury.[5][6]\\nAs in the previous major held a few months earlier, neither of the top two seeds advanced to the quarterfinals, with Nadal and Ruud both losing in the second round; ...(생략)"}]',
      '#E2': "HTTPError('400 Client Error: Bad Request for url: https://api.tavily.com/search')"
    }
  }
}
---
{
  'solve': {
    'result': '노박 조코비치의 고향은 세르비아의 벨그라드입니다.'
  }
}
---
# 최종 결과를 출력한다
print(s["solve"]["result"])
노박 조코비치의 고향은 세르비아의 벨그라드입니다.
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유