Перейти до вмісту
coden

Планування: STRIPS, світ кубиків, евристики з релаксації та планування в AI-агентах

Мова STRIPS і PDDL, пряме та зворотне планування, евристики hmax і hadd, аномалія Зуссмана, ієрархічне планування і перепланування — від робота Шейкі до AI-агентів, які складають собі список завдань.

14 хвилин Matěj Žalmánek 2026-10-15

Серія Основи ШІ Частина 25 з 52
  1. Мапа штучного інтелекту: що є що і як усе це пов’язано
  2. Коротка історія штучного інтелекту: від Тюрінга до мовних моделей
  3. Дані, модель, тренування, інференс: основні поняття на одному прикладі
  4. Як читати математичний запис: символи, індекси, суми та квантори
  5. Множини й відношення: конструктор, з якого складаються означення в ШІ
  6. Функції й відображення: чому кожна модель у ШІ — це функція
  7. Логіка й доведення: правила, за якими міркують у ШІ
  8. Графи й дерева: структура, на якій тримаються пошук і нейронні мережі
  9. Лінійна алгебра для машинного навчання: вектори, матриці й чому ШІ передусім множить
  10. Похідні, градієнт і правило ланцюжка: як модель дізнається, у якому напрямку вчитися
  11. Градієнтний спуск: як навчається модель
  12. Що таке ймовірність: від кидка кубика до аксіом Колмогорова
  13. Умовна ймовірність і теорема Баєса: як змінити оцінку, коли дізнаєтеся щось нове
  14. Випадкова величина і функція розподілу: одна функція, яка каже про розподіл усе
  15. Функція щільності та функція маси ймовірності: чому неперервна величина має в точці ймовірність нуль
  16. Математичне сподівання, дисперсія і коваріація: де лежить розподіл, наскільки він широкий і що з чим пов’язано
  17. Огляд розподілів ймовірностей: Бернуллі, біноміальний, Пуассона, рівномірний, нормальний і експоненційний
  18. Закон великих чисел і центральна гранична теорема: чому середні поводяться так передбачувано
  19. Статистика й оцінювання параметрів: максимальна правдоподібність, MAP і довірчі інтервали
  20. Теорія інформації: ентропія, перехресна ентропія і KL-дивергенція — звідки беруться функції втрат
  21. Пошук у просторі станів: BFS, DFS, UCS і A* з анімаціями
  22. Ігри та суперник: мінімакс, альфа-бета відсікання і Monte Carlo tree search
  23. Задоволення обмежень: бектрекінг, поширення обмежень і AC-3 на судоку та мапі чеських країв
  24. Логіка й системи, засновані на знаннях: пряме і зворотне виведення, резолюція та експертні системи
  25. Планування: STRIPS, світ кубиків, евристики з релаксації та планування в AI-агентах
  26. Локальний пошук та імітація відпалу: комівояжер через обласні центри Чехії
  27. Генетичні алгоритми: популяція, схрещування, мутація і добір на рюкзаку для походу
  28. Еволюційні стратегії і генетичне програмування: від правила однієї п’ятої до закону Кеплера
  29. Оптимізація мурашиною колонією: як феромоновий слід знаходить короткий шлях
  30. Оптимізація роєм частинок: інерція, власний досвід і досвід рою
  31. Нейроеволюція: еволюція ваг і форми нейронних мереж та алгоритм NEAT
  32. Нечіткі множини й функції належності: як записати «трохи тепло»
  33. Нечітка логіка та ймовірність: чому «трохи тепло» не означає «імовірно тепло»
  34. Нечітка логіка: t-норми, імплікації та правила ЯКЩО–ТО
  35. Нечітке керування: Мамдані та Суґено від вентилятора до маятника на візку
  36. Нечітка кластеризація: алгоритм fuzzy c-means і точки між кластерами
  37. Нейро-нечіткі системи (ANFIS): нечіткі правила, які навчаються з даних
  38. Марковські ланцюги: матриця переходів, стаціонарний розподіл і PageRank
  39. Приховані марковські моделі: алгоритм Вітербі й нечесне казино
  40. Байєсівські мережі: чому трава мокра і що з цього випливає для дощувача
  41. Фільтр Калмана: як за неточними вимірюваннями оцінити, де авто і як швидко воно їде
  42. Види машинного навчання: з учителем, без учителя, з частковим залученням учителя та інші
  43. Промпти, контекст і RAG: як дати моделі правильну інформацію
  44. Варіаційні автокодувальники (VAE): нові зображення з неперервного латентного простору
  45. Генеративні змагальні мережі (GAN): фальшувальник проти детектива
  46. Дифузійні моделі: як із чистого шуму постає зображення
  47. Марковські процеси ухвалення рішень: стани, дії, винагороди й рівняння Беллмана
  48. Q-навчання і DQN: як навчитися цінності дій — від таблиці до нейронної мережі
  49. Policy gradient і PPO: навчання безпосередньо політики
  50. Як працює ШІ-агент: мовна модель у циклі з інструментами
  51. Програмування з AI-агентами: завдання, тести й перевірка
  52. Оцінювання і безпека систем ШІ: як вимірювати і як стежити

Повна програма серії Готово: 52 з 83 тем

Що ви винесете
  • Як записати задачу планування як атоми, дії з передумовами й ефектами та ціль.
  • Як планувальник сам обчислює евристику з релаксованої задачі і скільки роботи це заощаджує.
  • Чому підцілі не можна виконувати одну за одною і як планують сучасні AI-агенти.

Що варто знати заздалегідь: Пошук у просторі станів: BFS, DFS, UCS і A* з анімаціями, Логіка й системи, засновані на знаннях: пряме і зворотне виведення, резолюція та експертні системи

Робот Шейкі зі Стенфордського дослідницького інституту наприкінці шістдесятих їздив кількома кімнатами, пересував коробки й вимикав світло. Це був перший робот, який міркував про власні дії: отримував ціль («перемісти коробку до сусідньої кімнати») і сам вигадував, які дії і в якому порядку до неї ведуть. Мова, якою він це робив, називається STRIPS — а її ідеї сьогодні ви знайдете в логістиці, у комп’ютерних іграх і в циклі AI-агента, який пише вам код.

Ця частина продовжує частини Пошук у просторі станів і Логіка й системи, засновані на знаннях.

1. Планування: пошук, який бачить усередину

Планування — це пошук послідовності дій, яка веде до цілі, — точнісінько як у частині про пошук. Різниця в тому, як записано задачу. У лабіринті стан був чорною скринькою, і алгоритм знав про нього лише те, куди з нього ведуть дії. Планувальник бачить усередину: стан — це список фактів про світ, а дія каже, які факти їй потрібні і які вона змінить. Завдяки цьому планувальник уміє сам вивести оцінку, наскільки далеко ціль, — жодної евристики вам не доведеться писати вручну.

2. Мова STRIPS

Означення — задача планування STRIPS

Задано скінченну множину атомів F (тверджень про світ). Стан — це підмножина s ⊆ F — атоми, які в ньому істинні; чого в s немає, те хибне.

Дія a — це трійка (pre(a), add(a), del(a)) підмножин F: передумови, додавані та вилучувані атоми. Дія застосовна в s, якщо pre(a) ⊆ s, і результівний стан — це

T(s, a) = (s ∖ del(a)) ∪ add(a)

T(s, a)
стан після виконання дії — функція переходу, яку в частині про пошук задавав програміст; тут планувальник виводить її з опису дії сам
s
вихідний стан: усе, про що дія не згадує, переходить у новий стан без змін (припущення STRIPS, яке розв’язує проблему фрейму, — не треба перелічувати, що не змінюється)
del(a)
атоми, які дія робить хибними; віднімаються різницею множин ∖
add(a)
атоми, які дія запроваджує; додаються об’єднанням аж після вилучення, тож атом, що є в обох списках, зрештою істинний

Задача — це четвірка (F, A, s0, G), де G ⊆ F — ціль. План — це послідовність дій a1, …, ak, кожна з яких застосовна в стані після попередніх, і в останньому стані sk виконується G ⊆ sk.

Простими словами

Стан — це список речень, які зараз істинні: «C лежить на A», «на B нічого не лежить». Дія — це рецепт із трьома графами: що має бути істинним, щоб її можна було виконати; що стане істинним; що істинним бути перестане. Ціль не мусить описувати весь світ — досить сказати, що для вас важливо, а решта байдужа.

Приклад — одна дія крок за кроком

Кубик C лежить на A, B стоїть поруч. Дія на стіл(C, A) має pre = {на(C, A), вільний(C)}, add = {стіл(C), вільний(A)}, del = {на(C, A)} (повний опис дій — у наступному розділі).

s = {на(C, A), стіл(A), стіл(B), вільний(B), вільний(C)}

pre ⊆ s?   на(C, A) ∈ s, вільний(C) ∈ s → дія застосовна

s ∖ del = {стіл(A), стіл(B), вільний(B), вільний(C)}

(s ∖ del) ∪ add = {стіл(A), стіл(B), стіл(C), вільний(A), вільний(B), вільний(C)} — усі три кубики на столі

STRIPS з’явився 1971 року (Файкс і Нільссон) саме для Шейкі. Його сучасний наступник — мова PDDL, якою з 1998 року задають задачі для міжнародних змагань планувальників. Дію в ній записують як схему зі змінними:

(:action na-stil
  :parameters (?a ?b)
  :precondition (and (na ?a ?b) (vilnyi ?a))
  :effect (and (stil ?a) (vilnyi ?b) (not (na ?a ?b))))

3. Світ кубиків

Класична тестова задача планування: на столі лежать кубики, деякі один на одному. Однією дією ви берете кубик, на якому нічого не лежить, і кладете його на стіл або на інший вільний кубик. Атоми бувають трьох видів: на(A, B) — A лежить на B, стіл(A) і вільний(A).

Дії у світі кубиків
  • на стіл(A, B): pre = {на(A, B), вільний(A)}, add = {стіл(A), вільний(B)}, del = {на(A, B)}
  • зі столу(A, C): pre = {стіл(A), вільний(A), вільний(C)}, add = {на(A, C)}, del = {стіл(A), вільний(C)}
  • перенести(A, B, C): pre = {на(A, B), вільний(A), вільний(C)}, add = {на(A, C), вільний(B)}, del = {на(A, B), вільний(C)}
Простими словами

Коли ви знімаєте A з B на стіл, A опиниться на столі, B звільниться, і A перестане лежати на B. Усе інше залишиться як було — і це важливо: STRIPS припускає, що дія змінює лише те, про що каже сама.

Схему зі змінними планувальник спершу заземлює: підставляє замість змінних усі комбінації кубиків. Для n кубиків є n(n − 1) дій «на стіл», стільки ж «зі столу» і n(n − 1)(n − 2) перенесень, разом n2(n − 1). Для трьох кубиків це 9 · 2 = 18 дій, для шести 36 · 5 = 180. Проте в кожному стані застосовні лише кілька з них — кубиків, на яких нічого не лежить, мало.

Світ виглядає як іграшка, але росте швидко. Три кубики можна розставити 13 способами, чотири — 73, п’ять — 501, а шість — уже 4 051. Звідки ці числа:

кількість станів = ∑k=1n n! · C(n − 1, k − 1) / k!

n!
вишикуємо всі кубики в один ряд — кожен порядок є кандидатом; вежі з нього отримаємо розрізанням
C(n − 1, k − 1)
ряд розріжемо на k непорожніх веж: виберемо k − 1 з n − 1 проміжків між кубиками. Порядок усередині вежі важливий (A на B — не те саме, що B на A), тому ріжемо порядок, а не множину
k!
порядок веж на столі не важливий — дві вежі поруч є тим самим станом, хоч би в якому порядку ми їх виписали. Кожен стан ми, отже, порахували k! разів
Доданки — це числа Лаха L(n, k), а сума — послідовність A000262 в енциклопедії цілочислових послідовностей OEIS.

n = 3:   k = 1: 6 · 1 / 1 = 6,   k = 2: 6 · 2 / 2 = 6,   k = 3: 6 · 1 / 6 = 1   → 13

n = 4:   24 + 36 + 12 + 1 = 73

n = 6:   720 + 1 800 + 1 200 + 300 + 30 + 1 = 4 051

Планувальник порахував їх інакше — пройшов усі досяжні стани — і отримав те саме. Спробуймо дві задачі. У більшій на столі стоять дві вежі A–B–C і D–E–F (записано знизу), а ціль — єдина вежа F–E–D–C–B–A:

початок A B C D E F · E на F · D на E · C на D · B на C · A на B крок 1: F на стіл A B C D E F · E на F · D на E · C на D · B на C · A на B крок 2: E на F A B C D F E ✓ E на F · D на E · C на D · B на C · A на B крок 3: D на E A B C F E D ✓ E на F ✓ D на E · C на D · B на C · A на B крок 4: C на D A B F E D C ✓ E на F ✓ D на E ✓ C на D · B на C · A на B крок 5: B на C A F E D C B ✓ E на F ✓ D на E ✓ C на D ✓ B на C · A на B крок 6: A на B F E D C B A ✓ E на F ✓ D на E ✓ C на D ✓ B на C ✓ A на B
Найкоротший план: з двох веж одна перевернута

4. Пряме планування

Означення — пряме планування (прогресія)

Пошук у просторі станів зі стану s0: наступники стану s — це стани T(s, a) для всіх дій a, застосовних у s. Перевірка цілі: G ⊆ s.

Простими словами

З вихідної ситуації ви пробуєте всі ходи, з усіх результатів — знову всі ходи, доки не натрапите на ситуацію, в якій ціль виконується. Нічого нового — це BFS або A* з частини про пошук, лише над станами, записаними як множини фактів.

from collections import deque

def proved(stav, akce):
    return (stav - akce.odeber) | akce.pridej

def planuj(start, cil, akce):
    okraj, rodic = deque([start]), {start: None}
    while okraj:
        stav = okraj.popleft()
        if cil <= stav:                      # усі цільові атоми істинні
            return plan_z(rodic, stav)
        for a in akce:
            if a.predpoklady <= stav:
                dalsi = proved(stav, a)
                if dalsi not in rodic:
                    rodic[dalsi] = (stav, a)
                    okraj.append(dalsi)

На задачі з вежею пошук у ширину знаходить найкоротший план із 6 дій, але розгортає при цьому 2 132 стани — понад половину з усіх 4 051. Він-бо нічого не знає про те, в якому напрямку лежить ціль.

5. Евристики з релаксації

У частині про пошук евристику для лабіринту ми вигадали самі. Планувальник уміє обчислити її з опису дій — так, що задачу спрощує, розв’язує її і ціну спрощеного розв’язку бере за оцінку.

Означення — релаксація без вилучення

Релаксована задача виникає, коли для всіх дій покладемо del(a) = ∅. Атом, який одного разу став істинним, тоді лишається істинним назавжди.

Ціну атома p зі стану s у релаксованій задачі оцінюємо рекурсивно: c(p) = 0 для p ∈ s, інакше c(p) = мінімум за діями a з p ∈ add(a) від значення 1 + c(pre(a)), де ціна множини атомів —

  • для hmax максимум цін її атомів — евристика допустима,
  • для hadd сума цін її атомів — точніший орієнтир, але може переоцінити.

Евристика стану s — це ціна цільової множини G.

c(p) = mina : p ∈ add(a) [ 1 + c(pre(a)) ]

c(p)
оцінка, скільки дій потрібно, щоб атом p став істинним (для атомів, які вже істинні, це 0)
min за діями з p ∈ add(a)
атом можуть запровадити кілька дій; вибираємо найдешевшу, бо шлях планувальник обирає сам
1
ціна самої дії, яка p зрештою додасть
c(pre(a))
ціна того, щоб дія взагалі була застосовна, — виконати всі її передумови. Тут hmax і hadd розходяться: максимум припускає, що передумови виконаються «одночасно» і їхні підготовки перекриваються, сума — що кожна коштує окремої роботи
Вилучення (del) у формулі немає — це і є релаксація. Рекурсія коректно визначена, бо ціни обчислюємо шарами від нуля вгору, доки щось змінюється.
Простими словами

Уявіть світ, у якому нічого не можна зіпсувати: коли кубик одного разу відкриєте, він лишиться відкритим, навіть якщо на нього щось покладете. У такому світі планувати легко, а ціна такого плану — оптимістична оцінка справжньої ціни. hmax зважає лише на найважчу підціль (обережна, ніколи не переоцінить), hadd додає всі (сміливіша, може схибити в більший бік, бо одна дія часом допомагає кільком цілям водночас).

def ceny(stav, akce, agreguj):             # agreguj = max (hmax) або sum (hadd)
    c = {p: 0 for p in stav}
    zmena = True
    while zmena:                             # повторюй, доки якась ціна покращується
        zmena = False
        for a in akce:
            if all(p in c for p in a.predpoklady):
                nova = 1 + agreguj(c[p] for p in a.predpoklady)
                for p in a.pridej:           # a.odeber ігнорується — релаксація
                    if nova < c.get(p, float("inf")):
                        c[p], zmena = nova, True
    return c

def h(stav, cil, akce, agreguj):
    c = ceny(stav, akce, agreguj)
    return agreguj(c[g] for g in cil)        # недосяжна ціль: h = ∞
Приклад — hmax і hadd для аномалії Зуссмана

Початок: C лежить на A, B стоїть поруч; ціль G = {на(A, B), на(B, C)} (задачу розберемо нижче). Ціни атомів по шарах:

ціна 0 (істинні): стіл(A), стіл(B), на(C, A), вільний(B), вільний(C)

ціна 1: на стіл(C, A) → стіл(C), вільний(A);   зі столу(B, C) → на(B, C);   перенести(C, A, B) → на(C, B)

ціна 2: зі столу(A, B) потребує стіл(A), вільний(A), вільний(B) з цінами 0, 1, 0

   hmax: 1 + max(0, 1, 0) = 2,   hadd: 1 + (0 + 1 + 0) = 2 → на(A, B) має ціну 2

hmax(s0) = max(2, 1) = 2,   hadd(s0) = 2 + 1 = 3,   насправді потрібно: h* = 3

hmax недооцінює, бо враховує лише найдорожчу ціль: на(A, B) коштує дві дії, але на(B, C) потребує ще власної третьої, і максимум її поглинає. Сума її врахує і тут влучає точно. На задачі з вежею це помітніше: зі старту h* = 6, hmax = 3, кількість невиконаних цілей 5, а hadd = 15 — сума враховує спільну підготовку (відкривання кубиків) для кожної цілі знову і переоцінює справжню ціну в два з половиною рази.

Теорема — hmax і кількість невиконаних цілей допустимі

Допустимість із частини про пошук: h(s) ≤ h*(s), де h*(s) — довжина найкоротшого плану з s.

hmax. Візьмімо найкоротший план a1, …, ak справжньої задачі. Він є планом і для релаксованої задачі (без вилучення на кожному кроці істинні щонайменше ті самі атоми, тож передумови лишаються виконаними). Індукцією за i: кожен атом, істинний після i дій, має cmax ≤ i — або він був істинний ще раніше, або його додала дія ai, передумови якої були істинні після i − 1 дій і тому мають ціну ≤ i − 1, отже 1 + max ≤ i. Усі цільові атоми істинні після k дій, тому hmax(s) = maxg ∈ G c(g) ≤ k = h*(s).

Кількість невиконаних цілей. Цілі світу кубиків — це атоми на(·, ·), і кожна дія додає щонайбільше один такий атом. Тож для виконання m відсутніх цілей потрібно щонайменше m дій.

Для hadd доведення не спрацьовує на останньому кроці: сума цін може бути більшою за k, бо ті самі дії плану враховуються для кількох цілей.

Три евристики на задачі з вежею, усі з алгоритмом A*:

ПошукРозгорнуто станівДовжина плану
У ширину (без евристики)2 1326
A* з hmax1366
A* з кількістю невиконаних цілей196
A* з hadd127

Допустима hmax знаходить оптимум і заощаджує понад 90 % роботи. Кількість невиконаних цілей у нашому світі теж допустима (кожна дія кладе лише один кубик, тож виконує щонайбільше одну ціль) і тут працює ще краще. hadd розгортає найменше, але платить за це: план має на крок більше, ніж потрібно. Жодна евристика не виграє завжди — і справжні планувальники, як-от FF чи Fast Downward, мають цілі набори евристик, які комбінують.

Як працює A* з hmax, найкраще видно на малій задачі — аномалії Зуссмана, яку розберемо двома розділами далі. Кожен рядок — один розгорнутий стан (вежі знизу); A* завжди бере стан із найменшим f = g + h, де g — кількість дій від початку, а за рівності — той, який виявив раніше:

КрокРозгорнутий станОстання діяghmaxf
1A–C, B— (початок)022
2A, B, Cна стіл(C, A)112
3A, B–Cперенести(C, A, B)123
4A, C–Bзі столу(B, C)213
5C–B–Aзі столу(A, B)303 — ціль

Пошук у ширину на тій самій задачі розгортає всі 13 станів, A* з hmax — лише 5. На третьому кроці він звертає не туди — кладе C на B, — бо цей стан має f = 3, так само як правильний шлях, і був виявлений раніше. Уже наступний крок це виправляє. Перший розгорнутий цільовий стан має найкоротший план — це A* гарантує, коли евристика допустима (а hmax навіть узгоджена, тож до закритих станів повертатися не треба).

6. Зворотне планування

Означення — зворотне планування (регресія)

Шукає від цілі: стан пошуку — це множина атомів g, які ще мають бути істинними (на початку g = G). Дія a релевантна для g, якщо add(a) ∩ g ≠ ∅ і del(a) ∩ g = ∅. Регресія цілі через a — це

g′ = (g ∖ add(a)) ∪ pre(a)

g′
що має бути істинним перед дією a, щоб після неї виконувалося g, — нова, раніша підціль
g
дотеперішня підціль; атоми, про які дія не згадує, мають бути істинними вже раніше, бо дія сама їх не запровадить (а завдяки умові релевантності й не знищить)
add(a)
віднімаємо, бо ці атоми забезпечить сама дія — раніше вони істинними бути не мусять
pre(a)
додаємо, бо без передумов дію виконати не можна — вони стають новими підцілями
Це «дзеркало» функції переходу T(s, a): там вилучається del і додається add, тут вилучається add і додається pre.

Пошук закінчується, коли g′ ⊆ s0; план — це знайдені дії у зворотному порядку.

Простими словами

Ви питаєте: «Яка дія могла б дати мені те, чого я хочу, і нічого з цього не зруйнувати? І що мало б бути істинним перед нею?» Те саме міркування, що й у зворотному виведенні в попередній частині. Перевага: ви досліджуєте лише дії, пов’язані з ціллю. Недолік: ви працюєте з множинами цілей замість повних станів, а їх важче оцінювати.

Приклад — регресія цілі аномалії Зуссмана

Ціль g = {на(A, B), на(B, C)}, початок s0 = {на(C, A), стіл(A), стіл(B), вільний(B), вільний(C)}. Дії беремо від останньої:

зі столу(A, B): add = {на(A, B)} зачіпає g, del = {стіл(A), вільний(B)} — ні → релевантна

   g′ = {на(B, C)} ∪ {стіл(A), вільний(A), вільний(B)}

зі столу(B, C): add = {на(B, C)}, del = {стіл(B), вільний(C)} → релевантна

   g″ = {стіл(A), вільний(A), вільний(B)} ∪ {стіл(B), вільний(B), вільний(C)} = {стіл(A), стіл(B), вільний(A), вільний(B), вільний(C)}

на стіл(C, A): add = {стіл(C), вільний(A)}, del = {на(C, A)} → релевантна

   g‴ = {стіл(A), стіл(B), вільний(B), вільний(C)} ∪ {на(C, A), вільний(C)} = {на(C, A), стіл(A), стіл(B), вільний(B), вільний(C)} ⊆ s0 ✓

План, прочитаний у зворотному порядку: на стіл(C, A), зі столу(B, C), зі столу(A, B). Але якби останньою дією ми спробували зі столу(B, C), отримали б підціль {на(A, B), стіл(B), вільний(B), вільний(C)} — A лежить на B, і водночас B вільний. Такого стану не існує, але регресія сама цього не розпізнає; тому зворотні планувальники потребують додаткових правил, які неможливі комбінації виявляють.

7. Аномалія Зуссмана

Найпростіша ідея, як планувати: розділити ціль на частини і виконати їх одну за одною. Часто це працює — і зазнає невдачі вже на трьох кубиках. Задача носить ім’я Джеральда Зуссмана, який розібрав її в сімдесятих роках: C лежить на A, B стоїть поруч, ціль — A на B і водночас B на C.

початок A C B · A на B · B на C крок 1: C на стіл A B C · A на B · B на C крок 2: A на B B A C ✓ A на B · B на C крок 3: A на стіл A B C · A на B · B на C крок 4: B на C A C B · A на B ✓ B на C
За цілями: спершу «A на B», потім «B на C»

Найкоротший план для «A на B» знімає C і кладе A на B. Але тоді B лежить під A, і щоб покласти B на C, доведеться A знову зняти. Наприкінці виконується лише друга ціль, і це коштувало 4 кроки. Протилежний порядок закінчується так само: покладемо B на C, але цим засиплемо A під C і B — а звільнити його означає знову зруйнувати першу ціль. Цілі заважають одна одній, хоч би з якої ви почали.

початок A C B · A на B · B на C крок 1: C на стіл A B C · A на B · B на C крок 2: B на C A C B · A на B ✓ B на C крок 3: A на B C B A ✓ A на B ✓ B на C
Найкоротший план: три кроки, цілі перемежовуються

Розв’язок вимагає кроки обох цілей перемежувати: спершу звільнити A (крок для першої цілі), потім B на C (друга ціль), і лише потім A на B (перша ціль). Планувальник, який шукає в просторі повних станів, знайде це сам — тут він пройшов усі 13 станів світу трьох кубиків. Планувальник, який складає часткові плани, мусить уміти їх розплутувати і знову з’єднувати. Це тема цілої родини «планування в просторі планів».

8. Ієрархічне планування

Означення — HTN

В ієрархічній мережі задач (HTN) окрім примітивних дій є й складені задачі. Кожна складена задача має один або кілька методів — послідовностей підзадач, на які її можна розкласти. Планування починається з однієї задачі найвищого рівня і розкладає її, доки не залишаться лише примітивні дії.

Простими словами

Ніхто не планує відпустку окремими кроками ніг. Ви розкладаєте її на «дістатися туди», «поселитися» і «повернутися»; «дістатися» — на «купити квиток» і «доїхати до вокзалу», — і лише в самому низу є конкретні дії. Методи — це перевірені рецепти, як розкласти складну річ, і завдяки ним планувальник не пробує нісенітниць.

def rozloz(ukoly, stav):                   # ukoly: список задач, що залишилися
    if not ukoly:
        return []                            # усе розкладено
    u, zbytek = ukoly[0], ukoly[1:]
    if u.primitivni:                         # звичайна дія STRIPS
        if not u.predpoklady <= stav:
            return None
        plan = rozloz(zbytek, proved(stav, u))
        return None if plan is None else [u] + plan
    for metoda in u.metody:                  # пробуй рецепти один за одним
        if metoda.podminka <= stav:
            plan = rozloz(metoda.podukoly + zbytek, stav)
            if plan is not None:
                return plan
    return None                              # жоден метод не вдався — повернення назад

Шукають тут у просторі розкладів, а не станів: розгалуження відбувається лише за тим, який метод вибрати для складеної задачі. Задачі розкладаються в тому порядку, в якому виконуватимуться, тож планувальник завжди знає поточний стан — так само діє, наприклад, планувальник SHOP2.

Ієрархічне планування використовують там, де фахівці мають перевірені процедури: у логістиці, в керуванні виробництвом і дуже часто в комп’ютерних іграх. Екшн-гра F.E.A.R. (2005) прославилася солдатами, які планували тактику на ходу мовою в стилі STRIPS, — і з тієї самої ідеї виходять планувальники в низці інших ігор.

9. Планування в невизначеному світі

STRIPS припускає, що світ точно такий, як ми думаємо, і дії дають точно той результат, що в описі. Але справжній робот кубик упустить, а справжній сервер не відповість. Найпоширеніших відповідей на це три:

  • Моніторинг і перепланування. Після кожної дії перевір, чи світ виглядає так, як очікував план. Якщо ні, сплануй знову з того, що є. Просто й напрочуд дієво (цикл наведено нижче).
  • Умовні плани мають розгалуження: «якщо двері відчиняться, іди далі, інакше скористайся другим входом».
  • Імовірнісне планування. Коли дія має кілька можливих результатів із заданими ймовірностями, шукаємо не послідовність дій, а стратегію для кожної ситуації — марковські процеси прийняття рішень мають у програмі серії власну частину.
def proved_s_dohledem(cil, akce, svet):
    plan = planuj(svet.pozoruj(), cil, akce)
    while plan:
        a = plan.pop(0)
        cekany = proved(svet.pozoruj(), a)   # що мало б бути істинним за моделлю
        svet.udelej(a)
        skutecny = svet.pozoruj()
        if cil <= skutecny:
            return True
        if skutecny != cekany:               # світ повівся інакше
            plan = planuj(skutecny, cil, akce)   # переплануй з того, що є
    return cil <= svet.pozoruj()

Причому досить стежити лише за тим, від чого залежить решта плану: коли зміниться атом, який не є передумовою жодної наступної дії і не входить до цілі, план лишається чинним. Цю економію запровадила ще система PLANEX, яка виконувала плани Шейкі (Файкс, Гарт і Нільссон, 1972).

10. Планування в AI-агентах

Мовний агент, який виправляє помилку в коді, теж планує: розписує собі завдання у список кроків, після кожного кроку дивиться на результат (тести пройшли? файл існує?) і відповідно коригує план. Це моніторинг і перепланування, лише дії — це команди в терміналі, а передумови й ефекти ніде формально не записані — модель оцінює їх із тексту.

Саме тут слабке місце. Дослідження на задачах зі світу кубиків (наприклад, набір PlanBench) показали, що мовні моделі в класичному плануванні часто помиляються, особливо коли кубики перейменувати на безглузді слова, тож модель не може спертися на завчені приклади. Новіші моделі з довшим міркуванням дають собі раду значно краще, але гарантії правильності не дають. Тому себе виправдовує поділ праці: модель перекладає задачу в PDDL або пропонує план, а класичний планувальник чи валідатор його перевіряє — кожен робить те, що вміє.

11. Підсумок

  • Планування — це пошук послідовності дій до цілі; STRIPS описує стан як множину атомів, а дію — передумовами, додаваними та вилучуваними атомами.
  • Пряме планування — це пошук у просторі станів; зворотне регресує ціль через релевантні дії.
  • Планувальник сам обчислює евристику з релаксованої задачі без вилучення: hmax допустима, hadd швидша, але може привести до довшого плану.
  • Аномалія Зуссмана показує, що підцілі не завжди можна виконувати одну за одною — кроки треба перемежувати.
  • Ієрархічне планування розкладає задачі за перевіреними методами; у невизначеному світі за планом стежать і перепланують — саме це роблять сучасні AI-агенти.

Хочете помацати це власноруч? Попросіть AI-агента записати світ кубиків у PDDL і запустити на ньому вільно доступний планувальник Fast Downward — а потім перевірте, зі скількома кубиками модель упорається сама, без нього.

Маєте іншу думку? Напишіть на support@coden-app.com.

Агент на вашому комп’ютері, керований з телефона