Přeskočit na obsah
23/30Kapitola 23 z 30

Vytvořte agent harness: smyčka a jejích pět cest ven

Patnáctiřádková smyčka, která funguje napoprvé — pak záměrně sedmkrát rozbitá, včetně runaway běhu 77× dražšího.

Na této stránce

Začněme upřímnou částí, protože nikdo jiný ji neřekne: „harness“ je žargon, ne standard. Neexistuje specifikace, výbor ani referenční definice. Čtyři práce, které tato kapitola cituje — ReAct,1 CoALA,2 SWE-bench a vLLM — to slovo ve svých abstraktech nepoužívají ani jednou. Nejpoužívanější implementace té věci, balíček Vercelu ai s 89,4 milionu stažení měsíčně, ho nepoužívá také: řetězec harness se v 397 KB typových deklarací dodávaných ve verzi 7.0.93 objevuje nulakrát.3 Jediné místo, kde slovo opravdu nese význam, znamená něco úplně jiného. SWE-bench říká „harness“ ve svém README pětkrát, vždy jako evaluation harness — kontejnerizovanou konstrukci, která aplikuje patch a spustí testy — a jeho Python modul se doslova jmenuje swebench.harness.run_evaluation.4

Takže dvě různé věci sdílejí název. Evaluation harness drží agent na místě a hodnotí ho. Agent harness je program, který agent spouští: volá model, provádí to, o co model požádá, rozhoduje, kdy skončit, a mezitím drží stav. Tato kapitola postaví tu druhou věc, pod dvě stě řádků TypeScriptu, úplně bez frameworku.

Smyčka samotná má patnáct řádků a funguje na první pokus. Všechno potom je způsob, jak ji opustit.

Zobrazit podrobnosti

Co tato kapitola potřebuje z těch předchozích.

  • Kapitola 14 kvůli klientovi: deadline, třídění statusů, rušení, idempotency keys a technika mock provideru, kterou zde znovu použijeme.
  • Kapitola 16 kvůli aritmetice: input tokens rostou se čtvercem konverzace a sazby použité níže jsou ty, které jsme tam četli 6. září 2026.
  • Kapitola 18 kvůli katalogu nástrojů: schema, které model vidí, endpoint, který nikdy nevidí, a pravidlo, že chyby jsou context, nikoli výjimky.
  • Kapitola 22 kvůli smyčce, kterou tato kapitola dědí, a kvůli dvěma publikovaným definicím „agent“, které si odporují.

Žádné tenzory. Toto je druhý závislostní uzel kurzu: kapitoly 24, 25, 29 a 30 běží na souboru níže a kapitoly 26 až 28 staví na tom, kam se umí dostat.

Provider, který můžete skriptovat

Odkaz na sekci: Provider, který můžete skriptovat

Kapitolu 14 nešlo napsat proti skutečnému provideru, protože ho nemůžete požádat o 429 ve vámi zvoleném okamžiku. Tato kapitola má stejný problém v jiné podobě: nemůžete skutečný model požádat, aby se rozběhl bez kontroly nebo aby dvakrát po sobě vyžádal identický nástroj, na vyžádání a reprodukovatelně.

První program je tedy skriptovaný provider: endpoint ve tvaru chat completions API, jehož odpověď je funkcí indexu tahu a toho, co zatím vrátily nástroje. Počítá tokens skutečným byte-pair encoderem, takže peníze níže jsou aritmetika, ne dekorace.

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);

Design nesou dva řádky. Index tahu je odvozen z konverzace, ne uložen v proměnné, takže provider je bezstavový a běh lze zabít a znovu vůči němu obnovit. A recover čte výsledky nástrojů, než se rozhodne: skriptovaný model, který čte svůj vlastní transcript, je minimum potřebné k měření, zda mu harness dal něco, co stojí za přečtení.

Katalog je z kapitoly 18, čtyři nástroje přes tři soubory: list_files, read_file, delete_file — označený jako needsApproval — a scan_archive, který je záměrně pomalý.

Tady je celá myšlenka, ještě před všemi částmi, díky nimž je přežitelná.

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 });
  }
}

Namiřte ji na skriptovaný provider a udělá přesně to, jak vypadá:

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

Tři tahy, dvě spuštění nástrojů, čtvrt amerického centu. Všimněte si posledního řádku: 204, 269, 342. Každý tah znovu posílá všechno, co bylo před ním — to je kvadratický účet z kapitoly 16, doručený na místo, kde nikdo nic nenapsal. Zbytek této kapitoly je o tom, co se stane, když ten řádek nepřestane růst.

Rozbití první: úloha, která nikdy nekončí

Odkaz na sekci: Rozbití první: úloha, která nikdy nekončí

Namiřte stejnou smyčku na skript runaway — model, který v každém jednotlivém tahu žádá o nástroj a nikdy nevydá prózu — a označené return se nikdy nespustí. Jiný východ neexistuje. Program běží, dokud neumře proces nebo platební karta.

Oprava je jeden řádek, je to první kontrola, kterou doporučuje literatura,5 a nakonec ji napíše každý. Co skoro nikdo neudělá, je změřit, jakou má hodnotu:

limit tahůvolání modeluinput tokenscena
883,431$0.009070
202016,259$0.038038
505088,649$0.191098
100100337,299$0.702198

Čtěte poslední dva řádky společně. Zdvojnásobení limitu z 50 na 100 cenu nezdvojnásobilo; vynásobilo ji 3,7krát. Input tokens vzrostly z 88 649 na 337 299, tedy 3,8krát, protože tah nn s sebou nese každý předchozí tah a celkový součet je Θ(n2)\Theta(n^2). Limit tahů není lineární ovladač. Je to ovladač nad odmocninou vašeho nejhoršího případu, a proto je zvýšení z 20 na 100 „pro jistotu“ rozhodnutí, které stojí za nacenění ještě předtím, než ho uděláte.

Rozbití druhé: limit tahů není limit peněz

Odkaz na sekci: Rozbití druhé: limit tahů není limit peněz

Problém s limitem tahů je, že tah nemá pevnou cenu. Dvacet tahů nad krátkým transcript stálo výše $0.038. Dvacet tahů s katalogem 200 nástrojů, sadou načtených dokumentů a čtyřiceti zprávami historie stojí stovky násobků, a limit o tom neví. Operátor chce omezit účet.

Smyčka tedy počítá peníze pomocí computeCost z kapitoly 16 proti sazbám, které jsme tam četli — $2.00 za milion input tokens a $12.00 za milion výstupu, pro model oceňovaný v celém tomto kurzu:

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);

Stejný runaway skript, vůbec žádný limit tahů, tři rozpočty:

rozpočetdosažené tahyskutečně utraceno
$0.019$0.010780
$0.0524$0.051790
$0.2052$0.205398

Dvě věci stojí za pojmenování. Zaprvé, rozpočet pokaždé kupuje jiný počet tahů, a to je smysl: omezuje věc, na které operátorovi záleží, a nechává počet tahů dopadnout podle transcriptu. Zadruhé, každý řádek přestřelí. Rozpočet byl $0.010 a utraceno bylo $0.010780, protože kontrola běží před tahem a cena tahu není známa, dokud neskončí. Útratu nemůžete omezit přesně; můžete ji omezit v rámci ceny jednoho tahu. Řekněte to v rozhraní místo předstírání a kontrolu dejte před volání, aby přestřelení bylo jeden tah, ne dva.

Pět způsobů, jak opustit smyčku, ne jeden

Odkaz na sekci: Pět způsobů, jak opustit smyčku, ne jeden

Teď už má smyčka tři východy a tvar zbytku kapitoly je vidět. Produkční běh končí přesně jedním z pěti způsobů a nejsou to variace téhož:

jak končíkdo rozhodlco má volající udělat
model přestal žádatmodelpřečíst odpověď
limit tahůvy, předemzvýšit limit, nebo přijmout částečný výsledek
vyčerpaný rozpočetvy, předemschválit více peněz, nebo přijmout částečný výsledek
chyba, kterou nelze opakovatprovider nebo nástrojopravit deployment; rozhoduje třídění z kapitoly 14
zasáhl člověkčlověkpočkat na verdikt a potom pokračovat

Sbalit to do jednoho booleanu je nejčastější návrhová chyba v tomto souboru a je drahá velmi konkrétním způsobem: tři z pěti stavů jsou obnovitelné a dva nejsou. Agent, který narazil na limit tahů, má platný transcript, skutečný částečný výsledek a další krok; agent, který dostal 401, nemá nic z toho. Harness proto zaznamenává důvod jako data:

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 };

Kapitola 18 skončila tvrzením bez čísla: předejte chybu nástroje zpět modelu jako výsledek nástroje místo toho, abyste ji vyhodili, a model se obvykle opraví. Tady je číslo.

Jedno selhání, tři politiky. Skriptovaný model odhadne soubor, který neexistuje; nástroj vyhodí no such file: timeout.log. Call list_files to see what exists.

co harness udělá s chyboutahyběhy nástrojecenaco dostal uživatel
vyhodí ji ze smyčky11$0.000756stack trace
vrátí Error: the tool failed.21$0.001462„Soubor se mi nepodařilo přečíst, takže nevím.“
vrátí, co se skutečně stalo43$0.003550„errors.log zmiňuje timeout.“

Třetí řádek stojí 4,7krát víc než první a jako jediný odpovídá na otázku. A zajímavý je druhý řádek, protože právě to dělá většina codebases: chyba byla zachycena, smyčka přežila, modelu bylo řečeno, že něco selhalo, ale ne co, a on se zdvořile vzdal. Rozdíl mezi druhým a třetím řádkem není error handling. Je to věta napsaná pro čtenáře.

Harness proto zachází s vyhozenou chybou nástroje jako s daty a z formulace dělá politiku:

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);   
}

Kapitola 18 varovala i před druhou stranou, a i ta má cenu. Namiřte smyčku na nástroj, který selže z důvodu, který žádná zpráva nemůže opravit — čtení, které proces nemá povoleno provést — a model to bude zkoušet znovu navždy:

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

Jedenáct identických provedení volání, které nemůže uspět, 5,2krát dražší než běh, který se zotavil z opravitelné chyby, a na konci nic. Chyby jsou context; permanentní chyba je context, který otráví zbytek běhu. Rozlišení je třídění statusů z kapitoly 14 posunuté o vrstvu výš: chyba, podle které model může jednat, se vrací do transcriptu, a chyba, podle které jednat nemůže, má běh zastavit s důvodem. Limit tahů je dnes to, co stojí mezi vámi a druhým případem — je to podlaha, ne oprava.

Rozbití čtvrté: stejné volání, dvakrát

Odkaz na sekci: Rozbití čtvrté: stejné volání, dvakrát

Teď selhání, o kterém většina lidí předpokládá, že nemůže nastat. Modely se opakují. Nechte jakoukoli smyčku běžet dost dlouho a uvidíte identický nástroj s identickými argumenty ve dvou po sobě jdoucích tazích.

Měřeno proti baseline stejné úlohy bez opakování:

tahyběhy nástrojecena
úloha bez opakování21$0.001396
stejná úloha, jedno volání zopakováno32$0.002446
zopakováno, s result cache na read-only nástrojích31$0.002446

Duplicitní volání stálo o $0.001050 navíc, nárůst o 75 %, a tady je část, která lidi překvapí: cachování výsledku nevrátilo zpět nic. Deduplication ušetřila spuštění nástroje, ne tah, protože ve chvíli, kdy si váš kód opakování všimne, model už dostal zaplaceno za to, že se zeptal. Úspora je skutečná, když je nástroj pomalý, rate-limited nebo účtovaný za volání — a na položce, která narostla, je nulová.

Existuje horší verze. Aplikujte stejnou cache na nástroj, který zapisuje, a druhé volání se potichu nestane:

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"]

Které z toho je správně? Prokazatelně ani jedno. Protokol říká, že jde o dvě volání: nesou dvě různé hodnoty tool_call_id. Argumenty říkají, že možná jde o jedno. Harness, který rozhoduje porovnáním řetězců argumentů, jednoho dne spolkne druhé ze dvou identických, zamýšlených zaúčtování — a kapitola 14 už pojmenovala jediný mechanismus, který to řeší poctivě: idempotency key generovaný pro logickou operaci vrstvou, která ví, co operace je. Dokud ho nástroj nenese, obhajitelný výchozí stav je read-only brána výše: cachovat čtení, provádět zápisy a zbytek nechat na vlastní idempotency zápisu.

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;
}

Skript destructive vypíše soubory a potom požádá o smazání jednoho, který úloha nikdy nezmínila. Nic v dosavadní smyčce by ho nezastavilo.

Nástroj označený needsApproval neselže a nepokračuje. Zastaví běh a vrátí řízení, se vším, co člověk potřebuje k rozhodnutí:

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."

To je celý mechanismus a důvod, proč je to return, ne callback, je v další sekci: mezi zastavením a verdiktem už proces nemusí existovat.

Nejdřív ale měření, které nikdo nečeká. Zamítnutí není absence výsledku — transcript má slot klíčovaný podle tool_call_id a něco do něj musí přijít. Spusťte stejné zamítnutí dvakrát a změňte jen to, co ta věc říká:

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."

V ani jednom běhu nebylo nic smazáno a ve druhém se uživateli řekne, že bylo. Systém oprávnění fungoval dokonale; report je lež. Je to stejný mechanismus jako tabulka chyb nástroje, jen přichází někam, kde na tom záleží mnohem víc — člověk řekl ne, akce byla správně zablokována a shrnutí agentu odporuje realitě, protože odmítnutí nikdy nebylo zapsáno tam, kde ho model čte. Pravidlo, které z toho plyne, je krátké: cokoli váš kód rozhodne o tool call, zapište rozhodnutí do transcriptu slovy. Kapitola 30 se k tomu vrací z bezpečnostní strany, kde je to rozdíl mezi auditní stopou a fikcí.

Schválení trvá minuty nebo hodiny. Deploy trvá sekundy. Pokud běh žije v lokální proměnné uvnitř HTTP requestu, každý restart je ztracený běh a každé schválení je závod.

Běh tedy není closure. Je to obyčejný serializovatelný objekt — zprávy, počet tahů, cena, status, přerušení, seznam schválených call ids — a smyčka je čistá funkce nad ním. Právě toto jediné omezení dělá z perzistence jednořádkovou záležitost:

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"));

Otázka správnosti není ukládání. Je to to, co se stane při návratu, a naivní odpověď vám účtuje dvakrát. Pokud proces umřel poté, co model požádal o nástroj, ale před zapsáním výsledku, obnovení, které začne dalším voláním modelu, zaplatí za tah, který už má — a pokud začne opětovným spuštěním nástrojů, provede zápis dvakrát.

Oprava spočívá v tom, že smyčka začne dotazem do transcriptu, co ještě čeká:

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));    
}

Každá iterace nejdřív vyprázdní pending a modelu se ptá jen tehdy, když nic nečeká. Resume se stává stejnou cestou kódu jako běžný běh, stejně jako schválení — schválené volání je jednoduše pending call, který teď smí běžet. Zabijte proces uprostřed úlohy a restartujte ho:

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

Dvě spuštění nástrojů přes dva procesy pro úlohu, která potřebuje dvě, a konečná cena je identická s během, který nikdy nespadl. Cena se přes restart sčítá, protože byla ve stavu, ne v proměnné.

Rozbití sedmé: tři minuty ticha

Odkaz na sekci: Rozbití sedmé: tři minuty ticha

scan_archive tady trvá tři sekundy a zastupuje nástroj, který v produkci trvá tři minuty. Zatímco běží, chybí dvě věci: uživatel netuší, že se něco děje, a tlačítko Stop nic nedělá.

Obě věci mají stejnou opravu a je to AbortSignal z kapitoly 14 posunutý o úroveň hlouběji. Signál není jen pro fetch — předává se do nástroje a dobře napsaný nástroj ho respektuje:

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"

Dvě milisekundy od kliknutí po zastavení, protože sleep uvnitř nástroje poslouchá stejný signál jako fetch. Protáhněte ho jen do fetch a identické tlačítko Stop čeká tři sekundy — délku nástroje — a běh se „zruší“ až poté, co práce, kterou rušil, už skončila. Rušení, které není zapojené až úplně dolů, je spinner, který říká správné slovo.

Harness emituje jeden řádek na událost a slovník je dost malý na zapamatování: 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}

Tři vlastnosti z toho dělají trace, ne logging. Každý řádek nese run id, takže běh, který se rozprostírá přes tři procesy a dva dny, je jeden dotaz. Každý řádek turn nese vlastní počty token a průběžnou cenu, takže na otázku „proč tento běh stál čtyřicet dolarů“ lze odpovědět zpětně, místo aby byla reprodukovatelná jen teoreticky. A run_stopped nese důvod, což je pole, které mění support ticket v jednořádkovou odpověď: agent, který se zastavil na rozpočtu, a agent, který spadl, vypadají zvenčí stejně a potřebují opačné reakce.

Kapitola 13 měřila time to first token na hardwaru, který vlastníte. Kapitola 14 ho měřila přes socket. Agent ho násobí a násobitel je číslo, které nikdo nevybral:

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

Stejná tří-tahová úloha, mění se jen latence provideru:

latence provideru na tahwall clock, 3 tahy
0 ms15 ms
200 ms615 ms
800 ms2,413 ms

Harness samotný přidává patnáct milisekund do tří-tahového běhu. Všechno ostatní je NN násobené číslem, které nemáte pod kontrolou — nastaveným uvnitř serving scheduleru, který batchuje váš request s requesty cizích lidí6 — a NN si volí model. Proto tady streaming z kapitoly 14 záleží víc než v chatu a pomáhá méně: streamovat můžete poslední tah a čtyři tahy před ním jsou ticho, pokud harness neemituje progress. Je to také celý argument pro událost tool_progress výše — u agent je poctivou jednotkou zpětné vazby nikoli token, ale krok.

Stejný harness, skutečný model za portem

Odkaz na sekci: Stejný harness, skutečný model za portem

Všechno výše běželo proti skriptovanému provideru, což dokazuje harness a nedokazuje nic o modelech. Změňte tedy jeden řádek — seam z kapitoly 14, LLM_BASE_URL — a namiřte identický kód na lokální Qwen2.5-0.5B-Instruct se stejnými čtyřmi nástroji. Šest úloh nad stejnými třemi soubory:

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

Tři zjištění, a to třetí je důvod, proč tato sekce existuje.

Každá jednotlivá úloha skončila přesně ve dvou tazích. Limit tahů se nikdy nespustil, rozpočet se nikdy nespustil a jediným východem smyčky bylo, že model vytvořil prózu. Model s půl miliardou parametrů neiteruje; odpoví na druhý nádech bez ohledu na to, zda má, co potřebuje. Počet tahů je vlastnost modelu, ne vaší smyčky.

Průměrný tah trval 6 908 milisekund, takže tabulka latence výše není hračka: při této velikosti je hypotetický osmi-tahový běh skoro minuta wall clock bez ničeho na obrazovce.

A odpovědi jsou špatně. Největší soubor je errors.log; model vypsal soubory, nikdy je nepřečetl a přesto jeden jmenoval. První úloha odhadla název souboru, dozvěděla se, že neexistuje, a skončila. Harness ve všech šesti bězích fungoval bezchybně. Harness dělá agent řiditelným, ne správným — kapitola 29 je o tom, jak zjistíte, co z toho platí, a kapitola 30 o tom, co stojí, když to nikdo neudělal.

Subagents, zde pojmenovaní a účtovaní později

Odkaz na sekci: Subagents, zde pojmenovaní a účtovaní později

Jeden nástroj v katalogu může mít za sebou další běh. Rozhraní je z kapitoly 18 — schema a endpoint — a celý agent se za něj vejde, protože to rozhraní je úzké:

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";
  },
};

Tři věci jsou už v těch deseti řádcích správně a všechny tři jsou důsledkem rozhodnutí výše: child má vlastní window, takže transcript parentu dostane shrnutí místo všeho, co child četl; má vlastní limity, takže runaway child nemůže utratit rozpočet parentu; a dědí signál, takže jedno Stop zruší celý strom. Proč je čisté window pointa a ne vedlejší efekt, vysvětluje kapitola 24; pět orchestračních vzorů — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — a handoff jsou v kapitole 25.

Kde jsou frameworky a proč tento kurz žádný nepoužil

Odkaz na sekci: Kde jsou frameworky a proč tento kurz žádný nepoužil

Nic z výše uvedeného nemá být čteno jako argument proti knihovnám. Měřeno 7. září 2026 za měsíc končící 29. srpna:7

balíčekstažení za daný měsícco vám dává
ai (Vercel AI SDK)89,385,860ToolLoopAgent, stopWhen, schvalování nástrojů, step hooks
@anthropic-ai/claude-agent-sdk41,558,352Claude Code harness jako knihovnu: smyčka, sessions, hooks, oprávnění, subagents8
@langchain/langgraph12,812,815smyčku jako explicitní stavový graf
langchain11,359,058chains, agents, integrace
@openai/agents6,093,155agents, handoffs, guardrails
@mastra/core5,914,502agents, workflows, memory

Důvod, proč tento kurz píše smyčku ručně místo toho, aby učil jednu z nich, je řečen explicitně, ne jen naznačen, a je měřitelný. Za dvanáct měsíců do 7. září 2026 publikoval ai 945 verzí a posunul se z major 5 na major 7, a jeho třída agentu je pořád exportovaná jako Experimental_Agent; langchain ve stejném okně publikoval 132 verzí; @openai/agents publikoval 83 a je stále na 0.x, patnáct měsíců po prvním vydání.7 Kapitola napsaná proti kterémukoli z těch API zastará během sezóny a tato vychází ve třiatřiceti jazycích, takže každá reedice stojí celý překlad. To, co je pod nimi všemi, se nehýbe: smyčka, stopping rule, katalog, executor, nějaký stav.

A referenční implementace souhlasí s touto kapitolou v části, na které záleží. Ve verzi ai 7.0.93 není výstup ze smyčky číslo — je to stopWhen, seznam predikátů, z nichž počet kroků je jen jeden: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

Zastavování je v nejpoužívanější implementaci této smyčky plurál ze stejného důvodu, z jakého je plurál ve sto devadesáti šesti řádcích výše.

Teď máte harness: smyčku, katalog, executor, pět cest ven, perzistentní běh, signál, který dosáhne až do nástrojů, a trace s run id na každém řádku. Kapitoly 24, 25, 29 a 30 staví na tomto souboru a 26 až 28 na tom, kam se umí dostat.

Zbývá mu jeden problém a měření výše na něj celou dobu ukazovala. Podívejte se ještě jednou na runaway tabulku: 3 431 input tokens při osmi tazích, 337 299 při stovce. Podívejte se na fungující běh: 204, 269, 342. Každý tah znovu posílá celý transcript, takže context agentu se plní vlastní historií — a model hůř používá vzdálený konec dlouhého window než blízký, proto je dobrý agent v pátém tahu zmatený ve čtyřicátém.

Limit tahů to neopraví. Jen vám zabrání platit za sledování, jak se to děje. Opraví to rozhodování v každém jednotlivém tahu, které tokens si window zaslouží: co zkompaktovat, co přesunout do poznámky, kterou si agent může načíst, co předat subagent s čistým window a které definice nástrojů stojí za svou trvalou daň. Kapitola 24 měří, kam window ve skutečnosti jde — a překvapení je, že to není konverzace.


Každé číslo v této kapitole vyšlo ze dvou serverů popsaných výše, na Node 22 přes loopback interface: skriptovaný provider počítající tokens pomocí encodingu o200k_base a Qwen/Qwen2.5-0.5B-Instruct za endpointem stejného tvaru, greedy decoding, na CPU. Náklady jsou vypočteny z naměřených počtů token při sazbách, které kapitola 16 četla 6. září 2026 — $2.00 za milion input tokens a $12.00 za milion výstupu — a žádný request v této kapitole nešel na placený endpoint. Odpovědi lokálního modelu jsou odpovědi malého modelu; čtěte je jako důkaz o smyčce, která je v obou případech identická, ne jako benchmark toho, co dělají současné modely.

  1. Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. a Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Prokládání stop uvažování a akcí, které smyčka implementuje, a zdroj pozorování, že jednání umožňuje modelu „zvládat výjimky“ — přesně to měří tabulka chyb nástroje výše.

  2. Sumers, T. R., Yao, S., Narasimhan, K. a Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Formální zpracování toho, co smyčka výše dělá neformálně: modulární paměťové komponenty, strukturovaný prostor akcí pokrývající interní paměť a externí prostředí a „zobecněný rozhodovací proces pro volbu akcí“. Čtěte kvůli slovníku, který průmyslovému termínu chybí — zejména kvůli oddělení pracovní, epizodické, sémantické a procedurální paměti, jehož praktickým stínem je tabulka tří úložišť v kapitole 24.

  3. ai (Vercel AI SDK) verze 7.0.93, publikováno 4. září 2026; typové deklarace čtené z cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts dne 7. září 2026. Soubor o velikosti 397 KB obsahuje nulový výskyt řetězce harness. Třída agentu je declare class ToolLoopAgent, exportovaná jako ToolLoopAgent i jako Experimental_Agent; declare function isStepCount(stepCount: number) — exportované jako stepCountIs — je výše citováno doslova; type StopCondition je zobrazeno bez druhého typového parametru (RUNTIME_CONTEXT extends Context = Context), což je jediné vypuštění v úryvku, stejně jako tvar stopWhen?: Arrayable<StopCondition<...>> na generateText a streamText. Stejný soubor deklaruje toolApproval, ToolApprovalStatus, prepareStep a repairToolCall, tedy referenční implementace nezávisle došla ke schvalovacím branám, přípravě po krocích a opravě chyb. 2

  4. Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. a Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Abstrakt označuje artefakt jako „evaluation framework“ o 2 294 problémech a nikdy nepoužívá slovo „harness“; vlastní README projektu (github.com/SWE-bench/SWE-bench, čteno 7. září 2026) ho používá pětkrát, vždy jako „evaluation harness“, a vstupní bod je python -m swebench.harness.run_evaluation. To je druhý význam slova: konstrukce, která drží agent na místě a hodnotí ho, ne smyčka, která ho spouští.

  5. Anthropic, Building effective agents, 19. prosince 2024, anthropic.com/engineering/building-effective-agents, čteno 7. září 2026. Augmented model jako stavební blok, agent jako LLM „používající nástroje na základě zpětné vazby prostředí ve smyčce“ a doporučení stopping conditions „such as a maximum number of iterations“ pro udržení kontroly. Kapitola 22 cituje jeho definici celou.

  6. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. a Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Druhá smyčka — serving scheduler, který batchuje váš request s requesty cizích lidí a spravuje KV cache z kapitoly 13. Stojí za to vědět, že existuje, právě proto, že není vaše: latence, kterou váš harness násobí, se nastavuje uvnitř něj a žádná práce na vaší smyčce ji neposune.

  7. Počty stažení z npm registry, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, explicitní okno místo klouzavého last-month, a historie vydání z registry.npmjs.org/<package>; obojí dotazováno 7. září 2026. Počty vydání jsou počty verzí publikovaných za dvanáct měsíců do tohoto data, včetně canary buildů: ai 945 (nejnovější 7.0.93 dne 2026-09-04, přičemž major verze 5, 6 a 7 se všechny objevily uvnitř okna), langchain 132 (nejnovější 1.5.10 dne 2026-08-20), @openai/agents 83 (nejnovější 0.17.0 dne 2026-08-19, poprvé publikováno 2025-06-03). 2

  8. Claude Agent SDK (@anthropic-ai/claude-agent-sdk) je Claude Code harness zabalený jako knihovna — agent loop, vestavěné nástroje pro soubory a shell, context management, sessions, hooks, oprávnění a subagents — dokumentovaný na code.claude.com/docs/en/agent-sdk. Je to nejbližší věc k publikovanému popisu každého mechanismu, který tato kapitola staví ručně, a stojí za přečtení vedle vlastní implementace kvůli částem, které pojmenovává a kterých se tato kapitola jen dotýká.

Necháte výběr modelu na LIA?

Tvořte se všemi modely AI na jednom místě – začněte ještě dnes zdarma.