Files

191 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_<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
```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"