Lessons available in both languages
Gen AI · Interview Prep

LangChain & LangGraph interview questions & answers

145+ real LangChain & LangGraph interview questions with model answers, plus free lessons to learn the concepts. Prepare in English & Hinglish, then practise with an AI mock interview.

13 topics · 145+ questions

How Hirenix teaches

One chapter. 90 minutes.
Interview-ready.

Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.

  • 📖Concept in 5 minutesNo jargon — straight to the point
  • 🛠️Real-world problemThe kind production code throws at you
  • 💬Model answerExactly what to say in the room
  • 🧠FlashcardsRevise in 10 minutes
  • 🤖AI mock interviewIt asks follow-ups too
  • 📊Weak topicsSee exactly where you're stuck
Start this chapter — free🌐 English🇮🇳 Hinglish
A student learning an interview concept on Hirenix at home
Video playlistbuilt around a syllabus18h+
Hirenix chapterbuilt around interviews90 min

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.

Lessons available in both languages

What you’ll learn

  • What is LangChain?
  • RAG with LangChain
  • Tools with @toolFree account
  • LangGraph basics: StateGraphFree account
  • LangGraph agentsFree account
  • Conditional routingFree account
  • Human in the loopFree account
  • Multi-agent systemsFree account
  • LangSmith: tracing & evalFree account
  • RecapFree account
  • Project: AI Research Agent
  • Project: Support GraphFree account
  • Project: Multi-Agent WriterFree account

What is LangChain?

Picture a car wash: your dusty car enters one end, and a series of connected stations — soap, scrub, rinse, dry — each hands the car to the next automatically. You don't carry the car between stations yourself; the stations are physically piped together into one line. Drive in dirty, drive out clean, in one pass.

In LangChain, that piped line is called a chain, and the pipe itself is a real operator: |. A prompt template is the soap station — it takes your raw variables and shapes them into the exact message a model expects. The chat model is the scrub station — it does the actual thinking/generation. An output parser is the dry station — it strips the wrapper off the model's reply and hands you back plain text (or structured data).

Ch5 built this loop by hand with the raw SDK: format a string yourself, call the model yourself, dig .content out of the response yourself. LangChain's contribution is naming that pattern LCEL (LangChain Expression Language) and turning it into one composable object: chain = prompt | model | parser. Build it once, and you get .invoke() (one input), .stream() (token by token), and .batch() (many inputs at once) for free — no extra code.

Older LangChain tutorials (0.x) built chains with a class called LLMChain and ran them with chain.run(...). Both are DELETED in the current 1.x line — importing LLMChain throws a ModuleNotFoundError before your program even starts. If a tutorial shows LLMChain or .run(), it is teaching a dead API; the | pipe is the only chain-building pattern that still works.

🌍 Real-world example: a support-ticket triage app takes a raw customer message, a prompt template turns it into a structured "classify this ticket" instruction, the model returns a category, and a parser strips the model's reply down to just the category string your database column expects — three stations, one pipe.

💡 Runnable = anything with .invoke(), .stream(), and .batch() — the one interface every LangChain building block (prompts, models, parsers, chains) shares.

💡 LCEL (LangChain Expression Language) = the | syntax that pipes Runnables together into a bigger Runnable, called a RunnableSequence.

💡 Output parser = a Runnable that reshapes a model's raw reply into the format your code actually wants (a string, JSON, etc.).

Standard definition: LangChain is a framework for composing LLM calls, built around the Runnable interface (.invoke()/.stream()/.batch()); LCEL uses the | operator to pipe Runnables — such as prompt | model | parser — into a single chain, replacing the deleted 0.x LLMChain/.run() pattern.

import os
os.environ.setdefault("OPENAI_API_KEY", "sk-...")  # normally already set, like in Ch5

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a {role}."),
    ("human", "{question}")
])

model = ChatOpenAI(model="gpt-4o-mini")  # model IDs change — check the provider's current list

chain = prompt | model | StrOutputParser()

print(type(chain).__name__)
print(hasattr(chain, "invoke"), hasattr(chain, "stream"), hasattr(chain, "batch"))

try:
    from langchain.chains import LLMChain
except ModuleNotFoundError as e:
    print("LLMChain is gone:", e)

# result = chain.invoke({"role": "Python tutor", "question": "What are decorators?"})
# print(result)  # illustrative only -- needs a real API key

RAG with LangChain

Think of a factory assembly line. A box enters at station 1, gets a part added, moves to station 2, gets another part added, moves to station 3, and comes out finished at the end. Nobody hand-carries the box between stations — a conveyor belt connects them. RAG in Ch3/Ch4 you built by hand: chunk the doc yourself, embed it yourself, do cosine similarity yourself, then hand-build the prompt string and call the model. LangChain's job is to be that conveyor belt — the same stations, wired together with one operator.

In LangChain, that conveyor belt is LCEL (LangChain Expression Language): | connects Runnables (anything with .invoke()/.stream()/.batch()). A RAG chain is retriever | prompt | model | parser — or, when the retrieved docs and the raw question both need to flow into the prompt at once, a dict shape: {"context": retriever | format_docs, "question": RunnablePassthrough()}. That dict runs BOTH branches on the same input in parallel and merges their outputs into one object the prompt template can fill. RunnablePassthrough() means "take the input as-is" — here, the raw question needs no transformation, only the context branch does (retrieve, then flatten the retrieved docs into one string).

Nothing about the underlying RAG idea changes: split into chunks, embed each chunk, store the vectors, retrieve the closest ones for a question, stuff them into a prompt, ask the model. LangChain doesn't make retrieval smarter or the LLM call cheaper — every node that calls an embedding model or a chat model is still a billed API call, same economics as Ch3-Ch5. What it buys is composition: swap the vector store, add streaming, add retries, without rewriting the plumbing by hand.

🌍 Real-world example: A company policy chatbot. retriever.invoke("How many sick leaves?") returns the top-k matching chunks from the HR policy doc as Document objects. format_docs joins their .page_content into one context string. The chain fills {context} and {question} into the prompt template, sends it to the model, and StrOutputParser() unwraps the reply from an AIMessage object down to plain text — so rag_chain.invoke("...") returns a string, not an object you have to dig into.

💡 Runnable = anything with .invoke()/.stream()/.batch() — a retriever, a prompt template, a chat model, and a parser are all Runnables, which is WHY they can all be piped with |.

💡 retriever = a vector store wrapped as a Runnable (vectorstore.as_retriever(search_kwargs={"k": N})); calling it returns the top-k most similar chunks as Document objects (Ch3's cosine-similarity search, done for you).

💡 RunnablePassthrough() = a Runnable that returns its input unchanged — used when one branch of a parallel step needs no transformation.

💡 StrOutputParser() = unwraps a model's AIMessage response down to a plain string, so the chain's final .invoke() result is text you can print or send to a UI directly.

One honest gotcha: langchain_community's Chroma import (the version most tutorials still show) still works but throws a DeprecationWarning — the package is being sunset. A dedicated langchain-chroma package, or InMemoryVectorStore from langchain_core for small/demo data, is the current path.

Standard definition: A LangChain RAG chain wires a text splitter, an embedding model, a vector store retriever, a prompt template, a chat model, and an output parser into one LCEL pipeline ({"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | model | parser) so the whole retrieve-then-generate flow runs as a single composable, streamable Runnable.

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# Step 1: split the source document into chunks (Ch3/Ch4 chunking, LangChain's splitter class)
text = """LangChain is a framework for building LLM applications.
It provides abstractions for chains, prompts, and retrievers.

RAG stands for Retrieval Augmented Generation.
It combines a retriever with a language model to answer questions using external documents.

The LangChain Expression Language (LCEL) lets you compose Runnables with the pipe operator.
This makes chains easy to read, stream, and debug."""

splitter = RecursiveCharacterTextSplitter(chunk_size=120, chunk_overlap=20)
chunks = splitter.split_text(text)
for i, c in enumerate(chunks):
    print(f"Chunk {i+1} ({len(c)} chars): {c!r}")

# Step 2: embed the chunks and store them (needs a real API key to actually run)
vectorstore = InMemoryVectorStore.from_texts(chunks, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

# Step 3: the LCEL RAG chain
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

prompt = ChatPromptTemplate.from_template(
    "Answer based ONLY on the following context:\n\n{context}\n\nQuestion: {question}"
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)  # model IDs change — check the provider's current list

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)
print(type(rag_chain))

answer = rag_chain.invoke("What does LCEL let you do?")
print(answer)

Project: AI Research Agent

What we're building: an AI Research Agent — a LangGraph graph that takes a question, loops reason → act → observe using a real web-search tool and a tool-calling LLM, and returns one synthesised answer. This is Ch5's hand-rolled agent loop and Ch6's whole LangGraph mental model, fused into one runnable project: a StateGraph with an agent node (the model, decides what to do), a tools node (a real ToolNode, executes what the model asked for), a conditional edge (more searching needed, or done?), and a recursion_limit so it can never spin forever.


Step 0 — The mental model (read first)

User question
     │
     ▼
[agent] ──asks model: "do I need to search, or can I answer now?"──┐
     │                                                              │
 has tool_calls?                                              no tool_calls
     │                                                              │
     ▼                                                              ▼
 [tools] ──runs web_search for real, appends result──▶ back to [agent]     [END] — final answer

This is EXACTLY Ch5's while loop (call model → check for tool calls → execute → feed result back → repeat), just drawn as an explicit, inspectable graph instead of hidden inside Python control flow. Every arrow above is a real LangGraph edge you will write below.

💡 Reason → Act → Observe = the model REASONS about what it needs (an LLM call), ACTS by requesting a tool call (it does not run the tool itself), and OBSERVES the tool's real result before reasoning again.


Step 1 — Give the agent a real tool

from langchain_core.tools import tool

@tool
def web_search(query: str) -> str:
    """Search the web for current information on a topic and return short snippets."""
    # A real build swaps this body for a live search API call.
    # For this project we keep it a stub so the WIRING runs with zero API keys.
    return f"[search stub] top result for '{query}'"

What's happening: @tool inspects the function's name, docstring and type hints and turns them into a schema the model can read — web_search.name, .description, .args. This is the exact same rule from langchain-tools: the model only ever sees this SCHEMA, never the function body. Whether the body does a real HTTP call to a search API or returns a stub string, the model's job (deciding when to call it) doesn't change — which is why you can verify the whole graph's wiring with zero API keys, and only swap this one function's body for the real search call later.

🌍 Real-world example: printing web_search.args on C:/lcv returns {'query': {'title': 'Query', 'type': 'string'}} — that dict IS the schema the model is shown. Nothing hidden, nothing magic.


Step 2 — Build a tool-calling model

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4.1-mini")  # model IDs change — check the provider's current list
model_with_tools = llm.bind_tools([web_search])

What's happening: bind_tools([...]) does not call the API — it returns a new Runnable that will ATTACH the tool schemas to every request it makes. ChatOpenAI(...) DOES need an API-key VALUE present (a real key, or even a placeholder string / env var) to construct, because it builds an internal OpenAI client object — but constructing it makes no network call and does not validate the key. Only actually invoking the model (Step 8) needs a genuinely working key. This line was verified to construct cleanly on C:/lcv with a placeholder key and zero network activity.


Step 3 — Define the State

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class ResearchState(TypedDict):
    messages: Annotated[list, add_messages]

What's happening: one field, messages, tagged with the add_messages reducer. Every node below returns a PARTIAL update — just the new message(s) — and LangGraph APPENDS them instead of replacing the list. Skip this tag and every tool result would wipe out the question that started the run.


Step 4 — The agent node (reason)

def agent(state: ResearchState):
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

What's happening: the ENTIRE reasoning step is one line — model_with_tools.invoke(...) sends the full message history and gets back one AIMessage. That message either carries .tool_calls (the model wants to search) or doesn't (the model is ready to answer). The node itself makes no decision — it just runs the model and returns its message; the DECIDING happens in Step 5's router. This mirrors Ch5's rule exactly: the model only ever RETURNS a request to call a tool, it never runs the tool itself.


Step 5 — The router + the real ToolNode (act)

from typing import Literal
from langgraph.prebuilt import ToolNode
from langgraph.graph import END

def should_continue(state: ResearchState) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    if last.tool_calls:
        return "tools"
    return END

tool_node = ToolNode([web_search])

What's happening: should_continue is the ROUTER — it inspects the last message and returns a plain string, the key of the next node. ToolNode is the piece that actually EXECUTES whatever tool calls the model requested, runs the real Python function, and wraps each result in a ToolMessage appended back onto messages. This is the "act" and "observe" beats: ToolNode acts (runs the function for real), and its ToolMessage output is what the agent node observes on its next turn.


Step 6 — Wire the graph

from langgraph.graph import StateGraph, START

graph = StateGraph(ResearchState)
graph.add_node("agent", agent)
graph.add_node("tools", tool_node)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")

research_agent = graph.compile()

What's happening: START → agent always runs first. From agent, add_conditional_edges branches on should_continue's return value: "tools" routes to the tools node, END stops the graph. tools → agent closes the loop — every tool result goes straight back to the agent for another reasoning pass. .compile() turns this wiring into one Runnable with .invoke(). Verified on C:/lcv: this exact graph compiles with a placeholder API-key value and zero network callsChatOpenAI needs a key VALUE present to construct its client object, but neither construction nor .compile() ever reaches the network; only Step 8's .invoke() would.


Step 7 — Prove the wiring with NO API key

Before trusting a real model, prove the graph's STRUCTURE with a placeholder agent node that hand-returns the same message shapes a real tool-calling model would:

from langchain_core.messages import AIMessage, HumanMessage

_turn = {"n": 0}
def stub_agent(state: ResearchState):
    _turn["n"] += 1
    if _turn["n"] == 1:
        return {"messages": [AIMessage(content="", tool_calls=[
            {"name": "web_search", "args": {"query": "LangGraph recursion_limit"}, "id": "call_1"}
        ])]}
    return {"messages": [AIMessage(content="Final synthesised answer using the real tool result.")]}

stub_graph = StateGraph(ResearchState)
stub_graph.add_node("agent", stub_agent)
stub_graph.add_node("tools", tool_node)
stub_graph.add_edge(START, "agent")
stub_graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
stub_graph.add_edge("tools", "agent")
stub_app = stub_graph.compile()

result = stub_app.invoke(
    {"messages": [HumanMessage(content="How does LangGraph stop an agent from looping forever?")]},
    {"recursion_limit": 10}
)
for m in result["messages"]:
    print(type(m).__name__, "|", getattr(m, "content", m)[:60])

This ACTUALLY RAN on C:/lcv — zero API keys — and produced exactly this state transition, message by message:

[0] HumanMessage: 'How does LangGraph stop an agent from looping forever?'
[1] AIMessage: ''  tool_calls=[{'name': 'web_search', 'args': {'query': 'LangGraph recursion_limit'}, ...}]
[2] ToolMessage: "[search stub] top result for 'LangGraph recursion_limit'"
[3] AIMessage: 'Final synthesised answer using the real tool result.'

Trace it beat by beat: [0] is the question entering state. [1] is the agent's FIRST reasoning pass — it decided it needed to search, so should_continue routed to "tools". [2] is the REAL ToolNode actually calling web_search(query="LangGraph recursion_limit") and appending its genuine return value as a ToolMessage — this part used the real tool function, not a stub. [3] is the agent's SECOND reasoning pass: this time the returned AIMessage has no tool_calls, so should_continue returned END and the graph stopped. Four messages in, all four kept — add_messages appended every one, never overwrote.


Step 8 — The real, model-backed run (illustrative)

Swap stub_agent back for the real agent node from Step 4 (model_with_tools.invoke(...)) and you have research_agent from Step 6. With a real OPENAI_API_KEY, calling it looks identical in SHAPE to Step 7's proven trace — only now [1]'s tool call and [3]'s final answer are genuinely generated by the model instead of hand-written:

result = research_agent.invoke(
    {"messages": [("user", "What is LangGraph's recursion_limit and why does it matter?")]},
    {"recursion_limit": 10}
)
print(result["messages"][-1].content)

⚠️ Illustrative — no live key was used to author this. The message SEQUENCE (Human → AI-with-tool-call → Tool → AI-final) is verified real, because Step 7 ran that exact routing with the exact same graph. Only the WORDING of the model's tool-call arguments and final answer below is illustrative, kept consistent with what the real web_search tool would have been asked and returned:

AI (illustrative): The recursion_limit config value caps how many super-steps a
LangGraph run can take. Once an agent loop exceeds it, LangGraph raises a
GraphRecursionError instead of looping forever, which matters because an agent
that never routes to END would otherwise call the model — and the search tool —
indefinitely, burning tokens and API quota on every extra pass.

Step 9 — Bound the loop for real

try:
    research_agent.invoke({"messages": [("user", "loop forever")]}, {"recursion_limit": 6})
except Exception as e:
    print(type(e).__name__, str(e)[:80])

This was tested on C:/lcv with a graph that never routes to END (a router hard-coded to always return "tools"), and it produced a real GraphRecursionError: Recursion limit of 6 reached without hitting a stop condition. — not a hang, not a silent failure. This is why recursion_limit is not optional polish: any agent graph with a router bug (or a genuinely confused model that keeps requesting tools) is bounded, loudly, instead of running your API bill up in an infinite loop.


✅ What you just built — and resume framing

You built a graph that: defines a real tool with @tool, binds it to a tool-calling model, tracks conversation state with a correct add_messages reducer, routes with a real conditional edge, executes tool calls with a real ToolNode, loops reason→act→observe until the model is done, and is bounded by recursion_limit so it can never run away. That is a legitimate LangGraph agent, not a toy — the SAME shape (agent node + tools node + conditional edge + loop-back edge) is what create_react_agent(model, tools) (note: deprecated since LangGraph 1.0 in favour of langchain.agents.create_agent, though this name still works and most tutorials use it) gives you prebuilt; you now know exactly what it does under the hood because you wired it by hand.

For your resume/interview: "Built an AI research agent in LangGraph — a StateGraph implementing a reason-act-observe loop with a real tool-calling model, a ToolNode executing web search, conditional routing between reasoning and tool execution, and a bounded recursion_limit to prevent runaway agent loops." That sentence demonstrates you understand agent architecture, not just that you called an API.

Bonus features to extend this (do NOT need to build all three to claim the project):

  1. Multiple tools — add a calculator tool alongside web_search; ToolNode already handles a model requesting several tool calls in one turn, no wiring change needed.
  2. Human-in-the-loop — insert interrupt() before the tools node so a human approves a search before it runs, compiled with a MemorySaver checkpointer and a thread_id (see human-in-the-loop).
  3. Streaming — swap .invoke() for .stream() on research_agent to print each node's output as it happens, instead of waiting for the whole run to finish.

Standard definition: an AI research agent is a LangGraph StateGraph that implements the reason-act-observe agent loop as an explicit graph — a tool-binding model node that reasons and requests tool calls, a ToolNode that acts by executing them for real, a conditional edge that routes between reasoning and acting until the model signals it is done, and a recursion_limit that bounds the loop against runaway execution.

from typing import Annotated, Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def web_search(query: str) -> str:
    """Search the web for current information on a topic and return short snippets."""
    # A real build swaps this body for a live search API call.
    return f"[search stub] top result for '{query}'"

llm = ChatOpenAI(model="gpt-4.1-mini")  # model IDs change — check the provider's current list
model_with_tools = llm.bind_tools([web_search])

class ResearchState(TypedDict):
    messages: Annotated[list, add_messages]  # reducer: APPENDS, never overwrites

def agent(state: ResearchState):
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def should_continue(state: ResearchState) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    if last.tool_calls:
        return "tools"
    return END

tool_node = ToolNode([web_search])

graph = StateGraph(ResearchState)
graph.add_node("agent", agent)
graph.add_node("tools", tool_node)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")

research_agent = graph.compile()

result = research_agent.invoke(
    {"messages": [("user", "What is LangGraph's recursion_limit and why does it matter?")]},
    {"recursion_limit": 10}
)
for m in result["messages"]:
    print(type(m).__name__, "|", getattr(m, "content", m))

LangChain & LangGraphinterview questions & answers

10 sample questions below — 145+ in the full bank inside.

Give a few realistic examples of when you'd add human-in-the-loop to an agent.

Any action that costs money, is hard to undo, or affects someone outside the system: sending an email or message on the user's behalf, making a payment or placing an order, deleting or overwriting a file/database row, or posting something publicly. Read-only or easily reversible actions — a search, a lookup, drafting text without sending it — usually don't need approval.

In simple terms: The rule of thumb is: would you let a new employee do this on day one without asking? If not, gate it. Example: an agent reading your calendar to check availability needs no approval; the same agent actually sending the calendar invite to a client does.

What does the `|` operator do when you write `chain = prompt | model | parser`?

It composes the three Runnables into one `RunnableSequence` — the output of `prompt` becomes the input to `model`, and the output of `model` becomes the input to `parser`. Calling `chain.invoke({...})` runs all three steps in order and returns the final parsed result.

In simple terms: It's an assembly line — raw material goes in one end, each station does its job, finished product comes out the other end. Example: `chain.invoke({"question": "What is Python?"})` first fills the prompt template, then sends the formatted messages to the model, then parses the model's reply into a plain string.

What is human-in-the-loop (HITL) in an agent, and why would you add it?

Human-in-the-loop means the agent graph pauses before a risky action — like sending an email, deleting a file, or making a payment — and waits for a human to approve, reject, or edit it before continuing. You add it because an LLM agent can decide to call a tool with the wrong args or the wrong intent, and some actions are too costly or irreversible to let it run unsupervised.

In simple terms: Think of a junior employee who is allowed to draft a client email but must show it to the manager before hitting send — the drafting is autonomous, the send is not. Example: an agent decides to call `delete_file("report.pdf")`; instead of executing immediately, the graph pauses and asks 'delete report.pdf — confirm?' before the tool actually runs.

In a human-in-the-loop flow, does the LLM itself decide to pause and wait, or is that the graph's job?

The graph's job. The LLM only decides which tool to call and with what arguments — it has no ability to pause execution or wait for a human. The pausing happens because the graph's routing logic and the `interrupt()` call inside a node explicitly stop the run before the risky tool actually executes; the LLM is unaware this is even happening.

In simple terms: The LLM is like an intern who fills out a purchase request form — deciding what to buy and how much — but it's the office's approval process (not the intern) that actually holds the form until a manager signs off. Example: `llm_with_tools.invoke(...)` returns a message saying 'call delete_file("x.txt")'; it's the `check_approval` router and `interrupt()` in the graph, not the LLM, that stop it from running immediately.

What is conditional routing in a LangGraph graph?

Conditional routing is how a graph branches: instead of a node always flowing to the same next node, a router function inspects the current State at runtime and decides which node to go to next. It's the mechanism that turns a straight-line chain into something that can behave like an if/elif/else or a decision tree.

In simple terms: It's a fork in the road with a signpost that reads the current situation before pointing — not a fixed one-way street. Example: after a `classify` node sets `state["category"]` to "billing", "technical", or "general", a router reads that field and sends the graph to the matching handler node.

What is LangChain, in one line?

LangChain is a framework for composing LLM calls into pipelines — prompts, models, output parsers, tools and retrievers are all standard building blocks you connect instead of writing the plumbing by hand. It sits on top of a raw SDK call like the one you used in Ch5, not instead of it.

In simple terms: Think of it like Express.js for Node — Node already lets you handle HTTP requests, but Express gives you routing and middleware as ready-made pieces instead of parsing raw sockets yourself. Example: instead of manually building a prompt string, calling the API, and parsing the reply, LangChain lets you write `prompt | model | parser` and get all three steps as one reusable unit.

Can a conditional edge branch to more than two nodes?

Yes — a `path_map` can hold any number of key-to-node-name mappings, so a single router can send execution down as many branches as you have categories. A router that classifies a query as "billing", "technical", or "general" and routes to three separate handler nodes is a normal, verified three-way branch, not a special case.

In simple terms: It's not a coin flip (only 2 outcomes) — it's more like a mail sorter that reads the address and drops the letter into one of many bins. Verified concretely: a `classify` node followed by `add_conditional_edges("classify", route_question, {"billing": "billing", "technical": "technical", "general": "general"})` correctly routed a refund message to `billing`, a bug-report message to `technical`, and a hours-of-operation message to `general`.

What is the signature of `add_conditional_edges`, and what does each argument do?

`graph.add_conditional_edges(source, router_fn, path_map)` — `source` is the node this branching happens after, `router_fn` is a function that takes the current State and returns a value naming the next node, and `path_map` is an optional dict mapping those return values to real node names. Verified on `langgraph` 1.x: `path_map` can be omitted, in which case `router_fn`'s return value is used directly as the destination node name.

In simple terms: `source` is 'after which stop does the decision happen', `router_fn` is 'who makes the call', and `path_map` is the translation table if the router's answer isn't already the exact node name. Example: `graph.add_conditional_edges("classify", route_question, {"billing": "billing", "technical": "technical", "general": "general"})` — here the map is an identity map, so it could also have been omitted.

In a 'classify then route' pattern, what does the classify node do versus the router?

The classify node is a regular node — it usually calls the model (or plain logic) to inspect the latest message and writes its decision into State, e.g. `{"category": "billing"}`. The router, wired via `add_conditional_edges`, then just reads that already-computed field back out of State and returns it, without doing any classification work itself.

In simple terms: Classify is the person filling out the intake form; the router is the receptionist reading that form and pointing you to the right room. Verified concretely: `classify(state)` returned `{"category": "billing"}` for a refund message, and `route_question(state)` (the router) simply returned `state["category"]`, sending execution to the `billing` node.

What does `ChatPromptTemplate.from_messages([...])` give you over an f-string?

It builds a reusable, typed prompt object with named `{variable}` placeholders across a list of role-tagged messages (system/human/etc.), and `.invoke({...})` fills them and returns a proper message list — not just a string. That message list is what LCEL pipes into the model, and it composes cleanly with `|` unlike a raw f-string.

In simple terms: An f-string is a one-off note you scribble each time; a `ChatPromptTemplate` is a printed form with labelled blanks you can reuse and hand to anyone. Example: `ChatPromptTemplate.from_messages([("system", "You are a {role}."), ("human", "{question}")])` then `.invoke({"role": "tutor", "question": "hi"})` returns a system + human message pair ready for the model.

135+ more LangChain & LangGraph questions inside

Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.

Unlock all questions — free

Ready to practise LangChain & LangGraph?

Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.