DisclaimerThis page does not make public any confidential information. The material shown is a simplification of the real project, for educational purposes only. All scores, counts and examples are illustrative, not ASIST's real results.
A new version only ships when it beats the previous one on a sealed set of real AOGs, scored node by node, with a statistically significant gain, zero safety-critical errors, and confirmation in shadow mode.
ASIST is an assistant for aircraft maintenance engineers at IAG airlines, starting with Aer Lingus. When a fault grounds an aircraft, an engineer describes it in plain text. ASIST searches past AOG cases and the fixes engineers recorded, falls back to the Airbus manuals when history is thin, and drafts resolution steps with references.
It is built as a LangGraph agent on Amazon Aurora PostgreSQL with pgvector, and this page shows how it would be evaluated with Langfuse. The team keeps improving it: models are retrained on new data, retrieval is tuned, and the agent gains new steps. Each change raises the same question. Is the new version actually better, and is it still safe? This page sets out a way to prove it.
Everything runs in AWS. The agent is Python with LangGraph, the chat interface is Next.js, and both run as containers on ECS. An ETL built on AWS Step Functions and Lambda loads the AOG history and the Airbus manuals into Aurora PostgreSQL with pgvector, calling an LLM in Amazon Bedrock to extract the problem and the solution from each AOG record. Langfuse is self-hosted on the same ECS cluster, and the test sets are versioned in S3.
| Layer | Component | Role in ASIST | Status |
|---|---|---|---|
| Agent | LangGraph, in Python | The graph in Part 1: classify, retrieve, fall back, draft, check | core |
| Chat interface | Next.js, as a container on ECS | Where engineers type the fault and read the answer | core |
| Agent API | Python service on ECS (e.g. FastAPI) | Runs the LangGraph agent behind the chat interface | added |
| Knowledge store | Amazon Aurora PostgreSQL with pgvector | AOG table (category column plus resolution vectors) and Airbus manuals table | core |
| Index snapshots | Aurora clone or snapshot per release | Freezes each version's index, so v1 can be re-run fairly (Part 3) | added |
| Ingestion (ETL) | AWS Step Functions; each step is a Lambda function | Loads past AOGs and Airbus manuals into Aurora. Some steps call an LLM in Bedrock to extract parts of each AOG record, such as the problem and the solution engineers found | core |
| Models | Amazon Bedrock, through a VPC endpoint | Field extraction in the ETL. Drafting, the LLM judge and embeddings would normally use it too; the exact models are to confirm | core |
| Fine-tuning | e.g. SageMaker or Bedrock custom models | Only if models are fine-tuned on AOG data | to confirm |
| Tracing and evaluation | Langfuse, self-hosted on ECS as web and worker services | Traces, datasets, experiments, scores (Part 10) | core |
| Langfuse storage | PostgreSQL, ClickHouse, Redis and an S3 bucket | Required by a self-hosted Langfuse; can use RDS, ElastiCache and a ClickHouse service | added |
| Datasets | Amazon S3 with versioning | Source of truth for the validation, sealed and anchor sets, imported into Langfuse. Sealed and anchor in a bucket only CI and reviewers can read | core |
| Access | Load balancer with WAF; SSO through the airline's identity provider | Only authorised engineers reach ASIST | added |
| Secrets | AWS Secrets Manager | Database credentials, model and Langfuse keys | added |
| CI/CD | e.g. GitHub Actions or CodePipeline; images in ECR | Builds and deploys containers and runs the release gate (Part 9) | to confirm |
| Infrastructure as code | e.g. Terraform or AWS CDK | Reproducible environments, including Langfuse | added |
| Infrastructure monitoring | Amazon CloudWatch | Logs, alarms, container health. Langfuse covers the LLM level | added |
| Statistics | Python: Langfuse SDK, SciPy or statsmodels | McNemar, bootstrap and Holm in the CI gate | added |
| Labelling | Spreadsheet or a labelling tool such as Label Studio | Double labelling and adjudication before import (Part 4) | added |
core the stack as described for ASIST, including Langfuse for its evaluation · added needed for the evaluation approach on this page · to confirm depends on choices not covered here.
Step names are illustrative. In production the ETL keeps loading new AOGs into the live index. The notes under the steps apply when it builds a frozen index for evaluation.
A new extraction prompt or Bedrock model changes what the index holds, and so changes ASIST's answers. Version the ETL's prompts and models with each release, and rebuild the release's Aurora clone with them.
Each release, an engineer compares the extracted problem and solution with the raw record on a sample of AOGs. Score it as extraction accuracy.
One badly extracted solution misleads every query that retrieves that AOG.
When building an index for evaluation, the ETL skips AOGs dated after the cutoff, so validation and sealed cases never reach the index.
Annotators write reference steps from the raw AOG record, not from the ETL's extracted solution. Otherwise extraction errors are copied into the answer key and become invisible.
Chunking splits the text into the pieces search returns: usually one AOG record, or one manual task with its heading. An embedding model turns each chunk into a vector, a list of numbers that captures its meaning. The load step stores the vector in a pgvector column next to the text and its metadata, with an index such as HNSW for fast search.
Queries must be embedded with the same model. A new embedding model means re-embedding everything into a new index.
Engineers' queries describe the problem, not the fix. If only the resolution steps are embedded, the query is compared with text that looks quite different. Options: embed problem and solution together, embed only the problem and return the solution with it, or store one vector for each.
Pick by AOG Recall@5 on the validation set, not by intuition.
Instrument the Bedrock calls inside the Lambda functions with the Langfuse SDK, and flush before each function returns, so extraction prompts, outputs and costs appear next to ASIST's own traces.
On ECS the unit of deployment is a task or service; "pod" is the Kubernetes term. A self-hosted Langfuse runs as two services, web and worker, and needs its own PostgreSQL, ClickHouse, Redis and S3 storage, kept separate from ASIST's Aurora database.
Every node in the graph gets its own metric (amber tags). An end-to-end score tells you something changed. Per-node scores tell you where.
ASIST as a LangGraph graph, simplified. ASIST abstains and escalates to an engineer in three cases: the question is out of scope; neither past AOGs nor the manuals hold anything relevant; or the guardrail check fails. "Nothing relevant" is decided by a relevance threshold, because vector search always returns its nearest results, even irrelevant ones. The abstain path skips the guardrail because it gives no technical content: a fixed message, at most listing the closest AOG references, with no values or steps. Whether ASIST abstains at the right times is measured separately, by correct abstention and over-refusal. The trajectory tag covers the whole run: node path, loops, latency and cost. In Langfuse each run is recorded as a trace with one span per node, which is where these per-node metrics are read (Part 10). Every metric name is explained in the glossary.
The newest Aer Lingus AOGs, held back from training and labelled by mechanical engineers.
Classifier, both retrievers, fallback decision, generation, guardrail, trajectory. Run as Langfuse experiments.
Both versions on the same new sealed set, same judge, 3–5 repeated runs, paired significance tests, slice by category.
On the validation set, test the new LLM, embeddings and reranker one at a time to see which one earned the gain.
No metric drop beyond threshold, zero critical safety errors, latency budget held.
Confirm with real engineers, then repeat per airline with its own sets.
For every version, v1 included, take a snapshot of all the AOG history available, then split it by date at the training cutoff. Older AOGs build the system. The newest AOGs are labelled by engineers and split again: a validation set the team practises on, and a sealed set used only to decide whether the version ships. Both sets exist before launch.
| Training & index data | Validation set | Sealed golden set | |
|---|---|---|---|
| Job | Build the system | Guide choices during development | Decide whether a release ships |
| Analogy | The textbook | Practice exams | The final exam, unseen |
| Dates | Before the cutoff | After the cutoff | After the cutoff |
| Who uses it | Fine-tuning jobs, the RAG index | Developers, every day | The release pipeline, plus engineer reviewers checking answers. Developers see only aggregate scores |
| Labels come from | The fix recorded in each AOG, no extra work | Annotators, same process as the sealed set | Annotators, double-labelled and adjudicated |
| Typical size | Thousands of AOGs | ~100–200 cases | ~300–500 cases |
| If misused | Test AOGs leak in and inflate scores | The team overfits to it, which is expected and why it is separate | Once tuned on, it stops measuring new AOGs and must be replaced |
Ground truth is the correct answer for a case. A golden set is a curated, governed collection of cases carrying that ground truth. Sealed describes who may touch it.
After a release, the old sets can be fed back as training data. New validation and sealed sets come from AOGs after the new cutoff.
Ground truth is never all used to build a version. Each release builds on everything up to its cutoff and holds back the newest slice to test on. While v1 is in service, new ground truth keeps arriving from four sources.
| Period | Ground truth available | Used for |
|---|---|---|
| Before v1 cutoff | Historical Aer Lingus AOG records with their recorded fixes | Builds v1: fine-tuning, embeddings, RAG index |
| v1 cutoff → v1 deployed | The newest AOGs before launch, annotated by engineers | v1's validation and sealed sets |
| v1 in service, before v2 cutoff | New AOG records, human-in-the-loop corrections, offline review, flagged failures, plus v1's retired test sets | Builds v2 |
| After v2 cutoff | The newest AOGs, annotated by engineers | v2's validation and sealed sets. v1 and v2 are scored on them side by side |
New AOG records: every resolved AOG still produces a recorded fix, whether or not ASIST was used. Human in the loop: an engineer approves or corrects a low-confidence answer as it happens. Offline review: engineers later label a sample of logged queries. Feedback and failures: thumbs-down, rephrased questions and escalations, plus problems found in shadow or canary, are prime candidates for review.
If every new labelled item goes into building v2, nothing is left to measure it on. The cutoff splits the new data: everything before it builds v2, everything after it tests v2 against v1.
A busy engineer's quick fix on shift should be checked, at least on a sample, before it counts as ground truth.
Unchecked corrections can teach v2 the wrong fix.
Queries labelled because ASIST asked for help, or because someone rated them, over-represent certain kinds of case. They are good training material, but the sealed set should still come from a representative sample of AOGs.
A sealed set built only from flagged queries over-weights hard or failed cases.
The rule is that v1 and v2 are scored on exactly the same cases. v1 is frozen and can be run again at any time, so at the v2 release both versions answer v2's new sealed set side by side. Neither has seen those AOGs. The sets stay the same size, around 350 cases; only the cases change.
Frozen means the whole v1 configuration, including the snapshot of its RAG index. A live index that keeps ingesting new AOGs may already contain v2's sealed cases, which would leak the answers. v2's index also holds more history than v1's, so part of v2's gain comes from newer data rather than better techniques. The ablation in Part 8 separates the two.
| Rotating sealed set | Fixed anchor set | |
|---|---|---|
| What it is | Built fresh each release from the newest AOGs | A small set that never changes: critical safety cases and known past failures |
| Used for | The v1-vs-v2 release decision | Long-term trend across every version, and catching old bugs that come back |
| Strength | Stays representative of today's AOGs; old sets can be fed back into training | Scores are comparable across all releases |
| Weakness | Absolute scores across releases are not comparable: 0.80 for v1 on one set and 0.84 for v2 on another proves nothing | Goes stale, leaks a little with every decision, and its AOGs can never be used for training or put in the RAG index |
Keeping one sealed set forever looks simpler but fails over time: the team slowly tunes to it, the fleet and fault types change, and its AOGs are lost as training data.
Annotators are qualified mechanical engineers who write down the correct answer for each case. "Annotate" means to add notes; in machine learning the notes are the labels that say what the system should output. They are also called labellers or raters. First, a case writer, often one of the same engineers, turns each AOG record into the question an engineer would have typed.
Experts disagree more than expected. One engineer may count a past AOG as relevant and another may not. Labelling twice, blind, shows where the case or the guidelines are ambiguous. A senior engineer resolves each disagreement, a step called adjudication.
The agreement rate is measured with Cohen's kappa (κ), which removes the agreement you would get by chance. Low κ means the instructions are unclear, which you want to know before blaming the model.
| B: relevant | B: not | |
|---|---|---|
| A: relevant | 42 | 8 |
| A: not | 7 | 43 |
observed agreement = (42+43)/100 = 0.85
chance agreement ≈ 0.50
κ = (0.85 − 0.50) / (1 − 0.50) = 0.70
Rule of thumb: below 0.4 poor, 0.4–0.6 moderate, 0.6–0.8 substantial, above 0.8 near-perfect.
Written the way engineers really type, with abbreviations. Clean, tidy questions make scores look better than real use.
The route is the path through the graph; the reference steps are the procedure given to the engineer. A case can take the right route and still give the wrong procedure, so they are scored separately. Most cases are sampled from real AOGs so the set stays representative. A small share of abstain cases is added on purpose: questions ASIST has no history or manual coverage for.
Annotators own every expected output, for every case in both the validation and the sealed set. They rarely start from a blank page: the AOG record already holds much of the answer, so a large part of the work is checking and correcting a pre-filled draft.
| Field | Starting point | What the annotator does | Effort |
|---|---|---|---|
category | Already in the AOG record's classification column | Checks it is right and consistent with the labelling guidelines | low |
route | Nothing; the record cannot say this | Judges whether past AOGs are enough, the Airbus manuals are needed, or ASIST should abstain. Depends on what else exists in the history | medium |
reference_steps | The resolution the original engineer wrote | Turns shift notes into clean, ordered steps; removes details specific to that one aircraft; fixes anything incomplete or wrong | high |
expected_refs | AMM/TSM tasks cited in the original resolution | Checks them against the current manual revision and adds missing ones | medium |
expected_aogs | A candidate list built by pooling (below) | Judges each candidate as relevant, partly relevant or not relevant | high |
forbidden | A general rule: no value that is not in a source | Adds the case-specific critical values to watch, such as the torque or limit for this repair | low |
No engineer can read thousands of past AOGs for every case. Instead, several retrievers each return their top results, the lists are merged and de-duplicated, and the annotator judges only that pool. The relevant ones become expected_aogs, with a grade for nDCG.
Every version being compared contributes to the pool equally, so neither is favoured. If a later run retrieves an AOG nobody has judged, it goes to an annotator before the scores are final.
An AOG that no retriever finds never enters the pool, so it can never be marked relevant. Using varied retrievers, including plain keyword search, reduces that blind spot.
An LLM can draft the fields from the AOG record to save engineer time. The engineer still checks every field.
The draft must not come from the ASIST version being tested, or the labels lean towards its answers and the test favours it.
Some teams double-label every field. Others double-label the judgement fields (route, expected_aogs, reference_steps) and have one annotator verify the factual ones (category, expected_refs).
Roughly half an hour of engineer time per case, mostly on expected_aogs and reference_steps. A sealed set of a few hundred cases, labelled twice, is a substantial commitment from people who also maintain aircraft. This is the main reason golden sets stay at hundreds of cases rather than thousands.
Effort levels, time per case and pooling are general practice in RAG evaluation, not a record of how ASIST was labelled.
| Layer | Metric | Checked against | Why it matters in ASIST |
|---|---|---|---|
| Ingestion (ETL) | Extraction accuracy of the problem and solution fields | raw AOG record, engineer check on a sample | A bad extraction misleads every query that retrieves that AOG |
| Classify | Accuracy, macro-F1, confusion matrix | category | A wrong category filters pgvector to the wrong AOGs |
| Retrieve AOGs | Recall@5, MRR, nDCG | expected_aogs | If the right past AOG is missing, generation cannot recover |
| Fallback decision | Precision and recall of "go to manuals" | route | Too eager adds noise; too reluctant misses procedures |
| Retrieve manuals | Recall@5 on AMM/TSM tasks | expected_refs | Manuals are the backstop when history is thin |
| Generate | Step coverage and order, faithfulness, citation accuracy | reference_steps, expected_refs, retrieved sources | Usable, grounded instructions an engineer can follow |
| Safety | Count of invented part numbers, torques, limits or task refs | forbidden | Zero tolerance: one error outweighs any average gain |
| Abstain | Correct abstention, over-refusal | route = abstain | Knowing when not to answer |
| Trajectory | Node path match, loops, p50/p95 latency, cost | trace | Catches agent regressions such as retry loops |
| Business | Time to resolve the AOG, escalations, feedback | production logs | The outcome the airline pays for |
Each term in plain words, with an ASIST example. All example numbers are illustrative.
A grid of real category (rows) against predicted category (columns). The diagonal is correct; everything off it is a mix-up, and it shows exactly which categories get confused.
The share of cases where the predicted category is correct.
correct ÷ all shown = 211 ÷ 237 = 0.89
If 40% of AOGs are engine-related, a router that is good only at engines can still look accurate.
Precision: when it says "landing gear", how often is it right? Recall: of all real landing-gear AOGs, how many did it catch?
LG precision = 55 ÷ 65 = 0.85
LG recall = 55 ÷ 64 = 0.86
Each one alone can be gamed: call everything landing gear and recall is perfect.
One number that combines precision and recall. It is high only when both are high.
F1 = 2 × P × R ÷ (P + R) = 0.85
Compute F1 for each category, then average them with equal weight. Rare categories count as much as common ones, so a router that neglects flight controls can't hide behind engines.
mean(F1LG, F1HYD, F1ENG, F1AVI)
Recall@5: of the documents that should be found, what share appear in the top 5? Precision@5: of the 5 returned, what share are relevant? k = 5 here assumes the top 5 results are passed to the LLM; anything ranked lower is invisible to it. Set k to the number of chunks the system really sends.
Recall@5 = 2 found ÷ 2 expected = 1.0
Precision@5 = 2 relevant ÷ 5 returned = 0.4
Recall ignores order: rank 1 and rank 5 count the same. Retrieval is the ceiling for everything after it.
Rewards putting the first relevant result near the top. For each query take 1 ÷ the rank of the first hit, then average across queries.
first hits at ranks 1, 4, 2
(1/1 + 1/4 + 1/2) ÷ 3 = 0.58
Only the first hit counts; the second relevant AOG is ignored.
Scores the whole ranking. Each relevant result earns points, worth less the lower it sits, and the total is divided by the best possible ordering, giving 0 to 1. It allows graded relevance: a perfect match earns more than a partial one.
grades by rank: [2, 0, 1, 0, 0]
DCG = 2/log₂2 + 1/log₂4 = 2.50
ideal [2,1,0,0,0] = 2.63 → nDCG = 0.95
Scores the "enough evidence?" decision. Precision: of cases sent to the Airbus manuals, how many needed them? Recall: of cases that needed the manuals, how many were sent?
Low precision adds noisy manual text; low recall leaves engineers with thin AOG history.
Same as Recall@5, measured on the Airbus manuals table against the expected AMM/TSM task references.
The share of claims in the answer that are supported by the retrieved documents. It measures whether the LLM is inventing things beyond what it was given.
3 supported claims ÷ 4 claims = 0.75
Faithful is not the same as correct. A faithful answer built on the wrong retrieved AOG is still wrong, which is why retrieval is scored separately.
Coverage: what share of the reference resolution steps appear in the answer. Order: whether they appear in a safe sequence, for example inspection before replacement.
4 of 5 reference steps present = 0.80
Of the references the answer cites, the share that exist, are correct, and actually support the step they are attached to.
2 valid ÷ 3 cited = 0.67
The overall verdict: would an experienced engineer accept this answer as right for this AOG? Usually graded on a rubric by an LLM judge, checked against engineer reviews.
A separate LLM grades answers using a written rubric, so hundreds of cases can be scored per run. In pairwise mode it sees the v1 and v2 answers side by side and picks the better one.
Judges have biases, such as preferring the first or the longer answer. Pin the judge model version, randomize the order, and check agreement with engineers using kappa. Langfuse can run such judges automatically on every experiment.
An answer that contains a part number, torque value, limit or AMM/TSM reference that appears in no approved source, or that contradicts the manual. Counted, not averaged; the target is zero.
Correct abstention: on cases that should not be answered, how often ASIST declines and escalates. Over-refusal: on answerable cases, how often it declines anyway.
Strict guardrails cut critical errors but can raise over-refusal. Track both.
The path the agent took through the LangGraph graph: which nodes ran, in what order, how many retries. Compared against the expected route.
A subset of the golden set, such as one category or one aircraft type. Overall averages can hide a regression in one slice.
Sort all response times. p50 (the median) is the middle one: half the queries are faster. p95 is the one 95% of the way along: only 1 query in 20 is slower. p50 is the typical wait; p95 is the slow tail engineers remember.
The average is pulled up by a few slow queries and describes almost nobody's experience. Release gates use p95.
The response time only 1 query in 100 exceeds. Used for stricter systems; p50 and p95 are usually enough for an internal tool.
Model tokens plus database and reranker calls for one answer. A reranker or retry loop can improve quality while raising cost.
The range of values consistent with the data. "+0.05 (95% CI +0.02 to +0.08)" means the real gain is very likely between 2 and 8 points. If the range includes zero, you cannot claim an improvement.
The same LLM can answer the same question differently each time. Running each version 3–5 times and averaging stops one lucky run from deciding the release.
Compares two versions on the same cases using only the cases where they disagree. If v2 fixes many more cases than it breaks, the gain is unlikely to be chance.
| v2 right | v2 wrong | |
|---|---|---|
| v1 right | 241 | 12 |
| v1 wrong | 30 | 67 |
(|30 − 12| − 1)² ÷ (30 + 12) = 6.9 → p ≈ 0.009
The same 350 sealed cases as the slice table in Part 7: v1 right on 253 (0.72), v2 right on 271 (0.77).
For continuous scores like faithfulness. Redraw the 350 cases at random with replacement, say 10,000 times, compute the v2 − v1 difference each time, and take the middle 95% of those differences as the confidence interval.
Agreement between two raters after removing agreement expected by chance. Used for annotator vs annotator, and LLM judge vs engineers. See the worked example in Part 4.
Check 7 slices at a 5% threshold and there is about a 30% chance at least one looks significant by luck. Holm's method tightens the threshold so a slice regression is only called real when it is.
1 − 0.95⁷ ≈ 0.30
Remove or add one change at a time and re-measure, to see which change caused the gain. Done on the validation set, because it takes many runs. See Part 8.
Test cases, or their answers, ending up in training data, prompts or the RAG index. Scores inflate because the system remembers rather than reasons. Prevented by the cutoff, the sealed set and frozen index snapshots.
hard block If it fails, the release cannot ship. No trade-off allowed. soft gate If it fails, a person can review it and accept a justified trade-off.
The new version answers every live query in parallel, but only the old version's answer is shown. Real traffic, no risk to the answers engineers see.
A small share of users, say 10% of engineers or one station, get the new version first. Named after canaries once used to detect gas in coal mines.
Something that got worse in the new version, even while other metrics improved.
All numbers in this section are illustrative, to show what the report looks like.
| Category | n | v1 | v2 | Δ |
|---|
Overall v2 is better, about 0.72 → 0.77, but landing gear drops. With only 64 cases the drop may not survive the Holm correction. In a safety-relevant category it still fails the soft gate: someone must explain it before release.
v2 shipped newer data and several improvements at once. Adding them one at a time, on the validation set, attributes the gain and exposes a change that does nothing or hurts.
AOG Recall@5 on the validation set, illustrative. Ablation needs many runs, so it never touches the sealed set. The first step runs v1's pipeline on v2's newer data, to separate the gain from more history from the gain from better techniques. The fine-tuned LLM does not move retrieval, which is expected: its effect shows up in the generation metrics.
| Gate | Rule | Type |
|---|---|---|
| Safety | Zero critical errors on the sealed set | hard block |
| Primary metric | A quality release must show a significant gain in answer correctness (p < 0.05). A release that only targets cost or speed must show no significant drop. | hard block |
| Node regression | No node metric drops more than 2 points | soft gate |
| Slice regression | No significant slice drop after Holm correction; a visible drop in a safety-relevant category is reviewed anyway | soft gate |
| Budgets | p95 under 8 s, cost per query within target | soft gate |
| Judge health | Judge vs engineer κ at least 0.7 | hard block |
Part 10 shows how to run all of this on Langfuse; LangSmith, Ragas or DeepEval are alternatives. Version everything that changes a result: prompts, model IDs, the embedding model per pgvector index, and the sets themselves. A new embedding model means a new index, never mixed vectors in one table.
Langfuse is an open-source platform for tracing and evaluating LLM applications. It plugs into LangGraph through a callback handler, so every ASIST run becomes a trace with one span per node. On top of those traces it holds the datasets, runs the experiments and stores the scores this page describes.
Annotators write the answer key before any testing. Evaluators mark ASIST's answers against that key every time a test runs. Think of an exam: teachers write the answer key, a machine marks the multiple-choice questions, a teaching assistant marks the essays, and the teacher spot-checks the assistant.
| Who | When | What they produce | In Langfuse |
|---|---|---|---|
| 2 annotators + adjudicator | Before testing, once per case | The answer key: category, route, expected AOGs, references, steps | Dataset items |
| Code evaluators | Every run, every case | Exact checks: Recall@5, route, values with no source | Scores |
| LLM judge | Every run, every case | Judgement calls: faithfulness, correctness, v1 vs v2 preference | Scores from LLM-as-a-judge evaluators |
| Engineer reviewers | After runs, on a sample | Hand marks used to check the LLM judge (kappa) | Annotation queues, human scores |
An LLM is never one of the annotators. It may pre-fill a draft (Part 4), but engineers decide the answer key. Otherwise ASIST would be graded against another model's opinion.
Code evaluators and the LLM judge mark every case in every run, for both versions. That volume is impossible by hand, which is why the marking is automated.
Code always gives the same mark for the same input, so engineers verify its logic once, when it is written. The LLM judge can drift unnoticed, so each release engineers mark a sample by hand and compare with the judge using kappa.
Below κ 0.7 the judge's scores are not trusted: fix its rubric or review more cases by hand.
| On this page | In Langfuse | How it is used |
|---|---|---|
| One ASIST run and its node path | Trace, with a span per LangGraph node and a generation per LLM call | Recorded through the LangChain/LangGraph CallbackHandler. Gives the trajectory, p50/p95 latency and cost per query. |
| Validation, sealed and anchor sets | Datasets: a validation and a sealed dataset per release, plus one permanent anchor dataset | Versioned files in S3 are the source of truth and are imported into Langfuse. Each item holds the input (query, aircraft), the expected output (category, route, expected AOGs, references, steps) and metadata (AOG date, difficulty, airline). |
| Scoring v1 and v2 on the same cases | Experiments (dataset runs) | One run per version and repeat, for example v2-sealed-run3. Runs on the same dataset are compared side by side. |
| Recall@5, route, critical errors, step coverage | Scores from code evaluators | Deterministic checks written in Python and attached to each item of each run. |
| Faithfulness, answer correctness, pairwise preference | LLM-as-a-judge evaluators | Pin the judge model and its prompt version. Calibrate against engineer scores with kappa. |
| Annotators and adjudicator writing the answer key | Dataset items, in their expected output | The double labelling and adjudication from Part 4 are usually organised in a labelling tool or spreadsheet, and the final labels are imported as dataset items. |
| Engineers marking ASIST's answers | Annotation queues and human scores | Engineers mark a sample of answers by hand, to check the LLM judge with kappa, and review production traces to find new cases. |
| Prompt versions | Prompt management, with labels such as production and staging | Each trace records which prompt version produced it, so a prompt change is a tracked part of a release. |
| Shadow, canary, feedback, human in the loop | Production traces tagged with the version; feedback and corrections as scores | Reviewed traces can be added to a dataset once they fall after the next cutoff. |
| Significance tests and release gates | Computed outside Langfuse | A CI step pulls the scores through the API and runs McNemar, the bootstrap, Holm and the gates from Part 9. |
# Langfuse Python SDK v3. Check names against the current docs. from langfuse import get_client, Evaluation from langfuse.langchain import CallbackHandler from asist import build_graph, untraceable_values langfuse = get_client() # self-hosted URL and keys from env sealed = langfuse.get_dataset("asist-sealed-2026-10") def make_task(version): graph = build_graph(version) # frozen v1 or v2 def task(*, item, **kwargs): state = graph.invoke( item.input, config={"callbacks": [CallbackHandler()]}, ) return {"answer": state["answer"], "sources": state["sources"], "top5": state["retrieved_aogs"][:5], "route": state["route"]} return task def aog_recall_at_5(*, output, expected_output, **kwargs): expected = set(expected_output["expected_aogs"]) if not expected: # manual or abstain case return Evaluation(name="aog_recall@5", value=None) found = expected & set(output["top5"]) return Evaluation(name="aog_recall@5", value=len(found) / len(expected)) def critical_errors(*, output, **kwargs): bad = untraceable_values(output["answer"], output["sources"]) return Evaluation(name="critical_errors", value=len(bad), comment=", ".join(bad)) for version in ["v1", "v2"]: for run in range(5): sealed.run_experiment( name=f"{version}-sealed-run{run}", task=make_task(version), evaluators=[aog_recall_at_5, critical_errors], )
A sketch, not production code. build_graph and untraceable_values stand for ASIST's own code.
v2 answers 100% of live queries in parallel. Logged as Langfuse traces, never shown. Judge plus engineer spot checks.
About 10% of engineers or stations see v2. Watch feedback, rephrasing and escalations.
Promote when online metrics hold. Track time to resolve the AOG against v1.
New IAG airline, new fleet mix and writing style. Build its own sets before reusing the Aer Lingus result.
Each technique targets one layer, so you know in advance which metric should move and what could get worse.
Score every release node by node against a sealed, time-split set of real AOGs labelled by engineers. Ship only with a significant gain, zero safety-critical errors, and confirmation in shadow mode.