creating-online-evaluations

作者: posthog

在PostHog AI可观测性中持续运行在线评估,基于你已识别的真实故障模式。当用户希望……时使用。

npx skills add https://github.com/posthog/ai-plugin --skill creating-online-evaluations

Creating online evaluations

An online evaluation automatically scores either each matching $ai_generation or the whole trace containing it, until disabled. A good eval comes from a real failure mode you've found in production traffic, not from a guess or a generic metric like "hallucination" or "helpfulness". This skill starts once that failure mode is identified and turns it into a scoped, continuously-running eval.

First, know what you're evaluating. Finding and ranking the failure modes worth catching is a separate job. If the user doesn't specify what they want to evaluate, ask them. If they are still vague about it and don't refer to a specific failure mode, run exploring-ai-failures to scope a use case, find failing traces, and produce a ranked list of failure modes.

For the mechanics of writing and iterating an evaluator (Hog source vs LLM-judge prompt, dry-running, debugging a live eval), defer to exploring-llm-evaluations.

Tools

ToolPurpose
posthog:llma-provider-key-listFind a usable (ok state) provider key to pin (llm_judge)
posthog:llma-evaluation-judge-modelsList valid provider+model combos
posthog:llma-evaluation-test-hogDry-run Hog source against recent generations before creating
posthog:llma-evaluation-createCreate the evaluation (always enabled: false first)
posthog:llma-evaluation-runSpot-run a draft eval against one generation
posthog:llma-evaluation-updateIterate config, then flip enabled: true
posthog:execute-sqlVerify a condition matches the events and volume you expect
posthog:generate-app-urlBuild a region- and project-qualified deep link to the eval

The full create payload (every field, the config schemas, the exact conditions shape) is in references/evaluation-payload.md.

Phase 1 — Pick the failure mode to evaluate

Start from a real, observed failure, not a metric you picked in advance. If you don't already have one, run exploring-ai-failures to scope a use case, find failing traces, and produce a ranked list of failure modes — then come back. With that list in hand, talk with the user to choose what to turn into an eval:

  • Most frequent, most painful first. A handful of modes usually cover the majority of failures.
  • Pair obvious fixes with the eval, don't skip it. If a prompt tweak would likely fix the failure, set up the eval anyway and suggest the fix alongside it — a rising pass rate is how you confirm the fix landed.
  • One mode per eval. Three failure modes is three evals, not one prompt trying to catch everything.

You should end with a single, crisp, checkable criterion — "the reply must stay on the user's topic", "the tool call must include an order_id". Then move to Phase 2.

Phase 2 — Build the online eval

2.1 — Choose the eval type

Use…When the criterion is…
hogStructural / rule-based (JSON parses, length, regex, tool-call shape). Cheap, deterministic, no provider key needed.
llm_judgeSubjective / fuzzy (tone, factuality, on-topic). Costs an LLM call per run; needs AI data-processing approval + a provider key.
sentimentYou want sentiment labels on user messages, not a pass/fail (unless very specifically asked for, usually not relevant to this skill).

Reach for hog first, escalate to llm_judge if there is no deterministic way to check for what we want to check.

2.2 — Choose the target

TargetBehavior
generationRuns once for each matching $ai_generation, immediately after ingestion. This is the default.
traceRuns once for the whole trace after the first matching generation and a configurable wait for the trace to finish.

For a trace target, send "target": "trace" and "target_config": { "window_seconds": 1800 }. The wait must be between 10 seconds and 2 hours and defaults to 30 minutes. Conditions still match the generation that triggers the run; the evaluator itself receives the complete trace. Sentiment evaluations support only the generation target.

New Hog source should use the globals shared by both targets:

GlobalMeaning
evaluation_eventsOne generation event for a generation target, or every captured event for a trace target.
targetThe target's type, id, total_cost_usd, and total_latency_seconds.
item.input_text / item.output_textBest-effort readable projections; use these for length, keyword, and regex checks.
item.input / item.outputOriginal serialized values; use these when the evaluator needs to parse the captured JSON itself.

Generation evaluations still expose top-level input, output, properties, and event. Trace evaluations still expose their original events and trace globals. Those globals are kept for compatibility with saved evaluators. Do not use target-specific globals in new source that needs to work for both targets. The text projections recognize common provider payloads but are not authoritative; use item.input / item.output when exact structure matters.

2.3 — Gate (llm_judge only)

Before creating an llm_judge eval, confirm it can actually run, or it errors on first fire. Hog and sentiment skip this.

posthog:llma-provider-key-list            // pick a key whose state == "ok"
posthog:llma-evaluation-judge-models      // { "provider": "openai" } → valid models

Every llm_judge eval runs on a provider key. Pick an ok-state key from llma-provider-key-list and set it as model_configuration.provider_key_id.

If there's no ok key, stop and ask the user to add/validate one in the UI — the agent can't create keys.

2.4 — Create it disabled

Create with enabled: false so nothing fires until the scope is verified. Minimal hog example:

posthog:llma-evaluation-create
{
  "name": "Output is not empty",
  "description": "Fails when a generation has no readable output",
  "evaluation_type": "hog",
  "evaluation_config": { "source": "let count := 0\nfor (let i, item in evaluation_events) {\n    if (item.event == '$ai_generation') {\n        count := count + 1\n        if (length(trim(item.output_text)) == 0) { return false }\n    }\n}\nreturn count > 0" },
  "output_type": "boolean",
  "output_config": { "allows_na": false },
  "target": "generation",
  "target_config": {},
  "conditions": [
    { "id": "default", "rollout_percentage": 100, "properties": [{ "key": "$ai_model", "type": "event", "operator": "icontains", "value": "gpt" }] }
  ],
  "enabled": false
}

For llm_judge, swap evaluation_config to { "prompt": "…" } and add "model_configuration": { "provider": "openai", "model": "gpt-5-mini", "provider_key_id": "<uuid of an ok-state key from llma-provider-key-list>" }. Full field reference: references/evaluation-payload.md.

2.5 — Verify the scope before enabling

conditions is where online evals go wrong: too broad and you evaluate (and bill) a firehose; too narrow and it never fires. Confirm the filter matches the events you expect, and roughly how many per day:

posthog:execute-sql
SELECT count() AS matched, count() / 7 AS per_day
FROM events
WHERE event = '$ai_generation'
    AND properties.$ai_model ILIKE '%gpt%'      -- mirror each condition property
    AND timestamp >= now() - INTERVAL 7 DAY

For generation targets, count() is the run volume. For trace targets, count distinct non-empty $ai_trace_id values because matching generations from the same trace schedule only one run.

If volume is high, set rollout_percentage below 100 to sample. Spot-check the evaluator with llma-evaluation-test-hog (hog) or llma-evaluation-run against one generation (llm_judge). Both tools currently use generation samples; for a trace target they can check shared source or prompt behavior, but they do not reproduce the complete settled trace. Review the first live trace results before increasing rollout.

Watch out: some orgs reuse a single $ai_trace_id across 100k+ events. Scoping by trace-ID prefix can match far more than expected — verify volume with the SQL above before enabling.

2.6 — Enable, then close the loop

posthog:llma-evaluation-update
{ "evaluationId": "<uuid>", "enabled": true }

It now runs on every new matching generation, or once per matching trace for a trace target. This isn't one-and-done: the user should be aware that they need to keep an eye on results and iterate if the outcome is not the expected one. To wire results into a Slack feed, see feature-usage-feed.

Scoping with conditions

conditions is a list of condition sets — OR between sets, AND within a set's properties. Each set is { id, rollout_percentage, properties[] }. There is no time window inside conditions; sampling is only rollout_percentage (0–100). Property filters use the standard PostHog shape (key, type, operator, value). For trace targets, these filters still select the generation that triggers the eventual whole-trace evaluation.

"conditions": [
  { "id": "openai",    "rollout_percentage": 100, "properties": [{"key": "$ai_provider", "type": "event", "operator": "exact", "value": "openai"}] },
  { "id": "anthropic", "rollout_percentage": 25,  "properties": [{"key": "$ai_provider", "type": "event", "operator": "exact", "value": "anthropic"}] }
]

Constructing UI links

Build links with posthog:generate-app-url — never hand-write the host or the /project/<id>/ prefix. The url must be a canonical catalog template; pass concrete ids via params, never inline them into the path.

  • Evaluations list: generate-app-url {url: "/ai-evals/evaluations"}
  • Single evaluation: generate-app-url {url: "/ai-evals/evaluations/{id}", params: {id: "<evaluation_id>"}}

These resolve to the correct region host and project prefix (e.g. https://us.posthog.com/project/<id>/ai-evals/evaluations/<evaluation_id>). Surface the link after creating so the user can review and toggle it in the UI.

Tips

  • Evals come from real failures, not generic metrics. Start from a failure found in this product's traffic (via exploring-ai-failures), not from "let's measure hallucination". A metric nobody traced back to a real bad output is noise.
  • One eval, one failure mode. Different failure modes need different evals; don't make one eval try to catch everything.
  • Suggest changes along with the eval if possible. If it's clear a prompt change would fix the issue, for instance, set up the eval but also suggest to the user they change the prompt: they should soon see the eval go from low pass rate to a higher pass rate.
  • hog first. No provider key, no AI approval, deterministic. Reach for llm_judge only when the criterion genuinely can't be coded.
  • Always create disabled, verify scope, then enable. An eval firing on the wrong events is worse than none — noise, and (for llm_judge) cost.
  • Gate llm_judge before creating, not after. A judge eval with no usable provider key errors on first run.
  • bytecode is server-written for hog evals — never pass it; send only evaluation_config.source.
  • For cluster-scoped evals, identify the cluster with exploring-llm-clusters, then translate its event filter into conditions.