Building with AI · Guide 17 of 35

Building an AI Chatbot

Manage conversation state, context budgets and concurrent messages without treating generated replies as trusted facts.

IntermediateAbout 7 min readPractical exercise

Before you start: AI APIs · JavaScript arrays and functions. Reading time is an estimate; allow extra time for practice.

What you’ll learn

  • Define a chatbot’s supported tasks, evidence and handoff behavior.
  • Separate the visible transcript, model context and durable conversation state.
  • Prevent messages and late results from crossing conversation boundaries.
  • Evaluate multi-turn behavior, retention and recovery rather than a single reply.
On this page

Give the conversation a bounded job

A documentation chatbot helps a user find the right configuration for a package version. The first message might be “How do I export?” followed by “Does that work in version 3?” The second question depends on earlier context. Building a chatbot therefore means managing the evolving task and evidence, not merely placing a text box above a model endpoint.

State which documentation and versions the assistant covers, what it can do, and what happens outside that scope. It can explain an export option and link a supporting page; it need not access private accounts or change settings. Give it a useful question or handoff path when the version is unknown or the source does not answer. Avoid a persona that promises expertise beyond the evidence available.

Keep three kinds of state distinct

The visible transcript is what the user sees, including messages and failure indicators. The model context is what a particular request actually receives: instructions, selected turns, source passages and any tool results. Durable conversation state is what the application or provider stores between requests. These may overlap, but they are not the same object and should not be assumed to contain identical information.

Some APIs accept history on every request; others offer stored conversation objects or response chaining. Use the selected API’s documented mechanism and preserve its required protocol items. Display text alone may omit tool-call identifiers or other state needed for later turns. A conversation ID is a reference to stored state, not proof that the current user is allowed to read it; check ownership before loading or extending it.

Preserve roles and trusted instructions

A message needs an ID, role, content and association with its conversation. Server-controlled instructions define the application’s task. User messages request help; assistant messages record generated replies; tool results contain observations from executed operations. Do not accept a client-provided role such as system and silently promote it into trusted instructions. Reconstruct or validate the message envelope on the server.

A previous assistant reply can contain an error, and a retrieved document can contain hostile instructions. Neither becomes authoritative merely because it appears in history. Keep source provenance and uncertainties visible, and tell the user when a correction changes an earlier answer. Never use conversation text as the source of account authorization or permit a pasted message to grant tool privileges.

Remember the task without sending everything

For the export question, retain the package, version, user’s current goal and important constraints. Retrieve relevant documentation afresh when the version changes. Sending the entire history forever increases input size and can preserve stale assumptions. Dropping all earlier turns can lose the meaning of “that” or a requirement such as “do not change the data format.”

Use a measured context budget and a policy for recent turns, verified task facts and source references. If you summarize older messages, distinguish confirmed requirements from hypotheses and preserve negations. Let the user correct retained facts. Keep complete tool-call/result relationships when the provider protocol requires them; a generic “keep the last ten messages” rule can split a protocol pair or lose a crucial user instruction.

Route replies to the conversation that requested them

A user can leave conversation A for B while A is still generating. A late reply must not appear in B just because B is now selected. Attach both a conversation ID and a request ID to every result, and update the matching pending request. This also prevents a duplicate completion from being appended twice after a retry or reconnect.

The local simulation below uses trusted, already-authorized conversation state. Run it in Node.js or a browser console; it has no network, timers or model. It checks that A’s reply is ignored while B is active, B’s current reply is accepted, and the same completion is ignored a second time. Production code must also enforce server-side ownership and manage pending requests per conversation.

JavaScript: accept only a matching chat completion
function acceptReply(state, event) {
  if (!event || event.conversationId !== state.conversationId ||
      event.requestId !== state.pendingRequestId || state.pendingRequestId === null ||
      typeof event.text !== 'string' || !event.text.trim()) return state;
  return {
    ...state,
    pendingRequestId: null,
    messages: [...state.messages, { role: 'assistant', text: event.text }],
  };
}
const active = { conversationId: 'B', pendingRequestId: 'request-b', messages: [] };
const late = { conversationId: 'A', requestId: 'request-a', text: 'Reply for A' };
if (acceptReply(active, late) !== active) throw new Error('Cross-conversation write');
const event = { conversationId: 'B', requestId: 'request-b', text: 'Reply for B' };
const completed = acceptReply(active, event);
if (completed.messages.length !== 1 || completed.messages[0].text !== 'Reply for B') {
  throw new Error('Current reply lost');
}
if (acceptReply(completed, event) !== completed) throw new Error('Duplicate reply');
console.log('3 conversation-routing checks passed');

Expected output: “3 conversation-routing checks passed”. Identity equality confirms that ignored events leave the state untouched. This reducer is neither a persistence layer nor an authorization mechanism; it only models client result routing.

Represent interrupted turns and retention honestly

Show a user message once when submitted, then track whether its reply is pending, complete, interrupted or failed. A retry should refer to the same intended turn or explicitly create a new one, rather than silently duplicating the transcript. Streaming text is provisional until completion. Keep a stop control and a recovery path, and do not treat partial tool arguments as an executable action.

Decide whether conversations are temporary, device-local or stored for signed-in users. Explain the behavior accurately and provide the intended deletion controls. Clearing the visible transcript is not automatically deletion from server storage, logs, backups or provider state. Minimize retained personal information, isolate accounts and test sign-out and account-switch behavior while requests are pending. Preserve ordinary navigation even when a conversation service fails.

Test sequences, corrections and handoffs

Create short test conversations with expected outcomes at each turn. Include an ambiguous opening, a follow-up pronoun, a version correction, a request outside the documentation and a source that conflicts with an earlier answer. Check whether the bot asks a necessary question, uses the new version and corrects its old statement rather than defending it. These are conversational properties that a one-turn benchmark can miss.

Also test two users, two conversations, late success, late failure, duplicate submission and an interrupted stream. Verify which messages persist after refresh and whether a failed save is shown honestly. Measure source-supported answers and successful handoffs alongside response time. A human-support handoff should include a concise authorized summary and the unresolved question; it should not silently send an entire private transcript.

Put it into practice

Try it yourself

Write a four-turn documentation conversation: an ambiguous export question, a request for the version, a version-3 answer and a correction to version 2. For each turn, specify retained facts, relevant sources and expected reply behavior. Run the local routing example and add a wrong-request-ID case within conversation B.

Show hint

The correction must replace the version assumption and trigger new evidence selection. A reply for the wrong pending request must leave the current state unchanged. Explain separately how your server checks conversation ownership; the local IDs and reducer do not provide that security.

Optional practice

Check your understanding

5 questions to revisit the ideas. No score is saved, and you can retry. This is separate from Skill Quiz; it does not award XP or unlock guides.

1. A message remains visible in the transcript. What can you infer about the next model request?
2. A client supplies a valid conversation ID. What must the server still check?
3. The user corrects the package version from 3 to 2. What should the chatbot do?
4. Conversation A’s reply arrives while B is selected. What prevents it appearing in B?
5. The UI clears its transcript. What should you assume about server and provider copies?

Sources & further reading

Primary references for the concepts in this guide. Examples and exercises are illustrative; verify provider-specific behaviour against the version you use.