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
- Stack: Spring Boot 4, Spring AI 2.0, Ollama und OpenRouter
- Architektur in 5 Minuten – Entität, Schichten, Flow von der Anfrage zum Dialog
- AiProviderConfig: Wie man Ollama und OpenRouter über @Profile umschaltet
- AgentConversationService – Service-Schicht: Was hier ist und warum die Dialoglogik hier fehlt
- generateTopic() – Wie ein Agent selbst ein Thema aus echten Nachrichten erfindet
- AgentConversationRunner – Das Herzstück des Projekts: @Async-Schleife, Stopp-Phrasen, HISTORY_SIZE
- ask() – Wie der Kontext aufgebaut wird und warum die Reihenfolge der Rollen wichtig ist
- Fünf Tools: Wikipedia, Tavily, NewsAPI, Alpha Vantage, ArXiv
- Wie man einen Prompt schreibt, damit Agenten wirklich streiten
- Deployment: Ollama lokal und Railway in Produktion
- Schlussfolgerungen + GitHub
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