모델

LLM은 인간처럼 텍스트를 해석하고 생성할 수 있는 강력한 AI 도구입니다. 콘텐츠 작성, 언어 번역, 요약, 질문 답변 등 각 작업에 대한 전문적인 훈련 없이도 다양한 작업을 수행할 수 있습니다.

텍스트 생성 외에도 많은 모델이 다음 기능을 지원합니다:

  • 도구 호출 - 외부 도구(데이터베이스 쿼리 또는 API 호출 등)를 호출하고 그 결과를 응답에 사용합니다.
  • 구조화된 출력 - 모델의 응답이 정의된 형식을 따르도록 제한됩니다.
  • 멀티모달 - 이미지, 오디오, 비디오 등 텍스트 이외의 데이터를 처리하고 반환합니다.
  • 추론 - 모델이 결론에 도달하기 위해 다단계 추론을 수행합니다.

모델은 에이전트의 추론 엔진입니다. 어떤 도구를 호출할지, 결과를 어떻게 해석할지, 언제 최종 답변을 제공할지 결정하는 에이전트의 의사결정 프로세스를 주도합니다.

선택하는 모델의 품질과 기능은 에이전트의 신뢰성과 성능에 직접적인 영향을 미칩니다. 모델마다 뛰어난 작업이 다릅니다. 복잡한 지시사항을 따르는 데 뛰어난 모델도 있고, 구조화된 추론에 능한 모델도 있으며, 더 많은 정보를 처리하기 위해 더 큰 컨텍스트 윈도우를 지원하는 모델도 있습니다.

LangChain의 표준 모델 인터페이스는 다양한 제공업체 통합에 대한 액세스를 제공하므로 모델을 쉽게 실험하고 전환하여 사용 사례에 가장 적합한 모델을 찾을 수 있습니다.

⚠️ 참고: 제공업체별 통합 정보 및 기능에 대해서는 해당 제공업체의 통합 페이지를 참조하세요.

설정

from dotenv import load_dotenv
from langchain_teddynote import logging
from libs.helpers import pretty_print, stream_print
 
 
load_dotenv("../.env", override=True)
 
logging.langsmith("LangChain-V1-Tutorial")
LangSmith 추적을 시작합니다.
[프로젝트명]
LangChain-V1-Tutorial

기본 사용법

모델은 두 가지 방식으로 활용할 수 있습니다:

  1. 에이전트와 함께 - 에이전트를 생성할 때 모델을 동적으로 지정할 수 있습니다.
  2. 독립 실행형 - 에이전트 프레임워크 없이 텍스트 생성, 분류 또는 추출과 같은 작업을 위해 모델을 직접(에이전트 루프 외부에서) 호출할 수 있습니다.

두 컨텍스트 모두에서 동일한 모델 인터페이스가 작동하므로 간단하게 시작하여 필요에 따라 더 복잡한 에이전트 기반 워크플로우로 확장할 수 있는 유연성을 제공합니다.

모델 초기화

LangChain에서 독립 실행형 모델을 시작하는 가장 쉬운 방법은 init_chat_model을 사용하여 선택한 provider(제공업체)에서 모델을 초기화하는 것입니다.

from langchain.chat_models import init_chat_model
 
 
model = init_chat_model("openai:gpt-4.1")

자세한 내용은 init_chat_model을 참조하세요. 모델 parameter(매개변수)를 전달하는 방법에 대한 정보도 포함되어 있습니다.

주요 메서드

  • invoke: 모델은 메시지를 입력으로 받아 완전한 응답을 생성한 후 메시지를 출력합니다.
  • stream: 모델을 호출하되, 실시간으로 생성되는 출력을 스트리밍합니다.
  • batch: 여러 요청을 일괄 처리하여 모델에 전송하여 더 효율적으로 처리합니다.

Parameter(매개변수)

chat model(채팅 모델)은 동작을 구성하는 데 사용할 수 있는 매개변수를 사용합니다. 지원되는 매개변수의 전체 집합은 모델 및 제공업체에 따라 다르지만 표준 매개변수는 다음과 같습니다:

  • model(필수): 제공업체와 함께 사용하려는 특정 모델의 이름 또는 식별자입니다.
  • api_key: 모델 제공업체의 인증에 필요한 키입니다. 일반적으로 모델에 대한 액세스 권한을 등록할 때 발급됩니다. environment variable(환경 변수)을 설정하여 액세스하는 경우가 많습니다.
  • temperature: 모델 출력의 무작위성을 제어합니다. 높은 값은 응답을 더 창의적으로 만들고, 낮은 값은 더 결정적으로 만듭니다.
  • timeout: 요청을 취소하기 전에 모델의 응답을 기다리는 최대 시간(초)입니다.
  • max_tokens: 응답의 총 token(토큰) 수를 제한하여 출력 길이를 효과적으로 제어합니다.
  • max_retries: 네트워크 시간 초과 또는 rate limit(속도 제한)과 같은 문제로 인해 요청이 실패할 경우 시스템이 요청을 재전송하는 최대 시도 횟수입니다.
model = init_chat_model(
    "openai:gpt-4.1-mini",
    temperature=0.7,
    timeout=30,
    max_tokens=1000,
    max_retries=2,
)

⚠️ 참고: 각 chat model(채팅 모델) 통합에는 제공업체별 기능을 제어하는 데 사용되는 추가 매개변수가 있을 수 있습니다. 예를 들어, ChatOpenAI에는 OpenAI Responses API 또는 Completions API를 사용할지 여부를 결정하는 use_responses_api가 있습니다.

특정 채팅 모델이 지원하는 모든 매개변수를 찾으려면 채팅 모델 통합 페이지를 참고하세요.

Invocation(호출)

chat model(채팅 모델)은 출력을 생성하기 위해 호출되어야 합니다. 각각 다른 사용 사례에 적합한 세 가지 주요 호출 메서드가 있습니다.

Invoke

모델을 호출하는 가장 간단한 방법은 단일 메시지 또는 메시지 목록과 함께 invoke()를 사용하는 것입니다.

response = model.invoke("앵무새는 왜 화려한 깃털을 가지고 있을까요?")
response.pretty_print()
================================== Ai Message ==================================

앵무새가 화려한 깃털을 가진 이유는 여러 가지가 있습니다:

1. **짝짓기와 번식**: 화려한 깃털은 건강하고 유전적으로 우수한 개체임을 나타내는 신호로 작용합니다. 밝고 선명한 색깔은 다른 앵무새들에게 매력적으로 보이며, 짝을 찾는 데 중요한 역할을 합니다.

2. **종 간 의사소통**: 깃털의 색깔과 무늬는 같은 종 내에서 개체를 구별하거나, 사회적 신호를 전달하는 데 도움을 줍니다.

3. **서식지와 위장**: 일부 앵무새는 화려한 색깔이 주변 환경과 조화를 이루어 위장 효과를 낼 수 있습니다. 예를 들어, 열대 우림의 다채로운 꽃과 잎 사이에 숨을 때 도움이 됩니다.

4. **포식자 회피**: 때로는 눈에 띄는 색깔이 포식자를 혼란스럽게 하거나, 위험 신호를 보내는 역할을 하기도 합니다.

이처럼 앵무새의 화려한 깃털은 생존과 번식에 유리한 여러 기능을 수행합니다.

대화 기록을 나타내기 위해 모델에 메시지 목록을 제공할 수 있습니다. 각 메시지에는 모델이 대화에서 메시지를 보낸 사람을 나타내는 데 사용하는 role(역할)이 있습니다. role, type 및 content에 대한 자세한 내용은 message(메시지) 가이드를 참조하세요.

# Dictionary format
conversation = [
    {
        "role": "system",
        "content": "당신은 한국어를 프랑스어로 번역하는 유용한 어시스턴트입니다.",
    },
    {"role": "user", "content": "번역: 나는 프로그래밍을 좋아합니다."},
    {"role": "assistant", "content": "J'adore la programmation."},
    {"role": "user", "content": "번역: 나는 애플리케이션 구축을 좋아합니다."},
]
 
response = model.invoke(conversation)
response.pretty_print()
================================== Ai Message ==================================

J'aime créer des applications.
# Message objects
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
 
 
conversation = [
    SystemMessage("당신은 한국어를 프랑스어로 번역하는 유용한 어시스턴트입니다."),
    HumanMessage("번역: 나는 프로그래밍을 좋아합니다."),
    AIMessage("J'adore la programmation."),
    HumanMessage("번역: 나는 애플리케이션 구축을 좋아합니다."),
]
 
response = model.invoke(conversation)
response.pretty_print()
================================== Ai Message ==================================

J'aime créer des applications.

Stream

대부분의 모델은 생성되는 동안 출력 콘텐츠를 스트리밍할 수 있습니다. 출력을 점진적으로 표시함으로써 스트리밍은 특히 긴 응답의 경우 사용자 경험을 크게 향상시킵니다.

stream()을 호출하면 생성되는 출력 청크를 생성하는 iterator를 반환합니다. 루프를 사용하여 각 청크를 실시간으로 처리할 수 있습니다:

# 기본 텍스트 스트리밍
for chunk in model.stream("앵무새는 왜 화려한 깃털을 가지고 있을까요?"):
    print(chunk.text, end="", flush=True)
앵무새가 화려한 깃털을 가지고 있는 이유는 주로 다음과 같은 이유들 때문입니다:

1. **짝짓기와 구애**: 화려한 깃털은 건강함과 유전적 우수성을 나타내는 신호로 작용합니다. 밝고 다양한 색깔은 암컷에게 매력적으로 보이며, 좋은 짝을 선택하는 데 도움을 줍니다.

2. **종 간 식별**: 다양한 색깔과 무늬는 같은 종끼리 쉽게 인식할 수 있도록 도와줍니다. 이는 교배 시 종의 혼합을 방지하는 역할을 합니다.

3. **서식지 적응**: 일부 앵무새는 화려한 색이 주변 환경과 어울려 위장 역할을 하기도 합니다. 예를 들어, 열대 우림의 다채로운 색깔과 어우러져 천적에게서 숨기 쉽습니다.

4. **사회적 신호**: 깃털의 색상과 상태는 사회적 지위나 기분을 나타낼 수 있습니다. 예를 들어, 스트레스나 건강 상태가 깃털에 반영되기도 합니다.

요약하면, 앵무새의 화려한 깃털은 생존과 번식에 유리한 여러 기능을 수행하기 위해 진화해 온 특징입니다.
# 도구 호출, 추론, 기타 콘텐츠 스트리밍
reasoning_started = False
reasoning_model = init_chat_model("groq:openai/gpt-oss-20b")
for chunk in reasoning_model.stream("하늘은 무슨 색인가요?"):
    for block in chunk.content_blocks:
        if block["type"] == "reasoning" and (reasoning := block.get("reasoning")):
            if not reasoning_started:
                print("🧠 추론:", end="\n", flush=True)
                reasoning_started = True
            print(reasoning, end="", flush=True)
        elif block["type"] == "tool_call_chunk":
            print(f"\n도구 호출 청크: {block}")
        elif block["type"] == "text":
            if reasoning_started:
                print("\n\n💬 답변:", end="\n")
                reasoning_started = False
            print(block["text"], end="", flush=True)
        else:
            print(block)  # 그 외
🧠 추론:
The user asks in Korean: "하늘은 무슨 색인가요?" meaning "What color is the sky?" The answer: typically blue. Could mention variations: blue during day, red/orange at sunrise/sunset, gray at overcast, etc. Provide answer in Korean.

💬 답변:
하늘은 보통 **파란색**으로 보입니다.  

- **낮**에는 태양빛이 대기 중의 입자에 산란되어 파란색이 가장 눈에 띕니다.  
- **해질 무렵**이나 **해돋이**에는 태양이 낮은 각도로 비추어 붉은 빛이 길게 퍼지면서 주황색·분홍색·빨간색으로 물듭니다.  
- **구름이 많거나 흐린 날**에는 회색빛을 띠기도 하고, 비가 올 때는 짙은 회색이나 어두운 파란색이 보이기도 합니다.  

따라서 가장 일반적인 상황에서는 하늘이 파란색이라고 할 수 있습니다.

모델이 전체 응답을 생성한 후 단일 AIMessage를 반환하는 invoke()와 달리, stream()은 각각 출력 텍스트의 일부를 포함하는 여러 AIMessageChunk 객체를 반환합니다. 중요한 점은 스트림의 각 청크가 합산을 통해 전체 메시지로 수집되도록 설계되었다는 것입니다.

from langchain_core.messages import AIMessageChunk
 
 
full: None | AIMessageChunk = None
for chunk in model.stream("하늘은 무슨 색인가요? 한문장으로 답변하시오."):
    full = chunk if full is None else full + chunk
    print(full.text)
하
하늘
하늘은
하늘은 대
하늘은 대개
하늘은 대개 파
하늘은 대개 파란
하늘은 대개 파란색
하늘은 대개 파란색입니다
하늘은 대개 파란색입니다.
하늘은 대개 파란색입니다.
하늘은 대개 파란색입니다.
하늘은 대개 파란색입니다.
print(full.content_blocks)
[{'type': 'text', 'text': '하늘은 대개 파란색입니다.'}]

결과 메시지는 invoke()로 생성된 메시지와 동일하게 처리할 수 있습니다. 예를 들어 메시지 기록에 집계하여 대화 컨텍스트로 모델에 다시 전달할 수 있습니다.

⚠️ 중의: 스트리밍은 프로그램의 모든 단계가 청크 스트림을 처리하는 방법을 알고 있는 경우에만 작동합니다. 예를 들어, 스트리밍이 불가능한 애플리케이션은 처리하기 전에 전체 출력을 메모리에 저장해야 하는 애플리케이션입니다.

Batch

독립적인 요청 컬렉션을 모델에 일괄 처리하면 처리를 병렬로 수행할 수 있으므로 성능을 크게 향상시키고 비용을 절감할 수 있습니다.

responses = model.batch(
    [
        "앵무새는 왜 화려한 깃털을 가지고 있나요?",
        "비행기는 어떻게 날 수 있나요?",
        "양자 컴퓨팅이란 무엇인가요?",
    ]
)
for response in responses:
    print(response)
content='앵무새가 화려한 깃털을 가지는 이유는 여러 가지가 있습니다:\n\n1. **짝짓기와 성적 선택**: 화려한 깃털은 건강하고 유전적으로 우수한 개체임을 나타내는 신호로 작용합니다. 밝고 선명한 색깔은 다른 앵무새들에게 매력적으로 보이기 때문에 짝을 찾는 데 유리합니다.\n\n2. **종 내 의사소통**: 깃털의 색상과 무늬는 같은 종 내에서 신호를 주고받는 데 사용됩니다. 예를 들어, 특정 색깔이나 패턴은 공격성, 친화성, 영역 표시 등 다양한 행동을 나타낼 수 있습니다.\n\n3. **서식지 적응**: 일부 앵무새의 화려한 깃털은 주변 환경과 어우러지거나 반대로 눈에 띄어 포식자를 혼란시키는 역할을 할 수도 있습니다.\n\n이처럼 앵무새의 화려한 깃털은 생존과 번식에 중요한 역할을 하는 진화적 결과라고 할 수 있습니다.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 243, 'prompt_tokens': 24, 'total_tokens': 267, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX69EOZmIxgUbcqMdqeTJ9avS03xv', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--1a778415-7c7c-43c4-bd28-d9dc37db5800-0' usage_metadata={'input_tokens': 24, 'output_tokens': 243, 'total_tokens': 267, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
content='비행기가 날 수 있는 원리는 주로 **양력(lift)** 때문입니다. 비행기가 하늘을 나는 과정을 간단히 설명하면 다음과 같습니다:\n\n1. **날개 모양 (에어포일)**  \n   비행기 날개는 위쪽이 둥글고 아래쪽이 평평하거나 덜 둥근 특수한 모양을 가지고 있습니다. 이를 에어포일(airfoil)이라고 합니다.\n\n2. **공기 흐름과 압력 차이**  \n   비행기가 앞으로 움직이면 공기가 날개 위와 아래를 지나갑니다. 날개 위쪽은 둥근 모양 때문에 공기가 더 빠르게 흐르고, 날개 아래쪽은 상대적으로 느리게 흐릅니다. 베르누이의 원리에 따라 빠른 공기 흐름은 압력을 낮추고, 느린 공기 흐름은 압력을 높입니다.\n\n3. **양력 발생**  \n   날개 아래의 공기 압력이 날개 위의 압력보다 높아지면서 위로 밀어 올리는 힘, 즉 양력이 발생합니다. 이 양력이 비행기의 무게를 이겨내면 비행기는 하늘로 떠오를 수 있습니다.\n\n4. **추진력과 항력**  \n   엔진이 비행기를 앞으로 밀어주는 추진력(thrust)이 있어야 하고, 공기 저항인 항력(drag)을 극복해야 합니다. 추진력이 충분히 크면 비행기는 원하는 속도와 높이를 유지할 수 있습니다.\n\n요약하자면, 비행기는 엔진의 추진력으로 앞으로 이동하며, 날개의 독특한 모양 덕분에 공기의 흐름에 의해 위로 밀어 올리는 양력이 생겨 하늘을 날 수 있습니다.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 382, 'prompt_tokens': 16, 'total_tokens': 398, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX69FpqcoFTXL5GVWW9vafs6vLfUT', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--49bc9b3d-6509-4d73-baa1-5b62e28f9443-0' usage_metadata={'input_tokens': 16, 'output_tokens': 382, 'total_tokens': 398, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
content='양자 컴퓨팅(Quantum Computing)은 양자역학의 원리를 이용하여 정보를 처리하는 컴퓨팅 기술입니다. 전통적인 컴퓨터가 비트(bit)를 사용해 0 또는 1의 값을 가지는 반면, 양자 컴퓨터는 큐비트(qubit)를 사용합니다. 큐비트는 0과 1의 상태를 동시에 가질 수 있는 중첩(superposition) 상태를 지닐 뿐만 아니라, 얽힘(entanglement)이라는 특성을 통해 여러 큐비트가 상호 연관된 상태로 작동할 수 있습니다.\n\n이러한 특성 덕분에 양자 컴퓨터는 특정 문제에 대해 기존 컴퓨터보다 훨씬 빠르게 계산할 수 있습니다. 예를 들어, 대규모 소인수분해, 최적화 문제, 양자 시뮬레이션 등에서 강점을 보입니다. 하지만 아직은 기술적 한계와 오류율 문제 등으로 인해 상용화 초기 단계에 있습니다.\n\n요약하자면, 양자 컴퓨팅은 양자역학의 원리를 활용하여 정보를 처리하는 새로운 계산 방식으로, 특정 복잡한 문제 해결에 혁신적인 성능 향상을 기대할 수 있는 기술입니다.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 265, 'prompt_tokens': 18, 'total_tokens': 283, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX69Fr3F8NWxFBX7ex5pEU5BgyEms', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--fc5f238a-e9e0-4269-b207-c73827fb1774-0' usage_metadata={'input_tokens': 18, 'output_tokens': 265, 'total_tokens': 283, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}

⚠️ 참고: 이 섹션에서는 클라이언트 측에서 모델 호출을 병렬화하는 chat model(채팅 모델) 메서드 batch()에 대해 설명합니다.

이는 OpenAI 또는 Anthropic과 같은 추론 제공업체가 지원하는 batch API(일괄 처리 API)와는 구별됩니다.

기본적으로 batch()는 전체 배치에 대한 최종 출력만 반환합니다. 각 개별 입력이 생성을 완료할 때 출력을 받으려면 batch_as_completed()로 결과를 스트리밍할 수 있습니다:

for response in model.batch_as_completed(
    [
        "앵무새는 왜 화려한 깃털을 가지고 있나요?",
        "비행기는 어떻게 날 수 있나요?",
        "양자 컴퓨팅이란 무엇인가요?",
    ]
):
    print(response)
(2, AIMessage(content='양자 컴퓨팅(Quantum Computing)은 양자역학의 원리를 이용하여 정보를 처리하는 컴퓨팅 기술입니다. 기존의 고전 컴퓨터가 비트(bit)를 사용해 0 또는 1의 값을 가지는 반면, 양자 컴퓨터는 양자 비트(큐비트, qubit)를 사용합니다. 큐비트는 0과 1의 상태를 동시에 가질 수 있는 중첩(superposition) 상태가 가능하며, 얽힘(entanglement)이라는 양자역학적 현상을 통해 큐비트들 간에 강한 상관관계를 형성할 수 있습니다.\n\n이러한 특성 덕분에 양자 컴퓨터는 특정 문제들, 예를 들어 대규모 소인수분해, 최적화 문제, 양자 시뮬레이션 등에서 기존 컴퓨터보다 훨씬 빠른 계산이 가능합니다. 다만, 양자 컴퓨팅 기술은 아직 연구 및 개발 단계에 있으며, 상용화되기까지는 여러 기술적 난제들이 남아 있습니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 228, 'prompt_tokens': 18, 'total_tokens': 246, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX69KzVlx73u2UtnCt8LAQEevgpgQ', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--b1f4fc3f-b5b1-443a-887d-08b3a8585d08-0', usage_metadata={'input_tokens': 18, 'output_tokens': 228, 'total_tokens': 246, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}))
(0, AIMessage(content='앵무새가 화려한 깃털을 가지고 있는 이유는 주로 다음과 같습니다:\n\n1. **짝짓기와 구애**: 화려한 깃털은 건강하고 강한 개체임을 나타내는 신호로 작용합니다. 밝고 선명한 색깔은 짝을 끌어들이는 데 도움이 되며, 이는 자연 선택과 성 선택의 결과입니다.\n\n2. **종 내 식별**: 다양한 색깔과 패턴은 같은 종 내에서 개체를 구별하는 데 유용합니다. 이를 통해 개체들은 서로를 인식하고 사회적 상호작용을 원활하게 할 수 있습니다.\n\n3. **서식지 적응**: 일부 앵무새는 주변 환경에 맞춰 위장하거나, 반대로 눈에 띄는 색깔로 포식자를 혼란시키는 역할을 할 수 있습니다.\n\n4. **사회적 신호**: 깃털의 색과 상태는 사회적 지위나 건강 상태를 나타내는 신호가 되기도 합니다.\n\n종합적으로, 앵무새의 화려한 깃털은 생존과 번식에 유리한 여러 기능을 수행하는 중요한 특징입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 261, 'prompt_tokens': 24, 'total_tokens': 285, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX69KHEciJHnctDULsQyn4EwY5Nwe', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--f50f9e4d-69a9-4d23-afb5-9d7d76ba69cb-0', usage_metadata={'input_tokens': 24, 'output_tokens': 261, 'total_tokens': 285, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}))
(1, AIMessage(content='비행기가 어떻게 날 수 있는지에 대해 설명해드릴게요.\n\n비행기가 날 수 있는 원리는 주로 **양력(lift)** 때문입니다. 양력은 비행기의 날개가 공기를 가르면서 발생하는 힘으로, 중력(무게)을 이겨내고 비행기를 하늘로 띄우는 역할을 합니다.\n\n### 비행기의 비행 원리\n\n1. **날개의 모양 (에어포일, Airfoil)**  \n   비행기 날개의 윗면은 아래쪽보다 더 둥글고 길게 설계되어 있습니다. 날개를 통해 공기가 흐를 때, 윗면을 지나는 공기가 더 빠르게 이동하고, 아랫면을 지나는 공기보다 압력이 낮아집니다. 이것이 바로 **베르누이의 원리**에 의해 발생하는 현상입니다.\n\n2. **양력 발생**  \n   날개 윗면의 공기압이 낮아지고, 아랫면의 공기압이 상대적으로 높아지면서 위로 밀어 올리는 힘이 생깁니다. 이 힘이 바로 양력입니다.\n\n3. **추진력 (Thrust)**  \n   엔진이나 프로펠러가 앞으로 나아가는 힘을 만들어 냅니다. 이 추진력이 있어야 비행기가 앞으로 움직이며 날개의 공기 흐름이 생겨 양력이 발생합니다.\n\n4. **항력 (Drag)**  \n   비행기가 공기 중을 움직일 때 발생하는 저항력입니다. 항력을 줄이는 것이 효율적인 비행을 위해 중요합니다.\n\n5. **중력 (Gravity)**  \n   비행기를 아래로 끌어당기는 힘입니다.\n\n### 간단히 정리하면  \n- 엔진이 비행기를 앞으로 밀고(추진력),  \n- 날개가 공기의 흐름을 이용해 위로 들어올리는 힘(양력)을 만들어내고,  \n- 이 힘이 중력보다 크면 비행기는 하늘을 날 수 있습니다.\n\n이 원리 덕분에 비행기는 하늘을 날면서 사람과 물자를 빠르고 안전하게 이동시킬 수 있습니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 466, 'prompt_tokens': 16, 'total_tokens': 482, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX69K17U8V2v2YjzF5Yffzf1RlVOg', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--8927046f-4ce2-4aff-8946-90b9a27a60c7-0', usage_metadata={'input_tokens': 16, 'output_tokens': 466, 'total_tokens': 482, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}))

⚠️ 참고: batch_as_completed()를 사용할 때 결과가 순서 없이 도착할 수 있습니다. 각각에는 필요에 따라 원래 순서를 재구성하기 위해 일치시키는 입력 인덱스가 포함됩니다.

💡 : batch() 또는 batch_as_completed()를 사용하여 많은 수의 입력을 처리할 때 최대 병렬 호출 수를 제어할 수 있습니다. 이는 RunnableConfig 딕셔너리에서max_concurrency 속성을 설정하여 수행할 수 있습니다.

model.batch(
    list_of_inputs,
    config={
        'max_concurrency': 5,  # 동시 호출을 5개로 제한합니다
    }
)

지원되는 속성의 전체 목록은 RunnableConfig 참조를 확인하세요.

일괄 처리에 대한 자세한 내용은 BaseChatModel.batch를 참조하세요.

Tool calling(도구 호출)

모델은 데이터베이스에서 데이터 가져오기, 웹 검색 또는 코드 실행과 같은 작업을 수행하는 도구를 호출하도록 요청할 수 있습니다.

Tool(도구)은 다음으로 구성됩니다:

  1. 도구 이름, 설명 및/또는 인수 정의(종종 JSON schema)를 포함하는 schema(스키마)
  2. 실행할 function(함수) 또는 coroutine(코루틴)

⚠️ 참고: “function calling(함수 호출)“이라는 용어를 들을 수 있습니다. 우리는 이를 “tool calling(도구 호출)“과 같은 의미로 사용합니다.

정의한 도구를 모델에서 사용할 수 있도록 하려면 bind_tools()를 사용하여 바인딩해야 합니다. 후속 호출에서 모델은 필요에 따라 바인딩된 도구 중 하나를 호출하도록 선택할 수 있습니다.

일부 모델 제공업체는 모델 또는 호출 매개변수를 통해 활성화할 수 있는 기본 제공 도구를 제공합니다(예: ChatOpenAI, ChatAnthropic). 자세한 내용은 해당 provider reference를 확인하세요.

💡 : 도구 생성에 대한 세부 정보 및 기타 옵션은 도구 가이드를 참조하세요.

from langchain.tools import tool
 
 
@tool
def get_weather(city: str) -> str:
    """도시의 날씨를 가져옵니다."""
    return f"{city} 날씨: 맑음, 27°C"
 
 
model_with_tools = model.bind_tools([get_weather])  # [!code highlight]
 
response = model_with_tools.invoke("보스턴의 날씨는 어때?")
for tool_call in response.tool_calls:
    # 모델이 호출한 도구 확인
    print(f"도구: {tool_call['name']}")
    print(f"인자: {tool_call['args']}")
도구: get_weather
인자: {'city': '보스턴'}

사용자 정의 도구를 바인딩할 때 모델의 응답에는 도구 실행에 대한 요청이 포함됩니다. agent와 별도로 모델을 사용하는 경우, 요청된 작업을 수행하고 후속 추론에 사용할 수 있도록 결과를 모델에 반환하는 것은 사용자의 책임입니다. agent를 사용하는 경우 에이전트 루프가 도구 실행 루프를 처리합니다.

아래에서는 tool calling을 사용할 수 있는 몇 가지 일반적인 방법을 보여줍니다.

Tool execution loop(도구 실행 루프)

모델이 tool call(도구 호출)을 반환하면 도구를 실행하고 결과를 모델에 다시 전달해야 합니다. 이렇게 하면 모델이 도구 결과를 사용하여 최종 응답을 생성할 수 있는 대화 루프가 생성됩니다. LangChain에는 이러한 오케스트레이션을 처리하는 agent 추상화가 포함되어 있습니다.

다음은 간단한 예입니다:

from langchain_core.messages import BaseMessage, HumanMessage
 
 
# 모델에 도구 바인딩
model_with_tools = model.bind_tools([get_weather])
 
# 1단계: 모델이 도구 호출 생성
messages: list[BaseMessage] = [HumanMessage("보스턴의 날씨는 어때?")]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)
 
# 2단계: 도구 실행 및 결과 수집
for tool_call in ai_msg.tool_calls:
    # 생성된 인자로 도구 실행
    tool_result = get_weather.invoke(tool_call)
    messages.append(tool_result)
 
# 3단계: 결과를 모델에 다시 전달하여 최종 응답 생성
final_response = model_with_tools.invoke(messages)
print(final_response.content)
보스턴은 현재 맑고 기온은 27도입니다. 더 궁금한 점 있으신가요?

도구가 반환한 각 ToolMessage에는 원래 도구 호출과 일치하는 tool_call_id가 포함되어 있어 모델이 결과를 요청과 연관시키는 데 도움이 됩니다.

Forcing tool calls(도구 호출 강제)

기본적으로 모델은 사용자 입력을 기반으로 사용할 바인딩된 도구를 자유롭게 선택할 수 있습니다. 그러나 특정 tool을 선택하도록 강제하거나 주어진 목록에서 임의의 tool을 사용하도록 보장할 수 있습니다

model_with_tools = model.bind_tools([get_weather], tool_choice="any")
 
messages: list[BaseMessage] = [HumanMessage("안녕하세요.")]
final_response = model_with_tools.invoke(messages)
final_response.pretty_print()
================================== Ai Message ==================================
Tool Calls:
  get_weather (call_oFZlf1HuhovweghBbQUAdKrc)
 Call ID: call_oFZlf1HuhovweghBbQUAdKrc
  Args:
    city: 서울

Parallel tool calls(병렬 도구 호출)

많은 모델이 적절한 경우 여러 도구를 병렬로 호출하는 것을 지원합니다. 이를 통해 모델이 동시에 다른 소스에서 정보를 수집할 수 있습니다.

from pprint import pprint
 
from langchain_core.messages import BaseMessage
 
 
messages: list[BaseMessage] = []
 
 
model_with_tools = model.bind_tools([get_weather])
 
response = model_with_tools.invoke("보스턴과 도쿄 날씨는 어때요?")
messages.append(response)
 
# 모델은 여러 개의 도구 호출을 생성할 수 있습니다
pprint(response.tool_calls)
 
 
# 모든 도구 실행 (async를 사용하여 병렬로 실행 가능)
for tool_call in response.tool_calls:
    if tool_call["name"] == "get_weather":
        result = get_weather.invoke(tool_call)
    messages.append(result)
[{'args': {'city': 'Boston'},
  'id': 'call_NlHChanfGT5uiw60FF3rnHrF',
  'name': 'get_weather',
  'type': 'tool_call'},
 {'args': {'city': 'Tokyo'},
  'id': 'call_f5WrgfplPINTw5btJmffuNb4',
  'name': 'get_weather',
  'type': 'tool_call'}]
for msg in messages:
    msg.pretty_print()
================================== Ai Message ==================================
Tool Calls:
  get_weather (call_NlHChanfGT5uiw60FF3rnHrF)
 Call ID: call_NlHChanfGT5uiw60FF3rnHrF
  Args:
    city: Boston
  get_weather (call_f5WrgfplPINTw5btJmffuNb4)
 Call ID: call_f5WrgfplPINTw5btJmffuNb4
  Args:
    city: Tokyo
================================= Tool Message =================================
Name: get_weather

Boston 날씨: 맑음, 27°C
================================= Tool Message =================================
Name: get_weather

Tokyo 날씨: 맑음, 27°C

Model은 요청된 작업의 독립성을 기반으로 병렬 실행이 적절한 시기를 지능적으로 결정합니다.

💡 : tool calling(도구 호출)을 지원하는 대부분의 모델은 기본적으로 parallel tool call(병렬 도구 호출)을 활성화합니다. 일부(OpenAI 및 Anthropic 포함)는 이 기능을 비활성화할 수 있습니다. 이렇게 하려면 parallel_tool_calls=False를 설정하세요:

model.bind_tools([get_weather], parallel_tool_calls=False)

Streaming tool calls(도구 호출 스트리밍)

응답을 스트리밍할 때 도구 호출은 ToolCallChunk를 통해 점진적으로 구축됩니다. 이를 통해 완전한 응답을 기다리지 않고 도구 호출이 생성될 때 확인할 수 있습니다.

for chunk in model_with_tools.stream("보스턴과 도쿄의 날씨는 어때요?"):
    # 도구 호출 청크가 점진적으로 도착합니다
    for tool_chunk in chunk.tool_call_chunks:
        if name := tool_chunk.get("name"):
            print(f"Tool: {name}")
        if id_ := tool_chunk.get("id"):
            print(f"ID: {id_}")
        if args := tool_chunk.get("args"):
            print(f"Args: {args}")
Tool: get_weather
ID: call_a9RIgMf6P5UoJfWMKHvAmwDg
Args: {"ci
Args: ty": 
Args: "Bosto
Args: n"}
Tool: get_weather
ID: call_OWjFmiCHvs6H49P8zwSFvjEY
Args: {"ci
Args: ty": 
Args: "Tokyo
Args: "}

청크를 누적하여 완전한 도구 호출을 구축할 수 있습니다:

gathered = None
for chunk in model_with_tools.stream("보스턴 날씨는 어때요?"):
    gathered = chunk if gathered is None else gathered + chunk
    print(gathered.tool_calls)
[{'name': 'get_weather', 'args': {}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': ''}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보스'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보스턴'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보스턴'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보스턴'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보스턴'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]
[{'name': 'get_weather', 'args': {'city': '보스턴'}, 'id': 'call_5TqZskihq3XCPL89CJxJlEbE', 'type': 'tool_call'}]

Structured output(구조화된 출력)

모델은 주어진 schema(스키마)와 일치하는 형식으로 응답을 제공하도록 요청할 수 있습니다. 이는 출력을 쉽게 구문 분석하고 후속 처리에 사용할 수 있도록 하는 데 유용합니다. LangChain은 구조화된 출력을 적용하기 위한 여러 스키마 유형과 메서드를 지원합니다.

Pydantic

Pydantic model(모델)은 필드 유효성 검사, 설명 및 중첩된 구조를 갖춘 가장 풍부한 기능 세트를 제공합니다.

from pydantic import BaseModel, Field
 
 
class Movie(BaseModel):
    """영화 상세 정보."""
 
    title: str = Field(..., description="영화 제목")
    year: int = Field(..., description="영화 개봉 연도")
    director: str = Field(..., description="영화 감독")
    rating: float = Field(..., description="10점 만점의 영화 평점")
 
 
model_with_structure = model.with_structured_output(Movie)
response = model_with_structure.invoke("인셉션 영화에 대한 상세 정보를 제공해주세요")
print(repr(response))
Movie(title='인셉션', year=2010, director='크리스토퍼 놀란', rating=8.8)

TypedDict

TypedDict는 런타임 유효성 검사가 필요하지 않을 때 이상적인 Python의 기본 제공 타이핑을 사용하는 더 간단한 대안을 제공합니다.

from typing import Annotated, TypedDict
 
 
class MovieDict(TypedDict):
    """영화 상세 정보."""
 
    title: Annotated[str, ..., "영화 제목"]
    year: Annotated[int, ..., "영화 개봉 연도"]
    director: Annotated[str, ..., "영화 감독"]
    rating: Annotated[float, ..., "10점 만점의 영화 평점"]
 
 
model_with_structure = model.with_structured_output(MovieDict)
response = model_with_structure.invoke("인셉션 영화에 대한 상세 정보를 제공해주세요")
print(response)
{'title': '인셉션', 'year': 2010, 'director': '크리스토퍼 놀란', 'rating': 8.8}

JSON Schema

최대 제어 또는 상호 운용성을 위해 원시 JSON Schema(스키마)를 제공할 수 있습니다.

json_schema = {
    "title": "Movie",
    "description": "영화 상세 정보",
    "type": "object",
    "properties": {
        "title": {"type": "string", "description": "영화 제목"},
        "year": {"type": "integer", "description": "영화 개봉 연도"},
        "director": {"type": "string", "description": "영화 감독"},
        "rating": {"type": "number", "description": "10점 만점의 영화 평점"},
    },
    "required": ["title", "year", "director", "rating"],
}
 
model_with_structure = model.with_structured_output(
    json_schema,
    method="json_schema",
)
response = model_with_structure.invoke("인셉션 영화에 대한 상세 정보를 제공해주세요")
print(response)
{'title': '인셉션', 'year': 2010, 'director': '크리스토퍼 놀란', 'rating': 8.8}

⚠️ 참고: 구조화된 출력에 대한 주요 고려 사항:

  • Method parameter(메서드 매개변수): 일부 제공업체는 다양한 메서드('json_schema', 'function_calling', 'json_mode')를 지원합니다
    • 'json_schema'는 일반적으로 제공업체가 제공하는 전용 구조화된 출력 기능을 나타냅니다
    • 'function_calling'은 주어진 스키마를 따르는 tool call(도구 호출)을 강제하여 구조화된 출력을 파생합니다
    • 'json_mode'는 일부 제공업체가 제공하는 'json_schema'의 전신입니다. 유효한 json을 생성하지만 스키마는 prompt(프롬프트)에 설명되어야 합니다
  • Include raw: include_raw=True를 사용하여 구문 분석된 출력과 원시 AI 메시지를 모두 얻습니다
  • Validation(유효성 검사): Pydantic 모델은 자동 유효성 검사를 제공하는 반면 TypedDict 및 JSON Schema는 수동 유효성 검사가 필요합니다

예: 구문 분석된 구조와 함께 메시지 출력

원시 AIMessage 객체를 구문 분석된 표현과 함께 반환하여 token count(토큰 수)와 같은 응답 메타데이터에 액세스하는 것이 유용할 수 있습니다. 이렇게 하려면 with_structured_output을 호출할 때 include_raw=True를 설정하세요:

from pydantic import BaseModel, Field
 
 
class Movie(BaseModel):
    """영화 상세 정보."""
 
    title: str = Field(..., description="영화 제목")
    year: int = Field(..., description="영화 개봉 연도")
    director: str = Field(..., description="영화 감독")
    rating: float = Field(..., description="10점 만점의 영화 평점")
 
 
model_with_structure = model.with_structured_output(Movie, include_raw=True)
response = model_with_structure.invoke("인셉션 영화에 대한 상세 정보를 제공해주세요")
response
{'raw': AIMessage(content='{"title":"인셉션","year":2010,"director":"크리스토퍼 놀란","rating":8.8}', additional_kwargs={'parsed': Movie(title='인셉션', year=2010, director='크리스토퍼 놀란', rating=8.8), 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 28, 'prompt_tokens': 200, 'total_tokens': 228, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX6Jc5PYx3AKpvSBVdCkhOQ5Vt65R', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--ccf7bd7b-3f63-4283-b865-dd6651bdccbb-0', usage_metadata={'input_tokens': 200, 'output_tokens': 28, 'total_tokens': 228, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
 'parsed': Movie(title='인셉션', year=2010, director='크리스토퍼 놀란', rating=8.8),
 'parsing_error': None}

예: 중첩된 구조

스키마는 중첩될 수 있습니다:

from pydantic import BaseModel, Field
 
 
class Actor(BaseModel):
    name: str
    role: str
 
 
class MovieDetails(BaseModel):
    title: str
    year: int
    cast: list[Actor]
    genres: list[str]
    budget: float | None = Field(None, description="예산 (백만 달러 단위)")
 
 
model_with_structure = model.with_structured_output(MovieDetails)
response = model_with_structure.invoke("인셉션 영화에 대한 상세 정보를 제공해주세요")
response
MovieDetails(title='Inception', year=2010, cast=[Actor(name='Leonardo DiCaprio', role='Dom Cobb'), Actor(name='Joseph Gordon-Levitt', role='Arthur'), Actor(name='Ellen Page', role='Ariadne'), Actor(name='Tom Hardy', role='Eames'), Actor(name='Ken Watanabe', role='Saito'), Actor(name='Cillian Murphy', role='Robert Fischer')], genres=['Action', 'Adventure', 'Sci-Fi', 'Thriller'], budget=160000000.0)

고급 주제

Multimodal(멀티모달)

특정 모델은 이미지, 오디오 및 비디오와 같은 비텍스트 데이터를 처리하고 반환할 수 있습니다. content block을 제공하여 모델에 비텍스트 데이터를 전달할 수 있습니다.

💡 : 기본 multimodal(멀티모달) 기능이 있는 모든 LangChain chat model(채팅 모델)은 다음을 지원합니다:

  1. cross-provider 표준 형식의 데이터(메시지 가이드 참조)
  2. OpenAI chat completions 형식
  3. 특정 제공업체에 고유한 형식(예: Anthropic 모델은 Anthropic 고유 형식을 허용)

자세한 내용은 메시지 가이드의 multimodal section을 참조하세요.

일부 모델은 응답의 일부로 멀티모달 데이터를 반환할 수 있습니다. 그렇게 호출된 경우 결과 AIMessage에는 멀티모달 유형의 content block(콘텐츠 블록)이 포함됩니다.

response = model.invoke("고양이 그림을 그리세요")
print(response.content_blocks)

특정 제공업체에 대한 자세한 내용은 통합 페이지를 참조하세요.

Reasoning(추론)

최신 모델은 결론에 도달하기 위해 다단계 추론을 수행할 수 있습니다. 여기에는 복잡한 문제를 더 작고 관리하기 쉬운 단계로 나누는 작업이 포함됩니다.

기본 모델이 지원하는 경우 이 추론 프로세스를 표시하여 모델이 최종 답변에 어떻게 도달했는지 더 잘 이해할 수 있습니다.

reasoning_model = init_chat_model("groq:openai/gpt-oss-20b")
for chunk in reasoning_model.stream(
    "앵무새는 왜 화려한 깃털을 가지고 있을까요? 10단어 문장으로 짧게 답변하세요."
):
    reasoning_steps = [r for r in chunk.content_blocks if r["type"] == "reasoning"]
    print(reasoning_steps if reasoning_steps else chunk.text)
reasoning_model = init_chat_model("groq:openai/gpt-oss-20b")
response = reasoning_model.invoke(
    "앵무새는 왜 화려한 깃털을 가지고 있을까요? 10단어 문장으로 짧게 답변하세요."
)
reasoning_steps = [b for b in response.content_blocks if b["type"] == "reasoning"]
print(" ".join(step["reasoning"] for step in reasoning_steps))
We need to respond in Korean. The user says: "앵무새는 왜 화려한 깃털을 가지고 있을까요? 10단어 문장으로 짧게 답변하세요."

We need to give a short answer in Korean, 10 words exactly. They want a sentence with 10 words. We must count Korean words. In Korean, words are separated by spaces. So we need 10 words. For example: "성장 경쟁, 짝짓기 매력, 시각적 신호 전달을 위해." Let's count: "성장(1) 경쟁(2), 짝짓기(3) 매력(4), 시각적(5) 신호(6) 전달을(7) 위해(8)." That's 8 words. Need 10. Let's craft: "자연 선택에 의해 짝짓기 매력과 보호를 위해." Count: "자연(1) 선택에(2) 의해(3) 짝짓기(4) 매력과(5) 보호를(6) 위해(7)." 7 words. Need 10. Let's add: "생존 경쟁, 번식 성공을 위해." Count: "생존(1) 경쟁,(2) 번식(3) 성공을(4) 위해(5)." That is 5 words. Combined with previous 7 = 12. We need exactly 10 words. Let's craft a full sentence of 10 words. Example: "성장 경쟁과 짝짓기 매력, 색다른 신호 전달을 위해." Count: "성장(1) 경쟁과(2) 짝짓기(3) 매력,(4) 색다른(5) 신호(6) 전달을(7) 위해(8)." 8 words. Need 2 more words: "자연에서" maybe. Add "자연에서" before? Let's insert: "자연에서 성장 경쟁과 짝짓기 매력, 색다른 신호 전달을 위해." Count: "자연에서(1) 성장(2) 경쟁과(3) 짝짓기(4) 매력,(5) 색다른(6) 신호(7) 전달을(8) 위해(9)." Still 9 words. Need one more. Add "효과적으로" maybe. "효과적으로" before "전달을 위해". Let's count: "자연에서(1) 성장(2) 경쟁과(3) 짝짓기(4) 매력,(5) 색다른(6) 신호(7) 효과적으로(8) 전달을(9) 위해(10)." That is 10 words. Sentence: "자연에서 성장 경쟁과 짝짓기 매력, 색다른 신호 효과적으로 전달을 위해." Wait punctuation: we need a sentence. Maybe we can adjust punctuation but still count words. Word count ignoring punctuation. The sentence: "자연에서 성장 경쟁과 짝짓기 매력, 색다른 신호 효과적으로 전달을 위해." Count: "자연에서(1) 성장(2) 경쟁과(3) 짝짓기(4) 매력,(5) 색다른(6) 신호(7) 효과적으로(8) 전달을(9) 위해(10)." Good. That is 10 words. It's a bit awkward but okay. Or we can produce: "자연에서 성장 경쟁과 짝짓기 매력, 색다른 신호 효과적으로 전달을 위해." It's fine. The user wants "10단어 문장으로 짧게 답변하세요." So we must give a short answer with 10 words. The sentence above is 10 words. Should we add period? It's okay. Let's output that.

모델에 따라 추론에 투입해야 하는 노력 수준을 지정할 수 있는 경우가 있습니다. 마찬가지로 모델이 추론을 완전히 끄도록 요청할 수 있습니다. 이는 추론의 범주형 “tier”(예: 'low' 또는 'high') 또는 정수 토큰 예산(token budget) 형태를 취할 수 있습니다.

자세한 내용은 해당 채팅 모델의 통합 페이지 또는 reference(참조)를 참조하세요.

로컬 모델

LangChain은 자체 하드웨어에서 로컬로 모델을 실행하는 것을 지원합니다. 이는 데이터 프라이버시가 중요하거나, 사용자 정의 모델을 호출하거나, 클라우드 기반 모델을 사용할 때 발생하는 비용을 피하려는 시나리오에 유용합니다.

Ollama는 로컬로 모델을 실행하는 가장 쉬운 방법 중 하나입니다. 로컬 통합의 전체 목록은 통합 페이지를 참조하세요.

프롬프트 캐싱

많은 제공업체는 동일한 토큰의 반복 처리에 대한 지연 시간(latency)와 비용을 줄이기 위해 프롬프트 캐싱 기능을 제공합니다. 이러한 기능은 암시적(implicit) 또는 **명시적(explicit)**일 수 있습니다:

  • Implicit prompt caching(암시적 프롬프트 캐싱): 요청이 cache(캐시)에 도달하면 제공업체가 자동으로 비용 절감을 전달합니다. 예: OpenAIGemini (Gemini 2.5 이상).
  • Explicit caching(명시적 캐싱): 제공업체를 통해 더 큰 제어를 위해 또는 비용 절감을 보장하기 위해 cache point(캐시 포인트)를 수동으로 표시할 수 있습니다. 예: ChatOpenAI (prompt_cache_key를 통해), Anthropic의 AnthropicPromptCachingMiddlewarecache_control 옵션, AWS Bedrock, Gemini.

⚠️ 주의: 프롬프트 캐싱은 최소 입력 token(토큰) 임계값을 초과하는 경우에만 작동하는 경우가 많습니다. 자세한 내용은 제공업체 페이지를 참조하세요.

캐시 사용은 모델 응답의 사용 메타데이터(usage metadata)에 반영됩니다.

서버 측 도구 사용(Server-side tool use)

일부 제공업체는 서버 측 tool-calling 루프를 지원합니다. 모델은 웹 검색(web search), 코드 인터프리터(code interpreter) 및 기타 도구와 상호 작용하고 단일 대화 턴에서 결과를 분석할 수 있습니다.

모델이 서버 측에서 도구를 호출하면 응답 메시지의 content에는 도구의 호출 및 결과를 나타내는 콘텐츠가 포함됩니다. 응답의 content block에 액세스하면 제공업체에 구애받지 않는 형식으로 서버 측 도구 호출 및 결과가 반환됩니다:

from langchain.chat_models import init_chat_model
 
 
model = init_chat_model("openai:gpt-4.1-mini")
tool = {"type": "web_search"}
model_with_tools = model.bind_tools([tool])
response = model_with_tools.invoke("오늘의 긍정적인 뉴스는 무엇이었나요?")
response.content_blocks
[{'type': 'server_tool_call',
  'name': 'web_search',
  'args': {'query': '오늘의 긍정적인 뉴스', 'type': 'search'},
  'id': 'ws_046d45885cfadcd9006906190421f8819c86104023538b2d4c'},
 {'type': 'server_tool_result',
  'tool_call_id': 'ws_046d45885cfadcd9006906190421f8819c86104023538b2d4c',
  'status': 'success'},
 {'type': 'text',
  'text': '2025년 11월 1일에 발표된 긍정적인 뉴스로는 다음과 같은 소식들이 있습니다:\n\n- **뉴욕증시 상승 마감**: 실적 호조로 인해 다우지수가 0.09% 상승하며 상승세로 마감했습니다. ([newsis.com](https://www.newsis.com/view/NISX20251031_0003384650?utm_source=openai))\n\n- **부산 지역 축제 개최**: 부산에서는 다양한 지역 축제와 행사가 열려 시민들의 참여와 화합을 도모하고 있습니다. ([newsis.com](https://www.newsis.com/view/NISX20251031_0003384650?utm_source=openai))\n\n- **울산 펫 페스티벌 개막**: 울산에서는 펫 페스티벌이 개막하여 반려동물과 함께하는 즐거운 시간을 제공합니다. ([newsis.com](https://www.newsis.com/view/NISX20251030_0003383949?utm_source=openai))\n\n- **문화체육관광부 APEC 정상회의 개최**: 문화체육관광부는 경북 경주에서 APEC 정상회의를 개최하여 국제적인 협력을 강화하고 있습니다. ([newsis.com](https://www.newsis.com/view/NISX20251031_0003385311?utm_source=openai))\n\n이러한 소식들은 지역 사회의 활발한 활동과 국제적인 협력을 보여주는 긍정적인 소식들입니다. ',
  'annotations': [{'end_index': 190,
    'start_index': 105,
    'title': '[오늘의 주요일정]부산(11월1일 토요일) :: 공감언론 뉴시스 ::',
    'type': 'citation',
    'url': 'https://www.newsis.com/view/NISX20251031_0003384650?utm_source=openai'},
   {'end_index': 344,
    'start_index': 259,
    'title': '[오늘의 주요일정]부산(11월1일 토요일) :: 공감언론 뉴시스 ::',
    'type': 'citation',
    'url': 'https://www.newsis.com/view/NISX20251031_0003384650?utm_source=openai'},
   {'end_index': 496,
    'start_index': 411,
    'title': '[오늘의 주요일정]울산(11월1일 토요일) :: 공감언론 뉴시스 ::',
    'type': 'citation',
    'url': 'https://www.newsis.com/view/NISX20251030_0003383949?utm_source=openai'},
   {'end_index': 664,
    'start_index': 579,
    'title': '[오늘의 주요일정]문화체육관광부(11월1일 토요일) :: 공감언론 뉴시스 ::',
    'type': 'citation',
    'url': 'https://www.newsis.com/view/NISX20251031_0003385311?utm_source=openai'}],
  'id': 'msg_046d45885cfadcd900690619052628819cb1a3297adb83da63'}]

이는 단일 대화 턴을 나타냅니다. 클라이언트 측 도구 호출(tool-calling)에서처럼 전달해야 하는 관련 ToolMessage 객체가 없습니다.

사용 가능한 도구 및 사용 세부 정보는 해당 제공업체의 통합 페이지를 참조하세요.

Rate limiting(속도 제한)

많은 chat model 제공업체는 주어진 기간 동안 수행할 수 있는 호출 수에 제한을 부과합니다. rate limit에 도달하면 일반적으로 제공업체로부터 rate limit error 응답을 받게 되며 더 많은 요청을 하기 전에 기다려야 합니다.

rate limit을 관리하는 데 도움이 되도록 chat model 통합은 초기화 중에 제공할 수 있는 rate_limiter 매개변수를 허용하여 요청이 이루어지는 속도를 제어합니다.

속도 제한기 초기화 및 사용

LangChain에는 (선택 사항) 기본 제공 InMemoryRateLimiter가 함께 제공됩니다. 이 제한기는 thread-safe하며 동일한 프로세스의 여러 스레드에서 공유할 수 있습니다.

from langchain_core.rate_limiters import InMemoryRateLimiter
 
 
rate_limiter = InMemoryRateLimiter(
    requests_per_second=0.1,  # 10초마다 1개의 요청
    check_every_n_seconds=0.1,  # 100ms마다 요청 허용 여부 확인
    max_bucket_size=10,  # 최대 버스트 크기 제어
)
 
model = init_chat_model(
    model="gpt-5",
    model_provider="openai",
    rate_limiter=rate_limiter,
)

⚠️ 경고: 제공된 rate limiter(속도 제한기)는 단위 시간당 요청 수만 제한할 수 있습니다. 요청 크기를 기반으로 제한해야 하는 경우에는 도움이 되지 않습니다.

기본 URL 또는 프록시(Base URL or proxy)

많은 chat model 통합의 경우 API 요청에 대한 base URL을 구성할 수 있으므로 OpenAI 호환 API가 있는 모델 제공업체를 사용하거나 proxy server를 사용할 수 있습니다.

Base URL

많은 모델 제공업체가 OpenAI 호환 API(예: Together AI, vLLM)를 제공합니다. 적절한 base_url 매개변수를 지정하여 이러한 제공업체와 함께 init_chat_model을 사용할 수 있습니다:

 model = init_chat_model(
    model="MODEL_NAME",
    model_provider="openai",
    base_url="BASE_URL",
    api_key="YOUR_API_KEY",
)

⚠️ 참고: 직접 chat model 클래스 인스턴스화를 사용하는 경우 매개변수 이름은 제공업체에 따라 다를 수 있습니다. 자세한 내용은 해당 reference를 확인하세요.

프록시 구성

HTTP 프록시가 필요한 배포의 경우 일부 모델 통합이 프록시 구성을 지원합니다:

from langchain_openai import ChatOpenAI
 
model = ChatOpenAI(
    model="gpt-4o",
    openai_proxy="http://proxy.example.com:8080"
)

⚠️ 참고: 프록시 지원은 통합에 따라 다릅니다. 프록시 구성 옵션은 특정 모델 제공업체의 reference를 확인하세요.

Log probabilities

특정 모델은 모델을 초기화할 때 logprobs 매개변수를 설정하여 주어진 token의 가능성을 나타내는 token-level log probability를 반환하도록 구성할 수 있습니다:

model = init_chat_model(model="gpt-4o", model_provider="openai").bind(logprobs=True)
response = model.invoke("앵무새는 왜 말을 할까요?")
print(response.response_metadata["logprobs"])
{'content': [{'token': '\\xec\\x95', 'bytes': [236, 149], 'logprob': -3.173704271830502e-06, 'top_logprobs': []}, {'token': '\\xb5', 'bytes': [181], 'logprob': 0.0, 'top_logprobs': []}, {'token': '무', 'bytes': [235, 172, 180], 'logprob': -1.9361264946837764e-07, 'top_logprobs': []}, {'token': '새', 'bytes': [236, 131, 136], 'logprob': -4.320199877838604e-07, 'top_logprobs': []}, {'token': '가', 'bytes': [234, 176, 128], 'logprob': -0.10025293380022049, 'top_logprobs': []}, {'token': ' 말을', 'bytes': [32, 235, 167, 144, 236, 157, 132], 'logprob': -0.014275981113314629, 'top_logprobs': []}, {'token': ' 할', 'bytes': [32, 237, 149, 160], 'logprob': -1.7126901149749756, 'top_logprobs': []}, {'token': ' 수', 'bytes': [32, 236, 136, 152], 'logprob': -0.0011706985533237457, 'top_logprobs': []}, {'token': ' 있는', 'bytes': [32, 236, 158, 136, 235, 138, 148], 'logprob': -2.558399319241289e-05, 'top_logprobs': []}, {'token': ' 이유', 'bytes': [32, 236, 157, 180, 236, 156, 160], 'logprob': -0.0286702960729599, 'top_logprobs': []}, {'token': '는', 'bytes': [235, 138, 148], 'logprob': -0.0001254693343071267, 'top_logprobs': []}, {'token': ' 그', 'bytes': [32, 234, 183, 184], 'logprob': -0.5631020665168762, 'top_logprobs': []}, {'token': '들의', 'bytes': [235, 147, 164, 236, 157, 152], 'logprob': -0.019804466515779495, 'top_logprobs': []}, {'token': ' 발', 'bytes': [32, 235, 176, 156], 'logprob': -1.288612723350525, 'top_logprobs': []}, {'token': '성', 'bytes': [236, 132, 177], 'logprob': -0.7669153213500977, 'top_logprobs': []}, {'token': ' 기관', 'bytes': [32, 234, 184, 176, 234, 180, 128], 'logprob': -0.16852501034736633, 'top_logprobs': []}, {'token': '과', 'bytes': [234, 179, 188], 'logprob': -0.13997919857501984, 'top_logprobs': []}, {'token': ' 뛰', 'bytes': [32, 235, 155, 176], 'logprob': -3.926103353500366, 'top_logprobs': []}, {'token': '어난', 'bytes': [236, 150, 180, 235, 130, 156], 'logprob': -1.0280383548888494e-06, 'top_logprobs': []}, {'token': ' 모', 'bytes': [32, 235, 170, 168], 'logprob': -0.11381027102470398, 'top_logprobs': []}, {'token': '방', 'bytes': [235, 176, 169], 'logprob': -0.0013426123186945915, 'top_logprobs': []}, {'token': ' 능', 'bytes': [32, 235, 138, 165], 'logprob': -0.0002534720697440207, 'top_logprobs': []}, {'token': '력', 'bytes': [235, 160, 165], 'logprob': -5.9153885558771435e-06, 'top_logprobs': []}, {'token': ' \\xeb\\x8d', 'bytes': [32, 235, 141], 'logprob': -0.35929611325263977, 'top_logprobs': []}, {'token': '\\x95', 'bytes': [149], 'logprob': 0.0, 'top_logprobs': []}, {'token': '분', 'bytes': [235, 182, 132], 'logprob': -7.941850526549388e-06, 'top_logprobs': []}, {'token': '입니다', 'bytes': [236, 158, 133, 235, 139, 136, 235, 139, 164], 'logprob': -0.00012844942102674395, 'top_logprobs': []}, {'token': '.', 'bytes': [46], 'logprob': -3.547789674485102e-05, 'top_logprobs': []}, {'token': ' \\xec\\x95', 'bytes': [32, 236, 149], 'logprob': -0.0026200124993920326, 'top_logprobs': []}, {'token': '\\xb5', 'bytes': [181], 'logprob': 0.0, 'top_logprobs': []}, {'token': '무', 'bytes': [235, 172, 180], 'logprob': -1.9361264946837764e-07, 'top_logprobs': []}, {'token': '새', 'bytes': [236, 131, 136], 'logprob': -1.3782830137643032e-05, 'top_logprobs': []}, {'token': '는', 'bytes': [235, 138, 148], 'logprob': -0.25318628549575806, 'top_logprobs': []}, {'token': ' 시', 'bytes': [32, 236, 139, 156], 'logprob': -1.6473534107208252, 'top_logprobs': []}, {'token': '린', 'bytes': [235, 166, 176], 'logprob': -0.08069829642772675, 'top_logprobs': []}, {'token': '지', 'bytes': [236, 167, 128], 'logprob': -0.049955688416957855, 'top_logprobs': []}, {'token': '스', 'bytes': [236, 138, 164], 'logprob': -0.015432138927280903, 'top_logprobs': []}, {'token': '라는', 'bytes': [235, 157, 188, 235, 138, 148], 'logprob': -0.30109456181526184, 'top_logprobs': []}, {'token': ' 특', 'bytes': [32, 237, 138, 185], 'logprob': -0.6425302624702454, 'top_logprobs': []}, {'token': '수', 'bytes': [236, 136, 152], 'logprob': -0.003387787379324436, 'top_logprobs': []}, {'token': '한', 'bytes': [237, 149, 156], 'logprob': -0.004313449375331402, 'top_logprobs': []}, {'token': ' 발', 'bytes': [32, 235, 176, 156], 'logprob': -0.014631875790655613, 'top_logprobs': []}, {'token': '성', 'bytes': [236, 132, 177], 'logprob': -0.0005301565979607403, 'top_logprobs': []}, {'token': ' 기관', 'bytes': [32, 234, 184, 176, 234, 180, 128], 'logprob': -0.056413859128952026, 'top_logprobs': []}, {'token': '을', 'bytes': [236, 157, 132], 'logprob': -0.0002707529056351632, 'top_logprobs': []}, {'token': ' 가지고', 'bytes': [32, 234, 176, 128, 236, 167, 128, 234, 179, 160], 'logprob': -0.028388777747750282, 'top_logprobs': []}, {'token': ' 있어', 'bytes': [32, 236, 158, 136, 236, 150, 180], 'logprob': -0.528700590133667, 'top_logprobs': []}, {'token': ' 다양한', 'bytes': [32, 235, 139, 164, 236, 150, 145, 237, 149, 156], 'logprob': -0.04291331768035889, 'top_logprobs': []}, {'token': ' 소', 'bytes': [32, 236, 134, 140], 'logprob': -0.002913884585723281, 'top_logprobs': []}, {'token': '리를', 'bytes': [235, 166, 172, 235, 165, 188], 'logprob': -0.00071386230411008, 'top_logprobs': []}, {'token': ' \\xeb\\x82', 'bytes': [32, 235, 130], 'logprob': -0.048139628022909164, 'top_logprobs': []}, {'token': '\\xbc', 'bytes': [188], 'logprob': -1.0280383548888494e-06, 'top_logprobs': []}, {'token': ' 수', 'bytes': [32, 236, 136, 152], 'logprob': -4.320199877838604e-07, 'top_logprobs': []}, {'token': ' 있습니다', 'bytes': [32, 236, 158, 136, 236, 138, 181, 235, 139, 136, 235, 139, 164], 'logprob': -0.07561430335044861, 'top_logprobs': []}, {'token': '.', 'bytes': [46], 'logprob': -3.054500666621607e-06, 'top_logprobs': []}, {'token': ' 게', 'bytes': [32, 234, 178, 140], 'logprob': -4.451381206512451, 'top_logprobs': []}, {'token': '다가', 'bytes': [235, 139, 164, 234, 176, 128], 'logprob': -6.2729995988775045e-06, 'top_logprobs': []}, {'token': ',', 'bytes': [44], 'logprob': -0.5820247530937195, 'top_logprobs': []}, {'token': ' 그', 'bytes': [32, 234, 183, 184], 'logprob': -2.87056303024292, 'top_logprobs': []}, {'token': '들은', 'bytes': [235, 147, 164, 236, 157, 128], 'logprob': -0.12718385457992554, 'top_logprobs': []}, {'token': ' 사회', 'bytes': [32, 236, 130, 172, 237, 154, 140], 'logprob': -1.2264224290847778, 'top_logprobs': []}, {'token': '적인', 'bytes': [236, 160, 129, 236, 157, 184], 'logprob': -1.9230698347091675, 'top_logprobs': []}, {'token': ' 동', 'bytes': [32, 235, 143, 153], 'logprob': -0.002815455198287964, 'top_logprobs': []}, {'token': '물', 'bytes': [235, 172, 188], 'logprob': -0.0382930189371109, 'top_logprobs': []}, {'token': '로', 'bytes': [235, 161, 156], 'logprob': -0.013620367273688316, 'top_logprobs': []}, {'token': '서', 'bytes': [236, 132, 156], 'logprob': -0.30064523220062256, 'top_logprobs': []}, {'token': ' 다른', 'bytes': [32, 235, 139, 164, 235, 165, 184], 'logprob': -1.9924943447113037, 'top_logprobs': []}, {'token': ' \\xec\\x95', 'bytes': [32, 236, 149], 'logprob': -0.4811660349369049, 'top_logprobs': []}, {'token': '\\xb5', 'bytes': [181], 'logprob': -3.173704271830502e-06, 'top_logprobs': []}, {'token': '무', 'bytes': [235, 172, 180], 'logprob': -1.9361264946837764e-07, 'top_logprobs': []}, {'token': '새', 'bytes': [236, 131, 136], 'logprob': -2.3199920178740285e-05, 'top_logprobs': []}, {'token': '나', 'bytes': [235, 130, 152], 'logprob': -0.0829845666885376, 'top_logprobs': []}, {'token': ' 주변', 'bytes': [32, 236, 163, 188, 235, 179, 128], 'logprob': -0.3723578155040741, 'top_logprobs': []}, {'token': ' 환경', 'bytes': [32, 237, 153, 152, 234, 178, 189], 'logprob': -0.907126247882843, 'top_logprobs': []}, {'token': '의', 'bytes': [236, 157, 152], 'logprob': -0.09961745887994766, 'top_logprobs': []}, {'token': ' 소', 'bytes': [32, 236, 134, 140], 'logprob': -2.5822400857578032e-05, 'top_logprobs': []}, {'token': '리를', 'bytes': [235, 166, 172, 235, 165, 188], 'logprob': -0.006191968452185392, 'top_logprobs': []}, {'token': ' 모', 'bytes': [32, 235, 170, 168], 'logprob': -0.29743802547454834, 'top_logprobs': []}, {'token': '방', 'bytes': [235, 176, 169], 'logprob': -2.816093228830141e-06, 'top_logprobs': []}, {'token': '함', 'bytes': [237, 149, 168], 'logprob': -2.682001829147339, 'top_logprobs': []}, {'token': '으로', 'bytes': [236, 156, 188, 235, 161, 156], 'logprob': -1.8624639324116288e-06, 'top_logprobs': []}, {'token': '써', 'bytes': [236, 141, 168], 'logprob': -1.2471589798224159e-05, 'top_logprobs': []}, {'token': ' 의', 'bytes': [32, 236, 157, 152], 'logprob': -0.911808431148529, 'top_logprobs': []}, {'token': '사', 'bytes': [236, 130, 172], 'logprob': -6.635164754698053e-05, 'top_logprobs': []}, {'token': '소', 'bytes': [236, 134, 140], 'logprob': -0.005235529970377684, 'top_logprobs': []}, {'token': '통', 'bytes': [237, 134, 181], 'logprob': 0.0, 'top_logprobs': []}, {'token': '을', 'bytes': [236, 157, 132], 'logprob': -0.18629735708236694, 'top_logprobs': []}, {'token': ' 시', 'bytes': [32, 236, 139, 156], 'logprob': -1.678897738456726, 'top_logprobs': []}, {'token': '도', 'bytes': [235, 143, 132], 'logprob': 0.0, 'top_logprobs': []}, {'token': '합니다', 'bytes': [237, 149, 169, 235, 139, 136, 235, 139, 164], 'logprob': -0.052944790571928024, 'top_logprobs': []}, {'token': '.', 'bytes': [46], 'logprob': -0.12692958116531372, 'top_logprobs': []}, {'token': ' 이러한', 'bytes': [32, 236, 157, 180, 235, 159, 172, 237, 149, 156], 'logprob': -1.1536396741867065, 'top_logprobs': []}, {'token': ' 능', 'bytes': [32, 235, 138, 165], 'logprob': -0.8053078055381775, 'top_logprobs': []}, {'token': '력', 'bytes': [235, 160, 165], 'logprob': -0.012905134819447994, 'top_logprobs': []}, {'token': '은', 'bytes': [236, 157, 128], 'logprob': -0.10468367487192154, 'top_logprobs': []}, {'token': ' \\xec\\x95', 'bytes': [32, 236, 149], 'logprob': -2.006305456161499, 'top_logprobs': []}, {'token': '\\xb5', 'bytes': [181], 'logprob': -2.2200749754119897e-06, 'top_logprobs': []}, {'token': '무', 'bytes': [235, 172, 180], 'logprob': -4.320199877838604e-07, 'top_logprobs': []}, {'token': '새', 'bytes': [236, 131, 136], 'logprob': -3.650518920039758e-06, 'top_logprobs': []}, {'token': '가', 'bytes': [234, 176, 128], 'logprob': -0.03883244842290878, 'top_logprobs': []}, {'token': ' 자연', 'bytes': [32, 236, 158, 144, 236, 151, 176], 'logprob': -1.6734697818756104, 'top_logprobs': []}, {'token': ' 상태', 'bytes': [32, 236, 131, 129, 237, 131, 156], 'logprob': -2.36165452003479, 'top_logprobs': []}, {'token': '에서', 'bytes': [236, 151, 144, 236, 132, 156], 'logprob': -0.11111121624708176, 'top_logprobs': []}, {'token': ' 다른', 'bytes': [32, 235, 139, 164, 235, 165, 184], 'logprob': -1.5469611883163452, 'top_logprobs': []}, {'token': ' 새', 'bytes': [32, 236, 131, 136], 'logprob': -0.7160435914993286, 'top_logprobs': []}, {'token': '들과', 'bytes': [235, 147, 164, 234, 179, 188], 'logprob': -0.1226692870259285, 'top_logprobs': []}, {'token': ' 교', 'bytes': [32, 234, 181, 144], 'logprob': -2.657003402709961, 'top_logprobs': []}, {'token': '류', 'bytes': [235, 165, 152], 'logprob': -0.023686813190579414, 'top_logprobs': []}, {'token': '하고', 'bytes': [237, 149, 152, 234, 179, 160], 'logprob': -4.34061861038208, 'top_logprobs': []}, {'token': '자', 'bytes': [236, 158, 144], 'logprob': -9999.0, 'top_logprobs': []}, {'token': ' 하는', 'bytes': [32, 237, 149, 152, 235, 138, 148], 'logprob': -0.9944992065429688, 'top_logprobs': []}, {'token': ' 본', 'bytes': [32, 235, 179, 184], 'logprob': -0.07761964946985245, 'top_logprobs': []}, {'token': '능', 'bytes': [235, 138, 165], 'logprob': -0.0017729965038597584, 'top_logprobs': []}, {'token': '에서', 'bytes': [236, 151, 144, 236, 132, 156], 'logprob': -0.6013312339782715, 'top_logprobs': []}, {'token': ' 비롯', 'bytes': [32, 235, 185, 132, 235, 161, 175], 'logprob': -0.10743808001279831, 'top_logprobs': []}, {'token': '된', 'bytes': [235, 144, 156], 'logprob': -0.30401018261909485, 'top_logprobs': []}, {'token': ' 것으로', 'bytes': [32, 234, 178, 131, 236, 156, 188, 235, 161, 156], 'logprob': -1.2343738079071045, 'top_logprobs': []}, {'token': ' 볼', 'bytes': [32, 235, 179, 188], 'logprob': -1.702530026435852, 'top_logprobs': []}, {'token': ' 수', 'bytes': [32, 236, 136, 152], 'logprob': -6.623244553338736e-05, 'top_logprobs': []}, {'token': ' 있습니다', 'bytes': [32, 236, 158, 136, 236, 138, 181, 235, 139, 136, 235, 139, 164], 'logprob': -0.06227262318134308, 'top_logprobs': []}, {'token': '.', 'bytes': [46], 'logprob': -0.01604539528489113, 'top_logprobs': []}, {'token': ' 사람', 'bytes': [32, 236, 130, 172, 235, 158, 140], 'logprob': -1.223806381225586, 'top_logprobs': []}, {'token': '의', 'bytes': [236, 157, 152], 'logprob': -0.3736502230167389, 'top_logprobs': []}, {'token': ' 말을', 'bytes': [32, 235, 167, 144, 236, 157, 132], 'logprob': -0.06463563442230225, 'top_logprobs': []}, {'token': ' 따라', 'bytes': [32, 235, 148, 176, 235, 157, 188], 'logprob': -0.5618042349815369, 'top_logprobs': []}, {'token': ' 하는', 'bytes': [32, 237, 149, 152, 235, 138, 148], 'logprob': -0.7109476327896118, 'top_logprobs': []}, {'token': ' 것도', 'bytes': [32, 234, 178, 131, 235, 143, 132], 'logprob': -0.5140809416770935, 'top_logprobs': []}, {'token': ' 이러한', 'bytes': [32, 236, 157, 180, 235, 159, 172, 237, 149, 156], 'logprob': -0.6430902481079102, 'top_logprobs': []}, {'token': ' 모', 'bytes': [32, 235, 170, 168], 'logprob': -0.029246438294649124, 'top_logprobs': []}, {'token': '방', 'bytes': [235, 176, 169], 'logprob': -6.432518421206623e-05, 'top_logprobs': []}, {'token': ' 능', 'bytes': [32, 235, 138, 165], 'logprob': -0.30914098024368286, 'top_logprobs': []}, {'token': '력', 'bytes': [235, 160, 165], 'logprob': -0.0016586360288783908, 'top_logprobs': []}, {'token': '의', 'bytes': [236, 157, 152], 'logprob': -0.0029162613209336996, 'top_logprobs': []}, {'token': ' 연', 'bytes': [32, 236, 151, 176], 'logprob': -0.7252622246742249, 'top_logprobs': []}, {'token': '장', 'bytes': [236, 158, 165], 'logprob': -0.0009155054576694965, 'top_logprobs': []}, {'token': '선', 'bytes': [236, 132, 160], 'logprob': -0.007772013545036316, 'top_logprobs': []}, {'token': '상', 'bytes': [236, 131, 129], 'logprob': -0.5408213138580322, 'top_logprobs': []}, {'token': '에', 'bytes': [236, 151, 144], 'logprob': -0.18337500095367432, 'top_logprobs': []}, {'token': ' 있으며', 'bytes': [32, 236, 158, 136, 236, 156, 188, 235, 169, 176], 'logprob': -0.7390093207359314, 'top_logprobs': []}, {'token': ',', 'bytes': [44], 'logprob': -0.0008388153510168195, 'top_logprobs': []}, {'token': ' 반', 'bytes': [32, 235, 176, 152], 'logprob': -4.595965385437012, 'top_logprobs': []}, {'token': '려', 'bytes': [235, 160, 164], 'logprob': -0.00015383612480945885, 'top_logprobs': []}, {'token': '동', 'bytes': [235, 143, 153], 'logprob': -0.11848678439855576, 'top_logprobs': []}, {'token': '물', 'bytes': [235, 172, 188], 'logprob': -0.0007862794445827603, 'top_logprobs': []}, {'token': '로', 'bytes': [235, 161, 156], 'logprob': -0.0008869222365319729, 'top_logprobs': []}, {'token': '서', 'bytes': [236, 132, 156], 'logprob': -0.43910467624664307, 'top_logprobs': []}, {'token': ' 사람', 'bytes': [32, 236, 130, 172, 235, 158, 140], 'logprob': -0.7877948880195618, 'top_logprobs': []}, {'token': '과', 'bytes': [234, 179, 188], 'logprob': -0.08117050677537918, 'top_logprobs': []}, {'token': ' 소', 'bytes': [32, 236, 134, 140], 'logprob': -2.5061166286468506, 'top_logprobs': []}, {'token': '통', 'bytes': [237, 134, 181], 'logprob': -0.0021019638516008854, 'top_logprobs': []}, {'token': '하는', 'bytes': [237, 149, 152, 235, 138, 148], 'logprob': -2.2894434928894043, 'top_logprobs': []}, {'token': ' 방법', 'bytes': [32, 235, 176, 169, 235, 178, 149], 'logprob': -0.8496363759040833, 'top_logprobs': []}, {'token': ' 중', 'bytes': [32, 236, 164, 145], 'logprob': -0.1696912795305252, 'top_logprobs': []}, {'token': ' 하나', 'bytes': [32, 237, 149, 152, 235, 130, 152], 'logprob': -0.0007185076246969402, 'top_logprobs': []}, {'token': '로', 'bytes': [235, 161, 156], 'logprob': -1.0043346881866455, 'top_logprobs': []}, {'token': ' 볼', 'bytes': [32, 235, 179, 188], 'logprob': -4.544769287109375, 'top_logprobs': []}, {'token': ' 수', 'bytes': [32, 236, 136, 152], 'logprob': -4.0126840758603066e-05, 'top_logprobs': []}, {'token': ' 있습니다', 'bytes': [32, 236, 158, 136, 236, 138, 181, 235, 139, 136, 235, 139, 164], 'logprob': -0.0009289718000218272, 'top_logprobs': []}, {'token': '.', 'bytes': [46], 'logprob': -0.00014895245840307325, 'top_logprobs': []}], 'refusal': None}

토큰 사용량(Token usage)

많은 모델 제공업체가 호출 응답의 일부로 token usage 정보를 반환합니다. 사용 가능한 경우 이 정보는 해당 모델에서 생성한 AIMessage 객체에 포함됩니다. 자세한 내용은 message 가이드를 참조하세요.

⚠️ 참고: OpenAI 및 Azure OpenAI chat completion를 포함한 일부 제공업체 API는 사용자가 스트리밍 컨텍스트에서 token usage data를 수신하도록 선택해야 합니다. 자세한 내용은 통합 가이드의 streaming usage metadata 섹션을 참조하세요.

아래와 같이 callback 또는 context manager를 사용하여 애플리케이션의 모델 전체에서 집계 token count를 추적할 수 있습니다:

Callback handler

from langchain_core.callbacks import UsageMetadataCallbackHandler
 
from langchain.chat_models import init_chat_model
 
 
model_1 = init_chat_model(model="openai:gpt-4.1-mini")
model_2 = init_chat_model(model="groq:openai/gpt-oss-20b")
 
callback = UsageMetadataCallbackHandler()
result_1 = model_1.invoke("안녕하세요", config={"callbacks": [callback]})
result_2 = model_2.invoke("안녕하세요", config={"callbacks": [callback]})
callback.usage_metadata
{'gpt-4.1-mini-2025-04-14': {'input_tokens': 9,
  'output_tokens': 10,
  'total_tokens': 19,
  'input_token_details': {'audio': 0, 'cache_read': 0},
  'output_token_details': {'audio': 0, 'reasoning': 0}},
 'openai/gpt-oss-20b': {'input_tokens': 73,
  'output_tokens': 67,
  'total_tokens': 140}}

Context manager

from langchain_core.callbacks import get_usage_metadata_callback
 
from langchain.chat_models import init_chat_model
 
 
model_1 = init_chat_model(model="openai:gpt-4.1-mini")
model_2 = init_chat_model(model="groq:openai/gpt-oss-20b")
 
with get_usage_metadata_callback() as cb:
    model_1.invoke("안녕하세요")
    model_2.invoke("안녕하세요")
    pprint(cb.usage_metadata)
{'gpt-4.1-mini-2025-04-14': {'input_token_details': {'audio': 0,
                                                     'cache_read': 0},
                             'input_tokens': 9,
                             'output_token_details': {'audio': 0,
                                                      'reasoning': 0},
                             'output_tokens': 10,
                             'total_tokens': 19},
 'openai/gpt-oss-20b': {'input_tokens': 73,
                        'output_tokens': 93,
                        'total_tokens': 166}}

호출 구성(Invocation config)

모델을 호출할 때 RunnableConfig 딕셔너리를 사용하여 config 매개변수를 통해 추가 구성을 전달할 수 있습니다. 이는 실행 동작, callback 및 metadata tracking에 대한 런타임 제어를 제공합니다.

일반적인 구성 옵션은 다음과 같습니다:

from langchain_core.callbacks import UsageMetadataCallbackHandler
 
 
my_callback_handler = UsageMetadataCallbackHandler()
 
response = model.invoke(
    "재미있는 농담 하나 해줘",
    config={
        "run_name": "joke_generation",  # 이 실행에 대한 사용자 정의 이름
        "tags": ["humor", "demo"],  # 분류를 위한 태그
        "metadata": {"user_id": "123"},  # 사용자 정의 메타데이터
        "callbacks": [my_callback_handler],  # 콜백 핸들러
    },
)
response
AIMessage(content='물론이죠! 왜 자전거는 꼬리가 없을까요?\n\n왜냐하면 항상 "두 바퀴" 때문에! 😄🚴\u200d♂️', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 36, 'prompt_tokens': 15, 'total_tokens': 51, '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_provider': 'openai', 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_cbf1785567', 'id': 'chatcmpl-CX79xE7nfef66o7cds2hSQ6xXeZwO', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': {'content': [{'token': '물', 'bytes': [235, 172, 188], 'logprob': -0.004416705574840307, 'top_logprobs': []}, {'token': '론', 'bytes': [235, 161, 160], 'logprob': -6.704273118884885e-07, 'top_logprobs': []}, {'token': '이', 'bytes': [236, 157, 180], 'logprob': -0.020117396488785744, 'top_logprobs': []}, {'token': '죠', 'bytes': [236, 163, 160], 'logprob': -0.00035727277281694114, 'top_logprobs': []}, {'token': '!', 'bytes': [33], 'logprob': -0.006455655209720135, 'top_logprobs': []}, {'token': ' 왜', 'bytes': [32, 236, 153, 156], 'logprob': -0.577566385269165, 'top_logprobs': []}, {'token': ' 자', 'bytes': [32, 236, 158, 144], 'logprob': -1.4182344675064087, 'top_logprobs': []}, {'token': '전', 'bytes': [236, 160, 132], 'logprob': -0.3213099539279938, 'top_logprobs': []}, {'token': '거', 'bytes': [234, 177, 176], 'logprob': -1.9981420336989686e-05, 'top_logprobs': []}, {'token': '는', 'bytes': [235, 138, 148], 'logprob': -0.07904169708490372, 'top_logprobs': []}, {'token': ' \\xea\\xbc', 'bytes': [32, 234, 188], 'logprob': -9999.0, 'top_logprobs': []}, {'token': '\\xac', 'bytes': [172], 'logprob': -0.22259581089019775, 'top_logprobs': []}, {'token': '리가', 'bytes': [235, 166, 172, 234, 176, 128], 'logprob': -1.2819671630859375, 'top_logprobs': []}, {'token': ' 없', 'bytes': [32, 236, 151, 134], 'logprob': -0.0026853985618799925, 'top_logprobs': []}, {'token': '을', 'bytes': [236, 157, 132], 'logprob': -0.0001625379954930395, 'top_logprobs': []}, {'token': '까요', 'bytes': [234, 185, 140, 236, 154, 148], 'logprob': -7.517272024415433e-05, 'top_logprobs': []}, {'token': '?\n\n', 'bytes': [63, 10, 10], 'logprob': -0.055675484240055084, 'top_logprobs': []}, {'token': '왜', 'bytes': [236, 153, 156], 'logprob': -1.0574109554290771, 'top_logprobs': []}, {'token': '냐', 'bytes': [235, 131, 144], 'logprob': -1.0802738870552275e-05, 'top_logprobs': []}, {'token': '하면', 'bytes': [237, 149, 152, 235, 169, 180], 'logprob': -0.007656677160412073, 'top_logprobs': []}, {'token': ' 항상', 'bytes': [32, 237, 149, 173, 236, 131, 129], 'logprob': -0.8185937404632568, 'top_logprobs': []}, {'token': ' "', 'bytes': [32, 34], 'logprob': -1.5765576362609863, 'top_logprobs': []}, {'token': '두', 'bytes': [235, 145, 144], 'logprob': -1.432828426361084, 'top_logprobs': []}, {'token': ' 바', 'bytes': [32, 235, 176, 148], 'logprob': -0.2352759838104248, 'top_logprobs': []}, {'token': '\\xed\\x80', 'bytes': [237, 128], 'logprob': -0.0003936152206733823, 'top_logprobs': []}, {'token': '\\xb4', 'bytes': [180], 'logprob': -1.9361264946837764e-07, 'top_logprobs': []}, {'token': '"', 'bytes': [34], 'logprob': -0.05340360850095749, 'top_logprobs': []}, {'token': ' 때문에', 'bytes': [32, 235, 149, 140, 235, 172, 184, 236, 151, 144], 'logprob': -2.021141529083252, 'top_logprobs': []}, {'token': '!', 'bytes': [33], 'logprob': -0.8149132132530212, 'top_logprobs': []}, {'token': ' \\xf0\\x9f\\x98', 'bytes': [32, 240, 159, 152], 'logprob': -0.6668349504470825, 'top_logprobs': []}, {'token': '\\x84', 'bytes': [132], 'logprob': -0.001466057845391333, 'top_logprobs': []}, {'token': '\\xf0\\x9f\\x9a', 'bytes': [240, 159, 154], 'logprob': -3.7746753692626953, 'top_logprobs': []}, {'token': '\\xb4', 'bytes': [180], 'logprob': -0.07889165729284286, 'top_logprobs': []}, {'token': '\u200d', 'bytes': [226, 128, 141], 'logprob': -0.029907269403338432, 'top_logprobs': []}, {'token': '♂', 'bytes': [226, 153, 130], 'logprob': -0.10020699352025986, 'top_logprobs': []}, {'token': '️', 'bytes': [239, 184, 143], 'logprob': -2.7610454708337784e-05, 'top_logprobs': []}], 'refusal': None}}, id='lc_run--850bf3a9-1e9b-40f7-9f15-266dcb0470c4-0', usage_metadata={'input_tokens': 15, 'output_tokens': 36, 'total_tokens': 51, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})

이러한 구성 값은 다음과 같은 경우에 특히 유용합니다:

  • LangSmith 추적으로 디버깅
  • 사용자 정의 로깅 또는 모니터링 구현
  • 프로덕션에서 리소스 사용 제어
  • 복잡한 파이프라인 전체에서 호출 추적

주요 구성 속성

  • run_name: 로그 및 추적에서 이 특정 호출을 식별합니다. 하위 호출에 의해 상속되지 않습니다.
  • tags: 디버깅 도구에서 필터링 및 구성을 위해 모든 하위 호출에 의해 상속되는 레이블입니다.
  • metadata: 추가 컨텍스트를 추적하기 위한 사용자 정의 키-값 쌍으로, 모든 하위 호출에 의해 상속됩니다.
  • max_concurrency: batch() 또는 batch_as_completed()를 사용할 때 최대 병렬 호출 수를 제어합니다.
  • callbacks: 실행 중 이벤트를 모니터링하고 응답하기 위한 핸들러입니다.
  • recursion_limit: 복잡한 파이프라인에서 무한 루프를 방지하기 위한 chain의 최대 재귀 깊이입니다.

⚠️ 참고: 지원되는 모든 속성은 전체 RunnableConfig 참조를 확인하세요.

Configurable models

configurable_fields를 지정하여 런타임 구성 가능한 모델을 만들 수도 있습니다. 모델 값을 지정하지 않으면 기본적으로 'model''model_provider'를 구성할 수 있습니다.

from langchain.chat_models import init_chat_model
 
 
configurable_model = init_chat_model(temperature=0)
 
configurable_model.invoke(
    "당신의 이름은 무엇인가요",
    config={"configurable": {"model": "gpt-5-nano"}},  # GPT-5-Nano로 실행
)
 
configurable_model.invoke(
    "당신의 이름은 무엇인가요",
    config={"configurable": {"model": "claude-sonnet-4-5"}},  # Claude로 실행
)

기본값이 있는 구성 가능한 모델

기본 모델 값으로 구성 가능한 모델을 만들고 구성 가능한 매개변수를 지정하고 구성 가능한 매개변수에 접두사를 추가할 수 있습니다:

first_model = init_chat_model(
    model="gpt-4.1-mini",
    temperature=0,
    configurable_fields=("model", "model_provider", "temperature", "max_tokens"),
    config_prefix="first",  # 여러 모델이 있는 체인에서 유용함
)
first_model.invoke("당신의 이름은 무엇인가요?")
AIMessage(content='안녕하세요! 저는 AI 언어 모델인 ChatGPT입니다. 어떻게 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 15, 'total_tokens': 35, '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_provider': 'openai', 'model_name': 'gpt-4.1-mini-2025-04-14', 'system_fingerprint': 'fp_4c2851f862', 'id': 'chatcmpl-CX7G1g7oWMB9rrxKxnrML5iRacvNC', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--04d73045-4f87-495e-b09f-e5e6a91b48fc-0', usage_metadata={'input_tokens': 15, 'output_tokens': 20, 'total_tokens': 35, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
first_model.invoke(
    "당신의 이름은 무엇인가요?",
    config={
        "configurable": {
            "first_model": "groq:groq/compound",
            "first_temperature": 0.5,
            "first_max_tokens": 100,
        }
    },
)
AIMessage(content='제 이름은 **Compound**입니다. 😊', additional_kwargs={'reasoning_content': '<Think>\n\n</Think>'}, response_metadata={'token_usage': {'completion_tokens': 78, 'prompt_tokens': 247, 'total_tokens': 325, 'completion_time': 0.168236, 'prompt_time': 0.00852, 'queue_time': 0.102906, 'total_time': 0.176756}, 'model_name': 'groq/compound', 'system_fingerprint': None, 'service_tier': 'on_demand', 'finish_reason': 'stop', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--bca1f233-e1a4-4c7a-a830-a312ac49de88-0', usage_metadata={'input_tokens': 247, 'output_tokens': 78, 'total_tokens': 325})

구성 가능한 모델을 선언적으로 사용

구성 가능한 모델에서 bind_tools, with_structured_output, with_configurable 등과 같은 선언적 작업을 호출하고 정기적으로 인스턴스화된 chat model 객체와 동일한 방식으로 구성 가능한 모델을 연결할 수 있습니다.

from pydantic import BaseModel, Field
 
 
class GetWeather(BaseModel):
    """주어진 위치의 현재 날씨를 가져옵니다"""
 
    location: str = Field(..., description="도시와 주, 예: San Francisco, CA")
 
 
class GetPopulation(BaseModel):
    """주어진 위치의 현재 인구를 가져옵니다"""
 
    location: str = Field(..., description="도시와 주, 예: San Francisco, CA")
 
 
model = init_chat_model(temperature=0)
model_with_tools = model.bind_tools([GetWeather, GetPopulation])
model_with_tools.invoke(
    "2024년에 LA와 NYC 중 어디가 더 큰가요",
    config={"configurable": {"model": "gpt-4.1-mini"}},
).tool_calls
[{'name': 'GetPopulation',
  'args': {'location': 'Los Angeles, CA'},
  'id': 'call_SYhT5VHkVUsa7r0L4ZQjLGiI',
  'type': 'tool_call'},
 {'name': 'GetPopulation',
  'args': {'location': 'New York, NY'},
  'id': 'call_kvJIlRQFOIT25TJkon2n2WnQ',
  'type': 'tool_call'}]
model_with_tools.invoke(
    "2024년에 LA와 NYC 중 어디가 더 큰가요",
    config={"configurable": {"model": "gpt-5-mini"}},
).tool_calls
[{'name': 'GetPopulation',
  'args': {'location': 'Los Angeles, CA'},
  'id': 'call_7peTAyYcwOP0wb7xLSQOzA17',
  'type': 'tool_call'},
 {'name': 'GetPopulation',
  'args': {'location': 'New York, NY'},
  'id': 'call_IbRkyNl0pN4MGPb1SZxNasYc',
  'type': 'tool_call'}]