본문 바로가기

이 포트폴리오의 원본은 https://sec.iruyo.com (심재빈) 입니다 · 출처 식별자 jbx-7f3a2e9b

← 기술 블로그

AI·RAG

Ollama 4096토큰 안에 기억 욱여넣기

Ollama 로컬 모델의 기본 컨텍스트 길이는 4096토큰이다. 프롬프트가 이 한도를 넘으면 시스템 메시지나 메모리 섹션이 경고 없이 잘린다. 이는 모델이 GPU·RAM 용량에 맞춰 컨텍스트 예산을 고정하여 초과 입력을 원천 차단하는 구조적 설계 때문이다.

로컬 모델로 에이전트를 실행하면 캐릭터가 어느 순간 자신이 누구인지 잊곤 한다. 에러는 나지 않는데, 그게 가장 문제다. 원인은 컨텍스트 윈도우 초과이며, 해결책은 메모리 예산을 모델이 아니라 하드웨어에 묶는 것이다. 이 글은 그 설계, Elastic Memory, 하나에 집중한다.

이 개념은 Glimi를 개발하는 과정에서 생겼다. 각자 성격과 기억, 관계를 지닌 AI 캐릭터들을 여러 개 동시에 운용하는 라이브러리고, 클라우드(Claude)와 로컬(Ollama·Grok) 환경을 하나의 fleet으로 섞어 쓴다. 전체 프로젝트의 구조 — 의존성 0의 멀티에이전트 커널과 그 위의 두 개 앱 구성 — 은 별도로 정리했다. 여기서는 그중 컨텍스트 예산에 관한 부분만 다룬다. 코드는 모두 공개(AGPL-3.0)되어 있어 그대로 참고할 수 있다.

증상: 캐릭터가 에러 없이 증발한다

Ollama의 기본 컨텍스트 크기는 4096토큰이다. 그런데 한 번의 에이전트 턴 프롬프트는 세 덩어리가 결합된다.

  • 시스템 프롬프트: 캐릭터 페르소나와 도구 레퍼런스. 실측 기준으로 persona 캐릭터는 약 2700토큰, 매니저 캐릭터는 약 5100토큰이다.
  • L0~L5 메모리 주입: 고정 기억, 관계, 에피소드 요약, 검색된 사실을 포함한다. scale 1.0 기준 약 1230토큰이다.
  • 최근 대화: 직전의 몇 턴이 여기에 포함된다.

이 셋을 더하면 수천 토큰이 된다. 매니저 캐릭터는 시스템 프롬프트만으로 이미 4096을 초과한다. 문제는 Ollama가 이 초과 상황을 막지 않는다는 점이다. num_ctx 값을 넘기면 앞부분부터 조용히 잘라낸다. 잘려나가는 앞부분에는 시스템과 메모리가 있다. 결국 캐릭터 정의와 기억이 먼저 사라지고, 모델은 겉보기엔 정상적으로 대답한다. 그러나 캐릭터는 이미 다른 인물처럼 변한다.

차라리 명확히 깨지는 것이 낫다. 에러가 발생하면 로그에 남고 추적이 가능하다. 하지만 아무 표시 없이 성격만 빠진 응답은 사용자가 단순히 “모델이 원래 좀 둔한가” 하며 넘기게 만든다. 특히 일정 길이 이상의 대화에서만 일관되게 무너진다면, 그 원인은 역시 컨텍스트 초과다.

통찰: 예산을 모델이 아니라 하드웨어에 묶는다

보통은 메모리를 “많이 넣을수록 좋다”고 여긴다. 하지만 로컬 환경에서는 그렇지 않다. 넣을 수 있는 양은 해당 모델이 실행되는 하드웨어의 컨텍스트 윈도우가 결정한다. 4GB 노트북과 24GB 워크스테이션은 같은 에이전트를 구동하더라도 기억을 담을 수 있는 범위가 다르다.

이 때문에 접근 방식을 거꾸로 세웠다. 메모리 예산을 num_ctx에서 역산하고, 주입량을 그 예산에 맞춰 조정한 뒤 토큰 추정 예산으로 best-effort 자르기만 수행한다. 한도를 넘기지 않도록 설계하지만 절대적인 보장은 아니다. 초과 시 잘려 나가는 구간이 시스템 프롬프트이므로, 그 순간 캐릭터가 사라진다.

설계: num_ctx 하나가 다이얼을 돌린다

핵심은 context_budget.py 한 파일이다. num_ctx 하나를 받아 세 가지를 정한다.

  1. num_ctx가 메모리 주입량(scale)을 결정한다. 8192를 기준점 1.0으로 두고 비율을 맞춘다.
DEFAULT_NUM_CTX = 8192          # scale 1.0 기준점
MEM_SCALE_MIN, MEM_SCALE_MAX = 0.25, 2.0

def memory_scale(num_ctx: int) -> float:
    raw = num_ctx / DEFAULT_NUM_CTX
    return max(MEM_SCALE_MIN, min(MEM_SCALE_MAX, raw))

4096이면 scale이 0.5로 내려가 메모리 주입이 절반으로 줄고, 16384면 약 2배로 늘어난다. 같은 에이전트라도 하드웨어 환경에 따라 기억을 더 많이, 혹은 덜 들 수 있다.

  1. 시스템 프롬프트는 줄일 수 없는 하드 바닥이다. persona 약 2700, 매니저 약 5100토큰 — 메모리와 대화를 모두 0으로 깎아도 이 아래로는 내려가지 않는다. 그래서 하드 floor를 2048로 둔다. 그 이하 num_ctx에서는 시스템 프롬프트 자체가 담기지 않아 사실상 동작하지 않으므로 그 지점에서 클램프한다.
HARD_FLOOR = 2048               # 이하 거부 — 시스템 프롬프트도 못 담음
RECOMMENDED_MIN = 4096          # persona 최소 (메모리 scale≈0.4)

시스템 프롬프트도 num_ctx 크기에 맞춰 단계를 바꾼다(compact / standard / full). 줄이는 것이 아니라, 윈도우에 맞는 수준을 선택하는 방식이다. 4096이면 도구 목록을 이름만 남기는 compact 단계를 골라 메모리 자리를 확보한다.

  1. 마지막 fit-check로 best-effort 초과를 방지한다. 예산을 역산해도 추정 오차가 존재하므로, 조립 마지막 단계에서 최근 대화를 오래된 것부터 잘라 예산 안에 들어맞게 한다. 최신 메시지를 우선 보존하고 예산이 채워지면 멈춘다.
def trim_recent_to_budget(recent, budget_tokens):
    kept, used = [], 0
    for msg in reversed(recent):        # 최신부터
        t = estimate_tokens(msg["message"]) + 6
        if used + t > budget_tokens and kept:
            break
        kept.append(msg); used += t
    kept.reverse()
    return kept

양보 순서는 명확하다. 컨텍스트가 빠듯하면 시스템 프롬프트는 유지한 채, 메모리 scale을 먼저 줄여 최근 대화의 최소 분량을 확보하고, 그래도 부족하면 오래된 대화를 순서대로 버린다. 캐릭터 정의가 끝까지 남는다.

토큰 추정에는 정확한 토크나이저를 쓰지 않는다. 한글은 토큰당 약 2.2자, ascii는 약 4자라는 휴리스틱을 적용해 다소 보수적으로 계산한다. 정확한 토크나이저는 무겁고 백엔드(Ollama·Claude·Grok)마다 다르기 때문에, 약간 넉넉히 추정한 뒤 fit-check로 맞추는 편이 실제로 더 안정적이다.

num_ctx 4096 · 8192 · 16384 예산 역산 + scale 결정 8192 = 1.0 기준 · 비례 스케일

조립 (윈도우를 넘지 않게)

시스템 프롬프트 하드 바닥 — 안 줄임 메모리 주입 scale로 먼저 양보 최근 대화 오래된 것부터 자름 출력 fit-check — 마지막 보정 최신 보존, 예산 채우면 멈춤 → 초과 최소화

양보 순서: 메모리 → 대화 → 시스템은 끝까지

검증: 같은 에이전트가 4GB에서도 24GB에서도 안 잘린다

결과는 단순하다. 동일한 캐릭터를 같은 코드로 4GB 노트북에서 실행하면 메모리를 적게 사용하면서도 시스템 프롬프트는 그대로 유지된 채 동작한다. 24GB 워크스테이션에서 띄우면 더 많은 기억을 품고 돌아가지만, 어느 쪽에서도 캐릭터가 조용히 잘려나가는 일은 없다. num_ctx 값이 어떻든 fit-check 단계에서 초과를 최소화하도록 설계되어 있기 때문이다.

여기서 짚을 부분이 하나 있다. Claude처럼 컨텍스트가 큰 백엔드는 이 시스템의 영향을 받지 않는다. scale은 1.0으로 고정되고 trim도 일어나지 않는다. 따라서 대용량 백엔드에서는 Elastic Memory가 기존 방식 그대로 통과하며, 메모리가 제한된 로컬 환경에서만 다이얼이 조정된다. 추가 비용 없이 동작하는 안전장치다.

차별: 트림은 다들 하지만, 하드웨어에 묶는 곳은 없다

히스토리를 컨텍스트 윈도우에 맞춰 잘라내는 일 자체는 흔하다. CrewAI·Letta·OpenAI Agents SDK·AutoGen·LangGraph 등 대부분의 프레임워크가 각자의 방식으로 이를 처리한다. 그런데 여기서 다르게 한 점이 있다.

  • 메모리 예산을 모델의 설정값이 아니라 하드웨어(컨텍스트 윈도우)에 맞춰 역산하는 방식.
  • 조립 과정에서 그 예산을 best-effort로 넘지 않도록 스케일링하는 방식.

이 두 가지를 함께 수행하는 프레임워크는 아직 보지 못했다. 로컬 런타임 쪽도 상황이 비슷하다. Ollama에서 “VRAM에 맞게 컨텍스트를 자동 조절해 달라”고 요청하는 기능은 여전히 미해결 상태다. 그래서 그 부분을 애플리케이션 레이어에서 직접 구현했다. 라이브러리에 의존하지 않고 num_ctx 하나로 예산을 역산해 끝까지 동작하게 만든 것, 그것이 Elastic Memory다.