Парсер формата ReAct

Модель говорит текстом — код должен понять этот текст. Пишем парсер, который превращает строку ReAct в структуру для исполнения.

Парсер ReAct — код, который из текста ответа модели извлекает: вызвать ли инструмент (имя и аргумент) или это финальный ответ.

Что нужно распознать

В типичном формате ReAct строка действия выглядит как Action: имя[аргумент], а конец — как Final Answer: текст. Парсер должен:

  • увидеть Final Answer: и вернуть финальный ответ — тогда цикл завершается;
  • иначе найти Action: имя[аргумент] и вернуть имя инструмента и аргумент.

Удобнее всего это делать регулярными выражениями.

Запускаемый парсер

import re

def parse_react(text):
    # сначала проверяем финал
    final = re.search(r"Final Answer:\s*(.*)", text)
    if final:
        return ("final", final.group(1).strip())
    # затем действие вида Action: имя[аргумент]
    action = re.search(r"Action:\s*(\w+)\[(.*?)\]", text)
    if action:
        return ("action", action.group(1), action.group(2))
    return ("unknown", text)

print(parse_react("Thought: посчитаю.\nAction: calc[2 + 2]"))
print(parse_react("Thought: знаю ответ.\nFinal Answer: 42"))
print(parse_react("Action: search[столица Японии]"))
print(parse_react("Thought: просто думаю вслух"))

Вывод:

('action', 'calc', '2 + 2')
('final', '42')
('action', 'search', 'столица Японии')
('unknown', 'Thought: просто думаю вслух')

Парсер вернул разобранную структуру: для действия — имя инструмента и аргумент, для финала — текст ответа, для непонятного — метку unknown.

Разбор регулярного выражения

  • Action:\s* — слово «Action:» и пробелы после.
  • (\w+) — имя инструмента (буквы/цифры), захватываем в группу 1.
  • \[(.*?)\] — аргумент в квадратных скобках; .*? — «нежадно», берём до первой закрывающей скобки.

Почему это место хрупкое

Модель — не компилятор: иногда она пишет Action: calc(2+2) вместо квадратных скобок, добавляет лишний текст или вообще забывает формат. Поэтому реальный парсер делают терпимым:

  • принимать несколько вариантов разделителей;
  • если формат не распознан — не падать, а вернуть unknown и попросить модель переформулировать;
  • в промпте давать примеры формата (few-shot), чтобы модель реже ошибалась.

Именно из-за этой хрупкости появились структурированные альтернативы — function calling, где модель возвращает не текст, а готовую структуру (раздел 6). Но понимать «ручной» ReAct важно: на нём строятся объяснимые и переносимые агенты.

Терпимый парсер

Добавим поддержку и круглых, и квадратных скобок — типичная поблажка реальному формату.

import re

def parse_action(text):
    # принимаем и Action: name[arg], и Action: name(arg)
    m = re.search(r"Action:\s*(\w+)[\[(](.*?)[\])]", text)
    if not m:
        return None
    return (m.group(1), m.group(2))

print(parse_action("Action: calc[2 + 2]"))
print(parse_action("Action: calc(2 + 2)"))
print(parse_action("Thought: без действия"))

Вывод:

('calc', '2 + 2')
('calc', '2 + 2')
None

Итог

  • Парсер превращает текст модели в структуру: финальный ответ или вызов инструмента (имя + аргумент).
  • Регулярные выражения удобны, но формат хрупок — модель часто отклоняется от него.
  • Терпимый парсер не падает на неожиданном вводе; альтернатива хрупкости — structured/function calling.
Проверьте себя
1. Что парсер ReAct должен вернуть, увидев в тексте «Final Answer: 42»?
AВызов инструмента с аргументом 42
BСигнал, что это финальный ответ, и сам ответ — цикл завершается
CОшибку формата
DНовую мысль
2. Почему «ручной» текстовый формат ReAct считается хрупким?
AРегулярные выражения работают медленно
BМодель — не компилятор и нередко отклоняется от формата (скобки, лишний текст)
CТекст занимает много памяти
DПарсер нельзя написать на Python
3. Как стоит поступить парсеру, если формат ответа не распознан?
AАварийно завершить программу
BНе падать, вернуть метку «непонятно» и, например, попросить модель переформулировать
CВызвать случайный инструмент
DСчитать это финальным ответом
Поддержать проект