Як агент вирішує, що робити далі (Planning vs Reactive) — Python (повна реалізація)

Повний runnable приклад, де одна задача розв'язується двома стратегіями агента — planning і reactive.
На цій сторінці
  1. Що цей приклад демонструє
  2. Структура проєкту
  3. Як запустити
  4. Що ми будуємо в коді
  5. Код
  6. tools.py — імітація джерел даних з випадковими збоями
  7. llm.py — утиліти для plan / replan / next-action
  8. planning_agent.py — спочатку план, потім виконання
  9. reactive_agent.py — рішення на кожному кроці
  10. main.py — порівняння двох підходів
  11. requirements.txt
  12. Приклад виводу
  13. Що видно на практиці
  14. Що змінити в цьому прикладі
  15. Повний код на GitHub

Це повна реалізація прикладу зі статті Як агент вирішує, що робити далі (Planning vs Reactive).

Якщо ти ще не читав статтю, почни з неї. Тут фокус лише на коді і поведінці двох стратегій.


Що цей приклад демонструє

  • Як одна й та сама задача виконується двома підходами: Planning і Reactive
  • Як planning-агент спочатку будує план, а при збої перебудовує його
  • Як reactive-агент обирає наступну дію після кожного результату
  • Чому reactive зазвичай стійкіший до флейків, а planning простіший для контролю

Структура проєкту

TEXT
examples/
└── foundations/
    └── planning-vs-reactive/
        └── python/
            ├── main.py             # запускає обидві стратегії і порівнює результат
            ├── llm.py              # виклики моделі: plan / replan / next action
            ├── planning_agent.py   # агент із попереднім планом
            ├── reactive_agent.py   # агент, що діє по ситуації
            ├── tools.py            # нестабільні зовнішні джерела (імітація)
            └── requirements.txt

tools.py навмисно містить випадкові збої, щоб різниця між підходами була видна в реальному рантаймі.


Як запустити

1. Клонуй репозиторій і перейди в папку:

BASH
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd examples/foundations/planning-vs-reactive/python

2. Встанови залежності:

BASH
pip install -r requirements.txt

3. Вкажи API-ключ:

BASH
export OPENAI_API_KEY="sk-..."

4. Запусти порівняння:

BASH
python main.py

Що ми будуємо в коді

Ми робимо двох роботів і даємо їм одну й ту саму задачу.

  • перший робот спочатку складає план, а потім іде по плану
  • другий робот вирішує наступний крок прямо під час роботи
  • якщо щось ламається, ми дивимось, хто швидше підлаштовується

Це як дві поїздки: один їде за готовою картою, інший орієнтується по дорозі.


Код

tools.py — імітація джерел даних з випадковими збоями

PYTHON
import random
from typing import Any


def fetch_profile(user_id: int) -> dict[str, Any]:
    # Стабільне джерело
    return {
        "profile": {
            "user_id": user_id,
            "name": "Anna",
            "tier": "pro",
        }
    }


def fetch_orders(user_id: int) -> dict[str, Any]:
    # Імітуємо флейк: іноді API "падає"
    if random.random() < 0.35:
        return {"error": "orders_api_timeout"}

    return {
        "orders": [
            {"id": "ord-1001", "total": 49.9, "status": "paid"},
            {"id": "ord-1002", "total": 19.0, "status": "shipped"},
        ]
    }


def fetch_balance(user_id: int) -> dict[str, Any]:
    if random.random() < 0.2:
        return {"error": "billing_api_unavailable"}
    return {"balance": {"currency": "USD", "value": 128.4}}


def build_summary(state: dict[str, Any]) -> dict[str, Any]:
    profile = state.get("profile")
    orders = state.get("orders")
    balance = state.get("balance")

    if not profile or not orders or not balance:
        return {"error": "not_enough_data_for_summary"}

    text = (
        f"User {profile['name']} ({profile['tier']}) has "
        f"{len(orders)} recent orders and balance {balance['value']} {balance['currency']}."
    )
    return {"summary": text}

llm.py — утиліти для plan / replan / next-action

PYTHON
import json
import os
from typing import Any

from openai import OpenAI

MODEL = "gpt-4.1-mini"
ALLOWED_ACTIONS = {"fetch_profile", "fetch_orders", "fetch_balance", "build_summary", "finish"}
api_key = os.environ.get("OPENAI_API_KEY")

if not api_key:
    raise EnvironmentError(
        "OPENAI_API_KEY is not set.\n"
        "Run: export OPENAI_API_KEY='sk-...'"
    )

client = OpenAI(api_key=api_key)


def _ask_json(prompt: str, fallback: dict[str, Any]) -> dict[str, Any]:
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": prompt}],
    )
    text = (resp.choices[0].message.content or "").strip()
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return fallback


def create_plan(task: str) -> list[str]:
    prompt = f"""
Return strict JSON: {{"plan": ["step_name", "..."]}}
Available steps: fetch_profile, fetch_orders, fetch_balance, build_summary.
Task: {task}
""".strip()
    data = _ask_json(prompt, {"plan": ["fetch_profile", "fetch_orders", "fetch_balance", "build_summary"]})
    plan = data.get("plan") or []
    return [str(step) for step in plan]


def replan(task: str, state: dict[str, Any], failed_step: str, error: str) -> list[str]:
    prompt = f"""
Return strict JSON: {{"plan": ["step_name", "..."]}}
Available steps: fetch_profile, fetch_orders, fetch_balance, build_summary.
Task: {task}
Failed step: {failed_step}
Error: {error}
Current state keys: {list(state.keys())}
Create only remaining steps.
""".strip()
    data = _ask_json(prompt, {"plan": ["fetch_orders", "fetch_balance", "build_summary"]})
    plan = data.get("plan") or []
    return [str(step) for step in plan]


def choose_next_action(task: str, state: dict[str, Any]) -> str:
    prompt = f"""
Return strict JSON: {{"action":"step_name"}}
Available actions: fetch_profile, fetch_orders, fetch_balance, build_summary, finish.
Task: {task}
Known state keys: {list(state.keys())}
If summary exists, choose finish.
If data missing, choose one action to progress.
If previous action failed, pick a different action.
""".strip()
    data = _ask_json(prompt, {"action": "fetch_profile"})
    action = str(data.get("action") or "fetch_profile")
    return action if action in ALLOWED_ACTIONS else "fetch_profile"

llm.py не виконує інструменти напряму. Вона лише пропонує, який крок робити далі.


planning_agent.py — спочатку план, потім виконання

PYTHON
from typing import Any

from llm import create_plan, replan
from tools import build_summary, fetch_balance, fetch_orders, fetch_profile

TOOLS = {
    "fetch_profile": lambda state: fetch_profile(state["user_id"]),
    "fetch_orders": lambda state: fetch_orders(state["user_id"]),
    "fetch_balance": lambda state: fetch_balance(state["user_id"]),
    "build_summary": build_summary,
}


def run_planning_agent(task: str, user_id: int, max_steps: int = 8) -> dict[str, Any]:
    state: dict[str, Any] = {"user_id": user_id}
    plan = create_plan(task)
    trace: list[str] = [f"Initial plan: {plan}"]

    step = 0
    while plan and step < max_steps:
        action = plan.pop(0)
        step += 1
        trace.append(f"[{step}] action={action}")

        tool = TOOLS.get(action)
        if not tool:
            trace.append(f"unknown_action={action}")
            continue

        result = tool(state)
        trace.append(f"result={result}")

        if "error" in result:
            trace.append("planning: replan after failure")
            plan = replan(task, state, failed_step=action, error=result["error"])
            trace.append(f"new_plan={plan}")
            continue

        state.update(result)
        if "summary" in state:
            return {"mode": "planning", "done": True, "steps": step, "state": state, "trace": trace}

    return {"mode": "planning", "done": False, "steps": step, "state": state, "trace": trace}

Planning-агент приймає стратегічне рішення наперед, і лише при збоях переходить до перебудови плану.


reactive_agent.py — рішення на кожному кроці

PYTHON
from typing import Any

from llm import choose_next_action
from tools import build_summary, fetch_balance, fetch_orders, fetch_profile

TOOLS = {
    "fetch_profile": lambda state: fetch_profile(state["user_id"]),
    "fetch_orders": lambda state: fetch_orders(state["user_id"]),
    "fetch_balance": lambda state: fetch_balance(state["user_id"]),
    "build_summary": build_summary,
}


def run_reactive_agent(task: str, user_id: int, max_steps: int = 8) -> dict[str, Any]:
    state: dict[str, Any] = {"user_id": user_id}
    trace: list[str] = []

    for step in range(1, max_steps + 1):
        action = choose_next_action(task, state)
        trace.append(f"[{step}] action={action}")

        if action == "finish":
            if "summary" in state:
                return {"mode": "reactive", "done": True, "steps": step, "state": state, "trace": trace}
            trace.append("finish_rejected: summary is missing")
            continue

        tool = TOOLS.get(action)
        if not tool:
            trace.append(f"unknown_action={action}")
            state["last_error"] = f"unknown_action:{action}"
            continue

        result = tool(state)
        trace.append(f"result={result}")

        if "error" in result:
            state["last_error"] = result["error"]
            continue

        state.update(result)
        state.pop("last_error", None)

    return {"mode": "reactive", "done": False, "steps": max_steps, "state": state, "trace": trace}

Reactive-агент не тримається за початковий план. Він оцінює стан після кожної дії, а build_summary обирає сама модель (без "auto" гілки в коді).


main.py — порівняння двох підходів

PYTHON
from planning_agent import run_planning_agent
from reactive_agent import run_reactive_agent

TASK = "Prepare a short account summary for user_id=42 with profile, orders, and balance."
USER_ID = 42


def print_result(result: dict):
    print(f"\n=== {result['mode'].upper()} ===")
    print(f"done={result['done']} | steps={result['steps']}")
    print("summary:", result["state"].get("summary"))
    print("\ntrace:")
    for line in result["trace"]:
        print(" ", line)


def main():
    planning = run_planning_agent(task=TASK, user_id=USER_ID)
    reactive = run_reactive_agent(task=TASK, user_id=USER_ID)

    print_result(planning)
    print_result(reactive)


if __name__ == "__main__":
    main()

requirements.txt

TEXT
openai>=1.0.0

Приклад виводу

TEXT
=== PLANNING ===
done=True | steps=6
summary: User Anna (pro) has 2 recent orders and balance 128.4 USD.

trace:
  Initial plan: ['fetch_profile', 'fetch_orders', 'fetch_balance', 'build_summary']
  [1] action=fetch_profile
  result={'profile': {'user_id': 42, 'name': 'Anna', 'tier': 'pro'}}
  [2] action=fetch_orders
  result={'error': 'orders_api_timeout'}
  planning: replan after failure
  new_plan=['fetch_orders', 'fetch_balance', 'build_summary']
  ...

=== REACTIVE ===
done=True | steps=5
summary: User Anna (pro) has 2 recent orders and balance 128.4 USD.

trace:
  [1] action=fetch_profile
  result={'profile': {'user_id': 42, 'name': 'Anna', 'tier': 'pro'}}
  [2] action=fetch_orders
  result={'orders': [{'id': 'ord-1001', 'total': 49.9, 'status': 'paid'}, {'id': 'ord-1002', 'total': 19.0, 'status': 'shipped'}]}
  [3] action=fetch_balance
  result={'balance': {'currency': 'USD', 'value': 128.4}}
  [4] action=build_summary
  result={'summary': 'User Anna (pro) has 2 recent orders and balance 128.4 USD.'}
  [5] action=finish

Що видно на практиці

Planning агентReactive агент
Коли обирає крокиНа старті (план)Після кожної дії
Реакція на збійПеребудовує планОдразу обирає новий крок
ПередбачуваністьВищаНижча
Стійкість до флейківСередняЗазвичай вища

Що змінити в цьому прикладі

  • Підніми імовірність збою в fetch_orders до 0.6 і подивись, хто частіше завершує задачу
  • Додай окремий ліміт на кількість replan у planning-агента
  • Додай правило "не повторювати одну й ту саму дію 3 рази підряд" для reactive-агента
  • Запусти обидва підходи 30 разів і порівняй відсоток успішних завершень

Повний код на GitHub

У репозиторії лежить повна версія цього демо: дві стратегії агента, спільні інструменти і трасування кроків.

Переглянути повний код на GitHub ↗
⏱️ 7 хв читанняОновлено Бер, 2026Складність: ★☆☆
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.