본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 4. · 61 Views
LangChain 기초 완벽 가이드
LangChain의 핵심 구성요소를 하나씩 살펴보며 대규모 언어 모델을 활용한 애플리케이션 개발의 기초를 다집니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.
목차
1. LangChain 구조 이해
어느 날 김개발 씨는 회사에서 새로운 프로젝트를 맡게 되었습니다. "GPT를 활용한 고객 상담 챗봇을 만들어 보세요." 막상 시작하려니 어디서부터 손을 대야 할지 막막했습니다.
그때 선배 박시니어 씨가 다가와 말했습니다. "LangChain부터 알아보는 게 좋을 거예요."
LangChain은 한마디로 대규모 언어 모델(LLM)을 활용한 애플리케이션을 쉽게 만들 수 있도록 도와주는 프레임워크입니다. 마치 레고 블록처럼 여러 컴포넌트를 조립해서 원하는 기능을 만들 수 있습니다.
이것을 제대로 이해하면 복잡한 AI 애플리케이션도 체계적으로 구축할 수 있습니다.
다음 코드를 살펴봅시다.
# LangChain의 기본 구조 이해하기
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 1. 모델 설정 - LLM과 대화할 준비
llm = ChatOpenAI(model="gpt-4o-mini")
# 2. 프롬프트 템플릿 - 질문의 형식을 정의
prompt = ChatPromptTemplate.from_template("다음 주제에 대해 설명해주세요: {topic}")
# 3. 출력 파서 - 응답을 원하는 형태로 변환
output_parser = StrOutputParser()
# 4. 체인으로 연결 - 파이프라인 구성
chain = prompt | llm | output_parser
# 5. 실행
result = chain.invoke({"topic": "파이썬 데코레이터"})
print(result)
김개발 씨는 입사 6개월 차 주니어 개발자입니다. Python은 어느 정도 다룰 줄 알지만, AI 관련 개발은 처음이었습니다.
ChatGPT API를 직접 호출해 본 적은 있지만, 실제 서비스에 적용하려니 고민이 많았습니다. "API 호출 한 번으로 끝나는 게 아니잖아요.
사용자 입력도 처리해야 하고, 응답도 가공해야 하고, 대화 내용도 기억해야 하고..." 김개발 씨의 고민에 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
그래서 LangChain이 필요한 거예요." 그렇다면 LangChain이란 정확히 무엇일까요? 쉽게 비유하자면, LangChain은 마치 주방의 조리 시스템과 같습니다.
냉장고에서 재료를 꺼내고, 도마에서 손질하고, 프라이팬에서 조리하고, 접시에 담아 내는 일련의 과정이 있습니다. 각 단계마다 필요한 도구가 다르고, 순서도 중요합니다.
LangChain은 이런 과정을 AI 애플리케이션에서 체계적으로 관리할 수 있게 해줍니다. LangChain이 없던 시절에는 어땠을까요?
개발자들은 LLM API를 직접 호출하고, 응답을 파싱하고, 대화 기록을 직접 관리해야 했습니다. 코드가 금방 복잡해지고, 비슷한 코드를 여러 곳에서 반복해서 작성해야 했습니다.
더 큰 문제는 여러 AI 기능을 조합할 때 발생했습니다. 검색 결과를 LLM에 전달하고, 그 응답을 다시 다른 서비스에 연결하는 식의 복잡한 파이프라인을 만들기가 어려웠습니다.
바로 이런 문제를 해결하기 위해 LangChain이 등장했습니다. LangChain의 핵심 철학은 컴포넌트화와 조합입니다.
각각의 기능을 독립적인 컴포넌트로 만들고, 이들을 레고 블록처럼 조립해서 원하는 기능을 구현합니다. 모델을 바꾸고 싶으면 모델 컴포넌트만 교체하면 됩니다.
프롬프트를 수정하고 싶으면 프롬프트 컴포넌트만 건드리면 됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 ChatOpenAI를 통해 사용할 모델을 설정합니다. 이것이 우리가 대화할 AI 두뇌입니다.
그 다음 ChatPromptTemplate으로 질문의 형식을 정의합니다. 중괄호 안의 변수는 나중에 실제 값으로 채워집니다.
StrOutputParser는 AI의 응답을 문자열로 깔끔하게 변환해주는 역할을 합니다. 가장 중요한 부분은 파이프 연산자(|)로 연결하는 체인 구성입니다.
prompt | llm | output_parser라고 작성하면, 프롬프트를 만들고, LLM에 전달하고, 결과를 파싱하는 일련의 과정이 하나의 파이프라인으로 연결됩니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 고객 문의 자동 응답 시스템을 만든다고 가정해봅시다. 고객의 질문을 받아서 적절한 프롬프트로 가공하고, LLM에게 답변을 요청하고, 그 결과를 고객에게 보여주는 전체 흐름을 LangChain으로 깔끔하게 구성할 수 있습니다.
나중에 모델을 GPT-4에서 Claude로 바꾸고 싶다면 ChatOpenAI 부분만 수정하면 됩니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 LangChain의 모든 기능을 한꺼번에 사용하려는 것입니다. 처음에는 기본적인 체인 구성부터 시작하고, 필요에 따라 하나씩 컴포넌트를 추가하는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, LangChain은 AI 기능을 조립하는 프레임워크군요!" 이제 본격적으로 각 컴포넌트를 하나씩 살펴볼 차례입니다.
실전 팁
💡 - LangChain 설치는 pip install langchain langchain-openai로 간단하게 할 수 있습니다
- 환경 변수로 OPENAI_API_KEY를 설정해두면 코드에서 직접 키를 작성하지 않아도 됩니다
- 공식 문서의 Quickstart를 먼저 따라해보면 전체 구조를 빠르게 이해할 수 있습니다
2. LLM과 Chat Model
김개발 씨가 LangChain의 기본 구조를 이해하고 나니, 새로운 궁금증이 생겼습니다. "선배, 그런데 LLM이랑 Chat Model이 다른 건가요?
코드를 보니까 둘 다 있던데요." 박시니어 씨가 미소를 지으며 답했습니다. "좋은 질문이에요.
이 차이를 이해하는 게 중요해요."
LLM은 텍스트를 입력받아 텍스트를 출력하는 기본 모델이고, Chat Model은 대화 형식에 최적화된 모델입니다. 마치 일방적으로 글을 쓰는 작가와 쌍방향으로 대화하는 상담사의 차이와 같습니다.
최근에는 Chat Model이 주류가 되어 대부분의 프로젝트에서 ChatOpenAI나 ChatAnthropic을 사용합니다.
다음 코드를 살펴봅시다.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
# Chat Model 초기화 - 다양한 파라미터 설정 가능
chat = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.7, # 창의성 조절 (0: 일관된 답변, 1: 다양한 답변)
max_tokens=500 # 최대 응답 길이
)
# 메시지 구조 - 역할별로 구분
messages = [
SystemMessage(content="당신은 친절한 Python 튜터입니다."),
HumanMessage(content="리스트 컴프리헨션이 뭔가요?"),
]
# Chat Model 호출
response = chat.invoke(messages)
print(response.content)
# 대화 이어가기
messages.append(response) # AI 응답 추가
messages.append(HumanMessage(content="예제 코드도 보여주세요."))
response2 = chat.invoke(messages)
박시니어 씨는 화이트보드에 그림을 그리기 시작했습니다. "LLM과 Chat Model의 차이를 이해하려면, 먼저 AI 모델이 어떻게 발전해왔는지 알아야 해요." 초기의 언어 모델은 단순했습니다.
텍스트를 넣으면 이어지는 텍스트를 생성하는 방식이었습니다. "옛날 옛적에 한 나라에"라고 입력하면 "왕이 살았습니다"처럼 자연스럽게 이어지는 문장을 만들어냈습니다.
이것이 바로 기본적인 LLM(Large Language Model)의 작동 방식입니다. 하지만 사람들은 AI와 대화하고 싶어했습니다.
질문을 하면 답변을 해주고, 그 답변에 대해 또 질문하고 싶었습니다. 이런 요구에 맞춰 등장한 것이 Chat Model입니다.
Chat Model은 마치 카카오톡이나 메신저처럼 대화 형식으로 작동합니다. 여기서 중요한 개념이 바로 메시지입니다.
단순한 텍스트가 아니라, 누가 한 말인지 역할이 명시된 메시지들의 모음으로 대화가 구성됩니다. LangChain에서는 세 가지 주요 메시지 타입을 제공합니다.
SystemMessage는 AI의 성격과 역할을 정의합니다. "당신은 친절한 Python 튜터입니다"라고 설정하면, AI는 그 역할에 맞게 답변합니다.
이 메시지는 보통 대화의 맨 처음에 한 번만 넣습니다. HumanMessage는 사용자의 말입니다.
질문이나 요청 등 사람이 AI에게 전달하는 모든 내용이 여기에 들어갑니다. AIMessage는 AI의 응답입니다.
Chat Model이 생성한 답변이 이 형태로 반환됩니다. 위의 코드를 살펴보면, ChatOpenAI를 초기화할 때 여러 파라미터를 설정할 수 있습니다.
temperature는 특히 중요한데, 0에 가까울수록 일관되고 예측 가능한 답변을, 1에 가까울수록 창의적이고 다양한 답변을 생성합니다. 코드 생성처럼 정확성이 중요한 작업에는 낮은 값을, 스토리 작성처럼 창의성이 필요한 작업에는 높은 값을 사용합니다.
max_tokens는 응답의 최대 길이를 제한합니다. 토큰은 대략 한글 한 글자 또는 영어 4글자 정도에 해당합니다.
비용 관리와 응답 시간 최적화를 위해 적절히 설정하는 것이 좋습니다. 실제 프로젝트에서는 대화를 이어가는 경우가 많습니다.
코드의 마지막 부분을 보면, AI의 응답을 messages 리스트에 추가하고, 새로운 사용자 메시지를 또 추가한 뒤 다시 호출합니다. 이렇게 하면 AI가 이전 대화 내용을 기억한 상태로 답변할 수 있습니다.
김개발 씨가 고개를 갸웃거렸습니다. "그런데 매번 전체 대화 내용을 다 보내야 하나요?
비용이 많이 들 것 같은데요." 박시니어 씨가 고개를 끄덕였습니다. "좋은 지적이에요.
그래서 Memory라는 컴포넌트가 필요한 거예요. 그건 나중에 다시 설명할게요."
실전 팁
💡 - 프로덕션에서는 temperature를 0.3~0.5 정도로 설정하면 적당히 다양하면서도 안정적인 답변을 얻을 수 있습니다
- ChatAnthropic, ChatCohere 등 다른 모델도 같은 인터페이스로 사용할 수 있어 모델 교체가 쉽습니다
- streaming=True 옵션을 주면 응답을 실시간으로 받아볼 수 있습니다
3. Prompt Templates
다음 날, 김개발 씨는 챗봇 프로토타입을 만들어 선배에게 보여줬습니다. "잘 작동하는데요, 문제가 하나 있어요.
사용자마다 다른 정보를 넣어야 하는데, 프롬프트를 매번 문자열 조합으로 만들자니 코드가 지저분해져요." 박시니어 씨가 웃으며 말했습니다. "Prompt Template을 써보세요."
Prompt Template은 변수를 포함한 프롬프트 틀을 만들어두고, 나중에 실제 값으로 채워 넣는 방식입니다. 마치 우편물의 편지 양식처럼, 받는 사람 이름과 내용만 바꿔서 여러 장의 편지를 만들 수 있습니다.
코드의 가독성을 높이고, 프롬프트 관리를 체계적으로 할 수 있습니다.
다음 코드를 살펴봅시다.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
# 기본 프롬프트 템플릿
simple_prompt = ChatPromptTemplate.from_template(
"{product}에 대한 {style} 스타일의 광고 문구를 작성해주세요."
)
# 변수 채워넣기
filled = simple_prompt.invoke({"product": "무선 이어폰", "style": "유머러스한"})
# 복잡한 대화형 프롬프트 템플릿
chat_prompt = ChatPromptTemplate.from_messages([
("system", "당신은 {company}의 고객 상담원입니다. {tone} 어조로 답변하세요."),
MessagesPlaceholder(variable_name="history"), # 대화 기록 삽입 위치
("human", "{question}")
])
# 대화 기록과 함께 사용
result = chat_prompt.invoke({
"company": "테크마트",
"tone": "친절한",
"history": [HumanMessage(content="배송은 얼마나 걸리나요?")],
"question": "반품도 가능한가요?"
})
김개발 씨의 코드를 보니, 이런 식으로 작성되어 있었습니다. prompt = "안녕하세요, " + user_name + "님.
" + product_name + "에 대해 궁금하신 점이 있으시군요. 제가 도와드리겠습니다..." 문자열을 더하기 연산자로 이어붙이는 방식입니다.
변수가 두세 개일 때는 괜찮아 보이지만, 프롬프트가 길어지고 변수가 많아지면 코드가 금방 읽기 어려워집니다. 무엇보다 프롬프트 자체를 수정하기가 번거롭습니다.
Prompt Template은 이런 문제를 깔끔하게 해결합니다. 비유하자면, 계약서 양식을 생각해보세요.
계약서에는 "갑: ______, 을: ______"처럼 빈칸이 있고, 실제 계약할 때 그 빈칸을 채웁니다. 양식 자체는 변하지 않고, 빈칸에 들어갈 내용만 달라집니다.
Prompt Template도 마찬가지입니다. from_template 메서드를 사용하면 간단한 템플릿을 만들 수 있습니다.
중괄호 안에 변수명을 넣으면, 나중에 그 변수에 실제 값을 채워넣을 수 있습니다. {product}는 "무선 이어폰"으로, {style}은 "유머러스한"으로 대체됩니다.
더 복잡한 대화형 템플릿이 필요할 때는 from_messages를 사용합니다. 이 방식은 시스템 메시지, 사용자 메시지, AI 메시지 등을 구조적으로 정의할 수 있습니다.
튜플 형태로 (역할, 내용)을 나열하면 됩니다. 여기서 MessagesPlaceholder는 특별한 역할을 합니다.
대화 기록처럼 메시지 목록 전체를 삽입해야 할 때 사용합니다. 이전 대화 내용을 history라는 변수로 전달하면, 그 위치에 메시지들이 삽입됩니다.
이렇게 하면 대화의 맥락을 유지하면서 새로운 질문에 답변할 수 있습니다. 실무에서 Prompt Template이 빛을 발하는 순간이 있습니다.
바로 프롬프트 버전 관리가 필요할 때입니다. 프롬프트를 별도의 파일이나 데이터베이스에 저장해두고, 필요할 때 불러와서 사용할 수 있습니다.
A/B 테스트를 하거나, 여러 버전의 프롬프트를 비교 실험할 때 매우 유용합니다. 또한 팀 협업에서도 장점이 있습니다.
기획자나 PM이 프롬프트 내용을 직접 수정할 수 있고, 개발자는 코드 로직에만 집중할 수 있습니다. 프롬프트가 코드와 분리되어 있으니까요.
하지만 주의할 점도 있습니다. 변수명을 잘못 쓰면 에러가 발생합니다.
{product}라고 정의해놓고 invoke할 때 {"item": "이어폰"}처럼 다른 이름을 쓰면 안 됩니다. 템플릿에 정의된 모든 변수에 값을 제공해야 합니다.
김개발 씨가 코드를 수정하고 나서 감탄했습니다. "와, 프롬프트가 훨씬 읽기 쉬워졌어요.
나중에 수정하기도 편하겠네요!"
실전 팁
💡 - 프롬프트 템플릿을 별도 파일(YAML, JSON)로 관리하면 버전 관리와 협업이 쉬워집니다
- partial 메서드를 사용하면 일부 변수만 미리 채워둘 수 있습니다
- input_variables 속성으로 필요한 변수 목록을 확인할 수 있습니다
4. Chain 구성하기
"선배, 그런데 저 파이프 연산자가 신기해요. prompt | llm | parser 이렇게 쓰는 게 어떻게 작동하는 건가요?" 김개발 씨의 질문에 박시니어 씨가 눈을 빛냈습니다.
"아, LCEL이요. LangChain의 핵심 중 핵심이에요.
이걸 알면 정말 다양한 것을 조합할 수 있어요."
Chain은 여러 컴포넌트를 연결하여 하나의 파이프라인으로 만드는 것입니다. LCEL(LangChain Expression Language)을 사용하면 파이프 연산자(|)로 직관적으로 연결할 수 있습니다.
마치 공장의 컨베이어 벨트처럼, 데이터가 한 단계씩 처리되어 최종 결과물이 나옵니다.
다음 코드를 살펴봅시다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
llm = ChatOpenAI(model="gpt-4o-mini")
# 기본 체인 구성
prompt = ChatPromptTemplate.from_template("{topic}에 대해 한 문장으로 설명해주세요.")
basic_chain = prompt | llm | StrOutputParser()
# 병렬 처리 체인 - 여러 작업을 동시에 실행
parallel_chain = RunnableParallel(
summary=ChatPromptTemplate.from_template("{text}를 한 줄로 요약해주세요.") | llm | StrOutputParser(),
keywords=ChatPromptTemplate.from_template("{text}에서 키워드 3개를 추출해주세요.") | llm | StrOutputParser()
)
# 체인 연결 - 결과를 다음 체인에 전달
full_chain = (
{"topic": RunnablePassthrough()}
| ChatPromptTemplate.from_template("{topic}에 대한 설명을 작성해주세요.")
| llm
| StrOutputParser()
| (lambda x: {"text": x})
| parallel_chain
)
result = full_chain.invoke("머신러닝")
print(result) # {"summary": "...", "keywords": "..."}
박시니어 씨가 공장 이야기를 시작했습니다. "자동차 공장을 상상해보세요.
철판이 들어오면 프레스 기계가 모양을 잡고, 용접 로봇이 이어붙이고, 도장 부스에서 색칠하고, 최종 검수를 거쳐 완성차가 나옵니다. 각 단계가 전문화되어 있고, 컨베이어 벨트로 연결되어 있죠." LCEL도 마찬가지입니다.
각 컴포넌트가 자기 역할을 수행하고, 그 결과를 다음 컴포넌트에 전달합니다. 파이프 연산자(|)가 바로 그 컨베이어 벨트 역할을 합니다.
prompt | llm | StrOutputParser() 이 한 줄이 의미하는 바를 풀어보면 이렇습니다. 먼저 prompt가 템플릿에 변수를 채워 완성된 프롬프트를 만듭니다.
이 결과가 llm으로 전달되어 AI가 응답을 생성합니다. 그 응답이 다시 StrOutputParser로 전달되어 깔끔한 문자열로 변환됩니다.
이 방식의 장점은 조합의 유연성입니다. 중간에 컴포넌트를 바꾸거나, 새로운 단계를 추가하거나, 분기를 만들거나 할 수 있습니다.
레고 블록을 조립하듯이요. RunnableParallel은 여러 작업을 동시에 실행할 때 사용합니다.
위 코드에서는 텍스트 요약과 키워드 추출을 병렬로 처리합니다. 순차적으로 실행하면 두 배의 시간이 걸리지만, 병렬로 실행하면 더 빠르게 결과를 얻을 수 있습니다.
결과는 딕셔너리 형태로 각각의 키에 담겨 반환됩니다. RunnablePassthrough는 입력을 그대로 통과시킵니다.
언뜻 보면 불필요해 보이지만, 체인 중간에 데이터를 재구성할 때 유용합니다. 예를 들어 원본 입력을 유지하면서 동시에 다른 처리를 해야 할 때 사용합니다.
코드의 full_chain을 보면, 여러 단계가 연결되어 있습니다. 먼저 주제에 대한 설명을 생성하고, 그 설명을 요약과 키워드 추출에 동시에 전달합니다.
람다 함수를 사용해서 중간에 데이터 형태를 변환하는 것도 가능합니다. 실무에서 이런 체인 구성이 필요한 경우는 많습니다.
예를 들어, 고객 문의를 받아서 먼저 의도를 파악하고, 그 의도에 따라 다른 처리를 하고, 최종 응답을 생성하는 플로우를 만들 수 있습니다. 각 단계가 독립적인 컴포넌트로 구성되어 있으니 테스트하기도 쉽고, 문제가 생겼을 때 어느 단계에서 발생했는지 파악하기도 쉽습니다.
주의할 점은 체인이 너무 복잡해지면 오히려 가독성이 떨어진다는 것입니다. 적절한 수준에서 체인을 나누고, 각각에 명확한 이름을 붙여주는 것이 좋습니다.
김개발 씨가 실습을 마치고 말했습니다. "이제 여러 기능을 조합하는 방법을 알겠어요.
그런데 대화 기록은 어떻게 관리하나요?"
실전 팁
💡 - invoke()는 동기 실행, ainvoke()는 비동기 실행입니다
- stream()을 사용하면 응답을 실시간으로 스트리밍할 수 있습니다
- 복잡한 체인은 작은 단위로 나눠서 테스트하세요
5. Memory 관리
김개발 씨가 만든 챗봇을 테스트하던 중 이상한 점을 발견했습니다. "제 이름이 뭐라고 했죠?"라고 물어봤는데, 챗봇이 "죄송합니다, 이름을 알려주신 적이 없습니다"라고 대답한 것입니다.
분명 대화 시작할 때 이름을 말했는데 말이죠. 박시니어 씨가 설명했습니다.
"그게 바로 Memory가 필요한 이유예요."
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
llm = ChatOpenAI(model="gpt-4o-mini")
# 프롬프트에 대화 기록 삽입 위치 지정
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 친절한 비서입니다. 사용자의 정보를 기억해주세요."),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
chain = prompt | llm
# 세션별 대화 기록 저장소
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# 메모리가 적용된 체인
chain_with_memory = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
# 사용 예시 - 같은 세션 ID로 대화하면 기억함
config = {"configurable": {"session_id": "user123"}}
response1 = chain_with_memory.invoke({"input": "안녕하세요, 제 이름은 김개발입니다."}, config=config)
response2 = chain_with_memory.invoke({"input": "제 이름이 뭐라고 했죠?"}, config=config)
박시니어 씨가 화이트보드에 그림을 그렸습니다. "LLM은 기본적으로 금붕어예요." 김개발 씨가 웃었습니다.
"금붕어요?" "네, 금붕어가 3초밖에 기억을 못한다고 하잖아요. LLM도 마찬가지예요.
매번 새로운 요청이 들어오면 이전에 무슨 대화를 했는지 전혀 모릅니다. 완전히 새로운 대화로 인식해요." 그렇다면 ChatGPT는 어떻게 이전 대화를 기억하는 것처럼 보일까요?
비밀은 매번 이전 대화 내용을 함께 전달하는 것입니다. 사용자가 "제 이름이 뭐라고 했죠?"라고 물어볼 때, 뒤에서는 이전의 모든 대화 내용이 함께 전달됩니다.
LLM은 그 내용을 읽고 이름을 찾아서 답변하는 것입니다. 기억하는 게 아니라 매번 읽는 것이죠.
InMemoryChatMessageHistory는 대화 기록을 메모리에 저장합니다. 가장 간단한 형태의 저장소입니다.
서버가 재시작되면 사라지지만, 개발과 테스트에는 충분합니다. RunnableWithMessageHistory는 체인에 메모리 기능을 추가합니다.
이 래퍼를 사용하면 매번 수동으로 대화 기록을 관리할 필요가 없습니다. 자동으로 이전 대화를 가져오고, 새 대화를 저장합니다.
중요한 것은 session_id 개념입니다. 여러 사용자가 동시에 챗봇을 사용할 수 있으니, 각 사용자의 대화를 구분해서 저장해야 합니다.
session_id가 "user123"인 대화와 "user456"인 대화는 완전히 별개로 관리됩니다. 코드를 보면, get_session_history 함수가 session_id를 받아서 해당 세션의 대화 기록을 반환합니다.
처음 요청하는 세션이면 새 저장소를 만들고, 기존 세션이면 이전 기록을 가져옵니다. 실무에서 주의해야 할 점이 있습니다.
대화가 길어지면 토큰 수가 계속 늘어납니다. 100번의 대화를 주고받았다면, 101번째 질문에는 이전 100개의 대화가 모두 포함됩니다.
비용도 늘어나고, 최대 토큰 수를 초과할 수도 있습니다. 이 문제를 해결하기 위한 여러 전략이 있습니다.
최근 N개의 대화만 유지하거나, 오래된 대화를 요약해서 저장하거나, 중요한 정보만 추출해서 보관하는 방식 등이 있습니다. 프로젝트의 요구사항에 맞게 선택하면 됩니다.
프로덕션 환경에서는 InMemoryChatMessageHistory 대신 Redis나 데이터베이스 기반의 저장소를 사용합니다. 서버가 재시작되어도 대화 기록이 유지되어야 하니까요.
김개발 씨가 테스트를 해보더니 환호했습니다. "이제 제 이름을 기억하네요!"
실전 팁
💡 - 대화 기록이 너무 길어지면 ConversationSummaryMemory를 사용해 요약을 저장하세요
- 프로덕션에서는 RedisChatMessageHistory 등 영구 저장소를 사용하세요
- 토큰 비용 관리를 위해 최대 대화 수를 제한하는 것이 좋습니다
6. Agent와 Tool 사용
드디어 마지막 주제입니다. 김개발 씨가 챗봇을 거의 완성했는데, 새로운 요구사항이 들어왔습니다.
"실시간 날씨 정보도 알려줬으면 좋겠어요." LLM은 학습된 데이터만 알고 있는데, 실시간 정보는 어떻게 가져올까요? 박시니어 씨가 말했습니다.
"이럴 때 Agent와 Tool이 필요해요."
Agent는 스스로 판단하여 필요한 도구를 선택하고 실행하는 LLM입니다. Tool은 Agent가 사용할 수 있는 외부 기능으로, 웹 검색, 계산기, API 호출 등 다양한 작업을 수행할 수 있습니다.
마치 스마트폰에 여러 앱을 설치하고, 상황에 따라 적절한 앱을 사용하는 것과 같습니다.
다음 코드를 살펴봅시다.
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
# 커스텀 Tool 정의
@tool
def calculate(expression: str) -> str:
"""수학 계산을 수행합니다. 예: '2 + 3 * 4'"""
try:
result = eval(expression) # 실무에서는 안전한 파서 사용
return f"계산 결과: {result}"
except Exception as e:
return f"계산 오류: {str(e)}"
@tool
def get_weather(city: str) -> str:
"""특정 도시의 날씨를 조회합니다."""
# 실제로는 API 호출
weather_data = {"서울": "맑음, 15도", "부산": "흐림, 18도"}
return weather_data.get(city, f"{city}의 날씨 정보를 찾을 수 없습니다.")
# Tool 목록
tools = [calculate, get_weather]
# Agent 생성 - LangGraph 사용
llm = ChatOpenAI(model="gpt-4o-mini")
agent = create_react_agent(llm, tools)
# Agent 실행
result = agent.invoke({"messages": [("human", "서울 날씨 어때?")]})
print(result["messages"][-1].content)
result2 = agent.invoke({"messages": [("human", "15% 팁 포함해서 85000원이면 얼마야?")]})
print(result2["messages"][-1].content)
박시니어 씨가 비유를 들었습니다. "만능 비서를 생각해보세요." 만능 비서에게 "내일 서울 날씨 알려주고, 우산 필요하면 구매 링크도 찾아줘"라고 요청했다고 합시다.
비서는 먼저 날씨 앱을 열어 내일 서울 날씨를 확인합니다. 비가 올 것 같으면 쇼핑 앱을 열어 우산을 검색합니다.
그리고 결과를 정리해서 알려줍니다. Agent도 마찬가지입니다.
사용자의 요청을 이해하고, 필요한 도구를 스스로 선택하여 실행하고, 그 결과를 종합해서 답변합니다. 이것이 단순한 LLM과 Agent의 가장 큰 차이점입니다.
단순한 LLM은 학습된 지식으로만 답변하지만, Agent는 외부 도구를 활용해서 실시간 정보를 가져오거나 실제 작업을 수행할 수 있습니다. Tool은 Agent가 사용할 수 있는 도구입니다.
@tool 데코레이터를 사용하면 일반 Python 함수를 Tool로 만들 수 있습니다. 중요한 것은 docstring입니다.
Agent는 이 설명을 읽고 어떤 도구를 사용할지 결정합니다. 설명이 명확할수록 Agent가 올바른 도구를 선택할 확률이 높아집니다.
위 코드에서 calculate 도구는 수학 계산을, get_weather 도구는 날씨 조회를 담당합니다. "서울 날씨 어때?"라고 물어보면 Agent는 get_weather 도구를 선택하고, "15% 팁 포함해서 85000원이면 얼마야?"라고 물어보면 calculate 도구를 선택합니다.
create_react_agent는 LangGraph 라이브러리에서 제공하는 Agent 생성 함수입니다. ReAct(Reasoning and Acting) 패턴을 사용하여, 생각하고 행동하고 관찰하는 과정을 반복합니다.
필요한 만큼 여러 도구를 순차적으로 실행할 수도 있습니다. 실무에서 자주 사용되는 Tool들이 있습니다.
웹 검색(Tavily, SerpAPI), 위키피디아 조회, 데이터베이스 쿼리, API 호출 등입니다. LangChain 커뮤니티에서 이미 만들어둔 Tool들이 많으니, 필요한 것을 가져다 쓰면 됩니다.
Agent 사용 시 주의할 점이 있습니다. Agent는 스스로 판단하기 때문에, 예상치 못한 동작을 할 수 있습니다.
엉뚱한 도구를 선택하거나, 불필요하게 여러 번 도구를 호출할 수도 있습니다. 따라서 중요한 작업을 수행하는 Tool에는 적절한 권한 검사와 로깅을 추가해야 합니다.
또한 Agent는 일반 LLM 호출보다 느리고 비용이 더 듭니다. 도구 선택과 실행이 추가되기 때문입니다.
단순한 질문 응답에는 일반 체인을, 외부 도구가 필요한 복잡한 작업에만 Agent를 사용하는 것이 좋습니다. 김개발 씨가 챗봇에 날씨 조회 기능을 추가했습니다.
테스트해보니 "서울 날씨가 어때?"라고 물어볼 때 자동으로 날씨 API를 호출해서 답변해주었습니다. "이제 진짜 유용한 챗봇이 된 것 같아요!" 박시니어 씨가 웃으며 말했습니다.
"축하해요. 이제 LangChain의 기본기를 마스터했네요.
이걸 바탕으로 더 복잡한 애플리케이션도 만들 수 있을 거예요!"
실전 팁
💡 - Tool의 docstring을 명확하고 구체적으로 작성하면 Agent의 정확도가 높아집니다
- 민감한 작업을 수행하는 Tool에는 반드시 권한 검사를 추가하세요
- LangGraph를 사용하면 더 복잡한 Agent 워크플로우를 구현할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.