Cracking ML Interviews

Open in the interactive study hub

LangSmith: LLM Observability & Evaluation Platform

What is LangSmith?

LangSmith is Anthropic/LangChain's platform for tracing, debugging, testing, and evaluating LLM applications. It provides end-to-end visibility into every LLM call, chain execution, and agent decision: critical for production AI systems.

  Your App → LangSmith SDK → LangSmith Platform
               (trace)          ↓
                          [Traces Dashboard]
                          [Evaluation Runs]
                          [Prompt Playground]
                          [Datasets & Evals]

Core Capabilities

Capability What It Does
Tracing Record every LLM call, tool use, chain step with inputs/outputs/latency
Debugging Inspect failed runs, see exact prompts sent, token counts
Datasets Curate golden datasets of (input, expected_output) pairs
Evaluations Run automated evals: LLM-as-judge, exact match, custom metrics
Prompt Hub Version-controlled prompt storage and deployment
A/B Testing Compare prompt versions on the same dataset
Monitoring Production dashboards for latency, cost, error rates

Setup

# Install
# pip install langsmith langchain-anthropic

import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "your-api-key"
os.environ["LANGSMITH_PROJECT"] = "my-production-app"  # Project for grouping traces
# (older LANGCHAIN_TRACING_V2 / LANGCHAIN_API_KEY / LANGCHAIN_PROJECT names still work)

# That's it: all LangChain/LangGraph calls are auto-traced
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-5")
result = llm.invoke("Explain RAG in 3 sentences")
# This call now appears in LangSmith dashboard

Manual Tracing (Non-LangChain Code)

from langsmith import traceable, Client

client = Client()

@traceable(name="custom-retrieval", tags=["rag", "production"])
def retrieve_documents(query: str, top_k: int = 5) -> list[dict]:
    """Any function decorated with @traceable is auto-traced"""
    # Your retrieval logic here
    results = vector_db.search(query, top_k=top_k)
    return results

@traceable(name="rag-pipeline")
def run_rag(question: str) -> str:
    docs = retrieve_documents(question)
    context = "\n".join([d["text"] for d in docs])
    response = llm.invoke(f"Context: {context}\n\nQuestion: {question}")
    return response.text  # text blocks only (.content can be a list when the model thinks)

# Nested traces are automatically linked as parent → child
answer = run_rag("What is RLHF?")

Dataset Creation & Evaluation

Create a Dataset

from langsmith import Client

client = Client()

# Create dataset from examples
examples = [
    {"input": {"question": "What is RAG?"},
     "output": {"answer": "RAG combines retrieval with generation..."}},
    {"input": {"question": "Explain RLHF"},
     "output": {"answer": "RLHF uses human feedback to fine-tune LLMs..."}},
]

dataset = client.create_dataset(
    dataset_name="ML Interview QA",
    description="Golden set for ML interview Q&A evaluation"
)

client.create_examples(
    inputs=[e["input"] for e in examples],
    outputs=[e["output"] for e in examples],
    dataset_id=dataset.id
)

Run Evaluation

from langsmith.evaluation import evaluate, LangChainStringEvaluator

# The function to evaluate
def my_rag_app(inputs: dict) -> dict:
    answer = run_rag(inputs["question"])
    return {"answer": answer}

# Evaluators
correctness_evaluator = LangChainStringEvaluator(
    "qa",  # Uses LLM to judge correctness
    config={"llm": ChatAnthropic(model="claude-opus-4-6")}
)

results = evaluate(
    my_rag_app,
    data="ML Interview QA",           # Dataset name
    evaluators=[correctness_evaluator],
    experiment_prefix="rag-v2-test",  # For comparison in UI
    num_repetitions=1                 # Run each example N times
)

print(results.to_pandas())  # DataFrame with scores per example

LLM-as-Judge Evaluators

from langsmith.evaluation import evaluate
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field

class JudgeResult(BaseModel):
    score: int = Field(description="Accuracy score from 1 to 5")
    reasoning: str = Field(description="Why this score was given")

# Custom LLM-as-judge evaluator
judge_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are an expert evaluator. Score the answer 1-5 for accuracy."),
    ("human", """
Question: {input}
Expected: {reference}
Actual: {prediction}
""")
])

def custom_judge(inputs: dict, outputs: dict, reference_outputs: dict) -> dict:
    # Structured output instead of json.loads(response.content): current Claude
    # models return thinking blocks, so .content is a list, not a JSON string.
    judge_llm = ChatAnthropic(model="claude-opus-5-5").with_structured_output(
        JudgeResult, method="json_schema"  # native structured outputs, no forced tool call
    )
    result = judge_llm.invoke(judge_prompt.format_messages(
        input=inputs["question"],
        reference=reference_outputs["answer"],
        prediction=outputs["answer"]
    ))
    return {"score": result.score / 5.0, "comment": result.reasoning}

results = evaluate(
    my_rag_app,
    data="ML Interview QA",
    evaluators=[custom_judge],
    experiment_prefix="claude-judge-v1"
)

Prompt Hub: Version-Controlled Prompts

from langsmith import Client

client = Client()

# Pull a prompt from the LangSmith prompt hub
prompt = client.pull_prompt("rag-qa-prompt:v3")  # Pin to a tag or commit

# Use it
chain = prompt | llm
result = chain.invoke({"context": "...", "question": "..."})

# Push updated prompt (creates a new commit)
client.push_prompt("rag-qa-prompt", object=updated_prompt)

# Older code used `from langchain import hub`; in LangChain 1.0 that moved to langchain-classic.

Production Monitoring

LangSmith captures these metrics automatically per project:

Metric Description
Latency P50/P95/P99 End-to-end and per-node latency
Token usage Input + output tokens per call
Cost Estimated cost based on model pricing
Error rate % of runs that raised exceptions
Feedback scores User thumbs up/down or custom ratings

Attaching User Feedback

from langsmith import Client

client = Client()

# After getting user feedback on a response
run_id = "..."  # From trace

client.create_feedback(
    run_id=run_id,
    key="user_rating",
    score=0.8,  # 0-1 scale
    comment="Good answer but missing implementation detail"
)

Regression Testing Workflow

# CI/CD pipeline integration
# 1. Add new test cases when bugs are found
# 2. Run eval suite on each PR
# 3. Block merge if eval score drops > 5%

import subprocess

def run_eval_gate(threshold: float = 0.85):
    results = evaluate(
        my_rag_app,
        data="ML Interview QA - Regression",
        evaluators=[correctness_evaluator]
    )

    avg_score = results.to_pandas()["feedback.qa"].mean()

    if avg_score < threshold:
        raise ValueError(f"Eval score {avg_score:.2f} below threshold {threshold}")

    print(f"Eval passed: {avg_score:.2f} >= {threshold}")

# In GitHub Actions:
# - run: python -c "from eval import run_eval_gate; run_eval_gate(0.85)"

Tracing Architecture

Request comes in
       │
       ▼
  [Root Run]  ← LangSmith traces this
  ├── [LLM Call 1]  {prompt, response, latency, tokens}
  ├── [Tool: search_web]  {input, output, duration}
  │   └── [HTTP Request]  {url, status, latency}
  ├── [LLM Call 2]  {prompt, response}
  └── [Output Parser]  {raw, parsed}
       │
       ▼
   Response returned

LangSmith vs Alternatives

Platform Strength Weakness
LangSmith Deep LangChain integration, evals, datasets Paid for production scale
Weights & Biases ML experiment tracking, sweeps Less LLM-native
Arize AI Enterprise ML monitoring, drift Complex setup
Helicone Simple proxy-based tracing Less eval tooling
Literal AI Open source, self-hostable Smaller ecosystem
Phoenix (Arize) Open source observability Less eval automation

Interview Questions

Q: Why is LangSmith important in production LLM systems?

Without observability, LLM apps are black boxes. LangSmith traces every call, showing exact prompts sent, tokens used, latency, and errors. This is essential for debugging hallucinations, optimizing costs, and catching regressions when prompts or models change.

Q: How do you evaluate whether a RAG system improved after a change?

Create a golden dataset with (question, expected_answer) pairs. Run the old and new system on the same dataset with LLM-as-judge evaluators. Compare scores in LangSmith's experiment comparison view. Block deployment if score drops below threshold.

Q: What's the difference between online and offline evaluation?

Offline: evaluate on a fixed dataset before deployment (regression tests, benchmarks). Online: collect real user feedback and monitor metrics in production (thumbs up/down, implicit signals like follow-up questions). Both are needed: offline catches regressions, online catches real-world failures.

Q: How would you implement regression testing for an LLM application?

Curate a golden dataset of critical test cases. Integrate evaluate() into CI/CD. Set a score threshold (e.g., 85%). Add new test cases whenever a production bug is found (golden negative examples). This creates a growing safety net.

Q: What is LLM-as-judge and what are its limitations?

Using a powerful LLM (like Claude Opus) to score another LLM's output. Limitations: positional bias (judges prefer first option), verbosity bias (longer = better), self-consistency issues, and cost. Mitigate with structured scoring rubrics, multiple judge calls, and human validation on a sample.

View or edit this guide on GitHub