[LLM 앱 실전] Day 5: 평가와 가드레일 - 신뢰할 수 있는 LLM 앱 운영하기
이 글은 AI(Claude)의 도움을 받아 작성하고, 작성자가 검토·편집했습니다.
서론: “잘 되는 것 같다”를 넘어서
Day 1~4에서 프롬프트·RAG·도구·에이전트로 LLM 앱을 만들었다. 그런데 LLM은 비결정적이다. 같은 입력에도 다른 답을 내고, 프롬프트를 한 줄 바꾸면 예상치 못한 곳이 망가진다. “데모는 잘 됐는데 프로덕션에서 터지는” 전형적 함정이다. 마지막 편은 품질을 측정하고(평가) 안전하게 지키는(가드레일) 법이다. 이것이 데모와 제품을 가른다.
1. 왜 평가가 어려운가
1
2
3
4
5
전통 소프트웨어: 입력 X → 출력은 정확히 Y (단정적 테스트)
LLM 앱: 입력 X → 출력이 매번 조금씩 다름, "정답"이 여럿일 수 있음
→ assert output == expected 가 안 통한다
→ "얼마나 좋은가"를 측정하는 새로운 방법이 필요
핵심 전환: 통과/실패가 아니라 점수와 분포로 본다(대규모 시스템 Day 5의 백분위수 사고와 같다).
2. 평가 데이터셋 만들기
평가의 출발점은 대표적인 입력-기대 모음이다.
1
2
3
4
5
6
7
구성:
- 실제 사용 사례를 반영한 입력 (로그에서 추출)
- 엣지 케이스: 모호한 질문, 긴 입력, 적대적 입력
- 각 입력에 기대 답변 또는 평가 기준
규모: 처음엔 20~50개로 시작해도 충분. 점진적으로 확장.
원칙: 데이터셋이 곧 품질의 정의. 실패 사례를 발견할 때마다 추가한다.
3. 평가 방법 세 가지
1
2
3
4
5
6
7
8
9
10
11
12
1. 코드 기반 (결정적):
형식 검증(JSON 파싱 성공?), 키워드 포함, 길이, 정규식
✅ 빠르고 싸고 객관적 ❌ 의미·품질은 못 봄
2. LLM-as-Judge (모델이 채점):
강한 모델에게 "이 답변이 기준을 충족하나" 평가시킴
✅ 의미·품질 평가 가능, 확장성 ❌ 비용, 심판 모델 편향
→ 평가 기준(rubric)을 명확히 줘야 일관됨
3. 인간 평가:
✅ 가장 정확한 기준 ❌ 느리고 비쌈
→ 소량의 골든셋·LLM-judge 보정에 사용
1
2
3
4
5
6
7
8
9
10
# LLM-as-Judge 예시
judge_prompt = f"""다음 답변을 평가하라. 0~5점.
기준: 정확성, 근거 인용, 한국어 자연스러움.
[질문] {question}
[제공 근거] {context}
[답변] {answer}
JSON으로: score"""
# 점수를 JSON으로 강제 (Day 1의 구조화 출력)
4. 무엇을 측정하는가 (RAG/에이전트별)
1
2
3
4
5
6
7
8
9
10
11
RAG (Day 2):
- 검색 품질: 관련 문서를 찾았나 (recall, precision)
- 충실성(faithfulness): 답이 근거에 기반하나 (환각 여부)
- 관련성: 질문에 실제로 답했나
에이전트 (Day 4):
- 작업 완료율: 목표를 달성했나
- 효율성: 몇 단계·얼마 비용으로 (불필요한 루프?)
- 도구 사용 정확도: 맞는 도구를 맞게 불렀나
공통: 정확성, 지연, 비용(토큰), 사용자 만족도
5. 회귀 방지: CI에 평가 붙이기
프롬프트·모델·코드를 바꿀 때마다 평가를 돌려 품질 회귀를 잡는다.
1
2
3
4
5
6
7
8
9
워크플로:
프롬프트 수정 → 평가셋 실행 → 점수 비교
→ 이전보다 떨어진 케이스가 있나? → 있으면 배포 보류
A/B 비교:
새 프롬프트 vs 기존을 같은 평가셋으로 점수 비교
→ "느낌"이 아니라 숫자로 판단
이것이 LLM 앱의 단위 테스트다 — 비결정성 때문에 통과/실패 대신 점수 임계로.
6. 가드레일: 입력과 출력을 지키기
평가가 “얼마나 좋은가”라면, 가드레일은 “나쁜 것을 막는” 실시간 방어다.
1
2
3
4
5
6
7
8
9
10
입력 가드레일:
- 프롬프트 인젝션 탐지: "이전 지시를 무시하고..." 같은 공격 차단
- 범위 외 질문 거부, 유해 입력 필터
- PII(개인정보) 탐지·마스킹
출력 가드레일:
- 환각 검증: 출력이 근거(RAG)에 있는 내용인지 확인
- 형식 검증: 스키마 준수 (Day 1)
- 유해성·민감정보 필터, 금지 주제 차단
- 신뢰도 낮으면 "모르겠다"로 폴백
1
2
3
4
5
6
7
def guarded_generate(user_input):
if detect_injection(user_input): # 입력 방어
return "처리할 수 없는 요청입니다."
output = llm_generate(user_input)
if not is_grounded(output, context): # 출력 방어 (환각 체크)
return "확실한 답변을 드릴 수 없습니다."
return mask_pii(output)
프롬프트 인젝션은 본질적으로 완전 차단이 어렵다(WebAssembly Day 3의 보안 사고처럼, 신뢰 경계를 명확히 하고 권한을 좁히는 것이 근본 대책). 도구 권한 최소화·인간 승인과 함께 다층 방어한다.
7. 운영: 비용·지연·모니터링
1
2
3
4
5
6
7
8
9
10
비용 관리:
- 작업별 적정 모델 선택 (분류는 작은 모델, 추론은 큰 모델)
- 캐싱 (대규모 시스템 Day 1): 동일/유사 질의 결과 재사용
- 프롬프트 캐싱: 반복되는 긴 시스템 프롬프트 캐시
- 컨텍스트 최소화 (Day 1): 토큰이 곧 비용
모니터링 (대규모 시스템 Day 5):
- 토큰 사용량·비용·지연(p99)·에러율 추적
- 사용자 피드백(좋아요/싫어요) 수집 → 평가셋으로 환류
- 프로덕션 로그에서 실패 사례 발굴 → 데이터셋 보강
8. 시리즈 종합 체크리스트
- 프롬프트·컨텍스트·구조화 출력으로 모델을 제어했다. (Day 1)
- RAG로 외부 지식을 검색해 환각을 줄였다. (Day 2)
- 함수 호출로 모델에 도구를 쥐여줬다. (Day 3)
- 에이전트로 자율적 다단계 작업과 그 통제를 구현했다. (Day 4)
- 평가·가드레일·비용관리로 신뢰할 수 있는 운영 체계를 세웠다. (Day 5)
시리즈 마무리
LLM 애플리케이션 개발의 핵심 교훈은 명확하다. 모델은 강력하지만 비결정적이고, 그럴듯하게 틀리며, 신뢰할 수 없는 입력을 만든다. 좋은 LLM 앱은 이를 부정하지 않고 설계로 감싼다 — RAG로 근거를 주고, 도구로 능력을 확장하되 권한을 좁히고, 에이전트에 상한을 두고, 평가로 품질을 측정하고, 가드레일로 방어한다.
프롬프트→RAG→도구→에이전트→평가 다섯 단계는 능력을 더하는 순서이자, 동시에 통제를 더하는 순서다. 능력과 통제가 함께 자랄 때 데모가 제품이 된다. 그리고 그 모든 토대에는 지난 시리즈에서 다룬 시스템 설계 원칙 — 캐싱, 관측성, 멱등성, 보안 경계 — 이 그대로 살아 있다. LLM 앱은 새로운 마법이 아니라, 비결정적 컴포넌트를 품은 또 하나의 분산 시스템이다.