-
LangChain이 LLM을 다루는 방식: 추상화, 팩토리, 체이닝IT 2026. 6. 22. 22:00
LLM마다 코드를 따로 써야 했던 시절
OpenAI GPT를 쓰다가 Anthropic Claude로 바꾸려면 얼마나 손봐야 할까? 2023년 초까지만 해도 대답이 단순했다. "코드를 거의 다시 써야 한다." 각 LLM 제공사(provider)가 저마다 다른 SDK, 다른 함수 이름, 다른 요청 형식을 썼기 때문이다.
LLM provider별로 완전히 다른 코드가 필요한 상황. 위 다이어그램은 동일한 "AI에게 질문하기"라는 목적을 달성하기 위해 provider마다 함수 이름, 매개변수 이름, 메시지 포맷이 모두 다른 현실을 보여 준다. OpenAI는
client.chat.completions.create(), Anthropic은client.messages.create(), Google은generate_content()를 쓴다. GPT에서 Claude로 교체하려면 API 호출 코드를 전부 수정해야 한다. 이 구조의 핵심 문제는 "모델을 바꾸면 코드도 바뀐다"는 결합(coupling)이다. 비즈니스 로직이 특정 제공사에 종속된다.더 심각한 문제는 스트리밍(streaming, 토큰이 생성될 때마다 실시간으로 전달하는 방식), 도구 호출(tool calling, LLM이 외부 함수를 실행하도록 요청하는 기능), 비동기(async) 실행 같은 공통 기능도 각자 다르게 구현돼 있다는 점이었다. GPT에서 스트리밍 코드를 짜면 Claude로는 그대로 가져다 쓸 수 없었다.
LangChain의 답: 추상화 계층 하나
LangChain은 이 문제를 공통 추상화 계층(abstraction layer)으로 풀었다. 모든 LLM 제공사를
BaseChatModel이라는 하나의 인터페이스로 감싸고, 코드는 그 인터페이스만 바라보게 만든다. 모델을 교체할 때 바꾸는 건 딱 한 줄 — 어떤 provider 구현체를 쓸지 지정하는 부분뿐이다.LangChain의 BaseChatModel 추상화 구조. 애플리케이션 코드는 맨 위의
BaseChatModel인터페이스만 호출한다. 실제 HTTP 요청을 처리하는 구현체는 아래쪽 각 provider 클래스(ChatOpenAI,ChatAnthropic등)에 숨어 있다. 이 패턴을 디자인 패턴 용어로 "전략 패턴(Strategy Pattern)"이라 부른다 — 알고리즘(여기서는 LLM 호출 방식)을 교체 가능한 캡슐로 분리해 두는 방식이다. 코드를 바꾸지 않고 전략만 교환한다. 놓치기 쉬운 함정: 추상화가 만능은 아니다. provider마다 지원하는 기능 범위가 다르기 때문에, 예를 들어 OpenAI만 지원하는 특수 파라미터를 쓰면 그 부분은 여전히 provider 종속 코드가 된다.메시지 타입 — 대화의 단위
추상화의 핵심은 메시지 형식의 통일이다. LangChain은 모든 LLM과의 대화를 역할(role)이 붙은 메시지 객체의 리스트로 표현한다. OpenAI, Anthropic, Google 등이 내부적으로 다른 JSON 구조를 쓰더라도, LangChain 레이어에서는 항상 같은 메시지 타입을 사용한다.
LangChain 메시지 타입 계층. 대화는 항상 이 네 종류 메시지의 시퀀스다.
SystemMessage는 대화 맨 앞에 한 번 두어 LLM의 인격이나 제약 조건을 설정한다.HumanMessage와AIMessage가 번갈아 오가는 것이 대화의 기본 패턴이다.ToolMessage는 LLM이 도구 호출을 요청했을 때(AIMessage.tool_calls에 기록됨) 그 결과를 돌려주기 위해 쓴다. 이 메시지 타입들이 provider 간 공통이기 때문에, 같은 메시지 리스트를 ChatOpenAI에도, ChatAnthropic에도 그대로 넘길 수 있다.init_chat_model — "모델 이름 한 줄"로 LLM 교체
추상화가 있어도, "어떤 provider 클래스를 임포트해서 인스턴스화할까"를 코드에서 결정해야 한다면 여전히 번거롭다.
init_chat_model()은 이 단계를 팩토리 함수(factory function, 입력값에 따라 적합한 객체를 생성해 반환하는 함수)로 해결한다. 모델 이름을 문자열로 넘기면 알맞은BaseChatModel구현체를 자동으로 골라 반환한다.init_chat_model의 팩토리 동작.
provider:model-name형태의 문자열을 입력하면 해당 provider의 구현 클래스가 자동으로 선택된다. 이 패턴의 가치는 모델 교체를 설정값 변경으로 만든다는 것이다. 환경변수LLM_MODEL=anthropic:claude-3-5-sonnet-20241022을 바꾸는 것만으로 프로덕션에서 모델을 교체할 수 있다. 코드를 배포하지 않아도 된다. 주의할 점: 각 provider의 Python 패키지(langchain-openai,langchain-anthropic등)는 별도 설치가 필요하다. 문자열에 provider를 명시하지 않으면OPENAI_API_KEY등 환경변수를 보고 자동 감지하는데, 여러 API 키가 설정된 환경에서는 예상과 다른 provider가 선택될 수 있다.LCEL — 파이프로 연결하는 체인
LLM 호출은 보통 "프롬프트 조립 → LLM 호출 → 결과 파싱"이라는 세 단계로 구성된다. LangChain은 이 단계들을 LCEL(LangChain Expression Language, 파이프라인을 선언형으로 조립하는 문법)로 연결한다. Python의
|(파이프) 연산자를 재정의해서 Unix 쉘의 파이프처럼 스테이지를 이어 붙이는 방식이다.LCEL 체인의 데이터 흐름.
chain = prompt | llm | parser라는 한 줄이 세 단계 파이프라인을 정의한다. 호출 시에는chain.invoke({'topic': 'LCEL', 'language': 'Python'})처럼 입력 딕셔너리만 넘기면 된다. 프롬프트 조립(ChatPromptTemplate)부터 LLM 호출, 결과 파싱(StrOutputParser)까지 순서대로 흐른다. 이 구조의 장점은 각 단계가 독립 교체 가능하다는 것이다.parser를JsonOutputParser로 바꾸면 JSON을 파싱한 딕셔너리가 나오고,llm을 다른 모델로 교체해도 나머지 코드는 그대로다. 함정:ChatPromptTemplate의 변수 이름과invoke()에 넘기는 딕셔너리 키가 정확히 일치해야 한다. 불일치하면 런타임 에러가 발생하는데 에러 메시지가 직관적이지 않을 수 있다.invoke / stream / batch — 하나의 인터페이스, 세 가지 실행 방식
같은
BaseChatModel인스턴스에서 호출 방식만 바꿔 동기·스트리밍·배치·비동기를 모두 쓸 수 있다. 이전에는 스트리밍을 원하면 별도 API 엔드포인트나 다른 함수를 써야 했지만, LangChain에서는 메서드 이름만 바뀐다.BaseChatModel이 제공하는 다섯 가지 실행 인터페이스. 같은
llm객체에서 메서드 이름만 바꾸면 동작 방식이 달라진다.invoke는 가장 단순한 단건 동기 호출이다.stream은 토큰이 생성될 때마다 즉시 전달하므로 사용자가 응답 시작을 빨리 볼 수 있다 — ChatGPT 웹사이트처럼 텍스트가 흘러나오는 UX를 만들 때 쓴다.batch는 여러 질문을 한꺼번에 보내 병렬 처리한다. 중요한 것: LCEL 체인도 동일한 메서드를 그대로 지원한다.chain.invoke(),chain.stream(),chain.batch()모두 쓸 수 있어서, 체인을 만든 다음에 실행 방식을 자유롭게 고를 수 있다.bind_tools — LLM에게 도구를 알려주는 방법
LLM이 단순히 텍스트를 생성하는 것을 넘어서, 외부 함수(날씨 조회, 데이터베이스 검색, 계산기 등)를 호출하도록 하려면 "이런 도구들이 있다"고 LLM에게 알려야 한다.
bind_tools()는 도구 목록을 LLM에 사전 등록하는 역할을 한다. LLM은 응답할 때 "이 도구를 이 인자로 호출해줘"라는 요청을AIMessage.tool_calls에 담아 반환한다.bind_tools를 통한 도구 호출 전체 흐름.
bind_tools()를 거치면 LLM은 요청을 받을 때마다 "지금 이 상황에 쓸 수 있는 도구가 있나?"를 판단한다. 도구가 필요하다고 판단하면 응답 텍스트 없이 도구 호출 요청(tool_calls)만 반환한다. 애플리케이션 코드는 그 요청을 읽고 실제 함수를 실행한 뒤, 결과를ToolMessage로 만들어 대화 히스토리에 추가하고 LLM을 다시 호출한다. 이때 LLM은 도구 결과를 보고 최종 텍스트 응답을 생성한다. 주의:bind_tools()는 도구를 "알린다"는 역할만 한다. 도구를 실제로 실행하는 것은 애플리케이션 코드가 담당한다. LangGraph의ToolNode나 LangChain의AgentExecutor를 쓰면 이 실행 루프를 자동화할 수 있다.with_structured_output — 출력 형식을 강제하는 방법
LLM의 출력은 기본적으로 자유 형식 텍스트다. "점수를 1~10으로, 이유는 한 문장으로 답해"라고 프롬프트에 써도 LLM이 형식을 지키지 않을 수 있다.
with_structured_output()은 Pydantic 모델(Python에서 데이터 유효성 검사와 타입 강제를 담당하는 라이브러리의 모델 클래스)이나 JSON 스키마를 넘기면 LLM이 반드시 그 형식으로 응답하도록 강제한다.with_structured_output을 통한 타입 안전 출력. 입력은 자연어지만 출력은 Python 객체다.
ReviewResult.score처럼 점 표기법으로 접근하면 되고, 타입 힌트(type hint, 변수가 어떤 타입인지 명시하는 파이썬 문법)까지 그대로 살아 있다. 내부 동작은 provider마다 다르다 — OpenAI는 "structured outputs" 모드나 function calling으로 구현하고, 다른 provider는 JSON mode를 쓰거나 프롬프트에 JSON 포맷을 요청하는 방식을 쓴다. 개발자가 이 차이를 신경 쓸 필요 없이 항상 같은 API(with_structured_output())를 쓰면 된다는 것이 요점이다. 단, 모델 성능이 낮거나 스키마가 복잡하면 여전히 형식을 어기는 경우가 있다.include_raw=True옵션으로 원본 응답도 받아 실패 원인을 진단할 수 있다.전체 그림으로 보기 — 추상화가 연결되는 방식
지금까지 본 개념들이 실제 애플리케이션에서 어떻게 조립되는지 전체 맥락으로 보자.
실제 애플리케이션에서 LangChain LLM 추상화 전체 조립 패턴. 설정(환경변수)에서 모델을 고르면 팩토리(
init_chat_model)가 맞는 클래스를 선택한다. 그 위에 도구 등록(bind_tools)과 출력 형식 강제(with_structured_output)를 얹고, 프롬프트 템플릿과 LCEL로 체인을 조립한다. 최종 실행 방식(invoke/stream)은 체인을 건드리지 않고 선택할 수 있다. 이 구조의 가장 큰 가치: 전체 파이프라인 코드를 변경하지 않고 모델만 교체할 수 있다. 개발 중에는 저렴한 로컬 모델(ollama:llama3)로 테스트하고, 프로덕션에서는 GPT-4o나 Claude로 전환하는 워크플로우가 가능해진다.이 설계가 가져온 변화
LangChain의 LLM 추상화가 등장하기 전에는 프로덕션 AI 애플리케이션을 provider 하나에 묶는 것이 기본이었다. 교체 비용이 컸기 때문이다. 이 추상화 이후 세 가지가 달라졌다.
첫째, 모델 경쟁이 코드 없이 가능해졌다. 같은 프롬프트로 GPT-4o와 Claude 3.5를 A/B 테스트하려면 이제 설정값 하나만 바꾸면 된다. 어느 모델이 특정 태스크에 더 적합한지 실험 비용이 거의 없다.
둘째, 공급 위험(vendor risk)이 줄었다. 특정 provider의 API 가격이 오르거나, 서비스 품질이 떨어지거나, API 정책이 바뀌어도 경쟁사로 전환하는 데 코드 재작성이 필요 없다.
셋째, 테스트 환경 분리가 쉬워졌다. CI/CD 파이프라인에서는 비용이 저렴한 모델을 쓰고, 프로덕션에서만 성능 좋은 모델을 쓰는 전략이 설정값 하나로 달성된다. 추상화는 "지금 당장 모델을 교체할 계획이 없어도" 효과를 낸다 — 교체 가능성을 열어 두는 것 자체가 설계 유연성이다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
LLM에 코드 실행 능력 붙이기 — PythonAstREPLTool (0) 2026.06.24 Pydantic Field로 LLM 출력 스키마를 제약하는 방법 (0) 2026.06.23 ProviderStrategy vs ToolStrategy — 구조화 출력 전략 선택 (0) 2026.06.23 LangChain ToolRuntime으로 런타임 컨텍스트 주입하기 (0) 2026.06.23 LangChain @tool 데코레이터의 3요소 — 에이전트가 도구를 이해하는 방법 (0) 2026.06.22 LangChain이 등장한 이유 — LLM 시대의 새로운 개발 패러다임 (0) 2026.06.22 JSON-RPC의 id는 누가 정하고 충돌하면 어떻게 되나 (0) 2026.06.21 서브에이전트를 200% 활용하는 노하우 — description부터 병렬 실행까지 (0) 2026.06.20 플러그인으로 서브에이전트 배포하기 — /plugin install부터 마켓플레이스까지 (0) 2026.06.19 서브에이전트를 GitHub로 배포하기 — 팀 공유부터 오픈소스까지 (0) 2026.06.19