Parent: #448
Context
현재 매거진 생성은 compose_layout 단일 Gemini Pro 호출이 angle 해석 / 톤 결정 / 섹션 순서 / 카피 작성을 동시에 처리한다 (packages/ai-server/src/editorial_article/nodes/compose_layout.py:55). 품질 제어가 어렵고 부분 재생성이 불가능하다.
파이프라인을 6 노드로 분해하면:
- 섹션별 병렬 생성으로 wall-clock 안정화
- Per-section retry 가능 (실패 섹션만)
- Template 분류 단계 추가로 매거진 톤 다양화 (Track 2/3 의 토대)
- Chat agent 의
change_template 등 정밀 도구가 가능해짐
MAGAZINE_PIPELINE_MODE=single_call (default) 유지 시 기존 동작 그대로. multi_stage 일 때만 새 그래프 활성화 — 안전한 graceful rollout.
Target graph
fetch_sources
→ classify_template (Gemini flash-lite — angle → template id)
→ plan_outline (Gemini Pro — SectionPlan[] 생성, 카피 X)
→ write_section (fan-out) (asyncio.gather + semaphore, section type 별 sub-prompt)
→ validate_sections (길이/금지어/id 검증, 실패 섹션 1회 retry)
→ [polish_layout] (env flag, Gemini Pro 전체 톤 일관성 패스)
→ assemble_layout (_filter_layout 이관 + MagazineLayout 조립)
→ generate_thumbnail
→ publish
Scope
신규 파일 (ai-server)
기존 파일 수정
재사용
call_gemini_with_fallback (packages/ai-server/src/post_editorial/gemini_retry.py:23)
fetch_sources_node (변경 X)
generate_thumbnail_node (변경 X)
publish_node (변경 X)
EditorialArticleService.editorial_article_job ARQ entry (변경 X)
DB migration
없음. editorial_article_events.step 컬럼이 text 라 새 step name 자유. LLM cost 는 note jsonb 컬럼에 {model, input_tokens, output_tokens} 누적 (기존 /admin/gemini-cost 페이지가 읽음).
Sub-PR 분할
PR-1a: foundation (non-breaking)
templates.py + prompts/ 디렉토리 + section_writers.py + state.py 필드 + models.py 추가 + config.py flag. 그래프 미연결, lint/test green 만 보장.
PR-1b: nodes
5 노드 파일 (classify, plan, write, validate, assemble) + unit test. graph.py 에는 mode 분기만 (multi_stage 빈 그래프 placeholder).
PR-1c: wire-up + polish
graph.py multi_stage builder 완성 + polish_layout 노드 (flag) + integration test + telemetry events INSERT.
Test
- Unit (각 노드별 fake LLM monkey-patch):
tests/unit/editorial_article/test_classify_template.py
tests/unit/editorial_article/test_plan_outline.py (invalid post_id 거부)
tests/unit/editorial_article/test_write_section.py (1개 실패 시 placeholder)
tests/unit/editorial_article/test_validate_sections.py
tests/unit/editorial_article/test_assemble_layout.py
tests/unit/editorial_article/test_graph_mode.py (ENV mode 분기)
- Integration:
tests/integration/editorial_article/test_graph_e2e.py (fake Gemini + 메모리 DB)
- Manual smoke: dev 에서
MAGAZINE_PIPELINE_MODE=multi_stage ENV 로 ai-server 재시작 → 1건 approve → events 타임라인에서 step 별 진행 확인
Risk
- Gemini RPM cap —
MAGAZINE_MAX_PARALLEL_SECTION_WRITERS=3 보수적. 429 모니터링
- 총 토큰 1.5-2x 예상 (호출 수 N+3). 같은 recommendation 을 single_call ↔ multi_stage 두 번 돌려
/admin/gemini-cost 에서 비교
- Template classifier accuracy — flash-lite 잘못 분류 시 fallback
editorial-deep + Track 3 의 change_template tool 로 보정
Out of scope
- Section type 확장 (
quote_pull, mood_board) → 별도 sub-issue (Track 2)
- Chat tool 의
change_template → 별도 sub-issue (Track 3)
- Percent rollout (
editorial_discovery_settings.pipeline_mode_override 컬럼) → 후속
Parent: #448
Context
현재 매거진 생성은
compose_layout단일 Gemini Pro 호출이 angle 해석 / 톤 결정 / 섹션 순서 / 카피 작성을 동시에 처리한다 (packages/ai-server/src/editorial_article/nodes/compose_layout.py:55). 품질 제어가 어렵고 부분 재생성이 불가능하다.파이프라인을 6 노드로 분해하면:
change_template등 정밀 도구가 가능해짐MAGAZINE_PIPELINE_MODE=single_call(default) 유지 시 기존 동작 그대로.multi_stage일 때만 새 그래프 활성화 — 안전한 graceful rollout.Target graph
Scope
신규 파일 (ai-server)
packages/ai-server/src/editorial_article/templates.py—TEMPLATE_REGISTRY(editorial-deep / shopping-guide / pinterest-board) +TemplateSpecdataclasspackages/ai-server/src/editorial_article/section_writers.py— type → writer fn dispatchpackages/ai-server/src/editorial_article/prompts/classify.pypackages/ai-server/src/editorial_article/prompts/outline.pypackages/ai-server/src/editorial_article/prompts/polish.pypackages/ai-server/src/editorial_article/prompts/sections/intro.pypackages/ai-server/src/editorial_article/prompts/sections/curation_card.pypackages/ai-server/src/editorial_article/prompts/sections/spotlight.pypackages/ai-server/src/editorial_article/prompts/sections/closing.pypackages/ai-server/src/editorial_article/nodes/classify_template.pypackages/ai-server/src/editorial_article/nodes/plan_outline.pypackages/ai-server/src/editorial_article/nodes/write_section.pypackages/ai-server/src/editorial_article/nodes/validate_sections.pypackages/ai-server/src/editorial_article/nodes/polish_layout.py(feature flag conditional)packages/ai-server/src/editorial_article/nodes/assemble_layout.py기존 파일 수정
packages/ai-server/src/editorial_article/graph.py— mode flag 분기 (_build_single_call_graph/_build_multi_stage_graph)packages/ai-server/src/editorial_article/state.py—template_id,section_plans,written_sections필드packages/ai-server/src/editorial_article/models.py—TemplateChoice,SectionPlanPydanticpackages/ai-server/src/post_editorial/config.py— 4개 ENV 필드:MAGAZINE_PIPELINE_MODE:single_call(default) |multi_stageMAGAZINE_POLISH_ENABLED: false (default)MAGAZINE_POLISH_TEMPERATURE: 0.4MAGAZINE_MAX_PARALLEL_SECTION_WRITERS: 3packages/ai-server/src/editorial_article/nodes/compose_layout.py:40—_filter_layout을assemble_layout.py로 이관 (single_call 모드도 그대로 사용)재사용
call_gemini_with_fallback(packages/ai-server/src/post_editorial/gemini_retry.py:23)fetch_sources_node(변경 X)generate_thumbnail_node(변경 X)publish_node(변경 X)EditorialArticleService.editorial_article_jobARQ entry (변경 X)DB migration
없음.
editorial_article_events.step컬럼이 text 라 새 step name 자유. LLM cost 는notejsonb 컬럼에{model, input_tokens, output_tokens}누적 (기존/admin/gemini-cost페이지가 읽음).Sub-PR 분할
PR-1a: foundation (non-breaking)
templates.py + prompts/ 디렉토리 + section_writers.py + state.py 필드 + models.py 추가 + config.py flag. 그래프 미연결, lint/test green 만 보장.
PR-1b: nodes
5 노드 파일 (classify, plan, write, validate, assemble) + unit test. graph.py 에는 mode 분기만 (multi_stage 빈 그래프 placeholder).
PR-1c: wire-up + polish
graph.py multi_stage builder 완성 + polish_layout 노드 (flag) + integration test + telemetry events INSERT.
Test
tests/unit/editorial_article/test_classify_template.pytests/unit/editorial_article/test_plan_outline.py(invalid post_id 거부)tests/unit/editorial_article/test_write_section.py(1개 실패 시 placeholder)tests/unit/editorial_article/test_validate_sections.pytests/unit/editorial_article/test_assemble_layout.pytests/unit/editorial_article/test_graph_mode.py(ENV mode 분기)tests/integration/editorial_article/test_graph_e2e.py(fake Gemini + 메모리 DB)MAGAZINE_PIPELINE_MODE=multi_stageENV 로 ai-server 재시작 → 1건 approve → events 타임라인에서 step 별 진행 확인Risk
MAGAZINE_MAX_PARALLEL_SECTION_WRITERS=3보수적. 429 모니터링/admin/gemini-cost에서 비교editorial-deep+ Track 3 의change_templatetool 로 보정Out of scope
quote_pull,mood_board) → 별도 sub-issue (Track 2)change_template→ 별도 sub-issue (Track 3)editorial_discovery_settings.pipeline_mode_override컬럼) → 후속