Проверьте сырой вызов инструмента от модели по схеме и дайте один повтор при нарушении.
Runtime агента получает сырой вызов инструмента, выданный моделью. До проверки ничего выполняться не должно.
Ограничения:
- Имя инструмента должно быть зарегистрированным; незнакомые имена отклоняются.
- Аргументы должны соответствовать объявленной схеме — обязательные поля на месте, типы верные, лишних полей нет.
- При нарушении верните наблюдение с текстом ошибки, читаемое моделью, и дайте не более одного повтора.
- Только стандартная библиотека.
import json
TOOLS = {
"get_weather": {"required": {"city": str}, "optional": {"units": str}},
}
def handle_tool_call(raw: str, ask_model) -> dict:
# `ask_model(error_text)` возвращает новую строку сырого вызова.
# TODO: разберите, проверьте по TOOLS, при успехе отправьте в работу;
# при нарушении верните наблюдение с ошибкой и дайте не более одного повтора.
...
Допишите реализацию.
Разберите сырой вызов, проверьте имя инструмента и каждый аргумент по объявленной схеме и отправляйте только при успехе. При нарушении верните текст ошибки как наблюдение, дайте один повтор, затем аккуратно завершитесь — частичный вызов не отправляется.
- ✗Отправлять в работу вызов, чьи аргументы не проверялись по схеме
- ✗Повторять бесконечно вместо ограничения числа повторов и аккуратного отказа
- ✗Возвращать голый сбой без текста ошибки, из-за чего модель не может исправиться
- →Почему текст ошибки проверки должен дойти до модели, а не только до логов?
- →Где ограничить повторы, чтобы плохой вызов не сжёг весь бюджет шагов?
Проверка идёт до любого выполнения: сначала имя, затем обязательные поля и типы, затем отсутствие лишних полей. Текст ошибки возвращается модели как наблюдение — это и есть шаг, дающий ей шанс исправиться.
import json
def _validate(call: dict) -> str | None:
name = call.get("name")
spec = TOOLS.get(name)
if spec is None:
return f"unknown tool {name!r}; registered: {sorted(TOOLS)}"
args = call.get("arguments") or {}
allowed = {**spec["required"], **spec["optional"]}
for field, typ in spec["required"].items():
if field not in args:
return f"missing required argument {field!r}"
if not isinstance(args[field], typ):
return f"argument {field!r} must be {typ.__name__}"
for field in args:
if field not in allowed:
return f"unknown argument {field!r}"
return None
def handle_tool_call(raw: str, ask_model) -> dict:
for attempt in range(2):
try:
call = json.loads(raw)
except json.JSONDecodeError as exc:
error = f"call is not valid JSON: {exc.msg}"
else:
error = _validate(call)
if error is None:
return dispatch(call["name"], call.get("arguments") or {})
if attempt == 0:
raw = ask_model(error)
return {"status": "rejected", "error": error}
Повтор ровно один, поэтому плохой вызов не выедает бюджет шагов. Частично валидный вызов не отправляется никогда — обработчик возвращает отказ с причиной.