> Nivel 13 de Agent Harness Patterns, un recorrido de patrones sobre cómo funcionan los agentes de IA. Versión web: https://harnesspatterns.dev/es/patterns/human-in-the-loop · Todos los patrones (en inglés): https://harnesspatterns.dev/llms.txt

# Humano en el bucle

El agente hace el trabajo; una persona conserva la última palabra. Aprueba lo que no se puede deshacer, lo corrige a mitad de camino y responde cuando duda.

## El problema

Un agente que corre sin una persona es rápido justo hasta el momento en que hace algo que nadie quería: paga la factura equivocada, le manda un email a todos los clientes, gasta la última moneda en el premio equivocado. Y un agente que se detiene a preguntar por todo no es más rápido que hacerlo tú mismo.

Ese es Autopilot Rex: nunca pregunta, nunca espera, nunca atiende un llamado. Se aferra a su primer plan, ignora lo que cambió desde entonces y adivina cuando debería haber preguntado.

## La solución

Pon a una persona en los pocos puntos donde su criterio importa, y deja que el agente corra en todos los demás. Hay tres de esos puntos, y se diferencian en quién rompe el silencio:

- **Aprobar**
   El agente espera
   Antes de que se ejecute una herramienta irreversible, un hook le muestra a la persona exactamente qué va a hacer y espera un sí o un no.
   Pagos, borrados, mensajes a clientes, la última moneda: cualquier cosa que no se pueda deshacer.
- **Corregir el rumbo**
   La persona interrumpe
   La persona deja una corrección en cola cuando quiere. En la siguiente llamada a herramienta, el bucle cancela esa llamada y le da al modelo la corrección en su lugar.
   Alguien está mirando y cambia de idea, o ve que el agente va en la dirección equivocada.
- **Escalar**
   El agente pregunta
   Una herramienta como ask_human que no devuelve nada hasta que una persona responde. El modelo decide cuándo usarla.
   Falta información, hay dos opciones igual de buenas, poca confianza. El system prompt dice cuándo preguntar.

Aprobar y corregir el rumbo viven en el hook `beforeToolExecution` del nivel 6: corre antes de cada herramienta, y puede esperar. Para una aprobación espera la respuesta de la persona; para corregir el rumbo revisa si hay una corrección en cola. En ambos casos, lo que dijo la persona vuelve al modelo como el resultado de la herramienta, así la ejecución sigue con la información nueva en lugar de romperse.

## El elenco

El mismo elenco de siempre, esta vez en el arcade.

- **La adivina** (el modelo): El Oráculo, en su cabina. Lee el historial y decide la siguiente llamada. Nunca toca la máquina.
- **Los controles** (las herramientas): `move_claw` es gratis y se puede deshacer. `drop_claw` gasta la última moneda: no se puede.
- **Tina** (la persona): Su globo dice quién habló primero: **!** ella interrumpió, **?** le preguntaron, **YES!** aprobó.
- **La pantalla** (la aprobación): DROP? YES NO: la garra queda quieta mientras el hook espera. Nada se ejecuta hasta que ella decide.

En el panel EventBus, la corrección de rumbo aparece como un evento `user_steering`, seguido del resultado de la llamada cancelada. Las aprobaciones y las respuestas no tienen un evento propio: son un hook y una herramienta que se tomaron su tiempo.

## El código

**Con astorlm:** Un hook de aprobación para las herramientas irreversibles, envuelto por `createSteeringController`, que agrega la cola de correcciones: llama a `steer(text)` desde tu interfaz y llega en la siguiente llamada a herramienta. Escalar es una herramienta común que espera a tu interfaz.

**Desde cero:** Tres verificaciones en el paso de herramientas del bucle del nivel 2: una corrección en cola, una aprobación para las herramientas riesgosas y una herramienta que espera a una persona.

**Con 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!"))
```

## Qué vigilar

- **Pide menos, y vale más.** Aprueba solo lo irreversible o lo caro. Una persona que hace clic en YES veinte veces por hora deja de leer, y la aprobación se vuelve un trámite.
- **Muestra la llamada real.** “¿Bajar?” no alcanza. Muestra la herramienta y sus argumentos exactos, el monto, el destinatario, el premio, para que la persona apruebe lo que de verdad se va a ejecutar.
- **Decide qué pasa cuando nadie responde.** Una persona puede irse. Pon un timeout, y elige el valor por defecto seguro: normalmente denegar, y decirle al modelo por qué.
- **Las correcciones llegan en la siguiente llamada a herramienta.** La corrección de rumbo no puede detener una herramienta a mitad de ejecución ni una respuesta a mitad de palabra. Si el agente solo está escribiendo texto, la corrección espera. Para una parada en seco, aborta la ejecución.
- **Guarda un registro.** Registra quién aprobó qué, cuándo y qué vio. Cuando algo sale mal, “lo hizo el agente” nunca es toda la historia.

## Patrones relacionados

- [6 · Hooks](https://harnesspatterns.dev/es/patterns/hooks.md)
- [12 · Planificar y reflexionar](https://harnesspatterns.dev/es/patterns/plan-and-reflect.md)
- [5 · Errores en el bucle](https://harnesspatterns.dev/es/patterns/errors-in-the-loop.md)
- [14 · Seguridad y sandboxing](https://harnesspatterns.dev/es/patterns/security.md)
- [16 · Agentes proactivos](https://harnesspatterns.dev/es/patterns/proactive-agents.md)
