LangChain 기초: LLM 애플리케이션을 조립하는 프레임워크
LangChain이 무엇을 해결하는 도구인지에서 출발해, 모델·프롬프트 템플릿·출력 파서·체인(LCEL)·도구 호출 같은 핵심 구성 요소를 정리하고 파이썬으로 파이프라인을 조립해 본 LangChain 기초 노트입니다.
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': ...}]
모델은 곱셈을 직접 하지 않고 “multiply를 a=23, b=17로 부르라”는 요청을 돌려줍니다. 우리 코드가 실제로 multiply(23, 17)을 실행해 결과를 다시 모델에게 건네면, 모델이 그 값을 반영해 최종 답을 만듭니다. 이 “판단은 모델이, 실행은 코드가” 구조가 다음 단계로 이어집니다.
여기서 한 걸음 더
지금까지의 체인은 정해진 순서대로 한 방향으로 흐릅니다. 프롬프트에서 모델로, 모델에서 파서로. 하지만 도구 호출을 여러 번 반복해야 하거나, 모델의 답에 따라 갈림길이 생기는 작업도 있습니다. “도구를 부르고, 결과를 보고, 더 부를지 답할지 스스로 정하는” 순환 구조가 필요해집니다.
LCEL의 한 방향 흐름만으로는 이 순환과 분기를 표현하기가 번거롭습니다. 이 지점을 다루기 위한 도구가 LangGraph이고, 다음 글에서 이어 다룹니다.
정리
| 부품 | 하는 일 |
|---|---|
| 모델 래퍼 | 제공자가 달라도 invoke로 같게 호출한다 |
| 프롬프트 템플릿 | 뼈대는 고정하고 바뀌는 값만 자리로 비워둔다 |
| 출력 파서 | 모델의 응답을 문자열이나 구조화된 형태로 바꾼다 |
| LCEL 파이프 | 부품을 이어 붙여 데이터 흐름을 그대로 표현한다 |
| 도구 호출 | 모델이 외부 기능을 언제 부를지 판단하게 한다 |
LangChain을 관통하는 생각은 “LLM 애플리케이션을 하나의 큰 함수가 아니라, 잇고 뺄 수 있는 부품의 연결로 다루자”는 것입니다. 부품을 표준화하니 제공자 교체, RAG 삽입, 도구 추가 같은 변경이 국소적으로 끝납니다. 다만 흐름이 단순한 한 방향을 넘어 순환과 분기로 가면 다른 도구가 필요한데, 그것이 LangGraph입니다.