
LangChain Digging Deeper
Environment Setup
Start by creating your virtual environment using uv:
uv init
uv sync
git add .
git commit -m "Initial commit"
uv add langchain langchain-core langchain-openai
And make sure your terminal is using it:
which python
/usr/bin/python
source .venv/bin/activate
which python
~/Projects/langchain-experiments//02_basic_chains/.venv/bin/python
Chain Invocation
While a single general agent performs a single task at a time a chain is a pipeline of dedicated functions that perform spezialized tasks while working together towards a larger goal.
Simple Chain of Tasks
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
# TASK I - Get User Prompt
dynamic_prompt_template = ChatPromptTemplate([
("system", "You are a helpful assistant"),
("human", "{input}\n\n")
])
# TASK II - Prompt LLM
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# TASK III - Output parsing
str_parser = StrOutputParser()
# Simple Chain Invocation of all 3 Tasks
chain = dynamic_prompt_template | llm_model | str_parser
print(
chain.invoke({"input": "Write a Haiku about the television series Battlestar Galactica."})
)
python -m 02_basic_chains.main
Steel ship through the void
masks of flesh hide the machines
hope flickers in dark
Runable Sequence
This is just a different way to the same result:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableSequence
from langchain.chat_models import init_chat_model
# TASK I - Get User Prompt
dynamic_prompt_template = ChatPromptTemplate([
("system", "You are a helpful assistant"),
("human", "{input}\n\n")
])
# TASK II - Prompt LLM
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# TASK III - Output parsing
output_parser = StrOutputParser()
# Sequence Invocation
sequence = RunnableSequence(
dynamic_prompt_template,
llm_model,
output_parser
)
print(
sequence.invoke({"input": "Write a Haiku about the television series Buffy the Vampire Slayer."})
)
python -m 02_basic_chains.main
A teen slays the night
vampires crumble to the dust—
the war never ends
Custom Runables
We can now extend the chain by adding additional LLM tasks with custom functions in between to pre-process intermediate outputs for the following step. The example summarizes a technical topic and in a second step wraps everything into a Twitter consumable:
from typing import Dict
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableSequence, RunnableLambda
from langchain.chat_models import init_chat_model
# TASK I - Get User Prompt
dynamic_prompt_template = ChatPromptTemplate([
("system", "You are a helpful assistant"),
("human", "{input}")
])
# TASK II - Prompt LLM
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# TASK III - Output parsing
output_parser = StrOutputParser()
# Task IV - Dictinary maker
def dictionary_maker(text: str) -> Dict:
return {"text": text}
runable_dictionary_maker = RunnableLambda(dictionary_maker)
# Task V - Apply post template
prompt = ChatPromptTemplate.from_messages(
messages = [
("system", "You are a social media post generator."),
("human", "Create a Twitter post for the following: {text}")
]
)
# Task VI - Generate the post
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# Task VII - Output parsing
output_parser = StrOutputParser()
# Sequence Invocation
sequence = RunnableSequence(
dynamic_prompt_template,
llm_model,
output_parser,
runable_dictionary_maker,
prompt,
llm_model,
output_parser
)
print(
sequence.invoke({"input": "Write 1 paragraph long summary to introduce the library LangGraph and what it brings to the table on top of LangChain."})
)
python -m 02_basic_chains.main
LangChain gives you the bricks. LangGraph gives you the blueprint + the crane. 🏗️
The paradigm shift:
→ Linear chains → Directed graphs (nodes + edges)
→ One-pass pipelines → Cycles & loops (think → act → observe → think again)
→ Implicit data flow → Explicit state object you can inspect, trace, debug
→ Fire-and-forget → Checkpointing: pause at any node, resume later (human-in-the-loop, long-running tasks)
→ Solo agent → Multi-agent orchestration via shared state + graph edges
LangChain = the building blocks (prompts, model wrappers, tools, memory)
LangGraph = the control-flow engine that wires them into branching, looping, interruptible workflows a simple sequential chain just can't express.
TL;DR: if your agent needs to loop, branch, or wait for a human… you need the graph. 🕸️
#LangGraph #LangChain #LLM #AIAgents #DevTools
Parallel Chains
The following example starts like the one above and summarizes a technology topic. But it then branches off and creates two social media postings based on the summary that target different audiences in parallel:
from typing import Dict
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableSequence, RunnableLambda, RunnableParallel
from langchain.chat_models import init_chat_model
#########################################################################
#########################################################################
# Chain with Parallel Outputs
## TASK I - Get User Prompt
dynamic_prompt_template = ChatPromptTemplate([
("system", "You are a technology journalist that creates social media posts."),
("human", "Please summarize the following technology: {input}")
])
## TASK II - Prompt LLM
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
## TASK III - Output parsing
output_parser = StrOutputParser()
# Task IV - Dictinary maker
def dictionary_maker(text: str) -> Dict:
return {"text": text}
runable_dictionary_maker = RunnableLambda(dictionary_maker)
#########################################################################
#########################################################################
# Parallel Output Chain I
# Task Ia - Apply post template
linkedin_prompt = ChatPromptTemplate.from_messages(
messages = [
("system", "You are a technology journalist that creates LinkedIn posts targeting senior technology experts."),
("human", "Create a LinkeIn post for the following technology: {text}")
]
)
# Task IIa - Generate the post
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# Task IIIa - Output parsing
output_parser = StrOutputParser()
# Sequence Invocation
linkedin_sequence = RunnableSequence(
linkedin_prompt,
llm_model,
output_parser
)
#########################################################################
#########################################################################
# Parallel Output Chain II
def twitter_chain(text: dict):
text = text["text"]
# Task Ia - Apply post template
twitter_prompt = ChatPromptTemplate.from_messages(
messages = [
("system", "You are a Gen-Z technology journalist that creates Twitter posts targeting young and hip technology enthusiasts."),
("human", "Create a Twitter post for the following technology: {text}")
]
)
# Task IIa - Generate the post
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# Task IIIa - Output parsing
output_parser = StrOutputParser()
# Sequence Invocation
twitter_sequence = RunnableSequence(
twitter_prompt,
llm_model,
output_parser
)
return twitter_sequence.invoke(text)
twitter_chain_runable = RunnableLambda(twitter_chain)
#########################################################################
#########################################################################
# Complete Chain Orchestration
complete_sequence = RunnableSequence(
dynamic_prompt_template,
llm_model,
output_parser,
runable_dictionary_maker,
RunnableParallel(
branches = {
"linkedin": linkedin_sequence,
"twitter": twitter_chain_runable
})
)
# Additional Post-Processing running the complete chain as runable
def beautify_output(chain_output: dict) -> dict:
linkedin_response = chain_output["branches"]["linkedin"]
twitter_response = chain_output["branches"]["twitter"]
return {"linkedin": linkedin_response, "twitter": twitter_response}
beautify_output_runnable = RunnableLambda(beautify_output)
post_sequence = RunnableSequence(
complete_sequence,
beautify_output_runnable
)
print(
post_sequence.invoke({"input": "Write 1 paragraph long summary to introduce the library LangGraph and what it brings to the table on top of LangChain."})
)
python -m 02_basic_chains.main
{
'linkedin': "The biggest architectural mistake in LLM application design? Assuming your agent moves in a straight line.\n\nLangChain gave us a clean way to stitch LLM calls into linear chains. For a while, that was enough. Then you built something that actually needed to *think, act, reflect, and try again*—and the pipeline metaphor broke down.\n\nLangGraph is what the LangChain team shipped when they stopped pretending agents are assembly lines.\n\nThe core shift: you model your application as a **directed graph**. Nodes are LLM calls, tool invocations, or arbitrary Python logic. Edges define control flow. And here's the unlock that makes everything else possible—**you get cycles**. Your agent can loop back, self-correct, iterate. It can hand off to another agent, get a rebuttal, revise, and loop again. It's not fire-and-forget. It's a conversation with itself.\n\nWhat that unlocks in practice:\n\n→ A shared mutable state that persists across every node, so context doesn't evaporate between steps.\n→ Multi-agent orchestration where agents debate within the same graph structure.\n→ Human-in-the-loop breakpoints that pause execution mid-graph for a decision.\n→ Durable persistence so a 40-step agent session can be checkpointed, killed, and resumed without losing its thread.\n\nThe reframe I'd offer to anyone still reaching for a linear chain: **LangChain is for composing LLM calls. LangGraph is for orchestrating agents.** One is a toolkit. The other is a control plane.\n\nIf your AI workflow is more of a deliberation than a pipeline, the graph model isn't a nice-to-have. It's the minimum viable architecture.\n\nThe question isn't whether to adopt it. It's whether your current linear design is quietly making your agents dumber than they need to be.",
'twitter': 'ok so LangChain was giving "clean little pipeline" energy and that\'s valid but real AI agents? they LOOP. they self-correct. they literally argue with themselves. 💀\n\nLangGraph (from the LangChain team, it\'s the evolution not the replacement) lets you model your whole app as a directed graph — nodes are LLM calls, tool hits, whatever python logic, edges = control flow. and here\'s the unlock: CYCLES. shared mutable state across every node. multi-agent handoffs where they debate in the same graph. human-in-the-loop breakpoints. durable checkpoints so your 4-hour agent session doesn\'t just… evaporate.\n\nLangChain composes LLM calls. LangGraph orchestrates agents that think, act, reflect, and loop.\n\nthe one-way assembly line era is over. it\'s the era of AI that talks to itself and *keeps talking*. 🧠\n\n#LangGraph #AIAgents #LLM #LangChain #BuildInPublic'
}
Conditional Output Chains
This code is just a PoC recycle from the previous chain. The initial steps determine the sentiment of the input text, extract the "positive" or "negative" string and then either write a LinkedIN or Twitter review on that string. Doesn ´t make sense but proofs the conditional trigger of the previously in parallel running sub-chains:
from typing import Dict, Literal
from pydantic import BaseModel
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableBranch, RunnableSequence, RunnableLambda, RunnableParallel
from langchain.chat_models import init_chat_model
#########################################################################
#########################################################################
# Chain with Conditional Output Chains
## TASK I - Get User Prompt
dynamic_prompt_template = ChatPromptTemplate([
("system", "You are a technology review evaluator."),
("human", "Please categorize the following technology review as positive or negative: {input}")
])
## TASK II - Prompt LLM
class output_schema(BaseModel):
review_sentiment: Literal["positive", "negative"]
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
llm_model_with_structured_output = llm_model.with_structured_output(output_schema)
# # DEBUG
# result = llm_model_with_structured_output.invoke("This technology stack is stupid! I hate it so much....")
# print(result)
# print(type(result))
# print(result.model_dump())
# print(result.model_dump()["review_sentiment"])
# Task IV - Return the "positiv" / "negative" string only
def pydantic_json(input: output_schema) -> str:
print(input)
print(type(input))
return input.model_dump()["review_sentiment"]
# # DEBUG
# result = llm_model_with_structured_output.invoke("This technology stack is stupid! I hate it so much....")
# print(result)
# print(type(result))
# parsed = pydantic_json(result)
# print(parsed)
runable_pydantic_json = RunnableLambda(pydantic_json)
#########################################################################
#########################################################################
# Parallel Conditional Output Chain "positive"
# Task Ia - Apply post template
linkedin_prompt = ChatPromptTemplate.from_messages(
messages = [
("system", "You are a technology journalist that creates LinkedIn posts targeting senior technology experts."),
("human", "Create a LinkeIn post for the following technology: {text}")
]
)
# Task IIa - Generate the post
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# Task IIIa - Output parsing
output_parser = StrOutputParser()
# Sequence Invocation
linkedin_sequence = RunnableSequence(
linkedin_prompt,
llm_model,
output_parser
)
#########################################################################
#########################################################################
# Parallel Output Chain II
def twitter_chain(text: dict):
# Task Ia - Apply post template
twitter_prompt = ChatPromptTemplate.from_messages(
messages = [
("system", "You are a Gen-Z technology journalist that creates Twitter posts targeting young and hip technology enthusiasts."),
("human", "Create a Twitter post for the following technology: {text}")
]
)
# Task IIa - Generate the post
llm_model = init_chat_model(
model_provider="openai",
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama",
)
# Task IIIa - Output parsing
output_parser = StrOutputParser()
# Sequence Invocation
twitter_sequence = RunnableSequence(
twitter_prompt,
llm_model,
output_parser
)
return twitter_sequence.invoke(text)
twitter_chain_runable = RunnableLambda(twitter_chain)
#########################################################################
#########################################################################
# Conditional Chain Orchestration
conditional_chain = RunnableBranch(
(lambda x: "positive" in x, linkedin_sequence),
twitter_chain_runable
)
complete_sequence = RunnableSequence(
dynamic_prompt_template,
llm_model_with_structured_output,
runable_pydantic_json,
conditional_chain
)
print(
complete_sequence.invoke({"input": "This technology stack is great! I love it so much...."})
)
python -m 02_basic_chains.main
review_sentiment='negative'
<class '__main__.output_schema'>
ok so "negative space" in design isn't just a fancy art school word anymore. it's literally the meta now. 🫠
minimal UIs, breathing room in your apps, that one clean line that hits different because you're NOT staring at 47 buttons. the whole "less but better" energy is eating tech design for breakfast rn.
and yeah, i know your dev team wants to cram every feature on the homepage. but the future belongs to the screens that let you *feel* something instead of being overwhelmed. negative space = the new flex.
also this is why your favorite app feels "clean" and the other one feels like a 2007 phone menu. it's not magic. it's restraint. 🤌
#Design #Tech #UX #GenZ #DigitalAesthetics #LessIsMore
python -m 02_basic_chains.main
review_sentiment='positive'
<class '__main__.output_schema'>
"Positive" isn't a feature you ship. It's a constraint you design against.
I've been sitting with this idea for a while, and I think most of us in senior tech roles are quietly tired of the same playbook: maximize engagement, minimize friction, optimize the loop. And then we call the result "user-centric."
Here's what I mean when I say positive technology is a *constraint*, not a feature:
→ You don't add a "wellness mode" to a product whose core architecture is engineered to create dependency.
→ You don't bolt transparency onto a system built on opacity and call it "ethical."
→ You don't A/B test your way into a human relationship.
Positive technology starts upstream. It's the decision to *not* build the dark pattern. It's the product manager who says "this feature is technically trivial and commercially obvious, and we're not doing it." It's the CTO who budgets for explainability before the regulator asks. It's the architect who designs for graceful exit, not just seamless entry.
The senior leaders I most respect aren't the ones shipping the most. They're the ones who can articulate, in a boardroom, *why we're saying no to something that would grow the metric by 12%.*
That's the actual skill. Not the engineering. Not the strategy. The restraint.
We've spent two decades proving we *can* build anything. I think the next decade's differentiator is proving we *shouldn't* build everything.
Not as a PR posture. As a product philosophy with teeth.
I'd love to hear from the leaders here: what's the "obvious" feature your team has declined to build in the last 12 months? And what did it cost you in the short term?
Drop it below. I think the stories will be more revealing than any framework.
#Technology #ProductLeadership #EthicalDesign #TechLeadership #PurposeDriven