This is the first post in our LangChain fundamentals series. Later posts cover RAG applications, memory, agents and tools, and debugging.
A single call to a model's chat completions API, covered directly in our OpenAI series, handles a huge share of real use cases on its own — plenty of applications never need anything more. LangChain earns its place once an application needs to compose several steps together: format a prompt from a template, call a model, parse the structured result, feed it into a second call — as one coherent, reusable pipeline, rather than hand-wiring that sequence with custom glue code every time.
Prompt templates: parameterizing prompts properly
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a technical writer explaining {topic} to {audience}."),
("user", "{question}"),
])
formatted = prompt.invoke({
"topic": "Kubernetes",
"audience": "engineers new to containers",
"question": "What's the difference between a Pod and a container?",
})This is the same instinct as a Jinja2 template in Ansible, or a parameterized Terraform module, applied to prompts specifically: the structure of the prompt is defined once, and the actual variable content is supplied per call — rather than string-concatenating a prompt by hand throughout an application's code, with the risk of subtle formatting inconsistencies creeping in at each call site.
Models: a consistent interface across providers
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o", temperature=0.2)
response = model.invoke(formatted)
print(response.content)LangChain wraps a given provider's actual API (OpenAI, Anthropic, and many others each have their own integration package) behind a consistent .invoke() interface. The genuine value here isn't hiding complexity that wasn't there — it's that swapping the underlying model provider, or comparing two providers on the same task, means changing one line (ChatOpenAI to, say, ChatAnthropic) rather than rewriting every call site's provider-specific API calls throughout an application.
Output parsers: turning a response into a usable value
from langchain_core.output_parsers import StrOutputParser
chain = prompt | model | StrOutputParser()
result = chain.invoke({
"topic": "Kubernetes",
"audience": "engineers new to containers",
"question": "What's the difference between a Pod and a container?",
})
print(result)A Pod is Kubernetes' smallest deployable unit — a thin wrapper around
one or more containers that are always scheduled together and share a
network namespace...
That | operator is LangChain's LCEL (LangChain Expression Language) — chaining a prompt template, a model, and an output parser into a single callable pipeline. Each stage's output becomes the next stage's input automatically: the prompt template produces formatted messages, the model consumes them and produces a response object, StrOutputParser extracts just the plain text string from that response object. Three separate concerns, composed into one reusable, callable chain.
Structured output parsing
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel
class CourseRecommendation(BaseModel):
course_name: str
reason: str
urgency: str
prompt = ChatPromptTemplate.from_messages([
("system", "Recommend a course based on the user's stated need. "
"Respond with JSON matching: course_name, reason, urgency."),
("user", "{request}"),
])
chain = prompt | model | JsonOutputParser(pydantic_object=CourseRecommendation)
result = chain.invoke({"request": "My team needs to learn container orchestration fast"})
print(result){'course_name': 'Kubernetes Fundamentals', 'reason': 'Directly addresses container orchestration', 'urgency': 'high'}
This is the same structured-output guarantee covered in our OpenAI series, expressed through LangChain's chain composition instead of calling the provider's structured-output parameter directly — useful specifically when this step is one part of a larger, multi-stage chain, rather than a single standalone call.
Runnables: the shared interface behind everything
Every piece in a chain — prompt templates, models, parsers, and (as later posts in this series cover) retrievers and tools — implements the same Runnable interface: .invoke() for a single call, .batch() for many inputs at once, .stream() for incremental output. This uniformity is what makes the | composition operator work at all — LCEL doesn't need special-case logic for "a prompt template followed by a model" versus "a retriever followed by a parser," because both sides of every | are just Runnables with the same interface.
results = chain.batch([
{"topic": "Kubernetes", "audience": "beginners", "question": "What is a Pod?"},
{"topic": "Terraform", "audience": "beginners", "question": "What is a provider?"},
]).batch() runs a chain across multiple inputs, with LangChain handling concurrency underneath — genuinely useful for processing many independent requests (a batch of support tickets, a set of documents to summarize) without writing your own threading or async logic to parallelize the calls.
Few-shot prompting inside a template
Beyond simple variable substitution, prompt templates support genuinely structured few-shot examples — the same technique covered as an alternative to fine-tuning in our OpenAI series, expressed as a reusable template component rather than a hand-assembled string:
from langchain_core.prompts import FewShotChatMessagePromptTemplate
examples = [
{"input": "What's a Pod?", "output": "A Pod is Kubernetes' smallest deployable unit."},
{"input": "What's a Service?", "output": "A Service provides a stable address for a set of Pods."},
]
example_prompt = ChatPromptTemplate.from_messages([
("human", "{input}"),
("ai", "{output}"),
])
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
final_prompt = ChatPromptTemplate.from_messages([
("system", "Answer concisely, in the style of these examples."),
few_shot_prompt,
("human", "{question}"),
])FewShotChatMessagePromptTemplate formats the entire examples list into properly-structured conversation turns automatically, inserted before the actual user question — genuinely more maintainable than hand-concatenating example strings into a system prompt, especially once an application has more than a couple of examples or needs to swap in a different example set for a different context.
Partial variables: binding some values ahead of time
A template sometimes has values known well before the actual call — a fixed persona, a constant configuration value — that shouldn't need to be re-supplied on every single invocation:
prompt = ChatPromptTemplate.from_messages([
("system", "You are a technical writer at NovuSpark, explaining {topic} to {audience}."),
("user", "{question}"),
]).partial(audience="engineers new to the topic")
formatted = prompt.invoke({"topic": "Kubernetes", "question": "What's a Pod?"}).partial() pre-fills audience once, leaving only topic and question to be supplied per actual call — a small but genuinely useful pattern once a template has several variables and some of them are effectively constant for a given deployment or use case, rather than something every call site needs to remember to pass explicitly.
Fallback chains: handling a provider outage gracefully
.with_fallbacks() lets a chain automatically retry against a different model or provider if the primary one fails or times out:
primary = ChatOpenAI(model="gpt-4o")
fallback = ChatAnthropic(model="claude-3-5-sonnet-20241022")
resilient_model = primary.with_fallbacks([fallback])A request that fails against OpenAI (a rate limit, an outage) automatically retries against Anthropic instead — genuinely useful for a production application where the consistent-interface benefit of LangChain's model abstraction pays off directly: swapping providers on failure is a configuration detail, not a rewrite.
Where a chain actually earns its complexity
A single prompt-model-parser chain, on its own, is genuinely not much simpler than calling the OpenAI SDK directly — and for a single call, calling the SDK directly is often the more transparent, more debuggable choice. LangChain's real value shows up in composing multiple chains together — the output of one becomes the input to constructing the next, conditionally, with retrieved context injected in the middle. That's exactly the shape RAG pipelines and multi-step agents take, covered in the next posts in this series.
What to actually remember from this post
- A single LLM call rarely needs LangChain — it earns its complexity once you're composing multiple steps into a pipeline.
- Prompt templates parameterize prompts the same way a Terraform module or Ansible role parameterizes infrastructure — structure defined once, values supplied per call.
- LCEL's
|operator chains prompt → model → parser into one reusable, callable pipeline, each stage's output feeding the next automatically. - Every stage shares the same
Runnableinterface (.invoke(),.batch(),.stream()) — that uniformity is what makes arbitrary composition via|work at all. - The consistent model interface is what makes swapping providers cheap — a genuine advantage once an application might need to compare or switch between them.
Next in the series: Building RAG Applications with LangChain, where this chain-composition pattern becomes a full retrieval-augmented pipeline.
