My agent pipeline has LangGraph in it and does not have LangChain in it, and people find that combination confusing enough to ask about.
The short version: one of them solves a problem I actually have, and the other solves a problem I stopped having when I decided which model to use.
What the pipeline is
Five layers, and the interesting thing is how little of it is framework.
What LangGraph earns
The pipeline has a loop in it: do the work, run QA, and if QA fails, feed the failure back and try again, up to a limit, then escalate to a person.
That is a state machine, and I have written that state machine badly before. Not the happy path, which is easy. The parts that go wrong:
Resuming after a crash. A run takes tens of minutes. If the process dies at minute forty of a QA retry, starting over is expensive and starting from the wrong place is worse. LangGraph's checkpointing gives me durable state per node with a SQLite backend and no code of my own.
The retry loop not being a while with a counter. My hand-rolled version accumulated conditions: retry unless it was a parse failure, unless it already retried for that reason, unless the QA output was itself malformed. Expressing it as explicit nodes and edges made those distinctions visible instead of nested.
Somewhere to attach observability. Node transitions are a natural span boundary, and having a real graph means the tracing has a shape to follow. (Though as I found out, the graph layer is emphatically not where the interesting traces are, since every judgement in this pipeline happens in a subprocess below it.)
None of that is impossible by hand. All of it is code I would write, get subtly wrong, and maintain forever.
What LangChain would have to earn
LangChain's value proposition is provider abstraction: a uniform interface across models so you can swap them, plus a large collection of pre-built components.
Both parts miss my situation.
I do not swap models. The pipeline uses one specific coding agent CLI. Not one model behind an abstraction, but a specific tool with a specific loop, tool-calling behaviour, and permission model. Abstracting over "which language model" would abstract away the thing I chose.
The components solve problems I do not have. Retrieval chains, memory, document loaders: I have a purpose-built code search tool that took months to tune. A generic retrieval abstraction over it would be a lossy wrapper around something already specialised.
And critically, the agent loop is not mine to build. Reading files, editing, running commands, deciding what to do next. The CLI does all of that. A framework that offers to orchestrate those steps is offering to duplicate a layer I already have and did not write.
That is an argument from fit, and I distrust arguments from fit, so I swapped in the LangChain equivalent at each of the four places it would touch and measured what happened.
| what it would replace | what happened |
|---|---|
| the LLM call | the CLI ships editing, bash, and permissions for free; going through the API means rebuilding the tool loop and carrying an 18,445-token harness on every call |
| the retriever | identical results, minus the agent's ability to re-query |
| prompt assembly | 0 of 4 real inputs survived the template, and it ran 15.6× slower |
| output parsing | 5 of 5 real outputs parsed wrong, 204× slower |
The parser is the one worth explaining, because it fails structurally rather than by accident. My prompts show the expected output by including a fenced JSON example. A parser that takes the first fenced block therefore picks the example rather than the answer. On one run it returned the number 3, pulled out of "3 times 7" in the prose above the real output. PydanticOutputParser scored 0 of 5 on the same inputs.
None of that means the library is bad. It means every one of its four jobs was already being done by something that fit my shape better, and I only know that because I tried it rather than reasoning about it.
One correction to my own title. langchain is installed here. Langfuse's callback handler imports it and LangGraph pulls langchain-core in regardless, so it sits in the tree whether I want it or not; exactly one line of my own code touches it. "Not in my dependencies" and "not used by my code" are different claims, and only the second one is mine to make.
The layer nobody mentions
The part of this stack that surprised me most is that the agent CLI is doing the heaviest lifting and it is not a library at all.
Reading files, deciding what to open next, running commands, interpreting their output, editing, retrying when an edit fails. That loop is where the intelligence lives, and I get it by shelling out to a binary. It has its own permission model, its own tool definitions, its own context management.
That has consequences the architecture diagrams do not show.
It is a subprocess, which means it is opaque to anything above it. My tracing integration was wired into the graph layer, which sees node transitions and nothing else, and I had zero traces from the component making every actual decision until I instrumented the subprocess directly.
It is also the reason the framework question is smaller than it looks. Most of what an agent framework offers (tool orchestration, the reasoning loop, deciding what to call next) is already inside that binary. What is left for the layer above is queueing, retries, and durable state, and that is a much narrower job.
If you are building something similar, this is worth deciding early: is your agent loop yours, or is it something you invoke? The answer changes what the surrounding framework is for. Mine is invoked, so I need orchestration and nothing else, and orchestration is a small enough problem that one focused library covers it.
What I use it as instead
There is a version of LangChain that I do use, without importing it.
The message formats, the tool-calling conventions, the way a chat exchange is represented as a list of typed messages: these have become de facto interfaces. When I write a component that talks to a model, I match those shapes because everything else does, and matching them means anything I build later can plug in.
That's a specification, and specifications are useful without a dependency. I get the interoperability without the abstraction layer, and the cost is a bit of manual structure-building that I would have written anyway.
How I decide, generally
The question I ended up asking about each dependency: would I write this myself, and would I get it right?
That last row is the one people skip. A convention can be adopted without adopting the library that popularised it, and for formats, where the value is that everyone agrees, that is often the better trade.
The row above it is where most unnecessary dependencies come from. "I already have one" is easy to overlook when a framework offers a better-documented version of a thing you built, and the honest question is whether replacing it is work you want to do rather than whether the framework's version is nicer.
Where the graph runtime stopped being enough
One more limit worth recording, since it took me a while to see.
LangGraph handles flow beautifully and knows nothing about why a node decided what it decided. It records that QA failed and a retry started. It does not record the QA prompt, what the QA step read, or what it said before my parser reduced the answer to pass or fail.
For a while I treated the graph's state as my observability, because it was structured, queryable, and right there. It answers "what happened": which node, in what order, how long. It cannot answer "why did it conclude that," and for an agent pipeline the second question is the one that occupies your week.
That gap is not a criticism of the library. Flow control is what it does, and it does it. The mistake was mine: I let the presence of good orchestration data delay the realisation that I had none of the judgement data.
The fix was to have each decision-making subprocess emit its own spans against the parent trace ID, over the wire protocol rather than through any SDK. Once that was in place, the graph runtime went back to being what it is, the thing that moves work between steps, and stopped being where I looked for answers.
What this cost me
Being honest about the downsides.
I write more glue. Passing trace IDs into subprocesses, formatting messages, handling the CLI's output format. Perhaps two hundred lines that a fuller framework would have covered.
I am off the documented path. When something breaks between my layers, there is no guide for that combination. Every integration point is mine to debug.
A future model switch would be more work. If I moved to a different agent, the parts that assume this one's behaviour would need rewriting. That is a real cost of not abstracting, and I accepted it because the abstraction has a cost today and the switch is hypothetical.
What I got back is that the layers I care about are legible. When the routing decision was unstable, the debugging path went straight to a prompt and a subprocess. There was no framework in between deciding things on my behalf.
The condition for changing my mind is narrow. If I needed to run the same pipeline against several providers, or found myself writing retrieval and memory components that already exist in the framework, I would add it. Neither is true today, and taking a dependency for a hypothetical future is how pipelines become hard to debug.