---
title: "LangChain, LangGraph, OpenRouter and Langfuse: what to choose and when"
source: "https://www.matteovincenti.com/en/blog/langchain-langgraph-openrouter-langfuse-which-to-choose"
language: en
author: "Matteo Vincenti"
published: 2026-10-20
updated: 2026-10-24
category: "AI"
tags: ["LangChain", "LangGraph", "OpenRouter", "Langfuse"]
---

# LangChain, LangGraph, OpenRouter and Langfuse: what to choose and when

Compare LangChain, LangGraph, OpenRouter and Langfuse by role and use case: practical criteria for business AI projects, with technical details.

![A theatre scene shows a track, a switch and an inspection light](https://cdn.sanity.io/images/xrqkwry7/production/ad5f1acecfaacff36e3a793f5cecfa109b65d373-1728x910.png?w=1400&fit=max&auto=format)

Category: AI · Tags: LangChain, LangGraph, OpenRouter, Langfuse

An application that assigns a category to a support ticket and one that prepares a purchase for approval can use the same AI model. The work around that model is quite different. The first needs a valid result; the second must recover a proposal hours later, identify who approved it and handle an order sent to another system.

That difference is where I would start when choosing between LangChain, LangGraph, OpenRouter and Langfuse. They can work together, and some of their capabilities overlap. Adopting all four also means maintaining all four integrations. I prefer to give each layer a job before adding it.

## What the four tools do

_What the four tools do_

| Tool | Responsibility relevant to this comparison | When to consider it |
| --- | --- | --- |
| LangChain | Build an agent by composing a model, tools, a prompt and middleware: rules that intervene during execution. | The model must choose tools and continue across multiple turns; recurring integrations and policies justify a shared harness. |
| LangGraph | Orchestrate a process as a graph: nodes perform operations, edges determine the next step and state holds the working data. | The application needs explicit control over branches, waits and resumption. |
| OpenRouter | Access models through an API and control routing to the providers serving them. | Several models or shared access and routing policies need a common integration point. |
| Langfuse | Reconstruct executions through traces, manage prompts and evaluate results. | The team needs to locate a failing step, compare variants or work with a set of evaluation cases. |

A *harness* is the code surrounding the model that governs its use: it executes requested tools, returns results and applies limits and stopping conditions. LangChain agents use LangGraph as their runtime; LangGraph can also be used without LangChain. Choosing LangChain and designing a graph directly therefore offer different levels of control. [LangChain overview](https://docs.langchain.com/oss/python/langchain/overview), [LangGraph overview](https://docs.langchain.com/oss/python/langgraph/overview).

OpenRouter also provides more than a gateway. Its Agent SDK handles the model–tool loop, approvals and state that can be persisted. Langfuse can integrate with applications that use neither framework. The table is a starting map; the two designs below assign the actual responsibilities. [OpenRouter Agent SDK](https://openrouter.ai/docs/agent-sdk/call-model/tool-approval-state), [Langfuse and OpenTelemetry](https://langfuse.com/integrations/native/opentelemetry).

## Categorising a ticket: start with the provider SDK

Take a bounded case: a ticket arrives, the model proposes one of the allowed categories and the application saves it. The existing service can handle the whole path:

1. The service reads the authorised ticket and prepares the request.
2. The provider SDK sends one model call.
3. Application code validates the response structure and checks that the category exists.
4. It saves an accepted result; invalid output follows the product's review path.

A category can be structurally valid and still be wrong. Detecting that requires tickets with reference classifications and an acceptance criterion. Format validation and checking the meaning serve different purposes.

I would initially leave all four tools out of this architecture.

_Categorising a ticket: start with the provider SDK_

| Component left out | Reason | What would change the decision |
| --- | --- | --- |
| LangChain | Code already controls the only call; there is no agent loop to manage. | The model must consult tools and choose how to continue, or integrations and shared policies accumulate. |
| LangGraph | The case finishes within the request and does not need later resumption. | The process acquires waits, branches and recovery paths it must preserve. |
| OpenRouter | The provider is already selected and its SDK meets the access requirement. | The team needs to compare models or centralise routing rules. |
| Langfuse | Existing logs still answer the initial diagnostic question. | The team needs to correlate steps, prompt versions and outcomes across many cases. |

I would retain the ticket identifier, requested model, operation stage, duration, outcome and error details. Those records help distinguish a failure in the call from one in validation or saving. For the classifier, I give the application service ownership of the time limit and maximum number of attempts. SDK retries must fit within that budget too; if they cannot be controlled, I disable them and handle retries in the service. Once the budget is exhausted, the ticket goes to review.

I agree with Anthropic's recommendation to start with the simplest solution suited to the task and add complexity when it addresses a recognisable need. [Building effective agents](https://www.anthropic.com/research/building-effective-agents).

## A purchase waiting for approval: make the process explicit

Now the requirement changes. The application gathers authorised data, prepares a purchase proposal and waits for a manager. An incomplete proposal returns for preparation; a rejected one closes; an approved one can become an order. The wait can last hours, and the service may restart meanwhile.

I would choose LangGraph here when I want these steps and branches represented in the workflow code. The proposed architecture is:

1. **Preparation.** Gather the data and produce a proposal with a version, amount and recipient.
2. **Validation.** Check the purchase requirements. Missing information sends the proposal back for preparation with the errors to correct.
3. **Approval.** Preserve the proposal and pause the process. The interface shows the manager the exact version being reviewed; the backend authenticates the user and checks their authority to approve it.
4. **Resumption.** Load the same case. A rejection closes it; a changed proposal needs new approval.
5. **Order submission.** A separate node checks permissions again, submits the approved action and records the identifier returned by the external system. An uncertain outcome goes to reconciliation before another order is attempted.

For the purchase, the workflow keeps the case deadline and attempts already consumed in its state. Every node and client works within the remaining budget; resumption does not reset it after an error. When it runs out, the case goes to manual review. An uncertain order outcome follows the reconciliation path rather than authorising another submission.

In LangGraph, a *checkpointer* saves snapshots of an individual process's state, identified by a `thread_id`. Recovery after a restart needs durable storage: `InMemorySaver` holds data only in the process's RAM. That configuration applies to library use; Agent Server manages the persistence infrastructure. A *Store* holds application data across threads, so it is not needed solely to pause this case. [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence).

The process pauses with `interrupt()` and resumes through `Command(resume=...)` on the same thread. The interrupted node starts again from the beginning, which is why I place order submission in the node after approval. The external action still needs a stable idempotency key: repeating the same request should recover the existing order rather than create another. If the recipient does not support that contract, the integration needs a reconciliation procedure. [Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts).

One failure point needs a specific test: the order may be accepted while the local save fails. The graph then has incomplete state, although the purchase already exists externally. Separating nodes makes the transition visible; handling that uncertain outcome belongs to the integration with the receiving system.

### When I would leave LangGraph out too

A single approval before a tool call can be handled by OpenRouter's Agent SDK using `requireApproval` and a `StateAccessor` connected to persistent storage. Its `deferred` tools can wait for an external system and receive a result from another process. “We need to wait for a person” is therefore insufficient by itself to justify a graph. [Tool approval and state](https://openrouter.ai/docs/agent-sdk/call-model/tool-approval-state), [Deferred tools](https://openrouter.ai/docs/agent-sdk/call-model/async-tools).

If that loop already meets the process requirements, I would keep it. For the proposal with corrections, rejections and reconciliation, I would choose a graph when it makes ownership of each transition clearer. If the application already has a suitable persistent state machine, I would first consider extending it: adopting LangGraph also means maintaining its state model, dependencies and recovery policies.

A direct model call may be enough inside a node. LangChain becomes relevant if that particular step needs an agent with tools; OpenRouter can provide model access; Langfuse can observe the path. I would not add them simply to fill out the stack.

## LangChain: delegating the agent loop and integrations

LangChain is useful for an assistant that consults several tools and uses their responses to decide what to do next. The harness runs the loop, while middleware can intervene in calls, tools, limits and retries. [Middleware](https://docs.langchain.com/oss/python/langchain/middleware/overview).

The choice makes sense when it replaces repeated integrations and rules with behaviour the team can read and change. For a short sequence fixed in code, writing the functions directly may remain easier to follow.

I would pay particular attention to retry ownership. If the workflow repeats a node, the client retries a call and the tool repeats an external action, one error can trigger attempts at several layers. I would give the task an overall budget and expose every attempt, including failed ones. The tool contract remains central: inputs, results and authorisation are covered in the [MCP guide](https://www.matteovincenti.com/en/blog/mcp-server-tools-skills-guide).

## OpenRouter: separate model, provider and fallback decisions

A model may be served by several providers. OpenRouter routing therefore involves both the model and the conditions for reaching the operator that runs it. The `provider.only` field restricts allowed providers; `order` expresses a preference. With `allow_fallbacks` enabled, the service can try providers beyond those preferred. [Provider routing](https://openrouter.ai/docs/guides/routing/provider-selection).

Model fallback is a separate decision: a `models` list permits alternative models after an error. The response identifies the model actually used. For a service, that may be an availability choice; in a model comparison, it may change the very variable under investigation. [Model fallbacks](https://openrouter.ai/docs/guides/routing/model-fallbacks).

I would keep those two permissions separate in configuration and execution records. When a capability is essential, `provider.require_parameters: true` excludes providers that do not support the requested parameters. Parameter support remains separate from answer correctness.

Before adoption, I would also follow the data path. OpenRouter's policy and the provider's policy need separate checks; disabling content logging does not mean no metadata is retained. A gateway adds a shared configuration point and a service to include in error diagnosis. [Data collection](https://openrouter.ai/docs/guides/privacy/data-collection).

## Langfuse: reconstruct a failure, then evaluate its effect

A trace connects the operations in a request: model calls, document retrieval, tools and application steps. For the purchase, I want to see which proposal was approved, which attempt reached the external system and where its result was lost. That is a reason to introduce structured observability. [Langfuse observability](https://langfuse.com/docs/observability/overview).

Evaluation answers another question. The classifier's trace may contain no execution errors and still show the wrong category. In Langfuse, manual or automated evaluations can be linked to cases and executions, but the criterion needs to be chosen: agreement with a reference category, proposal completeness or respect for approval. [Evaluation](https://langfuse.com/docs/evaluation/overview).

Before exporting data, I would choose what to collect and how long to retain it. Masking removes or replaces data before export. It needs checking on the actual export path. Current Python documentation recommends `mask_otel_spans` for new setups; the older `mask` hook has different coverage. The hook does not modify other export paths or every property of a span, an individual traced operation. Sensitive data elsewhere needs separate checks. I would use recognisable synthetic values to inspect inputs, outputs, metadata and errors before they leave the application. [Masking](https://langfuse.com/docs/observability/features/masking).

Hosting also affects the decision. Langfuse Cloud is managed by the vendor; with self-hosting, the team takes responsibility for infrastructure, updates and retention. The Docker Compose configuration presented for local testing does not provide high availability and backups by itself. [Self-hosting](https://langfuse.com/self-hosting).

## The test I would use to choose the purchase architecture

Before accepting the proposed design, I would put the candidate and the simplest alternative through the same case: leave a proposal awaiting approval, stop the worker, resume the case from a new process and deliver the same approval twice. The expected outcome is the correct version, authorised by the manager, with one identifiable order.

I would also fix the checkpoint durability mode and backend guarantees for the comparison. Writing to durable storage and confirming the save before advancing are related decisions; LangGraph exposes distinct durability modes. [Checkpointers](https://docs.langchain.com/oss/python/langgraph/checkpointers).

I would add a stop after external order acceptance and before the local save. That distinguishes demonstrated resumption during a wait from recovery of an action with an uncertain outcome. In a separate case, I would change the proposal while it is awaiting approval and submit the approval for its previous version: no order should be placed until the current version is approved.

If both architectures meet those requirements, resumption no longer justifies another layer. The decision then depends on how readable the process is and how much maintenance it requires as approvals, branches and integrations change.
