Полный цикл ReAct с заглушкой LLM
Кульминация раздела: соединяем парсер, инструменты и модель-заглушку в работающий цикл ReAct, который реально решает задачу.
Цикл ReAct — пока нет финального ответа: спросить модель → распарсить → если действие, выполнить инструмент и добавить наблюдение → повторить.
Что соберём
Задача: «Сколько людей живёт в Токио, округлённо в миллионах?» Агент должен сначала найти население, затем при необходимости посчитать. У нас есть:
- два инструмента —
search(заглушка-база) иcalc; - заглушка LLM, ведущая себя по правилам ReAct в зависимости от истории;
- парсер и цикл, который всё связывает.
Заглушка детерминирована, поэтому трасса воспроизводима — вы видите механику настоящего агента без обращения к API.
Рабочий цикл целиком
import re
# --- инструменты ---
def search(q):
db = {"население токио": "около 14 миллионов"}
return db.get(q.lower().strip(), "не найдено")
def calc(expr):
return str(eval(expr))
TOOLS = {"search": search, "calc": calc}
# --- заглушка LLM в формате ReAct ---
def react_llm(history):
text = "\n".join(history)
if "Observation:" not in text:
return ("Thought: мне нужно узнать население Токио.\n"
"Action: search[население Токио]")
if "около 14 миллионов" in text:
return ("Thought: население — около 14 миллионов, это и есть ответ.\n"
"Final Answer: около 14 миллионов человек")
return "Final Answer: не удалось"
# --- парсер ---
def parse(text):
f = re.search(r"Final Answer:\s*(.*)", text)
if f:
return ("final", f.group(1).strip())
a = re.search(r"Action:\s*(\w+)\[(.*?)\]", text)
if a:
return ("action", a.group(1), a.group(2))
return ("unknown", text)
# --- цикл ReAct ---
def run(question, max_steps=5):
history = [f"Question: {question}"]
for step in range(1, max_steps + 1):
block = react_llm(history)
history.append(block)
print(f"--- шаг {step} ---")
print(block)
parsed = parse(block)
if parsed[0] == "final":
return parsed[1]
if parsed[0] == "action":
name, arg = parsed[1], parsed[2]
obs = TOOLS.get(name, lambda x: "нет инструмента")(arg)
history.append(f"Observation: {obs}")
print("Observation:", obs)
return "лимит шагов исчерпан"
answer = run("Сколько людей живёт в Токио?")
print("=== ОТВЕТ ===")
print(answer)
Вывод:
--- шаг 1 --- Thought: мне нужно узнать население Токио. Action: search[население Токио] Observation: около 14 миллионов --- шаг 2 --- Thought: население — около 14 миллионов, это и есть ответ. Final Answer: около 14 миллионов человек === ОТВЕТ === около 14 миллионов человек
Что здесь произошло
- Шаг 1: модель рассудила, что нужен поиск, и запросила инструмент. Цикл выполнил
searchи добавил наблюдение в историю. - Шаг 2: увидев наблюдение, модель решила, что ответ найден, и выдала
Final Answer— цикл завершился.
Подменив react_llm на реальный вызов LLM с тем же промпт-форматом, вы получите настоящего ReAct-агента. Вся остальная инфраструктура — парсер, диспетчер, цикл, история — остаётся прежней.
Роль истории
Список history — это память агента. На каждом шаге он целиком уходит модели, чтобы та видела весь контекст: вопрос, прошлые мысли, действия и наблюдения. Без накопления истории агент «забывал» бы, что уже сделал, и зацикливался. Памяти посвящён следующий раздел.
Итог
- Полный ReAct-агент = инструменты + заглушка/модель + парсер + цикл, связанные через историю.
- Цикл повторяет «спросить → распарсить → выполнить → наблюдать», пока не появится финальный ответ.
- История — это память: она целиком отдаётся модели, чтобы агент помнил весь ход решения.