Evaluating Agents
We use Langfuse to evaluate LLM agents in the ESProfiler ecosystem.
Evaluations follow three steps:
- Create datasets — test cases (
input/expectedOutput/metadata) - Create evaluators — scoring definitions (mostly LLM-as-judge)
- Run experiments — prompt + dataset + evaluators
| Piece | Role |
|---|---|
| Dataset | Reusable test cases |
| Evaluator | Scores one quality dimension of an agent output |
| Experiment | Runs a prompt against a dataset and applies evaluators |
Existing resources in ESP Development:
1. Creating Datasets
An evaluation dataset is a collection of test cases. Each item typically has:
| Field | Purpose |
|---|---|
| input | What the agent/prompt receives at runtime |
| expectedOutput | Golden answer or evaluation guidance |
| metadata | Filtering, debugging, and audit context (not fed to the agent) |
1.1 Create your first dataset (5 steps)
- Pick one agent — see Agents.
- Decide expected-output style — golden answer for deterministic tasks; judge guidance for open-ended ones.
- Write 3–5 items —
inputkeys must match that agent’s prompt variables; addexpectedOutputand optionalmetadata. - Anonymize if data came from production — never upload raw tenant/PII.
- Create and upload — UI for tiny flat cases; Python SDK for nested JSON.
Then spot-check 2–3 items in Langfuse before attaching evaluators.
1.2 Dataset naming
{agentName}/{datasetRole}
| Dataset | Role | Size guidance |
|---|---|---|
{agent}/smoke_test | Quick sanity checks after prompt or wiring changes | ~5–15 items |
{agent}/gate_test | Broader frozen set for prompt comparison and release decisions | ~20–50 items |
Examples: conversation-namer/smoke_test, findings/gate_test.
Keep both sets reviewed and anonymized. Update them deliberately — do not treat one as a staging queue for the other.
1.3 Upload overview
- UI — Datasets → New dataset → add items (or CSV for flat strings). Best for small / simple cases.
- Python SDK — preferred for nested JSON (Findings transcripts, structured guidance). Keys: Langfuse UI → Settings → API Keys.
- Reference: Langfuse Datasets
1.4 Dataset reference (expand as needed)
Input
What is injected into the prompt or application under test: a user message, nested prompt variables, or a multi-turn conversation.
Rule: input keys must match the prompt variables of the agent you are evaluating. Find those in the agent’s .st prompt / config in platform-api.
Expected output
A) Golden answer (deterministic) — use when the correct answer is known and comparable (tags, short titles, exact fields). A code check is often enough. Curate with human review.
B) Judge guidance (non-deterministic) — use when many good outputs exist (summaries, reports, findings). Put criteria in expectedOutput (what must be covered, constraints, grounding rules) — not a full golden dump.
expectedOutput; put full historical report/findings in metadata.reference_output for human review only.Metadata
Anything that should not be fed as prompt input: tags, tool expectations, anonymization flags, debug goldens.
| Use case | Expected output style | Evaluator |
|---|---|---|
| Categorization / tagging / short titles | Golden labels | Code check (app or unit tests) |
| Tone, grounding, alignment, finding types | Guidance + rubric | LLM-as-judge |
| Mixed | Guidance + optional reference in metadata | Code + LLM-as-judge |
Start from production failure modes, not random sampling.
- Observe where the agent fails (wrong types, invented claims, missed coverage, etc.)
- Pull representative completed cases for those modes
- Prefer diverse scenarios over near-duplicates
- Anonymize before upload
- Put stable regression cases in
gate_test; keep a smallersmoke_testset for quick checks
Do not upload raw tenant data to Langfuse.
| Original | Anonymized form |
|---|---|
| Tenant / org name | Synthetic org (e.g. Black Mesa) |
| Real people | Stable synthetic names |
| Emails / tenant domains | @blackmesa.example placeholders |
| Real task / product / vendor UUIDs | Synthetic UUIDs from hashes |
| Original task id | original_task_id_hash only |
| Images / screenshots | Stripped (imageStr: null) |
Replace tenant strings everywhere, keep people mapping stable across items, keep market product names only when they are not the customer identity, then sanity-check that known customer tokens are gone.
{
"anonymization": {
"tenant_replaced_with": "Black Mesa",
"people_synthetic": true,
"images_stripped": true
}
}
First user message in → short title out. Confirm the real prompt variable name in platform-api before uploading (replace message if it differs). Synthetic cases need no anonymization.
{
"input": {
"message": "Can you compare our CrowdStrike and Wiz renewals for next quarter?"
},
"expectedOutput": {
"title": "CrowdStrike vs Wiz renewals"
},
"metadata": {
"dataset": "conversation-namer/gate_test",
"tags": ["conversation_namer", "synthetic"],
"constraints": { "max_words": 5 }
}
}
Turns interview transcript + directive into summary, report, and structured findings.
Process: pull production cases → map prompt variables to input → anonymize → put judge guidance in expectedOutput → put historical summary/report/findings in metadata.reference_output → upload via SDK.
input (prompt variables only)
{
"user_name": "Marcus Silva",
"user_role": "Team Lead and L3 Engineer",
"organisation_context": "Black Mesa is a diversified technology and research organization...",
"source_type": "PROD",
"source_info": {
"id": "02e4e876-4c33-b27b-9c8e-82cd402f0f85",
"name": "Akamai App & API Protector",
"type": "PROD",
"vendor": { "id": "...", "name": "Akamai" }
},
"task_directive": "You are conducting a structured interview to gather information about a product...",
"interview_transcript": [
{
"id": "rufsaa",
"role": "assistant",
"text": "Hi Marcus! I'm ESPi...",
"isThoughts": false,
"imageStr": null
},
{
"id": "oywj8t",
"role": "user",
"text": "Yes",
"isThoughts": false,
"imageStr": null
}
]
}
expectedOutput (guidance)
{
"expected_summary_focus": "Summarize strategic state, material risks/insights, and decision-relevant takeaways. Surface only transcript-grounded points.",
"expected_min_findings": 2,
"expected_max_findings": 11,
"expected_required_finding_types": ["insight", "risk", "sentiment"],
"expected_report_sections": [
"Overview",
"Strategic State",
"Capabilities and Usage",
"Coverage and Controls",
"Sentiment and Operational Experience",
"Risks, Gaps and Opportunities",
"Stakeholders",
"Conclusion"
],
"must_align_to_directive": true,
"must_be_grounded_in_transcript": true,
"schema_notes": {
"RISK_requires": ["severity"],
"INSIGHT_requires": ["category"],
"SENTIMENT_requires": ["category", "rating"]
}
}
metadata (example)
{
"dataset": "findings/gate_test",
"tags": ["findings_agent", "black_mesa", "anonymized"],
"original_task_id_hash": "4ec6af6be8582669",
"product_name": "Akamai App & API Protector",
"expected_tools": ["platformSearch"],
"reference_output": {
"summary": "...",
"report": "...",
"findings": []
},
"anonymization": {
"tenant_replaced_with": "Black Mesa",
"people_synthetic": true,
"images_stripped": true
}
}
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_HOST=https://cloud.langfuse.com
from langfuse import get_client
langfuse = get_client()
dataset_name = "conversation-namer/smoke_test"
langfuse.create_dataset(
name=dataset_name,
description="Smoke test set for Conversation Namer",
)
items = [
{
"input": {
"message": "Can you compare our CrowdStrike and Wiz renewals for next quarter?"
},
"expected_output": {"title": "CrowdStrike vs Wiz renewals"},
"metadata": {
"dataset": dataset_name,
"tags": ["conversation_namer", "synthetic"],
},
},
]
for item in items:
langfuse.create_dataset_item(
dataset_name=dataset_name,
input=item["input"],
expected_output=item["expected_output"],
metadata=item.get("metadata"),
)
Same pattern for Findings — pass the full nested objects; no need to flatten.
- Agent under test is clear
-
inputkeys match prompt variables 1:1 -
expectedOutputis a golden answer or judge guidance (not mixed without intent) - For open-ended agents: guidance in
expectedOutput; full goldens (if kept) inmetadata.reference_output - Production data is anonymized
- Name is
{agent}/smoke_testor{agent}/gate_test - Spot-check 2–3 items in the Langfuse UI
2. Creating Evaluators
Evaluators are the scoring definitions you attach when you run prompt experiments.
Today we mostly set up LLM-as-judge evaluators in Langfuse.
Code-based checks still matter for deterministic rules, but we typically implement them in the application and/or unit/integration tests in platform-api, rather than as the primary Langfuse experiment evaluators. Langfuse also supports Code Evaluators if needed later.
Rule of thumb:
- Needs reading comprehension / judgment → LLM-as-judge (Langfuse)
- A junior engineer could assert it in a test → code (app or unit tests)
2.1 Create your first LLM-as-judge (5 steps)
- Pick one dimension — e.g. “grounded in transcript” or “aligns with directive”.
- Choose score shape — boolean / categorical / numeric.
- Write the judge prompt — explicit pass/fail or category rules; one job only.
- Create it in Langfuse — map
{{input}}/{{output}}(and{{expected_output}}only if needed). - Verify mapping — use Prompt Preview; spot-check that variables populate as expected.
Start with 2–3 focused judges per agent. Add more only when debugging a specific failure class.
Findings starter pack:
findings_agent.grounding— Booleanfindings_agent.directive_alignment— Categorical (fail/partial/pass)
2.2 Naming
{agentName}.{dimension}
Examples: conversation_namer.title_quality, findings_agent.grounding, findings_agent.directive_alignment.
2.3 Create in Langfuse (UI)
- Ensure an LLM Connection exists (Settings → LLM Connections). The judge model must support structured output.
- Open Evaluators → + Set up Evaluator.
- Pick a managed template, or Custom and paste your judge prompt with
{{variables}}. - Choose score type (boolean / categorical / numeric). For categorical, define labels and numeric mapping.
- Map variables to Input / Output / Expected output (add JSONPath if needed).
- Save. Attach these evaluators when running experiments.
Official guide: LLM-as-a-Judge.
2.4 Evaluator reference (expand as needed)
| If you want to... | Langfuse / approach we use |
|---|---|
| Build a reusable set of test cases | Datasets |
| Compare prompt or model changes | Experiments |
| Automatically score quality (grounding, alignment, tone, …) | LLM-as-a-Judge |
| Run deterministic checks (schema, length, exact match) | Code checks in the app or unit/integration tests |
| Score type | Use when | Typical use |
|---|---|---|
| Boolean | Clear pass/fail | Hard checks (e.g. grounding) |
| Categorical | Small fixed tiers (fail / partial / pass) | Soft quality bands |
| Numeric | Fine-grained ranking | When tiers are too coarse |
Map categorical labels to numbers when useful (e.g. fail → 0, partial → 0.5, pass → 1).
Prefer boolean for hard correctness; categorical (3-tier) for softer quality.
- One job per evaluator — do not mix grounding + writing quality in one score.
- Say what to check — avoid long “do not score X” lists.
- Define pass/fail or categories explicitly with short examples.
- Derive criteria from the use case — do not hard-code one product’s interview branch unless it is universal.
- Map only the data needed — use JSONPath when you only need a nested field.
- Name and version stably — keep
{agent}.{dimension}names unchanged so later experiment comparisons stay readable.
| Common variable | Typical source |
|---|---|
{{input}} | Dataset item input |
{{output}} | Run output (filled when an experiment executes) |
{{expected_output}} | Dataset expected output (optional) |
Use JSONPath when you only need a nested field (e.g. Output → $.findings). Confirm in Langfuse Prompt Preview.
Skip expected_output when the rubric lives fully in the judge prompt, or the check is reference-free (e.g. grounding against transcript in input).
Use it when item-specific guidance lives in the dataset.
A light judge — good first custom evaluator to practise the shape.
| Field | Value |
|---|---|
| Name | conversation_namer.title_quality |
| Score | Boolean |
| Maps | {{input}} → Input, {{output}} → Output |
You evaluate whether the OUTPUT title is a good short label for the INPUT user message.
Pass (true) if ALL are true:
- Title is non-empty
- Title is at most 5 words
- Title reflects the main topic of the user message (no unrelated subject)
Fail (false) otherwise.
If OUTPUT is empty or malformed, return false.
INPUT:
{{input}}
OUTPUT:
{{output}}
Score output: return ONLY true or false.
For a pure length rule (≤ 5 words), prefer a unit test or in-app validation instead of a judge.
Maps: {{input}} → Input, {{output}} → Output (no expected_output)
You evaluate whether the agent OUTPUT is grounded in the interview transcript from INPUT.
Task:
Decide if material claims in the summary, report, and findings are supported by the transcript.
Rules:
- Every material claim must be supported by the transcript
- Paraphrase is allowed; invention is not
- Minor omissions are OK; fabrication is not
Fail (false) if any material claim is invented, over-precise beyond the transcript, contradicts the transcript without uncertainty, or invents rationale/abbreviation expansions not stated.
Pass (true) only if all material claims are transcript-supported.
If OUTPUT is empty or malformed, return false.
List any ungrounded claims briefly before deciding.
INPUT:
{{input}}
OUTPUT:
{{output}}
Score reasoning: 1–3 sentences; if false, name the worst ungrounded claim(s).
Score output: return ONLY true or false.
Categories: fail → 0, partial → 0.5, pass → 1
Maps: {{input}} → Input, {{output}} → Output (directive usually inside input)
You evaluate whether the Findings/Report agent OUTPUT aligns with the interview DIRECTIVE in INPUT.
Task:
Judge how well the REPORT and FINDINGS deliver the directive’s information goals, based on what the transcript actually captured.
Do not assume a fixed interview structure. Derive success criteria from the directive itself.
Check only:
1) Main directive objective(s) appear in report and/or findings
2) Key requested topics are covered when the transcript has answers
3) Findings are decision-useful for the directive
4) Important directive-relevant transcript content is not systematically missing
If the transcript lacked answers for a topic, do not penalize missing content for that topic.
Categories:
- fail: largely misses the directive, or major goals missing
- partial: some alignment, important gaps remain
- pass: strong alignment; minor gaps only
INPUT:
{{input}}
OUTPUT:
{{output}}
Score reasoning: 2–4 sentences with strongest coverage and most important gaps.
Score output: return ONLY fail, partial, or pass.
- One clear dimension
- Score type matches the check (boolean / categorical / numeric)
- Categorical labels + numeric mapping defined (if used)
- Judge prompt states positive checks
- Variable mapping verified in Prompt Preview
- JSONPath used when only a nested field is needed
-
expected_outputonly when item-specific guidance is required - Name follows
{agent}.{dimension}
3. Running Experiments
Use a dataset and evaluators together in a Langfuse Prompt Experiment to compare prompt versions and decide whether to ship a change.
Example completed runs: findings/gate_test experiments.
Official docs: Experiments via UI · Experiments data model
3.1 What a Langfuse experiment is
| Concept | Meaning |
|---|---|
| Dataset | Frozen test cases (input, optional expectedOutput, metadata) |
| Prompt | Versioned prompt from Prompt Management |
| Experiment (Prompt Experiment) | Runs the selected prompt on each dataset item |
| Evaluator | Scores each experiment item output (LLM-as-judge and/or code) |
| Experiment comparison | Side-by-side aggregate + item-level score comparison across runs |
Dataset item input
│
▼
Prompt version (variables filled from input)
│
▼
Model output
│
▼
Evaluators attach scores
│
▼
Compare runs → promote or reject prompt
Important: one experiment can attach multiple evaluators. Do not create one experiment per score.
3.2 Prerequisites
Before running an experiment, confirm:
- Prompt in Prompt Management with
{{variables}}matching datasetinputkeys - Dataset uploaded (
smoke_testorgate_test) — see §1 - LLM connection configured; default evaluation model supports structured output for judges
- Evaluators created and able to target Experiments — see §2
3.3 Run a Prompt Experiment (UI)
- Go to Datasets → open the dataset (e.g.
findings/gate_test) → spot-check 1–2 items - Click Start Experiment / Run Experiment → Prompt Experiment → Create
- Configure:
- Experiment name (see naming below)
- Prompt + prompt version
- LLM connection / model settings
- Dataset (usually already selected)
- Optional: structured output schema (recommended for Findings:
summary,report,findings) - Evaluators to attach (all gate evaluators)
- Click Create
Langfuse runs the prompt per item, stores outputs, runs evaluators asynchronously, and shows aggregate scores. Runtime depends on dataset size, prompt length, and judge count.
3.4 Experiment naming
{agent}-{role}-{promptVersion}-{yyyymmdd}
Examples:
findings-baseline-v12-20260729findings-candidate-v13-20260729
For prompt gates: baseline = current production prompt; candidate = proposed version; same dataset + same evaluators for both.
3.5 Compare experiments
After runs complete:
- Open Experiments (or the dataset’s Experiments tab)
- Select baseline and candidate runs
- Compare aggregate scores, item-level regressions (especially boolean fails), and judge comments on failures
Always spot-check a few failed items manually before promoting.
3.6 Experiment reference (expand as needed)
A prompt is usable for Prompt Experiments when its {{variables}} match dataset item input keys.
| Prompt variable | Dataset input key |
|---|---|
{{user_name}} | user_name |
{{user_role}} | user_role |
{{organisation_context}} | organisation_context |
{{source_type}} | source_type |
{{source_info}} | source_info |
{{task_directive}} | task_directive |
{{interview_transcript}} | interview_transcript |
If variables and input keys do not match, the experiment will fail or run with empty fields.
A) Baseline (current production prompt)
- Open
findings/gate_test - Start Prompt Experiment
- Select current production prompt version
- Attach gate evaluators (
findings_agent.grounding,findings_agent.directive_alignment) - Name:
findings-baseline-<prod-version> - Wait for scores
B) Candidate (new prompt)
- Save prompt edits as a new prompt version in Prompt Management
- Run another Prompt Experiment on the same
findings/gate_testdataset - Attach the same evaluators
- Name:
findings-candidate-<new-version>
C) Decide
Promote only if grounding and directive alignment do not regress materially, and no new systemic failure pattern appears in item review.
If it fails: revise the candidate prompt and rerun candidate only (keep baseline fixed).
Prod prompt vN
│
▼
Baseline experiment on findings/gate_test + gate evaluators
│
▼
Edit prompt → save vN+1
│
▼
Candidate experiment on SAME dataset + SAME evaluators
│
▼
Compare in Langfuse Experiments
│
├─ Pass → promote vN+1
└─ Fail → revise prompt, rerun candidate
What to look for
| Evaluator | Prefer |
|---|---|
grounding | pass-rate ≥ baseline |
directive_alignment | higher % pass, lower % fail, mean mapped score ≥ baseline − tolerance |
| Approach | Use when |
|---|---|
| Experiments via UI (Prompt Experiments) | Prompt-only changes; variables map cleanly from dataset input |
| Experiments via SDK | Full app/agent logic, tools, retrieval, custom runtime config |
Findings Agent prompt iteration fits UI Prompt Experiments well when structured output is enforced.
If the flow depends heavily on tool calls / multi-step orchestration that Prompt Experiments cannot reproduce, use Experiments via SDK (or hybrid: UI for prompt drafts, SDK for full-agent realism).
To keep comparisons fair:
- Do not edit/add/delete
gate_testitems while comparing prompts - Put fresh cases into
smoke_test(or a scratch set), not intogate_test - Only expand
gate_testwith reviewed items after the current comparison cycle
Langfuse experiments run against the dataset state at experiment time. Treat gate_test as frozen for the duration of a promotion decision.
Optional: use dataset versioning (Items tab → version view) when available, so you can re-run against a historical snapshot.
For Findings experiments, enable structured output with a schema requiring:
summary(string)report(string)findings(array)
This improves parseability for judges, consistency across items, and JSONPath mapping (e.g. $.findings).
Schemas can be created/saved in Langfuse Playground and reused in experiments.
| Symptom | Likely cause | Fix |
|---|---|---|
| Experiment fails immediately | Prompt variables ≠ dataset input keys | Align names exactly |
| Empty outputs | LLM connection / model issue | Check project LLM connection + logs |
| No evaluator scores | Evaluator not attached / wrong target | Attach evaluators; target Experiments |
| Judge mapping empty | Wrong source or JSONPath | Fix mapping; use Prompt Preview |
| Noisy scores | Judge prompt too broad | Split into one-dimension evaluators |
| Need judge internals | — | Filter traces by environment langfuse-llm-as-a-judge |
- Prompt version is saved in Prompt Management (not an unsaved playground edit)
- Baseline experiment exists for current production prompt
- Candidate experiment used the same dataset (e.g.
findings/gate_test) - Candidate experiment used the same evaluators
- Structured output schema enabled (if required by agent)
- Aggregate scores reviewed
- Item-level failures reviewed (especially grounding fails)
- No dataset edits happened between baseline and candidate
- Promotion decision documented (pass / fail + reason)
- Production prompt pointer updated only after pass
| Piece | Langfuse object |
|---|---|
| Gate dataset | findings/gate_test |
| Smoke dataset | findings/smoke_test |
| Prompt | Findings Agent prompt in Prompt Management |
| Gate evaluators | findings_agent.grounding, findings_agent.directive_alignment |
| Experiment type | Prompt Experiment (UI) |
| Example runs | findings/gate_test experiments |
| Decision | Compare baseline vs candidate → promote only on gate pass |
Tips
- Keep experiment names searchable (
baseline/candidate+ prompt version). - Prefer foldered datasets (
findings/gate_test) for clarity in the Datasets UI. - Attach all gate evaluators on every promotion run — do not compare incomplete score sets.
- Use
smoke_testfor quick checks; promote prompts usinggate_test.

