Ugrás a tartalomra
23/3023/30. fejezet

Agent harness építése: a loop és öt kiútja

Egy 15 soros loop elsőre működik, majd szándékosan hétszer eltörjük — egy runaway példával, ami 77-szer többe került.

Ezen az oldalon

Kezdjük az őszinte résszel, mert más úgysem mondja ki: a „harness” zsargon, nem szabvány. Nincs specifikáció, nincs bizottság, nincs referencia-definíció. A négy tanulmány, amelyre ez a fejezet hivatkozik — ReAct,1 CoALA,2 SWE-bench és vLLM — az absztraktjaikban egyszer sem használja ezt a szót. A dolog legtöbbet letöltött implementációja, a Vercel ai csomagja havi 89,4 millió letöltéssel, szintén nem használja: a harness sztring nulla alkalommal jelenik meg a 7.0.93-as verzióval szállított 397 KB-nyi típusdeklarációban.3 Az egyetlen hely, ahol a szó tényleg jelentést hordoz, egészen mást jelent. A SWE-bench ötször írja le a README-ben, mindig evaluation harness értelemben — azt a konténerizált vázat, amely alkalmaz egy patchet és lefuttatja a teszteket —, a Python modulja pedig szó szerint swebench.harness.run_evaluation.4

Tehát két különböző dolog ugyanazt a nevet viseli. Egy evaluation harness mozdulatlanul tartja az agentet és pontozza. Egy agent harness az a program, amely futtatja az agentet: meghívja a modellt, végrehajtja, amit a modell kér, eldönti, mikor kell megállni, és közben megtartja az állapotot. Ez a fejezet a másodikat építi fel, kétszáz sornál kevesebb TypeScriptben, mindenféle framework nélkül.

Maga a loop tizenöt sor, és első próbálkozásra működik. Minden ezután következő rész egy mód arra, hogy kilépj belőle.

Részletek megjelenítése

Amire ennek a fejezetnek szüksége van a korábbiakból.

  • 14. fejezet a klienshez: határidők, státusz-triázs, megszakítás, idempotenciakulcsok és az itt újra használt mock provider technika.
  • 16. fejezet a számoláshoz: az input tokenek a beszélgetés négyzetével nőnek, és az alábbi díjak azok, amelyeket ott 2026. szeptember 6-án olvastunk.
  • 18. fejezet a tool katalógushoz: egy schema, amelyet a modell lát, egy endpoint, amelyet sosem lát, és a szabály, hogy a hibák kontextusok, nem kivételek.
  • 22. fejezet ahhoz a loophoz, amelyet ez megörököl, valamint az „agent” két publikált definíciójához, amelyek nem értenek egyet egymással.

Itt nincsenek tenzorok. Ez a kurzus második függőségi csomópontja: a 24., 25., 29. és 30. fejezet az alábbi fájlon fut, a 26–28. fejezet pedig arra épít, amit ez elér.

A 14. fejezetet nem lehetett valódi provider ellen megírni, mert nem kérhetsz tőle 429-et egy kiválasztott pillanatban. Ennek a fejezetnek ugyanez a problémája más alakban: nem kérhetsz meg egy valódi modellt arra, hogy runaway legyen, vagy hogy reprodukálhatóan, kérésre kétszer egymás után ugyanazt a toolt kérje ugyanazokkal az argumentumokkal.

Ezért az első program egy scriptelt provider: egy chat completions API alakú endpoint, amelynek válasza a turn indexétől és attól függ, hogy a toolok eddig mit adtak vissza. Valódi byte-pair encoderrel számolja a tokeneket, így az alábbi pénzösszegek számítások, nem díszletek.

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

Két sor hordozza a dizájnt. A turn indexe a beszélgetésből van levezetve, nem egy változóban tároljuk, ezért a provider állapotmentes, és egy futtatás megölhető, majd folytatható vele szemben. A recover pedig elolvassa a tool eredményeit, mielőtt dönt: egy scriptelt modell, amely elolvassa a saját átiratát, a minimum ahhoz, hogy mérni lehessen, adott-e neki a harness bármit, amit érdemes elolvasni.

A katalógus a 18. fejezeté, négy tool három fájlon keresztül: list_files, read_file, delete_fileneedsApproval jelöléssel — és scan_archive, amely szándékosan lassú.

Itt van az egész ötlet, még azelőtt, hogy bejönnének azok a részek, amelyek túlélhetővé teszik.

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

Irányítsd a scriptelt providerre, és pontosan azt teszi, aminek látszik:

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

Három turn, két tool-végrehajtás, negyed amerikai cent. Figyeld az utolsó sort: 204, 269, 342. Minden turn újraküld mindent, ami előtte volt; ez a 16. fejezet kvadratikus számlája, csak itt úgy érkezik meg, hogy senki sem gépelt semmit. A fejezet többi része arról szól, mi történik, amikor ez a sor nem hagyja abba a növekedést.

Első törés: a feladat, amely sosem ér véget

Link a szakaszhoz: Első törés: a feladat, amely sosem ér véget

Irányítsd ugyanezt a loopot a runaway scriptre — egy modellre, amely minden egyes turnben toolt kér, és sosem bocsát ki prózát —, és a megjelölt return sosem fut le. Nincs más kijárat. A program addig fut, amíg a process meg nem hal, vagy a bankkártya fel nem adja.

A javítás egy sor, ez az első kontroll, amelyet az irodalom ajánl,5 és végül mindenki megírja. Amit szinte senki sem tesz meg: megméri, mennyit ér:

turn capmodel callsinput tokenscost
883,431$0.009070
202016,259$0.038038
505088,649$0.191098
100100337,299$0.702198

Olvasd együtt az utolsó két sort. A cap 50-ről 100-ra duplázása nem duplázta a költséget; 3,7-szeresére szorozta. Az input tokenek 88,649-ről 337,299-re nőttek, 3,8-szorosára, mert a nn turn minden korábbi turnt visz magával, az összeg pedig Θ(n2)\Theta(n^2). A turn cap nem lineáris tekerő. A legrosszabb eset négyzetgyökén van a tekerő, ezért a 20-ról 100-ra emelés „csak a biztonság kedvéért” olyan döntés, amelyet érdemes beárazni, mielőtt meghozod.

Második törés: a turn cap nem költségplafon

Link a szakaszhoz: Második törés: a turn cap nem költségplafon

A turn cap baja az, hogy egy turnnek nincs fix ára. Húsz turn egy rövid átiraton fent $0.038-ba került. Húsz turn egy 200 toolból álló katalógussal, egy retrieved dokumentumkészlettel és negyven előzményüzenettel ennek százszorosaiba kerülhet, a cap pedig erről nem tud. Az operátor azt akarja korlátozni, ami számlaként megjelenik.

Ezért a loop pénzt számol, a 16. fejezet computeCost kódját használva az ott olvasott díjak ellenében — $2.00 egymillió input tokenenként és $12.00 egymillió output tokenenként ahhoz a modellhez, amelyet a kurzus végig áraz:

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

Ugyanaz a runaway script, turn cap nélkül, három büdzsével:

budgetturns reachedactually spent
$0.019$0.010780
$0.0524$0.051790
$0.2052$0.205398

Két dolgot érdemes néven nevezni. Először: a budget minden alkalommal más számú turnt vásárol, és ez a lényeg: azt korlátozza, ami az operátort érdekli, a turnszámot pedig hagyja oda esni, ahová az átirat teszi. Másodszor: minden sor túllő. A budget $0.010 volt, és $0.010780 ment el, mert az ellenőrzés a turn előtt fut, a turn ára pedig csak utána ismert. A költést nem tudod pontosan korlátozni; legfeljebb egy turn költségén belül. Mondd ezt ki a felületen ahelyett, hogy úgy tennél, mintha nem így lenne, és tedd az ellenőrzést a hívás elé, hogy a túllövés egy turn legyen, ne kettő.

Mostanra a loopnak három kijárata van, és látszik a fejezet hátralévő része. Egy production futtatás pontosan ötféleképpen ér véget, és ezek nem egymás variációi:

how it endswho decidedwhat the caller should do
the model stopped askingthe modelread the answer
turn capyou, in advanceraise the cap, or accept a partial result
budget exhaustedyou, in advanceapprove more money, or accept a partial result
an error you cannot retrythe provider or a toolfix the deployment; Chapter 14's triage decides
a human interveneda personwait for a verdict, then resume

Ezeket egyetlen boolean értékbe összevonni a leggyakoribb tervezési hiba ebben a fájlban, és nagyon konkrét módon drága: az ötből három folytatható, kettő nem. Egy agent, amely elérte a turn capet, érvényes átirattal, valódi részleges eredménnyel és következő lépéssel rendelkezik; egy agent, amely 401-et kapott, ezek egyikével sem. Ezért a harness adatként rögzíti az okot:

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

A 18. fejezet számszerűsítés nélküli állítással zárult: add vissza a tool hibáját a modellnek tool resultként ahelyett, hogy kivételt dobnál, és a modell általában kijavítja magát. Itt a szám.

Egy hiba, három policy. A scriptelt modell olyan fájlt tippel, amely nem létezik; a tool no such file: timeout.log. Call list_files to see what exists. kivételt dob

what the harness does with the errorturnstool runscostwhat the user got
throws it out of the loop11$0.000756a stack trace
returns Error: the tool failed.21$0.001462„Nem tudtam elolvasni a fájlt, ezért nem tudom.”
returns what actually happened43$0.003550„az errors.log timeoutot említ.”

A harmadik sor 4,7-szer annyiba kerül, mint az első, és ez az egyetlen, amely megválaszolja a kérdést. A második sor az érdekes, mert a legtöbb codebase valójában ezt csinálja: a hibát elkapta, a loop túlélte, a modell megtudta, hogy valami elromlott, de azt nem, mi, és udvariasan feladta. A második és harmadik sor közötti különbség nem error handling. Hanem egy mondat, amelyet egy olvasónak írtak.

A harness ezért adatként kezeli a kidobott toolt, és a megfogalmazást policyvé teszi:

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

A 18. fejezet a másik oldalra is figyelmeztetett, és annak is ára van. Irányítsd a loopot egy olyan toolra, amely olyan okból hibázik, amelyet semmilyen üzenet nem tud kijavítani — egy olvasásra, amelyet a process nem végezhet el —, és a modell örökké újrapróbálja:

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

Tizenegy azonos végrehajtása egy hívásnak, amely nem sikerülhet, 5,2-szeres költség a javítható hibából felépülő futtatáshoz képest, és a végén semmi. A hibák kontextusok; a permanens hiba olyan kontextus, amely megmérgezi a futtatás többi részét. A különbség a 14. fejezet státusz-triázsa egy réteggel feljebb: az a hiba, amelyre a modell cselekedhet, visszamegy az átiratba, amelyre nem, annak okkal meg kell állítania a futtatást. Ma a turn cap áll közted és a második eset között; ez padló, nem javítás.

Negyedik törés: ugyanaz a hívás, kétszer

Link a szakaszhoz: Negyedik törés: ugyanaz a hívás, kétszer

Most jön az a hiba, amelyről a legtöbben azt hiszik, nem történhet meg. A modellek ismétlik magukat. Kérj meg bármelyik loopot, hogy elég sokáig fusson, és látni fogod ugyanazt a toolt ugyanazokkal az argumentumokkal két egymást követő turnben.

Az ismétlés nélküli ugyanazon feladat baseline-jához mérve:

turnstool runscost
the task, no repeat21$0.001396
the same task, one call repeated32$0.002446
repeated, with a result cache on read-only tools31$0.002446

A duplikált hívás $0.001050 extra költség volt, 75%-os növekedés, és itt jön az a rész, amely meglepi az embereket: az eredmény cache-elése semmit nem hozott vissza ebből. A deduplikáció megspórolta a tool-végrehajtást, de nem a turnt, mert mire a kódod észreveszi az ismétlést, a modellért már fizettél, amiért kérte. A megtakarítás valós, ha a tool lassú, rate-limited vagy hívásonként számlázott — és nulla azon a költségsoron, amely megnőtt.

Van rosszabb verzió. Alkalmazd ugyanazt a cache-t egy író toolra, és a második hívás csendben nem történik meg:

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

Melyik a helyes? Tudhatóan egyik sem. A protokoll szerint ez két hívás: két különböző tool_call_id értéket hordoznak. Az argumentumok szerint lehet, hogy egy. Egy harness, amely argumentumsztringek összehasonlításával dönt, egy nap el fogja nyelni két azonos, szándékolt terhelés közül a másodikat — a 14. fejezet pedig már megnevezte az egyetlen mechanizmust, amely ezt tisztességesen feloldja: az idempotenciakulcsot, amelyet logikai műveletenként az a réteg generál, amely tudja, mi a művelet. Amíg a tool nem hordoz ilyet, a védhető alapértelmezés a fenti read-only kapu: cache-eld az olvasásokat, hajtsd végre az írásokat, a maradékot pedig bízd az írás saját idempotenciájára.

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

A destructive script listázza a fájlokat, majd egy olyat kér törölni, amelyet a feladat sosem említett. Az eddigi loopban semmi sem állítaná meg.

Egy needsApproval jelölésű tool nem hibázik és nem is halad tovább. Megállítja a futtatást és visszaadja az irányítást, mindennel együtt, amire egy embernek szüksége van a döntéshez:

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

Ez az egész mechanizmus, és azért return, nem callback, mert a következő szakasz erről szól: a megállás és az ítélet között lehet, hogy a process már nem is létezik.

De előbb a mérés, amelyre senki sem számít. Az elutasítás nem eredmény hiánya — az átiratban van egy tool_call_id kulcsú slot, és valaminek bele kell kerülnie. Futtasd ugyanazt az elutasítást kétszer, csak azt változtatva, hogy ez a valami mit mond:

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

Egyik futtatásban sem törlődött semmi, a másodikban mégis azt mondják a usernek, hogy törlődött. A jogosultsági rendszer tökéletesen működött; a riport hazugság. Ugyanaz a mechanizmus, mint a tool-hiba táblában, csak sokkal fontosabb helyre érkezve — egy ember nemet mondott, a művelet helyesen blokkolva lett, az agent összefoglalója pedig ellentmond a valóságnak, mert az elutasítás sosem lett leírva oda, ahol a modell olvas. Az ebből következő szabály rövid: bármit is dönt a kódod egy tool callról, írd be a döntést az átiratba, szavakkal. A 30. fejezet a biztonsági oldalról tér vissza ehhez, ahol ez a különbség audit trail és fikció között.

Egy approval percekig vagy órákig tart. Egy deploy másodpercekig. Ha a futtatás egy HTTP requesten belüli lokális változóban él, minden restart elveszett futtatás, és minden approval versenyhelyzet.

Ezért a futtatás nem closure. Hanem egy egyszerűen szerializálható objektum — üzenetek, turnszám, költség, státusz, megszakítás, a jóváhagyott call id-k listája —, a loop pedig tiszta függvény rajta. Ez az egyetlen megkötés teszi a perzisztenciát egysoros üggyé:

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 helyességi kérdés nem a mentés. Hanem az, mi történik visszatéréskor, és a naiv válasz duplán terhel. Ha a process azután halt meg, hogy a modell toolt kért, de az eredmény még nem lett beírva, egy olyan resume, amely azzal kezdi, hogy újra meghívja a modellt, fizet egy turnért, amely már megvan — ha pedig azzal kezdi, hogy újrafuttatja a toolokat, kétszer végez el egy írást.

A javítás az, hogy a loop azzal kezdődjön: megkérdezi az átiratot, mi van még függőben:

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

Minden iteráció először kiüríti a pending listát, és csak akkor kérdezi a modellt, amikor nincs semmi függőben. A resume ugyanaz a kódút lesz, mint a normál futás, és ugyanígy az approval is — egy jóváhagyott call egyszerűen egy pending call, amelyet most már szabad futtatni. Öld meg a processt feladat közben, majd indítsd újra:

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

Két tool-végrehajtás két processen át egy olyan feladathoz, amelynek kettő kell, a végső költség pedig azonos azzal a futtatással, amely sosem omlott össze. A költség azért gyűlik tovább a restarton át, mert az állapotban volt, nem egy változóban.

A scan_archive itt három másodpercig tart, és azt a toolt helyettesíti, amely productionben három percig fut. Két dolog hiányzik, amíg fut: a usernek fogalma sincs, hogy bármi történik, és a Stop gomb semmit sem csinál.

Mindkettőnek ugyanaz a javítása, és ez a 14. fejezet AbortSignal eleme egy szinttel mélyebbre tolva. A signal nem csak a fetchnek szól — bekerül a toolba is, és egy jól megírt tool tiszteletben tartja:

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"

Két ezredmásodperc a kattintástól a megállásig, mert a tool belsejében lévő sleep ugyanarra a signalra figyel, mint a fetch. Fűzd csak a fetch hívásba, és ugyanaz a Stop gomb három másodpercet vár — a tool hosszát —, a futtatás pedig azután „szakad meg”, hogy a munka, amelyet megszakított, már befejeződött. A cancellation, amely nincs végig levezetve, csak egy spinner, amely a megfelelő szót írja ki.

A harness eseményenként egy sort bocsát ki, és a szókészlet elég kicsi ahhoz, hogy megjegyezd: 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}

Három tulajdonság teszi ezt trace-szé, nem logginggá. Minden sor hordozza a run id-t, így egy három processen és két napon átívelő futtatás egyetlen query. Minden turn sor hordozza a saját token számait és a futó költséget, így a „miért került ez a futtatás negyven dollárba” utólag megválaszolható, nem csak elméletben reprodukálható. A run_stopped pedig hordozza az okot, vagyis azt a mezőt, amely egy support ticketből egysoros választ csinál: egy budgetnél megállt agent és egy összeomlott agent kívülről ugyanúgy néz ki, de ellentétes reakciót igényel.

A 13. fejezet az általad birtokolt hardveren mérte a time to first tokent. A 14. fejezet socketen keresztül mérte. Egy agent megszorozza, és a szorzó olyan szám, amelyet senki sem választott:

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

Ugyanaz a három turnös feladat, csak a provider latency változik:

provider latency per turnwall clock, 3 turns
0 ms15 ms
200 ms615 ms
800 ms2,413 ms

Maga a harness tizenöt ezredmásodpercet ad hozzá egy három turnös futtatáshoz. Minden más NN szorozva egy olyan számmal, amelyet nem te kontrollálsz — egy serving scheduler belsejében állítják be, amely a kérésedet idegenek kéréseivel batch-eli6 —, a NN-et pedig a modell választja. Ezért számít itt jobban a 14. fejezet streamingje, mint egy chatben, és ezért segít kevesebbet: streamelheted az utolsó turnt, az előtte lévő négy turn pedig csend, hacsak a harness nem bocsát ki progresszt. Ez a teljes érv a fenti tool_progress event mellett is — egy agentben a visszajelzés őszinte egysége nem a token, hanem a lépés.

Ugyanez a harness, valódi modellel a port mögött

Link a szakaszhoz: Ugyanez a harness, valódi modellel a port mögött

Fent minden scriptelt provider ellen futott, ami bizonyítja a harnesst, de semmit sem bizonyít a modellekről. Változtass meg tehát egy sort — a 14. fejezet seamjét, a LLM_BASE_URL elemet —, és irányítsd ugyanazt a kódot egy lokális Qwen2.5-0.5B-Instruct modellre ugyanazzal a négy toollal. Hat feladat ugyanazon a három fájlon:

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

Három megállapítás, és a harmadik miatt létezik ez a szakasz.

Minden egyes feladat pontosan két turnben fejeződött be. A turn cap sosem sült el, a budget sosem sült el, és a loop egyetlen kijárata az volt, hogy a modell prózát adott. Egy félmilliárd paraméteres modell nem iterál; a második levegővételére válaszol, függetlenül attól, megvan-e neki, amire szüksége van. A turnszám a modell tulajdonsága, nem a loopodé.

Az átlagos turn 6,908 ezredmásodpercig tartott, tehát a fenti latency táblázat nem játék: ekkora méretnél egy hipotetikus nyolc turnös futtatás majdnem egy perc wall clock, semmivel a képernyőn.

És a válaszok rosszak. A legnagyobb fájl errors.log; a modell listázta a fájlokat, sosem olvasta el őket, mégis megnevezett egyet. Az első feladat tippelt egy fájlnevet, megtudta, hogy nem létezik, majd levonta a következtetést. A harness mind a hat futtatásban hibátlanul végrehajtott. A harness az agentet irányíthatóvá teszi, nem helyessé — a 29. fejezet szól arról, hogyan deríted ki, melyikről van szó, a 30. fejezet pedig arról, mennyibe kerül, ha senki sem tette meg.

Subagentek, itt megnevezve, később kiszámlázva

Link a szakaszhoz: Subagentek, itt megnevezve, később kiszámlázva

A katalógus egyik toolja mögött egy másik futtatás állhat. Az interface a 18. fejezeté — egy schema és egy endpoint —, és egy teljes agent befér mögé, mert ez az interface szűk:

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

Három dolog már ebben a tíz sorban is helyes, és mindhárom a fent meghozott döntések következménye: a childnak saját window-ja van, így a parent átirata összefoglalót kap, nem mindent, amit a child olvasott; saját limitjei vannak, így egy runaway child nem költheti el a parent budgetjét; és örökli a signalt, így egy Stop leállítja a fát. Hogy miért a tiszta window a lényeg, nem mellékhatás, az a 24. fejezet; az öt orchestration pattern — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — és a handoff pedig a 25. fejezet.

Hol vannak a frameworkök, és ez a kurzus miért nem használt egyet sem

Link a szakaszhoz: Hol vannak a frameworkök, és ez a kurzus miért nem használt egyet sem

A fentiek közül semmit sem szabad libraryk elleni érvként olvasni. 2026. szeptember 7-én mérve, az augusztus 29-ével záruló hónapra:7

packagedownloads that monthwhat it gives you
ai (Vercel AI SDK)89,385,860ToolLoopAgent, stopWhen, tool approval, step hooks
@anthropic-ai/claude-agent-sdk41,558,352a Claude Code harness libraryként: loop, sessionök, hookok, jogosultságok, subagentek8
@langchain/langgraph12,812,815a loop explicit állapotgráfként
langchain11,359,058chains, agentek, integrációk
@openai/agents6,093,155agentek, handoffok, guardrailek
@mastra/core5,914,502agentek, workflow-k, memória

Az ok, amiért ez a kurzus kézzel írja meg a loopot, nem pedig ezek egyikét tanítja, kimondott, nem sugallt, és mérhető. A 2026. szeptember 7-ig tartó tizenkét hónapban a ai 945 verziót publikált, és major 5-ről major 7-re lépett, az agent class pedig még mindig Experimental_Agent néven exportálódik; a langchain ugyanebben az időablakban 132 verziót publikált; a @openai/agents 83-at publikált, és tizenöt hónappal az első release után még mindig 0.x-en áll.7 Egy olyan fejezet, amely ezek bármelyikének API-jára épül, egy évszakon belül elavul, ez pedig harminchárom nyelven jelenik meg, így minden újrakiadás az egész fordításba kerül. Ami mindegyik alatt van, nem mozog: egy loop, egy stopping rule, egy katalógus, egy executor, némi állapot.

A referencia-implementáció pedig egyetért ezzel a fejezettel abban a részben, amely számít. A ai 7.0.93-as verziójában a loop kijárata nem szám — hanem stopWhen, predikátumok listája, amelyek közül a lépésszám csak az egyik: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

A megállás többes számú ennek a loopnak a legtöbbet használt implementációjában, ugyanazért, amiért a fenti százkilencvenhat sorban is többes számú.

Most már van harnessed: egy loop, egy katalógus, egy executor, öt kiút, perzisztált futtatás, egy signal, amely eléri a toolokat, és egy trace, amelynek minden sorában run id van. A 24., 25., 29. és 30. fejezet erre a fájlra épít, a 26–28. pedig arra, amit elér.

Egyetlen problémája maradt, és a fenti mérések végig erre mutattak. Nézd meg újra a runaway táblát: 3,431 input token nyolc turnnél, 337,299 száznál. Nézd meg a működő futtatást: 204, 269, 342. Minden turn újraküldi a teljes átiratot, így egy agent contextje a saját történetével telik meg — a modell pedig rosszabbul használja egy hosszú window távoli végét, mint a közeli végét, ezért a turn ötben jó agent turn negyvenben zavarodott agent lesz.

A turn cap ezt nem javítja. Csak megállítja, hogy fizess azért, hogy végignézd. Az javítja, ha minden egyes turnben eldöntöd, mely tokenek érdemlik meg a window-t: mit tömörítesz, mit mozgatsz ki egy jegyzetbe, amelyet az agent lekérhet, mit adsz át egy subagentnek tiszta window-val, és mely tool definíciók érik meg az állandó adójukat. A 24. fejezet megméri, valójában hová megy a window — és a meglepetés az, hogy nem a beszélgetésbe.


Ebben a fejezetben minden szám a fent leírt két szerverből származott, Node 22 alatt, loopback interface-en: egy scriptelt provider, amely a o200k_base encodinggal számolja a tokeneket, és Qwen/Qwen2.5-0.5B-Instruct ugyanilyen alakú endpoint mögött, greedy decodinggal, CPU-n. A költségeket mért token counts alapján számoltuk azokkal a díjakkal, amelyeket a 16. fejezet 2026. szeptember 6-án olvasott — $2.00 egymillió input tokenenként és $12.00 egymillió output tokenenként —, és ebben a fejezetben egyetlen request sem ment fizetős endpointnak. A lokális modell válaszai egy kis modell válaszai; a loopról szóló bizonyítékként olvasd őket, amely mindkét esetben azonos, ne pedig benchmarkként arról, mire képesek a jelenlegi modellek.

  1. Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. és Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). A reasoning trace-ek és actionök váltakozása, amelyet a loop implementál, valamint annak a megfigyelésnek a forrása, hogy a cselekvés lehetővé teszi a modellnek az „exceptions” kezelését — pontosan ezt méri a fenti tool-hiba tábla.

  2. Sumers, T. R., Yao, S., Narasimhan, K. és Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Annak formális tárgyalása, amit a fenti loop informálisan végez: moduláris memóriakomponensek, strukturált cselekvési tér belső memória és külső környezetek között, valamint „a generalized decision-making process to choose actions”. Olvasd a szókészletért, amely az iparági kifejezésből hiányzik — különösen a working, episodic, semantic és procedural memory szétválasztásáért, amelynek gyakorlati árnyéka a 24. fejezet háromtárolós táblája.

  3. ai (Vercel AI SDK) 7.0.93-as verzió, publikálva 2026. szeptember 4-én; a típusdeklarációkat a cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts fájlból olvastuk 2026. szeptember 7-én. A 397 KB-os fájl nulla előfordulását tartalmazza a harness sztringnek. Az agent class declare class ToolLoopAgent, exportálva ToolLoopAgent és Experimental_Agent néven is; a declare function isStepCount(stepCount: number)stepCountIs néven exportálva — fent szó szerint idézve; a type StopCondition a második típusparamétere (RUNTIME_CONTEXT extends Context = Context) nélkül szerepel, ez az egyetlen kihagyás a részletben, ahogy a stopWhen?: Arrayable<StopCondition<...>> alakja is a generateText és streamText elemen. Ugyanez a fájl deklarálja a toolApproval, ToolApprovalStatus, prepareStep és repairToolCall elemeket is, vagyis a referencia-implementáció önállóan is eljutott az approval kapukhoz, a lépésenkénti előkészítéshez és a hibajavításhoz. 2

  4. Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. és Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Az absztrakt az artefaktumot 2,294 problémából álló „evaluation frameworkként” nevezi meg, és sosem használja a „harness” szót; a projekt saját README-je (github.com/SWE-bench/SWE-bench, olvasva 2026. szeptember 7-én) ötször használja, mindig „evaluation harness” értelemben, a belépési pont pedig python -m swebench.harness.run_evaluation. Ez a szó másik jelentése: egy váz, amely mozdulatlanul tartja az agentet és pontozza, nem az a loop, amely futtatja.

  5. Anthropic, Building effective agents, 2024. december 19., anthropic.com/engineering/building-effective-agents, olvasva 2026. szeptember 7-én. Az augmented model mint építőelem, az agent mint olyan LLM, amely „using tools based on environmental feedback in a loop”, valamint a kontroll fenntartására ajánlott stopping conditionök „such as a maximum number of iterations”. A 22. fejezet teljes egészében idézi a definícióját.

  6. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. és Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). A másik loop — az a serving scheduler, amely a kérésedet idegenek kéréseivel batch-eli, és kezeli a 13. fejezet KV cache-ét. Pont azért érdemes tudni a létezéséről, mert nem a tiéd: a latency, amelyet a harnessed megszoroz, benne van beállítva, és a loopodon végzett bármennyi munka sem mozdítja el.

  7. npm registry letöltésszámok, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, explicit időablakkal, nem a gördülő last-month ablakkal, valamint release historyk a registry.npmjs.org/<package> alapján; mindkettő lekérdezve 2026. szeptember 7-én. A release-számok az adott dátumig tartó tizenkét hónapban publikált verziók számai, canary buildekkel együtt: ai 945 (legfrissebb 7.0.93, 2026-09-04, major 5, 6 és 7 verziók is megjelentek az ablakon belül), langchain 132 (legfrissebb 1.5.10, 2026-08-20), @openai/agents 83 (legfrissebb 0.17.0, 2026-08-19, első publikálás 2025-06-03). 2

  8. A Claude Agent SDK (@anthropic-ai/claude-agent-sdk) a Claude Code harness libraryként csomagolva — agent loop, beépített file és shell toolok, context management, sessionök, hookok, jogosultságok és subagentek — dokumentálva itt: code.claude.com/docs/en/agent-sdk. Ez áll a legközelebb egy publikált beszámolóhoz mindazokról a mechanizmusokról, amelyeket ez a fejezet kézzel épít fel, és érdemes a saját implementációd mellett olvasni azokért a részekért, amelyeket megnevez, ez a fejezet pedig csak jelez.


Készítette

David Vicente Campos

A NeuraLIA Labs alapítója és a MyRealFood társalapítója

Mérnökinformatikus vagyok, a Leóni Egyetemen végeztem. Társalapítottam a MyRealFoodot, ahol CTO-ként felépítettem azt az alkalmazást, amelyet emberek milliói használtak arra, hogy egészségesebben táplálkozzanak, és megalapítottam a NeuraLIA Labst, ahol AI-termékeket fejlesztek. Itt arról írok, amit menet közben meg kellett értenem, úgy, ahogy szerettem volna, hogy valaki elmagyarázza nekem.

Továbbiak a szerzőről

Közzétette a NeuraLIA Labs.

Kapj új bejegyzéseket a postaládádba

AI-hírek, útmutatók és termékfrissítések — rövid email, amikor valami igazán hasznosat publikálunk.

Kurzusindex

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev11 perc olvasás

A Jev AI-modell döntésekre készült, nem prózára

A TypeSafe AI Jev modellje azért kap figyelmet, mert a szoftveres intelligenciát valószínűségi problémaként kezeli: válaszd ki a megfelelő ágat, rendelj hozzá bizalmi szintet, és ne fizess egy LLM-nek szövegírásért, amikor a kódnak döntésre van szüksége.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering11 perc olvasás

Kontextustervezés hosszú távú AI-ügynökökhöz

A hosszú ideig futó ügynökök nem csak azért vallanak kudarcot, mert kicsi az ablak. Akkor hibáznak, amikor a fájlok, eszközkimenetek és elavult előzmények kiszorítják azt a feladatot, amelyet az ügynöknek be kellett volna fejeznie.

Készen állsz, hogy a LIA válasszon helyetted?

Építs az összes AI-modellel egy helyen – kezdd el ma, ingyen.