Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

LangChain Basics: Interacting with Foundation Models

Recommended resources

Hello! Welcome back to your course on LangChain.

In our last lesson, you successfully configured your development environment, installed the necessary LangChain packages, and confirmed that your API keys for OpenAI and Anthropic are working correctly by making your first model.invoke() call.

Today, we'll build directly on that foundation. The goal of this lesson is to master the fundamental ways to interact with language models through LangChain. We will go beyond a simple request-response and explore the primary methods LangChain provides for making API calls: invoke, stream, and batch. Understanding these interaction patterns is essential before we can start composing more complex chains and applications.

1. The Modern Way to Initialize a Model

In the previous lesson, you initialized models by directly using classes like ChatOpenAI. While that works perfectly, LangChain also provides a convenient helper function, init_chat_model, that can simplify this process and make your code more provider-agnostic.

The following video clip shows this function in action.

LangChain Full Crash Course - AI Agents in Python

This segment from 'LangChain Full Crash Course' by NeuralNine demonstrates how to use init_chat_model to create a model instance and then call it.

Watch from 11:40 to 13:41. Focus on how init_chat_model is used to get a model object by just providing a model identifier string (e.g., 'gpt-4.1-mini'). Notice how the .invoke() method is then used to get a response.

As you saw, init_chat_model is a straightforward factory function. You provide a model string, and it returns the correct, configured model object, automatically using the API keys from your environment. This is a clean pattern you'll see used in modern LangChain examples.


2. The Three Key Invocation Methods

Once you have a model object, there are three primary ways to call it, each suited for different scenarios:

  1. invoke(): For a single, synchronous call. You send one input and wait for the complete output.
  2. stream(): For receiving the output in real-time. Instead of waiting for the full response, you get it piece by piece, or token by token.
  3. batch(): For processing multiple inputs efficiently. This sends multiple requests in parallel, improving throughput.

The official LangChain documentation provides an excellent overview of these methods.

Models - Docs by LangChain

Please read the 'Invocation' section of the LangChain documentation on Models. This will be our primary reference for the rest of the lesson.

Read the entire section titled 'Invocation', including its three subsections: 'Invoke', 'Stream', and 'Batch'. Pay close attention to the code examples for each method.

Now, let's break down each method with additional context relevant to your background.

invoke(): The Standard Request-Response

You've already used invoke(). It's the most basic way to interact with a model. However, there's more to it than just passing a string.

Passing Conversation History

For a chat application, you need to provide the model with the history of the conversation so it has context for the new user query. You do this by passing a list of message objects to invoke(). LangChain provides classes for this:

  • SystemMessage: Sets the overall behavior and persona of the AI (e.g., "You are a helpful assistant.").
  • HumanMessage: Represents a message from the user.
  • AIMessage: Represents a previous response from the AI.

Let's look at a practical example. The following code demonstrates how to structure a conversation:

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

load_dotenv()
model = ChatOpenAI(model="gpt-4o-mini")

conversation = [
    SystemMessage(content="You are a sarcastic but helpful assistant who specializes in cloud computing."),
    HumanMessage(content="What is the difference between ECS and EKS?"),
    AIMessage(content="Oh, you mean the classic 'build it for me' vs. 'I'll build it myself' dilemma? ECS is AWS's simpler, proprietary orchestrator, while EKS is managed Kubernetes, for when you enjoy complexity."),
    HumanMessage(content="Which one has a steeper learning curve?")
]

response = model.invoke(conversation)

print(response.content)

The response object itself is an AIMessage instance, not just a plain string. It contains the text in the .content attribute, along with other useful metadata in .response_metadata, such as token usage, which is crucial for monitoring costs.

stream(): For Real-Time User Experience

Waiting several seconds for a long response can feel slow. In any user-facing application, streaming the response back as it's generated provides a much better experience. Given your work on React dashboards, you can immediately see the value here for building responsive UIs.

The stream() method returns an iterator. Each item it yields is an AIMessageChunk. You can print the content of each chunk as it arrives.

print("Streaming response:")
for chunk in model.stream("Explain the concept of Infrastructure as Code (IaC) like I'm five."):
    print(chunk.content, end="", flush=True)
print("\n")

A powerful feature is that these chunks can be added together to reconstruct the full AIMessage object at the end, which you might need for logging or for adding to your conversation history.

full_response = None
for chunk in model.stream("What is the role of an API Gateway in a microservices architecture?"):
    if full_response is None:
        full_response = chunk
    else:
        full_response += chunk
    print(chunk.content, end="|", flush=True)

print(f"\n\n--- Full Response Object ---")
print(full_response)

This summation pattern ensures you don't lose any information while still getting the real-time benefits of streaming.

batch(): For Scalability and Throughput

Imagine you need to categorize 1,000 user feedback entries, a task you've worked on before. Making 1,000 sequential API calls would be incredibly slow. The batch() method addresses this by sending multiple requests in parallel.

This is analogous to how a load balancer distributes requests to multiple workers. LangChain manages the concurrent API calls for you.




# A list of independent inputs
feedback_list = [
    "The new dashboard is amazing and so intuitive!",
    "I can't find the export button, this is frustrating.",
    "The application is constantly crashing on my phone.",
    "Billing section needs more detailed reports.",
]




# Get responses for all inputs
responses = model.batch(feedback_list)

for i, response in enumerate(responses):
    print(f"Input {i+1}: {feedback_list[i]}")
    print(f"Response {i+1}: {response.content}\n")

You can control the level of parallelism using the max_concurrency config setting, which is useful for managing rate limits.




# Limit to 2 concurrent calls at a time
responses_concurrent = model.batch(feedback_list, config={"max_concurrency": 2})

3. Your Turn: Putting It All Together

Now it's time to test these methods yourself. The following script combines invoke, stream, and batch. Run it in your environment and observe the different outputs.

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage




# --- Setup ---
load_dotenv()



# Make sure your OPENAI_API_KEY is set in your .env file
if not os.getenv("OPENAI_API_KEY"):
    raise ValueError("OPENAI_API_KEY not found in .env file")

model = ChatOpenAI(model="gpt-4o-mini")

print("--- 1. Using invoke() with a conversation ---")
conversation = [
    SystemMessage(content="You are a Go programming language expert."),
    HumanMessage(content="What are goroutines and channels used for?")
]
response_invoke = model.invoke(conversation)
print(response_invoke.content)
print("-" * 20)


print("\n--- 2. Using stream() for real-time output ---")
stream_prompt = "Explain the publish/subscribe pattern in the context of a message broker like Redis."
for chunk in model.stream(stream_prompt):
    print(chunk.content, end="", flush=True)
print("\n" + "-" * 20)


print("\n--- 3. Using batch() for parallel processing ---")
batch_inputs = [
    "What is the primary key in a relational database?",
    "What is a NoSQL database good for?",
    "Explain database indexing in simple terms."
]
batch_responses = model.batch(batch_inputs)

for i, resp in enumerate(batch_responses):
    print(f"\nQuery {i+1}: {batch_inputs[i]}")
    print(f"Answer {i+1}: {resp.content}")
print("-" * 20)

Experiment by changing the prompts to topics you're familiar with, like those related to your projects (gRPC, Prisma, load balancing), to see how the model responds.


Conclusion

In this lesson, you've moved from a simple "Hello, World" to a robust understanding of the three fundamental ways to execute calls to language models using LangChain.

Key Takeaways:

  • invoke(): Your go-to for simple, single, synchronous requests. It's powerful when combined with a list of SystemMessage, HumanMessage, and AIMessage objects to provide conversational context.
  • stream(): Essential for building responsive, user-facing applications. It provides real-time feedback by yielding AIMessageChunk objects as they are generated.
  • batch(): The key to high-throughput processing. It allows you to run multiple independent queries in parallel, which is critical for scalable applications.

Preview of the Next Lesson:
So far, we've exclusively used "Chat Models" (like ChatOpenAI). However, you might come across another type of model in older LangChain code called a standard "LLM". In the next lesson, we will differentiate between standard LLMs and Chat Models, understand why Chat Models are the modern standard, and see how their inputs and outputs differ.