The LangChain Expression Language (LCEL) transforms how we build LLM workflows. Instead of managing execution flow manually, LCEL lets you compose components declaratively—like Unix pipes for AI. Combined with tool integration, LCEL enables building agents that reason and act in the real world.
The Philosophy Behind LCEL
Before LCEL, building LLM workflows meant manually orchestrating each step. You’d invoke a prompt template, pass the result to a model, then parse the output—all with explicit variable handling and error checking at each stage. This approach works but creates verbose, error-prone code that’s difficult to modify.
LCEL introduces a declarative paradigm borrowed from functional programming and Unix shell philosophy. The pipe operator (|) connects components, with each stage automatically receiving the previous stage’s output. This isn’t just syntactic sugar—it enables powerful features like automatic batching, streaming, and async execution.
1 | # Traditional approach - explicit orchestration |
The LCEL version isn’t just shorter—it’s semantically richer. The chain object understands its complete structure. It can introspect inputs and outputs at each stage, optimize execution, and handle errors consistently.
The Runnable Protocol: A Universal Interface
Every component in LangChain implements the Runnable interface. This design decision, inspired by the Gang of Four’s Strategy pattern, enables polymorphism across wildly different components. A prompt template and a language model share the same interface despite doing completely different things.
graph LR
subgraph "Runnable Interface"
A[invoke] --> B[Single Input → Output]
C[batch] --> D[Multiple Inputs → Outputs]
E[stream] --> F[Chunked Output]
end
classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
class A,C,E blueClass
| Method | Purpose | When to Use |
|---|---|---|
invoke() |
Process single input synchronously | Default for most operations |
batch() |
Process multiple inputs in parallel | Bulk operations, evaluations |
stream() |
Output token-by-token | Real-time chat interfaces |
ainvoke() |
Async single input | Web servers, non-blocking I/O |
The Runnable interface also provides introspection capabilities. You can examine input_schema and output_schema to understand what data flows through each component—invaluable for debugging and documentation.
Composition Patterns in LCEL
LCEL provides several composition primitives that cover most workflow patterns you’ll encounter.
Sequential Chains: The Pipe Operator
The simplest pattern chains components end-to-end. Each component receives the previous one’s output as input.
1 | from langchain_openai import ChatOpenAI |
This chain expects a dictionary with an article key, produces a prompt, sends it to GPT-4o, and extracts the string content from the response. The entire pipeline is a single Runnable with invoke(), batch(), and stream() methods.
Parallel Execution: RunnableParallel
When you need multiple operations on the same input, RunnableParallel executes branches concurrently and collects results into a dictionary.
graph TD
A[Input Text] --> B[RunnableParallel]
B --> C[Summary Chain]
B --> D[Sentiment Chain]
B --> E[Keywords Chain]
C --> F[Combined Output]
D --> F
E --> F
classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
class A blueClass
class B,C,D,E orangeClass
class F greenClass
This pattern is particularly powerful when combined with batch mode. If you need to analyze 100 documents with summary, sentiment, and keyword extraction, RunnableParallel multiplies your throughput by running all three analyses concurrently for each document.
The performance implications are significant. Three sequential LLM calls might take 6 seconds total; parallel execution reduces this to roughly 2 seconds—the duration of the slowest individual call.
Conditional Routing: RunnableBranch
Real applications need dynamic behavior. RunnableBranch routes inputs to different chains based on conditions, enabling specialized handling for different input types.
Consider a customer service bot that needs different expertise for technical issues versus billing questions. Rather than one overloaded prompt, you create specialized chains and route based on classification:
1 | from langchain_core.runnables import RunnableBranch |
This pattern keeps prompts focused and maintainable. Each chain can be tested independently, and adding new categories means adding new branches rather than modifying existing prompts.
Custom Logic: RunnableLambda
When you need Python logic that doesn’t fit existing components, RunnableLambda wraps any function as a Runnable. This is your escape hatch for custom transformations, validations, or side effects.
1 | from langchain_core.runnables import RunnableLambda |
The wrapped function becomes a full Runnable with automatic batching and streaming support where applicable.
Tools: Giving LLMs Hands
Language models reason but can’t act. They can explain how to check the weather but can’t actually make an API call. Tools bridge this gap by defining actions the model can request.
The Anatomy of a Tool
A tool is fundamentally a function with rich metadata. The LLM sees the function’s name, description, and parameter schema—this information guides when and how to call the tool.
1 | from langchain_core.tools import tool |
Three elements matter for effective tool design:
- Name: Clear, action-oriented.
get_stock_pricenotstockorgsp - Description: Explains when to use the tool, not just what it does
- Parameter types: Guides the model on valid inputs
The docstring is particularly important—it’s the primary mechanism by which the LLM decides whether to use this tool for a given query.
The Tool Calling Protocol
Modern LLMs don’t output tool calls as text. Instead, they return structured objects indicating which tool to call with what arguments. This is more reliable than parsing natural language outputs.
sequenceDiagram
participant User
participant LLM
participant Tool
participant App
User->>LLM: "What's Apple's stock price?"
LLM->>App: tool_calls: [{name: "get_stock_price", args: {symbol: "AAPL"}}]
App->>Tool: invoke(symbol="AAPL")
Tool->>App: {price: 150.25, change: 2.3}
App->>LLM: ToolMessage(content="{price: 150.25...}")
LLM->>User: "Apple (AAPL) is trading at $150.25, up 2.3%"
The application orchestrates this dance: receiving tool requests from the LLM, executing them, and feeding results back. The LLM then decides whether to call more tools or provide a final answer.
Binding Tools to Models
To enable tool calling, you bind tools to the model. This modifies how the model processes inputs—it now considers whether tools might be helpful before generating a response.
1 | from langchain_openai import ChatOpenAI |
The bound model returns AIMessage objects that may contain tool_calls—a list of requested tool invocations. Your application must execute these and return results as ToolMessage objects.
The ReAct Pattern: Reasoning and Acting
ReAct (Reason + Act) is an agent architecture that alternates between thinking and tool use. The model explicitly reasons about what it needs, decides on an action, observes the result, and continues reasoning.
graph TD
A[User Query] --> B[Think]
B --> C{Need Tool?}
C -->|Yes| D[Select Tool]
D --> E[Execute Tool]
E --> F[Observe Result]
F --> B
C -->|No| G[Final Answer]
classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
class A,G greenClass
class B,C orangeClass
class D,E,F blueClass
This loop continues until the model decides it has enough information to answer. The explicit reasoning traces make debugging easier—you can see exactly why the agent chose specific tools and how it interpreted results.
Why ReAct Works
ReAct’s power comes from chain-of-thought reasoning combined with grounded actions. By forcing the model to articulate its reasoning before acting, it makes fewer errors in tool selection. By observing real results rather than hallucinating, it grounds responses in facts.
Consider a query like “What’s the weather in the city where Apple headquarters is located?”. A pure LLM might confidently answer with outdated or incorrect weather data. A ReAct agent would:
- Think: “I need to find Apple’s headquarters location”
- Act: Search for Apple headquarters → Cupertino, CA
- Think: “Now I need the weather in Cupertino”
- Act: Get weather for Cupertino → 72°F, sunny
- Answer: “It’s 72°F and sunny in Cupertino, where Apple headquarters is located”
Each step is verifiable. If the answer is wrong, you can identify exactly where the reasoning failed.
Building a ReAct Agent
LangChain provides AgentExecutor to handle the ReAct loop automatically:
1 | from langchain.agents import create_react_agent, AgentExecutor |
The max_iterations parameter is crucial for production. Without it, a confused agent might loop indefinitely. The verbose flag reveals the agent’s reasoning during development.
The Limitations of LCEL for Agents
LCEL excels at static workflows—chains where the structure is known at design time. But agents pose challenges:
- Dynamic iteration: The number of tool calls isn’t known in advance
- Complex state: Beyond just passing output to input, agents need conversation history, intermediate results, and execution context
- Conditional loops: The same node (think) might be revisited many times
The tool loop we manually implemented earlier works but is brittle. What if you need:
- Parallel tool execution
- Human approval for certain actions
- Persistent state across sessions
- Debugging with state snapshots
These requirements push beyond LCEL’s design. This is precisely why LangGraph exists—it provides explicit graph structures for complex agent workflows. We’ll explore it in the next post.
Error Handling in Tool Execution
Tools interact with external systems that fail. APIs timeout, databases disconnect, rate limits kick in. Robust agents need graceful degradation.
LangChain tools support error handlers that transform exceptions into messages the LLM can understand:
1 | from langchain_core.tools import tool, ToolException |
With error handling enabled, the exception message becomes a ToolMessage that the LLM can interpret and potentially work around—perhaps by asking the user for clarification.
Key Takeaways
LCEL is declarative composition: The pipe operator creates readable chains that are more than syntax—they enable automatic optimization and introspection.
Runnables are the universal interface: Every component shares
invoke(),batch(), andstream(), making them interchangeable and composable.Composition patterns solve common needs: Sequential, parallel, conditional, and custom logic cover most workflow patterns without escaping to imperative code.
Tools extend LLM capabilities to actions: Well-designed tools with clear descriptions let LLMs interact with APIs, databases, and external systems.
ReAct combines reasoning with action: The think-act-observe loop grounds LLM responses in real data while maintaining explicit reasoning traces.
LCEL has limits for dynamic agents: Complex state, conditional loops, and human-in-the-loop require more structure than LCEL provides—enter LangGraph.
Next: LangGraph: State Graphs for Agentic Workflows - We’ll explore LangGraph’s StateGraph for building complex agents with explicit state management, cycles, and production-ready control flow.
Comments