autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
191
skills/reverse-proxy-gateway/references/gateway-tool-use.md
Normal file
191
skills/reverse-proxy-gateway/references/gateway-tool-use.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 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"
|
||||
Reference in New Issue
Block a user