매일매일
AI에 관련한 질문이에요.

AI 하네스(Harness)는 무엇이고 왜 필요한가요?

2026-04-17AILLM하네스테스트프롬프트 평가

핵심 요약 (Summary)

AI 하네스(Harness)는 LLM(대형 언어 모델)의 출력을 평가하고 테스트하기 위한 프레임워크입니다. 일반 코드는 입력이 같으면 출력이 항상 같지만, AI는 같은 입력에도 매번 다른 결과를 반환할 수 있습니다. 하네스는 이 비결정적인 출력을 체계적으로 검증하고, 프롬프트가 바뀌거나 모델이 업데이트될 때 품질이 유지되는지 측정하는 역할을 합니다. AI 기능을 서비스에 통합할 때 테스트 전략 없이 배포하면 출력 품질을 보장할 수 없습니다.


왜 이런 문제가 발생하나요? (Why)

AI를 서비스에 통합할 때 두 가지 문제가 반복됩니다.

첫째, 프롬프트를 수정했더니 다른 기능이 망가집니다. 블로그 요약 기능의 프롬프트를 개선했는데, 태그 추출 기능의 품질이 떨어지는 경우입니다. 모든 AI 기능이 프롬프트와 모델에 의존하기 때문에 한 곳의 변경이 다른 곳에 영향을 줄 수 있지만, 테스트가 없으면 배포 후에야 발견합니다.

둘째, 모델이 업데이트되면 기존 동작이 달라집니다. OpenAI나 Anthropic이 모델을 업데이트하면 같은 프롬프트에 대한 응답 형식이나 품질이 바뀔 수 있습니다. 하네스 없이는 이 변화를 감지할 방법이 없습니다.

일반 코드 테스트와 AI 하네스의 차이를 비교하면 다음과 같습니다.

항목일반 테스트AI 하네스
출력 특성결정적 (항상 동일)비결정적 (매번 다를 수 있음)
검증 방식정확한 값 비교 (toBe)패턴, 형식, 의미적 유사도 검증
실패 기준값이 다르면 실패품질 점수가 임계값 미달이면 실패
비용무료API 호출 비용 발생
속도빠름느림 (네트워크 지연)

예시 코드 — 잘못된 사용 (Bad Example)

hljs
// AI 출력을 테스트 없이 그대로 사용하는 경우
// 프롬프트가 바뀌거나 모델이 업데이트되면 출력 형식이 달라질 수 있습니다.

async function extractTags(content: string): Promise<string[]> {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'user',
        content: `다음 글에서 태그를 추출해줘:\n\n${content}`,
        // 출력 형식을 지정하지 않아 "React, Next.js, TypeScript"가 될 수도
        // ["React", "Next.js", "TypeScript"]가 될 수도 있습니다.
      },
    ],
  });

  // 어떤 형식으로 오든 그냥 split — 형식이 바뀌면 런타임 에러
  return response.choices[0].message.content!.split(', ');
}
hljs
// 프롬프트를 바꿀 때마다 수동으로 확인하는 경우
// 어떤 케이스가 잘 동작하고 어떤 케이스가 망가졌는지 추적할 수 없습니다.

// 개발자가 콘솔에서 직접 호출해 결과를 눈으로 확인
const result = await extractTags('React hooks에 대한 글입니다.');
console.log(result); // 매번 다를 수 있고, 히스토리가 남지 않습니다.

올바른 사용법 (Good Example)

hljs
// 1단계 — 출력 형식을 구조화해 비결정성을 줄입니다.
// JSON 형식을 강제하면 파싱 실패를 방지할 수 있습니다.

async function extractTags(content: string): Promise<string[]> {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: '태그를 추출하면 반드시 JSON 배열 형식으로만 응답하세요. 예: ["React", "TypeScript"]',
      },
      {
        role: 'user',
        content: `다음 글에서 관련 태그를 최대 5개 추출해주세요:\n\n${content}`,
      },
    ],
    response_format: { type: 'json_object' }, // 구조화된 출력 강제
  });

  const parsed = JSON.parse(response.choices[0].message.content!);
  return parsed.tags ?? [];
}
hljs
// 2단계 — 하네스 테스트 케이스 작성
// 실제 API를 호출하는 통합 테스트와 모킹을 사용하는 단위 테스트를 분리합니다.

// harness/tag-extraction.test.ts

describe('태그 추출 하네스', () => {
  // 단위 테스트 — API 모킹으로 형식과 처리 로직만 검증
  test('JSON 배열을 올바르게 파싱한다', async () => {
    vi.mocked(openai.chat.completions.create).mockResolvedValue({
      choices: [{ message: { content: '{"tags": ["React", "TypeScript"]}' } }],
    } as any);

    const tags = await extractTags('React와 TypeScript에 대한 글');
    expect(tags).toEqual(['React', 'TypeScript']);
    expect(Array.isArray(tags)).toBe(true);
  });

  // 통합 테스트 — 실제 API 호출 (CI에서는 스킵, 배포 전 수동 실행)
  test.skipIf(!process.env.RUN_AI_TESTS)(
    '실제 API: 프론트엔드 글에서 관련 태그를 반환한다',
    async () => {
      const tags = await extractTags(
        'useEffect에서 비동기 함수를 직접 사용하면 안 되는 이유'
      );

      // 정확한 값 비교 대신 형식과 최소 품질 기준만 검증
      expect(tags.length).toBeGreaterThan(0);
      expect(tags.length).toBeLessThanOrEqual(5);
      expect(tags.every((t) => typeof t === 'string')).toBe(true);

      // 의미적 관련성 — 핵심 키워드 포함 여부
      const lowerTags = tags.map((t) => t.toLowerCase());
      const hasRelevantTag = lowerTags.some((t) =>
        ['react', 'useeffect', 'async', '비동기'].includes(t)
      );
      expect(hasRelevantTag).toBe(true);
    },
    30_000 // AI 응답 대기 타임아웃
  );
});
hljs
// 3단계 — 골든 데이터셋으로 회귀 테스트
// 프롬프트나 모델을 바꿀 때 기준 케이스 대비 품질을 측정합니다.

const GOLDEN_DATASET = [
  {
    input: 'React의 useState와 useReducer의 차이점',
    expectedTags: ['React', 'useState', 'useReducer'],
    minMatchCount: 2, // 3개 중 최소 2개는 포함되어야 합니다.
  },
  {
    input: 'Next.js App Router에서 서버 컴포넌트 사용법',
    expectedTags: ['Next.js', 'App Router', 'Server Component'],
    minMatchCount: 2,
  },
];

async function runRegressionTest() {
  let passed = 0;

  for (const { input, expectedTags, minMatchCount } of GOLDEN_DATASET) {
    const tags = await extractTags(input);
    const lowerTags = tags.map((t) => t.toLowerCase());
    const matchCount = expectedTags.filter((e) =>
      lowerTags.some((t) => t.includes(e.toLowerCase()))
    ).length;

    const ok = matchCount >= minMatchCount;
    console.log(`${ok ? 'PASS' : 'FAIL'} | "${input}" → ${tags.join(', ')}`);
    if (ok) passed++;
  }

  const score = (passed / GOLDEN_DATASET.length) * 100;
  console.log(`\n통과율: ${score}% (${passed}/${GOLDEN_DATASET.length})`);

  if (score < 80) {
    throw new Error('AI 품질 기준 미달 — 프롬프트를 검토하세요.');
  }
}

골든 데이터셋 기반 회귀 테스트를 배포 파이프라인에 연결하면, 프롬프트나 모델이 바뀔 때 품질 저하를 자동으로 감지할 수 있습니다.


정리 (Conclusion)

AI 하네스는 비결정적인 LLM 출력을 체계적으로 검증하는 테스트 전략입니다. 정확한 값 비교가 아닌 형식, 형태, 의미적 관련성을 기준으로 검증합니다. 출력 형식을 구조화해 비결정성을 줄이고, 골든 데이터셋으로 회귀 테스트를 운영하면 프롬프트나 모델 변경에 안전하게 대응할 수 있습니다.

전략방법목적
출력 구조화response_format: json_object파싱 안정성 확보
단위 테스트API 모킹 + 형식 검증빠른 피드백, CI 통합
통합 테스트실제 API 호출 + 품질 기준실제 동작 검증
회귀 테스트골든 데이터셋 + 통과율 측정프롬프트/모델 변경 감지

추가 학습 자료 공유합니다.


목록으로 돌아가기