Exploring Chat Models with LangChain
Data Mastery Series — Episode 30: LangChain Website (Part 5)
Exploring Chat Models with LangChain
Data Mastery Series — Episode 30: LangChain Website (Part 5)

Connect with me and follow our journey: Linkedin, Facebook
Hey everyone! Welcome back to the Data Mastery Series! We’re continuing our LangChain adventure, exploring even more cool things you can do with Chat Models. Think of it like leveling up your AI conversation skills! If you’re new here, catch up on the earlier episodes:
- Part 1: LangChain Model I/O Basics
- Part 2: Unpacking Prompt Templates with LangChain
- Part 3: Exploring Few-Shot Prompts with LangChain
- Part 4: Unpacking Chat Models with LangChain
Note: As always, I’ll be sharing insights and practical examples based on my own experience with the LangChain documentation. Let’s jump in!
Source: https://python.langchain.com/v0.1/docs/modules/model%5Fio/chat/
Message Types: Who’s Saying What?
When you chat with an AI, it’s like a regular conversation. Each message has a role (who’s talking) and content (what they’re saying). LangChain uses these message types:
- HumanMessage: What you tell the AI. Basically, your text.
- AIMessage: The AI’s reply. This can include extra info like tool or function calls.
- SystemMessage: Instructions to guide the AI. Like telling it to act like a helpful friend or a serious expert.
- FunctionMessage: Results of a function call, containing both the function name and its result.
- ToolMessage: Similar to a FunctionMessage, but specific to tools. This message includes a
tool_call_idto indicate which tool was called.
Streaming: Getting Answers Piece by Piece
Imagine streaming a movie — you don’t wait for the whole download. LangChain’s streaming lets you see the AI’s response as it’s being created. This makes interactions feel more dynamic and engaging.
Example - Streaming (As seen in Episode 29)
chat = ChatOpenAI(model="gpt-3.5-turbo", api_key = OPENAI_API_KEY) # or "gpt-4" if you have access to it
for chunk in chat.stream("Write me a song about goldfish on the moon. only 1st Verse"):
print(chunk.content, end="", flush=True)
Output (printed word-by-word):
'''
Verse 1:
Swimming in the sky, in a world so high
Goldfish on the moon, shining like a star
Their scales glisten in the lunar glow
In a place where only dreamers go
'''
Tool Calling: AI Using Helpers
Tool Calling Tool calling lets the model produce output based on a defined schema, allowing for specific formats. This is especially useful for structured tasks, like performing calculations or fetching data. In LangChain, you can define tool schemas in two ways: using the LangChain Tool decorator or Pydantic classes.
1. Request: Passing Tools to the Model
A. Defining Tool Schemas: LangChain Tool
The @tool decorator is a simple way to label functions as tools that the model can call directly. Here’s an example:
Example - LangChain Tool
@tool
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b.
Args:
a: first int
b: second int
"""
return a * b
tools1 = [add, multiply]
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", api_key = OPENAI_API_KEY)
llm_with_tools1 = llm.bind_tools(tools1)
llm_with_tools1
Output:
'''
RunnableBinding(bound=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x7b1aa2725480>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x7b1aa2727040>, root_client=<openai.OpenAI object at 0x7b1aa28971c0>, root_async_client=<openai.AsyncOpenAI object at 0x7b1aa27254e0>, model_name='gpt-3.5-turbo-0125', model_kwargs={}, openai_api_key=SecretStr('**********')), kwargs={'tools': [{'type': 'function', 'function': {'name': 'add', 'description': 'Adds a and b.\n\n Args:\n a: first int\n b: second int', 'parameters': {'properties': {'a': {'type': 'integer'}, 'b': {'type': 'integer'}}, 'required': ['a', 'b'], 'type': 'object'}}}, {'type': 'function', 'function': {'name': 'multiply', 'description': 'Multiplies a and b.\n\n Args:\n a: first int\n b: second int', 'parameters': {'properties': {'a': {'type': 'integer'}, 'b': {'type': 'integer'}}, 'required': ['a', 'b'], 'type': 'object'}}}]}, config={}, config_factories=[])
'''
B. Defining Tool Schemas: Pydantic Class Alternatively, you can use Pydantic classes to define tools, which allows for more detailed type hints and parameter descriptions:
Example - Pydantic class
class add(BaseModel):
"""Add two integers together."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
class multiply(BaseModel):
"""Multiply two integers together."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
tools2 = [add, multiply]
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", api_key = OPENAI_API_KEY)
llm_with_tools2 = llm.bind_tools(tools2)
llm_with_tools2
Output:
'''
RunnableBinding(bound=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x7b1aa2795e70>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x7b1aa25d02b0>, root_client=<openai.OpenAI object at 0x7b1aa2797cd0>, root_async_client=<openai.AsyncOpenAI object at 0x7b1aa2795e10>, model_name='gpt-3.5-turbo-0125', model_kwargs={}, openai_api_key=SecretStr('**********')), kwargs={'tools': [{'type': 'function', 'function': {'name': 'add', 'description': 'Add two integers together.', 'parameters': {'type': 'object', 'properties': {'a': {'description': 'First integer', 'type': 'integer'}, 'b': {'description': 'Second integer', 'type': 'integer'}}, 'required': ['a', 'b']}}}, {'type': 'function', 'function': {'name': 'multiply', 'description': 'Multiply two integers together.', 'parameters': {'type': 'object', 'properties': {'a': {'description': 'First integer', 'type': 'integer'}, 'b': {'description': 'Second integer', 'type': 'integer'}}, 'required': ['a', 'b']}}}]}, config={}, config_factories=[])
'''
2. Response: Reading Tool Calls from Model Output
When a Chat Model generates tool calls in its response, these calls are included within the AIMessage or AIMessageChunk (if using streaming). These calls are stored in the .tool_calls attribute as a list of ToolCall objects, which detail the tool name, arguments, and optionally, an identifier. If no tool calls are generated, this list will simply be empty.
Example - Reading tool calls from model output
llm_with_tools1 and llm_with_tools2 have same output
query = "What is 3 * 12? Also, what is 11 + 49?"
llm_with_tools1.invoke(query).tool_calls
Output:
'''
[{'name': 'multiply',
'args': {'a': 3, 'b': 12},
'id': 'call_GL2piovzfNyaDwWd4VppwmdX',
'type': 'tool_call'},
{'name': 'add',
'args': {'a': 11, 'b': 49},
'id': 'call_KFFA6QJD1Al1aRVlqjqd73UE',
'type': 'tool_call'}]
'''
You can also use parsers to convert tool call output back into a structured format, such as Pydantic classes:
Example - Reading tool calls from model output
llm_with_tools1 and llm_with_tools2 have same output
chain = llm_with_tools1 | PydanticToolsParser(tools=[multiply, add])
chain.invoke(query)
Output:
'''
[multiply(a=3, b=12), add(a=11, b=49)]
'''
3. Response: Streaming
In a streaming context, each streamed message chunk includes tool call details in .tool_call_chunks. This feature allows you to handle responses piece-by-piece, providing an interactive experience.
Example - Response: Streaming
llm_with_tools1 and llm_with_tools2 have same output
async for chunk in llm_with_tools1.astream(query):
print(chunk.tool_call_chunks)
Output:
'''
[]
[{'name': 'multiply', 'args': '', 'id': 'call_5fGs3v0AjQwQdr5UvwNLZljk', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"a"', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ': 3, ', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '"b": 1', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '2}', 'id': None, 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'add', 'args': '', 'id': 'call_z3cyhgy2FnGafZsOh5AVuf6R', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '{"a"', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ': 11,', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': ' "b": ', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': None, 'args': '49}', 'id': None, 'index': 1, 'type': 'tool_call_chunk'}]
[]
'''
To see the complete tool call details, you can gather the chunks like this:
Example - Response: Streaming
llm_with_tools1 and llm_with_tools2 have same output
first = True
async for chunk in llm_with_tools1.astream(query):
if first:
gathered = chunk
first = False
else:
gathered = gathered + chunk
print(gathered.tool_call_chunks)
Output:
'''
[]
[{'name': 'multiply', 'args': '', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a"', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, ', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 1', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}, {'name': 'add', 'args': '', 'id': 'call_4auC5ckbHxSk1v2kQTPUOlXf', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}, {'name': 'add', 'args': '{"a"', 'id': 'call_4auC5ckbHxSk1v2kQTPUOlXf', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}, {'name': 'add', 'args': '{"a": 11,', 'id': 'call_4auC5ckbHxSk1v2kQTPUOlXf', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}, {'name': 'add', 'args': '{"a": 11, "b": ', 'id': 'call_4auC5ckbHxSk1v2kQTPUOlXf', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}, {'name': 'add', 'args': '{"a": 11, "b": 49}', 'id': 'call_4auC5ckbHxSk1v2kQTPUOlXf', 'index': 1, 'type': 'tool_call_chunk'}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_k91VaGIVquU7rIaN4S8iWkV4', 'index': 0, 'type': 'tool_call_chunk'}, {'name': 'add', 'args': '{"a": 11, "b": 49}', 'id': 'call_4auC5ckbHxSk1v2kQTPUOlXf', 'index': 1, 'type': 'tool_call_chunk'}]
'''
4. Request: Passing tool outputs to model
You can give the tool outputs back to the AI to continue the conversation.
Example - Request: Passing tool outputs to model
Define the tools
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b.
Args:
a: first int
b: second int
"""
return a * b
Initialize tools and model
tools = [add, multiply]
llm_with_tools3 = llm.bind_tools(tools)
query = "What is 2 * 3? Also, what is 4 + 5?"
Create initial message and process tool calls
messages = [HumanMessage(query)]
ai_msg = llm_with_tools3.invoke(messages)
messages.append(ai_msg)
Process each tool call and feed back to messages
for tool_call in ai_msg.tool_calls:
selected_tool = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
tool_output = selected_tool.invoke(tool_call["args"])
messages.append(ToolMessage(tool_output, tool_call_id=tool_call["id"]))
Re-invoke with updated messages
final_message = llm_with_tools3.invoke(messages)
print(final_message.content)
Output:
'''
The result of 2 * 3 is 6, and the result of 4 + 5 is 9.
'''
5. Request: Few-shot prompting
For complex tasks, few-shot prompting can be highly effective. By providing example tool calls, you help the model understand how to use specific tools for similar tasks.
Example - Request: Few-shot prompting
Define additional tools
@tool
def subtract(a: int, b: int) -> int:
"""Subtract a and b.
Args:
a: first int
b: second int
"""
return a - b
Set up few-shot examples
examples = [
HumanMessage(
"What's the product of 317253 and 128472 plus four", name="example_user" # ในโค้ดนี้ เราเริ่มด้วยการสร้างรายการตัวอย่าง (examples) ของการใช้เครื่องมือคำนวณ (multiply และ add)
),
AIMessage(
"",
name="example_assistant",
tool_calls=[
{"name": "multiply", "args": {"x": 317253, "y": 128472}, "id": "1"} # อันนี้ multiply
],
),
ToolMessage("16505054784", tool_call_id="1"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "add", "args": {"x": 16505054784, "y": 4}, "id": "2"}], # อันนี้ add
),
ToolMessage("16505054788", tool_call_id="2"),
AIMessage(
"The product of 317253 and 128472 plus four is 16505054788",
name="example_assistant",
),
]
Create a system message to set context
system = """You are bad at math but are an expert at using a calculator.
Use past tool usage as an example of how to correctly use the tools."""
Define a prompt with few-shot examples
few_shot_prompt = ChatPromptTemplate.from_messages(
[
("system", system),
*examples,
("human", "{query}"),
]
)
Invoke and process tool calls with few-shot examples
query = "What's 119 times 8 minus 20"
messages = [HumanMessage(query)]
ai_msg = chain.invoke(query)
messages.append(ai_msg)
for tool_call in ai_msg.tool_calls:
selected_tool = {"add": add,"subtract": subtract, "multiply": multiply}[tool_call["name"].lower()]
tool_output = selected_tool.invoke(tool_call["args"])
messages.append(ToolMessage(tool_output, tool_call_id=tool_call["id"]))
Final invocation with processed tool calls
final_message = llm_with_tools3.invoke(messages)
print(final_message.content)
Output:
'''
119 times 8 is 952, and when you subtract 20 from that, you get 932.
'''
By referencing previous interactions, the model can replicate similar logic, resulting in more accurate and relevant responses. This concludes our exploration of Chat Models with LangChain. Stay tuned as we dive even deeper into the powerful features of LangChain in future episodes!
Data Science Explore the world of data science with Donato_Story
Dashboard Discover the power of data visualization with Donato_Story
Donato_Journey Join me on my journey (Thai version)
Course_Review Discover the training courses with Donato_Story (Thai version)
I really value your thoughts and feedback. Please share any comments or questions you have below, or reach out to me on:
Medium: medium.com/donato-story
Facebook: web.facebook.com/DonatoStory
Linkedin: linkedin.com/in/nattapong-thanngam
Originally published on Medium
Related
Chat with Document: A Closer Look at Splitting, Embeddings, and RAG
Data Mastery Series — Episode 21: The Chat with Document and Langchain Series (Part 2)
Chat with Document: Basics and Demonstrations
Data Mastery Series — Episode 20: The Chat with Document and Langchain Series (Part 1)
Continue Exploring Chat Models with LangChain
Data Mastery Series — Episode 31: LangChain Website (Part 6)
Continue Exploring Retrievers with LangChain
Data Mastery Series — Episode 39: LangChain Website (Part 14 )