- 루트의 IMPROVEMENT-PHASE-*.md, PHASE-*.md 등 45개 → docs/history/로 이동 - docs/block-tests/ 오래된 블록 테스트 HTML 삭제 (figma_to_html_agent로 대체) - docs/figma-analysis/, docs/figma-assets/, docs/figma-screenshots/ 정리 - docs/test-*.html 등 초기 테스트 파일 정리 - 참고 페이지/ 스크린샷 정리 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Phase G: Kei API 통신 정상화 — 실행 상세
Kei persona_agent와의 통신이 실패하는 근본 원인 해결. design_agent만 수정. persona_agent 코드 수정 0건. 원칙: Kei API만 사용. Sonnet fallback 금지. 하드코딩 금지. 회귀 금지.
문제 진단 요약
Kei API 호출 시 30분~1시간 무응답 후 BrokenResourceError로 끊김. 원인은 design_agent가 SSE 스트리밍 응답을 non-streaming 방식으로 받고 있어서, persona_agent의 전체 파이프라인(RAG + Opus planning + Sonnet 응답)이 끝날 때까지 대기.
G-1: httpx non-streaming → streaming 전환 (핵심)
문제
# 현재 코드 (3개 파일 동일 패턴)
response = await client.post(url, json={...}, timeout=None)
full_text = response.text # ← 전체 응답 완료까지 대기 (30분+)
persona_agent는 EventSourceResponse로 SSE 스트리밍 응답을 보냄.
httpx client.post()는 응답 body 전체가 끝날 때까지 버퍼링.
persona_agent 파이프라인(RAG→Opus→Sonnet)이 전부 끝나야 response.text 반환.
해결
httpx.AsyncClient.stream() 사용하여 SSE 토큰을 실시간 수신:
async with client.stream("POST", url, json={...}) as response:
async for line in response.aiter_lines():
# SSE 이벤트를 한 줄씩 실시간 처리
if line.startswith("event:"):
event_type = line[6:].strip()
elif line.startswith("data:"):
event_data = line[5:].strip()
if event_type == "token":
tokens.append(parse_token(event_data))
elif event_type == "done":
break
수정 파일 (3개)
src/kei_client.py—_call_kei_api(): 1단계 Kei 실장src/content_editor.py—_call_kei_editor(): 3단계 Kei 편집자src/design_director.py—_opus_block_recommendation(): 2단계 Opus 추천
충돌/회귀
- persona_agent 변경 없음 ✅
- SSE 이벤트 형식(event:/data:) 동일 — 파싱 로직만 실시간으로 전환
_extract_sse_text()함수를_stream_sse_text()로 대체 (streaming 버전)
G-2: Sonnet fallback 완전 제거
문제
사용자 요청: "무조건 늦더라도 persona agent에게 요청해서 답을 받아" 현재 코드: Kei API 실패 → Sonnet 직접 호출로 fallback
해결
kei_client.py:_call_anthropic_direct()호출 제거. Kei API 실패 시 에러 반환 또는 재시도.content_editor.py: Sonnet fallback 제거. Kei API만 사용.- Kei API 실패 시: 로그에 에러 기록 + SSE로 사용자에게 "Kei API 연결 실패" 알림
수정 파일
src/kei_client.py—classify_content()에서 Sonnet fallback 분기 제거src/content_editor.py—fill_content()에서 Sonnet fallback 분기 제거
충돌/회귀
_call_anthropic_direct()함수는 남겨도 됨 (호출만 안 함). 또는 삭제.- manual_classify()는 유지 (Kei API 자체가 완전히 불가능할 때의 최소 안전망)
G-3: _parse_json() 마크다운 제거 — 3개 파일 동기화
문제
kei_client.py에만 - 접두사 제거 로직이 있고,
content_editor.py와 design_director.py의 _parse_json()에는 없음.
Kei가 마크다운으로 JSON을 감싸면 3단계/2단계에서 파싱 실패.
해결
content_editor.py와 design_director.py의 _parse_json()에
kei_client.py와 동일한 마크다운 접두사 제거 전처리 추가.
수정 파일
src/content_editor.py—_parse_json()src/design_director.py—_parse_json()
충돌/회귀
- 정상 JSON은 기존과 동일하게 파싱 (원본 먼저 시도 → 클린 버전 시도)
- 마크다운 JSON만 추가로 파싱 가능해짐
G-4: FAISS를 CPU로 전환 (GPU 메모리 경쟁 해소)
문제
persona_agent가 GPU에 bge-m3 + reranker 로딩 (수 GB). design_agent의 FAISS도 bge-m3을 GPU에 로드하려고 시도 → OOM:
not enough memory: you tried to allocate 1024008192 bytes
해결
src/block_search.py에서 SentenceTransformer를 CPU로 강제:
_model = SentenceTransformer(EMBEDDING_MODEL, device="cpu")
scripts/build_block_index.py에서도 동일하게 CPU 지정.
46개 블록 검색은 CPU로도 충분히 빠름 (< 1초).
수정 파일
src/block_search.py—_ensure_loaded()에서 device="cpu"scripts/build_block_index.py— SentenceTransformer에 device="cpu"
충돌/회귀
- persona_agent 영향 없음 (GPU 독점 사용 가능)
- 검색 속도: GPU 0.03초 → CPU 0.1~0.3초 (46개 블록 기준, 무시할 수준)
G-5: streaming 파서에 event: error 처리 추가 (정밀 검토에서 발견)
문제
persona_agent가 에러 발생 시 event: error SSE 이벤트를 보냄.
현재 design_agent는 token과 done만 처리하고 error를 무시.
→ streaming 전환 후 persona_agent 에러 시 done을 기다리며 무한 대기.
해결
3개 파일의 streaming 파서에서 event: error 시 즉시 중단 + 에러 로그:
elif event_type == "error":
logger.warning(f"Kei API 에러: {data}")
break # 즉시 중단
또한 planning, planning_done, research_progress, warning 이벤트는 스킵 (기존 동작 유지).
수정 파일
src/kei_client.py,src/content_editor.py,src/design_director.py(G-1과 같은 위치)
G-6: content_editor.py None 가드 추가 (정밀 검토에서 발견)
문제
G-2에서 Sonnet fallback 제거 후, Kei API 실패 시 result_text = None.
_parse_json(None) 호출 → TypeError/AttributeError 발생.
except로 잡히지만 불필요한 예외.
해결
result_text = await _call_kei_editor(user_prompt)
if result_text is None:
logger.warning("Kei API 편집 실패. 기본값 적용.")
_apply_defaults(blocks)
continue # 다음 페이지로
수정 파일
src/content_editor.py—fill_content()내
G-7: "mode" → "mode_hint" 필드명 수정 (정밀 검토에서 발견)
문제
design_agent가 "mode": "chat"을 보내지만, persona_agent의 UnifiedMessageRequest는
mode_hint 필드를 사용. mode는 무시됨. 현재 우연히 chat으로 분류되지만 명시적이지 않음.
해결
3개 호출처에서 "mode": "chat" → "mode_hint": "chat" 변경.
수정 파일
src/kei_client.py—_call_kei_api()json bodysrc/content_editor.py—_call_kei_editor()json bodysrc/design_director.py—_opus_block_recommendation()json body
수정 파일 총괄
| 파일 | 항목 | 변경 성격 |
|---|---|---|
src/kei_client.py |
G-1, G-2, G-5, G-7 | httpx streaming + Sonnet fallback 제거 + error 처리 + mode_hint |
src/content_editor.py |
G-1, G-2, G-3, G-5, G-6, G-7 | httpx streaming + fallback 제거 + _parse_json 동기화 + error 처리 + None 가드 + mode_hint |
src/design_director.py |
G-1, G-3, G-5, G-7 | httpx streaming + _parse_json 동기화 + error 처리 + mode_hint |
src/block_search.py |
G-4 | SentenceTransformer device="cpu" |
scripts/build_block_index.py |
G-4 | SentenceTransformer device="cpu" |
persona_agent 수정: 0건
예상 효과
| 항목 | 현재 | 수정 후 |
|---|---|---|
| 1단계 Kei API 대기 | 30분+ (전체 완료 대기) → 실패 | 토큰 실시간 수신 → 정상 완료 |
| 3단계 Kei API 대기 | 6분+ → 간헐적 실패 | 토큰 실시간 수신 → 안정적 |
| 2단계 Opus 추천 | 6분 대기 | 토큰 실시간 수신 → 체감 빨라짐 |
| Sonnet fallback | Kei 실패 시 자동 전환 | 제거. Kei만 사용. |
| GPU OOM | FAISS + persona 경쟁 | FAISS CPU 전환 → 경쟁 없음 |
검증 체크리스트
- G-1: Kei API SSE 토큰이 실시간으로 수신되는지 (로그에 토큰 출력)
- G-1: 30분 무응답 없이 정상 완료
- G-2: Kei API 실패 시 Sonnet fallback이 발동하지 않는지
- G-3: content_editor, design_director에서 마크다운 JSON 파싱 성공
- G-4: FAISS 로드 시 GPU OOM 없음. CPU에서 정상 동작.
- G-5: persona_agent 에러 시 무한 대기 안 하고 즉시 중단
- G-6: Kei API 실패 시 TypeError 없이 _apply_defaults 적용
- G-7: persona_agent 로그에 mode_hint=chat 확인
- persona_agent 코드 변경 0건 확인
수정 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-25 | 초안. Kei API 통신 실패 원인 5개 진단 + 4개 수정 항목 정리. |
| 2026-03-25 | 정밀 검토로 G-5/G-6/G-7 추가. 총 7개 항목. |
| 2026-03-26 | Phase G 전체 구현 완료. 검증 통과. |
구현 완료 확인
| 항목 | 검증 결과 |
|---|---|
| G-1 | 3개 파일 모두 client.stream("POST") + response.aiter_lines() 전환. 기존 client.post() 0건. _stream_sse_tokens() 함수로 SSE 실시간 수신. |
| G-2 | kei_client.py: _call_anthropic_direct() 호출 제거. content_editor.py: Sonnet fallback 분기 제거. Sonnet 직접 호출 0건. |
| G-3 | content_editor.py + design_director.py의 _parse_json()에 마크다운 - 접두사 제거 전처리 추가. "원본 먼저 → 클린 버전" 순서 유지. kei_client.py와 동기화. |
| G-4 | block_search.py + build_block_index.py: SentenceTransformer(EMBEDDING_MODEL, device="cpu"). persona_agent GPU 독점 가능. |
| G-5 | 3개 파일의 _stream_sse_tokens()에서 event_type == "error" 시 로그 + break. 무한 대기 방지. |
| G-6 | content_editor.py: Kei API 실패(result_text is None) 시 _apply_defaults(blocks); continue. _parse_json(None) TypeError 방지. |
| G-7 | 3개 파일의 API 요청 body에서 "mode": "chat" → "mode_hint": "chat". persona_agent의 실제 필드명에 맞춤. |
파일별 구현 상세
kei_client.py:
classify_content(): Sonnet fallback 6줄 제거. Kei API 실패 → None 반환 → pipeline.py manual_classify() 안전망._call_kei_api():client.post()→client.stream("POST")+_stream_sse_tokens(response)."mode"→"mode_hint"._stream_sse_tokens(): 신규 함수.aiter_lines()로 SSE 실시간 수신. token/done/error 처리.
content_editor.py:
fill_content(): Sonnet fallback 7줄 제거.result_text is None시_apply_defaults(blocks); continue._call_kei_editor():client.post()→client.stream("POST")+_stream_sse_tokens(response)."mode"→"mode_hint"._stream_sse_tokens(): 신규 함수. kei_client.py와 동일 패턴._parse_json(): 마크다운-접두사 제거 전처리 추가. 원본 먼저 → 클린 버전.
design_director.py:
_opus_block_recommendation(): 인라인 SSE 파싱 30줄 →client.stream("POST")+_stream_sse_tokens(response)."mode"→"mode_hint"._stream_sse_tokens(): 신규 함수. 동일 패턴._parse_json(): 마크다운 제거 전처리 추가. 원본 먼저 → 클린 버전.import httpx모듈 레벨로 이동 (기존 지역 import →_stream_sse_tokens에서도 참조 가능).
block_search.py + build_block_index.py:
SentenceTransformer(EMBEDDING_MODEL)→SentenceTransformer(EMBEDDING_MODEL, device="cpu")