Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

callback
A function the library calls at fixed points in a run, such as when a step starts or ends.
chain
A series of steps where each one passes its result to the next, ending in the answer.
invoke
The method that runs a step once with one input and returns its result.
memory
Stored history from earlier turns, added back to a request so the model can refer to what was said.
message
One labelled turn in a chat request, marked as coming from the system, the user or the model.
model
The trained program that reads a message list and returns the next turn.
output parser
The step that turns a model's raw text into a value your program can use, or raises an error.
pipe
The operator that joins two steps into one new step, running the left side first.
prompt
The text a model receives for one request, built from fixed wording and the values filled into it.
prompt template
A reusable prompt with named blanks that are filled in for each request.
runnable
Any step with a standard way to run it, so steps can be joined without knowing each other's code.
streaming
Sending a reply in pieces as it is produced, rather than waiting for the whole text.
tool
A function exposed to a model, with a name, described inputs and a result.
tool call
A model's request to run a named tool with specific arguments, which your code may accept or refuse.

LangChain from the ground up

Composing with pipes

The pipe operator is one method: a step that knows how to call another step. Implement it once and the rest of the framework becomes obvious.

Part 5 of 10

Intermediate15 min readBack to Courses

One method, not a language feature

prompt | model | parser looks like syntax, but Python has no pipe for functions. It is prompt.__or__(model), called by the interpreter, returning a new object that represents the two steps joined. Chain three and you get a nested pair whose invoke runs the left side, then the right.

That is worth internalising because it explains the error messages. A pipe that fails usually failed inside one of its steps, and the traceback names the step, not the pipeline. Reading it from the inside out is the fastest way to the bug.

The evaluation order is left to right, so prompt | model | parser builds a pair from the first two, then pairs that with the parser. Each __or__ returns a new object; nothing runs until invoke. That is why you can build a chain at import time and call it many times, and why an exception at build time is a wiring bug rather than a model failure.

Parallel steps and pass-through

Not every arrangement is a straight line. Sometimes one input feeds two steps whose results are merged — a summary and a list of keywords from the same document. RunnableParallel expresses that: run these branches on the same input, collect a dictionary.

RunnablePassthrough covers the other common shape: a branch that needs the original input alongside a computed value. Without it you end up wrapping the input in a tuple just to carry it through, which makes the chain harder to read than the code it replaced.

Fan out, then merge

from langchain_core.runnables import RunnableParallel, RunnablePassthrough

analyse = RunnableParallel(
    summary=summary_prompt | model | StrOutputParser(),
    keywords=keywords_prompt | model | StrOutputParser(),
    original=RunnablePassthrough(),
)

result = analyse.invoke("A long document about vector search ...")
print(result.keys())      # summary, keywords, original
print(result["summary"])

Build the pipe yourself

Run it, then chain a third step. The `|` operator should keep working without any change to `Pipe` — that is the whole trick.

Output
Click "Run" to execute your code

The pipe is not the program

A long pipe reads like a sentence and debugs like a stack trace. When it has more than four stages, name the intermediate values: build the prompt, call the model, parse the reply, then compose those three in a short chain. The named version is longer and easier to test, which is the trade you want at that size.

A common mistake is adding a stage to fix a problem in an earlier one. The chain grows, the original bug remains, and the new stage hides it. The symptom is a pipeline that works only in the order it was written. Invoke each step alone with the same input; the step that misbehaves is the one to change.

Why the framework bothers

If a pipe were only sequential calls, nobody would need a library for it. The value appears at the edges: the same object can be invoked once, batched over a list, streamed token by token, or awaited asynchronously, without rewriting the steps.

Those four modes are why steps must share a shape. A step that only works when called once with a string cannot be batched or streamed. The interface is narrow on purpose so the runtime can do more with it.

That is also why a step should not reach for global state. A step that reads a module-level variable behaves differently when batched, because the batch shares the process. Keep the input explicit and the runtime can reorder or parallelise without changing the answer. The constraint feels fussy until the first time you stream a chain and watch a cached value leak between requests.

When a chain misbehaves, invoke each step on its own with the same input before blaming the composition. Nine times out of ten the pipe is fine and one step is not.

Readability has a limit

A pipe is clearer than nested calls for three or four steps. At ten, the line becomes a diagram nobody can hold in their head, and naming the intermediate stages is better than extending the pipe. Composition is a tool for expressing structure, not a contest.

The same applies to branching. Two parallel branches are readable; a tree of six is a function that deserves a name and its own tests.

Two answers from one input

The parallel runner already returns a dictionary. Add a third branch that counts the words, and make sure the original text still comes through untouched.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a | b do in Python?