> Nível 12 de Agent Harness Patterns, uma trilha de padrões sobre como funcionam os agentes de IA. Versão web: https://harnesspatterns.dev/pt/patterns/plan-and-reflect · Todos os padrões (em inglês): https://harnesspatterns.dev/llms.txt

# Planejar e refletir

Escreva o plano antes de mexer em qualquer coisa, e confira o trabalho antes de aceitar a resposta. O plano mantém o agente no rumo; a revisão pega o que ele diz que fez, mas não fez.

## O problema

Dê a um agente um trabalho com várias partes e ele começa pelo que estiver na frente dele. No meio do caminho, os primeiros passos ficaram lá atrás no histórico, e ele esquece um. No fim, responde “pronto!” com total confiança, porque nada o obriga a olhar.

Esse é o Scatterbrain. Duas falhas numa só: sem plano, os passos se perdem; sem checagem, um passo que deu errado é relatado como feito. Na rua Tango, a ferramenta disse com todas as letras que o jornal caiu nos arbustos. O modelo leu e marcou a caixa mesmo assim.

## A solução

**Primeiro, planeje.** Antes da primeira ação de verdade, o agente escreve o trabalho como uma lista de tarefas, e marca cada uma conforme avança. O truque que faz isso funcionar: o plano inteiro é adicionado a toda requisição, então o modelo sempre vê o que está feito e o que falta, por mais longo que fique o histórico. No astorlm, isso é `pattern: 'PLAN_EXECUTE'`: duas ferramentas, `add_plan_item` e `update_plan_item`, e o plano no system prompt a cada turno.

**Reflita antes de aceitar.** Uma caixa marcada é só o que o modelo diz. Quando o `run()` retorna, o seu código revisa o resultado antes de aceitá-lo. Se algo estiver faltando, a constatação volta como uma nova mensagem na *mesma* sessão: o agente mantém o histórico e o plano, e corrige só o que está errado. Limite o número de rodadas. Há três jeitos de revisar, do mais forte ao mais fraco:

- **Conferir o mundo**
   O seu código olha o resultado em si: as varandas, as linhas do banco de dados, a suíte de testes, o arquivo no disco.
   O melhor revisor quando você pode tê-lo: barato, exato, e ninguém consegue convencê-lo de nada. Exige que o trabalho possa ser conferido por código.
- **Um modelo crítico**
   Uma segunda chamada lê a tarefa, a resposta e uma checklist, e lista o que está errado ou faltando.
   Para trabalho que nenhum código consegue conferir: um resumo, um e-mail, um plano. Custa uma chamada, também pode deixar coisas passarem, e precisa de critérios concretos, não de “isto está bom?”.
- **Perguntar ao agente**
   O system prompt manda o agente reler o trabalho antes de responder.
   De graça e, às vezes, suficiente. Mas é o mesmo modelo dando nota a si mesmo, com os mesmos pontos cegos: ele já marcou o #14 uma vez.

É a mesma ideia de uma avaliação do nível 11, usada em tempo de execução: uma avaliação dá nota a execuções depois do fato, para melhorar o agente; uma revisão dá nota a esta execução antes que o usuário a veja.

## O elenco

O mesmo elenco de sempre, desta vez numa entrega de jornais.

- **A banca** (o modelo): O Oráculo, atrás do balcão. Ele decide cada passo, e nunca pedala.
- **A folha de rota** (o plano): As tarefas e suas caixas. Ela brilha em dourado a cada turno: vai em toda requisição.
- **A bike do Astor** (o loop): Leva cada chamada de ferramenta, ida e volta, com o histórico no bandoneón.
- **Um arremesso** (deliver): Uma ferramenta comum. Ela diz onde o jornal caiu.
- **O editor** (o seu código): Passa o trabalho, e revisa as varandas antes de aceitar a resposta. A lupa dele é o `review()`.

## O código

**Com astorlm:** `pattern: 'PLAN_EXECUTE'` adiciona as ferramentas do plano e coloca o plano em toda requisição; `getPlan()` o lê de volta. A revisão é código comum depois do `run()`, e um segundo `run()` no mesmo agente continua a mesma sessão.

**Do zero:** Uma lista, duas ferramentas que a editam, e um system prompt reconstruído com a lista a cada turno. O histórico vive fora do `run()`, então a correção continua a mesma conversa.

**Com astorlm**

```ts
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

const SUBSCRIBERS = [12, 14, 18]
const porches = new Set<number>() // the real world: which porches have a paper

const deliver = tool({
  name: 'deliver',
  description: 'Ride to a house and throw today’s paper onto its porch. Says where the paper landed.',
  schema: z.object({ house: z.number() }),
  execute: async ({ house }) => {
    const landed = throwPaper(house) // your code: 'porch' or 'bushes'
    if (landed === 'porch') porches.add(house)
    return landed === 'porch' ? `Paper on the porch at #${house}.` : `Paper landed in the bushes at #${house}.`
  },
})

// PLAN: the agent gets add_plan_item and update_plan_item,
// and the current plan is added to the system prompt on every turn.
const agent = await createLocalAgent({
  provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
  pattern: 'PLAN_EXECUTE',
  systemPrompt: 'You deliver newspapers. Plan every stop before you start, and tick each task as you go.',
  tools: [deliver],
  maxTurns: 20,
})

// REFLECT: check the work itself before accepting the answer. Deterministic when you can;
// a second model with a rubric when you can't.
const review = (): string[] => SUBSCRIBERS.filter((house) => !porches.has(house)).map((house) => `#${house} has no paper on the porch`)

let answer = await agent.run(`Deliver today’s paper to every subscriber on Tango Street: ${SUBSCRIBERS.join(', ')}.`)
for (let round = 1; round <= 2; round++) {
  const problems = review()
  if (problems.length === 0) break
  // Same agent, same session: it keeps its history and its plan, and fixes what's missing.
  answer = await agent.run(`Review found: ${problems.join('; ')}. Fix it.`)
}

console.log(agent.getPlan()) // [{ id: '1', description: 'Deliver to #12', status: 'completed' }, …]
console.log(answer.content)
```

**TypeScript**

```ts
// Plan and reflect, from scratch. Plain fetch, no SDK.

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = {
  baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
  model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
  apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}

const SUBSCRIBERS = [12, 14, 18]
const porches = new Set<number>() // the real world: which porches have a paper

// 1. The plan: a list the model writes and ticks with two tools.
type Task = { id: number; description: string; status: 'pending' | 'completed' }
const plan: Task[] = []

type ToolFn = (args: Record<string, string | number>) => string
const tools: Record<string, ToolFn> = {
  add_plan_item: ({ description }) => {
    plan.push({ id: plan.length + 1, description: String(description), status: 'pending' })
    return `Task added with ID: ${plan.length}`
  },
  update_plan_item: ({ id, status }) => {
    const task = plan.find((t) => t.id === Number(id))
    if (!task) return `No task ${id}`
    task.status = status === 'completed' ? 'completed' : 'pending'
    return `Task ${id} status updated to ${task.status}.`
  },
  deliver: ({ house }) => {
    const landed = throwPaper(Number(house)) // your code: 'porch' or 'bushes'
    if (landed === 'porch') porches.add(Number(house))
    return landed === 'porch' ? `Paper on the porch at #${house}.` : `Paper landed in the bushes at #${house}.`
  },
}
const toolSchemas = [/* one JSON Schema per tool: add_plan_item(description), update_plan_item(id, status), deliver(house) */]

// 2. The plan goes into the system prompt on EVERY turn, so the model never loses track.
const system = (): string =>
  'You deliver newspapers. Plan every stop with add_plan_item before you start, and tick each task as you go.\n' +
  (plan.length ? plan.map((t) => `- [${t.status}] ${t.description} (id ${t.id})`).join('\n') : '(no plan yet)')

type ToolCall = { id: string; function: { name: string; arguments: string } }
type Message =
  | { role: 'user'; content: string }
  | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
  | { role: 'tool'; tool_call_id: string; content: string }

// The history lives outside run(), so a second run continues the same session.
const messages: Message[] = []

async function run(prompt: string, maxTurns = 20): Promise<string> {
  messages.push({ role: 'user', content: prompt })
  for (let turn = 1; turn <= maxTurns; turn++) {
    const res = await fetch(`${LLM.baseURL}/chat/completions`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
      body: JSON.stringify({ model: LLM.model, messages: [{ role: 'system', content: system() }, ...messages], tools: toolSchemas }),
    })
    const [choice] = (await res.json()).choices
    const reply: Message = choice.message
    messages.push(reply)
    if (choice.finish_reason !== 'tool_calls') return reply.content ?? ''

    for (const call of reply.tool_calls ?? []) {
      const fn = tools[call.function.name]
      const output = fn ? fn(JSON.parse(call.function.arguments)) : `Unknown tool: ${call.function.name}`
      messages.push({ role: 'tool', tool_call_id: call.id, content: output })
    }
  }
  throw new Error(`No answer after ${maxTurns} turns`)
}

// 3. Reflect: check the work itself before accepting the answer, and send what's missing back.
const review = (): string[] => SUBSCRIBERS.filter((house) => !porches.has(house)).map((house) => `#${house} has no paper on the porch`)

let answer = await run(`Deliver today’s paper to every subscriber on Tango Street: ${SUBSCRIBERS.join(', ')}.`)
for (let round = 1; round <= 2; round++) {
  const problems = review()
  if (problems.length === 0) break
  answer = await run(`Review found: ${problems.join('; ')}. Fix it.`)
}
console.log(answer)
```

**Python**

```python
# Plan and reflect, from scratch. Standard library only, no SDK.
import json
import urllib.request

# Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy...
LLM = {
    "base_url": "http://localhost:11434/v1",  # e.g. Ollama's default address
    "model": "your-model",  # e.g. "llama3.1", "gpt-4o-mini"
    "api_key": "YOUR_API_KEY",  # local servers usually ignore it
}

def post(path, payload):
    request = urllib.request.Request(
        f"{LLM['base_url']}{path}",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM['api_key']}"},
    )
    with urllib.request.urlopen(request) as response:
        return json.load(response)

SUBSCRIBERS = [12, 14, 18]
PORCHES = set()  # the real world: which porches have a paper

# 1. The plan: a list the model writes and ticks with two tools.
PLAN = []

def add_plan_item(description):
    PLAN.append({"id": len(PLAN) + 1, "description": description, "status": "pending"})
    return f"Task added with ID: {len(PLAN)}"

def update_plan_item(id, status):
    task = next((t for t in PLAN if t["id"] == int(id)), None)
    if task is None:
        return f"No task {id}"
    task["status"] = "completed" if status == "completed" else "pending"
    return f"Task {id} status updated to {task['status']}."

def deliver(house):
    landed = throw_paper(house)  # your code: "porch" or "bushes"
    if landed == "porch":
        PORCHES.add(house)
        return f"Paper on the porch at #{house}."
    return f"Paper landed in the bushes at #{house}."

TOOLS = {"add_plan_item": add_plan_item, "update_plan_item": update_plan_item, "deliver": deliver}
TOOL_SCHEMAS = [...]  # one JSON Schema per tool: add_plan_item(description), update_plan_item(id, status), deliver(house)

# 2. The plan goes into the system prompt on EVERY turn, so the model never loses track.
def system():
    lines = [f"- [{t['status']}] {t['description']} (id {t['id']})" for t in PLAN] or ["(no plan yet)"]
    return "You deliver newspapers. Plan every stop with add_plan_item before you start, and tick each task as you go.\n" + "\n".join(lines)

# The history lives outside run(), so a second run continues the same session.
MESSAGES = []

def run(prompt, max_turns=20):
    MESSAGES.append({"role": "user", "content": prompt})
    for _ in range(max_turns):
        request = [{"role": "system", "content": system()}, *MESSAGES]
        choice = post("/chat/completions", {"model": LLM["model"], "messages": request, "tools": TOOL_SCHEMAS})["choices"][0]
        reply = choice["message"]
        MESSAGES.append(reply)
        if choice["finish_reason"] != "tool_calls":
            return reply.get("content") or ""

        for call in reply.get("tool_calls", []):
            output = TOOLS[call["function"]["name"]](**json.loads(call["function"]["arguments"]))
            MESSAGES.append({"role": "tool", "tool_call_id": call["id"], "content": output})

    raise RuntimeError(f"No answer after {max_turns} turns")

# 3. Reflect: check the work itself before accepting the answer, and send what's missing back.
def review():
    return [f"#{house} has no paper on the porch" for house in SUBSCRIBERS if house not in PORCHES]

answer = run(f"Deliver today's paper to every subscriber on Tango Street: {', '.join(map(str, SUBSCRIBERS))}.")
for _ in range(2):
    problems = review()
    if not problems:
        break
    answer = run(f"Review found: {'; '.join(problems)}. Fix it.")
print(answer)
```

## O que observar

- **Mantenha as tarefas pequenas e verificáveis.** “Entregar no #14” dá para verificar; “cuidar da rua”, não. Uma tarefa que você consegue conferir é uma tarefa que a revisão consegue pegar.
- **Deixe o plano mudar.** Planos esbarram na realidade: uma rua está fechada, um cliente cancela. O agente deve poder adicionar, remover ou reordenar tarefas, e não seguir uma lista desatualizada.
- **Revise o mundo, não o plano.** A folha dizia três marcações. Conferir a folha teria passado. A revisão precisa olhar o resultado em si.
- **Limite as rodadas.** Uma revisão que nunca pode passar, ou um agente que não consegue corrigir o que encontra, fica em loop para sempre. Duas ou três rodadas, e depois passe para uma pessoa (nível 13).
- **Não planeje um trabalho de uma linha.** Planejar custa turnos e tokens. Para uma única consulta, pule. Compensa quando o trabalho tem vários passos fáceis de perder.

## Padrões relacionados

- [2 · O loop do agente](https://harnesspatterns.dev/pt/patterns/agent-loop.md)
- [11 · Observabilidade e avaliações](https://harnesspatterns.dev/pt/patterns/observability.md)
- [10 · Voltas limpas](https://harnesspatterns.dev/pt/patterns/fresh-laps.md)
- [13 · Humano no loop](https://harnesspatterns.dev/pt/patterns/human-in-the-loop.md)
- [15 · Subagentes](https://harnesspatterns.dev/pt/patterns/subagents.md)
