LangChain is a Python framework for building applications powered by large language models, and in current LangChain 1.x you compose those applications with LCEL, the LangChain Expression Language, written as prompt | model | parser. This post builds a working support-ticket triage chain with that pattern, running offline with a stand-in model so you can learn the shape before spending anything on API calls.
Picture a support inbox on a Monday morning. Two hundred messages arrived over the weekend: billing questions, outage reports, one customer who cannot check out. Reading each one by hand takes hours, and the urgent ones hide among the routine ones. A language model can read and sort them in seconds, but wiring that into real code means formatting prompts, calling the API, parsing replies, and handling failures. That wiring is the job LangChain is built for, and this post shows it on a small, honest example.
My first check is whether the work is really a pipeline. LangChain earns its keep when the same task runs again and again: read a ticket, classify it, route it. A single one-off call needs no framework.
My second check is how fast the framework moves. LangChain has changed its recommended patterns more than once, and old tutorials still teach LLMChain and SequentialChain, both deprecated since 0.1.17 and gone from the 1.0 way of working. Read the current docs and pin your versions before you build on it.
My third check is where the model lives. LangChain is not a model, it is the wiring around one. You still need an API key and a paid account for a real model, or a local model of your own. The demo uses FakeListLLM, a stand-in returning canned replies, so you can learn the wiring without an account or a bill.
My fourth check is where the result goes next. A triage verdict is only useful if it lands somewhere, a queue, a database, or a chat channel. If the output stays as a printed string, you have built a demo, not a feature. A real deployment would parse it and hand it to a small web service, like the one in the Flask post on this blog.
My verdict, as judgement: use LangChain for a repeated LLM pipeline, when you accept that the API moves, and you have a real model and a real destination for the output. Skip it for one-off calls. The recommendation flips the moment a simpler direct API call covers the whole job.
The example sorts four support messages for a small software company. Each ticket gets an urgency, a topic, and one short action, in one line for easy routing. The chain is three pieces joined with the pipe operator: a ChatPromptTemplate that fills in the ticket text, a model, and a StrOutputParser that turns the reply into a plain string. There is no API key on this machine, so the model is FakeListLLM, a stand-in from langchain_core that returns canned replies in order. The replies below are the ones I wrote into the stand-in, not a real model's judgement. Swapping in a real chat model later means changing one line.
from langchain_core.language_models import FakeListLLM
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
tickets = [
{"id": "TKT-101", "message": "I was charged twice for my March subscription and the second charge is still showing as pending."},
{"id": "TKT-102", "message": "The dashboard shows a blank page after I log in. I have tried two browsers and cleared the cache."},
{"id": "TKT-103", "message": "Can I change the email address on my account? I no longer use the old one."},
{"id": "TKT-104", "message": "Our checkout has been down for 40 minutes and customers cannot pay. This is urgent."},
]
prompt = ChatPromptTemplate.from_template(
"You triage support tickets for a small software company. "
"Read the ticket and reply in exactly one line with this shape: "
"URGENCY: <low, medium, or high> | TOPIC: <billing, technical, or account> "
"| ACTION: <one short action, under 10 words>. "
"Ticket: {message}"
)
model = FakeListLLM(
responses=[
"URGENCY: medium | TOPIC: billing | ACTION: Refund the duplicate March charge",
"URGENCY: high | TOPIC: technical | ACTION: Reproduce the blank dashboard and check logs",
"URGENCY: low | TOPIC: account | ACTION: Confirm identity, then update the email",
"URGENCY: high | TOPIC: technical | ACTION: Page the on-call engineer for checkout",
]
)
chain = prompt | model | StrOutputParser()
for ticket in tickets:
result = chain.invoke({"message": ticket["message"]})
print(f"{ticket['id']}: {result}")
Here is the real output from the run:
TKT-101: URGENCY: medium | TOPIC: billing | ACTION: Refund the duplicate March charge
TKT-102: URGENCY: high | TOPIC: technical | ACTION: Reproduce the blank dashboard and check logs
TKT-103: URGENCY: low | TOPIC: account | ACTION: Confirm identity, then update the email
TKT-104: URGENCY: high | TOPIC: technical | ACTION: Page the on-call engineer for checkout
The thing that went wrong was a small slip in how the chain was called. The prompt template has one placeholder, {message}, so the input has to be a dict with the key "message". I called the chain with {"ticket": "I was charged twice."} instead, and the run stopped with this error: KeyError: "Input to ChatPromptTemplate is missing variables {'message'}. Expected: ['message'] Received: ['ticket']". The fix is to match the key to the placeholder, chain.invoke({"message": ticket["message"]}), and the error names the missing variable. One surprise: passing a plain string instead of a dict did not fail, because a one-variable template accepts the string as its value.
First, passing the wrong key in the input dict. The symptom is KeyError: "Input to ChatPromptTemplate is missing variables {'message'}. Expected: ['message'] Received: ['ticket']", and the chain never reaches the model. The fix is to read the placeholder names in your template and use exactly those keys in the dict you pass to invoke.
Second, copying LLMChain from an old tutorial. The symptom is a deprecation warning, and in LangChain 1.x the old chain classes are gone from the recommended path. The fix is to rewrite the call as LCEL, prompt | model | parser, as this post does.
Third, trusting FakeListLLM past its list. The symptom is subtle: the stand-in cycles through its canned replies and starts again from the top, so a fifth ticket silently gets the first ticket's answer. I verified this by calling a one-reply stand-in twice and getting the same reply both times. The fix is to give the stand-in as many replies as calls you plan to make, and to treat it as a wiring test only.
Do I need an API key to try LangChain? No. The example here runs fully offline with FakeListLLM, a stand-in returning canned replies, so no account or bill is needed.
What is LCEL, and why does every current tutorial use it? LCEL is the LangChain Expression Language, the pipe syntax that composes runnables, prompt | model | parser. It replaced LLMChain and SequentialChain, giving every chain streaming, batching, and async support through one interface.
Is LangChain the model itself? No. LangChain is the wiring around a model: prompts, chains, parsers, retrievers, and memory. The model still comes from a provider or a local model, and that choice decides cost, speed, and privacy.
Can I validate the chain output as structured data instead of a string? Yes, and for a real service you should. LangChain pairs with Pydantic models through output parsers, so the reply comes back as a validated object. The Pydantic post on this blog explains that validation pattern.
You now have a chain that runs and a clear picture of what LangChain adds. The triage example is small on purpose: four tickets, three pieces, one pipe operator, and the whole thing runs offline. From here, swap FakeListLLM for a real chat model on one ticket type, watch what it gets wrong, and tighten the prompt before you add more pieces. If you serve the result over HTTP later, the Express post on this blog shows a small API service. Start with one pipeline and let the chain earn the next piece.
Waqas Shahid is a software engineer from Karachi with more than 12 years of experience, including 6+ years in engineering leadership. He writes The Practical Developer.
Originally published at The Practical Developer