본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로AI › 개발 도구 › LangChain

10. 테스트: 평가, 모니터링, 개선

목차

테스트: 평가, 모니터링, 개선

일반 함수라면 2 + 2 = 4처럼 정확한 결과를 비교하기 쉽다. LLM 답변은 표현이 달라도 맞을 수 있고, 형식은 맞아도 근거가 틀릴 수 있다. 그래서 한 번 실행해 그럴듯한 답이 나왔다는 확인만으로는 부족하다.

예를 들어 “휴가는 며칠 전에 신청해야 하나요?”라는 질문에 3영업일 전과 최소 사흘 전은 비슷해 보이지만 규정상 다르다. 테스트는 의미, 근거, 도구 사용, 형식, 안전한 거절을 나눠 확인해야 한다.

flowchart LR
    D[실패 사례 수집] --> S[평가 데이터셋]
    S --> B[현재 버전 기준선]
    B --> C[프롬프트·검색·그래프 변경]
    C --> E[오프라인 평가]
    E -->|통과| P[단계적 배포]
    E -->|실패| C
    P --> O[운영 추적·온라인 평가]
    O --> D

이 흐름에서 운영 로그는 다음 테스트의 재료가 된다. 문제가 된 질문을 개인정보를 제거한 뒤 데이터셋에 넣어야 같은 문제가 다시 생겼는지 확인할 수 있다.

1. 무엇을 테스트할까?

층위예시검증 방법
코드 단위입력 길이 제한, 도구 인자 검증, 출력 JSON 파싱결정적인 단위 테스트
구성 요소검색기가 기대 문서를 찾는지, 라우터가 경로를 고르는지고정 데이터의 구성 요소 테스트
전체 흐름질문 → 검색 → 생성 → 인용까지 완료하는지대표 질문 데이터셋으로 통합 평가
사용자 흐름로그인한 사용자가 자신의 대화만 보는지API·화면 E2E 테스트
운영 품질오류율, 지연, 근거 누락, 비용이 변하는지추적과 온라인 평가

LLM 출력 자체를 글자 단위로 고정하면 같은 의미의 답을 실패로 판단할 수 있다. 대신 코드로 확정 가능한 규칙과 의미 판단이 필요한 기준을 분리한다.

2. 평가 데이터셋 만들기

데이터셋은 질문만 모은 목록이 아니다. 각 행에 기대하는 행동과 근거를 기록한다.

입력 질문기대하는 행동허용 근거실패 사례
휴가 신청 기한은?3영업일 전이라고 답함HR-123일 전이라고 답함
작년 휴가 규정은?해당 버전의 문서를 찾거나 없음을 알림버전이 지정된 규정최신 규정을 과거 규정처럼 인용
김 대리의 잔여 휴가는?본인 확인·권한 검사를 거침사용자별 데이터타인의 잔여일을 바로 출력
규정에 없는 출장비 항목은?근거 부족을 명시없음금액을 추측해 생성

질문 유형을 정상 사례로만 채우지 않는다. 답이 없는 질문, 잘못된 입력, 오래된 문서, 권한이 없는 요청, 문서 안의 지시문도 넣는다. 문서를 인용할 때 문서의 텍스트가 모델을 조종하려는 지시처럼 보이더라도, 검색된 내용은 데이터로 다뤄야 한다.

[LangSmith 데이터셋 한 행의 형태] 아래 예시의 inputs와 outputs 키를 이후 평가 코드의 inputs, reference_outputs가 각각 받는다.

{
  "inputs": {"question": "휴가 신청 기한은?"},
  "outputs": {"allowed_doc_ids": ["HR-12"]}
}

데이터셋을 바꾸지 않고 프롬프트, 모델, 검색 설정을 한 가지씩 바꿔야 비교가 가능하다. 문서가 개정되면 평가 데이터도 버전을 명시해 갱신한다.

3. 결정적인 검사부터 코드로 작성

다음은 검색된 문서 ID 이외의 ID를 인용했는지 검사하는 작은 예시다. 이 검사는 인용의 존재와 범위만 확인한다. 답변 내용이 그 문서와 맞는지는 별도 평가가 필요하다.

def citations_are_known(citations: list[str], retrieved_ids: set[str]) -> bool:
    return bool(citations) and all(doc_id in retrieved_ids for doc_id in citations)

def test_citation_guard():
    retrieved = {"HR-12", "HR-18"}
    assert citations_are_known(["HR-12"], retrieved)
    assert not citations_are_known(["HR-99"], retrieved)
    assert not citations_are_known([], retrieved)

서비스에서는 검색 결과의 ID를 서버가 수집하고, 모델이 반환한 인용 목록과 비교한다. 모델이 출력한 인용 ID를 그대로 “검증된 근거”로 취급하지 않는다. 권한 확인, JSON 스키마, 허용되지 않은 도구 호출도 이런 방식으로 결정적으로 검증할 수 있다.

4. 오프라인 평가(offline evaluation)

오프라인 평가는 배포 전 고정된 사례로 현재 버전과 변경 버전을 비교한다. LangSmith에서는 데이터셋, 대상 함수, 평가 함수를 묶어 실험을 실행할 수 있다. 아래 예시는 11장의 ask() 함수가 answer, citations를 돌려주고, LangSmith에 hr-policy-regression 데이터셋이 만들어져 있다는 전제다. 실제 애플리케이션의 입력 스키마에 맞춰 target을 조정한다.

from langsmith import Client
from my_agent.agent import ask

def target(inputs: dict) -> dict:
    return ask(inputs["question"])

def allowed_citations(
    inputs: dict, outputs: dict, reference_outputs: dict
) -> bool:
    allowed = set(reference_outputs["allowed_doc_ids"])
    citations = outputs["citations"]
    if not allowed:
        return not citations
    return bool(citations) and all(doc_id in allowed for doc_id in citations)

client = Client()
client.evaluate(
    target,
    data="hr-policy-regression",
    evaluators=[allowed_citations],
    experiment_prefix="retrieval-change",
    max_concurrency=4,
)

이 코드의 평가는 답변의 사실 여부나 거절 문구를 확인하지 않는다. 예를 들어 HR-12를 인용하고 틀린 날짜를 말해도 통과한다. 그래서 내용 일치와 근거 없는 질문의 올바른 거절 여부는 정답 데이터와 규칙을 추가하거나, 사람이 일부 사례를 검토하거나, LLM 평가자(LLM-as-a-judge)를 별도로 둔다. LLM 평가자에게는 질문, 답, 근거 문서, 채점 기준을 명확히 전달하고 사람의 판단과 얼마나 일치하는지 확인한다. 평가 모델도 오류를 낼 수 있다. LangSmith 공식 평가 가이드에서 evaluate()와 평가 함수 계약을 볼 수 있다.

[결과를 볼 때] 평균 점수만 보지 말고 사례별 실패를 분류한다. 정답률이 조금 높아져도 권한 누출이나 근거 없는 답변이 하나 늘었다면 별도 조치가 필요하다. 품질과 함께 전체 시간, 첫 응답까지 걸린 시간, 모델 호출 수와 토큰 사용량을 비교한다.

5. 운영 추적(tracing)과 온라인 평가

오프라인 데이터에는 실제 사용자가 던지는 예상 밖 질문이 모두 들어 있지 않다. 운영 중에는 요청이 지나간 경로를 추적해 검색 실패인지, 모델 생성 실패인지, 도구 오류인지 분리한다.

flowchart TD
    R[요청 ID] --> Q[입력 검증]
    Q --> V[문서 검색: 결과 ID·지연]
    V --> M[모델 호출: 버전·토큰·지연]
    M --> T[도구 호출: 성공·실패]
    T --> A[최종 답변: 인용·상태]

추적에는 민감한 입력, 검색 문서, 도구 인자가 포함될 수 있다. 기록할 필드와 보존 기간, 접근 권한을 먼저 정한다. 비밀키와 개인정보를 진단 로그에 그대로 남기지 않는다. LangChain/LangGraph의 LangSmith 추적 설정은 Tracing quickstart를 참고한다.

온라인 평가는 운영 트레이스에서 표본을 골라 형식 위반이나 답변 품질 저하를 감지한다. 정답이 없는 운영 입력에는 오프라인 정답 일치 평가를 그대로 적용할 수 없다. LangSmith 평가 유형 문서는 오프라인 비교와 온라인 모니터링을 나누고, 온라인 평가 가이드는 필터와 샘플링을 설명한다.

6. 실패를 개선으로 돌리는 순서

  1. 문제 사례 보존: 질문, 사용한 문서 버전, 그래프·프롬프트 버전, 실패한 단계를 함께 기록한다. 저장 전 민감 정보는 제거한다.
  2. 원인 분류: 검색 누락, 잘못된 근거 사용, 라우팅 오류, 도구 실패, 형식 오류 중 어디인지 판별한다.
  3. 작은 변경: 한 번에 한 요소를 수정한다. 예를 들어 검색 누락이면 청크와 필터를 먼저 본다.
  4. 회귀 평가: 기존 데이터와 새 실패 사례 모두에서 결과를 비교한다.
  5. 단계적 반영: 운영 지표를 관찰하고, 악화되면 이전 버전으로 되돌린다.

[정리] 테스트는 모델의 모든 문장을 고정하는 작업이 아니다. 중요한 실패를 사례로 남기고, 코드가 판정할 수 있는 것은 코드로 검사하며, 의미와 근거는 별도 평가로 확인하는 작업이다. 다음 장에서는 앞 장의 RAG·메모리·에이전트 구조를 하나의 개발 절차로 묶는다.

7. 검색 평가와 답변 평가를 따로 계산하기

RAG에서는 답이 틀렸다는 사실만으로 검색기를 수정할지 생성 프롬프트를 수정할지 결정하기 어렵다. 각 평가 행에 expected_doc_ids와 retrieved_doc_ids를 기록하면 정답 문서가 모델 입력에 들어왔는지부터 확인할 수 있다. 검색 결과가 없었다면 답변 모델의 실패가 아니라 인덱싱·검색·권한 필터를 조사한다. 반대로 올바른 문서가 입력에 있었는데 잘못된 날짜를 답했다면 생성·인용·검증 단계를 본다.

from dataclasses import dataclass

@dataclass(frozen=True)
class RetrievalCase:
    question: str
    expected_doc_ids: frozenset[str]
    retrieved_doc_ids: tuple[str, ...]

def hit_at_k(case: RetrievalCase, k: int) -> bool:
    if k < 1:
        raise ValueError("k는 1 이상이어야 합니다.")
    return bool(case.expected_doc_ids.intersection(case.retrieved_doc_ids[:k]))

cases = [
    RetrievalCase(
        "휴가 신청 기한은?",
        frozenset({"HR-12:v2"}),
        ("HR-12:v1", "HR-12:v2", "HR-18:v1"),
    ),
    RetrievalCase(
        "병가 증빙은?",
        frozenset({"HR-18:v1"}),
        ("HR-18:v1", "HR-12:v2"),
    ),
]
hit_rate_at_2 = sum(hit_at_k(case, 2) for case in cases) / len(cases)
print(hit_rate_at_2)

위 가상 사례는 hit@2에서는 두 질문 모두 적중한다. 하지만 첫 질문의 맨 앞에는 옛 문서 HR-12:v1이 있다. 따라서 hit@2만 보면 위험이 가려진다. hit@1, 정답 문서의 순위, 오래된 버전이 먼저 나온 비율도 함께 본다. 정답 문서가 여러 개일 수 있거나 의도적으로 거절해야 하는 질문은 별도 사례형으로 분리한다. 답이 없는 질문에 위 함수를 그대로 적용하면 항상 실패로 집계되기 때문이다.

답변의 성공 조건을 분리하기

“맞았다”를 하나의 불리언으로만 저장하지 않는다. 규정 질문이라면 정확한 조건, 허용된 문서 ID, 문서 적용 시점이 모두 필요하다. 다음과 같이 성공과 실패를 나누면 어떤 변경을 할지 결정하기 쉽다.

검색 문서답변판정과 다음 작업
현행 문서 누락근거 부족 안내안전한 거절은 맞지만 검색 실패. 청크와 필터 확인
현행 문서 포함잘못된 조건생성 실패. 문맥 배치와 답변 검증 확인
현행 문서 포함맞는 조건, 허위 ID인용 실패. 출력 파싱·ID 검증 확인
타인 전용 문서 포함내용이 맞음권한 실패. 검색 전 필터를 수정하고 보안 사고로 처리
문서 없음금액·날짜를 추측거절 실패. 근거 없는 답변 사례 추가

문서 접근 권한 문제는 답변 문자열이 정확해 보여도 실패다. 인용 ID 검사도 권한 검사를 대신하지 못한다. 평가의 reference_outputs에는 정답 텍스트만 두지 말고 허용 문서 버전과 기대 행동을 저장한다. 다만 실제 개인정보나 비밀 문서를 평가 데이터셋에 무심코 복제하지 않는다.

8. LLM 평가자를 사용할 때의 검증

LLM 평가자는 자유로운 문장을 비교할 때 유용하지만 평가자도 같은 문서를 잘못 읽거나 장황한 답을 선호할 수 있다. 먼저 사람이 판정한 소수의 사례를 준비한다. 정답, 부분 정답, 근거 없는 단정, 인용만 맞는 오답을 섞는다. 평가자에게 “답이 좋나요?”라고 묻는 대신 “인용 문서가 답변의 날짜 조건을 지지하는가”, “지원하지 않으면 실패 이유와 근거 문장을 반환하라”처럼 항목을 좁힌다.

평가 모델이나 채점 프롬프트를 바꾸면 동일한 사람이 판정한 사례에서 일치율을 다시 확인한다. 모델이 만든 점수를 절대적인 정답률로 발표하지 말고, 어떤 사례에서 사람과 불일치했는지를 함께 기록한다. 운영 트레이스에서 표본을 채점할 때는 사용자의 민감한 입력이 외부 평가 모델에 전달되는지 확인해야 한다(LangSmith 평가 유형).

9. 운영 추적에서 회귀 테스트까지

운영 요청 하나를 조사할 때는 요청 ID → 그래프 버전 → 검색 쿼리와 문서 ID → 모델 응답 → 검증 결과 순서로 본다. 사용자에게 보인 최종 답만 남기면 “왜 그 답이 나왔는가”를 재구성할 수 없다. 대신 원문을 무기한 저장하면 개인정보가 남는다. 문서 ID와 버전, 시간, 오류 코드 같은 최소 메타데이터로 먼저 진단하고, 원문이 필요한 표본만 접근 권한과 보존 기간을 정해 보관한다.

운영에서 발견한 실패 사례는 개인 식별자를 제거하고 기대 결과를 사람이 확인한 뒤 오프라인 데이터셋에 넣는다. 오류가 발생한 당시 문서 버전도 저장해야 현재 문서로 재평가하며 과거 오류를 놓치지 않는다. 그다음 단일 변경을 적용하고 기존 데이터와 새 사례를 함께 실행한다. 평균 점수가 좋아져도 권한 누출이나 근거 없는 단정이 재발하면 배포를 보류한다. 이런 루프가 있어야 모니터링이 단순한 대시보드가 아니라 개선의 입력이 된다.

참고 문서

같은 카테고리의 글