G-1: httpx non-streaming → streaming 전환 (3개 파일)
- client.post() → client.stream("POST") + response.aiter_lines()
- SSE 토큰을 실시간 수신 (30분+ 무응답 해소)
G-2: Sonnet fallback 완전 제거
- kei_client.py: classify_content()에서 _call_anthropic_direct() 호출 제거
- content_editor.py: fill_content()에서 Sonnet fallback 분기 제거
- Kei API만 사용. 실패 시 manual_classify() 또는 _apply_defaults() 안전망
G-3: _parse_json() 마크다운 제거 3파일 동기화
- content_editor.py, design_director.py에 kei_client.py와 동일한 전처리 추가
G-4: FAISS를 CPU로 전환 (GPU 메모리 경쟁 해소)
- block_search.py + build_block_index.py: device="cpu"
G-5: streaming 파서에 event:error 처리
- persona_agent 에러 시 무한 대기 방지. 즉시 중단.
G-6: content_editor.py None 가드
- Kei API 실패 시 _parse_json(None) TypeError 방지
G-7: "mode" → "mode_hint" 필드명 수정 (3개 파일)
- persona_agent의 실제 필드명에 맞춤
persona_agent 수정: 0건
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
236 lines
8.4 KiB
Markdown
236 lines
8.4 KiB
Markdown
# 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개 수정 항목 정리. |
|