🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

LangChain 테스트 전략 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 1. · 18 Views

LangChain 테스트 전략 완벽 가이드

LangChain 에이전트와 LLM 기반 애플리케이션을 효과적으로 테스트하는 방법을 다룹니다. 모킹부터 통합 테스트, LLM 판정관까지 실무에서 바로 적용할 수 있는 테스트 전략을 소개합니다.


목차

  1. GenericFakeChatModel_모킹
  2. Unit_Test_개별_도구_검증
  3. Integration_Test_궤적_평가
  4. AgentEvals_패키지_활용
  5. strict_unordered_궤적_매칭
  6. vcrpy_HTTP_녹음_재생
  7. LLM_판정관_품질_평가

1. GenericFakeChatModel 모킹

김개발 씨는 LangChain으로 챗봇을 개발하고 있었습니다. 테스트 코드를 작성하려는데 문득 고민이 생겼습니다.

"매번 OpenAI API를 호출하면 비용도 들고, 응답도 달라지는데 어떻게 테스트하지?"

GenericFakeChatModel은 실제 LLM을 호출하지 않고도 테스트할 수 있게 해주는 모킹 도구입니다. 마치 영화 촬영에서 스턴트맨이 배우를 대신하는 것처럼, 실제 LLM 대신 미리 정해진 응답을 반환합니다.

이를 활용하면 비용 걱정 없이 빠르고 일관된 테스트가 가능합니다.

다음 코드를 살펴봅시다.

from langchain_core.messages import AIMessage
from langchain_core.language_models import GenericFakeChatModel

# 테스트용 가짜 LLM 생성
fake_llm = GenericFakeChatModel(
    messages=iter([
        AIMessage(content="안녕하세요! 무엇을 도와드릴까요?"),
        AIMessage(content="서울의 날씨는 맑습니다."),
    ])
)

# 실제 LLM처럼 사용 가능
response = fake_llm.invoke("안녕하세요")
print(response.content)  # "안녕하세요! 무엇을 도와드릴까요?"

# 두 번째 호출은 다음 메시지 반환
response = fake_llm.invoke("날씨 알려줘")
print(response.content)  # "서울의 날씨는 맑습니다."

김개발 씨는 입사 6개월 차 주니어 개발자입니다. LangChain으로 고객 상담 챗봇을 개발하던 중, 테스트 코드를 작성해야 할 시점이 왔습니다.

그런데 문제가 있었습니다. 테스트를 실행할 때마다 OpenAI API를 호출하면 비용이 계속 발생하고, 같은 질문에도 매번 다른 답변이 나올 수 있었습니다.

선배 개발자 박시니어 씨가 김개발 씨의 화면을 보더니 말했습니다. "LLM 테스트할 때는 모킹을 사용해야 해요.

GenericFakeChatModel이라고 들어봤어요?" 그렇다면 GenericFakeChatModel이란 정확히 무엇일까요? 쉽게 비유하자면, 영화 촬영 현장의 스턴트맨과 같습니다.

위험한 액션 장면에서 실제 배우 대신 스턴트맨이 연기하듯이, 테스트 환경에서는 실제 LLM 대신 GenericFakeChatModel이 그 역할을 대신합니다. 외부에서 보기에는 똑같이 동작하지만, 내부적으로는 미리 정해진 응답만 반환합니다.

모킹 없이 테스트하던 시절에는 어땠을까요? 개발자들은 매번 실제 API를 호출해야 했습니다.

테스트를 10번 돌리면 10번의 API 비용이 발생했습니다. 더 큰 문제는 LLM의 비결정적 특성이었습니다.

같은 입력에도 다른 출력이 나올 수 있어서, 테스트가 어떨 때는 통과하고 어떨 때는 실패하는 불안정한 상황이 벌어졌습니다. 바로 이런 문제를 해결하기 위해 GenericFakeChatModel이 등장했습니다.

이 도구를 사용하면 테스트 비용이 0원이 됩니다. 네트워크 연결 없이도 테스트가 가능하고, 항상 동일한 응답을 반환하므로 테스트 결과가 일관됩니다.

무엇보다 테스트 속도가 비약적으로 빨라집니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 messages 파라미터에 iterator를 전달합니다. 이 iterator는 호출될 때마다 순서대로 응답을 반환합니다.

첫 번째 invoke 호출에는 첫 번째 메시지가, 두 번째 호출에는 두 번째 메시지가 반환됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 챗봇의 대화 흐름을 테스트한다고 가정해봅시다. 사용자가 인사하면 인사로 응답하고, 날씨를 물으면 날씨 정보를 제공하는 시나리오를 테스트할 수 있습니다.

미리 정의된 응답 시퀀스로 전체 대화 흐름이 의도대로 동작하는지 검증할 수 있습니다. 하지만 주의할 점도 있습니다.

GenericFakeChatModel은 단위 테스트에 적합합니다. 실제 LLM의 응답 품질이나 창의성을 테스트하려면 별도의 방법이 필요합니다.

또한 messages iterator가 소진되면 에러가 발생하므로, 충분한 수의 응답을 미리 준비해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다. "이거 쓰면 CI/CD 파이프라인에서도 안심하고 테스트를 돌릴 수 있겠네요!" GenericFakeChatModel을 활용하면 LLM 기반 애플리케이션도 일반 소프트웨어처럼 안정적으로 테스트할 수 있습니다.

실전 팁

💡 - messages에 리스트 대신 iterator를 사용하면 무한 응답도 생성 가능합니다

  • 실제 LLM과 동일한 인터페이스를 제공하므로 의존성 주입 패턴과 함께 사용하면 효과적입니다

2. Unit Test 개별 도구 검증

김개발 씨가 만든 에이전트에는 여러 도구가 연결되어 있었습니다. 검색 도구, 계산기 도구, 날씨 조회 도구까지.

어느 날 에이전트가 이상한 결과를 내놓았는데, 도대체 어떤 도구에서 문제가 생긴 건지 알 수가 없었습니다.

Unit Test는 에이전트의 개별 도구를 독립적으로 검증하는 테스트입니다. 마치 자동차 정비소에서 엔진, 브레이크, 타이어를 각각 점검하듯이, 각 도구가 올바르게 동작하는지 하나씩 확인합니다.

문제가 생겼을 때 원인을 빠르게 찾을 수 있습니다.

다음 코드를 살펴봅시다.

from langchain_core.tools import tool
import pytest

# 테스트할 도구 정의
@tool
def calculate_discount(price: float, percent: float) -> float:
    """가격에서 할인율을 적용한 최종 가격을 계산합니다."""
    if percent < 0 or percent > 100:
        raise ValueError("할인율은 0~100 사이여야 합니다")
    return price * (1 - percent / 100)

# 단위 테스트 작성
def test_calculate_discount_normal():
    """정상 케이스: 10000원에서 20% 할인"""
    result = calculate_discount.invoke({"price": 10000, "percent": 20})
    assert result == 8000.0

def test_calculate_discount_invalid_percent():
    """예외 케이스: 잘못된 할인율"""
    with pytest.raises(ValueError):
        calculate_discount.invoke({"price": 10000, "percent": 150})

김개발 씨는 쇼핑몰 고객 상담 에이전트를 개발하고 있었습니다. 이 에이전트에는 할인 계산 도구, 재고 조회 도구, 배송 추적 도구 등 여러 도구가 연결되어 있었습니다.

어느 날 고객이 "이 상품 30% 할인하면 얼마예요?"라고 물었는데, 에이전트가 엉뚱한 가격을 알려주는 일이 발생했습니다. 박시니어 씨가 다가와 물었습니다.

"각 도구별로 단위 테스트는 작성했어요?" 김개발 씨는 고개를 저었습니다. "에이전트 전체 테스트만 했는데요..." 박시니어 씨가 설명을 시작했습니다.

"에이전트 테스트에서 가장 기본이 되는 건 개별 도구의 단위 테스트예요." 단위 테스트란 무엇일까요? 비유하자면, 자동차를 조립하기 전에 각 부품을 검사하는 것과 같습니다.

엔진이 제대로 작동하는지, 브레이크가 잘 듣는지, 타이어 공기압은 적절한지 각각 확인합니다. 완성차가 문제를 일으켰을 때 어느 부품이 원인인지 바로 알 수 있습니다.

도구 단위 테스트가 없던 시절에는 어땠을까요? 에이전트 전체를 테스트해야 했습니다.

문제가 발생하면 LLM의 판단 오류인지, 도구의 버그인지, 입력 파싱 문제인지 구분하기 어려웠습니다. 디버깅에 몇 시간씩 소요되는 일이 다반사였습니다.

개별 도구를 단위 테스트하면 이런 문제가 해결됩니다. 도구는 순수 함수처럼 동작합니다.

입력이 같으면 출력도 항상 같습니다. LLM의 비결정성과 무관하게 도구의 로직만 검증할 수 있습니다.

버그가 발생하면 어떤 도구가 문제인지 즉시 파악됩니다. 위의 코드를 분석해보겠습니다.

@tool 데코레이터로 정의된 함수는 .invoke() 메서드로 호출할 수 있습니다. 테스트에서는 이 invoke 메서드를 사용해 도구를 직접 실행합니다.

정상 케이스에서는 10000원의 20% 할인이 8000원인지 확인합니다. 예외 케이스에서는 150%라는 불가능한 할인율에 대해 ValueError가 발생하는지 검증합니다.

실제 현업에서는 어떻게 활용할까요? 모든 도구에 대해 경계값 테스트를 작성하는 것이 좋습니다.

할인율 0%, 100%, 음수, 100 초과 등 극단적인 값에서 도구가 어떻게 동작하는지 확인합니다. 이렇게 하면 에이전트가 예상치 못한 입력을 받았을 때도 안정적으로 동작합니다.

주의할 점도 있습니다. 도구 단위 테스트는 도구 자체의 로직만 검증합니다.

LLM이 도구를 올바르게 선택하는지, 적절한 인자를 전달하는지는 별도의 통합 테스트가 필요합니다. 김개발 씨는 모든 도구에 단위 테스트를 추가한 후, 할인 계산 도구에서 버그를 발견했습니다.

"아, 소수점 계산에서 반올림 오류가 있었네요!" 문제의 원인을 5분 만에 찾을 수 있었습니다.

실전 팁

💡 - 모든 도구에 정상 케이스와 예외 케이스 테스트를 작성하세요

  • pytest의 parametrize를 활용하면 여러 입력값을 한 번에 테스트할 수 있습니다

3. Integration Test 궤적 평가

단위 테스트를 모두 통과했는데도 에이전트가 이상하게 동작했습니다. 각 도구는 문제없는데, 전체 흐름에서 뭔가 잘못된 것입니다.

김개발 씨는 에이전트가 도구를 호출한 순서를 확인해봐야겠다고 생각했습니다.

Integration Test 궤적 평가는 에이전트가 도구를 호출한 순서와 방식을 검증하는 테스트입니다. 마치 네비게이션이 목적지까지 안내한 경로를 확인하는 것처럼, 에이전트가 문제 해결을 위해 걸어간 경로(궤적)가 올바른지 평가합니다.

다음 코드를 살펴봅시다.

from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.language_models import GenericFakeChatModel

# 에이전트가 도구를 호출하는 시나리오 모킹
fake_responses = [
    AIMessage(
        content="",
        tool_calls=[{"name": "search_product", "args": {"query": "노트북"}, "id": "1"}]
    ),
    AIMessage(
        content="",
        tool_calls=[{"name": "check_inventory", "args": {"product_id": "LP001"}, "id": "2"}]
    ),
    AIMessage(content="노트북 LP001 상품이 재고 5개 있습니다.")
]

fake_llm = GenericFakeChatModel(messages=iter(fake_responses))

# 궤적 검증 함수
def verify_trajectory(messages, expected_tools):
    """에이전트의 도구 호출 순서가 올바른지 검증"""
    actual_tools = [
        msg.tool_calls[0]["name"]
        for msg in messages
        if hasattr(msg, "tool_calls") and msg.tool_calls
    ]
    assert actual_tools == expected_tools, f"Expected {expected_tools}, got {actual_tools}"

김개발 씨의 쇼핑 에이전트는 고객 질문에 답하기 위해 여러 도구를 순차적으로 호출해야 했습니다. "노트북 재고 있어요?"라는 질문에는 먼저 상품을 검색하고, 그 다음 재고를 확인해야 합니다.

그런데 어떤 경우에는 재고 확인을 먼저 하려다 에러가 발생했습니다. 박시니어 씨가 말했습니다.

"단위 테스트만으로는 부족해요. 에이전트의 궤적을 테스트해야 해요." 궤적이란 무엇일까요?

네비게이션을 생각해보세요. 서울에서 부산까지 가는 방법은 여러 가지입니다.

경부고속도로를 탈 수도 있고, 해안도로를 따라 갈 수도 있습니다. 네비게이션이 안내한 경로 전체가 바로 궤적입니다.

에이전트도 마찬가지로, 문제를 해결하기 위해 거쳐간 도구 호출의 순서가 궤적입니다. 왜 궤적 평가가 필요할까요?

각 도구가 완벽하게 동작해도, 호출 순서가 잘못되면 전체 결과가 틀어집니다. 상품 ID를 모르는 상태에서 재고를 조회하면 에러가 납니다.

로그인 전에 장바구니를 조회하면 빈 결과가 반환됩니다. 궤적 평가는 이런 통합적인 흐름을 검증합니다.

위 코드에서 fake_responses를 보면, 에이전트가 먼저 search_product를 호출하고, 그 다음 check_inventory를 호출하는 시나리오를 설정했습니다. verify_trajectory 함수는 실제 에이전트가 이 순서대로 도구를 호출했는지 확인합니다.

실무에서는 어떻게 활용할까요? 복잡한 에이전트일수록 궤적 테스트가 중요합니다.

고객 환불 처리 에이전트라면 주문 조회 → 환불 가능 여부 확인 → 환불 처리 → 알림 발송 순서가 지켜져야 합니다. 이 순서가 틀어지면 환불이 중복 처리되거나 알림이 누락될 수 있습니다.

주의할 점이 있습니다. 모든 궤적을 테스트할 필요는 없습니다.

핵심 비즈니스 로직이 담긴 주요 시나리오만 테스트하세요. 궤적 테스트가 너무 많으면 유지보수가 어려워집니다.

김개발 씨는 궤적 테스트를 추가한 후, 에이전트가 가끔 도구 호출 순서를 건너뛰는 문제를 발견했습니다. 프롬프트를 수정해 문제를 해결할 수 있었습니다.

실전 팁

💡 - 핵심 시나리오의 정상 궤적과 예외 궤적을 모두 테스트하세요

  • 궤적이 너무 길면 중요한 단계만 추출하여 검증하는 것도 방법입니다

4. AgentEvals 패키지 활용

김개발 씨는 궤적 검증 코드를 직접 작성하다 보니 테스트 코드가 점점 복잡해졌습니다. 비슷한 패턴의 코드를 반복 작성하는 것도 지쳤습니다.

"이런 걸 쉽게 해주는 라이브러리가 없을까요?"

AgentEvals는 LangChain 에이전트 테스트를 위한 전문 패키지입니다. 마치 pytest가 일반 파이썬 테스트를 편리하게 해주듯이, AgentEvals는 에이전트의 궤적 평가, 도구 호출 검증 등을 간편하게 수행할 수 있게 해줍니다.

복잡한 검증 로직을 한 줄의 코드로 처리할 수 있습니다.

다음 코드를 살펴봅시다.

from agent_evals.trajectory import check_trajectory
from agent_evals.matchers import ToolCallMatcher

# 예상 궤적 정의
expected_trajectory = [
    ToolCallMatcher(name="search_product", args={"query": "노트북"}),
    ToolCallMatcher(name="check_inventory"),  # args 생략 가능
    ToolCallMatcher(name="calculate_price"),
]

# 실제 에이전트 실행 결과
actual_messages = run_agent("노트북 가격 알려줘")

# 궤적 검증 (한 줄로 완료)
result = check_trajectory(
    actual=actual_messages,
    expected=expected_trajectory,
    mode="strict"  # 순서와 내용 모두 일치해야 함
)

assert result.passed, f"궤적 불일치: {result.diff}"

김개발 씨는 테스트 코드를 작성하면서 점점 지쳐가고 있었습니다. 궤적을 검증하려면 메시지를 파싱하고, 도구 호출을 추출하고, 순서를 비교하고...

비슷한 코드를 계속 반복 작성해야 했습니다. 박시니어 씨가 조언했습니다.

"AgentEvals 패키지를 써보세요. 궤적 테스트에 특화된 도구예요." AgentEvals란 무엇일까요?

일반 테스트에 pytest가 있다면, 에이전트 테스트에는 AgentEvals가 있습니다. 에이전트 특유의 테스트 패턴들을 추상화하여 제공합니다.

궤적 비교, 도구 호출 매칭, 부분 일치 검증 등 자주 필요한 기능이 이미 구현되어 있습니다. 직접 구현했을 때와 비교해볼까요?

직접 구현하면 메시지에서 tool_calls를 추출하고, 이름과 인자를 비교하고, 차이점을 찾아 리포트하는 코드를 모두 작성해야 합니다. 수십 줄의 코드가 필요합니다.

AgentEvals를 사용하면 check_trajectory 한 줄로 끝납니다. 코드를 살펴보겠습니다.

ToolCallMatcher는 예상되는 도구 호출을 정의합니다. name은 도구 이름, args는 인자입니다.

args를 생략하면 인자와 관계없이 도구 이름만 매칭합니다. 이렇게 정의한 예상 궤적을 check_trajectory에 전달하면 실제 궤적과 비교합니다.

**mode="strict"**는 순서와 내용이 모두 일치해야 통과합니다. 나중에 살펴볼 "unordered" 모드도 있습니다.

result 객체에는 테스트 통과 여부(passed)와 차이점(diff)이 담겨 있습니다. 테스트가 실패하면 어디가 다른지 상세히 알려줍니다.

실무에서는 어떻게 활용할까요? CI/CD 파이프라인에 에이전트 테스트를 포함할 때 유용합니다.

코드 변경 후 에이전트의 동작이 달라졌는지 자동으로 검증할 수 있습니다. 특히 프롬프트 변경 시 의도치 않은 궤적 변화를 감지하는 데 효과적입니다.

주의할 점도 있습니다. AgentEvals는 외부 패키지이므로 버전 호환성을 확인해야 합니다.

LangChain 버전이 올라가면 메시지 구조가 변경될 수 있으므로, 패키지 업데이트 시 테스트가 깨지지 않는지 확인하세요. 김개발 씨는 AgentEvals를 도입한 후, 테스트 코드가 절반으로 줄었습니다.

"이렇게 편한 도구가 있었다니!"

실전 팁

💡 - args를 부분적으로만 검증하고 싶다면 args={"key": ANY}처럼 ANY 매처를 활용하세요

  • check_trajectory의 결과에서 diff를 출력하면 디버깅에 도움이 됩니다

5. strict unordered 궤적 매칭

김개발 씨가 새로운 테스트를 작성하다가 고민에 빠졌습니다. 에이전트가 상품 정보와 리뷰를 조회하는데, 이 두 가지를 어떤 순서로 조회하든 상관없었습니다.

그런데 strict 모드에서는 순서가 다르면 테스트가 실패했습니다.

strictunordered는 궤적 매칭의 두 가지 모드입니다. strict는 도구 호출 순서까지 정확히 일치해야 하고, unordered는 순서와 상관없이 호출된 도구들만 일치하면 됩니다.

마치 요리 레시피에서 "순서대로 넣으세요"와 "재료만 모두 넣으면 됩니다"의 차이와 같습니다.

다음 코드를 살펴봅시다.

from agent_evals.trajectory import check_trajectory
from agent_evals.matchers import ToolCallMatcher

expected = [
    ToolCallMatcher(name="get_product_info"),
    ToolCallMatcher(name="get_reviews"),
    ToolCallMatcher(name="calculate_rating"),
]

# strict 모드: 순서까지 일치해야 함
result_strict = check_trajectory(
    actual=agent_messages,
    expected=expected,
    mode="strict"
)
# get_reviews → get_product_info 순서면 실패

# unordered 모드: 호출된 도구만 일치하면 됨
result_unordered = check_trajectory(
    actual=agent_messages,
    expected=expected,
    mode="unordered"
)
# 순서 상관없이 세 도구가 모두 호출되면 통과

# 부분 일치: 특정 도구만 포함되어 있는지 확인
result_contains = check_trajectory(
    actual=agent_messages,
    expected=[ToolCallMatcher(name="get_product_info")],
    mode="contains"
)

김개발 씨의 상품 상세 페이지 에이전트는 상품 정보와 리뷰를 함께 보여줘야 했습니다. 그런데 LLM의 특성상, 어떤 때는 상품 정보를 먼저 조회하고 어떤 때는 리뷰를 먼저 조회했습니다.

결과는 동일한데 순서만 달라서 테스트가 실패하는 게 합리적이지 않았습니다. 박시니어 씨가 말했습니다.

"순서가 중요하지 않은 경우에는 unordered 모드를 사용하면 돼요." 두 모드의 차이를 요리에 비유해볼까요? strict 모드는 정밀한 베이킹 레시피와 같습니다.

밀가루를 먼저 넣고, 설탕을 넣고, 달걀을 넣는 순서가 정해져 있습니다. 순서가 바뀌면 케이크가 제대로 안 부풀 수 있습니다.

unordered 모드는 샐러드 만들기와 같습니다. 토마토를 먼저 넣든 양상추를 먼저 넣든, 모든 재료가 들어가기만 하면 됩니다.

순서는 결과에 영향을 주지 않습니다. 언제 어떤 모드를 사용해야 할까요?

strict 모드는 순서가 비즈니스 로직상 중요할 때 사용합니다. 인증 → 조회 → 수정 순서가 필수인 경우, 로그인 전에 다른 작업이 불가능한 경우 등입니다.

unordered 모드는 순서가 결과에 영향을 주지 않을 때 사용합니다. 여러 데이터를 병렬로 조회하는 경우, 독립적인 검증 단계들이 있는 경우 등입니다.

코드에서 contains 모드도 있습니다. 이 모드는 특정 도구가 포함되어 있는지만 확인합니다.

다른 도구가 추가로 호출되어도 상관없습니다. 에이전트가 확장될 때 기존 테스트가 깨지지 않도록 하는 데 유용합니다.

실무에서의 활용 팁을 알려드리겠습니다. 처음에는 strict 모드로 시작하세요.

테스트가 불안정하게 실패한다면, 그 실패가 정말 버그인지 아니면 순서만 다른 건지 분석하세요. 순서가 중요하지 않다고 판단되면 unordered로 변경합니다.

주의할 점이 있습니다. unordered 모드를 남용하면 안 됩니다.

순서가 중요한데 unordered를 사용하면 실제 버그를 놓칠 수 있습니다. 각 테스트 케이스의 의도를 명확히 하고 적절한 모드를 선택하세요.

김개발 씨는 상품 조회 테스트를 unordered로 변경하고, 결제 처리 테스트는 strict로 유지했습니다. 테스트가 안정적으로 통과하면서도 중요한 순서는 검증할 수 있게 되었습니다.

실전 팁

💡 - 기본적으로 strict를 사용하고, 필요한 경우에만 unordered로 완화하세요

  • contains 모드는 회귀 테스트에 유용합니다. 핵심 기능이 유지되는지만 확인합니다

6. vcrpy HTTP 녹음 재생

테스트 환경에서 외부 API를 호출하는 것은 항상 고민거리였습니다. 네트워크가 불안정하면 테스트가 실패하고, API 서버가 다운되면 개발이 멈춥니다.

김개발 씨는 "실제 API 응답을 저장해두고 재사용할 수 없을까?"라고 생각했습니다.

vcrpy는 HTTP 요청과 응답을 녹음하고 재생하는 라이브러리입니다. 마치 비디오 레코더가 영상을 녹화하고 재생하듯이, 처음 테스트할 때 실제 API 응답을 파일로 저장하고, 이후 테스트에서는 저장된 응답을 재생합니다.

네트워크 없이도 일관된 테스트가 가능해집니다.

다음 코드를 살펴봅시다.

import vcr
from langchain_openai import ChatOpenAI

# VCR 카세트 설정 (응답 저장 위치)
my_vcr = vcr.VCR(
    cassette_library_dir="tests/cassettes",
    record_mode="once",  # 처음 한 번만 녹음
    match_on=["method", "scheme", "host", "port", "path"],
    filter_headers=["authorization"],  # API 키 제거
)

# 테스트에서 사용
@my_vcr.use_cassette("test_chat_completion.yaml")
def test_openai_chat():
    llm = ChatOpenAI(model="gpt-4o-mini")
    response = llm.invoke("안녕하세요")

    # 첫 실행: 실제 API 호출 후 응답 저장
    # 두 번째 실행부터: 저장된 응답 재생
    assert response.content is not None
    assert len(response.content) > 0

# 민감 정보 필터링
@my_vcr.use_cassette("test_filtered.yaml")
def test_with_filtering():
    # API 키가 자동으로 필터링되어 저장됨
    pass

김개발 씨는 OpenAI API를 사용하는 기능을 테스트하고 있었습니다. 그런데 문제가 있었습니다.

회사 네트워크가 불안정할 때 테스트가 실패하고, 주말에 집에서 작업할 때 API 키 없이는 테스트를 돌릴 수도 없었습니다. 박시니어 씨가 해결책을 제시했습니다.

"vcrpy를 써봐요. HTTP 응답을 녹음해두면 네트워크 없이도 테스트할 수 있어요." vcrpy의 이름은 어디서 왔을까요?

옛날 비디오테이프 레코더(VCR)를 기억하시나요? TV 프로그램을 테이프에 녹화해두고 나중에 재생하던 그 장치입니다.

vcrpy도 마찬가지로, HTTP 통신을 카세트라는 파일에 녹화해두고 나중에 재생합니다. 어떻게 동작하는지 살펴볼까요?

처음 테스트를 실행하면 vcrpy는 실제 네트워크 요청을 수행합니다. 이때 요청과 응답을 YAML 파일로 저장합니다.

두 번째 실행부터는 네트워크 요청을 가로채서, 저장된 응답을 반환합니다. 실제 API는 호출되지 않습니다.

코드를 분석해보겠습니다. **record_mode="once"**는 카세트 파일이 없을 때만 녹음하고, 이미 있으면 재생만 합니다.

match_on 설정은 어떤 요청을 같은 요청으로 볼지 결정합니다. filter_headers는 민감한 정보를 필터링합니다.

API 키가 포함된 Authorization 헤더가 파일에 저장되지 않습니다. vcrpy를 사용하면 얻는 이점이 많습니다.

테스트 속도가 빨라집니다. 네트워크 왕복 시간이 없으므로 밀리초 단위로 테스트가 완료됩니다.

테스트 안정성이 높아집니다. 네트워크 상태나 API 서버 상태와 무관하게 동일한 결과를 얻습니다.

비용 절감도 됩니다. LLM API 호출 비용이 한 번만 발생합니다.

실무에서 주의할 점이 있습니다. API 응답이 변경되면 카세트 파일을 재녹음해야 합니다.

오래된 카세트로 테스트하면 실제 API와 다른 동작을 테스트하게 됩니다. 주기적으로 카세트를 갱신하는 프로세스가 필요합니다.

또한 민감 정보 관리에 주의하세요. filter_headers로 API 키를 필터링해도, 응답 본문에 민감한 정보가 포함될 수 있습니다.

카세트 파일을 Git에 커밋하기 전에 검토하세요. 김개발 씨는 vcrpy를 도입한 후, 비행기 안에서도 테스트를 돌릴 수 있게 되었습니다.

"오프라인에서도 개발이 되다니!"

실전 팁

💡 - record_mode="new_episodes"를 사용하면 새로운 요청만 추가 녹음됩니다

  • 카세트 파일은 코드 리뷰 대상에 포함하여 예상치 못한 데이터 노출을 방지하세요

7. LLM 판정관 품질 평가

김개발 씨는 테스트를 모두 통과했는데도 찜찜했습니다. 궤적은 올바르지만, 에이전트의 최종 답변 품질은 어떻게 평가해야 할까요?

사람이 일일이 확인하기에는 케이스가 너무 많았습니다.

LLM 판정관은 다른 LLM의 출력을 평가하는 LLM입니다. 마치 논문 심사위원이 논문을 평가하듯이, 판정관 LLM이 에이전트의 답변이 정확하고 적절한지 점수를 매깁니다.

정량적인 품질 평가를 자동화할 수 있어, 대규모 테스트에 유용합니다.

다음 코드를 살펴봅시다.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

# 판정관 LLM 설정
judge_llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 평가 프롬프트 템플릿
evaluation_prompt = ChatPromptTemplate.from_messages([
    ("system", """당신은 AI 응답 품질 평가 전문가입니다.
주어진 질문과 응답을 평가하여 1-5점 척도로 점수를 매기세요.

평가 기준:
- 정확성: 사실적으로 올바른가?
- 완전성: 질문에 충분히 답했는가?
- 명확성: 이해하기 쉽게 작성되었는가?

JSON 형식으로 응답하세요: {{"score": N, "reasoning": "이유"}}"""),
    ("human", "질문: {question}\n\n응답: {answer}")
])

# 평가 체인
evaluation_chain = evaluation_prompt | judge_llm

# 품질 평가 실행
def evaluate_response(question: str, answer: str) -> dict:
    result = evaluation_chain.invoke({
        "question": question,
        "answer": answer
    })
    import json
    return json.loads(result.content)

# 사용 예시
score = evaluate_response(
    question="파이썬의 리스트와 튜플의 차이점은?",
    answer="리스트는 수정 가능하고 튜플은 수정 불가능합니다."
)
print(f"점수: {score['score']}, 이유: {score['reasoning']}")

김개발 씨의 고객 상담 에이전트는 수백 가지 질문에 답변해야 했습니다. 테스트 케이스를 모두 작성했지만, 답변의 품질까지 검증하기는 어려웠습니다.

"문법적으로 맞는 답변"과 "고객에게 도움이 되는 답변"은 다른 문제였습니다. 박시니어 씨가 제안했습니다.

"LLM으로 LLM을 평가해보는 건 어때요? LLM 판정관이라고 해요." LLM 판정관이란 무엇일까요?

논문 심사 과정을 생각해보세요. 연구자가 논문을 작성하면, 다른 전문가들이 그 논문을 평가합니다.

내용이 정확한지, 논리가 타당한지, 기여도가 있는지 점수를 매깁니다. LLM 판정관도 마찬가지입니다.

하나의 LLM이 다른 LLM의 출력을 평가합니다. 왜 LLM을 평가자로 사용할까요?

사람이 평가하면 가장 정확하지만, 확장성이 없습니다. 수천 개의 테스트 케이스를 사람이 일일이 평가할 수 없습니다.

LLM 판정관은 24시간 쉬지 않고 평가할 수 있고, 일관된 기준을 적용합니다. 코드를 살펴보겠습니다.

evaluation_prompt에서 평가 기준을 명확히 정의합니다. 정확성, 완전성, 명확성 세 가지 기준으로 1-5점을 매깁니다.

JSON 형식으로 응답하도록 하여 프로그래밍적으로 처리하기 쉽게 합니다. temperature=0으로 설정한 이유가 있습니다.

평가는 일관되어야 합니다. 같은 응답에 대해 어떨 때는 3점, 어떨 때는 5점을 주면 신뢰할 수 없습니다.

temperature를 낮추면 결정적인 출력을 얻을 수 있습니다. 실무에서는 어떻게 활용할까요?

회귀 테스트에 유용합니다. 프롬프트를 변경한 후, 기존 테스트 케이스에 대한 평균 점수가 떨어졌는지 확인합니다.

점수가 떨어지면 변경을 재고합니다. A/B 테스트에도 활용됩니다.

두 가지 프롬프트 버전의 평균 점수를 비교하여 더 나은 버전을 선택합니다. 주의할 점이 있습니다.

LLM 판정관도 완벽하지 않습니다. 특히 전문 분야의 정확성은 잘못 평가할 수 있습니다.

의료, 법률 등 전문 지식이 필요한 영역에서는 사람의 검토를 병행해야 합니다. 또한 비용을 고려하세요.

매 테스트마다 GPT-4o를 호출하면 비용이 누적됩니다. 중요한 테스트 케이스만 선별하거나, 저렴한 모델로 1차 필터링 후 고품질 모델로 최종 평가하는 방법을 사용할 수 있습니다.

김개발 씨는 LLM 판정관을 도입한 후, 주간 품질 리포트를 자동으로 생성할 수 있게 되었습니다. 에이전트의 품질이 꾸준히 향상되는 것을 수치로 확인할 수 있었습니다.

실전 팁

💡 - 평가 기준은 구체적일수록 좋습니다. "좋은 답변"보다 "사실적으로 정확한 답변"이 낫습니다

  • 여러 판정관의 점수를 평균 내면 단일 판정관보다 안정적인 결과를 얻습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#LangChain#Testing#GenericFakeChatModel#AgentEvals#vcrpy#AI,LLM,Python,LangChain

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스