티스토리 뷰
에이전트 데모는 대개 잘 동작합니다. 입력이 몇 개 없고, 설계자가 기대한 경로로만 흘러가기 때문입니다. 하지만 실제 사용자 요청이 들어오기 시작하면 그 데모 품질은 놀랄 만큼 빨리 무너집니다.
문제는 에이전트가 끝났다고 말하는 순간을 그대로 믿기 쉽다는 데 있습니다. 자연어로 된 "완료했습니다"는 증거가 아닙니다. 완료 조건을 자동 검증 가능한 테스트로 바꾸지 않으면 시스템은 매 릴리스마다 조용히 나빠집니다.
Test Harness는 이 조용한 품질 하락을 막는 층입니다. 무엇을 통과로 볼지 고정하고, 그 기준을 unit, integration, eval로 나눠 반복 실행하게 만듭니다.
이 글은 Harness Engineering 101 시리즈의 6번째 글입니다.
에이전트 품질은 느낌이 아니라 반복 가능한 테스트 결과로 관리해야 합니다.
이 글에서 다룰 문제
- 에이전트 테스트는 전통적인 소프트웨어 테스트와 무엇이 같고 무엇이 다를까요?
- 왜 unit, integration, eval 세 층을 따로 가져가야 할까요?
- eval 데이터셋은 어디에서 가져와야 현실적인 품질을 볼 수 있을까요?
- 정답이 하나가 아닌 에이전트 출력을 어떻게 점수화할 수 있을까요?
- 테스트를 CI에 연결하지 않으면 어떤 문제가 생길까요?
왜 이 글이 중요한가
Test Harness가 중요한 첫 번째 이유는 증거입니다. 데모에서 몇 번 잘 나온다는 사실은 실제 운영 품질을 보장하지 않습니다. 반복 가능한 체크가 있어야만 변경 전후를 비교할 수 있습니다.
두 번째 이유는 회귀 방지입니다. 프롬프트, 모델, 도구, 정책 어느 하나가 바뀌어도 품질은 쉽게 흔들립니다. 자동 테스트가 없으면 이 흔들림은 실제 사용자에게 먼저 발견됩니다.
세 번째 이유는 디버깅 비용입니다. Unit이 없으면 어디가 깨졌는지 찾기 어렵고, eval이 없으면 전체 품질이 좋아졌는지 나빠졌는지 판단할 수 없습니다. 세 층은 서로 대체 관계가 아닙니다.
Test Harness를 이해하는 가장 좋은 방법: 완료 조건을 자연어 약속에서 실행 가능한 검증으로 바꾸는 일로 보는 것입니다
에이전트는 전통적인 함수보다 비결정성이 크기 때문에 더 엄격한 검증 체계가 필요합니다. 같은 의미를 다른 문장으로 표현할 수 있고, 때로는 작은 프롬프트 변경이 전체 행동을 바꿉니다.
그래서 Test Harness는 결과를 한 가지 정답 문자열로만 보지 않습니다. 정확 매치가 가능한 부분은 그대로 검사하고, 의미 평가가 필요한 부분은 heuristic과 LLM-as-judge를 섞어 다층적으로 봅니다.
이 글에서 가장 중요한 일은 완료 조건을 사람이 나중에 읽고 판단하도록 남겨 두지 않고, 시스템이 릴리스마다 반복 실행할 수 있는 형태로 고정하는 것입니다.
"잘 동작한다"는 말은 증거가 아닙니다. 자동으로 다시 돌릴 수 있는 테스트만이 증거입니다.
핵심 개념
Agent가 "끝났습니다"라고 말해도 정말 끝났는지는 테스트가 결정합니다. Test Harness는 완료 조건을 자동 검증 가능한 테스트로 고정합니다.

"잘 동작합니다"라는 말은 증거가 아닙니다
Agent를 만들고 데모를 돌리면 잘 동작합니다. 실제 사용자에게 풀면 한 주 안에 망가집니다. 두 상황의 차이는 입력의 다양성입니다. 데모는 5개의 잘 짜인 입력으로 동작하지만, production은 수천 개의 예측 못 한 입력을 마주합니다.
이 간극을 메우는 것이 Test Harness입니다. Agent의 완료 조건을 자연어가 아니라 자동 실행 가능한 테스트로 표현하고, 모든 변경에 대해 그 테스트를 돌립니다. "잘 동작합니다"가 아니라 "이 50개의 테스트가 통과합니다"가 증거입니다.
이번 글에서는 Agent용 테스트의 종류, 평가 데이터셋 만들기, 그리고 회귀 방지 자동화를 다룹니다.
Agent 테스트의 3 계층

전통적인 소프트웨어 테스트와 비슷하지만, 비결정성이 추가됩니다.
1. Unit tests: 각 도구가 schema대로 동작하는지. 결정적, 빠릅니다.
2. Integration tests: 도구 조합이 task 시나리오에서 동작하는지. 실제 LLM 또는 mock LLM 사용.
3. Eval tests: 평가 데이터셋에 대해 정성적 품질을 측정. 비결정적이지만 통계적으로 안정.
import pytest
from dataclasses import dataclass
# 1. Unit test — tool schema
def test_create_user_input_validation():
from pydantic import ValidationError
with pytest.raises(ValidationError):
CreateUserInput(email="invalid", name="A", role="admin")
# 2. Integration test — task flow
def test_report_generation_flow(mock_llm):
"""The report generation task uses only read_db."""
agent = build_agent(tools=["read_db"], llm=mock_llm)
result = agent.run(task=ReportTaskSpec(date="2026-05-03"))
assert result.status == "completed"
assert all(call.tool == "read_db" for call in result.tool_calls)
# 3. Eval test — qualitative quality
def test_summary_quality(eval_dataset):
agent = build_summary_agent()
scores = []
for example in eval_dataset:
output = agent.run(input=example.input)
scores.append(rubric_score(output, example.expected))
assert sum(scores) / len(scores) >= 0.85
세 계층 모두 필요합니다. Unit이 빠진 채 Eval만 있으면 디버깅이 불가능합니다. Eval이 빠진 채 Unit만 있으면 production 품질을 보장 못 합니다.
Eval Dataset 만들기

평가 데이터셋이 없으면 품질 측정이 불가능합니다. 데이터셋은 세 가지 출처에서 만듭니다.
1. Production logs: 실제 사용자의 요청을 샘플링합니다. 가장 현실적이지만 PII 처리가 필요합니다.
2. Synthetic generation: LLM으로 다양한 변형을 생성합니다. 빠르지만 실제 분포와 다를 수 있습니다.
3. Adversarial examples: 일부러 어렵게 만든 입력. Edge case와 prompt injection 시도.
@dataclass
class EvalExample:
"""A single eval example."""
id: str
input: dict
expected: dict # exact match or rubric-evaluated
category: str # "happy_path" | "edge" | "adversarial"
source: str # "production" | "synthetic" | "manual"
def build_eval_dataset() -> list[EvalExample]:
"""Balance the dataset across categories."""
examples = []
examples.extend(sample_from_production_logs(n=50, category="happy_path"))
examples.extend(generate_synthetic_variations(n=30, category="happy_path"))
examples.extend(load_manual_edge_cases(n=15, category="edge"))
examples.extend(load_adversarial_examples(n=5, category="adversarial"))
return examples
데이터셋 크기는 task의 복잡도에 따라 다릅니다. 단순 분류는 50
100개, 복잡한 multi-step task는 200
500개가 일반적입니다.
Rubric 기반 채점
Eval 결과를 어떻게 점수화할 것인가. 기대 출력과의 정확 매치는 Agent 출력에는 거의 안 맞습니다. 같은 의미를 다른 단어로 표현하기 때문입니다.
세 가지 채점 방식.
1. Exact match: 가능한 곳에는 사용. JSON 필드, 숫자, ID.
2. Heuristic checks: 정규식, 길이, 필수 단어 포함. 빠르고 결정적.
3. LLM-as-judge: 다른 LLM에게 채점을 맡깁니다. 비용 들지만 의미적 평가 가능.
from typing import Callable
@dataclass
class Rubric:
"""A bundle of scoring criteria."""
name: str
weight: float
check: Callable[[dict, dict], float] # (output, expected) -> 0.0..1.0
def has_required_sections(output: dict, expected: dict) -> float:
required = expected.get("required_sections", [])
if not required:
return 1.0
present = sum(1 for s in required if s in output.get("text", ""))
return present / len(required)
def numbers_match(output: dict, expected: dict) -> float:
e_nums = expected.get("numbers", {})
o_nums = output.get("numbers", {})
if not e_nums:
return 1.0
correct = sum(1 for k, v in e_nums.items() if abs(o_nums.get(k, 0) - v) < 0.01)
return correct / len(e_nums)
def llm_judge_helpfulness(output: dict, expected: dict) -> float:
"""Have an LLM rate helpfulness from 0 to 1."""
return 0.85 # actual: call judge LLM
RUBRICS = [
Rubric("structure", weight=0.3, check=has_required_sections),
Rubric("accuracy", weight=0.5, check=numbers_match),
Rubric("helpfulness", weight=0.2, check=llm_judge_helpfulness),
]
def rubric_score(output: dict, expected: dict, rubrics=RUBRICS) -> float:
return sum(r.check(output, expected) * r.weight for r in rubrics)
LLM-as-judge는 강력하지만 위험합니다. judge 모델의 편향이 점수에 그대로 반영됩니다. 사람 평가와 정기적으로 비교해서 calibration을 맞춥니다.
회귀 방지 자동화

테스트가 있어도 안 돌리면 의미가 없습니다. CI/CD에 통합해서 모든 변경에 대해 자동 실행합니다.
세 단계로 구성합니다.
1. Fast unit tests: PR마다 실행. 1분 이내.
2. Integration tests: PR마다 실행, mock LLM 사용. 5분 이내.
3. Full eval suite: 매일 또는 모델/프롬프트 변경 시 실행. 30분 이상 가능.
# .github/workflows/agent-tests.yml
"""
name: Agent Tests
on: [pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- run: pytest tests/unit -x --timeout=60
integration:
runs-on: ubuntu-latest
steps:
- run: pytest tests/integration -x --timeout=300
env:
USE_MOCK_LLM: "true"
eval:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'run-eval')
steps:
- run: python scripts/run_eval.py --dataset eval/v1 --threshold 0.85
"""
def run_eval_suite(dataset_path: str, threshold: float) -> bool:
"""Run the full eval and compare to threshold."""
examples = load_dataset(dataset_path)
results = []
for ex in examples:
output = run_agent(ex.input)
score = rubric_score(output, ex.expected)
results.append((ex.id, score))
avg = sum(s for _, s in results) / len(results)
failed = [(eid, s) for eid, s in results if s < 0.7]
print(f"Average: {avg:.3f}, Threshold: {threshold}")
print(f"Failed (<0.7): {len(failed)}")
return avg >= threshold
회귀가 발견되면 PR을 머지하지 않습니다. 이것이 Test Harness의 핵심 가치입니다 — 코드, 프롬프트, 모델 어느 것이 바뀌어도 품질이 떨어지지 않는다는 보장.
Snapshot Testing
Agent 출력의 미세한 변화를 잡고 싶을 때 snapshot test가 유용합니다. 첫 실행 결과를 저장하고, 다음 실행이 그 결과와 다르면 실패합니다.
import json
from pathlib import Path
import hashlib
def assert_snapshot(name: str, actual: dict, snapshot_dir: Path = Path("tests/snapshots")):
"""Compare against a saved snapshot."""
snapshot_dir.mkdir(parents=True, exist_ok=True)
snapshot_file = snapshot_dir / f"{name}.json"
actual_str = json.dumps(actual, sort_keys=True, indent=2)
if not snapshot_file.exists():
snapshot_file.write_text(actual_str)
print(f"snapshot created: {snapshot_file}")
return
expected_str = snapshot_file.read_text()
if actual_str != expected_str:
actual_hash = hashlib.sha256(actual_str.encode()).hexdigest()[:8]
expected_hash = hashlib.sha256(expected_str.encode()).hexdigest()[:8]
raise AssertionError(
f"snapshot mismatch for {name}\n"
f" expected: {expected_hash}\n"
f" actual: {actual_hash}\n"
)
def test_classification_snapshot(deterministic_agent):
"""The classification task's output does not change."""
result = deterministic_agent.classify("This product is amazing!")
assert_snapshot("positive_review_classification", result)
Snapshot test는 의도적인 변경에는 약합니다. 의도적으로 출력을 바꾸면 snapshot을 갱신해야 하는데, 그 갱신이 실수인지 의도인지 사람이 검토해야 합니다. PR 리뷰의 핵심 항목으로 만듭니다.
Common Mistakes
"개발하면서 만들겠다"는 보통 안 만듭니다. 첫 task부터 최소 20개의 예시를 준비합니다.
PII가 포함되어 있고, happy path에 편향되어 있습니다. 샘플링 + 마스킹 + adversarial 추가가 필요합니다.
judge 모델의 편향이 점수에 반영됩니다. 정기적으로 사람 평가와 비교합니다.
수동으로 가끔 돌리는 테스트는 곧 안 돌리게 됩니다. PR마다 자동 실행이 필수입니다.
"diff가 있네, 갱신!"으로 매번 통과시키면 snapshot의 의미가 없습니다. 차이는 사람이 검토합니다.
흔히 헷갈리는 지점
- eval 데이터셋은 나중에 쌓이면 된다고 생각하기 쉽지만, 대부분의 팀은 그렇게 미루다가 끝내 만들지 못합니다.
- production 로그를 그대로 쓰면 현실적이라고 보기 쉽지만, PII와 happy-path 편향 때문에 그대로는 쓸 수 없습니다.
- LLM-as-judge를 쓰면 사람이 전혀 필요 없다고 생각하기 쉽지만, judge 편향은 정기적으로 사람 평가와 맞춰야 합니다.
- 테스트를 수동으로 돌려도 된다고 생각하기 쉽지만, 수동 테스트는 곧 아무도 돌리지 않는 테스트가 됩니다.
- snapshot diff는 자동 승인해도 된다고 여기기 쉽지만, intentional change인지 regression인지 사람 검토가 필요합니다.
운영 체크리스트
- unit, integration, eval 세 층을 분리해 관리합니다.
- 최소 20개 이상의 초기 eval 예시를 준비한 뒤 첫 태스크를 배포합니다.
- production 로그, synthetic, adversarial 사례를 섞어 eval 데이터셋을 구성합니다.
- exact match, heuristic, LLM-as-judge를 함께 사용하고 judge를 정기적으로 보정합니다.
- 모든 테스트를 CI에서 자동 실행하고, eval은 임계값 미달 시 merge를 막습니다.
정리
Test Harness는 에이전트가 끝났다고 말하는 순간을 믿지 않고, 시스템 바깥의 검증 기준으로 다시 판정하는 층입니다. 이것이 있어야 데모와 운영 사이의 간극을 줄일 수 있습니다.
중요한 것은 하나의 거대한 테스트가 아니라 세 층의 조합입니다. Unit은 디버깅을 가능하게 만들고, integration은 흐름을 보장하며, eval은 전체 품질 변화를 수치로 보여 줍니다.
다음 글에서는 Feedback Loop를 다룹니다. 테스트가 실패했을 때 시스템은 그 실패를 사용자에게 그대로 돌려주지 않고, 다음 시도를 더 낫게 만드는 신호로 바꿔야 합니다.
시리즈 목차
- Harness Engineering이란 무엇인가?
- Task Harness — 모호한 일을 실행 가능한 작업으로 바꾸기
- Context Harness — Agent에게 줄 정보와 숨길 정보 설계하기
- Constraint Harness — 규칙, 경계, 금지 행동 정의하기
- Tool Harness — Agent가 사용할 도구를 안전하게 설계하기
- Test Harness — 완료 조건을 테스트로 고정하기 (현재 글)
- Feedback Loop — 실패를 고치게 만드는 반복 구조 (예정)
- Approval Gate — 사람 승인이 필요한 지점 설계하기 (예정)
- Observability — Agent 작업을 추적하고 재현하기 (예정)
- Production Harness — 운영 가능한 Agent 작업 환경 만들기 (예정)
참고 자료
공식 문서
'AI·LLM' 카테고리의 다른 글
| Harness Engineering 101 : Approval Gate — 사람 승인이 필요한 지점 설계하기 (2) | 2026.05.17 |
|---|---|
| Harness Engineering 101 : Feedback Loop — 실패를 고치게 만드는 반복 구조 (0) | 2026.05.17 |
| Harness Engineering 101 : Tool Harness — Agent가 사용할 도구를 안전하게 설계하기 (0) | 2026.05.17 |
| Harness Engineering 101 : Constraint Harness — 규칙, 경계, 금지 행동 정의하기 (0) | 2026.05.17 |
| Harness Engineering 101 : Task Harness — 모호한 일을 실행 가능한 작업으로 바꾸기 (0) | 2026.05.17 |
- Total
- Today
- Yesterday
- http
- serverless
- Cleancode
- Refactoring
- Prompt engineering
- LLM
- langchain
- Architecture
- Agent
- APIDesign
- harness
- openAI
- Tool Use
- rag
- backend
- Python
- AZURE
- Computer Science
- reliability
- Production
- ai safety
- embeddings
- Cloud
- DevOps
- softwaredesign
- Azure Functions
- AI Evaluation
- DesignPatterns
- ai agent
- vector search
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |

