İçeriğe geç
23/3030 bölümden 23. bölüm

Agent Harness Oluşturmak: Döngü ve Ondan Çıkmanın Beş Yolu

İlk denemede çalışan 15 satırlık döngü; sonra bilerek 7 kez kırılıyor. İlk örnek, sıkı sınırlı olana göre 77 kat pahalıya kaçıyor.

Bu sayfada

Dürüst kısımla başlayalım, çünkü bunu başka kimse söylemeyecek: "harness" jargon, standart değil. Bir şartname yok, komite yok, referans tanım yok. Bu bölümün alıntıladığı dört makale — ReAct,1 CoALA,2 SWE-bench ve vLLM — özetlerinde bu kelimeyi bir kez bile kullanmıyor. Bu şeyin en çok indirilen implementasyonu, Vercel’in ayda 89,4 milyon indirilen ai paketi de kullanmıyor: harness dizgesi, 7.0.93 sürümüyle gelen 397 KB’lık type declaration dosyalarında sıfır kez geçiyor.3 Kelimenin gerçekten yük taşıdığı tek yerde ise bambaşka bir anlama geliyor. SWE-bench README’sinde "harness" beş kez geçiyor, her seferinde evaluation harness olarak — bir patch uygulayıp testleri çalıştıran container’lı iskele — ve Python modülü kelimenin tam anlamıyla swebench.harness.run_evaluation.4

Yani iki farklı şey aynı adı paylaşıyor. Bir evaluation harness, agent’ı sabit tutar ve puanlar. Bir agent harness, agent’ı çalıştıran programdır: modeli çağırır, modelin istediğini yürütür, ne zaman duracağına karar verir ve aradaki state’i tutar. Bu bölüm ikincisini, hiçbir framework kullanmadan, iki yüz satırdan az TypeScript ile kuruyor.

Döngünün kendisi on beş satır ve ilk denemede çalışıyor. Bundan sonraki her şey, ondan çıkmanın bir yolu.

Ayrıntıları göster

Bu bölümün önceki bölümlerden ihtiyaç duyduğu şeyler.

  • Bölüm 14 client için: deadline’lar, status triage, cancellation, idempotency key’ler ve burada yeniden kullanılan mock provider tekniği.
  • Bölüm 16 aritmetik için: input token’lar konuşmanın karesiyle büyür ve aşağıda kullanılan fiyatlar 6 Eylül 2026’da orada okunan fiyatlardır.
  • Bölüm 18 araç kataloğu için: modelin gördüğü bir schema, asla görmediği bir endpoint ve hataların exception değil context olduğu kuralı.
  • Bölüm 22 bu bölümün devraldığı döngü ve birbiriyle çelişen iki yayımlanmış "agent" tanımı için.

Burada tensor yok. Bu, kursun ikinci dependency hub’ı: 24, 25, 29 ve 30. bölümler aşağıdaki dosyanın üzerinde çalışıyor; 26’dan 28’e kadar olanlar ise onun erişebildiklerinin üstüne kuruluyor.

Bölüm 14 gerçek bir provider’a karşı yazılamazdı, çünkü ondan seçtiğin anda bir 429 vermesini isteyemezsin. Bu bölümde aynı problem farklı bir biçimde var: gerçek bir modelden kontrolden çıkmasını ya da aynı aracı art arda iki kez, talep üzerine ve yeniden üretilebilir şekilde istemesini isteyemezsin.

Bu yüzden ilk program bir scripted provider: chat completions API biçiminde bir endpoint; yanıtı, tur indeksinin ve araçların o ana kadar döndürdüklerinin bir fonksiyonu. Token’ları gerçek bir byte-pair encoder ile sayıyor; dolayısıyla aşağıdaki para hesabı süs değil aritmetik.

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

Tasarımı iki satır taşıyor. Tur indeksi bir değişkende tutulmuyor, konuşmadan türetiliyor; böylece provider stateless kalıyor ve bir run öldürülüp ona karşı sürdürülebiliyor. Ayrıca recover karar vermeden önce araç sonuçlarını okuyor: kendi transcript’ini okuyan bir scripted model, harness’in ona okumaya değer bir şey verip vermediğini ölçmek için gereken minimum şey.

Katalog Bölüm 18’deki katalog: üç dosya üzerinde dört araç: list_files, read_file, delete_fileneedsApproval olarak işaretli — ve özellikle yavaş olan scan_archive.

Onu yaşanabilir kılan parçaların hiçbirinden önce, tüm fikir burada.

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’a yönelt ve tam olarak göründüğü şeyi yapar:

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

Üç tur, iki araç yürütmesi, bir ABD sentinin çeyreği. Son satıra dikkat et: 204, 269, 342. Her tur kendinden önceki her şeyi yeniden gönderiyor; bu, Bölüm 16’daki quadratic faturanın kimse hiçbir şey yazmamışken geldiği yer. Bu bölümün geri kalanı, o satır büyümeyi bırakmadığında ne olduğunu anlatıyor.

Aynı döngüyü runaway script’ine yönelt — her turda araç isteyen ve asla prose üretmeyen bir model — ve işaretli return hiçbir zaman çalışmaz. Başka çıkış yoktur. Program, process ölene ya da kredi kartı bitene kadar çalışır.

Çözüm tek satırdır, literatürün önerdiği ilk controldür,5 ve eninde sonunda herkes yazar. Neredeyse kimsenin yapmadığı şey ise ne kadar değerli olduğunu ölçmektir:

tur sınırımodel çağrısıinput tokenmaliyet
883.431$0,009070
202016.259$0,038038
505088.649$0,191098
100100337.299$0,702198

Son iki satırı birlikte oku. Sınırı 50’den 100’e çıkarmak maliyeti ikiye katlamadı; 3,7 ile çarptı. Input token’lar 88.649’dan 337.299’a çıktı, yani 3,8 katına; çünkü nn turu önceki her turu da taşır ve toplam Θ(n2)\Theta(n^2) olur. Tur sınırı doğrusal bir düğme değildir. En kötü durumunun karekökü üzerinde bir düğmedir; bu yüzden 20’den 100’e "güvende olmak için" çıkarmak, vermeden önce fiyatlandırmaya değer bir karardır.

Kırılma iki: tur sınırı para sınırı değildir

Bölüme bağlantı: Kırılma iki: tur sınırı para sınırı değildir

Tur sınırının sorunu, bir turun sabit fiyatı olmamasıdır. Kısa bir transcript üzerinde yirmi tur yukarıda $0,038 tutar. 200 araçlık katalog, retrieve edilmiş belge seti ve kırk mesajlık geçmişle yirmi tur bunun yüzlerce katına mal olur; sınır bunu bilmez. Operatörün sınırlamak istediği şey faturadır.

Bu yüzden döngü parayı sayar; Bölüm 16’daki computeCost, orada okunan fiyatlarla kullanılır — bu kurs boyunca fiyatlandırılan model için milyon input token başına $2,00 ve milyon output başına $12,00:

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

Aynı runaway script, hiç tur sınırı yok, üç bütçe:

bütçeulaşılan turgerçekten harcanan
$0,019$0,010780
$0,0524$0,051790
$0,2052$0,205398

Adını koymaya değer iki şey var. Birincisi, bütçe her seferinde farklı sayıda tur satın alıyor; zaten amaç bu: operatörün önemsediği şeyi sınırlıyor ve tur sayısını transcript’in getirdiği yere bırakıyor. İkincisi, her satır aşım yapıyor. Bütçe $0,010 idi ve $0,010780 harcandı; çünkü kontrol turdan önce çalışır, turun fiyatı ise bitene kadar bilinmez. Harcamayı tam olarak sınırlayamazsın; onu bir turun maliyeti içinde sınırlayabilirsin. Arayüzde bunu saklamaya çalışmak yerine söyle ve kontrolü çağrıdan önce koy ki aşım iki tur değil bir tur olsun.

Döngüden çıkmanın tek değil, beş yolu

Bölüme bağlantı: Döngüden çıkmanın tek değil, beş yolu

Bu noktada döngünün üç çıkışı var ve bölümün geri kalanının biçimi görünüyor. Production run tam olarak beş yoldan biriyle biter ve bunlar birbirinin varyasyonu değildir:

nasıl biterkim karar verdicaller ne yapmalı
model istemeyi bıraktımodelyanıtı oku
tur sınırısen, öncedensınırı yükselt ya da kısmi sonucu kabul et
bütçe tükendisen, öncedendaha fazla parayı onayla ya da kısmi sonucu kabul et
retry edemeyeceğin bir hataprovider veya araçdeployment’ı düzelt; Bölüm 14’ün triage’ı karar verir
insan müdahale ettibir kişikararı bekle, sonra sürdür

Bunları tek bir boolean’a indirgemek bu dosyadaki en yaygın tasarım hatasıdır ve belirli bir şekilde pahalıdır: beşinden üçü resumable, ikisi değildir. Tur sınırına ulaşan bir agent’ın geçerli bir transcript’i, gerçek bir kısmi sonucu ve bir sonraki adımı vardır; 401 alan bir agent’ta bunların hiçbiri yoktur. Bu yüzden harness nedeni data olarak kaydeder:

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

Bölüm 18 bir sayı vermeden şu iddiayla bitmişti: bir aracın hatasını raise etmek yerine araç sonucu olarak modele geri ver, model çoğunlukla kendini düzeltir. İşte sayı.

Bir failure, üç policy. Scripted model var olmayan bir dosya tahmin eder; araç no such file: timeout.log. Call list_files to see what exists. fırlatır

harness hatayla ne yaparturaraç çalışmasımaliyetkullanıcı ne aldı
döngüden dışarı fırlatır11$0,000756stack trace
Error: the tool failed. döndürür21$0,001462"Dosyayı okuyamadım, bu yüzden bilmiyorum."
gerçekten olanı döndürür43$0,003550"errors.log bir timeout’tan bahsediyor."

Üçüncü satır birincinin 4,7 katına mal olur ve soruyu yanıtlayan tek satırdır. İlginç olan ikinci satırdır; çünkü çoğu codebase’in gerçekten yaptığı şey budur: hata yakalandı, döngü hayatta kaldı, modele bir şeyin başarısız olduğu söylendi ama neyin olduğu söylenmedi ve model kibarca vazgeçti. İkinci ve üçüncü satır arasındaki fark error handling değildir. Bir okur için yazılmış bir cümledir.

Bu yüzden harness, throw edilmiş aracı data olarak ele alır ve ifade biçimini bir policy yapar:

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

Bölüm 18 diğer taraf konusunda da uyarmıştı ve onun da bir bedeli var. Döngüyü hiçbir mesajın düzeltemeyeceği bir nedenle başarısız olan bir araca yönelt — process’in yapmasına izin verilmeyen bir okuma — ve model sonsuza kadar tekrar dener:

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

Başarılı olamayacak bir çağrının on bir aynı yürütmesi, düzeltilebilir bir hatadan toparlanan run’ın maliyetinin 5,2 katı ve sonunda hiçbir şey yok. Hatalar context’tir; kalıcı hata, run’ın geri kalanını zehirleyen context’tir. Ayrım, Bölüm 14’ün status triage’ının bir katman yukarı taşınmış halidir: modelin üzerine hareket edebileceği hata transcript’e geri döner; edemeyeceği hata ise run’ı bir reason ile durdurmalıdır. Bugün seninle ikinci durumun arasında duran şey tur sınırıdır; bu bir taban, çözüm değil.

Şimdi çoğu insanın olamayacağını varsaydığı failure. Modeller kendini tekrar eder. Herhangi bir döngüden yeterince uzun çalışmasını iste, iki ardışık turda aynı argümanlarla aynı aracı göreceksin.

Aynı görevin tekrarsız baseline’ına karşı ölçüldüğünde:

turaraç çalışmasımaliyet
görev, tekrar yok21$0,001396
aynı görev, bir çağrı tekrarlandı32$0,002446
tekrarlandı, read-only araçlarda result cache ile31$0,002446

Yinelenen çağrı $0,001050 ekstra maliyete, yani %75 artışa neden oldu; insanları şaşırtan kısım ise şu: sonucu cache’lemek bunun hiçbirini geri kazanmadı. Deduplication araç yürütmesini kurtardı, turu değil; çünkü kodun tekrar fark ettiğinde model sorduğu için zaten ücretlendirilmiştir. Araç yavaş, rate-limit’li veya çağrı başına ücretli olduğunda tasarruf gerçektir — ama büyüyen kalem üzerinde sıfırdır.

Daha kötü bir versiyon var. Aynı cache’i yazan bir araca uygula, ikinci çağrı sessizce gerçekleşmez:

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

Bunlardan hangisi doğru? Bilinebilir şekilde, hiçbiri. Protocol bunların iki çağrı olduğunu söyler: iki farklı tool_call_id değeri taşırlar. Argümanlar bir olabileceklerini söyler. Argüman string’lerini karşılaştırarak karar veren bir harness, bir gün amaçlanmış iki aynı ücretlendirmenin ikincisini yutacaktır — ve Bölüm 14 bunun dürüstçe çözen tek mekanizmayı zaten adlandırdı: operasyonun ne olduğunu bilen katman tarafından logical operation başına üretilen bir idempotency key. Araç böyle bir key taşıyana kadar savunulabilir default yukarıdaki read-only kapıdır: okumaları cache’le, yazmaları yürüt ve geri kalanını yazmanın kendi idempotency’sine bırak.

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 script’i dosyaları listeler ve sonra görevin hiç bahsetmediği bir dosyayı silmek ister. Döngüde şimdiye kadar hiçbir şey onu durdurmazdı.

needsApproval olarak işaretlenmiş bir araç başarısız olmaz ve devam da etmez. Run’ı durdurur ve kontrolü geri verir; bir kişinin karar vermesi için gereken her şeyle birlikte:

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

Mekanizmanın tamamı bu; bunun callback değil return olmasının nedeni ise bir sonraki bölüm: durma ile karar arasında process artık var olmayabilir.

Ama önce kimsenin beklemediği ölçüm. Bir reddetme, sonucun yokluğu değildir — transcript’te tool_call_id ile anahtarlanmış bir slot vardır ve içine bir şey konmalıdır. Aynı reddi iki kez çalıştır; yalnızca o şeyin ne söylediğini değiştir:

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

İki run’da da hiçbir şey silinmedi, ikincisinde ise kullanıcıya silindiği söylendi. Permission system kusursuz çalıştı; rapor yalan. Bu, tool-error tablosuyla aynı mekanizmadır ama çok daha önemli bir yere gelir — bir insan hayır dedi, eylem doğru şekilde engellendi ve agent’ın özeti gerçekle çelişiyor; çünkü ret, modelin okuduğu yere hiç yazılmadı. Buradan çıkan kural kısa: kodun bir tool call hakkında neye karar verirse versin, kararı transcript’e kelimelerle yaz. Bölüm 30 buna güvenlik tarafından geri döner; orada bu, audit trail ile kurgu arasındaki farktır.

Bir onay dakikalar ya da saatler sürer. Bir deploy saniyeler sürer. Run bir HTTP request içindeki local variable’da yaşıyorsa, her restart kayıp bir run ve her onay bir race’tir.

Bu yüzden run bir closure değildir. Düz, serialisable bir object’tir — messages, turn count, cost, status, interruption, approved call id listesi — ve döngü onun üzerinde pure function’dır. Persistence’ı tek satırlık bir mesele yapan şey bu tek kısıttır:

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

Doğruluk sorusu kaydetmek değildir. Geri gelindiğinde ne olduğudur ve naif yanıt seni iki kez ücretlendirir. Process, model bir araç istedikten sonra ama sonuç yazılmadan önce öldüyse, modeli yeniden çağırarak başlayan bir resume zaten sahip olduğu bir tur için para öder — araçları yeniden çalıştırarak başlarsa, bir yazmayı iki kez yapar.

Çözüm, döngüyü transcript’e neyin outstanding olduğunu sorarak başlatmaktır:

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

Her iteration önce pending boşaltır ve ancak outstanding hiçbir şey kalmadığında modele sorar. Resume normal code path ile aynı hale gelir; approval da öyle — onaylanmış çağrı, artık çalışmasına izin verilen pending çağrıdan ibarettir. Process’i task ortasında öldür ve yeniden başlat:

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

İki araç gerektiren bir task için iki process boyunca iki araç yürütmesi; final maliyet hiç crash etmemiş run ile aynı. Cost restart boyunca birikir, çünkü bir değişkende değil state’in içindeydi.

scan_archive burada üç saniye sürer ve production’da üç dakika süren aracın yerine geçer. Çalışırken iki şey eksiktir: kullanıcı hiçbir şey olup bittiğini bilmez ve Stop düğmesi hiçbir şey yapmaz.

İkisi de aynı fix’tir ve Bölüm 14’ün AbortSignal öğesinin bir katman aşağı itilmiş halidir. Signal yalnızca fetch için değildir — aracın içine geçirilir ve iyi yazılmış bir araç buna uyar:

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"

Tıklamadan durmaya iki milisaniye; çünkü aracın içindeki sleep, fetch’in dinlediği signal’ın aynısını dinler. Bunu yalnızca fetch içine geçirirsen, aynı Stop düğmesi üç saniye — yani aracın süresi — bekler ve run iptal ettiği iş zaten bittikten sonra "cancels" olur. En aşağıya kadar bağlanmamış cancellation, doğru kelimeyi söyleyen bir spinner’dır.

Harness event başına bir satır yayar ve vocabulary ezberlenecek kadar küçüktür: 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}

Bunu logging değil trace yapan üç özellik var. Her satır run id taşır; böylece üç process ve iki güne yayılan bir run tek bir query’dir. Her turn satırı kendi token count’larını ve running cost’u taşır; böylece "bu run neden kırk dolar tuttu" sorusu sonradan yanıtlanabilir, yalnızca teoride yeniden üretilebilir kalmaz. Ve run_stopped reason taşır; bu alan support ticket’ı tek satırlık cevaba çevirir: bütçede duran agent ile crash eden agent dışarıdan aynı görünür ve zıt yanıtlar gerektirir.

Bölüm 13 time to first token’ı sahip olduğun hardware üzerinde ölçtü. Bölüm 14 onu bir socket üzerinden ölçtü. Bir agent bunu çarpar ve çarpan, kimsenin seçmediği bir sayıdır:

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

Aynı üç turlu task, yalnızca provider latency’si değiştirilerek:

tur başına provider latencywall clock, 3 tur
0 ms15 ms
200 ms615 ms
800 ms2.413 ms

Harness’in kendisi üç turlu run’a on beş milisaniye ekler. Geri kalan her şey NN çarpı kontrol etmediğin bir sayı — request’ini yabancıların request’leriyle batch’leyen serving scheduler içinde ayarlanır6 — ve NN model tarafından seçilir. Bu yüzden Bölüm 14’ün streaming’i burada chat’te olduğundan daha önemlidir ve daha az yardımcı olur: final turu stream edebilirsin, ondan önceki dört tur ise harness progress yaymazsa sessizliktir. Yukarıdaki tool_progress event’inin tüm argümanı da budur — bir agent’ta dürüst feedback birimi token değil, step’tir.

Aynı harness, portun arkasında gerçek bir model

Bölüme bağlantı: Aynı harness, portun arkasında gerçek bir model

Yukarıdaki her şey scripted provider’a karşı çalıştı; bu harness’i kanıtlar, modeller hakkında hiçbir şeyi kanıtlamaz. Bu yüzden bir satırı değiştir — Bölüm 14’teki seam, LLM_BASE_URL — ve aynı kodu, aynı dört araçla local Qwen2.5-0.5B-Instruct’a yönelt. Aynı üç dosya üzerinde altı task:

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

Üç bulgu var; üçüncüsü bu bölümün var olma nedeni.

Her bir task tam olarak iki turda bitti. Tur sınırı hiç devreye girmedi, bütçe hiç devreye girmedi ve döngünün tek çıkışı modelin prose üretmesiydi. Yarım milyar parametreli model iterate etmez; ihtiyacı olan şeye sahip olsun ya da olmasın ikinci nefesinde yanıt verir. Tur sayısı döngünün değil modelin özelliğidir.

Ortalama tur 6.908 milisaniye sürdü, yani yukarıdaki latency tablosu oyuncak değil: bu boyutta hipotetik sekiz turlu run ekranda hiçbir şey yokken neredeyse bir dakikalık wall clock demektir.

Ve yanıtlar yanlış. En büyük dosya errors.log; model dosyaları listeledi, hiçbirini okumadı ve yine de birini adlandırdı. İlk task bir dosya adı tahmin etti, var olmadığı söylendi ve sonuca vardı. Harness altı run’ın tamamında kusursuz çalıştı. Harness bir agent’ı yönetilebilir yapar, doğru değil — Bölüm 29 hangisi olduğunu nasıl anlayacağını, Bölüm 30 ise kimse bunu yapmadığında maliyetini anlatır.

Subagent’lar, burada adlandırıldı ve sonra ücretlendirilecek

Bölüme bağlantı: Subagent’lar, burada adlandırıldı ve sonra ücretlendirilecek

Katalogdaki bir aracın arkasında başka bir run olabilir. Interface Bölüm 18’deki gibidir — bir schema ve endpoint — ve bu interface dar olduğu için bütün bir agent onun arkasına sığar:

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

Bu on satırda üç şey şimdiden doğru ve üçünün de nedeni yukarıda verilen kararlardır: child’ın kendi window’u vardır, bu yüzden parent transcript’i child’ın okuduğu her şeyi değil bir summary alır; kendi limitleri vardır, bu yüzden runaway child parent budget’ını harcayamaz; ve signal’ı miras alır, böylece tek Stop tüm tree’yi iptal eder. Temiz window’un yan etki değil asıl nokta olmasının nedeni Bölüm 24; beş orchestration pattern — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — ve handoff ise Bölüm 25.

Framework’ler nerede ve bu kurs neden birini kullanmadı

Bölüme bağlantı: Framework’ler nerede ve bu kurs neden birini kullanmadı

Yukarıdaki hiçbir şey libraries’e karşı bir argüman olarak okunmamalı. 7 Eylül 2026’da, 29 Ağustos’ta biten ay için ölçüldüğünde:7

packageo ayki indirmesana ne verir
ai (Vercel AI SDK)89.385.860ToolLoopAgent, stopWhen, tool approval, step hooks
@anthropic-ai/claude-agent-sdk41.558.352library olarak Claude Code harness: döngü, sessions, hooks, permissions, subagents8
@langchain/langgraph12.812.815explicit state graph olarak döngü
langchain11.359.058chains, agents, integrations
@openai/agents6.093.155agents, handoffs, guardrails
@mastra/core5.914.502agents, workflows, memory

Bu kursun bunlardan birini öğretmek yerine döngüyü elle yazmasının nedeni ima edilmek yerine açıkça söyleniyor ve ölçülebilir. 7 Eylül 2026’ya kadar olan on iki ayda ai 945 sürüm yayımladı ve major 5’ten major 7’ye geçti; agent class hâlâ Experimental_Agent olarak export ediliyor; langchain aynı aralıkta 132 sürüm yayımladı; @openai/agents 83 sürüm yayımladı ve ilk release’inden on beş ay sonra hâlâ 0.x’te.7 Bu API’lerin herhangi birine karşı yazılmış bölüm bir sezon içinde eskir; bu bölüm otuz üç dilde yayımlandığı için her yeniden basım bütün çeviri yüküne mal olur. Hepsinin altında kalan şey hareket etmez: bir döngü, bir stopping rule, bir katalog, bir executor, biraz state.

Ve reference implementation, önemli olan kısımda bu bölümle aynı fikirde. ai 7.0.93 sürümünde döngünün çıkışı bir sayı değildir — stopWhen, yani predicate listesi; step count bunlardan yalnızca biridir: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

Stopping, bu döngünün en çok kullanılan implementasyonunda çoğuldur; yukarıdaki yüz doksan altı satırda çoğul olmasının nedeni de aynıdır.

Artık bir harness’in var: döngü, katalog, executor, beş çıkış yolu, persisted run, araçlara ulaşan signal ve her satırında run id olan trace. 24, 25, 29 ve 30. bölümler bu dosyanın, 26’dan 28’e kadar olanlar ise onun erişebildiklerinin üstüne kuruluyor.

Geriye bir problemi kaldı ve yukarıdaki ölçümler baştan beri onu işaret ediyor. Runaway tablosuna bir kez daha bak: sekiz turda 3.431 input token, yüz turda 337.299. Çalışan run’a bak: 204, 269, 342. Her tur bütün transcript’i yeniden gönderiyor; dolayısıyla agent’ın context’i kendi history’siyle doluyor — ve model uzun window’un uzak ucunu yakın ucu kadar iyi kullanamıyor; bu yüzden beşinci turda iyi olan agent kırkıncı turda kafası karışmış oluyor.

Tur sınırı bunu düzeltmez. Yalnızca bunun olmasını izlemek için para ödemenizi durdurur. Bunu düzelten şey, her tek turda hangi token’ların window’u hak ettiğine karar vermektir: ne compact edilecek, ne agent’ın fetch edebileceği bir nota taşınacak, ne temiz window’lu bir subagent’a verilecek ve hangi tool definition’lar kalıcı vergilerine değer. Bölüm 24 window’un gerçekte nereye gittiğini ölçüyor — sürpriz şu ki, konuşmaya değil.


Bu bölümdeki her sayı yukarıda anlatılan iki server’dan çıktı; Node 22 üzerinde loopback interface ile: o200k_base encoding’iyle token sayan scripted provider ve aynı biçimdeki endpoint’in arkasında CPU üzerinde greedy decoding yapan Qwen/Qwen2.5-0.5B-Instruct. Maliyetler, Bölüm 16’nın 6 Eylül 2026’da okuduğu fiyatlarla — milyon input token başına $2,00 ve milyon output başına $12,00 — ölçülen token count’lardan hesaplandı ve bu bölümdeki hiçbir request ücretli bir endpoint’e gitmedi. Local model’in yanıtları küçük bir model’in yanıtlarıdır; onları güncel modellerin ne yaptığına dair benchmark olarak değil, her iki durumda da aynı olan döngü hakkında evidence olarak oku.

  1. Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. ve Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Döngünün implement ettiği reasoning trace’ler ile eylemlerin iç içe geçmesi ve acting’in bir modelin "handle exceptions" yapmasına izin verdiği gözleminin kaynağı — yukarıdaki tool-error tablosu tam olarak bunu ölçüyor.

  2. Sumers, T. R., Yao, S., Narasimhan, K. ve Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Yukarıdaki döngünün informal olarak yaptığı şeyin formal treatment’ı: modular memory components, internal memory ve external environment’ları kapsayan structured action space ve "a generalized decision-making process to choose actions". Industry term’ün kaçırdığı vocabulary için oku — özellikle working, episodic, semantic ve procedural memory ayrımı; bunun pratik gölgesi Bölüm 24’ün üç-store tablosudur.

  3. ai (Vercel AI SDK) sürüm 7.0.93, 4 Eylül 2026’da yayımlandı; type declaration’lar 7 Eylül 2026’da cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts üzerinden okundu. 397 KB’lık dosyada harness dizgesi sıfır kez geçiyor. Agent class declare class ToolLoopAgent; hem ToolLoopAgent hem de Experimental_Agent olarak export ediliyor; declare function isStepCount(stepCount: number)stepCountIs olarak export edilen — yukarıda aynen alıntılandı; type StopCondition ikinci type parameter’ı (RUNTIME_CONTEXT extends Context = Context) olmadan gösterildi; alıntıdaki tek elision budur, generateText ve streamText üzerindeki stopWhen?: Arrayable<StopCondition<...>> biçimi de öyle. Aynı dosya toolApproval, ToolApprovalStatus, prepareStep ve repairToolCall tanımlar; yani reference implementation bağımsız olarak approval gate’lere, per-step preparation’a ve error repair’e varmıştır. 2

  4. Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. ve Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Özet, artefact’ı 2.294 problemlik bir "evaluation framework" olarak adlandırır ve "harness" kelimesini hiç kullanmaz; projenin kendi README’si (github.com/SWE-bench/SWE-bench, 7 Eylül 2026’da okundu) kelimeyi beş kez kullanır, her seferinde "evaluation harness" olarak; entry point python -m swebench.harness.run_evaluation. Bu, kelimenin diğer anlamıdır: agent’ı sabit tutup puanlayan bir iskele, onu çalıştıran döngü değil.

  5. Anthropic, Building effective agents, 19 Aralık 2024, anthropic.com/engineering/building-effective-agents, 7 Eylül 2026’da okundu. Building block olarak augmented model, "using tools based on environmental feedback in a loop" yapan LLM olarak agent ve kontrolü korumak için "such as a maximum number of iterations" stopping condition önerisi. Bölüm 22 tanımını tam olarak alıntılar.

  6. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. ve Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Diğer döngü — request’ini yabancıların request’leriyle batch’leyen ve Bölüm 13’ün KV cache’ini yöneten serving scheduler. Tam da sana ait olmadığı için var olduğunu bilmeye değer: harness’inin çarptığı latency onun içinde belirlenir ve kendi döngün üzerinde ne kadar çalışırsan çalış bunu oynatmaz.

  7. npm registry download count’ları, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>; rolling last-month yerine explicit window ve release history’leri registry.npmjs.org/<package> üzerinden; ikisi de 7 Eylül 2026’da sorgulandı. Release count’ları, canary build’ler dahil o tarihe kadarki on iki ayda yayımlanan version sayısıdır: ai 945 (latest 7.0.93, 2026-09-04; major 5, 6 ve 7’nin hepsi window içinde görünüyor), langchain 132 (latest 1.5.10, 2026-08-20), @openai/agents 83 (latest 0.17.0, 2026-08-19; ilk yayımlanma 2025-06-03). 2

  8. Claude Agent SDK (@anthropic-ai/claude-agent-sdk), library olarak paketlenmiş Claude Code harness’tir — agent loop, built-in file ve shell araçları, context management, sessions, hooks, permissions ve subagents — code.claude.com/docs/en/agent-sdk adresinde belgelenir. Bu bölümün elle kurduğu her mekanizmanın yayımlanmış anlatımına en yakın şeydir ve bu bölümün yalnızca işaret ettiği parçaları adlandırdığı için kendi implementasyonunun yanında okumaya değerdir.


Hazırlayan

David Vicente Campos

NeuraLIA Labs kurucusu ve MyRealFood kurucu ortağı

León Üniversitesinden mezun bir bilgisayar mühendisiyim. MyRealFood’un kurucu ortaklarındanım; orada CTO olarak milyonlarca insanın daha iyi beslenmek için kullandığı uygulamayı geliştirdim. Ayrıca NeuraLIA Labs’i kurdum ve burada AI ürünleri geliştiriyorum. Bu sitede yol boyunca anlamak zorunda kaldığım şeyleri, keşke biri bana böyle anlatsaydı dediğim şekilde yazıyorum.

Yazar hakkında daha fazla

Yayımlayan: NeuraLIA Labs.

Yeni yazılar gelen kutuna gelsin

AI haberleri, rehberler ve ürün güncellemeleri — zamanına değecek bir şey yayımladığımızda kısa bir e-posta.

Mesajlaşmayı mı tercih ediyorsun? Aynı yazılar, burada:WhatsApp topluluğu (yeni sekmede açılır)Telegram kanalı (yeni sekmede açılır)

Kurs dizini

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev10 dk okuma

Jev AI modeli düzyazı için değil, kararlar için tasarlandı

TypeSafe AI’ın Jev’i dikkat çekiyor çünkü yazılım zekâsını bir olasılık problemi olarak ele alıyor: doğru dalı seç, güven düzeyini ekle ve kodun bir karara ihtiyacı varken bir LLM’ye metin yazdırmak için ödeme yapma.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering10 dk okuma

Uzun süreli AI ajanları için bağlam mühendisliği

Uzun süre çalışan ajanlar yalnızca pencere küçük olduğu için başarısız olmaz. Dosyalar, araç çıktıları ve bayatlamış geçmiş, ajanın tamamlaması gereken görevi arka plana ittiğinde başarısız olurlar.

Seçimi LIA'ya bırakmaya hazır mısın?

Tüm yapay zeka modelleriyle tek yerde üret — bugün ücretsiz başla.