에이전트가 모든 도구 결과를 다시 읽게 만드리기: TypeScript로 작은 컨텍스트 압축기 구축하기
요약
에이전트가 매 턴마다 전체 대화 기록을 다시 전송하는 문제로 인해 발생하는 높은 토큰 비용 문제를 다룹니다. 본 글에서는 TypeScript를 사용하여 컨텍스트 압축기(context compressor)를 직접 구축하는 방법을 안내하며, 이를 통해 에이전트의 효율성과 비용 절감 방안을 제시합니다.
핵심 포인트
- 에이전트 루프는 매 턴마다 전체 대화 내용을 전송하여 토큰 비용이 증가합니다.
- 컨텍스트 관리는 LLM 요약보다 규칙 기반으로 생략하는 것이 더 효과적일 수 있습니다.
- TypeScript를 이용해 컨텍스트 압축기를 직접 구현하고 테스트할 수 있습니다.
- 토큰 절감과 동시에 중요한 정보를 유지하는 것이 핵심 과제입니다.
에이전트 루프에는 조용한 습관이 있습니다.
모델을 호출할 때마다 전체 대화 내용을 다시 전송합니다.
시스템 프롬프트. 작업 내용. 모든 도구 호출. 모든 도구 결과.
따라서 20,000 토큰짜리 테스트 로그는 한 번만 지불되는 것이 아닙니다.
이것은 에이전트가 도착한 후의 매 턴마다 비용이 발생합니다.
제가 계속 생각하는 부분이 바로 이것입니다. 왜냐하면 최근 3주간의 에이전트 관련 뉴스가 이 점을 가리키고 있었기 때문입니다.
- 2026년 9월 17일, 연구원들은 An Empirical Study of Harness Design for Coding Agents를 발표했습니다. 176개의 매칭된 설정에 걸쳐, 그들은 컨텍스트 관리가 창(window)이 좁아질수록 더 중요하며, 대부분의 이점은 컨텍스트 오버플로우 실패를 방지하는 데서 온다는 것을 발견했습니다. LLM 요약 전에 규칙 기반으로 내용을 생략하는 것이 전반적인 효율성에서 가장 좋았습니다.
- 2026년 9월 21일, Strands Agents 팀은 Strands harness를 출시하고, 동일한 Claude 또는 GPT 모델을 사용한 여섯 가지 벤치마크에서 28% 낮은 토큰 비용을 보고했습니다. 그들의 말에 따르면, "우리의 기본 컨텍스트 관리가 토큰 효율성과 정확도를 크게 이끌었습니다": 약 1,500 토큰이 넘는 도구 결과는 잘리고, 압축은 창의 85% 이상에서 발생합니다. 이것은 독립적인 벤치마크가 아닌 그들의 벤치마크입니다.
- 2026년 9월 22일, CliffCompaction은 제한된 컨텍스트 하에서 최대 50% 낮은 비용을 보고했습니다. 그 규칙은 엄격합니다: 내용을 재구성하거나(rephrase) 하지 않고, 잘라내거나(truncate) 버리는 것만 하며, 압축을 압축하지 않습니다.
- 2026년 10월 3일, DeepSeek은 Harness v0.2.1-alpha.1을 발표하며, 자체 오픈소스의 모든 것이 플러그인인 하네스(harness)의 최신 빌드를 공개했습니다. Strands 벤치마크에서 DeepSeek Harness는 전반적으로 가장 토큰 효율적이었으며, 일반적으로 정확도도 가장 낮았습니다.
다른 팀들. 같은 레버리지 포인트.
이 마지막 점 또한 중요합니다.
토큰을 적게 사용하는 것은 쉽습니다. 중요한 한 줄의 내용을 잃지 않으면서 토큰을 적게 사용하는 것이 실제 작업입니다.
자, 이제 작은 버전을 만들어 보겠습니다.
이 과정을 마치면 다음 하나의 명령어를 실행하게 됩니다:
npx tsx compact.ts
그리고 네 가지 컨텍스트 정책(context policies) 하에서 스크립트로 재현된 에이전트가 모델에 실제로 전송하는 토큰을 보면서 돌아가는 것을 지켜보세요.
API 키도 필요 없고.
모델도 필요 없습니다.
단지 TypeScript만 있으면 됩니다.
솔직히 말씀드리자면: 이것은 제가 생각한 아이디어를 바탕으로 만든 작은 모델일 뿐이며, Strands나 DeepSeek의 구현체는 아닙니다. 도구(tools), 출력물(outputs), 토큰 카운터는 모킹(mocked)되었으며, 출력에 나오는 모든 숫자는 예시 입력값에서 가져온 것입니다.
목차
- 우리가 만들 것 (What We Are Building)
- 프로젝트 설정 (Project Setup)
- 1단계: 스크립트 에이전트 실행 (Step 1: A Scripted Agent Run)
- 2단계: 큰 도구 결과 처리 (Step 2: Cap Big Tool Results)
- 3단계: 오래된 결과 제거 (Step 3: Elide Old Results)
- 4단계: 보기 구성 (Step 4: Build the View)
- 5단계: 재현 및 카운트 (Step 5: Replay and Count)
- 문제가 발생하는 지점 (Where It Breaks Down)
- 더 큰 아이디어 (The Bigger Idea)
우리가 만들 것

에이전트는 전체 로그(full log)를 유지합니다. 이 로그는 절대 변하지 않습니다.
변하는 것은 보기(view), 즉 모델이 각 호출에서 읽게 되는 그 로그의 일부입니다.
전체 로그 (수정되지 않음)
↓
Cap: 큰 도구 결과가 미리 보기 + 참조로 바뀜
...
요약기(summarizer) 없이 네 가지 규칙만 적용합니다.
프로젝트 설정
mkdir tiny-context-compactor && cd tiny-context-compactor
npm init -y
npm install -D tsx typescript @types/node
다음 TypeScript 블록들을 순서대로 compact.ts에 저장하세요.
1단계: 스크립트 에이전트 실행
// compact.ts: 에이전트 루프를 위한 작은 컨텍스트 압축기입니다.
// 모든 것이 모킹되었습니다: 도구, 그 출력물, 그리고 토큰 카운터(토큰당 약 4자).
// 실행 내용, 32,000 토큰 창, 임계값 등은 예시 입력값입니다. 모델도, API 키도 없습니다.
...
이 실행 내용은 실패한 체크아웃 테스트를 수정하는 코딩 에이전트의 과정입니다. 총 아홉 번의 도구 호출이 이루어집니다.
그중 세 가지는 큰 결과물입니다: 각각 1,800줄 분량의 run_tests 로그 두 개와 900줄 분량의 CHANGELOG가 있습니다. 첫 번째 테스트 로그에서 중요한 것은 단지 한 줄뿐입니다.
토큰 카운터는 대략적인 휴리스틱(heuristic)이며, 토큰당 약 네 개의 문자를 사용합니다. 실제 토크나이저는 다르므로, 여기서의 모든 카운트는 상대적인 것으로 간주해야 합니다.
2단계: 큰 도구 결과 제한하기 (Cap Big Tool Results)
// Step 2: cap big tool results and keep the full text out of the context
const store = new Map<string, string>();
const CAP = 1500; // offload a tool result above this many tokens
...
가장 중요한 함수는 errorsFirst()입니다.
headTail()은 큰 결과의 시작과 끝을 유지하는데, 이는 대부분의 자르기(truncation) 작업이 하는 방식입니다.
하지만 실패한 테스트는 로그의 상단이나 하단에 예의 바르게 위치하지 않습니다.
errorsFirst()는 동일한 예산을 사용하며 FAIL 또는 ERROR처럼 보이는 모든 것을 미리보기(preview) 영역에 고정합니다. 이 전체 텍스트는 스토어(store)에 저장되고, 모델은 이를 retrieve()로 전달할 수 있는 참조(ref)를 얻습니다.
3단계: 오래된 결과 생략하기 (Elide Old Results)
// Step 3: elide old tool results with a rule, before any summarizing
function elided(m: Msg, now: number, keepRecent: number): string | null {
if (m.role !== "tool" || now - m.turn <= keepRecent) return null;
...
도구 결과가 두 턴보다 오래되면 한 줄로 요약됩니다. 즉, 어떤 도구인지, 몇 번째 턴인지, 얼마나 큰지, 그리고 어디에 있는지만 표시됩니다.
모델은 여전히 그 결과가 존재한다는 것을 알고 있습니다.
단지 그것을 위해 비용을 지불하는 것만 중단할 뿐입니다.
4단계: 보기 구성하기 (Build the View)
// Step 4: build the view for each model call. Truncate or drop. Never rewrite.
type Policy = { name: string; preview?: (t: string) => string; keepRecent?: number; window?: number };
const WINDOW = 32_000;
...
중요한 부분은 view()의 첫 번째 줄입니다.
뷰는 호출될 때마다 원본 로그로부터 재구성됩니다. 아무것도 제자리에서 편집되지 않기 때문에, 스텁(stub)이 다른 스텁으로부터 절대 만들어지지 않습니다.
그런 다음, 뷰가 여전히 창 크기의 85%를 초과하면 가장 오래된 고정되지 않은 메시지를 통째로 삭제합니다. 시스템 프롬프트, 작업 내용, 그리고 마지막 두 턴은 고정됩니다.
자르거나(Truncate) 삭제하거나(Drop). 절대 재작성하지 않습니다(Never rewrite).
// Step 5: 각 정책(policy)별로 동일한 실행을 재현하고 모델이 실제로 읽는 내용을 계산합니다.
const POLICIES: Policy[] = [
{ name: "naive" },
...
실행하기:
npx tsx compact.ts
제 실행 결과:
10 model calls, 9 tool results, window 32,000 tokens (예시 실행)
policy billed peak fits FAIL seen at call 3
...
제가 이 표를 읽는 방법은 다음과 같습니다.
naive는 10번의 호출에 걸쳐 총 290,208개의 입력 토큰을 읽지만, 기회조차 얻지 못합니다. CHANGELOG를 읽은 직후 첫 번째 테스트 로그가 위에 올라오면서 32,000 토큰 창(window)을 초과하는 것은 7번째 호출에서 발생합니다.
cap은 쉽게 적합합니다. 하지만 헤드와 테일 미리보기(head-and-tail preview) 기능이 첫 번째 테스트 로그의 FAIL 라인을 잘라냈습니다. 바로 다음 호출에서 모델은 왜 테스트가 실패했는지 알 수 없습니다.
cap+errors는 실행 전체에 걸쳐 cap보다 152 토큰을 더 사용하지만, 실패 라인은 돌아왔습니다.
full은 생략(elision)과 85% 드롭 규칙(drop rule)을 추가합니다. 이 기능은 실행 전체에 걸쳐 9,367개의 토큰을 읽으며, 최고치는 1,637입니다.
그리고 제가 좋아하는 세부 사항이 있습니다. 이번 실행에서는 85% 규칙이 한 번도 발동하지 않았습니다. Capping과 elision만으로 모든 보기(view)가 이미 작았기 때문입니다.
이는 harness 연구 결과와 일치합니다. 저렴한 규칙부터 적용하는 것이 좋습니다. 값비싼 장치는 안전망 역할을 합니다.
작동이 멈추는 지점 (Where It Breaks Down)
이 데모는 의도적으로 작습니다. 이 바깥에 무엇이 있는지 살펴보겠습니다.
-
Recoverable한 것이 recovered하다는 것과 같지 않습니다. 제
retrieve()도구는FAIL라인을 가져올 수 있습니다. 하네스 스터디(harness study)에 따르면, 생략된 콘텐츠를 recoverable하게 만드는 것은 "모델이 거의 사용하지 않는 장치를 추가하며 정확도 향상을 가져오지 않는다"고 합니다. 참조(ref)는 모델이 따르려고 생각할 때만 도움이 됩니다. 신호를 미리 보기(preview)에 넣어주세요. -
Regex는 무엇이 중요한지에 대한 추측입니다.
FAIL|ERROR는 이 테스트 러너(test runner)에서 작동합니다. 스택 트레이스(stack trace), 장애로 이어지는 경고, 또는status라는 이름의 JSON 필드는 일치하지 않을 것입니다. 각 도구는 자체적인 미리 보기 규칙을 가질 자격이 있습니다. -
압축은 프롬프트 캐시를 손상시킬 수 있습니다. 제공업체(Provider)들은 요청의 재사용된 접두사(prefix)를 캐싱합니다. 결과가 스텁(stub)으로 바뀌면, 그 이후의 모든 것이 변경되고 캐시는 다시 워밍업(warm up)해야 합니다. Strands는 프롬프트 캐싱과 컨텍스트 관리(context management)를 기본값으로 나란히 제공하며, 이 둘은 서로 충돌합니다. 매 턴마다가 아니라 안정적인 경계에서 생략하세요.
-
전체 메시지를 삭제하는 것에는 규칙이 있습니다. 실제 채팅 API는 각 도구 결과가 해당 도구 호출을 따르기를 기대합니다. 하나를 빠뜨리면 요청이 실패할 수 있습니다. 제 데모는 일반 텍스트(plain text)를 삭제합니다.
-
카운터는 가짜입니다. 토큰당 네 글자는 경험적 규칙(heuristic)입니다. 임계값(threshold)을 신뢰하기 전에 제공업체의 토큰 계산 기능을 사용하세요.
-
요약은 표류합니다. 저는 LLM 요약을 의도적으로 제외했습니다. CliffCompaction의 '재진술하지 않기'라는 규칙이 존재하는 이유는 요약본의 요약본이 점차 사실이 아니게 되기 때문입니다.
더 큰 아이디어 (The Bigger Idea)
Tool result
↓
Full log (the truth, never edited)
...
모델은 추론(reasoning)을 제공합니다.
도구는 증거(evidence)를 제공합니다.
로그는 진실(truth)을 제공합니다.
뷰(view)가 모델이 주의를 기울일 수 있는 것을 결정합니다.
그렇기 때문에 하네스 자체가 제품이 되고 있습니다. 동일한 모델을 사용하는 두 에이전트도 매우 다른 대화를 읽을 수 있습니다.
실패하는 라인을 잊어버린 저렴한 에이전트는 저렴하지 않습니다. 그것은 할인을 받은 오류입니다.
사용자의 컨텍스트 창(context window)은 예산입니다. 증거에 사용하세요.
Roster를 시도해 보세요 (Try Roster)
저는 이 아이디어를 바탕으로 Roster를 구축하고 있습니다: 실제 책임, 도구, 메모리, 스케줄을 가진 AI 직원과 매 단계마다 무엇을 봐야 할지 결정하는 하네스(harness).
만약 동일한 후속 조치, 인수인계, 대기 루프가 당신의 한 주를 계속 소모하고 있다면, 그것들을 AI 직원에게 맡겨보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기