← AI + Backend
LLM & GenAI Systems

LLM Orchestration Frameworks

Watch: "What is LangChain?" — IBM Technology

A real LLM feature is rarely one prompt in and one string out. It is a small program: retrieve some documents, build a prompt, call a model, parse the answer, maybe call a tool and loop, then log what happened. Orchestration frameworks such as LangChain, LlamaIndex, LangGraph, Semantic Kernel, and DSPy package the recurring parts of that program. Interviewers probe this topic to see whether you can reason about an abstraction layer instead of cargo-culting it: what does it actually buy, what does it hide, how do you debug it at 2 a.m., and when would you delete it. For a Backend or AI engineer, the useful skill is to see every framework as a way to structure a directed graph of steps with state, retries, and tracing, whether you adopt a library or write forty lines yourself.

What Is an LLM Orchestration Framework?

An LLM orchestration framework is a toolkit that helps developers wire together the multiple steps a real AI feature usually needs, rather than just one prompt and one reply. Behind almost any useful AI assistant there's a small sequence of actions: look something up, build a well-formed question for the model, send it, read the answer, maybe use a tool like a calculator or a search engine, and log what happened for debugging. Orchestration frameworks provide ready-made pieces for that sequence — connecting to different AI models, managing conversation memory, and handling errors — so a team doesn't have to build all of that plumbing from scratch for every new feature.

Where This Is Used

A Day-to-Day Example

Picture a restaurant kitchen during a dinner rush. A waiter doesn't cook the whole meal alone — the order goes to the right station (grill, salad, dessert), each cook does their one job, and a expediter checks the plate before it goes out, sending anything wrong back to be redone. An orchestration framework plays the role of that kitchen's workflow: it makes sure each step of preparing an answer happens in the right order, by the right "station," with a system for catching and fixing mistakes before the final plate reaches the customer.

Deep Dive: What an Orchestration Layer Actually Does

What an orchestration layer doesorchestration layerretrievevector DBprompttemplate/registrymodelprovider APIparseschemarun toolfinal answertool callresultno toolcross-cutting: state/memory · retries · timeouts · fallbacktracing · streaming · cost + token accounting
A request flows through steps (retrieve, prompt, model, parse). If the model asks for a tool, the result loops back into the model; otherwise the parsed output is the answer. Retries, tracing and cost accounting wrap every step. The building blocks are listed below.
model / answertool loop
            +------------------- orchestration layer -------------------+
 request -> |  step: retrieve  ->  step: prompt  ->  step: model  ->  parse   |
            |      |                 |                |             |       |
            |   vector DB        template/registry   provider API   schema   |
            |                                             |                 |
            |                    tool call? -- yes --> run tool --+         |
            |                         |                           |         |
            |                         +---- no ----> final answer  <-- loop  |
            |                                                               |
            |  cross-cutting:  state/memory | retries | timeouts | fallback |
            |                  tracing | streaming | cost + token accounting |
            +---------------------------------------------------------------+

Building blocks common to most frameworks (names differ):
  runnable/step     one unit: input -> output
  chain / graph     how steps connect (linear, branching, looping)
  tool calling      model requests a function; the framework executes and feeds the result back
  memory / state    conversation history or a typed state object carried between steps
  retriever         embed query -> search index -> return chunks
  callbacks/tracing hooks fired before and after each step

Strip away the vocabulary and the layer solves four problems. It gives steps a common interface so they can be composed and swapped (a different model or vector store behind the same shape). It runs the control flow, including the agent loop where the model asks for a tool, the runtime executes it, and the result goes back in. It carries state between steps, whether that is chat history or a typed object. And it provides hooks so each step can be traced. None of this is conceptually hard, which is exactly why the build-versus-adopt question is real: the value is in the integrations, the shared vocabulary, and the ecosystem of ready-made connectors, not in some capability you could not write yourself.

Deep Dive: The Framework Landscape at a Working Level

Framework        Center of gravity (generally)                         Typical reason to pick it
---------------  ----------------------------------------------------  ---------------------------------
LangChain        general-purpose components + integrations (models,     breadth of connectors, fast
                 retrievers, tools, output parsers), composable steps    prototyping, large community
LangGraph        agents and workflows as an explicit graph of nodes     stateful, looping, branching flows
                 over shared state, with persistence/checkpointing      that need control and resumability
LlamaIndex       data-centric: ingestion, indexing, query engines,      RAG-heavy apps where the
                 retrieval over your own documents                       indexing/retrieval side dominates
Semantic Kernel  Microsoft's SDK: plugins/functions the model can call,  .NET / Java / Python shops,
                 planning and orchestration                              Azure-adjacent environments
DSPy             programs written as modules with declared signatures;   when you want to optimize
                 an optimizer tunes prompts/examples against a metric    prompts against a metric, not
                                                                          hand-write them

Also seen: Haystack, provider agent SDKs, and plain code with a provider client.

Treat the table as a working map, not a spec. These libraries evolve quickly, split and merge packages, and rename concepts, so specific class names, features, and recommended patterns change between releases; verify against current documentation before relying on any detail. The stable distinction is philosophy. Component-and-chain libraries emphasize composition and integrations, graph runtimes emphasize explicit control flow and state, data-centric libraries emphasize retrieval quality over your corpus, enterprise SDKs emphasize plugins and language support, and DSPy-style tools shift effort from writing prompts to defining metrics and letting an optimizer search. Choosing among them is mostly choosing which of those centers matches the dominant complexity of your application.

Deep Dive: Graphs and State Machines for Agents

An agent as a graph with loops and exitsSTARTplancall_modelneeds_tool?run_toolsteps ≥ MAX_STEPS?abort / escalatevalidateok?ENDrepair (bounded)yesyesno → back to call_modelnoyesnostate: {messages, retrieved, tool_results, step_count, error}each transition logged and checkpointed → resume after a crash, human approval
Once there are loops and branches, a graph fits better than a linear chain (retrieve → prompt → generate → parse). Tools loop back to the model until a step cap aborts; failed validation gets a bounded repair. Every edge is a function of the typed state.
happy pathtool looprepair / abort
Show text version
Linear chain (fine for fixed pipelines):
  retrieve -> prompt -> generate -> parse

Graph / state machine (needed once there are loops or branches):

            +--------------------+
            v                    |
  START -> plan -> call_model -> needs_tool? --yes--> run_tool ---+
                         |                                        |
                         no                          steps >= MAX_STEPS? --> abort/escalate
                         v
                      validate -> ok? --yes--> END
                                     |
                                     no --> repair (bounded) --> call_model

State (typed, explicit):  {messages, retrieved, tool_results, step_count, error}
Every edge is a function of state; every node writes to state; each transition can be logged
and (with persistence) checkpointed so a crashed run can resume or a human can approve a step.

An agent is a loop, and loops need guard rails that a linear chain never did: a maximum step count so a confused model cannot spin forever (and burn money), an explicit terminal condition, bounded repair attempts, and an escalation path. Modelling the flow as a state machine or graph makes those rules visible: nodes are operations, edges are conditions over a shared state object, and the whole run is a sequence of state transitions you can log, replay, and unit test node by node. That is the main reason graph-style runtimes exist. Persisting state between transitions (checkpointing) also enables human approval steps and resuming after a crash, at the cost of designing state that serializes cleanly. For tool semantics and safety see agentic systems and tool use.

Deep Dive: Retries, Timeouts, Fallbacks, and Observability

Worst-case latency of a retried stepone step, worst case: 2 attempts, then fallback (illustrative)attempt 1timeoutbackoffattempt 2timeoutbackofffallbackworst-case latency = attempts × timeout + Σ backoffs + fallback timeretry only transient errors (timeouts, 429, 5xx), backoff + jitter, one total deadlinetrace span per step: run_id, step, attempt, status, latency, tokens, prompt_version
Retries add up: every failed attempt waits the full timeout, plus the backoff, before the fallback runs. That's why each step needs its own timeout and the whole request a deadline. The per-step policy and trace fields are below.
failed attemptfallback
Per-step policy (declared on the step, not scattered through code):

  timeout        per attempt, so one slow provider call cannot eat the whole request
  retries        only for transient errors (timeouts, 429, 5xx), with exponential backoff + jitter
  fallback       cheaper/other model, cached answer, or a canned "could not answer"
  idempotency    a retried step that has side effects (send email, charge) must be safe to repeat

Worst-case latency = attempts x timeout + sum(backoffs) + fallback time
Retries multiply load on a struggling dependency: pair with circuit breakers and a total deadline.

Trace record per step (one span each):
  run_id, step, attempt, status, latency_ms, model, prompt_version,
  input_tokens, output_tokens, error, parent_span

Retry policy is where orchestration most often goes wrong at scale. Retrying every error hides bugs, retrying without backoff and jitter turns a brief provider blip into a self-inflicted outage, and stacking retries at several layers (client, framework, gateway) multiplies attempts silently, so a per-request deadline and one owner of the retry policy matter. Only transient failures are worth retrying; a schema-validation failure deserves a bounded repair attempt, not blind repetition; and side-effecting steps need idempotency keys. See fault tolerance and circuit breakers. For observability, one trace per request with one span per step (model, prompt version, tokens, latency, error) is what makes a multi-step failure debuggable; frameworks generally expose callback or hook mechanisms, and standards such as OpenTelemetry can carry the data, but check what a given framework emits by default. More on this in observability for AI systems.

Deep Dive: Build vs Adopt, Abstraction Cost, and Lock-in

When to hand-roll and when to adopt a frameworkwhat you're building → what to use1 model call + parseplain codefixed 3–5 step RAG pipelineplain code / light libraryloops, branching, checkpoints, HITLgraph runtime or own state machinemany integrations (loaders, stores)adopt for the connectorsprompt quality is the bottleneckmetric-driven optimisation tools
Simple pipelines stay clearer as plain code; frameworks pay off when you need graphs, checkpoints, human-in-the-loop, or many ready-made connectors. The full comparison is below.
plain codeframework / runtime
                    hand-rolled (client SDK + your code)      framework
first demo          slower to first result                    fast (ready-made connectors)
what is sent        you see the exact prompt/request          may be assembled behind layers
debugging           stack trace = your code                   stack trace crosses library internals
upgrades            you control the pace                      library changes may force rewrites
provider swap       your own thin interface                   often easier IF the abstraction holds
team onboarding     read your code                            shared vocabulary, docs, examples
lock-in             low                                       moderate: prompts, state, tracing formats

Decision aids:
  1 model call + parse                       -> plain code
  fixed 3-5 step RAG pipeline                -> plain code or light library use
  loops, branching, checkpoints, HITL        -> graph runtime, or your own state machine
  many integrations (loaders, stores)        -> adopt for the connectors
  prompt quality is the bottleneck           -> consider metric-driven optimization tools

The cost of an abstraction is paid at debugging time. When an answer is wrong you need the exact final prompt, the raw model response, and which step produced what; if a framework assembles the prompt through several layers you must be able to see it, so insist on tracing before adopting. The other cost is churn: fast-moving APIs mean upgrades can be breaking, and pipelines woven through framework-specific classes are expensive to migrate. A common mitigation is a thin boundary of your own (an interface for the model, the retriever, and the tools) so the framework sits behind it and can be replaced. Adopt when connectors and shared structure save real time; write your own when the flow is small and the failure modes matter more than the feature list. Neither is free.

Key Pointers

How It Works

  1. Each unit of work (retrieve, format prompt, call model, parse, call tool) is defined as a step with a name, an input from shared state, and an output written back to state.
  2. The executor starts at the entry step and, after each step, evaluates the outgoing edge (a function of state) to choose the next step or to stop; a linear chain is the special case with one edge each.
  3. For each step the executor applies the declared policy: a per-attempt timeout, bounded retries on transient errors with backoff, and a fallback if attempts are exhausted.
  4. In an agent loop the model may return a tool request; the executor runs the tool, appends the result to state, and re-enters the model step until a final answer, an error, or the step cap.
  5. Every attempt emits a trace span (step, attempt, status, latency, tokens, error) to a tracing backend, tied together by a run id.
  6. On completion or failure the executor returns the final state and the trace; with checkpointing enabled it can persist state between steps to resume or pause for human approval.

Try It Yourself

This stdlib-only program is a minimal graph executor: steps, a conditional edge, bounded retries, a fallback, and a trace row per attempt. The primary "model" is a fake that fails its first two calls, so the fallback path is exercised deterministically. It is a teaching sketch, not how any particular framework is implemented.

# Python 3, standard library only: a minimal pipeline/graph executor with retries,
# fallback and per-step tracing. The "model" is a fake deterministic function.

class TransientError(Exception): pass

class FakeModel:
    """Deterministic: primary fails on its first 2 calls, fallback always works."""
    def __init__(self, name, fail_first=0):
        self.name, self.fail_first, self.calls = name, fail_first, 0
    def __call__(self, prompt):
        self.calls += 1
        if self.calls <= self.fail_first:
            raise TransientError(f"{self.name} 503 (call {self.calls})")
        return f"[{self.name}] answer to: {prompt[:30]}"

class Trace:
    def __init__(self): self.rows = []
    def add(self, step, attempt, status, detail=""):
        self.rows.append((step, attempt, status, detail))
    def show(self):
        for i, (s, a, st, d) in enumerate(self.rows):
            print(f"{i:02d} step={s:<9} attempt={a} status={st:<9} {d}")

class Step:
    def __init__(self, name, fn, retries=0, fallback=None):
        self.name, self.fn, self.retries, self.fallback = name, fn, retries, fallback

def run_step(step, state, trace):
    for attempt in range(1, step.retries + 2):
        try:
            out = step.fn(state)
            trace.add(step.name, attempt, "ok")
            return out
        except TransientError as e:
            trace.add(step.name, attempt, "error", str(e))
            # a real system would sleep with exponential backoff + jitter here
    if step.fallback:
        out = step.fallback(state)
        trace.add(step.name, "-", "fallback", "used fallback")
        return out
    raise RuntimeError(f"step {step.name} failed with no fallback")

def run_graph(steps, edges, state, trace):
    """Follow edges: each edge is a function state -> next step name (or None to stop)."""
    name = steps[0].name
    by_name = {s.name: s for s in steps}
    while name:
        state[name] = run_step(by_name[name], state, trace)
        name = edges[name](state)
    return state

primary, backup = FakeModel("primary", fail_first=2), FakeModel("backup")
docs = {"refund": "Refunds are allowed within 14 days.", "shipping": "Shipping takes 3-5 days."}

def retrieve(st):
    q = st["question"].lower()
    return [t for k, t in docs.items() if k in q] or []
def generate(st):
    ctx = " ".join(st["retrieve"]) or "(no context)"
    return primary(f"{ctx} Q: {st['question']}")
def generate_fallback(st):
    return backup(f"{' '.join(st['retrieve'])} Q: {st['question']}")
def refuse(st):
    return "I could not find that in the knowledge base."

steps = [Step("retrieve", retrieve),
         Step("generate", generate, retries=1, fallback=generate_fallback),
         Step("refuse", refuse)]
edges = {"retrieve": lambda st: "generate" if st["retrieve"] else "refuse",   # conditional edge
         "generate": lambda st: None,
         "refuse":   lambda st: None}

for q in ("What is the refund window?", "Do you sell gift cards?"):
    trace = Trace()
    result = run_graph(steps, edges, {"question": q}, trace)
    final = result.get("generate") or result.get("refuse")
    print("Q:", q); trace.show(); print("=>", final); print()
print("primary calls:", primary.calls, "| backup calls:", backup.calls)
# Real output from python3:
# Q: What is the refund window?
# 00 step=retrieve  attempt=1 status=ok        
# 01 step=generate  attempt=1 status=error     primary 503 (call 1)
# 02 step=generate  attempt=2 status=error     primary 503 (call 2)
# 03 step=generate  attempt=- status=fallback  used fallback
# => [backup] answer to: Refunds are allowed within 14 
# 
# Q: Do you sell gift cards?
# 00 step=retrieve  attempt=1 status=ok        
# 01 step=refuse    attempt=1 status=ok        
# => I could not find that in the knowledge base.
# 
# primary calls: 2 | backup calls: 1

The second program does the worst-case latency arithmetic for a retried step. The timeout, backoff, and deadline values are assumptions chosen for illustration, not measurements of any provider.

# Python 3: worst-case latency of a step with per-attempt timeout, retries and backoff.
# Numbers are ILLUSTRATIVE assumptions, not measurements.
def worst_case(timeout_s, retries, base_backoff_s, then_fallback_timeout_s=0.0):
    attempts = retries + 1
    backoffs = sum(base_backoff_s * 2**i for i in range(retries))   # 1x, 2x, 4x ...
    return attempts * timeout_s + backoffs + then_fallback_timeout_s

for retries in (0, 2, 4):
    w = worst_case(timeout_s=10, retries=retries, base_backoff_s=0.5, then_fallback_timeout_s=10)
    print(f"retries={retries}: worst case before fallback answers = {w:.1f} s, fits a 30 s deadline: {w <= 30}")
# Real output from python3:
# retries=0: worst case before fallback answers = 20.0 s, fits a 30 s deadline: True
# retries=2: worst case before fallback answers = 41.5 s, fits a 30 s deadline: False
# retries=4: worst case before fallback answers = 67.5 s, fits a 30 s deadline: False

Interview Q&A

Q: What problem do LLM orchestration frameworks solve?

A: They standardize the plumbing around model calls: a common step interface, composition of steps into chains or graphs, tool-calling loops, memory or state, retrieval integrations, and tracing hooks. That speeds up prototypes and gives a team shared vocabulary and ready-made connectors. What they do not add is capability you could not hand-write, so the honest answer is that they trade control and transparency for development speed.

Q: When would you not use a framework?

A: When the task is a single model call plus parsing, or a small fixed pipeline, because the framework adds dependency churn and indirection without saving much code. Also when the hot path needs tight control over the exact prompt, latency, and error handling. A thin wrapper around the provider SDK, with your own retry, timeout, and tracing, is often clearer and easier to debug.

Q: Why model an agent as a graph or state machine instead of a while loop?

A: A bare loop hides the control flow, so limits, branches, and error paths get scattered through code. A graph makes nodes, transitions, and terminal conditions explicit, so you can enforce a max step count, unit test one node, log every transition, and checkpoint state to resume or pause for human approval. The cost is more up-front structure and a state object that must serialize cleanly.

Q: How do you decide retry and timeout settings for a step?

A: Work backwards from the user-facing deadline: attempts times per-attempt timeout plus backoff plus fallback time must fit. Retry only transient errors (timeouts, rate limits, 5xx) with exponential backoff and jitter, and never retry blindly on validation failures. With illustrative numbers, a 10 s timeout and 2 retries already reaches 30+ s before the fallback responds, so a tight deadline forces fewer retries or shorter timeouts.

Q: What would you trace in an LLM pipeline?

A: One trace per request with a span per step: step name, attempt number, latency, model and prompt version, input and output token counts, status and error, plus the retrieved document IDs for RAG steps. This lets you answer where time and cost went and which step produced a wrong answer. Be careful with user data: prompts and responses often contain sensitive content, so redact or sample what you store.

Q: How do you limit vendor or framework lock-in?

A: Put your own small interfaces (model client, retriever, tool registry) at the boundary and keep framework types out of your domain logic, keep prompts in a versioned store you own, and emit traces in a neutral format. That way replacing a provider or the framework means rewriting adapters, not the application. Some coupling is unavoidable and the abstraction itself may leak provider-specific features, so test the swap rather than assume it.

Q: Where do retrieval and orchestration meet?

A: Retrieval is one step in the graph: embed the query, search the index, and put the chunks into the prompt budget. Retrieval quality bounds answer quality, so evaluate that step separately from generation; an orchestration layer helps by tracing what was retrieved for each request. See RAG for chunking, ranking, and evaluation.

Follow-up: A chain that worked yesterday now returns worse answers after a dependency upgrade. How do you investigate?

A: Compare traces before and after, focusing on the final rendered prompt, since a library upgrade can change default templates, parser instructions, or message formatting without any change in your code. Pin versions, diff the exact request sent to the model, and re-run the golden set against both. This is the concrete argument for pinning dependencies and for being able to see the raw request.

Follow-up: How do you make a retried tool call safe?

A: Give each logical action an idempotency key generated once per run and step, and have the tool or downstream API deduplicate on it, so a retry or a resumed run returns the earlier result instead of repeating the side effect. Delivery to the tool is at-least-once; idempotency makes the effect effectively once. Read-only tools need no key, and irreversible actions should also require confirmation; see idempotency and exactly-once semantics.

Follow-up: A fallback model returns a lower-quality answer. Is that acceptable?

A: It depends on the product. A degraded answer is usually better than an error for low-stakes queries, but for high-stakes ones a clear "try again later" or human escalation is safer. Record that a fallback was used (the trace shows it), alert on the fallback rate, and evaluate the fallback path with the same golden set so you know its quality before an outage forces it on users.

System Design Tie-in

In a RAG support-bot design, the orchestration layer is the service that owns the request lifecycle: authenticate, retrieve, build the prompt under a token budget, call the model with a timeout and retry policy, validate the output, fall back to a smaller model or a canned reply, and emit a trace. State your assumptions when estimating: at 1M requests/day, the average is about 12 requests/second (peak perhaps several times higher), and if each request makes 3 model calls, the layer coordinates roughly 3M calls/day, so a retry rate of even a few percent adds noticeable extra load and cost (illustrative arithmetic, not a measurement). The step deadline budget matters as much as throughput: a 30 s user-facing deadline split across retrieval, generation, and validation dictates the timeouts of each step. Multi-provider routing and rate limits usually sit in a gateway in front, see LLM gateway and multi-provider routing, while the orchestration layer stays focused on the workflow. Long-running agent tasks belong on a queue with checkpointed state rather than a held HTTP connection, and a human-approval node is just another state in the graph.

Trade-offs

Adopting a framework buys speed to first result, connectors, a shared vocabulary, and sometimes built-in tracing and persistence; it costs transparency (what exactly was sent to the model), a moving dependency that can break on upgrade, and lock-in that grows as framework types spread through your code. Hand-rolling costs more up front and means you own retries, tracing, and integrations, but the code is small, direct, and easy to debug, and the failure modes are yours. The wrong choice is adopting a heavy framework for a two-step flow, or hand-writing a full agent runtime with checkpointing when a maintained graph library already does it. Graph-style control adds structure that pays off with loops and branches and is overhead for a linear chain. Whatever you pick, keep a thin boundary of your own, pin versions, insist on seeing the raw prompt and response, and revisit the decision once the pipeline stabilizes and performance and cost matter more than iteration speed.