멀티스텝 AI 파이프라인 구현 검토 - 100-hours-a-week/KTB4-1st-wiki GitHub Wiki
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
| 기능 | 멀티스텝 여부 | 사유 |
|---|---|---|
| 1. 사진 등록 | O | 검증·식별·초안생성이 순서를 가진 논리적 파이프라인 (단, 물리적 호출은 1회 — 4.1 참고) |
| 2. 텍스트검증 | O | 규칙 기반 필터링→LLM 2단계 캐스케이드 |
| 3. 가격검색 | O | 웹검색→근거추출→구조화 3단계 |
| 4. 챗봇 | 대상 제외 | 검색+생성 구조이나 RAG 특화 흐름이므로 단계5에서 별도로 다룸 |
| 5. 스마트검색 | 대상 제외 | 생성 없는 단일 임베딩 유사도 연산 |
| 6. 채팅다듬기 | 대상 제외 | 단일 생성 호출 |
| 모델/도구 | 적용 기능 | 선택 이유 | 기대 효과 |
|---|---|---|---|
| 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단계)는 이 표에 포함하지 않았습니다.
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
검열→식별→초안생성은 하나의 호출 안에서 프롬프트 순서로 유도되는 논리적 단계이며, 모델이 중간에 멈추는 물리적 분기가 아니다. 판정에 따른 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 "사진만으로 등록 부담을 줄임" 목표 — 등록 흐름이 느리면 이탈로 이어짐
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 반환"]
금지 품목은 총기·마약·주류 등 닫힌 목록에 가까워 금칙어 템플릿(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 호출량·비용 감소
- 서비스 요구와의 관련성: 게시글이 많아질수록 검증 비용이 선형 증가 — 대량 트래픽에서 비용 통제 필요
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)"]
의사코드
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,
}도입 이점
- 멀티스텝이 없었다면: (멀티스텝 자체가 구조적으로 필수 — 웹검색 없이는 최신 가격을 알 방법이 없음)
- 이점: 조사(사실 확인)와 구조화(형식 변환)를 분리해 각 프롬프트를 단순하게 유지 → 파싱 실패율 감소
- 서비스 요구와의 관련성: 물물교환 시 "적정 가치 판단"이 핵심 기능이라 시세 정확도가 곧 서비스 신뢰도
| 기능 | 핵심 이점 | 서비스 요구 관련성 |
|---|---|---|
| 1 | 논리적 단계는 유지하되 물리적 호출은 1회로 묶어 이미지 재전송 비용 회피 | 등록 흐름 속도 → 이탈률 |
| 2 | 위반 비율만큼 비용/지연 감소 | 트래픽 증가 시 비용 통제 |
| 3 | 조사/구조화 분리로 파싱 실패율 감소 | 시세 정확도 = 서비스 신뢰도 |
멀티스텝 구조는 무조건 도입한 게 아니라, 실패 지점을 분리해 낭비 비용을 줄일 수 있는 기능(2)과 구조적으로 여러 단계가 필수인 기능(3), 그리고 논리적 단계는 있으나 물리적으로는 통합하는 게 더 유리한 기능(1)으로 나눠 각기 다르게 설계했다. 나머지 기능(4~6)은 단일 호출로 충분해 오히려 불필요한 복잡도를 피했다.