대화 기록 붙이기 — ConversationChain

📌 3줄 요약
· ConversationChain은 LLM 호출에 이전 대화 기록을 자동으로 끼워 넣어, 앞서 나눈 이야기를 기억한 채 답하게 해 주는 LangChain 체인입니다.
· 매번 전체 대화를 손으로 프롬프트에 붙일 필요 없이, Memory 객체가 주고받은 말을 저장하고 다음 호출에 다시 넣어 줍니다.
· 저장 방식을 Memory 종류로 바꿀 수 있어, 전체를 다 기억할지 최근 몇 턴만 남길지 상황에 맞게 고를 수 있습니다.
💬 새 질문 입력 → 🧠 저장된 대화 기록 불러오기 → 📝 기록+질문 합쳐 LLM 호출 → 💾 이번 대화도 다시 저장

챗봇을 만들다 보면 금방 벽에 부딪히는 지점이 있습니다. 방금 "제 이름은 승일이에요"라고 알려 줬는데, 바로 다음 질문에서 이름을 물으면 모델이 전혀 모른다고 답하는 상황입니다. 사람 사이의 대화라면 당연한 기억이 LLM에게는 기본으로 주어지지 않기 때문입니다. 오늘은 이 문제를 해결하려고 LangChain이 제공하는 ConversationChain을 차근차근 살펴보겠습니다.


🧩 LLM이 대화를 못 기억하는 이유

LLM은 기본적으로 이전 호출을 기억하지 못합니다. 한 번의 호출에서 모델이 볼 수 있는 것은 그때 함께 건네진 프롬프트뿐이기 때문입니다. 호출이 끝나면 모델 쪽에는 아무것도 남지 않습니다. 그래서 다음 질문을 던질 때 앞선 대화를 알려 주지 않으면, 모델은 매번 처음 보는 사람처럼 반응합니다.

그렇다면 기억을 흉내 내는 방법은 분명합니다. 지금까지 주고받은 말을 프롬프트에 직접 붙여서 함께 보내면 됩니다. 문제는 대화가 길어질수록 이 작업을 매번 손으로 하기가 번거롭다는 점입니다.

모델이 기억하는 것처럼 보이게 하려면, 결국 이전 대화를 다시 프롬프트에 넣어 주는 누군가가 필요합니다 — 그 역할을 자동화한 것이 ConversationChain입니다.

💡 ConversationChain의 아이디어

ConversationChain의 발상은 간단합니다. 대화 기록을 붙이는 귀찮은 일을 사람 대신 체인이 알아서 처리하자는 것입니다. 이를 위해 체인은 Memory라는 저장 공간을 하나 갖습니다.

사용자가 말을 건네면 체인은 먼저 Memory에 쌓인 지난 대화를 꺼내 이번 프롬프트 앞에 붙입니다. 그렇게 만든 프롬프트로 LLM을 호출해 답을 받고, 이번에 오간 질문과 답변까지 다시 Memory에 저장합니다. 다음 차례가 오면 이 과정이 되풀이되면서, 겉으로는 모델이 대화 맥락을 기억하는 것처럼 보이게 됩니다.


🔁 동작 흐름 따라가기

체인이 한 번의 대화에서 내부적으로 하는 일을 단계로 나눠 보면 구조가 한눈에 들어옵니다.

🧠 불러오기 단계
Memory에 저장된 이전 대화를 꺼내, 이번 질문과 함께 프롬프트에 끼워 넣습니다.
💾 저장하기 단계
LLM의 답을 받은 뒤, 이번에 오간 말을 다시 Memory에 넣어 다음 호출을 준비합니다.

흐름은 이렇게 이어집니다. 사용자가 입력하면 체인이 Memory에서 지난 대화를 불러와 프롬프트를 조립하고, 그 프롬프트로 LLM을 호출합니다. 답이 돌아오면 이번 입력과 응답을 Memory에 추가하고 결과를 사용자에게 돌려줍니다. 매 턴마다 이 과정이 반복되므로, 대화가 쌓일수록 프롬프트에 붙는 기록도 함께 늘어나는 구조입니다.


⚙️ 코드로 살펴보기

가장 기본적인 형태는 ConversationChain에 LLM과 Memory를 넘겨 조립하는 것입니다. 여기서는 대화 전체를 그대로 쌓아 두는 ConversationBufferMemory를 함께 써 보겠습니다.

from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(temperature=0)

# 주고받은 대화를 그대로 저장하는 Memory
memory = ConversationBufferMemory()

conversation = ConversationChain(
    llm=llm,
    memory=memory,
)

이제 predict(또는 invoke)로 말을 건네 보면, 두 번째 질문에서 앞선 대화를 기억한 채 답하는 것을 확인할 수 있습니다.

# 첫 번째 턴 — 이름을 알려 준다
conversation.predict(input="제 이름은 승일이에요.")

# 두 번째 턴 — 이전 대화를 기억하는지 확인
answer = conversation.predict(input="제 이름이 뭐라고 했죠?")
print(answer)
# → "승일님이라고 하셨어요." 처럼 이름을 기억한 답이 나온다

두 번째 호출에서 이름을 다시 알려 주지 않았는데도 모델이 답할 수 있는 이유는, 체인이 첫 턴의 대화를 Memory에 저장했다가 두 번째 프롬프트에 자동으로 붙여 줬기 때문입니다.


🪜 기억 방식 바꿔 보기

대화를 어떻게 저장할지는 Memory 종류를 바꾸는 것만으로 조정할 수 있습니다. 체인 코드는 그대로 두고 Memory 객체만 교체하면 되기 때문에 전환이 간단합니다. 대표적인 몇 가지를 표로 정리했습니다.

Memory 종류 기억 방식
BufferMemory주고받은 대화를 전부 그대로 저장합니다. 단순하지만 길어지면 프롬프트가 커집니다.
BufferWindowMemory최근 몇 턴만 남기고 오래된 대화는 버립니다. 길이를 일정하게 유지할 때 좋습니다.
SummaryMemory지난 대화를 요약문으로 압축해 저장합니다. 긴 대화를 짧게 담고 싶을 때 쓰입니다.

예를 들어 최근 대화만 유지하고 싶다면, 아래처럼 ConversationBufferWindowMemory로 바꾸고 k 값으로 남길 턴 수를 정하면 됩니다.

from langchain.memory import ConversationBufferWindowMemory

# 최근 3턴만 기억하도록 설정
memory = ConversationBufferWindowMemory(k=3)

conversation = ConversationChain(
    llm=llm,
    memory=memory,
)

이렇게 하면 대화가 아무리 길어져도 프롬프트에 붙는 기록은 최근 3턴으로 제한됩니다. 기억의 범위와 프롬프트 길이를 Memory 선택만으로 조절할 수 있는 셈입니다.


🔧 실무 팁

  • 기록이 곧 토큰 비용 — 대화를 전부 쌓는 BufferMemory는 편하지만, 길어질수록 매 호출의 프롬프트가 커져 토큰 비용과 지연이 늘어납니다.
  • 길이 제한이 필요하면 Window — 상담 봇처럼 턴이 계속 이어지는 서비스는 최근 몇 턴만 남기는 방식이 안정적입니다.
  • 맥락 보존엔 Summary — 오래된 대화를 버리기 아쉽다면 요약으로 압축해, 전체 흐름은 유지하면서 길이를 줄일 수 있습니다.
  • 사용자별로 Memory 분리 — 여러 사람이 동시에 쓰는 서비스라면, 대화 기록이 섞이지 않도록 사용자마다 Memory를 따로 둬야 합니다.

❓ 자주 묻는 질문 (FAQ)

Q. 모델이 정말로 대화를 기억하는 건가요?
A. 아닙니다. 모델 자체는 여전히 이전 호출을 기억하지 못합니다. 체인이 지난 대화를 프롬프트에 다시 붙여 주기 때문에 기억하는 것처럼 보일 뿐입니다.

Q. 대화가 아주 길어지면 어떻게 되나요?
A. BufferMemory는 기록을 계속 쌓으므로 프롬프트가 점점 길어져 토큰 한도에 걸릴 수 있습니다. 이럴 때는 Window나 Summary 계열 Memory로 길이를 관리하는 것이 좋습니다.

Q. 서버를 재시작하면 대화가 사라지나요?
A. 기본 Memory는 메모리 안에만 기록을 두므로 재시작하면 지워집니다. 영구 보관이 필요하면 대화 기록을 외부 저장소에 두는 방식과 함께 써야 합니다.


마무리

정리하면, ConversationChain은 LLM이 이전 대화를 기억하지 못하는 한계를, Memory에 기록을 저장했다가 다음 프롬프트에 다시 붙여 주는 방식으로 메워 주는 체인입니다. 사용자가 말을 건넬 때마다 지난 대화를 불러와 합치고, 이번 대화를 다시 저장하는 과정이 반복되면서 맥락이 이어집니다. 저장 방식은 Memory 종류로 바꿀 수 있어, 전체를 다 담는 Buffer부터 최근 몇 턴만 남기는 Window, 요약으로 압축하는 Summary까지 상황에 맞게 고르는 것이 핵심이었습니다. 다음 편에서는 이 기억을 더 유연하게 다루도록 LangChain이 새로 제시한 RunnableWithMessageHistory 이야기로 이어가 보겠습니다.

댓글

이 블로그의 인기 게시물

LCEL — LangChain 표현식으로 체인 깔끔하게 잇기

Streaming — 답변을 실시간으로 흘려보내기

Callback와 로깅 — LangChain 내부 들여다보기