포스트

LangChain 기초: LLM 애플리케이션을 조립하는 프레임워크

LangChain이 무엇을 해결하는 도구인지에서 출발해, 모델·프롬프트 템플릿·출력 파서·체인(LCEL)·도구 호출 같은 핵심 구성 요소를 정리하고 파이썬으로 파이프라인을 조립해 본 LangChain 기초 노트입니다.

LangChain 기초: LLM 애플리케이션을 조립하는 프레임워크

LLM 하나를 호출하는 것과, 프롬프트와 검색과 도구 호출을 엮어 하나의 애플리케이션을 만드는 것은 다른 일입니다. LangChain은 이 조립 과정을 정돈된 부품으로 다루게 해주는 프레임워크입니다. 이 글은 LangChain이 무엇을 해결하는지에서 출발해 핵심 구성 요소를 정리하고, 파이썬으로 파이프라인을 만들어봅니다. 앞선 프롬프트RAG 글을 먼저 보면 흐름이 이어집니다.

LangChain은 무엇을 해결하나

LLM API를 직접 호출하는 것 자체는 어렵지 않습니다. 어려움은 그 주변에서 생깁니다. 실제 애플리케이션은 대개 이런 조각들의 연결입니다.

  • 사용자 입력을 프롬프트 템플릿에 끼워 넣기
  • 여러 LLM 제공자(Anthropic, OpenAI 등)를 같은 방식으로 다루기
  • 모델의 답을 다음 단계가 쓸 수 있게 파싱하기
  • 문서를 검색해 근거로 넣기(RAG)
  • 모델이 계산기나 검색 같은 외부 도구를 호출하게 하기

이걸 매번 직접 문자열과 조건문으로 엮으면 코드가 금세 지저분해집니다. LangChain은 이 조각들을 표준화된 부품으로 만들고, 부품을 |로 이어 붙이는 방식을 제공합니다. 핵심은 “LLM을 감싸는 편의 기능”이 아니라 “조각들을 일관된 인터페이스로 연결하는 방식”에 있습니다.

부품 1: 모델을 감싸는 공통 인터페이스

LangChain은 제공자마다 다른 API를 같은 모양으로 감쌉니다. Anthropic이든 다른 곳이든 invoke로 호출하는 방식은 동일합니다.

1
2
3
4
5
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-sonnet-5")
answer = llm.invoke("한 문장으로 자기소개 해줘.")
print(answer.content)

제공자를 바꾸고 싶으면 이 llm 객체 한 줄만 다른 클래스로 교체하면 됩니다. 나머지 파이프라인은 그대로 둘 수 있습니다. 이 교체 가능성이 LangChain을 쓰는 큰 이유 중 하나입니다.

부품 2: 프롬프트 템플릿

프롬프트의 뼈대는 고정하고, 그때그때 바뀌는 값만 자리로 비워두는 것이 템플릿입니다. 프롬프트 엔지니어링 글에서 다룬 역할 지정과 형식 고정을, 문자열을 손으로 이어 붙이지 않고 구조적으로 관리할 수 있습니다.

1
2
3
4
5
6
7
8
9
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "너는 {language} 번역가야. 뜻을 살려 자연스럽게 옮겨."),
    ("user", "{text}"),
])

# 자리를 값으로 채운 결과를 확인
print(prompt.invoke({"language": "영어", "text": "밥은 먹고 다니냐?"}))

부품 3: 출력 파서

모델의 답은 기본적으로 문자열입니다. 다음 단계가 순수 텍스트만 필요하거나, 구조화된 데이터가 필요할 수 있습니다. 출력 파서는 모델의 응답을 원하는 형태로 바꿔줍니다. 가장 단순한 것은 응답에서 텍스트만 뽑는 파서입니다.

1
2
3
from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()

조립: LCEL로 부품을 잇는다

이제 부품을 연결합니다. LangChain은 | 기호로 부품을 이어 붙이는 문법을 제공하는데, 이를 LCEL(LangChain Expression Language)이라고 부릅니다. 왼쪽 부품의 출력이 오른쪽 부품의 입력으로 흘러갑니다.

1
2
3
4
chain = prompt | llm | parser

result = chain.invoke({"language": "영어", "text": "밥은 먹고 다니냐?"})
print(result)  # 문자열 하나가 바로 나온다

prompt | llm | parser를 읽으면 데이터 흐름이 그대로 보입니다. 입력 딕셔너리가 prompt에서 완성된 프롬프트가 되고, llm이 그것을 응답으로 바꾸고, parser가 응답에서 문자열을 뽑습니다. 각 부품은 독립적이라, 중간에 다른 부품을 끼우거나 빼기 쉽습니다.

RAG를 체인으로 표현하기

RAG 글에서 만든 검색과 생성도 하나의 체인으로 엮을 수 있습니다. 검색기(retriever)가 이미 있다고 할 때, 질문을 받아 관련 문서를 찾고 그것을 프롬프트에 넣어 답까지 만드는 흐름입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_core.runnables import RunnablePassthrough

rag_prompt = ChatPromptTemplate.from_messages([
    ("system", "아래 문서만 근거로 답해.\n\n{context}"),
    ("user", "{question}"),
])

def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
    | parser
)

print(rag_chain.invoke("환불은 며칠까지 가능한가요?"))

{"context": ..., "question": ...} 부분이 핵심입니다. 들어온 질문을 두 갈래로 흘려보내는데, 한 갈래는 검색기를 거쳐 context가 되고 다른 갈래는 질문 그대로 question이 됩니다. 두 값이 rag_prompt의 자리를 채웁니다. RAG의 각 단계가 하나의 표현식으로 정리됩니다.

부품 4: 도구 호출

LLM은 스스로 계산을 하거나 실시간 정보를 가져오지 못합니다. 대신 “이런 도구가 있으니 필요하면 불러”라고 알려주면, 모델이 언제 어떤 인자로 도구를 호출할지 판단해 알려줍니다. 실제 실행은 우리 코드가 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """두 정수를 곱한다."""
    return a * b

llm_with_tools = llm.bind_tools([multiply])

response = llm_with_tools.invoke("23 곱하기 17은?")
print(response.tool_calls)
# [{'name': 'multiply', 'args': {'a': 23, 'b': 17}, 'id': ...}]

모델은 곱셈을 직접 하지 않고 “multiplya=23, b=17로 부르라”는 요청을 돌려줍니다. 우리 코드가 실제로 multiply(23, 17)을 실행해 결과를 다시 모델에게 건네면, 모델이 그 값을 반영해 최종 답을 만듭니다. 이 “판단은 모델이, 실행은 코드가” 구조가 다음 단계로 이어집니다.

여기서 한 걸음 더

지금까지의 체인은 정해진 순서대로 한 방향으로 흐릅니다. 프롬프트에서 모델로, 모델에서 파서로. 하지만 도구 호출을 여러 번 반복해야 하거나, 모델의 답에 따라 갈림길이 생기는 작업도 있습니다. “도구를 부르고, 결과를 보고, 더 부를지 답할지 스스로 정하는” 순환 구조가 필요해집니다.

LCEL의 한 방향 흐름만으로는 이 순환과 분기를 표현하기가 번거롭습니다. 이 지점을 다루기 위한 도구가 LangGraph이고, 다음 글에서 이어 다룹니다.

정리

부품하는 일
모델 래퍼제공자가 달라도 invoke로 같게 호출한다
프롬프트 템플릿뼈대는 고정하고 바뀌는 값만 자리로 비워둔다
출력 파서모델의 응답을 문자열이나 구조화된 형태로 바꾼다
LCEL 파이프부품을 이어 붙여 데이터 흐름을 그대로 표현한다
도구 호출모델이 외부 기능을 언제 부를지 판단하게 한다

LangChain을 관통하는 생각은 “LLM 애플리케이션을 하나의 큰 함수가 아니라, 잇고 뺄 수 있는 부품의 연결로 다루자”는 것입니다. 부품을 표준화하니 제공자 교체, RAG 삽입, 도구 추가 같은 변경이 국소적으로 끝납니다. 다만 흐름이 단순한 한 방향을 넘어 순환과 분기로 가면 다른 도구가 필요한데, 그것이 LangGraph입니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.