First hands-on practice: full project setup with API keys from .env, initializing ChatOpenAI and ChatAnthropic, then all invocation modes — invoke, ainvoke, stream, astream, batch, abatch — along with the contents of the response object such as content and token usage.

In episode 2, you got to know LangChain's architecture map: the langchain-core primitives, LCEL with the pipe operator, and the modular package structure. Now it's time to stop merely understanding concepts — we're going to actually run a model. This episode is the gateway to practice: setting up a project, initializing a chat model, and mastering all the invocation modes.
All the code in this episode and beyond runs inside the virtual environment you created in episode 0. Make sure pip show langchain-openai doesn't error before continuing.
First, install the dependencies we'll use to load the .env file:
pip install python-dotenv langchain langchain-openaiCreate a chat_basic.py file at the root of your project, then load the keys from .env:
from dotenv import load_dotenv
load_dotenv()belajar-langchain/
├── .env
├── chat_basic.py
└── .venv/load_dotenv() reads the .env file and injects its values into the environment. Once called, the OPENAI_API_KEY variable is automatically available for LangChain to pick up. This mechanism works for every provider — OpenAI, Anthropic, and others alike.
Initializing a chat model means one thing: creating an instance that represents a model from a specific provider. Because LangChain standardizes through BaseChatModel, these two providers are used with the same pattern:
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
gpt = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
claude = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0.3)The two most common parameters: model to select the model name, and temperature to control determinism. A value of temperature=0 makes output almost always identical for the same input — suitable for tasks that need consistency like data extraction; higher values are more creative for brainstorming.
Info
If you use a provider whose package isn't installed yet, just add the integration package — for example pip install langchain-anthropic — and the initialization pattern follows the same shape. That's the standardization we discussed in episode 1.
The most basic mode is invoke — send a single input, receive the complete result:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
jawaban = llm.invoke("Sebutkan tiga keunggulan LCEL.")
print(jawaban.content)Note that invoke returns an AIMessage object (not a string), so the text content is accessed via the .content attribute. For async applications — for example inside an async web handler — use ainvoke:
import asyncio
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
async def utama():
jawaban = await llm.ainvoke("Apa itu RAG?")
return jawaban.content
hasil = asyncio.run(utama())
print(hasil)The async versions matter when you build a server that must handle many concurrent requests — blocking one request while waiting for the model to respond would waste resources. We'll explore these patterns in depth in episodes 5 and 18.
Streaming changes the user experience drastically: instead of waiting for the entire answer, chunks appear as soon as they're available — this is what makes an application feel alive. The pattern is just as simple:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
for chunk in llm.stream("Ceritakan singkat sejarah LangChain."):
print(chunk.content, end="", flush=True)Each chunk is a partial piece of the answer; we print it without a newline so it appears to flow. For async, replace with astream inside an async function:
async def streaming():
async for chunk in llm.astream("Ceritakan singkat sejarah LangChain."):
print(chunk.content, end="", flush=True)
asyncio.run(streaming())batch sends many inputs at once in a single call — useful for efficiently processing a list of questions or documents:
daftar_pertanyaan = [
"Apa itu token?",
"Apa itu temperature?",
"Apa itu prompt?",
]
jawaban = llm.batch(daftar_pertanyaan)
for item in jawaban:
print(item.content, "\n")Under the hood, batch executes each input sequentially via .invoke (or in parallel if configured). Its async variant is abatch.
Finally, notice the contents of the response object returned by all the modes above — because it isn't just a string:
jawaban = llm.invoke("Hitung 2 + 2.")
print("Isi:", jawaban.content)
print("Tipe:", type(jawaban).__name__)
print("Token pemakaian:", jawaban.usage_metadata)The usage_metadata output contains the input_tokens, output_tokens, and total_tokens details — important data for tracking costs in production, which we'll leverage when discussing cost optimization in episode 18.
Warning
Sending a large list to batch without managing concurrency can trigger rate limits from the model provider. For production applications, set the degree of parallelism explicitly — we'll see how in episodes 5 and 18.
Episode 3 was your first hands-on practice: loading API keys from .env with python-dotenv, initializing ChatOpenAI and ChatAnthropic, mastering invoke/ainvoke, streaming with stream/astream, batching with batch/abatch, and getting to know the response object that carries content and usage_metadata.
Key takeaways:
load_dotenv() and store the keys in a .env file that's never committed.ChatOpenAI/ChatAnthropic; temperature controls determinism.invoke for a single input, batch for many inputs, stream for incremental output.ainvoke, abatch, astream are a must for concurrent server applications..content and token usage in .usage_metadata.In episode 4 we'll go deeper into advanced prompts & chat models: PromptTemplate and ChatPromptTemplate with system/human messages, few-shot prompting, input variables, per-provider model profiles, and content-block v2/v3 streaming with version="v3" on stream_events. See you there!