Open in Colab Open in LangChain Academy

체인

복습

이전 시간에 노드, 일반 에지, 조건부 에지로 구성된 간단한 그래프를 만들었습니다.

목표

이번에는 4가지 개념을 결합한 간단한 체인을 구축해 보겠습니다:

%%capture --no-stderr
%pip install --quiet -U langchain_openai langchain_core langgraph

Messages(메시지)

챗 모델은 대화 내 다양한 역할을 포착하는 messages를 사용할 수 있습니다.

LangChain은 HumanMessage, AIMessage, SystemMessage, ToolMessage 등 다양한 메시지 유형을 지원합니다.

이들은 각각 사용자의 메시지, 채팅 모델의 메시지, 채팅 모델의 행동 지시 메시지, 도구 호출 메시지를 나타냅니다.

메시지 목록을 생성해 보겠습니다.

각 메시지는 다음과 같은 정보를 포함할 수 있습니다:

  • content - 메시지 내용
  • name - 선택적으로 메시지 작성자
  • response_metadata - 선택적으로 메타데이터 사전 (예: AIMessages의 경우 모델 제공자가 자주 채움)
from pprint import pprint
from langchain_core.messages import AIMessage, HumanMessage
 
messages = [AIMessage("그러니까 해양 포유류를 연구하고 있다고 하셨나요?", name="Model")]
messages.append(HumanMessage("네, 맞아요.", name="Lance"))
messages.append(AIMessage("좋아요, 무엇을 배우고 싶으신가요?", name="Model"))
messages.append(
    HumanMessage(
        "미국에서 범고래를 관찰하기 가장 좋은 장소에 대해 알고 싶습니다.", name="Lance"
    )
)
 
for m in messages:
    m.pretty_print()
================================== Ai Message ==================================
Name: Model

그러니까 해양 포유류를 연구하고 있다고 하셨나요?
================================ Human Message =================================
Name: Lance

네, 맞아요.
================================== Ai Message ==================================
Name: Model

좋아요, 무엇을 배우고 싶으신가요?
================================ Human Message =================================
Name: Lance

미국에서 범고래를 관찰하기 가장 좋은 장소에 대해 알고 싶습니다.

채팅 모델

Chat models은 메시지 시퀀스를 입력으로 사용할 수 있으며, 앞서 논의한 바와 같이 다양한 메시지 유형을 지원합니다.

선택할 수 있는 모델이 많습니다! OpenAI와 함께 작업해 보겠습니다.

OPENAI_API_KEY가 설정되어 있는지 확인하세요. 설정되어 있지 않으면 입력하라는 메시지가 표시됩니다.

import os, getpass
from dotenv import load_dotenv
 
load_dotenv("../.env", override=True, verbose=True)
 
 
def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")
 
 
_set_env("OPENAI_API_KEY")

채팅 모델을 로드하고 메시지 목록으로 호출할 수 있습니다.

결과는 특정 response_metadata를 가진 AIMessage임을 확인할 수 있습니다.

from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-4o")
result = llm.invoke(messages)
type(result)
langchain_core.messages.ai.AIMessage
result
AIMessage(content='미국에서 범고래를 관찰하기 좋은 장소로는 다음과 같은 곳들이 있습니다:\n\n1. **샌환 제도(San Juan Islands), 워싱턴주**: 이 지역은 범고래를 관찰하기에 가장 유명한 장소 중 하나입니다. 특히 여름철에 많은 범고래가 이 지역을 찾습니다. 일대에서는 배를 타고 관찰할 수 있는 투어도 많이 운영되고 있습니다.\n\n2. **올림픽 해안 국립공원(Olympic National Park), 워싱턴주**: 이곳은 태평양을 접하고 있어 해안가에서 범고래를 볼 수 있는 기회가 있습니다. 특히 스프링 웨일링 시즌에 주목할 만합니다.\n\n3. **푸겟 사운드(Puget Sound), 워싱턴주**: 이 지역도 범고래를 관찰하기에 적합한 장소입니다. 이곳에서는 종종 캐하기, 회색고래 같은 다른 해양생물도 볼 수 있습니다.\n\n4. **몬터레이 베이(Monterey Bay), 캘리포니아**: 비록 주로 회색고래와 돌고래로 유명하지만, 가끔씩 몬터레이 베이에서도 범고래를 볼 수 있습니다. 이곳에서는 다양한 바다 생물을 관찰할 수 있는 투어가 많이 있습니다.\n\n5. **캘리포니아 해안**: 남부 캘리포니아의 해안을 따라, 특히 봄철과 가을철 이동 시즌 동안 범고래를 가끔씩 볼 수 있습니다.\n\n범고래를 관찰할 때는 지역 생태 시스템과 해양 생물에 미치는 영향을 최소화하면서 자연을 존중하는 태도를 갖는 것이 중요합니다. 즐거운 관찰 경험이 되기를 바랍니다!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 392, 'prompt_tokens': 79, 'total_tokens': 471, '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_f33640a400', 'id': 'chatcmpl-CLNuPQWIumG8BqZw2Ukxm2T3zWzZt', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--105c53ab-2eea-445b-9bfe-3f19ec692ef8-0', usage_metadata={'input_tokens': 79, 'output_tokens': 392, 'total_tokens': 471, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
result.response_metadata
{'token_usage': {'completion_tokens': 392,
  'prompt_tokens': 79,
  'total_tokens': 471,
  '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_f33640a400',
 'id': 'chatcmpl-CLNuPQWIumG8BqZw2Ukxm2T3zWzZt',
 'service_tier': 'default',
 'finish_reason': 'stop',
 'logprobs': None}

Tools(도구)

도구는 모델이 외부 시스템과 상호작용해야 할 때 유용합니다.

외부 시스템(예: API)은 자연어 대신 특정 입력 스키마나 페이로드를 요구하는 경우가 많습니다.

예를 들어 API를 도구로 바인딩하면, 모델이 필요한 입력 스키마를 인식하도록 합니다.

모델은 사용자의 자연어 입력에 따라 도구를 호출할지 선택합니다.

그리고 해당 도구의 스키마를 준수하는 출력을 반환합니다.

많은 LLM 공급자가 도구 호출을 지원하며, LangChain의 도구 호출 인터페이스는 간단합니다.

ChatModel.bind_tools(function)에 어떤 Python function든 전달하기만 하면 됩니다.

도구 호출의 간단한 예시를 살펴보겠습니다!

multiply 함수가 우리의 도구입니다.

def multiply(a: int, b: int):
    """Multiply a and b.
 
    Args:
        a: first int
        b: second int
    """
    return a * b
 
 
llm_with_tools = llm.bind_tools([multiply])

입력값(예: "2에 3을 곱하면 얼마인가요?")을 전달하면 도구 호출이 반환되는 것을 확인할 수 있습니다.

이 도구 호출은 호출할 함수의 이름과 함께 함수의 입력 스키마에 맞는 특정 인수를 포함합니다.

{'arguments': '{"a":2,"b":3}', 'name': 'multiply'}
tool_cool = llm_with_tools.invoke(
    [HumanMessage("2에 3을 곱하면 얼마인가요?", name="Lance")]
)
tool_cool.tool_calls
[{'name': 'multiply',
  'args': {'a': 2, 'b': 3},
  'id': 'call_S8ST5hiD6wgNtgqsB69eT1sa',
  'type': 'tool_call'}]

메시지를 상태로 사용하기

이러한 기반이 마련되었으므로 이제 그래프 상태에서 messages를 사용할 수 있습니다.

MessagesState 상태를 단일 키 messages를 가진 TypedDict로 정의해 보겠습니다.

messages는 위에서 정의한 대로 단순히 메시지 목록입니다(예: HumanMessage 등).

from typing import TypedDict
from langchain_core.messages import AnyMessage
 
 
class MessageState(TypedDict):
    message: list[AnyMessage]

Reducers(리듀서)

이제 사소한 문제가 생겼습니다!

앞서 논의한 대로, 각 노드는 상태 키 messages에 대한 새 값을 반환합니다.

하지만 이 새 값은 이전 messages 값을 덮어씁니다.

그래프가 실행될 때, 우리는 messages 상태 키에 메시지를 추가하고 싶습니다.

이를 해결하기 위해 리듀서 함수를 사용할 수 있습니다.

리듀서를 사용하면 상태 업데이트가 수행되는 방식을 지정할 수 있습니다.

리듀서 함수가 지정되지 않으면, 앞서 본 것처럼 키에 대한 업데이트가 기존 값을 덮어씁니다.

그러나 메시지를 추가하려면 미리 정의된 add_messages 리듀서를 사용할 수 있습니다.

이렇게 하면 모든 메시지가 기존 메시지 목록에 추가됩니다.

messages 키에 add_messages 리듀서 함수를 메타데이터로 주석 처리하기만 하면 됩니다.

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
 
 
class MessageState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

그래프 상태에 메시지 목록을 갖는 것이 매우 흔하기 때문에, LangGraph에는 미리 구축된 MessagesState가 있습니다!

MessagesState는 다음과 같이 정의됩니다:

  • 미리 구축된 단일 messages 키로 구성
  • AnyMessage 객체들의 리스트 형태
  • add_messages 리듀서를 사용

위에서 보여준 것처럼 커스텀 TypedDict를 정의하는 것보다 덜 장황하기 때문에 일반적으로 MessagesState를 사용할 것입니다.

from langgraph.graph import MessagesState
 
 
class CustomState(MessagesState):
    # messages 외에 필요한 키를 추가하세요. messages는 미리 빌드되어 있습니다.
    pass

좀 더 깊이 들어가서, add_messages 리듀서가 독립적으로 어떻게 작동하는지 살펴볼 수 있습니다.

# Initial state
initial_message = [
    AIMessage("안녕하세요! 무엇을 도와드릴까요?"),
    HumanMessage("해양 생물학에 관한 정보를 찾고 있습니다."),
]
 
# New message to add
new_message = AIMessage(
    "물론 도와드릴 수 있어요. 구체적으로 어떤 부분에 관심이 있으신가요?"
)
 
# Test
add_messages(initial_message, [new_message])
[AIMessage(content='안녕하세요! 무엇을 도와드릴까요?', additional_kwargs={}, response_metadata={}, id='e29e4caa-516f-414d-bdfe-0c2437637a24'),
 HumanMessage(content='해양 생물학에 관한 정보를 찾고 있습니다.', additional_kwargs={}, response_metadata={}, id='6c2ed04b-8099-432c-98d2-37e9c5855eed'),
 AIMessage(content='물론 도와드릴 수 있어요. 구체적으로 어떤 부분에 관심이 있으신가요?', additional_kwargs={}, response_metadata={}, id='da3ad4f1-4218-4e31-b61b-fa27806fc51c')]

그래프

이제 MessagesState를 그래프와 함께 사용해 보겠습니다.

from langgraph.graph import StateGraph
 
 
def agent(state: CustomState) -> CustomState:
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}
 
 
builder = StateGraph(CustomState)
builder.add_node("agent", agent)
builder.set_entry_point("agent")
builder.set_finish_point("agent")
graph = builder.compile()
from IPython.display import display, Image
 
# display(Image(graph.get_graph().draw_mermaid_png()))
print(graph.get_graph().draw_mermaid())
---
config:
  flowchart:
    curve: linear
---
graph TD;
	__start__([<p>__start__</p>]):::first
	llm(llm)
	__end__([<p>__end__</p>]):::last
	__start__ --> llm;
	llm --> __end__;
	classDef default fill:#f2f0ff,line-height:1.2
	classDef first fill-opacity:0
	classDef last fill:#bfb6fc

도구 없이 호출하기

안녕!를 입력하면 LLM은 도구 호출 없이 응답합니다.

response = graph.invoke({"messages": [HumanMessage("안녕!")]})
 
for m in response["messages"]:
    m.pretty_print()
================================ Human Message =================================

안녕!
================================== Ai Message ==================================

안녕하세요! 어떻게 도와드릴까요?

도구 사용하기

LLM은 입력 또는 작업이 해당 도구가 제공하는 기능을 필요로 한다고 판단할 때 해당 도구를 사용하기로 선택합니다.

response = graph.invoke({"messages": [HumanMessage("2 곱하기 3은?")]})
 
for m in response["messages"]:
    m.pretty_print()
================================ Human Message =================================

2 곱하기 3은?
================================== Ai Message ==================================
Tool Calls:
  multiply (call_Vk9dJEkcSPIFXLLeyReOwpHz)
 Call ID: call_Vk9dJEkcSPIFXLLeyReOwpHz
  Args:
    a: 2
    b: 3