번역 자료 / / 2025. 5. 19. 12:50

복잡한 에이전트 평가하기

주요 개념

  • 에이전트 평가(Agent evaluation)
  • 평가자(Evaluators)
  • LLM-as-judge 평가자

이 튜토리얼에서는 사용자가 디지털 음악 스토어를 탐색할 수 있도록 돕는 고객 지원 챗봇을 구축합니다. 그리고 챗봇에 대해 실행할 수 있는 세 가지 가장 효과적인 평가 유형을 살펴봅니다.

  1. 최종 응답 평가: 에이전트의 최종 응답을 평가합니다.
  2. 경로(trajectory) 평가: 에이전트가 최종 답변에 도달하기 위해 예상한 경로(예: 도구 호출 순서)를 따랐는지 평가합니다.
  3. 단일 단계 평가: 에이전트의 특정 단계를 개별적으로 평가합니다(예: 주어진 입력에 대해 적절한 첫 번째 도구를 선택했는지 등).

이 튜토리얼에서는 LangGraph를 사용하여 에이전트를 구축하지만, 여기서 소개하는 기법과 LangSmith 기능은 프레임워크에 구애받지 않습니다.


환경 설정

환경 구성

필요한 의존성을 설치합니다.

pip install -U langgraph langchain[openai]

OpenAI와 LangSmith를 위한 환경 변수를 설정합니다.

import getpass
import os

def _set_env(var: str) -> None:
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"Set {var}: ")

os.environ["LANGSMITH_TRACING"] = "true"
_set_env("LANGSMITH_API_KEY")
_set_env("OPENAI_API_KEY")

데이터베이스 다운로드

이 튜토리얼에서는 SQLite 데이터베이스를 사용합니다. SQLite는 간단하게 사용할 수 있는 경량 데이터베이스입니다.
예시로 디지털 미디어 스토어를 나타내는 샘플 데이터베이스인 chinook를 사용합니다.

아래 코드를 통해 데이터베이스를 다운로드할 수 있습니다.

import requests

url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db"

response = requests.get(url)

if response.status_code == 200:
    with open("chinook.db", "wb") as file:
        file.write(response.content)
    print("File downloaded and saved as Chinook.db")
else:
    print(f"Failed to download the file. Status code: {response.status_code}")

데이터베이스의 일부 예시 데이터:

import sqlite3
# ... (생략)
[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), ...]

데이터베이스 스키마는 여기에서 확인할 수 있습니다.


고객 지원 에이전트 정의

LangGraph 에이전트를 생성합니다. 이 에이전트는 데이터베이스에 제한적으로 접근할 수 있습니다.
데모 목적상, 에이전트는 두 가지 기본 요청을 지원합니다.

  • 조회(Lookup): 고객이 곡 제목, 아티스트 이름, 앨범을 조회할 수 있습니다.
    예: "Jimi Hendrix의 곡이 있나요?"
  • 환불(Refund): 고객이 과거 구매에 대해 환불을 요청할 수 있습니다.
    예: "Claude Shannon인데, 지난주에 구매한 것 환불해줄 수 있나요?"

데모의 단순화를 위해 환불은 해당 데이터베이스 레코드를 삭제하는 방식으로 처리합니다.
실제 서비스에서는 사용자 인증 등 보안 조치가 필요합니다.

에이전트의 로직은 두 개의 서브그래프(조회, 환불)와 이를 라우팅하는 상위 그래프로 구성됩니다.

환불 에이전트

환불 처리 에이전트는 다음을 수행해야 합니다.

  1. 데이터베이스에서 고객의 구매 기록을 찾기
  2. 환불을 위해 관련 Invoice 및 InvoiceLine 레코드 삭제

테스트를 쉽게 하기 위해 "mock" 모드를 추가하여 실제 데이터 변경 없이 동작을 시뮬레이션할 수 있습니다.

import sqlite3

def _refund(invoice_id: int | None, invoice_line_ids: list[int] | None, mock: bool = False) -> float:
    # ... (구현 생략)
def _lookup(...):
    # ... (구현 생략)

그래프는 다음과 같이 세 가지 주요 경로로 구성됩니다.

  1. 대화에서 고객 및 구매 정보 추출
  2. 아래 세 경로 중 하나로 라우팅
    • 환불 경로: 환불 처리에 충분한 구매 정보가 있을 때
    • 조회 경로: 고객 정보(이름, 전화번호 등)로 구매 내역을 조회할 수 있을 때
    • 응답 경로: 추가 정보가 필요할 때 사용자에게 요청

그래프의 상태는 다음을 추적합니다.

  • 대화 내역(사용자와 에이전트 간 메시지)
  • 대화에서 추출된 모든 고객 및 구매 정보
  • 사용자에게 보낼 다음 메시지 등

평가

최종 응답 평가자

에이전트의 최종 응답이 정답과 일치하는지 평가합니다.

from langsmith.evaluation import EvaluationClient

client = EvaluationClient()

async def final_answer_correct(outputs: dict, reference_outputs: dict, inputs: dict, **kwargs) -> bool:
    # outputs, reference_outputs는 모두 {"response": ...} 형태의 딕셔너리여야 합니다.
    user = f"""QUESTION: {inputs['question']}
    GROUND TRUTH RESPONSE: {reference_outputs['response']}
    STUDENT RESPONSE: {outputs['response']}"""

    grade = await grader_llm.ainvoke([{"role": "system", "content": grader_instructions}, {"role": "user", "content": user}])
    return grade["is_correct"]

평가 실행 예시:

# Target function
async def run_graph(inputs: dict) -> dict:
    result = await graph.ainvoke({"messages": [
        { "role": "user", "content": inputs['question']},
    ]}, config={"env": "test"})
    return {"response": result["followup"]}

experiment_results = await client.aevaluate(
    run_graph,
    data=dataset_name,
    evaluators=[final_answer_correct],
    experiment_prefix="sql-agent-gpt4o-e2e",
    num_repetitions=1,
    max_concurrency=4,
)
experiment_results.to_pandas()

경로(trajectory) 평가자

에이전트가 올바른 경로(예: 도구 호출 순서)를 따랐는지 평가합니다.

def trajectory_subsequence(outputs: dict, reference_outputs: dict) -> float:
    """에이전트가 원하는 단계 중 몇 개를 수행했는지 확인합니다."""
    if len(reference_outputs['trajectory']) > len(outputs['trajectory']):
        return False

    i = j = 0
    while i < len(reference_outputs['trajectory']) and j < len(outputs['trajectory']):
        if reference_outputs['trajectory'][i] == outputs['trajectory'][j]:
            i += 1
        j += 1

    return i / len(reference_outputs['trajectory'])

평가 실행 예시:

async def run_graph(inputs: dict) -> dict:
    trajectory = []
    async for namespace, chunk in graph.astream({"messages": [
            {
                "role": "user",
                "content": inputs['question'],
            }
        ]}, subgraphs=True, stream_mode="debug"):
        if chunk['type'] == 'task':
            trajectory.append(chunk['payload']['name'])
            if chunk['payload']['name'] == 'tools' and chunk['type'] == 'task':
                for tc in chunk['payload']['input']['messages'][-1].tool_calls:
                    trajectory.append(tc['name'])

    return {"trajectory": trajectory}

experiment_results = await client.aevaluate(
    run_graph,
    data=dataset_name,
    evaluators=[trajectory_subsequence],
    experiment_prefix="sql-agent-gpt4o-trajectory",
    num_repetitions=1,
    max_concurrency=4,
)
experiment_results.to_pandas()

단일 단계 평가자

특정 단계(예: 의도 분류 등)를 개별적으로 평가할 수 있습니다.

# 데이터셋 생성
examples = [
    {
        "inputs": {"messages": [{"role": "user", "content": "i bought some tracks recently and i dont like them"}]}, 
        "outputs": {"route": "refund_agent"},
    },
    {
        "inputs": {"messages": [{"role": "user", "content": "I was thinking of purchasing some Rolling Stones tunes, any recommendations?"}]}, 
        "outputs": {"route": "question_answering_agent"},
    },
    # ... (생략)
]

dataset_name = "Chinook Customer Service Bot: Intent Classifier"
if not client.has_dataset(dataset_name=dataset_name):
    dataset = client.create_dataset(dataset_name=dataset_name)
    client.create_examples(
        dataset_id=dataset.id,
        examples=examples
    )

# 평가자
def correct(outputs: dict, reference_outputs: dict) -> bool:
    return outputs["route"] == reference_outputs["route"]

# 평가 실행
async def run_intent_classifier(inputs: dict) -> dict:
    command = await graph.nodes['intent_classifier'].ainvoke(inputs)
    return {"route": command.goto}

experiment_results = await client.aevaluate(
    run_intent_classifier,
    data=dataset_name,
    evaluators=[correct],
    experiment_prefix="sql-agent-gpt4o-intent-classifier",
    max_concurrency=4,
)

참고 코드

전체 코드를 하나로 모은 예시는 원문을 참고하세요.


출처

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유