方寸 Portal

AI Engineer Portal

Your personal operating system for career transition.

Private mode
Back to Learning Paths
Lesson

LangChain fundamentals: chains, prompts, and parsers

Learn when LangChain adds value, how LCEL composes pipelines, and how to produce structured output with Pydantic parsers.

45 min
llm-app-foundationsphase-1portfolio

Why this matters

LangChain is everywhere in AI engineering job descriptions, but it is also one of the most misunderstood tools in the ecosystem. Engineers either avoid it entirely ("too much magic") or reach for it by default and then fight its abstractions. The productive middle ground is knowing exactly what problem it solves, when it is worth its weight, and how to use its best ideas without letting it own your architecture.

For a senior engineer moving into AI roles, demonstrating clear-headed framework judgment — rather than cargo-culting or reflexive rejection — is a strong signal.

Core concepts

What LangChain solves (and when NOT to use it)

LangChain solves three real problems:

  1. Provider portability — swap OpenAI for Anthropic for a local model with one line change.
  2. Pipeline composition — chain prompt → LLM → parser into a readable, testable unit.
  3. Output parsing — structured extraction from LLM responses without boilerplate.

When NOT to use it:

  • Single-provider apps with no plans to switch. Direct SDK calls are simpler and more debuggable.
  • Streaming-heavy applications. LCEL adds latency overhead on the first token.
  • Teams unfamiliar with LangChain internals. Debugging a RunnableSequence stack trace is painful without context.
  • Tiny scripts. The import cost alone is not worth it for a 30-line utility.

Chain composition: PromptTemplate → LLM → OutputParser

The fundamental LangChain pattern is a three-step chain:

from langchain_core.prompts import ChatPromptTemplate
from langchain_anthropic import ChatAnthropic
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    ("human", "{question}"),
])

llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
parser = StrOutputParser()

chain = prompt | llm | parser

result = chain.invoke({"question": "What is LCEL?"})
print(result)

The | operator is LCEL. Each component is a Runnable with a consistent .invoke() / .stream() / .batch() interface.

LCEL (LangChain Expression Language) for readable pipelines

LCEL makes complex pipelines readable by expressing them as data flow:

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_anthropic import ChatAnthropic
from langchain_core.output_parsers import StrOutputParser

def retrieve_context(query: str) -> str:
    # Placeholder for a real retrieval step
    return f"Relevant context for: {query}"

llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")

rag_chain = (
    {"context": RunnableLambda(retrieve_context), "question": RunnablePassthrough()}
    | ChatPromptTemplate.from_messages([
        ("system", "Answer using only this context:\n{context}"),
        ("human", "{question}"),
    ])
    | llm
    | StrOutputParser()
)

answer = rag_chain.invoke("How does retrieval work?")

This reads like a pipeline diagram. Each | is a step; each dict is a data transformation. Compare this to equivalent imperative code with four separate function calls and you will see why LCEL exists.

Structured output parsing with Pydantic

For structured extraction, LangChain's .with_structured_output() method wires a Pydantic model to the LLM call automatically:

from pydantic import BaseModel, Field
from langchain_anthropic import ChatAnthropic

class SupportTicketClassification(BaseModel):
    category: str = Field(description="One of: billing, technical, account, feature_request")
    priority: str = Field(description="One of: low, medium, high, urgent")
    summary: str = Field(description="One-sentence summary of the issue")
    requires_human: bool = Field(description="True if this needs a human agent")

llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
structured_llm = llm.with_structured_output(SupportTicketClassification)

ticket = "My payment failed three times and I can't access my account. This is urgent."
result = structured_llm.invoke(ticket)

print(result.category)        # "billing"
print(result.priority)        # "urgent"
print(result.requires_human)  # True

.with_structured_output() handles tool-calling under the hood and returns a validated Pydantic object. No regex, no JSON parsing, no try/except around json.loads.

Full working example: support ticket classifier chain

from pydantic import BaseModel, Field
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate

class TicketClassification(BaseModel):
    category: str = Field(description="One of: billing, technical, account, feature_request")
    priority: str = Field(description="One of: low, medium, high, urgent")
    summary: str = Field(description="One-sentence summary")
    requires_human: bool = Field(description="True if escalation is needed")

def build_classifier():
    llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
    structured_llm = llm.with_structured_output(TicketClassification)

    prompt = ChatPromptTemplate.from_messages([
        ("system", (
            "You classify customer support tickets. "
            "Respond with accurate category, priority, and escalation need."
        )),
        ("human", "Ticket: {ticket_text}"),
    ])

    return prompt | structured_llm

classifier = build_classifier()

tickets = [
    "Can't log in after resetting password",
    "Your AI feature is wrong and I want a refund NOW",
    "Would love dark mode in the dashboard",
]

for ticket in tickets:
    result = classifier.invoke({"ticket_text": ticket})
    print(f"[{result.priority.upper()}] {result.category}: {result.summary}")
    if result.requires_human:
        print("  -> Escalate to human agent")

Common mistakes

Wrapping a single LLM call in a chain. If you are calling one model with one prompt and not sharing the chain anywhere, LCEL adds indirection without benefit. Use the SDK directly.

Using LangChain's memory classes. LangChain's built-in memory abstractions (ConversationBufferMemory, etc.) are confusing and often leaky. Manage conversation history yourself as a list of messages — it takes 10 lines and is fully transparent.

Ignoring fallback configuration. A chain that raises an exception on provider errors is not production-ready. Use .with_fallbacks([backup_llm]) or handle errors at the application layer.

Parsing errors silently discarded. .with_structured_output() can raise OutputParserException. Always wrap classifier chains in try/except and handle parse failures explicitly.

Try it yourself

Build a DocumentTagger chain that takes a document string and returns a Pydantic model with: topics: list[str], sentiment: Literal["positive", "neutral", "negative"], word_count_estimate: int, and is_technical: bool. Test it on five documents from different domains. Then swap the LLM from Anthropic to OpenAI and verify the same chain works without changing the prompt or parser.

Lesson Deep Dive

Ask a follow-up question about this lesson and get an AI-powered explanation.

Loading previous questions...