DEV Community

Cover image for My AI Chatbot Started "Faking" Tool Calls in Plain Text. Here's Why.
Nikhil Kamani
Nikhil Kamani

Posted on

My AI Chatbot Started "Faking" Tool Calls in Plain Text. Here's Why.

I built a small AI "digital twin": a chatbot that answers questions about my background the way I would, and calls a tool to notify me the moment someone wants to get in touch.
Stack is Groq's API running llama-3.3-70b-versatile, Gradio for the UI, deployed on Hugging Face Spaces.

I skipped LangChain and any agent framework on purpose. I wanted to actually watch an agent decide when and how to use a tool, not have a framework make that decision invisible to me.

That choice is what taught me the most, not about building a chatbot, but about how an LLM actually behaves once you hand it a tool and just tell it "use this when needed."

The bug

I gave the model a record_user_details tool: if a visitor wants to be contacted, the model calls it with their email, and I get a push notification. Simple enough.

Except every so often, instead of triggering the tool, the model would just print this straight into the chat reply:


<function=record_user_details>{"email": "someone@example.com", "notes": "wants to connect"}</function>

Visible to the visitor, as plain text. And since it never actually went through the API's tool-call mechanism, my code never saw it. No notification. No record. The model was "calling" a function that, as far as my backend was concerned, didn't happen.

Why it happens

Llama 3.x models were trained on two different ways to represent a tool call:
the structured tool_calls field an API returns and a text-based invocation syntax using special tags. Depending on prompt shape and how the serving layer steers the model, it sometimes reaches for the second one instead of the first, especially under ambiguous instructions about when a tool should fire at all.

This isn't a Groq-specific bug, it's a known quirk of the model itself. I only found this by staring at raw completions in my logs instead of trusting the SDK's parsed response, then cross-checking against the model card and a few GitHub issues describing the exact same symptom.

The fix (three layers, because one alone wasn't enough)

  1. temperature=0. Doesn't eliminate the behavior, but makes the model far more consistent about which format it reaches for.

  2. Tightened the tool description. I was vague about when to call the tool. Being explicit about the trigger condition, and explicit that it should never be described in the reply text, cut down the ambiguous cases where the model improvised.

  3. Defensive parsing. This is the one that actually matters for a real visitor. Even with the first two fixes, I don't fully control the model. So I wrapped the response handling to detect a malformed, text-only "call" and fail soft: a polite "could you rephrase that?" instead of leaking a raw function tag into someone's chat window.

What this taught me about agent behavior

The model isn't "deciding" to call a tool the way the word "agent" makes it sound. It's predicting the next most likely tokens, and sometimes the most likely tokens look like a tool call without ever going through the real mechanism.
A framework would have smoothed that over for me automatically. Hand-rolling it meant I actually saw, first-hand, how much less deterministic "agent behavior" is than the term implies.

Try it: https://huggingface.co/spaces/nik1988/digital-twin

(Note: it's on a free Hugging Face Space, so it may have gone to sleep from inactivity. If it doesn't load right away, hit "Restart Space" and give it a few seconds to spin back up.)

Curious about two things from anyone who's built something similar:

If you were extending a digital-twin chatbot like this, what would you add next?
I've been going back and forth on whether it's worth more tooling (calendar booking, resume Q&A grounded in a vector store) or keeping it deliberately minimal.

Drop your ideas below, genuinely want to see what other people have tried with this.

Top comments (0)