- 루트의 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>
190 lines
7.6 KiB
Markdown
190 lines
7.6 KiB
Markdown
# Phase Q 수정 계획: 정확한 문제 분석 + 정확한 해법
|
|
|
|
> 작성일: 2026-03-30
|
|
> 상태: 분석 완료, 수정 대기
|
|
> 근거: Phase Q 5차 테스트 결과 + Phase P/이전 run 비교 분석
|
|
|
|
---
|
|
|
|
## 1. 문제의 정확한 진단
|
|
|
|
### Phase Q에서 바꿔야 했던 것 vs 실제로 바꾼 것
|
|
|
|
| 구분 | 바꿔야 했던 것 | 실제로 바꾼 것 | 결과 |
|
|
|------|-------------|-------------|------|
|
|
| 블록 선택 | FAISS+Opus 환각 → 제약 기반 | ✅ 제대로 바꿈 | 블록 선택 개선 |
|
|
| 글자수 예산 | 없음 → 사전 계산 | ✅ 제대로 바꿈 | overflow 감소 |
|
|
| 텍스트 채우기 | **바꾸면 안 됐음** | ❌ fill_candidates → fill_content로 교체 | **텍스트 품질 파괴** |
|
|
| overflow 조정 | 피드백 루프 → 수학적 조정 | ✅ 글루 모델 추가 | 작동 |
|
|
| 품질 게이트 | 없음 → 비전 모델 | ✅ 추가 | 작동 |
|
|
|
|
**핵심 오류: 텍스트 채우기 방식을 바꿔서는 안 됐다.**
|
|
|
|
### Phase P의 텍스트 채우기 (잘 작동함)
|
|
|
|
```
|
|
fill_candidates(): topic 1개 + 후보 블록 3개 → Kei API 1회 호출
|
|
↓
|
|
Kei가 topic의 source_data를 보고 블록 슬롯에 맞게 풍부하게 채움
|
|
↓
|
|
결과: 604자 (DX vs BIM 상세 비교), 사례 2건, 출처 포함
|
|
```
|
|
|
|
**Phase P `step3_edited_variants.json` 실제 결과:**
|
|
- topic 2 (사례): 2건 모두 포함, 불릿 상세 (스마트건설방안 + 제7차 기본계획)
|
|
- topic 3 (핵심): DX vs BIM 8개 항목 비교, 604자
|
|
- topic 4 (용어): 3개 용어 풀 정의 + 출처 (국토교통부, 2020 / IBM, 2011)
|
|
- topic 5 (결론): 원문 그대로 ("BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서...")
|
|
|
|
### Phase Q의 텍스트 채우기 (파괴됨)
|
|
|
|
```
|
|
fill_content(): 전체 블록 5-6개를 한 번에 → Kei API 1회 호출
|
|
↓
|
|
Kei가 한 번에 5-6개 블록을 처리하느라 각 블록을 축약
|
|
↓
|
|
결과: topic당 30-50자 수준으로 과축약
|
|
```
|
|
|
|
**Phase Q `step3_fill_content.json` 실제 결과:**
|
|
- topic 2 (사례): 1건만 (제7차 기본계획 누락)
|
|
- topic 3 (핵심): "상위개념" 한 단어 수준 (604자 → ~20자)
|
|
- topic 4 (용어): 수식어 삭제, 출처 없음
|
|
- topic 5 (결론): 원문 보존 (이건 OK)
|
|
|
|
### 왜 이렇게 됐나
|
|
|
|
`fill_content()`는 원래 Phase O 이전부터 있던 함수로, **전체 슬라이드의 모든 블록을 한 번에 처리**한다.
|
|
한 번의 API 호출에 블록 5-6개의 슬롯 정보를 모두 담으니, 각 블록에 할당되는 응답 분량이 자연스럽게 줄어든다.
|
|
|
|
반면 `fill_candidates()`는 **topic 1개씩 개별 호출**이므로, Kei가 해당 topic에 집중하여 풍부한 텍스트를 생성한다.
|
|
|
|
**이건 프롬프트 문제가 아니라 호출 구조 문제.**
|
|
|
|
---
|
|
|
|
## 2. 정확한 해법
|
|
|
|
### 원칙
|
|
|
|
```
|
|
Phase Q가 개선한 것: 블록 선택 (FAISS → 제약 기반) ← 유지
|
|
Phase P에서 가져올 것: 텍스트 채우기 (topic별 개별 호출) ← 복원
|
|
합치면: 제약 기반 블록 선택 + topic별 풍부한 텍스트 채우기
|
|
```
|
|
|
|
### 수정 대상: pipeline.py의 Step 3
|
|
|
|
**현재 (Phase Q — 잘못된 방식):**
|
|
```python
|
|
# 전체 블록을 한 번에 fill_content() 호출
|
|
layout_concept = await fill_content(content, layout_concept, analysis)
|
|
```
|
|
|
|
**수정 (Phase P 방식 복원 + Phase Q 블록 선택 유지):**
|
|
```python
|
|
# topic별로 개별 호출 — Phase P의 fill_candidates() 방식
|
|
for topic in topics:
|
|
tid = topic.get("id")
|
|
block = selected_blocks.get(tid)
|
|
if not block:
|
|
continue
|
|
|
|
# Phase Q에서 선택된 단일 블록을 리스트로 감싸서 fill_candidates 호출
|
|
await fill_candidates(content, topic, [block], analysis)
|
|
```
|
|
|
|
### 변경 파일 + 범위
|
|
|
|
| 파일 | 변경 | 범위 |
|
|
|------|------|------|
|
|
| `src/pipeline.py` | Step 3에서 `fill_content()` → topic별 `fill_candidates()` 호출로 교체 | ~15줄 교체 |
|
|
| `src/content_editor.py` | `fill_candidates()`에 Phase Q 글자수 예산(`_char_budget`) 전달 추가 | ~5줄 추가 |
|
|
| `src/content_editor.py` | EDITOR_PROMPT 변경 **롤백** — Phase P 원본으로 복원 | 프롬프트 복원 |
|
|
|
|
### 건드리지 않는 것
|
|
|
|
| 파일 | 이유 |
|
|
|------|------|
|
|
| `src/block_selector.py` | Phase Q 블록 선택 — 잘 작동하고 있음 |
|
|
| `src/space_allocator.py` | 예산 계산 + 글루 모델 — 잘 작동하고 있음 |
|
|
| `src/kei_client.py` | Q-4 블록 선택 + Q-6 품질 게이트 — 잘 작동하고 있음 |
|
|
| `templates/catalog.yaml` | Phase Q 메타데이터 — 잘 작동하고 있음 |
|
|
| `personas/` | Kei persona — 절대 수정 금지 |
|
|
|
|
---
|
|
|
|
## 3. 구체적 수정 내용
|
|
|
|
### 3-A: pipeline.py Step 3 교체
|
|
|
|
```python
|
|
# 현재 (삭제 대상)
|
|
layout_concept = await fill_content(content, layout_concept, analysis)
|
|
|
|
# 수정 (Phase P 방식 복원)
|
|
from src.content_editor import fill_candidates
|
|
|
|
yield {"event": "progress", "data": "3/5 Kei 편집자가 텍스트를 정리 중..."}
|
|
|
|
for topic in topics:
|
|
tid = topic.get("id")
|
|
block = selected_blocks.get(tid)
|
|
if not block:
|
|
continue
|
|
|
|
# fill_candidates는 topic 1개 + 블록 리스트를 받으므로 [block]으로 감쌈
|
|
await fill_candidates(content, topic, [block], analysis)
|
|
|
|
logger.info(
|
|
f"[Q Step 3] topic {tid}: {block['type']} → "
|
|
f"data={'있음' if block.get('data') else '없음'}"
|
|
)
|
|
```
|
|
|
|
### 3-B: fill_candidates()에 Phase Q 예산 전달
|
|
|
|
`fill_candidates()`의 컨테이너 제약 전달 부분에 `_char_budget`도 포함:
|
|
|
|
```python
|
|
# fill_candidates() 내부 — 이미 _container_height_px 전달하는 부분에 추가
|
|
char_budget = block.get("_char_budget", {})
|
|
if char_budget:
|
|
section += (
|
|
f"\n ★ 글자수 예산 (하드 제약):"
|
|
f"\n 총 글자: {char_budget.get('total_chars', '제한 없음')}자"
|
|
f"\n 최대 항목: {char_budget.get('max_items', '제한 없음')}개"
|
|
f"\n 항목당 글자: {char_budget.get('chars_per_item', '제한 없음')}자"
|
|
)
|
|
```
|
|
|
|
### 3-C: EDITOR_PROMPT 롤백
|
|
|
|
Phase Q에서 5번 수정한 EDITOR_PROMPT를 **Phase P 원본 기반으로 복원**.
|
|
단, Phase Q의 핵심 규칙 2개만 추가:
|
|
1. "글자수 예산(★) 초과 금지"
|
|
2. "source_data가 있으면 그것을 우선 사용"
|
|
|
|
---
|
|
|
|
## 4. 기대 효과
|
|
|
|
| 지표 | Phase P (20점) | Phase Q 현재 | Phase Q 수정 후 (예상) |
|
|
|------|---------------|-------------|---------------------|
|
|
| 블록 선택 | 3종류, 유령 5개 | 5종류, 유령 0개 | 5종류, 유령 0개 (유지) |
|
|
| 텍스트 품질 | 풍부 (604자) | 축약 (~30자) | **풍부 (Phase P 수준 복원)** |
|
|
| overflow | 213px | 0~45px | 예산 제약으로 방지 |
|
|
| 사례 수 | 2건 | 1건 | **2건 (복원)** |
|
|
| 용어 정의 | 풀 버전 | 축약 | **풀 버전 + 출처 (복원)** |
|
|
| 의미 왜곡 | 있음 (순차↔포함) | 없음 | 없음 (유지) |
|
|
| 처리 시간 | ~40분 | ~6분 | ~8분 (topic별 호출 추가) |
|
|
|
|
---
|
|
|
|
## 5. 교훈
|
|
|
|
1. **작동하는 것을 바꾸지 마라.** Phase P의 텍스트 채우기는 잘 작동했다. Phase Q에서 바꿀 이유가 없었다.
|
|
2. **프롬프트 탓을 하기 전에 호출 구조를 확인하라.** 5번 프롬프트를 수정했지만, 문제는 "한 번에 6개 블록 요청"이라는 호출 구조였다.
|
|
3. **이전 결과물과 비교하라.** `step3_edited_variants.json`(Phase P)과 `step3_fill_content.json`(Phase Q)을 처음부터 비교했으면 원인을 즉시 찾았을 것이다.
|
|
4. **조사 결과를 적용할 때, 기존에 잘 작동하는 부분은 보존하라.** "계산 먼저, AI 판단 나중에"는 블록 선택에 적용할 원칙이었지, 텍스트 채우기에 적용할 원칙이 아니었다.
|