
exploring-llm-evaluations
β 59by posthog Β· part of posthog/ai-plugin
PostHog evaluations score $ai_generation events. Each evaluation is one of two types, both first-class:
This is the playbook your agent receives when the skill activates β you don't need to read it to use the skill, but it's here to audit before installing.
by posthog
PostHog evaluations score $ai_generation events. Each evaluation is one of two types, both first-class:
npx skills add https://github.com/posthog/ai-plugin --skill exploring-llm-evaluations
Download ZIPGitHub59
Exploring AI observability evaluations
PostHog evaluations score $ai_generation events. Each evaluation is one of three
types:
-
hogβ deterministic Hog code that returnstrue/false(and optionally N/A). Best for objective rule-based checks: format validation (JSON parses, schema matches), length limits, keyword presence/absence, regex patterns, structural assertions, latency thresholds, cost guards. Cheap, fast, reproducible β no LLM call per run. Prefer this when the criterion can be expressed as code. -
llm_judgeβ an LLM scores generations against a prompt you write. Best for subjective or fuzzy checks: tone, helpfulness, hallucination detection, off-topic drift, instruction-following. Costs an LLM call per run and requires AI data processing approval at the org level. -
sentimentβ classifies sentiment from user messages on each matching generation. Returns a sentiment label and score, not a pass/fail verdict.
Results from all types land in ClickHouse as $ai_evaluation events. Boolean
evaluations (llm_judge and hog) set $ai_evaluation_result; sentiment
evaluations set $ai_sentiment_* properties instead.
This skill covers the full lifecycle: list/inspect/manage evaluation configs, run them on specific generations, query individual results, and get an AI-generated summary of pass/fail/N/A patterns across many boolean runs.
Tools
Tool Purpose
posthog:llma-evaluation-list List/search evaluation configs (filter by name, enabled flag)
posthog:llma-evaluation-get Get a single evaluation config by UUID
posthog:llma-evaluation-create Create a new llm_judge, hog, or sentiment evaluation
posthog:llma-evaluation-update Update an existing evaluation (name, prompt, enabled, β¦)
posthog:llma-evaluation-delete Soft-delete an evaluation
posthog:llma-evaluation-run Run an evaluation against a specific $ai_generation event
posthog:llma-evaluation-test-hog Dry-run Hog source against recent generations (no save)
posthog:llma-evaluation-summary-create AI-powered summary of pass/fail/N/A patterns across runs
posthog:execute-sql Ad-hoc HogQL over $ai_evaluation events
posthog:query-llm-trace Drill into the underlying generation that an evaluation scored
All llma-evaluation-* tools are defined in products/ai_observability/mcp/tools.yaml.
Event schema
Every run of an evaluation emits an $ai_evaluation event. Key properties:
Property Meaning
$ai_evaluation_id UUID of the evaluation config
$ai_evaluation_name Human-readable name
$ai_target_event_id UUID of the $ai_generation event being scored
$ai_trace_id Parent trace ID (for jumping to the trace UI)
$ai_evaluation_result_type Result kind: boolean or sentiment
$ai_evaluation_result For boolean evaluations: true = pass, false = fail
$ai_evaluation_reasoning Free-text explanation (set by the LLM judge or Hog code)
$ai_evaluation_applicable false when the evaluator decided the generation is N/A
$ai_sentiment_label For sentiment evaluations: positive, neutral, or negative
$ai_sentiment_score Confidence score for the winning sentiment label
When $ai_evaluation_applicable = false, the run counts as N/A regardless of $ai_evaluation_result.
For evaluations that don't support N/A, this property may be null β treat null as "applicable".
Workflow: investigate why an evaluation is failing
Works the same way for boolean llm_judge and hog evaluations β the differences
only matter when you eventually go to fix the evaluator (edit the prompt vs. edit
the Hog source). Sentiment evaluations should be inspected by sentiment label and
score rather than pass/fail filters.
Step 1 β Find the evaluation
posthog:llma-evaluation-list
{ "search": "hallucination", "enabled": true }
Look at the returned id, name, evaluation_type, and either:
-
evaluation_config.promptfor anllm_judge -
evaluation_config.sourcefor ahogevaluator
The Hog source is the ground truth for why a hog evaluator passes or fails β read it before assuming the failure is in the generation.
Step 2 β Get the AI-generated summary
posthog:llma-evaluation-summary-create
{
"evaluation_id": " ",
"filter": "fail"
}
Returns:
-
overall_assessmentβ natural-language summary -
fail_patternsβ grouped patterns withtitle,description,frequency, andexample_generation_ids -
pass_patternsandna_patternsβ same shape, populated whenfilterincludes them -
recommendationsβ actionable next steps -
statisticsβtotal_analyzed,pass_count,fail_count,na_count
The endpoint analyses the most recent ~250 runs (EVALUATION_SUMMARY_MAX_RUNS).
Results are cached for one hour per (evaluation_id, filter, set_of_generation_ids).
Pass force_refresh: true to recompute.
Compare filters in two calls to spot what's distinctive about failures vs passes:
posthog:llma-evaluation-summary-create
{ "evaluation_id": " ", "filter": "pass" }
Then diff the pass_patterns against the fail_patterns from Step 2.
Step 3 β Drill into example failing runs
Each pattern surfaces example_generation_ids. Pull the underlying trace for the most
representative example:
posthog:query-llm-trace
{ "traceId": " ", "dateRange": {"date_from": "-30d"} }
(If you only have a generation ID, query for it via execute-sql first to find the
parent trace ID β see below.)
Step 4 β Verify the pattern with raw SQL
The summary is LLM-generated and should be verified. Use execute-sql to count and
spot-check:
posthog:execute-sql
SELECT
properties.$ai_target_event_id AS generation_id,
properties.$ai_trace_id AS trace_id,
properties.$ai_evaluation_reasoning AS reasoning,
timestamp
FROM events
WHERE event = '$ai_evaluation'
AND properties.$ai_evaluation_id = ' '
AND properties.$ai_evaluation_result = false
AND (
properties.$ai_evaluation_applicable IS NULL
OR properties.$ai_evaluation_applicable != false
)
AND timestamp >= now() - INTERVAL 7 DAY
ORDER BY timestamp DESC
LIMIT 25
The N/A guard (IS NULL OR != false) is important β it matches the same logic the
backend uses to bucket runs.
Workflow: run an evaluation against a specific generation
Use this when the user pastes a trace/generation URL and asks "what would evaluation X say about this?".
posthog:llma-evaluation-run
{
"evaluationId": " ",
"target_event_id": " ",
"timestamp": "2026-04-01T19:39:20Z",
"event": "$ai_generation"
}
The timestamp is required for an efficient ClickHouse lookup of the target event.
Pass distinct_id if you have it β it speeds up the lookup further.
Workflow: build and test a new evaluator
Hog evaluator (deterministic, code-based)
Reach for this first when the criterion is rule-based β it's cheaper, faster, and
reproducible. Prototype with llma-evaluation-test-hog (no save):
posthog:llma-evaluation-test-hog
{
"source": "return event.properties.$ai_output_choices[1].content contains 'sorry';",
"sample_count": 5,
"allows_na": false
}
The handler returns the boolean result for each of the most recent N $ai_generation
events. Iterate on the source until it behaves as expected, then promote it via
llma-evaluation-create:
posthog:llma-evaluation-create
{
"name": "Output is valid JSON",
"description": "Fails when the assistant message can't be parsed as JSON",
"evaluation_type": "hog",
"evaluation_config": {
"source": "let raw := event.properties.$ai_output_choices[1].content; try { jsonParseStr(raw); return true; } catch { return false; }"
},
"output_type": "boolean",
"enabled": true
}
Hog evaluators have full access to the event and its properties β common patterns include schema validation, length/token limits, regex matches, and tool-call shape checks. Because they're deterministic, results are reproducible across reruns and trivially diff-able.
LLM-judge evaluator (subjective, prompt-based)
Use this when the criterion is fuzzy and a code rule would be brittle (tone, factuality,
helpfulness, on-topic-ness). There's no equivalent of llma-evaluation-test-hog for LLM
judges β the typical loop is to create the evaluator with enabled: false, run it
manually against a handful of representative generations via llma-evaluation-run, inspect
the results, refine the prompt with llma-evaluation-update, and then flip enabled: true
when you're satisfied:
posthog:llma-evaluation-create
{
"name": "Response stays on-topic",
"description": "LLM judge β fails if the assistant changes topic from the user's question",
"evaluation_type": "llm_judge",
"evaluation_config": {
"prompt": "You are evaluating whether the assistant's reply stays on-topic relative to the user's most recent question. Return true if it does, false if the assistant changed the subject. Return N/A if the user did not actually ask a question."
},
"output_type": "boolean",
"output_config": { "allows_na": true },
"model_configuration": {
"provider": "openai",
"model": "gpt-5-mini"
},
"enabled": false
}
Then dry-run against a known-good and a known-bad generation:
posthog:llma-evaluation-run
{
"evaluationId": " ",
"target_event_id": " ",
"timestamp": "2026-04-01T19:39:20Z"
}
LLM judges require organisation AI data processing approval. Hog evaluators do not.
Workflow: manage the evaluation lifecycle
Action Tool
Add a Hog evaluator llma-evaluation-create with evaluation_type: "hog" and evaluation_config.source
Add an LLM-judge evaluator llma-evaluation-create with evaluation_type: "llm_judge", evaluation_config.prompt, and a model_configuration
Tweak the source or prompt llma-evaluation-update (edits evaluation_config.source for Hog, evaluation_config.prompt for LLM judge)
Toggle N/A handling llma-evaluation-update with output_config.allows_na
Disable temporarily llma-evaluation-update with enabled: false
Remove llma-evaluation-delete (soft-delete via PATCH {deleted: true})
llm_judge evaluations require AI data processing approval at the org level
(is_ai_data_processing_approved). The same gate applies to
llma-evaluation-summary-create. Hog evaluations do not require this gate
β they run as plain code on the ingestion pipeline.
When to use Hog vs LLM judge
Reach for Hog by default. Switch to LLM judge only when the criterion can't be expressed as code.
Use Hog when⦠Use LLM judge when⦠The check is structural (JSON parses, schema matches) The check is about meaning (on-topic, helpful, factual) You need a deterministic, reproducible result A small amount of judgement variability is acceptable The criterion is cheap to compute The criterion requires reading and understanding text You can't get AI data processing approval You have approval and the criterion is genuinely fuzzy You need to enforce a hard limit (length, cost, etc.) You need to rate a quality dimension You want sub-millisecond evaluation A few hundred milliseconds + LLM cost are acceptable
A common pattern is to layer them: a Hog evaluator gates obvious format/length
violations cheaply, and an LLM-judge evaluator only fires on the generations that pass
the Hog gate (via conditions).
Investigation patterns
The summarisation tool works the same way regardless of whether the evaluator is hog
or llm_judge β it analyses the resulting $ai_evaluation events, not the evaluator
itself. The fix path differs (edit Hog source vs. edit prompt) but the diagnosis is
identical.
"Why is evaluation X suddenly failing more?"
llma-evaluation-list β confirm the evaluation is still enabled and unchanged
(compare evaluation_config.source or evaluation_config.prompt to the version you
expect)
llma-evaluation-summary-create with filter: "fail" β get the dominant
failure patterns and example IDs
SQL count of fails per day to confirm the regression window:
SELECT toDate(timestamp) AS day, count() AS fails
FROM events
WHERE event = '$ai_evaluation'
AND properties.$ai_evaluation_id = ' '
AND properties.$ai_evaluation_result = false
AND timestamp >= now() - INTERVAL 30 DAY
GROUP BY day
ORDER BY day
Drill into a representative trace per pattern via query-llm-trace
"Are passes and fails caused by the same root content?"
-
Generate two summaries: one with
filter: "pass", one withfilter: "fail" -
If
pass_patternsandfail_patternsdescribe similar content: -
For an
llm_judge: the prompt or rubric is probably ambiguous β rewordevaluation_config.promptand usellma-evaluation-update -
For a
hogevaluator: the rule is probably under- or over-matching β read the source viallma-evaluation-get, narrow the predicate, and retest withllma-evaluation-test-hogbefore pushing the fix viallma-evaluation-update
"Did a Hog evaluator regression after a code change?"
Hog evaluators are reproducible β if the source hasn't changed, identical inputs should yield identical outputs. When fail rates jump for a Hog evaluator:
-
llma-evaluation-getβ note the current source andupdated_at -
Spot-check the latest failing runs with the SQL query from Step 4 above
-
Re-run the source against those exact generations using
llma-evaluation-test-hogwith a modifiedconditionsfilter that targets them -
If the test results match the live results, the change is in the generations , not the evaluator (a model upgrade, prompt change upstream, etc.) β investigate the producer
-
If they diverge, the evaluator was edited; check git history of the source field via the activity log
"What kinds of generations does this evaluator skip as N/A?"
posthog:llma-evaluation-summary-create
{ "evaluation_id": " ", "filter": "na" }
Inspect na_patterns to see whether the N/A logic is doing the right thing. If a
pattern in na_patterns looks like something that should have been scored:
-
For an
llm_judge: the applicability instruction in the prompt is too broad β narrow it -
For a
hogevaluator withoutput_config.allows_na: true: the source is returningnull(or whatever the N/A signal is) too eagerly β tighten the precondition
"Score this single generation right now"
llma-evaluation-run with the trace's generation ID and timestamp. Useful for spot-checking
or wiring evaluations into a larger agent loop.
Constructing UI links
-
Evaluations list:
https://app.posthog.com/ai-evals/evaluations -
Single evaluation:
https://app.posthog.com/ai-evals/evaluations/<evaluation_id> -
Underlying generation/trace: see the
exploring-llm-tracesskill's URL conventions
Always surface the relevant link so the user can verify in the UI.
Tips
-
The summary tool is rate-limited (burst, sustained, daily) and caches results for one hour β repeated calls with the same
(evaluation_id, filter)are cheap; useforce_refresh: trueonly when you genuinely need fresh analysis -
Pass
generation_ids: [...]to scope a summary to a specific cohort of runs (max 250) -
The
statisticsblock in the summary response is computed from raw data, not the LLM β trust those counts even if a pattern'sfrequencyfield is qualitative -
For rich filtering not supported by
llma-evaluation-list(e.g. by author or model configuration), fall back toexecute-sqlagainst theevaluationsPostgres table or the$ai_evaluationClickHouse events -
When showing failure patterns to the user, always include 1-2 example trace links so they can validate the pattern visually
-
llma-evaluation-*tools useevaluation:readfor read tools andevaluation:writefor mutating tools;llma-evaluation-summary-createusesllm_analytics:write -
Hog evaluators are reproducible β if you suspect a regression,
llma-evaluation-test-hogwith the suspect source against the failing generations is the fastest way to bisect whether the change is in the evaluator or in the producer of the generations -
LLM-judge evaluators are non-deterministic across reruns; expect 1-5% noise even with a fixed prompt and model. If you're chasing a small regression in fail rate, prefer Hog or pin a deterministic provider/seed in the
model_configuration
npx skills add https://github.com/posthog/ai-plugin --skill exploring-llm-evaluationsRun this in your project β your agent picks the skill up automatically.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.