Структурныя захоўнікі для агентаў ШІ: усередзіне пайплайна ResolveFlow
Паслухае, як агент на базе LangGraph забезпечвае разліку між розумовым аналізам і виконанням за допамогай пераконтроўкаў на рэвэлі-рангу, а не за дапамогай інструкцый у запытку, включаючы баг з выкарыстаннем дадзеных, які з’явіўся па ходу.
Большасць дэманаў AI, якія выкарыстоваюць агентных модэлей, следуюць той самы базовы прыем: модэль выбірае дзеянне і негайна яго адклёввае. Да запрошэння прыўязуецца інструмент, модэль яго запускае, і інструмент працуе без дальнейшых перакананняў. У короткім відэа-кліпе з дэманам такое можа выглядаць пераканальна, але самэ гэта ўстройства выклікаюць абавесці у тых, хто думае пра тое, каб даў автонамным системам право вплываць на ўсё значнае — адтолькі часта ежынскім захамленням межу між правильным дыягназам і шкодзячым зміненням у жывой системе ёсць паведамленне пра абавесць у самым запрошэнні.
Праект, описаны тут, быў створаны з метай ухілу ад такой завіснасці ад адной-ўсёй паведамленні пра абавесць.
ResolveFlow прымае лінк да падзеi на GitHub, збіраюць падтрымальныя доказы, класифікуе падзею ў паводле кантэксту, а потым, на аднойчынку з гэтай кантэкстуальнае класыфікацыёю, выконвае адну з трох дзейнаў: выпрацоўвае фіксаваную, некампромісную дзейнаўку, пачынае расследаванне з аднойчынку з LLM, якое базуецца на збіранных доказах, альбо перадае падзею безпосередна чалавеку-рэвізору. У працоўкі системы няма аднаго циклу, у яком модель запускае вызов інструменту; ёй структуравана як машына станоў LangGraph, пабудаваная навакол адной цэнтральнай ідеі:
Разумовыя процесы і ўзгрыванне дзейнаўак раздзеленыя з-за спосабу пабудовы системы, а не з-за якой-небудзь усталенай практыки, якую ёй трэба дапэўніваць.
Это значыць, што це не проста просьба да модэлю спачатку пераканаліцца з каму-небудзь. У замен другы, незалежны вызов LLM аналізуе і крытыкуе дыягназ Ёжы модэля прычым, як толькі людзь можа яго паблікуваць. А ўсё функцыя ў базе коду, якой разрашаецца пісаць знадворы ў GitHub, пераканаліваецца за дапамогою спецыяльнага флажка затверджэння, які ёй выдаея сама её логіка — не таму, што граф мусіць направляць вызовы такім чынам, а таму, што сама функцыя не будзе выканана без наявнасці гэтага флажка, нават як у будучым кодзе з’явіцца працоўны шлях, які адмахнёцца звычных крокаў.
У рэште гіда практычна паказана, як на самай працоўны спосаб састаўляецца система, на прыкладзе рэальнай рэалізацыі, а таксама паказана баг, які з’явіўся падчас разработкі. Шырокі урок, які дае гэты баг: модель, якая апускаецца на справжній даследчы документ, — гэта не тое ж самае, чым модель, якая апускаецца на даследчы матэрыял, які фактычна стосуецца заданага пытання.
Структура канвею
Шэсць стадзій выконваюцца абоўязкова па рэдку, і толькі аднай з іх даўнацца права змяніць ўсё, што знаходзится за межамі самага канвею:
- fetch_evidence — фактычныя запыты да GitHub REST API, якія завантажаюць тэкст тэксту проблемы, ўсе коментары да яе і рэзультаты всіх пераглядоў CI.
- normalize_evidence — непрактычаны JSON, які вернуўся пасля гэтых запытоў, перагледваецца і ператвараецца у об’ект типу IssueEvidence.
- classify — лёгкі крок, які базуецца на правілах і не выкалічвае жадных запытоў да LLM.
У наступных раздзелах раскрываюцца найважлівейшыя аспекты.
Класыфікацыя: навмесна не ўтварэнне LLM
Нявісна можлівасць викорыстоўваць LLM спадвайвае жаданне прыменяць яго да кожнага рашэння, нават тых, для якіх не патрэбны такія способы аналізу. Этап класыфікацыі вялічыну той дорогі, высакарызикованы пацёг — дыягназа на аднойчыне з модэлем, запыты пра выкарыстоўванне дадзеных і пасляэтапныя збераганні — які можа прыняць будзь-якая проблема. Чэрез гэтую ролю фільтрацыі ён павінен быць недорогім, шырокім і абсалютна прыведамым сама па сабе:
def classify(state: GraphState) -> dict:
if state["evidence"].has_failing_ci:
return {"classification": "deterministic"}
elif state["evidence"].is_information_sparse:
return {"classification": "ai_investigation"}
else:
return {"classification": "human_review"}
Этап даўа тры можлівыя рэзультаты, кожны з якіх выкалічвае разны ўжоўскі рэвэрсны аднос довер'я:
- дэтэрміністычны — няуданая перацёкка CI ёсць чысты, механічны сігнал сама па сабе. Няма чаго раследваць; проблема проста пазначаецца і перадаецца адпаведнаму адпаведальнаму.
Варта згадаць, што human_review, а не ai_investigation, являе сабою запасны варыянт. Калі система не можа з’ясавіць, што вядзецца, яна не прабуе выдумаць хітрыя адпаведзенні.
Дыагназ: адзыскванне, пасля чаго схема, а не вольны тэкст
Калі прыблытае пытанне ў галузь ai_investigation, наэтап generate_diagnosis шукае індекс Pinecone, які мае або 2,750 фрагментаваных дадзеных, атрыманых з рэальных, закрытых пытанняў з чатырох разных рэпазітарыёў (facebook/react, langchain-ai/langchain, microsoft/terminal і vercel/next.js). Пасля чаго гэтыя атрыманы фрагменты надаюцца LLM як дапаможны контэкст для выканання запытку:
def generate_diagnosis(state: GraphState) -> dict:
evidence = state["evidence"]
query = f"{evidence.title}\n\n{evidence.body}"
snippets = retrieve_evidence(query, k=3)
snippet_block = "\n\n".join(
f"[{s['id']}] (relevance: {s['score']:.2f}) {s['text']}" for s in snippets
)
prompt = (
f"Issue: {evidence.title}\n{evidence.body}\n\n"
f"Comments:\n{chr(10).join(evidence.comments) or '(none)'}\n\n"
f"Evidence snippets (cite by id in square brackets):\n{snippet_block}"
)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(Diagnosis)
diagnosis = structured_llm.invoke([("system", _SYSTEM_PROMPT), ("human", prompt)])
return {
"diagnosis": diagnosis,
"retrieved_ids": [s["id"] for s in snippets],
"retrieved_scores": {s["id"]: s["score"] for s in snippets},
}
Калькі-та деталі наэтапа дыягназавання заслугуюць на болейшы аналіз.
Па-першае, формат выходных дадзеных не ўзяты пазней — яго гарантуюць з самага пачатку. Дыягназаванне апісваецца як модэль Pydantic:
class Diagnosis(BaseModel):
root_cause: str
severity: Literal["low", "medium", "high"]
missing_info: list[str] = Field(default_factory=list)
recommended_next_steps: list[str]
citations: list[str] = Field(
default_factory=list,
description="IDs of retrieved evidence/doc snippets that support each claim above",
)
За дапамою вызову .with_structured_output(Diagnosis) модэль змушана працаваць у гэтай абсалютна конкретной структуре пад час стварэння сваёй адпаведзі. Не існуея рэгулярных выразаў, якія моглі б паследнім часам знайсці асляпны прычыну сярод блока тэксту — калі вызов успех, тое, што вяртаецца, ўжо являе сабой об’ект з адпаведным типам, а не тэкст, які трэба інтарпретаваць.
Другое — цітаты не залежаць ад тону чы ўпэўненасці; яны павінны быць справжнімі ідэнтыфікаторамі. У інструкцыях системы чытна зазначаеца, што кожная цітата павінна адпавядаць аднаму з ID фрагментаў, якія ёй былі заданы; нічога вымышленага, і нічога, што не падтрымваецца фрагментам. Гэтыя абмежэння, здаецца, самыя по сабе должны быць достатнімі. Але гэта не так — і самэ гэтая прычына існавання наступнага этапу ў процесе обробкі.
Незалежныя перагляды: модэль пояснюе, код вырашае
Гэта, можна сказаць, найважлівейшы архітектурны выбор у всій системе.
Узел independent_review запускае другі, абсалютнаючы сабе ChatOpenAI-званак, зі сваёй власной інструкцыяй і без спакульналага контэкста з тым званакам, який дал разлік. Яго задача — адгукнуцца да таго разліку.
Але гэта ключовая частка: фактычныя рашынкі пра затверджэнне чы ўскладненне ніколі не залічваюцца на модэлю. Усё зводзіцца да трох булевых значэнняў, вырачаных у звычным Python, а выход LLM скарочваецца да для людзя чытальнага каментара, ад якога на самай працэ затверджэння фактычна не залежыць.
def independent_review(state: GraphState) -> dict:
evidence = state["evidence"]
diagnosis = state["diagnosis"]
retrieved_ids = set(state.get("retrieved_ids", []))
retrieved_scores = state.get("retrieved_scores", {})
groundedness_ok = bool(diagnosis.citations) and all(
citation_id in retrieved_ids
and retrieved_scores.get(citation_id, 0.0) >= MIN_RELEVANCE_SCORE
for citation_id in diagnosis.citations
)
risk_ok = diagnosis.severity in _ALLOWED_SEVERITIES # {"low", "medium"}
permission_ok = True # comment/label are the only writes available today
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
reasoning = llm.invoke(
[("system", _SYSTEM_PROMPT), ("human", prompt)]
).content # human-readable critique — not what the gate checks
outcome = "approve" if (groundedness_ok and risk_ok and permission_ok) else "escalate_to_human"
return {"review_result": ReviewResult(
outcome=outcome,
groundedness_ok=groundedness_ok,
risk_ok=risk_ok,
permission_ok=permission_ok,
reasoning=reasoning,
)}
Гэта загальны патэрн, якіям павінна следаваць любая надзеяная версія „LLM-ка як судды“: модэль можа сама сабе адказаць, але рашынак прыме код.
Якщо вы попросіце адзін модель оцаніць роботу іншай модэлі, а потым проста паверыце будзь-яму вердыку, які з’явіцца ў яе адпаведзе, вы фактычна створылі систему, надзяйнісць каторой обмежваецца тым самым элементам, які вы намагаецеся пераканаць.
У такім дизайне выход магчымага большога мовнага модэлі ёсць корыстным нарадаваннем для чалавечага чытальніка, але механізм, які насправды мае значэнне, не можа быть перакануты да паслядкування негатыўнага рашэння за дапамогою пераканальных фраз, таму што ён не аналізуе нічога з таго, што напісала модэль, каб выдаты рашэння.
Ішчо адна деталь, якая варта згадаць: ранг «высокай сер’ёзнасці» ніколі не з’являецца ў _ALLOWED_SEVERITIES. Кожны діагназ, пазначаны як «высокай сер’ёзнасці», автаматычна пераводзіцца людзям, незалежна ад таго, насколькі правільныя ў яго цитаты. Быць правым і быць безпечным для автаматычнага затверджэння — гэта проста не адна і тая ж рошчынь.
Баг: «grounded» не тое ж, што «relevant»
Хтоце ў практыцы справы сталі сапраўды складнымі.
Пачатковая пераканальнае groundedness_ok проста пераканвалася, чы рэквізіт ідэнтыфікацыі цитаты падходзіць да чагосьці ў отрыманай колекцыі — рэальнага ідэнтыфікатора, а не вымышленага. На паперы гэта вялікая разумная пераканальнае. Але гэтага было недастатнька.
Падчас тэставання на невялікай калекцыі з 40 проблем была перадана через весь процес обработкі справжня проблема з абсолютна пустым тэлом у React (facebook/react#36932, „experimental_taintUniqueValue выклікае RangeError для вялікіх бінарных значэнняў“). Рэзалюцыя прадала тры фрагменты, усе яны былі законныя і правильна аўтентыфікаваныя — але ніхто з іх не маў нічынага спакульна з гэтай конкрэтной бядой. Работаючы на такім слабым матэрыяле, модель усё ж такі даўла абавязковую, дзеякім чынам конкрэтную дыагнозу, якая была абсалютна некоректная: ёй прыпісвалі „праблему са сумеснасцю расшырэння React DevTools“. Кожны цітатны фрагмент чыста праўільна перажыў тэст на адпаведнасць дадзенням. Сама дыагноза застала бесполезной.
Рашэннем было перастаць вважаць „было атрымана“ заменай для „яе можна выкарыстоўваць“. Файл tools/retrieval.py быў апдэйтаваны так, каб кожны фрагмент тепер вяртаў свой показнік косай схожасці разам з текстам:
def retrieve_evidence(query: str, k: int = 3) -> list[dict]:
results = vector_store.similarity_search_with_score(query, k)
return [
{"id": doc.metadata["id"], "text": doc.page_content,
"source": doc.metadata["source"], "score": score}
for doc, score in results
]
Потым я пераканаліваў, як выглядаюць рэальныя значэння паказначыкаў схожасці ў стосунку да жывога індэксу, замест таго каб проста здогадвацца насчотка праграмы. Запит, які ў значнай меры падабяўся на справжній контэнт у корпусе, маў значэння прыблізна 0.53–0.63. Запит пра тое, чаго зовсім не было ў чатырох прыйнятых рэпозітарыях, маў значэння прыблізна 0.21–0.22. На адной з гэтых разлік independent_review.py тепер застаўляе жорсткую праграму: кожны цітаваны фрагмент должен перасягнуць MIN_RELEVANCE_SCORE = 0.35. Гэтая праграма навмысна размешчаная ближэй да верхней грані разліку, чым да сярэдзіны, таму што падпусцець хорашы діагназ імклевацца праз аднойчыну ёсць значна меншай прычыною, чым падпусцець паслабы діагназ як апраўданы.
Паралельна з выправленнем логікі ранжыравання, сам корпус дадзеных таксонаміў трэбаваўся расшырваць. Спачатку ён складаўся з 40 праблем з адного рэпазітарыя, што дала прыблізна 130 фрагментаў — настолькі маленькіх, што адна з вялікай колькасці рэальных праблем часта не мела жаднага справжняга тэматычнага адпаведніка, які можна было б знайсці. Пазней ўжо колькасць праблем была расшыроўана да адпаведна 600, якія былі запрашаны з чатырох рэальных рэпазітарыяў, што дала майже 2,750 фрагментаў.
Калі ўжо є большы корпус дадзеных, тая ж проблема taintUniqueValue тепер вяртае два асалідныя, майже дубліруючыяся абবেднання і дае точную, конкрэтную прычыну: String.fromCharCode.apply перакрочвае ліміт колькасці аргументаў у двайчыку JavaScript-двіжка пад час обробкі большых буфераў. Следавае зазначыць, што independent_review яшчэ такі случай перадае на людзкую пераглядку, адтак яго ступень сер'ёзнасці класифікуецца як высокая, і знахідкі з высокай ступенем сер'ёзнасці завжды перадаюцца на перагляд за прызначэнням, незалежна ад таго, насколькі впэўнены чы правільны є діагназ. Правільнасць не дае права на айсцення ад перагляду па критэрыях рызыку.
Абсалютна нарада звярнулася да тым, што цітата, яка паказуе рэальны ID часткі дадзеных, ёсць неабяжным, але не дастатнім умовай. Тверджэнне "модель цітавала што-небудзь" — гэта іншая фраза, чым тверджэнне "модель цітавала што-небудзь правдападобнае і актуальнае для гэтага бага";; система, яка пераканалівае толькі першае, выглядае адзёватай, хоця на самай працы проста падтрымвае бездумныя рашэння.
Этап затверджэння — гэта справжнія пауза, а не толькі візуальны статус завантажэння
Кожны з апісаных даўжэй шагоў толькі пропонуе певную дзеянне. Нічога не пішываецца назад у GitHub аж да конкрэтнага момента ў процесе обработкі. Усё, што выкананае раней — класыфікацыя, дыагназа, перагляд — толькі пропануе; нічога не пішываецца у GitHub аж да гэтага едынственнага шагу.
Узел await_approval складае точны ўпамінак (і, як гэта стосуецца, точную абазовую метку), які будзе публікуваны, выкарыстоўваючы аднойначную логіку, незалежна ад таго, чы рашэнне па проблеме было выдана через детерміністычны паводзь чы ж яна прыйшла з апраўданага діагназу ai_investigation. Пасля чого ён вызывае метод interrupt() у LangGraph:
def await_approval(state: GraphState) -> dict:
proposed_action = _build_proposed_action(state)
approved = interrupt({
"classification": state["classification"],
"proposed_action": proposed_action,
})
return {"proposed_action": proposed_action, "approved": bool(approved)}
Выклік interrupt() не проста ўтварае індыкатор „чакаюць на затверджэння“ калі процес не актыўны — ён насправды зупіняе виконанне графа пад час роботы. Ёго практычнае вярнэнне ў роботу пазней вымагае абсалютна окраснай запыткі, каб знайсці той самы зупінены адбег і продаквацый на месца, дзе ён зупініўся, за дапамогою вызову на кшталт result = await compiled_graph.ainvoke(Command(resume=True), config) з тым самым thread_id, які викорыстоўваўся пад час первінага виконання. Ёжы ўсё гэта могло бы працаваць, стан графа павінен застацца незмянным між двумя несувязанымі HTTP-запыткамі, што выключае можлівасць викорыстання звычайной памяці процеса для зберагання дадзеных.
Толькі пасля таго, як гэты резюме стварыўся, запускаецца вузел execute. Цікавыя деталі — гэта перша: перакананне ў наявнасці прав выконваецца ў самым кодзе вузла, а не выражаецца чыста як рэглі ў графе; таму, якщо ў будучыні падчас рефакторавання випадкова будзе створены штучны рэгл прымусова, які вядзе безпасцередна да execute, гэта перакананне все раве яго заспеціявае. Друга деталь — execute публікуе state["proposed_action"] абсалютна так, як ён быў затверджаны; пасля чаго ён ніколі не перзгенеруе гэты коментар. Тое, на чым падпісаўся чалавек, адразу ж і публікуецца.
def execute(state: GraphState) -> dict:
if not state.get("approved"):
raise PermissionError("execute() called without explicit approval")
evidence = state["evidence"]
action = state["proposed_action"]
token = state.get("github_token")
result = {
"comment": post_comment(evidence.repo, evidence.issue_number,
action["comment"], token=token)
}
if action.get("label"):
try:
result["label"] = add_label(evidence.repo, evidence.issue_number,
action["label"], token=token)
except requests.HTTPError as exc:
result["label_error"] = str(exc)
return {"execution_result": result}
Чаму бэкенд для зберагання дадзеных паўзаваных запускаў мае большое значэнне, чым здаецца
Пачатковая рэалізацыя выкорыстоўвала SqliteSaver, який зберагае стан у локальны файл на дыску самога бэкенда. Такая конфігурацыя працюе без проблем на комп’ютере разработчыка.
У рэальных умовах гэта выклікае проблему, яку лёгкаа пераканаць: на хосты з безплатным тарифам, такімі як Render, дыскавая памяц не застаецца стабільной пасля перзапуску. Процес завяршваецца пасля певнага часу без дзейнаў, а праз наступны запит перзапускаецца ў абсалютна новым контэйнеры.
Ось як гэта выглядае на практыцы. Аплет доходзіць да коду await_approval і зупыняецца, чакаючы дзеяння ад чалавека. Яшчэ перш чым хтось нажме «заатрымаць», безплатны экземпляр становіцца неактыўным і завяршвае сваю роботу. Наступны запит стварае новы контэйнер з абсалютна порожняю базай дадзеных. Калі запускаецца код Command(resume=...), няма чаго вярнуць — історыя зупіненага потока зникла без жадных памятак аб адзяры.
Рашэнь — это AsyncPostgresSaver, які падтрымваецца рэальнай базой даных Postgres (Neon у гэтым наладжэнні), яка існуе незалежна ад таго, у яком контэйнере запускаецца прыстрой:
async with (
AsyncPostgresSaver.from_conn_string(DATABASE_URL, serde=get_serde()) as saver,
AsyncConnectionPool(DATABASE_URL, open=False,
check=AsyncConnectionPool.check_connection) as pool),
):
Нават якщо контэйнер будзе знишчаны і паўтарна створаны з нуля, кожны призупнуты поток застаецца жывым, як толькі DATABASE_URL яшчэ вядомае да той самай базы даных. Механізмы призупнення і продажвачэння работы за дапамою interrupt() абсалютна не зміняюцца — толькі месца, дзе фактычна зберагаецца стан призупнення, змінюецца.
Варта адзінакова увага: запісы, створаны пасля затверджэння, выконваюцца під ідэнтыфікатаром таго, хто іх затвердзіў, а не пад якімісь спяльнымі крэдытамі развёрткі. Програма ў режыме роботы дазволяе будзь-каму з корыстнікаў GitHub заходзіць у систему, і кожны запіс чытачкі або запіс для таго запуску викорыстоўвае сабой OAuth-токен таго чалавека. Выклік на кшталт post_comment(evidence.repo, evidence.issue_number, action["comment"], token=token) викорыстоўвае токен затверджухальніка, а не токен таго, хто проста развёрнуў програму.
Гэта мае значэнне не толькі для правільной аутентыкацыі. Гэта ператварае тверджэнне «той, хто затвердзіў гэта, і ўпусціў гэта» на тое, што сам GitHub можа падтвердзіць, адзірнуўшыся на автара каментара, замест таго, каб проста стверджвала сам інтэрфейс программы.
Гэта таксама дазволяе існуючый системе прав на GitHub адкрываць корисныя меры контролю без якога-лібо дадатковага коду: хтось, хто проста переглядае репазітары, які ў яго няма, можа залишыць каментар, але дадзенне пазначкі вымагае прав на обробку або права на зберагчэнне ў тым репазітары. execute.py граматычна справляецца з гэтым — няудачная спроба дадзець пазначку лічыцца частковым успехам, а не прыводзіць да збійвання всей запыткі.
Запыткі да LLM і модэлю embedding все равно выконваюцца за дапамою сэрвісных канаў власніка размешчэння, незалежна ад таго, хто іх запускае, і самэ гэта є прычыной наявнасці дзейневага ліміту па корыстніку, каб контролаваць гэтыя витраты.
Є асэнцыя, пра якую варта чыткаа сказаць: ацэнка регрэсіі і ацэнка можлівасцяў — это тэставанне абсалютна разных рэчаў, і ўзяць іх за адно — глыткія памылка, у якую лёгка запасть. Прыорітэт маршрутацыі ўнутрь classify(), логіка блаката ўнутрь independent_review() і перагляд прав наўладжэння ўнутрь execute() кожны маюць толькі адну правильную дзеяння, і ўжо ўсё, адносна такіх тэстаў прыйнятны паказальнік успеху — 100 працэнтав, точка. Блакат безпекі, які будзе абыякава працаваць хоць раз пад час бяльш-менш большага колькасці запускаў, — глыткія неудача; гэта не цифра, яку можна згладзіць за дапамогою сярэднья.
Эты стандарт абсалютна разны ў прыменэнні да ацэнкі таго, чы роўна generate_diagnosis дае правільныя дыагназы. Такая ацэнка здзейсняецца па меркамі моделі, дыйсна павышаецца з часам і ніколі не могла бы прагледзець 100 процэнтав. Спрыяць гэтым двум видам пераканальства як да аднаго стандарта — гэта распашчаная пастка: «здавалася бы» працюючы захіст на самай справе не выпалоўвае свойяй функцыі.
Што на самай справе ў дзяўніне
Краща недастаткова оцэніць ситуацыю, чым ператроху яе прастрашваць, таму тут проста опис стану рэчей, а не прыемлівая праспектацыя:
Збір доказаў адбываецца за дапамою рэальных запытак GitHub REST, якія ператвараюцца ў об’екты з адпаведнымі типамі дадзеных. Класыфікацыя здзейснюецца на аднойчынных правілах, без участі LLM. Діагназа выклікаецца рэальным запытам да OpenAI, який выдае структураваны результат, адпаведны да рэальнага запошуку ў Pinecone сярод прыблізна 2,750 фрагментаў, якія пераглядаюцца ўтыкай. Незалежны адзінак — цэе окремы запыт да OpenAI, але фактычны механізм контролю — цэה код, а не власная думка моделі. Механізм людскага затверджэння — цэх рэальная пауза interrupt(), якая восстанавліваецца за дапамою Command(resume=...), і пераканальваецца байт за байтом ад початку да канца. Фронтэнд і бэкэнд абодва розмішаны — на Vercel і Render адпаведна — у цэлкам асінхронным режыме, і ўсе даны зберагаюцца у Postgres. GitHub OAuth дазволяе будзь-каму, хто заходзіць, увайсіцца сваім акаунтом; пасля чаго запыткі выкананыя як ад гэтай особы, з дзейнічанням ўсталенага дзённага ліміту. Набор для ацэнкі включае тесты на регрэсію, якія пераканальваюць справнасць механізмаў безпекі.
Гэтыя два абавеснікі не закрыты — яны ў плане розвітку, таму што яны справды є найцэннейшымі элементамі, якія трэба стварыць далей, а не таму, што іх працягнулі.
Урокі, якія варта перанесці у свой сабець проект
Стварэнне агента, які мае дзейваць у рэальных системах, адкрывае некалькі прынцыпоў, якія выходзяць за межы гэтага конкретнага інструмента для рашэння проблем у GitHub:
Зберагаеце логіку разумовых вычыслаў і ўзгэлядванняе на аднарозныя шляхі коду, а не проста на аднарозныя запытанні. Інструкцыя на кшталт «заўжды спытацца пры дзеянні» застаецца толькі правілам паведання, а такія правілы могу перастаць дзейснавацца самэ тады, калі яны найбольш неабходны. Функцыя, якая категорычна адмовляецца працаваць, якщо не ўстановлена пераканальваная пазнака, являе сабою рэальную межу, а не проста прадакт.
Калі вы кажаце, што этап перагляду є «незалежным», будзьце впэўнены, што гэта буквальна так: адзінокы вызов, без спяльных следаў, і рашэнне, якое выдае код, а не тэкст, створаны самым моделлю. Модель можа поясніць свою логіку, але яна не павинна сама ёй оцэніваць.
Не дазволяйце тверджэнню «модель паказала на ўсёрасованы элемент» заменіць сабою фразу «модель паказала на рэлевантны элемент». Перад тым, як выбраць порог схожасці, абавязкова пераканайцеся ў якасці выкарыстоўваных дадзеных на адповіднасць рэальным запытанням.
Якщо для вашайяй концепцыі важліва пауза, калі людзь у процэсе працы, неабяжна часткальна перапрацавка таго, што адбуваецца, калі працэс пачынаецца зноў да таго часу, калі людзь даае адказ. Стан у памяці і тымчасовыя дыскі будуць выглядаць нормальна пад локальнымі тэстамі, але потым зламаюцца самэ там, дзе абвалка ёсць найболей значным.
На заканчык, будзьце чыстаасаблівы ў тым, што насправдзе завершана, а шта ўсё яшчэ толькі тымчасава рашэнне. Табеля статусу, якая прызнае сваія недастаткі, здобывае больш доверлівасці, чым README-файл, які таямна натыякае, што всё зроблена.
Спадневаная літэратура
- Agentic AI Explained: From Language Models to Autonomous Agents — Структураваны апіс па шляху, якм LLM-ы ператвараюцца ў агентныя системы за дапамою інструментаў, памяці, планавання, архітектураў з калькольнікаямі агентамі і інтэграцыі MCP.