본문 바로가기

Gemini

Gemini 3.5 Flash Thought Preservation 완전분석 — 멀티턴 추론이 자동으로 이어지는 것, 비용은 어떻게 올라가나

반응형

Thought Preservation은 Gemini 3.5 Flash 출시와 함께 기본값으로 켜졌습니다. API 설정 변경도 없고, 공지도 조용했습니다. 그리고 수많은 팀의 청구서가 예상보다 50~80% 높아졌습니다.


핵심 요약 → Thought Preservation: 이전 턴의 추론 토큰(Thought Tokens)을 다음 턴 입력에 자동 포함 → Gemini 3.5 Flash 출시와 함께 기본값 ON — API 변경 없이 자동 적용 → 효과: 멀티턴 에이전트 루프에서 맥락 일관성·품질 향상 → 비용: 10턴 에이전트 루프 입력 토큰 50~80% 증가 → Antigravity 쿼터 사태의 숨겨진 주범 — 단가 3배 × 토큰 소비 증가 = 5.6배 청구서 → GenerateContent API: thoughts 포함 전체 히스토리 전달 필요 → Interactions API: 서버가 자동 처리 (개발자 개입 불필요) → 방어 전략 4가지: thinking_level 낮추기, 세션 분리, 명시적 삭제, Interactions API 전환


Thought Preservation이란 무엇인가

# 기존 모델의 추론 방식 (3 Flash Preview까지)

턴 1: 사용자 "이 코드 버그 찾아줘"
      → 모델이 내부적으로 생각함 (Thought Tokens)
      → 답변 반환
      → 추론 과정 폐기 ← 다음 턴에서 모름

턴 2: 사용자 "고쳐줘"
      → 모델이 다시 처음부터 생각함
      → 이전에 어떻게 분석했는지 기억 없음
      → 일관성 낮음

# Gemini 3.5 Flash Thought Preservation

턴 1: 사용자 "이 코드 버그 찾아줘"
      → 모델이 추론 (Thought Tokens 생성)
      → 답변 반환
      → 추론 과정 보존 ← thought signature로 히스토리에 포함

턴 2: 사용자 "고쳐줘"
      → 이전 턴의 추론 컨텍스트 포함해서 생각
      → "아, 이전에 off-by-one 에러라고 분석했지"
      → 더 일관되고 정확한 수정

Gemini 3.5 Flash는 멀티턴 대화에서 중간 추론을 자동으로 유지합니다. API 변경 없이 작동하며, 모델이 이전에 어떻게 추론했는지를 참조할 수 있어 긴 에이전트 루프에서 일관성이 향상됩니다. 단, 보존된 추론 토큰은 이후 턴마다 입력으로 카운트됩니다.


1. 내부 동작 메커니즘 — Thought Signature란

# Thought Signature: 추론 토큰을 히스토리에 포함시키는 마커

import google.generativeai as genai

client = genai.Client()

# GenerateContent API 사용 시
# SDK가 자동으로 thought_signature를 히스토리에 포함

response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="복잡한 에이전트 태스크를 분석해줘",
    config={"thinking_level": "medium"}
)

# 응답에 포함된 것들
print(response.usage_metadata)
# CachedContentTokenCount:  3,072
# PromptTokenCount:         12,450
# CandidatesTokenCount:     891
# ThoughtsTokenCount:       4,203  ← 추론 토큰
# TotalTokenCount:          17,544

# thought_signature가 응답 파트에 포함됨
for part in response.candidates[0].content.parts:
    if hasattr(part, 'thought') and part.thought:
        # 이게 thought signature
        # 다음 턴 히스토리에 그대로 포함해야 함
        thought_content = part.text

2. 비용이 어떻게 누적되는가 — 실제 계산

10턴 에이전트 대화에서 구 API 기준 턴당 50,000 입력 토큰을 소비했다면 Thought Preservation 활성화 시 턴당 75,000~90,000 입력 토큰을 소비할 수 있습니다. thinking_level에 따라 50~80% 인플레이션이 발생합니다.

# Thought Preservation 비용 누적 시뮬레이션

def simulate_thought_preservation_cost(
    turns: int = 10,
    base_input_tokens: int = 2000,   # 턴당 사용자 입력
    base_output_tokens: int = 500,   # 턴당 모델 출력
    thinking_level: str = "medium",  # low/medium/high
    input_price: float = 1.50,       # $1.50/1M tokens
    output_price: float = 9.00       # $9.00/1M tokens
) -> dict:

    # thinking_level별 턴당 thought 토큰 추정
    thought_tokens_per_turn = {
        "low":    800,
        "medium": 2500,
        "high":   6000
    }[thinking_level]

    total_cost_old = 0  # 3 Flash Preview 방식 (Thought 폐기)
    total_cost_new = 0  # 3.5 Flash (Thought Preservation)

    accumulated_thoughts = 0  # 누적 thought 토큰

    for turn in range(1, turns + 1):
        # 이번 턴 thought 토큰
        this_turn_thoughts = thought_tokens_per_turn

        # 구 방식: 매 턴 기본 입력만
        old_input = base_input_tokens
        old_cost = (
            old_input / 1_000_000 * input_price +
            base_output_tokens / 1_000_000 * output_price
        )
        total_cost_old += old_cost

        # 신규 방식: 누적된 thought 토큰 포함
        new_input = base_input_tokens + accumulated_thoughts
        new_cost = (
            new_input / 1_000_000 * input_price +
            base_output_tokens / 1_000_000 * output_price
        )
        total_cost_new += new_cost

        # thought 토큰 누적
        accumulated_thoughts += this_turn_thoughts

    inflation = (total_cost_new - total_cost_old) / total_cost_old * 100

    return {
        "turns": turns,
        "thinking_level": thinking_level,
        "old_cost": round(total_cost_old * 1000, 4),  # 밀리달러
        "new_cost": round(total_cost_new * 1000, 4),
        "inflation_pct": round(inflation, 1),
        "accumulated_thoughts_at_end": accumulated_thoughts
    }

# 시뮬레이션 결과
results = [
    simulate_thought_preservation_cost(thinking_level="low"),
    simulate_thought_preservation_cost(thinking_level="medium"),
    simulate_thought_preservation_cost(thinking_level="high"),
    simulate_thought_preservation_cost(turns=20, thinking_level="medium"),
]

# 결과 출력
# turns=10, low:    old=$0.33 → new=$0.45 (+36%)
# turns=10, medium: old=$0.33 → new=$0.63 (+91%)
# turns=10, high:   old=$0.33 → new=$1.23 (+273%)
# turns=20, medium: old=$0.66 → new=$2.01 (+205%)

# ⚠ 핵심:
# medium 10턴: 91% 비용 증가
# high 10턴:  273% 비용 증가
# medium 20턴: 205% 비용 증가
# → 대화가 길어질수록 비용이 선형이 아닌 2차 함수로 증가

3. Antigravity 쿼터 사태의 진짜 구조

출시 당시 "3배 가격 인상"만 논란이 됐는데, 실제 청구서가 5.6배가 된 이유가 여기에 있습니다.

# 비용 폭발의 3중 구조

1. 토큰 단가 인상:
   3 Flash Preview: $0.50/1M input
   3.5 Flash:       $1.50/1M input
   → 3배 인상

2. Thought Preservation 토큰 누적:
   에이전트 루프 10턴 기준 입력 토큰 +91%
   → 동일 작업에 약 2배 입력 토큰 소비

3. thinking_level 기본값:
   출시 당시: high (가장 많은 thought 토큰)
   현재: medium (조정됨)

3 × 2 ≈ 5.6배 (Artificial Analysis 실측치와 일치)

Antigravity 유료 사용자 입장:
  전날까지:  에이전트 루프 100회 → X 크레딧 소비
  출시 당일: 에이전트 루프 100회 → 5.6X 크레딧 소비
  → 1시간 만에 주간 쿼터 소진

4. API별 처리 방식 차이

# ── GenerateContent API: 수동 관리 필요 ──

import google.generativeai as genai

client = genai.Client()
conversation_history = []

def chat_with_thought_preservation(user_message: str) -> str:
    conversation_history.append({
        "role": "user",
        "parts": [{"text": user_message}]
    })

    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=conversation_history,
        config={"thinking_level": "medium"}
    )

    # ⚠ 중요: thought_signature 포함해서 히스토리에 추가
    # SDK가 자동 처리하지만 수동 구현 시 반드시 포함
    model_parts = []
    for part in response.candidates[0].content.parts:
        model_parts.append(part)  # thought signature 포함 전체 파트

    conversation_history.append({
        "role": "model",
        "parts": model_parts  # ← thought signature 포함
        # 이걸 빠트리면 다음 턴에서 Thought Preservation 안 됨
    })

    # 텍스트 추출
    return next(
        p.text for p in response.candidates[0].content.parts
        if not (hasattr(p, 'thought') and p.thought)
    )


# ── Interactions API: 자동 처리 ──

def chat_interactions_api(user_message: str, previous_id: str | None) -> tuple[str, str]:
    """
    Interactions API에서는 thought signature 관리 불필요
    서버가 전담 처리
    """
    interaction = client.interactions.create(
        model="gemini-3.5-flash",
        input=user_message,
        previous_interaction_id=previous_id,  # 이것만 전달
        config={"thinking_level": "medium"}
    )

    for step in interaction.steps:
        if step.type == "model_output":
            return step.content[0].text, interaction.id

    return "", interaction.id

5. 방어 전략 4가지

# ── 전략 1: thinking_level 낮추기 (가장 간단) ──

# 에이전트 루프 기본값 = low
# medium 대비 thought 토큰 ~68% 절감
response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents=history,
    config={"thinking_level": "low"}  # medium → low 전환
)


# ── 전략 2: 세션 적극 분리 ──

class SessionManager:
    """
    자연스러운 단계 경계에서 히스토리 초기화
    thought 토큰 누적 리셋
    """
    MAX_TURNS = 8  # 8턴 이후 세션 교체

    def __init__(self):
        self.history = []
        self.turn_count = 0

    def should_reset(self) -> bool:
        return self.turn_count >= self.MAX_TURNS

    def add_turn(self, user_msg, model_response):
        self.history.append(...)
        self.turn_count += 1

        if self.should_reset():
            # 핵심 컨텍스트만 요약해서 새 세션 시작
            summary = self._summarize_context()
            self.history = [{"role": "user", "parts": [{"text": f"이전 컨텍스트 요약: {summary}"}]}]
            self.turn_count = 0

    def _summarize_context(self) -> str:
        # 현재 히스토리 요약 생성 (thought 토큰 없는 단순 요약)
        summary_response = client.models.generate_content(
            model="gemini-3.5-flash",
            contents=[{
                "role": "user",
                "parts": [{"text": f"다음 대화를 3문장으로 요약해줘: {self.history}"}]
            }],
            config={"thinking_level": "minimal"}  # 요약은 minimal
        )
        return summary_response.text


# ── 전략 3: thought 토큰 명시적 삭제 ──

def strip_thoughts_from_history(history: list) -> list:
    """
    단순 쿼리에서 thought signature를 히스토리에서 제거
    Thought Preservation 비활성화 효과
    """
    cleaned = []
    for turn in history:
        if turn["role"] == "model":
            # thought 파트 필터링
            clean_parts = [
                p for p in turn["parts"]
                if not (hasattr(p, 'thought') and p.thought)
            ]
            cleaned.append({"role": "model", "parts": clean_parts})
        else:
            cleaned.append(turn)
    return cleaned

# 사용: 단순 질의응답에서만 적용
# 복잡한 에이전트 루프에서는 유지 권장
simple_history = strip_thoughts_from_history(full_history)


# ── 전략 4: Interactions API 전환 (장기적 최선) ──

# Interactions API는 서버가 Thought Preservation 최적화 처리
# 개발자가 thought 토큰을 직접 다룰 필요 없음
# 서버사이드 히스토리 → 전체 입력 토큰 절감과 시너지

# 단, 6월 8일 스키마 마이그레이션 필요 (별도 가이드 참조)

6. 모니터링 — Thought Token을 추적하는 방법

import logging

logger = logging.getLogger(__name__)

def monitored_generate(contents, thinking_level="medium"):
    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=contents,
        config={"thinking_level": thinking_level}
    )

    usage = response.usage_metadata
    thought_tokens = getattr(usage, 'thoughts_token_count', 0)
    total_input = usage.prompt_token_count

    # 경고: thought 비율이 입력의 30% 초과 시
    thought_ratio = thought_tokens / total_input if total_input > 0 else 0
    if thought_ratio > 0.30:
        logger.warning(
            f"Thought 토큰 비율 높음: {thought_ratio:.1%} "
            f"(thought={thought_tokens}, total_input={total_input})\n"
            f"→ thinking_level 낮추거나 세션 분리 고려"
        )

    logger.info(
        f"level={thinking_level} | "
        f"input={total_input} | "
        f"thought={thought_tokens} ({thought_ratio:.1%}) | "
        f"output={usage.candidates_token_count}"
    )

    return response

언제 Thought Preservation을 활용하고 언제 억제하나

# Thought Preservation 활성화 권장

✅ 복잡한 멀티스텝 디버깅 (이전 분석 맥락 유지가 핵심)
✅ 장기 코드 리팩토링 루프 (파일 간 연관성 추적)
✅ 멀티턴 수학/추론 문제 풀기
✅ 연구 에이전트 (긴 탐색 과정의 일관성)
→ 이런 케이스는 품질 향상이 비용 증가를 정당화

# Thought Preservation 억제 권장

❌ 독립적인 단건 쿼리 반복 (각 턴이 무관한 작업)
❌ 대량 배치 처리 (비용이 핵심 변수)
❌ 단순 분류·추출 태스크
❌ 에이전트 루프 > 15턴 (누적 비용이 기하급수적)
→ strip_thoughts_from_history() 또는 세션 분리 적용

결론

Thought Preservation이 진짜로 좋은 것

  • 멀티턴 에이전트에서 "이전에 어떻게 분석했는지" 자동 기억
  • 복잡한 디버깅·리팩토링에서 일관성 대폭 향상
  • API 변경 없이 자동 적용 — 기존 코드도 혜택

모르면 당하는 것

  • medium 10턴: 비용 91% 증가 / high 10턴: 273% 증가
  • Antigravity 쿼터 5.6배 소진의 숨겨진 원인
  • GenerateContent API: thought signature 포함 히스토리 전달 필수
  • 단순 쿼리 배치에 그냥 쓰면 의미 없는 비용만 증가

지금 해야 할 것

  • usage_metadata.thoughts_token_count 모니터링 추가
  • 에이전트 루프 8~10턴 기준 세션 분리 설계
  • 단순 태스크는 thinking_level: "low" 또는 "minimal" 명시
  • 장기적으로 Interactions API 전환 (서버가 최적화 처리)

관련 글


 

반응형