문서 정리: Phase 히스토리 md를 docs/history/로 이동 + 오래된 테스트/에셋 정리
- 루트의 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>
This commit is contained in:
271
docs/history/IMPROVEMENT-PHASE-G.md
Normal file
271
docs/history/IMPROVEMENT-PHASE-G.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# 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 전환 (핵심)
|
||||
|
||||
### 문제
|
||||
```python
|
||||
# 현재 코드 (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 토큰을 실시간 수신:
|
||||
|
||||
```python
|
||||
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로 강제:
|
||||
|
||||
```python
|
||||
_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` 시 즉시 중단 + 에러 로그:
|
||||
|
||||
```python
|
||||
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로 잡히지만 불필요한 예외.
|
||||
|
||||
### 해결
|
||||
```python
|
||||
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 body
|
||||
- `src/content_editor.py` — `_call_kei_editor()` json body
|
||||
- `src/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")`
|
||||
Reference in New Issue
Block a user