Systematic evaluation is what separates production-grade LLM applications from prototypes. This lesson covers the tooling landscape for measuring, comparing, and regression-testing LLM outputs.
Manual spot-checking does not scale. As your prompt library grows, you need automated, reproducible evaluation to answer three questions:
Define test cases → Run against providers → Apply assertions / graders → Report pass/fail + scores → Iterate on prompts
promptfoo is an open-source CLI tool (Node.js) that lets you define evaluations in YAML and run them from the terminal or CI.
{{variables}}# promptfooconfig.yaml
providers:
- openai:gpt-4o
- anthropic:messages:claude-sonnet-4-20250514
prompts:
- "Summarise this article in 2 sentences: {{article}}"
tests:
- vars:
article: "The Federal Reserve held interest rates steady..."
assert:
- type: contains
value: "interest rates"
- type: llm-rubric
value: "The summary is factually accurate and concise"
- type: cost
threshold: 0.01
promptfoo eval to demonstrate the output table.Assertions are the heart of promptfoo. They define what "correct" means for each test case.
| Assertion Type | What It Checks | Example |
|---|---|---|
contains | Output includes a substring | value: "Python" |
not-contains | Output must not include substring | value: "I'm sorry" |
regex | Output matches a regex | value: "\\d{4}-\\d{2}-\\d{2}" |
is-json | Output is valid JSON | (no value needed) |
javascript | Custom JS function returns true | value: "output.length < 500" |
similar | Cosine similarity above threshold | value: "expected text", threshold: 0.8 |
llm-rubric | An LLM judges the output | value: "Answer is helpful and accurate" |
cost | API cost below threshold | threshold: 0.02 |
Each test can have multiple assertions. A test passes only if all assertions pass. Use weight to score partial credit when using --grading mode.
promptfoo is not the only option. Here is how the major frameworks compare:
assert_test()| Feature | promptfoo | OpenAI Evals | Ragas | DeepEval | LangSmith |
|---|---|---|---|---|---|
| Language | Node.js CLI | Python | Python | Python | Python + UI |
| Config format | YAML | YAML + code | Code | Code (pytest) | Code + UI |
| Multi-provider | Built-in | Limited | Any LLM | Any LLM | LangChain providers |
| RAG-specific | Partial | No | Yes (core focus) | Yes | Yes |
| CI/CD ready | Excellent | Good | Good | Excellent | Good |
| LLM-as-judge | llm-rubric | Model-graded | Built-in | Built-in | Built-in |
| Dashboard | Local web UI | None | None | Cloud (optional) | Cloud |
| License | MIT | MIT | Apache 2.0 | Apache 2.0 | Proprietary |
Treat prompts like code: version them, test them, gate deployments on eval results.
PR opened → CI runs promptfoo eval → assertions checked → pass/fail reported on PR → merge only if evals pass
# .github/workflows/eval.yml
name: Prompt Eval
on: [pull_request]
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g promptfoo
- run: promptfoo eval --no-cache
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- run: promptfoo eval --output results.json
- name: Check pass rate
run: |
PASS_RATE=$(node -e "const r=require('./results.json'); const s=r.results.stats; console.log((s.successes/s.total*100).toFixed(1))")
echo "Pass rate: ${PASS_RATE}%"
if (( $(echo "$PASS_RATE < 90" | bc -l) )); then exit 1; fi