Към съдържанието
23/30Глава 23 от 30

Изградете agent harness: цикълът и петте изхода от него

Цикъл от 15 реда, който тръгва от първия път, после нарочно се чупи 7 пъти — започвайки с runaway 77 пъти по-скъп от лимитиран.

На тази страница

Да започнем с честната част, защото никой друг няма да го каже: „harness“ е жаргон, не стандарт. Няма спецификация, няма комитет, няма референтна дефиниция. Четирите статии, които тази глава цитира — ReAct,1 CoALA,2 SWE-bench и vLLM — не използват думата нито веднъж в резюметата си. Най-теглената имплементация на това нещо, пакетът ai на Vercel с 89,4 милиона изтегляния месечно, също не я използва: низът harness се появява нула пъти в 397 KB декларации за типове, доставяни с версия 7.0.93.3 Единственото място, където думата наистина носи смисъл, означава нещо съвсем друго. SWE-bench казва „harness“ пет пъти в своя README, винаги като evaluation harness — containerised scaffold, който прилага patch и пуска тестовете — а Python модулът му е буквално swebench.harness.run_evaluation.4

Така две различни неща споделят едно име. Evaluation harness държи agent неподвижен и го оценява. Agent harness е програмата, която изпълнява agent: тя извиква model, изпълнява това, което model поиска, решава кога да спре и пази state междувременно. Тази глава изгражда второто — под двеста реда TypeScript, без никакъв framework.

Самият цикъл е петнадесет реда и работи от първия опит. Всичко след това е начин да излезете от него.

Покажи подробности

Какво е нужно на тази глава от предишните.

  • Глава 14 за клиента: deadlines, status triage, cancellation, idempotency keys и техниката с mock provider, използвана отново тук.
  • Глава 16 за аритметиката: input tokens растат с квадрата на разговора, а използваните по-долу тарифи са прочетените там на 6 септември 2026 г.
  • Глава 18 за каталога с инструменти: schema, която model вижда, endpoint, който никога не вижда, и правилото, че грешките са контекст, а не exceptions.
  • Глава 22 за цикъла, който тази глава наследява, и за двете публикувани дефиниции на „agent“, които не са съгласни една с друга.

Тук няма tensors. Това е вторият dependency hub на курса: глави 24, 25, 29 и 30 стъпват върху файла по-долу, а 26 до 28 градят върху това, до което той може да стигне.

Глава 14 не можеше да бъде написана срещу реален provider, защото не можете да поискате от него 429 в избран момент. Тази глава има същия проблем в друга форма: не можете да поискате от реален model да се отплесне безкрайно или да поиска един и същ инструмент два пъти поред, по заявка и възпроизводимо.

Затова първата програма е scripted provider: endpoint с формата на chat completions API, чийто отговор е функция от индекса на хода и от това какво са върнали инструментите досега. Той брои tokens с реален byte-pair encoder, така че парите по-долу са аритметика, не декорация.

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

Два реда носят дизайна. Индексът на хода е изведен от разговора, не се държи в променлива, така че provider е stateless и изпълнение може да бъде убито и подновено срещу него. А recover чете резултатите от инструментите, преди да реши: scripted model, който чете собствения си transcript, е минимумът, нужен за измерване дали harness му е дал нещо, което си струва да прочете.

Каталогът е този от глава 18, четири инструмента в три файла: list_files, read_file, delete_file — маркиран needsApproval — и scan_archive, който нарочно е бавен.

Ето цялата идея, преди всяка от частите, които я правят оцеляваща.

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

Насочете го към scripted provider и той прави точно това, което изглежда, че прави:

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

Три хода, две изпълнения на инструменти, четвърт от един щатски цент. Обърнете внимание на последния ред: 204, 269, 342. Всеки ход изпраща отново всичко преди него — квадратичната сметка от глава 16 пристига на място, където никой не е написал нищо. Останалата част от тази глава е какво се случва, когато този ред не спира да расте.

Счупване едно: задачата, която никога не свършва

Връзка към раздела: Счупване едно: задачата, която никога не свършва

Насочете същия цикъл към скрипта runaway — model, който иска инструмент на всеки ход и никога не излъчва проза — и маркираният return никога не се задейства. Няма друг изход. Програмата работи, докато процесът умре или кредитната карта не издържи.

Поправката е един ред, тя е първият контрол, който литературата препоръчва,5 и всички в крайна сметка го пишат. Това, което почти никой не прави, е да измери колко струва:

лимит на ходоветеизвиквания на modelinput tokensцена
883.431$0.009070
202016.259$0.038038
505088.649$0.191098
100100337.299$0.702198

Прочетете последните два реда заедно. Удвояването на лимита от 50 на 100 не удвои цената; умножи я по 3,7. Input tokens се качиха от 88.649 на 337.299, фактор 3,8, защото ход nn носи всеки предишен ход със себе си, а общото е Θ(n2)\Theta(n^2). Лимитът на ходовете не е линеен регулатор. Той е регулатор върху квадратния корен на най-лошия ви случай, затова вдигането му от 20 на 100 „за всеки случай“ е решение, което си струва да остойностите, преди да го вземете.

Счупване две: лимит на ходовете не е лимит на парите

Връзка към раздела: Счупване две: лимит на ходовете не е лимит на парите

Проблемът с лимита на ходовете е, че един ход няма фиксирана цена. Двадесет хода върху кратък transcript струваха $0.038 по-горе. Двадесет хода с каталог от 200 инструмента, набор извлечени документи и четиридесет съобщения история струват стотици пъти повече, а лимитът не знае това. Това, което operator иска да ограничи, е сметката.

Затова цикълът брои пари, използвайки computeCost от глава 16 срещу прочетените там тарифи — $2.00 на милион input tokens и $12.00 на милион output за model, чиято цена използваме в целия този курс:

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

Същият runaway скрипт, без никакъв лимит на ходовете, три бюджета:

бюджетдостигнати ходовереално похарчено
$0.019$0.010780
$0.0524$0.051790
$0.2052$0.205398

Две неща си струва да бъдат назовани. Първо, бюджетът купува различен брой ходове всеки път, което е целта: той ограничава нещото, за което operator се интересува, и оставя броя ходове да падне там, където transcript го постави. Второ, всеки ред надхвърля бюджета. Бюджетът беше $0.010, а бяха похарчени $0.010780, защото проверката се изпълнява преди ход, а цената на ход не е известна, докато не приключи. Не можете да ограничите разхода точно; можете да го ограничите до цената на един ход. Кажете го в интерфейса, вместо да се преструвате, и поставете проверката преди извикването, така че надхвърлянето да е един ход, а не два.

Пет начина да излезете от цикъла, не един

Връзка към раздела: Пет начина да излезете от цикъла, не един

Дотук цикълът има три изхода и формата на останалата глава вече се вижда. Production изпълнение приключва по точно един от пет начина и те не са вариации един на друг:

как приключвакой решикакво трябва да направи caller
model спря да питаmodelпрочетете отговора
лимит на ходоветевие, предварителновдигнете лимита или приемете частичен резултат
изчерпан бюджетвие, предварителноодобрете повече пари или приемете частичен резултат
грешка, която не можете да retryprovider или инструментпоправете deployment; triage от глава 14 решава
намеси се човекчовекизчакайте verdict, после подновете

Свиването на тези случаи до един boolean е най-честата дизайнерска грешка в този файл и е скъпа по конкретен начин: три от петте са resumable, а два не са. Agent, който е ударил лимита на ходовете си, има валиден transcript, реален частичен резултат и следваща стъпка; agent, който е получил 401, няма нищо от това. Затова harness записва причината като данни:

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

Глава 18 завърши с твърдение без число: върнете грешката на инструмента на model като резултат от инструмент, вместо да я хвърляте, и model обикновено се поправя сам. Ето числото.

Един failure, три политики. Scripted model познава файл, който не съществува; инструментът хвърля no such file: timeout.log. Call list_files to see what exists.

какво прави harness с грешкатаходовеизпълнения на инструментценакакво получи потребителят
хвърля я извън цикъла11$0.000756stack trace
връща Error: the tool failed.21$0.001462„Не успях да прочета файла, затова не знам.“
връща какво реално се случи43$0.003550„errors.log споменава timeout.“

Третият ред струва 4,7 пъти повече от първия и е единственият, който отговаря на въпроса. А вторият ред е интересният, защото това е, което повечето codebases всъщност правят: грешката е хваната, цикълът оцелява, model е уведомен, че нещо се е провалило, но не какво, и се отказва учтиво. Разликата между втори и трети ред не е error handling. Тя е изречение, написано за читател.

Затова harness третира хвърлен инструмент като данни и прави формулировката политика:

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

Глава 18 предупреди и за другата страна, която също има цена. Насочете цикъла към инструмент, който се проваля по причина, която никое съобщение не може да поправи — read, който процесът няма право да извърши — и model го retry завинаги:

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

Единадесет идентични изпълнения на call, който не може да успее, 5,2 пъти цената на изпълнението, възстановило се от поправима грешка, и нищо накрая. Грешките са контекст; постоянната грешка е контекст, който отравя остатъка от изпълнението. Разграничението е status triage от глава 14, преместен един слой нагоре: грешка, върху която model може да действа, се връща в transcript, а грешка, върху която не може, трябва да спре изпълнението с причина. Днес лимитът на ходовете е това, което стои между вас и втория случай, което е под, а не поправка.

Сега идва провалът, който повечето хора предполагат, че не може да се случи. Models се повтарят. Оставете който и да е цикъл да работи достатъчно дълго и ще видите идентичния инструмент с идентичните аргументи в два последователни хода.

Измерено спрямо baseline на същата задача без повторението:

ходовеизпълнения на инструментцена
задачата, без повторение21$0.001396
същата задача, един call повторен32$0.002446
повторена, с result cache върху read-only инструменти31$0.002446

Дублираният call струва $0.001050 допълнително, увеличение от 75 %, а ето и частта, която изненадва хората: кеширането на резултата не възстанови нищо от това. Deduplication спести изпълнението на инструмента, но не и хода, защото когато кодът ви забележи повторението, model вече е бил платен, за да го поиска. Спестяването е реално, когато инструментът е бавен, rate-limited или таксуван на call — и е нула по реда от сметката, който нарасна.

Има по-лоша версия. Приложете същия cache към инструмент, който пише, и вторият call тихо не се случва:

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

Кое от тези е правилно? Нито едно, доколкото може да се знае. Протоколът казва, че това са два call-а: те носят две различни стойности tool_call_id. Аргументите казват, че може да са един. Harness, който решава чрез сравняване на низове с аргументи, един ден ще погълне второто от две идентични, умишлени таксувания — а глава 14 вече назова единствения механизъм, който решава това честно: idempotency key, генериран за логическа операция от слоя, който знае какво е операцията. Докато инструментът не носи такъв, защитимият default е read-only gate по-горе: кеширайте reads, изпълнявайте writes и оставете собствената idempotency на write да се погрижи за останалото.

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

Скриптът destructive изброява файловете и после иска да изтрие един, който задачата никога не е споменавала. Нищо в цикъла досега не би го спряло.

Инструмент, маркиран needsApproval, не се проваля и не продължава. Той спира изпълнението и връща контрола, с всичко, от което човек има нужда, за да реши:

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

Това е целият механизъм, а причината да е return, а не callback, е следващият раздел: между спирането и verdict процесът може вече да не съществува.

Но първо — измерването, което никой не очаква. Отхвърлянето не е липса на резултат — transcript има slot, ключиран по tool_call_id, и нещо трябва да влезе в него. Пуснете същото отхвърляне два пъти, като промените само какво казва това нещо:

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

Нищо не беше изтрито и в двете изпълнения, а във второто на потребителя му се казва, че е било. Permission system работи перфектно; отчетът е лъжа. Това е същият механизъм като таблицата с грешки от инструменти, пристигащ на място, което има много по-голямо значение — човек каза „не“, действието беше правилно блокирано, а summary на agent противоречи на реалността, защото отказът никога не беше записан там, където model чете. Правилото, което следва, е кратко: каквото и да реши кодът ви за tool call, запишете решението в transcript с думи. Глава 30 се връща към това от страната на сигурността, където това е разликата между audit trail и измислица.

Approval отнема минути или часове. Deploy отнема секунди. Ако изпълнението живее в локална променлива вътре в HTTP request, всеки restart е изгубено изпълнение и всеки approval е race.

Затова изпълнението не е closure. То е обикновен serialisable object — messages, брой ходове, цена, status, interruption, списъкът с approved call ids — а цикълът е pure function върху него. Това единствено ограничение прави persistence едноредов проблем:

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

Въпросът за correctness не е запазването. Той е какво се случва по пътя обратно, а наивният отговор ви таксува два пъти. Ако процесът е умрял, след като model е поискал инструмент, но преди резултатът да бъде записан, resume, който започва с ново извикване на model, плаща за ход, който вече има — а ако започне с повторно пускане на инструментите, извършва write два пъти.

Поправката е цикълът да започва, като пита transcript какво е outstanding:

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

Всяка итерация първо източва pending и пита model само когато няма нищо outstanding. Resume става същият code path като нормалния, както и approval — approved call е просто pending call, който вече е позволено да се изпълни. Убийте процеса по средата на задачата и го рестартирайте:

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

Две изпълнения на инструменти през два процеса за задача, която има нужда от две, а крайната цена е идентична с изпълнението, което никога не се е crash-нало. Цената се натрупва през restart, защото беше в state, не в променлива.

scan_archive отнема три секунди тук и представя инструмент, който отнема три минути в production. Две неща липсват, докато работи: потребителят няма представа, че нещо се случва, а бутонът Stop не прави нищо.

И двете имат една и съща поправка — AbortSignal от глава 14, избутан един слой по-надолу. Signal не е само за fetch — той се подава в инструмента, а добре написан инструмент го зачита:

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"

Две милисекунди от клика до спирането, защото sleep вътре в инструмента слуша същия signal като fetch. Прекарайте го само в fetch и идентичният бутон Stop чака три секунди — дължината на инструмента — а изпълнението „се отменя“, след като работата, която е отменяло, вече е приключила. Cancellation, която не е прокарана докрай надолу, е spinner, който казва правилната дума.

Harness излъчва по един ред на event, а речникът е достатъчно малък, за да се запомни: 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}

Три свойства правят това trace, а не logging. Всеки ред носи run id, така че изпълнение, което обхваща три процеса и два дни, е една query. Всеки ред turn носи собствените си token counts и текущата цена, така че „защо това изпълнение струваше четиридесет долара“ има отговор след факта, вместо да е възпроизводимо само на теория. А run_stopped носи причината, което е полето, превръщащо support ticket в едноредов отговор: agent, спрял на бюджета, и agent, който е crash-нал, изглеждат идентични отвън и изискват противоположни реакции.

Глава 13 измери time to first token върху hardware, който притежавате. Глава 14 го измери през socket. Agent го умножава, а множителят е число, което никой не е избрал:

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

Същата задача от три хода, като се променя само latency на provider:

latency на provider за ходwall clock, 3 хода
0 ms15 ms
200 ms615 ms
800 ms2.413 ms

Самият harness допринася петнадесет милисекунди към изпълнение от три хода. Всичко останало е NN, умножено по число, което не контролирате — зададено вътре в serving scheduler, който batch-ва вашия request с requests на непознати6 — а NN се избира от model. Затова streaming от глава 14 има по-голямо значение тук, отколкото в chat, и помага по-малко: можете да stream-вате последния ход, а четирите хода преди него са тишина, освен ако harness не излъчва progress. Това е и целият аргумент за event tool_progress по-горе — в agent честната единица feedback не е token, а стъпката.

Всичко по-горе работеше срещу scripted provider, което доказва harness и не доказва нищо за models. Затова сменете един ред — seam от глава 14, LLM_BASE_URL — и насочете идентичния код към локален Qwen2.5-0.5B-Instruct със същите четири инструмента. Шест задачи върху същите три файла:

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

Три открития, като третото е причината този раздел да съществува.

Всяка една задача приключи в точно два хода. Лимитът на ходовете никога не се задейства, бюджетът никога не се задейства и единственият изход на цикъла беше model да произведе проза. Model с половин милиард параметри не iterate-ва; той отговаря на втория си дъх, независимо дали има това, което му трябва. Броят ходове е свойство на model, не на вашия цикъл.

Средният ход отне 6.908 милисекунди, така че таблицата с latency по-горе не е играчка: при този размер хипотетично изпълнение от осем хода е почти минута wall clock без нищо на екрана.

И отговорите са грешни. Най-големият файл е errors.log; model изброи файловете, никога не ги прочете и въпреки това посочи един. Първата задача позна име на файл, беше уведомена, че той не съществува, и приключи. Harness изпълни безупречно във всичките шест run-а. Harness прави agent governable, не правилен — глава 29 е как разбирате кое от двете, а глава 30 е какво струва, когато никой не го е направил.

Един инструмент в каталога може да има друго изпълнение зад себе си. Интерфейсът е този от глава 18 — schema и endpoint — и цял agent се побира зад него, защото интерфейсът е тесен:

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

Три неща вече са правилни в тези десет реда и трите са последствия от решенията по-горе: child има собствен window, така че transcript на parent получава summary, а не всичко, което child е прочел; има собствени limits, така че runaway child не може да похарчи бюджета на parent; и наследява signal, така че един Stop отменя дървото. Защо чистият window е целта, а не side effect, е глава 24; петте orchestration patterns — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — и handoff са глава 25.

Къде са frameworks и защо този курс не използва такъв

Връзка към раздела: Къде са frameworks и защо този курс не използва такъв

Нищо по-горе не трябва да се чете като аргумент срещу libraries. Измерено на 7 септември 2026 г. за месеца, приключващ на 29 август:7

packageизтегляния през този месецкакво ви дава
ai (Vercel AI SDK)89.385.860ToolLoopAgent, stopWhen, approval за инструменти, step hooks
@anthropic-ai/claude-agent-sdk41.558.352Claude Code harness като library: цикъл, sessions, hooks, permissions, subagents8
@langchain/langgraph12.812.815цикълът като explicit state graph
langchain11.359.058chains, agents, integrations
@openai/agents6.093.155agents, handoffs, guardrails
@mastra/core5.914.502agents, workflows, memory

Причината този курс да пише цикъла на ръка, вместо да преподава един от тях, е заявена, не подразбрана, и е измерима. За дванадесетте месеца до 7 септември 2026 г. ai публикува 945 версии и се премести от major 5 към major 7, а неговият agent class все още се export-ва като Experimental_Agent; langchain публикува 132 версии в същия прозорец; @openai/agents публикува 83 и все още е на 0.x, петнадесет месеца след първото си release.7 Глава, написана срещу който и да е от тези API, остарява за един сезон, а тази се публикува на тридесет и три езика, така че всяко преиздание струва целия превод. Това, което стои под всички тях, не се движи: цикъл, правило за спиране, каталог, executor, малко state.

И референтната имплементация е съгласна с тази глава за частта, която има значение. В ai версия 7.0.93 изходът на цикъла не е число — той е stopWhen, списък от predicates, от които броят стъпки е само един: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

Спирането е в множествено число в най-използваната имплементация на този цикъл по същата причина, по която е в множествено число и в сто деветдесет и шестте реда по-горе.

Вече имате harness: цикъл, каталог, executor, пет изхода, persisted run, signal, който стига до инструментите, и trace с run id на всеки ред. Глави 24, 25, 29 и 30 градят върху този файл, а 26 до 28 — върху това, до което той може да достигне.

Остава му един проблем, а измерванията по-горе сочеха към него през цялото време. Погледнете runaway таблицата още веднъж: 3.431 input tokens при осем хода, 337.299 при сто. Погледнете работещия run: 204, 269, 342. Всеки ход изпраща отново целия transcript, така че context на agent се пълни със собствената му история — а model използва далечния край на дълъг window по-зле от близкия, затова добър agent на ход пет е объркан на ход четиридесет.

Лимитът на ходовете не поправя това. Той само ви спира да плащате, за да го гледате как се случва. Това, което го поправя, е да решавате на всеки отделен ход кои tokens заслужават window: какво да compact-нете, какво да преместите в note, който agent може да fetch-не, какво да дадете на subagent с чист window и кои дефиниции на инструменти си струват постоянния данък. Глава 24 измерва къде window реално отива — и изненадата е, че не е разговорът.


Всяко число в тази глава излезе от двата сървъра, описани по-горе, на Node 22 през loopback interface: scripted provider, броящ tokens с encoding o200k_base, и Qwen/Qwen2.5-0.5B-Instruct зад endpoint със същата форма, greedy decoding, на CPU. Разходите са изчислени от измерените token counts по тарифите, които глава 16 прочете на 6 септември 2026 г. — $2.00 на милион input tokens и $12.00 на милион output — и нито един request в тази глава не отиде към платен endpoint. Отговорите на локалния model са отговори на малък model; четете ги като evidence за цикъла, който е идентичен и в двата случая, а не като benchmark за това какво правят текущите models.

  1. Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. and Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Преплитането на reasoning traces и actions, което цикълът имплементира, и източникът на наблюдението, че acting позволява на model да „handle exceptions“ — което е точно това, което таблицата с tool-error по-горе измерва.

  2. Sumers, T. R., Yao, S., Narasimhan, K. and Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Формалното третиране на това, което цикълът по-горе прави неформално: modular memory components, structured action space, обхващащ internal memory и external environments, и „a generalized decision-making process to choose actions“. Прочетете го за речника, който липсва на индустриалния термин — особено разделянето на working, episodic, semantic и procedural memory, чиято практическа сянка е таблицата с три store-а в глава 24.

  3. ai (Vercel AI SDK) версия 7.0.93, публикувана на 4 септември 2026 г.; декларациите за типове са прочетени от cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts на 7 септември 2026 г. Файлът от 397 KB съдържа нула срещания на низа harness. Agent class е declare class ToolLoopAgent, export-нат и като ToolLoopAgent, и като Experimental_Agent; declare function isStepCount(stepCount: number) — export-нат като stepCountIs — е цитиран дословно по-горе; type StopCondition е показан без втория си type parameter (RUNTIME_CONTEXT extends Context = Context), което е единственото съкращение в excerpt-а, както и shape на stopWhen?: Arrayable<StopCondition<...>> върху generateText и streamText. Същият файл декларира toolApproval, ToolApprovalStatus, prepareStep и repairToolCall, тоест референтната имплементация независимо е стигнала до approval gates, per-step preparation и error repair. 2

  4. Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. and Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Резюмето нарича артефакта „evaluation framework“ от 2.294 проблема и никога не използва думата „harness“; собственото README на проекта (github.com/SWE-bench/SWE-bench, прочетено на 7 септември 2026 г.) я използва пет пъти, винаги като „evaluation harness“, а entry point е python -m swebench.harness.run_evaluation. Това е другият смисъл на думата: scaffold, който държи agent неподвижен и го оценява, не цикълът, който го изпълнява.

  5. Anthropic, Building effective agents, 19 декември 2024 г., anthropic.com/engineering/building-effective-agents, прочетено на 7 септември 2026 г. Augmented model като building block, agent като LLM, „using tools based on environmental feedback in a loop“, и препоръката за stopping conditions „such as a maximum number of iterations“ за запазване на контрол. Глава 22 цитира дефиницията му изцяло.

  6. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. and Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Другият цикъл — serving scheduler, който batch-ва вашия request с requests на непознати и управлява KV cache от глава 13. Струва си да знаете, че съществува точно защото не е ваш: latency, която вашият harness умножава, се задава вътре в него и никаква работа по вашия цикъл не я премества.

  7. Броеве изтегляния от npm registry, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, explicit window вместо rolling last-month, и release histories от registry.npmjs.org/<package>; и двете query-нати на 7 септември 2026 г. Release counts са броят версии, публикувани за дванадесетте месеца до тази дата, включително canary builds: ai 945 (последна 7.0.93 на 2026-09-04, с major версии 5, 6 и 7, всички появили се в window), langchain 132 (последна 1.5.10 на 2026-08-20), @openai/agents 83 (последна 0.17.0 на 2026-08-19, първо публикуван на 2025-06-03). 2

  8. Claude Agent SDK (@anthropic-ai/claude-agent-sdk) е Claude Code harness, пакетиран като library — agent loop, built-in file и shell tools, context management, sessions, hooks, permissions и subagents — документиран на code.claude.com/docs/en/agent-sdk. Това е най-близкото нещо до публикуван разказ за всеки механизъм, който тази глава изгражда на ръка, и си струва да се чете до собствената ви имплементация за частите, които назовава, а тази глава само маркира.


Създадено от

David Vicente Campos

Основател на NeuraLIA Labs и съосновател на MyRealFood

Компютърен инженер съм, завършил Университета в Леон. Съосновах MyRealFood, където като CTO създадох приложението, което милиони хора са използвали, за да се хранят по-здравословно, и основах NeuraLIA Labs, където изграждам AI продукти. Тук пиша за това, което трябваше да разбера по пътя, така, както ми се иска някой да ми го беше обяснил.

Още за автора

Публикувано от NeuraLIA Labs.

Получавайте нови публикации във входящата си поща

Новини за AI, ръководства и продуктови обновления — кратък имейл, когато публикуваме нещо, което си заслужава.

Индекс на курса

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev12 мин четене

AI моделът Jev е създаден за решения, не за проза

Jev на TypeSafe AI привлича внимание, защото разглежда софтуерната интелигентност като проблем на вероятностите: изберете правилния клон, добавете увереност и не плащайте на LLM да пише текст, когато кодът има нужда от решение.

Abstract legal research workspace with documents, search nodes and governance controls.
openai11 мин четене

Astra for Law на OpenAI е правна AI система, не нов модел

Правният старт на OpenAI е не толкова за нов базов модел, колкото за системата около него: домейн извличане, надеждни инструменти, права, бенчмаркове и пътища за преглед.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering12 мин четене

Инженеринг на контекста за AI агенти с дълъг хоризонт

Дълго работещите агенти не се провалят само защото прозорецът е малък. Те се провалят, когато файлове, изходи от инструменти и остаряла история изтласкат задачата, която агентът е трябвало да завърши.

Готови ли сте LIA да избира вместо вас?

Създавайте с всички AI модели на едно място — започнете безплатно още днес.