Function calling (also called tool use) is what transforms a chatbot into an agent. The model decides when to call your code, what arguments to pass, and how to interpret the results — all through a standardized API format.
The good news: Chinese AI models support function calling through the exact same OpenAI-compatible interface you already know. No new SDK, no new format. Just change the base URL and model name.
In this tutorial, you'll build a working agent with three tools (weather lookup, database query, web search), test it against DeepSeek V4 Pro, GLM-5, and Kimi K3, and learn the differences between their function calling implementations.
Function calling works in a loop:
1. User sends a message
2. Model decides: reply directly OR call a tool
3. If tool call: your code executes the function, returns result
4. Model reads the result, decides: call another tool OR reply to user
5. Repeat 3-4 until model generates a final answer
The model outputs structured JSON specifying which function to call and with what parameters. Your code parses this, runs the function, and feeds the result back. The model never "runs" your code directly — it's a controlled delegation pattern.
| Model | Function Calling | Parallel Calls | Input $/1M | Best For |
|---|---|---|---|---|
| DeepSeek V4 Pro | ✅ Full | ✅ Yes | $0.48 | General-purpose agents |
| DeepSeek V4 Flash | ✅ Full | ✅ Yes | $0.14 | High-volume, simple tools |
| GLM-5 | ✅ Full | ⚠️ Limited | $0.15 | Chinese-language agents |
| GLM-5-turbo | ✅ Full | ⚠️ Limited | $0.15 | Fast tool calls |
| Kimi K3 | ✅ Full | ✅ Yes | $1.09 | Long-context tool use |
| ERNIE 5.0 | ⚠️ Varies by version | ❌ No | $2.47 | Not recommended — use DeepSeek instead |
Recommendation: Use DeepSeek V4 Pro as your default for function calling. It has the best balance of quality, speed, and parallel call support. Use DeepSeek V4 Flash for high-volume simple tool calls (cost: $0.14/M).
Let's start with the simplest possible function calling setup:
import os
import json
import openai
client = openai.OpenAI(
api_key=os.environ["AIWAVE_API_KEY"],
base_url="https://aiwave.live/v1", # OpenAI-compatible
)
# Define your tool
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Tokyo' or 'New York'",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["city"],
},
},
}
]
# Simulated weather function (replace with real API)
def get_weather(city: str, unit: str = "celsius") -> dict:
"""Your actual implementation here."""
# In production: call a weather API
temps = {"Tokyo": 28, "New York": 32, "London": 18, "Singapore": 30}
temp = temps.get(city, 22)
if unit == "fahrenheit":
temp = temp * 9 / 5 + 32
return {"city": city, "temperature": temp, "unit": unit, "condition": "sunny"}
# Make the call
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
tools=tools,
)
# Check if model wants to call a function
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# Execute the function
if func_name == "get_weather":
result = get_weather(**func_args)
print(f"Weather data: {result}")
# Feed result back to the model
response2 = client.chat.completions.create(
model="deepseek-v4-pro",
messages=[
{"role": "user", "content": "What's the weather in Tokyo?"},
message, # The tool call message
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
},
],
tools=tools,
)
print(f"Final answer: {response2.choices[0].message.content}")
else:
print(f"Direct reply: {message.content}")
Key point: The
base_urlis the only thing that changes from standard OpenAI code. The function calling format, message structure, and tool response format are identical. This is why AIWave calls itself "OpenAI-compatible" — it really is.
Now let's build a proper agent that automatically executes tool calls in a loop:
def execute_tools(tool_calls: list) -> list[dict]:
"""Execute all tool calls and return results."""
results = []
for tc in tool_calls:
func_name = tc.function.name
func_args = json.loads(tc.function.arguments)
# Dispatch to registered functions
if func_name == "get_weather":
result = get_weather(**func_args)
elif func_name == "query_database":
result = query_database(**func_args)
elif func_name == "web_search":
result = web_search(**func_args)
else:
result = {"error": f"Unknown function: {func_name}"}
results.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result),
})
return results
def run_agent(query: str, model: str = "deepseek-v4-pro", max_turns: int = 10) -> str:
"""Run an agent loop until it produces a final answer."""
messages = [{"role": "user", "content": query}]
for turn in range(max_turns):
response = client.chat.completions.create(
model=model,
messages=messages,
tools=all_tools,
)
message = response.choices[0].message
messages.append(message) # Add assistant message to history
if not message.tool_calls:
# Model gave a final answer (no more tool calls)
return message.content
# Execute tools and add results
tool_results = execute_tools(message.tool_calls)
messages.extend(tool_results)
print(f"Turn {turn + 1}: Called {len(message.tool_calls)} tool(s)")
return "Agent reached maximum turns without producing an answer."
We tested all three models with identical prompts. Here's how they differ in function calling behavior:
| Model | Correct Tools | Tool Arguments | Multi-step Reasoning | Latency |
|---|---|---|---|---|
| DeepSeek V4 Pro | ✅ 2 calls in sequence | ✅ Correct args | ✅ Connected results | 3.2s |
| GLM-5 | ✅ 2 calls | ✅ Correct args | ⚠️ Slightly generic | 2.8s |
| Kimi K3 | ✅ 2 calls (parallel attempted) | ✅ Correct args | ✅ Connected results | 4.1s |
DeepSeek V4 Pro and Kimi K3 support parallel tool calls — calling multiple independent tools simultaneously:
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=[{"role": "user", "content":
"Compare the weather in Tokyo, London, and New York"}],
tools=tools,
)
message = response.choices[0].message
# DeepSeek V4 Pro returns multiple tool_calls in one response:
# tool_calls[0]: get_weather(city="Tokyo")
# tool_calls[1]: get_weather(city="London")
# tool_calls[2]: get_weather(city="New York")
# Execute all in parallel (they're independent)
import concurrent.futures
def execute_single(tc):
args = json.loads(tc.function.arguments)
return {"role": "tool", "tool_call_id": tc.id,
"content": json.dumps(get_weather(**args))}
with concurrent.futures.ThreadPoolExecutor() as pool:
results = list(pool.map(execute_single, message.tool_calls))
messages.extend(results)
response2 = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=tools,
)
print(response2.choices[0].message.content)
Parallel calls save 50-70% latency for multi-tool queries. Instead of 3 sequential API calls (3 × 1s = 3s), you get 1 call with 3 tools + parallel execution (~1.5s total).
Always validate tool results before feeding them back to the model:
def safe_execute(tool_call) -> dict:
"""Execute tool call with error handling and validation."""
try:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# Validate arguments
if func_name == "get_weather":
if not func_args.get("city"):
return {"role": "tool", "tool_call_id": tool_call.id,
"content": json.dumps({"error": "City is required"})}
result = TOOL_REGISTRY[func_name](**func_args)
# Truncate large results to prevent context overflow
result_str = json.dumps(result)
if len(result_str) > 4000:
result_str = result_str[:4000] + "... (truncated)"
return {"role": "tool", "tool_call_id": tool_call.id, "content": result_str}
except json.JSONDecodeError:
return {"role": "tool", "tool_call_id": tool_call.id,
"content": json.dumps({"error": "Invalid JSON arguments"})}
except Exception as e:
return {"role": "tool", "tool_call_id": tool_call.id,
"content": json.dumps({"error": str(e)})}
from functools import wraps
TOOL_REGISTRY = {}
def register_tool(name: str, description: str, parameters: dict):
"""Decorator to register a function as a tool."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
TOOL_REGISTRY[name] = {
"function": wrapper,
"definition": {
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
},
},
}
return wrapper
return decorator
# Usage
@register_tool(
name="get_weather",
description="Get weather for a city",
parameters={
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
)
def get_weather(city: str, unit: str = "celsius") -> dict:
temps = {"Tokyo": 28, "New York": 32, "London": 18}
return {"city": city, "temp": temps.get(city, 22), "unit": unit}
# Get tool definitions for API call
all_tools = [v["definition"] for v in TOOL_REGISTRY.values()]
MAX_AGENT_TOKENS = 50_000 # Stop agent if conversation exceeds this
def run_agent_with_budget(query: str) -> str:
messages = [{"role": "user", "content": query}]
while True:
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=all_tools,
)
# Check token budget
total_tokens = response.usage.total_tokens
if total_tokens > MAX_AGENT_TOKENS:
return f"Agent stopped: token budget exceeded ({total_tokens:,} tokens). Last response: {response.choices[0].message.content}"
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content
messages.extend(execute_tools(message.tool_calls))
The model needs to see the tool_call message AND the tool response in the conversation history. If you skip either, the model loses context:
# ❌ WRONG: Only sending tool result
messages.append({"role": "tool", "content": result})
# ✅ CORRECT: Send assistant's tool_call, then tool result
messages.append(assistant_message) # Contains tool_calls
messages.append({"role": "tool", "tool_call_id": "...", "content": result})
Keep tool parameters simple. Complex nested schemas confuse the model:
# ❌ TOO COMPLEX (model struggles)
{"properties": {"address": {"properties": {"street": ..., "city": ...}}}}
# ✅ FLAT (model nails it)
{"properties": {"street": {"type": "string"}, "city": {"type": "string"}}}
For function calling, use temperature=0 or 0.1. Higher temperatures cause the model to hallucinate tool names or invent parameters:
# ❌ temperature=0.7 → model invents function names
# ✅ temperature=0.1 → reliable function calling
"""
Production Agent Framework with Chinese AI Models
pip install openai
"""
import os, json
import openai
from typing import Callable
client = openai.OpenAI(
api_key=os.environ["AIWAVE_API_KEY"],
base_url="https://aiwave.live/v1",
)
class Agent:
def __init__(self, model: str = "deepseek-v4-pro", system_prompt: str = "You are a helpful assistant."):
self.model = model
self.system_prompt = system_prompt
self.tools: list[dict] = []
self.functions: dict[str, Callable] = {}
self.conversation: list[dict] = []
def tool(self, name: str, description: str, parameters: dict):
"""Decorator to register a tool."""
def decorator(func):
self.functions[name] = func
self.tools.append({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
},
})
return func
return decorator
def run(self, query: str, max_turns: int = 10) -> str:
"""Execute agent loop."""
self.conversation = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": query},
]
for turn in range(max_turns):
response = client.chat.completions.create(
model=self.model,
messages=self.conversation,
tools=self.tools if self.tools else openai.NOT_GIVEN,
temperature=0.1,
)
msg = response.choices[0].message
self.conversation.append(msg)
if not msg.tool_calls:
return msg.content
for tc in msg.tool_calls:
try:
func = self.functions[tc.function.name]
args = json.loads(tc.function.arguments)
result = func(**args)
self.conversation.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result),
})
except Exception as e:
self.conversation.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps({"error": str(e)}),
})
return "Agent exceeded maximum turns."
# === Usage ===
agent = Agent(
model="deepseek-v4-pro",
system_prompt="You are a weather and data assistant.",
)
@agent.tool("get_weather", "Get current weather for a city", {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
})
def get_weather(city: str) -> dict:
temps = {"Tokyo": 28, "New York": 32, "London": 18}
return {"city": city, "temp": temps.get(city, 22), "unit": "celsius"}
answer = agent.run("What's the weather in Tokyo and how does it compare to London?")
print(answer)
Get $5 to test function calling with DeepSeek V4 Pro, GLM-5, Kimi K3, and 50+ more models. Same OpenAI SDK, 95% lower cost.