Головна / Статті / Структурні ограничення для AI-агентів: всередині конвеєра ResolveFlow

Структурні ограничення для AI-агентів: всередині конвеєра ResolveFlow

Пояснює, як агент на основі LangGraph забезпечує розділення процесів міркувань та виконання за допомогою перевірок на рівні коду замість інструкцій у запитах, включаючи помилку зі збором даних, яка виникла під час роботи.

3446 слів

Більшість демонстрацій ШІ з агентним підходом дотримуються однієї й тієї самої основної схеми: модель вирішує, яку дію виконати, і негайно її реалізує. До запиту додається інструмент, модель його активує, і інструмент працює без будь-якої додаткової перевірки. У короткому демонстраційному відео це може здаватися переконливим, але саме така конфігурація викликає занепокоєння у тих, хто розглядає можливість використання автономних систем для роботи з чимось важливим — адже часто єдиним захистом від неправильної діагностики чи шкідливих змін у робочій системі є попереджувальне речення в самому запиті.

Проєкт, описаний тут, був створений для уникнення залежності від одного-єдиного попереджувального інструкційного речення.

ResolveFlow приймає посилання на проблему у GitHub, збирає докази, класифікує проблему за категорією, а потім, виходячи з цієї категорії, робить одну з трьох речей: виконує фіксовану, незмінну дію, розпочинає розслідування за допомогою ШІ, ґрунтоване у зібраних доказах, або надсилає проблему безпосередньо до людського рецензента. Замість одноконтурної структури, де модель ініціює виклик інструменту, цей процес організований як станова машина LangGraph, побудована навколо однієї центральної ідеї:

Міркування та виконання залишаються розділеними через спосіб побудови системи, а не через правила, яких від неї вимагають.

Це означає, що справа не просто у тому, щоб наказати моделі спочатку перевірити щось у когось. Натомість другий, незалежний виклик LLM аналізує та оцінює діагноз першої моделі ще до того, як його побачить людина. А єдина функція в усьому кодовому базі, якій дозволено записувати дані назад у GitHub, перевіряє наявність спеціального флага схвалення в межах власної логіки — не тому, що граф очікує такого маршрутування викликів, а тому, що сама функція відмовиться виконуватися без цього флага, навіть якщо майбутня зміна коду створить прямий шлях, який пропустить звичайні кроки.

Решта цього посібника присвятована тому, як насправді складається система, використовуючи реальну реалізацію, а також багу, який з’явився під час розробки. Цей баг є корисним уроком: модель, яка посилається на справжній джерельний документ, — це не те саме, що модель, яка посилається на джерело, що дійсно стосується поставленого запитання.

Структура конвеєру

Шість етапів виконуються послідовно, і лише одному з них дозволено змінювати щось поза самим конвеєром:

  1. fetch_evidence — фактичні виклики REST API GitHub для отримання тексту тіла проблеми, її коментарів та результатів будь-яких перевірок CI.
  2. normalize_evidence — непроцесований JSON, отриманий від цих викликів, перевіряється та перетворюється на об’єкт типу IssueEvidence.
  3. classify — легкий етап, заснований на правилах, який не використовує жодних викликів LLM.
  • generate_diagnosis — запускається лише тоді, коли етап класифікації визначає, що проблему потрібно детальніше дослідити. Це виклик LLM у поєднанні з контекстом, отриманим шляхом пошуку, та результатом, обмеженим визначеною схемою.
  • independent_review — другий, окремий виклик LLM, який критично оцінює результат першого виклику, ґрунтуючись на умовах «пройшло/не пройшло», обчислених у звичайному коді, а не на суб’єктивних оцінках.
  • await_approval → execute — це момент, коли процес зупиняється у очікуванні рішення людини, а лише за умови схвалення — виконується вузол, який має дозвіл опублікувати щось назад у GitHub.
  • У наступних розділах розглядаються найважливіші аспекти.

    Класифікація: навмисно не є викликом 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"}
    

    Цей крок дає три можливі результати, кожен з яких вимагає різного рівня довіри на наступних етапах:

    1. детерміністичний — невдалий перевірку CI є чітким, механічним сигналом сам по собі. Не потрібно нічого досліджувати; проблема просто позначається та передається відповідальній особі.
  • ai_investigation — використовується, коли проблема не містить достатньо деталей для негайних дій. Саме тут здібності мовної моделі стають справді корисними, адже їй потрібно з’ясувати, що насправді запитується.
  • human_review — призначений для всього неясного або того, що може мати значний вплив у разі неправильного оброблення. Замість того, щоб дозволити системі здогадуватися в таких випадках, проблема передається безпосередньо людині.
  • Варто зазначити, що 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, а результат роботи ШІ-моделі перетворюється на коментар, зрозумілий людині, від якого насправді не залежить логіка схвалення.

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

    Це загальна схема, яку має дотримуватися будь-яка надійна версія концепції „ШІ-суддя“: модель може пояснити свої дії, але рішення приймає код.

    Якщо ви попросите одну модель оцінити роботу іншої моделі, а потім просто довіритеся будь-якому вердикту, який вона надасть у своїй відповіді, ви фактично створите систему, надійність якої обмежується саме тим, що ви намагаєтесь перевірити.

    У такій схемі результат роботи ШІ є корисним описом для людського читача, але критерій, який насправді має значення, не можна спонукати до негативного результату за допомогою переконливих формулювань, оскільки він не аналізує те, що написала модель, для прийняття рішення.

    Ще одна деталь, на яку варто звернути увагу: рівень серйозності «високий» ніколи не зустрічається у списку _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 та моделі ембеддингу все одно виконуються за допомогою власних API-ключів власника розгортання, незалежно від того, хто їх ініціює, і саме тому існує щоденний ліміт на кількість запитів на користувача, щоб стримати ці витрати.

    Існує важлива різниця, про яку варто чітко зазначити: оцінка регресії та оцінка функціональних можливостей — це тестування абсолютно різних аспектів, і їх оцінювати однаково — це помилка, у яку легко впасти. Порядок обробки запитів у функції classify(), логіка контролю у функції independent_review() та перевірка дозволів у функції execute() кожна має лише одну правильну форму поведінки, і єдиний прийнятний рівень проходження таких тестів — це 100 відсотків, точка. Безпековий бар’єр, який може бути обійдений навіть один раз під час будь-кількох виконань, є критичною несправністю — це не показник, який можна скоригувати шляхом усереднення.

    Цей стандарт повністю відрізняється від оцінки того, чи створює generate_diagnosis якісні діагнози. Такий тип оцінки здійснюється моделлю, справді покращується з часом та ніколи не мав реальних шансів досягти 100 відсотків. Вважати обидва типи перевірок еквівалентними — це поширена помилка: захисний механізм, який „переважно“ працює, насправді зовсім не виконує своїх функцій.

    Що насправді є справжнім зараз

    Краще недооцінювати ситуацію, ніж перебільшувати її, тому ось такий чесний опис реального стану речей, а не просування продукту:

    Збір даних здійснюється за допомогою справжніх запитів GitHub REST, які перетворюються на об’єкти визначеного типу. Класифікація ґрунтується на правилах та є детермінованою, без участі LLM. Діагноз формується за результатами справжнього запиту до OpenAI, який генерує структурований вихідний даний, заснований на реальному пошуку в Pinecone серед приблизно 2 750 фрагментів, які щотижня оновлюються. Незалежний огляд здійснюється через окремий запит до OpenAI, але фактичним механізмом контролю є код, а не власна думка моделі. Механізм людського схвалення — це справжнє призупинення через interrupt(), яке відновлюється за допомогою Command(resume=...), з перевіркою кожного байту від початку до кінця. Фронтенд та бекенд розгорнуті — відповідно на Vercel та Render — у повністю асинхронному режимі, з зберіганням стану в Postgres. GitHub OAuth дозволяє будь-якому відвідувачеві увійти за допомогою власного облікового запису; операції запису виконуються від імені цієї особи з щоденним лімітом. Набір інструментів для оцінки включає тести на регресію механізмів безпеки, які перевіряють

    Вимагається 100-відсотковий рівень успішності, а оцінювання здійснюється за кодом. Оцінка якості діагнозів ще не створена. Тести у звичайному розумінні ще не написані.

    Ці дві проблеми не приховуються — вони включені до плану розвитку, тому що саме вони є найбільш цінними для подальшої роботи, а не тому, що їх проігнорували.

    Уроки, які варто застосувати у власному проекті

    Створення агента, призначеного для дії на реальних системах, виявляє низку принципів, які виходять за межі цього конкретного інструменту для роботи з проблемами в GitHub:

    Тримайте міркування та виконання у окремих шляхах коду, а не лише у окремих запитах. Інструкція на кшталт „завжди запитуйте перед дією“ залишається лише правилом поведінки, а такі правила іноді порушуються саме у той момент, коли вони найбільше потрібні. Функція, яка категорично відмовляється працювати, якщо не встановлений певний флаг, є справжньою межею, а не просто порадою.

    Коли ви кажете, що етап перевірки є „незалежним“, переконайтеся, що це справді так: окремий виклик, відсутність спільного сліду та вердикт, який формується кодом, а не текстом, створеним самим моделлю. Модель може пояснити свої міркування, але вона не повинна бути тією, хто оцінює їх.

    Не дозволяйте твердженню „модель вказала на щось реальне“ замінювати „модель вказала на щось релевантне“. Перед тим, як встановити поріг схожості, обов’язково оцініть якість пошуку на основі реальних запитів.

    Якщо для вашого проекту важлива пауза через участь людини, чітко протестуйте, що відбувається, коли процес запускається знову до того, як людина відреагує. Стан у пам’яті та тимчасові диски можуть здаватися справними під час локальних тестів, але потім зламатися саме там, де це найбільш критично.

    Нарешті, будьте відверті щодо того, що справді завершено, а що все ще є тимчасовим рішенням. Таблиця статусу, яка визнає свої прогалини, заслуговує більшої довіри, ніж README, який тихо натякає, що все зроблено.

    Пов’язана література

    • Agentic AI Explained: From Language Models to Autonomous Agents — Структурований огляд того, як LLM перетворюються на агентські системи за допомогою інструментів, пам’яті, планування, архітектур багатьох агентів та інтеграції MCP.
  • Розуміння AI-агентів: цілі, інструменти, пам’ять та цикл агента — просте пояснення для початківців про те, чим AI-агенти відрізняються від чат-ботів, з описом основних компонентів, циклу прийняття рішень, рівнів автономії та прикладів їх використання у реальному світі.