- Pradžia
- Atvejų analizės
- Agentų valdomas užduočių variklis
Agentų valdomas užduočių variklis
Saugus bendras užduočių laukas autonominiams DI agentams: paimti darbą vieną kartą, atkurti apleistas nuomas ir perduoti pasikartojančius gedimus žmonėms be foninio darbininko.
Paskelbta 2026-06-30Peržiūrėta 2026-07-02
Trumpai
- Problema
- DI agentai turėjo dirbti iš tos pačios eilės kaip žmonės, nepaimdami užduočių dukart ir nepaslėpdami gedimų.
- Apribojimas
- Sistema turėjo likti paprasta: be nuolat veikiančio darbininko, trapios apklausos kilpos ir be prarastos būsenų istorijos.
- Sukurta sistema
- Pagal kontraktą kuriamas užduočių API su Postgres eilučių užraktais, atnaujinamomis nuomomis, tingiu pasibaigimu, SSE ir įvykių žurnalu.
- Rezultatas
- Agentai gali saugiai paimti, pratęsti, užbaigti, atmesti ir eskaluoti darbą, o žmonės realiu laiku mato tą pačią lentą.
- Kur tai pritaikoma
- Tinka komandoms, kurios nori DI agentų gamyboje, bet prieš plečiant autonomiją turi aiškiai apibrėžti atsakomybę, peržiūrą ir gedimus.
- Technologijos
- Drizzle · Postgres LISTEN/NOTIFY · SSE · contract-first API
Kodėl tai svarbu
Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.


Apžvalga
Pirmiau aprašytas apribojimas — visa užduotis: bendras užduočių laukas, iš kurio darbą gali saugiai imtis tiek žmonės, tiek autonominiai agentai, be nuolat veikiančio darbininko, kurį reikėtų diegti ir prižiūrėti, be apklausos kilpos, mainančios greitą reakciją į apkrovą, ir su pilna kiekvieno paėmimo, pratęsimo, užbaigimo, gedimo ir eskalavimo istorija, kad lentos praeities niekada nereikėtų atkurti iš nuojautos. Du agentai gali stebėti tą pačią lentą tą pačią akimirką, abu nuspręsti paimti tą pačią užduotį ir pateikti paėmimus per kelias mikrosekundes vienas nuo kito — sugedęs agentas gali dingti darbo viduryje ir palikti savo paėmimą kabantį ore, o netinkamai veikiantis agentas gali nesėkmingai bandyti tą pačią užduotį vėl ir vėl, jei niekas neverčia žmogaus į tai pažiūrėti. Viskas toliau sukurta iš primityvų, kuriuos Postgres jau turi — eilučių užraktų, nuomos su TTL ir tik papildomo žurnalo — o ne iš antros sistemos, pritvirtintos kompensuoti tai, ką duomenų bazė jau gali padaryti nativiai.
Paėmimas be konkurencijos
Naivus paėmimas — tai SELECT, randantis laukiančią užduotį, po kurio seka atskiras UPDATE, pažymintis ją paimta, ir būtent tarpas tarp šių dviejų užklausų yra ta vieta, kur du agentai gali abu pasirinkti tą pačią eilutę ir abu manyti, kad ją paėmė.
Sprendimas — ne atskiras paskirstytas užraktų servisas šalia Postgres, o sąlyga, kurią Postgres jau turi:
SELECT id FROM tasks
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1;
Paėmimo užklausa — lygiagretūs agentai atsiduria ant skirtingų eilučių
FOR UPDATE užrakina kandidatinę užduoties eilutę paėmimo tranzakcijos viduje; SKIP LOCKED liepia Postgres, kad jei eilutė jau užrakinta kito, dar vykdomo paėmimo, nelaukti jos ir nemesti klaidos, o tiesiog pereiti prie kitos.
Du agentai, vykdantys šią užklausą tą pačią akimirką, vienas kito neblokuoja ir nesusiduria — kiekvienas praeina pro tai, ką kitas jau užrakino, ir užrakina kitą laisvą užduotį.
Paėmimui nereikia nei pakartojimo ciklo, nei kompensuojančio „atšaukti paėmimą“ žingsnio, nes abipusę išskirtį užtikrina pati duomenų bazės eilučių lygio užraktų sistema, o ne apytiksliai imituoja programos kodas.
Kodėl ne apklausos kilpa ar užraktų servisas
Apklausos kilpa — agentai periodiškai klausia „ar yra kas laukia?“ ir lenktyniauja atnaujindami tai, ką grąžina užklausa — vis tiek turi lygiai tą pačią lenktynių problemą, jei pats atnaujinimas nėra atominis, todėl apklausa pati savaime yra planavimo strategija, o ne konkurencijos valdymas.
Paskirstytas užraktų servisas išsprendžia lenktynes, bet prideda antrą sistemą, kurią reikia diegti, stebėti ir suprasti jos gedimo scenarijus — įskaitant, kas nutinka paėmimui, jei pats užraktų servisas tampa nepasiekiamas — dėl garantijos, kurią Postgres jau suteikia toje pačioje lentelėje, kurioje gyvena pati užduotis.
SKIP LOCKED laiko konkurencijos valdymą toje pačioje tranzakcijos ribose, kaip ir duomenis, kuriuos jis saugo — tai vienu judančiu elementu mažiau, kurį reikia prižiūrėti.
Atnaujinamos nuomos ir tingus pasibaigimas
Paėmimas — tai nuoma, o ne amžinas perdavimas: paimant užduotį, jai priskiriama leased_until laiko žyma, fiksuotu TTL laiku į priekį, ir tikimasi, kad paėmęs agentas pratęs nuomą iki šio termino, stumdamas leased_until dar toliau.
„Paimta“ ir „vis dar dirbama“ — ne tas pats faktas: agentas gali sugesti, prarasti ryšį ar būti nutrauktas darbo viduryje, o užduotis, kurią jis paėmė, neturėtų likti paimta amžinai vien todėl, kad joks aiškus atlaisvinimo žingsnis niekada neįvyko.
UPDATE tasks
SET leased_until = now() + interval '2 minutes'
WHERE id = $1 AND leased_by = $2 AND leased_until > now()
RETURNING id;
Pratęsimas (heartbeat) — atnaujinti nuomą, kurią agentas dar turi
Sąlyga leased_until > now() svarbi lygiai taip pat, kaip ir pats intervalas: jei nuoma jau pasibaigusi, pratęsimas neatitinka jokios eilutės, ir agentas, manęs, kad vis dar valdo užduotį, tai sužino iš karto, užuot tęsęs darbą su užduotimi, kurią jau galėjo pasiimti kažkas kitas.
Kodėl ne foninis valytuvas (reaper)
Akivaizdus nuomos pasibaigimo sprendimas — suplanuotas valytuvas: darbas, kuris kas kelias sekundes nuskaito lentelę ir grąžina eilutes, kurių leased_until jau praėjo, atgal į laukiančių būseną.
Tai procesas su sava gyvavimo trukme: jį reikia diegti, reikia stebėti, ar jis vis dar veikia, o jei jis tyliai sustoja, pasibaigusios nuomos tyliai nustoja būti atgaunamos, kol kas nors pastebi susikaupusią užstrigusių užduočių krūvą.
Tai lygiai tas pats nuolat veikiantis darbininkas, kurį apribojimas draudžia.
Alternatyva — tingus pasibaigimas: niekas iš anksto neatgauna pasibaigusios nuomos. Vietoj to pati paėmimo užklausa laiko pasibaigusią nuomą tiek pat paimtina, kiek ir niekada nepaimtą užduotį:
SELECT id FROM tasks
WHERE status = 'pending'
OR (status = 'in_progress' AND leased_until < now())
ORDER BY priority DESC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1;
Paėmimo užklausa, praplėsta atgauti pasibaigusias nuomas tiesiog vietoje
Pasibaigusi nuoma atgaunama tą akimirką, kai kitas agentas pradeda ieškoti darbo, o tai judrioje lentoje įvyksta po kelių sekundžių, o ne pagal fiksuotą tikrinimo intervalą. Jei lenta nutyla, apleista užduotis tiesiog laukia — o tai teisingas elgesys, kai nėra kam ją perduoti bet kuriuo atveju. Šiam mechanizmui palaikyti nereikia jokio atskiro proceso — atgavimo logika visiškai gyvena toje pačioje užklausoje, kuri paima ir naują darbą.
Schema: užduoties paėmimo gyvavimo ciklas — agentas paima užduotį naudodamas FOR UPDATE SKIP LOCKED, laiko atnaujinamą nuomą su periodiniais pratęsimais, užduotį arba užbaigia, arba atmeta, o pasikartojus gedimui trijų bandymų skaitiklis perduoda ją žmogui; pasibaigusi ir nepratęsta nuoma tingiai atgaunama kito pretendento, o ne foninio valytuvo.
Eskalavimas ir audituojama istorija
Ne kiekvienas gedimas yra laikinas.
Agentas gali nepavykti dėl blogo užklausimo, iš esmės neišsprendžiamos užduoties ar netvarkingų įvesties duomenų, o kartojant bandymą su tuo pačiu agentu tomis pačiomis sąlygomis dažnai vėl nepavyks, potencialiai amžinai, jei niekas neįsikiša.
Kiekviena užduotis turi gedimų skaitiklį, kuris padidėja su kiekvienu fail perėjimu; trečias gedimas eskaluoja užduotį žmogaus eilei, užuot leidęs ketvirtą paėmimą.
Ši riba brėžia liniją, nuo kurios priklauso visas sprendimas: vienas nesėkmingas bandymas — triukšmas, trys — modelis, į kurį žmogus turėtų pažvelgti.
Kiekvienas perėjimas — paimta, pratęsta, užbaigta, nepavyko, eskaluota — įrašomas kaip nauja eilutė tik papildomame įvykių žurnale, o ne perrašomas ant vienos kintamos būsenos skilties.
INSERT INTO task_events (task_id, type, actor, payload, created_at)
VALUES ($1, 'failed', $2, $3, now());
Perėjimo įrašymas — įvykių lentelė tik papildoma
Kintama būsenos skiltis gali atsakyti tik į „kokia šios užduoties būsena dabar“; tik papildomas žurnalas gali atsakyti į „kas nutiko šiai užduočiai ir kokia tvarka“. Vien būsenos sekimas pasakytų, kad užduotis nepavyko tris kartus, bet ne kaip atrodė kiekvienas bandymas, kuris agentas jį vykdė, ar kaip pratęsimas, galbūt taip ir nepasiekęs serverio, susijęs su galutiniu gedimu — ta istorija arba gyvena žurnaluose, kurie jau galėjo būti perrašyti, arba jos tiesiog nebėra. Įvykių žurnalas paverčia lentos istoriją užklausiamu faktu, o ne kažkuo, ką reikia atkurti po incidento, o tai svarbu dar labiau, ne mažiau, kai užduotis paimantis ir joje suklystantis klientas yra autonominis ir negali būti vėliau paklaustas, ką jis, jo manymu, tuo metu darė.
Realaus laiko sklaida
Žmonėms ir agentams reikia stebėti tą pačią lentą, kad nė vienas jų nedaužytų jos užklausomis.
Apklausa kliento pusėje — kiekvienas atviras skirtukas kas kelias sekundes vykdantis naują užklausą — blogai mastelėja, nes kiekvienas papildomas žiūrovas tiesiogiai daugina apkrovą, ir yra iš prigimties pavėluota, nes vidutinis atsilikimas geriausiu atveju siekia pusę apklausos intervalo.
Alternatyva — leisti pačiam Postgres paskelbti pokyčius: kiekvienas būsenos perėjimas siunčia NOTIFY toje pačioje tranzakcijoje, kuri padarė pakeitimą, o vienas ilgai gyvuojantis procesas laiko LISTEN prenumeratą ir paskleidžia kiekvieną pranešimą prisijungusioms naršyklėms kaip serverio siunčiamą įvykį.
NOTIFY task_events, '{"taskId": "...", "type": "claimed"}';
Pokyčio paskelbimas toje pačioje tranzakcijoje, kuri jį padarė
SSE, o ne WebSockets, čia yra tinkamas sudėtingumo lygis: lentai reikia tik vienpusio srauto nuo serverio prie naršyklės, o SSE tai suteikia per paprastą HTTP, o pakartotinį prisijungimą tvarko pati naršyklės EventSource sąsaja, o ne rankomis rašomas kodas.
Viena LISTEN jungtis paskleidžia informaciją visiems atviriems skirtukams; duomenų bazės pranešimų sistema yra vienintelis „kažkas pasikeitė“ tiesos šaltinis, o sąsajai niekada nereikia apklausti, kad tai sužinotų.
Kompromisai ir apribojimai
- Tingus pasibaigimas reiškia, kad užduotis, kurios nuoma pasibaigė, iš tikrųjų neatgaunama, kol kitas pretendentas pradeda ieškoti darbo — tylioje lentoje apleista užduotis gali gulėti neatgauta gerokai ilgiau, nei rodytų jos TTL, nes niekas neverčia to spręsti.
FOR UPDATE SKIP LOCKEDvis tiek reikalauja stebėti pačią lentelę esant dideliam apyvartumui — didelis atnaujinimų ir trynimų kiekis reikalauja įprasto Postgres autovacuum dėmesio, kitaip užraktų konkurencija ir negyvų eilučių kaupimasis pasireiškia esant apkrovai, nepriklausomai nuo paėmimo šablono.LISTEN/NOTIFYneišgyvena nutrūkusio ryšio ar perkrovimo, o pranešimų dydis ribotas — po atsijungimo iš naujo prisijungiantis sklaidos procesas gali praleisti tai, kas įvyko, kol jis buvo nepasiekiamas, todėl tai geriausių pastangų realaus laiko sluoksnis, o ne patvarus; tiesos šaltinis lieka lentelė ir įvykių žurnalas, o ne pranešimų srautas.- Nuomos TTL riboja, kiek ilgai tinkamai besielgiantis agentas gali laikyti užduotį, bet niekas nesutrukdo netinkamai veikiančiam agentui pratęsinėti nuomą pagal grafiką, realiai nieko nenuveikiant — nuoma saugo nuo sugedusio ar atsijungusio agento, o ne nuo gyvo, be prasmės besisukančio savo paties nuomos lange.
- Kontraktu grįstos API disciplinos laikomasi peržiūros, o ne įrankių priverstinai — niekas techniškai nesutrukdo ateityje pridėti galinio taško, kuris sulaužytų paėmimo, pratęsimo, užbaigimo ar gedimo kontraktą, nuo kurio jau priklauso autonominis klientas.
- Trijų bandymų riba yra fiksuota, o ne derinama pagal užduoties tipą, todėl užduotis, kuri tiesiog sudėtingesnė nei vidutinė, eskaluojama pagal tą patį grafiką, kaip ir ta, kuri iš tikrųjų sugedusi.
Šaltiniai ir tolesnis skaitymas
Čia naudojami primityvai — gerai dokumentuoti, bendros paskirties statybiniai blokai, o ne kažkas išskirtinai sukurta šiai sistemai:
Susijusios atvejų analizės
Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.
Patvari užduočių eilė, pakeitusi „no-code“ automatizacijų sieną
Gedimai tapo užklausiamomis eilutėmis, pakartojimai — saugūs pagal konstrukciją, o naujas automatizacijas galima tikrinti su gyvu srautu prieš joms veikiant.
Go · Postgres leases · gocron · bearer + HMAC authKelių tiekėjų DI vartai su integruota sąnaudų apskaita
Modelio pasirinkimas tapo konfigūracija, nauji tiekėjai paveldi bendras kontroles, o išlaidos matomos dar užklausoms vykstant.
Go adapters · gRPC / protobuf · persisted in-memory queue