Skip to content
MechAI← All notes
Aug 20, 2026·11 min read

Show Me Your Graph. Tell Me You're a Rookie.

A graph is not an architecture, it is a control flow representation. When an LLM agent framework becomes the default shape, the diagram answers the design question for you.

There is a particular kind of architecture diagram that has become almost unavoidable in AI projects. A user enters on the left, then:

router → planner → retriever → evaluator → generator → validator

A few conditional arrows, maybe one loop back, every box named. Someone calls it an agentic architecture.

My first question is not why these nodes or why LangGraph. It is:

Why is this a graph at all?

A graph is not an architecture. It is a representation of control flow. Confusing the two is becoming one of the most common forms of cargo-cult architecture in AI engineering.

You have a hammer. Suddenly everything is a graph.

Frameworks shape how we think. Give someone Kubernetes and suddenly a three-service application needs a cluster. Teach every Gang of Four pattern and a one-line if becomes a StrategyFactoryProvider. Give someone an agent graph framework and this:

docs = retrieve(query)
docs = rerank(query, docs)
answer = generate(query, docs)
return validate(answer)

needs five nodes, shared state, conditional edges and an execution runtime.

Nothing was gained. The program was merely translated from Python into a poorer control-flow language implemented inside Python.

Python already has if, for, while, functions, exceptions, coroutines, pattern matching and composition: constructs every Python developer already understands. Consider:

result = retrieve(query)

if result.is_weak:
    result = retrieve(rewrite(query))

return generate(result)

Turn that into a graph and you now need nodes for retrieval, grading, rewriting and generation, state passed between them, conditional transitions and graph construction. Depending on the framework, reducers, commands, checkpoint configuration or execution context may follow.

The business logic did not become more sophisticated. Its representation did.

FIG. 1 · REPRESENTATION
Same logic, two representations: graph nodes and state versus plain PythonAS A GRAPHretrievegraderewritegenerateAgentStatereasoning spread across nodes, state,edges, conditions, runtimeAS PYTHONresult = retrieve(query)if result.is_weak:result = retrieve(rewrite(query))return generate(result)execution path visible in one place
The business logic did not get more sophisticated. Its representation did.

That is accidental complexity, and more specifically complexity by dispersion. In the ordinary Python version, the execution path is visible in one place. In the graph version, the same reasoning is distributed across node implementations, state definitions, edge registration, conditional edge functions and runtime configuration. Each individual piece looks simple; understanding the whole becomes harder.

A graph is not decomposition

Putting code into separate boxes does not give you good boundaries.

Good decomposition asks: What changes independently? What belongs to the domain? Where are the side effects? Which contracts must stay stable? What should be independently testable? What can be replaced without affecting the rest of the system?

Graph decomposition asks something dangerously different:

What happens next?

Those are not the same question.

RetrieveNode, RerankNode, GenerateNode and ValidateNode may look beautifully separated on a diagram while all sharing the same giant AgentState. That is not modular architecture. It is procedural code distributed across boxes, with shared mutable state connecting them.

The arrows look clean. The dependencies are not.

The state bag is the new global variable

This is one of my favourite graph anti-patterns. It starts innocently:

class State(TypedDict):
    query: str
    documents: list[Document]
    answer: str

Then requirements arrive. Six months later:

query, rewritten_query, documents, filtered_documents,
retrieval_score, reranking_score, answer, previous_answer,
critique, retry_count, route, tool_results, messages,
metadata, error, status, ...

Every node reads some fields and writes others. Congratulations, you have reinvented global mutable state, except now it has a type annotation.

FIG. 2 · SHARED STATE
The state bag is the new global variablereadwriteretrievereadwritegeneratereadwritegradereadwriterouteAgentStatequeryrewritten_querydocumentsfiltered_documentsretrieval_scorereranking_scoreanswerprevious_answercritiqueretry_countroutetool_resultsmessagesmetadataerrorstatus
Every node reads some fields and writes others. Global mutable state, now with a type annotation.

Yes, graph frameworks provide mechanisms for scoped state, subgraphs and more explicit contracts. That does not change the architectural pressure created by the default abstraction: a shared state object is often the path of least resistance, and defaults matter.

The graph tells you where execution goes. It tells you remarkably little about who owns the data and behaviour.

Pretty diagrams are not architecture

Graphs have one enormous psychological advantage: they look architectural. A twelve-node workflow looks more sophisticated than twenty lines of Python, and that is precisely the danger. Visual sophistication is easily mistaken for engineering sophistication.

A typical top-level diagram becomes:

START → CLASSIFY → PLAN → RETRIEVE → GENERATE → VALIDATE → END

But these are execution stages, not necessarily architectural components. Where is the domain logic? Where are the external system boundaries? What owns retrieval policy? What owns validation rules? What can be replaced independently? What survives if the agent framework is removed tomorrow?

If your architecture diagram mostly reproduces the API of your orchestration framework, you have documented the framework, not designed the system. It is the same mistake as drawing:

Controller → Service → Repository

and calling it software architecture. It tells me where some classes are. It does not explain why the system has those boundaries.

Execution topology asks what happens after this step? Architecture asks what responsibilities exist, where do they belong, and how are dependencies controlled? Those questions can overlap, but they are not interchangeable.

A graph may accurately show that retrieval happens before reranking. It tells you far less about whether retrieval policy belongs to the domain or infrastructure, whether the retriever can be replaced, whether persistence leaks into application code, whether the model provider sits behind a port, or whether the system can be tested without the orchestration runtime.

A good architecture often removes boxes. A rookie architecture often adds them. Sometimes the right architecture fits on a napkin. That is a feature.

Framework leakage is the real cost

The first few nodes usually look harmless. Then framework-specific concepts start leaking into application code.

Business functions stop receiving domain objects and start receiving graph state. Handlers return framework commands. Application services know about runtime context. Tests require graph configuration. Persistence starts following checkpoint semantics. Error handling becomes shaped by the execution engine.

At that point the graph is no longer orchestrating your application. Your application is being designed around the graph.

That is exactly the kind of coupling architecture is supposed to prevent.

A useful test is simple:

Could I remove the graph framework and keep most of my application logic untouched?

If the answer is no, there should be a substantial capability justifying that dependency. "The framework made the demo easy" is not enough.

Testing makes the smell obvious

Take three operations with explicit inputs and outputs:

plan = create_plan(task)
result = execute(plan)
validated = validate(result)

Unit testing is trivial. Each operation can be tested independently, while the orchestration itself barely deserves a test because there is almost nothing there.

Put the same logic behind graph state and tests often become: construct state, invoke a node, inspect mutated state, simulate transitions, compile the graph runtime, execute it and inspect final state.

You are now testing orchestration mechanics that did not previously need to exist.

This does not mean graph-based code cannot be tested well. Of course it can. The question is why those mechanics were introduced in the first place. A useful abstraction should remove complexity from the problem. If it creates a new category of tests, configuration, contracts and failure modes, it had better buy something substantial in return.

Bundled features are not requirements

Graph frameworks often come with excellent tooling: tracing, observability, streaming intermediate results, parallel branches, checkpointing and execution inspection. Those are real benefits.

But a bundled capability is not the same thing as an architectural requirement.

Tracing is a property of instrumentation, not topology. OpenTelemetry does not care whether work happens inside a graph node, a function, a method or a coroutine. A framework may make tracing easier to configure, and that is a legitimate reason to like the framework. It is not evidence that the problem itself required a graph.

The same applies to streaming and parallel execution. Python already provides generators, async iterators, tasks and concurrency primitives. A graph framework may package these capabilities more conveniently and reduce implementation effort, but convenience is a different claim from necessity.

"The framework gives me this feature" is not the same as "this feature requires the framework."

Branching is not a graph requirement

"We have conditional routing, so we need a graph."

No. You have conditional routing.

Python has:

if request.kind == RequestKind.SEARCH:
    return search(request)

if request.kind == RequestKind.ANALYSIS:
    return analyse(request)

return fallback(request)

A conditional does not become an architectural problem because an LLM participates in choosing the branch.

Likewise, a loop does not become a graph requirement because the stopping condition comes from a model:

for _ in range(max_attempts):
    result = generate(context)

    if evaluator.accepts(result):
        return result

    context = improve_context(context, result)

raise MaxAttemptsExceeded

This is already a perfectly reasonable representation of an agent loop. You can test it, instrument it, read it and debug it with a normal Python debugger.

The presence of dynamic control flow is not enough. The interesting question is whether you need a runtime that manages that control flow as persistent execution state.

That is where the trade-off changes.

The honest counterargument

There is one class of problem where graph-like orchestration starts earning its complexity.

Imagine an operation that chooses future actions based on runtime results, cycles through parts of the process, persists execution state, stops for human approval, waits minutes or days, resumes from exactly the previous checkpoint, survives process failure and may require replay or intervention during execution.

Now execution topology itself has become part of the problem.

You no longer have a function call that happens to branch. You have a durable state machine. Checkpointing matters. State recovery matters. Idempotency matters. Interrupt and resume matter. Long-running execution matters.

FIG. 3 · THE BAR
Where the complexity threshold actually sitsPLAIN CONTROL FLOWbranchingloopsretriesdispatchstreamingconcurrencytracingDURABLE EXECUTIONRUNTIMEcheckpointingpause for approvalresume after failurereplayTHE BARless complexitymore complexity
A graph runtime earns its complexity to the right of the line, not to the left of it.

At that point a framework such as LangGraph can absolutely earn its complexity.

And that distinction matters: LangGraph is not the problem.

Using it where its execution model solves a real requirement is perfectly reasonable. The problem is treating it as the default architecture for anything containing an LLM.

Not because the system contains an LLM. Not because it is called an agent. Not because it has two branches and a retry.

Because durable, branching, resumable execution is an actual requirement.

That is a much higher bar. It should be.

The architecture test

Before creating the first node, ask:

What capability do I lose if I implement this as ordinary Python?

  • "The graph is easier to visualise." → Draw a diagram. Keep the implementation simple.
  • "We have branching." → Use if.
  • "We have repetition." → Use a loop.
  • "Different requests need different handlers." → Use dispatch.
  • "We need retries." → Implement a retry policy.
  • "We need multiple components." → Compose them.
  • "We want observability." → Instrument the application.
  • "We want intermediate output." → Stream it.
  • "We have independent work." → Execute it concurrently.
  • "Execution must survive process failure, persist state, stop, resume and continue across dynamic transitions." → Now we are talking.

The burden of proof belongs to the abstraction.

Start from the failure, not the shape

I apply the same rule to agentic systems generally: pick the least autonomy that solves the problem. A single model call before a workflow. A deterministic workflow before an agent loop. One agent before five.

The same principle holds one level lower: plain control flow before a graph runtime.

Let the failure mode drive the decision. Need evidence gathered iteratively? Add a loop. Need dynamic decomposition? Add an orchestrator. Need independent tasks? Parallelise them. Need different strategies for different inputs? Route them. Need durable pause and resume? Now add durable workflow machinery.

Architecture should grow because requirements force it to grow, not because a framework made another box easy to draw.

Architecture is subtraction

There is a strange tendency in software engineering to associate sophistication with the number of moving parts. Senior-looking diagrams contain more boxes. Senior-looking systems contain more services. Senior-looking AI systems contain more agents, routers, evaluators, planners and graphs.

Experience usually teaches the opposite: how much you can remove. How many abstractions you do not need. How many services should remain one service. How many agents should remain one function call. How many patterns should remain an if. How much framework code should never enter the domain.

The interesting architectural question is rarely what else can we add?

It is:

What can we avoid adding without losing a required property of the system?

That is why unnecessary graphs bother me. They are often not evidence of sophistication. They are evidence that nobody asked what could be removed.

Show me your graph

So yes, show me your graph. But don't start by explaining the nodes.

Tell me what requirement forced the graph to exist. Tell me why ordinary functions, composition, branching and loops were insufficient. Tell me why execution topology needed to become a first-class runtime concept. Tell me what complexity the graph removes, not what features the framework provides.

If you can answer those questions, you made an architectural decision.

If you cannot, the diagram has already answered for you:

you started with the solution and went looking for a problem.