CI/CD에서 실험하기(Experiments in CI/CD)
CI/CD에서 실험하기(Experiments in CI/CD)
Langfuse 실험을 CI/CD 파이프라인에서 사용해 품질 회귀를 배포 전에 잡아낼 수 있습니다. 이 문서는 릴리스 정책 선택, GitHub Actions 워크플로우, 실험 정의, 액션 입력/출력, 회귀 실패 처리, 승인된 baseline과의 비교를 다룹니다.
출처: 문서
본문
Langfuse 실험을 CI/CD 파이프라인에서 사용해 품질 회귀를 배포 전에 잡아내세요.
워크플로우는 다음과 같습니다:
- 테스트 케이스로 Langfuse 데이터셋 생성
- 데이터셋에 대해 애플리케이션을 테스트하는 Python 또는 JS/TS SDK 실험 작성
- task 출력에 점수를 매길 평가자 추가
- 점수가 내 임계값을 위반하면
RegressionError발생 - langfuse/experiment-action 으로 스크립트를 실행하는 GitHub Actions 워크플로우 생성
릴리스 정책 선택
게이트를 작성하기 전에 무엇이 릴리스를 막아야 하는지 결정하세요:
| 정책 | 릴리스를 막는 경우 |
|---|---|
| 알려진 실패 | 모든 필수 케이스가 절대 품질 요구사항을 통과해야 함 |
| 검토 후 수정되거나 필수 집합에서 명시적으로 제거되기 전까지 차단 | |
| 승인된 통과 케이스의 회귀 없음 | 승인된 baseline에서 통과했던 케이스가 이제 실패 |
이 정책을 결합할 수 있습니다. 중요 케이스는 통과를 요구하고 나머지 승인된 통과 케이스는 회귀로부터 보호하세요. 누락된 케이스, 중복 케이스 ID, task 실패, 누락되거나 유효하지 않은 평가자 결과는 통과로 취급하지 말고 게이트를 실패시켜야 합니다.
실패한 실행이 첫 번째나 최신 실행이라는 이유만으로 승인된 baseline이 되지는 않습니다. 알려진 실패를 명시적으로 검토하세요. 입력, expected output, 필수 케이스 집합, 평가자 정의가 바뀌면 릴리스를 비교하기 전에 baseline을 검토·업데이트하세요. baseline 아티팩트와 버전 식별자를 저장소에 유지하세요. 구현은 승인된 baseline과 비교 를 참고하세요.
GitHub Actions 워크플로우
필요한 트리거 로 워크플로우를 만들고, 예를 들어 pull_request나 release를 사용하세요. langfuse/experiment-action 릴리스 의 릴리스로 액션을 고정하세요.
액션은 기본적으로 최신 SDK 버전을 설치합니다. 특정 SDK 버전을 원할 때만 python_sdk_version이나 js_sdk_version을 설정하세요.
GitHub Action은 Langfuse Python SDK v4.6.0+ 또는 JS SDK v5.3.0+를 요구합니다.
.github/workflows/langfuse-experiment.yml:
name: Langfuse experiment gate
on:
# Run the gate for every pull request. Change this to `push`, `release`, or another
# trigger if you want to run experiments at a different point in your workflow.
pull_request:
permissions:
# Required to check out the repository.
contents: read
# Required to post or update the experiment result comment on pull requests.
pull-requests: write
# Optional: lets the result link to this specific job's logs.
# Without this permission, the action falls back to the workflow-run URL.
actions: read
jobs:
experiment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Add this only if your experiments use the Python SDK
- uses: actions/setup-python@v6
with:
python-version: "3.14"
# Add this only if your experiments use the JS/TS SDK
- uses: actions/setup-node@v6
with:
node-version: "24"
- uses: langfuse/experiment-action@<release tag>
with:
# the credentials for Langfuse
langfuse_public_key: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
langfuse_secret_key: ${{ secrets.LANGFUSE_SECRET_KEY }}
langfuse_base_url: https://cloud.langfuse.com
# the location of your experiment scripts
experiment_path: experiments/support-agent-gate
# the dataset to run the experiment against
dataset_name: support-agent-regression-set
# GitHub token so that the action can comment on PRs
github_token: ${{ github.token }}
실험 정의(Experiment definition)
액션은 워크플로우에 구성된 experiment_path에서 내 실험 코드를 실행합니다.
각 스크립트는 context 매개변수를 받는 experiment(context) 함수를 정의해야 합니다.
이 컨텍스트는 GitHub Action이 만들며 CI 관련 설정을 대신 처리합니다:
- 액션 입력에서 Langfuse SDK 클라이언트 초기화
dataset_name에서 데이터셋 아이템을 로드하고dataset_version적용- commit SHA, branch, job URL, actor 같은
langfuse.*기본 메타데이터 추가. 이 값들은 Langfuse UI에서 볼 수 있습니다.
이 기본값들과 함께 실험을 실행하려면 context.runExperiment(JS/TS) 또는 context.run_experiment(Python)을 사용하세요.
experiments/support-agent-gate.py:
from langfuse import RunnerContext
from langfuse.api import DatasetItem
# Define a task that calls your agent with each dataset item.
def my_task(item: DatasetItem, **kwargs):
...
def experiment(context: RunnerContext):
return context.run_experiment(
name="PR gate",
task=my_task,
)
experiments/support-agent-gate.ts:
import type { ExperimentTaskParams, RunnerContext } from "@langfuse/client";
// Define a task that calls your agent with each dataset item.
async function myTask(item: ExperimentTaskParams) {
// ...
}
export async function experiment(context: RunnerContext) {
return await context.runExperiment({
name: "PR gate",
task: myTask,
});
}
data나 metadata 같은 액션 제공 기본값을 덮어쓰려면 context.runExperiment / context.run_experiment에 명시적 값을 전달하세요.
액션 입력과 출력(Action inputs and outputs)
| 입력 | 필수 | 설명 |
|---|---|---|
langfuse_public_key |
예 | SDK 클라이언트가 사용하는 Langfuse 공개 키. GitHub secret 으로 저장 |
langfuse_secret_key |
예 | SDK 클라이언트가 사용하는 Langfuse 비밀 키. GitHub secret 으로 저장 |
langfuse_base_url |
아니요 | Langfuse 호스트. 기본값 https://cloud.langfuse.com. 다른 Langfuse 인스턴스를 쓰면 리전 및 셀프 호스팅 URL 참고 |
experiment_path |
예 | 실험 스크립트 파일, 스크립트를 담는 디렉토리, 또는 glob 패턴 경로. Python, TypeScript, JavaScript 지원 |
dataset_name |
아니요 | 액션이 로드해 RunnerContext로 SDK에 제공하는 Langfuse 데이터셋. 생략하면 스크립트가 자체 데이터 제공 |
dataset_version |
아니요 | 재현 가능한 CI 실행을 위해 데이터셋 버전을 고정하는 선택적 타임스탬프. 기본값은 최신 데이터셋 버전 |
experiment_metadata |
아니요 | 기본 GitHub 메타데이터와 함께 실험에 추가되는 key=value 메타데이터. Langfuse UI에서 볼 수 있음 |
should_fail_on_regression |
아니요 | 실험이 RegressionError를 일으키면 CI 작업 실패. 기본값 true |
should_fail_on_script_error |
아니요 | 실험 스크립트가 크래시하거나 비회귀 오류를 일으키면 CI 작업 실패. 기본값 true |
should_comment_on_pr |
아니요 | 실험 리포트를 pull request 코멘트로 게시·업데이트. 기본값 true |
python_sdk_version |
아니요 | .py 실험용으로 액션이 설치하는 Langfuse Python SDK 버전. 기본값 latest. v4.6.0 이상 사용 |
js_sdk_version |
아니요 | TypeScript/JavaScript 실험용으로 액션이 설치하는 @langfuse/client 버전. 기본값 latest. v5.3.0 이상 사용 |
should_skip_sdk_installation |
아니요 | 이 액션 전에 Python/Node 환경을 직접 관리할 때 SDK 설치 건너뛰기. TypeScript 실험은 @langfuse/client, @langfuse/tracing, @langfuse/otel, @opentelemetry/sdk-node, tsx를 직접 제공. 기본값 false |
github_token |
아니요 | PR 코멘트 게시와 현재 job URL 확인에 사용하는 GitHub 토큰. 비워두면 둘 다 생략 |
전체 입력 참조는 langfuse/experiment-action README 를 참고하세요.
| 출력 | 설명 |
|---|---|
result_json |
다운스트림 워크플로우 단계용 정규화된 JSON 결과 |
failed |
어떤 실험 스크립트가 오류를 일으키거나 회귀를 발생시켰으면 true, 아니면 false |
회귀 시 실패(Failing on regressions)
결과가 워크플로우를 막아야 할 때 RegressionError를 발생시키세요. 아래 예시는 평균 exact-match 정확도가 임계값보다 낮을 때 실패합니다.
experiments/support-agent-gate.py:
from langfuse import Evaluation, RegressionError, RunnerContext
THRESHOLD = 0.95
def experiment(context: RunnerContext):
result = context.run_experiment(
name="PR gate: support agent",
task=answer_support_question,
evaluators=[exact_match],
run_evaluators=[avg_accuracy],
)
accuracy = next(
(
evaluation.value
for evaluation in result.run_evaluations
if evaluation.name == "avg_accuracy"
),
None,
)
if not isinstance(accuracy, (int, float)) or accuracy < THRESHOLD:
raise RegressionError(
# Attach the result so the action can include scores in the PR comment and `result_json` output.
result=result,
metric="avg_accuracy",
value=float(accuracy) if isinstance(accuracy, (int, float)) else 0.0,
threshold=THRESHOLD,
)
return result
def answer_support_question(item, **kwargs):
# Replace this stub with your application logic.
return item.input["question"]
def exact_match(*, output, expected_output, **kwargs):
passed = output.strip() == (expected_output or "").strip()
return Evaluation(
name="exact_match",
value=1.0 if passed else 0.0,
comment="match" if passed else "mismatch",
)
def avg_accuracy(*, item_results, **kwargs):
scores = [
evaluation.value
for item in item_results
for evaluation in item.evaluations
if evaluation.name == "exact_match" and isinstance(evaluation.value, (int, float))
]
return Evaluation(name="avg_accuracy", value=sum(scores) / len(scores) if scores else 0.0)
experiments/support-agent-gate.ts:
import {
RegressionError,
type Evaluation,
type ExperimentTaskParams,
type RunnerContext,
} from "@langfuse/client";
const THRESHOLD = 0.95;
export async function experiment(context: RunnerContext) {
const result = await context.runExperiment({
name: "PR gate: support agent",
task: answerSupportQuestion,
evaluators: [exactMatch],
runEvaluators: [avgAccuracy],
});
const accuracy = result.runEvaluations.find(
(evaluation) => evaluation.name === "avg_accuracy",
)?.value;
if (typeof accuracy !== "number" || accuracy < THRESHOLD) {
throw new RegressionError({
// Attach the result so the action can include scores in the PR comment and `result_json` output.
result,
metric: "avg_accuracy",
value: typeof accuracy === "number" ? accuracy : 0,
threshold: THRESHOLD,
});
}
return result;
}
async function answerSupportQuestion(item: ExperimentTaskParams) {
const { question } = item.input as { question: string };
// Replace this with your application logic, for example calling your agent.
return await supportAgent(question);
}
async function supportAgent(question: string) {
return question;
}
async function exactMatch({
output,
expectedOutput,
}: {
output: string;
expectedOutput?: string;
}): Promise<Evaluation> {
const passed = output.trim() === expectedOutput?.trim();
return { name: "exact_match", value: passed ? 1 : 0 };
}
async function avgAccuracy({
itemResults,
}: {
itemResults: Array<{ evaluations: Evaluation[] }>;
}): Promise<Evaluation> {
const scores = itemResults
.flatMap((item) => item.evaluations)
.filter((evaluation) => evaluation.name === "exact_match")
.map((evaluation) => Number(evaluation.value))
.filter((score) => Number.isFinite(score));
return {
name: "avg_accuracy",
value: scores.length
? scores.reduce((sum, score) => sum + score, 0) / scores.length
: 0,
};
}
액션 출력(Action output)
github_token이 제공되고 워크플로우가 pull-requests: write 권한을 가지면 액션은 pull request 코멘트를 게시하거나 업데이트합니다:
- 실험 스크립트별 pass, regression, script-error 상태
avg_accuracy같은 run-level 점수- GitHub Action 실행 링크
- 데이터셋 기반 실행의 Langfuse 실험 비교 뷰 링크
- 아이템 출력과 아이템 수준 점수의 컴팩트 테이블
같은 정규화된 데이터가 result_json 액션 출력으로 제공됩니다. 이후 워크플로우 단계가 결과를 아티팩트로 업로드하거나, Slack 알림을 보내거나, 다른 보고 시스템에 공급해야 할 때 사용하세요. 출력 스키마는 langfuse/experiment-action 저장소 에 있습니다.
.github/workflows/langfuse-experiment.yml:
- uses: langfuse/experiment-action@<release tag>
id: experiment
with:
# ...
- name: Store experiment result
if: always()
env:
RESULT_JSON: ${{ steps.experiment.outputs.result_json }}
run: printf '%s' "$RESULT_JSON" > experiment-result.json
추가 시크릿(Additional secrets)
실험이 제공자 키나 다른 시크릿을 필요로 하면 액션 단계에 환경 변수로 설정하세요. 실험 하위 프로세스는 단계 환경을 상속합니다.
.github/workflows/langfuse-experiment.yml:
- uses: langfuse/experiment-action@<release tag>
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
langfuse_public_key: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
langfuse_secret_key: ${{ secrets.LANGFUSE_SECRET_KEY }}
experiment_path: experiments/support-agent-gate
dataset_name: support-agent-regression-set
내 실험은 Python의 os.environ[...], TypeScript/JavaScript의 process.env...로 이 값을 읽을 수 있습니다. 자세한 내용은 langfuse/experiment-action README 를 참고하세요.
승인된 baseline과 비교
집계 임계값은 새로 실패하는 케이스를 놓칠 수 있습니다. 예를 들어 두 케이스를 고치고 하나를 깨뜨리는 candidate는 평균 점수가 더 높지만, 깨진 케이스는 릴리스 차단 요인이 될 수 있습니다.
검토된 baseline 아티팩트를 저장소에 유지하세요. 완전한 실행에서 생성하고, Compare experiments 에서 출력을 검토하고, 일반 코드 리뷰를 통해 변경을 승인하세요. 데이터셋 버전, 평가자 버전, 소스 실행 정체성, 모든 케이스의 pass/fail 판정을 저장하세요. 테스트 중인 candidate에서 승인된 아티팩트를 자동 생성하지 마세요.
experiments/approved-baseline.json:
{
"run": "reviewed-release-run-id",
"dataset_version": "2026-07-01T00:00:00Z",
"evaluator_version": "refund-window-v1",
"cases": { "standard": true, "sale": false }
}
아래 헬퍼는 Evaluate an existing application 의 case_id 메타데이터와 refund_window 평가자를 사용합니다. 예시 타임스탬프와 run ID를 내 검토된 실행의 값으로 바꾸세요. candidate의 데이터셋을 로드할 때 같은 타임스탬프를 사용하세요. 실험 실행 후 그 타임스탬프를 헬퍼에 전달하세요. 이는 게이트가 승인된 구성을 사용하는지 확인합니다.
experiments/baseline_gate.py:
import json
from datetime import datetime
from pathlib import Path
from langfuse import RegressionError
EVALUATOR_VERSION = "refund-window-v1"
def parse_version(value: str | datetime) -> datetime:
parsed = (
datetime.fromisoformat(value.replace("Z", "+00:00"))
if isinstance(value, str)
else value
)
if parsed.utcoffset() is None:
raise ValueError("Dataset version must include a timezone")
return parsed
def check_approved_baseline(result, *, dataset_version: str | datetime):
baseline = json.loads(Path("experiments/approved-baseline.json").read_text())
if (
parse_version(baseline["dataset_version"])
!= parse_version(dataset_version)
or baseline["evaluator_version"] != EVALUATOR_VERSION
):
raise ValueError(
"Dataset or evaluator version differs from the approved baseline"
)
approved = baseline["cases"]
if not approved or any(type(value) is not bool for value in approved.values()):
raise ValueError("Baseline must contain reviewed boolean verdicts")
current = {}
for row in result.item_results:
item = row.item
metadata = item.get("metadata") if isinstance(item, dict) else item.metadata
case_id = (metadata or {}).get("case_id")
scores = [e.value for e in row.evaluations if e.name == "refund_window"]
if (
not isinstance(case_id, str)
or case_id in current
or len(scores) != 1
or scores[0] not in (0, 1)
):
raise ValueError("Incomplete, duplicate, or invalid case result")
current[case_id] = scores[0] == 1
if current.keys() != approved.keys():
raise ValueError("Candidate and baseline contain different cases")
regressions = [
case_id for case_id in approved if approved[case_id] and not current[case_id]
]
if regressions:
print("Newly failing cases:", ", ".join(regressions))
raise RegressionError(
result=result,
metric="newly_failing_cases",
value=float(len(regressions)),
threshold=0.0,
)
result를 반환하기 전에 실험 진입점에서 check_approved_baseline(result, dataset_version=version)을 호출하세요. 여기서 version은 candidate 데이터셋을 가져오는 데 사용한 시간대 인식 datetime 또는 ISO 타임스탬프입니다. 그레이더가 바뀔 때마다 EVALUATOR_VERSION을 증가시키고 사용하기 전에 새 baseline을 검토하세요.
experiments/baseline-gate.ts:
import { readFileSync } from "node:fs";
import { RegressionError, type ExperimentResult } from "@langfuse/client";
const EVALUATOR_VERSION = "refund-window-v1";
export function checkApprovedBaseline(
result: ExperimentResult,
datasetVersion: string,
) {
const baseline = JSON.parse(
readFileSync("experiments/approved-baseline.json", "utf8"),
);
if (
!Number.isFinite(Date.parse(datasetVersion)) ||
Date.parse(baseline.dataset_version) !== Date.parse(datasetVersion) ||
baseline.evaluator_version !== EVALUATOR_VERSION
) {
throw new Error(
"Dataset or evaluator version differs from the approved baseline",
);
}
const approved = baseline.cases as Record<string, boolean>;
if (
!approved ||
Object.keys(approved).length === 0 ||
Object.values(approved).some((value) => typeof value !== "boolean")
) {
throw new Error("Baseline must contain reviewed boolean verdicts");
}
const current = new Map<string, boolean>();
for (const row of result.itemResults) {
const metadata = row.item.metadata as { case_id?: unknown } | undefined;
const caseId = metadata?.case_id;
const scores = row.evaluations.filter((e) => e.name === "refund_window");
if (
typeof caseId !== "string" ||
current.has(caseId) ||
scores.length !== 1 ||
(scores[0].value !== 0 && scores[0].value !== 1)
) {
throw new Error("Incomplete, duplicate, or invalid case result");
}
current.set(caseId, scores[0].value === 1);
}
if (
current.size !== Object.keys(approved).length ||
Object.keys(approved).some((id) => !current.has(id))
) {
throw new Error("Candidate and baseline contain different cases");
}
const regressions = Object.keys(approved).filter(
(id) => approved[id] && !current.get(id),
);
if (regressions.length) {
console.error("Newly failing cases:", regressions.join(", "));
throw new RegressionError({
result,
metric: "newly_failing_cases",
value: regressions.length,
threshold: 0,
});
}
}
result를 반환하기 전에 실험 진입점에서 checkApprovedBaseline(result, version)을 호출하세요. 여기서 version은 candidate 데이터셋을 가져오는 데 사용한 ISO 타임스탬프입니다. 그레이더가 바뀔 때마다 EVALUATOR_VERSION을 증가시키고 사용하기 전에 새 baseline을 검토하세요.
이 예시는 이전에 통과한 모든 케이스를 보호 대상으로 취급합니다. 일부만 릴리스 중요하다면 그 검토된 하위 집합을 baseline 정책에 명시적으로 저장하세요. 집계 임계값은 추가 검사로 유지하세요.
누락된 평가, 중복 식별자, 변경된 케이스 집합은 script error를, 새로 실패하는 케이스는 RegressionError를 발생시킵니다. should_fail_on_script_error와 should_fail_on_regression을 모두 활성화하세요. 실패한 평가자가 분모에서 조용히 사라져 실행이 더 좋아 보이게 해서는 안 됩니다.
기타 CI/CD 시스템
내 기존 애플리케이션과 그레이더를 Pytest나 Vitest에 유지하세요. 이 예시들은 로컬 JSON 케이스를 로드하고, 애플리케이션을 호출하고, 그 출력과 체크 결과를 Langfuse에 게시하고, 모든 케이스가 통과할 것을 요구합니다. 호스팅 데이터셋이나 별도 LLM 판사가 필요하지 않습니다.
Langfuse 자격 증명과 SDK 의존성 을 구성하세요. 애플리케이션과 그레이더 import를 내 모듈로 바꾸세요. 여기서 run_application(input) / runApplication(input)은 null이 아닌 출력을, grade(output, expected_output)은 boolean을 반환합니다. 내 스위트가 run_checks()를 사용한다면 그 명명된 체크를 평가자 결과로 변환하고 모든 필수 체크가 존재하고 통과하도록 요구하세요.
cases.json:
[
{
"id": "standard-refund",
"input": { "question": "What is the standard refund window?" },
"expected_output": { "refund_days": 30 }
}
]
APP_VERSION을 테스트 중인 애플리케이션 커밋 또는 릴리스로 설정하세요. 예시는 케이스 파일을 해시하고 그레이더 버전을 기록합니다. 그레이더가 바뀔 때마다 그 버전을 업데이트하세요. 애플리케이션은 자체 구성에 따라 모델 호출을 할 수 있습니다. 애플리케이션이나 그레이더를 다시 호출하지 않고 이미 완료된 실행을 게시하려면 Publish saved check results 를 참고하세요.
test_application_experiment.py:
import hashlib
import json
import os
from pathlib import Path
from langfuse import Evaluation, get_client
from my_app import run_application # Replace with your application's import.
from my_checks import grade # Reuse your existing boolean grader.
def test_application_checks():
case_file = Path("cases.json").read_bytes()
cases = json.loads(case_file)
ids = [case["id"] for case in cases]
assert ids and all(ids) and len(set(ids)) == len(ids), "Invalid case IDs"
def task(*, item, **kwargs):
return run_application(item["input"])
def evaluator(*, output, expected_output, **kwargs):
passed = grade(output, expected_output)
if type(passed) is not bool:
raise ValueError("grade must return a boolean")
return Evaluation(name="existing_checks", value=int(passed))
langfuse = get_client()
try:
result = langfuse.run_experiment(
name="Application checks",
data=[{
"input": case["input"],
"expected_output": case["expected_output"],
"metadata": {"case_id": case["id"]},
} for case in cases],
task=task,
evaluators=[evaluator],
metadata={
"application_version": os.environ["APP_VERSION"],
"cases_sha256": hashlib.sha256(case_file).hexdigest(),
"evaluator_version": "existing-checks-v1",
},
)
print(result.format()) # Includes the experiment link.
returned_ids = [item.item["metadata"]["case_id"] for item in result.item_results]
assert sorted(returned_ids) == sorted(ids), "Incomplete results"
for item in result.item_results:
assert item.output is not None, "Missing application output"
scores = [e for e in item.evaluations if e.name == "existing_checks"]
assert len(scores) == 1 and scores[0].value == 1, (
f"Failed or missing check: {item.item['metadata']['case_id']}"
)
finally:
langfuse.flush()
Pytest가 설치된 상태에서 pytest -s test_application_experiment.py로 실행하세요. -s 플래그는 실험 링크를 CI 로그에서 볼 수 있게 유지합니다.
test/application-experiment.test.ts:
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { it, expect } from "vitest";
import { LangfuseClient } from "@langfuse/client";
import { LangfuseSpanProcessor } from "@langfuse/otel";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { runApplication } from "../src/app"; // Replace with your application's import.
import { grade } from "../src/checks"; // Reuse your existing boolean grader.
it("passes every required application check", async () => {
const caseFile = readFileSync("cases.json");
const cases: { id: string; input: unknown; expected_output: unknown }[] =
JSON.parse(caseFile.toString("utf8"));
const ids = cases.map((item) => item.id);
expect(ids.length).toBeGreaterThan(0);
expect(ids.every(Boolean)).toBe(true);
expect(new Set(ids).size).toBe(ids.length);
const applicationVersion = process.env.APP_VERSION;
if (!applicationVersion) throw new Error("Set APP_VERSION");
const otel = new NodeSDK({ spanProcessors: [new LangfuseSpanProcessor()] });
otel.start();
try {
const langfuse = new LangfuseClient();
const result = await langfuse.experiment.run({
name: "Application checks",
data: cases.map((item) => ({
input: item.input,
expectedOutput: item.expected_output,
metadata: { case_id: item.id },
})),
task: async (item) => runApplication(item.input),
evaluators: [async ({ output, expectedOutput }) => {
const passed = await grade(output, expectedOutput);
if (typeof passed !== "boolean") throw new Error("grade must return a boolean");
return { name: "existing_checks", value: Number(passed) };
}],
metadata: {
application_version: applicationVersion,
cases_sha256: createHash("sha256").update(caseFile).digest("hex"),
evaluator_version: "existing-checks-v1",
},
});
console.log(await result.format()); // Includes the experiment link.
expect(result.itemResults.map((item) =>
(item.item.metadata as { case_id: string }).case_id,
).sort())
.toEqual([...ids].sort());
for (const item of result.itemResults) {
expect(item.output).not.toBeNull();
expect(item.output).not.toBeUndefined();
const scores = item.evaluations.filter((score) => score.name === "existing_checks");
expect(scores).toHaveLength(1);
expect(scores[0].value).toBe(1);
}
} finally {
await otel.shutdown();
}
}, 60_000); // Adjust for the runtime of your application and case set.
Vitest가 설치된 상태에서 npx vitest run test/application-experiment.test.ts로 실행하세요.
이 단언들은 모든 필수 케이스가 통과해야 함 정책을 구현합니다. 명시적으로 승인된 실패가 있는 스위트라면 승인된 baseline 게이트 를 대신 사용하세요. 게이트의 단언이 던져질 것을 기대하는 테스트는 실제 회귀를 통과하는 CI 작업으로 바꿔버립니다.