06
Multi-step Planning
Продвинутый

В уроке 5 мы собрали агента из skills. Но стратегия была одна — отпустить агента в свободный полёт. Теперь мы добавим два новых skill'а и покажем, что одни и те же skills поддерживают принципиально разные стратегии: Plan-then-Execute и ReAct. Разница не в tools, а в том, как agent loop их оркестрирует.

Plan-then-Execute
Отдельный LLM-вызов для плана → потом выполнение
ReAct
Think → Act → Think → Act на каждом шаге

Plan-then-Execute — два отдельных этапа. Сначала LLM (без tools) создаёт структурированный JSON-план, не зная реального состояния системы. Потом другой вызов получает план и выполняет по шагам. Плюс: предсказуемо, план можно показать пользователю. Минус: план может не совпасть с реальностью.

ReAct — один цикл, но с think tool. Перед каждым действием агент «думает вслух»: что я знаю, что делать дальше, почему. Плюс: агент адаптируется на лету, видит реальные данные. Минус: менее предсказуемо, больше итераций.

think tool — хитрый приём. Он ничего не делает технически, просто возвращает "OK". Но заставляет модель вербализовать рассуждение перед действием — и это сильно повышает качество решений.

Шаг 1. Создаём reasoning skill

Think tool — это не просто функция, это skill. Оформляем его как отдельный пакет с SKILL.md, чтобы любой агент мог его подключить.

Создаём структуру
mkdir -p skills/reasoning
skills/reasoning/SKILL.md
---
name: reasoning
description: Internal scratchpad for thinking and planning. Use when the agent needs to reason through a problem before acting.
metadata:
  author: ai-agents-book
  version: "1.0"
---

# Reasoning Skill

Ты можешь «думать вслух» — использовать внутренний блокнот для рассуждений.

## Доступные инструменты
- **think** — записать мысль, план или рассуждение. Результат не виден пользователю.

## Правила
- Используй think перед сложным действием: обдумай, что знаешь и что делать дальше
- Используй think после получения результата: оцени, совпало ли с ожиданиями
- Чередуй think → action → think → action для лучших результатов

Обрати внимание на description: "Use when the agent needs to reason..." — это подсказка для Discovery-уровня. Агент читает это и решает: нужен ли мне этот skill?

Шаг 2. Создаём bash skill

Второй новый skill — выполнение shell-команд. Вместе с filesystem из урока 5 это даёт агенту полный набор для работы с кодом.

Создаём структуру
mkdir -p skills/bash
skills/bash/SKILL.md
---
name: bash
description: Execute shell commands. Use when the agent needs to run scripts, install packages, or interact with the system.
metadata:
  author: ai-agents-book
  version: "1.0"
---

# Bash Skill

Ты умеешь выполнять shell-команды через bash.

## Доступные инструменты
- **run_bash** — выполнить bash-команду и получить stdout

## Правила
- Используй для чтения структуры проекта (find, ls, tree)
- Используй для анализа кода (grep, wc, diff)
- Таймаут: 10 секунд. Не запускай долгие процессы
- Будь осторожен с деструктивными командами (rm, mv)
Теперь у нас 4 skill'а
ls skills/
# bash/  filesystem/  reasoning/  web-search/

Шаг 3. Shared модуль — skill-utils.ts

В уроке 5 весь код был в одном файле. Теперь выносим общую инфраструктуру в src/skill-utils.ts — чтобы уроки 6, 7, 8 могли переиспользовать discoverSkills(), activateSkill() и createAgent() без дублирования кода.

Ключевое расширение: createAgent(skills, options?) теперь принимает optionssystemPrompt, maxIterations, extraTools, extraHandlers. Это позволяет кастомизировать агента без изменения самого createAgent.

src/skill-utils.ts
// src/skill-utils.ts — Shared модуль для уроков 05–08
import OpenAI from "openai";
import { execSync } from "child_process";
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
import { join } from "path";
import dotenv from "dotenv";

dotenv.config();

const client = new OpenAI({
    baseURL: "https://openrouter.ai/api/v1",
    apiKey: process.env.OPENROUTER_API_KEY ?? "",
});

const MODEL = "anthropic/claude-sonnet-4.5";

// === Интерфейс Skill ===
export interface Skill {
    name: string;
    description: string;       // Discovery-уровень
    instructions: string;      // Activation-уровень
    location: string;
    tools: OpenAI.ChatCompletionTool[];
    handlers: Record<string, (args: any) => string | Promise<string>>;
}

// === Парсер YAML frontmatter ===
export function parseFrontmatter(raw: string) {
    const parts = raw.split("---");
    if (parts.length < 3) throw new Error("Invalid SKILL.md: no frontmatter");
    const frontmatter = parts[1].trim();
    const body = parts.slice(2).join("---").trim();
    const meta: Record<string, string> = {};
    for (const line of frontmatter.split("\n")) {
        const idx = line.indexOf(":");
        if (idx > 0 && !line.startsWith(" "))
            meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
    }
    return { name: meta.name, description: meta.description, body };
}

// === Discovery + Activation ===
export function discoverSkills(dir: string) { /* ... сканируем папку ... */ }
export function activateSkill(location: string) { /* ... загружаем SKILL.md ... */ }

// === Фабрики ===
export function createFilesystemSkill(meta) { /* tools: read_file, write_file, list_dir */ }
export function createBashSkill(meta)       { /* tools: run_bash */ }
export function createReasoningSkill(meta)  { /* tools: think → "OK" */ }
export function createWebSearchSkill(meta)  { /* tools: web_search */ }

// === createAgent с options ===
interface AgentOptions {
    systemPrompt?: string;   // кастомный prompt ({{SKILLS}} заменяется)
    maxIterations?: number;
    extraTools?: OpenAI.ChatCompletionTool[];    // системные tools
    extraHandlers?: Record<string, Function>;    // их обработчики
}

export function createAgent(skills: Skill[], options: AgentOptions = {}) {
    const allTools = [...skills.flatMap(s => s.tools), ...(options.extraTools ?? [])];
    const allHandlers = { ...Object.assign({}, ...skills.map(s => s.handlers)),
                          ...(options.extraHandlers ?? {}) };
    // System prompt формируется из skills XML + options.systemPrompt
    // Agent loop: итерации до maxIterations, async handlers
}

Шаг 4. Код планирующего агента

Теперь главное: обе стратегии используют одни и те же skills, но по-разному. Plan-then-Execute — два вызова LLM (планировщик без tools + исполнитель с tools). ReAct — один createAgent() с особым system prompt.

src/06-planning.ts
// src/06-planning.ts — Одни skills, две стратегии
import OpenAI from "openai";
import {
    discoverSkills, activateSkill, createAgent,
    createFilesystemSkill, createBashSkill, createReasoningSkill,
    Skill
} from "./skill-utils";

import dotenv from "dotenv";
dotenv.config();

const client = new OpenAI({
    baseURL: "https://openrouter.ai/api/v1",
    apiKey: process.env.OPENROUTER_API_KEY ?? "",
});
const MODEL = "anthropic/claude-sonnet-4.5";

// === Загружаем skills ===
function loadSkills(): Skill[] {
    const discovered = discoverSkills("./skills");
    console.log("\n🔌 Activation:");

    const skills: Skill[] = [];
    for (const d of discovered) {
        const meta = activateSkill(d.location);
        switch (meta.name) {
            case "filesystem": skills.push(createFilesystemSkill(meta)); break;
            case "bash":       skills.push(createBashSkill(meta)); break;
            case "reasoning":  skills.push(createReasoningSkill(meta)); break;
            // web-search пропускаем — для этой задачи не нужен
        }
    }

    console.log(`\n🚀 Активировано ${skills.length} skills: ${skills.map(s => s.name).join(", ")}`);
    return skills;
}

// ============================================================
// ПОДХОД 1: Plan-then-Execute
// ============================================================

async function planThenExecute(skills: Skill[], task: string) {
    console.log("╔══════════════════════════════════════════╗");
    console.log("║   ПОДХОД 1: Plan-then-Execute            ║");
    console.log("╚══════════════════════════════════════════╝\n");

    // ФАЗА 1: Планирование (LLM без tools)
    console.log("📋 ФАЗА 1: ПЛАНИРОВАНИЕ\n");

    const planResponse = await client.chat.completions.create({
        model: MODEL,
        messages: [
            {
                role: "system",
                content: `Ты планировщик. Составь пошаговый план.
Доступные инструменты: run_bash, read_file, write_file, list_dir, think.
Ответь ТОЛЬКО валидным JSON:
{
  "goal": "краткое описание цели",
  "steps": [
    { "id": 1, "action": "описание", "tool": "имя_tool", "reason": "зачем" }
  ]
}`,
            },
            { role: "user", content: task },
        ],
    });

    const planText = planResponse.choices[0].message.content ?? "";
    const cleanJson = planText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
    const plan = JSON.parse(cleanJson);

    console.log(`🎯 Цель: ${plan.goal}`);
    plan.steps.forEach((s: any) => {
        console.log(`   ${s.id}. [${s.tool}] ${s.action}`);
    });

    // ФАЗА 2: Выполнение (createAgent с планом в system prompt)
    console.log("\n\n⚡ ФАЗА 2: ВЫПОЛНЕНИЕ\n");

    const agent = createAgent(skills, {
        systemPrompt: `Ты исполнитель задач. Выполни план пошагово, используя инструменты.

ПЛАН:
${JSON.stringify(plan, null, 2)}

<available_skills>
{{SKILLS}}
</available_skills>

Выполняй шаги по порядку. Когда все выполнены — подведи итог.`,
        maxIterations: 15,
    });

    await agent(task);
}

// ============================================================
// ПОДХОД 2: ReAct (Reasoning + Acting)
// ============================================================

async function reactAgent(skills: Skill[], task: string) {
    console.log("╔══════════════════════════════════════════╗");
    console.log("║   ПОДХОД 2: ReAct (Reason + Act)         ║");
    console.log("╚══════════════════════════════════════════╝\n");

    const agent = createAgent(skills, {
        systemPrompt: `Ты агент, который решает задачи пошагово.

МЕТОД РАБОТЫ (ReAct):
1. Перед КАЖДЫМ действием — вызови "think": что известно? что дальше? почему?
2. Выполни действие нужным tool
3. После результата — снова "think": получилось? что это значит?

Всегда чередуй think → action → think → action.

<available_skills>
{{SKILLS}}
</available_skills>`,
        maxIterations: 20,
    });

    await agent(task);
}

// ============================================================
// MAIN
// ============================================================

async function main() {
    const skills = loadSkills();

    const task =
        "Проанализируй все .ts файлы в директории src/. " +
        "Найди дублирование кода между файлами. " +
        "Создай файл REFACTORING.md с рекомендациями по рефакторингу.";

    const approach = process.argv[2] ?? "plan";

    if (approach === "plan") {
        await planThenExecute(skills, task);
    } else if (approach === "react") {
        await reactAgent(skills, task);
    } else {
        console.log("Использование: npx tsx src/06-planning.ts [plan|react]");
    }
}

main();
npx tsx src/06-planning.ts plan  # или "react"
Демо в формате диалога
1/1
Пример вывода. У тебя может быть иначе — это нормально.
✅ Что должно получиться
В режиме plan — сначала JSON-план, затем пошаговое выполнение. В режиме react — чередование think и action на каждом шаге. Оба используют одни и те же 3 skills: filesystem, bash, reasoning.
🧯 Если не работает
Убедись, что папки skills/reasoning/ и skills/bash/ созданы с корректными SKILL.md. Проверь что src/skill-utils.ts на месте.
💡 Ключевая идея: одни и те же skills, разные стратегии. createAgent(skills) собирает tools из skills — а system prompt определяет как агент ими пользуется. Plan-then-Execute и ReAct — это не про tools, а про оркестрацию.

Think tool — хитрый приём: он ничего не делает (возвращает "OK"), но заставляет модель вербализовать рассуждение. Это skill, а не хак — любой агент может его подключить через skills/reasoning/SKILL.md.

Сравнение:
Plan-then-Execute: ✅ предсказуемо, план виден юзеру · ❌ план может не совпасть с реальностью
ReAct: ✅ адаптивно, агент видит реальные данные · ❌ менее предсказуемо, больше итераций
На практике лучший результат даёт комбинация обоих — именно это мы сделаем в следующем шаге.