티스토리 뷰

반응형

LangChain 백엔드를 테스트 가능하게 만들기 — 프롬프트, tool, structured output을 어떻게 검증하나

운영 이야기를 하고 나면, 꼭 다음 질문이 나옵니다.

“문제가 생기면 trace로 보면 되지 않나?”
맞습니다. 그런데 그건 이미 깨진 뒤 이야기예요.

실제로 더 중요한 건 그 앞 단계입니다.
애초에 깨지기 전에 잡는 것.

특히 LangChain 백엔드는 더 그렇습니다.
일반 API처럼 입력과 출력이 100% 결정적이지 않아서, “대충 잘 되네”로 넘어가면 나중에 같은 질문에 조금 다른 답이 나와도 놓치기 쉽거든요. LangSmith는 바로 이 지점을 위해 observability와 별도로 evaluation을 제공하고, “Test before you ship”라는 표현으로 개발 단계에서 curated dataset으로 버전 비교와 회귀(regression) 탐지를 하라고 안내합니다. (LangChain Docs)

저는 이 구간이 진짜 중요하다고 생각합니다.
여기서부터 AI 백엔드가 “잘 되는 것 같음”에서 “검증 가능한 시스템”으로 넘어가거든요.


왜 LLM 백엔드는 테스트 방식이 달라야 할까

기존 백엔드 테스트는 보통 이런 식이죠.

  • 입력을 넣는다
  • 예상 출력과 정확히 같은지 본다

그런데 LLM은 같은 의미를 다른 문장으로 표현할 수 있습니다.
그래서 문자열 완전 일치만으로는 품질을 보기 어렵습니다.

대신 보통 이런 층위로 나눠서 봐야 합니다.

  • Structured output이 스키마에 맞는가
  • 필수 필드가 빠지지 않았는가
  • tool을 써야 할 때 제대로 썼는가
  • 최종 답이 의도한 기준을 충족하는가

LangSmith의 agent evaluation 문서는 agent 평가를 최소 세 가지 층위로 보라고 설명합니다.
최종 응답(final response), 도구 호출 궤적(trajectory), 개별 step(single step) 입니다. 이 분리가 중요한 이유는, 최종 답이 틀렸을 때도 문제 원인이 tool 선택인지, tool 인자인지, 마지막 정리 단계인지 다를 수 있기 때문입니다. (LangChain Docs)

즉, 이제부터는 “정답 문장 하나”만 보는 게 아니라
프롬프트, tool, structured output을 나눠서 검증해야 합니다.


이번 글에서 얻어갈 것

이번 글에서는 딱 이 네 가지를 잡겠습니다.

  • structured output은 어떻게 테스트하면 좋은지
  • tool 호출은 무엇을 검증해야 하는지
  • FastAPI 엔드포인트는 어디까지 테스트하면 되는지
  • LangSmith dataset/evaluation 흐름을 어떻게 붙이면 좋은지

너무 거대한 테스트 플랫폼 얘기보다는,
지금 만든 LangChain + FastAPI 사내 도우미에 바로 적용할 수 있는 방식으로 가겠습니다.


1. 테스트 대상을 먼저 나눠야 한다

처음에는 보통 “assistant 전체를 테스트하자”로 가기 쉬워요.
그런데 그렇게 하면 문제 원인이 섞입니다.

예를 들어 답이 이상했을 때,

  • 프롬프트 문제인지
  • tool 설계 문제인지
  • structured output 문제인지
  • API 직렬화 문제인지

한 번에 다 섞여 보이죠.

그래서 저는 보통 아래처럼 나눕니다.

1) schema 테스트

structured output이 항상 우리가 정한 형태를 지키는지

2) tool 테스트

tool 함수 자체가 안정적으로 동작하는지

3) agent 테스트

질문에 따라 tool을 제대로 고르고, 답을 제대로 만드는지

4) API 테스트

FastAPI endpoint가 계약대로 응답하는지

LangSmith의 evaluation concepts 문서는 experiment를 dataset 위에서 특정 application version을 돌린 결과로 설명하고, output, evaluator scores, execution traces를 함께 본다고 안내합니다. 즉, “앱 전체”도 볼 수 있지만, 실제로는 구성 요소별 관점을 같이 가져가는 게 훨씬 실용적입니다. (LangChain Docs)


2. structured output은 가장 먼저 테스트하기 좋은 레이어다

이건 꽤 좋은 출발점입니다.

왜냐면 structured output은 비교적 결정적이기 때문이에요.
문장 스타일은 조금 달라도, 최소한 아래는 지켜야 하죠.

  • answer가 비어 있지 않다
  • response_type이 허용 enum 중 하나다
  • used_tools는 리스트다
  • sources는 리스트다

LangChain structured output 문서는 create_agent(..., response_format=...)를 쓰면 최종 응답이 스키마에 맞게 검증된 structured data로 반환된다고 설명합니다. 또 v1 문서는 structured output이 메인 agent loop 안에 통합돼 있고, provider-native structured output이나 tool strategy를 선택할 수 있다고 안내합니다. (LangChain Docs)

즉, 최소 테스트는 이 정도로 시작할 수 있습니다.

from app.agent import ask_assistant


def test_structured_response_shape():
    result = ask_assistant(
        session_id="test-session",
        question="재택근무 정책 알려줘.",
    )

    assert result.answer
    assert result.response_type in {
        "policy_answer",
        "request_status",
        "summary",
        "unknown",
    }
    assert isinstance(result.used_tools, list)
    assert isinstance(result.sources, list)

이 테스트가 좋은 이유는
“문장 완전 일치”를 강요하지 않으면서도
API 계약이 깨졌는지는 바로 볼 수 있기 때문입니다.


3. tool 함수는 LLM 바깥에서 먼저 테스트해야 한다

이건 정말 중요합니다.

agent 테스트에서 tool이 잘못되면 원인을 보기가 어려워집니다.
그래서 tool은 먼저 일반 Python 함수처럼 독립 테스트하는 게 좋습니다.

LangChain tools 문서는 tool이 결국 well-defined inputs and outputs를 가진 callable function이라고 설명합니다. 즉 agent에 붙기 전에 함수 자체의 안정성부터 보는 게 맞습니다. (LangChain Docs)

예를 들어 get_request_status는 이런 식으로 볼 수 있습니다.

import json
from app.tools import get_request_status


def test_get_request_status_found():
    raw = get_request_status.invoke({"request_id": "2048"})
    data = json.loads(raw)

    assert data["found"] is True
    assert data["request"]["request_id"] == "2048"
    assert data["request"]["status"] == "처리 완료"


def test_get_request_status_not_found():
    raw = get_request_status.invoke({"request_id": "9999"})
    data = json.loads(raw)

    assert data["found"] is False
    assert data["request_id"] == "9999"

이런 테스트는 되게 기본적이지만 효과가 큽니다.
나중에 agent가 이상하게 행동했을 때 “적어도 tool 자체는 멀쩡하다”는 걸 먼저 확보할 수 있으니까요.


4. agent 테스트는 “최종 답”만 보지 말고 tool 사용도 같이 봐야 한다

반응형

이제부터 조금 더 LangChain스러운 테스트입니다.

예를 들어 이런 질문이 있죠.

  • “재택근무 정책 알려줘.”
  • “요청 ID 2048 상태 알려줘.”

이 두 질문은 최종 답도 중요하지만,
사실 어떤 tool을 썼는지도 같이 중요합니다.

LangSmith complex agent evaluation 문서는 agent 평가에서 final response뿐 아니라 trajectory도 보라고 설명합니다. trajectory는 어떤 tool call이 어떤 순서로 일어났는지를 보는 관점이에요. (LangChain Docs)

즉, 이런 테스트 감각이 필요합니다.

from app.agent import ask_assistant


def test_policy_question_uses_policy_tool():
    result = ask_assistant(
        session_id="test-policy",
        question="재택근무 정책 알려줘.",
    )

    assert result.response_type == "policy_answer"
    assert "search_policy_documents" in result.used_tools


def test_request_question_uses_request_tool():
    result = ask_assistant(
        session_id="test-request",
        question="요청 ID 2048 상태 알려줘.",
    )

    assert result.response_type == "request_status"
    assert "get_request_status" in result.used_tools

이건 문장 자체를 꽉 묶지 않으면서도,
agent가 적어도 올바른 방향으로 행동했는지는 확인할 수 있습니다.


5. FastAPI 테스트는 응답 계약 중심으로 보면 된다

FastAPI는 여기서 꽤 편합니다.
공식 문서가 request body와 response model을 Pydantic으로 다루는 방식을 기본으로 안내하는 이유도 바로 이런 테스트 용이성 때문이에요. response model은 반환 데이터 검증과 필터링을 맡습니다. (LangChain Docs)

예를 들어 endpoint 테스트는 이렇게 볼 수 있습니다.

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)


def test_assistant_ask_endpoint():
    response = client.post(
        "/assistant/ask",
        json={
            "session_id": "api-test-1",
            "question": "재택근무 정책 알려줘."
        },
    )

    assert response.status_code == 200
    data = response.json()

    assert "answer" in data
    assert "response_type" in data
    assert "used_tools" in data
    assert "sources" in data
    assert data["response_type"] in {
        "policy_answer",
        "request_status",
        "summary",
        "unknown",
    }

이 테스트의 포인트는
응답 문장 자체보다 API 계약이 유지되는지를 본다는 점입니다.

즉, 프론트엔드가 기대하는 JSON shape가 깨지지 않는지 보는 거예요.


6. 스트리밍 endpoint는 어떻게 테스트할까

이건 조금 더 까다롭습니다.
그래도 핵심은 단순합니다.

  • 줄 단위 JSON이 오는지
  • type 필드가 있는지
  • 마지막에 final 이벤트가 오는지

FastAPI StreamingResponse는 chunk를 그대로 전달하므로, raw response stream을 읽어 줄 단위로 검증하는 방식이 자연스럽습니다. FastAPI 문서도 스트리밍 응답은 일반 response model 검증 흐름과 다르다고 설명합니다. (LangChain Docs)

개념 예시는 이런 느낌입니다.

import json
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)


def test_stream_endpoint_emits_final_event():
    with client.stream(
        "POST",
        "/assistant/stream",
        json={
            "session_id": "stream-test-1",
            "question": "재택근무 정책 알려줘."
        },
    ) as response:
        assert response.status_code == 200

        final_seen = False
        for line in response.iter_lines():
            if not line:
                continue
            event = json.loads(line)
            assert "type" in event

            if event["type"] == "final":
                final_seen = True
                assert "data" in event
                assert "answer" in event["data"]

        assert final_seen is True

이 정도만 해도 스트리밍 엔드포인트가 최소한 프로토콜을 지키는지는 확인할 수 있습니다.


7. LangSmith evaluation은 “좋은 질문 세트”가 핵심이다

여기서부터는 본격적인 평가 얘기입니다.

LangSmith evaluation 문서는 curated dataset 위에서 application version을 비교하고, regression을 잡는 흐름을 설명합니다. evaluate-a-chatbot, evaluate-a-RAG-app 튜토리얼도 공통적으로 dataset → run app → measure with evaluators 구조를 따릅니다. (LangChain Docs)

이 말은 결국 이겁니다.

좋은 평가를 하려면 좋은 테스트 질문 세트가 먼저 필요하다.

예를 들어 지금 사내 도우미라면 이런 식이 좋아요.

정책 질문

  • “재택근무 정책 알려줘.”
  • “출장비 정산은 언제까지 해야 해?”
  • “연차 신청은 어떻게 해?”

상태 조회 질문

  • “요청 ID 2048 상태 알려줘.”
  • “3001번 요청은 지금 어디까지 됐어?”

요약 질문

  • “재택근무 정책 한 줄로 요약해줘.”
  • “출장비 정책이랑 요청 3001 상태를 짧게 정리해줘.”

엣지 케이스

  • 존재하지 않는 요청 ID
  • 문서에 없는 질문
  • 정책과 상태 조회가 섞인 질문

이런 세트를 dataset으로 만들어두면,
나중에 프롬프트를 바꾸거나 tool을 수정해도
“전보다 나아졌는지, 나빠졌는지”를 비교할 수 있습니다.

LangSmith dataset 관리 문서는 datasets가 repeatable evaluations의 핵심이라고 설명합니다. examples는 inputs와 기대 output 또는 metadata를 담는 단위예요. (LangChain Docs)


8. LLM 평가에서는 “정확히 같은 문장”보다 기준 기반 평가가 더 낫다

이건 꼭 짚고 가야 합니다.

예를 들어 기대 답이
“재택근무는 주 2회까지 가능하며, 예외는 팀장 승인 후 가능합니다.”
였다고 해봅시다.

실제 모델 답이
“재택근무는 기본적으로 주 2회까지 허용되고, 예외는 팀장 승인을 거쳐 가능합니다.”
라고 나오면 틀린 걸까요?

문장만 보면 다르지만 의미는 비슷하죠.

그래서 LangSmith evaluation approaches 문서는 LLM-as-judge를 포함한 evaluator를 소개하고, RAG나 chatbot 평가는 reference output이 필요한 경우와 아닌 경우를 나눠서 보라고 설명합니다. factual accuracy, consistency 같은 관점도 여기에 포함됩니다. (LangChain Docs)

즉, LLM 백엔드 평가는 보통 이렇게 나뉩니다.

  • Deterministic checks
    • 스키마 맞는가
    • 필수 필드 있는가
    • tool 썼는가
    • source 비어 있지 않은가
  • Semantic checks
    • 의미가 맞는가
    • 정책 내용을 왜곡하지 않았는가
    • 존재하지 않는 정보를 지어내지 않았는가

두 가지를 섞어야 실제 품질이 잘 보입니다.


9. 실전에서 가장 먼저 잡기 좋은 회귀(regression) 포인트

처음부터 평가 지표를 열 개씩 만들 필요는 없습니다.
저는 보통 아래부터 잡습니다.

1) structured output 회귀

  • response_type enum이 깨지지 않는가
  • used_tools가 list를 유지하는가
  • sources 필드가 사라지지 않는가

2) tool selection 회귀

  • 정책 질문인데 search_policy_documents를 여전히 쓰는가
  • 상태 질문인데 get_request_status를 여전히 쓰는가

3) hallucination 회귀

  • 문서/도구에 없는 걸 추측하지 않는가

4) latency 회귀

  • 응답 시간이 갑자기 늘지 않았는가

LangSmith observability와 evaluation은 서로 연결돼 있습니다. trace로 왜 느려졌는지 보고, dataset evaluation으로 버전별 차이를 비교할 수 있어요. (LangChain Docs)

즉, 평가는 “품질”만 보는 게 아니라
기능 계약 + 행동 계약 + 성능 계약을 같이 보는 쪽이 좋습니다.


10. 테스트를 어디까지 자동화해야 할까

이건 현실적으로 보는 게 좋습니다.

처음부터 모든 걸 완벽 자동화하려고 하면 지칩니다.
대신 이렇게 나누는 게 좋습니다.

로컬/CI에서 항상 도는 것

  • tool unit test
  • structured output shape test
  • FastAPI endpoint contract test

수시로 돌리는 것

  • LangSmith dataset evaluation
  • prompt 변경 전후 비교
  • tool 설명 변경 전후 비교

운영에서 보는 것

  • trace
  • latency
  • token usage
  • failure rate

LangSmith home과 evaluation 문서는 local development부터 production까지 observability와 evaluation을 한 워크플로로 가져가라고 설명합니다. (LangChain Docs)

이 구분이 꽤 중요합니다.
모든 걸 CI에 몰아넣기보다,
무엇을 어디서 검증할지 나누는 편이 훨씬 실용적이거든요.


11. 지금 우리 사내 도우미에 바로 적용하면 좋은 테스트 세트

이건 정말 바로 써먹기 좋습니다.

tool unit tests

  • search_policy_documents가 관련 정책을 찾는가
  • get_request_status가 존재/미존재 ID를 구분하는가

agent structured output tests

  • 정책 질문 → response_type == "policy_answer"
  • 상태 질문 → response_type == "request_status"

API contract tests

  • /assistant/ask가 answer, response_type, used_tools, sources를 항상 반환하는가
  • /assistant/stream이 final 이벤트를 항상 보내는가

dataset evaluation examples

  • 재택근무 정책 질문
  • 출장비 정산 질문
  • 존재하지 않는 요청 ID 질문
  • 정책 + 상태 혼합 요약 질문

이 정도만 갖춰도
“기능이 된다”에서 “품질을 관리한다”로 한 단계 올라갑니다.


오늘 글에서 꼭 가져가야 할 한 문장

오늘 내용을 한 줄로 줄이면 이겁니다.

LangChain 백엔드 테스트의 핵심은 답변 문장을 완전 일치로 맞추는 게 아니라, structured output 계약, tool 사용 행동, API 응답 형태를 나눠서 검증 가능한 것으로 만드는 것이다.

이 감각이 생기면
프롬프트를 바꾸거나 tool을 수정할 때도 덜 불안해집니다.

왜냐면 “망가지면 잡아줄 그물”이 생기니까요.


마무리

저는 예전엔 LLM 기능은 테스트가 너무 어렵다고 생각했어요.
문장이 늘 조금씩 달라지니까요.

근데 조금 지나고 나서 생각이 바뀌었습니다.

모든 걸 문장 비교로 보려고 하니까 어려웠던 거더라고요.

대신 이렇게 나누면 훨씬 쉬워졌습니다.

  • 스키마는 스키마대로
  • tool은 tool대로
  • API는 API대로
  • 의미 평가는 dataset과 evaluator로

그렇게 쪼개서 보니까
갑자기 “테스트 불가능한 AI”가 아니라
그래도 꽤 검증 가능한 시스템처럼 보이기 시작했어요.

저는 그게 되게 중요하다고 생각합니다.
결국 서비스는 자신감이 아니라 검증 위에 올라가야 하니까요.


다음 글 예고

다음 글에서는
LangChain 프로젝트를 더 오래 가게 만드는 법 — 프롬프트, tool, agent, API 코드를 어떻게 분리하고 리팩터링할까
로 이어가겠습니다.

이제부터는 기능과 운영을 넘어서,
프로젝트가 커져도 무너지지 않게 만드는 코드 구조 이야기를 하게 될 거예요.

출처

  • LangSmith Evaluation 문서: curated dataset으로 버전 비교, benchmark, regression detection을 하라고 안내합니다. (LangChain Docs)
  • LangSmith Evaluate a chatbot / RAG tutorial: dataset을 만들고 app을 실행하고 evaluator로 성능을 측정하는 흐름을 설명합니다. (LangChain Docs)
  • LangSmith Evaluation concepts: experiment는 dataset 위에서 특정 app version의 outputs, evaluator scores, execution traces를 함께 담는다고 설명합니다. (LangChain Docs)
  • LangSmith Evaluation approaches: LLM-as-judge와 reference-based / non-reference-based 평가 접근을 소개합니다. (LangChain Docs)
  • LangChain Structured Output 문서: agent의 structured output은 검증된 predictable format으로 반환된다고 설명합니다. (LangChain Docs)
  • LangChain Tools / Agents 문서: tools는 well-defined inputs/outputs를 가진 callable function이고, agent 평가는 final response뿐 아니라 trajectory와 single-step도 볼 수 있다고 설명합니다. (LangChain Docs)
  • FastAPI Response Model / TestClient 관련 문서 흐름: response model은 반환 데이터 검증과 필터링을 담당하고, FastAPI는 typed API contract를 만들기 좋다고 설명합니다. (LangChain Docs)
  • LangSmith Home / Observability: observability와 evaluation을 개발부터 운영까지 한 워크플로로 통합한다고 설명합니다. (LangChain Docs)

LangChain, LangSmith, FastAPI, Structured Output, Testing, Evaluation, 생성형AI, 백엔드테스트, AI Agent, 주니어개발자

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함
반응형