How do I trace a research agent built with LangChain?
Rius traces a LangChain research agent once you install the langchain extra and call init(): chains, tools, retrievers and model calls arrive as nested spans with tokens and cost. You then add a root span for each research run, a span for each stage, and a retrieval span for searches that bypass LangChain. Wrap the run in a session and a user scope so you can find it later.
How it works
The LangChain integration hooks LangChain’s callback system, so anything that runs through it is traced: LCEL chains, agents and LangGraph nodes. Each step becomes one span typed by what it is. Chains are CHAIN spans, tool calls are TOOL, retrievers are RETRIEVER, and model calls are LLM spans with model, token usage and messages. Rius computes cost on its side from the model and token counts, so you never send a cost value.
A research agent usually has more structure than the callbacks show:
- A run root. Nothing in LangChain marks where one research run starts and ends, so you open an agent span around it.
- A step per stage. Planning queries, gathering sources and writing the report each get a span, so the trace reads as stages rather than a flat list of model calls.
- Your own retrieval. A search API or index you call directly is invisible to LangChain’s callbacks. A retrieval span records the data source, how many documents you asked for, and their ids and scores.
Spans you open with observe and the span helpers become the parents of the LangChain spans created inside them, so the framework’s spans nest under your stages. When a LangChain span wraps a client-library span, each model call is still counted once in token and cost totals.
Set it up
-
Install the integration and start the SDK. This is the only shared setup, and the LangChain page covers it in full, including the TypeScript package.
pip install "glassflow-rius[langchain]" langchain -
Add a run root and a span per stage.
observecreates a CHAIN span named after the function by default. Passkind=SpanKind.AGENTon the root so it reads as the agent. -
Open a retrieval span around any search that does not go through a LangChain retriever. It records the document ids and scores, not the text. If you want the retrieved text captured as content, pass it to
set_output(). -
Wrap each run in a session scope and each request in a user scope.
import rius
from rius import SpanKind
rius.init(api_key="ri_...", service_name="research-agent")
@rius.observe(kind=SpanKind.AGENT)
def run_research(question: str) -> str:
queries = plan_queries(question)
sources = gather(queries)
return write_report(question, sources)
@rius.observe
def plan_queries(question: str) -> list[str]:
return planner_chain.invoke({"question": question}) # your LangChain chain
@rius.observe
def gather(queries: list[str]) -> list[str]:
texts = []
for query in queries:
with rius.start_as_current_span(
kind=SpanKind.RETRIEVER, data_source_id="web-search", top_k=5
) as obs:
obs.set_input(query)
docs = search(query) # your own search call
obs.set_retrieved_documents([{"id": d.id, "score": d.score} for d in docs])
texts += [d.text for d in docs]
return texts
@rius.observe
def write_report(question: str, sources: list[str]) -> str:
return writer_chain.invoke({"question": question, "sources": sources})
with rius.session(conversation_id), rius.user(request.user_id):
run_research(question)The names planner_chain, writer_chain, search, conversation_id and request.user_id are yours. In TypeScript the same pieces are observe, startAsCurrentSpan, withSession and withUser. A named function expression gives each observe span a name, because an arrow function traces as anonymous.
Choose the session id to match how people use the agent. If a user asks follow-up questions to refine one report, use one id for the conversation, so every turn’s trace is grouped together. With no session scope, each trace is its own session. Do not set a process-wide session id in a server that handles many users, because their traces would merge. Send an opaque user id rather than an email.
What you get
- One trace per research run. The agent root contains your stages, and each stage contains the LangChain chains, tools and model calls it triggered.
- Retrieval spans that show which data source was searched, how many documents were requested, and which ids came back with what scores.
- Tokens and list-price cost estimates in US dollars on every model call, rolled up to the run.
- Runs grouped by session, so you can follow a multi-turn research conversation across traces.
- A Users view of what each person’s runs cost and where they failed.
Open a trace after one run to check it. You should see the root, then the stages, then the LangChain spans, with the model calls carrying model, tokens and cost.
When something else fits
- If your agent is a single LangChain graph and you only need its structure, the integration alone is enough. Add your own spans when you want named stages or a retrieval step LangChain does not see.
- If part of the agent calls a provider directly rather than through LangChain, pair the integration with the matching client library integration. The Integrations page lists them all.