늦게 로드하고 적게 로드하기: 대화 기록을 위한 적시(Just-in-time) 컨텍스트
요약
에이전트의 대화 기록 관리 시 비용 절감과 품질 향상을 위해 '적시(Just-in-time) 컨텍스트' 방식을 제안합니다. 모든 기록을 컨텍스트 윈도우에 넣는 대신, 압축된 핫 인덱스를 유지하며 필요한 시점에만 상세 데이터를 로드하는 계층적 구조를 다룹니다.
핵심 포인트
- 전체 컨텍스트를 매번 전송할 때 발생하는 비용 문제 해결
- 긴 컨텍스트로 인한 모델의 주의력 분산 및 품질 저하 방지
- 핫 인덱스(요약/임베딩)와 콜드 스토어(전체 기록)의 계층적 설계
- 가상 메모리 페이징 개념을 활용한 효율적인 컨텍스트 관리
대부분의 에이전트(agents)는 매 턴마다 과거의 모든 내용을 끌어옵니다. 더 나은 기본 방식은, 무엇이 말해졌는지에 대한 얇은 인덱스(index)를 활성화된 상태로 유지하고, 실제로 필요한 몇 개의 턴(turn)만을 요청 시점에 온전하게 가져오는 것입니다.
Code: github.com/NirajPandey05/jit_context
대부분의 에이전트가 메모리를 처리하는 방식에는 하나의 조용한 가정이 깔려 있습니다. 바로 컨텍스트(context)는 적은 것보다 많은 것이 더 안전하다는 것입니다. 모델이 무언가를 필요로 할 수도 있다면, 컨텍스트 윈도우(window)에 넣어두라는 것이죠. 대화가 길어짐에 따라 이전의 모든 턴이 매 새로운 요청마다 함께 전달되며, 우리는 모델이 중요한 부분을 찾아낼 것이라고 믿습니다.
이 가정은 두 가지 측면에서 무너집니다. 첫째는 비용(cost) 측면입니다. 에이전트 루프(agent loop)는 매 단계마다 전체 윈도우를 다시 전송하기 때문입니다. 백 개의 오래된 턴은 한 번만 비용을 지불하는 것이 아니라, 101번째, 102번째 턴, 그리고 그 이후의 모든 단계에서 계속 비용이 발생합니다. 둘째는 품질(quality) 측면입니다. 모델은 긴 윈도우를 균일하게 읽지 않기 때문입니다. 중간에 묻혀 있는 관련 사실들은 가중치가 낮게 매겨지며, 무관한 방대한 양의 데이터가 질문에 실제로 답하는 요소와 주의력(attention)을 두고 경쟁하게 됩니다. 일정 지점을 넘어서면, 더 큰 컨텍스트는 단순히 비용만 많이 드는 것이 아니라 더 나쁜 답변을 생성하게 됩니다.
따라서 흥미로운 질문은 "어떻게 더 많이 집어넣을 것인가?"가 아닙니다. "어떻게 중요한 과거의 턴 하나를 놓치지 않으면서, 윈도우를 작고 밀도 있게 유지할 것인가?"입니다. 이 포스트는 이 질문을 바탕으로 우리가 설계한 방식 — 특히 긴 대화 기록(conversation history) 사례에 특화된 설계 — 그리고 스스로를 객관적으로 검증하기 위해 사용한 벤치마크(benchmark)에 대해 다룹니다.
01 · 메커니즘: 콜드 스토어(cold store) 위의 핫 인덱스(hot index)
이 설계는 컴퓨터가 용량에 맞지 않는 메모리를 관리해 온 방식에서 직접 차용했습니다. 항상 존재하는 작고 빠른 계층(tier), 대량의 데이터를 보유하는 크고 느린 계층, 그리고 이들 사이에서 데이터를 이동시키는 규칙이 그것입니다. RAM과 디스크 사이의 가상 메모리 페이지(Virtual memory pages)처럼, 우리는 주소 공간(address space) 대신 *주의력(attention)*을 위해 컨텍스트 윈도우와 외부 스토어(external store) 사이에서 페이징(paging)을 수행합니다.
구체적으로는 두 가지 계층이 존재합니다. **콜드 스토어 (cold store)**는 모든 턴(turn)을 ID를 키로 하여 완전한 충실도(full fidelity)로 보관하며, 아무것도 버리지 않습니다. **핫 인덱스 (hot index)**는 턴당 하나의 압축된 항목을 보유합니다: 짧은 요약, 약간의 메타데이터(엔티티, 해당 턴이 결정을 기록했는지 여부 등), 그리고 해당 요약의 임베딩(embedding)입니다. 인덱스는 컨텍스트 윈도우(context window)에 영구적으로 유지할 수 있을 만큼 비용이 저렴하지만, 페이로드(payload)는 그렇지 않습니다.
이러한 영구성(permanence)은 보기보다 중요합니다. 인덱스(목차 역할)가 항상 존재하기 때문에, 모델은 세부 사항을 로드하지 않았더라도 무언가가 존재한다는 사실을 항상 인지할 수 있습니다. 예를 들어, "14번째 턴에서 배포 시점에 대한 결정이 있었다"라는 내용은 한 줄의 요약 형태로 계속 가시성을 유지합니다. 모든 검색 시스템(retrieval system)의 최악의 실패 모드는 관련 사실을 조용히 누락시켜 모델이 이를 요청해야 한다는 사실조차 모르게 만드는 것입니다. 항상 켜져 있는 인덱스는 이를 방지하는 보호 장치입니다.
02 · 검색(Retrieval): 유사도로 후보를 추린 후 모델이 선택하게 하기
새로운 턴이 도착하고 대화가 신경을 써야 할 만큼 길어지면, 어떤 과거 턴들을 다시 가열(reheat)할지 결정해야 합니다. 우리는 이를 두 단계로 수행합니다. 먼저 '저렴하고 광범위하게', 그다음 '정밀하고 좁게' 진행합니다.
첫째, 의미론적 후보 명단 (semantic shortlist) 단계입니다: 현재 요청을 임베딩(embed)하고, 모든 인덱스 항목을 유사도(similarity)에 따라 순위를 매긴 뒤 상위 12개를 뽑습니다. 이 과정은 빠르고 재현율(recall) 중심적이며, 모델 호출 비용이 들지 않습니다. 둘째, 모델 선택 (model picks) 단계입니다: 작고 빠른 모델이 요청과 그 12개의 후보 _요약본(summaries로, 전체 텍스트가 아닌 요약본을 봅니다)을 확인한 뒤, 실제 로드할 가치가 있는 몇 개의 턴 ID를 반환합니다. 유사도는 그럴듯한 후보를 찾아내고, 선택기(picker)는 임베딩이 할 수 없는 판단력을 적용합니다.
새로운 턴 ──▶ 의미론적 후보 명단 ──▶ 모델 선택 ──▶ 윈도우 조립
(query) 코사인 유사도 상위 12개 요약본 → 인덱스 + 선택된 턴
[ #12, #14 ] + 최근 N개 원문
...
최종적으로 전송되는 윈도우(window)는 네 가지 부분으로 구성됩니다: 시스템 프롬프트(system prompt), 범위가 지정된 인덱스(scoped index; 목차 자체가 무한히 커지는 것을 방지하기 위해 관련 내용과 결정 플래그가 지정된 라인으로 구성), 완전한 충실도로 검색된 소수의 턴(retrieved turns), 그리고 있는 그대로 유지된 가장 최근의 몇 개 턴입니다. 최근성(recency)은 비용이 들지 않으며 대개 관련성이 높기 때문입니다.
03 · 작동 예시: 9번째 턴에 있는 바늘
구체적인 예를 들어보겠습니다. 60개의 턴으로 이루어진 대화가 있습니다. 9번째 턴에서 어시스턴트가 _"결정(Decision): 배포 윈도우는 화요일 02:00 UTC입니다. 이를 확정합시다."_라고 말했습니다. 그 후 50개의 턴 동안 관련 없는 잡담이 이어집니다. 이제 사용자가 묻습니다: "다시 알려줘, 배포 윈도우를 무엇으로 결정했었지?"
단순한 최근성 윈도우(recency window) 방식은 마지막 4개의 턴(모두 잡담)만 유지하므로 답변은 완전히 사라집니다. 전체 컨텍스트(full context) 방식은 답변을 포함한 모든 것을 유지하지만, 60개의 턴에 대한 비용을 지불해야 하며 그중에서 바늘(needle)을 찾는 과정이 희석됩니다. 대신 JIT는 다음과 같이 수행합니다:
# 요청이 들어옴; 대화가 임계값을 초과함 → JIT 작동
query = "배포 윈도우를 무엇으로 결정했었지?"
...
바늘이 온전하게 검색되고, 답변은 정확하며, 윈도우 크기는 3분의 1 수준입니다. 핵심은 퍼센트가 아닙니다. JIT가 두 가지 베이스라인(baseline)을 동시에 이겼다는 점입니다. 즉, 해당 턴을 놓친 최근성 방식보다 더 정확하고, 60개를 모두 유지한 전체 컨텍스트 방식보다 훨씬 저렴합니다.
04 · 솔직한 부분: 작동하지 않은 결과를 포함한 삼자 비교
중요한 비교는 결코 "JIT vs. 거대한 윈도우"가 아닙니다. 각 베이스라인이 서로 다른 방식으로 실패하기 때문에 삼자 비교가 이루어져야 합니다:
| 접근 방식 (approach) | 정확도 (accuracy) | 토큰 (tokens) | 문제점 (the catch) |
|---|---|---|---|
| full (전체) | 높음 | 100% | 완전하지만 희석됨; 매 단계마다 비용 발생 |
| ... | |||
| 마지막 문제점이 바로 핵심이며, 이는 양날의 검과 같습니다. 검색 (retrieval)이 잘 이루어질 때, JIT는 작고 밀도 높은 윈도우 (window)를 제공하여 더 저렴하면서도 더 높은 품질을 보장합니다. 반면 검색이 실패할 경우, JIT는 전체 윈도우 (full window)라면 최소한 어딘가에는 유지했을 턴 (turn)을 제거해 버린 셈이 됩니다. 따라서 JIT는 검색 정밀도 (retrieval precision)가 특정 기준을 통과할 때만 승리하며, 그 기준 미만에서는 오히려 느린 누수 (slow leak)를 만드는 꼴이 됩니다. |
기록으로 남겨두는 부정적인 결과. 우리의 첫 번째 버전은 매 턴마다 인덱스 (index) 전체를 주입했습니다. 긴 대화의 경우 목차 (table of contents)가 너무 커져서, 이를 통해 절약하려던 턴 (turn)의 비용보다 더 많은 비용이 발생했습니다. 토큰 "절약" 수치는 음수를 기록했으며, 대화가 길어질수록 상황은 악화되었습니다. 해결책은 주입되는 인덱스의 범위를 쇼트리스트 (shortlist)와 결정 플래그 (decision-flagged)가 지정된 턴으로 제한하는 것이었습니다. 그 후에야 절약 수치가 양수로 전환되었고 대화 길이에 따라 증가했습니다. 벤치마크 (benchmark)는 바로 이러한 문제를 포착하기 위해 존재합니다. 승리만을 보여주는 평가 (eval)는 아무것도 측정하지 못하는 것입니다.
인덱스 범위 제한 후, 토큰 절약량 vs 대화 길이 (오프라인 하네스 (offline harness) 기준):
| 턴 (turns) | 20 | 40 | 80 | 160 | 320 |
|---|---|---|---|---|---|
| scoped index | 3% | 50% | 74% | 87% | 94% |
| full index (매 턴) | −30% | −35% | −38% | −40% | −41% |
05 · 제대로 증명하기: LoCoMo 벤치마크
합성 생성기 (synthetic generator)를 통한 자체 측정 결과는 냄새를 맡는 테스트 (smell test)일 뿐, 증명이 아닙니다. 누구나 믿을 수 있는 주장을 하려면, 다른 시스템들도 보고하는 공개 벤치마크 (public benchmark)에서 실행해야 합니다. 장기적인 대화 메모리 (conversational memory)를 위한 해당 벤치마크는 LoCoMo (Maharana et al., ACL 2024에서 발표, snap-research에서 공개)입니다.
LoCoMo는 JIT가 목표로 하는 환경, 즉 질문에 대한 답이 훨씬 이전에 언급되어 노이즈 속에 묻혀 있는 매우 길고 다중 세션의 대화에서 작동하도록 구축되었습니다. 그 규모 자체가 핵심입니다. 이 데이터셋은 총 10개의 긴 대화로 구성되어 있으며, 각각 평균 약 600턴(turns)과 약 26세션에 달하고, 전체 세트에는 대략 약 1,500개의 질문-답변 쌍이 주석 처리되어 있습니다. 짧은 컨텍스트만으로는 이를 견딜 수 없습니다.
이것을 단순히 하나의 점수가 아닌 진정으로 유용한 진단 도구로 만드는 것은 질문들이 명확히 구분되는 유형들로 구성되어 있으며, 이들이 검색 기반(retrieval-based) 설계를 매우 다르게 부하한다는 점입니다:
| 질문 유형 | 무엇을 묻는가 | 어떤 것을 강조하는가… |
|---|---|---|
| single-hop | 한 턴의 하나의 사실 | 순수 검색 — JIT의 최적 지점 |
| ... | ||
| 다음 표준 프로토콜에 따라, 결과가 발표된 수치와 비교 가능하도록 적대적(adversarial) 카테고리는 제외됩니다. 실행 자체는 아키텍처를 반영합니다: **쓰기 단계(write phase)**에서 전체 대화를 인덱스 및 콜드 스토어에 주입하고, 그런 다음 **읽기 단계(read phase)**에서 창(window)을 조립하여 각 질문에 답하고 이를 실제 모델에 전달합니다. 신뢰성을 위해 두 가지 점수 측정기가 함께 보고됩니다 — 참조 대비 단어 중복 F1과 의미적 정확도를 위한 LLM-as-judge — 그리고 결정적으로, 정확도는 카테고리별로 분해되어 보고됩니다. |
왜 카테고리별 분석이 신뢰를 얻는가. 하나의 종합적인 숫자는 이야기를 숨깁니다. LoCoMo에서 검색 우선(retrieval-first) 디자인이 보여주는 정직한 패턴은 강력한 single-hop 성능과 상대적으로 약한 시간적(temporal) 성능입니다 — 그리고 이 분해 분석은 왜 그런지 알려줍니다: 시간적 질문은 완벽한 증거 회상(올바른 턴을 가져왔음)을 할 수 있지만, 여전히 놓칠 수 있습니다. 왜냐하면 답변하려면 요약본이 제거했을 수 있는 순서화 작업이 필요하기 때문입니다. 이는 _검색하지 못한 경우_와 _검색했지만 사용하지 못한 경우_라는 구분할 가치가 있는 두 가지 실패 모드를 깔끔하게 분리하며, 막연한
네 가지 방식의 LoCoMo 비교는 이 아이디어를 다른 메모리 시스템 옆에 배치할 수 있는 숫자로 만드는 실험입니다. 규칙은 다음과 같습니다. 동일한 테스트 환경(harness)을 거치지 않은 다른 시스템의 수치는 절대 본인의 수치 옆에 놓아서는 안 되며, 대신 '보고된 수치로 직접 비교 불가'라고 표시해야 합니다. 서로 다른 평가 모델과 답변 매칭 방식 때문에 논문 간의 수치를 비교하는 것은 함정이 될 수 있습니다.
실제 수치
실제 임베딩(gemini-embedding-001)과 실제 답변 생성기/평가자(gemini-2.5-flash-lite)를 사용하여 총 10개 대화(1,540개의 비적대적 QA 쌍)에서 실행한 결과는 다음과 같습니다:
| 모드 | F1 | 평가자 (judge) | 재현율 (ret. recall) | 토큰 비용 |
|---|---|---|---|---|
| 전체(full) | 0.41 | 0.43 | — | 100% |
| ... |
이 모양 자체가 논문으로 구현된 가설입니다: JIT는 전체 컨텍스트 F1의 약 90%에 도달하지만, 토큰은 그 절반 정도만 사용하는 반면, 최신성(recency)은 답변이 가장 최근 턴에 거의 포함되지 않기 때문에 거의 0에 가까워집니다. JIT와 최신성 사이의 이 간극 — 동일한 토큰 예산 범위에서 극도로 다른 정확도 —이야말로 잘라내기(truncation)보다 인덱싱을 사용해야 한다는 전체 주장의 핵심입니다.
이 수치를 읽는 데 있어 한 가지 주의할 점이 있습니다. 절대적인 점수 자체가 낮은 이유는 답변 생성기/평가자가 작은 고속 모델이기 때문이며, 전체 컨텍스트 F1은 단지 0.41에 불과합니다. 세 가지 모드 모두 더 강력한 모델을 사용하면 상승할 것이므로, 신뢰할 수 있는 신호는 '상대적인' 격차(JIT ≈ 전체, 최신성 ≈ 0)이지, 고립된 상태에서의 90% 비율이 아닙니다 — 두 개의 작은 숫자로 이루어진 비율은 노이즈가 많습니다. 백분율보다는 구조에 초점을 맞추세요.
카테고리별 세부 분석은 위에서 약속한 진단 결과를 정확히 보여줍니다:
| category (jit) | F1 | judge | ret. recall |
|---|---|---|---|
| single-hop | 0.48 | 0.51 | 0.74 |
| ... | |||
| Single-hop이 가장 강력합니다. 이는 순수 검색 (pure retrieval)이며, 이 설계의 본거지입니다. Temporal(시간적) 요소가 핵심적인 단서입니다. 검색 재현율 (retrieval recall)은 0.69로 양호하지만 (올바른 턴들이 가져와졌음), 판정 정확도 (judge accuracy)는 0.10에 불과합니다. 이는 증거는 존재하지만, 요약 과정에서 순서가 누락되었음을 보여주는 증거입니다. Open-domain은 두 축 모두에서 가장 취약한데, 이는 분산된 추론 컨텍스트 (inferential context)를 유사도만으로 표면화하는 것이 진정으로 어렵기 때문입니다. 이 중 어느 것도 숨겨진 것이 아닙니다. 이는 다음에 무엇을 수정해야 할지에 대한 지도입니다. |
06 · 위치: 더 큰 패턴의 한 조각
"적시(Just-in-time) 컨텍스트"라는 문구는 우리가 만든 것이 아닙니다. 이는 Anthropic의 2025년 컨텍스트 엔지니어링 (context-engineering) 가이드라인에서 유래되었으며, 해당 가이드라인은 컨텍스트 윈도우(window) 내에는 가벼운 식별자(identifiers)를 유지하고, 런타임(runtime)에 이를 전체 콘텐츠로 해결(resolve)할 것을 주장했습니다. 이러한 더 넓은 패턴은 여러 곳에서 나타납니다: 활성화 시 로드되는 기술(skills), 핸들(handles)로 오프로드되는 대규모 도구 결과, 부모의 윈도우에서 무거운 하위 작업을 가져가는 하위 에이전트(sub-agents) 등이 그것입니다.
이들 대부분은 _미래 지향적(forward-looking)_입니다. 즉, 에이전트가 작업하면서 무엇을 로드할지 결정합니다. 여기서 설명하는 조각은 _회고적(retrospective)_입니다. 즉, 대화 자체의 과거를 인덱싱하고, 과거의 턴들이 관련성이 생길 때 다시 불러오는 것입니다. 긴 히스토리를 처리하는 일반적인 지름길이 압축(compaction) — 즉, 대화 내용을 요약하고 세부 사항을 버리는 것 — 인 반면, 여기서의 베팅은 그 반대입니다: 모든 턴을 콜드 스토리지(cold storage)에 온전하게 보관하고, 이를 통째로 가져오는 것입니다. 그래야 어떤 세부 사항이 중요해질지 아무도 모르는 시점에 만들어진 손실 압축 요약(lossy summary)으로 인해 아무것도 유실되지 않습니다. 압축이 아닌 선택; 전체 콘텐츠가 아닌 대화 기록입니다. 이것이 이 기술이 차지하는 영역입니다.
07 · 여전히 어려운 부분들
검색 정밀도(Retrieval precision)가 승부의 핵심입니다. 검색 실패는 모델이 종종 감지할 수 없는 조용한 품질 저하를 야기합니다. 상시 가동되는 인덱스(always-on index)가 최악의 상황을 완화해주기는 하지만, 이를 완전히 없애지는 못합니다.
인덱스 생성 시점에 요약하는 것은 정보 손실이 발생하며 시점이 너무 빠릅니다. 미래에 어떤 질문이 던져질지 알기도 전에 하나의 턴(turn)을 압축하게 되는데, 이는 실질적인 순환 의존성(circular dependency) 문제를 야기합니다. 전체 턴을 가져올 수 있도록 유지하는 것이 일종의 헤지(hedge)가 될 수는 있지만, 인덱스 요약 정보가 해당 데이터를 실제로 가져올지 여부를 결정하는 관문(gate) 역할을 하게 됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기