1. Initialising the model
from langchain.chat_models import init_chat_model
llm = init_chat_model("gpt-4o-mini", model_provider="openai")
init_chat_model wraps the underlying provider SDK. No network call happens here — it just configures a client object.
2. Defining tools with @tool
from langchain_core.tools import tool
tools = []
@tool
def extract_video_id(url: str) -> str:
"""Extracts the 11-character YouTube video ID from a URL."""
...
tools.append(extract_video_id)
@tool is a LangChain-specific decorator — not a Python built-in. The moment Python reads the @tool line, it:
- Reads
__name__→ becomes the tool's name - Reads
__doc__→ becomes the description sent to the LLM - Reads type annotations → builds a Pydantic input schema
- Wraps everything into a
StructuredToolobject
extract_video_id.invoke({"url": "https://youtube.com/watch?v=abc"})
extract_video_id.func("https://youtube.com/watch?v=abc") # raw function
3. Binding tools to the model
llm_with_tools = llm.bind_tools(tools)
This is pure preparation — no network call. It creates a new runnable that remembers the tools as default kwargs. When you later call .invoke(), LangChain serialises the tool definitions into the OpenAI JSON format:
{
"model": "gpt-4o-mini",
"messages": [...],
"tools": [
{
"type": "function",
"function": {
"name": "extract_video_id",
"description": "...",
"parameters": { "type": "object", "properties": { "url": { "type": "string" } } }
}
}
]
}
4. Message types
LangChain uses typed message classes. Each serialises differently into the JSON sent to the API. The direction is always consistent:
| Class | Direction | Who creates it |
|---|---|---|
SystemMessage | client → LLM | developer (instructions, persona) |
HumanMessage | client → LLM | end user |
AIMessage | LLM → client | the model |
ToolMessage | client → LLM | your tool execution code |
The classes are not enforced by Python's type system — they're self-documenting structs with serialisation logic. They give you IDE autocomplete, Pydantic validation, and isinstance checks that LangChain's internals rely on.
5. The tool call loop
When you call llm_with_tools.invoke(messages), the LLM does not execute your tools. It returns an AIMessage that says "I want to call this tool with these arguments". Your code must execute the tool and send the result back.
sequenceDiagram
participant P as Your program
participant L as LLM API
P->>L: `① POST /v1/messages — messages:[{role:"user",content:"summarize this video"}], tools:[extract_video_id,fetch_transcript]`
L-->>P: `② 200 stop_reason:"tool_use" — content:[{type:"tool_use",id:"tu_abc",name:"extract_video_id",input:{url:"youtube.com/..."}}]`
Note over P: `AIMessage(tool_calls) → invoke extract_video_id → returns "T-D1OfcDW1M"`
P->>L: `③ POST /v1/messages — role:"assistant":{type:"tool_use",id:"tu_abc"}, role:"user":{type:"tool_result",tool_use_id:"tu_abc",content:"T-D1OfcDW1M"}`
L-->>P: `④ 200 stop_reason:"tool_use" — content:[{type:"tool_use",id:"tu_xyz",name:"fetch_transcript",input:{video_id:"T-D1OfcDW1M"}}]`
Note over P: `AIMessage(tool_calls) → invoke fetch_transcript → returns "[full transcript]"`
P->>L: `⑤ POST /v1/messages — role:"user":{type:"tool_result",tool_use_id:"tu_xyz",content:"[full transcript]"}`
L-->>P: `⑥ 200 stop_reason:"end_turn" — content:[{type:"text",text:"Here is a summary..."}]`
Key rules:
- Always append the
AIMessagetomessages[]before appending theToolMessage. The API requires them to appear in order. finish_reason: tool_callsmeans the loop continues.finish_reason: stopmeans the LLM is done.AIMessage.contentis empty ('') while the LLM is in tool-calling mode — it has nothing to say to the user yet.
6. The tool mapping
tool_mapping = {t.name: t for t in tools}
Built from the tools list automatically — no need to maintain it by hand. Used to look up the callable from the string name the LLM returns:
tool_fn = tool_mapping[tool_call["name"]]
result = tool_fn.invoke(tool_call["args"])
7. A reusable execute_tool helper
def execute_tool(tool_call):
try:
result = tool_mapping[tool_call["name"]].invoke(tool_call["args"])
return ToolMessage(content=str(result), tool_call_id=tool_call["id"])
except Exception as e:
return ToolMessage(content=f"Error: {str(e)}", tool_call_id=tool_call["id"])
The try/except matters: if the tool throws, you still need to return a ToolMessage with the matching tool_call_id, otherwise messages[] becomes malformed and the next API call fails. Returning an error message lets the LLM see what went wrong and potentially recover.
8. A minimal agent loop
Putting it all together:
def run_agent(messages):
while True:
response = llm_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls: # finish_reason: stop
return response
for tool_call in response.tool_calls:
messages.append(execute_tool(tool_call))
The LLM can request multiple tools in a single response — hence the for loop over response.tool_calls. All results are appended before the next invoke().
This is essentially what LangGraph's ToolNode does internally — the tutorial is showing you the manual version so you understand what the framework automates.
Quick reference
| Class | Direction |
|---|---|
@tool decorator | converts a function into a StructuredTool at decoration time |
bind_tools(tools) | stores tool definitions locally, no API call |
invoke(messages) | sends full message history + tool schemas to the API |
AIMessage.tool_calls | list of tool calls the LLM wants executed |
ToolMessage | carries tool result back to the LLM |
finish_reason=stop | LLM is done, no more tool calls needed |
messages[] | the LLM's entire working memory, grows with every exchange |
0 comments:
Post a Comment