본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 07:13

데이터 집약적인 RAG 애플리케이션에서 LLM 비용을 절감하는 두 가지 패턴

요약

RAG 애플리케이션 구축 시 발생하는 컨텍스트 비대화 문제를 해결하고 LLM 비용을 절감하는 두 가지 패턴을 소개합니다. 의도 탐지를 통한 조건부 컨텍스트 구축과 사전 생성 패턴을 통해 토큰 사용량을 줄이고 응답 품질을 높이는 방법을 다룹니다.

핵심 포인트

  • 컨텍스트 비대화는 비용 폭증과 모델 성능 저하를 유발함
  • 의도 탐지(Intent Detection)를 통해 필요한 데이터만 선택적으로 로드
  • 조건부 컨텍스트 구축으로 불필요한 토큰 소모 방지
  • 실제 F1 데이터 분석 프로젝트를 통한 비용 절감 사례 제시

컨텍스트 윈도우(context window)에 무엇을, 언제 넣을지 재고함으로써 F1 텔레메트리 분석기에서 토큰 사용량을 어떻게 대폭 줄였는지에 대하여.

구조화된 데이터(데이터베이스, API, 텔레메트리)를 기반으로 RAG 애플리케이션을 구축할 때, 가장 단순한 접근 방식은 모든 것을 컨텍스트(context)에 쏟아붓고 LLM이 알아서 판단하게 하는 것입니다. 이 방식은 작동은 하지만, 비용이 많이 들고 느립니다. FastF1 + Supabase + Claude를 기반으로 Formula 1 레이스 분석을 위한 채팅 인터페이스인 F1 Analyst Pro를 구축한 후, 우리는 응답 품질을 희생하지 않으면서 토큰 사용량과 API 비용을 모두 크게 줄일 수 있는 두 가지 패턴을 정립했습니다.

이 포스트에서는 프로젝트의 실제 코드를 통해 두 가지 패턴을 모두 다룹니다.

문제점: 컨텍스트 비대화 (Context Bloat)

F1 Analyst Pro의 일반적인 레이스 주말에는 FP1, FP2, FP3, Qualifying(예선), Race(결승) 등 여러 세션에 걸친 데이터가 존재합니다. 각 세션에는 22명의 드라이버에 대한 랩 바이 랩(lap-by-lap) 데이터—컴파운드(compound), 랩 타임(lap time), 순위(position), 스틴트(stint), 섹터 타임(sector times), 타이어 수명(tyre life)—가 포함되어 있습니다. 여기에 더해 예선 결과, 레이스 결과, 스틴트 요약, 피트 스톱 분석, 주요 순간, 레이스 사고, 그리고 기자들의 노트까지 포함됩니다.

모든 쿼리마다 이 모든 데이터를 LLM에 전송하면 두 가지 문제가 발생합니다:

  1. 비용 폭증. 레이스 주말 전체에 대한 전체 컨텍스트 덤프는 쿼리당 쉽게 8,00012,000 토큰에 달합니다. Claude Sonnet 입력 비용이 $3/MTok일 때, 이는 쿼리당 $0.024$0.036에 해당하며, 한 명의 사용자에게는 괜찮을지 몰라도 많은 사용자에게는 고통스러운 비용입니다.

  2. 품질 저하. 컨텍스트에 관련 없는 정보가 포함되어 있으면 LLM의 성능이 떨어집니다. 예선에 관한 질문에는 레이스 스틴트 요약이 필요하지 않으며, 이를 포함하는 것은 모델이 중요한 것에 집중하지 못하게 방해하는 노이즈를 추가하는 셈입니다.

해결책은 상호 보완적인 두 가지 패턴인 **조건부 컨텍스트 (conditional context)**와 **사전 생성 (pre-generation)**입니다.

패턴 1: 의도 탐지(Intent Detection)를 통한 조건부 컨텍스트

항상 전체 컨텍스트를 구축하는 대신, 사용자가 실제로 무엇에 대해 묻고 있는지 탐지하여 관련 데이터만 가져옵니다.

의도 탐지 (Intent Detection)

import unicodedata

def _detect_intents(prompt: str) -> dict:
...

조건부 컨텍스트 구축 (Conditional Context Building)

각 의도(intent)는 특정 SQL 쿼리를 트리거합니다. 결정적으로, 비용이 많이 드는 분석(undercut/overcut, 주요 순간(key moments), 레이스 사고(race incidents))은 실제로 관련이 있는 경우에만 가져옵니다:

def build_context(self, prompt: str, gp_name: str, year: int) -> str:
    intents = _detect_intents(prompt)
    context = ""
...

영향 (The Impact)

_"¿Cuál fue la pole?"_와 같은 쿼리는 과거에 레이스 스틴트(race stints), 피트 스톱 분석(pit stop analysis), 주요 순간(key moments), FP 랩(FP laps) 등을 모두 불러왔으며, 이는 모두 무관한 정보였습니다. 의도 탐지(intent detection)를 사용하면 예선(qualifying) 결과만 가져옵니다: 동일한 질문에 대해 약 400 토큰 vs 약 6,000 토큰을 사용합니다. 이는 예선 관련 쿼리에 대해 입력 토큰(input tokens)을 15배 절감한 것입니다.

일반적인 레이스 쿼리(load_all=False, wants_race=True)의 경우, 컨텍스트(context)가 여전히 상당하지만 스틴트(stints), 언더컷 분석(undercut analysis), 주요 순간(key moments), 사고(incidents) 등 모든 레이스 질문에 직접적으로 관련된 내용에 집중됩니다.

패턴 2: 차트를 위한 사전 생성 (Pre-Generation for Charts)

사용자가 차트를 요청할 때 사용하는 표준적인 접근 방식은 LLM이 플로팅(plotting) 코드를 생성하게 하고, 이를 클라이언트에 반환하여 실행한 뒤 결과를 표시하는 것입니다. 이는 합리적으로 들리지만 실제로는 세 가지 문제가 있습니다:

  1. 토큰 비용이 발생합니다. matplotlib 또는 Plotly 코드 조각은 50~150줄에 달하며, 이 모든 것이 출력 창에 나타납니다.
  2. 신뢰할 수 없습니다. LLM이 API 호출을 환각(hallucinate)하거나, 지원 중단된(deprecated) 메서드를 사용하거나, 조용히 실패하는 코드를 생성할 수 있습니다.
  3. 속도가 느립니다. 사용자는 무언가를 보기 전에 전체 코드 생성이 완료될 때까지 기다려야 합니다.

대안은 API 호출 전에 실제 데이터베이스 데이터로부터 차트를 생성한 다음, LLM에게 해당 차트가 존재함을 알려주는 것입니다.

구현 (Implementation)

def send_message(self, prompt: str, gp_name: str, year: int) -> dict:
    intents = _detect_intents(prompt)

...

예선 세그먼트 상세 (The Qualifying Segment Detail)

강조할 만한 한 가지 개선 사항은 F1의 예선 텔레메트리(qualifying telemetry)에는 세 가지 세그먼트(Q1, Q2, Q3)가 있다는 점입니다. Q1에서 탈락한 드라이버는 Q3에서 주행한 적이 없으므로, 해당 드라이버의 "Q에서의 최고 랩 타임"을 Q3 드라이버의 최고 랩 타임과 비교하는 것은 의미가 없습니다.

생성 전 (pre-generation) 단계에서는 프롬프트에서 해당 세그먼트를 감지하여 이를 처리합니다:

def _detect_qualifying_segment(self, prompt: str) -> str | None:
    match = re.search(r'\b(q[123])\b', prompt.lower())
    return match.group(1).upper() if match else None

그리고 plot_telemetry_trace 내에서는 다음과 같이 처리됩니다:

if qualifying_segment:
    stint_map = {"Q1": 1, "Q2": 2, "Q3": 3}
    seg_laps = drv_laps[drv_laps["Stint"] == stint_map[qualifying_segment]]
...

영향 (The Impact)

생성 전 (pre-generation) 패턴을 사용하면 LLM 출력에서 차트 코드를 완전히 제거할 수 있습니다. 이전에는 약 800개의 출력 토큰(코드 + 설명)이 소모되었던 텔레메트리 (telemetry) 응답이 이제는 약 200개의 토큰(설명만 포함)으로 줄어듭니다. 차트의 경우, 이는 출력 토큰의 75% 감소를 의미합니다.

더 중요한 점은, 차트가 실제 데이터를 사용하도록 보장된다는 것입니다. 환각 (hallucination)된 드라이버 코드, 더 이상 사용되지 않는 (deprecated) FastF1 API 호출, 혹은 조용한 실패 (silent failures)가 발생하지 않습니다.

두 패턴의 결합 (Combining Both Patterns)

두 패턴은 자연스럽게 결합됩니다. _"Q2에서 COL 대 GAS의 텔레메트리를 보여줘"_와 같은 쿼리가 들어왔을 때 발생하는 과정은 다음과 같습니다:

  1. 의도 감지 (Intent detection)wants_telemetry=True, wants_qualy=True
  2. 컨텍스트 구축 (Context building) → 예선 (qualifying) 결과만 가져옴 (~400 토큰)
  3. 생성 전 (Pre-generation) → FastF1에서 직접 COL과 GAS를 위한 Q2 필터링된 텔레메트리 차트를 생성
  4. API 호출 (API call) → 예선 컨텍스트 + 차트 알림을 전송 (총 입력 ~600 토큰)
  5. 응답 (Response) → LLM이 차트를 참조하여 데이터를 분석 (~200 토큰 출력)

총합: 약 800 토큰. 이러한 패턴이 없다면 동일한 쿼리에 약 7,000 토큰(전체 컨텍스트 덤프 + 차트 코드 생성)이 사용되었을 것입니다.

다루지 않는 내용 (What This Doesn't Cover)

이 패턴들은 구조화된 데이터베이스 기반의 RAG에 효과적입니다. 비구조화된 문서 RAG(PDF, 기사, 지식 베이스)의 경우, 임베딩 (embedding) 기반 검색을 사용하는 벡터 검색 (vector search)이 여전히 적절한 도구입니다. 핵심 통찰은 데이터 구조에 맞춰 검색 메커니즘을 매칭하는 것입니다. 즉, 표 형식의 데이터에는 키워드/의도 기반 방식을, 텍스트에는 의미론적 (semantic) 방식을 사용하는 것입니다.

전체 프로젝트 (The Full Project)

F1 Analyst Pro는 오픈 소스 (open source)입니다. 여기에 설명된 패턴들은 core/consultant_agent.pycore/chart_builder.py에 구현되어 있습니다.

🏎️ f1-analyst.streamlit.app
📦 github.com/luc45hn/f1-analyst-pro

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0