본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 20. 17:09

내 앱에 AI 채팅을 추가하려다 컨텍스트 토큰(Context Tokens)의 벽에 부딪힌 이야기

요약

사이드 프로젝트에 AI 채팅을 구현하며 겪은 컨텍스트 윈도우 제한, 토큰 관리, 지연 시간 문제를 다룹니다. 대화 기록 자르기, 요약, 슬라이딩 윈도우 등 다양한 시도 끝에 스트리밍과 컨텍스트 예산 전략을 통한 해결책을 제시합니다.

핵심 포인트

  • 대화 기록이 길어질수록 토큰 제한 초과 및 API 에러 발생 위험 증가
  • 단순 기록 자르기는 이전 맥락 상실, 요약 방식은 비용과 지연 시간 증가 초래
  • 스트리밍 응답을 통해 사용자 경험(UX) 측면의 지연 시간 문제 완화
  • 효율적인 컨텍스트 관리를 위한 토큰 예산 전략의 필요성

몇 달 전, 저는 제 사이드 프로젝트인 간단한 프로젝트 관리 대시보드에 AI 채팅 어시스턴트를 추가하기로 결정했습니다. 목표는 사용자가 "기한이 지난 작업이 뭐야?" 또는 "지난 팀 회의 내용을 요약해 줘."와 같은 질문을 할 수 있도록 하는 것이었습니다. 간단해 보이죠? 그냥 API 호출을 연결하기만 하면 끝나는 일처럼 말입니다.

스포일러: 그렇게 간단하지 않았습니다. "이건 쉬울 거야"라고 생각했던 제가 어떻게 컨텍스트 윈도우 (Context Window), 토큰 수학 (Token Math), 그리고 지연 시간 (Latency)과 씨름하게 되었는지, 그리고 결국 어떤 결론을 내렸는지에 대한 이야기입니다.

순진한 접근 방식 (The naïve approach)

저는 가장 뻔한 방법부터 시작했습니다. 전체 대화 기록과 사용자의 최신 메시지를 LLM API에 보내는 것이었죠. 처음 몇 번의 대화는 아주 잘 작동했습니다. 하지만 10~15개의 메시지가 지나자 응답이 느려지거나, 앞뒤가 맞지 않거나, (더 심하게는) 제 컨텍스트가 토큰 제한을 초과하여 API가 400 에러를 던지기 시작했습니다.

저는 8k 토큰 제한이 있는 GPT-4를 사용하고 있었습니다. 어시스턴트의 각 메시지에는 전체 작업 설명, 회의록, 그리고 마크다운 (Markdown) 형식이 포함되어 있었습니다. 기록은 빠르게 늘어났습니다. 20번째 메시지에 도달했을 때, 대화 내용만으로 이미 7k 토큰에 육박했고, 시스템 프롬프트 (System Prompt)나 새로운 질의 (Query)를 위한 공간은 거의 남아있지 않았습니다.

제가 시도했던 것들:

  • 대화 기록 자르기 (Truncating history): 마지막 N개의 메시지만 유지하는 방식입니다. 속도 면에서는 효과적이었지만, 어시스턴트가 이전 컨텍스트에 대한 기억을 잃게 만들었습니다. "마감일에 대해 뭐라고 했었지?"라고 물으면 멍한 대답만 돌아왔습니다.
  • 요약 (Summarization): 매 5개의 메시지마다 다른 LLM에게 지금까지의 대화를 요약하도록 요청하고, 그 요약본을 하나의 메시지로 주입하는 방식입니다. 이는 기억력에는 도움이 되었지만, API 비용과 지연 시간을 두 배로 늘렸습니다. 게다가 요약 과정에서 정보 손실이 발생하는 경우가 많았습니다.
  • 관련성 점수를 활용한 슬라이딩 윈도우 (Sliding window with relevance scoring): 각 과거 메시지를 현재 질의와의 유사도에 따라 점수를 매기고 상위 몇 개만 유지하는 방식을 시도했습니다. 이를 위해서는 임베딩 모델 (Embedding Model), 벡터 스토어 (Vector Store)가 필요했고, 아직은 필요하지 않은 복잡성이 추가되었습니다.

돌파구: 스트리밍 (Streaming) + 컨텍스트 윈도우 관리

저는 문제가 두 가지 부분으로 나뉜다는 것을 깨달았습니다. 바로 긴 응답을 생성할 때 발생하는 **지연 시간 (Latency)**과 대화 기록을 유지하기 위한 **토큰 예산 (Token Budget)**이었습니다. 저의 해결책은 단일 도구가 아니라, 제가 읽었던 두 가지 기술의 조합이었습니다. 바로 스트리밍 응답 (Streaming responses)과 다양한 유형의 콘텐츠에 우선순위를 두는 고정 크기 컨텍스트 윈도우 (Context window)였습니다.

스트리밍 응답 (Streaming responses)

전체 답변이 완료될 때까지 기다리는 대신, 청크 (Chunks)가 도착하는 대로 스트리밍했습니다. 이를 통해 전체 생성 시간은 동일하더라도 UX (사용자 경험)는 즉각적인 것처럼 느껴졌습니다. 사용자는 엔터를 누른 후 불과 몇백 밀리초 만에 문자가 나타나는 것을 볼 수 있었습니다. 저는 OpenAI Python SDK의 표준 stream=True 파라미터를 사용하였고, Server-Sent Events를 통해 청크를 전송했습니다.

컨텍스트 예산 전략 (The context budgeting strategy)

저는 컨텍스트를 세 가지 논리적 슬롯으로 나누었습니다:

  1. 시스템 프롬프트 (System prompt) (~500 토큰) – 고정되어 있으며 절대 변하지 않습니다.
  2. 동적 컨텍스트 (Dynamic context) (~2000 토큰) – 최근 작업, 작업 요약, 프로젝트 상태. 사용자의 행동이 무언가를 변경할 때마다 업데이트됩니다.
  3. 대화 기록 (Conversation history) (~4000 토큰) – 마지막 N번의 대화가 포함된 슬라이딩 윈도우 (Sliding window) 방식이지만, 한 가지 추가 규칙이 있습니다. 만약 사용자가 더 오래된 내용을 언급하면, 캐시 (Cache)에서 압축된 버전을 주입합니다.

핵심 통찰은 다음과 같습니다: 저는 전체 채팅 기록을 그대로 유지할 필요가 없었습니다. 현재 질문에 답하고 약 30개의 메시지로 구성된 세션 동안 일관성을 유지할 수 있을 만큼의 정보만 있으면 되었습니다. 세션이 종료되면 (예: 사용자가 패널을 닫으면), 다음 세션을 위해 기록을 유지하지 않았습니다.

다음은 제가 Python으로 컨텍스트 빌더 (Context builder)를 구현한 간략한 예시입니다:

def build_messages(user_query, session_state, system_prompt, history, max_tokens=6000):
    # 토큰 예약
    sys_tokens = count_tokens(system_prompt)
...

이것은 대략적인 코드이며—실제 계산에는 tiktoken을 사용했습니다—하지만 아이디어를 보여줍니다. 또한 폴백 (Fallback) 메커니즘도 추가했습니다. 만약 예산이 이전 메시지 하나를 담기에도 너무 작다면, 대신 짧은 요약을 주입합니다.

이 모든 것을 하나로 묶어준 도구

실제 API 호출을 위해 몇 가지 제공업체(Provider)를 시도해 보았습니다. 그중 InterWest AI가 잘 맞았는데, 스트리밍 (Streaming)을 기본적으로 지원하고 컨텍스트 제한 (Context limits)을 설정할 수 있었기 때문입니다. 저는 단순히 max_tokens를 4096으로 설정하고 클라이언트가 청킹 (Chunking)을 처리하도록 두었습니다. 특별한 SDK는 필요하지 않았습니다. 베이스 URL (Base URL)만 다를 뿐 OpenAI 클라이언트와 호환되었습니다.

하지만 솔직히 말해서, 동일한 접근 방식은 어떤 제공업체와도 작동합니다. 엔드포인트 (Endpoint)보다 기술 (Technique)이 더 중요합니다.

트레이드오프 (Trade-offs) 및 한계

  • 비용 (Cost): 여전히 토큰 (Token)당 비용을 지불하지만, 매번 전체 이력을 보내지 않기 때문에 페이로드 (Payload) 크기를 약 40% 줄였습니다. 이를 통해 비용과 지연 시간 (Latency)을 절약했습니다.
  • 메모리 (Memory): 매우 긴 세션 (50개 이상의 메시지)의 경우, 주요 사실을 캐싱 (Caching)하지 않으면 어시스턴트 (Assistant)가 초기 컨텍스트를 잊어버립니다. 이를 위해 간단한 벡터 스토어 (Vector store)를 추가했지만, 대부분의 사용 사례에는 과한 측면이 있습니다.
  • 세션 연속성 (Session continuity): 사용자가 다음 날 돌아오면 대화는 새로 시작됩니다. 어떤 앱들은 장기 기억 (Long-term memory)이 필요하지만, 이 방식은 이를 기본적으로 처리하지 못합니다.

다음에 다시 한다면 다르게 할 점

만약 오늘 다시 시작한다면, 수동으로 토큰을 계산하는 대신 aispy나 LangChain의 메모리 모듈 (Memory modules) 같은 라이브러리를 사용할 것입니다. 하지만 그러한 도구들은 그 자체의 복잡성과 의존성 (Dependencies)을 수반합니다. 단순한 채팅 기능을 구현하는 데에는 제가 만든 커스텀 예산 (Custom budget) 방식이 이해하기 더 쉽습니다.

또한 **속도 제한 (Rate limiting)**과 **재시도 로직 (Retry logic)**에 더 일찍 투자했을 것입니다. 스트리밍은 사용자 경험 (UX)에 도움이 되지만, 스트리밍 도중에 API 속도 제한에 걸리면 사용자 경험이 나빠지기 때문입니다.

진짜 교훈

앱에 AI를 추가하는 것은 단순히 API를 호출하는 것만이 아닙니다. 토큰 예산을 초과하지 않으면서 사용자에게 자연스럽게 느껴지도록 컨텍스트를 관리하는 것에 관한 문제입니다. 스트리밍은 속도에 대한 인식을 제공하지만, 좋은 컨텍스트 전략은 지능을 제공합니다.

궁금합니다. 여러분은 앱에서 대화 메모리를 어떻게 처리하시나요? 슬라이딩 윈도우 (Sliding windows), 요약 (Summarization), 아니면 완전히 다른 방식을 사용하시나요? 여러분에게 효과적이었던 방법이 무엇인지 듣고 싶습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0