📚 Agentic AI - 기업용 자율 에이전트 개발 2장 · LLM API 기초 Agentic AI란

따라하기 — 대화를 손으로 쌓아 멀티턴 만들기

한 줄 요약

리스트 하나에 대화를 차곡차곡 담아 매번 통째로 보냅니다. 그것만으로 LLM이 "기억하는 것처럼" 동작합니다. 7장에서 라이브러리가 대신해 줄 일을, 여기서는 손으로 해 봅니다.


1. 아이디어

앞 절에서 확인한 문제는 입력에 지난 대화가 없다는 것이었습니다. 그러면 넣어 주면 됩니다.

호출 ①contents = ["김민준입니다"]호출 ②contents = ["김민준입니다", "김민준 고객님…", "제 이름이 뭐죠?"]지난 대화를 함께 보낸다모델은 지난 대화를 기억하지 않습니다. 기억하는 것처럼 보이는 것은 매번 이전 대화도 함께 보내기 때문입니다.
멀티턴은 지난 대화를 매번 다시 실어 보내는 것이다

이게 전부입니다. 어려운 기술이 아니라 리스트에 계속 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칸을 차지하게 맞춰 찍어라" 는 뜻입니다 — 그래서 usermodel 뒤 글자가 세로로 줄이 맞습니다.

  [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)

스스로 해 보기

  1. [3] 에서 ③번 줄(history.append(say("model", answer)))을 지우고 실행해 보세요. 어떻게 달라지나요? 왜 그런지 한 문장으로 설명해 보세요.
  2. [4] 를 추가해 5마디 이상 대화해 보세요. 대화 끝에 len(history) 는 몇이 될까요? 실행 전에 예측하고 확인하세요.
  3. chat_multi 함수 안의 temperature=01.5 로 고쳐 같은 대화를 해 보세요. 기억은 여전히 되나요? (기억과 온도는 별개라는 걸 확인하는 문제입니다)

chat() 과 달리 chat_multi() 에는 temperature 인자가 없습니다. 함수 본문 안에 temperature=0 이 박혀 있죠. 그래서 chat_multi(SYSTEM, history, temperature=1.5) 라고 부르면 TypeError 가 납니다. 위 3번은 함수 정의 안의 숫자를 직접 고치는 것입니다.


핵심 정리

  • 멀티턴의 정체는 리스트에 대화를 쌓아 매번 통째로 보내는 것입니다.
  • 한 턴은 세 단계입니다 — ① 사용자 말 기록 → ② 전체 전송 → ③ 답변 기록.
  • chat_multichatcontents 에 목록을 넣는 것만 다릅니다.
  • ③(답변 기록)을 빠뜨리면 모델은 자기가 한 말을 모릅니다.
  • 손으로 하는 방식의 한계는 셋 — 여러 사용자 분리, 저장, 길이 관리. 7장에서 라이브러리가 맡습니다.
  • 기억은 모델의 능력이 아니라 우리가 만드는 구조입니다.
← 이전 절05. LLM은 방금 한 말도 잊는다다음 절 →07. 토큰과 비용 감각