실무 Multi-Agent 오케스트레이션 16장 · 멀티턴 대화와 세션 관리 2 / 7 ← 이전목차다음 → TechLead Cro

16장. 멀티턴 대화와 세션 관리

대화 이력 — 기억은 우리가 보관한다

한 줄 요약

모델은 기억하지 않으므로 기억은 우리 코드가 보관합니다. 오간 메시지를 리스트에 쌓아 두었다가, 새 문의가 올 때마다 "지금까지의 대화 전체 + 새 문의" 를 보냅니다. 모델은 매번 대화 전체를 다시 읽기 때문에 기억하는 것처럼 동작합니다.


1. 이력은 메시지의 리스트다

대화 이력(message history) 은 지금까지 오간 메시지를 순서대로 담은 리스트입니다. Gemini API에서 메시지 하나는 types.Content이고, 누가 한 말인지를 role로 적습니다.

self.history: list[types.Content] = []   # 대화 이력이 곧 '기억'

self.history.append(
    types.Content(role="user", parts=[types.Part.from_text(text=text)]))

호출할 때는 문자열 하나 대신 이 리스트를 통째로 넘깁니다.

response = client.models.generate_content(
    model=MODEL,
    contents=self.history,            # 매번 '전체 이력'이 입력으로 나간다
    config=...,
)

2장부터 써 온 contents=에 문자열이 아니라 리스트가 들어간 것이 전부입니다.


2. 이력 안에는 말만 있는 것이 아니다

우리 상담원은 도구를 씁니다. 그래서 고객 문의 하나를 처리하는 동안 메시지가 네 개 생깁니다. 「따라하기」의 코드에서 첫 문의를 처리한 직후의 이력을 찍어 보면 이렇습니다.

순서 role 내용물 누가 만들었나
0 user 글: "제 최근 주문 상태 알려주세요." 고객
1 model 도구 호출(function_call): get_my_orders 모델
2 user 도구 결과(function_response): 주문 3건 우리 코드
3 model 글: 최종 답변 모델

여기서 두 가지를 봅니다.

도구 결과의 role이 user입니다. 2번 메시지는 고객이 한 말이 아니라 우리 코드가 도구를 실행해 얻은 결과인데, role은 user로 들어갑니다. "모델이 아닌 쪽에서 온 것"이라는 뜻으로 읽으면 됩니다. 이 사실은 뒤에서 이력을 자를 때 중요해집니다.

1번과 2번은 짝입니다. 도구 호출과 도구 결과는 한 묶음으로 움직입니다. 5장에서 손으로 왕복시켰던 바로 그 두 메시지입니다.


3. 도구 왕복도 이력에 남겨야 한다

이력을 가볍게 만들고 싶으면 0번(고객의 말)과 3번(최종 답변)만 남기고 1·2번을 버릴 수도 있습니다. 그러면 무엇이 사라질까요.

"아까 조회한 그 주문"의 조회 결과가 사라집니다. 주문번호, 옵션, 배송 완료일 같은 값은 최종 답변의 문장이 아니라 도구 결과 안에 온전히 들어 있습니다. 다음 턴에 고객이 "그거 취소하면…"이라고 물었을 때 모델이 주문번호를 꺼내 쓸 수 있는 것은 2번 메시지가 이력에 남아 있기 때문입니다.

우리 코드는 SDK가 도구를 자동으로 왕복시키게 하고(5장), 그 왕복 기록을 통째로 이력으로 삼습니다.

# 자동 호출 왕복 이력(도구 결정·결과)도 대화 이력에 보존해야
# 다음 턴에 "아까 조회한 그 주문"을 이해할 수 있다.
if response.automatic_function_calling_history:
    self.history = list(response.automatic_function_calling_history)
if response.candidates:
    self.history.append(response.candidates[0].content)
코드 하는 일
automatic_function_calling_history 우리가 보낸 이력에 이번 턴의 도구 호출과 도구 결과가 덧붙은 리스트. 도구를 쓰지 않은 턴에는 비어 있습니다
response.candidates[0].content 모델의 최종 답변 메시지. 이것까지 붙여야 한 턴이 끝납니다

기억할 가치가 있는 것은 말만이 아닙니다. 모델이 한 행동(도구 호출)과 그 결과도 대화의 일부입니다.


4. 누가 무엇을 하는가

일 누가
지시어("그거")가 무엇을 가리키는지 이력에서 찾아 해석한다 모델
어떤 도구를 부를지 고른다 모델
도구를 실제로 실행하고 결과를 돌려준다 우리 코드(SDK의 자동 호출)
이력을 보관하고, 매번 함께 보낸다 우리 코드
이력을 얼마나 길게 둘지, 언제 버릴지 정한다 개발자

모델이 하는 일은 "읽고 해석하는 것"까지입니다. 읽을 것을 준비하는 일은 전부 우리 몫입니다.

시스템 프롬프트에도 한 줄을 보탰습니다.

대화 맥락을 기억하고, '그거/아까 그 주문' 같은 지시어를 앞선 대화에서 찾아 이해합니다.

이력을 넣어 주는 것에 더해, 지시어는 이력에서 찾아 해석하라고 분명히 적어 준 것입니다.

시스템 프롬프트에는 한 가지가 더 있습니다.

환불 기간·배송비 같은 정책 내용은 이 도구들로 조회할 수 없습니다. 지어내지 않고
"확인 후 안내드리겠습니다"라고 답합니다.

이 상담원이 가진 도구는 주문·상품 조회용입니다. 정책 문서를 찾는 도구는 없습니다. 가진 도구로 확인할 수 있는 것과 없는 것의 경계를 프롬프트에 적어 주면, 모델은 확인할 수 없는 것을 짐작으로 채우지 않습니다.


5. 참고 — 서버가 이력을 맡아 주는 방식

이 과정에서는 이력을 우리 쪽에 보관합니다. 무엇이 모델에게 들어가는지 눈으로 볼 수 있고, 자르거나 요약하는 일(17장)을 우리가 정할 수 있기 때문입니다.

Gemini API에는 이력을 서버가 맡아 주는 방식(Interactions API)도 있습니다. 앞 응답의 ID를 다음 요청에 넘기면 대화가 이어집니다. 다만 2026년 10월 기준으로 이 과정에서 설치한 SDK는 이 기능을 쓸 때 "실험적 기능이며 바뀔 수 있다"는 경고를 띄웁니다. 자세한 내용은 공식 문서(https://ai.google.dev/gemini-api/docs)를 참고하세요.

어느 방식이든 원리는 같습니다. 모델은 기억하지 않고, 누군가 대화를 보관했다가 다시 넣어 줍니다.


핵심 정리

  • 대화 이력은 메시지(types.Content)의 리스트이고, contents=에 통째로 넘깁니다.
  • 도구를 쓰는 턴 하나는 고객의 말 → 도구 호출 → 도구 결과 → 최종 답변 네 메시지입니다.
  • 도구 결과 메시지의 role은 user 입니다. 도구 호출과 도구 결과는 짝입니다.
  • 도구 왕복까지 이력에 남겨야 고객이 말한 적 없는 값(주문번호 등) 을 다음 턴에 꺼내 쓸 수 있습니다.
  • 해석은 모델이, 보관과 재전송은 우리 코드가 합니다.
← 이전 절왜 멀티턴이 필요한가 — "그거"가 뭔지 모른다다음 절 →세션과 스레드 — 누구의, 어느 대화인가
오명운 · macro@prag-ai.com