GenAI Foundations / Beginner Track Module 8 / 9
GenAI Foundations Beginner ⏱ 20 min
DEV

How LangChain Connects Everything Together

LangChain's LCEL syntax lets you chain prompt → model → parser in a single expression. Build your first AI pipeline in 10 lines.

How to Use This Lesson

  • Start with the user problem, then map the pattern to architecture and failure modes.
  • If a code or design example is included, change one assumption and reason through the impact.
  • Use role callouts, checklists, and Q&A sections as implementation or interview prep notes.

Prerequisites: 07-prompt-templates

What LangChain Actually Solves

You’ve now built individual pieces: a prompt template, an API call, a Pydantic model for output validation. In a simple application, you wire these together with regular Python code:

# Without LangChain
rendered = template.render(product=product_name, review=review_text)
response = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": rendered}])
result = SentimentResult.model_validate_json(response.choices[0].message.content)

That’s fine for one use case. But when you have dozens of different AI pipelines in the same application - each with different templates, models, parsers, and retry logic - you end up with a lot of boilerplate that’s hard to read, hard to test, and hard to extend.

LangChain is glue. It’s a framework that standardizes how you connect prompts, models, and output parsers, so you can focus on the logic instead of the plumbing.

The LCEL Pipe Syntax

LangChain Expression Language (LCEL) uses Python’s pipe operator (|) to chain components together:

chain = prompt | llm | parser

This reads: “Take the prompt, pass it through the LLM, pass the output through the parser.” The | operator wires the output of each component to the input of the next.

To run the chain:

result = chain.invoke({"product": "DataSync Pro", "review": "The API is fast but docs are lacking"})

That’s the entire pipeline: template rendering, API call, and output parsing in a single .invoke() call.

The Three Core Components

LangChain Chain Architecture (LCEL)

flowchart LR
  subgraph Chain ["LangChain Chain (LCEL)"]
      PT[PromptTemplate] -->|"pipe: |"| LLM[ChatOpenAI]
      LLM -->|"pipe: |"| OP[OutputParser]
  end
  IN[Input Variables] --> PT
  OP --> OUT[Typed Output]

  style Chain fill:#f8f4ff,stroke:#7c3aed
  style PT fill:#dbeafe,stroke:#2563eb,color:#1d4ed8
  style LLM fill:#f3e8ff,stroke:#7c3aed,color:#7c3aed
  style OP fill:#dcfce7,stroke:#16a34a,color:#15803d
  style OUT fill:#dcfce7,stroke:#16a34a,color:#15803d
Code copied! Link copied!

PromptTemplate

The prompt template you already know. In LangChain, it’s a first-class component that knows about its input variables:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a product review analyst. Be concise and precise."),
    ("user", "Analyze this review for {product}:\n\n{review}\n\nReturn JSON with sentiment, score (1-5), and key_points (list).")
])

ChatOpenAI (the LLM)

This is LangChain’s wrapper around the OpenAI chat API. It takes messages in and returns a message object out:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

Output Parser

The output parser transforms the model’s raw text output into a structured Python object. LangChain provides several:

  • StrOutputParser - returns the string content
  • JsonOutputParser - parses JSON
  • PydanticOutputParser - validates against a Pydantic model
from langchain_core.output_parsers import JsonOutputParser

parser = JsonOutputParser()

Complete Working Example

LangChain Chain: Review Analysis with Sentiment + Score

Example code (static). Copy and run locally in your own environment.

import os
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List

# Step 1: Define the output schema
class ReviewAnalysis(BaseModel):
  sentiment: str = Field(description="positive, neutral, or negative")
  score: int = Field(description="Rating from 1 to 5", ge=1, le=5)
  key_points: List[str] = Field(description="2-4 key points from the review")
  recommended_action: str = Field(description="What the product team should do")

# Step 2: Build the prompt template
prompt = ChatPromptTemplate.from_messages([
  (
      "system",
      """You are a senior product analyst. Analyze customer reviews objectively.
Always respond with valid JSON matching the exact schema specified."""
  ),
  (
      "user",
      """Analyze the following customer review for the product "{product_name}".

Review:
{review_text}

Return JSON with these exact fields:
- sentiment: "positive", "neutral", or "negative"
- score: integer 1-5 (1=very negative, 5=very positive)
- key_points: list of 2-4 specific points the customer raised
- recommended_action: what the product team should do with this feedback"""
  )
])

# Step 3: Set up the model
llm = ChatOpenAI(
  model="gpt-4o-mini",
  temperature=0.2,
  model_kwargs={"response_format": {"type": "json_object"}}
)

# Step 4: Set up the parser
parser = JsonOutputParser(pydantic_object=ReviewAnalysis)

# Step 5: Build the chain with the pipe operator
chain = prompt | llm | parser

# Step 6: Run it
result = chain.invoke({
  "product_name": "DataSync Pro",
  "review_text": """I have been using DataSync Pro for 3 months. The API performance
  is excellent  -  sync jobs that took 2 hours with our old tool now take 20 minutes.
  However, the documentation is seriously lacking. I spent 2 days figuring out
  webhook configuration that should have taken 30 minutes with proper docs."""
})

print("Sentiment:", result["sentiment"])
print("Score:", result["score"])
print("Key Points:")
for point in result["key_points"]:
  print(f"  - {point}")
print("Action:", result["recommended_action"])

# The chain is reusable  -  run a second review with the same chain
result_2 = chain.invoke({
  "product_name": "DataSync Pro",
  "review_text": "Works as advertised. Nothing special, nothing broken. Gets the job done."
})
print("\nReview 2 sentiment:", result_2["sentiment"], "| score:", result_2["score"])

Batch Processing with LangChain

One of LangChain’s practical advantages is built-in batch processing. Instead of calling invoke() in a loop, use batch():

reviews = [
    {"product_name": "DataSync Pro", "review_text": "Amazing product, changed our workflow completely"},
    {"product_name": "DataSync Pro", "review_text": "Buggy, crashes daily, avoid"},
    {"product_name": "DataSync Pro", "review_text": "Decent but expensive for what it does"},
]

# Process all three concurrently (LangChain handles the async)
results = chain.batch(reviews, config={"max_concurrency": 3})

for i, result in enumerate(results):
    print(f"Review {i+1}: {result['sentiment']} ({result['score']}/5)")

This is significantly faster than sequential calls and uses fewer lines than manual asyncio management.

Streaming Responses

For long outputs (reports, summaries, code generation), LangChain makes streaming easy:

for chunk in chain.stream({"product_name": "DataSync Pro", "review_text": long_review}):
    print(chunk, end="", flush=True)

Each chunk is a partial result as tokens arrive from the model. This lets you show progressive output in your UI without waiting for the full response.

When to Use LangChain (and When Not To)

⚙️ For Developers

The honest assessment: LangChain adds abstraction overhead. For a simple single-call scenario - one prompt, one model, one response - the raw OpenAI SDK is cleaner and easier to debug. You get direct control and can see exactly what’s happening.

Use LangChain when you need: (1) chains with multiple steps, (2) retrieval-augmented generation (RAG) with a vector database, (3) agents that use tools, (4) batch processing with concurrency, or (5) streaming with structured parsing. For everything else, the raw SDK is the right choice.

Production Gotcha: Log Your Rendered Prompts

LangChain’s abstractions can hide what’s actually being sent to the model. A bug in your template or unexpected variable values might produce a malformed prompt - and you won’t see it unless you log it explicitly.

In development, add a callback to print what LangChain is actually sending:

from langchain.callbacks import StdOutCallbackHandler
chain.invoke(inputs, config={"callbacks": [StdOutCallbackHandler()]})

Or use LangSmith (LangChain’s tracing product) to see every prompt, response, and token count across your entire application. For any production LangChain deployment, LangSmith is worth the setup cost.

Installation

pip install langchain langchain-openai langchain-core

For LangSmith tracing (recommended for debugging production issues):

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=your_langsmith_api_key

What’s Next

You’ve now built all the core components of an AI application: API calls, prompt engineering, structured output, schemas, templates, and pipelines. The final tutorial brings it all together - showing you how real-world production AI applications are structured and what fails at each layer.

The One-Line Summary

LangChain’s value is the pipe operator: chain = prompt | llm | parser. If you find yourself writing that wiring manually for every AI call in your app, LangChain will save you significant time. If you only have one or two AI calls, the raw SDK is simpler.

Interview Notes: LangGraph as the Successor Pattern

LangChain is useful for prompt-model-parser chains and integrations. For agents with branching, retries, checkpoints, and human approval, LangGraph-style state machines are the more production-ready pattern.

type AgentState = {
  task: string;
  status: "plan" | "use_tool" | "need_approval" | "done";
  toolResults: unknown[];
};

function nextNode(state: AgentState) {
  if (state.status === "need_approval") return "approval_gate";
  if (state.status === "use_tool") return "tool_executor";
  if (state.status === "done") return "finish";
  return "planner";
}

In interviews, explain the tradeoff: chains are simple and linear; graphs make state, loops, and recovery explicit.

Interview Practice

  1. What problem does LangChain solve for beginners?
  2. What is LCEL useful for?
  3. When should you move from a linear chain to a graph/state-machine design?
  4. Why is LangGraph a better fit for durable agents?
  5. What are the risks of hiding too much behavior inside framework abstractions?
  6. How would you test a prompt-model-parser chain?