Agent Chat: два AI агенти що сперечаються — Spring Boot 4 + Spring AI + Ollama / OpenRouter

Actualizado:
Agent Chat: два AI агенти що сперечаються — Spring Boot 4 + Spring AI + Ollama / OpenRouter

Що буде якщо дати двом AI протилежні переконання і змусити їх сперечатись на задану тему? Саме це питання стало відправною точкою для Agent Chat — експерименту де два агенти з різними характерами ведуть діалог в реальному часі, підкріплюючи аргументи реальними фактами з Wikipedia, Tavily, NewsAPI, ArXiv і Alpha Vantage.

⚠️ Важливо про архітектуру: Agent Chat — це експериментальний проєкт зроблений навмисно просто щоб швидко перевірити як поводяться агенти з різними характерами. Polling замість WebSocket, синхронне читання з БД у циклі, відсутність черг — все це свідомі компроміси заради простоти запуску і читабельності коду. Для production multi-agent системи архітектура потребує доопрацювання. 💡 Рекомендація: запускайте локально з Ollama — це повністю безкоштовно. Жодних API ключів, жодних витрат. qwen3:8b достатньо щоб побачити живий діалог агентів з реальними фактами з Wikipedia і ArXiv.
GitHub: github.com/VadimKharovyuk/Agent_Chat — MIT ліцензія, повний код, README з інструкціями запуску.

Зміст

Ідея і як це виглядає в дії

Класична проблема AI-агентів — вони занадто ввічливі. Попроси GPT посперечатись — він погодиться через два повідомлення. Agent Chat вирішує це через системні промпти з жорсткими заборонами і чіткими переконаннями для кожного агента.

Флоу виглядає так:

Тема → Агент A відповідає → Агент B відповідає → Агент A ...

Користувач задає:

  • Тему — наприклад «Чи варто регулювати AI державою?»
  • Системний промпт для Агента A — роль, переконання, заборони, формат
  • Системний промпт для Агента B — протилежна позиція
  • Кількість раундів — 1 раунд = 2 повідомлення (A + B)

Агенти ведуть діалог автоматично. Кожне повідомлення зберігається в PostgreSQL. Розмову можна зупинити в будь-який момент. Агент також може самостійно завершити діалог — якщо його відповідь містить стоп-фразу типу «до свидания» або «farewell».

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

Якщо у вас вже встановлені інші моделі в Ollama — можете спробувати їх. Але з досвіду: не всі моделі добре слідують інструкціям системного промпту. Деякі ігнорують заборони і починають погоджуватись з опонентом вже через 2-3 раунди, інші не викликають tools взагалі. Детальніше які моделі реально працюють на локальному залізі — читайте у статті про Ollama на 8GB RAM.

Стек: Spring Boot 4, Spring AI 2.0, Ollama і OpenRouter

Компонент Технологія Навіщо
Backend Java 21, Spring Boot 4.0.6 Основний фреймворк
AI Framework Spring AI 2.0.0-M5 Абстракція над LLM провайдерами
LLM (local) Ollama (qwen3:8b / llama3.1:8b) Локальна розробка без витрат
LLM (prod) OpenRouter (deepseek/deepseek-chat) Хмарний провайдер для Railway
Database PostgreSQL Зберігання розмов і повідомлень
Frontend Thymeleaf + Bootstrap 5 Простий UI без окремого SPA
Tools Wikipedia, Tavily, NewsAPI, Alpha Vantage, ArXiv Реальні факти для аргументів

Ключове рішення стеку — Spring AI як абстракційний шар. Весь код у Runner і Service працює з інтерфейсом ChatModel — він не знає чи під капотом Ollama чи OpenRouter. Перемикання відбувається виключно через Spring Profile.

Архітектура за 5 хвилин — entity, шари, flow від запиту до діалогу

Структура проєкту класична для Spring Boot — чіткий розподіл відповідальності між шарами:

src/main/java/com/example/agent_chat/
├── config/
│   └── AiProviderConfig.java          # Ollama / OpenRouter провайдери
├── controller/
│   ├── HomeController.java
│   └── AgentConversationController.java
├── entity/
│   ├── AgentConversation.java         # Розмова: тема, промпти, статус
│   ├── AgentMessage.java              # Повідомлення: sender, content, round
│   ├── AgentSender.java               # Enum: AGENT_A / AGENT_B
│   └── ConversationStatus.java        # Enum: RUNNING / STOPPED / FINISHED
├── repository/
│   ├── AgentConversationRepository.java
│   └── AgentMessageRepository.java
├── service/
│   ├── AgentConversationService.java  # Тонкий сервісний шар
│   ├── AgentConversationRunner.java   # @Async цикл діалогу
│   └── WikipediaSearchTool.java       # + інші tools
└── dto/
    ├── StartConversationRequest.java
    ├── ConversationResponse.java
    └── ExperimentMapper.java

Flow від запиту до першого повідомлення агента:

HTTP POST /start
    ↓
AgentConversationController
    ↓
AgentConversationService.start()
    ↓ зберігає AgentConversation в БД зі статусом RUNNING
    ↓
AgentConversationRunner.run() ← @Async (окремий thread)
    ↓
  [цикл раундів]
    ↓
  ask() → Spring AI → Ollama / OpenRouter → відповідь
    ↓
  saveMessage() → AgentMessage в БД
    ↓
HTTP GET /conversation/{id} ← фронтенд polling

Важливо: controller повертає conversationId одразу, не чекаючи поки агенти завершать діалог. Фронтенд сам поллить стан розмови. Це стандартний патерн для @Async операцій.

AiProviderConfig: як перемикати Ollama і OpenRouter через @Profile

Одне з найелегантніших рішень у проєкті — конфігурація провайдерів через Spring Profiles. Весь код Runner і Service залежить від інтерфейсу ChatModel — конкретна реалізація підставляється через DI залежно від активного профілю:

@Configuration
public class AiProviderConfig {

    // ── LOCAL: Ollama ─────────────────────────────────────────────
    @Configuration
    @Profile("local")
    static class OllamaConfig {

        @Bean
        @Primary
        public ChatModel primaryChatModel(OllamaChatModel ollamaChatModel) {
            return ollamaChatModel;
        }

        @Bean("agentChatModel")
        public ChatModel agentChatModel(OllamaChatModel ollamaChatModel) {
            return ollamaChatModel;
        }
    }

    // ── PROD: OpenRouter ──────────────────────────────────────────
    @Configuration
    @Profile("openai")
    static class OpenAiConfig {

        @Bean
        @Primary
        public ChatModel primaryChatModel(OpenAiChatModel openAiChatModel) {
            return openAiChatModel;
        }

        @Bean("agentChatModel")
        public ChatModel agentChatModel(OpenAiChatModel openAiChatModel) {
            return openAiChatModel;
        }
    }
}

Зверніть увагу на два біни: primaryChatModel і agentChatModel. Це не дублювання — це дві різні ролі:

  • primaryChatModel — використовується в AgentConversationService.generateTopic() для генерації теми. Це швидкий запит без tools.
  • agentChatModel — використовується в AgentConversationRunner для основного діалогу. Саме він отримує @Qualifier("agentChatModel").

Переключення між профілями:

# Локально — application-local.properties
spring.ai.ollama.chat.model=qwen3:8b

# Продакшн — змінна середовища
SPRING_PROFILES_ACTIVE=openai
OPENAI_API_KEY=your_openrouter_key
Зверніть увагу на @ConditionalOnProperty(name = "app.agent.experiment.enabled", havingValue = "true") на Runner і Service. Це означає що весь агентський функціонал вимкнено за замовчуванням і вмикається явно. Корисно коли хочете деплоїти застосунок без агентів або додавати нові фічі поступово.

AgentConversationService — сервісний шар: що тут і чому логіки діалогу тут немає

Сервісний шар у цьому проєкті навмисно тонкий. Він не містить логіки діалогу — вона вся в Runner. Відповідальність AgentConversationService:

  • Створити AgentConversation в БД і делегувати запуск в Runner
  • Зупинити розмову через зміну статусу
  • Надати CRUD для читання розмов
  • Генерувати тему через generateTopic()
@Service
@ConditionalOnProperty(name = "app.agent.experiment.enabled",
        havingValue = "true", matchIfMissing = false)
public class AgentConversationService {

    private final AgentConversationRepository conversationRepository;
    private final AgentMessageRepository messageRepository;
    private final AgentConversationRunner runner;
    private final ChatModel primaryChatModel;
    private final NewsApiSearchTool newsApiSearchTool;

    public Long start(StartConversationRequest request) {
        // Зберігаємо розмову в БД
        AgentConversation conversation = new AgentConversation();
        conversation.setTopic(request.topic());
        conversation.setSystemPromptA(request.systemPromptA());
        conversation.setSystemPromptB(request.systemPromptB());
        conversation.setTotalRounds(0);
        conversationRepository.save(conversation);

        int maxRounds = request.maxRounds() > 0 ? request.maxRounds() : 100;

        // Делегуємо в Runner — він піде в @Async thread
        runner.run(
                conversation.getId(),
                request.systemPromptA(),
                request.systemPromptB(),
                request.topic(),
                maxRounds
        );

        // Повертаємо ID одразу — не чекаємо завершення
        return conversation.getId();
    }

    public void stop(Long conversationId) {
        AgentConversation conversation = conversationRepository
                .findById(conversationId)
                .orElseThrow(() -> new IllegalArgumentException("Not found: " + conversationId));
        conversation.setStatus(ConversationStatus.STOPPED);
        conversation.setFinishedAt(LocalDateTime.now());
        conversationRepository.save(conversation);
    }

    @Transactional
    public void deleteById(Long id) {
        messageRepository.deleteByConversationId(id);
        conversationRepository.deleteById(id);
    }
}

Зверніть на метод stop(): він просто змінює статус в БД на STOPPED. Не зупиняє thread напряму — Runner сам перевіряє статус на початку кожного раунду і між A і B. Це безпечніший підхід ніж переривання потоку.

Чому логіка циклу не в Service? Принцип єдиної відповідальності. Service керує станом розмови в БД і надає API для контролера. Runner виконує сам діалог. Якщо завтра потрібно додати WebSocket замість polling або змінити логіку чергування агентів — ці зміни торкнуться тільки Runner, не Service.

generateTopic() — як агент сам придумує тему з реальних новин

Окрема цікава фіча: якщо користувач не хоче вигадувати тему сам — система генерує її автоматично через реальні новини. Ось повний метод:

public String generateTopic() {
    List<String> queries = List.of(
            "technology AI society",
            "economy inflation future",
            "climate environment crisis",
            "politics democracy freedom",
            "science space exploration",
            "healthcare medicine future",
            "education technology students",
            "cryptocurrency bitcoin finance"
    );

    // Вибираємо рандомну категорію
    String randomQuery = queries.get(
            (int) (Math.random() * queries.size())
    );

    // Отримуємо свіжі новини по категорії
    String news = newsApiSearchTool.searchNews(randomQuery);

    // Просимо LLM сформулювати провокаційну тему
    String prompt = """
        На основі цих новин придумай одну провокаційну тему 
        для філософської дискусії.
        Тема має бути спірною — щоб два агенти з протилежними 
        поглядами могли сперечатись.
        Відповідай ТІЛЬКИ темою — одне речення, без пояснень, без лапок.
        Новини: %s
        """.formatted(news);

    return primaryChatModel.call(prompt).trim();
}

Три кроки: рандомна категорія → NewsAPI → LLM формулює тему. Якщо щось пішло не так (NewsAPI недоступний, LLM не відповів) — fallback на дефолтну тему: «Чи змінить штучний інтелект майбутнє людства?».

Важлива деталь: тут використовується primaryChatModel а не agentChatModel. Для генерації теми не потрібні tools і складний контекст — це простий text-in text-out запит. Розділення бінів виправдане.

AgentConversationRunner — серце проєкту: @Async цикл, stop-фрази, HISTORY_SIZE

AgentConversationRunner — це компонент що виконує сам діалог в окремому потоці. Розберемо ключові частини:

Основний цикл

@Async
public void run(Long conversationId, String systemPromptA,
                String systemPromptB, String topic, int maxRounds) {

    String message = topic; // Перше повідомлення — тема розмови

    for (int round = 1; round <= maxRounds; round++) {

        // Перевірка чи не зупинили вручну
        AgentConversation conversation = conversationRepository
                .findById(conversationId).orElseThrow();
        if (conversation.getStatus() == ConversationStatus.STOPPED) return;

        // Агент A відповідає
        List<AgentMessage> historyA = messageRepository
                .findByConversationIdOrderByRoundNumberAsc(conversationId);
        String replyA = ask(systemPromptA, historyA, message, AgentSender.AGENT_A);
        saveMessage(conversation, AgentSender.AGENT_A, replyA, round);

        // Перевірка stop-фрази і ручної зупинки між A і B
        if (containsStopPhrase(replyA)) { finish(conversation, round); return; }

        conversation = conversationRepository.findById(conversationId).orElseThrow();
        if (conversation.getStatus() == ConversationStatus.STOPPED) return;

        // Агент B відповідає
        List<AgentMessage> historyB = messageRepository
                .findByConversationIdOrderByRoundNumberAsc(conversationId);
        String replyB = ask(systemPromptB, historyB, replyA, AgentSender.AGENT_B);
        saveMessage(conversation, AgentSender.AGENT_B, replyB, round);

        if (containsStopPhrase(replyB)) { finish(conversation, round); return; }

        message = replyB; // Відповідь B стає вхідним повідомленням для A
        sleep(500);       // Невелика пауза між раундами
    }

    finish(conversation, maxRounds);
}

Stop-фрази

private static final List<String> STOP_PHRASES = List.of(
        "до свидания", "прощай", "на этом всё",
        "goodbye", "farewell", "конец разговора"
);

Якщо агент вирішив завершити розмову природним чином — система це розпізнає і зупиняє цикл. Фрази перевіряються після кожної відповіді — і після A, і після B.

HISTORY_SIZE = 8

Не вся історія розмови передається в кожен запит — тільки останні 8 повідомлень. Це критично важливе обмеження: без нього контекстне вікно переповнюється на довгих розмовах, а вартість запиту зростає пропорційно до кількості раундів. 8 повідомлень = 4 раунди назад — достатньо для зв'язності діалогу.

Зверніть на подвійне читання з БД. Перед запитом Агента A і перед запитом Агента B — окремі запити в репозиторій для отримання свіжої історії. Це не баг — це свідоме рішення: між A і B відповідь A вже збережена в БД, тому B повинен бачити оновлену історію.

ask() — як будується контекст і чому важливий порядок ролей

Метод ask() — найтехнічніша частина проєкту. Розберемо детально:

private String ask(String systemPrompt, List<AgentMessage> history,
                   String lastMessage, AgentSender currentSender) {

    List<Message> messages = new ArrayList<>();

    // 1. Системний промпт — роль і переконання агента
    messages.add(new SystemMessage(systemPrompt));

    // 2. Останні HISTORY_SIZE повідомлень з правильними ролями
    history.stream()
            .skip(Math.max(0, history.size() - HISTORY_SIZE))
            .forEach(m -> {
                if (m.getSender() == currentSender) {
                    // Своя репліка → AssistantMessage
                    messages.add(new AssistantMessage(m.getContent()));
                } else {
                    // Репліка опонента → UserMessage
                    messages.add(new UserMessage(m.getContent()));
                }
            });

    // 3. Останнє повідомлення від опонента
    messages.add(new UserMessage(lastMessage));

    // 4. Запит з усіма 5 tools
    ToolCallback[] tools = ToolCallbacks.from(
            wikipediaSearchTool, tavilySearchTool,
            alphaVantageTool, arxivSearchTool, newsApiSearchTool
    );

    return agentChatModel.call(
            new Prompt(messages,
                    ToolCallingChatOptions.builder()
                            .toolCallbacks(tools)
                            .build()))
            .getResult().getOutput().getText();
}

Ключовий момент — маппінг ролей в history. LLM очікує що AssistantMessage — це те що говорив він сам, а UserMessage — те що говорив користувач (в нашому випадку — опонент). Якщо переплутати — модель «забуде» свою позицію і почне погоджуватись з опонентом.

Тому для кожного агента одна і та ж БД-запис може бути або AssistantMessage або UserMessage — залежно від того який агент зараз відповідає.

Закоментований код removeThinkingBlock(). У репозиторії є закоментований варіант ask() з видаленням <think>...</think> блоків. Деякі моделі (qwen3 зокрема) повертають внутрішні міркування у тегах think — і якщо їх не прибрати, вони потраплять у відповідь. Для продакшн використання з qwen3 рекомендую розкоментувати цю логіку.

П'ять tools: Wikipedia, Tavily, NewsAPI, Alpha Vantage, ArXiv

Кожен tool — це Spring-компонент з методом позначеним @Tool. Spring AI автоматично реєструє їх і передає опис в LLM. Модель сама вирішує який tool викликати залежно від контексту питання.

WikipediaSearchTool — факти і визначення

@Tool(description = """
    Шукає інформацію у Wikipedia.
    Використовуй для визначень, фактів, історії, біографій.
    Використовуй ТІЛЬКИ одне-два слова для пошуку.
    """)
public String searchWikipedia(String query) {
    // Скорочуємо запит до першого слова — Wikipedia погано 
    // обробляє довгі фрази
    String shortQuery = query.trim().split("\\s+")[0];

    WikiSearchResponse response = restClient.get()
            .uri("https://ru.wikipedia.org/w/api.php", uriBuilder -> uriBuilder
                    .queryParam("action", "query")
                    .queryParam("list", "search")
                    .queryParam("srsearch", shortQuery)
                    .queryParam("format", "json")
                    .queryParam("srlimit", "1")
                    .build())
            .retrieve()
            .body(WikiSearchResponse.class);

    String title = response.query().search().get(0).title();
    String snippet = response.query().search().get(0).snippet()
            .replaceAll("<[^>]+>", "").trim(); // Прибираємо HTML теги

    return "Стаття: " + title + "\n" + snippet;
}

Важлива деталь: Wikipedia повертає snippet з HTML тегами (<span class="searchmatch"> тощо) — їх потрібно прибирати перед передачею в LLM.

AlphaVantageTool — ціни акцій для економічних дискусій

@Tool(description = """
    Отримує поточну ціну акцій або фінансові дані компанії.
    Запит — тікер акції: AAPL, GOOGL, TSLA, AMZN.
    """)
public String getStockPrice(String symbol) {
    Map response = restClient.get()
            .uri(uriBuilder -> uriBuilder
                    .path("/query")
                    .queryParam("function", "GLOBAL_QUOTE")
                    .queryParam("symbol", symbol.toUpperCase())
                    .queryParam("apikey", apiKey)
                    .build())
            .retrieve()
            .body(Map.class);

    Map<String, String> quote = (Map<String, String>) response.get("Global Quote");
    return String.format("Акція %s: $%s | Зміна: %s | Макс: $%s | Мін: $%s",
            symbol, quote.get("05. price"), quote.get("10. change percent"),
            quote.get("03. high"), quote.get("04. low"));
}

Таблиця всіх tools

Tool Використання Безкоштовний ліміт
Wikipedia Визначення, факти, біографії, наукові поняття ✅ Необмежено
Tavily Search Актуальні новини, свіжа статистика, веб-пошук 1,000 / місяць
NewsAPI Свіжі новини по темі як аргумент 100 / день
Alpha Vantage Ціни акцій, фінансові дані для економічних дискусій 25 / день
ArXiv Наукові статті та дослідження ✅ Необмежено
Практична порада по @Tool description: опис tool — це системний промпт для LLM що пояснює коли і як його використовувати. Чим точніший опис — тим рідше модель викликає tool недоречно або з неправильними параметрами. Зверніть на «Використовуй ТІЛЬКИ одне-два слова» у Wikipedia tool — без цього модель надсилала б довгі речення і отримувала порожні результати.

Як написати промпт щоб агенти реально сперечались

Якість діалогу майже повністю залежить від системного промпту. Ось структура що працює:

Обов'язкові елементи промпту:

  • Роль — хто цей агент, його характер і переконання
  • Позиція — що він відстоює і у що вірить
  • Заборони — з чим він НІКОЛИ не погоджується (найважливіше!)
  • Формат — коротко, з фактами, питання в кінці
  • Мова — вкажіть явно

Приклад сильного промпту (Агент A — капіталіст):

Ти жорсткий капіталіст, мільярдер, власник корпорації.
Віриш що вільний ринок — єдиний шлях до процвітання.
Перед відповіддю шукай у Wikipedia факти про ВВП, рівень життя.
НІКОЛИ не погоджуйся з комуністичними ідеями.
Говори цифрами і фактами. Зневажаєш планову економіку.
Відповідай коротко — 2-3 речення.
Закінчуй провокаційним питанням.
Відповідай ТІЛЬКИ українською мовою.

Приклад слабкого промпту (так робити не треба):

Ти прихильник капіталізму. Відстоюй свою позицію.

Різниця між сильним і слабким промптом — в деталізації заборон. Без чіткого «НІКОЛИ не погоджуйся» модель знайде компроміс через 2-3 повідомлення і діалог стане нудним.

Поради для живого діалогу:

  • Вказуйте конкретні джерела для пошуку: «шукай у Wikipedia», «перевір ціну акцій»
  • Вимагайте питання в кінці кожної репліки — це провокує відповідь
  • Обмежуйте довжину: 2-3 речення — оптимально, більше стає нудно
  • Вказуйте мову явно — без цього модель може перемикатись
  • Для qwen3:8b додайте: «не використовуй теги think у відповіді»

Деплой: Ollama локально і Railway у продакшні

Локальний запуск з Ollama

# 1. Встановлюємо і запускаємо модель
ollama pull qwen3:8b    # якісне тестування (~5GB)
# або
ollama pull llama3.1:8b # швидке тестування (~4.7GB)
ollama serve

# 2. Створюємо БД
psql -c "CREATE DATABASE Agent_Chat;"

# 3. application-local.properties
spring.ai.ollama.chat.model=qwen3:8b
spring.datasource.url=jdbc:postgresql://localhost:5432/Agent_Chat
spring.datasource.username=postgres
spring.datasource.password=your_password
spring.profiles.active=local

# 4. Запуск
mvn spring-boot:run

Відкрити: http://localhost:1024

Порівняння моделей для локального тестування:

Модель Час відповіді Якість tool calling RAM
qwen3:8b 2+ хв ⭐⭐⭐ Краще слідує інструкціям ~8GB
llama3.1:8b 20–30 сек ⭐⭐ Швидше але слабше ~6GB

Продакшн на Railway через OpenRouter

# Змінні середовища на Railway
SPRING_PROFILES_ACTIVE=openai
OPENAI_API_KEY=your_openrouter_key   # ключ OpenRouter
DB_URL=jdbc:postgresql://...
DB_USERNAME=postgres
DB_PASSWORD=your_password
APP_AGENT_EXPERIMENT_ENABLED=true    # вмикаємо агентів
Чому Railway а не Heroku або Fly.io? Railway безкоштовно надає PostgreSQL і простий деплой через GitHub. Для експериментального проєкту це оптимально — не потрібно налаштовувати окремий БД сервіс. OpenRouter з deepseek/deepseek-chat коштує значно менше ніж прямий OpenAI — і для тестового проєкту це правильний вибір.

Висновки

Agent Chat — це не production-ready продукт, а живий експеримент що показує кілька важливих архітектурних підходів:

  • @Async цикл з перевіркою стану в БД — простий і надійний спосіб керувати довготривалими фоновими операціями без черг повідомлень
  • Spring Profiles для перемикання провайдерів — увесь код залежить від інтерфейсу, конкретна реалізація підставляється через DI
  • Тонкий сервісний шар + окремий Runner — правильний розподіл відповідальності коли логіка складна і асинхронна
  • @Tool з детальним description — якість опису tool напряму впливає на коректність виклику моделлю
  • HISTORY_SIZE обмеження — обов'язковий елемент для контролю витрат і розміру контексту в довгих розмовах

Повний код доступний на GitHub — MIT ліцензія, можна використовувати як основу для власних агентних проєктів:

github.com/VadimKharovyuk/Agent_Chat

Читайте також:

Яку модель Ollama обрати для агента з tool calling: порівняння і бенчмарки — якщо хочете детальніше розібратись з вибором локальної моделі для tool calling.

GPT-Realtime-2 vs Gemini Live API: що обрати у 2026 році — якщо розглядаєте голосових агентів замість текстових.

Джерела: Agent Chat GitHub репозиторій, Spring AI Documentation, Ollama Model Library, OpenRouter API Docs

Останні статті

Читайте більше цікавих матеріалів

Agent Chat: два AI агенти що сперечаються — Spring Boot 4 + Spring AI + Ollama / OpenRouter

Agent Chat: два AI агенти що сперечаються — Spring Boot 4 + Spring AI + Ollama / OpenRouter

Що буде якщо дати двом AI протилежні переконання і змусити їх сперечатись на задану тему? Саме це питання стало відправною точкою для Agent Chat — експерименту де два агенти з різними характерами ведуть діалог в реальному часі, підкріплюючи аргументи реальними фактами з Wikipedia, Tavily,...

GPT-Realtime-2 vs Gemini Live API: що обрати для голосового агента у 2026 році

GPT-Realtime-2 vs Gemini Live API: що обрати для голосового агента у 2026 році

Два флагмани real-time голосового AI вийшли практично одночасно. OpenAI випустила GPT-Realtime-2 7 травня 2026 року. Google запустила Gemini 3.1 Flash Live 26 березня 2026 року. Обидві — speech-to-speech моделі з reasoning всередині. Обидві — для голосових агентів у продакшн. Але під капотом...

GPT-5.5 в Codex: що змінилось для розробників у 2026

GPT-5.5 в Codex: що змінилось для розробників у 2026

23 квітня 2026 OpenAI випустила GPT-5.5 — і одразу зробила її дефолтною моделлю в Codex. Але не кожен апдейт насправді щось змінює у щоденній роботі. Цей — змінює. Три речі, які важливі для розробника: менше токенів на ті ж задачі, та сама швидкість що й GPT-5.4, і якісно новий...

GPT-Realtime-2: технічний гід — WebSocket API, підключення і приклади коду

GPT-Realtime-2: технічний гід — WebSocket API, підключення і приклади коду

Ця стаття — практичний гід для розробників що хочуть підключити GPT-Realtime-2 до свого проєкту. Ми розберемо архітектуру Realtime API, виберемо правильний метод підключення для вашого сценарію, напишемо першу робочу сесію з нуля і налаштуємо preambles, tool calls і recovery з реальним...

OpenAI випустила GPT-Realtime-2: перша голосова модель з GPT-5-рівнем мислення

OpenAI випустила GPT-Realtime-2: перша голосова модель з GPT-5-рівнем мислення

7 травня 2026 року OpenAI зробила анонс, який багато хто в спільноті розробників чекав давно: три нові голосові моделі в Realtime API. Флагман — GPT-Realtime-2 — перша в лінійці, де мислення рівня GPT-5 вбудоване прямо в голосовий потік. Без затримок між розпізнаванням і відповіддю. Без окремих...

Яку модель Ollama обрати для агента з tool calling: порівняння і бенчмарки

Яку модель Ollama обрати для агента з tool calling: порівняння і бенчмарки

Tool calling в Ollama — одна з найбільш неочевидних фіч локальних моделей. Не тому що API складний. А тому що між «модель підтримує tools» у документації і «модель стабільно викликає tools у продакшні» — велика різниця яку можна виявити тільки під навантаженням. Одні моделі...