DEV Community

shashank ms
shashank ms

Posted on

Building a Chatbot with LLM and Dialogue Management: Best Practices

Building a production-grade chatbot requires more than wrapping a large language model in a REST endpoint. You need a dialogue management layer that tracks state, handles context windows, and orchestrates tool use. Without clear boundaries between generation and control, conversations drift, loops form, and user intent gets lost. This article covers architectural patterns and implementation details for building reliable, stateful chatbots, and where an inference backend like Oxlo.ai removes friction from that stack.

Core Architecture

A maintainable chatbot stack splits language generation from flow control. The dialogue manager owns the session state, decides when to prompt the LLM, and validates outputs. The LLM backend handles natural language understanding, rephrasing, and user-facing text. This separation prevents the model from having to reason about business logic and syntax constraints at the same time, which reduces hallucinations.

A minimal implementation looks like this:

import openai
from typing import List, Dict, Any

class DialogueManager:
    def __init__(self, api_key: str):
        self.client = openai.OpenAI(
            api_key=api_key,
            base_url="https://api.oxlo.ai/v1"
        )
        self.history: List[Dict[str, str]] = []
        self.slots: Dict[str, Any] = {}

    def run_turn(self, user_text: str) -> str:
        self.history.append({"role": "user", "content": user_text})
        response = self.client.chat.completions.create(
            model="llama-3.3-70b",
            messages=self.history,
            temperature=0.3
        )
        assistant_text = response.choices[0].message.content
        self.history.append({"role": "assistant", "content": assistant_text})
        return assistant_text
Enter fullscreen mode Exit fullscreen mode

This separation lets you swap models or providers without rewriting turn logic. Because Oxlo.ai is fully OpenAI SDK compatible, switching to it is a single base_url change.

Managing Dialogue State and Context

Conversations accumulate tokens quickly. A naive implementation passes the entire message history on every turn, eventually hitting context limits or inflating costs on token-based platforms. A dialogue manager should maintain a compressed representation of the session.

Use explicit slots for known entities, such as dates, product IDs, or locations. When the history grows past a threshold, summarize older turns into a system prompt or a memory snippet rather than retaining the raw transcript.

With Oxlo.ai, the cost structure is request-based, not token-based. That means long context sessions, summarization passes, and state-heavy prompts do not scale in price with input length. You can keep richer context in play without the cost spikes common on token-based providers. For exact plan details, see the Oxlo.ai pricing page.

    def compress_history(self):
        if len(self.history) > 10:
            summary_prompt = [
                {"role": "system", "content": "Summarize the following conversation for a support agent."},
                {"role": "user", "content": str(self.history[:-4])}
            ]
            summary = self.client.chat.completions.create(
                model="qwen-3-32b",
                messages=summary_prompt,
                max_tokens=256
            ).choices[0].message.content
            self.history = [
                {"role": "system", "content": f"Prior conversation summary: {summary}"}
            ] + self.history[-4:]
Enter fullscreen mode Exit fullscreen mode

Implementing Memory Layers

Session state handles the current task, but user-specific memory spans across sessions. Store facts, preferences, and prior issues in a vector database, then retrieve relevant chunks before each turn.

Oxlo.ai offers embedding endpoints, including BGE-Large and E5-Large, which you can use to index and query long-term memory without leaving the platform.

    def embed_text(self, text: str) -> List[float]:
        resp = self.client.embeddings.create(
            model="bge-large",
            input=text
        )
        return resp.data[0].embedding

    def recall(self, query: str, top_k: int = 3) -> List[str]:
        q_vector = self.embed_text(query)
        # pseudocode: search your vector store
        return vector_db.search(q_vector, top_k)
Enter fullscreen mode Exit fullscreen mode

Inject retrieved memories into the system prompt so the model grounds its response in facts rather than generic defaults.

Function Calling for Task Completion

A chatbot that only chats is a demo. Production bots need to check order status, book meetings, or modify records. Do not prompt the model to emit pseudo-API calls in free text. Instead, expose formal tool schemas and let the dialogue manager execute them.

Oxlo.ai supports function calling and JSON mode across its chat models, including Llama 3.3 70B and Qwen 3 32B, so you can enforce structured outputs reliably.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_delivery_date",
            "description": "Get the delivery date for an order",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string"}
                },
                "required": ["order_id"]
            }
        }
    }
]

response = self.client.chat.completions.create(
    model="llama-3.3-70b",
    messages=self.history,
    tools=tools,
    tool_choice="auto"
)

if response.choices[0].message.tool_calls:
    # Dialogue manager validates and executes
    pass
Enter fullscreen mode Exit fullscreen mode

Keep side effects inside the manager. The LLM proposes actions; your code approves them.

Handling Multi-Turn Conversations

Users do not state intent in a single turn. They correct themselves, ask follow-ups, and change topics. The dialogue manager should track the active topic and handle carry-over slots.

Set a concise system prompt that defines the bot's persona, allowed topics, and output rules. Use streaming to reduce perceived latency, especially when the model is reasoning through a long chain of thought.

Oxlo.ai provides streaming responses and no cold starts on popular models, so multi-turn sessions feel responsive even when you switch between a lightweight routing model and a heavy reasoning model like DeepSeek R1 671B MoE for complex clarification steps.

stream = self.client.chat.completions.create(
    model="deepseek-r1-671b",
    messages=self.history,
    stream=True
)

chunks = []
for chunk in stream:
    delta = chunk.choices[0].delta.content or ""
    chunks.append(delta)
    # yield delta to frontend
Enter fullscreen mode Exit fullscreen mode

Error Recovery and Fallbacks

LLMs fail silently. They produce malformed JSON, refuse valid requests, or loop on ambiguous slots. Your dialogue manager needs a validation layer.

Parse all model outputs inside try/except blocks. If JSON mode returns invalid syntax, retry once with a lower temperature or a stronger system prompt. If the model calls a tool with missing parameters, ask the user for clarification rather than hallucinating a value.

Define a maximum turn count per task. If the user and bot oscillate beyond four turns without slot resolution, trigger a human handoff. Log full traces, including prompts and tool responses, so you can audit failures without repro

Top comments (0)