# Learning LangChain

> Source: <https://dev.to/sabimantock/learning-langchain-4jlp>
> Published: 2026-10-12 17:44:01+00:00

A while back I built Synapse, a small app that answers questions using my own notes. I did every part of it by hand: load the files, turn them into vectors, find the closest ones to the question, and hand those to an LLM. It worked, and I understood it, because I'd written every line myself.

So when I started LangChain, a framework that promised to do all of that for me felt a little suspicious. My honest question was: what is this doing that I haven't already done?

It turns out the answer is less "new ideas" and more "a standard shape for the ones you already have." Let me walk through it the way I learned it.

A chat model doesn't take one big string. It takes a list of messages: a system message that sets the role, then human messages from you, then AI messages from the model. LangChain gives each of those its own class. Nothing magic, it's the same thing the raw API wants.

Prompt templates were the next small step. Instead of building a string every time, you write the prompt once with a gap in it:

``` python
from langchain_core.prompts import ChatPromptTemplate

template = ChatPromptTemplate.from_template(
    "I've recently adopted a {pet}. Suggest three {pet} names."
)
```

Then an output parser takes the model's text reply and turns it into something you can actually use, like a Python list. The thing that stuck with me here is that the parser is only as good as your instructions. Vague instructions about the format gave me a messy list. Strict ones gave me a clean one.

This is the bit where it clicked for me. LangChain lets you join those pieces with a `|`, like a Unix pipe:

```
chain = chat_template | chat | list_output_parser
chain.invoke({"pet": "dog"})
# ['Max', 'Bella', 'Luna']
```

A prompt goes in one end, the model sits in the middle, and a clean list comes out the other end. Each step's output is the next step's input. That's the whole idea.

It even works for ordinary Python functions. Wrap them in `RunnableLambda` and they snap in like any other piece:

``` python
from langchain_core.runnables import RunnableLambda

add_up = RunnableLambda(lambda x: sum(x))
square = RunnableLambda(lambda x: x**2)

(add_up | square).invoke([1, 2, 5])   # 8, then 64
```

You can also draw a chain. `chain.get_graph().print_ascii()` prints the boxes and arrows, which is surprisingly handy when a chain gets long.

Two more things I liked. `RunnableParallel` runs two chains side by side and gives both results to the next step. And `batch` is quicker than looping: two requests through `batch` took 2.67 seconds in my notebook, while the same two as separate calls took about 7.5.

Remember my five hand-built steps? LangChain has a piece for each.

For loading, there are loaders for different file types. I used one for PDFs and one for Word documents. Splitting was the new part, since Synapse never did any. I split a six-page document twice. First by its headings, so every chunk remembers which lecture it came from, then by size, about 500 characters with a little overlap. That left me with 20 chunks, each labelled with where it came from.

Each chunk then became 1,536 numbers using OpenAI's embedding model. I wanted to see if it really worked, so I compared three pairs of chunks. They scored 0.88, 0.80 and 0.79, so related text does end up close together. The vectors all have a length of about 1, which is why a plain dot product works as a similarity score.

Those vectors went into Chroma, which saves them to a folder on disk so I don't have to re-embed every time I open the notebook.

Retrieval comes in two flavours. A plain similarity search gives you the closest chunks. MMR gives you close chunks that are also different from each other, so you don't get five copies of the same paragraph.

And the last step is where the pipe comes back. The whole thing is one chain:

```
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt_template
    | chat
    | StrOutputParser()
)

chain.invoke("What software do data scientists use?")
```

The retriever fills in the context, the question passes straight through, the prompt tells the model to answer only from that context and name the lecture it used, and the parser hands back plain text. The answer ended with:

Resources: Programming Languages & Software Employed in Data Science - All the Tools You Need

That's the same "only use these notes" guard I wrote by hand in Synapse, plus the sources I'd been putting off adding.

LangChain didn't teach me a new idea. Everything in that pipeline is something I'd already done by hand: load, embed, find the closest, ask. What it gave me was a standard shape for each step, and the pipe to join them. Having built it the long way first is what made the short way make sense.

Next up is LangGraph.
