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

# Humano no loop

O agente faz o trabalho; uma pessoa fica com a palavra final. Ela aprova o que não dá para desfazer, corrige no meio do caminho e responde quando ele está em dúvida.

## O problema

Um agente que roda sem uma pessoa é rápido até o momento em que faz algo que ninguém queria: paga a fatura errada, manda e-mail para todos os clientes, gasta a última ficha no prêmio errado. E um agente que para para perguntar sobre tudo não é mais rápido do que fazer você mesmo.

Esse é o Autopilot Rex: nunca pergunta, nunca espera, nunca atende um chamado. Ele se agarra ao primeiro plano, ignora o que mudou desde então e chuta quando deveria ter perguntado.

## A solução

Coloque uma pessoa nos poucos pontos em que o julgamento dela importa, e deixe o agente rodar em todos os outros. São três pontos, e eles se diferenciam por quem quebra o silêncio:

- **Aprovar**
   O agente espera
   Antes de uma ferramenta irreversível rodar, um hook mostra à pessoa exatamente o que ela vai fazer e espera um sim ou um não.
   Pagamentos, exclusões, mensagens a clientes, a última ficha: qualquer coisa que não dá para desfazer.
- **Corrigir o rumo**
   A pessoa interrompe
   A pessoa coloca uma correção na fila quando quiser. Na próxima chamada de ferramenta, o loop cancela essa chamada e entrega a correção ao modelo no lugar.
   Alguém está olhando e muda de ideia, ou vê o agente indo na direção errada.
- **Escalar**
   O agente pergunta
   Uma ferramenta como ask_human que só retorna quando uma pessoa responde. O modelo decide quando usá-la.
   Falta informação, duas opções igualmente boas, pouca confiança. O system prompt diz quando perguntar.

Aprovar e corrigir o rumo vivem no hook `beforeToolExecution` do nível 6: ele roda antes de toda ferramenta, e pode esperar. Numa aprovação, ele espera a resposta da pessoa; numa correção de rumo, confere se há uma correção na fila. Nos dois casos, o que a pessoa disse volta para o modelo como resultado da ferramenta, então a execução segue com a informação nova em vez de quebrar.

## O elenco

O mesmo elenco de sempre, desta vez no fliperama.

- **A cartomante** (o modelo): O Oráculo, na cabine. Lê o histórico e decide a próxima chamada. Nunca encosta na máquina.
- **Os controles** (as ferramentas): `move_claw` é de graça e pode ser desfeito. `drop_claw` gasta a última ficha: não pode.
- **Tina** (a pessoa): O balão dela diz quem falou primeiro: **!** ela interrompeu, **?** perguntaram a ela, **YES!** ela aprovou.
- **A tela** (a aprovação): DROP? YES NO: a garra fica parada enquanto o hook espera. Nada roda até ela decidir.

No painel EventBus, a correção de rumo aparece como um evento `user_steering`, seguido do resultado da chamada cancelada. Aprovações e respostas não têm evento próprio: são um hook e uma ferramenta que levaram o seu tempo.

## O código

**Com astorlm:** Um hook de aprovação para as ferramentas irreversíveis, embrulhado pelo `createSteeringController`, que adiciona a fila de correções: chame `steer(text)` a partir da sua interface e ela chega na próxima chamada de ferramenta. Escalar é uma ferramenta comum que espera pela sua interface.

**Do zero:** Três checagens na etapa de ferramentas do loop do nível 2: uma correção na fila, uma aprovação para as ferramentas arriscadas e uma ferramenta que espera por uma pessoa.

**Com astorlm**

```ts
import { OpenAIProvider, createLocalAgent, createSteeringController, 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

// Your UI: each of these resolves when the person clicks or types.
declare function approve(what: string): Promise<boolean> // shows YES / NO
declare function answer(question: string): Promise<string> // shows a text box

// 1. ESCALATE: a tool the model calls when it isn't sure. It waits for a person.
const askKid = tool({
  name: 'ask_kid',
  description: 'Ask Tina when you are not sure what she wants. Waits for her answer.',
  schema: z.object({ question: z.string() }),
  execute: async ({ question }) => answer(question),
})

// 2. APPROVE: irreversible tools wait for a yes before they run.
const NEEDS_APPROVAL = new Set(['drop_claw'])
const steering = createSteeringController({
  beforeToolExecution: async ({ toolName, input }) => {
    if (!NEEDS_APPROVAL.has(toolName)) return { authorize: true }
    const ok = await approve(`${toolName}(${JSON.stringify(input)})`) // show the real call
    return ok ? { authorize: true } : { authorize: false, mockResult: 'Tina said no. Ask her what to do.' }
  },
})

const agent = await createLocalAgent({
  provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
  systemPrompt: 'You work a claw machine for Tina. If you are not sure which prize she means, ask her before acting.',
  tools: [moveClaw, dropClaw, askKid], // moveClaw, dropClaw: your code
  hooks: steering.hooks, // the steering controller wraps the approval hook
  maxTurns: 20,
})

// 3. STEER: the person can correct the agent at any moment, e.g. from a button.
// The loop cancels the next tool call and hands the model this feedback instead.
onTinaShouts((text) => steering.steer(text)) // your UI: 'No, wait! The penguin!'

const result = await agent.run('Get me the bear!')
console.log(result.content)
```

**TypeScript**

```ts
// Human in the loop, 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
}

// Your UI: each of these resolves when the person clicks or types.
declare function approve(what: string): Promise<boolean>
declare function answer(question: string): Promise<string>

type ToolFn = (args: Record<string, string | number>) => Promise<string>
const tools: Record<string, ToolFn> = {
  move_claw: moveClaw, // your code
  drop_claw: dropClaw, // your code
  // 1. ESCALATE: the model asks, a person answers.
  ask_kid: ({ question }) => answer(String(question)),
}
const toolSchemas = [/* one JSON Schema per tool: move_claw(to), drop_claw(), ask_kid(question) */]
const NEEDS_APPROVAL = new Set(['drop_claw'])

// 3. STEER: a person can queue a correction at any time, e.g. from a button.
let steer: string | null = null
export const queueCorrection = (text: string) => {
  steer = text
}

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

export async function runAgent(prompt: string, maxTurns = 20): Promise<string> {
  const messages: Message[] = [
    { role: 'system', content: 'You work a claw machine for Tina. If you are not sure which prize she means, ask her before acting.' },
    { 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, 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 name = call.function.name
      const args = JSON.parse(call.function.arguments)
      let output: string
      if (steer !== null) {
        // The tool boundary: a queued correction cancels this call, and the model reads it instead.
        output = `Cancelled. The person says: ${steer}`
        steer = null
      } else if (NEEDS_APPROVAL.has(name) && !(await approve(`${name}(${call.function.arguments})`))) {
        // 2. APPROVE: irreversible tools wait here for a yes.
        output = 'Tina said no. Ask her what to do.'
      } else {
        output = tools[name] ? await tools[name](args) : `Unknown tool: ${name}`
      }
      messages.push({ role: 'tool', tool_call_id: call.id, content: output })
    }
  }
  throw new Error(`No answer after ${maxTurns} turns`)
}

console.log(await runAgent('Get me the bear!'))
```

**Python**

```python
# Human in the loop, from scratch. Standard library only, no SDK.
import json
import queue
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)

# In a terminal the person is input(); in an app, whatever your UI sends back.
def approve(what):
    return input(f"Approve {what}? [y/N] ").strip().lower() == "y"

def ask_kid(question):  # 1. ESCALATE: the model asks, a person answers.
    return input(f"The agent asks: {question}\n> ")

TOOLS = {"move_claw": move_claw, "drop_claw": drop_claw, "ask_kid": ask_kid}  # move_claw, drop_claw: your code
TOOL_SCHEMAS = [...]  # one JSON Schema per tool: move_claw(to), drop_claw(), ask_kid(question)
NEEDS_APPROVAL = {"drop_claw"}

# 3. STEER: another thread (a UI, a chat) can queue a correction at any time.
CORRECTIONS = queue.Queue()

def run_agent(prompt, max_turns=20):
    messages = [
        {"role": "system", "content": "You work a claw machine for Tina. If you are not sure which prize she means, ask her before acting."},
        {"role": "user", "content": prompt},
    ]
    for _ in range(max_turns):
        choice = post("/chat/completions", {"model": LLM["model"], "messages": messages, "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", []):
            name = call["function"]["name"]
            args = json.loads(call["function"]["arguments"])
            if not CORRECTIONS.empty():
                # The tool boundary: a queued correction cancels this call, and the model reads it instead.
                output = f"Cancelled. The person says: {CORRECTIONS.get()}"
            elif name in NEEDS_APPROVAL and not approve(f"{name}({args})"):
                # 2. APPROVE: irreversible tools wait here for a yes.
                output = "Tina said no. Ask her what to do."
            else:
                output = TOOLS[name](**args)
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": output})

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

print(run_agent("Get me the bear!"))
```

## O que observar

- **Peça menos, e o pedido vale mais.** Aprove só o que é irreversível ou caro. Uma pessoa que clica em YES vinte vezes por hora para de ler, e a aprovação vira formalidade.
- **Mostre a chamada real.** “Descer?” não basta. Mostre a ferramenta e os argumentos exatos, o valor, o destinatário, o prêmio, para a pessoa aprovar o que de fato vai rodar.
- **Decida o que acontece quando ninguém responde.** Uma pessoa pode ir embora. Defina um timeout e escolha o padrão seguro: geralmente negar, e dizer ao modelo por quê.
- **As correções chegam na próxima chamada de ferramenta.** A correção de rumo não consegue parar uma ferramenta no meio nem uma resposta no meio de uma palavra. Se o agente só está escrevendo texto, a correção espera. Para uma parada brusca, aborte a execução.
- **Mantenha um registro.** Registre quem aprovou o quê, quando e o que viu. Quando algo dá errado, “foi o agente” nunca é a história toda.

## Padrões relacionados

- [6 · Hooks](https://harnesspatterns.dev/pt/patterns/hooks.md)
- [12 · Planejar e refletir](https://harnesspatterns.dev/pt/patterns/plan-and-reflect.md)
- [5 · Erros no loop](https://harnesspatterns.dev/pt/patterns/errors-in-the-loop.md)
- [14 · Segurança e sandboxing](https://harnesspatterns.dev/pt/patterns/security.md)
- [16 · Agentes proativos](https://harnesspatterns.dev/pt/patterns/proactive-agents.md)
