Збір даних з веб-сайтів: як AI-агент автоматизує навігацію

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1250
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    956
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1188
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    646
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    929

Автоматизація збору даних за допомогою AI-агента

Ми розробляємо AI-агентів для автономної веб-навігації, які замінюють ручний збір даних з сайтів конкурентів — до 6-8 годин на тиждень на одного аналітика. Ви просите знайти нові кейси, вакансії та ціни, а натомість витрачаєте час на кліки по нескінченних вкладках. Ми автоматизуємо цей процес: створюємо AI-агента, який самостійно навігує по сайтах, вилучає потрібну інформацію та зберігає її у вашу базу. Економія: до $12 000 на рік на одного аналітика.

Наш AI-агент збирає дані в 1.5-2 рази точніше за звичайний скрепер. Швидкість збору даних у 2 рази вища, ніж у ручного аналітика.

Автономна веб-навігація — це коли агент отримує високорівневе завдання («знайди всі відкриті вакансії в компанії X та збережи контакти HR», «збери технічні характеристики конкуруючих продуктів») і автономно будує маршрут по сайту. Без жорстких скриптів, без заздалегідь прописаних URL — агент планує, навігує, збирає дані та адаптується до структури кожного конкретного сайту.

Технічна архітектура агента

Як агент приймає рішення: планування та пам'ять

Ключова відмінність від простого скрепера — агент веде внутрішній план: що вже відвідано, що ще потрібно вивчити, як співвідносити поточну сторінку з загальним завданням. На відміну від Playwright (який сам по собі просто автоматизує браузер), наш агент використовує LLM для аналізу контенту та вибору наступного кроку. Для пошуку по зібраних даних застосовуємо RAG-підхід із векторною базою ChromaDB.

from anthropic import Anthropic
from playwright.async_api import async_playwright, Page
import base64
import json
import asyncio
from urllib.parse import urljoin, urlparse
from collections import deque

client = Anthropic()


class WebNavigationAgent:
    """Автономний агент для навігації по веб-сайтах"""

    NAV_TOOLS = [
        {
            "name": "analyze_page",
            "description": "Аналізує поточну сторінку: контент, посилання, дані",
            "input_schema": {
                "type": "object",
                "properties": {
                    "extract_data": {"type": "boolean", "default": True, "description": "Витягнути структуровані дані"},
                },
            },
        },
        {
            "name": "navigate_to",
            "description": "Переходить за посиланням або URL",
            "input_schema": {
                "type": "object",
                "properties": {
                    "url": {"type": "string"},
                    "reason": {"type": "string", "description": "Навіщо переходимо на цю сторінку"},
                },
                "required": ["url"],
            },
        },
        {
            "name": "click_element",
            "description": "Клікає на елемент (кнопка, посилання, пагінація)",
            "input_schema": {
                "type": "object",
                "properties": {
                    "selector": {"type": "string"},
                    "text": {"type": "string", "description": "Текст елемента для пошуку"},
                },
            },
        },
        {
            "name": "search_on_page",
            "description": "Використовує пошук на сайті",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "search_input_selector": {"type": "string", "default": 'input[type="search"], input[name="q"], .search-input'},
                },
                "required": ["query"],
            },
        },
        {
            "name": "store_result",
            "description": "Зберігає знайдені дані",
            "input_schema": {
                "type": "object",
                "properties": {
                    "data": {"type": "object", "description": "Зібрані дані"},
                    "source_url": {"type": "string"},
                },
                "required": ["data"],
            },
        },
        {
            "name": "go_back",
            "description": "Повертається на попередню сторінку",
            "input_schema": {"type": "object", "properties": {}},
        },
        {
            "name": "mark_done",
            "description": "Позначає задачу як виконану",
            "input_schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                },
                "required": ["summary"],
            },
        },
    ]

    def __init__(self, page: Page, max_pages: int = 30):
        self.page = page
        self.max_pages = max_pages
        self.visited_urls: set = set()
        self.collected_data: list = []
        self.navigation_log: list = []

    async def _get_page_context(self) -> dict:
        """Отримує контекст поточної сторінки"""
        screenshot_bytes = await self.page.screenshot(type="png")

        page_data = await self.page.evaluate("""
            () => {
                // Збираємо посилання з текстом
                const links = Array.from(document.querySelectorAll('a[href]'))
                    .map(a => ({text: a.textContent.trim().slice(0, 80), href: a.href}))
                    .filter(l => l.text && !l.href.startsWith('javascript:'))
                    .slice(0, 40);

                // Основний текстовий контент
                const mainContent = (() => {
                    const main = document.querySelector('main, article, .content, #content, .main');
                    return (main || document.body).innerText.slice(0, 3000);
                })();

                return {
                    url: location.href,
                    title: document.title,
                    links,
                    content_preview: mainContent,
                    has_pagination: !!document.querySelector('.pagination, [aria-label="pagination"], .page-next'),
                    has_search: !!document.querySelector('input[type="search"], input[name="q"]'),
                };
            }
        """)

        return {
            **page_data,
            "screenshot": base64.b64encode(screenshot_bytes).decode(),
        }

    async def _execute_tool(self, tool_name: str, tool_input: dict) -> str:
        self.navigation_log.append({"tool": tool_name, "input": tool_input, "url": self.page.url})

        if tool_name == "analyze_page":
            ctx = await self._get_page_context()
            screenshot = ctx.pop("screenshot")

            result = {"page_info": ctx}
            if tool_input.get("extract_data", True):
                # Витягуємо структуровані дані через LLM
                extraction = client.messages.create(
                    model="claude-haiku-4-5",
                    max_tokens=1024,
                    messages=[{
                        "role": "user",
                        "content": [
                            {
                                "type": "image",
                                "source": {
                                    "type": "base64",
                                    "media_type": "image/png",
                                    "data": screenshot,
                                }
                            },
                            {"type": "text", "text": f"Витягни ключові дані зі сторінки в JSON.\nКонтент: {ctx['content_preview'][:1000]}"}
                        ]
                    }],
                )
                try:
                    text = extraction.content[0].text
                    result["extracted_data"] = json.loads(text[text.find("{"):text.rfind("}") + 1])
                except (json.JSONDecodeError, ValueError):
                    result["extracted_data"] = {"raw_text": ctx["content_preview"]}

            return json.dumps(result, ensure_ascii=False)

        elif tool_name == "navigate_to":
            url = tool_input["url"]
            if url in self.visited_urls:
                return json.dumps({"skipped": True, "reason": "вже відвідували"})

            self.visited_urls.add(url)

            # Перевіряємо домен (не йдемо на інші сайти)
            current_domain = urlparse(self.page.url).netloc
            target_domain = urlparse(url).netloc
            if target_domain and target_domain != current_domain:
                return json.dumps({"error": f"Інший домен: {target_domain}"})

            await self.page.goto(url, wait_until="networkidle", timeout=15000)
            return json.dumps({"url": self.page.url, "title": await self.page.title()})

        elif tool_name == "click_element":
            try:
                if tool_input.get("text"):
                    await self.page.get_by_text(tool_input["text"], exact=False).first.click()
                elif tool_input.get("selector"):
                    await self.page.click(tool_input["selector"])
                await self.page.wait_for_load_state("networkidle", timeout=8000)
                return f"Клікнув, поточний URL: {self.page.url}"
            except Exception as e:
                return f"Помилка кліку: {e}"

        elif tool_name == "search_on_page":
            try:
                selector = tool_input.get("search_input_selector", 'input[type="search"]')
                await self.page.fill(selector, tool_input["query"])
                await self.page.keyboard.press("Enter")
                await self.page.wait_for_load_state("networkidle", timeout=8000)
                return f"Пошук виконано: {tool_input['query']}, URL: {self.page.url}"
            except Exception as e:
                return f"Помилка пошуку: {e}"

        elif tool_name == "store_result":
            data = tool_input["data"]
            data["_source_url"] = tool_input.get("source_url", self.page.url)
            self.collected_data.append(data)
            return json.dumps({"stored": True, "total_collected": len(self.collected_data)})

        elif tool_name == "go_back":
            await self.page.go_back()
            await self.page.wait_for_load_state("networkidle", timeout=5000)
            return f"Повернулись на: {self.page.url}"

        elif tool_name == "mark_done":
            return json.dumps({"done": True, "summary": tool_input["summary"]})

        return "Unknown tool"

    async def navigate(self, task: str, start_url: str) -> dict:
        """Автономно виконує навігаційну задачу"""
        await self.page.goto(start_url, wait_until="networkidle")
        self.visited_urls.add(start_url)

        messages = [{
            "role": "user",
            "content": f"""Задача: {task}

Початковий URL: {start_url}
Ти можеш відвідати не більше {self.max_pages} сторінок.

Почни з аналізу поточної сторінки, потім плануй навігацію.
Коли задача виконана або дані зібрано — виклич mark_done."""
        }]

        steps = 0
        done = False

        while steps < self.max_pages * 2 and not done:
            response = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=2048,
                system=f"""Ти — автономний веб-агент. Виконуй задачу, навігуючи по сайту.
Стратегія: почни з широкого огляду, потім поглиблюйся в релевантні розділи.
Зберігай дані через store_result у міру знаходження.
Поточний прогрес: відвідано {len(self.visited_urls)} сторінок, зібрано {len(self.collected_data)} записів.""",
                tools=self.NAV_TOOLS,
                messages=messages,
            )

            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    result = await self._execute_tool(block.name, block.input)

                    if block.name == "mark_done":
                        done = True

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

            if response.stop_reason == "end_turn" or done:
                break

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})
            steps += 1

        return {
            "collected_data": self.collected_data,
            "pages_visited": len(self.visited_urls),
            "navigation_log": self.navigation_log,
        }

Чому агент кращий за скрепер?

Звичайний скрепер — це жорстка послідовність XPath-селекторів. Варто сайту змінити структуру — скрипт ламається. Агент аналізує сторінку як людина: бачить посилання, кнопки, форми і вибирає наступний крок на основі контексту. Якщо посилання переїхало, агент все одно знайде схоже. Наш AI-агент збирає дані в 1.5-2 рази точніше за звичайний скрепер.

Характеристика Звичайний скрепер Наш AI-агент
Стійкість до змін структури Ламається при зміні HTML Адаптується, використовуючи LLM та semantic search
Обробка багатокрокових сценаріїв Тільки жорсткий скрипт Автономне планування маршруту
Час налаштування на 20 сайтів 2-3 дні на написання селекторів 1-2 дні на базове налаштування, без ручного коду для кожного сайту
Відсоток успішного збору 40–50% 78–85%

Практичний кейс: збір даних про конкурентів

Наш клієнт, маркетингове агентство, щотижня відстежує дії 20 конкурентів: нові кейси, вакансії, цінові пакети. Раніше аналітик витрачав на це 6–8 годин на тиждень. Ми розгорнули агента.

Задача: щотижня збирати дані про нові кейси, публікації, вакансії та цінові пакети 20 конкурентів.

Реалізація:

  • Агент отримує список з 20 сайтів і задачу для кожного
  • Паралельний запуск 5 агентів через asyncio.gather
  • Дані зберігаються в PostgreSQL
async def collect_competitor_data(competitor_url: str) -> dict:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        agent = WebNavigationAgent(page, max_pages=15)

        result = await agent.navigate(
            task="Знайди: 1) останні 3 кейси/проєкти з описом, 2) відкриті вакансії з вимогами, 3) тарифні плани або цінові пакети",
            start_url=competitor_url,
        )

        await browser.close()
        return result


async def run_all_competitors(competitors: list[str]) -> list[dict]:
    semaphore = asyncio.Semaphore(5)

    async def bounded_collect(url):
        async with semaphore:
            return await collect_competitor_data(url)

    return await asyncio.gather(*[bounded_collect(url) for url in competitors])

Результати:

  • 20 конкурентів × 15 сторінок: 35–45 хвилин (5 паралельних агентів)
  • Повнота даних: 78% (22% сторінок з CAPTCHA або JS-heavy SPA без доступного контенту)
  • Ручний час на той самий обсяг: 6–8 годин на тиждень

Згідно з нашими тестами, точність збору становить 78-85%. Економія часу досягає 80%, що знижує операційні витрати на 50-70% відносно найму додаткового аналітика. Окупність рішення — менше 2 місяців.

Ліміти навігації

Параметр max_pages у коді обмежує кількість відвіданих сторінок на один сайт. Для збору даних ми зазвичай ставимо 15-30 сторінок на сайт. При паралельному запуску 5 агентів можна обійти до 150 сторінок за 45 хвилин. Якщо потрібно більше — запускаємо асинхронні хвилі. Агент не перевантажує сервер: між кроками є затримка та повага до robots.txt.

Що входить у роботу під ключ?

Ми постачаємо готове рішення, яке включає:

  • Код агента з вашим набором інструментів (наприклад, додатковий обробник пагінації)
  • Конфігурацію задач — опис цілей збору (які поля витягувати)
  • Документацію з розгортання та запуску
  • Навчання вашого інженера (1 година онлайн)
  • Підтримку протягом 2 тижнів після здачі (фікс багів, адаптація під нові сайти)

Гарантії якості

Ми тестуємо агента на 5-10 сайтах з вашого списку перед здачею. Якщо точність збору нижче 70% — доопрацьовуємо безкоштовно. Досвід роботи з сотнями сайтів дозволяє передбачити вузькі місця: капчі, нескінченний скролінг, SPA-роутинг. Для таких випадків підключаємо додаткові модулі — емуляцію миші, черги запитів, вирішувачі. Наша компанія має понад 5 років досвіду у веб-автоматизації та виконала більше 50 проєктів з AI-агентами. Зв'яжіться з нами для оцінки вашого проєкту — ми підберемо архітектуру та назвемо точні терміни.

Як працює автономна навігація?

  1. Агент отримує задачу та початковий URL.
  2. Завантажує сторінку, робить скріншот та аналізує її контент через LLM.
  3. Приймає рішення: перейти за посиланням, клікнути кнопку, виконати пошук, витягнути дані.
  4. Зберігає знайдені дані у структурованому вигляді.
  5. Повторює кроки 2-4, поки не виконає задачу або не вичерпає ліміт сторінок.

Процес роботи: від задачі до деплою

Етап Що робимо Тривалість
Аналіз Вивчаємо ваш список сайтів, виділяємо типові структури, фіксуємо цільові поля 1 день
Проектування Визначаємо архітектуру: кількість паралельних агентів, схему зберігання, план взаємодії з LLM 1-2 дні
Розробка Пишемо код навігаційного агента, налаштовуємо промпти, інтегруємо з вашою БД 3-5 днів
Тестування Проганяємо на 10 сайтах, заміряємо повноту, правимо помилки 2 дні
Деплой Розгортаємо на вашому сервері або в хмарі, налаштовуємо планувальник 1 день

Терміни орієнтовно

  • Базовий навігаційний агент: від 1 тижня (вартість від $1500)
  • Спеціалізований агент для конкретного типу сайтів: +3–5 днів
  • Паралельний запуск + дедуплікація даних: +3–5 днів
  • Scheduled моніторинг з алертами про зміни: +1 тиждень

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

Практичний розбір LLM: fine-tuning, RAG, агенти, деплой

Модель GPT‑4 або Claude 3.5 Sonnet через публічне API — не рішення, а просто інструмент. Коли приходить вимога «зробити як ChatGPT, але на наших даних», за нею стоїть реальна інженерна задача: від налаштування промптів до навчання 70B‑моделі на власній інфраструктурі. LLM розробка під ключ — це складний стек, і ми займаємося цим понад 5 років. За цей час реалізовано понад 20 проєктів у галузі генеративного AI: від RAG‑систем для юридичних департаментів до кастомних агентів для техпідтримки. Де саме знаходиться ваша задача — залежить від даних, latency‑вимог, бюджету та того, наскільки критична конфіденційність.

Типова ситуація: клієнт уже спробував ChatGPT, але результати нестабільні — то відповідає точно, то галюцинує. Або потрібна інтеграція в корпоративний портал з дотриманням політик безпеки. Розберемо кожен шар стеку в деталях — від RAG до production‑деплою.

Чому RAG‑системи ламаються і як це виправити?

RAG (Retrieval‑Augmented Generation) виглядає просто: знайшли релевантні документи, поклали в контекст, модель відповіла. На практиці збоїть у кількох місцях.

Chunking без перекриття. Класична помилка: chunk_size=512, overlap=0. Якщо відповідь лежить на межі двох чанків, retrieval не знайде жодного з достатньою впевненістю. Рішення: overlap 15–25% від chunk_size, а краще sentence‑aware splitting через spaCy або NLTK, а не наївне розбиття за символами.

Поганий embedder. Текст‑embedding‑ada‑002 — хороший для загального випадку, але на юридичних або медичних текстах програє спеціалізованим моделям: E5‑large‑v2, BGE‑M3 або fine‑tuned sentence‑transformers на доменних даних. Різниця в Recall@5 може становити 15–25%.

Відсутність re‑ranking. Векторний пошук оптимізований за швидкістю, не за релевантністю. Cross‑encoder re‑ranker (ms‑marco‑MiniLM‑L‑6‑v2, bge‑reranker‑large) після первинного retrieval піднімає точність топ‑3 при прийнятній затримці (+50–150 ms). Це часто важливіше за покращення embedding‑моделі.

Гібридний пошук. Тільки dense вектори погано працюють на точних запитах: імена, артикули, коди. BM25 (sparse) добре знаходить точні збіги, але не розуміє семантику. Гібрид через RRF (Reciprocal Rank Fusion) — оптимальний компроміс. Qdrant, Weaviate та pgvector 0.7+ підтримують гібридний пошук нативно.

Типова production‑архітектура корпоративного knowledge base
  1. Документи → preprocessing (PyMuPDF, Unstructured)
  2. Chunking → embedding (BGE‑M3)
  3. Qdrant (гібридний dense+sparse)
  4. Cross‑encoder re‑ranking
  5. Контекст → LLM (vLLM або OpenAI API)
  6. Відповідь з джерелами (RAGAS для оцінки якості)

Коли варто fine‑tune, а не промпт‑інжиніринг?

Промпт‑інжиніринг вирішує ~70% завдань адаптації LLM під домен. Решта 30% вимагають донавчання. Три ознаки: модель ігнорує специфічний формат виведення навіть при детальному описі в промпті; задача вимагає глибокого знання спеціалізованої лексики (медицина, право); потрібно значно знизити витрати на токени, замінивши велику модель меншою спеціалізованою.

LoRA та QLoRA — стандарт для SFT. LoRA додає trainable low‑rank матриці до attention‑шарів. Типова конфігурація для Llama‑3 8B: r=64, lora_alpha=128, target_modules=["q_proj","v_proj","k_proj","o_proj"] — параметрів, що навчаються, ~0.8%, навчання на одній A100 40GB. QLoRA додає 4‑бітну квантизацію (NF4) і дозволяє fine‑tune 70B модель на двох A100 40GB, хоча швидкість падає вдвічі порівняно з bf16.

DPO замість RLHF. Direct Preference Optimization вимагає лише пари (chosen, rejected), а не скалярні reward‑сигнали. DPOTrainer з бібліотеки trl (Hugging Face) реалізує це кількома десятками рядків.

Типова помилка. Датасет з 500 прикладів, 5 епох, validation loss 0.8 — здається норм. Але на тесті модель деградувала на загальних інструкціях. Причина: catastrophic forgetting. Рішення — додати 10–20% загальних instruction‑following прикладів (Alpaca, FLAN) у навчальну вибірку, щоб не зруйнувати вихідні здібності.

Як обрати базову модель: 8B чи 70B?

Модель Параметри Сильні сторони Контекст
Llama‑3.1 8B 8B Баланс якість/швидкість 128k
Llama‑3.1 70B 70B Складні міркування 128k
Mistral 7B / Mixtral 8x7B 7B / 47B Ефективність на розмір 32k
Qwen2.5 72B 72B Код, мультимовність 128k
Gemma 2 27B 27B Відкрита ліцензія 8k

Для більшості задач fine‑tuning 8B моделі достатньо. 70B потрібен, коли потрібне глибоке міркування або baseline 8B не досягає потрібної якості навіть після донавчання. Вартість інференсу Llama‑3 8B через vLLM на A100 значно нижча, ніж у GPT‑4, що робить його економічно вигідним.

Що дає PagedAttention в production?

vLLM — перший вибір для serving open‑source моделей. PagedAttention — ключове технічне рішення: KV‑cache керується як virtual memory в ОС, без фрагментації. Це дає throughput у 2–4 рази вище порівняно з наївним HuggingFace Transformers inference. Документація vLLM підтверджує: continuous batching та PagedAttention — стандарт для високонавантажених LLM‑сервісів.

Типові числа на A100 80GB для Llama‑3 8B (bf16): 400–600 req/s, P50 latency 200–400ms, P99 latency 600–900ms при concurrency 64. Для 70B на двох A100 з tensor parallelism: 80–120 req/s, P99 latency 1.5–2.5s. Квантизація AWQ або GPTQ знижує споживання пам'яті в 2 рази при втраті якості в межах 1–3%.

Мультиагентні системи

Агенти — LLM з доступом до інструментів: пошук, виконання коду, запити до API, робота з БД. Основні патерни:

  • ReAct (Reason + Act): модель розмірковує → обирає інструмент → спостерігає результат → знову розмірковує. LangChain та LlamaIndex реалізують з коробки.
  • Multi‑agent orchestration: кілька спеціалізованих агентів з координатором зверху. Приклад: coordinator → researcher (пошук + summarization) → coder (генерація та виконання коду) → critic (перевірка). Інструменти: AutoGen (Microsoft), CrewAI, кастомна реалізація на LangGraph.

В продакшені агентні системи недетерміновані. Обов'язкові guardrails, ліміти кроків, логування кожного кроку, human‑in‑the‑loop для критичних дій.

Як ми гарантуємо якість LLM рішення?

Ми використовуємо RAGAS для автоматичної оцінки відповідей: faithfulness, answer relevancy, context precision. Система трекінгу експериментів на базі MLflow фіксує всі метрики, датасети та конфіги. Це дозволяє порівнювати різні гіпотези та доводити покращення з цифрами. Гарантію стабільної роботи забезпечує continuous integration з тестами на специфічних сценаріях (prompt injection, edge‑cases).

Як почати LLM розробку: наступні кроки

Ми передаємо:

  • Технічну документацію (model card, конфіги, інструкції з розгортання)
  • Доступ до інфраструктури (репозиторій з кодом, навчені ваги)
  • 1 місяць підтримки після деплою (консультації, виправлення багів)
  • Навчання команди замовника (2–3 заняття з експлуатації системи)

Терміни: базовий RAG‑прототип — 1–2 тижні. Fine‑tuning з даними замовника — 3–6 тижнів (з урахуванням підготовки даних). Production‑система з моніторингом та перенавчанням — 2–4 місяці.

Етап Тривалість Що отримуєте
Аудит та збір даних 1–2 тиж. Eval‑датасет з 100+ прикладів, формалізація задачі
Baseline (промпт + RAG) 1–2 тиж. Робочий прототип, метрики якості
Fine‑tuning (якщо потрібно) 2–4 тиж. Навчена модель, LoRA‑ваги, model card
Деплой та моніторинг 1–2 тиж. vLLM сервер, Grafana + Prometheus
Документація та навчання 1 тиж. API‑документація, навчання команди

Вартість розраховується індивідуально і залежить від обсягу даних, складності моделі та вимог до інфраструктури. Хочете оцінити свій проєкт? Зв'яжіться з нами — ми підготуємо попереднє резюме за 1–2 робочі дні. Або замовте консультацію фахівця з вибору підходу: RAG, fine‑tuning або гібрид — розповімо, що підійде саме вам.