LangChain 시작 가이드 — 체인, 에이전트, 메모리

LangChain이란?

LangChain은 LLM(대규모 언어 모델)을 활용한 애플리케이션을 구축하기 위한 프레임워크입니다. 레고 블록처럼 프롬프트, 모델, 도구, 메모리 등의 컴포넌트를 조합하여 복잡한 AI 워크플로우를 만들 수 있습니다.

단순히 LLM API를 호출하는 것과 LangChain을 사용하는 것의 차이는, 재료만 있는 것과 레시피가 있는 것의 차이와 같습니다. LangChain은 검증된 패턴과 추상화를 제공하여 반복적인 코드를 줄여줍니다.

핵심 개념 정리

개념설명비유
Chain여러 단계를 순차 실행하는 파이프라인공장 조립 라인
AgentLLM이 도구를 선택하고 실행하는 자율 시스템만능 비서
Memory대화 이력을 저장하고 참조하는 모듈대화 노트
Tool에이전트가 사용할 수 있는 외부 기능비서의 도구함
Prompt Template동적으로 생성되는 프롬프트 틀빈칸 채우기 양식

설치 및 기본 설정

# LangChain 및 OpenAI 패키지 설치
pip install langchain langchain-openai langchain-community

# 환경변수 설정 (.env 파일 또는 export)
export OPENAI_API_KEY="sk-your-api-key"

Chain: 단계별 파이프라인 구성

Chain은 여러 처리 단계를 연결하는 핵심 개념입니다. LangChain Expression Language(LCEL)를 사용하면 | 연산자로 직관적으로 체인을 구성할 수 있습니다.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 모델 초기화
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)

# 프롬프트 템플릿 정의
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {role} 전문가입니다. 한국어로 답변하세요."),
    ("human", "{question}")
])

# LCEL로 체인 구성 (프롬프트 → 모델 → 출력 파서)
chain = prompt | llm | StrOutputParser()

# 체인 실행
result = chain.invoke({
    "role": "Python",
    "question": "데코레이터의 동작 원리를 설명해주세요"
})
print(result)
# 출력: 데코레이터는 함수를 인자로 받아 새로운 함수를 반환하는 고차 함수입니다...

여러 체인 연결하기

하나의 체인 출력을 다른 체인의 입력으로 연결할 수 있습니다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o")

# 1단계: 주제에 대한 요약 생성
summary_prompt = ChatPromptTemplate.from_template(
    "{topic}에 대해 3문장으로 요약해주세요."
)
summary_chain = summary_prompt | llm | StrOutputParser()

# 2단계: 요약을 기반으로 퀴즈 생성
quiz_prompt = ChatPromptTemplate.from_template(
    "다음 내용을 바탕으로 객관식 퀴즈 2문제를 만들어주세요:\n{summary}"
)
quiz_chain = quiz_prompt | llm | StrOutputParser()

# 두 체인을 연결하여 실행
full_chain = (
    summary_chain
    | (lambda summary: {"summary": summary})
    | quiz_chain
)

result = full_chain.invoke({"topic": "Python 비동기 프로그래밍"})
print(result)
# 출력: 1. Python에서 비동기 함수를 정의할 때 사용하는 키워드는?
#        a) async  b) await  c) yield  d) thread
#        ...

Agent: LLM이 도구를 선택하고 실행

에이전트는 LLM이 스스로 판단하여 적절한 도구를 선택하고 실행하는 시스템입니다. 계산기, 검색, API 호출 등 다양한 도구를 연결할 수 있습니다.

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool

# 커스텀 도구 정의
@tool
def calculate(expression: str) -> str:
    """수학 표현식을 계산합니다. 예: '2 + 3 * 4'"""
    try:
        # 안전한 수식 평가
        result = eval(expression, {"__builtins__": {}})
        return f"계산 결과: {result}"
    except Exception as e:
        return f"계산 오류: {e}"

@tool
def get_current_weather(city: str) -> str:
    """도시의 현재 날씨를 조회합니다."""
    # 실제로는 외부 API 호출
    weather_data = {
        "서울": "맑음, 15°C",
        "부산": "흐림, 18°C",
        "제주": "비, 16°C"
    }
    return weather_data.get(city, f"{city}의 날씨 정보를 찾을 수 없습니다")

# 에이전트 구성
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [calculate, get_current_weather]

prompt = ChatPromptTemplate.from_messages([
    ("system", "도구를 활용하여 사용자의 질문에 답변하세요."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}")
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 에이전트 실행
response = executor.invoke({
    "input": "서울 날씨 알려주고, 15의 제곱근을 계산해줘"
})
print(response["output"])
# 출력:
# > Entering new AgentExecutor chain...
# > Tool: get_current_weather("서울") → 맑음, 15°C
# > Tool: calculate("15 ** 0.5") → 계산 결과: 3.872983...
# > 서울의 현재 날씨는 맑음, 15°C이고, 15의 제곱근은 약 3.87입니다.

Memory: 대화 맥락 유지

LLM은 기본적으로 이전 대화를 기억하지 못합니다. Memory 모듈을 사용하면 대화 이력을 저장하고, 이전 맥락을 참조하여 자연스러운 대화가 가능합니다.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 세션별 대화 이력 저장소
store = {}

def get_session_history(session_id: str):
    """세션 ID별 대화 이력을 반환합니다."""
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# 프롬프트에 대화 이력 포함
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 친절한 AI 어시스턴트입니다. 한국어로 답변하세요."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}")
])

llm = ChatOpenAI(model="gpt-4o")
chain = prompt | llm

# 메모리가 포함된 체인 생성
chain_with_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history"
)

# 대화 실행 — 같은 세션 ID로 맥락 유지
config = {"configurable": {"session_id": "user-001"}}

# 첫 번째 질문
response1 = chain_with_history.invoke(
    {"input": "제 이름은 김철수입니다"},
    config=config
)
print(response1.content)
# 출력: 안녕하세요, 김철수님! 무엇을 도와드릴까요?

# 두 번째 질문 — 이전 맥락을 기억
response2 = chain_with_history.invoke(
    {"input": "제 이름이 뭐라고 했죠?"},
    config=config
)
print(response2.content)
# 출력: 김철수님이라고 하셨습니다!

RAG와 LangChain 연동

LangChain은 RAG(검색 증강 생성) 파이프라인도 쉽게 구성할 수 있습니다.

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1. 문서 준비 및 분할
documents = [
    "LangChain은 LLM 애플리케이션 개발 프레임워크입니다.",
    "LCEL은 LangChain Expression Language의 약자입니다.",
    "에이전트는 LLM이 도구를 선택하여 실행하는 시스템입니다.",
]

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,       # 청크 최대 길이
    chunk_overlap=50      # 청크 간 겹침
)

# 2. 벡터 스토어 생성
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(documents, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

# 3. RAG 체인 구성
rag_prompt = ChatPromptTemplate.from_template("""
다음 컨텍스트를 기반으로 질문에 답변하세요.

컨텍스트: {context}
질문: {question}
""")

def format_docs(docs):
    """검색된 문서를 하나의 문자열로 결합합니다."""
    return "\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | ChatOpenAI(model="gpt-4o")
    | StrOutputParser()
)

# 4. RAG 체인 실행
answer = rag_chain.invoke("LCEL이 뭔가요?")
print(answer)
# 출력: LCEL은 LangChain Expression Language의 약자로,
#        LangChain에서 체인을 구성하는 표현 언어입니다.

LangChain 프로젝트 구조 권장

실제 프로젝트에서는 다음과 같은 구조를 권장합니다.

my-langchain-app/
├── chains/           # 체인 정의
│   ├── summary.py
│   └── rag.py
├── agents/           # 에이전트 정의
│   └── assistant.py
├── tools/            # 커스텀 도구
│   ├── calculator.py
│   └── search.py
├── prompts/          # 프롬프트 템플릿
│   └── templates.py
├── config.py         # 설정 (모델, 파라미터)
├── main.py           # 진입점
└── .env              # API 키 (gitignore 필수)

실전 팁

  • LCEL을 사용하세요: LLMChain 등 레거시 API 대신 | 연산자 기반 LCEL이 현재 권장 방식입니다.
  • Streaming을 활용하세요: chain.stream()으로 토큰 단위 스트리밍이 가능하여 UX가 크게 개선됩니다.
  • LangSmith로 디버깅하세요: LangSmith를 연동하면 체인의 각 단계별 입출력을 추적할 수 있어 디버깅이 쉬워집니다.
  • Temperature를 용도에 맞게 조절하세요: 사실 기반 답변은 0, 창의적 생성은 0.7~1.0이 적합합니다.
  • 에이전트의 도구 설명을 명확하게 작성하세요: @tool 데코레이터의 docstring이 곧 LLM이 도구를 선택하는 기준이 됩니다.
  • 비용 관리: callbacks를 사용하여 토큰 사용량을 추적하고, 캐싱(SQLiteCache)으로 동일 쿼리의 중복 호출을 줄일 수 있습니다.

이 글이 도움이 되었나요?