Обработка ошибок инструментов
Реальные инструменты ломаются. Разница между хрупким и надёжным агентом — в том, как он обрабатывает ошибки.
Ошибка как наблюдение — приём, при котором сбой инструмента не роняет агента, а возвращается модели текстом, чтобы она поняла причину и попробовала иначе.
Почему инструменты падают
- Плохие аргументы — модель передала
"10 / 0"или текст вместо числа. - Внешний сбой — API недоступен, тайм-аут, нет сети.
- Пустой результат — поиск ничего не нашёл (это не ошибка, но требует реакции).
Если такой сбой выбросит исключение и уронит цикл — агент мёртв. Правильно — поймать ошибку и превратить её в наблюдение, на которое модель отреагирует.
Диспетчер, устойчивый к ошибкам
Оборачиваем вызов в try/except. Что бы ни случилось внутри инструмента, диспетчер вернёт строку, а не упадёт.
def divide(arg):
a, b = arg.split(",")
return str(int(a) / int(b))
TOOLS = {"divide": divide}
def safe_dispatch(name, arg):
if name not in TOOLS:
return f"ОШИБКА: инструмент '{name}' не найден"
try:
return TOOLS[name](arg)
except Exception as e:
# ошибку не «глотаем», а возвращаем модели как наблюдение
return f"ОШИБКА инструмента: {e}"
print(safe_dispatch("divide", "10,2"))
print(safe_dispatch("divide", "10,0"))
print(safe_dispatch("divide", "десять,2"))
Вывод:
5.0 ОШИБКА инструмента: division by zero ОШИБКА инструмента: invalid literal for int() with base 10: 'десять'
Программа не упала ни разу. Каждая ошибка — это информативная строка, которую можно показать модели.
Как агент исправляется по ошибке
Теперь главное: получив ошибку как наблюдение, модель меняет действие. Смоделируем это заглушкой LLM, которая после ошибки выбирает другой аргумент.
def divide(arg):
a, b = int(arg.split(",")[0]), int(arg.split(",")[1])
if b == 0:
return "ОШИБКА: деление на ноль"
return str(a // b)
def llm(history):
text = "\n".join(history)
if "Observation:" not in text:
return "Action: divide[10,0]" # сначала пробует ноль
if "ОШИБКА: деление на ноль" in text and "divide[10,2]" not in text:
return "Action: divide[10,2]" # после ошибки исправляется
if "Observation: 5" in text:
return "Final Answer: 5"
return "Final Answer: ?"
history = ["Question: дай результат деления"]
for step in range(1, 5):
block = llm(history)
history.append(block)
print(f"[{step}] {block}")
if "Final Answer:" in block:
break
arg = block.split("divide[")[1].rstrip("]")
obs = divide(arg)
history.append(f"Observation: {obs}")
print(f" Observation: {obs}")
Вывод:
[1] Action: divide[10,0]
Observation: ОШИБКА: деление на ноль
[2] Action: divide[10,2]
Observation: 5
[3] Final Answer: 5
Агент наткнулся на ошибку, увидел её в наблюдении и на следующем шаге выбрал корректный аргумент. Это и есть устойчивость: сбой не фатален, а информативен.
Практические правила
- Никогда не давайте исключению уронить цикл — оборачивайте вызовы в try/except.
- Сообщения об ошибках должны быть понятны модели — «деление на ноль» полезнее, чем код
0x5. - Различайте «ошибку» и «пустой результат» — «ничего не найдено» тоже валидное наблюдение.
- Ставьте лимит на повторы — иначе агент будет вечно биться об один и тот же сбой (раздел 3).
Итог
- Инструменты падают по многим причинам; цикл агента не должен падать вместе с ними.
- Приём «ошибка как наблюдение»: ловим сбой и возвращаем его модели текстом.
- Получив понятную ошибку, модель способна исправить действие — но нужен лимит повторов.