Agenten-Chat: Zwei KI-Agenten im Streit – Spring Boot 4 + Spring AI + Ollama / OpenRouter

Aktualisiert:
Agenten-Chat: Zwei KI-Agenten im Streit – Spring Boot 4 + Spring AI + Ollama / OpenRouter

Was passiert, wenn man zwei KIs gegensätzliche Überzeugungen gibt und sie zwingt, über ein bestimmtes Thema zu streiten? Genau diese Frage war der Ausgangspunkt für Agent Chat – ein Experiment, bei dem zwei Agenten mit unterschiedlichen Persönlichkeiten in Echtzeit einen Dialog führen, gestützt durch reale Fakten aus Wikipedia, Tavily, NewsAPI, ArXiv und Alpha Vantage.

⚠️ Wichtig zur Architektur: Agent Chat ist ein experimentelles Projekt, das bewusst einfach gehalten wurde, um schnell zu testen, wie sich Agenten mit unterschiedlichen Persönlichkeiten verhalten. Polling statt WebSocket, synchrones Lesen aus der Datenbank in einer Schleife, keine Warteschlangen – all dies sind bewusste Kompromisse zugunsten einer einfachen Inbetriebnahme und Lesbarkeit des Codes. Für eine Produktions-Multi-Agenten-Systemarchitektur ist eine Überarbeitung erforderlich. 💡 Empfehlung: Lokal mit Ollama starten – das ist komplett kostenlos. Keine API-Schlüssel, keine Kosten. qwen3:8b reicht aus, um einen lebendigen Dialog von Agenten mit echten Fakten aus Wikipedia und ArXiv zu sehen.
GitHub: github.com/VadimKharovyuk/Agent_Chat – MIT-Lizenz, vollständiger Code, README mit Startanweisungen.

Inhalt

Idee und wie es in Aktion aussieht

Das klassische Problem von KI-Agenten – sie sind zu höflich. Bitten Sie GPT zu streiten – er wird nach zwei Nachrichten zustimmen. Agent Chat löst dies durch System-Prompts mit strengen Verboten und klaren Überzeugungen für jeden Agenten.

Der Flow sieht so aus:

Thema → Agent A antwortet → Agent B antwortet → Agent A ...

Der Benutzer gibt ein:

  • Thema – zum Beispiel „Sollte KI staatlich reguliert werden?“
  • System-Prompt für Agent A – Rolle, Überzeugungen, Verbote, Format
  • System-Prompt für Agent B – entgegengesetzte Position
  • Anzahl der Runden – 1 Runde = 2 Nachrichten (A + B)

Die Agenten führen den Dialog automatisch. Jede Nachricht wird in PostgreSQL gespeichert. Das Gespräch kann jederzeit gestoppt werden. Ein Agent kann den Dialog auch selbst beenden – wenn seine Antwort eine Stopp-Phrase wie „Auf Wiedersehen“ oder „farewell“ enthält.

Der interessanteste Teil: Agenten können auf reale Quellen zugreifen, um Argumente zu untermauern – Wikipedia, aktuelle Nachrichten, wissenschaftliche Artikel, Aktienkurse. Das macht den Dialog lebendiger und weniger halluzinatorisch.

Wenn Sie bereits andere Modelle in Ollama installiert haben – können Sie diese ausprobieren. Aber aus Erfahrung: Nicht alle Modelle folgen den Anweisungen des System-Prompts gut. Einige ignorieren Verbote und beginnen, dem Gegner nach 2-3 Runden zuzustimmen, andere rufen überhaupt keine Tools auf. Detaillierter, welche Modelle auf lokaler Hardware wirklich funktionieren – lesen Sie im Artikel über Ollama auf 8GB RAM.

Stack: Spring Boot 4, Spring AI 2.0, Ollama und OpenRouter

Komponente Technologie Zweck
Backend Java 21, Spring Boot 4.0.6 Haupt-Framework
AI Framework Spring AI 2.0.0-M5 Abstraktion über LLM-Anbieter
LLM (lokal) Ollama (qwen3:8b / llama3.1:8b) Lokale Entwicklung ohne Kosten
LLM (prod) OpenRouter (deepseek/deepseek-chat) Cloud-Anbieter für Railway
Datenbank PostgreSQL Speicherung von Gesprächen und Nachrichten
Frontend Thymeleaf + Bootstrap 5 Einfache UI ohne separates SPA
Tools Wikipedia, Tavily, NewsAPI, Alpha Vantage, ArXiv Reale Fakten für Argumente

Die Schlüsselentscheidung des Stacks – Spring AI als Abstraktionsschicht. Der gesamte Code in Runner und Service arbeitet mit dem Interface ChatModel – er weiß nicht, ob unter der Haube Ollama oder OpenRouter läuft. Das Umschalten erfolgt ausschließlich über Spring Profile.

Architektur in 5 Minuten — Entität, Schichten, Flow von Anfrage bis Dialog

Die Projektstruktur ist klassisch für Spring Boot — klare Aufteilung der Verantwortlichkeiten zwischen den Schichten:

src/main/java/com/example/agent_chat/
├── config/
│   └── AiProviderConfig.java          # Ollama / OpenRouter Provider
├── controller/
│   ├── HomeController.java
│   └── AgentConversationController.java
├── entity/
│   ├── AgentConversation.java         # Konversation: Thema, Prompts, Status
│   ├── AgentMessage.java              # Nachricht: Sender, Inhalt, Runde
│   ├── AgentSender.java               # Enum: AGENT_A / AGENT_B
│   └── ConversationStatus.java        # Enum: RUNNING / STOPPED / FINISHED
├── repository/
│   ├── AgentConversationRepository.java
│   └── AgentMessageRepository.java
├── service/
│   ├── AgentConversationService.java  # Dünne Service-Schicht
│   ├── AgentConversationRunner.java   # @Async Dialog-Schleife
│   └── WikipediaSearchTool.java       # + andere Tools
└── dto/
    ├── StartConversationRequest.java
    ├── ConversationResponse.java
    └── ExperimentMapper.java

Flow von der Anfrage zur ersten Nachricht des Agenten:

HTTP POST /start
    ↓
AgentConversationController
    ↓
AgentConversationService.start()
    ↓ speichert AgentConversation in der DB mit Status RUNNING
    ↓
AgentConversationRunner.run() ← @Async (separater Thread)
    ↓
  [Runden-Schleife]
    ↓
  ask() → Spring AI → Ollama / OpenRouter → Antwort
    ↓
  saveMessage() → AgentMessage in der DB
    ↓
HTTP GET /conversation/{id} ← Frontend-Polling

Wichtig: Der controller gibt die conversationId sofort zurück, ohne auf die Beendigung des Dialogs durch die Agenten zu warten. Das Frontend pollt selbst den Gesprächsstatus. Dies ist ein Standardmuster für @Async-Operationen.

AiProviderConfig: Wie man Ollama und OpenRouter über @Profile umschaltet

Eine der elegantesten Lösungen im Projekt ist die Konfiguration von Providern über Spring Profiles. Der gesamte Code von Runner und Service hängt vom Interface ChatModel ab — die konkrete Implementierung wird über DI je nach aktivem Profil eingefügt:

@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;
        }
    }
}

Beachten Sie die beiden Beans: primaryChatModel und agentChatModel. Dies ist keine Duplizierung — es sind zwei verschiedene Rollen:

  • primaryChatModel — wird in AgentConversationService.generateTopic() zur Generierung des Themas verwendet. Dies ist eine schnelle Anfrage ohne Tools.
  • agentChatModel — wird in AgentConversationRunner für den Hauptdialog verwendet. Er erhält @Qualifier("agentChatModel").

Umschalten zwischen Profilen:

# Lokal — application-local.properties
spring.ai.ollama.chat.model=qwen3:8b

# Produktion — Umgebungsvariable
SPRING_PROFILES_ACTIVE=openai
OPENAI_API_KEY=your_openrouter_key
Beachten Sie @ConditionalOnProperty(name = "app.agent.experiment.enabled", havingValue = "true") auf Runner und Service. Das bedeutet, dass die gesamte Agentenfunktionalität standardmäßig deaktiviert ist und explizit aktiviert werden muss. Nützlich, wenn Sie die Anwendung ohne Agenten bereitstellen oder neue Funktionen schrittweise hinzufügen möchten.

AgentConversationService — die Service-Schicht: Was ist hier und warum ist die Dialoglogik hier nicht?

Die Service-Schicht ist in diesem Projekt bewusst dünn gehalten. Sie enthält keine Dialoglogik — diese befindet sich vollständig im Runner. Die Verantwortlichkeiten von AgentConversationService:

  • Erstellen von AgentConversation in der DB und Delegieren des Starts an den Runner
  • Beenden der Konversation durch Ändern des Status
  • Bereitstellen von CRUD für das Lesen von Konversationen
  • Generieren des Themas über 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) {
        // Speichern der Konversation in der DB
        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;

        // Delegieren an den Runner — er wird in einem @Async-Thread ausgeführt
        runner.run(
                conversation.getId(),
                request.systemPromptA(),
                request.systemPromptB(),
                request.topic(),
                maxRounds
        );

        // Sofortige Rückgabe der ID — kein Warten auf Abschluss
        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);
    }
}

Beachten Sie die Methode stop(): Sie ändert einfach den Status in der DB auf STOPPED. Sie stoppt den Thread nicht direkt — der Runner prüft den Status zu Beginn jeder Runde und zwischen A und B selbst. Dies ist ein sichererer Ansatz als das Unterbrechen des Threads.

Warum ist die Schleifenlogik nicht im Service? Prinzip der einzigen Verantwortung. Der Service verwaltet den Zustand der Konversation in der DB und stellt eine API für den Controller bereit. Der Runner führt den Dialog selbst aus. Wenn morgen WebSocket anstelle von Polling hinzugefügt oder die Logik der Agentenreihenfolge geändert werden muss — diese Änderungen betreffen nur den Runner, nicht den Service.

generateTopic() — wie ein Agent selbst ein Thema aus echten Nachrichten erfindet

Eine weitere interessante Funktion: Wenn der Benutzer kein eigenes Thema erfinden möchte — das System generiert es automatisch anhand von echten Nachrichten. Hier ist die vollständige Methode:

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"
    );

    // Zufällige Kategorie auswählen
    String randomQuery = queries.get(
            (int) (Math.random() * queries.size())
    );

    // Aktuelle Nachrichten für die Kategorie abrufen
    String news = newsApiSearchTool.searchNews(randomQuery);

    // LLM bitten, ein provokatives Thema zu formulieren
    String prompt = """
        Basierend auf diesen Nachrichten, erfinde ein provokatives Thema 
        für eine philosophische Diskussion.
        Das Thema sollte umstritten sein — damit zwei Agenten mit gegensätzlichen 
        Ansichten streiten können.
        Antworte NUR mit dem Thema — ein Satz, ohne Erklärungen, ohne Anführungszeichen.
        Nachrichten: %s
        """.formatted(news);

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

Drei Schritte: zufällige Kategorie → NewsAPI → LLM formuliert das Thema. Wenn etwas schiefgeht (NewsAPI nicht verfügbar, LLM antwortet nicht) — Fallback auf ein Standardthema: „Wird künstliche Intelligenz die Zukunft der Menschheit verändern?“.

Wichtiges Detail: Hier wird primaryChatModel und nicht agentChatModel verwendet. Für die Themen-Generierung sind keine Tools und kein komplexer Kontext erforderlich — dies ist eine einfache Text-in-Text-out-Anfrage. Die Trennung der Beans ist gerechtfertigt.

AgentConversationRunner — das Herzstück des Projekts: @Async-Schleife, Stopp-Phrasen, HISTORY_SIZE

AgentConversationRunner ist eine Komponente, die den Dialog selbst in einem separaten Thread ausführt. Lassen Sie uns die Schlüsselkomponenten aufschlüsseln:

Hauptschleife

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

    String message = topic; // Die erste Nachricht ist das Thema des Gesprächs

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

        // Überprüfen, ob es nicht manuell gestoppt wurde
        AgentConversation conversation = conversationRepository
                .findById(conversationId).orElseThrow();
        if (conversation.getStatus() == ConversationStatus.STOPPED) return;

        // Agent A antwortet
        List<AgentMessage> historyA = messageRepository
                .findByConversationIdOrderByRoundNumberAsc(conversationId);
        String replyA = ask(systemPromptA, historyA, message, AgentSender.AGENT_A);
        saveMessage(conversation, AgentSender.AGENT_A, replyA, round);

        // Überprüfung auf Stopp-Phrase und manuelles Stoppen zwischen A und B
        if (containsStopPhrase(replyA)) { finish(conversation, round); return; }

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

        // Agent B antwortet
        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; // Die Antwort von B wird zur Eingabenachricht für A
        sleep(500);       // Eine kleine Pause zwischen den Runden
    }

    finish(conversation, maxRounds);
}

Stopp-Phrasen

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

Wenn ein Agent beschließt, das Gespräch auf natürliche Weise zu beenden, erkennt das System dies und stoppt die Schleife. Die Phrasen werden nach jeder Antwort überprüft – sowohl nach A als auch nach B.

HISTORY_SIZE = 8

Nicht die gesamte Gesprächshistorie wird an jede Anfrage übergeben – nur die letzten 8 Nachrichten. Dies ist eine kritische Einschränkung: Ohne sie überläuft das Kontextfenster bei langen Gesprächen, und die Kosten der Anfrage steigen proportional zur Anzahl der Runden. 8 Nachrichten = 4 Runden zurück – das reicht für die Kohärenz des Dialogs.

Beachten Sie das doppelte Lesen aus der DB. Vor der Anfrage von Agent A und vor der Anfrage von Agent B gibt es separate Abfragen an das Repository, um die aktuelle Historie abzurufen. Dies ist kein Fehler – es ist eine bewusste Entscheidung: Zwischen A und B ist die Antwort von A bereits in der DB gespeichert, daher sollte B die aktualisierte Historie sehen.

ask() — wie der Kontext aufgebaut wird und warum die Reihenfolge der Rollen wichtig ist

Die Methode ask() ist der technischste Teil des Projekts. Lassen Sie uns ins Detail gehen:

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

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

    // 1. System-Prompt – Rolle und Überzeugungen des Agenten
    messages.add(new SystemMessage(systemPrompt));

    // 2. Die letzten HISTORY_SIZE Nachrichten mit den richtigen Rollen
    history.stream()
            .skip(Math.max(0, history.size() - HISTORY_SIZE))
            .forEach(m -> {
                if (m.getSender() == currentSender) {
                    // Eigene Aussage → AssistantMessage
                    messages.add(new AssistantMessage(m.getContent()));
                } else {
                    // Aussage des Gegners → UserMessage
                    messages.add(new UserMessage(m.getContent()));
                }
            });

    // 3. Die letzte Nachricht vom Gegner
    messages.add(new UserMessage(lastMessage));

    // 4. Anfrage mit allen 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();
}

Der entscheidende Punkt ist die Zuordnung der Rollen in der Historie. Die LLM erwartet, dass AssistantMessage das ist, was sie selbst gesagt hat, und UserMessage das, was der Benutzer (in unserem Fall der Gegner) gesagt hat. Wenn man dies verwechselt, "vergisst" das Modell seine Position und beginnt, dem Gegner zuzustimmen.

Daher kann für jeden Agenten derselbe DB-Eintrag entweder eine AssistantMessage oder eine UserMessage sein – je nachdem, welcher Agent gerade antwortet.

Auskommentierter Code removeThinkingBlock(). Im Repository gibt es eine auskommentierte Version von ask() mit der Entfernung von <think>...</think>-Blöcken. Einige Modelle (insbesondere qwen3) geben interne Überlegungen in Think-Tags zurück – und wenn diese nicht entfernt werden, gelangen sie in die Antwort. Für die produktive Nutzung mit qwen3 empfehle ich, diese Logik zu entkommentieren.

Fünf Tools: Wikipedia, Tavily, NewsAPI, Alpha Vantage, ArXiv

Jedes Tool ist eine Spring-Komponente mit einer Methode, die mit @Tool gekennzeichnet ist. Spring AI registriert sie automatisch und übergibt die Beschreibung an die LLM. Das Modell entscheidet selbst, welches Tool je nach Kontext der Frage aufgerufen werden soll.

WikipediaSearchTool — Fakten und Definitionen

@Tool(description = """
    Sucht nach Informationen auf Wikipedia.
    Verwenden Sie es für Definitionen, Fakten, Geschichte, Biografien.
    Verwenden Sie NUR ein oder zwei Wörter für die Suche.
    """)
public String searchWikipedia(String query) {
    // Kürzen der Anfrage auf das erste Wort – Wikipedia verarbeitet lange Sätze schlecht
    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(); // Entfernen von HTML-Tags

    return "Artikel: " + title + "\n" + snippet;
}

Ein wichtiger Punkt: Wikipedia gibt Snippets mit HTML-Tags zurück (<span class="searchmatch"> usw.) – diese müssen entfernt werden, bevor sie an die LLM übergeben werden.

AlphaVantageTool — Aktienkurse für wirtschaftliche Diskussionen

@Tool(description = """
    Ruft den aktuellen Aktienkurs oder Finanzdaten eines Unternehmens ab.
    Anfrage – Aktiensymbol: 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("Aktie %s: $%s | Veränderung: %s | Max: $%s | Min: $%s",
            symbol, quote.get("05. price"), quote.get("10. change percent"),
            quote.get("03. high"), quote.get("04. low"));
}

Tabelle aller Tools

Tool Verwendung Kostenloses Limit
Wikipedia Definitionen, Fakten, Biografien, wissenschaftliche Konzepte ✅ Unbegrenzt
Tavily Search Aktuelle Nachrichten, frische Statistiken, Websuche 1.000 / Monat
NewsAPI Aktuelle Nachrichten zu einem Thema als Argument 100 / Tag
Alpha Vantage Aktienkurse, Finanzdaten für wirtschaftliche Diskussionen 25 / Tag
ArXiv Wissenschaftliche Artikel und Forschung ✅ Unbegrenzt
Praktischer Tipp zu @Tool description: Die Beschreibung eines Tools ist ein System-Prompt für die LLM, der erklärt, wann und wie es verwendet werden soll. Je genauer die Beschreibung, desto seltener ruft das Modell ein Tool unangemessen oder mit falschen Parametern auf. Achten Sie auf "Verwenden Sie NUR ein oder zwei Wörter" im Wikipedia-Tool – ohne dies hätte das Modell lange Sätze gesendet und leere Ergebnisse erhalten.

Wie man einen Prompt schreibt, damit Agenten wirklich streiten

Die Qualität des Dialogs hängt fast vollständig vom System-Prompt ab. Hier ist eine Struktur, die funktioniert:

Erforderliche Elemente des Prompts:

  • Rolle – wer ist dieser Agent, sein Charakter und seine Überzeugungen
  • Position – was er verteidigt und woran er glaubt
  • Verbote – womit er NIEMALS einverstanden ist (das Wichtigste!)
  • Format – kurz, mit Fakten, Frage am Ende
  • Sprache – explizit angeben

Beispiel für einen starken Prompt (Agent A – Kapitalist):

Du bist ein harter Kapitalist, Milliardär, Unternehmenseigner.
Du glaubst, dass der freie Markt der einzige Weg zum Wohlstand ist.
Suche vor der Antwort auf Wikipedia nach Fakten über BIP, Lebensstandard.
Stimme NIEMALS kommunistischen Ideen zu.
Sprich mit Zahlen und Fakten. Du verachtest Planwirtschaft.
Antworte kurz – 2-3 Sätze.
Beende mit einer provokanten Frage.
Antworte NUR auf Ukrainisch.

Beispiel für einen schwachen Prompt (so sollte man es nicht machen):

Du bist ein Befürworter des Kapitalismus. Verteidige deine Position.

Der Unterschied zwischen einem starken und einem schwachen Prompt liegt in der Detaillierung der Verbote. Ohne ein klares "NIEMALS zustimmen" findet das Modell nach 2-3 Nachrichten einen Kompromiss, und der Dialog wird langweilig.

Tipps für einen lebendigen Dialog:

  • Geben Sie konkrete Quellen für die Suche an: „Suche auf Wikipedia“, „Überprüfe den Aktienkurs“
  • Fordern Sie am Ende jeder Aussage eine Frage – das provoziert eine Antwort
  • Begrenzen Sie die Länge: 2-3 Sätze sind optimal, mehr wird langweilig
  • Geben Sie die Sprache explizit an – ohne sie kann das Modell wechseln
  • Für qwen3:8b fügen Sie hinzu: „verwende keine Think-Tags in der Antwort“

Deployment: Ollama lokal und Railway in Produktion

Lokaler Start mit Ollama

# 1. Modell installieren und starten
ollama pull qwen3:8b    # qualitative Tests (~5GB)
# oder
ollama pull llama3.1:8b # schnelle Tests (~4.7GB)
ollama serve

# 2. DB erstellen
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. Starten
mvn spring-boot:run

Öffnen Sie: http://localhost:1024

Vergleich von Modellen für lokale Tests:

Modell Antwortzeit Qualität des Tool Callings RAM
qwen3:8b 2+ Min ⭐⭐⭐ Folgt Anweisungen besser ~8GB
llama3.1:8b 20–30 Sek ⭐⭐ Schneller, aber schwächer ~6GB

Produktion auf Railway über OpenRouter

# Umgebungsvariablen auf Railway
SPRING_PROFILES_ACTIVE=openai
OPENAI_API_KEY=your_openrouter_key   # OpenRouter-Schlüssel
DB_URL=jdbc:postgresql://...
DB_USERNAME=postgres
DB_PASSWORD=your_password
APP_AGENT_EXPERIMENT_ENABLED=true    # Agenten aktivieren
Warum Railway und nicht Heroku oder Fly.io? Railway bietet kostenlos PostgreSQL und einfaches Deployment über GitHub. Für ein experimentelles Projekt ist dies optimal – es muss kein separater DB-Service konfiguriert werden. OpenRouter mit deepseek/deepseek-chat ist deutlich günstiger als direktes OpenAI – und für ein Testprojekt ist dies die richtige Wahl.

Schlussfolgerungen

Agent Chat ist kein produktionsreifes Produkt, sondern ein lebendiges Experiment, das mehrere wichtige Architekturansätze zeigt:

  • @Async-Schleife mit Statusprüfung in der DB – eine einfache und zuverlässige Methode zur Verwaltung langlaufender Hintergrundoperationen ohne Nachrichtenwarteschlangen
  • Spring Profiles zum Umschalten von Anbietern – der gesamte Code hängt von der Schnittstelle ab, die konkrete Implementierung wird über DI bereitgestellt
  • Dünne Service-Schicht + separater Runner – richtige Verantwortungsverteilung, wenn die Logik komplex und asynchron ist
  • @Tool mit detaillierter Beschreibung – die Qualität der Tool-Beschreibung beeinflusst direkt die korrekte Ausführung durch das Modell
  • HISTORY_SIZE-Beschränkung – ein obligatorisches Element zur Kostenkontrolle und zur Steuerung der Kontextgröße bei langen Gesprächen

Der vollständige Code ist auf GitHub verfügbar – MIT-Lizenz, kann als Grundlage für eigene Agentenprojekte verwendet werden:

github.com/VadimKharovyuk/Agent_Chat

Lesen Sie auch:

Welches Ollama-Modell für einen Agenten mit Tool Calling wählen: Vergleich und Benchmarks – wenn Sie die Wahl eines lokalen Modells für Tool Calling genauer verstehen möchten.

GPT-Realtime-2 vs Gemini Live API: Was wählen im Jahr 2026 – wenn Sie Sprachagenten anstelle von Textagenten in Betracht ziehen.

Quellen: Agent Chat GitHub Repository, 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 у продакшні» — велика різниця яку можна виявити тільки під навантаженням. Одні моделі...