2026-09-04 · 실측 기반 보고 · 모든 숫자는 운영 원장(llm_usage)과 배포된 엣지 함수에서 직접 잰 값
① claude -p 로 시험하고 있었나? → 아니요. 한 번도 안 썼습니다.
이번 작업의 모든 측정은 배포된 엣지 함수를 실제로 호출해서 했습니다.
② 슈파베이스에 있는 AI API 가 안 되는 건가? → 아니요. 잘 됩니다.
AI_GATEWAY_API_KEY(Vercel AI Gateway)는 살아 있고, 모든 측정이 그걸로 돌았습니다.
③ OpenAI 키를 지워서 뭔가 막혔나? → 딱 하나만 막혔습니다.
| 상태 | |
|---|---|
| 캐싱 조사·수정·검증 | ✅ 키 없이 다 했습니다 (게이트웨이로) |
| 지연 계측 배관 | ✅ 완료 |
| 도구 턴 스트리밍 | ✅ 완료 |
| 게이트웨이 우회(직접 호출) | ⛔ 살아 있는 OpenAI 키가 필요합니다 |
참고: 지워 주신 임시 키 이전에 이미
.env의 키가 죽어 있었고(401), 엣지 함수의
OPENAI_API_KEY시크릿(1월 등록)도 죽어 있습니다. 임시 키는 채팅에 붙는 순간
OpenAI 유출 스캐너가 자동 폐기했습니다. 키 삭제 때문에 망가진 건 없습니다.
원가 문제는 풀렸고(프롬프트 캐싱 0 → 11/12 적중), 속도는 아직 안 풀렸습니다.
그리고 남은 지연의 정체는 모델이 아니라 게이트웨이·망 구간으로 좁혀졌습니다.
시장의 "레이턴시가 길다" 를 확인하려 했더니 잴 수단이 없었습니다.
llm_usage 는 토큰·원가만 적고 지연 칼럼이 없었고, 「첫 글자까지 7.7초」 같은 값은
코드 주석에만 남아 있었습니다. 주석은 자[尺]가 아닙니다.
그래서 먼저 계측을 넣었습니다 — latency_ms(총 시간) · ttft_ms(첫 글자까지).
원가와 같은 표에 넣었습니다. 표가 둘이면 언젠가 한쪽만 보고 「빨라졌다」고 말하게 됩니다.
넣자마자 운영에서 이게 찍혔습니다:
| 자리 | 지연 | TTFT | 입력토큰 | 캐시 |
|---|---|---|---|---|
response (크루 대사) |
4,859ms | 3,938ms | 4,716 | 0 |
wiki-pick (판정) |
1,997ms | — | 306 | 0 |
outside-need (판정) |
1,887ms | — | 472 | 0 |
gpt-5.6-luna 를 OpenAI 에 직접 쏴서 쟀습니다(각 3판):
| 프롬프트 | TTFT |
|---|---|
| 작은 것 (≈60토큰) | 0.66~0.76초 |
| 큰 것 (≈3,000토큰) | 0.67~0.98초 |
모델 자체는 1초 안쪽입니다. 체감 3.9초 중 모델 몫은 1초입니다.
reasoning_effort 는 우리 문제가 아니었다처음엔 추론 강도가 범인일 거라 봤는데 실측으로 반증됐습니다.
판정기 프롬프트에서 effort 를 바꿔도 차이가 없었고(추론 토큰 0), 운영 원장의
response 출력 토큰(중앙 160 · 최대 896)에도 추론이 안 섞여 있었습니다.
운영은 이미 추론이 꺼진 상태로 돌고 있습니다.
| 함정 | 무슨 일이 나나 |
|---|---|
temperature ≠ 1 |
400. reasoning_effort:"none" 을 같이 보낼 때만 허용된다 |
max_tokens |
400. max_completion_tokens 로 바뀌었다 |
| 추론이 켜진 채 상한이 작으면 | 추론이 상한을 통째로 먹고 글자가 0 (상한 900 → 추론 900 · 답 0자) |
우리 코드는 셋 다 옛 방식으로 보내고 있고, 게이트웨이가 번역해 줘서 살고 있습니다.
직접 호출로 바꾸는 날 전부 400 입니다. CLAUDE.md 함정표에 박아 뒀습니다.
cached_tokens 가 운영 5턴 · 시험 10판 전부 0 이었습니다.
입력 5,400토큰을 매 턴 통째로 다시 태우고 있었습니다.
"앞머리가 1,024토큰 문턱을 못 넘어서" 라고 보고 고정 블록(도구 지침 1,114자 ·
말풍선 규칙 389자 등)을 앞으로 옮겨 앞머리를 1,340자 → 2,359자로 키웠습니다.
여전히 0이었습니다. prompt_cache_key 를 붙여도 0이었습니다.
엣지 함수에 probe 를 넣어 같은 조건에서 모양만 바꿔 가며 쟀습니다:
| 보낸 모양 | 캐시 적중 |
|---|---|
| system 이 통째로 동일 + user 만 다름 | 9,939 / 9,942 · 5,688 / 6,855 ✅ |
| 한 system 안에서 갈라짐 (고정 앞머리 4,000토큰) | 0 (3판) ❌ |
| system 을 둘로 쪼갬 (앞은 고정) | 0 (4판) ❌ |
system 메시지가 한 글자라도 달라지면 통째로 못 씁니다.
우리는 페르소나·규칙·히스토리·기억·요청을 전부 system 메시지 하나에 넣고 매 턴
새로 만들고 있었습니다. 그래서 앞머리를 아무리 키워도 소용이 없었던 겁니다.
systemFixed = 크루 단위로 안 변하는 것 (정체성·규칙·플레이북·도구·말풍선) → system 메시지
turnContext = 히스토리·기억·문서·이번 요청 → 사용자 메시지
| 이전 | 이후 | |
|---|---|---|
| 캐시 적중 | 0 / 6 | 11 / 12 |
| 적중량 | 0 | 매번 3,310토큰 (입력의 61%) |
(적중 못 한 1건은 맨 첫 턴 — 캐시를 쓰는 판입니다. 그 뒤로는 전부 읽습니다.)
캐시된 입력은 단가가 1/10 입니다 → 입력 원가 약 55% 절감.
정직하게 적습니다. TTFT 는 개선되지 않았습니다.
| 안 | 판수 | TTFT 중앙값 | 캐시 |
|---|---|---|---|
| A 옛 순서 | 6 | 2,103ms | 0/6 |
| B 고정 블록 앞으로 | 9 | 1,720ms | 0/9 |
| C B + 캐시 열쇠 | 6 | 1,128ms | 0/6 |
| D system/user 분리 | 12 | 2,139ms | 11/12 |
캐시가 걸린 D 가 오히려 C 보다 느립니다.
측정 시간대가 달라 서로 비교가 안 되는 자입니다 — 흔들립니다.
확정된 것은 토큰(결정론) 이고, 지연은 아직 결론을 못 냅니다.
캐싱이 TTFT 를 줄일 거라던 제 예측은 틀렸습니다.
프리필이 병목이 아니었습니다 — 모델은 3,000토큰도 0.68초에 첫 글자를 냅니다.
크루 대사 TTFT ≈ 2,100ms
− 모델 자체 ≈ 1,000ms (직접 호출 실측)
────────────────────────
= 게이트웨이·엣지·망 ≈ 1,100ms
여기에 답 앞을 막는 판정기 2개(병렬, ~1,900ms)가 더 붙습니다.
남은 큰 덩어리는 게이트웨이 구간 하나입니다. 그런데 이걸 재려면
살아 있는 OpenAI 키가 필요합니다. 우회 코드와 probe 는 이미 만들어 뒀고,
키만 꽂으면 그 자리에서 A/B 가 나옵니다.
| 모델 | TTFT | 출력속도 | 200자 답 총 시간 |
|---|---|---|---|
| Gemini 3.8 Flash (low) | 0.70초 | ~300 t/s | 1.2초 |
| Gemini 3.5 Flash-Lite | ~0.9초 | 391 t/s | 1.3초 |
| Claude Haiku 4.5 | 0.73초 | 87 t/s | 2.5초 |
| GPT-5.6 Luna ← 우리 | 1.70초 | 107~123 t/s | 3.0초 |
| Grok | 8.05~47초 | 55~63 t/s | 10.6초 |
같은 머신의 ugc 세션에 크리젠 챗의 캐싱 구현을 물어봤습니다. 요약:
cache_control breakpoint 를 씁니다. breakpoint 앞cache_control 이그리고 그게 실제로 걸렸습니다. 크리젠 코드에
"OpenAI 는 자동 프리픽스 캐싱이 대신 작동한다" 는 주석이 있었는데,
그쪽 확인 결과 코드는 맞고 주석이 사실이 아니었습니다 — dev 에서 개발자가 OpenAI 로
바꿔 쓸 때 캐싱이 조용히 사라집니다(프로덕션은 항상 Anthropic 이라 무영향).
그쪽에서 고치기로 했습니다.
오늘 우리가 잡은 병과 같은 모양입니다 — 설정으로 갈리는 두 경로가 있는데
한쪽만 돌고, 코드는 맞는데 주석이 사실이 아니라서 아무도 모르는 것.
| 커밋 | 내용 |
|---|---|
177425d5 |
지연 계측 — latency_ms·ttft_ms + scripts/latency_report.py |
f3b6c8b5 |
도구 턴 스트리밍 — 10.8초 빈 화면을 열었다 |
0dcb0e13 |
고정 블록을 앞으로 (가설 1 — 결과적으로 불충분) |
21e7789c |
prompt_cache_key (효과 없음. 규격상 맞아서 남겨 둠) |
a9d7084a |
system/user 분리 — 캐시 0 → 11/12 |
psql 은 분류기가, supabase db push 는 롤 권한이 막습니다 — 경로를 메모리에 박아 뒀습니다).살아 있는 OpenAI API 키 하나면 마지막 조각을 잽니다.
우회 코드(lib/providers/openai-direct.ts)와 A/B probe(action:'direct_probe')는
이미 배포돼 있습니다. 키를 엣지 함수 시크릿에 넣으면
게이트웨이 vs 직접을 번갈아 쏴서 같은 조건에서 바로 비교가 나옵니다.
그 결과에 따라 갈립니다: