Reading a Trace
A trace is only useful if you can read it. Walk through the helpdesk agent’s real trace: the tree of runs, which step took the time, how many tokens each model call used, exactly what the model saw - and why the same question took 10.8 seconds once and 5.7 the next time.
What you will be able to do
- Read a trace as a tree: root run, child runs, run types
- Find where the time goes, and why
- Read token counts per model call
- Open one model run and see exactly what the model saw and answered
- Compare two traces of the same question
- Find the things LangSmith shows that our runs contained: tags, metadata, errors
The idea, in plain English
Lesson 5.2 sent the helpdesk agent’s runs to LangSmith. In the LangSmith app you click a trace and see a tree on the left and the details of the selected run on the right. This lesson teaches you to read that tree - what each line means, and where to look for each kind of problem.
To be sure every number is real, we read the same data the app shows: the runs our agent sent (captured in Lesson 5.2), printed as a tree by a short script. In the LangSmith app you will see the same names, types, times and tokens.
langsmith 0.14.7, LangGraph 1.2.14, llama3 on Ollama.
Worked example: Two real traces of "Should I bring an umbrella in Mumbai, and what is 12 * 8?" - one with a cold model (10.80 s), one warm (5.73 s) - read step by step.
1 - What happened?
The root is the whole run. Its children, top to bottom, are the steps in order: agent, tools, agent, tools, agent.
The real trace, read in four questions. Cold model, 10.80 s in total.
Words you will see in this lesson
The words on a LangSmith trace page.
Root runThe top of the tree: the whole run of your app.Child runA step inside another run, shown indented under it.run_typeWhat a run is: chain (a step or graph), llm (a model call), tool, parser, retriever...LatencyHow long a run took, from start to end.Tokens in / outHow much text the model read and wrote (Lesson 5.6).Cold startThe first call after the model was not in memory - it must load first.An everyday example: a delivery tracking page
- A parcel tracking page lists every stop: picked up, sorting centre, out for delivery, delivered - each with a time. (Runs, in order, with times.)
- If the parcel is late, you look for the stop that took the longest. (Find the slowest run.)
- If it arrived broken, you look at what happened at each stop. (Open a run and read its inputs and outputs.)
- Comparing two parcels on the same route shows whether the delay was normal or a one-off. (Compare traces.)
The tree, line by line
This is the trace of our umbrella question, printed from the runs the agent sent. Read it like this:
- LangGraph is the root: the whole graph.invoke(). 10.80 s in total.
- agent and tools are the graph’s nodes, in the order they ran: agent, tools, agent, tools, agent.
- Inside each agent: RunnableSequence (our prompt | model | parser), with ChatOllama (the model call, run_type llm) and PydanticOutputParser (turning the text into a Step).
- should_continue is the router from Lesson 3.4 - it shows up as its own tiny run after each agent step.
- In the LangSmith app this is the tree on the left; clicking a line shows its details on the right.
"""Print a trace the way LangSmith shows it: a tree of runs with type, time and tokens."""
from datetime import datetime
from read_runs import load_runs # turns the uploads saved in Lesson 5.2 back into runs
def seconds(run):
start = datetime.fromisoformat(run["start_time"].replace("Z", "+00:00"))
end = datetime.fromisoformat(run["end_time"].replace("Z", "+00:00"))
return (end - start).total_seconds()
def tokens(run):
for gen in (run.get("outputs") or {}).get("generations", [[]])[0] if run["run_type"] == "llm" else []:
usage = gen.get("message", {}).get("kwargs", {}).get("usage_metadata")
if usage:
return f' tokens {usage["input_tokens"]} in / {usage["output_tokens"]} out'
return ""
runs = load_runs()
children = {}
for run in runs.values():
children.setdefault(run.get("parent_run_id"), []).append(run)
def show(run, depth=0):
print(f'{" " * depth}{run["name"]:<{34 - 3 * depth}} {run["run_type"]:<8} {seconds(run):5.2f} s{tokens(run)}'
+ (" ERROR" if run.get("error") else ""))
for child in sorted(children.get(run["id"], []), key=lambda r: r["dotted_order"]):
show(child, depth + 1)
for root in children[None]:
print("project:", root.get("session_name"), "| trace:", root["trace_id"][:8], "| tags:", root.get("tags"))
show(root)project: helpdesk-agent | trace: 01a12eb8 | tags: []
LangGraph chain 10.80 s
agent chain 6.07 s
RunnableSequence chain 6.07 s
ChatOllama llm 6.06 s tokens 91 in / 16 out
PydanticOutputParser parser 0.00 s
should_continue chain 0.00 s
tools chain 0.00 s
agent chain 1.02 s
RunnableSequence chain 1.02 s
ChatOllama llm 1.02 s tokens 108 in / 17 out
PydanticOutputParser parser 0.00 s
should_continue chain 0.00 s
tools chain 0.00 s
agent chain 3.68 s
RunnableSequence chain 3.67 s
ChatOllama llm 3.67 s tokens 123 in / 69 out
PydanticOutputParser parser 0.00 s
should_continue chain 0.00 sQuestion 1 - where did the time go?
Look down the time column. Almost all of the 10.80 s is in the three ChatOllama runs:
- Tools, parsers and the router took 0.00 s. Our code is not the problem - the model is.
- Call 1: 6.06 s for 16 output tokens. Call 2: 1.02 s for 17. Almost the same work, six times the time.
- That is a cold start: llama3 was not in memory, so the first call loaded it (Lesson 5.6).
- Call 3: 3.67 s for 69 output tokens - writing the long final answer. Writing is the slow part.
- So the two ways to make this faster: keep the model loaded, and make the final answer shorter.
Question 2 - where did the tokens go?
- Tokens in grew with every call: 91 -> 108 -> 123. Each agent step sends the whole history again (Lesson 5.6). Long loops get more expensive with every round.
- Tokens out were small for decisions (16, 17) and large for the answer (69).
- In the LangSmith app, token counts appear on each llm run and are added up for the whole trace. With a paid model, cost = tokens x price.
Question 3 - what exactly did the model see?
When an answer is wrong, open the llm run. It shows the exact messages the model received and the exact text it returned. Here is the second model call:
- metadata says which node and step it belongs to (langgraph_node "agent", step 3), and which model, provider and temperature were used.
- inputs are the real messages: our system prompt, then the question with the history so far - the weather observation is already there.
- The output text is the model’s raw JSON. The parser run after it turns it into a Step.
- This is where you find most agent bugs: a missing observation, a confusing prompt, or a model answer the parser misread (Lesson 5.1).
import json
from read_runs import load_runs
runs = sorted(load_runs().values(), key=lambda r: r["dotted_order"])
llm = [r for r in runs if r["run_type"] == "llm"][1] # the second model call
print("name:", llm["name"], "| run_type:", llm["run_type"])
meta = llm["extra"]["metadata"]
print("metadata:", {k: meta[k] for k in sorted(meta) if k.startswith("ls_") or k in ("langgraph_node", "langgraph_step")})
print("--- inputs (the messages the model saw):")
for m in llm["inputs"]["messages"][0]:
print(f' [{m["kwargs"]["type"]}]', m["kwargs"]["content"])
gen = llm["outputs"]["generations"][0][0]
print("--- output text:", gen["text"])
print("--- usage:", gen["message"]["kwargs"]["usage_metadata"])
tools = [r for r in runs if r["name"] == "tools"][0]
print("--- first tools run, outputs:", tools["outputs"]["history"])name: ChatOllama | run_type: llm
metadata: {'langgraph_node': 'agent', 'langgraph_step': 3, 'ls_integration': 'langchain_chat_model', 'ls_model_name': 'llama3', 'ls_model_type': 'chat', 'ls_provider': 'ollama', 'ls_temperature': 0.0}
--- inputs (the messages the model saw):
[system] You answer questions using two tools: get_weather(city) and calculate(expression). Choose ONE next action. Use a tool for each piece of information you do not have yet. Never repeat an action that already has an Observation. When the Observations contain everything needed, reply with final_answer.
[human] Question: Should I bring an umbrella in Mumbai, and what is 12 * 8?
Action: get_weather('Mumbai')
Observation: Rainy, 27°C
--- output text: {"action": "calculate", "action_input": "12 * 8"}
--- usage: {'input_tokens': 108, 'output_tokens': 17, 'total_tokens': 125}
--- first tools run, outputs: ["Action: get_weather('Mumbai')", 'Observation: Rainy, 27°C']Question 4 - is it always like this? Compare two traces
One trace is one example. We ran the same question again, later, with the model still loaded:
- Total: 10.80 s the first time, 5.73 s the second.
- The first model call: 6.06 s -> 1.02 s. The model was already loaded.
- Everything else was almost the same: the same tree, the same tokens (91/16, 108/17, 123/69), the last call 3.67 s -> 3.66 s.
- So the slow first trace was a cold start, not a problem in the agent. Without comparing, you might have "fixed" the wrong thing (Lesson 5.1: optimising the wrong step).
project: helpdesk-agent | trace: 01a12eba | tags: []
LangGraph chain 5.73 s
agent chain 1.03 s
RunnableSequence chain 1.03 s
ChatOllama llm 1.02 s tokens 91 in / 16 out
PydanticOutputParser parser 0.00 s
should_continue chain 0.00 s
tools chain 0.00 s
agent chain 1.01 s
RunnableSequence chain 1.01 s
ChatOllama llm 1.01 s tokens 108 in / 17 out
PydanticOutputParser parser 0.00 s
should_continue chain 0.00 s
tools chain 0.00 s
agent chain 3.66 s
RunnableSequence chain 3.66 s
ChatOllama llm 3.66 s tokens 123 in / 69 out
PydanticOutputParser parser 0.00 s
should_continue chain 0.00 sQuestion 5 - did anything fail?
Errors are marked on the run where they happened, and on the runs above it:
- In Lesson 5.2 the calculate tool raised ZeroDivisionError. Its run was sent with an error field holding the exception and the traceback - in the app it is shown in red.
- But an error returned as TEXT is not an error to LangSmith. Our helpdesk calculate() returns "Error: only + - * / ..." as a normal result - the run looks successful.
- So read the outputs too, not only the red marks. Or raise real exceptions where you want failures to be visible.
In the LangSmith app
With your real key (Lesson 5.2), open your project. You will find:
- A list of traces, newest first, with their latency, token count and status.
- Clicking a trace opens the tree (left) and the selected run’s details (right): inputs, outputs, metadata, timings, tokens, errors.
- Filters: by tag, by metadata (user_id = u-42), by status, by latency - useful when you have thousands of traces.
- The same five questions work there: what happened, where did the time go, where did the tokens go, what did the model see, did anything fail.
Tip: Before changing an agent, save the link to a typical trace. After the change, compare the new trace with it. A trace before and after is the simplest proof that a change helped.
The recipe: five questions for every trace
- 1. What happened? Read the tree top-down: which nodes ran, in what order.
- 2. Where did the time go? Find the biggest latency - usually an llm run.
- 3. Where did the tokens go? Look at tokens in (history growing?) and out (long answers?).
- 4. What did the model see? Open the llm run: exact messages and exact output.
- 5. Did anything fail? Check errors - and outputs that contain error text.
- Then compare with another trace of the same question before you change anything.
Reading a trace at a glance
Root runThe whole request.
LangGraph chain 10.80 s
Node runOne graph step.
agent / tools chain
Model callTime and tokens.
ChatOllama llm 6.06 s 91 in / 16 out
ParserText -> structured.
PydanticOutputParser parser
RouterWhich way next.
should_continue chain 0.00 s
Model metadataOn every llm run.
ls_model_name, ls_provider, ls_temperature
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Trace a question that needs only the calculator. How many agent steps do you expect, and how many do you see?”
“Stop Ollama (or wait until it unloads the model) and trace the same question twice. Compare the first ChatOllama run.”
“Open the third llm run. How many Observation lines did the model see?”
“Change calculate() to raise an exception instead of returning "Error: ...". How does the trace change?”
What usually goes wrong
Our first trace was 10.80 s because of a cold start; the next was 5.73 s. Compare before you fix.
The total hides the cause. The tree showed 0.00 s for tools and almost everything in model calls.
Errors returned as text look like success. Read the outputs too.
Key points
- A trace is a tree: the root is the whole run, children are the steps, in order.
- run_type tells you what each step is: chain, llm, tool, parser.
- In our trace almost all time was in model calls; the first call was slow because the model was loading.
- Tokens in grow every round (91 -> 108 -> 123) because the history is sent again.
- An llm run shows the exact messages the model saw and its exact output - where most bugs are found.
- Compare two traces before changing anything: cold 10.80 s vs warm 5.73 s.
Quick check before you move on
Quiz
- 1.
The last model call wrote 69 tokens and took 3.67 s. What does that tell you?
- 2.
What is should_continue in the tree?
- 3.
A tool returns "Error: ..." as text. How does it look in the trace?
- 4.
Why compare two traces of the same question?
Interview questions
How do you debug a slow LLM agent?
Open its trace, find the runs with the largest latency (usually model calls), check tokens in/out per call, look for cold starts, repeated calls and growing context, and compare against other traces before optimising.
What information does a trace give you that logs do not?
The causal tree of steps with timings, the exact model inputs and outputs, token usage, and errors in context - for one specific request.
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...