These tools solve different layers of an LLM application stack, and a lot of interview confusion comes from treating them as competitors rather than complements. A production RAG system commonly uses several at once.
Tool
Layer
Core job
LangChain
Orchestration
Chains LLM calls, tools, and prompts together; broad integration ecosystem
LangGraph
Orchestration (stateful)
Graph-based control flow for agents — cycles, branching, persistence, human-in-the-loop
LlamaIndex
Data / retrieval
Ingests, indexes, and retrieves data for RAG; deep indexing strategies
Ollama
Inference / serving
Runs open-source LLMs locally, exposes a local API endpoint
A realistic stack: LlamaIndex handles ingestion and chunking of documents into a vector store (which could be pgvector or Pinecone, from the prior guide), LangGraph orchestrates a multi-step agent that decides when to retrieve vs. call a tool vs. answer, and Ollama serves the model itself locally for dev/test or cost-sensitive environments, with LangChain components used as connective tissue throughout.
LangChain
general orchestration, huge integration surface
LangGraph
stateful, cyclic agent workflows
LlamaIndex
retrieval-first, deep data connectors
Ollama
local model serving, no cloud dependency
LangChain
LangChain is a general-purpose orchestration framework for building LLM applications: chaining prompts, models, tools, memory, and output parsers into a pipeline. It popularized the "chain" abstraction and now also ships LCEL (LangChain Expression Language) for composing components declaratively.
Tool-using agents that call APIs, run code, or query a database based on the model's decisions
Standardized integrations across model providers, vector stores, and document loaders without rewriting glue code per provider
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template(
"Answer using only this context:\n{context}\n\nQuestion: {question}"
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | StrOutputParser()
answer = chain.invoke({"context": retrieved_docs, "question": user_query})
LCEL gives composable, declarative chains with built-in streaming and async support
Large community, extensive examples, fast to prototype with
Works well for linear or moderately branching pipelines
Disadvantages
Abstraction layers can obscure what's actually happening under the hood, complicating debugging
History of breaking changes across versions; pinning versions matters in production
Not designed for complex stateful control flow with cycles — that's LangGraph's job
Can be heavier than needed for a simple single LLM call
LangGraph
LangGraph extends the LangChain ecosystem with a graph-based execution model purpose-built for agents: nodes are steps (often LLM calls or tool calls), edges define transitions, and the graph can branch, loop, and persist state across steps — including pausing for human approval mid-execution.
What you can actually build
A multi-step agent that retrieves, evaluates whether the answer is sufficient, and loops back to retrieve again if not — a cycle a plain linear chain can't express cleanly
Human-in-the-loop workflows where the graph pauses and waits for approval before a sensitive action (e.g. sending an email, executing a transaction)
Multi-agent systems where different nodes represent different specialized agents collaborating on a task
Durable execution — state checkpoints so a long-running workflow can resume after a failure
Natively expresses cycles, branching, and retries — what real agent behavior needs
Built-in state persistence/checkpointing for long-running or resumable workflows
Explicit graph structure makes complex control flow easier to reason about and debug than nested chains
Supports human-in-the-loop interrupts natively
Disadvantages
Steeper learning curve than a simple LangChain chain; more upfront design needed
Overkill for simple, linear pipelines — added complexity with no payoff
Still a fairly young framework relative to LangChain core; patterns and best practices are still settling
Debugging graph state across many nodes can get complex at scale
LlamaIndex
LlamaIndex is a data framework focused on the ingestion, indexing, and retrieval side of RAG. Where LangChain is broad orchestration, LlamaIndex goes deep on how to structure and query your own data: chunking strategies, multiple index types, and a large library of data connectors.
What you can actually build
A document ingestion pipeline pulling from PDFs, databases, Slack, Notion, etc. via built-in connectors (LlamaHub)
Multiple index strategies beyond flat vector search — tree indexes for hierarchical summarization, keyword indexes, knowledge graph indexes
Query engines with automatic query routing — deciding which index or retrieval strategy fits a given question
Sub-question decomposition: breaking one complex query into several smaller retrievals and synthesizing the results
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=5)
response = query_engine.query("What are the EDI 277CA status codes?")
Advantages
Strongest ecosystem specifically for data ingestion and retrieval strategy
Multiple index types beyond flat vector search, useful for structured or hierarchical content
Query routing and sub-question decomposition handle complex queries better than naive single-shot retrieval
Large connector library (LlamaHub) for pulling in varied data sources
Disadvantages
Less general-purpose for agent orchestration than LangChain/LangGraph — often paired with one of them for control flow
Some overlap in functionality with LangChain causes confusion about which tool owns which responsibility
Advanced index types add complexity and cost (more LLM calls for tree/hierarchical indexing)
Smaller integration surface for non-retrieval tasks like tool-calling agents
Ollama
Ollama packages open-source LLMs (Llama, Mistral, Gemma, and others) for easy local execution. It exposes a local REST API compatible-ish with common conventions, handles model downloading/quantization, and removes the need to send data to a third-party API for inference.
What you can actually build
A fully local RAG pipeline with no data leaving your machine or network — relevant for HIPAA-sensitive prototyping
Cost-free development and testing loops without burning API credits
On-prem or air-gapped deployments where cloud LLM APIs aren't an option
Easy local model swapping to compare outputs across open-source models before committing to a cloud provider
# pull and run a model locally
ollama pull llama3.1
ollama run llama3.1
# call it like an API from your application
curl http://localhost:11434/api/generate -d '{
"model": "llama3.1",
"prompt": "Summarize this EDI 277CA response."
}'
Advantages
Data never leaves the local machine/network — strong fit for compliance-sensitive testing
No per-token API cost; good for heavy dev-loop iteration
Simple setup; handles quantization and model management for you
Works offline; no dependency on external API uptime
Disadvantages
Local hardware is the ceiling — open models on consumer hardware generally trail frontier hosted models on complex reasoning
You own scaling, GPU provisioning, and uptime if used in production rather than dev/test
No built-in enterprise SLAs, audit logging, or managed compliance certifications — that's on you to build
Production-grade serving at scale typically needs more than Ollama alone (e.g. vLLM, TGI, or a hosted endpoint)
Ollama and local-model details change quickly; verify current model list, performance, and licensing against Ollama's own docs before relying on specifics in an interview.
Tool Calling & Function Calling
Tool calling lets an LLM decide at runtime to invoke an external function rather than generating a text answer directly. The model outputs a structured request — call lookup_claim_status with claim_id=CLM123 — your code executes it, feeds the result back, and the model uses it to generate a final answer. This is the bridge between an LLM and the real world.
User query
→
LLM: answer or call tool?
→
Structured call emitted
→
Code executes function
→
Result fed back
→
Final answer
LangChain tool definition
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
@tool
def lookup_claim_status(claim_id: str) -> dict:
"""Look up the current status of an insurance claim."""
return db.query("SELECT status FROM claims WHERE id=%s", claim_id)
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools([lookup_claim_status])
# Model decides which tool to call
response = llm_with_tools.invoke("What is the status of claim CLM-9821?")
Structured outputs with Pydantic
from pydantic import BaseModel
class ClaimExtraction(BaseModel):
claim_id: str
patient_name: str
diagnosis_code: str
amount: float
structured_llm = ChatOpenAI(model="gpt-4o-mini").with_structured_output(ClaimExtraction)
result = structured_llm.invoke(
"Extract: Patient John Doe, claim CLM-991, $2400, ICD-10 Z00.00"
)
# result.claim_id == "CLM-991", result.amount == 2400.0
Tool call vs structured output — when to use which
Pattern
When to use
Example
Tool / function call
Model needs to fetch data or trigger an action with side effects
Query a DB, call an API, send an email
Structured output / JSON mode
Model should return data in a fixed schema — no side effects
Classify a ticket, extract named entities
Response format (Pydantic)
Enforce schema on output with validation and type-checking
Extract structured patient data from clinical notes
Best Practices
Write clear, specific docstrings — the model reads them to decide when to call each tool
Always validate tool outputs before feeding back to the model
Log every tool call and result for debugging and audit
Scope permissions tightly — tools should only do what the task requires
Common Failure Modes
Model calls the wrong tool due to ambiguous docstrings
Tool returns an error and model hallucinates a plausible result instead
Infinite tool-call loops when the model cannot satisfy a query
Schema mismatch — model passes wrong argument types
Missing error handling — tool exceptions propagate silently into context
Memory & State Management
LLMs are stateless by default — each API call has no knowledge of previous ones. Memory in LLM frameworks is the engineering work of deciding what context to maintain across turns, how to store it, and how much to inject into each prompt. The wrong strategy either wastes tokens on irrelevant history or loses critical context mid-conversation.
The four memory types
Buffer Memory
Keep all conversation turns verbatim. Simple and accurate but token cost grows linearly.
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(return_messages=True)
Window Memory
Keep only the last K turns. Caps token cost but older context is lost entirely.
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(k=5, return_messages=True)
Summary Memory
Periodically compress old turns into a running summary via an LLM call. Preserves signal at reduced token cost.
from langchain.memory import ConversationSummaryMemory
memory = ConversationSummaryMemory(llm=ChatOpenAI())
Vector Store Memory
Store all turns as embeddings; retrieve only semantically relevant ones per query. Best for very long histories.
from langchain.memory import VectorStoreRetrieverMemory
memory = VectorStoreRetrieverMemory(
retriever=vectorstore.as_retriever(k=4))
Memory comparison
Type
Token cost
Information loss
Latency
Best for
Buffer
High — grows linearly
None
None
Short conversations
Window
Fixed
Drops old turns
None
Task-focused sessions
Summary
Medium
Summarization loses detail
Extra LLM call
Long conversations needing broad retention
Vector
Low — only relevant
Non-retrieved turns invisible
Retrieval latency
Very long sessions, knowledge-heavy assistants
LangGraph persistent state (checkpointing)
from langgraph.graph import StateGraph
from langgraph.checkpoint.sqlite import SqliteSaver
from typing import TypedDict, List
class AgentState(TypedDict):
messages: List
claim_id: str
retrieval_count: int
approved: bool
checkpointer = SqliteSaver.from_conn_string(":memory:")
graph = StateGraph(AgentState)
app = graph.compile(checkpointer=checkpointer)
# Resume a previous run using its thread_id
config = {"configurable": {"thread_id": "session-42"}}
result = app.invoke({"messages": [...]}, config=config)
Memory Anti-patterns to Avoid
Unlimited buffer growth — token cost spikes unpredictably and hits context limits in long sessions
PHI in vector memory — if conversations contain health data, the vector memory store requires HIPAA controls
Mixing session state with shared state — per-user history should never be shared across users; namespace it explicitly
No eviction strategy — define what gets dropped and when before shipping, not after hitting a context-length error in production
Debugging & Evaluation
LLM framework bugs are different from regular software bugs — the failure is usually not an exception but a subtly wrong answer. Debugging requires visibility into every step: what prompt was sent, what tool was called, what context was injected, and where reasoning went off-track.
LangSmith tracing — one-line setup
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "lsv2_your_key"
os.environ["LANGCHAIN_PROJECT"] = "eaize-rag-prod"
# All LangChain and LangGraph calls now trace automatically.
# Each trace shows: prompt sent, LLM response, tool calls,
# retrieved context, latency and token cost per step.
Trace symptom → diagnosis table
Symptom
Where to look in trace
Likely cause
Wrong or hallucinated answer
Retrieval step — what context was retrieved?
Irrelevant chunks retrieved; model ignoring context
Tool called with wrong args
Tool call step — check schema passed
Ambiguous docstring or missing arg descriptions
High latency
Waterfall — which step is the bottleneck?
Slow retrieval, large context, sequential tool calls
Wrong output format
Final LLM call — was format instruction in prompt?
Missing or late-position format instruction
Agent not terminating
Graph — how many steps before timeout?
Missing termination condition or tool-call loop
DeepEval CI integration
from deepeval import evaluate
from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric
from deepeval.test_case import LLMTestCase
test_cases = [
LLMTestCase(
input="What is the 277CA transaction?",
actual_output=chain.invoke("What is the 277CA transaction?"),
retrieval_context=retrieved_docs
)
]
# Run in CI — fails build if scores drop below threshold
evaluate(test_cases, [
AnswerRelevancyMetric(threshold=0.75),
FaithfulnessMetric(threshold=0.80)
])
Unit Testing Strategy
Test individual chain steps with mocked LLM responses
Test tool functions independently from the LLM call
Test output parsers with known inputs and edge cases
Test LangGraph routing logic with fixed state inputs — don't rely on a live model
Debugging Checklist
Set temperature=0 during debugging — deterministic output is comparable run-to-run
Print the exact prompt string with prompt.format(**inputs) — it often differs from design-time assumptions
Use return_intermediate_steps=True on agents to see every tool call and result
Version prompts in LangSmith Hub or version control — bisect regressions to the exact change
Production-Level Interview Q&A
Click a question to expand the answer.
When would you reach for LangGraph instead of a plain LangChain chain?
When the workflow needs cycles, conditional branching, retries, or persistent state across steps — anything beyond a straight pipeline. A self-correcting RAG agent that re-retrieves when its first answer is graded as insufficient is a cycle, which LCEL chains can't express cleanly but LangGraph models directly as a graph with conditional edges. If the flow is genuinely linear (retrieve → prompt → generate), a plain chain is simpler and sufficient.
How do LangChain and LlamaIndex actually differ, given they both 'do RAG'?
LlamaIndex is data-and-retrieval-first: its strength is ingestion connectors, chunking strategies, and multiple index types (vector, tree, keyword, knowledge graph) plus query routing across them. LangChain is orchestration-first: its strength is chaining together LLM calls, tools, memory, and arbitrary logic, with retrieval as just one node in that chain. In practice teams often use LlamaIndex for the ingestion/retrieval layer and LangChain or LangGraph for the surrounding orchestration and agent logic.
What are the production risks of using Ollama for serving, and how would you mitigate them?
Local/self-hosted serving means you own GPU provisioning, scaling under load, and uptime — there's no managed SLA. Open models on local hardware can also underperform frontier hosted models on complex reasoning tasks. Mitigation: use Ollama for development, testing, and any case where data residency requires fully local inference, but evaluate a dedicated serving layer (vLLM, TGI) or a hosted endpoint for production traffic that needs guaranteed throughput and uptime.
How would you test an agent built with LangGraph?
Test at multiple levels: unit-test individual node functions in isolation (does retrieve() return expected shape given mocked input), test conditional edge logic directly (does the router send the right path given different state), and run end-to-end scenario tests against the compiled graph with known inputs and expected terminal states. For agents with loops, also test that termination conditions actually fire — an infinite or excessively long loop is a common production bug class.
What's a common failure mode when chaining LLM calls (LangChain-style), and how do you catch it?
Error propagation and silent failure: if one step in a chain returns a malformed or unexpected output (e.g. the output parser fails to parse JSON), it can silently produce garbage downstream rather than failing loudly. Mitigation: validate structured outputs with schema enforcement (Pydantic models, output parsers with retry logic), add explicit error handling between chain steps, and log intermediate outputs so you can trace where a bad result originated.
Why would a team choose LlamaIndex's tree index or knowledge graph index over flat vector search?
Flat vector search treats every chunk independently and can miss relationships or fail at summarization tasks across many documents. A tree index builds hierarchical summaries, useful when a query needs synthesis across a large document set rather than retrieval of a few matching chunks. A knowledge graph index captures explicit entity relationships, useful when answers depend on multi-hop reasoning (e.g. 'who reports to the person who approved this claim') that pure semantic similarity handles poorly.
How would you design a system that needs to fall back from a local Ollama model to a cloud model under certain conditions?
Define clear trigger conditions — e.g. local model confidence/quality below a threshold, query complexity exceeding what the local model handles well, or local inference timing out. Implement this as a routing node (natural fit for LangGraph) that tries the local model first and conditionally escalates to a cloud provider, logging which path was taken for cost and quality monitoring. This pattern is also useful for cost control: route simple queries locally, escalate only complex ones to paid APIs.
What's the difference between an 'agent' and a 'chain', and why does that distinction matter in interviews?
A chain follows a fixed, predetermined sequence of steps. An agent uses the LLM itself to decide what to do next — which tool to call, whether to retrieve again, when to stop — making the control flow dynamic rather than fixed. This matters because agents introduce real production risks (unpredictable tool calls, runaway loops, higher latency and cost variance) that a fixed chain doesn't have, and interviewers often probe whether a candidate understands when the added flexibility of an agent is actually justified versus over-engineering a simple chain.
How do you handle versioning and reproducibility across LangChain/LangGraph/LlamaIndex upgrades in production?
Pin exact versions in your dependency files rather than floating ranges, given these frameworks have a history of breaking changes between minor versions. Maintain integration tests against your actual chains/graphs/indexes so an upgrade that silently changes behavior gets caught before deploy. Read changelogs deliberately before upgrading rather than auto-updating, and stage upgrades in a non-production environment with your real evaluation set (recall@K, MRR) to confirm retrieval quality hasn't regressed.