Skip to content

feat(editorial,ai-server): 매거진 생성 파이프라인 multi-stage 분해 (Track 1 of #448) #547

@cocoyoon

Description

@cocoyoon

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)

  • packages/ai-server/src/editorial_article/templates.pyTEMPLATE_REGISTRY (editorial-deep / shopping-guide / pinterest-board) + TemplateSpec dataclass
  • packages/ai-server/src/editorial_article/section_writers.py — type → writer fn dispatch
  • packages/ai-server/src/editorial_article/prompts/classify.py
  • packages/ai-server/src/editorial_article/prompts/outline.py
  • packages/ai-server/src/editorial_article/prompts/polish.py
  • packages/ai-server/src/editorial_article/prompts/sections/intro.py
  • packages/ai-server/src/editorial_article/prompts/sections/curation_card.py
  • packages/ai-server/src/editorial_article/prompts/sections/spotlight.py
  • packages/ai-server/src/editorial_article/prompts/sections/closing.py
  • packages/ai-server/src/editorial_article/nodes/classify_template.py
  • packages/ai-server/src/editorial_article/nodes/plan_outline.py
  • packages/ai-server/src/editorial_article/nodes/write_section.py
  • packages/ai-server/src/editorial_article/nodes/validate_sections.py
  • packages/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.pytemplate_id, section_plans, written_sections 필드
  • packages/ai-server/src/editorial_article/models.pyTemplateChoice, SectionPlan Pydantic
  • packages/ai-server/src/post_editorial/config.py — 4개 ENV 필드:
    • MAGAZINE_PIPELINE_MODE: single_call (default) | multi_stage
    • MAGAZINE_POLISH_ENABLED: false (default)
    • MAGAZINE_POLISH_TEMPERATURE: 0.4
    • MAGAZINE_MAX_PARALLEL_SECTION_WRITERS: 3
  • packages/ai-server/src/editorial_article/nodes/compose_layout.py:40_filter_layoutassemble_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_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

  1. Gemini RPM capMAGAZINE_MAX_PARALLEL_SECTION_WRITERS=3 보수적. 429 모니터링
  2. 총 토큰 1.5-2x 예상 (호출 수 N+3). 같은 recommendation 을 single_call ↔ multi_stage 두 번 돌려 /admin/gemini-cost 에서 비교
  3. 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 컬럼) → 후속

Metadata

Metadata

Assignees

No one assigned

    Labels

    aiAI/자동화backend백엔드/APIenhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions