Google이 출시한 Gemini 4 Argon을 이용해 TypeScript로 작은 Continuation Loop를 구축하기
요약
Google이 Gemini 4 Argon을 출시하며 긴 답변 처리를 위한 새로운 연속 실행 프로토콜을 도입했습니다. 이 프로토콜은 단일 요청의 토큰 제한(최대 65,536 토큰)을 넘어서면 `continuation_token`을 반환하여 개발자가 이를 재전송함으로써 모델이 작업을 계속 수행할 수 있게 합니다. 개발자는 이 과정을 루프 형태로 구현해야 하며, API가 제공하는 가이드라인을 정확히 따라야 함을 강조합니다.
핵심 포인트
- Gemini 4 Argon은 긴 답변 처리를 위한 연속 실행 프로토콜을 도입했습니다.
- 단일 요청의 토큰 제한 초과 시 `continuation_token`이 반환되어 루프 재개가 가능합니다.
- 개발자는 이 과정을 클라이언트 측에서 직접 구현해야 하며, API 가이드라인 준수가 필수입니다.
긴 답변 처리를 위한 새로운 모델 연속 실행 프로토콜
대부분의 모델 호출은 한 가지 방식으로 끝납니다. 요청을 보내고, 답변을 받고, 끝나는 식입니다.
하지만 긴 답변들은 이러한 패턴을 깨기 시작했습니다.
2026년 10월 13일, Google은 Gemini 4 Argon을 출시했습니다. 해당 발표 글에서는 "Gemini API를 통한 업계 최고 수준의 1M 통합 입력-출력 토큰 제한"을 언급하며 개발자들에게 "새로운 연속 실행 프로토콜(continuation protocol)"에 대해 숙지할 것을 요청했습니다. Gemini API 릴리스 노트에는 gemini-4-argon이 같은 날 일반 사용 가능(generally available)으로 등재되어 있습니다.
제가 계속 생각하는 부분은 바로 그 프로토콜입니다.
요청 하나가 전체 답변을 담고 있을 필요가 없어졌습니다. Google의 Argon 개발자 가이드에 따르면, 단일 요청은 최대 65,536 토큰(비스트리밍 직접 HTTP의 경우 32,768 토큰)으로 제한됩니다. 이 한계를 넘어서면 API는 finish_reason=CONTINUATION과 함께 "원래 프롬프트와 그때까지의 생성 상태에 대한 해시값"을 담고 있는 암호화된 continuation_token을 반환합니다. 개발자는 이를 다시 전송하면 모델이 계속해서 작업을 수행합니다.
동일한 가이드에서는 무엇을 하지 말아야 하는지에 대해 매우 명확하게 지적하고 있습니다:
부분적인 모델 출력물을
contents에 추가하거나, 모델에게 계속하도록 요청하는 후속 프롬프트를 보내지 마십시오.
또한 "에이전트 하네스(agent harnesses)에서 컨텍스트 압축을 유발하기 위해 prompt_token_count를 사용하지 마십시오."라고 명시하고 있습니다.
Google의 긴 디코드 페이지(최종 업데이트일 10월 13일)에서는 서버가 직접 64K 호핑을 처리해주는 이와 같은 점을 강조합니다: "total_input_tokens는 컨텍스트 크기가 아니라 모든 호프(hop)의 입력값 합계입니다."
같은 날, Vercel의 AI SDK는 PR #23263을 병합하면서 자동 연속 실행 기능을 갖춘 gemini-4-argon을 추가했습니다. 이 업데이트는 "컨텍스트 창(context-window) 검사가 합산된 입력 토큰을 사용하지 않도록" initialPromptTokenCount를 노출합니다.
서로 다른 팀들, 하지만 동일한 경고입니다.
긴 답변은 이제 루프가 됩니다. 토큰 계산이 그것을 알아야 합니다.
자, 그럼 그 루프의 클라이언트 측 아주 작은 버전을 만들어 봅시다.
마지막에는 하나의 명령어를 실행하게 될 겁니다:
npx tsx continuation.ts
그리고 컨티뉴에이션 루프가 생각만 하는 세그먼트를 거치고, 이유 없이 발동했을 컴팩션을 건너뛰며, 텍스트를 중복하지 않고 드롭된 스트림에서 살아남아, 호출자의 제한(cap)이 소진된 후 재개되는 것을 지켜보세요.
이것은 제가 구상한 아이디어의 작은 모델입니다. Gemini나 Google Gen AI SDK 내부 작동 방식과는 다릅니다. 이 모델은 가짜이며, 단어 하나가 하나의 토큰으로 계산되고, 40토큰 세그먼트 크기와 1,000토큰 창(window)은 데모에 맞추기 위한 예시 입력값입니다. Argon의 실제 수치는 위에 링크된 문서를 참고하세요.
목차
- 문제점 (The Problem)
- 1단계: 요청, 체크포인트 및 사용량 (Requests, Checkpoints, and Usage)
- 텍스트를 세그먼트로 디코드하는 가짜 모델 (A Fake Model That Decodes in Segments)
- 루프 (The Loop)
- 컨텍스트가 아닌 사용량을 계산하기 (Count the Context, Not the Bill)
- 사람들이 처음 작성하는 루프들 (The Loops People Write First)
- 실행해보기 (Run It)
- 문제가 발생하는 지점 (Where It Breaks Down)
- 더 큰 아이디어 (The Bigger Idea)
1. 문제점 (The Problem)
컨티뉴에이션 프로토콜은 하나의 답변을 여러 요청으로 바꿉니다.
이건 마치 배관 공사 같습니다. 여러분의 코드가 이전에는 무료로 얻던 네 가지 것을 망가뜨립니다.
요청은 동일하게 유지되어야 합니다. 토큰은 원래 프롬프트의 해시를 담고 있으며, 가이드라인은 매번 그 프롬프트를 다시 보내라고 말합니다.
텍스트는 안전하기 전에 도착할 수 있습니다. 스트림은 세그먼트 중간에 끊길 수 있습니다.
세그먼트는 모두 생각만 할 수도 있습니다. 빈 텍스트가 끝을 의미하지 않습니다.
사용량(Usage)은 이상한 방식으로 쌓입니다. 각 컨티뉴에이션은 프롬프트와 지금까지 디코드된 모든 것을 다시 보내기 때문에, 합산 입력 토큰이 컨텍스트보다 훨씬 빠르게 증가합니다.
이것들은 모델 버그가 아닙니다. 루프 버그입니다.
모델은 여러분에게 체크포인트를 제공합니다. 여러분의 루프가 무엇을 커밋된 것으로 간주할지 결정합니다.
2. 1단계: 요청, 체크포인트 및 사용량 (Requests, Checkpoints, and Usage)
다음 TypeScript 블록들을 순서대로 continuation.ts로 저장하세요.
// tiny-continuation-loop: 토큰에 대해 거짓말하지 않고 긴 디코딩을 완료합니다.
// 가짜 모델, 가짜 토큰 (단어 1개 = 토큰 1개). API 키 없음. 네트워크 연결 없음.
import { createHash } from "node:crypto";
...
Request는 가이드가 매번 다시 전송하라고 말하는 세 가지 요소입니다: contents, system instruction, 그리고 tools.
Checkpoint는 요청이 끝날 때 도착하는 것입니다: 텍스트, 종료 이유(finish reason), 어쩌면 토큰, 그리고 사용량(usage). 가이드에 따르면 토큰과 사용량은 "각 요청의 끝"에 돌아옵니다.
제 토큰은 해시가 포함된 일반 base64 형식입니다. 실제 토큰은 암호화되어 있습니다. 우리가 필요한 아이디어는 이 '해시' 자체입니다.
3. 단계 2: 분할로 디코딩하는 가짜 모델 (Fake Model)
// 각 요청은 최대 `perRequest` 토큰을 디코딩하며, 먼저 생각하고(thinking), 그 다음 텍스트를 생성합니다.
// 텍스트는 청크(chunk) 단위로 스트리밍됩니다. 체크포인트(토큰 + 사용량)는 끝에 도착합니다.
class FakeModel {
...
이 가짜 모델은 한 번에 최대 perRequest 토큰만큼, 먼저 생각하는 과정과 그 다음 텍스트를 디코딩합니다.
세 가지 규칙은 의도적으로 문서에서 차용되었습니다.
해시가 요청의 해시와 일치하지 않는 토큰은 400 오류를 받습니다. 이것이 "토큰이 원래 프롬프트를 인코딩한다"는 것의 대리 역할을 합니다.
프롬프트 카운트에는 pos가 포함되는데, 이는 모든 연속 디코딩(continuation)이 이미 디코딩된 내용을 다시 채우기(re-prefills)하기 때문입니다.
만약 호출자(caller)의 maxOutput이 먼저 소진되면, 종료 이유는 MAX_TOKENS가 되며 토큰은 여전히 반환됩니다.
dropNextStreamAfter()는 데모가 의도적으로 스트림을 종료할 수 있도록 존재합니다.
4. 단계 3: 루프 (The Loop)
이것은 파일에서 가장 중요한 함수입니다.
type Run = {
status: "done" | "raise-cap" | "failed";
parts: string[];
...
중요한 부분은 순서(sequence)입니다:
- 요청은 한 번만 동결합니다. 모든 세그먼트가 동일한 것을 전송합니다.
- 텍스트를 답변으로 보내지 않고
buffer로 스트리밍합니다. - 체크포인트가 도착했을 때만 버퍼를 커밋하고 사용량을 추가합니다.
- 토큰이 있는
CONTINUATION: 텍스트가 비어 있더라도 계속 진행합니다. - 토큰이 있는
MAX_TOKENS: 중지하고 호출자가 제한을 높일 수 있도록 토큰을 반환합니다. - 스트림 손실: 버퍼를 폐기하고 동일한 토큰부터 재시도합니다.
마지막 규칙이 제가 가장 좋아하는 부분입니다. 이전 토큰은 여전히 유효하므로 재시도는 마지막으로 커밋된 체크포인트부터 시작됩니다. 미커밋된 것은 결코 답변에 도달하지 않습니다.
prev는 호출자가 더 높은 제한으로 재개할 수 있게 합니다. Vercel의 PR은 동일한 움직임을 설명합니다: "재개를 위해 더 높은 maxOutputTokens와 함께 토큰을 반환합니다."
5. 단계 4: 청구서가 아닌 컨텍스트를 계산하기
두 개의 작은 함수입니다.
// 디코딩 후 실제로 컨텍스트 창에 존재하는 것.
const contextSize = (r: Run) => r.initialPrompt + r.billed.text + r.billed.thoughts;
const shouldCompact = (tokens: number, window: number) => tokens >= window * 0.8;
run.billed.prompt는 모든 요청의 입력 합계입니다. 이것이 지불하는 금액에 더 가깝습니다. 창(window)에 있는 것과는 다릅니다.
창에 들어있는 것은 한 번 보낸 프롬프트와 돌아온 생각(thoughts) 및 텍스트입니다.
가이드는 바로 이 목적을 위해 initial_prompt_token_count를 지적하며, 압축하기 전에 디코딩(decode)이 완료될 때까지 기다리라고 말합니다. 80% 임계값은 예시입니다.

6. 단계 5: 사람들이 가장 먼저 작성하는 루프들
지친 오후에 제가 직접 코딩하고 있는 두 개의 루프가 그려집니다.
// 사람들이 가장 먼저 작성하는 두 개의 루프.
async function naiveDecode(model: FakeModel, req: Request, maxOutput: number, mode: "ask-to-continue" | "keep-partials"): Promise<string> {
let r = req;
...
ask-to-continue는 가이드에서 하지 말라고 하는 일을 합니다. 부분 출력을 프롬프트에 붙여넣고 "Please continue."를 추가합니다. 이것은 요청을 변경하므로 토큰 매칭이 멈춥니다.
keep-partials는 청크를 답변으로 바로 스트리밍합니다. 스트림이 끊어지면, 동일한 토큰부터 재시도하고 해당 세그먼트를 다시 붙입니다.
둘 다 합리적으로 보입니다. 둘 다 긴 출력에서만 실패합니다.
다음은 데모들입니다:
const files = Array.from({ length: 150 }, (_, i) => `src/billing/file${i}.ts`).join(" ");
const req: Request = {
contents: `Plan a safe migration for these files: ${files}`,
...
프롬프트는 150개의 가짜 파일 경로 목록입니다. 답변은 제가 손으로 작성한 짧은 마이그레이션 계획입니다. 둘 다 예시 입력값입니다.
7. 단계 6: 실행하기
설치 및 실행:
npm install
npx tsx continuation.ts
예시 출력 (예시 입력값만):
=== Demo 1: the loop, through a thinking-only segment ===
seg 1 CONTINUATION prompt 163 thoughts 40 text 0 (thinking only, keep going)
seg 2 CONTINUATION prompt 203 thoughts 20 text 20
...
데모 1은 하나의 함정을 가진 정상적인 경로입니다.
세그먼트 1 전체가 생각(thinking)으로 이루어져 있습니다. 텍스트는 없습니다. 빈 텍스트를 완료로 간주하는 루프는 바로 그 자리에서 중단됩니다. 이 루프는 계속 진행하여 네 개의 세그먼트로 전체 답변을 얻습니다.
다음은 토큰 계산입니다. 합산된 프롬프트 토큰은 892개로, 1,000토큰 창의 예시 80% 선을 넘어섭니다. 실제 컨텍스트는 310개입니다. 여기서 압축하는 것은 아무것도 아닌 좋은 컨텍스트를 버립니다.
데모 2는 "please continue"입니다. 생각만 하는 세그먼트, 수정된 프롬프트가 있고 다음 요청이 거부됩니다. 텍스트는 없습니다.
데모 3은 끊어진 스트림입니다. naive 루프는 87개 토큰 답변에 대해 107개의 토큰으로 끝납니다. 실제 루프는 커밋되지 않은 20개 토큰을 폐기하고, 동일한 토큰부터 재시도하며, 한 번의 추가 요청으로 정확하게 일치합니다.
데모 4는 최대치(cap)입니다. 토큰이 100개일 때 루프가 raise-cap으로 중지되고 해당 토큰을 유지합니다. 제한(cap)이 200인 두 번째 호출은 이전에 중단된 지점부터 다시 시작합니다.
이것이 전체 기능입니다.
8. 문제점 (Where It Breaks Down)
가짜 시나리오가 너무 친절합니다. 실제 세그먼트는 수천 개의 토큰이며, 실제 실패는 "스트림 드롭(stream dropped)"으로 표시되지 않습니다. 사용자의 재시도 규칙에는 실제 오류 코드가 필요합니다.
재시도는 공짜가 아닙니다. 데모 3에서는 드롭된 세그먼트가 두 번 디코딩됩니다. 실제 드롭된 스트림이 어떻게 청구되는지는 모르니, 제 가짜 시나리오가 아닌 사용량 보고서를 확인하세요.
가짜 시나리오는 캐싱을 무시합니다. Google의 긴 디코드 문서는 반복되는 호프 입력(repeated hop input)에 대해 95%의 암묵적 캐시 할인(implicit cache discount)을 설명합니다. 여기서 892는 가격이 아니라 토큰 개수입니다.
시간 제한이 빠져 있습니다. 가이드에는 요청당 20분 제한이 명시되어 있습니다. 실제 루프는 타임아웃(timeouts), 취소(cancellation), 그리고 눈에 보이는 진행률을 필요로 합니다.
SDK가 이미 이 기능을 수행할 수 있습니다. Google Gen AI SDK는 기본적으로 continuation_token을 자동으로 따라가며, Vercel의 AI SDK도 이제 그렇게 합니다. 자체 재시도가 필요한 경우에만 직접 루프를 작성하세요. 어느 쪽이든, 사용 중인 하네스(harness)가 어떤 토큰 개수를 읽는지 확인하십시오.
지연된 커밋은 지연 시간(latency)을 발생시킵니다. 아직 커밋되지 않은 부분을 되돌릴 수만 있다면, 커밋 지점(checkpoint)에서 여전히 커밋할 수 있도록 화면으로 스트리밍할 수 있습니다.
9. 핵심 개념 (The Bigger Idea)
request (frozen)
└→ segment
├→ 버퍼로 스트림 입력
├→ 드롭됨? 버퍼 폐기, 동일 토큰 재시도
└→ 체크포인트 도착
├→ 버퍼 커밋, 사용량 추가
├→ CONTINUATION + 토큰: 다음 세그먼트
├→ MAX_TOKENS + 토큰: 최대치 증가(raise the cap), 재개
└→ STOP: 부분들을 결합한 후 컨텍스트 확인
context = 초기 프롬프트 + 생각(thoughts) + 텍스트
청구액 = 모든 요청의
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기