langgraph / / 2024. 11. 30. 22:28

[langgraph] 도구 호출을 리뷰(review)하는 방법

LangGraph 공식문서를 번역한 내용입니다. 필요한 경우 부연 설명을 추가하였고 이해하기 쉽게 예제를 일부 변경하였습니다. 문제가 되면 삭제하겠습니다.

https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/review-tool-calls/

사람이 개입하는(Human-in-the-loop, HIL) 상호작용은 에이전트 시스템에서 매우 중요하다. 일반적인 패턴은 특정 도구 호출 후에 사람이 개입하는 단계를 추가하는 것이다. 이러한 도구 호출은 종종 함수 호출이나 정보 저장으로 이어진다. 예를 들면 다음과 같다.

  • SQL을 실행하는 도구 호출, 이 후 도구에 의해 실행됨
  • 요약을 생성하는 도구 호출, 이 후 그래프의 상태에 저장됨

도구 호출을 사용하는 것은 실제 도구를 호출하든 아니든 일반적 방법이다.

여기서 구현하고 싶은 상호작용은 다음과 같다.

  1. 도구 호출을 승인하고 계속 진행
  2. 도구 호출을 수동으로 수정하고 계속 진행
  3. 자연어 피드백을 주고, 이를 에이전트로 전달한 후 계속 진행하지 않음

LangGraph에서 이를 구현할 수 있는데, 여기서는 중단점을 사용한다. 중단점은 특정 단계 전에 그래프 실행을 중단하도록 하여, 해당 지점에서 그래프 상태를 수동으로 업데이트하고 위의 세 가지 옵션 중 하나를 선택할 수 있게 해준다.

준비

우선, 필요한 패키지를 설치하자.

pip install langgraph langchain_openai

기본적인 사용법

구현하기 위해 간단한 그래프를 설정해 보자. 먼저, 어떤 행동을 취할지 결정하는 LLM 호출이 있다. 그 다음에는 Human 노드로 이동한다. 이 노드는 실제로 아무 작업도 수행하지 않지만, 이 노드 전에 그래프를 중단하고 상태를 업데이트하는 것이 목적이다. 그 후 상태를 확인하고, LLM으로 다시 라우팅하거나 올바른 도구로 라우팅한다.

이제 실행해보자.

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict, Literal
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.tools import tool
from langchain_core.messages import AIMessage
from IPython.display import Image, display

load_dotenv()


@tool
def weather_search(city: str):
    """Search for the weather"""
    print("----")
    print(f"{city}을 검색하고 있어요!")
    print("----")
    return "Sunny!"


model = ChatOpenAI(model_name="gpt-4o-mini").bind_tools([weather_search])


class State(MessagesState):
    """Simple state."""


def call_llm(state):
    return {"messages": [model.invoke(state["messages"])]}


def human_review_node(state):
    pass


def run_tool(state):
    new_messages = []
    tools = {"weather_search": weather_search}
    tool_calls = state["messages"][-1].tool_calls
    for tool_call in tool_calls:
        tool = tools[tool_call["name"]]
        result = tool.invoke(tool_call["args"])
        new_messages.append(
            {
                "role": "tool",
                "name": tool_call["name"],
                "content": result,
                "tool_call_id": tool_call["id"],
            }
        )
    return {"messages": new_messages}


def route_after_llm(state) -> Literal[END, "human_review_node"]:
    if len(state["messages"][-1].tool_calls) == 0:
        return END
    else:
        return "human_review_node"


def route_after_human(state) -> Literal["run_tool", "call_llm"]:
    if isinstance(state["messages"][-1], AIMessage):
        return "run_tool"
    else:
        return "call_llm"


builder = StateGraph(State)
builder.add_node(call_llm)
builder.add_node(run_tool)
builder.add_node(human_review_node)
builder.add_edge(START, "call_llm")
builder.add_conditional_edges("call_llm", route_after_llm)
builder.add_conditional_edges("human_review_node", route_after_human)
builder.add_edge("run_tool", "call_llm")

memory = MemorySaver()

graph = builder.compile(checkpointer=memory, interrupt_before=["human_review_node"])

display(
    Image(
        graph.get_graph().draw_mermaid_png(
            output_file_path="how-to-review-tool-calls.png"
        )
    )
)

리뷰 없는 예시

도구 호출이 없어서 리뷰가 필요 없는 예시를 살펴보자.

# Input
initial_input = {"messages": [{"role": "user", "content": "안녕!"}]}

# Thread
thread = {"configurable": {"thread_id": "1"}}

for event in graph.stream(initial_input, thread, stream_mode="values"):
    print(event)
{'messages': [HumanMessage(content='안녕!', additional_kwargs={}, response_metadata={}, id='9eb97603-d399-44dc-bc37-b32a8611d676')]}
{'messages': [HumanMessage(content='안녕!', additional_kwargs={}, response_metadata={}, id='9eb97603-d399-44dc-bc37-b32a8611d676'), AIMessage(content='안녕하세요! 어떻게 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 45, 'total_tokens': 56, '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-mini-2024-07-18', 'system_fingerprint': 'fp_3de1288069', 'finish_reason': 'stop', 'logprobs': None}, id='run-ca7cd2ab-df36-436f-91d8-0e0471c986c3-0', usage_metadata={'input_tokens': 45, 'output_tokens': 11, 'total_tokens': 56, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

상태를 확인하면, 작업이 완료된 것을 알 수 있다.

print("Pending Executions!")
print(graph.get_state(thread).next)
Pending Executions!
()

승인 도구를 가진 예시

이제 도구 호출을 승인하는 모습이 어떻게 보이는지 살펴보자.

initial_input = {"messages": [{"role": "user", "content": "서울 날씨 어때?"}]}

thread = {"configurable": {"thread_id": "2"}}

for event in graph.stream(initial_input, thread, stream_mode="values"):
    print(event)
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='ed4d4a97-2aca-4c34-b0c2-ae8233038ec4')]}
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='ed4d4a97-2aca-4c34-b0c2-ae8233038ec4'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_awwt2ptdLFl293xpQKUZwXIf', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-58b6e6b0-5df4-4579-b84c-fcffb61673ff-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_awwt2ptdLFl293xpQKUZwXIf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

지금 확인해보면, 그것이 사람의 검토를 기다리고 있다는 것을 알 수 있다.

print("Pending Executions!")
print(graph.get_state(thread).next)
Pending Executions!
('human_review_node',)

툴 호출을 승인하려면 수정 없이 스레드를 계속 진행하면 된다. 이를 위해 입력 없이 새 실행을 생성하면 된다.

for event in graph.stream(None, thread, stream_mode="values"):
    print(event)
----
서울을 검색하고 있어요!
----
{
  'messages': [
    HumanMessage(content=
    '서울 날씨 어때?',
    ...),
    AIMessage(content=
    '',
    ...),
    tool_calls=
    [
      {
        'name': 'weather_search',
        'args': {
          'city': '서울'
        },
        'id': 'call_awwt2ptdLFl293xpQKUZwXIf',
        'type': 'tool_call'
      }
    ],
    ...,
    ToolMessage(content=
    'Sunny!',
    name=
    'weather_search',
    id=
    '215074da-5613-4f9a-a65a-dbf98332498f',
    tool_call_id=
    'call_awwt2ptdLFl293xpQKUZwXIf'
    )
  ]
}
{
  'messages': [
    HumanMessage(content=
    '서울 날씨 어때?',
    ...,
    AIMessage(content=
    '',
    additional_kwargs=
    {
      'tool_calls': [
        {
          'id': 'call_awwt2ptdLFl293xpQKUZwXIf',
          'function': {
            'arguments': '{"city":"서울"}',
            'name': 'weather_search'
          },
          'type': 'function'
        }
      ],
      'refusal': None
    },
    ...,
    tool_calls=
    [
      {
        'name': 'weather_search',
        'args': {
          'city': '서울'
        },
        'id': 'call_awwt2ptdLFl293xpQKUZwXIf',
        'type': 'tool_call'
      }
    ],
    ...,
    ToolMessage(content=
    'Sunny!',
    name=
    'weather_search',
    id=
    '215074da-5613-4f9a-a65a-dbf98332498f',
    tool_call_id=
    'call_awwt2ptdLFl293xpQKUZwXIf'
    ),
    AIMessage(content=
    '서울의 날씨는 맑습니다!',
    additional_kwargs=
    {
      'refusal': None
    },
    ...,
  ]
}

도구 호출 수정

이제 툴 호출을 수정해보자. 예를 들어, 일부 매개변수를 변경하거나(심지어 호출된 툴을 변경할 수도 있다) 그런 다음 해당 툴을 실행하는 경우이다.

initial_input = {"messages": [{"role": "user", "content": "서울 날씨 어때?"}]}

thread = {"configurable": {"thread_id": "5"}}

for event in graph.stream(initial_input, thread, stream_mode="values"):
    print(event)
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='31db76fd-47f9-43ef-9165-98c0ce864926')]}
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='31db76fd-47f9-43ef-9165-98c0ce864926'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-e76eccc9-80a7-4dbb-9fd0-90a70644bbc9-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
print("Pending Executions!")
print(graph.get_state(thread).next)
Pending Executions!
('human_review_node',)

이를 위해 먼저 상태를 업데이트해야 한다. 업데이트하려는 메시지의 ID와 동일한 ID를 가진 메시지를 전달하면 해당 메시지가 덮어쓰기 된다. 이는 동일한 ID를 가진 메시지를 교체하는 리듀서를 사용하기 때문에 가능한데, 이에 대한 자세한 내용은 여기에서 확인할 수 있다.

# 변경하려는 메시지의 ID를 가져오려면 현재 상태를 가져와야 한다.
state = graph.get_state(thread)
print("Current State:")
print(state.values)
print("\nCurrent Tool Call ID:")
current_content = state.values["messages"][-1].content
current_id = state.values["messages"][-1].id
tool_call_id = state.values["messages"][-1].tool_calls[0]["id"]
print(tool_call_id)

# 이제 대체 도구 호출을 구성해야 한다.
# 인수를 `서울, 대한민국`으로 변경할 것이다.
# 어떤 수의 인수나 도구 이름을 변경할 수 있다는 점에 유의하자.
new_message = {
    "role": "assistant",
    "content": current_content,
    "tool_calls": [
        {
            "id": tool_call_id,
            "name": "weather_search",
            "args": {"city": "서울, 대한민국"},
        }
    ],
    # 이건 중요하다 - 이것은 대체하는 메시지와 동일해야 한다!
    # 그렇지 않으면 별도의 메시지로 표시된다.
    "id": current_id,
}
graph.update_state(
    thread,
    {"messages": [new_message]},
    as_node="human_review_node",
)

for event in graph.stream(None, thread, stream_mode="values"):
    print(event)
Current State:
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='31db76fd-47f9-43ef-9165-98c0ce864926'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-e76eccc9-80a7-4dbb-9fd0-90a70644bbc9-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

Current Tool Call ID:
call_F6IiIWx1hKTANI5wli7GH2eW
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='31db76fd-47f9-43ef-9165-98c0ce864926'), AIMessage(content='', additional_kwargs={}, response_metadata={}, id='run-e76eccc9-80a7-4dbb-9fd0-90a70644bbc9-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'type': 'tool_call'}])]}
----
서울, 대한민국을 검색하고 있어요!
----
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='31db76fd-47f9-43ef-9165-98c0ce864926'), AIMessage(content='', additional_kwargs={}, response_metadata={}, id='run-e76eccc9-80a7-4dbb-9fd0-90a70644bbc9-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'type': 'tool_call'}]), ToolMessage(content='Sunny!', name='weather_search', id='22e0952b-7249-4a85-acbb-9a8753b04e22', tool_call_id='call_F6IiIWx1hKTANI5wli7GH2eW')]}
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='31db76fd-47f9-43ef-9165-98c0ce864926'), AIMessage(content='', additional_kwargs={}, response_metadata={}, id='run-e76eccc9-80a7-4dbb-9fd0-90a70644bbc9-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_F6IiIWx1hKTANI5wli7GH2eW', 'type': 'tool_call'}]), ToolMessage(content='Sunny!', name='weather_search', id='22e0952b-7249-4a85-acbb-9a8753b04e22', tool_call_id='call_F6IiIWx1hKTANI5wli7GH2eW'), AIMessage(content='서울의 날씨는 맑습니다!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 74, 'total_tokens': 84, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-4feb0ad1-39ce-4f25-819e-75350768970f-0', usage_metadata={'input_tokens': 74, 'output_tokens': 10, 'total_tokens': 84, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

도구 호출에 피드백 하기

때로는 도구 호출을 실행하지 않거나 사용자가 도구 호출을 수동으로 수정하는 것을 원하지 않을 수 있다. 이 경우 사용자의 자연어 피드백을 받는 것이 더 나을 수 있다. 그런 다음 이 피드백을 도구 호출의 가짜 결과로 삽입할 수 있다.

이를 수행하는 방법에는 여러 가지가 있다.

  • 상태에 새로운 메시지를 추가하여 "도구 호출 결과"를 나타낼 수 있다.
  • 두 개의 새로운 메시지를 상태에 추가할 수 있다. 하나는 도구 호출에서 발생한 "오류"를 나타내고, 다른 하나는 피드백을 나타내는 HumanMessage이다.

두 방법은 모두 상태에 메시지를 추가하는 방식에서 유사하지만, human_node 이후의 로직과 다양한 유형의 메시지를 처리하는 방식에서 차이가 있다.

이번 예제에서는 피드백을 나타내는 단일 도구 호출 메시지를 추가할 것이다. 이를 실습해보자.

initial_input = {"messages": [{"role": "user", "content": "서울 날씨 어때?"}]}

thread = {"configurable": {"thread_id": "6"}}

for event in graph.stream(initial_input, thread, stream_mode="values"):
    print(event)
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='c486b0d8-b280-4e83-ab91-286a008e5e47')]}
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='c486b0d8-b280-4e83-ab91-286a008e5e47'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ieMC3Fqu30mxhPfQ8MtlZ8aD', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-c1d0959f-e3aa-45f1-b514-7cdaeab015cb-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_ieMC3Fqu30mxhPfQ8MtlZ8aD', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
print("Pending Executions!")
print(graph.get_state(thread).next)
Pending Executions!
('human_review_node',)

이를 수행하려면 먼저 상태를 업데이트해야 한다. 이를 위해 응답하려는 도구 호출의 동일한 도구 호출 ID를 가진 메시지를 전달할 수 있다. 위의 ID와는 다른 ID임을 유의하자.

state = graph.get_state(thread)
print("Current State:")
print(state.values)
print("\nCurrent Tool Call ID:")
tool_call_id = state.values["messages"][-1].tool_calls[0]["id"]
print(tool_call_id)

new_message = {
    "role": "tool",
    # 자연어 피드백이다.
    "content": "사용자 요청 변경: 국가도 함께 전달",
    "name": "weather_search",
    "tool_call_id": tool_call_id,
}
graph.update_state(
    thread,
    {"messages": [new_message]},
    as_node="human_review_node",
)

for event in graph.stream(None, thread, stream_mode="values"):
    print(event)
Current State:
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='90437134-46fb-414f-926c-cdedaa630fa0'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7107924-7fa4-47dd-83f9-914382969cae-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

Current Tool Call ID:
call_b0JTEG1k8XeziTOZhrltMF0N
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='90437134-46fb-414f-926c-cdedaa630fa0'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7107924-7fa4-47dd-83f9-914382969cae-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='사용자 요청 변경: 국가도 함께 전달', name='weather_search', id='91392f4c-e017-4b02-8162-4fe680675869', tool_call_id='call_b0JTEG1k8XeziTOZhrltMF0N')]}
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='90437134-46fb-414f-926c-cdedaa630fa0'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7107924-7fa4-47dd-83f9-914382969cae-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='사용자 요청 변경: 국가도 함께 전달', name='weather_search', id='91392f4c-e017-4b02-8162-4fe680675869', tool_call_id='call_b0JTEG1k8XeziTOZhrltMF0N'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'function': {'arguments': '{"city":"서울, 대한민국"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 79, 'total_tokens': 95, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b113e051-8963-4f85-98fc-3ebbf7879a2d-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'type': 'tool_call'}], usage_metadata={'input_tokens': 79, 'output_tokens': 16, 'total_tokens': 95, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

이제 다른 중단점에 도달한 것을 볼 수 있다. 왜냐하면 모델로 돌아가서 호출할 내용을 전혀 새로운 예측으로 받았기 때문이다. 이제 이것을 승인하고 계속 진행한다.

print("Pending Executions!")
print(graph.get_state(thread).next)

for event in graph.stream(None, thread, stream_mode="values"):
    print(event)
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='90437134-46fb-414f-926c-cdedaa630fa0'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7107924-7fa4-47dd-83f9-914382969cae-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='사용자 요청 변경: 국가도 함께 전달', name='weather_search', id='91392f4c-e017-4b02-8162-4fe680675869', tool_call_id='call_b0JTEG1k8XeziTOZhrltMF0N'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'function': {'arguments': '{"city":"서울, 대한민국"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 79, 'total_tokens': 95, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b113e051-8963-4f85-98fc-3ebbf7879a2d-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'type': 'tool_call'}], usage_metadata={'input_tokens': 79, 'output_tokens': 16, 'total_tokens': 95, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
----
서울, 대한민국을 검색하고 있어요!
----
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='90437134-46fb-414f-926c-cdedaa630fa0'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7107924-7fa4-47dd-83f9-914382969cae-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='사용자 요청 변경: 국가도 함께 전달', name='weather_search', id='91392f4c-e017-4b02-8162-4fe680675869', tool_call_id='call_b0JTEG1k8XeziTOZhrltMF0N'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'function': {'arguments': '{"city":"서울, 대한민국"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 79, 'total_tokens': 95, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b113e051-8963-4f85-98fc-3ebbf7879a2d-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'type': 'tool_call'}], usage_metadata={'input_tokens': 79, 'output_tokens': 16, 'total_tokens': 95, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='Sunny!', name='weather_search', id='87fd3de0-aeb8-4577-93a6-d1959fd14b91', tool_call_id='call_hABBP0o1UwHLYyPeysyFsQsa')]}
{'messages': [HumanMessage(content='서울 날씨 어때?', additional_kwargs={}, response_metadata={}, id='90437134-46fb-414f-926c-cdedaa630fa0'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'function': {'arguments': '{"city":"서울"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 48, 'total_tokens': 62, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7107924-7fa4-47dd-83f9-914382969cae-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울'}, 'id': 'call_b0JTEG1k8XeziTOZhrltMF0N', 'type': 'tool_call'}], usage_metadata={'input_tokens': 48, 'output_tokens': 14, 'total_tokens': 62, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='사용자 요청 변경: 국가도 함께 전달', name='weather_search', id='91392f4c-e017-4b02-8162-4fe680675869', tool_call_id='call_b0JTEG1k8XeziTOZhrltMF0N'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'function': {'arguments': '{"city":"서울, 대한민국"}', 'name': 'weather_search'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 79, 'total_tokens': 95, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b113e051-8963-4f85-98fc-3ebbf7879a2d-0', tool_calls=[{'name': 'weather_search', 'args': {'city': '서울, 대한민국'}, 'id': 'call_hABBP0o1UwHLYyPeysyFsQsa', 'type': 'tool_call'}], usage_metadata={'input_tokens': 79, 'output_tokens': 16, 'total_tokens': 95, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='Sunny!', name='weather_search', id='87fd3de0-aeb8-4577-93a6-d1959fd14b91', tool_call_id='call_hABBP0o1UwHLYyPeysyFsQsa'), AIMessage(content='서울의 날씨는 맑습니다!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 105, 'total_tokens': 115, '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-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-04f817ca-fea6-40e0-aea4-2541c4a60f09-0', usage_metadata={'input_tokens': 105, 'output_tokens': 10, 'total_tokens': 115, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유