GPT를 Claude로 바꾸는 데 필요한 코드 한 줄, LangChain 첫걸음

해외여행을 갈 때 멀티 어댑터를 챙깁니다. 콘센트 모양은 나라마다 달라도 어댑터를 끼우면 같은 충전기를 그대로 씁니다.

Agent 프로그램도 비슷한 상황을 만납니다. 모델을 서비스하는 회사(provider)마다 요청 모양이 달라서 회사를 바꾸려면 호출 코드를 고쳐야 합니다. LangChain은 이 사이에 끼우는 어댑터 역할을 하는 프레임워크(framework, 자주 쓰는 뼈대를 미리 만들어 둔 도구 묶음)입니다.

그러면 모델을 바꿀 때 정말 한 줄만 고치면 될까요? 대부분은 그렇습니다. 다만 어댑터는 콘센트 모양만 맞춰 줄 뿐 전압까지 바꿔 주지는 않습니다. 회사를 바꿔 보세요.

모델 회사
temperature

          

모델 이름 문자열 하나가 서랍을 고릅니다

init_chat_model에 "회사:모델" 모양의 문자열을 넘기면 LangChain이 콜론 앞의 회사 이름을 보고 알맞은 클래스를 꺼내 줍니다. 회사마다 따로 만들어진 클래스가 서랍장에 들어 있는 셈입니다.

그 클래스는 회사별 패키지 안에 있어서 해당 패키지를 설치해야 합니다. API 키는 환경변수(프로그램 밖에서 넘겨주는 설정값)에 둡니다. 모델 이름은 바뀌지 않는 고정 이름을 쓰라고 LangChain 레퍼런스가 권합니다.

이 구조 덕분에 코드의 나머지는 어느 회사의 클래스인지 몰라도 됩니다. 클래스마다 같은 invoke가 있어서 부르는 쪽 코드는 그대로입니다.

클래스·패키지·환경변수 이름은 langchain 1.4와 각 회사 패키지 기준입니다. 모델 이름은 예시입니다.

llm.pypython
# pip install -U "langchain[anthropic]"
from langchain.chat_models import init_chat_model

# 1 회사와 모델을 문자열로 고른다
model = init_chat_model("anthropic:claude-sonnet-5-5")
# 2 OpenAI로 바꾸려면 이 줄만 바꾼다
# model = init_chat_model("openai:gpt-5.5")
  1. 모델을 만드는 한 줄입니다. 콜론 앞이 회사이고 뒤가 모델 이름입니다. 회사 패키지는 미리 설치해 둡니다(OpenAI는 "langchain[openai]"). 모델 이름은 예시이며 실제로는 회사 문서의 최신 이름을 씁니다.
  2. 회사를 바꾸는 방법입니다. 줄 하나만 바꾸면 되고 이 파일의 나머지 코드는 건드리지 않습니다. API 키는 ANTHROPIC_API_KEY나 OPENAI_API_KEY처럼 회사별 환경변수에서 읽습니다.

메시지는 말한 사람의 이름표를 붙인 쪽지입니다

단톡방에서 누가 한 말인지는 이름표로 구분합니다. 모델에 보내는 대화도 쪽지마다 역할(role) 이름표가 붙어 있고 LangChain에는 이름표별로 클래스가 네 개 있습니다. 클래스 대신 이름표와 내용을 묶은 딕셔너리(이름과 값을 짝지어 담는 Python 자료형)로 써도 같은 뜻으로 받아 줍니다. 스크롤하며 채팅창에 쪽지가 하나씩 붙는 모습을 보세요.

system: 모델이 지킬 규칙

SystemMessage는 모델이 어떻게 행동할지와 상황을 알려 줍니다. 대화 맨 앞에 한 번 붙이는 안내문입니다. 딕셔너리로는 {"role": "system", "content": "…"}입니다.

user: 사용자가 한 말

HumanMessage는 사용자가 쓴 질문입니다. 이름표는 "user"입니다. 이 쪽지까지 보내면 모델이 답할 수 있습니다.

assistant: 모델의 답

AIMessage는 모델이 쓴 응답입니다. 글만 있을 수도 있고 도구를 부르는 요청이 들어 있을 수도 있습니다. 이 쪽지는 우리가 쓰지 않고 모델이 돌려줍니다.

tool: 도구를 실행한 결과

ToolMessage는 코드가 도구를 실행하고 받은 결과입니다. 어느 요청의 결과인지 알 수 있게 번호(tool_call_id)가 함께 붙습니다.

설명을 위한 예시 문구입니다. 단톡방 이름표는 사람이 보려는 표시이지만 role은 모델이 누구 말인지 읽는 데 쓰는 값이라 비유가 꼭 맞지는 않습니다. 이 글의 코드는 읽기 쉬운 딕셔너리 모양을 씁니다.

Anthropic 응답 content: [글, 도구 요청] usage: input/output stop_reason OpenAI 응답 message.content tool_calls · usage finish_reason LangChain 어느 회사든 같은 AIMessage contenttool_callsusage_metadatacontent_blocks response_metadata: 멈춘 이유는 원본 이름 그대로

같은 질문에 두 회사 모델이 답한 예시를 정리한 값입니다. response_metadata에는 이보다 많은 항목이 들어갑니다.

응답에는 글만 있지 않습니다

모델의 응답은 글 한 줄이 아니라 AIMessage 객체입니다. 객체(object)는 값 여러 개를 이름을 붙여 한 덩어리로 묶은 것입니다. content_blocks는 글과 도구 요청을 같은 모양의 조각 목록으로 정리한 것이고 tool_calls는 모델이 부른 도구 요청 목록이며 usage_metadata는 토큰(token, 모델이 글을 읽고 쓰는 단위) 수입니다. 그림에서 초록 칸은 어느 회사 모델이든 모양이 같습니다.

주황 스티커는 회사가 보내 준 원래 정보를 그대로 담은 곳이라 이름이 다릅니다. 모델이 답을 멈춘 이유를 적은 값이 Anthropic은 stop_reason이고 OpenAI는 finish_reason입니다. LangChain이 맞춰 주는 곳은 글과 도구 요청과 토큰 수이고 멈춘 이유는 아직 회사 이름 그대로입니다.

llm.pypython
messages = [
    {"role": "system", "content": "너는 IT 헬프데스크 상담원이다."},
    {"role": "user", "content": "T-1042 티켓 어떻게 됐어요?"},
]
# 3 호출은 회사와 상관없이 같다
res = model.invoke(messages)
# 4 응답은 AIMessage
print(type(res).__name__, res.content)
print(res.usage_metadata)
print(res.response_metadata)
터미널
$ python llm.py AIMessage T-1042는 부품 대기 중입니다. {'input_tokens': 20, 'output_tokens': 12, 'total_tokens': 32, 'input_token_details': {}} {'stop_details': None, 'stop_reason': 'end_turn', 'stop_sequence': None}

출력은 예시 답입니다. 코드는 langchain 1.4와 langchain-anthropic 1.7 기준이고 response_metadata에는 이보다 많은 항목이 들어갑니다.

  1. 호출 한 줄입니다. invoke는 메시지 목록을 보내고 응답이 다 만들어질 때까지 기다렸다가 AIMessage 하나를 돌려줍니다.
  2. 응답을 열어 보는 곳입니다. content는 글이고 usage_metadata는 입력과 출력 토큰 수입니다. response_metadata에는 멈춘 이유를 포함한 회사 원본 정보가 들어서 회사에 따라 키 이름이 다릅니다.

invoke는 한 번에 stream은 조금씩 batch는 여러 개를 한꺼번에

호출 방식은 세 가지입니다. invoke는 컵에 물이 다 찬 뒤에 한 번에 받습니다. stream은 물이 차는 대로 조각(chunk)을 받아 바로 화면에 찍을 수 있습니다. batch는 컵 여러 개를 동시에 채웁니다. 세 가지 모두 어느 회사 모델이든 이름과 사용법이 같습니다.

설명을 위한 예시 시간입니다. 답이 완성되는 데 2.4초가 걸린다고 가정했습니다. 실제 시간은 모델과 네트워크에 따라 다릅니다.

llm.pypython
# 5 조각이 오는 대로 찍는다
for chunk in model.stream(messages):
    print(chunk.content, end="|")
# 6 조각을 더하면 하나의 메시지가 된다
full = None
for chunk in model.stream(messages):
    full = chunk if full is None else full + chunk
# 7 질문 여러 개는 한 번에 보낸다
answers = model.batch([messages_a, messages_b])
터미널
$ python llm.py T-1042는 |부품 대기 |중입니다.|

출력은 예시입니다. 조각을 나누는 단위는 모델이 정하며 빈 조각이 섞여 오기도 합니다.

  1. 조각을 바로 출력합니다. stream은 AIMessageChunk 조각을 하나씩 내놓는 반복자입니다. end="|"는 조각이 나뉘는 것을 보이려는 구분 표시입니다.
  2. 조각을 더합니다. 조각끼리 +로 더하면 invoke가 돌려주는 메시지와 같은 모양이 됩니다. 처음에는 더할 대상이 없어서 None에서 시작합니다.
  3. 여러 질문을 동시에 보냅니다. batch는 목록의 질문마다 따로 호출하고 답을 같은 순서의 목록으로 돌려줍니다. 답이 모두 끝나야 결과가 나옵니다.

문자열만 바꾸면 되지 않는 경우

같은 호출이 회사마다 다른 요청 봉투가 되는 것은 위의 히어로에서 보았습니다. Anthropic은 system을 메시지 목록 밖의 별도 칸으로 받고 max_tokens(답 길이 한도)가 꼭 필요합니다. LangChain이 이 둘을 알아서 맞춰 줍니다.

모델의 성격은 LangChain도 맞춰 주지 못합니다. 답의 무작위성을 조절하는 설정인 temperature와 top_p와 top_k(답 후보를 몇 개로 좁힐지 정하는 값)가 그렇습니다. Anthropic 문서에 따르면 Claude Sonnet 5.5와 Opus 5.5는 이 값을 기본값이 아닌 값으로 보내면 요청을 거절하고 400 오류(요청이 잘못됐다는 응답)를 돌려줍니다. 다이얼을 돌려 보세요.

Claude Sonnet 5.5·Opus 5.5 기준입니다. top_p와 top_k도 같은 방식으로 기본값이 아니면 거절됩니다. 바늘 위치는 설명을 위한 예시입니다.

실제로 나가는 요청 두 장 보기

anthropic:claude-sonnet-5-5

POST /v1/messages
{
  "model": "claude-sonnet-5-5",
  "max_tokens": 128000,
  "system": "너는 IT 헬프데스크 상담원이다.",
  "messages": [
    {"role": "user",
     "content": "T-1042 티켓 어떻게 됐어요?"}
  ]
}

openai:gpt-5.5

POST /v1/chat/completions
{
  "model": "gpt-5.5",
  "messages": [
    {"role": "system",
     "content": "너는 IT 헬프데스크 상담원이다."},
    {"role": "user",
     "content": "T-1042 티켓 어떻게 됐어요?"}
  ]
}

같은 코드로 보낸 요청을 줄여 옮긴 예시입니다. 128000은 이 환경에서 LangChain이 모델 정보를 보고 채운 값이며 버전에 따라 다를 수 있습니다.

모델을 런타임에 바꾸고 싶다면

설정에서 모델 이름을 읽어 실행 중에 바꾸려면 configurable_fields를 씁니다. 모델을 만들 때 이름을 비워 두고 호출할 때 넘깁니다. 샘플링 파라미터는 모델마다 허용 여부가 달라서 코드에 박아 두지 말고 모델별 설정에 둡니다.

llm.pypython
# 8 모델을 비워 두고 바꿀 수 있는 항목만 연다
model = init_chat_model(
    configurable_fields=("model", "model_provider"))
# 9 호출할 때 모델을 고른다
res = model.invoke(messages, config={"configurable": {
    "model": "gpt-5.5", "model_provider": "openai"}})
  1. 바꿀 수 있는 항목을 정해 둡니다. 모델 이름을 비워 두면 model과 model_provider가 호출 때 정해지는 항목이 됩니다. 문서는 모든 항목을 여는 "any"를 보안상 위험하다고 경고합니다.
  2. 호출할 때 고릅니다. config에 고른 값을 넘기면 그 모델로 호출합니다. 값은 설정 파일이나 사용자 선택에서 가져올 수 있습니다.

프롬프트 틀과 LCEL은 지금 어디에 있나

ChatPromptTemplate은 빈칸이 있는 메시지 틀입니다. 변수를 채워 invoke하면 메시지 목록이 나옵니다. | 연산자로 틀과 모델을 이어 붙이는 문법은 LCEL(LangChain Expression Language, 부품을 파이프로 잇는 문법)이라 부릅니다. 둘 다 공통 부품 패키지인 langchain-core에 그대로 있습니다.

LangChain 1.x에서 Agent를 만들 때 권하는 방법은 create_agent입니다. 예전의 LLMChain 같은 낡은 체인은 langchain-classic 패키지로 옮겨졌습니다. 이 시리즈는 create_agent를 쓰는 쪽으로 갑니다.

ChatPromptTemplate 예시 보기
prompt.pypython
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "너는 IT 헬프데스크 상담원이다."),
    ("user", "{question}"),
])
messages = prompt.invoke({"question": "T-1042 티켓 어떻게 됐어요?"})
res = (prompt | model).invoke({"question": "T-1042 티켓 어떻게 됐어요?"})

첫 호출은 메시지 목록(ChatPromptValue)을 돌려줄 뿐 모델을 부르지 않습니다. prompt | model은 틀을 채운 뒤 모델에 넘기는 한 줄입니다.

한 줄로 바뀌는 것과 안 바뀌는 것

LangChain은 호출 모양을 맞춰 줍니다. 모델의 성격까지 맞춰 주지는 않습니다.

모델을 바꿀 때는 문자열을 바꾼 뒤 샘플링 파라미터, 멈춘 이유 이름, 모델이 허용하는 설정을 한 번 확인하면 됩니다. 다음 글에서는 모델이 정해진 모양의 데이터로 답하게 하는 Structured Output을 알아보겠습니다.

맞춰 줌 직접 확인 요청 모양system 자리max_tokens 채움 응답 모양content · tool_callsusage_metadata 호출 이름invoke · stream · batch 모델 설정temperature · top_p· top_k를 받는지 원본 이름stop_reason과finish_reason 모델 이름회사 문서의 최신 이름

참고 자료

출처 7개 펼치기