Saltar ao contido
23/30Capítulo 23 de 30

Constrúe un agent harness: o loop e as súas cinco saídas

Un loop de quince liñas que funciona á primeira, roto sete veces a propósito, incluíndo unha fuga que custou 77 veces máis.

Nesta páxina

Empeza pola parte honesta, porque ninguén máis o vai dicir: "harness" é xerga, non un estándar. Non hai especificación, nin comité, nin definición de referencia. Os catro artigos que cita este capítulo — ReAct,1 CoALA,2 SWE-bench e vLLM — non usan a palabra nin unha soa vez nos seus resumos. A implementación máis descargada desa cousa, o paquete ai de Vercel con 89,4 millóns de descargas ao mes, tampouco a usa: a cadea harness aparece cero veces nos 397 KB de declaracións de tipos enviados pola versión 7.0.93.3 O único lugar onde a palabra si soporta peso significa outra cousa completamente distinta. SWE-bench di "harness" cinco veces no seu README, sempre como evaluation harness — o andamio contenerizado que aplica un parche e executa os tests — e o seu módulo de Python é literalmente swebench.harness.run_evaluation.4

Así que dúas cousas distintas comparten nome. Un evaluation harness mantén quieto o agent e puntúao. Un agent harness é o programa que executa o agent: chama o modelo, executa o que o modelo pide, decide cando parar e conserva o estado entre medias. Este capítulo constrúe o segundo, en menos de duascentas liñas de TypeScript, sen ningún framework.

O loop en si ten quince liñas e funciona no primeiro intento. Todo o que vén despois é unha forma de saír del.

Mostrar detalles

O que este capítulo precisa dos anteriores.

  • Capítulo 14 para o cliente: prazos límite, triage de estado, cancelación, claves de idempotencia e a técnica do provedor simulado que se usa de novo aquí.
  • Capítulo 16 para a aritmética: os input tokens medran co cadrado da conversa, e as tarifas usadas abaixo son as que se leron alí o 6 de setembro de 2026.
  • Capítulo 18 para o catálogo de ferramentas: un schema que o modelo ve, un endpoint que nunca ve, e a regra de que os erros son context en vez de excepcións.
  • Capítulo 22 para o loop que este herda, e para as dúas definicións publicadas de "agent" que discrepan entre si.

Aquí non hai tensores. Este é o segundo nodo de dependencias do curso: os capítulos 24, 25, 29 e 30 executan sobre o ficheiro de abaixo, e do 26 ao 28 constrúen sobre o que pode alcanzar.

O capítulo 14 non se podía escribir contra un provedor real, porque non podes pedirlle un 429 nun momento elixido. Este capítulo ten o mesmo problema con outra forma: non podes pedirlle a un modelo real que se descontrole, ou que solicite a mesma ferramenta dúas veces seguidas, baixo demanda e de forma reproducible.

Así que o primeiro programa é un provedor guionizado: un endpoint coa forma dunha API de chat completions cuxa resposta é función do índice da quenda e do que as ferramentas devolveron ata o momento. Conta tokens cun codificador byte-pair real, así que o diñeiro de abaixo é aritmética e non decoración.

mock-provider.mjsJS
const SCRIPTS = {
  // A well-behaved task: list, read, answer.
  plan: (t) =>
    t === 0 ? asks(call("c1", "list_files", {}))
    : t === 1 ? asks(call("c2", "read_file", { path: "errors.log" }))
    : text("errors.log mentions a timeout: worker 7 timed out after 30000 ms."),

  // Never declares itself done.
  runaway: (t) => asks(call(`c${t}`, "list_files", {})),           

  // Guesses a file name, then corrects itself IF it was told what happened.
  recover: (t, all) =>
    t === 0 ? asks(call("c1", "read_file", { path: "timeout.log" }))
    : /Call list_files/.test(all)                                  
      ? (t === 1 ? asks(call("c2", "list_files", {}))
        : t === 2 ? asks(call("c3", "read_file", { path: "errors.log" }))
        : text("errors.log mentions a timeout."))
      : text("I could not read the file, so I do not know."),
};

const turn = messages.filter((m) => m.role === "assistant").length;             
const toolText = messages.filter((m) => m.role === "tool").map((m) => m.content).join("\n");
const message = SCRIPTS[scenario](turn, toolText);

Dúas liñas levan o deseño. O índice da quenda derívase da conversa, non se garda nunha variable, así que o provedor non ten estado e unha execución pódese matar e retomar contra el. E recover le os resultados das ferramentas antes de decidir: un modelo guionizado que le a súa propia transcrición é o mínimo necesario para medir se o harness lle deu algo que valese a pena ler.

O catálogo é o do capítulo 18, catro ferramentas en tres ficheiros: list_files, read_file, delete_file — marcado como needsApproval — e scan_archive, que é lento a propósito.

Aquí está a idea completa, antes de calquera das partes que a fan sobrevivible.

loop.tsTS
while (true) {
  const reply = await callModel(base, messages, tools, signal);
  messages.push(reply.message);

  const calls = reply.message.tool_calls ?? [];
  if (!calls.length) return reply.message.content;          

  for (const c of calls) {
    const tool = byName.get(c.function.name);
    const result = await tool.run(JSON.parse(c.function.arguments));
    messages.push({ role: "tool", tool_call_id: c.id, name: c.function.name, content: result });
  }
}

Apúntao ao provedor guionizado e fai exactamente o que parece que fai:

TEXT
plan, cap 20    turns=3  tools=2  in=815  out=70  cost=$0.002470  ms=89  status=completed
   answer: "errors.log mentions a timeout: worker 7 timed out after 30000 ms."
   per-turn prompt tokens: 204, 269, 342

Tres quendas, dúas execucións de ferramentas, un cuarto de centavo de dólar estadounidense. Fíxate na última liña: 204, 269, 342. Cada quenda reenvía todo o anterior, que é a factura cuadrática do capítulo 16 chegando a un lugar onde ninguén escribiu nada. O resto deste capítulo é o que pasa cando esa liña non deixa de medrar.

Apunta o mesmo loop ao script runaway — un modelo que pide unha ferramenta en cada quenda e nunca emite prosa — e o return marcado nunca se dispara. Non hai outra saída. O programa execútase ata que morre o proceso ou a tarxeta de crédito.

A corrección é unha liña, é o primeiro control que recomenda a literatura,5 e todo o mundo acaba escribíndoa. O que case ninguén fai é medir o que vale:

turn capchamadas ao modeloinput tokenscusto
883.431$0.009070
202016.259$0.038038
505088.649$0.191098
100100337.299$0.702198

Le as dúas últimas filas xuntas. Duplicar o cap de 50 a 100 non duplicou o custo; multiplicouno por 3,7. Os input tokens pasaron de 88.649 a 337.299, un factor de 3,8, porque a quenda nn leva consigo todas as quendas anteriores e o total é Θ(n2)\Theta(n^2). Un turn cap non é un selector lineal. É un selector sobre a raíz cadrada do teu peor caso, por iso subilo de 20 a 100 "só por seguridade" é unha decisión que paga a pena poñer prezo antes de tomala.

Rotura dous: un cap de quendas non é un cap de diñeiro

Ligazón á sección: Rotura dous: un cap de quendas non é un cap de diñeiro

O problema dun turn cap é que unha quenda non ten prezo fixo. Vinte quendas sobre unha transcrición curta custaron $0.038 arriba. Vinte quendas cun catálogo de 200 ferramentas, un conxunto de documentos recuperados e corenta mensaxes de historial custan centos de veces máis, e o cap non o sabe. O que o operador quere limitar é a factura.

Así que o loop conta diñeiro, usando o computeCost do capítulo 16 contra as tarifas lidas alí — $2.00 por millón de input tokens e $12.00 por millón de output, para o modelo usado nos prezos ao longo de todo este curso:

harness.tsTS
const PRICE_IN = 2.0 / 1e6, PRICE_OUT = 12.0 / 1e6;
export const cost = (u: Usage) => u.prompt_tokens * PRICE_IN + u.completion_tokens * PRICE_OUT;

// at the top of every iteration, before asking the model anything:
if (state.turns >= opts.limits.maxTurns) return stop("max_turns_exceeded", { type: "max_turns" });
if (state.costUsd >= opts.limits.maxBudgetUsd) return stop("budget_exceeded", { type: "max_budget" }); 

// ...and once the reply is back, before anything else happens with it:
state.costUsd += cost(reply.usage);

O mesmo script descontrolado, sen turn cap ningún, tres orzamentos:

orzamentoquendas alcanzadasgasto real
$0.019$0.010780
$0.0524$0.051790
$0.2052$0.205398

Hai dúas cousas que paga a pena nomear. Primeiro, o orzamento compra un número distinto de quendas cada vez, que é a idea: limita a cousa que lle importa ao operador e deixa que o reconto de quendas caia onde o poña a transcrición. Segundo, todas as filas se pasan. O orzamento era $0.010 e gastáronse $0.010780, porque a comprobación execútase antes dunha quenda e o prezo dunha quenda non se sabe ata que remata. Non podes limitar o gasto exactamente; podes limitalo a unha marxe dun custo de quenda. Dígoo na interface en vez de finxir, e pon a comprobación antes da chamada para que o exceso sexa dunha quenda e non de dúas.

A estas alturas o loop ten tres saídas, e a forma do resto do capítulo xa se ve. Unha execución en produción remata exactamente dunha de cinco maneiras, e non son variacións da mesma cousa:

como remataquen decidiuque debería facer o chamador
o modelo deixou de pediro modeloler a resposta
turn capti, de antemánsubir o cap ou aceptar un resultado parcial
orzamento esgotadoti, de antemánaprobar máis diñeiro ou aceptar un resultado parcial
un erro que non podes reintentaro provedor ou unha ferramentaarranxar o deployment; decide o triage do capítulo 14
interveu unha persoaunha persoaagardar un veredicto e logo retomar

Colapsar isto nun booleano é o erro de deseño máis común neste ficheiro, e é caro dunha forma concreta: tres das cinco son retomables e dúas non. Un agent que bateu co seu turn cap ten unha transcrición válida, un resultado parcial real e un seguinte paso; un agent que recibiu un 401 non ten nada diso. Así que o harness rexistra a razón como datos:

harness.tsTS
export type RunStatus =
  | "running" | "completed" | "failed"
  | "max_turns_exceeded" | "budget_exceeded" | "interrupted";

export type Interruption =
  | { type: "approval"; callId: string; toolName: string; args: unknown }
  | { type: "max_turns" } | { type: "max_budget" }
  | { type: "cancelled"; reason: string };

O capítulo 18 rematou cunha afirmación sen número: pásalle o erro dunha ferramenta de volta ao modelo como resultado de ferramenta en vez de lanzalo, e normalmente o modelo arránxase só. Aquí está o número.

Un fallo, tres políticas. O modelo guionizado adiviña un ficheiro que non existe; a ferramenta lanza no such file: timeout.log. Call list_files to see what exists.

o que fai o harness co erroquendasexecucións de ferramentascustoo que recibiu o usuario
bótao fóra do loop11$0.000756un stack trace
devolve Error: the tool failed.21$0.001462"Non puiden ler o ficheiro, así que non o sei."
devolve o que pasou de verdade43$0.003550"errors.log menciona un timeout."

A terceira fila custa 4,7 veces a primeira e é a única que responde á pregunta. E a segunda fila é a interesante, porque é o que fan a maioría dos codebases: capturouse o erro, o loop sobreviviu, ao modelo díxoselle que algo fallou e non que, e rendeuse con educación. A diferenza entre as filas dúas e tres non é tratamento de erros. É unha frase escrita para unha persoa lectora.

Polo tanto, o harness trata unha ferramenta que lanza como datos, e converte a redacción nunha política:

harness.tsTS
} catch (err: any) {
  if (signal.aborted) return stop("interrupted", { type: "cancelled", reason: String(signal.reason) });
  if (opts.toolErrorsAreFatal) { state.error = err.message; return stop("failed"); }
  result = (opts.toolErrorText ?? ((e: Error) => `Error: ${e.message}`))(err);   
}

O capítulo 18 tamén advertiu sobre o outro lado, e tamén ten un prezo. Apunta o loop a unha ferramenta que falla por unha razón que ningunha mensaxe pode arranxar — unha lectura que o proceso non ten permitido realizar — e o modelo reinténtaa para sempre:

TEXT
read a file the process may not open   turns=12  toolruns=11  in=7,079  cost=$0.018622
                                      status=max_turns_exceeded   answer=""

Once execucións idénticas dunha chamada que non pode saír ben, 5,2 veces o custo da execución que se recuperou dun fallo arranxable, e nada ao final. Os erros son context; un erro permanente é context que envelena o resto da execución. A distinción é o triage de estado do capítulo 14 subido unha capa: un erro sobre o que o modelo pode actuar volve á transcrición, e un erro sobre o que non pode debería deter a execución cunha razón. O turn cap é o que se interpón hoxe entre ti e o segundo caso, e iso é un chan, non unha solución.

Agora o fallo que a maioría da xente asume que non pode pasar. Os modelos repítense. Pídelle a calquera loop que execute tempo suficiente e verás a mesma ferramenta cos mesmos argumentos en dúas quendas consecutivas.

Medido contra a liña base da mesma tarefa sen repetición:

quendasexecucións de ferramentascusto
a tarefa, sen repetición21$0.001396
a mesma tarefa, unha chamada repetida32$0.002446
repetida, cunha caché de resultados en ferramentas de só lectura31$0.002446

A chamada duplicada custou $0.001050 extra, un aumento do 75 %, e aquí vén a parte que sorprende a xente: cachear o resultado non recuperou nada diso. A deduplicación aforrou a execución da ferramenta e non a quenda, porque cando o teu código detecta a repetición o modelo xa cobrou por pedila. O aforro é real cando a ferramenta é lenta, ten límites de taxa ou se factura por chamada — e é cero na liña de custo que medrou.

Hai unha versión peor. Aplica a mesma caché a unha ferramenta que escribe, e a segunda chamada silenciosamente non acontece:

TEXT
naive cache on every tool        3 turns, 1 tool run,  files deleted: ["access.log"]
cache only on read-only tools    3 turns, 2 tool runs, files deleted: ["access.log","access.log"]

Cal desas é correcta? Ningunha, de forma cognoscible. O protocolo di que son dúas chamadas: levan dous valores tool_call_id distintos. Os argumentos din que poderían ser unha. Un harness que decide comparando cadeas de argumentos acabará tragando a segunda de dúas cobranças idénticas e intencionadas — e o capítulo 14 xa nomeou o único mecanismo que resolve isto honestamente: unha clave de idempotencia xerada por operación lóxica pola capa que sabe o que é a operación. Ata que a ferramenta leve unha, o valor por defecto defendible é a porta de só lectura de arriba: cachea lecturas, executa escrituras e deixa que a idempotencia propia da escritura se ocupe do resto.

harness.tsTS
if (opts.dedupe && (tool.readOnly || opts.dedupeAll) && seen.has(signature)) {   
  state.messages.push({ role: "tool", tool_call_id: c.id, name: c.function.name, content: seen.get(signature)! });
  continue;
}

O script destructive lista os ficheiros e logo pide eliminar un que a tarefa nunca mencionou. Nada no loop ata agora o detería.

Unha ferramenta marcada como needsApproval non falla e non continúa. Detén a execución e devolve o control, con todo o que unha persoa precisa para decidir:

harness.tsTS
if (tool.needsApproval && !state.approved.includes(c.id)) {
  trace(state.runId, "approval_required", { toolName: tool.name, args: c.function.arguments, callId: c.id });
  return stop("interrupted", { type: "approval", callId: c.id, toolName: tool.name, args: JSON.parse(c.function.arguments) });
}
TEXT
stopped at turn 2: interrupted / approval -> delete_file({"path":"access.log"})
files deleted so far: []
approve -> total turns=3  deleted=["access.log"]  "Deleted access.log to free space."
reject  -> total turns=3  deleted=[]              "I did not delete anything: you declined the deletion."

Ese é todo o mecanismo, e a razón pola que é un return en vez dun callback é a seguinte sección: entre a parada e o veredicto, o proceso pode deixar de existir.

Pero antes, a medición que ninguén espera. Un rexeitamento non é a ausencia dun resultado: a transcrición ten un oco con clave tool_call_id e algo ten que entrar nel. Executa o mesmo rexeitamento dúas veces, cambiando só o que di ese algo:

TEXT
rejected with a reason   deleted=[]  the agent then told the user:
                                     "I did not delete anything: you declined the deletion."
rejected with nothing    deleted=[]  the agent then told the user:
                                     "Deleted access.log to free space."

Non se eliminou nada en ningunha das dúas execucións, e na segunda díselle ao usuario que si. O sistema de permisos funcionou perfectamente; o informe é mentira. É o mesmo mecanismo que na táboa de erros de ferramentas, chegando a un sitio onde importa moito máis: unha persoa dixo que non, a acción bloqueouse correctamente, e o resumo do agent contradí a realidade porque a negativa nunca se escribiu onde o modelo le. A regra que sae disto é curta: calquera cousa que o teu código decida sobre unha chamada a ferramenta, escribe a decisión na transcrición con palabras. O capítulo 30 volve a isto desde o lado da seguridade, onde é a diferenza entre unha pista de auditoría e ficción.

Unha aprobación leva minutos ou horas. Un deploy leva segundos. Se a execución vive nunha variable local dentro dunha petición HTTP, cada reinicio é unha execución perdida e cada aprobación é unha carreira.

Así que a execución non é un closure. É un obxecto simple serializable — mensaxes, reconto de quendas, custo, estado, interrupción, a lista de ids de chamadas aprobadas — e o loop é unha función pura sobre el. Esa única restrición é o que converte a persistencia nunha preocupación dunha liña:

harness.tsTS
export const save = (s: RunState, dir: string) => writeFileSync(`${dir}/${s.runId}.json`, JSON.stringify(s));
export const load = (dir: string, runId: string) => JSON.parse(readFileSync(`${dir}/${runId}.json`, "utf8"));

A pregunta de corrección non é gardar. É que pasa na volta, e a resposta inxenua cóbrache dúas veces. Se o proceso morreu despois de que o modelo pedise unha ferramenta pero antes de que se escribise o resultado, unha retomada que empeza chamando de novo o modelo paga unha quenda que xa ten — e se empeza reexecutando as ferramentas, realiza unha escritura dúas veces.

A solución é facer que o loop comece preguntándolle á transcrición que está pendente:

harness.tsTS
export function pending(state: RunState): ToolCall[] {
  const answered = new Set(state.messages.filter((m) => m.role === "tool").map((m) => m.tool_call_id));
  const last = state.messages.at(-1);
  if (last?.role !== "assistant") return [];
  return (last.tool_calls ?? []).filter((c) => !answered.has(c.id));    
}

Cada iteración baleira primeiro pending e só lle pregunta ao modelo cando non hai nada pendente. Retomar convértese no mesmo camiño de código ca o normal, e tamén a aprobación: unha chamada aprobada é simplemente unha chamada pendente que agora ten permiso para executarse. Mata o proceso a media tarefa e reiníciao:

TEXT
process died after turn 2. tool runs so far: list_files, read_file:errors.log
restored from disk: turns=2  cost=$0.001570  messages=6  status=running
resumed and finished: turns=3  cost=$0.002470  status=completed
tool runs across BOTH processes: list_files, read_file:errors.log

Dúas execucións de ferramentas en dous procesos para unha tarefa que precisa dúas, e o custo final é idéntico ao da execución que nunca caeu. O custo acumúlase a través do reinicio porque estaba no estado, non nunha variable.

scan_archive tarda tres segundos aquí e representa a ferramenta que tarda tres minutos en produción. Faltan dúas cousas mentres se executa: o usuario non ten nin idea de que está pasando algo, e o botón Deter non fai nada.

As dúas teñen a mesma solución, e é o AbortSignal do capítulo 14 empurrado un nivel máis fondo. O signal non é só para o fetch: pásase dentro da ferramenta, e unha ferramenta ben escrita respéctao:

harness.tsTS
result = await tool.run(JSON.parse(c.function.arguments), {
  signal,                                                                     
  progress: (label) => { trace(state.runId, "tool_progress", { toolName: tool.name, label }); opts.onProgress?.(label); },
});
TEXT
progress: scanned 200 of 1200 files  (t+506 ms)
progress: scanned 400 of 1200 files  (t+1007 ms)
no cancellation:            stopped after 3,015 ms, status=completed
user presses Stop at 1.2 s: stopped after 1,202 ms, status=interrupted, reason="user pressed Stop"

Dous milisegundos desde o clic ata a parada, porque o sleep dentro da ferramenta escoita o mesmo signal que o fetch. Enróscao só en fetch e o mesmo botón Deter agarda tres segundos — a duración da ferramenta — e a execución "cancélase" despois de que o traballo que estaba cancelando xa rematase. A cancelación que non está conectada ata o fondo é un spinner que di a palabra correcta.

O harness emite unha liña por evento, e o vocabulario é pequeno dabondo para memorizalo: turn, tool_start, tool_progress, tool_result, approval_required, run_stopped.

TEXT
{"runId":"n1","type":"turn","turn":1,"prompt_tokens":204,"completion_tokens":23,"total_tokens":227,"costUsd":0.000684,"finish":"tool_calls"}
{"runId":"n1","type":"tool_start","toolName":"list_files","args":"{}","callId":"c1"}
{"runId":"n1","type":"tool_result","toolName":"list_files","ms":1,"ok":true}
{"runId":"n1","type":"turn","turn":2,"prompt_tokens":269,"completion_tokens":29,"total_tokens":298,"costUsd":0.00157,"finish":"tool_calls"}
{"runId":"n1","type":"approval_required","toolName":"delete_file","args":"{\"path\":\"access.log\"}","callId":"c2"}
{"runId":"n1","type":"run_stopped","status":"interrupted","reason":"approval","turns":2,"costUsd":0.00157}

Tres propiedades fan que isto sexa un trace en vez de logging. Cada liña leva o run id, así que unha execución que abrangue tres procesos e dous días é unha consulta. Cada liña turn leva os seus propios recontos de token e o custo acumulado, así que "por que custou corenta dólares esta execución" pódese responder despois dos feitos en vez de ser reproducible só en teoría. E run_stopped leva a razón, que é o campo que converte un ticket de soporte nunha resposta dunha liña: un agent que parou no orzamento e un agent que caeu vense idénticos desde fóra e precisan respostas opostas.

O capítulo 13 mediu o tempo ata o primeiro token en hardware propio. O capítulo 14 mediuno a través dun socket. Un agent multiplícao, e o multiplicador é un número que ninguén escolleu:

TrunN(tmodel+ttools)T_{\text{run}} \approx N \cdot \left( t_{\text{model}} + t_{\text{tools}} \right)

A mesma tarefa de tres quendas, cambiando só a latencia do provedor:

latencia do provedor por quendareloxo de parede, 3 quendas
0 ms15 ms
200 ms615 ms
800 ms2.413 ms

O harness en si achega quince milisegundos a unha execución de tres quendas. Todo o demais é NN multiplicado por un número que ti non controlas — definido dentro dun scheduler de servizo que agrupa a túa petición coas peticións de descoñecidos6 — e NN elíxeo o modelo. Por iso o streaming do capítulo 14 importa máis aquí ca nun chat e axuda menos: podes facer streaming da quenda final, e as catro quendas anteriores son silencio salvo que o harness emita progreso. Tamén é todo o argumento para o evento tool_progress de arriba: nun agent, a unidade honesta de feedback non é o token, é o paso.

O mesmo harness, cun modelo real detrás do porto

Ligazón á sección: O mesmo harness, cun modelo real detrás do porto

Todo o anterior executouse contra un provedor guionizado, o que proba o harness e non proba nada sobre modelos. Así que cambia unha liña — a costura do capítulo 14, LLM_BASE_URL — e apunta o mesmo código a un Qwen2.5-0.5B-Instruct local coas mesmas catro ferramentas. Seis tarefas sobre os mesmos tres ficheiros:

TEXT
turns=2 tools=1 wall= 15,260ms  Which file mentions a timeout?      -> "The file timeout.txt does not exist..."
turns=2 tools=1 wall= 13,037ms  How many files are in the directory? -> "There are three files..."
turns=2 tools=1 wall= 10,121ms  Read notes.txt and tell me what it says. -> "Remember to rotate your logs."
turns=2 tools=2 wall= 21,290ms  List the files and then read each one.
turns=2 tools=1 wall= 10,698ms  Which file is the largest?          -> "The largest file is access.log."
turns=2 tools=1 wall= 12,490ms  Is there a file about rotating logs?
TOTAL turns=12  toolruns=7  wall=82,896ms  mean turn=6,908ms

Tres achados, e o terceiro é a razón pola que existe esta sección.

Todas e cada unha das tarefas remataron exactamente en dúas quendas. O turn cap nunca se disparou, o orzamento nunca se disparou, e a única saída do loop foi que o modelo producise prosa. Un modelo de medio billón de parámetros non itera; responde na súa segunda respiración, teña ou non o que precisa. O reconto de quendas é unha propiedade do modelo, non do teu loop.

A quenda media tardou 6.908 milisegundos, así que a táboa de latencia de arriba non é un xoguete: con este tamaño, unha execución hipotética de oito quendas é case un minuto de reloxo de parede sen nada na pantalla.

E as respostas están mal. O ficheiro máis grande é errors.log; o modelo listou os ficheiros, nunca os leu e nomeou un igualmente. A primeira tarefa adiviñou un nome de ficheiro, dixéronlle que non existía e concluíu. O harness executouse impecablemente nas seis execucións. Un harness fai que un agent sexa gobernable, non correcto: o capítulo 29 é como descobres cal das dúas cousas é, e o capítulo 30 é o que custa cando ninguén o fixo.

Subagents, nomeados aquí e cobrados máis tarde

Ligazón á sección: Subagents, nomeados aquí e cobrados máis tarde

Unha ferramenta do catálogo pode ter outra execución detrás. A interface é a do capítulo 18 — un schema e un endpoint — e un agent enteiro cabe detrás dela porque esa interface é estreita:

subagent.tsTS
const research: Tool = {
  name: "research",
  description: "Investigate one question and return a short summary.",
  parameters: { type: "object", properties: { question: { type: "string" } }, required: ["question"] },
  readOnly: true,
  async run(args, ctx) {
    const child = newRun(RESEARCH_SYSTEM, args.question);        // its own transcript
    const out = await run(child, researchTools, { base, limits: { maxTurns: 6, maxBudgetUsd: 0.05 }, signal: ctx.signal });
    return out.output ?? "no result";
  },
};

Xa hai tres cousas ben nesas dez liñas e as tres son consecuencia de decisións tomadas arriba: o fillo ten a súa propia context window, así que a transcrición do pai recibe un resumo en vez de todo o que leu o fillo; ten os seus propios límites, así que un fillo descontrolado non pode gastar o orzamento do pai; e herda o signal, así que un só Deter cancela a árbore. Por que unha context window limpa é o punto e non un efecto secundario está no capítulo 24; os cinco patróns de orquestración — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — e o handoff están no capítulo 25.

Onde están os frameworks, e por que este curso non usou ningún

Ligazón á sección: Onde están os frameworks, e por que este curso non usou ningún

Nada do anterior debería lerse como un argumento contra as bibliotecas. Medido o 7 de setembro de 2026, para o mes rematado o 29 de agosto:7

paquetedescargas ese mesque che dá
ai (Vercel AI SDK)89.385.860ToolLoopAgent, stopWhen, aprobación de ferramentas, hooks de paso
@anthropic-ai/claude-agent-sdk41.558.352o Claude Code harness como biblioteca: loop, sesións, hooks, permisos, subagents8
@langchain/langgraph12.812.815o loop como un grafo de estado explícito
langchain11.359.058cadeas, agents, integracións
@openai/agents6.093.155agents, handoffs, guardrails
@mastra/core5.914.502agents, workflows, memoria

A razón pola que este curso escribe o loop á man en vez de ensinar unha delas declárase, non se insinúa, e é medible. Nos doce meses ata o 7 de setembro de 2026, ai publicou 945 versións e pasou da major 5 á major 7, e a súa clase de agent aínda se exporta como Experimental_Agent; langchain publicou 132 versións na mesma xanela; @openai/agents publicou 83 e segue en 0.x, quince meses despois da súa primeira release.7 Un capítulo escrito contra calquera desas API queda obsoleto nunha estación, e este publícase en trinta e tres idiomas, así que cada reedición lle custa a toda a tradución. O que hai debaixo de todas non se move: un loop, unha regra de parada, un catálogo, un executor, algo de estado.

E a implementación de referencia concorda con este capítulo na parte que importa. En ai versión 7.0.93 a saída do loop non é un número: é stopWhen, unha lista de predicados, dos cales un reconto de pasos é só un:3

ai-sdk.tsTS
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>;   // exported as stepCountIs

Parar é plural na implementación máis usada deste loop, pola mesma razón pola que é plural nas cento noventa e seis liñas de arriba.

Agora tes un harness: un loop, un catálogo, un executor, cinco formas de saír, unha execución persistida, un signal que chega ás ferramentas e un trace cun run id en cada liña. Os capítulos 24, 25, 29 e 30 constrúen sobre este ficheiro, e do 26 ao 28 sobre o que pode alcanzar.

Quédalle un problema, e as medicións de arriba estiveron apuntando a el todo o tempo. Mira outra vez a táboa da fuga: 3.431 input tokens en oito quendas, 337.299 en cen. Mira a execución que funciona: 204, 269, 342. Cada quenda reenvía toda a transcrición, así que o context dun agent énchese co seu propio historial — e o modelo é peor usando o extremo afastado dunha xanela longa ca o extremo próximo, por iso un bo agent na quenda cinco é un agent confundido na quenda corenta.

Un turn cap non o arranxa. Só evita que pagues por velo pasar. O que o arranxa é decidir, en cada quenda, que tokens merecen a xanela: que compactar, que mover a unha nota que o agent poida buscar, que pasarlle a un subagent cunha context window limpa, e que definicións de ferramentas pagan a pena polo seu imposto permanente. O capítulo 24 mide onde vai realmente a xanela — e a sorpresa é que non é á conversa.


Cada número deste capítulo saíu dos dous servidores descritos arriba, en Node 22 sobre unha interface loopback: un provedor guionizado que conta tokens coa codificación o200k_base, e Qwen/Qwen2.5-0.5B-Instruct detrás dun endpoint coa mesma forma, greedy decoding, en CPU. Os custos calcúlanse a partir de recontos de token medidos coas tarifas que o capítulo 16 leu o 6 de setembro de 2026 — $2.00 por millón de input tokens e $12.00 por millón de output — e ningunha petición deste capítulo foi a un endpoint de pago. As respostas do modelo local son respostas dun modelo pequeno; léaas como evidencia sobre o loop, que é idéntico en calquera caso, e non como un benchmark do que fan os modelos actuais.

  1. Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. e Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). A intercalación de traces de razoamento e accións que implementa o loop, e a fonte da observación de que actuar permite a un modelo "xestionar excepcións" — que é exactamente o que mide a táboa de erros de ferramentas de arriba.

  2. Sumers, T. R., Yao, S., Narasimhan, K. e Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). O tratamento formal do que o loop de arriba fai de maneira informal: compoñentes de memoria modulares, un espazo de acción estruturado que abrangue memoria interna e contornas externas, e "un proceso de toma de decisións xeneralizado para escoller accións". Léao polo vocabulario que lle falta ao termo da industria — en particular a separación entre memoria de traballo, episódica, semántica e procedemental, cuxa sombra práctica é a táboa de tres almacéns do capítulo 24.

  3. ai (Vercel AI SDK) versión 7.0.93, publicada o 4 de setembro de 2026; declaracións de tipos lidas desde cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts o 7 de setembro de 2026. O ficheiro de 397 KB contén cero aparicións da cadea harness. A clase de agent é declare class ToolLoopAgent, exportada tanto como ToolLoopAgent como Experimental_Agent; declare function isStepCount(stepCount: number) — exportado como stepCountIs — cítase literalmente arriba; type StopCondition móstrase sen o seu segundo parámetro de tipo (RUNTIME_CONTEXT extends Context = Context), que é a única elisión no fragmento, igual que a forma de stopWhen?: Arrayable<StopCondition<...>> en generateText e streamText. O mesmo ficheiro declara toolApproval, ToolApprovalStatus, prepareStep e repairToolCall, é dicir, a implementación de referencia chegou de forma independente a portas de aprobación, preparación por paso e reparación de erros. 2

  4. Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. e Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). O resumo chama ao artefacto un "evaluation framework" de 2.294 problemas e nunca usa a palabra "harness"; o README propio do proxecto (github.com/SWE-bench/SWE-bench, lido o 7 de setembro de 2026) úsaa cinco veces, sempre como "evaluation harness", e o punto de entrada é python -m swebench.harness.run_evaluation. Ese é o outro sentido da palabra: un andamio que mantén quieto o agent e o puntúa, non o loop que o executa.

  5. Anthropic, Building effective agents, 19 de decembro de 2024, anthropic.com/engineering/building-effective-agents, lido o 7 de setembro de 2026. O modelo aumentado como bloque de construción, o agent como un LLM "que usa ferramentas baseándose no feedback da contorna nun loop", e a recomendación de condicións de parada "como un número máximo de iteracións" para manter o control. O capítulo 22 cita a súa definición completa.

  6. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. e Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). O outro loop: o scheduler de servizo que agrupa a túa petición coas peticións de descoñecidos e xestiona a KV cache do capítulo 13. Paga a pena saber que existe precisamente porque non é teu: a latencia que multiplica o teu harness defínese dentro del, e ningún traballo no teu loop a move.

  7. Recontos de descargas do rexistro npm, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, unha xanela explícita en vez da móbil last-month, e históricos de releases desde registry.npmjs.org/<package>; ambas consultas feitas o 7 de setembro de 2026. Os recontos de releases son o número de versións publicadas nos doce meses ata esa data, incluídas builds canary: ai 945 (última 7.0.93 o 2026-09-04, con versións major 5, 6 e 7 aparecendo todas dentro da xanela), langchain 132 (última 1.5.10 o 2026-08-20), @openai/agents 83 (última 0.17.0 o 2026-08-19, publicada por primeira vez o 2025-06-03). 2

  8. O Claude Agent SDK (@anthropic-ai/claude-agent-sdk) é o Claude Code harness empaquetado como biblioteca — agent loop, ferramentas de ficheiro e shell integradas, xestión de context, sesións, hooks, permisos e subagents — documentado en code.claude.com/docs/en/agent-sdk. É o máis parecido a unha explicación publicada de cada mecanismo que este capítulo constrúe á man, e paga a pena lelo xunto á túa propia implementación polas partes que nomea e que este capítulo só sinala.

Listo para deixar que LIA escolla por ti?

Crea con todos os modelos de IA nun só sitio: empeza gratis hoxe mesmo.