This article is published in English.
A Hand-Rolled LangChain Tool Loop for Comparing Live Stock Metrics
Build a small LangChain stock-comparison assistant step by step and learn the create, bind, invoke and execute loop that lets an LLM request live data instead of guessing.
A language model only knows what was in its training data, so asking it for today's share price produces a confident guess rather than a fact. For anything financial that is a serious problem. Tool calling fixes it by letting the model ask your code to fetch live data and then reason over the result. This walkthrough builds a compact assistant that compares two stocks by price and P/E ratio, and along the way makes each stage of LangChain's tool-calling workflow explicit: create, bind, invoke, execute.
Why a model without tools guesses
Picture a store associate who knows every product and policy by heart but has no access to the inventory system. Ask whether a jacket is in stock in medium and you get a confident answer based on last week. That is an LLM on its own: articulate and blind to the present.
Hand the associate a barcode scanner and they verify instead of guessing. In LangChain, the scanner is a tool: an ordinary function the model can ask to run, such as a price lookup, a database query or an API call. Tool calling is the mechanism that lets the model decide when to reach for the scanner, which item to scan, and how to use what comes back.
The four-stage loop
Tool calling (often called function calling) always follows the same cycle:
- Create the tool. Write a Python function with a descriptive name, typed parameters and a docstring explaining what it does.
- Bind it to the model. Call
.bind_tools()on the chat model so it knows which tools exist and what arguments they take. - Invoke the model. Send the user's question. If it needs outside data, the model returns a structured request such as "call
get_stock_pricewithticker='MSFT'" instead of a final answer. - Execute and return results. Your code runs the requested function and hands the output back to the model so it can compose the answer.
The important property is that the model, not your code, chooses when a tool is needed. A question like "What is 2+2?" gets answered directly, while "Compare MSFT and AAPL" can produce two tool calls in a single response. The model only requests the calls; running them remains your job. For how frameworks automate this loop, see what LangChain actually automates once you have built an agent loop.
Step 1: Install dependencies on first run
The script begins by checking for its packages and installing any that are missing, so someone can run it without a separate pip install step. It converts each package name into its import name by swapping hyphens for underscores (langchain-groq becomes langchain_groq) and falls back to calling pip through the current interpreter:
import sys
import subprocess
required_packages = ["langchain-groq", "langchain-core", "yfinance"]
for package in required_packages:
try:
__import__(package.replace("-", "_"))
except ImportError:
print(f"📦 Package '{package}' not found. Installing now...")
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
That suits demos, but real projects should pin dependencies in requirements.txt or pyproject.toml; runtime installs make builds unreproducible.
Step 2: Provide the API key
The model runs on Groq, so the Groq client needs an API key in the environment. The demo assigns it inline as a placeholder:
import os
os.environ["GROQ_API_KEY"] = "Your-API"
Real keys belong in a .env file excluded from version control or in a secrets manager, never in committed code.
Step 3: Define the stock data tool
The tool uses yfinance to look up a ticker, take the latest closing price from one day of history, and read market capitalization and trailing P/E from the ticker's info:
from langchain_core.tools import tool
import yfinance as yf
@tool
def get_stock_price(ticker: str) -> str:
"""Fetches the current stock price and key statistics for a given ticker symbol."""
try:
stock = yf.Ticker(ticker)
todays_data = stock.history(period='1d')
if todays_data.empty:
return f"Could not find data for ticker {ticker}."
price = todays_data['Close'].iloc[-1]
info = stock.info
market_cap = info.get('marketCap', 'N/A')
pe_ratio = info.get('trailingPE', 'N/A')
return f"{ticker} Current Price: ${price:.2f}, Market Cap: {market_cap}, P/E Ratio: {pe_ratio}"
except Exception as e:
return f"Error fetching data for {ticker}: {str(e)}"
Three details make this function a good tool:
- The
@tooldecorator turns the function into a LangChain tool and derives an input schema from its type hints. - The docstring is not decoration. The model reads it, along with the name, to decide when the tool applies, so it should say plainly what the tool returns.
- Errors are returned as descriptive strings instead of raised. An unknown ticker or a failed request yields a message the model can reason about ("could not find data"), rather than an exception that aborts the whole run.
Note that yfinance is an unofficial Yahoo Finance wrapper: quotes may be delayed and fields missing, hence the 'N/A' fallbacks.
Step 4: Create the model and bind the tool
Next the chat model is created and the tool list attached to it:
from langchain_groq import ChatGroq
llm = ChatGroq(
model="openai/gpt-oss-120b",
temperature=0
)
tools = [get_stock_price]
llm_with_tools = llm.bind_tools(tools)
bind_tools() sends the tools' names, descriptions and argument schemas to the model with each request, so it knows what it can ask for. Note that the unbound llm is kept as well; it is reused later for the final summary. The model name reflects what Groq offered at the time of writing, so check the provider's current model list before running it.
Setting temperature=0 keeps output focused and consistent, though it reduces randomness rather than guaranteeing identical responses.
Step 5: Build a prompt chain
A prompt template supplies a system instruction and slots the user's question into the human message. The pipe operator then joins the prompt and the tool-aware model into one runnable, similar to a Unix pipeline:
from langchain_core.prompts import ChatPromptTemplate
from IPython.display import display, Markdown
prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert financial analyst. Use the tools provided to pull real-time data before comparing or concluding."),
("human", "{input}")
])
chain = prompt | llm_with_tools
The system message does real work here: it tells the model to fetch data with the tools before drawing any comparison. Without it, the model may answer from stale training data.
Step 6: Run the loop and handle multiple tool calls
The main block ties everything together. It invokes the chain, checks whether the response contains tool_calls, runs each requested lookup, collects the outputs, and finally asks the plain model to write a comparison from the gathered data. If no tools were requested, it prints the model's direct answer:
if __name__ == "__main__":
query = "Compare the current stock price and P/E ratio of Microsoft (MSFT) AND Apple (AAPL). Which one looks cheaper based on P/E?"
print(f"🚀 Invoking Financial Pipeline with query: '{query}'\n")
# 1. Ask the model what tools it wants to use
ai_message = chain.invoke({"input": query})
# 2. Check if the model requested tool use
if ai_message.tool_calls:
print(f"🛠️ Model requesting {len(ai_message.tool_calls)} real-time tool lookups...\n")
tool_outputs = []
# 3. Execute ALL generated tool calls
for tool_call in ai_message.tool_calls:
if tool_call["name"] == "get_stock_price":
ticker_symbol = tool_call["args"]["ticker"]
print(f" -> Executing tool lookup for: {ticker_symbol}")
result = get_stock_price.invoke(tool_call["args"])
print(f" [Tool Output] {result}")
tool_outputs.append(result)
# 4. Supply the full collective data back to the LLM
summary_prompt = f"""
User Query: {query}
Real-time Data Harvested: {'; '.join(tool_outputs)}
Synthesize a final response evaluating which asset looks cheaper.
"""
final_answer = llm.invoke(summary_prompt)
print("\n--- Final Analysis Output ---")
display(Markdown(final_answer.content))
else:
print("\n--- Final Analysis Output ---")
print(ai_message.content)
For a question about both Microsoft and Apple, the model typically returns two tool calls in one response, one per ticker. The loop executes all of them before moving on, so the final step sees both sets of numbers at once. The model requests the calls in parallel, but this code runs them one after another; for slow APIs you could execute them concurrently.
Two refinements are worth knowing once this works. First, the name check inside the loop is how you would dispatch among several tools; a dictionary mapping tool names to tool objects scales better than a chain of if statements. Second, this version passes results back by building a fresh text prompt. The more idiomatic LangChain approach appends each result as a ToolMessage carrying the matching tool_call_id to the conversation and invokes the tool-bound model again, which preserves the full exchange and lets the model request further calls if the first results are insufficient.
Extending the assistant is incremental: a get_financial_news or calculate_valuation tool plugs in through the same bind_tools() call, and the model picks among tools by their descriptions.
Key takeaways
- A tool is a function with a clear name, type hints and a docstring; the
@tooldecorator does the rest. .bind_tools()connects your functions to the model by describing them in each request.- The model only asks for tool calls. Your code executes them and returns the results, and that control is useful, not a limitation.
- Return errors from tools as readable strings so a single bad lookup does not end the run.
- Start with one tool and one loop, then add tools, message-based result passing and concurrency as needed.
The same pattern applies wherever a model needs current information, from weather and inventory to CRM records or your own database, so assistants can reason over live systems rather than a frozen snapshot.