WRITING · 2026-11-02

LLM 응답을 믿지 않는 파이프라인 설계

LLM에는 판단만 맡기고, 결과는 코드로 검증한다. 댓글 분석 파이프라인과 AI 도서 추천을 만들며 정한 네 가지 원칙.

LLM데이터 파이프라인Django검증

SSAFY 관통 프로젝트에서 LLM을 붙인 기능을 두 번 만들었습니다. 주식 커뮤니티 댓글을 수집해 정제·증강·요약하는 데이터 파이프라인(관통 5)과, 회원 정보를 보고 책을 골라 주는 AI 도서 추천(관통 4)입니다.

처음에는 "LLM에게 결과물을 통째로 만들게 하면 편하겠다"고 생각했습니다. 그런데 실제로 돌려 보니 LLM은 가끔 틀립니다. 개수를 하나 빼먹고, 없는 책을 지어내고, JSON 앞에 설명을 붙입니다. 문제는 '가끔'이라서, 테스트 몇 번으로는 드러나지 않는다는 점이었습니다.

그래서 설계 원칙을 하나로 정했습니다.

LLM에는 판단만 맡기고, 결과는 코드로 검증한다.

1. 원본을 고치게 하지 말고, 번호만 받는다

부적절한 댓글을 걸러낼 때, LLM에게 "부적절한 댓글을 뺀 목록을 돌려줘"라고 하면 LLM이 남은 댓글을 다시 써서 돌려줍니다. 띄어쓰기가 바뀌거나 문장이 살짝 다듬어져 원본과 비교할 수 없게 됩니다.

대신 제거할 댓글의 번호만 받고, 실제 제거는 코드가 합니다.

numbers = parse_json_array(response)
if numbers is None:
    # 형식 오류를 성공으로 취급하지 않는다
    raise LLMError('부적절 댓글 판정 응답이 유효한 JSON 배열이 아닙니다.')

# 정수이면서 실제 댓글 번호 범위 안인 값만 사용 (중복 제거 후 정렬)
to_remove = sorted({n for n in numbers if type(n) is int and 0 <= n < len(comments)})

# 앞에서부터 지우면 뒤쪽 번호가 밀리므로 뒤에서부터 제거
kept = list(comments)
for i in reversed(to_remove):
    removed.append(kept.pop(i))

여기에 검증이 세 겹 들어 있습니다.

그리고 뒤에서부터 지웁니다. 앞에서부터 pop(2), pop(5)를 하면 첫 번째 삭제 뒤 5번이 원래 6번 댓글을 가리키게 됩니다. 사소해 보이지만, 놓치면 에러 없이 엉뚱한 댓글이 지워지는 종류의 버그입니다.

2. 개수와 형식이 맞지 않으면 재시도한다

댓글 증강은 "같은 의미, 다른 표현"의 문장을 만드는 작업입니다. 결과가 원본과 1:1로 짝이 맞아야 학습 데이터로 쓸 수 있습니다.

for attempt in range(1, max_attempts + 1):
    augmented = parse_json_array(run_llm(build_augment_prompt(comments)))
    if augmented is None:
        continue
    if len(augmented) != len(comments):          # 개수 불일치
        continue
    if any(not isinstance(a, str) or not a.strip() for a in augmented):
        continue                                  # null·숫자·빈 문장
    ...
    return [a.strip() for a in augmented]
return []

20개를 넣었는데 19개가 돌아오면, 어느 하나가 빠졌는지 알 수 없습니다. 이때 앞에서부터 짝을 지으면 모든 쌍이 한 칸씩 어긋난 데이터가 조용히 저장됩니다. 그래서 개수가 다르면 그 결과는 버리고 다시 요청하고, 정해진 횟수 안에 성공하지 못하면 빈 결과를 돌려줘 화면에 실패를 알립니다.

금융 댓글이라는 특성도 반영했습니다. "-3% 하락"이 "3% 상승"으로 바뀌면 의미가 정반대가 되므로, 증강 전후로 숫자·부호·상승/하락 방향·종목명이 바뀐 문장이 있으면 그 결과도 재시도합니다.

3. 목록에 없는 것은 받아들이지 않는다

AI 도서 추천에서는 LLM에게 등록된 도서 목록(id, 제목, 저자 등)을 주고 몇 권을 고르게 했습니다. LLM은 종종 목록에 없는 유명한 책을 추천합니다. 그럴듯하지만 우리 서비스에는 없는 책입니다.

by_id = {book.pk: book for book in books}
for item in data.get('recommendations', []):
    try:
        book_id = int(item.get('id'))
    except (TypeError, ValueError, AttributeError):
        continue
    # 목록에 없는 id(AI가 지어낸 책) · 중복은 버림
    if book_id in by_id and book_id not in seen:
        seen.add(book_id)
        results.append({'id': book_id, 'title': by_id[book_id].title, ...})

제목과 저자도 LLM 응답이 아니라 DB의 값으로 채웁니다. LLM이 제목을 살짝 틀리게 적어도 화면에는 정확한 정보가 나갑니다. LLM에게서 받는 것은 "어떤 id를, 왜 골랐는지"뿐입니다.

4. 실패해도 서비스는 멈추지 않는다

LLM API는 느리고, 가끔 실패합니다. 그래서 LLM 호출은 공통 함수 하나로 모으고, 실패를 구분해 다뤘습니다.

검증은 테스트로 고정한다

이런 규칙은 말로만 정해 두면 금방 깨집니다. 그래서 LLM 호출을 mock으로 바꿔, 일부러 틀린 응답을 주는 테스트를 만들었습니다.

외부 API 없이 실행되고, 관통 4·5를 합쳐 자동 테스트 86개가 이 규칙들을 지키고 있습니다.

정리

LLM은 "무엇이 부적절한지", "어떤 책이 어울리는지" 같은 판단을 잘합니다. 하지만 원본을 보존하고, 개수를 맞추고, 존재하는 데이터만 쓰는 일은 코드가 훨씬 정확합니다. 역할을 이렇게 나누고 나니, LLM 응답이 가끔 틀려도 서비스의 데이터는 틀리지 않게 되었습니다.

데이터 파이프라인에 LLM을 넣을 때도 같은 원칙을 적용하려고 합니다. LLM의 출력은 입력 데이터처럼 검증한다.

SSAFY 16기 관통 프로젝트 — ThRead: LLM 댓글 분석 파이프라인 · AI 도서 서비스이 글에서 다룬 코드는 저장소에서 볼 수 있습니다.
GitHub 저장소 →포트폴리오로
다른 글겹쳐 있는 두 신호를 위상으로 가르기 — 파수 상관 필터링 석사 연구 정리중력과 자력 데이터를 같은 주파수 성분끼리 비교해, 함께 움직이는 성분과 반대로 움직이는 성분을 나눴다. 임계값을 감으로 정하지 않으려고 한 일들.점수를 지키면서 올리기 — VQA 챌린지에서 앙상블 후보를 고른 기준정확도가 조금 올라도 원래 맞히던 문제를 많이 틀리면 불안정한 개선이다. fixes와 breaks로 순기여를 따져 0.94608에서 시작한 점수를 쌓아 올린 과정.LLM이 붙인 [근거 N]을 코드로 다시 검사하기 — 국보 RAG 가이드의 인용 설계근거를 인용하라고 프롬프트에 써도 LLM은 없는 번호를 붙입니다. 근거 번호를 검색 단계에서 고정하고, 답변의 인용을 코드로 파싱해 없는 번호는 지우고 실제로 인용한 근거만 보여 주는 구조를 만든 기록.YAML 파일 하나로 포트폴리오·이력서·비공개 페이지까지 — 이 사이트를 만든 방식내용은 한 곳에만 쓰고, 웹 페이지·PDF 이력서·노션 문서는 빌드로 만든다. 공개하면 안 되는 내용은 저장소에 평문으로 남기지 않는다.데이터를 해석하기 전에 그 데이터가 만들어진 역사를 본다 — 학부 제1저자 논문 회고해외 대규모 CO2 저장소 8곳을 판구조 형성사와 탄성파 영상으로 비교했다. 처리 결과를 '그럴듯한지' 판단하는 기준을 어디서 가져올 것인가에 대한 이야기.길찾기에서 의사결정으로 — 배틀싸피 자동 플레이 봇을 세 번 고친 기록목표까지 가는 BFS 한 줄짜리 봇이, 사거리·장애물·탄약·보급·암호 해독까지 판단하는 봇이 되기까지. 매 턴 다시 계획하는 구조로 바꾼 이유.산불 4건으로 시공간 딥러닝 학습하기데이터가 35개 시점뿐일 때 어떻게 학습시켰고, 다시 보니 무엇이 부족했는지. U-LSTM 산불 확산 예측 모델 회고.순차 학습이 성능을 떨어뜨린 이유 — catastrophic forgetting 실험기단어 단위 → 문맥 단위로 차례로 학습하면 더 좋아질 줄 알았는데, BERTScore가 0.47에서 0.32로 떨어졌다. FLAN-T5 LoRA 학습 전략 비교.