멀티스텝 AI 파이프라인 구현 검토 - 100-hours-a-week/KTB4-1st-wiki GitHub Wiki

단계 4. 멀티스텝 AI/파이프라인 설계

1. 전체 파이프라인 흐름도

flowchart TD
    FE[Frontend] --> BE[Backend] --> AI["AI Server (FastAPI)"]

    subgraph SINGLE["단일 호출 / 대상 제외"]
        direction TB
        F4["기능4 · 챗봇<br/>(RAG는 단계5에서 별도 다룸)"]
        F5["기능5 · 스마트검색<br/>임베딩 유사도 랭킹 (생성 없음)"]
        F6["기능6 · 채팅 다듬기"]
    end

    subgraph MULTI["멀티스텝 파이프라인 대상"]
        direction TB
        F1["기능1 · 사진 등록<br/>사진 검증→식별→초안 (1콜 내 논리적 3단계)"]
        F2["기능2 · 텍스트 검증<br/>규칙 기반 필터링→LLM 캐스케이드"]
        F3["기능3 · 가격 검색<br/>웹검색→추출→구조화"]
    end

    

    AI --> F1
    AI --> F2
    AI --> F3
    AI --> F4
    AI --> F5
    AI --> F6
Loading

2. 대상 기능 판단

기능 멀티스텝 여부 사유
1. 사진 등록 O 검증·식별·초안생성이 순서를 가진 논리적 파이프라인 (단, 물리적 호출은 1회 — 4.1 참고)
2. 텍스트검증 O 규칙 기반 필터링→LLM 2단계 캐스케이드
3. 가격검색 O 웹검색→근거추출→구조화 3단계
4. 챗봇 대상 제외 검색+생성 구조이나 RAG 특화 흐름이므로 단계5에서 별도로 다룸
5. 스마트검색 대상 제외 생성 없는 단일 임베딩 유사도 연산
6. 채팅다듬기 대상 제외 단일 생성 호출

3. 사용 모델·도구 목록

모델/도구 적용 기능 선택 이유 기대 효과
Claude Vision (sonnet) 1 이미지 이해 + 텍스트 생성을 동시에 지원해 검열·식별·초안을 한 프롬프트로 처리 가능 이미지 재전송 없이 1콜로 등록 정보 완성, 지연 최소화
규칙 기반 필터링 (정규식+금칙어 템플릿) 2 (1단계) 연락처·스팸·욕설/혐오·금지품목처럼 패턴·고정 목록으로 판별 가능한 위반은 결정적으로 처리 가능 LLM 호출 자체 제거 → 비용 0, 응답 즉시
Claude haiku 2 (2단계) 사기성 문구·물건 혼재·가격언급처럼 패턴화하기 어렵거나 우회 표현이 흔한 판단만 LLM에 위임, 저비용 모델로 충분 앞선 1단계에서 연락처노출·스팸·욕설/혐오·금지품목이 이미 걸러진 뒤라 소량 호출만 발생
Claude sonnet + web_search 도구 3 (1단계) 실시간 쇼핑몰 가격은 학습 데이터에 없어 검색 도구 필수 최신 시세 반영
Claude sonnet (재호출, 도구 없음) 3 (3단계) 자유 텍스트 조사 결과를 구조화된 JSON으로 변환하는 데 집중 파싱 안정성(도구 호출과 형식 변환을 분리)
Qwen2.5-7B-Instruct
(Modal 서버리스 + vLLM, AWQ INT4, prefix caching)
6 자유 텍스트 생성이 실제로 필요하고 호출 볼륨도 커 self-hosting 이득이 명확 API 비용 절감, 간헐적 트래픽에 맞는 종량 과금, prefix caching으로 반복 프롬프트 재계산 생략

괄호 안 숫자는 해당 기능 내부 파이프라인의 몇 번째 단계인지를 나타냅니다(예: 2 (1단계) = 기능2의 1번째 단계). 괄호가 없으면 그 기능 전체(단일 호출)에 적용됨을 뜻합니다. 파싱·후처리처럼 모델/도구를 호출하지 않는 단계(예: 기능3의 2단계)는 이 표에 포함하지 않았습니다.


4. 기능별 파이프라인 상세

4.1 기능1 — 사진 등록 (/api/vision/analyze-item)

flowchart TD
    A["이미지 1~3장 입력"] --> B["Claude Vision (claude-sonnet-5)<br/>단일 호출, 프롬프트 안에서<br/>검열→식별→초안 순서로 생성 지시"]
    B --> R["Claude 응답(JSON)<br/>isAppropriate / rejectionReason /<br/>title / content / confidence"]
    R --> C{"AI 서버: isAppropriate?"}
    C -->|false| D["422 반환<br/>(title/content는 응답에 포함하지 않음)"]
    C -->|true| E{"confidence < 0.25?"}
    E -->|Yes| F["title/content → null 치환"]
    E -->|No| G["title/content 그대로 사용"]
    F --> H["200 응답"]
    G --> H
Loading

검열→식별→초안생성은 하나의 호출 안에서 프롬프트 순서로 유도되는 논리적 단계이며, 모델이 중간에 멈추는 물리적 분기가 아니다. 판정에 따른 title/content 사용 여부는 응답이 전부 생성된 뒤 AI 서버가 사후에 결정한다.

1콜 구조 유지 근거: 검열을 별도 호출로 분리하면(2콜 구조), 검열 통과 여부와 무관하게 이미지를 두 번(1차 검열용, 2차 식별·초안용) 전송해야 해 비전 토큰 비용이 오히려 늘어날 수 있다. 특히 정상 이미지 비율이 높은 서비스 특성상, 대다수 케이스에서 이미지 재전송 비용이 절감 효과보다 커질 가능성이 높아 1콜 구조를 유지하기로 결론지었다.

의사코드

def analyze_item(images: list[bytes]) -> dict:
    response = claude.messages.create(
        model="claude-sonnet-5",
        messages=[{
            "role": "user",
            "content": [
                *[{"type": "image", "source": {"type": "base64", "data": img}} for img in images],
                {"type": "text", "text": VISION_PROMPT},  # 검열→식별→초안 순서로 지시
            ],
        }],
    )
    result = parse_json(response)

    if not result["isAppropriate"]:
        # 모델이 title/content를 채워 반환했더라도 응답에는 포함하지 않는다
        return {"status": 422, "error": "content_rejected", "message": result["rejectionReason"]}

    if result["confidence"] < 0.25:
        result["title"] = None
        result["content"] = None

    return {"status": 200, **result}

도입 이점

  • 2콜로 나눴다면: 검열 통과 여부와 무관하게 이미지를 두 번 전송해야 해 비전 토큰 비용이 늘어남
  • 이점: 논리적으로는 검열→식별→초안 순서를 프롬프트로 유지하면서, 물리적 호출은 1회로 묶어 이미지 재전송 비용을 피함
  • 서비스 요구와의 관련성: 4.1 "사진만으로 등록 부담을 줄임" 목표 — 등록 흐름이 느리면 이탈로 이어짐

4.2 기능2 — 텍스트검증 (/api/moderation/check-text) — 캐스케이드

flowchart TD
    A["title, content 입력"] --> B["1단계: 규칙 기반 필터링<br/>(정규식+금칙어 템플릿: 연락처·스팸·욕설혐오·금지품목)"]
    B -->|걸림| B1["422 반환<br/>LLM 미호출"]
    B -->|안걸림| D["2단계: Claude haiku<br/>(사기성 문구 + 물건 혼재 + 가격언급 판단)"]
    D -->|위반| D1["422 반환"]
    D -->|통과| E["200 반환"]
Loading

금지 품목은 총기·마약·주류 등 닫힌 목록에 가까워 금칙어 템플릿(1단계)으로 처리한다. 반면 가격언급은 "저렴하게 드릴게요"처럼 우회 표현이 많아 단순 패턴 매칭으로는 탐지가 어려워, 문맥 이해가 필요한 2단계(LLM) 판단으로 남긴다.

의사코드

def check_text(title: str, content: str) -> dict:
    text = f"{title} {content}"

    if rule_based_filter(text):                       # 1단계: 정규식+금칙어 템플릿 (연락처·스팸·욕설혐오·금지품목)
        return {"status": 422, "error": "content_rejected"}

    response = claude.messages.create(                 # 2단계: 사기성 문구·물건 혼재·가격언급 판단
        model="claude-haiku-5",
        messages=[{"role": "user", "content": MODERATION_PROMPT.format(text=text)}],
    )
    result = parse_json(response)
    if not result["passed"]:
        return {"status": 422, "error": "content_rejected"}

    return {"status": 200}

도입 이점

  • 멀티스텝이 없었다면: 모든 텍스트가 예외 없이 LLM 호출
  • 이점: 위반 비율만큼 LLM 호출량·비용 감소
  • 서비스 요구와의 관련성: 게시글이 많아질수록 검증 비용이 선형 증가 — 대량 트래픽에서 비용 통제 필요

4.3 기능3 — 가격검색 (/api/value/estimate-price)

flowchart TD
    A["title, content, valueTolerance, tradeSpeed"] --> B["1단계: Claude sonnet + web_search<br/>국내 쇼핑몰 시세 조사 (최대 5회 검색)"]
    B --> C["2단계: 응답 블록에서<br/>검색어·인용 출처 코드로 추출"]
    C --> D["3단계: Claude sonnet 재호출<br/>결과를 구조화 JSON으로 변환"]
    D --> E["서버 후처리<br/>이상치 제거(IQR) → 평균 → 할인율 계산 → minUnitPrice"]
    E --> F{"검색 성공?"}
    F -->|Yes| G["200 응답 (priceSource: new)"]
    F -->|No| H["200 응답 (priceSource: estimated)"]
Loading

의사코드

def estimate_price(title, content, value_tolerance, trade_speed):
    # 1단계: 웹검색 기반 시세 조사
    message = claude.messages.create(
        model="claude-sonnet-5",
        tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}],
        messages=[{"role": "user", "content": RESEARCH_PROMPT.format(item=f"{title} {content}")}],
    )

    # 2단계: 검색 근거 추출 (코드로, LLM 서술에 의존하지 않음)
    evidence = extract_evidence(message)  # queries, citations, answer

    # 3단계: 결과 구조화
    structured = claude.messages.create(
        model="claude-sonnet-5",
        messages=[{"role": "user", "content": STRUCTURE_PROMPT.format(research=evidence["answer"])}],
    )

    # 서버 후처리
    prices = remove_outliers_iqr(structured["prices"])
    reference = round(mean(prices), -1)
    discount_rate = min((value_tolerance * 0.7 + trade_speed * 0.3) * 0.3, 0.3)
    return {
        "referenceUnitPrice": reference,
        "minUnitPrice": int(reference * (1 - discount_rate)),
        "maxUnitPrice": reference,
    }

도입 이점

  • 멀티스텝이 없었다면: (멀티스텝 자체가 구조적으로 필수 — 웹검색 없이는 최신 가격을 알 방법이 없음)
  • 이점: 조사(사실 확인)와 구조화(형식 변환)를 분리해 각 프롬프트를 단순하게 유지 → 파싱 실패율 감소
  • 서비스 요구와의 관련성: 물물교환 시 "적정 가치 판단"이 핵심 기능이라 시세 정확도가 곧 서비스 신뢰도

5. 종합 정리

5.1 기능별 이점 요약

기능 핵심 이점 서비스 요구 관련성
1 논리적 단계는 유지하되 물리적 호출은 1회로 묶어 이미지 재전송 비용 회피 등록 흐름 속도 → 이탈률
2 위반 비율만큼 비용/지연 감소 트래픽 증가 시 비용 통제
3 조사/구조화 분리로 파싱 실패율 감소 시세 정확도 = 서비스 신뢰도

5.2 종합 결론

멀티스텝 구조는 무조건 도입한 게 아니라, 실패 지점을 분리해 낭비 비용을 줄일 수 있는 기능(2)과 구조적으로 여러 단계가 필수인 기능(3), 그리고 논리적 단계는 있으나 물리적으로는 통합하는 게 더 유리한 기능(1)으로 나눠 각기 다르게 설계했다. 나머지 기능(4~6)은 단일 호출로 충분해 오히려 불필요한 복잡도를 피했다.

⚠️ **GitHub.com Fallback** ⚠️