Almost every LangChain project you'll build starts with the same three pieces, piped together: a prompt template, a model, and something that shapes the model's reply into what your code actually needs. Get comfortable with this trio and you can build a surprising amount before you ever need anything fancier.
Prompt templates: reusable prompts with blanks to fill in
Instead of string-concatenating your prompt by hand every time, a ChatPromptTemplate lets you define the shape once, with variables you fill in per call:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a concise assistant. Answer in one sentence."),
("human", "{question}"),
])
For anything conversational, you'll also reach for MessagesPlaceholder, which reserves a spot in the template for a whole list of prior messages - the chat history - rather than a single string variable.
Models: one interface, swap providers freely
LangChain's chat model classes all implement the same interface, so a chain built against one provider works against another with essentially a one-line change:
from langchain.chat_models import init_chat_model
model = init_chat_model("claude-sonnet-5", model_provider="anthropic")
# swapping to a different provider is just a different string here
init_chat_model is the current recommended entry point precisely because it keeps that provider choice as configuration rather than baked into your imports - handy when you want to compare models or swap providers without touching the rest of your chain.
Output parsers: turning a reply into something usable
A raw model response is a message object, not automatically the plain string or structured value your downstream code wants. That's what output parsers are for.
For plain text, StrOutputParser just extracts the text content:
from langchain_core.output_parsers import StrOutputParser
chain = prompt | model | StrOutputParser()
answer = chain.invoke({"question": "What's LCEL in one line?"})
For anything you actually want to use programmatically - a category, a confidence score, a list of extracted fields - the current recommended path is with_structured_output, which takes a schema (usually a Pydantic model) and gets the model to return data matching it directly, rather than you writing a custom parser to coax structure out of free text:
from pydantic import BaseModel
class Sentiment(BaseModel):
label: str # "positive", "neutral", or "negative"
confidence: float
structured_model = model.with_structured_output(Sentiment)
result = (prompt | structured_model).invoke({"question": review_text})
print(result.label, result.confidence)
Same prompt and model, two different endings - which parser you attach decides what shape comes out.
Putting it together, and streaming it
Every piece above is a Runnable, which is why they pipe together so cleanly - and it's also why every chain you build this way automatically gets a few things for free, including .stream() for token-by-token output instead of waiting for the whole response:
for chunk in chain.stream({"question": "Explain RAG in two sentences."}):
print(chunk, end="", flush=True)