Home 01 · Vector DB 02 · LLM Frameworks 03 · AI Systems 04 · Prompt & Tuning 05 · AI Testing 06 · AI Architect 07 · AI Integration

How these four fit together

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.

ToolLayerCore job
LangChainOrchestrationChains LLM calls, tools, and prompts together; broad integration ecosystem
LangGraphOrchestration (stateful)Graph-based control flow for agents — cycles, branching, persistence, human-in-the-loop
LlamaIndexData / retrievalIngests, indexes, and retrieves data for RAG; deep indexing strategies
OllamaInference / servingRuns 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.

What you can actually build

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})
Advantages
  • Huge integration ecosystem — model providers, vector stores, document loaders, tools
  • 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

from langgraph.graph import StateGraph, END

def retrieve(state): ...
def grade_docs(state): ...
def generate(state): ...

graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve)
graph.add_node("grade", grade_docs)
graph.add_node("generate", generate)

graph.add_edge("retrieve", "grade")
graph.add_conditional_edges(
    "grade",
    lambda s: "generate" if s["relevant"] else "retrieve"
)
graph.add_edge("generate", END)

app = graph.compile()
Advantages
  • 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

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

# 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

PatternWhen to useExample
Tool / function callModel needs to fetch data or trigger an action with side effectsQuery a DB, call an API, send an email
Structured output / JSON modeModel should return data in a fixed schema — no side effectsClassify a ticket, extract named entities
Response format (Pydantic)Enforce schema on output with validation and type-checkingExtract structured patient data from clinical notes
Best Practices
  • Write clear, specific docstrings — the model reads them to decide when to call each tool
  • Keep tool schemas simple and flat — nested schemas increase parsing errors
  • 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

TypeToken costInformation lossLatencyBest for
BufferHigh — grows linearlyNoneNoneShort conversations
WindowFixedDrops old turnsNoneTask-focused sessions
SummaryMediumSummarization loses detailExtra LLM callLong conversations needing broad retention
VectorLow — only relevantNon-retrieved turns invisibleRetrieval latencyVery 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

SymptomWhere to look in traceLikely cause
Wrong or hallucinated answerRetrieval step — what context was retrieved?Irrelevant chunks retrieved; model ignoring context
Tool called with wrong argsTool call step — check schema passedAmbiguous docstring or missing arg descriptions
High latencyWaterfall — which step is the bottleneck?Slow retrieval, large context, sequential tool calls
Wrong output formatFinal LLM call — was format instruction in prompt?Missing or late-position format instruction
Agent not terminatingGraph — 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.