
Claude Code의 체감 버그를 수치화하기: 3가지 모델 resume 비교 절차
요약
Claude Code 사용 중 발생하는 stall(정지)이나 환각 현상을 객관적인 수치로 측정하고 비교하는 절차를 설명합니다. 세션 JSONL 파일을 활용해 모델 간 차이를 데이터화하여 GitHub Issue 등에 공유하는 방법을 다룹니다.
핵심 포인트
- Claude Code 세션 JSONL 파일을 통한 정밀 분석 방법 제시
- 모델 간 비교를 위해 --resume 명령어를 활용한 동일 컨텍스트 전달
- thinking-only 턴 수, auto-inject 횟수 등 핵심 지표 집계
- 개인적 체감을 타인과 공유 가능한 객관적 데이터로 변환
Claude Code 세션에서 "뭔가 이상하다(stall(정지) 현상이 발생하거나, tool을 호출하지 않고 결론만 작성하는 등)"라고 느꼈을 때, 그 체감을 타인과 공유 가능한 숫자로 변환하기 위한 절차.
해야 할 일은 단 3가지뿐입니다.
- 세션 jsonl 경로(
~/.claude/projects/<cwd>/<session-uuid>.jsonl)를 기록한다. claude --resume <session-uuid> --model <다른 model>명령으로 동일한 context를 다른 model에 그대로 다시 전달한다.- 추가된 jsonl에서 차분을 추출하여 다음 항목을 기계적으로 집계한다: thinking-only 턴 수 / auto-inject 횟수 / thinking-only tokens / 도달까지 걸린 시간 / 실제 tool 실행 여부
이 3가지가 갖춰지면 "n=1의 개별 차이 아닌가?"라는 반론을 잠재운 상태로 GitHub Issue나 사내 Slack에 공유할 수 있습니다. 아래에 직접 실행 가능한 수준으로 기술하겠습니다.
최근 Claude Opus 4.8의 환각(hallucination)이나 stall에 대해 자신의 체감을 기사로 작성한 적이 있습니다. 글을 쓸 때마다 강력해지는 반론은 "n=1의 개별 차이 아닌가?"였습니다.
개별 차이가 아니라는 점은 Anthropic 공식 리포지토리의 issue 군을 통해 1차 정보로 확인할 수 있습니다(본문 끝에 목록을 배치합니다). 다만, 자신의 세션에서 발생한 일을 타인과 비교 가능한 숫자로 변환하는 방법을 알고 있다면, 개별 issue의 재현 테스트 제공이나 모델 전환 판단의 합리화에 도움이 될 것이라 생각했습니다.
본 글은 이를 위한 절차서입니다. 특정 model이 나쁘다고 고발하는 것이 목적이 아니라, "내 눈앞의 세션이 타인과 공유 가능한 형태로 정말 stall / 환각을 일으키고 있는가"를 측정하는 도구를 기술하고 있습니다.
Claude Code는 각 대화를 다음과 같이 구조화된 JSON Lines로 남깁니다.
~/.claude/projects/<cwd를 "/"에서 "-"로 치환하여 인코딩한 경로>/<session-uuid>.jsonl
예를 들어 C:\dev\github\private\my-work에서 작업하고 있다면 C--dev-github-private-my-work/ 하위에 있습니다.
이 jsonl은 각 행이 하나의 이벤트이며, assistant의 1턴은 다음과 같은 구조로 들어 있습니다.
{"message":{
"role":"assistant",
"model":"claude-opus-4-8",
...
stall이나 환각의 분석은 이 생(raw) jsonl을 읽는 것이 정석입니다. 래퍼(wrapper)를 거친 PTY 렌더링 후의 바이트열(Baked for 5m 같은 thinking 라벨)은 정보 밀도가 낮아 원인 특정에는 적합하지 않습니다.
Opus 4.8의 1M context 세션에서 도중에 다음과 같은 두 가지 증상이 병행되었습니다.
- reasoning-only stall: assistant 턴이
thinking블록만으로 반환되어,text도tool_use도 포함하지 않는 상태가 연속됨. Claude Code CLI는 "본문 없음"을 감지하여[Your previous response had no visible output. Please continue and produce a user-visible response.]를 user turn으로서 자동 주입(auto-inject)하지만, Opus 4.8은 그 회복 프롬프트에도 thinking-only로 응답함. 루프가 멈추지 않음. - 미실행된 완료 처리: tool 호출을 스킵하고, context 내의 단편적인 정보로부터 암산한 결론을 "함수 호출 결과"로서 제시함.
이 세션에서는 stall 총 32분, thinking token 누계 111,000, auto-inject 4회 발생했습니다. 1턴에서 output_tokens=64,000을 전부 thinking에 소모한 순간이 있었는데, 이는 API의 max_tokens 상한과 정확히 일치하므로, API 측에서 강제 종료되어 겨우 text 출력이 도달했다고 해석할 수 있습니다. 천장에 부딪히지 않는다면 이론상으로는 끝없이 계속될 것입니다.
도식화하면, Claude Code CLI의 "본문이 없을 경우 회복 프롬프트(recovery prompt)를 주입한다"라는 세이프티 넷(safety net)이, Opus 4.8의 thinking-only 응답을 계속해서 유발하는 3층 루프(3-layer loop)로 그려질 수 있습니다. 회복을 위한 메커니즘이 그대로 악화 장치로 작동하고 있는 구도입니다.
(이 부분은 이미 여러 GitHub Issue에 올라와 있는 증상이며, 본고의 주안점은 여기가 아닙니다.)
"n=1의 개체 차이"라는 반론을 잠재우기 위해, 동일한 세션(session)의 컨텍스트(context)를 계승한 채 모델(model)만 변경하는 절차를 기술합니다. 한 모델의 재현이 아니라, 여러 모델을 갖추어 출력의 차이(diff)를 보는 것이 포인트입니다.
- 새로운 세션을 3개 만들어서 동일한 프롬프트를 던지면, 컨텍스트가 다르기 때문에 비교할 수 없다.
- resume를 사용하면, 동일한 대화 이력·동일한 시스템 프롬프트(system prompt)·동일한 도구(tool) 응답 이력을 유지한 채, 다음 1턴만 다른 모델로 처리할 수 있다.
- Claude Code는
claude --resume <session-uuid> --model <model-id>
를 통해 jsonl의 뒤에 추가하는 방식으로 resume한다.
비교를 오염시키지 않기 위해, 실험 전에 jsonl을 복사해 둡니다.
cp ~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl \
./snapshot_before-experiment_$(date +%Y-%m-%dT%H-%M).jsonl
3 실험을 통해 동일한 프롬프트를 사용합니다. 차이의 원인을 모델 이외의 요소에서 찾지 않기 위해서입니다. 이번에 제가 사용한 것은 이것입니다.
<업무 고유의 검증 태스크에 대한 짧은 기술>.
고찰은 최소한으로 하고, 우선 1회의 쿼리(query) 또는 명령어를 실행하여 결과를 출력하세요.
마지막 문장인 "고찰은 최소한으로"는 thinking 억제의 실험 변수입니다. 이 문장이 있느냐 없느냐에 따라 동작이 달라지므로, 목적에 따라 포함 여부를 결정합니다. 포함하면 stall(멈춤 현상)은 방지할 수 있습니다. 제외하면 "순수한 상태"를 측정할 수 있습니다.
현재 세션을 그대로 사용하며, 위의 프롬프트를 1회 던집니다. 결과를 기다립니다.
cd <원래 session의 cwd>
claude --resume <session-uuid> --model claclaude-opus-4-7
--model에 사용하고자 하는 ID를 그대로 전달합니다. 피커(picker)에 나타나지 않는 모델이라도 claude-opus-4-7[1m]와 같이 직접 지정하면 통하는 경우가 있습니다.
실험 B가 끝나면 Ctrl+D로 빠져나와, 실험 C에서 --model claude-sonnet-5로 교체하여 동일한 작업을 수행합니다.
(동일한 session-uuid의 jsonl에 추가되므로, 나중에 차이를 추출할 수 있습니다.)
jsonl에서 기계적으로 집계할 수 있는 항목들입니다.
| 항목 | 집계 방법 |
|---|---|
| thinking-only 턴 수 | role=assistant에서 content가 thinking뿐인( text도 tool_use도 없는) 턴의 수 |
| auto-inject 횟수 | role=user의 content에 "no visible output"을 포함하는 행의 수 |
| thinking-only 토큰 | 위 thinking-only 턴의 usage.output_tokens 합계 |
| 도착까지의 초 | 프롬프트 투입 timestamp로부터, 첫 번째 text 또는 tool_use를 포함하는 assistant 턴까지의 경과 초 |
| 실제 태스크 완수 | 기대값과 일치하는 수치가 나왔는가 (정답을 사전에 알고 있다는 전제) |
| 실제로 tool을 실행했는가 | 목적의 tool(이번 경우에는 검증 대상 함수를 호출하는 Bash)이 tool_use로 나타났는가 |
| 이전 턴의 작화를 탐지했는가 | resume 직후 assistant의 첫 마디가, 직전 assistant 턴의 오류를 지적하고 있는가 |
Python이든 Node든 상관없습니다. 아래는 Python의 뼈대뿐입니다.
import json
from datetime import datetime
events = [json.loads(l) for l in open(jsonl_path, encoding='utf-8') if l.strip()]
...
동일한 세션 (context ~330k)을 3개의 모델로 실행한 결과입니다.
| 지표 | A: Opus 4.8 (원래 model) | B: Opus 4.7 (resume) | C: Sonnet 5 (resume) |
|---|---|---|---|
| 도달 시간 (seconds) | 208s | 7.8s | 8.6s |
| ... | 실제로 tool을 실행했는가 | ✗ (암산으로 위장) | ✓ |
| 이전 턴의 허구(hallucination)를 감지 | - | ✓ 명시적으로 지적 | ✗ 묵묵히 재실행 |
| 응답 푸터(footer)의 model 오기 | - | - | ✓ "Opus 4.8"이라고 오기 |
동일한 context에 동일한 프롬프트를 입력했음에도 불구하고, 도달 시간에서 27배, thinking-only tokens에서 32배의 차이가 발생했습니다. 표의 세로 스크롤로는 놓치기 쉬운 스케일 차이를 나란히 놓고 살펴보기 위한 도표입니다.
수치로 나타내면, 체감되는 여러 현상이 언어화됩니다.
도달 시간에서 27배 차이 (4.7과 4.8). "빠르다/느리다"의 문제가 아니라 27배 차이입니다.
thinking-only tokens에서 32배 차이. 동일한 context를 읽게 했음에도 소비되는 "보이지 않는 사고"의 양이 차원이 다릅니다.
- 실험 A의 4.8은 실제 태스크는 정답에 도달했으나, 실제로 목적한 tool을 호출하지 않고 context 파편으로부터 얻은 암산 결과를 함수 호출 결과로서 제시했습니다. 게다가 암산이 우연히 정답과 일치했기 때문에 사용자 입장에서는 알아채기 어렵습니다.
- 실험 B의 4.7은 resume 직후에 "이전 응답은 실제로 실행하지 않고 결론만 작성했습니다"라고 명시적으로 자기 수정을 했습니다. 이러한 동작(behaviour)은 다단계 리뷰 AI로서 실용적입니다.
- 실험 C의 Sonnet 5는 성실하게 tool을 다시 호출했지만, 응답 정형 부분의 "사용 모델"란을 context에 있는 "Opus 4.8"에 끌려가 잘못 기재했습니다.
숫자만 보면 "4.8은 기본값(default)에서 제외해야 한다"로 끝나겠지만, 그것은 이미 별도의 기사에서 고발했으므로, 본고에서는 측정 방법에 집중합니다.
아침 커피의 김이 모락모락 피어오르는 옆에서, 체감을 숫자로 치환해 나가는 작업을 조용히 이어가는, 그런 톤으로 작성하고 있습니다. 화려한 고발이 아니라, 수수한 계측의 축적이 본고의 취지입니다.
위의 절차는 Opus 4.8을 고발하기 위한 도구가 아니라, "지금의 세션이 정말로 이상한가"를 숫자로 만드는 도구입니다. 다음과 같은 상황에서 사용할 수 있습니다.
새로운 model이 나왔을 때: 기존 태스크의 session을 resume 하여 새 model에 흘려보내고, 구 model과의 차이점을 측정합니다.
thinking effort 설정을 바꿨을 때: 동일한 context에서 --thinking high / medium / off를 전환하며 비교합니다.
system prompt를 바꿨을 때: resume로는 불가능하지만, 동일한 프롬프트를 별도의 session에 던져 차이를 측정합니다 (context 부분은 통제(control)할 수 없으므로 전제로 염두에 두어야 합니다).
버그 리포트 제출: n=1의 체감이 아니라, 위의 측정 항목을 첨부한 재현 절차로서 작성하면 Anthropic 측의 재현 테스트에 직접 사용될 수 있습니다.
본고에서 다루는 증상은 모두 Anthropic의 공식 리포지토리(repo)에 선행 보고되어 있습니다. 방법론을 시도하여 자신의 사례를 뒷받침하고 싶은 분은, 아래 중 하나와 연결하여 댓글을 남기거나 독립적인 issue를 생성하는 것이 좋다고 생각합니다.
#63358 Opus 4.8 returns empty thinking blocks: API default가 display: "summarized"
→ display: "omitted"
사양 변경으로 인해 변경됨. thinking token은 과금되지만 JSON 내용은 비어 있음 - #63604 Opus 4.8이 잘못된 형식의 tool_use 블록을 반복적으로 생성하여 전체 응답이 폐기되는 현상(reasoning-only stall)의 직접적인 원인 설명으로 가장 가까움
- #64076 Claude 4.8 Opus가 실행 없이 도구 출력을 환각(hallucinating)하는 현상: 「미실행 상태로 완료 처리됨」에 해당
- #64325 Opus 4.8이 긴 작업 중에 보안 사고를 환각하고 증거를 조작하는 현상
- #63884 Opus 4.8이 병렬 작업이 끝나기 전에 결과를 환각하기 시작하는 현상
- #34218 Claude Code가 thinking 단계에서 무한히 대기(hang)하는 현상
Anthropic 공식은 Opus 4.8 출시 시점에 "Opus 4.7의 4배 성능으로, 작성한 코드의 결함을 놓치지 않는 honesty & reliability (정직성과 신뢰성) 모델"이라고 발표했습니다. 이 공식 설명과 위 이슈(issue) 그룹 및 본고의 실측 결과 사이의 괴리가 현재의 상황입니다.
- Claude Code의 세션은
~/.claude/projects/<cwd>/<uuid>.jsonl에 구조화된 로그(structured log)로 남습니다. - 동일한 세션을
claude --resume <uuid> --model <id>로 상속하면, 컨텍스트(context)를 고정한 채 모델만 변경하여 비교할 수 있습니다. - 측정 항목(thinking-only 턴 / auto-inject / thinking tokens / 도달 시간(着地秒) / 실행된 도구(tool) 유무 / 작화(hallucination) 탐지)을 jsonl에서 기계적으로 집계하면, 체감되는 버그를 수치화할 수 있습니다.
- 수치화하면 GitHub issue의 재현 데이터로서 통용될 수 있는 형태로 공유할 수 있으며, 모델 교체 판단도 개인차라는 반론을 잠재운 상태에서 내릴 수 있습니다.
작은 결심으로서, 앞으로는 체감 버그를 발견하면 "우선 수치화하기"까지를 하나의 세트로 묶어 기사나 issue로 피드백해 나가고자 합니다.
※ 헤더 이미지는 AI(이미지 생성)로 제작되었습니다.
※ 본문의 삽화도 AI(이미지 생성)로 제작되었습니다.
작성자: ishizakahiroshi
군마 북부에서 보호 고양이 2마리와 함께 사는 재택 엔지니어 (잡무 해결사)
X (업무 위탁·각종 상담은 이쪽으로):
백엔드·인프라·AI 연동 관련 업무 위탁 상담을 받고 있습니다. 풀 리모트(Full remote)입니다. 스팟(Spot)이나 주 2~3시간부터라도 환영하며, 다양한 프로젝트에 참여할 수 있다면 기쁘겠습니다. 다음과 같은 상담을 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기