← 목록으로

teamai 채팅 지연 조사 — 캐싱은 해결, 속도는 아직

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 적중), 속도는 아직 안 풀렸습니다.
그리고 남은 지연의 정체는 모델이 아니라 게이트웨이·망 구간으로 좁혀졌습니다.


1. 출발점 — 우리는 지연을 재고 있지 않았다

시장의 "레이턴시가 길다" 를 확인하려 했더니 잴 수단이 없었습니다.
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

2. 모델은 범인이 아니다

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 함정표에 박아 뒀습니다.


3. 진짜 원인 — 캐시가 한 번도 안 걸리고 있었다

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% 절감.


4. 🔴 안 된 것 — 속도는 안 빨라졌습니다

정직하게 적습니다. 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초에 첫 글자를 냅니다.


5. 그래서 남은 지연은 어디에 있나

크루 대사 TTFT   ≈ 2,100ms
  − 모델 자체     ≈ 1,000ms   (직접 호출 실측)
  ────────────────────────
  = 게이트웨이·엣지·망  ≈ 1,100ms

여기에 답 앞을 막는 판정기 2개(병렬, ~1,900ms)가 더 붙습니다.

남은 큰 덩어리는 게이트웨이 구간 하나입니다. 그런데 이걸 재려면
살아 있는 OpenAI 키가 필요합니다. 우회 코드와 probe 는 이미 만들어 뒀고,
키만 꽂으면 그 자리에서 A/B 가 나옵니다.


6. 참고 — 모델 속도 순위 (인터넷 공개 데이터)

모델 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초

7. 교차 확인 — 크리젠(Mastra) 세션

같은 머신의 ugc 세션에 크리젠 챗의 캐싱 구현을 물어봤습니다. 요약:

그리고 그게 실제로 걸렸습니다. 크리젠 코드에
"OpenAI 는 자동 프리픽스 캐싱이 대신 작동한다" 는 주석이 있었는데,
그쪽 확인 결과 코드는 맞고 주석이 사실이 아니었습니다 — dev 에서 개발자가 OpenAI 로
바꿔 쓸 때 캐싱이 조용히 사라집니다(프로덕션은 항상 Anthropic 이라 무영향).
그쪽에서 고치기로 했습니다.

오늘 우리가 잡은 병과 같은 모양입니다 — 설정으로 갈리는 두 경로가 있는데
한쪽만 돌고, 코드는 맞는데 주석이 사실이 아니라서 아무도 모르는 것.


8. 오늘 들어간 것

커밋 내용
177425d5 지연 계측 — latency_ms·ttft_ms + scripts/latency_report.py
f3b6c8b5 도구 턴 스트리밍 — 10.8초 빈 화면을 열었다
0dcb0e13 고정 블록을 앞으로 (가설 1 — 결과적으로 불충분)
21e7789c prompt_cache_key (효과 없음. 규격상 맞아서 남겨 둠)
a9d7084a system/user 분리 — 캐시 0 → 11/12

9. 다음 — 필요한 것 하나

살아 있는 OpenAI API 키 하나면 마지막 조각을 잽니다.

우회 코드(lib/providers/openai-direct.ts)와 A/B probe(action:'direct_probe')는
이미 배포돼 있습니다. 키를 엣지 함수 시크릿에 넣으면
게이트웨이 vs 직접을 번갈아 쏴서 같은 조건에서 바로 비교가 나옵니다.

그 결과에 따라 갈립니다: