이 가이드에서는 챗봇에 대한 평가를 설정합니다. 이를 통해 애플리케이션이 데이터 집합에서 얼마나 잘 동작하는지 측정할 수 있습니다. 이러한 인사이트를 빠르고 신뢰성 있게 얻으면 자신 있게 반복 개발할 수 있습니다. 이 튜토리얼에서는 다음과 같은 과정을 진행합니다:
  • 성능을 측정할 초기 골든 데이터셋 생성
  • 성능 측정에 사용할 메트릭 정의
  • 여러 프롬프트 또는 모델에서 평가 실행
  • 결과를 수동으로 비교
  • 시간에 따라 결과 추적
  • CI/CD에서 자동화된 테스트 설정
LangSmith가 지원하는 평가 워크플로우에 대한 자세한 내용은 how-to guides를 참고하거나, evaluate 및 비동기 버전인 aevaluate 레퍼런스 문서를 확인하세요. 다룰 내용이 많으니, 바로 시작해봅시다!

설정

먼저 이 튜토리얼에 필요한 의존성을 설치합니다. 여기서는 OpenAI를 사용하지만, LangSmith는 어떤 모델과도 사용할 수 있습니다:
pip install -U langsmith openai
그리고 LangSmith 트레이싱을 활성화하기 위해 환경 변수를 설정합니다:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="<Your LangSmith API key>"
export OPENAI_API_KEY="<Your OpenAI API key>"

데이터셋 생성

애플리케이션을 테스트하고 평가할 준비를 할 때 첫 번째 단계는 평가할 데이터 포인트를 정의하는 것입니다. 여기서 고려해야 할 몇 가지 측면이 있습니다:
  • 각 데이터 포인트의 스키마는 어떻게 되어야 할까?
  • 얼마나 많은 데이터 포인트를 수집해야 할까?
  • 데이터 포인트를 어떻게 수집해야 할까?
스키마: 각 데이터 포인트는 최소한 애플리케이션의 입력을 포함해야 합니다. 가능하다면, 기대하는 출력도 정의하는 것이 매우 도움이 됩니다. 이는 애플리케이션이 제대로 동작할 때 어떤 출력을 내야 하는지 나타냅니다. 완벽한 출력을 정의할 수 없는 경우도 많지만, 괜찮습니다! 평가는 반복적인 과정입니다. 때로는 각 예시에 대해 더 많은 정보를 정의하고 싶을 수도 있습니다. 예를 들어 RAG에서는 기대하는 문서, 에이전트에서는 기대하는 단계 등입니다. LangSmith 데이터셋은 매우 유연하여 임의의 스키마를 정의할 수 있습니다. 수량: 몇 개를 수집해야 한다는 엄격한 규칙은 없습니다. 중요한 것은 방어하고 싶은 엣지 케이스를 충분히 커버하는 것입니다. 10~50개의 예시만으로도 큰 가치를 얻을 수 있습니다! 처음부터 많은 수를 준비할 필요는 없습니다. 시간이 지나면서 계속 추가하면 됩니다! 수집 방법: 이 부분이 가장 까다로울 수 있습니다. 데이터셋을 수집하기로 결정했다면, 실제로 어떻게 진행할까요? 대부분의 신규 프로젝트 팀은 처음 1020개의 데이터 포인트를 직접 수집하는 것으로 시작합니다. 이렇게 시작한 데이터셋은 일반적으로 살아있는 구조로 시간이 지나면서 성장합니다. 실제 사용자가 애플리케이션을 사용하는 모습을 보고, 문제점을 파악한 뒤 일부 데이터 포인트를 이 집합에 추가하는 방식입니다. 데이터셋을 보강하기 위해 합성 데이터 생성 등의 방법도 있지만, 처음에는 신경 쓰지 말고 직접 1020개의 예시를 라벨링하는 것을 추천합니다. 데이터셋을 준비했다면, LangSmith에 업로드하는 방법은 여러 가지가 있습니다. 이 튜토리얼에서는 클라이언트를 사용하지만, UI를 통해 업로드하거나 UI에서 직접 생성할 수도 있습니다. 이번 튜토리얼에서는 평가를 위해 5개의 데이터 포인트를 생성합니다. 질문-답변 애플리케이션을 평가할 것이며, 입력은 질문, 출력은 답변입니다. 질문-답변 애플리케이션이므로 기대하는 답변을 정의할 수 있습니다. 이 데이터셋을 LangSmith에 생성하고 업로드하는 방법을 살펴봅시다!
from langsmith import Client

client = Client()

# Define dataset: these are your test cases
dataset_name = "QA Example Dataset"
dataset = client.create_dataset(dataset_name)

client.create_examples(
    dataset_id=dataset.id,
    examples=[
        {
            "inputs": {"question": "What is LangChain?"},
            "outputs": {"answer": "A framework for building LLM applications"},
        },
        {
            "inputs": {"question": "What is LangSmith?"},
            "outputs": {"answer": "A platform for observing and evaluating LLM applications"},
        },
        {
            "inputs": {"question": "What is OpenAI?"},
            "outputs": {"answer": "A company that creates Large Language Models"},
        },
        {
            "inputs": {"question": "What is Google?"},
            "outputs": {"answer": "A technology company known for search"},
        },
        {
            "inputs": {"question": "What is Mistral?"},
            "outputs": {"answer": "A company that creates Large Language Models"},
        }
    ]
)
이제 LangSmith UI에서 Datasets & Testing 페이지의 QA Example Dataset을 찾아 클릭하면, 5개의 새로운 예시가 추가된 것을 확인할 수 있습니다.

메트릭 정의

데이터셋을 만든 후에는 응답을 평가할 메트릭을 정의할 수 있습니다. 기대하는 답변이 있으므로, 평가 시 이를 기준으로 비교할 수 있습니다. 하지만 애플리케이션이 정확히 그 답변을 출력하리라 기대하지는 않고, 비슷한 답변을 출력하길 기대합니다. 이로 인해 평가가 조금 더 까다로워집니다. 정확성 평가 외에도, 답변이 짧고 간결한지 확인해봅시다. 이 부분은 더 쉽습니다. 응답의 길이를 측정하는 간단한 Python 함수를 정의할 수 있습니다. 이 두 가지 메트릭을 정의해봅시다. 첫 번째로, LLM을 사용하여 출력이 기대하는 출력과 맞는지 판단하도록 합니다. 이런 LLM-as-a-judge 방식은 단순 함수로 측정하기 어려운 복잡한 경우에 흔히 사용됩니다. 평가에 사용할 프롬프트와 LLM을 직접 정의할 수 있습니다:
import openai
from langsmith import wrappers

openai_client = wrappers.wrap_openai(openai.OpenAI())

eval_instructions = "You are an expert professor specialized in grading students' answers to questions."

def correctness(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
    user_content = f"""You are grading the following question:
{inputs['question']}
Here is the real answer:
{reference_outputs['answer']}
You are grading the following predicted answer:
{outputs['response']}
Respond with CORRECT or INCORRECT:
Grade:"""
    response = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        temperature=0,
        messages=[
            {"role": "system", "content": eval_instructions},
            {"role": "user", "content": user_content},
        ],
    ).choices[0].message.content
    return response == "CORRECT"
응답의 길이를 평가하는 것은 훨씬 쉽습니다! 실제 출력이 기대하는 결과의 2배 미만인지 확인하는 간단한 함수를 정의하면 됩니다.
def concision(outputs: dict, reference_outputs: dict) -> bool:
    return int(len(outputs["response"]) < 2 * len(reference_outputs["answer"]))

평가 실행

좋습니다! 이제 평가를 어떻게 실행할까요? 데이터셋과 평가자(evaluator)가 준비되었으니, 이제 필요한 것은 애플리케이션입니다! 시스템 메시지에 응답 방법에 대한 지침을 넣고, 이를 LLM에 전달하는 간단한 애플리케이션을 만들어봅니다. OpenAI SDK를 직접 사용하여 구현합니다:
default_instructions = "Respond to the users question in a short, concise manner (one short sentence)."

def my_app(question: str, model: str = "gpt-4o-mini", instructions: str = default_instructions) -> str:
    return openai_client.chat.completions.create(
        model=model,
        temperature=0,
        messages=[
            {"role": "system", "content": instructions},
            {"role": "user", "content": question},
        ],
    ).choices[0].message.content
LangSmith 평가를 실행하기 전에, 데이터셋의 입력 키를 우리가 호출할 함수에 매핑하고, 함수의 출력을 기대하는 출력 키에 매핑하는 간단한 래퍼를 정의해야 합니다.
def ls_target(inputs: str) -> dict:
    return {"response": my_app(inputs["question"])}
좋습니다! 이제 평가를 실행할 준비가 되었습니다. 실행해봅시다!
experiment_results = client.evaluate(
    ls_target, # Your AI system
    data=dataset_name, # The data to predict and grade over
    evaluators=[concision, correctness], # The evaluators to score the results
    experiment_prefix="openai-4o-mini", # A prefix for your experiment names to easily identify them
)
이 코드는 URL을 출력합니다. 클릭하면 평가 결과를 확인할 수 있습니다! 데이터셋 페이지로 돌아가서 Experiments 탭을 선택하면, 한 번의 실행 요약을 볼 수 있습니다! 이번에는 다른 모델로 시도해봅시다! gpt-4-turbo를 사용해봅시다.
def ls_target_v2(inputs: str) -> dict:
    return {"response": my_app(inputs["question"], model="gpt-4-turbo")}

experiment_results = client.evaluate(
    ls_target_v2,
    data=dataset_name,
    evaluators=[concision, correctness],
    experiment_prefix="openai-4-turbo",
)
그리고 이제 GPT-4를 사용하면서, 답변이 짧아야 한다는 요구사항을 더 엄격하게 프롬프트에 추가해봅시다.
instructions_v3 = "Respond to the users question in a short, concise manner (one short sentence). Do NOT use more than ten words."

def ls_target_v3(inputs: str) -> dict:
    response = my_app(
        inputs["question"],
        model="gpt-4-turbo",
        instructions=instructions_v3
    )
    return {"response": response}

experiment_results = client.evaluate(
    ls_target_v3,
    data=dataset_name,
    evaluators=[concision, correctness],
    experiment_prefix="strict-openai-4-turbo",
)
데이터셋 페이지의 Experiments 탭으로 돌아가면, 세 번의 실행 결과가 모두 표시되는 것을 볼 수 있습니다!

결과 비교

좋습니다, 세 번의 실행을 평가했습니다. 그런데 결과를 어떻게 비교할 수 있을까요? 첫 번째 방법은 Experiments 탭에서 실행을 직접 확인하는 것입니다. 여기서 각 실행에 대한 메트릭을 한눈에 볼 수 있습니다: 좋습니다! GPT-4가 GPT-3.5보다 기업 정보를 더 잘 알고 있고, 엄격한 프롬프트가 답변 길이에 큰 도움이 된 것을 알 수 있습니다. 더 자세히 살펴보고 싶다면 어떻게 해야 할까요? 비교하고 싶은 실행(여기서는 세 개 모두)을 선택하고, 비교 뷰에서 열어볼 수 있습니다. 세 가지 테스트가 나란히 표시됩니다. 일부 셀은 색상으로 구분되어 있는데, 이는 특정 메트릭특정 기준과 비교해 어떻게 변화했는지 보여줍니다. 기준과 메트릭은 기본값으로 자동 선택되지만, 직접 변경할 수도 있습니다. Display 컨트롤을 사용해 표시할 열과 메트릭을 선택할 수 있습니다. 상단 아이콘을 클릭하면 개선/퇴보가 있는 실행만 자동으로 필터링할 수도 있습니다. 더 많은 정보를 보고 싶다면, 행 위에 마우스를 올리면 나타나는 Expand 버튼을 클릭해 사이드 패널에서 상세 정보를 확인할 수 있습니다:

CI/CD에서 자동화된 테스트 설정

이제 일회성으로 실행해봤으니, 자동화 방식으로 실행하도록 설정할 수 있습니다. pytest 파일로 만들어 CI/CD에서 실행하면 매우 쉽게 자동화할 수 있습니다. 이 과정에서 결과만 기록하거나, 통과 여부를 판단하는 기준을 설정할 수도 있습니다. 예를 들어, 생성된 응답 중 최소 80%가 length 체크를 통과해야 한다는 기준을 설정하려면 다음과 같이 테스트할 수 있습니다:
def test_length_score() -> None:
    """Test that the length score is at least 80%."""
    experiment_results = evaluate(
        ls_target, # Your AI system
        data=dataset_name, # The data to predict and grade over
        evaluators=[concision, correctness], # The evaluators to score the results
    )
    # This will be cleaned up in the next release:
    feedback = client.list_feedback(
        run_ids=[r.id for r in client.list_runs(project_name=experiment_results.experiment_name)],
        feedback_key="concision"
    )
    scores = [f.score for f in feedback]
    assert sum(scores) / len(scores) >= 0.8, "Aggregate score should be at least .8"

시간에 따라 결과 추적

이제 실험을 자동화 방식으로 실행하게 되었으니, 시간에 따라 결과를 추적하고 싶을 것입니다. 데이터셋 페이지의 전체 Experiments 탭에서 이를 확인할 수 있습니다. 기본적으로 시간에 따른 평가 메트릭(빨간색 강조)을 표시합니다. 또한 git 메트릭도 자동으로 추적하여 코드의 브랜치와 쉽게 연관시킬 수 있습니다(노란색 강조).

결론

이 튜토리얼은 여기까지입니다! 초기 테스트 세트 생성, 평가 메트릭 정의, 실험 실행, 수동 비교, CI/CD 설정, 시간에 따른 결과 추적까지 살펴보았습니다. 이 과정이 자신 있게 반복 개발하는 데 도움이 되길 바랍니다. 이것은 시작에 불과합니다. 앞서 언급했듯, 평가는 지속적인 과정입니다. 예를 들어, 평가하고 싶은 데이터 포인트는 시간이 지나면서 계속 바뀔 수 있습니다. 다양한 평가자(evaluator)도 탐색해볼 수 있습니다. 이에 대한 정보는 how-to guides를 참고하세요. 또한, 이와 같은 “오프라인” 방식 외에도 데이터를 평가하는 다른 방법이 있습니다(예: 운영 데이터 평가). 온라인 평가에 대한 자세한 내용은 이 가이드를 참고하세요.

참고 코드

import openai
from langsmith import Client, wrappers

# Application code
openai_client = wrappers.wrap_openai(openai.OpenAI())

default_instructions = "Respond to the users question in a short, concise manner (one short sentence)."

def my_app(question: str, model: str = "gpt-4o-mini", instructions: str = default_instructions) -> str:
    return openai_client.chat.completions.create(
        model=model,
        temperature=0,
        messages=[
            {"role": "system", "content": instructions},
            {"role": "user", "content": question},
        ],
    ).choices[0].message.content

client = Client()

# Define dataset: these are your test cases
dataset_name = "QA Example Dataset"
dataset = client.create_dataset(dataset_name)

client.create_examples(
    dataset_id=dataset.id,
    examples=[
        {
            "inputs": {"question": "What is LangChain?"},
            "outputs": {"answer": "A framework for building LLM applications"},
        },
        {
            "inputs": {"question": "What is LangSmith?"},
            "outputs": {"answer": "A platform for observing and evaluating LLM applications"},
        },
        {
            "inputs": {"question": "What is OpenAI?"},
            "outputs": {"answer": "A company that creates Large Language Models"},
        },
        {
            "inputs": {"question": "What is Google?"},
            "outputs": {"answer": "A technology company known for search"},
        },
        {
            "inputs": {"question": "What is Mistral?"},
            "outputs": {"answer": "A company that creates Large Language Models"},
        }
    ]
)

# Define evaluators
eval_instructions = "You are an expert professor specialized in grading students' answers to questions."

def correctness(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
    user_content = f"""You are grading the following question:
{inputs['question']}
Here is the real answer:
{reference_outputs['answer']}
You are grading the following predicted answer:
{outputs['response']}
Respond with CORRECT or INCORRECT:
Grade:"""
    response = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        temperature=0,
        messages=[
            {"role": "system", "content": eval_instructions},
            {"role": "user", "content": user_content},
        ],
    ).choices[0].message.content
    return response == "CORRECT"

def concision(outputs: dict, reference_outputs: dict) -> bool:
    return int(len(outputs["response"]) < 2 * len(reference_outputs["answer"]))

# Run evaluations
def ls_target(inputs: str) -> dict:
    return {"response": my_app(inputs["question"])}

experiment_results_v1 = client.evaluate(
    ls_target, # Your AI system
    data=dataset_name, # The data to predict and grade over
    evaluators=[concision, correctness], # The evaluators to score the results
    experiment_prefix="openai-4o-mini", # A prefix for your experiment names to easily identify them
)

def ls_target_v2(inputs: str) -> dict:
    return {"response": my_app(inputs["question"], model="gpt-4-turbo")}

experiment_results_v2 = client.evaluate(
    ls_target_v2,
    data=dataset_name,
    evaluators=[concision, correctness],
    experiment_prefix="openai-4-turbo",
)

instructions_v3 = "Respond to the users question in a short, concise manner (one short sentence). Do NOT use more than ten words."

def ls_target_v3(inputs: str) -> dict:
    response = my_app(
        inputs["question"],
        model="gpt-4-turbo",
        instructions=instructions_v3
    )
    return {"response": response}

experiment_results_v3 = client.evaluate(
    ls_target_v3,
    data=dataset_name,
    evaluators=[concision, correctness],
    experiment_prefix="strict-openai-4-turbo",
)

Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I