LLM 에이전트의 불확실한 응답 품질을 자동화된 파이프라인으로 검증하고 관리합니다.
LLM 에이전트 개발은 일반적인 백엔드 개발과 다릅니다. 프롬프트 한 줄, 소스코드 한 줄의 수정이 수백 개의 테스트 케이스에 어떤 영향을 줄지 예측하기 어렵습니다. 이 프로젝트는 "딱 떨어지지 않는 LLM 응답"을 효율적으로 검증하기 위해 시작되었습니다. 매일 반복되는 수동 테스트와 번역 작업을 자동화하여, 개발자가 본질적인 제품의 가치에 집중할 수 있도록 돕습니다.
- Multi-Environment Support: Local부터 Production까지 다양한 환경의 API를 한 번에 테스트합니다.
- Sync + Streaming 동시 검증: 각 테스트 케이스를 일반(
agentChat)과 스트리밍(agentChatStream, SSE) 엔드포인트 양쪽으로 호출하여 모드별로 시트에 기록합니다. 스트리밍 행에는 TTFT(첫 토큰까지 시간)와 토큰 수 지표가 함께 남습니다. - AI-Driven Judge: Google Gemini API 또는 로컬 LLM(Ollama/Gemma2)을 활용하여 응답의 적절성을 자동으로 판독합니다.
- Auto Translation: Google Sheets의
=GOOGLETRANSLATE를 활용하여 베트남어 등 외국어 응답을 실시간으로 번역하여 확인합니다. - Slack Integration: 테스트 완료 후 성공/실패 여부를 슬랙으로 즉시 알림 받아 리얼타임 피드백 루프를 형성합니다.
- Universal Framework: 날씨, 음악, 일정 등 모든 도메인의 에이전트 테스트에 적용 가능한 범용적인 구조를 제공합니다.
- 검증 리소스 95.8% 절감: 기존 8시간 소요되던 전수 검사를 20분 내외(검토 시간 기준)로 단축.
- 품질 안정성 확보: 수백 개의 케이스를 상시 검증하여 공식 시연 및 UAT 리스크 최소화.
- Node.js (Jest)
- Ollama (Local LLM 사용 시)
- Google Cloud Platform (Google Sheets API 사용 시)
프로젝트 루트에 .env 파일을 생성하고 아래 형식을 참고하여 설정.
(보안을 위해 .env 파일은 절대 Git에 커밋 금지!)
CONTROL_BASE_URL="" # fallback for legacy commands
X_API_KEY=""
AI_API_KEY=""
AI_MODEL="gemini-3-flash-preview"
# GPT judge (Redis config:llm:<id>)
GPT_JUDGE_LLM_ID=6
GPT_JUDGE_MAX_TOKEN=12000
REDIS_URL="redis://127.0.0.1:6379"
GOOGLE_SERVICE_ACCOUNT_EMAIL=""
GOOGLE_SHEET_ID=""
# Optional fallback for legacy commands
GOOGLE_SHEET_NAME=""
# Optional: override the profile config file path
TEST_PROFILE_CONFIG="tests/config/profiles.yaml"
GOOGLE_PRIVATE_KEY=""
ACCOUNT_ID=""
# Number of isolated account lanes used for parallel testcase execution.
PARALLEL_ACCOUNT_COUNT=5
AGENT_VERSION=""
DEVICE_ID=""
OS_APP_TYPE=""
OS_APP_VERSION=""
ACCEPT_LANGUAGE=""
TRACE_ID=""
LANGUAGE=""
TEST_TIMEOUT="120000"
# [Service Delay]
SERVICE_DELAY_SEC=1
# [Judge Delay]
# for Gemini API
DELAY_API=12
# for local Ollama
DELAY_LOCAL=0
LOCAL_AI_MODEL='gemma2:27b'
LOCAL_AI_TEMPERATURE=0.1 # 일관된 판정을 위해 낮게 설정
LOCAL_AI_MAX_TOKEN=200 # 답변이 너무 길어지지 않게 제한
GOOGLETRANSLATE_SOURCE_LANGUAGE="auto"
GOOGLETRANSLATE_TARGET_LANGUAGE="en"
RESPONSE_TRANSLATION_PROVIDER="googletranslate" # googletranslate | gpt
# Required only when RESPONSE_TRANSLATION_PROVIDER=gpt
RESPONSE_TRANSLATION_LLM_ID=""
RESPONSE_TRANSLATION_MAX_TOKEN=1000
RESPONSE_TRANSLATION_PROMPT_FILE="prompt.translate.yaml"
# GPT translation reads Redis config:llm:<RESPONSE_TRANSLATION_LLM_ID> using REDIS_URL below.
# Redis stream answer compare watcher
REDIS_URL="redis://127.0.0.1:6379"
WEATHER_ANSWER_COMPARE_STREAM_KEY="weather:answer-compare"
WEATHER_ANSWER_COMPARE_STREAM_GROUP="weather-answer-compare"
WEATHER_ANSWER_COMPARE_STREAM_CONSUMER="watcher-1"
WEATHER_AGENT_RESPONSE_STREAM_KEY="weather:agent-response"
PUBLISH_AGENT_RESPONSE_STREAM=0
JOIN_AGENT_RESPONSE_STREAM=0
AGENT_RESPONSE_CACHE_TTL_SEC=3600
AGENT_RESPONSE_JOIN_TIMEOUT_MS=10000
READ_EXISTING_PAYLOADS=0
STREAM_BLOCK_MS=5000
PENDING_RETRY_INTERVAL_MS=30000
GEMMA_TEST_LLM_ID=2
GEMMA_TEST_MODEL="mediaai1/gemma-27b-generation-v3.0.0"
GPT_TEST_LLM_ID=3
OLLAMA_URL="http://localhost:11434/v1/chat/completions"
OLLAMA_MODEL="gemma3:27b"
GOOGLE_SHEET_TAB="WeatherAnswerCompare"
EVALUATE_WITH_GPT=0
EVALUATE_PAYLOAD_WITH_GPT=0
GPT_JUDGE_LLM_ID=3
EVALUATE_CASE_KEYS="" # optional: gemini_t0,gemini_t03
# Slack Settings
SLACK_WEBHOOK_URL=""
SLACK_CHANNEL=""-
Google Sheets API Key
https://console.cloud.google.com/welcome/new
Enable Google Sheets API
Credentials 탭 클릭 → Create Credentials → Service Account Service Accounts→ Keys→ Create private key→ Json 다운로드된 JSON 파일에서
client_email→ Service Account 이메일private_key→ Private Key
이 두 개를
.env에 세팅GOOGLE_SERVICE_ACCOUNT_EMAIL="" GOOGLE_PRIVATE_KEY=""
-
Google Sheets ID
google docs로 파일을 만든 후
주소를 복사하여
https://docs.google.com/spreadsheets/d/ + GOOGLE_SHEET_ID + /edit?gid=0#gid=0
이 구조로 되어있으니 GOOGLE_SHEET_ID를 추출하고 .env에 설정
GOOGLE_SHEET_ID="" GOOGLE_SHEET_NAME="" # legacy fallback TEST_PROFILE_CONFIG="tests/config/settings/profiles.yaml"
GOOGLE_SHEET_NAME은 결과를 기록할 스프레드시트 탭 이름입니다. 프로필을 지정하지 않고 실행할 때만 이 값을 사용합니다. 해당 탭이 없으면 테스트 실행 중 자동으로 생성됩니다.프로필 실행은
tests/config/settings/profiles.yaml에서 읽습니다. 새 환경은 YAML에 추가만 하면 되고,crow같은 이름도 코드 수정 없이 바로 사용할 수 있습니다. -
Gemini
https://ai.google.dev/gemini-api/docs/models 여기 가서 모델 고르기
.env에 설정
AI_MODEL="gemini-3-flash-preview"
-
Ollama
- Ollama 공식 홈페이지에서 다운로드 후 설치
- 모델 고르기
- 다운받기
ollama pull gemma2:9b
- .env 설정
LOCAL_AI_MODEL='gemma2:27b'
Wrap sheet config: tests/config/settings/sheet.yaml
어떤 column을 wrap 허용할지 설정(Google Sheet에서)
wrapColumns:
- E
- FProfile config: tests/config/settings/profiles.yaml
profiles:
prod:
baseUrl: https://prod.com/api
sheetName: prod
stg:
baseUrl: https://stg.com/api
sheetName: stgnpm install
npm run test:sheet:api:dev
npm run test:sheet:internal:stg
npm run test:sheet:local:local
npm run test:terminal:prod
node scripts/run-test-profile.js sheet:api:crow
npm run test:profile -- sheet:gpt:local --mode syncRedis Stream watcher can also ask GPT to judge each candidate answer and append the result to the same Google Sheet.
REPORT_TO=sheet EVALUATE_WITH_GPT=1 npm run watch:weather:answer-compareOptional knobs:
GPT_JUDGE_LLM_ID: Redisconfig:llm:<id>used for the GPT judge. Defaults toGPT_TEST_LLM_ID.EVALUATE_CASE_KEYS: comma-separated candidate keys to judge, such asgemini_t0,gemini_t03. Empty means all configured cases.
Judge columns are added per candidate: verdict, score, categories, summary, issues, and judge error. The rubric covers data fidelity, relative date/time alignment, summary/range aggregation, unsupported inference, advice policy, unavailable data handling, and field mapping.
Set EVALUATE_PAYLOAD_WITH_GPT=1 to independently judge the dumped pre-LLM payload against the user query. The sheet receives payload verdict/score, expected and actual intent, per-policy checks, issues, errors, and latency. Missing entity/card/context fields are marked NA rather than failed.
For an end-to-end audit, set PUBLISH_AGENT_RESPONSE_STREAM=1 in the regression runner and JOIN_AGENT_RESPONSE_STREAM=1 in the answer-compare watcher. The runner publishes API response snapshots to WEATHER_AGENT_RESPONSE_STREAM_KEY; the watcher joins them to LLM dumps by transactionId/trxId. The combined judge can then verify response entity and cards against the normalized LLM weather data and records API entity/today/hourly/weekly cards in the same sheet row. Snapshots are cached only for the configured TTL to make the asynchronous stream join reliable.
The normal sync regression command also joins its own successful request to the Redis dump and writes payload, prompt, weather data, and the GPT payload-policy evaluation into the same result row. Requests rejected before the LLM stage (non-200 product result codes such as unavailable past weather) are marked payloadStatus=NOT_APPLICABLE; when that response also omits entity, entity golden status is NA rather than a false missing-field failure.
Entity goldens are paired inline with each YAML testcase using expectedEntity:. Matching defaults to subset mode: every declared semantic field is strict, while unrelated entity fields are ignored. Set entityMatchMode: exact only for a full-shape contract test. Legacy expect: blocks are not treated as current goldens because they describe an older parser contract. Optional leaf matchers are { $any: true }, { $regex: "..." }, and { $oneOf: [...] }. Golden status, expected JSON, and field-level differences are written to both the normal regression sheet and the combined model sheet; a mismatch fails the testcase.
Redis stream answer compare watcher
npm run watch:weather:answer-compare
REPORT_TO=sheet npm run watch:weather:answer-compare
READ_EXISTING_PAYLOADS=1 npm run watch:weather:answer-compare이 watcher는 WEATHER_ANSWER_COMPARE_STREAM_KEY stream의 payload 필드(JSON)를 읽어서 GemmaProd, Ollama, GPT 세 모델 응답을 비교하고 terminal 또는 Google Sheet에 기록합니다.
Legacy-compatible selectors
"test:all": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/runner --config jest.config.ts",
"test:profile": "node scripts/run-test-profile.js",
"test:sheet:none": "node scripts/run-test-profile.js",
"test:sheet:api:dev": "node scripts/run-test-profile.js",
"test:sheet:local:local": "node scripts/run-test-profile.js",
"test:terminal:prod": "node scripts/run-test-profile.js",
"test:terminal:ai:crow": "node scripts/run-test-profile.js"기존 npm run test:sheet:api -- --dev 같은 -- 방식도 그대로 지원합니다.
나의 구글 시트에 들어가보면 다음과같이 잘 정리된 테스트 케이스들이 있고 나는 Fail인 것만 검토를 하면 됩니다.

- Jest/Axios: 설정된 환경별로 테스트 케이스(JSON) 기반 API 호출 수행.
- Local AI (Gemma2): 수집된 응답 데이터가 질문 의도에 부합하는지 1차 판독.
- Google Sheet: 결과를 시트로 전송하여 자동 번역 및 시각화 수행.
- Slack Webhook: 전체 리포트 요약을 개발자에게 전송.
이 프로젝트는 더 효율적인 AI 에이전트 개발 문화를 지향합니다. 버그 리포트나 기능 제안은 언제나 환영합니다!
