Can We Find the Run Behind This Score?
A dashboard says answers improved. Connect scores to their requests and prompt versions so we can see which answers were actually checked.
19 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
The Answer and Its Test Used to Live Apart
BOTIE
This is an implementation of evidence we already need, rather than a new definition of correctness. In our maintenance example, identifying the run lets us inspect its retrieved manual, the answer and the evaluator when a score looks suspicious.
Sources for this history
Which Version Gave That Answer?
Botie’s new prompt seems better at answering maintenance questions. The average quality score has gone up. Before we celebrate, which requests received scores? Were both prompt versions tested on comparable questions? Did a tool-level score get mistaken for a judgement of the final answer?
Langfuse gives us somewhere to connect execution records, prompt versions and evaluations. We will work through the records first, then see how the SDK records those relationships. A useful dashboard depends on those relationships being correct.
The records are invented exports. No data is sent to Langfuse by this workshop.
Step 1 of 2
A score needs an address
The score rows arrived in a different order from the request rows. Joining by row position would attach a good answer’s score to the wrong request.
| Trace ID | Prompt version | Score with rubric grounded-v2 |
|---|---|---|
| visit-A | 3 | 0 |
| visit-B | 3 | 1 |
| visit-C | 3 | Not scored |
visit-A
- Prompt version
- 3
- Score with rubric grounded-v2
- 0
visit-B
- Prompt version
- 3
- Score with rubric grounded-v2
- 1
visit-C
- Prompt version
- 3
- Score with rubric grounded-v2
- Not scored
Step 2 of 2
Keep the denominator visible
Only two of the three requests have the selected judgement. Report both what those judgements say and how much of the version they cover.
| Measure | Calculation | Interpretation |
|---|---|---|
| Available-score mean | (0 + 1) / 2 = 0.5 | Mean of evaluated requests only |
| Coverage | 2 / 3 ≈ 0.667 | About two thirds received this judgement |
Available-score mean
- Calculation
- (0 + 1) / 2 = 0.5
- Interpretation
- Mean of evaluated requests only
Coverage
- Calculation
- 2 / 3 ≈ 0.667
- Interpretation
- About two thirds received this judgement
Make every judgement traceable to its run
The score identity and evaluation coverage survive changes in arrival order, prompt revision and evaluator revision.
Are the improved scores covering the whole version?
Attach one rubric version’s scores by trace ID, select the prompt version, and report the available-score mean beside its coverage.
BOTIE
[Mean available score, fraction of runs scored]
- 00.5
- 11
Follow the operations
1. Recorded runs and prompt revisions
[ { "traceId": "A", "promptVersion": 3 }, { "traceId": "B", "promptVersion": 3 }, { "traceId": "C", "promptVersion": 2 } ]2. Trace scores and rubric revisions
[ { "traceId": "B", "value": 1, "rubric": "grounded-v2" }, { "traceId": "C", "value": 1, "rubric": "grounded-v2" }, { "traceId": "A", "value": 0, "rubric": "grounded-v2" } ]3. Requested rubric revision
grounded-v2
4. Keep matching records
[ { "traceId": "B", "value": 1, "rubric": "grounded-v2" }, { "traceId": "C", "value": 1, "rubric": "grounded-v2" }, { "traceId": "A", "value": 0, "rubric": "grounded-v2" } ]5. Attach scores by trace ID
[ { "traceId": "A", "promptVersion": 3, "score": 0 }, { "traceId": "B", "promptVersion": 3, "score": 1 }, { "traceId": "C", "promptVersion": 2, "score": 1 } ]6. Requested prompt revision
3
7. Keep matching records
[ { "traceId": "A", "promptVersion": 3, "score": 0 }, { "traceId": "B", "promptVersion": 3, "score": 1 } ]8. Average available scores
0.5
9. Keep scored runs
[ { "traceId": "A", "promptVersion": 3, "score": 0 }, { "traceId": "B", "promptVersion": 3, "score": 1 } ]10. Count items
2
11. Count items
2
12. Divide
1
13. Collect two results
- 00.5
- 11
14. Result
- 00.5
- 11
These simplified records allow one score per trace for the selected rubric. Real systems also distinguish observation IDs, evaluator sources and repeated runs.
1. Choose the boundary of one trace
Use one trace for a maintenance request whose outcome we want to inspect. Its observations describe operations within that request, such as retrieving a manual and producing an answer. A session can group several requests from the same conversation.
Keep these meanings stable. If one deployment records a whole conversation as one trace and another records each turn separately, a trace-level latency comparison mixes different units of work.
The trace boundary should answer a question someone actually asks about the service.
2. Give observations the right jobs
A general span can enclose application work. A generation describes a model interaction and can carry the model identifier, input, output and usage. A retrieval operation should keep the retrieved document identities attached to that operation.
The resulting tree lets us inspect the evidence available before an answer. It cannot reveal private model reasoning, and it should not be presented as a complete explanation of why the model chose particular words.
Instrument actual operations. A recorded answer fixture should not be labelled as a live model call.
3. Send a small real execution to Langfuse
Start with a harmless local lookup and a surrounding request observation. The context manager keeps the active parent while the child runs. When the script ends, flush queued telemetry so a short-lived process does not exit before export.
Configure credentials and the destination in your own environment. The browser workshop has neither, so its records stay local. A successful export checks the instrumentation path, not the correctness of a maintenance assistant.
This example deliberately records a span for a dictionary lookup. It does not claim to run a model.
Run in your own environment
Trace a lookup before adding model instrumentation
The lookup below actually runs locally. Its timing is measured by the SDK and its records are sent to your configured Langfuse project.
Install langfuse in a separate Python environment.
Set LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY and LANGFUSE_BASE_URL for your project. Keep credentials out of browser code and source control.
Use an appropriate test project and inspect its data retention before sending real requests.
from langfuse import get_client
client = get_client()
manuals = {"P7": {"revision": "r4", "procedure": "inspection-required"}}
with client.start_as_current_observation(as_type="span", name="maintenance-request") as request:
with client.start_as_current_observation(as_type="span", name="manual-lookup") as lookup:
result = manuals.get("P7")
lookup.update(output={"found": result is not None, "revision": result["revision"]})
request.update(output={"lookup_complete": result is not None})
client.flush()Open the request and inspect the manual-lookup child. These fixture values describe a local dictionary, not a production retrieval result.
Langfuse tracing quickstart. Documentation checked .
4. Record the prompt that was actually used
A label such as production can point to a different prompt tomorrow. A version identifies a particular saved revision. Record the resolved version with the run so we can distinguish changes to instructions from changes in questions.
The compiled text also depends on its variables. Keep the manual revision, the retrieval configuration and the model configuration identifiable. A prompt version alone cannot reproduce an answer whose evidence changed.
Resolved versions describe what ran. A movable label describes how the application chose it.
5. Connect a managed prompt to its generation
Passing a managed prompt object to a Langfuse generation links the observation to that prompt. The helper below accepts a real text-generation callable from your application; the lesson does not replace it with a canned model answer.
Create a text prompt named maintenance-answer with a question variable before using the helper. Decide separately whether retaining its compiled input is appropriate for the data your service handles.
This records the input and output of the supplied callable. Its result and cost depend on the provider and implementation you supply.
Run in your own environment
Wrap your application’s text generator
Supply generate as a callable that receives the compiled prompt and returns the real model’s text response. This helper does not invoke itself.
Configure the Langfuse SDK as in the preceding example.
Create the named text prompt and provide your application’s authenticated generator and matching model name.
Call client.flush() during orderly shutdown of a short-lived script. Add actual provider usage if your integration does not already record it.
from langfuse import get_client
def traced_answer(question, generate, model_name):
client = get_client()
prompt = client.get_prompt("maintenance-answer")
compiled = prompt.compile(question=question)
with client.start_as_current_observation(
as_type="generation", name="maintenance-answer",
model=model_name, prompt=prompt, input=compiled,
) as generation:
answer = generate(compiled)
generation.update(output=answer)
return answerKeep the provider call inside the observation so the recorded duration covers it. Do not report invented token counts.
Langfuse: link prompts to traces. Documentation checked .
6. A score needs a meaning as well as a number
An outcome score might describe the whole request. A retrieval score might describe whether a particular result contains the answer. Attach each score at the level it evaluates, using the trace ID and, for an observation-level judgement, its observation ID.
Name the criterion and identify the evaluator or rubric version. A score named quality is hard to compare if one evaluator measures citation support and another measures writing style. A human judgement and a model judgement can disagree without either being a transmission error.
The numeric scale is part of the contract. Our workshop uses zero to one; other evaluations can use categories or different ranges.
7. Join by identity before averaging
The scoring worker may finish requests in a different order. It may also score yesterday’s request while today’s run is still waiting. Use trace IDs to connect scores to runs, then filter the desired prompt version.
For this workshop, select one rubric revision first. Each run has at most one resulting score. Duplicate scores for that selection are an error that requires a policy, not an invitation to keep whichever arrived last.
An identity join protects the meaning of a row when execution order changes.
A score of zero is present
A dictionary indexed by trace ID can distinguish a missing judgement from a low judgement.
scores = {"visit-B": 1.0, "visit-A": 0.0}
for trace_id in ["visit-A", "visit-B", "visit-C"]:
print(trace_id, scores.get(trace_id))visit-A 0.0
visit-B 1.0
visit-C NoneUsing a truthiness check would incorrectly treat the zero score as absent. Compare against None or check key membership.
Check yourself
Questions on Parts 1 to 7
Question 1 of 2
How should we connect these results?
Use the information below.
Run order: A, B, C. Scored results arrive as B=1 and A=0; C has no score.8. Show coverage beside the available-score mean
If the evaluator fails on malformed answers, discarding those rows can make a broken prompt look excellent. Keep the run in the denominator for coverage even when its score is absent. Report evaluation errors separately so someone can fix them.
Our mean uses only present scores and is explicitly labelled that way. If none are present, its value is missing rather than zero. Deciding that an evaluation failure should fail a release is a separate policy we will define later.
Available-score mean = sum of present scores / number of present scores
Evaluation coverage = number of scored runs / number of selected runsNeither replacing every missing score with zero nor silently dropping it answers all questions. Preserve its missing state first.
9. Keep experiments attached to their cases
A dataset item describes one task and its expected behaviour. An experiment runs a candidate on items and connects its outputs to the corresponding cases. The experiment identity should include the application and prompt revisions being compared.
A production conversation is useful evidence, but copying it into an evaluation dataset requires review. Remove sensitive fields, preserve the information boundary and record why the case matters. Do not expose a hidden expected answer to the agent under test.
The next lesson builds these cases so an experiment can measure more than a few favourable conversations.
10. Use the viewer to ask a specific question
When a score falls, open the underlying run. Did retrieval return the right manual revision? Did the answer omit a required warning? Did the evaluator judge against evidence that the agent never received?
Then compare like cases across versions. A chart filtered to prompt 3 is useful, but a higher score on an easier set of requests does not establish that prompt 3 improved the same task.
Observability helps us find evidence. Evaluation design determines what comparison that evidence supports.
11. Plan for the tracing service being unavailable
A telemetry timeout should have a defined effect on the application. Decide whether export is buffered, retried, sampled or allowed to fail without blocking the maintenance response. Monitor dropped observations so an empty dashboard cannot quietly appear healthy.
Separate optional diagnostic export from the durable evidence required to authorise a write. If the application requires an approval record before dispatch, losing that record should prevent the write under that policy. A temporarily unavailable dashboard does not need to stop an otherwise permitted read-only manual lookup.
Treat exporting input and output as a data flow with access controls. The presence of a tracing SDK does not make every field appropriate to collect. Keep organisation boundaries in the tracing project’s access policy too, and test that a dashboard user cannot inspect another organisation’s requests merely by knowing a trace ID.
Keep a stable set of non-sensitive identifiers even when you reduce payload detail.
12. Choose an inspection tool that fits the environment
Langfuse combines tracing with prompt and evaluation records. Phoenix also provides trace inspection and evaluations, using OpenTelemetry and OpenInference conventions for model-related data. Existing teams may already have an OpenTelemetry collector and a tracing backend.
Compare how the tools preserve your required IDs, where the data is stored, and how evaluators attach their results. Installing several exporters does not by itself produce better evidence, and can duplicate records if instrumentation overlaps.
The portable skill is following a judgement back to the exact execution. The named tools provide different places to do that.
Check yourself
Questions on Parts 1 to 12
Question 1 of 2
What must accompany the mean when discussing prompt 3?
Use the information below.
Prompt 3 has scores [1,1] on two of twenty requests. Prompt 2 has scores on all twenty comparable requests.Try it with Botie
Build with Botie
Write and run Python