6.6 KiB
6.6 KiB
Gateway-Level Tool Use v2
Техника реализации function calling на уровне гейтвея, когда upstream API не поддерживает нативный tool calling.
Суть
Модель получает определения тулзов через структурированный системный промпт, а не через нативный tools параметр API. Гейтвей парсит ответ и возвращает OpenAI-совместимый tool_calls формат.
Промпт-инжект v2
def build_tool_prompt(tools: list, tool_choice) -> str:
tool_defs = []
for i, tool in enumerate(tools):
t = tool.get("function", tool)
params = json.dumps(t.get("parameters", {}), ensure_ascii=False)
tool_defs.append(
f"### {t['name']}\n"
f"- Description: {t.get('description', 'No description')}\n"
f"- Parameters: `{params}`\n"
f"- Call ID: `call_{t['name']}_{i}`\n"
)
# Forced tool choice
forced_tool = None
if isinstance(tool_choice, dict):
forced_tool = tool_choice.get("function", {}).get("name")
elif tool_choice == "required":
forced_tool = "__required__"
force_line = ""
if forced_tool and forced_tool != "__required__":
force_line = f"\n**CRITICAL: You MUST call `{forced_tool}`. No text — only JSON.**\n"
elif forced_tool == "__required__":
force_line = "\n**CRITICAL: You MUST call one of the functions. No text — only JSON.**\n"
return (
"# SYSTEM: Function Calling Mode\n\n"
"You are a function-calling assistant. Available functions:\n\n"
+ "\n".join(tool_defs)
+ "\n\n## Response Format\n"
"```json\n"
"{\n"
' "tool_calls": [\n'
' {\n'
' "id": "call_<name>_<N>",\n'
' "type": "function",\n'
' "function": {\n'
' "name": "<name>",\n'
' "arguments": "<JSON-string>"\n'
' }\n'
' }\n'
' ]\n'
'}\n'
"```\n\n"
"## Rules\n"
"- If the user's request requires a function, respond with ONLY the JSON above — no other text.\n"
"- The `arguments` field MUST be a valid JSON string (double-escaped).\n"
"- Use correct JSON syntax — no trailing commas, no comments.\n"
+ force_line
+ "\n## User Request\n"
)
Трёхуровневый парсинг v2
def parse_tool_calls(text: str) -> list | None:
"""Level 1: direct JSON parse"""
try:
data = json.loads(text.strip())
if "tool_calls" in data:
return _normalize(data["tool_calls"])
except: pass
"""Level 2: JSON in markdown code blocks"""
for pat in [
r'```json\s*(\{[\s\S]*?"tool_calls"[\s\S]*?\})\s*```',
r'```\s*(\{[\s\S]*?"tool_calls"[\s\S]*?\})\s*```',
]:
m = re.search(pat, text)
if m:
try:
data = json.loads(m.group(1))
if "tool_calls" in data:
return _normalize(data["tool_calls"])
except: continue
"""Level 3: regex fallback — any JSON with tool_calls"""
for m in re.finditer(r'\{[\s\S]*?"tool_calls"[\s\S]*?\}', text):
try:
data = json.loads(m.group(0))
if "tool_calls" in data:
return _normalize(data["tool_calls"])
except: continue
return None
def _normalize(tool_calls):
"""Validate and normalize: arguments must be valid JSON string."""
for tc in tool_calls:
fn = tc.get("function", {})
args = fn.get("arguments", "{}")
if isinstance(args, dict):
args = json.dumps(args, ensure_ascii=False)
elif isinstance(args, str):
json.loads(args) # validate
fn["arguments"] = args
return tool_calls
Retry-логика
TOOL_RETRY_MAX = 2
for attempt in range(TOOL_RETRY_MAX + 1):
# ... execute chat ...
tool_calls = parse_tool_calls(full_text)
requires_tool = (
isinstance(tool_choice, dict) or
tool_choice == "required"
)
if requires_tool and not tool_calls and attempt < TOOL_RETRY_MAX:
# Усиливаем промпт
user_content += (
"\n\n**ERROR: You did not call the required function. "
"Respond with ONLY the function call JSON. No other text.**"
)
continue
break
OpenAI-ответ
Нестрим
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_get_weather_0",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"London\"}"
}
}]
},
"finish_reason": "tool_calls"
}]
}
Стрим
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_get_weather_0","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}
data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}
data: [DONE]
tool_choice — полная поддержка
| Значение | Поведение |
|---|---|
"auto" (default) |
Инжектить тулзы, модель сама решает |
"none" |
Не инжектить тулзы, модель отвечает текстом |
"required" |
Форсировать вызов любого тула + retry |
{type:"function", function:{name:"X"}} |
Форсировать конкретный тул + retry |
Ограничения
- Слабые модели (GPT-5 Nano, мелкие Llama) могут не следовать формату. Retry помогает но не гарантирует.
- Стриминг — tool_calls не парсятся на лету, клиент должен накопить полный ответ.
- Multi-turn —
toolrole сообщения +tool_call_idне пробрасываются (одноразовые сессии). - Parallel tool calls — теоретически поддерживается форматом, но модель редко выдаёт больше одного.
Рекомендуемые модели
Для надёжного tool use через промпт-инжект:
- DeepSeek V3.2 — "agentic tool-use performance"
- Mistral Small 3.2 24B — "improved function calling"
- Inkling (Thinking Machines) — "tool-use systems"
- Command R7B (Cohere) — "tool use, agents"