Инструмент (Tool) — это механизм интеграции LLM с внешним миром. Технически инструменты реализуются в виде функций или методов классов и позволяют компенсировать врождённые ограничения языковых моделей.
Основные ограничения LLM:
- Временные (Knowledge Cutoff): модель знает только данные, актуальные на момент окончания обучения. Например, если обучение завершилось в 2025 году, события 2026 года ей будут неизвестны.
- Вычислительные: LLM плохо справляется с точной арифметикой и сложными математическими операциями. Это статистическая машина предсказания токенов, поэтому она не может надёжно вычислять значения (например, считать количество символов в строке или возводить числа в степень).
- Цитируемость (Галлюцинации): LLM не гарантирует точную цитату из источника. Она может дополнять, перефразировать или воспроизводить текст вероятностно.
Замечание 1. Это далеко не все ограничения, но именно эти три лучше всего демонстрируют роль инструментов, которые позволяют легко обходить «врождённые» слабости моделей.
Замечание 2. Инструменты являются ключом к трансформации LLM в автономного агента. Согласно концепции Лилиан Вэн (Lilian Weng), автономный агент описывается формулой:
Агент = LLM + Память + Планирование + Использование инструментов (Tool Use)
(где планирование — это умение разбивать задачу на подзадачи и проверять результаты на каждом этапе).
Генерация JSON вместо обычного текста
В действительности модель ничего «не знает» про инструменты и тем более не вызывает их сама. Интеграция с внешним миром — это просто генерация текста в специальном структурированном формате, который легко парсится внешним кодом для вызова нужной функции с заданными аргументами. Наиболее подходящий кандидат на роль такого формата — JSON.
Таким образом, LLM должна быть заранее обучена распознавать специальные паттерны вызова: не вести диалог, а генерировать строго структурированный ответ для исполнения внешним скриптом.
Замечание 3. Что значит «LLM должна быть заранее подготовлена»?
Базовые модели после этапа предобучения (pre-training) проходят стадию дообучения (fine-tuning). Именно на этом этапе модель учится понимать: на какой запрос нужно вернуть не обычный текст, а JSON с именем функции и её аргументами.
Важно отметить: не все модели умеют генерировать корректный JSON для вызова внешних функций. Если модель не обучали распознавать JSON на этапе дообучения, она не сможет полноценно работать с инструментами.Замечание 4. LLM может выдавать «битый» (невалидный) JSON — и это вполне нормальное поведение. У модели нет встроенного «понимания» синтаксиса JSON и компилятора для самопроверки.
В современных API (OpenAI, Anthropic) и локальных инференс-серверах (vLLM, Ollama) валидация встроена не в саму нейросеть, а в движок инференса (сэмплер), который её обслуживает. Движок принудительно направляет генерацию токенов по грамматике JSON (технология Structured Output).
Порядок работы с инструментами в LLM
Упрощённая (линейная) схема для наглядности:
1. Промт с описанием доступных инструментов (запрос к LLM)
↓
2. Генерация JSON с выбранным инструментом (ответ LLM)
↓
3. Парсинг JSON: извлечение имени функции и аргументов
↓
4. Вызов реальной функции/метода с этими аргументами
↓
5. Подстановка результатов вызова обратно в контекст (запрос к LLM)
↓
6. Генерация финального ответа с учётом полученных данных
Данная схема описывает простейший сценарий с одним вызовом.
Реальная картина (Агентский цикл)
В действительности процесс цикличен: шаг генерации JSON с запросом к инструменту может повторяться многократно. Такой цикл часто называют классическим агентским циклом (Agent Loop / паттерн ReAct).
Важно отметить: классический агентский цикл подразумевает, что LLM помнит историю ВСЕГО диалога, иначе было бы проблематично вызывать подходящие инструменты.
LLM может запросить 1, 2 или даже 10 инструментов подряд, прежде чем решит выдать финальный ответ пользователю. Цикл прерывается только тогда, когда модель решает, что собрала достаточно данных, и возвращает обычный текст вместо JSON.
Этот псевдокод отражает реальную архитектуру:
context = [user_query, tools_description]
while True:
response = LLM.generate(context)
# ПРОВЕРКА: LLM хочет использовать инструмент?
if response.get("tool_calls"):
tool_name, args = parse_json(response.tool_call)
tool_result = execute_function(tool_name, args)
# Замыкаем цикл: добавляем результат в память
context.append(tool_result)
else:
# Разрываем цикл и отдаём финальный ответ
return response.text
Цикл while крутится до тех пор, пока API LLM не вернёт признак остановки (например, отсутствие поля tool_calls в ответе или специальный stop_reason).
Готовим агентский цикл — рецепт
1. Настройка клиента и системного промпта
import json
import requests
from datetime import datetime, timedelta
LLM_MODEL = "qwen2.5:14b"
LLM_BASE_URL = "http://localhost:11434/v1" # запуск Ollama с локальной моделью
LLM_HEADERS = {
"Content-Type": "application/json"
}
# Задать промпт — он должен содержать информацию об инструментах
SYSTEM_PROMPT = """\
Ты помощник, который умеет считать даты на разные дни. В ответе только нужен конечный результат без рассуждений.
Ты имеешь доступ к инструменту get_future_date, который возвращает дату через N дней от сегодня.
"""
# Задать список инструментов:
LLM_TOOLS = [
{
"type": "function",
"function": {
"name": "get_future_date",
"description": "Возвращает дату через N дней от сегодня.",
"parameters": {
"type": "object",
"properties": {
"days": {
"type": "integer",
"description": "Количество дней",
}
},
"required": ["days"],
},
},
}
]
И функция для вызова LLM:
def llm_generate(messages: list[dict[str, object]], tools: list[dict]) -> dict:
payload = {
"model": LLM_MODEL,
"messages": messages,
"tools": tools,
"tool_choice": "auto",
"temperature": 0.1,
"max_tokens": 4096,
}
response = requests.post(
f"{LLM_BASE_URL}/chat/completions", json=payload, headers=LLM_HEADERS
)
response.raise_for_status()
return response.json()["choices"][0]["message"]
2. Реализовать функцию-инструмент
def get_future_date(days: int) -> str:
"""Возвращает дату через N дней от сегодня."""
future = datetime.now() + timedelta(days=days)
return future.strftime("%Y-%m-%d")
AVAILABLE TOOLS = {
"get_future_date": get_future_date
}
3. Реализовать агентский цикл
MAX_ITERATIONS = 10 # защита от бесконечного цикла при галлюцинациях LLM
def agent_cycle(
user_query: str,
system_promt: str = SYSTEM_PROMPT,
tools: list[dict] = LLM_TOOLS,
available_fc: dict = AVAILABLE TOOLS
) -> str | None:
messages: list[dict[str, object]] = [
{"role": "system", "content": system_promt},
{"role": "user", "content": user_query},
]
for step in range(MAX_ITERATIONS):
print(f"🔄 Итерация {step + 1}/{MAX_ITERATIONS}")
response = llm_generate(messages, tools)
content = (response.get("content") or "").strip()
if not response.get("tool_calls"):
print("Финальный ответ:", response["content"])
break
messages.append(
{
"role": "assistant",
"tool_calls": response["tool_calls"],
"content": content or None,
}
)
for tool_call in response["tool_calls"]:
name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
print(f"🔧 Вызов инструмента: {name}({args})")
result = str(available_fc.get(name)(**args))
messages.append(
{"role": "tool", "tool_call_id": tool_call["id"], "content": result}
)
return "⚠️ Достигнут лимит итераций."
4. Пример вызова
agent_cycle("Какой день недели будет через 100 дней?")
Полный пример кода доступен — ....
Важно отметить: из-за потенциальных проблем с галлюцинациями обычно рекомендуют реализовывать конечный цикл, а не бесконечный.
На сегодняшний день агентов реализуют с помощью различных библиотек. Наиболее популярное решение — это LangChain. Тут важно прописывать docstring для того, чтобы передать эти данные в список инструментов при запросе к API LLM. Для этого используют декоратор @tool:
from langchain_core.tools import tool
@tool
def bash(command: str) -> str:
"""Execute a shell command and return the output."""
...
Поэтому для работы с инструментами крайне важно составить качественную схему для их применения. Так как LLM "видит" только структуру JSON и ориентируется по ней — какие аргументы нужно извлечь из запроса пользователя, какую функцию вызвать (название функции в промпте).
Важно отметить: для актуализации информации доступны пакеты DuckDuckGo, SearXNG и др. Для сложных вычислений можно использовать eval в Python или shell (оба решения небезопасны), поэтому лучше использовать сторонние библиотеки simple_eval. Для цитирования и точного воспроизведения хранить данные и осуществлять по ним поиск.
Скиллы (Skills) как надстройка над инструментами
Скилл (skill) — это логически завершённая композиция из:
- набора инструментов (tools),
- специализированного системного промпта,
- сценария (workflow) их совместного использования.
Если инструмент решает одну атомарную задачу (bash, get_future_date, search_web), то скилл решает составную — например, «исследовать тему в интернете», «написать и протестировать скрипт», «проанализировать репозиторий».
Рассмотрим пример с датами (get_future_date) и упакуем его в полноценный скилл «Ассистент по датам».
Пример скилла «Ассистент по датам»
1. Внутренние инструменты скилла
Скилл содержит несколько атомарных функций:
from datetime import datetime, timedelta
def get_current_date() -> str:
"""Возвращает сегодняшнюю дату."""
return datetime.now().strftime("%Y-%m-%d")
def get_future_date(days: int) -> str:
"""Возвращает дату через N дней от сегодня."""
future = datetime.now() + timedelta(days=days)
return future.strftime("%Y-%m-%d")
def get_weekday(date_str: str) -> str:
"""Возвращает день недели для даты в формате YYYY-MM-DD."""
weekdays = ["понедельник", "вторник", "среда", "четверг",
"пятница", "суббота", "воскресенье"]
try:
d = datetime.strptime(date_str, "%Y-%m-%d")
return weekdays[d.weekday()]
except ValueError:
return "Неверный формат даты. Ожидается YYYY-MM-DD."
2. Схема и промпт скилла
import json
# Внутренние инструменты для скилла
TOOLS = [
{
"type": "function",
"function": {
"name": "get_current_date",
"description": "Возвращает сегодняшнюю дату.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "get_future_date",
"description": "Возвращает дату через N дней от сегодня.",
"parameters": {
"type": "object",
"properties": {
"days": {"type": "integer", "description": "Количество дней"}
},
"required": ["days"],
},
},
},
{
"type": "function",
"function": {
"name": "get_weekday",
"description": "Возвращает день недели для указанной даты.",
"parameters": {
"type": "object",
"properties": {
"date_str": {
"type": "string",
"description": "Дата в формате YYYY-MM-DD"
}
},
"required": ["date_str"],
},
},
},
]
SYSTEM_PROMPT = """\
Ты — ассистент по датам. У тебя есть инструменты для работы с датами.
Если пользователь спрашивает про будущую дату — используй get_future_date,
затем при необходимости get_weekday, чтобы узнать день недели.
Отвечай кратко и по делу на русском языке.
"""
TOOL_REGISTRY = {
"get_current_date": get_current_date,
"get_future_date": get_future_date,
"get_weekday": get_weekday,
}
3. Функция-обёртка скилла (ассистент по датам)
def skill_cycle(
user_query: str
) -> str | None:
return agent_cycle(
user_query,
SYSTEM_PROMPT,
TOOLS,
TOOL_REGISTRY
)
4. Регистрация скилла у агента
SKILL = [
{
"type": "function",
"function": {
"name": "skill_cycle",
"description": (
"Отвечает на любые вопросы про даты: сегодняшняя дата, "
"дата через N дней, день недели и т.п. "
"Передавай вопрос пользователя как есть."
),
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "Вопрос пользователя про даты."
}
},
"required": ["question"],
},
},
}
]
SKILL_REGISTRY = {
"skill_cycle": skill_cycle,
}
5. Пример вызова скилла
agent_cycle(
user_query="Какой день недели будет через 100 дней?",
system_promt="Ты главный ассистент. Используй доступные скиллы.",
tools=SKILL,
available_fc=SKILL_REGISTRY
)
В данной реализации легко запутаться, потому что заворачивается целый агентский цикл в одну функцию, а затем регистрируете её как обычный инструмент для внешнего агента. Поэтому субагент и есть скилл для главного агента.
Главный агент
└─ вызывает skill_cycle(question="Какой день недели будет через 100 дней?")
└─ Субагент (тот же agent_cycle, но с tools, которые составляют атомарный skill)
├─ get_future_date(days=100) → "2026-11-18"
├─ get_weekday(date_str="2026-11-18") → "среда"
└─ формирует финальный ответ
Скилл можно реализовать и без субагента, например, как обычную Python-функцию, которая просто вызывает несколько тулов по фиксированному сценарию:
def skill_cycle_fixed(question: str) -> str:
# Без LLM, без цикла — просто жёсткий сценарий
future = get_future_date(days=100)
weekday = get_weekday(future)
return f"Через 100 дней будет {weekday}, {future}"
В этом случае скилл всё ещё остаётся скиллом для главного агента, но внутри уже нет «интеллекта» — только код. Разработчик сам решает, что такое скилл и последовательно вызывает функции.
При реализации субагента и создается "правдоподобный" скилл, потому что:
- скилл сам решал, какие тулы вызывать и в каком порядке;
- логика скилла сложная и требует рассуждений.
Подведем итоги:
- Инструменты (Tools) компенсируют врождённые ограничения LLM (незнание будущего, ошибки в вычислениях, галлюцинации), позволяя модели делегировать атомарные задачи внешним функциям.
- Механизм вызова основан на генерации JSON: LLM не исполняет код сама, а отдаёт структурированный запрос (имя функции и аргументы), который перехватывает и выполняет внешний скрипт.
- Агентский цикл (ReAct) работает итеративно: результаты выполнения инструментов возвращаются в контекст (память) модели, и цикл повторяется до тех пор, пока LLM не выдаст финальный текстовый ответ.
- Контроль и безопасность требуют жесткого ограничения числа итераций (защита от зацикливания), валидации JSON на стороне инференс-движка и использования строгих реестров доступных функций.
- Скиллы (Skills) — это надстройка, объединяющая набор инструментов, специализированный промпт и логику субагента для решения сложных составных задач (в отличие от одиночного инструмента).
Оставить отзыв
Комментарии
Загрузка комментариев...
★ Оставить отзыв