# How to Build a Chatbot with Complex Multi-Turn Prompts Using Claude: A Complete Guide

> Build a complex multi-turn chatbot with Claude. Learn how to manage conversation history and leverage system prompts for engaging AI interactions. Get the complete guide.

- Repository: [Anthropic/prompt-eng-interactive-tutorial](https://github.com/anthropics/prompt-eng-interactive-tutorial)
- Tags: how-to-guide
- Published: 2026-03-09

---

**You can build a multi-turn chatbot with Claude by maintaining a single conversation list that includes a system prompt, appending each user message and assistant response, and passing the entire history to the `client.messages.create` method with every new turn.**

The `anthropics/prompt-eng-interactive-tutorial` repository provides a comprehensive blueprint for assembling complex multi-turn chatbots using Claude's Messages API. By structuring prompts as modular components and managing conversation state through a simple Python list, you can create sophisticated conversational agents that maintain context across dozens of turns.

## Setting Up the Anthropic Client

### Installation and Initialization

Before building the chatbot loop, you must install the Anthropic SDK and initialize the client with your API credentials. As demonstrated in `09_Complex_Prompts_from_Scratch.ipynb`, the setup involves loading environment variables and creating an `Anthropic` client instance.

```python
!pip install anthropic

import anthropic

# Initialize with your API key and chosen model

API_KEY = "YOUR_API_KEY"
MODEL_NAME = "claude-3-haiku-20240307"

client = anthropic.Anthropic(api_key=API_KEY)

```

## Creating the Conversation Loop

### The get_completion Helper Function

The tutorial provides a reusable `get_completion` function that wraps `client.messages.create`. This helper accepts a **system prompt**, a **user prompt**, and an optional **prefill** (the assistant's first reply), allowing you to control the model's behavior and output format consistently.

```python
def get_completion(prompt: str, system_prompt: str = "", prefill: str = "") -> str:
    message = client.messages.create(
        model=MODEL_NAME,
        max_tokens=2000,
        temperature=0.0,
        system=system_prompt,
        messages=[
            {"role": "user", "content": prompt},
            {"role": "assistant", "content": prefill},
        ],
    )
    return message.content[0].text

```

### Managing Conversation State

For true multi-turn capabilities, you must maintain a **conversation state** as a Python list of message dictionaries. Each dictionary contains `"role"` and `"content"` keys. Before each API call, append the newest user message; after receiving the response, append Claude's reply to the list. This list becomes the `messages` argument for `client.messages.create`.

```python
conversation = [{"role": "system", "content": SYSTEM_PROMPT}]

while True:
    user_msg = input("You: ")
    conversation.append({"role": "user", "content": user_msg})
    
    reply = client.messages.create(
        model=MODEL_NAME,
        max_tokens=1000,
        temperature=0.0,
        messages=conversation
    ).content[0].text
    
    print("Claude:", reply)
    conversation.append({"role": "assistant", "content": reply})

```

## Structuring Complex Multi-Turn Prompts

### Modular Prompt Components

Complex chatbots require prompts that combine multiple elements without becoming unwieldy. The tutorial demonstrates breaking prompts into reusable blocks: **system description**, **task instructions**, **example interactions**, and **data context**. This modular approach allows you to swap task-specific sections while maintaining a consistent system persona.

### Combining Elements for Each Turn

The `09_Complex_Prompts_from_Scratch.ipynb` notebook includes a **"Combine Elements"** section that demonstrates concatenating these blocks in a deterministic order before feeding them to Claude. This ensures that every turn receives the full context required for coherent responses while keeping the overall prompt size within the model's token limits.

## Advanced Patterns for Complex Chatbots

### Chaining Prompts for Retrieval-Augmented Generation

For chatbots that require external knowledge, you can **chain** multiple Claude calls. As shown in `AmazonBedrock/10.1_Appendix_Chaining Prompts.ipynb`, a first call performs a retrieval step (searching a knowledge base), and a second call reasons over the retrieved data to generate the final response.

```python

# Step 1 – Retrieve relevant docs (illustrative; actual retrieval logic lives in Appendix 10.4)

def retrieve(query: str) -> str:
    # Placeholder – in a real notebook you would call a vector store or search API

    return "Relevant excerpt about tax elections..."

# Step 2 – Build a prompt that includes retrieved context

def build_chat_prompt(history: str, user_msg: str, retrieved: str) -> str:
    return (
        f"{history}\n"
        f"Context: {retrieved}\n"
        f"User: {user_msg}\n"
        f"Assistant:"
    )

conversation_history = ""
while True:
    user_msg = input("\nYou: ")
    if user_msg.lower() in {"exit", "quit"}:
        break
    ctx = retrieve(user_msg)
    prompt = build_chat_prompt(conversation_history, user_msg, ctx)
    reply = get_completion(prompt, system_prompt="You are a tax‑law chatbot.")
    print("\nClaude:", reply)
    conversation_history = f"{prompt} {reply}"

```

### Integrating Tool Use

The `AmazonBedrock/10.2_Appendix_Tool Use.ipynb` notebook demonstrates Claude's native **tool-use capability**, allowing your chatbot to call external functions (such as Python code execution or API lookups) during a conversation. This enables smarter bots that can execute code or retrieve real-time data on the fly.

## Key Files in the Tutorial Repository

| File | What you’ll find | Why it matters |
|------|------------------|----------------|
| [`Anthropic 1P/09_Complex_Prompts_from_Scratch.ipynb`](https://github.com/anthropics/prompt-eng-interactive-tutorial/blob/master/Anthropic%201P/09_Complex_Prompts_from_Scratch.ipynb) | Full walkthrough of building a complex multi‑turn chatbot, including the `get_completion` helper and the “Example Playground”. | Primary source for the chatbot pattern. |
| [`Anthropic 1P/hints.py`](https://github.com/anthropics/prompt-eng-interactive-tutorial/blob/master/Anthropic%201P/hints.py) | Small utility functions (e.g., token‑count helpers, pretty‑printing) used throughout the notebooks. | Helpful when scaling the chatbot to respect token limits. |
| [`AmazonBedrock/10.1_Appendix_Chaining Prompts.ipynb`](https://github.com/anthropics/prompt-eng-interactive-tutorial/blob/master/AmazonBedrock/anthropic/10.1_Appendix_Chaining%20Prompts.ipynb) | Demonstrates how to chain multiple Claude calls (search → reasoning). | Extends the basic chatbot to include tool use or retrieval. |
| [`AmazonBedrock/10.2_Appendix_Tool Use.ipynb`](https://github.com/anthropics/prompt-eng-interactive-tutorial/blob/master/AmazonBedrock/anthropic/10.2_Appendix_Tool%20Use.ipynb) | Shows Claude’s native tool‑use capability (e.g., calling a Python function). | Enables smarter bots that can execute code or look up data on the fly. |
| [[`AmazonBedrock/requirements.txt`](https://github.com/anthropics/prompt-eng-interactive-tutorial/blob/main/AmazonBedrock/requirements.txt)](https://github.com/anthropics/prompt-eng-interactive-tutorial/blob/master/AmazonBedrock/requirements.txt) | Lists required Python packages (`anthropic`, `boto3`, etc.). | Quick way to set up a development environment for the examples. |

## Summary

- **Maintain conversation state** as a Python list of message dictionaries with `"role"` and `"content"` keys, passing the entire list to `client.messages.create` on every turn.
- **Use the `get_completion` helper** from `09_Complex_Prompts_from_Scratch.ipynb` to standardize API calls with system prompts, user prompts, and optional prefills.
- **Structure complex prompts** by breaking them into modular blocks (system description, instructions, examples) that you combine deterministically before each API call.
- **Chain prompts** for retrieval-augmented generation by making an initial Claude call to fetch context, then a second call to generate the response based on that context.
- **Integrate tool use** by leveraging Claude's native function-calling capabilities demonstrated in the Appendix notebooks to execute code or query external APIs during conversations.

## Frequently Asked Questions

### How do I maintain conversation history in a Claude chatbot?

You maintain conversation history by storing each interaction in a Python list of dictionaries, where each dictionary contains `"role"` (either `"user"`, `"assistant"`, or `"system"`) and `"content"` keys. Before every API call to `client.messages.create`, append the new user message to this list; after receiving Claude's response, append the assistant's reply. Pass the entire list as the `messages` parameter so Claude sees the full context of the conversation.

### What is the purpose of the system prompt in multi-turn conversations?

The **system prompt** sets the global behavior, persona, and constraints for Claude across all turns of the conversation. By including it as the first message with `"role": "system"` (or via the `system` parameter in the API call), you ensure that Claude maintains a consistent tone, follows specific formatting rules, and adheres to safety guidelines throughout the entire chat session, regardless of how many turns the conversation lasts.

### How can I add external knowledge retrieval to my Claude chatbot?

You can add external knowledge by **chaining prompts**, as demonstrated in `AmazonBedrock/10.1_Appendix_Chaining Prompts.ipynb`. First, make a Claude call (or use a traditional search method) to retrieve relevant documents based on the user's query. Then, construct a second prompt that includes both the conversation history and the retrieved context, and pass this to Claude for the final response. This retrieval-augmented generation pattern ensures your chatbot answers based on up-to-date or proprietary information not contained in the model's training data.

### What is the difference between using prefill and appending assistant messages?

**Prefill** is a technique where you include an initial assistant message in the `messages` array (with `"role": "assistant"`) when calling the API. This primes Claude to continue from that specific starting point, ensuring it adopts a desired format or tone immediately. In contrast, **appending assistant messages** refers to adding Claude's actual generated responses to the conversation history after each API call, which builds the ongoing context for subsequent turns. Prefill happens before generation to steer the output; appending happens after generation to preserve history.