Chatbot & Conversational AI Architecture
Design production chatbots in Java: intent classification, dialog state management, slot filling, multi-turn context, tool integration and handoff to humans — beyond a single ChatClient call.
On this page
A single ChatClient call is a chatbot the way a single SQL query is a database application — the core, surrounded by everything that makes it real. A production chatbot needs memory, intent understanding, tools, guardrails and human escalation. This tutorial covers the architecture.
Key Takeaways
- A production chatbot is a ChatClient plus memory, intent, tools, guardrails and escalation.
- Dialog state tracks a multi-turn flow so information can arrive across messages.
- Tools let it take real actions; guardrails keep it in scope.
- Always provide a graceful human handoff — never trap a user.
The architecture
Memory and multi-turn context
A conversation is stateful; the model is not. Bounded chat memory carries context across turns:
@Service
public class ChatbotService {
private final ChatClient chatClient;
public ChatbotService(ChatClient.Builder builder, ChatbotTools tools) {
this.chatClient = builder
.defaultSystem(SYSTEM_PROMPT)
.defaultTools(tools) // real actions
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
}
public Reply chat(String conversationId, String message) {
String response = chatClient.prompt()
.user(message)
// Conversation ID from the authenticated session, keeping each
// user's context separate and private.
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
return new Reply(response);
}
}Dialog state and slot filling
For task-oriented flows — booking, ordering, troubleshooting — track what has been established. Modern LLMs handle this more fluidly than old rule-based systems, but the concept remains: know which information you still need.
record BookingState(
Optional<LocalDate> date,
Optional<LocalTime> time,
Optional<Integer> partySize) {
boolean isComplete() {
return date.isPresent() && time.isPresent() && partySize.isPresent();
}
List<String> missingSlots() { /* return the empty ones */ }
}public Reply handleBooking(String conversationId, String message, BookingState state) {
BookingState updated = slotExtractor.extract(message, state); // fill from this message
if (!updated.isComplete()) {
// Ask only for what is still missing — not for what the user already gave.
return askFor(updated.missingSlots());
}
return confirmAndBook(updated);
}Scope guardrails
A chatbot that answers anything confidently is a liability. Keep it in its lane:
private static final String SYSTEM_PROMPT = """
You are a booking assistant for Acme Restaurants.
Help only with reservations, menus, hours and locations.
If asked about anything else, politely say it is outside what you can
help with, and offer to connect the user to a person.
Never invent availability, prices or policies.
""";Combine the prompt with output validation — see guardrails & safety systems.
Tools for real actions
A chatbot that can only talk is half a product. Tools let it check availability, make bookings, look up orders — with the same discipline: read tools freely, write tools authorized in code and confirmed.
@Tool("Check table availability for a date, time and party size.")
String checkAvailability(LocalDate date, LocalTime time, int partySize) { /* read */ }
@Tool("Book a table. Only call after the user confirms the details.")
String book(LocalDate date, LocalTime time, int partySize, String confirmation) {
if (!"CONFIRMED".equals(confirmation)) return "CONFIRMATION_REQUIRED";
// authorize, validate, book
}Graceful human handoff
The most important design principle: never trap a user in a bot that cannot help them.
public Reply handle(String conversationId, String message) {
if (frustrationDetector.isFrustrated(message) || userRequestsHuman(message)) {
// Hand off WITH the conversation so the human continues, not restarts.
return escalate(conversationId, "user requested or frustrated");
}
Reply reply = process(conversationId, message);
if (reply.confidence() < ESCALATION_THRESHOLD) {
return escalate(conversationId, "low confidence");
}
return reply;
}Streaming for responsiveness
Chat is interactive, so stream the response — the user sees it forming rather than waiting for a complete reply. The exception is when you must validate the whole response before showing it, where you block, validate, then reply.
Putting it together
A production chatbot is: bounded memory + intent understanding + dialog state for tasks + tools for actions + scope guardrails + output validation + streaming + graceful escalation. Each piece is covered elsewhere in this curriculum; the architecture is assembling them around the core model call.
Next
- AI for data engineering
- Multi-tenant AI architectures
- AgenticHR project — a chatbot in a real system
Frequently Asked Questions
How do I build a production chatbot beyond a single model call?
What is dialog state management?
What is slot filling?
When should a chatbot hand off to a human?
Related tutorials
- AI-Powered Search ApplicationsBuild AI-powered search in Java: hybrid keyword-plus-vector search, faceted filtering, query understanding, personalization and re-ranking — beyond both keyword search and naive RAG.
- AI for Data EngineeringApply LLMs to data engineering in Java: text-to-SQL with safety guards, AI-assisted data cleaning, schema mapping and anomaly detection — where AI helps and where it must be constrained.
- AI in CI/CD PipelinesIntegrate AI into CI/CD pipelines: automated code review, test generation, documentation and PR triage — with the precision discipline and guardrails that keep these bots useful, not noisy.
- AI Observability & LLM TracingObserve LLM applications in production: distributed tracing of model and retrieval calls, LangFuse and OpenTelemetry GenAI conventions, span attributes, and cost dashboards for Java teams.