Part of my 100-Day AI Engineering Bootcamp Day 24 — building one AI project a day to become job-ready.
Days 22-23 gave the AI one tool each, in isolation. Today's goal: combine the calculator and web search into a single agent that picks the right tool — or chains both, in sequence, when a question genuinely needs it. The test case I built around: "What's 15% of the current Bitcoin price?" — a question with no answer unless you search first, then calculate using exactly what you found.
This one took three real bugs to get right, each more interesting than the last.
First attempt, with both tools wired up correctly: I asked the Bitcoin question and got back:
"To find 15% of the current Bitcoin price, I'll first need to look up the current price... /search_web current Bitcoin price (I'll wait for the search results...)"
No tool call expander appeared. The model typed out what a tool call looks like, as plain text, instead of emitting a real structured tool_calls request. I added a stronger system prompt insisting it must use search — that made it worse, not better, producing the same narration pattern with more confidence.
I went looking for why, and found I wasn't alone: other developers have hit the exact same symptom with llama-3.3-70b-versatile on Groq — tool calling that's "unreliable/broken," with the model narrating its intended action as a string instead of a real call, and "two successive runs of the same prompt resulting in one succeeding and one failing." This wasn't a prompt-wording problem. It was a model-reliability problem.
The fix: switch models entirely, to openai/gpt-oss-120b — the same model Groq's own official tool-calling documentation uses in its examples. Groq even maintains a separate, specially fine-tuned "Tool-Use" model variant, built specifically because their general-purpose models aren't optimized for this. Once switched, real tool calls started appearing immediately.
With the new model, the sequential chain worked beautifully: search for Bitcoin's real price, then calculate 15% of that exact number, correct math both times, multiple runs in a row.
Then, on a later run, a new crash:
groq.BadRequestError: Tool choice is none, but model called a tool
failed_generation: '{"name": "search_web", "arguments": {"cursor": 0, "id": 2}}'
This was a bug in my own fallback code, not the model. My tool_use_failed recovery path retried the question without passing the tools parameter at all — but my system prompt was now so insistent ("you MUST use search_web") that the model tried to fake a tool call anyway, with no real schema to follow, inventing nonsense arguments (cursor, id) that didn't match my actual tool's expected shape.
The fix: the fallback now explicitly passes tool_choice="none" alongside the real tools schema — so the model can see what the tool looks like, but is given a hard, explicit instruction it cannot violate: don't call anything right now.
- 🧮 Calls the calculator for math
- 🔎 Calls web search for current information
- 🔗 Chains them sequentially when a question needs both — search for a number, then calculate using exactly that number
- 📋 Shows a step-by-step log of every tool call made, so you can watch the agent's reasoning unfold
- 🛡️ Falls back gracefully, twice over now, if tool calling fails at the model level
- Python
- Groq API (
openai/gpt-oss-120b) — switched from LLaMA 3.3 70B specifically for tool-calling reliability - ddgs (DuckDuckGo search wrapper)
- Streamlit
git clone https://github.com/PrashikSawant/Research-Agent
cd Day24-ResearchAgent
python -m venv venv
.\venv\Scripts\activate
pip install -r requirements.txt
# Add GROQ_API_KEY to .env
streamlit run app.py- Not all models on the same provider handle tool calling equally well —
llama-3.3-70b-versatileandopenai/gpt-oss-120bare both "function-calling capable" on paper, but only one of them reliably emitted real structured calls in my testing - A stronger system prompt can fix one problem and quietly create another — my fix for "the model won't search" caused "the model tries to fake a search call even when tools are disabled," because I'd made the instruction too forceful without constraining the fallback path to match
tool_choice="none"is a real, explicit way to forbid tool use for one specific call — silently omitting thetoolsparameter isn't the same thing, and a model conditioned strongly enough will still try to call something even with no schema in front of it- Multi-step agentic behavior (search → reason → calculate → answer) is genuinely a different engineering problem than single-tool calling — more moving parts, more failure surface, and the bugs compound on each other instead of staying isolated
Day 25 — Multi-Agent System with CrewAI, moving from one agent juggling multiple tools to multiple agents, each with their own role, collaborating on a shared task.
I'm Prashik, building one AI project a day for 100 days to become a job-ready AI Engineer. Follow my journey on LinkedIn.