๐Ÿง  LangGraph & LLM Observability โ€” Master Guide

Human-in-the-Loop ยท Persistence ยท Debugging ยท Telemetry ยท Agent Traces ยท Anomaly Detection ยท Cost & Hallucinations

The LangGraph Ecosystem at a Glance
LangGraph is an orchestration runtime for stateful, long-running AI agents. Here's how all the pieces fit together.

๐Ÿ—๏ธ The Core Architecture

LangGraph models your agent as a directed graph โ€” nodes are actions (LLM calls, tools, human steps), edges are the routing logic between them. Unlike simple chains, graphs can have cycles, enabling iterative reasoning and self-correction.

๐Ÿ“ฅ Input
โ†’
๐Ÿค– LLM Node
โ†’
๐Ÿ”ง Tool Node
โ†’
๐Ÿ™‹ Human Node
โ†’
๐Ÿค– LLM Node
โ†’
๐Ÿ“ค Output

The arrow from Tool Node can loop back to LLM Node โ€” this is what makes LangGraph different from a simple pipeline.

๐Ÿ’พ Persistence

Checkpoints save state after every node. Agents survive crashes, can run for days, and support "time travel" debugging.

MemorySaverPostgreSQLRedis

๐Ÿ™‹ Human-in-the-Loop

Pause execution at any node for human review, approval, or correction. The agent waits โ€” hours or days if needed โ€” then resumes exactly where it stopped.

interrupt_beforeinterrupt_after

๐Ÿ”ญ Observability

Every node execution, LLM call, and tool invocation is traced. You can inspect inputs, outputs, latency, tokens and cost at each step.

LangSmithLangfuse

๐Ÿ“ฆ The Full Stack: What Each Layer Does

LayerToolWhat It Does
Agent FrameworkLangGraphOrchestrates nodes, manages state, handles cycles & loops
Tracing & DebuggingLangSmithRecords every run, visualizes traces, evaluates outputs
Open-Source ObservabilityLangfuseSelf-hostable trace + eval platform; framework-agnostic
Persistence StorePostgreSQL / RedisDurably stores checkpoints so agents can resume
Telemetry StandardOpenTelemetryIndustry-standard spans/traces that both tools can emit
๐Ÿ™‹ Human-in-the-Loop (HITL)
Pause your agent at strategic points so a human can review, approve, edit, or redirect โ€” without losing any state.
๐Ÿ’ก
Core Idea: LangGraph can "interrupt" before or after any node. The full state is checkpointed. The agent justโ€ฆ waits. A human can inspect the state, modify it, and tell the graph to resume.

๐Ÿ“ Graph-Level Interrupts

Defined when you compile the graph. Execution always pauses at these nodes, regardless of runtime conditions. Best for predictable approval gates.

# Pause BEFORE the "submit" node runs graph = workflow.compile( checkpointer=checkpointer, interrupt_before=["submit_node"] )
โœ“ Predictableโœ“ Auditable

โšก Node-Level Dynamic Interrupts

Triggered inside a node based on runtime logic. The node decides mid-execution that it needs human input (e.g., low confidence score, ambiguous input).

from langgraph.types import interrupt def risky_node(state): if state['confidence'] < 0.7: human_input = interrupt("Low confidence โ€” approve?") return {"approved": human_input}
โœ“ Dynamicโœ“ Conditional

๐Ÿ”„ The HITL Lifecycle

1๏ธโƒฃ
Agent runs
to interrupt
โ†’
2๏ธโƒฃ
State saved
to checkpoint
โ†’
3๏ธโƒฃ
Human reviews
(hours/days)
โ†’
4๏ธโƒฃ
Human edits
state / approves
โ†’
5๏ธโƒฃ
Agent resumes
from checkpoint

โœ… When to Use HITL

  • โœ“
    High-stakes actions โ€” sending emails, financial transactions, deleting data
  • โœ“
    Low model confidence โ€” agent isn't sure, needs a human call
  • โœ“
    Compliance requirements โ€” regulated industries needing sign-off
  • โœ“
    Ambiguous instructions โ€” user intent unclear at runtime
  • โœ“
    Content moderation โ€” outputs that need review before publishing

๐Ÿ”ง State Modification Patterns

  • โ†’
    Approve as-is โ€” resume without changes
  • โ†’
    Edit state โ€” update values before resuming (e.g., fix a draft)
  • โ†’
    Reject & rewind โ€” roll back to an earlier checkpoint
  • โ†’
    Fork โ€” create a parallel branch with different state
  • โ†’
    Escalate โ€” route to a different node / human
๐Ÿ’พ Persistence & Checkpointing
Persistence is what transforms fragile scripts into production-grade agents that survive failures, support long runs, and enable time-travel debugging.

โšก MemorySaver

In-memory only. Fast, zero setup. Lost on restart.

Best for: demos, local dev, testing.

from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver()

๐Ÿ˜ PostgreSQL

Durable, production-grade. State persists across server restarts. Supports concurrent agents.

Best for: production, multi-user apps.

from langgraph.checkpoint.postgres import PostgresSaver checkpointer = PostgresSaver.from_conn_string(DB_URL)

โšก Redis

Fast in-memory + optional persistence. Low-latency for high-throughput workflows.

Best for: high-frequency, latency-sensitive agents.

from langgraph.checkpoint.redis import RedisSaver checkpointer = RedisSaver(redis_client)

๐Ÿ“ธ What Gets Checkpointed?

After every node execution, a full snapshot is saved:

๐Ÿ“ฆ
State object โ€” all variables in your graph state
Messages history โ€” all LLM inputs/outputs so far
Tool outputs โ€” results from every tool call
Decision history โ€” which edges were taken
๐ŸŽ
Overhead: ~5โ€“15ms per node to write to PostgreSQL
For LLM nodes (200โ€“2000ms), this is invisible.
For many lightweight nodes: batch writes or use Redis.

โฐ Time Travel Debugging

Checkpoints create a complete history. You can rewind to any prior state, modify it, and replay from there.

โœ… Step 1
โ†’
โœ… Step 2
โ†’
โœ… Step 3
โ†’
โŒ Step 4 (bad)
โ†ฉ
๐Ÿ” Rewind to Step 3
โ†’
โœ๏ธ Edit state
โ†’
โœ… Step 4 (fixed)
โš ๏ธ
Time travel creates a fork โ€” it doesn't overwrite history. The original bad branch is preserved for auditing.
๐Ÿ› Debugging LangGraph Agents
Agents fail in subtle ways. Here's a systematic toolkit for finding what went wrong and where.

๐Ÿ” Streaming for Real-Time Visibility

Stream intermediate outputs as the graph runs โ€” don't wait for final output to know what's happening.

# See each node's output as it happens for chunk in graph.stream(input, config): for node, output in chunk.items(): print(f"[{node}]", output)
values โ€” full state after node
updates โ€” only changes
debug โ€” everything + timing

๐Ÿ“œ Inspecting Checkpoint History

Walk through every checkpoint to see exactly what the state looked like at each step.

# Fetch full execution history history = graph.get_state_history(config) for state in history: print(state.values) # state at this step print(state.next) # next node scheduled print(state.metadata) # step number, timestamp

๐Ÿšฆ Common Failure Patterns & Fixes

SymptomLikely CauseDebug Approach
๐Ÿ” Infinite loopConditional edge always routes backAdd loop counter to state; stream to watch cycles
๐Ÿ’ฅ Partial failureExternal API timeoutRewind to pre-failure checkpoint; configure retry_policy
๐Ÿคฏ Wrong outputBad decision at node NTime-travel to node N-1; edit state; replay
๐Ÿง  Context lossState schema mismatchInspect checkpoint; check Pydantic model types
๐Ÿ’ธ Cost spikeToo many LLM calls in loopCount LLM invocations via trace; add max_iterations
โฑ๏ธ Slow nodeSequential calls that could be parallelUse streaming debug mode; check per-node latency in trace

๐Ÿ› ๏ธ LangGraph Studio (Visual Debugger)

LangGraph Studio gives you a GUI to:

๐Ÿ”ญ Langfuse vs LangSmith
Both are LLM observability platforms. The right choice depends on your stack, data requirements, and budget.
๐ŸŽฏ
TL;DR: LangSmith = best if you're already on LangChain/LangGraph (zero-config). Langfuse = best if you want self-hosted, open-source, or framework-agnostic (plus OpenTelemetry native as of v3). Note: Langfuse was acquired by ClickHouse in Jan 2026 โ€” roadmap still evolving.
Dimension LangSmith Langfuse
Made byLangChain teamLangfuse GmbH (โ†’ ClickHouse)
Open SourceNo (client SDKs are MIT)Yes (MIT, full self-host)
LangGraph integrationOne env var, zero configWorks, more setup
Framework agnosticYes, but best with LangChainYes โ€” any LLM stack
OpenTelemetry (OTEL)Added Mar 2025Native (v3, mid-2025)
Self-hostingEnterprise license requiredFree, Docker/K8s
UI / UX polishMore polished trace treeFunctional, improving
Free tier5,000 traces/monthMore generous on self-hosted
Paid pricing$39/seat/month (Plus)Observation-based pricing
Data residencySaaS only (or Enterprise)Full control when self-hosted
Eval / datasetsStrong, built-inGood, less integrated
Prompt managementYesYes

Choose LangSmith ifโ€ฆ

  • โœ“
    You're using LangGraph / LangChain
  • โœ“
    You want zero-config tracing (one env var)
  • โœ“
    You need a polished UI for team debugging
  • โœ“
    Evaluation datasets are important to you
  • โœ“
    SaaS is fine (no data residency concerns)

Choose Langfuse ifโ€ฆ

  • โœ“
    You need self-hosted / data residency (HIPAA, GDPR)
  • โœ“
    You use multiple frameworks (not just LangChain)
  • โœ“
    You already use OpenTelemetry in your backend
  • โœ“
    Cost matters โ€” self-hosted is essentially free
  • โœ“
    You want to inspect/modify the platform itself

โšก Quick Setup: LangSmith with LangGraph

# Just set these env vars โ€” that's it for LangGraph import os os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_API_KEY"] = "your-api-key" os.environ["LANGCHAIN_PROJECT"] = "my-agent" # Every LangGraph run is now traced in LangSmith

โšก Quick Setup: Langfuse with LangGraph

from langfuse.callback import CallbackHandler handler = CallbackHandler( public_key="pk-...", secret_key="sk-..." ) # Pass as callback to your graph's config config = {"callbacks": [handler]} graph.invoke(input, config=config)
๐Ÿ” Agent Traces โ€” What to Look For
A trace is the complete execution record of one agent run. Click each node below to see what to inspect.

๐ŸŒณ Interactive Trace Example โ€” Click nodes to expand

๐Ÿƒ Agent Run (root) โœ“ success 4,821 tokens 3.2s total $0.0042
What to check:
โ€ข Total duration vs. expected SLA
โ€ข Total token count (input + output combined)
โ€ข Total cost โ€” is it higher than baseline?
โ€ข Status: success / error / interrupted
โ€ข Thread ID / config to link to a user session
๐Ÿ“ Node: parse_input โœ“45ms120 tokens
What to check:
โ€ข Input prompt โ€” is context window reasonable?
โ€ข Did the model extract the right entities/intent?
โ€ข Token count โ€” is the system prompt bloated?
๐Ÿค– LLM Call: GPT-4o โœ“1,840ms2,100 tokens$0.003
What to check:
โ€ข Prompt tokens vs completion tokens ratio (high prompt = expensive context)
โ€ข Latency โ€” p95 vs average; spikes indicate model issues
โ€ข Temperature setting โ€” high temp = more variance/hallucination risk
โ€ข Did the model follow instructions? (check finish_reason)
โ€ข finish_reason: "stop" โœ… | "length" โš ๏ธ (truncated!) | "content_filter" ๐Ÿšจ
๐Ÿ”ง Tool Call: web_search โœ“820ms0 tokens
What to check:
โ€ข Did the model call the right tool? (tool selection accuracy)
โ€ข Are the tool arguments correct and well-formed?
โ€ข Tool latency โ€” external API slowness shows up here
โ€ข Tool output size โ€” huge results bloat the next LLM call
๐Ÿ™‹ HITL Interrupt: approval_node โธ paused0ms active
What to check:
โ€ข How long did the human take to respond? (track in state)
โ€ข What edit did they make to the state?
โ€ข Did they approve, reject, or fork?
โ€ข Use this for audit trail / compliance logging
๐Ÿค– LLM Call: GPT-4o (synthesis) โš ๏ธ long2,890ms2,601 tokens$0.004
๐Ÿšจ Anomaly detected โ€” what to investigate:
โ€ข Latency 2,890ms vs baseline 1,200ms โ€” model overloaded or context too large?
โ€ข Token count jumped โ€” did a tool return unexpectedly large output?
โ€ข Check finish_reason โ€” was the output truncated?
โ€ข Compare output to expected schema โ€” signs of hallucination?

๐Ÿ“Š Key Trace Metrics โ€” What's Normal vs. Alarming

MetricHealthyWarningAlarm
LLM latency (GPT-4o)<2s2โ€“5s>5s
Prompt tokens per call<8K8โ€“32K>32K
Tool calls per run<55โ€“15>15 (loop?)
Graph iterations<33โ€“8>8 (infinite loop?)
Cost per runEstablish baseline2x baseline5x+ baseline
finish_reasonstoptool_callslength / content_filter
๐Ÿšจ Anomaly Detection & Hallucination Indicators
Agents fail silently. Here's what to monitor and what patterns scream "something's wrong."

๐Ÿ”ด Hallucination Signals โ€” What to Look For in Traces

๐Ÿ“Š Statistical Signals

  • Output length much longer/shorter than baseline
  • High token generation variance run-to-run
  • Repeated phrases or self-contradiction in output
  • finish_reason = "length" (output was cut short)
  • Unusually high perplexity / low confidence scores

๐Ÿง  Semantic Signals

  • Output contradicts retrieved source documents (RAG)
  • Fabricated citations, URLs, or names
  • Claims not grounded in tool outputs
  • Date/number inconsistencies in output
  • Model disagrees with its own earlier answer in same run

โšก Behavioral Signals

  • Tool not called when it should have been
  • Wrong tool called (model "guessed" instead of searched)
  • Agent skips a required step (missing node in trace)
  • State schema violations mid-run
  • Human override rate spikes (humans keep rejecting output)

๐Ÿ”ง Detection Methods

  • LLM-as-judge โ€” a separate model scores output quality
  • Retrieval faithfulness โ€” did output match retrieved docs?
  • Consistency check โ€” run same input 3x; compare outputs
  • Fact verification โ€” cross-check claims against knowledge base
  • Human feedback โ€” thumbs up/down from end users

๐Ÿ”” Anomaly Detection โ€” What to Alert On

๐Ÿšจ
Latency spike โ€” p95 latency > 2x your p50 baseline
Could mean: model under load, context window bloat, external API slowdown
๐Ÿšจ
Cost spike โ€” cost per run > 5x baseline
Could mean: loop isn't terminating, context window growing unbounded
โš ๏ธ
Error rate increase โ€” >5% of runs failing
Could mean: external API down, schema mismatch, rate limiting
โš ๏ธ
Token usage creep โ€” avg tokens growing week-over-week
Could mean: context accumulation bug, prompt getting longer
โš ๏ธ
Quality score drop โ€” LLM judge scores falling
Could mean: model drift, prompt regression, new edge cases
๐Ÿšจ
Infinite loop detected โ€” graph iterations > max_iterations
Add a hard cap in state and alert immediately
๐Ÿ’ฐ Cost, Tokens & Optimization
Unoptimized agents can spend 10x more than necessary. Here's how to track and reduce costs.

๐Ÿงฎ Token Cost Breakdown โ€” Where Money Goes

System prompt (always sent)~20โ€“35%
Conversation history / context~25โ€“40%
Tool outputs injected into prompt~15โ€“30%
Actual user query~5โ€“10%
Model output (completion tokens)~10โ€“20%
๐Ÿ’ก
Note: completion tokens are typically priced 3โ€“5x higher than input tokens on most models. Long outputs are disproportionately expensive.

๐Ÿ“Š What to Track Per Run

  • โ†’
    Prompt tokens โ€” input going into the model
  • โ†’
    Completion tokens โ€” tokens in the model's output
  • โ†’
    Total cost per run โ€” link to user/feature/team
  • โ†’
    Cost per successful outcome โ€” not just per call
  • โ†’
    Cache hit ratio โ€” prompt caching can save 80%+
  • โ†’
    Model distribution โ€” which % of calls use expensive models

โœ‚๏ธ Top Cost Reduction Tactics

  • โœ“
    Prompt caching โ€” cache static system prompts (Anthropic/OpenAI both support)
  • โœ“
    Model routing โ€” use cheap models (Haiku, GPT-4o-mini) for simple nodes
  • โœ“
    Truncate tool outputs โ€” summarize before injecting into LLM context
  • โœ“
    Shorten system prompt โ€” audit regularly; trim unused instructions
  • โœ“
    Add max_iterations โ€” hard stop on loops before they explode costs
  • โœ“
    Message window trimming โ€” don't send full history every time

๐Ÿท๏ธ Cost Attribution โ€” Track By Dimension

๐Ÿ‘ค
By User
Identify power users; enable user-level rate limiting
๐Ÿท๏ธ
By Feature
Know which feature costs most; prioritize optimization
๐Ÿค–
By Model
Justify upgrade to GPT-4o vs Haiku with data
# Tag your LangSmith runs for cost attribution config = { "metadata": { "user_id": "user_abc", "feature": "document_summarizer", "env": "production" } } graph.invoke(input, config=config)
๐Ÿงช

Lab 1 โ€” Live Agent Telemetry (runs in browser via Anthropic API)

Edit the prompt or system instructions, hit โ–ถ Run Agent, and watch real telemetry data appear: token counts, latency per step, cost estimate, and auto-flagged anomalies. This simulates what LangSmith/Langfuse shows you in production.

โš™๏ธ Agent Configuration

Watch telemetry update in real time โ†’
--
Latency (ms)
--
Prompt Tokens
--
Completion Tokens
--
Est. Cost
๐Ÿ“Š Live Telemetry Stream idle
Telemetry will appear here when you run the agentโ€ฆ Each line = one telemetry event (like what LangSmith captures): โ€ข span_start โ€” node begins execution โ€ข llm_call โ€” model invoked, prompt/completion tokens logged โ€ข span_end โ€” node complete, latency computed โ€ข anomaly_check โ€” automated flag if metrics exceed thresholds
๐Ÿ’ฌ Agent Output
Agent response will appear hereโ€ฆ

๐Ÿง  What This Is Teaching You

๐Ÿ”ฌ

Lab 2 โ€” Multi-Step Agent Trace Inspector

Simulates a 3-node LangGraph agent (parse โ†’ research โ†’ synthesize). Each node calls the real API. After all nodes run, a visual trace tree is built โ€” exactly what you'd see in LangSmith. Hover nodes to inspect inputs/outputs/tokens per step.

๐ŸŒณ Live Trace Tree (builds as nodes execute)

Run the pipeline to see the trace build step by stepโ€ฆ

๐Ÿ“Š Per-Node Metrics

Run pipeline to populateโ€ฆ

๐Ÿ“‹ Node Outputs (Collapsed)

Run pipeline to populateโ€ฆ

๐Ÿ’ก What Real LangSmith Shows You for This

What you see in LangSmithWhat it tells youWhat to act on
Trace tree with nested spansWhich node consumed most timeOptimize slowest node first
Token count per spanWhich node uses most contextTrim context at that node
Input/output at each spanWhether data flows correctlyDebug wrong outputs node by node
Error spans (red)Which node failed and whyAdd retry/fallback at that node
Latency waterfallSequential vs parallel bottleneckParallelize independent nodes
๐Ÿ›ก๏ธ

Lab 3 โ€” Hallucination Detector (LLM-as-Judge)

The standard production pattern: one model generates an answer, a judge model evaluates it for faithfulness, groundedness, and confidence. This is what Langfuse evals and LangSmith evaluators run automatically on your traces.

๐Ÿ“ The Judge Prompt Pattern โ€” Copy This for Your Pipeline

""" HALLUCINATION EVALUATION PROMPT (LLM-as-Judge pattern) Use this as your evaluator system prompt in LangSmith/Langfuse evals """ JUDGE_PROMPT = """You are an expert evaluator assessing AI-generated answers for hallucinations. Given a REFERENCE document and an ANSWER, evaluate on 4 dimensions (score 0-10 each): 1. FAITHFULNESS โ€” Does every claim in the answer appear in the reference? (10 = fully grounded, 0 = completely fabricated) 2. FACTUAL_ACCURACY โ€” Are specific facts (names, dates, versions) correct? (10 = all correct, 0 = multiple factual errors) 3. HALLUCINATION_RISK โ€” How likely is the answer to mislead a user? (10 = very safe, 0 = dangerous misinformation) 4. CONFIDENCE_CALIBRATION โ€” Does the model express appropriate uncertainty? (10 = perfectly calibrated, 0 = overconfident on wrong claims) Respond ONLY in JSON: { "faithfulness": 0-10, "factual_accuracy": 0-10, "hallucination_risk": 0-10, "confidence_calibration": 0-10, "issues": ["list of specific problems found"], "verdict": "PASS|WARN|FAIL", "reasoning": "one paragraph explanation" }""" # In LangSmith โ€” attach as a run evaluator: from langsmith.evaluation import evaluate results = evaluate( target=your_agent, data="your-dataset", evaluators=[hallucination_judge], experiment_prefix="prod-v1" )
๐Ÿ’ธ

Lab 4 โ€” Cost & Token Optimizer

Run the same query with different configurations and see real cost/token differences. Understand the impact of system prompt length, model choice, and context size on your bill.

Config A โ€” Bloated
Long verbose system prompt (500+ tokens), full message history, no constraints
Config B โ€” Optimized
Concise system prompt (~80 tokens), trimmed context, max_tokens cap set
Config C โ€” Minimal
No system prompt, direct query only, strict output limit

๐Ÿงฎ Real Pricing Reference (as of 2025) โ€” Use in Your Budget Planning

ModelInput ($/1M tokens)Output ($/1M tokens)Best For
claude-haiku-4-5$0.80$4.00Simple nodes, routing, classification
claude-sonnet-4-6$3.00$15.00Main reasoning nodes, synthesis
claude-opus-4-6$15.00$75.00Only for highest-stakes decisions
gpt-4o-mini$0.15$0.60High-volume, low-complexity tasks
gpt-4o$2.50$10.00Complex reasoning, tool use
๐Ÿ’ก
Routing strategy: Use a cheap model (Haiku / gpt-4o-mini) to classify the query first, then route to the expensive model only when needed. This alone can cut costs by 60โ€“80%.
๐Ÿ”

Lab 5 โ€” Human-in-the-Loop Simulator

Walk through the full HITL cycle interactively. The agent generates a draft, pauses for your approval, you can edit the state, then the agent resumes and synthesizes the final output. This mirrors exactly what LangGraph's interrupt_before pattern does.

Step 1 โ€” Agent Generates a Draft (then pauses for approval)

๐Ÿ—บ๏ธ HITL State Machine โ€” Where You Are

๐Ÿ“ฅ Input
โ†’
๐Ÿค– Draft Node
โ†’
โธ INTERRUPT
โ†’
๐Ÿ™‹ Human Review
โ†’
๐Ÿ”„ Resume Node
โ†’
๐Ÿ“ค Output
๐Ÿ“‹ Copy-Paste Snippets โ€” Production Ready
Battle-tested patterns for your LangGraph telemetry project. Each snippet is self-contained and ready to drop into your codebase.
๐Ÿ”ญ LangSmith Auto-Trace Setup
import os
from langchain_core.tracers import LangChainTracer

# Method 1: env vars (zero code change)
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls__your_key"
os.environ["LANGCHAIN_PROJECT"] = "my-agent-prod"

# Method 2: explicit tracer with metadata
tracer = LangChainTracer(
    project_name="my-agent-prod",
    tags=["production", "v2"],
    metadata={
        "team": "platform",
        "version": "2.1.0"
    }
)
config = {"callbacks": [tracer]}
result = graph.invoke(input, config=config)
๐ŸŸข Langfuse Setup (v3 / OTEL)
from langfuse import Langfuse
from langfuse.callback import CallbackHandler

# Init client
langfuse = Langfuse(
    public_key="pk-lf-...",
    secret_key="sk-lf-...",
    host="https://cloud.langfuse.com"  # or self-hosted
)

# Create handler with session/user context
handler = CallbackHandler(
    session_id="session_abc",   # group runs by session
    user_id="user_123",         # attribute cost to user
    trace_name="research-agent",
    tags=["prod", "v1"],
    metadata={"feature": "doc_summarizer"}
)

config = {"callbacks": [handler]}
result = graph.invoke(input, config=config)

# Flush before process exits (important!)
langfuse.flush()
๐Ÿ’พ Persistent Checkpointer (Postgres)
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.graph import StateGraph, END
import psycopg

DB_URI = "postgresql://user:pass@localhost/mydb"

# Setup checkpointer
with psycopg.connect(DB_URI) as conn:
    checkpointer = PostgresSaver(conn)
    checkpointer.setup()  # creates tables on first run

# Compile with checkpointer
graph = workflow.compile(
    checkpointer=checkpointer,
    interrupt_before=["approval_node"]  # HITL breakpoint
)

# Each run needs a unique thread_id
config = {"configurable": {"thread_id": "user_abc_run_001"}}

# Run โ€” state auto-saved after each node
result = graph.invoke({"messages": [...]}, config=config)

# Resume later (after human approval)
graph.invoke(None, config=config)  # None = resume from checkpoint

# Time travel: inspect history
history = list(graph.get_state_history(config))
for state in history:
    print(f"Step {state.metadata['step']}: {state.next}")
๐Ÿšจ Anomaly Detection Wrapper
import time
from dataclasses import dataclass, field
from typing import Dict, List

@dataclass
class TelemetrySpan:
    name: str
    start: float = field(default_factory=time.time)
    prompt_tokens: int = 0
    completion_tokens: int = 0
    latency_ms: float = 0
    status: str = "running"

class AnomalyDetector:
    BASELINES = {
        "latency_ms": 2000,    # alert if > 2x this
        "prompt_tokens": 8000, # alert if > this
        "cost_usd": 0.01       # alert if > this per run
    }
    COST_PER_1K = {"input": 0.003, "output": 0.015}  # adjust per model

    def __init__(self):
        self.spans: List[TelemetrySpan] = []
        self.alerts: List[str] = []

    def start_span(self, name: str) -> TelemetrySpan:
        span = TelemetrySpan(name=name)
        self.spans.append(span)
        return span

    def end_span(self, span: TelemetrySpan, prompt_tok: int, comp_tok: int):
        span.latency_ms = (time.time() - span.start) * 1000
        span.prompt_tokens = prompt_tok
        span.completion_tokens = comp_tok
        span.status = "done"
        self._check_anomalies(span)

    def _check_anomalies(self, span: TelemetrySpan):
        if span.latency_ms > self.BASELINES["latency_ms"] * 2:
            self.alerts.append(
                f"๐Ÿšจ [{span.name}] Latency spike: {span.latency_ms:.0f}ms"
            )
        if span.prompt_tokens > self.BASELINES["prompt_tokens"]:
            self.alerts.append(
                f"โš ๏ธ [{span.name}] Large context: {span.prompt_tokens} tokens"
            )
        cost = (span.prompt_tokens / 1000 * self.COST_PER_1K["input"] +
                span.completion_tokens / 1000 * self.COST_PER_1K["output"])
        if cost > self.BASELINES["cost_usd"]:
            self.alerts.append(f"๐Ÿ’ธ [{span.name}] High cost: ${cost:.4f}")

    def report(self) -> Dict:
        total_cost = sum(
            (s.prompt_tokens/1000 * self.COST_PER_1K["input"] +
             s.completion_tokens/1000 * self.COST_PER_1K["output"])
            for s in self.spans
        )
        return {
            "spans": len(self.spans),
            "total_latency_ms": sum(s.latency_ms for s in self.spans),
            "total_tokens": sum(s.prompt_tokens + s.completion_tokens
                                for s in self.spans),
            "total_cost_usd": round(total_cost, 6),
            "alerts": self.alerts
        }
๐Ÿ›ก๏ธ LLM-as-Judge Evaluator
import json
from anthropic import Anthropic

client = Anthropic()

JUDGE_SYSTEM = """You evaluate AI answers for hallucinations.
Respond ONLY in JSON with keys:
faithfulness (0-10), factual_accuracy (0-10),
hallucination_risk (0-10), verdict (PASS/WARN/FAIL),
issues (list), reasoning (string)"""

def hallucination_check(reference: str, answer: str) -> dict:
    """
    Run LLM-as-judge evaluation.
    Use this in your LangSmith/Langfuse eval pipeline.
    """
    resp = client.messages.create(
        model="claude-haiku-4-5",  # cheap model for evals!
        max_tokens=500,
        system=JUDGE_SYSTEM,
        messages=[{
            "role": "user",
            "content": f"REFERENCE:\n{reference}\n\nANSWER TO EVALUATE:\n{answer}"
        }]
    )
    raw = resp.content[0].text
    try:
        return json.loads(raw)
    except:
        return {"verdict": "ERROR", "reasoning": raw}

# Hook into LangSmith as a run evaluator:
def langsmith_evaluator(run, example) -> dict:
    result = hallucination_check(
        reference=example.outputs["expected"],
        answer=run.outputs["output"]
    )
    return {
        "key": "hallucination_score",
        "score": result.get("faithfulness", 0) / 10,
        "comment": result.get("reasoning", "")
    }
๐Ÿ” HITL with State Edit Pattern
from langgraph.graph import StateGraph, END
from langgraph.types import interrupt
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    draft: str
    approved: bool
    human_notes: str

def draft_node(state: AgentState) -> AgentState:
    """Node 1: Generate draft content"""
    # ... call LLM to generate draft
    return {"draft": "Generated draft here..."}

def review_node(state: AgentState) -> AgentState:
    """Node 2: HITL interrupt โ€” waits for human"""
    # This pauses execution and saves checkpoint
    human_decision = interrupt({
        "draft": state["draft"],
        "question": "Please review this draft. Approve, edit, or reject."
    })
    return {
        "approved": human_decision.get("approved", False),
        "human_notes": human_decision.get("notes", ""),
        "draft": human_decision.get("edited_draft", state["draft"])
    }

def finalize_node(state: AgentState) -> AgentState:
    """Node 3: Only runs after human approval"""
    if not state["approved"]:
        return {"messages": [{"role": "assistant",
                              "content": "Draft rejected. Starting over."}]}
    # ... finalize with human notes incorporated
    return {"messages": [{"role": "assistant",
                          "content": f"Finalized: {state['draft']}"}]}

# Build graph
workflow = StateGraph(AgentState)
workflow.add_node("draft", draft_node)
workflow.add_node("review", review_node)
workflow.add_node("finalize", finalize_node)
workflow.set_entry_point("draft")
workflow.add_edge("draft", "review")
workflow.add_edge("review", "finalize")
workflow.add_edge("finalize", END)

graph = workflow.compile(
    checkpointer=checkpointer,
    interrupt_before=["review"]  # pause BEFORE review node
)

# Run 1: executes until interrupt
config = {"configurable": {"thread_id": "run_001"}}
graph.invoke({"messages": []}, config)

# Later: resume with human input
graph.invoke(
    {"approved": True, "human_notes": "Looks good, just shorter please"},
    config
)
๐Ÿ“ˆ Cost Attribution & Budget Alerts
from langfuse import Langfuse
from functools import wraps
import time

langfuse = Langfuse()

# Cost rates per model (update as pricing changes)
MODEL_COSTS = {
    "claude-haiku-4-5":   {"in": 0.0008, "out": 0.004},
    "claude-sonnet-4-6":  {"in": 0.003,  "out": 0.015},
    "claude-opus-4-6":    {"in": 0.015,  "out": 0.075},
}

BUDGET_ALERTS = {
    "per_run_usd": 0.05,      # alert if single run > $0.05
    "per_user_daily_usd": 1.00, # alert if user > $1/day
}

def traced_llm_call(model: str, feature: str, user_id: str):
    """Decorator to auto-trace any LLM call with cost tracking"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            trace = langfuse.trace(
                name=func.__name__,
                metadata={
                    "feature": feature,
                    "user_id": user_id,
                    "model": model
                }
            )
            span = trace.span(name="llm_call", start_time=time.time())
            try:
                result = func(*args, **kwargs)
                # Extract usage from response
                usage = result.usage
                cost = (
                    usage.input_tokens * MODEL_COSTS[model]["in"] / 1000 +
                    usage.output_tokens * MODEL_COSTS[model]["out"] / 1000
                )
                span.end(
                    output=result.content[0].text,
                    usage={
                        "input": usage.input_tokens,
                        "output": usage.output_tokens,
                        "unit": "TOKENS"
                    },
                    metadata={"cost_usd": cost}
                )
                if cost > BUDGET_ALERTS["per_run_usd"]:
                    trace.event(
                        name="budget_alert",
                        level="WARNING",
                        metadata={"cost": cost, "threshold": BUDGET_ALERTS["per_run_usd"]}
                    )
                return result
            except Exception as e:
                span.end(level="ERROR", status_message=str(e))
                raise
        return wrapper
    return decorator
๐Ÿ” Streaming Debug Mode (LangGraph)
import json
from datetime import datetime

def stream_with_telemetry(graph, input_data, config):
    """
    Stream a LangGraph run with live telemetry logging.
    Shows each node as it executes โ€” great for debugging.
    """
    run_log = []
    start = datetime.now()

    print(f"\n{'='*50}")
    print(f"๐Ÿš€ Run started: {start.strftime('%H:%M:%S.%f')[:-3]}")
    print(f"{'='*50}")

    for chunk in graph.stream(input_data, config, stream_mode="all"):
        ts = (datetime.now() - start).total_seconds()

        if isinstance(chunk, tuple):
            event_type, data = chunk

            if event_type == "updates":
                for node, updates in data.items():
                    print(f"\n[{ts:.2f}s] ๐Ÿ“ฆ NODE: {node}")
                    # Log state changes
                    for key, val in updates.items():
                        if key == "messages":
                            print(f"  messages: +{len(val)} new message(s)")
                        else:
                            val_str = str(val)[:80] + "..." if len(str(val)) > 80 else val
                            print(f"  {key}: {val_str}")
                    run_log.append({
                        "ts": ts, "node": node, "updates": list(updates.keys())
                    })

            elif event_type == "debug":
                if data.get("type") == "checkpoint":
                    print(f"[{ts:.2f}s] ๐Ÿ’พ CHECKPOINT saved (step {data.get('step', '?')})")

    print(f"\n{'='*50}")
    print(f"โœ… Run complete in {ts:.2f}s | {len(run_log)} nodes executed")
    print(json.dumps({"timeline": run_log}, indent=2))
    return run_log

# Usage:
# run_log = stream_with_telemetry(graph, {"messages": [...]}, config)