따라하기 — 대화를 손으로 쌓아 멀티턴 만들기
한 줄 요약
리스트 하나에 대화를 차곡차곡 담아 매번 통째로 보냅니다. 그것만으로 LLM이 "기억하는 것처럼" 동작합니다. 7장에서 라이브러리가 대신해 줄 일을, 여기서는 손으로 해 봅니다.
1. 아이디어
앞 절에서 확인한 문제는 입력에 지난 대화가 없다는 것이었습니다. 그러면 넣어 주면 됩니다.
이게 전부입니다. 어려운 기술이 아니라 리스트에 계속 append 하는 일입니다.
2. 코드 — [3] 부분
앞에서 만든 ch02_chat.py 의 마지막 부분입니다.
아래는 code/ch02_chat.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
history = [] # 여기에 대화를 차곡차곡 쌓는다
first_turn = "제 이름은 김민준입니다."
history.append(say("user", first_turn)) # ① 고객 말을 기록
answer = chat_multi(SYSTEM, history) # ② 기록 전체를 보내 답을 받음
history.append(say("model", answer)) # ③ 상담원 답도 기록
print(f" 고객 : {first_turn}")
print(f" 상담원: {answer}")
second_turn = "제 이름이 뭐라고 했죠?"
history.append(say("user", second_turn))
answer = chat_multi(SYSTEM, history)
history.append(say("model", answer))
print(f" 고객 : {second_turn}")
print(f" 상담원: {answer}")
①②③ 세 단계가 한 턴입니다. 이 셋을 반복하면 대화가 이어집니다.
③을 빠뜨리면 안 됩니다. 고객 말만 쌓고 상담원 답은 안 쌓으면, 모델은 자기가 뭐라고 답했는지 모르는 채로 대화하게 됩니다. 양쪽 다 기록해야 합니다.
chat_multi() 는 chat() 과 무엇이 다른가
아래는 code/ch02_chat.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
def chat_multi(system: str, history: list) -> str:
resp = client.models.generate_content(
model=GEMINI_MODEL,
contents=history, # ← 문자열이 아니라 목록
config=types.GenerateContentConfig(
system_instruction=system, temperature=0),
)
return (resp.text or "").strip()
딱 한 글자 차이입니다 — contents 에 문자열 대신 목록을 넣습니다. 나머지는 똑같습니다.
3. 실행 결과
==============================================================
[3] 대화를 쌓아 보내는 상담원
고객 : 제 이름은 김민준입니다.
상담원: 네, 김민준 고객님. 무엇을 도와드릴까요?
고객 : 제 이름이 뭐라고 했죠?
상담원: 김민준 고객님이십니다.
→ 이번에는 기억합니다. 쌓인 대화 4개를 통째로 보냈기 때문입니다.
→ 대화가 길어질수록 매번 보내는 양이 늘어납니다(= 비용 증가).
7장에서 이 일을 라이브러리에게 맡깁니다.
같은 질문인데 이번엔 답합니다. 모델이 똑똑해진 게 아니라, 우리가 정보를 넣어 준 것입니다.
4. history 안을 들여다보기
두 번째 호출 시점에 history 는 이렇게 생겼습니다.
history = [
Content(role="user", parts=[Part(text="제 이름은 김민준입니다.")]),
Content(role="model", parts=[Part(text="네, 김민준 고객님. 무엇을 도와드릴까요?")]),
Content(role="user", parts=[Part(text="제 이름이 뭐라고 했죠?")]),
]
요소 3개짜리 평범한 파이썬 리스트입니다. 특별할 것이 없습니다.
직접 찍어 보고 싶다면 [3] 마지막에 이 줄을 추가해 보세요.
직접 고쳐 보기 —
agentic_ai/code/ch02_chat.py를 열어 아래처럼 바꾸거나 덧붙인 뒤 다시 실행하세요.
for i, c in enumerate(history):
print(f" [{i}] {c.role:6s} {c.parts[0].text[:40]}")
낯선 표기 두 개만 짚습니다.
enumerate(...)는 목록을 돌면서 번호와 값을 같이 꺼내 줍니다.{c.role:6s}의:6s는 "글자 6칸을 차지하게 맞춰 찍어라" 는 뜻입니다 — 그래서user와model뒤 글자가 세로로 줄이 맞습니다.
[0] user 제 이름은 김민준입니다.
[1] model 네, 김민준 고객님. 무엇을 도와드릴까요?
[2] user 제 이름이 뭐라고 했죠?
[3] model 김민준 고객님이십니다.
5. 대화를 계속 이어 가려면
지금은 두 마디로 끝나지만, 반복문으로 감싸면 진짜 챗봇이 됩니다. [3] 아래에 붙여 실행해 보세요.
직접 고쳐 보기 —
agentic_ai/code/ch02_chat.py를 열어 아래처럼 바꾸거나 덧붙인 뒤 다시 실행하세요.
# ── 4. 진짜 대화처럼 이어 가기 (직접 추가해 보기) ─────────
print("\n" + "=" * 62)
print("[4] 직접 대화해 보기 — 그만하려면 그냥 엔터")
while True:
message = input(" 고객 : ").strip()
if not message:
break
history.append(say("user", message))
answer = chat_multi(SYSTEM, history)
history.append(say("model", answer))
print(f" 상담원: {answer}")
print(f" (총 {len(history)}개의 대화가 쌓였습니다)")
앞에서 이름을 말했으니, 몇 마디 뒤에 물어봐도 기억합니다. 직접 확인해 보세요.
6. 이 방식의 한계 — 7장으로 넘기는 이유
손으로 쌓는 방식에는 세 가지 문제가 있습니다.
① 손님이 여러 명이면?
history 리스트가 하나뿐입니다. 고객 A와 고객 B가 동시에 문의하면 대화가 뒤섞입니다. 고객마다 리스트를 따로 만들고, 요청이 올 때마다 맞는 리스트를 찾아 오는 코드를 직접 짜야 합니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
histories = {} # 고객ID → 대화 목록
histories["C0001"] = [...]
histories["C0002"] = [...]
7장에서 이 일을 라이브러리가 대신해 줍니다. 고객마다 대화를 나눠 두는 이 꼬리표를 라이브러리는
thread_id라고 부릅니다.
② 프로그램을 끄면 사라진다
리스트는 메모리에 있습니다. 프로그램이 종료되면 대화가 통째로 날아갑니다. 파일이나 DB에 저장하는 코드를 또 직접 짜야 합니다.
7장에서는 "대화를 어디에 저장할지 정하는 부품" 을 갈아 끼우는 것만으로 해결됩니다. 라이브러리는 이 부품을 체크포인터(checkpointer) 라고 부릅니다 — 메모리에 둘지, 파일에 둘지, DB에 둘지를 이 부품 하나로 바꿉니다.
③ 무한정 길어진다
앞 절에서 본 대로, 대화가 길어질수록 매번 보내는 양이 늘어납니다. 어느 시점에는 오래된 대화를 잘라 내거나 요약해야 합니다.
7. 지금 정리해 둘 것
"기억"은 모델의 능력이 아니라 우리가 만드는 구조입니다.
이 문장이 이번 장에서 가장 중요합니다. 7장에서 checkpointer=InMemorySaver() 한 줄을 쓸 때, 그 한 줄이 하는 일이 정확히 이 절에서 손으로 한 append 라는 걸 알고 쓰게 됩니다.
오른쪽 괄호 안의 이름들은 7장에서 하나씩 다룹니다. 지금은 "왼쪽 일을 라이브러리가 대신한다"는 것만 보세요.
| 여기서 손으로 한 일 | 7장에서 라이브러리가 하는 일 |
|---|---|
history = [] |
대화 목록을 라이브러리가 대신 들고 있음 (상태 · State) |
history.append(...) |
새 대화를 알아서 이어 붙임 (add_messages) |
| 고객마다 리스트 분리 | 고객마다 자동으로 나눠 둠 (thread_id) |
| 파일에 저장 | 저장 방식만 갈아 끼움 (체크포인터 · checkpointer) |
스스로 해 보기
[3]에서 ③번 줄(history.append(say("model", answer)))을 지우고 실행해 보세요. 어떻게 달라지나요? 왜 그런지 한 문장으로 설명해 보세요.- 위
[4]를 추가해 5마디 이상 대화해 보세요. 대화 끝에len(history)는 몇이 될까요? 실행 전에 예측하고 확인하세요. chat_multi함수 안의temperature=0을1.5로 고쳐 같은 대화를 해 보세요. 기억은 여전히 되나요? (기억과 온도는 별개라는 걸 확인하는 문제입니다)
chat()과 달리chat_multi()에는temperature인자가 없습니다. 함수 본문 안에temperature=0이 박혀 있죠. 그래서chat_multi(SYSTEM, history, temperature=1.5)라고 부르면TypeError가 납니다. 위 3번은 함수 정의 안의 숫자를 직접 고치는 것입니다.
핵심 정리
- 멀티턴의 정체는 리스트에 대화를 쌓아 매번 통째로 보내는 것입니다.
- 한 턴은 세 단계입니다 — ① 사용자 말 기록 → ② 전체 전송 → ③ 답변 기록.
chat_multi는chat과contents에 목록을 넣는 것만 다릅니다.- ③(답변 기록)을 빠뜨리면 모델은 자기가 한 말을 모릅니다.
- 손으로 하는 방식의 한계는 셋 — 여러 사용자 분리, 저장, 길이 관리. 7장에서 라이브러리가 맡습니다.
- 기억은 모델의 능력이 아니라 우리가 만드는 구조입니다.