# Gateway-Level Tool Use v2 Техника реализации function calling на уровне гейтвея, когда upstream API не поддерживает нативный tool calling. ## Суть Модель получает определения тулзов через структурированный системный промпт, а не через нативный `tools` параметр API. Гейтвей парсит ответ и возвращает OpenAI-совместимый `tool_calls` формат. ## Промпт-инжект v2 ```python 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__",\n' ' "type": "function",\n' ' "function": {\n' ' "name": "",\n' ' "arguments": ""\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 ```python 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-логика ```python 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-ответ ### Нестрим ```json { "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** — `tool` role сообщения + `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"