When developing complex AI agents, we often encounter situations where linear LangChain Expression Language (LCEL) chains stop being effective. When cyclical processing, conditional transitions based on intermediate step results, or pauses for human confirmation are required, LCEL falls short. That's exactly where LangGraph comes in—a library that extends LangChain and allows building agents as directed graphs with explicit state. Our experience deploying LangGraph in production includes projects for fintech, logistics, and legal services. We guarantee a fault-tolerant architecture and full documentation.
LangGraph implements the StateGraph concept, where each node is a function and edges are transitions. The agent's state is described with a TypedDict and can automatically merge messages. This makes it easy to implement loops and multi-agent systems.
Why agents need a graph, not a chain?
Linear LCEL chains are fine for simple pipelines: take input, apply a sequence of steps, get output. But real agent scenarios often require going back to a previous step, running parallel checks, or pausing execution for manual control. LangGraph's graph model solves these problems naturally: cycles are edges leading back; parallelism is multiple nodes executing simultaneously; human-in-the-loop is built-in interruptions.
Basic graph structure
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from typing import TypedDict, Annotated
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add] # Automatically concatenated
user_id: str
iteration_count: int
llm = ChatOpenAI(model="gpt-4o")
def agent_node(state: AgentState) -> AgentState:
response = llm.bind_tools(tools).invoke(state["messages"])
return {"messages": [response], "iteration_count": state["iteration_count"] + 1}
def should_continue(state: AgentState) -> str:
last_msg = state["messages"][-1]
if last_msg.tool_calls:
return "tools"
return END
# Build the graph
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode(tools))
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent") # Loop: after tools, back to agent
app = graph.compile(checkpointer=MemorySaver())
Persistent state and interrupts
LangGraph supports checkpoint saving between runs and pauses for human approval:
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg import Connection
# Persistence in PostgreSQL
conn = Connection.connect("postgresql://user:pass@localhost/langgraph_db")
checkpointer = PostgresSaver(conn)
# Interrupt: graph stops before the specified node
app = graph.compile(
checkpointer=checkpointer,
interrupt_before=["execute_payment"], # Requires human confirmation
)
config = {"configurable": {"thread_id": "order_12345"}}
# Run until the break point
result = app.invoke({"messages": [HumanMessage("Pay the invoice for 50000 RUB")]}, config)
# Graph stopped before execute_payment
# After human review, continue
app.invoke(None, config) # None = resume from current state
Multi-agent: Supervisor pattern
from langgraph.graph import StateGraph, END
from typing import Literal
class SupervisorState(TypedDict):
messages: Annotated[list, operator.add]
next_agent: str
AGENTS = ["researcher", "analyst", "writer"]
supervisor_prompt = f"""You are a supervisor of a multi-agent system.
Based on the request and current progress, choose the next agent: {AGENTS}
Or return FINISH if the task is complete.
"""
def supervisor_node(state: SupervisorState):
response = llm.with_structured_output(
{"next": {"type": "string", "enum": AGENTS + ["FINISH"]}}
).invoke([{"role": "system", "content": supervisor_prompt}] + state["messages"])
return {"next_agent": response["next"]}
def route_to_agent(state: SupervisorState) -> str:
if state["next_agent"] == "FINISH":
return END
return state["next_agent"]
# Create agents
def make_agent_node(name: str, system_prompt: str):
agent_llm = ChatOpenAI(model="gpt-4o").bind_tools(get_tools_for(name))
def node(state):
result = agent_llm.invoke(
[{"role": "system", "content": system_prompt}] + state["messages"]
)
return {"messages": [result]}
return node
graph = StateGraph(SupervisorState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("researcher", make_agent_node("researcher", "Research the topic and find facts"))
graph.add_node("analyst", make_agent_node("analyst", "Analyze data and draw conclusions"))
graph.add_node("writer", make_agent_node("writer", "Formulate the final answer"))
graph.set_entry_point("supervisor")
graph.add_conditional_edges("supervisor", route_to_agent)
for agent in AGENTS:
graph.add_edge(agent, "supervisor")
multi_agent = graph.compile()
Streaming and real-time output
# Streaming events from the graph
async for event in app.astream_events(
{"messages": [HumanMessage("Analyze Q1 sales")]},
config={"configurable": {"thread_id": "analysis_001"}},
version="v2",
):
kind = event["event"]
if kind == "on_chat_model_stream":
print(event["data"]["chunk"].content, end="", flush=True)
elif kind == "on_tool_start":
print(f"\n[Tool call: {event['name']}]")
elif kind == "on_tool_end":
print(f"[Tool result received]")
How nested graphs simplify modularity?
LangGraph supports SubGraphs—nested graphs. You can define a graph for document processing and then include it as a regular node in the parent graph. This allows decomposing complex systems into reusable components.
# Subgraph for document processing
doc_graph = StateGraph(DocumentState)
doc_graph.add_node("extract", extract_text)
doc_graph.add_node("classify", classify_document)
doc_graph.add_node("validate", validate_structure)
# ... build the subgraph
doc_subgraph = doc_graph.compile()
# Include subgraph in parent
main_graph = StateGraph(MainState)
main_graph.add_node("process_document", doc_subgraph) # Subgraph as node
main_graph.add_node("send_result", send_to_crm)
main_graph.add_edge("process_document", "send_result")
Practical case: contract review system (from our practice)
Task: one of our clients—a legal department of a large company—received 30–50 contracts daily. Each contract required 1–2 hours of lawyer time. We built a graph agent on LangGraph that automated the review.
Graph:
- extract_node — parse PDF, extract structure
- classify_node — contract type (supply, services, lease, NDA)
- risk_check_node — parallel checks: financial terms, duration, liability
- legal_rules_node — check against corporate list of prohibited clauses
- human_review — interrupt for contracts with
risk_score > 7 - finalize_node — generate conclusion and recommendations
app = graph.compile(
checkpointer=PostgresSaver(conn),
interrupt_before=["human_review"], # Pause only for risky ones
)
Routing: low risk → automatic approval; high risk → pause for lawyer with agent's draft conclusion.
Results:
- Standard contract review time: 90 min → 8 min
- Automatic approval without lawyer: 61% of contracts
- Missed non-standard clauses: 0 (vs ~3% manual due to fatigue)
- Legal department workload: -58%
Comparison of LangGraph and LCEL
| Criteria | LCEL | LangGraph |
|---|---|---|
| Structure | Linear chain | Arbitrary graph |
| Loops | No | Yes |
| State | Passed via pipe | TypedDict with merge strategy |
| Checkpoint | No | PostgreSQL, Redis, SQLite |
| Human-in-the-loop | No | interrupt_before/after |
| Use case | Simple pipelines | Agents, multi-agent systems |
LangGraph components
| Component | Purpose | Example |
|---|---|---|
| StateGraph | Defines state and nodes | StateGraph(AgentState) |
| Node | Processing function | agent_node |
| Edge | Connection between nodes | graph.add_edge("tools", "agent") |
| Conditional Edge | Conditional transition | should_continue |
| Checkpointer | State persistence | MemorySaver, PostgresSaver |
| Interrupt | Pause for HITL | interrupt_before=["node"] |
What our LangGraph integration includes
- Architecture session: analyze the task, select graph topology, define human-in-the-loop points.
- Code development: implement nodes, edges, state, integrate with your LLM and tools.
- Persistence setup: PostgreSQL, Redis, or SQLite for checkpoints.
- Environment integration: deploy via Docker/Kubernetes, connect monitoring (LangSmith).
- Testing: unit tests, load testing with latency recording (p99).
- Documentation: full graph description, API, operation guide.
- Team training: workshop on LangGraph for your developers.
Estimated timelines
- Basic ReAct agent on LangGraph: 3 to 5 days.
- Multi-agent system with supervisor: 2 to 3 weeks.
- Human-in-the-loop workflow with persistence: 1 to 2 weeks.
- Production integration with PostgreSQL checkpoint: +3–5 days.
Pricing is determined individually—we assess the project free of charge within one business day. Contact us for a consultation: our certified engineers will help choose the architecture for your task. Request a preliminary assessment, and we will prepare a commercial proposal with a quality guarantee and transparent timelines.







