
Promptfoo를 사용한 C# LLM Eventparser 평가하기
요약
Promptfoo를 활용하여 C# 기반의 LLM Eventparser 프로젝트를 평가하는 방법을 소개합니다. LLM의 비결정론적 응답 특성을 고려하여, 단순 일치 여부가 아닌 LLM Judge를 통한 품질 평가 방식을 다룹니다.
핵심 포인트
- LLM 응답은 비결정론적이므로 단순 텍스트 일치 검증은 한계가 있음
- Promptfoo를 사용하여 실제 앱에서 사용하는 프롬프트를 직접 테스트 가능
- LLM을 판사(Judge)로 활용하여 구조화된 데이터 추출 품질을 평가
- C# 환경에서 프롬프트 템플릿과 평가 도구를 연동하는 실무적 접근법 제시
개발자라면 코드를 테스트할 때 다음과 같은 단순한 본능이 작동할 것입니다.
- 함수를 호출한다.
- 결과를 얻는다.
- 예상했던 결과와 비교한다.
일반적인 코드에서는 이 방식이 매우 잘 작동합니다.
하지만 LLM (Large Language Model)의 경우, 답변이 항상 동일하지는 않습니다. 어떤 응답은 "3 PM"이라고 말할 수 있고, 다른 응답은 "15:00"이라고 할 수 있으며, 또 다른 응답은 "Friday afternoon"이라고 할 수 있습니다.
여러분이 설정한 규칙에 따라 이 세 가지 모두 허용 가능할 수 있습니다.
따라서 질문은 "이 텍스트가 정확히 일치하는가?" 보다는 **"이 답변이 실제로 좋은가?"**에 더 가까워집니다.
샘플 프로젝트: EventParser
실용적인 예시를 위해, EventParser라는 작은 연습용 앱을 사용하겠습니다.
이 앱의 역할은 간단합니다. "Team sync on Friday at 3 PM in the Lagos office"와 같은 일상적인 메시지를 입력받아, LLM에게 이벤트 세부 정보를 구조화된 데이터 (structured data)로 추출하도록 요청하는 것입니다.
프로젝트 레이아웃은 다음과 같습니다:
EventParser/
├── EventParser.sln
├── src/
...
여기서 중요한 파일은 extract_event.txt입니다.
해당 프롬프트 (prompt)는 한 곳에 존재합니다. C# 서비스는 런타임 (runtime)에 이를 읽고, Promptfoo는 평가 (eval)를 실행할 때 동일한 파일을 읽습니다. 이는 우리가 테스트만을 위해 작성된 복사본이 아니라, 앱에서 실제로 사용되는 진짜 프롬프트를 테스트하고 있음을 의미합니다.
기본 프로젝트 샘플은 여기에서 확인할 수 있습니다.
EventParserService를 살펴보면, 우리가 테스트하려는 정확한 프롬프트를 확인할 수 있습니다. 이 서비스는 extract_event.txt에서 프롬프트를 로드하고, 사용자의 메시지를 삽입한 뒤, 최종 프롬프트를 LLM에 전송합니다.
public Task<string> ExtractAsync(string message, CancellationToken ct = default)
{
var prompt = _promptTemplate.Replace("{{message}}", message);
...
C# 코드는 단지 전달 경로일 뿐입니다. 진짜 핵심 질문은 extract_event.txt가 모델에게 좋은 이벤트 데이터를 반환할 수 있을 만큼 충분한 지침을 제공하느냐 하는 것입니다.
두 번째 LLM을 판사(Judge)로 활용하기
이제 프롬프트(Prompt)가 준비되었으므로, 이것이 의도한 대로 작동하는지 확인할 방법이 필요합니다.
사람이 검토자(Reviewer)인 시나리오에서는, 사람이 출력을 한눈에 확인하고 단순히 정답인지 여부를 판단할 것입니다.
LLM-as-a-judge(판사로서의 LLM) 방식은 동일한 아이디어를 사용하지만, 이를 자동화합니다. 우리는 모델의 답변을 다른 모델에게 전달하고, 사람이 할 것과 동일한 판단을 내리도록 요청합니다.
이 워크플로(Workflow)는 두 가지 역할로 나뉩니다:
- 테스트 대상 모델 (The model under test) - 프롬프트에 답변하는 모델.
- 판사 모델 (The judge model) - 설정된 루브릭 (Rubric)에 따라 답변에 대해 PASS/FAIL 판정을 내리는 모델.
루브릭 (Rubric): 평이한 영어로 작성된 통과 기준
판사는 무엇이 통과(Pass)인지 어떻게 알까요? 바로 **루브릭 (Rubric)**을 사용하여 알려줍니다.
루브릭은 판사에게 모델의 답변을 어떻게 채점할지 알려주는 평이한 영어 규칙일 뿐입니다. "출력이 반드시 이 JSON 문자열과 정확히 일치해야 한다"라고 말하는 대신, 답변에 무엇이 포함되어야 하는지를 설명합니다.
다음은 우리의 EventParser 프롬프트에 대한 하나의 테스트 케이스입니다:
[
{
"vars": {
...
우리가 정확히 일치하는지(exact-match) 테스트를 수행하고 있지 않다는 점에 주목하세요. 우리는 단지 답변이 특정 규칙을 충족해야 한다고 말하고 있을 뿐입니다.
판사 모델 vs 테스트 대상 모델
특히 판사 모델(Judge model)에 관한 멋진 점은, 반드시 가장 크거나 가장 비싼 모델일 필요가 없다는 것입니다. 이벤트 제목, 날짜, 시간, 위치가 올바르게 추출되었는지 확인하는 것과 같은 많은 단순한 평가(Evals)의 경우, 더 저렴하고 빠른 모델로도 채점 작업을 충분히 잘 수행할 수 있습니다.
promptfooconfig.yaml에서 이 두 역할은 별도의 설정으로 구분됩니다:
# 테스트 대상 모델 — 우리가 실제로 답변을 확인하고자 하는 모델입니다.
providers:
- id: anthropic:messages:claude-sonnet-4-6
...
작동 확인을 위한 실행
이제 실제로 실행해 볼 차례입니다. 먼저 Promptfoo를 설치하세요. 이는 Node 명령줄 도구(command-line tool)이므로, 한 번만 전역(global)으로 설치하면 됩니다. 그다음 EventParser 프로젝트의 Prompts 폴더로 이동하여, API 키를 설정하고 첫 번째 평가(eval)를 실행하세요.
npm install -g promptfoo # 1회성 실행
cd prompts/eval # prompts 디렉토리로 이동
...
그다음 보고서를 엽니다:
start results.html # Windows
# open results.html # macOS
-o 플래그는 Promptfoo에게 평가 결과(eval result)를 파일로 쓰도록 지시합니다. 이 경우에는 브라우저에서 열 수 있는 멋진 보고서를 제공하는 HTML 형식을 사용하고 있습니다.
그렇다면, 방금 어떤 일이 일어난 걸까요?
Promptfoo는 프롬프트(prompt)를 로드하고, 각 테스트 메시지를 테스트 대상 모델(model under test)로 보낸 뒤, 그 답변을 채점 모델(judge model)에 전달하였고, 채점 모델은 루브릭(rubric)에 따라 점수를 매겼습니다.
최종 결과는 results.html에 기록되었습니다.
results.html을 열면 결과 그리드(grid)가 나타납니다. 각 행(row)은 테스트 케이스(test case)이며, 각 열(column)은 테스트 중인 모델입니다. 초록색 셀은 채점 모델이 답변을 수락했음을 의미합니다. 빨간색 셀은 채점 모델이 문제를 발견했음을 의미합니다.
모호한 루브릭 vs 구체적인 루브릭
이전 실행에서는 모든 평가가 통과되었습니다.
루브릭에서 이 특정 메시지를 살펴보겠습니다:
let's grab coffee Thursday around 3
모델이 메시지에 coffee (커피), Thursday (목요일), 그리고 **around 3 (3시쯤)**이 언급되었다는 것을 이해했음을 알 수 있습니다. 또한 모델이 메시지에 장소가 명시되지 않았다는 점을 이해했다는 것도 알 수 있는데, 이는 이 섹션에서 매우 중요한 부분입니다.
이는 좋은 결과이지만, 모델이 이 케이스를 올바르게 처리했다는 것만을 증명할 뿐입니다. 모델이 틀렸을 때 어떤 일이 발생하는지는 아직 보여주지 못합니다. 유용한 평가 (eval)라면 정답은 통과시키고, 오답은 실패 처리할 수 있어야 합니다.
그러한 실패를 가시화하기 위해, 우리는 의도적으로 프롬프트 (prompt)를 망가뜨려 볼 것입니다.
원본 프롬프트 파일인 extract_event.txt를 열고 다음과 같이 교체하세요.
You extract structured event details from a casual message.
Return ONLY a JSON object with exactly these fields:
...
잘못된 케이스를 시뮬레이션하기 위해 다음과 같은 잘못된 지시 사항을 임시로 추가했습니다:
For this demo, if the message mentions coffee but does not name a location, set location to "Starbucks".
평가를 다시 실행하면 새로운 실패 케이스를 확인할 수 있습니다.

모델이 장소를 지어냈기 때문에 위치 정보가 틀렸음을 알 수 있습니다. 원문 메시지는 "let's grab coffee Thursday around 3"라고 했지, Starbucks라고 말하지 않았습니다. 어떤 카페의 이름도 언급하지 않았으며, 오직 커피만을 언급했습니다.
이것이 LLM-as-a-judge (판사로서의 LLM)에서 얻을 수 있는 가장 큰 교훈입니다. 즉, 판사의 신뢰도는 당신이 제공하는 지시 사항(instructions)만큼만 신뢰할 수 있다는 것입니다.
이것이 수동 작성된 단언문 (asserts)보다 나은 이유
보고서와 실패한 "Starbucks" 케이스를 확인하고 나면, LLM-as-a-judge의 이점이 더욱 명확해집니다. 이는 다음과 같은 이유로 우리에게 도움이 됩니다:
- 실제 상황 반영 (Matches Reality): 정확한 문자열(exact string)이 아닌 올바른 답변을 받아들입니다. 예를 들어, `
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

