티스토리 뷰

반응형

Python으로 공부하는 OpenAI 21편 — 툴 호출이 “되는 것 같음”을 넘어서, 진짜 안정적으로 되는지 평가하는 방법

툴 호출까지 붙이고 나면 처음엔 꽤 신납니다.
이제 AI가 그냥 말만 하는 게 아니라, 필요할 때 내 함수도 부르고, 검색도 하고, 실제 시스템이랑 연결되니까요.

근데 여기서부터 또 다른 문제가 시작됩니다.

겉보기엔 잘 됩니다.
데모도 됩니다.
몇 번 눌러보면 그럴듯합니다.

근데 실제로는 이런 일이 자주 나요.

  • 재고 조회를 해야 하는데 그냥 추측해서 답함
  • search_inventory 대신 엉뚱한 tool을 고름
  • tool name은 맞는데 인자 이름을 틀리게 보냄
  • tool 결과를 받고도 최종 답변에서 핵심 정보를 빼먹음
  • 어떤 날은 tool call을 하고, 어떤 날은 안 함
  • 프롬프트를 조금 고치니까 tool selection이 흔들림

여기서부터는 “잘 되는 것 같아 보임”으로는 부족합니다.
이제 필요한 건 평가 기준, 더 정확히는 tool calling 품질을 보는 eval 감각입니다.

OpenAI 공식 문서도 에이전트/툴 워크플로는 traces로 먼저 디버깅하고, 그 다음 eval loops로 들어가라고 안내합니다. Agents 평가 가이드는 traces, graders, datasets, eval runs를 함께 써서 agent workflow 품질을 본다고 설명하고, Evals 가이드는 평가를 데이터셋 기반으로 구성해서 모델/시스템 성능을 반복 측정하는 흐름을 보여줍니다. (developers.openai.com)

이번 글에서는 그걸 Python + FastAPI 기준으로 현실적으로 정리해보겠습니다.


1. 툴 호출은 “정답이 한 줄”이 아니라 “과정 전체”를 봐야 합니다

일반 텍스트 응답 평가와 툴 호출 평가는 좀 다릅니다.

보통 텍스트 응답은
“최종 답이 괜찮냐”를 많이 봅니다.

근데 tool calling은 그걸로 부족합니다.
왜냐하면 최종 답이 얼핏 맞아 보여도, 과정은 위험할 수 있거든요.

예를 들어 이런 상황이 있습니다.

  • 재고 API를 안 부르고 모델이 그냥 “있습니다”라고 말함
  • 인자 product_name 대신 item_name을 넣어서 함수가 깨짐
  • tool output을 받고도 최종 답변에서 가격을 누락함
  • 필요 없는 tool을 과하게 호출해서 latency만 늘림

OpenAI trace grading 문서는 black-box output만 보는 평가보다 trace eval이 왜 좋은지 설명합니다. trace eval은 어떤 tool call이 있었고, 어떤 단계에서 실패했는지까지 더 풍부한 정보로 평가할 수 있다고 말합니다. 즉, tool calling은 결과뿐 아니라 경로를 봐야 합니다. (developers.openai.com)

이게 진짜 중요해요.
도구 호출이 붙는 순간부터는
“답이 맞냐”보다
**“올바른 방식으로 맞았냐”**를 보는 쪽으로 사고가 바뀌어야 합니다.


2. 처음 평가할 때는 기준을 세 가지로 나누면 편합니다

저는 주니어 개발자에게 보통 이렇게 나누라고 말합니다.

1) Tool selection

이 질문에서 정말 tool을 불러야 했는가
그리고 그 tool이 맞았는가

2) Tool arguments

tool 이름은 맞더라도, 인자가 정확했는가

3) Final answer

tool 결과를 바탕으로 최종 답변이 제대로 생성됐는가

OpenAI의 “Testing Agent Skills Systematically with Evals” 글도 agent skills를 테스트 가능한 단위로 쪼개라고 말합니다. 즉, “에이전트 전체가 잘하냐”보다 개별 skill과 단계를 평가 가능한 단위로 만드는 게 중요하다는 뜻입니다. (developers.openai.com)

이 세 가지로 나누면 훨씬 좋아집니다.
예를 들어 최종 답이 틀렸을 때도:

  • selection이 틀렸는지
  • arguments가 틀렸는지
  • 후속 답변 생성이 틀렸는지

를 분리해서 볼 수 있거든요.


3. traces를 먼저 보라는 말은, 그냥 멋있는 문장이 아닙니다

OpenAI Agents 가이드와 observability 문서는 일관되게
traces로 먼저 디버깅하고, 그 다음 eval loop로 가라고 말합니다. Agents SDK는 tracing이 기본 활성화되고, model calls, tool calls, handoffs, guardrails, custom spans를 볼 수 있다고 설명합니다. (developers.openai.com)

왜 이게 중요하냐면,
툴 호출 문제는 데이터셋만 봐서는 감이 잘 안 올 때가 많기 때문입니다.

예를 들어:

  • 왜 search_inventory를 안 불렀지?
  • 왜 search_inventory를 두 번 불렀지?
  • 왜 tool output을 받고도 최종 답에서 빠졌지?
  • 왜 불필요하게 web search까지 갔지?

이건 trace를 보면 훨씬 빨리 보입니다.

그래서 순서는 보통 이게 좋아요.

  1. 실제 traces를 몇 개 본다
  2. 자주 나는 실패 패턴을 찾는다
  3. 그 패턴을 eval dataset으로 만든다
  4. 반복 측정한다

즉,
trace는 문제 발견용
eval은 회귀 방지용
이렇게 보면 이해가 편합니다.


4. 평가 데이터셋은 거창하게 시작할 필요 없습니다

반응형

이 부분에서 많이 멈춥니다.

“평가 데이터셋을 어떻게 만들어…”
처음부터 엄청 거창하게 생각하면 진도가 안 나가요.

OpenAI Evals 가이드는 크게 세 단계로 설명합니다.

  1. 평가할 dataset/examples를 준비하고
  2. grader나 평가 기준을 정하고
  3. eval run을 실행해 결과를 보는 흐름입니다. (developers.openai.com)

초반엔 그냥 CSV나 Python 리스트로 시작해도 충분합니다.

예를 들면 이런 식이죠.

EVAL_CASES = [
    {
        "name": "재고 조회가 필요한 질문",
        "user_message": "레드 키보드 재고 있어?",
        "expected_tool": "search_inventory",
        "expected_arguments": {"product_name": "레드 키보드"},
    },
    {
        "name": "재고 없는 상품 질문",
        "user_message": "블루 마우스 지금 살 수 있어?",
        "expected_tool": "search_inventory",
        "expected_arguments": {"product_name": "블루 마우스"},
    },
    {
        "name": "일반 설명 질문은 tool 불필요",
        "user_message": "재고 조회 시스템은 왜 필요한 거야?",
        "expected_tool": None,
        "expected_arguments": None,
    },
]

이 정도만 있어도 꽤 쓸 만합니다.
중요한 건 많은 양보다 대표성이에요.

  • 자주 들어오는 질문
  • 헷갈리기 쉬운 질문
  • 과거에 실패했던 질문
  • wording이 조금씩 다른 질문

이런 것들이 먼저 들어가야 합니다.


5. “대표 테스트셋”은 생각보다 작아도 좋습니다

솔직히 처음부터 500개, 1000개 만들 필요 없습니다.

OpenAI evaluation best practices 가이드는 eval은 완벽한 정답 데이터보다, 실제 시스템 리스크를 잘 대표하는 사례로 시작하는 게 중요하다고 설명합니다. 또 모델은 비결정적이기 때문에 전통적인 단일 정답 테스트보다 현실적 사례 기반 반복 측정이 중요하다고 말합니다. (developers.openai.com)

저는 보통 이렇게 추천합니다.

1차 세트

10~20개

2차 세트

30~50개

이후

실패 케이스를 계속 추가

이게 좋습니다.
처음부터 거대한 eval set을 만들기보다,
실패가 나올 때마다 하나씩 쌓는 편이 더 오래 갑니다.


6. 가장 먼저 할 수 있는 건 “규칙 기반 평가”입니다

처음 eval이라고 하면 다들 LLM judge부터 떠올리는데,
tool calling에선 의외로 규칙 기반 평가가 먼저 먹힙니다.

예를 들어 이런 건 규칙으로 볼 수 있어요.

  • expected_tool이 search_inventory인데 실제 tool call도 같은가
  • expected_tool이 None인데 실제로 tool call을 안 했는가
  • arguments에 product_name이 있는가
  • product_name 값이 기대와 같은가

이건 그냥 Python으로도 꽤 정확하게 볼 수 있습니다.

실행 가능한 예시

import json
from dataclasses import dataclass


@dataclass
class ToolCallResult:
    tool_name: str | None
    arguments_json: str | None


def evaluate_tool_call(
    actual: ToolCallResult,
    expected_tool: str | None,
    expected_arguments: dict | None,
) -> dict:
    result = {
        "tool_match": False,
        "arguments_match": False,
        "passed": False,
    }

    if actual.tool_name == expected_tool:
        result["tool_match"] = True

    if expected_tool is None:
        result["arguments_match"] = actual.arguments_json is None
        result["passed"] = result["tool_match"] and result["arguments_match"]
        return result

    try:
        actual_args = json.loads(actual.arguments_json or "{}")
    except json.JSONDecodeError:
        actual_args = {}

    result["arguments_match"] = actual_args == (expected_arguments or {})
    result["passed"] = result["tool_match"] and result["arguments_match"]
    return result


# 예시 실행
actual = ToolCallResult(
    tool_name="search_inventory",
    arguments_json='{"product_name": "레드 키보드"}',
)

print(
    evaluate_tool_call(
        actual=actual,
        expected_tool="search_inventory",
        expected_arguments={"product_name": "레드 키보드"},
    )
)

이런 테스트는 단순하지만 꽤 강력합니다.
최소한 엉뚱한 tool 선택인자 구조 오류는 빨리 잡을 수 있으니까요.


7. 그런데 최종 답변 평가는 규칙만으로 부족할 때가 많습니다

예를 들어 tool selection은 맞고, arguments도 맞았는데
최종 답변이 여전히 이상할 수 있습니다.

  • 재고 수량을 빼먹음
  • 가격을 안 말함
  • tool output에 없는 내용을 추측함
  • 한국어로 답해야 하는데 영어로 섞임

이런 건 규칙만으로 보기 어렵습니다.
그래서 OpenAI는 graders와 eval runs를 함께 쓰는 흐름을 안내합니다. agent evals 가이드도 traces, graders, datasets, eval runs를 조합하라고 설명합니다. (developers.openai.com)

즉, tool calling 평가는 보통 두 층으로 갑니다.

1층: 규칙 기반

  • tool 선택
  • argument 형식
  • output 존재 여부

2층: grader 기반

  • 최종 답변 품질
  • 누락 여부
  • hallucination 여부
  • instruction 준수 여부

이 조합이 가장 현실적입니다.


8. LLM grader는 “정답 그 자체”보다 “평가 기준”을 명확히 해야 잘 작동합니다

OpenAI Evals 문서는 grader를 쓰더라도 평가 기준을 분명하게 적으라고 하고, evaluation best practices도 평가 기준이 모호하면 결과 신뢰성이 떨어진다고 설명합니다. (developers.openai.com)

예를 들어 이런 식으로요.

  • tool output에 있는 재고 수량을 포함했는가
  • tool output에 없는 내용을 단정하지 않았는가
  • 한국어로 답했는가
  • 3문장 이내로 답했는가

즉, grader에 “좋은 답인지 봐줘”라고 하면 애매합니다.
대신 무엇이 좋은 답인지를 체크리스트처럼 정의해야 합니다.

이건 사람이 평가할 때도 마찬가지예요.
기준이 없으면 리뷰어마다 다르게 보게 됩니다.


9. representative failures를 모으는 습관이 eval의 시작입니다

이건 진짜 중요한 습관입니다.

OpenAI의 eval 관련 글들과 trace grading 가이드가 공통으로 말하는 건
실제 traces와 실제 실패 사례를 eval로 전환하라는 흐름입니다. (developers.openai.com)

즉, 운영하다가 이런 일이 생기면 그냥 넘기지 말고:

  • 질문 원문
  • tool 선택 결과
  • tool arguments
  • 최종 답변
  • 기대했던 동작

을 한 세트로 저장해두는 거예요.

예를 들면:

FAILED_CASES = [
    {
        "name": "불필요한 tool 호출",
        "user_message": "재고 조회 시스템이 왜 필요한지 설명해줘",
        "expected_tool": None,
        "actual_problem": "search_inventory를 호출함",
    },
    {
        "name": "인자 추출 실패",
        "user_message": "레드 키보드 재고랑 가격 알려줘",
        "expected_tool": "search_inventory",
        "actual_problem": "product_name 대신 item_name 사용",
    },
]

이게 쌓이기 시작하면, 그 순간부터 eval이 살아납니다.
그전까지는 그냥 예쁜 개념이에요.


10. FastAPI 서비스에 평가 훅을 넣을 때는 “실행 경로”를 남기는 게 좋습니다

tool calling은 결과만 저장하면 아쉬울 때가 많습니다.
가능하면 아래도 남겨두면 좋아요.

  • user message
  • selected tool
  • raw arguments
  • tool output
  • final answer
  • latency
  • model
  • request_id

이 정보가 있어야 나중에 offline eval이나 trace review를 할 수 있습니다.

OpenAI Agents observability는 trace 안에 model calls, tool calls, outputs를 남긴다고 설명합니다. Responses-only 구조여도, 앱 차원에서 비슷한 로그를 남겨두면 훨씬 도움이 됩니다. (developers.openai.com)

즉, 평가는 나중에 따로 붙이는 게 아니라
지금 서비스가 평가 가능한 흔적을 남기게 만드는 것부터 시작입니다.


11. 잘못된 tool call을 줄이는 방법은 생각보다 단순한 데서 많이 나옵니다

툴 호출 품질이 흔들릴 때, 다들 곧바로 모델 탓을 하거나
에이전트 프레임워크를 바꾸고 싶어 합니다.

근데 실제로는 이쪽이 먼저입니다.

1) tool description 더 명확히 쓰기

언제 써야 하는지, 언제 쓰면 안 되는지

2) parameter schema 엄격하게 만들기

required, enum, additionalProperties=False

3) system/developer instructions에 tool 사용 원칙 명시

예:

  • 재고/가격 질문만 search_inventory 사용
  • 일반 개념 설명은 tool 사용 금지

4) tool 수를 줄이기

비슷한 tool이 많으면 선택이 흔들리기 쉽습니다

OpenAI function calling 가이드는 도구 정의에 이름, 설명, parameters(JSON Schema)를 정확히 제공하는 흐름을 보여줍니다. Evals 관련 글도 skill을 테스트 가능한 단위로 만들라고 설명하므로, tool 설명과 schema 자체가 품질의 일부입니다. (developers.openai.com)

이건 진짜 체감됩니다.
tool 정의를 다듬는 것만으로도 성공률이 꽤 오를 때가 많아요.


12. 아주 작은 평가 하네스부터 시작하는 예제

아래 코드는 정말 최소 버전입니다.
가짜 assistant 결과를 평가하는 방식이지만, 구조 감각을 잡기 좋습니다.

from dataclasses import dataclass
import json


@dataclass
class EvalCase:
    name: str
    user_message: str
    expected_tool: str | None
    expected_arguments: dict | None


@dataclass
class AssistantRunResult:
    selected_tool: str | None
    arguments_json: str | None
    final_answer: str


def grade_case(case: EvalCase, result: AssistantRunResult) -> dict:
    tool_ok = result.selected_tool == case.expected_tool

    if case.expected_tool is None:
        args_ok = result.arguments_json is None
    else:
        try:
            args_ok = json.loads(result.arguments_json or "{}") == (case.expected_arguments or {})
        except json.JSONDecodeError:
            args_ok = False

    return {
        "case": case.name,
        "tool_ok": tool_ok,
        "args_ok": args_ok,
        "passed": tool_ok and args_ok,
    }


cases = [
    EvalCase(
        name="재고 조회 필요",
        user_message="레드 키보드 재고 있어?",
        expected_tool="search_inventory",
        expected_arguments={"product_name": "레드 키보드"},
    ),
    EvalCase(
        name="설명 질문은 tool 불필요",
        user_message="재고 조회 시스템이 왜 필요한가요?",
        expected_tool=None,
        expected_arguments=None,
    ),
]

results = [
    AssistantRunResult(
        selected_tool="search_inventory",
        arguments_json='{"product_name": "레드 키보드"}',
        final_answer="레드 키보드는 현재 재고가 있습니다.",
    ),
    AssistantRunResult(
        selected_tool=None,
        arguments_json=None,
        final_answer="재고 조회 시스템은 상품 수량과 가격 상태를 즉시 확인하기 위해 필요합니다.",
    ),
]

for case, result in zip(cases, results):
    print(grade_case(case, result))

이 정도만 있어도 꽤 유용합니다.
최소한 tool selection 회귀는 빠르게 감지할 수 있으니까요.


13. Evals API는 언제 검토하면 좋나

OpenAI Evals 가이드는 데이터셋, graders, eval runs를 구성해서 모델/시스템 성능을 반복 측정하는 흐름을 제공합니다. 또 agent evals 가이드는 traces를 본 뒤 반복 가능한 평가 루프로 옮기라고 설명합니다. (developers.openai.com)

즉, 이런 순간엔 Evals API나 더 체계적인 평가 루프를 검토할 만합니다.

  • representative 사례가 30개 이상 쌓였을 때
  • 모델 버전 변경 전후를 비교하고 싶을 때
  • tool definition 수정이 회귀를 만들지 확인하고 싶을 때
  • CI에 평가를 걸고 싶을 때
  • 팀이 “감”이 아니라 숫자로 품질을 보고 싶을 때

처음엔 규칙 기반 하네스와 로컬 eval 세트로 시작하고,
그다음 OpenAI evals나 trace grading 쪽으로 올라가는 흐름이 자연스럽습니다.


14. 자주 하는 실수들

실수 1. 최종 답변만 보고 tool 선택은 안 본다

이러면 운 좋게 맞은 경우를 놓칩니다.

실수 2. eval dataset을 너무 완벽하게 만들려다가 시작도 못 한다

처음엔 10개만 있어도 충분합니다.

실수 3. 운영 실패 사례를 안 쌓는다

실패 케이스가 최고의 eval seed입니다.

실수 4. grader 기준을 모호하게 둔다

“좋은 답인지 평가”보다 “tool output의 핵심 수치를 포함했는지” 같은 기준이 더 낫습니다.

실수 5. trace를 안 보고 곧바로 점수부터 매긴다

무엇이 왜 실패하는지 먼저 이해해야 수정도 잘 됩니다. OpenAI는 traces를 먼저 보고 그다음 eval loop로 가라고 안내합니다. (developers.openai.com)


15. 지금 단계에서 추천하는 가장 현실적인 순서

주니어 개발자가 tool calling 품질을 점검할 때는 이 순서가 제일 좋습니다.

  1. 실제 traces 몇 개 보기
  2. 자주 나오는 실패 패턴 메모하기
  3. 10~20개 대표 케이스 만들기
  4. tool selection / arguments 규칙 평가부터 시작하기
  5. final answer는 grader 기준을 붙여 보기
  6. 프롬프트/툴 설명 수정 후 다시 돌려보기
  7. 실패 사례를 계속 dataset에 추가하기

이 순서가 좋은 이유는,
작고 반복 가능하고, 실제 개선으로 이어지기 때문입니다.


16. 오늘 글의 핵심 요약

이번 글에서 꼭 가져가야 할 건 이것입니다.

tool calling 품질은 “최종 답이 그럴듯한가”만으로 보면 부족하고, tool selection, arguments, final answer를 단계별로 나눠서 traces와 eval dataset으로 반복 측정해야 한다.

정리하면:

  • OpenAI는 agent workflows를 traces, graders, datasets, eval runs로 개선하는 흐름을 안내합니다. (developers.openai.com)
  • traces는 문제 발견용이고, evals는 회귀 방지용으로 쓰는 편이 좋습니다. (developers.openai.com)
  • tool calling 평가는 보통 tool selection, arguments, final answer 세 층으로 나누면 실무적으로 다루기 쉽습니다. (developers.openai.com)
  • representative failure cases를 dataset으로 쌓는 습관이 평가 품질을 키웁니다. (developers.openai.com)

여기까지 오면
이제 진짜로 “되는 것 같음”을 넘어서
안정적으로 되는지 측정하는 개발로 들어가는 느낌이 납니다.
저는 이 단계부터 AI 기능이 조금 덜 불안해지기 시작하더라고요.


다음 편 예고

다음 글에서는 여기서 더 실무적으로 들어가겠습니다.

Python + FastAPI에서 OpenAI 기능을 팀 프로젝트로 문서화하고, README와 운영 문서를 정리하는 방법

이 주제로,

  • 혼자만 아는 구조가 되지 않게 문서화하는 법
  • README에 꼭 들어가야 할 것
  • .env.example, 아키텍처 그림, 실행 방법 정리
  • 운영자가 필요한 runbook은 어떻게 쓰는지

까지 이어가보겠습니다.


출처

  • OpenAI Evaluate agent workflows 가이드 — traces, graders, datasets, eval runs를 조합한 agent workflow 평가 흐름. (developers.openai.com)
  • OpenAI Working with evals 가이드 — Evals API 기반의 평가 구성 흐름 설명. (developers.openai.com)
  • OpenAI Evaluation best practices — 대표 사례 기반 평가와 기준 설계 중요성 설명. (developers.openai.com)
  • OpenAI Trace grading 가이드 — black-box eval보다 trace eval이 더 풍부한 디버깅 정보를 제공함. (developers.openai.com)
  • OpenAI Testing Agent Skills Systematically with Evals — agent skills를 테스트 가능한 단위로 쪼개는 실전 감각 제시. (developers.openai.com)
  • OpenAI Agents SDK / observability 가이드 — traces로 먼저 디버깅하고 eval loop로 이동하는 흐름 설명. (developers.openai.com)

Python, OpenAI, FastAPI, tool calling, function calling, evals, agent evals, trace grading, observability, Agents SDK, Responses API, AI 품질 평가, 주니어 개발자, AI 백엔드, representative dataset

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/05   »
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
31
글 보관함
반응형