Most AutoGen function calling debugging sessions waste time because engineers blame the model before checking the contract. In a multi-agent setup, a failed tool invocation is usually a schema mismatch, a swallowed exception, or a misrouted handoff. This guide gives an ordered path to find the real cause without guesswork.
1. Isolate the failing agent
Stop debugging the group chat first. Spin up a single AssistantAgent with the exact function_map and llm_config you use in production, and drive it with a UserProxyAgent in NEVER human mode.
import autogen
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": "sk-..."}],
"functions": [{
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}],
}
assistant = autogen.AssistantAgent("assistant", llm_config=llm_config)
user = autogen.UserProxyAgent("user", code_execution_config=False, human_input_mode="NEVER")
user.register_function({"get_weather": lambda city: f"Sunny in {city}"})
user.initiate_chat(assistant, message="What's the weather in Berlin?")
If the call works here, the bug lives in agent coordination or message history. If it fails here, the problem is schema, parsing, or model behavior.
2. Validate the function schema before send
AutoGen forwards your schema verbatim to the OpenAI-compatible endpoint. The most common failure is a schema that is valid Python but invalid for the model: missing required, type mismatches, or unsupported constructs like additionalProperties on nested objects without explicit handling.
Use jsonschema to validate the schema itself, not just the args:
from jsonschema import Draft7Validator
schema = llm_config["functions"][0]["parameters"]
Draft7Validator.check_schema(schema) # raises if malformed
Common pitfall: declaring a parameter as {"type": "string", "enum": [...]} but passing None as a default in the Python function signature. AutoGen does not auto-sync signatures; you must keep them aligned manually. Tradeoff: strict schemas improve reliability but reduce the model’s flexibility to pass loosely typed dicts.
3. Enable the only logs that matter
AutoGen swallows most internals unless you set its logger. Do this before agent construction:
import logging, autogen
autogen.logger.setLevel(logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
Watch for lines containing function_call and tool_responses. If you see the model return arguments as an empty string "", the model declined to call—often because the description was ambiguous. If you see a Python traceback inside the executor, your function threw and AutoGen caught it silently unless code_execution_config is set to surface errors.
4. Intercept the raw payload
When logs are insufficient, wrap the request. AutoGen’s OpenAIWrapper accepts a custom base_url. Point it at a local proxy (e.g., mitmproxy) or use a gateway that echoes requests. At minimum, print the constructed messages:
wrapper = autogen.OpenAIWrapper(config_list=llm_config["config_list"])
resp = wrapper.create(
messages=[{"role": "user", "content": "Weather in Berlin?"}],
functions=llm_config["functions"],
)
print(resp.choices[0].message.function_call)
Pitfall: AutoGen truncates system messages over max_tokens indirectly by context window limits, but it does not warn you. If your function descriptions live in a long system prompt, the model may ignore them. Keep function descriptions under 200 words total where possible.
5. Parse arguments defensively
Models do not always emit valid JSON in function_call.arguments. They emit a string that looks like JSON, sometimes with trailing commas or single quotes. Never eval it.
import json
def safe_parse(args_str):
try:
return json.loads(args_str)
except json.JSONDecodeError:
# strip single quotes, repair trailing commas
repaired = args_str.replace("'", '"').rstrip(",")
try:
return json.loads(repaired)
except json.JSONDecodeError:
return {}
In the agent loop, wrap the registered function:
def guarded_get_weather(city=None, **kwargs):
if not city:
return "Error: missing city"
return f"Sunny in {city}"
Tradeoff: repairing JSON hides model weakness but keeps the system running. Log every repair so you can later fine-tune the prompt or switch models.
6. Check the executor side
If the function is called but the agent acts like it wasn’t, the executor swallowed the result. With UserProxyAgent, set code_execution_config={"use_docker": False} and capture stdout. For pure function calls (not code blocks), ensure register_function maps the exact name returned by the model.
user.register_function({
"get_weather": guarded_get_weather
})
A frequent bug: the model returns getWeather (camelCase) but your schema said get_weather. OpenAI-compatible endpoints are case-sensitive on function names. AutoGen will error with “function not found” but may log it only at DEBUG.
7. Multi-agent handoff failures
In a GroupChat, each agent carries its own llm_config. If agent A can call get_weather but agent B cannot, and the conversation hands off to B after the user asks about weather, B will either hallucinate or stay silent.
Fix by sharing the function list:
def make_agent(name):
return autogen.AssistantAgent(name, llm_config=shared_llm_config)
group = autogen.GroupChat(agents=[make_agent("a"), make_agent("b")], messages=[])
Pitfall: GroupChatManager does not forward function_call results to agents that didn’t originate the call unless you explicitly include the tool response in the shared message history. Inspect group.messages after a failed turn; missing role: "tool" entries are the tell.
8. Use a resilient model gateway
When debugging points to provider flakiness—rate limits or 5xx errors during function calling—point AutoGen at an OpenAI-compatible gateway that handles fallback. For example, n4n.ai exposes one endpoint covering 240+ models and automatically routes to a healthy provider when the primary is degraded, while honoring your functions schema and cache-control hints.
llm_config = {
"config_list": [{
"model": "openai/gpt-4o",
"base_url": "https://api.n4n.ai/v1",
"api_key": os.environ["N4N_KEY"],
}],
"functions": llm_config["functions"],
}
This removes a whole class of “intermittent function calling failures” that are actually transport errors. Keep per-token metering on so you can correlate cost with debugging time.
9. Tradeoffs: when to abandon function calling
If after steps 1–7 you still see the model ignore a function 30% of the time, stop forcing it. Switch to structured output via JSON mode and parse the response yourself. Function calling adds latency and depends on the model’s instruction-following for tool use; for deterministic workflows, a strict prompt with response_format={"type": "json_object"} is easier to debug because the contract is just a schema, not a two-phase call.
resp = wrapper.create(
messages=[{"role": "user", "content": "Return JSON: {city: string}"}],
response_format={"type": "json_object"},
)
AutoGen function calling debugging is mostly hygiene: validate schemas, surface logs, guard parsing, and isolate agents. Do that in order and you will cut debug time from hours to minutes.