An agent becomes useful when a chat model can choose an action, receive the action's result, and continue toward a final response. LangChain provides that model-and-tool loop through create_agent, so a small deterministic program can prove the complete execution path before an application connects to a hosted model or an external service.
The local program uses a small deterministic BaseChatModel implementation. Its first call requests a temperature-conversion tool, while its second call reads the resulting ToolMessage and formats that value into the final answer. This removes API keys and network responses from the smoke test while still exercising the agent's real tool node and message state.
The final state must contain the requested tool name, its calculated value, and the agent reply. A provider-backed chat model can replace the deterministic model later without changing the tool definition, create_agent call, or message-shaped input.
Related: How to install LangChain with pip
Related: How to call a chat model in LangChain
Related: How to build a tool-calling agent in LangChain
$ python -m pip install --upgrade langchain
LangChain requires Python 3.10 or newer. Provider integrations remain separate packages when the deterministic model is replaced.
Related: How to install LangChain with pip
from typing import Any from langchain.agents import create_agent from langchain.messages import AIMessage, ToolMessage from langchain.tools import tool from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import BaseMessage from langchain_core.outputs import ChatGeneration, ChatResult @tool def convert_celsius_to_fahrenheit(celsius: float) -> float: """Convert a Celsius temperature to Fahrenheit.""" return celsius * 9 / 5 + 32
class ToolResultModel(BaseChatModel): @property def _llm_type(self) -> str: return "tool-result-model" def bind_tools(self, tools, *, tool_choice=None, **kwargs): return self def _generate( self, messages: list[BaseMessage], stop: list[str] | None = None, run_manager: CallbackManagerForLLMRun | None = None, **kwargs: Any, ) -> ChatResult: tool_message = next( ( message for message in reversed(messages) if isinstance(message, ToolMessage) ), None, ) if tool_message is None: response = AIMessage( content="", tool_calls=[ { "name": "convert_celsius_to_fahrenheit", "args": {"celsius": 20}, "id": "call_temperature", } ], ) else: fahrenheit = float(tool_message.content) response = AIMessage( content=( "20 degrees Celsius equals " f"{fahrenheit:g} degrees Fahrenheit." ) ) return ChatResult(generations=[ChatGeneration(message=response)]) model = ToolResultModel()
The bind_tools() override supplies the binding surface expected by create_agent. The deterministic branches make this a repeatable local agent test, not a substitute for a provider model in production.
agent = create_agent( model=model, tools=[convert_celsius_to_fahrenheit], system_prompt="Use the temperature tool for conversions.", )
result = agent.invoke( {"messages": [{"role": "user", "content": "Convert 20 C to Fahrenheit."}]} ) tool_call = next( call for message in result["messages"] if isinstance(message, AIMessage) for call in message.tool_calls ) tool_message = next( message for message in result["messages"] if isinstance(message, ToolMessage) ) final_reply = result["messages"][-1].content expected_reply = ( "20 degrees Celsius equals " f"{float(tool_message.content):g} degrees Fahrenheit." ) assert final_reply == expected_reply, "Agent reply did not use the tool result." print(f"Tool called: {tool_call['name']}") print(f"Tool result: {tool_message.content}") print(f"Agent reply: {final_reply}")
from typing import Any from langchain.agents import create_agent from langchain.messages import AIMessage, ToolMessage from langchain.tools import tool from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import BaseMessage from langchain_core.outputs import ChatGeneration, ChatResult @tool def convert_celsius_to_fahrenheit(celsius: float) -> float: """Convert a Celsius temperature to Fahrenheit.""" return celsius * 9 / 5 + 32 class ToolResultModel(BaseChatModel): @property def _llm_type(self) -> str: return "tool-result-model" def bind_tools(self, tools, *, tool_choice=None, **kwargs): return self def _generate( self, messages: list[BaseMessage], stop: list[str] | None = None, run_manager: CallbackManagerForLLMRun | None = None, **kwargs: Any, ) -> ChatResult: tool_message = next( ( message for message in reversed(messages) if isinstance(message, ToolMessage) ), None, ) if tool_message is None: response = AIMessage( content="", tool_calls=[ { "name": "convert_celsius_to_fahrenheit", "args": {"celsius": 20}, "id": "call_temperature", } ], ) else: fahrenheit = float(tool_message.content) response = AIMessage( content=( "20 degrees Celsius equals " f"{fahrenheit:g} degrees Fahrenheit." ) ) return ChatResult(generations=[ChatGeneration(message=response)]) model = ToolResultModel() agent = create_agent( model=model, tools=[convert_celsius_to_fahrenheit], system_prompt="Use the temperature tool for conversions.", ) result = agent.invoke( {"messages": [{"role": "user", "content": "Convert 20 C to Fahrenheit."}]} ) tool_call = next( call for message in result["messages"] if isinstance(message, AIMessage) for call in message.tool_calls ) tool_message = next( message for message in result["messages"] if isinstance(message, ToolMessage) ) final_reply = result["messages"][-1].content expected_reply = ( "20 degrees Celsius equals " f"{float(tool_message.content):g} degrees Fahrenheit." ) assert final_reply == expected_reply, "Agent reply did not use the tool result." print(f"Tool called: {tool_call['name']}") print(f"Tool result: {tool_message.content}") print(f"Agent reply: {final_reply}")
$ python agent_demo.py Tool called: convert_celsius_to_fahrenheit Tool result: 68.0 Agent reply: 20 degrees Celsius equals 68 degrees Fahrenheit.
The model builds the final line from the received ToolMessage. The equality assertion raises an error if the completed reply no longer matches that tool result.