Парсер формата 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.