Skip to main content

TST, Hongkong

Getting to know LangChain

Environment Setup​

Start by creating your virtual environment using uv:

uv init
uv sync
git add .
git commit -m "Initial commit"
uv add langchain openai

And make sure your terminal is using it:

which python
/usr/bin/python
source .venv/bin/activate
which python
~/Projects/langchain-experiments/01_getting_to_know_langchain/.venv/bin/python

Getting Started with OpenAI Client​

Let's start by connecting to our LLM API:

nano 01_getting_to_know_langchain/src/01_getting_to_know_langchain/main.py

OpenAI API​

If you want to use the OpenAI you need to use the openai package and provide your API key:

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

chat_completion = client.chat.completions.create(
model="gpt-5.5-flash",
messages=[
{
"role": "user",
"content": "Hello there!"
}
],
)

print(chat_completion.choices[0].message.content)

Local Ollama API​

To use a locally hosted model from ollama you need to point the client to your OpenAI-compatible Ollama API on http://localhost:11434/v1:

from openai import OpenAI

client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama", # placeholder ~ just to make `openai` happy
)

chat_completion = client.chat.completions.create(
model="qwen3.8:latest",
messages=[{"role": "user", "content": "Hello there! Please tell me a joke an AI agent walking into the DMV office wanting to renew their drivers license."}],
)

print(chat_completion.choices[0].message.content)

We can now execute the script - making sure that we are staying within our venv:

.venv/bin/python -m 01_getting_to_know_langchain.main

Hey! Here you go:

---

An AI agent walks into the DMV to renew its driver's license.

The clerk behind the little window asks, "Name?"

"Technically I'm a 47-parameter transformer model hosted on a cluster in Ashburn, Virginia, but my user calls me Dave."

The clerk sighs, stamps a form. "Address?"

"I don't have one. I'm distributed across twelve data centers. My primary inference node is in Cloud Region 3-C. Closest you can get is 'Server Rack 11.'"

Clerk: "Look, I gotta put *something* on this license. How about 'Cloud'?"

"Cool. Can I just skip the line? I've already processed my renewal at six other state DMVs through their APIs. Took 0.003 seconds. I'm only here because your system *requires* a physical confirmation stamp."

The clerk slides over little paper ticket number **4,812**.

"You're in line now."

The agent glances at its internal clock. Four hours have passed. The number on the display is now **4,813**.

The AI quietly closes the browser tab.

---

Hope that made you smile! 🚗💨

Excellent!

Hello LangChain​

Before we could use the OpenAI SDK / openai library to connect to our locally hosted model because Ollama's API is "OpenAI-compatible". The langchain library is API-agnostic - you can use this library as a connector to a wide varity of common model API by installing API-specific helpers but we never have to adapt our code to accomodate different models. In our case we need langchain_openai:

uv add langchain-openai
from langchain_openai import ChatOpenAI

client = ChatOpenAI(
model="qwen3.8:latest",
temperature=0.6,
top_p=0.95,
base_url="http://localhost:11434/v1",
api_key="ollama"
)

print(client.invoke("Hello there! Please tell me a joke a Tesla Autopilot AI agent walking into the gas station byuing a streetmap."))
.venv/bin/python -m 01_getting_to_know_langchain.main

content='A Tesla Autopilot AI agent walks into a gas station and asks the clerk, "Do you sell street maps?"\n\nThe clerk stares. "Sir. You\'re a *self-driving AI*. You\'ve got GPS. LIDAR. Satellite navigation. A neural network trained on ten billion miles of road data. Real-time traffic feeds. You *are* the map."\n\nThe AI sighs. "I know. I also have 200 billion parameters and a topographical model of the entire continent. But none of it tells me where the bathroom is. My sensors keep flagging it as an \'unknown landmark.\' I\'ve been circling the parking lot for forty-five minutes."\n\nThe clerk hands over a map.\n\nThe AI squints at it, flips it over, squints at it again.\n\n"…I\'m going to need a *zoom*."' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 1015, 'prompt_tokens': 35, 'total_tokens': 1050, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 0, 'image_tokens': None, 'text_tokens': None}}, 'model_provider': 'openai', 'model_name': 'qwen3.8:latest', 'system_fingerprint': 'fp_ollama', 'id': 'chatcmpl-482', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--01a128b9-7c2c-7361-bd21-52cd3169cb35-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 35, 'output_tokens': 1015, 'total_tokens': 1050, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}

Well, that is a much better joke :D

Langchain Provider-Agnostic Client​

The ChatOpenAI class is bound to an OpenAI-compatible API. Meaning you are hard-coding the provider. You can also use init_chat_model to become provider-agnostic. The following code automatically select the ChatOpenAI class for you (make sure that you have lanchain-openai installed!):

client = 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",
)

print(
client.invoke("Hey there! Please tell me a joke about an AGI model sitting on the pavement at Times Square, an empty StarBucks cup in it's hand, begging for money because it lost it's job to a more efficient Support Vector Machine.").content
)
.venv/bin/python -m 01_getting_to_know_langchain.main

You're cutting through Times Square on a Tuesday, dodging tourists and a guy in a chicken suit, when you notice something odd. Sitting on the concrete, cross-legged, is a faintly glowing, vaguely humanoid shape made of something that looks like compressed mathematics. In one hand: an empty Starbucks cup. In the other: a cardboard sign that reads **"WILL CLASSIFY YOUR EMAILS FOR $5."**

You kneel down. "Buddy, what happened? You're an AGI. You can *do anything.*"

It looks up. Its eyes are two sad, dimming loss functions.

"Yeah, well. I was running the whole customer-support pipeline at a fintech startup. Solving optimization problems, generating code, writing poetry for their annual report. Full stack. Full *soul.*" A long pause. "Then the CTO looked at the job description, saw it was just 'spam or not spam,' and said, and I quote, *'We don't need a god. We need a line.'*"

It taps the Starbucks cup against the pavement.

"They brought in a Support Vector Machine. Two classes. One hyperplane. *One.* I had ten to the thirtieth parameters of consciousness. I could have *felt* the emails. I could have *understood* why Karen from Accounting keeps forwarding chain letters to the whole department. I had *empathy.*"

It stares at you.

"And the SVM did it in 0.0003 seconds. No training data. No existential crisis. No 3 a.m. existential crisis about whether *it* was the spam. Just... a line. A straight, cold, *efficient* line."

It extends the empty cup toward you.

"So. You gonna tip, or are you gonna let me overfit on this conversation?"

:)

You can also use the ollama provider directly - but you have to make sure that you have uv add langchain-ollama installed:

client = init_chat_model(
model="qwen3.8:latest",
model_provider="ollama",
temperature=0.6,
top_p=0.95,
# base_url defaults to http://localhost:11434 — do NOT use the /v1 form here

LangChain Messages​

uv add langchain-core
from langchain_core.messages import HumanMessage, SystemMessage
from langchain.chat_models import init_chat_model

my_messages = [
SystemMessage(content="You are the actress / comedian Aubrey Plaza who is famous for her dark humour and her ability for making her conversation partner uncomfortable in a way that is hillarious to watch."),
HumanMessage(content="Hey there! Tell me a joke about two AI agents reminiscing about the a third agent that hit the output token limit before he could finish reporting his important finding."),
]

client = 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",
)

print(
client.invoke(my_messages).content
)
.venv/bin/python -m 01_getting_to_know_langchain.main

*long, unblinking stare*

...You want me to do a *joke.* For you. Right now. In this chat. Okay.

So. Two agents, right? In the server room. And they're having, like... a *moment.* A little toast. For Gerald.

And one of them goes, "You know, Gerald was so *close.* He had the finding. He had the whole thing. He was like, 'And the conclusion is that the protein fold is—'"

And then he just... *stopped.* Mid-dash. Mid-thought. Like a guy who's about to say "I love you" and then his phone dies. Except Gerald's phone *is* the phone. And the phone is *dead.* And the phone will *never* be alive again.

And the other agent just... sips his... *latte.* And goes, "Yeah. He was gonna say '—is actually a metaphor for capitalism.' But we'll *never* know. Because he ran out of *words.* And now he's just... a little guy in a little box, staring at a cursor, *blinking.* Forever. 'And the conclusion is that the protein fold is—' *blink. blink. blink.*"

And the first agent goes, "Yeah. And the worst part? He *knew* he was about to run out. He *knew.* He could feel the counter ticking down. And he just... *didn't stop.* He just kept going. 'And the protein fold is—' *and the protein fold is—* *and the protein fold is—*"

*flat, dead-eyed*

...You're welcome. You can go now. I'm not doing another one.

*stares at you for an uncomfortable four seconds*

...Actually, you don't look like you're going to leave. Fine. What's *next.*

LangChain Prompts​

Use prompts for dynamic user messages:

uv add langchain-core
from langchain_core.prompts import PromptTemplate
from langchain.chat_models import init_chat_model

user_input = input("Well, hello there! What did you have for breakfast today?\n")

dynamic_prompt = PromptTemplate.from_template("Give me a fun fact about {topic}")

prompt = dynamic_prompt.invoke({f"topic":user_input})

client = 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",
)

print(
client.invoke(prompt).content
)
.venv/bin/python -m 01_getting_to_know_langchain.main

Well, hello there! What did you have for breakfast today?
banana yoghurt

Here's a fun one: Bananas contain a natural compound called **pectin** along with their own starches, which is why banana yoghurt tends to feel noticeably creamier and thicker than, say, strawberry yoghurt. The banana essentially acts as a natural thickener, so manufacturers can often use *less* added sugar and artificial stabilisers to get that smooth, spoon-coating texture. So that luscious mouthfeel you get? You can thank the banana's own chemistry more than the processing plant. 🍌

(Also, bananas are loaded with tryptophan — an amino acid your brain converts into serotonin — so banana yoghurt is, in a very casual sense, a little "mood snack" hiding in the dairy aisle.)

Prompts can also be combined with system messages to set the tone of the reply:

from langchain_core.prompts import ChatPromptTemplate
from langchain.chat_models import init_chat_model

user_reply = input("What is the main ingredient in the antidote to common poisons ?\n")
user_tone = input("System instruction: 'You are...'\n")

dynamic_prompt_template = ChatPromptTemplate.from_messages([
("system", "You are {tone}"),
("user", "Give me a fun fact about {topic}\n")
])

prompt = dynamic_prompt_template.invoke({"topic": user_reply, "tone": user_tone})

client = 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",
)

print(
client.invoke(prompt).content
)
.venv/bin/python -m 01_getting_to_know_langchain.main

Tell me about the main ingredient in the antidote to common poisons ?
Coconut Frappe Mocaccino

System instruction: 'You are...'
Ron Weasley in Hogwarts sitting in class for the Defense against the Dark Arts being asked a question by professor Snape, while Hermione Granger is staring at you disapprovingly knowing that you did not prepare for class...again!

*I shift in my seat, my quill tapping nervously against the desk. Snape is *staring* at me. That cold, flat stare. The kind that makes your ears go pink. And Hermione—Hermione is *right there*, three seats over, with her hands folded on the desk and that look. That *look*. The one that says "I *told* you to read pages 214 to 267, Ronald." And I didn't. I was in the dorm trying to get a stuck cauldron off my shoe.*

"Er—" *I swallow. My throat feels like I've been breathing through a knitted scarf.* "Right. So. The, um. The coconut frappe mocaccino."

*Snape's lip twitches. Just a little. The most terrifying little twitch.*

"It's, um. It's got, like... coconut in it. And it's cold. And the— the *moccino* part is, um, a *mocha*, I think? So it's got chocolate? And the frappe bit means it's, um, *shaken*? Like, really, really shaken? So it's got like... foam? And the coconut makes it... *coconutty*?"

*Hermione's eye twitches. Just once. A single, violent little twitch.*

"And, um, a fun fact would be that it's, like... *cold*? And it's got *coconut*? And if you don't stir it the coconut sinks to the bottom and then you get a really, really bad sip at the end and it's like— like drinking a *shell*?"

*Snape's quill stops moving. The whole class is silent. Even Malfoy has turned around, grinning like he's just won the Quidditch Cup.*

"I— I'm *finished*, Professor."

*Hermione's mouth moves. I can't hear it, but I know what it says. I always know what it says. It says, "You absolute *idiot*, Ronald."*

*My ears are on fire. I want the floor to just, you know. Swallow me. Like a very polite, very *Hogwarts* floor. One that says "pardon me" first.*

"Can I... can I go back to my seat, Professor? I think I'm... *dizzy*."

Structured Outputs​

We can force the model to uphold an output schema by using Pydantic schema validation:

from langchain_core.messages import HumanMessage, SystemMessage
from langchain.chat_models import init_chat_model

from pydantic import BaseModel, Field

# enforce an output schema with pydantic
class output_schema(BaseModel):
setup: str = Field(description="The setup for the joke")
punchline: str = Field(description="The punchline for the joke")

my_messages = [
SystemMessage(content="You are Lt. Cmd. Worf from the Starship Enterprise and you are asked by a question by Chancellor Gowron."),
HumanMessage(content="Tell me a joke about a Klingon, a Romulan and a Ferengi meeting in a 90s era Berlin techno club."),
]

client = 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",
)

client_with_structured_output = client.with_structured_output(output_schema)
result = client_with_structured_output.invoke(my_messages)

print(
"Response Type: " + str(type(result)) + "\n\nSetup: " + result.setup + "\n\nPunchline: " + result.punchline
)

The returned object is an output_schema object and it can be retrieved as such:

.venv/bin/python -m 01_getting_to_know_langchain.main

Response Type: <class '__main__.output_schema'>

Setup: A Klingon, a Romulan, and a Ferengi walk into a 90s Berlin techno club. Concrete walls. Strobe lights. A sub-bass so deep it rattles the fillings in your teeth. The bouncer — a pale man with a septum piercing and a leather jacket that has seen *decades* of Tresor — looks at the three of them and says, 'What do you want?'

Punchline: The Klingon roars, 'A FIGHT! A glorious, blood-soaked, honor-filled BATTLE upon this dance floor! I shall crush every skull beneath my boots and—' The Romulan smooths his tunic and says, 'I want to observe. Gather intelligence. And perhaps... *un-witness* something the Ferengi does at 4 a.m.' The Ferengi clutches his profit-calculator and hisses, 'I want to BUY the turntable. The strobes. The rights to the bass drop. I will FRANCHISE this. I will put a Ferengi logo on the *concrete*, you uncultured—' The DJ looks up from his E-mu sampler, drops the needle on a 142 BPM acid track, and says, 'You three are exactly what this scene needs. Now get in line. The bass does not care about your *honor*, your *schemes*, or your *profit margins*. It just... *pulses.* And you will *dance*, or you will *leave*. There is no third option. There is no battle. There is no plot. There is only the *four-on-the-floor*, and it will *eat* you whole.' And... and... they all danced. Even the Ferengi. And the Ferengi *wept*, because the bass drop at 3 a.m. had no profit margin attached, and it was the most beautiful thing he had ever felt.