# RAG Chatbot - Quản lý Chat History & Tối ưu Token

## 1. Tại sao cần gửi history?

```
User: "Chính sách bảo hành là gì?"
Bot:  "Bảo hành 12 tháng cho sản phẩm điện tử..."

User: "Thời hạn đó có áp dụng cho hàng nhập khẩu không?"
                          ↑
          Không có history → LLM không biết "thời hạn đó" = 12 tháng
```

## 2. Cấu trúc message gửi lên LLM

```python
messages = [
    {"role": "system", "content": f"""
Bạn là trợ lý trả lời dựa trên tài liệu.
--- TÀI LIỆU ---
{retrieved_chunks}
"""},
    # Lịch sử chat
    {"role": "user",      "content": "Chính sách bảo hành là gì?"},
    {"role": "assistant", "content": "Bảo hành 12 tháng cho sản phẩm điện tử..."},
    # Câu hỏi hiện tại
    {"role": "user",      "content": "Thời hạn đó có áp dụng cho hàng nhập khẩu không?"}
]
```

## 3. Vấn đề: Token window có hạn

```
Token budget = System prompt + History + Retrieved chunks + Câu hỏi + Output

GPT-4o (128K context):
├── System prompt    : ~500 tokens
├── Retrieved chunks : ~2,000 tokens (5 chunks × 400)
├── History (10 lượt): ~4,000 tokens   ← chiếm nhiều
├── Câu hỏi          : ~50 tokens
├── Output           : ~1,000 tokens
└── Tổng             : ~7,550 tokens

Model nhỏ hơn (8K context):
├── System prompt    : ~500 tokens
├── Retrieved chunks : ~2,000 tokens
├── History (10 lượt): ~4,000 tokens   ← gần hết budget!
├── Câu hỏi          : ~50 tokens
└── Output           : ~1,000 tokens   ← không còn chỗ
```

## 4. Chiến lược tối ưu Token

Dùng nhiều LLM model, mỗi model xử lý 1 chức năng(model giá rẻ dùng cho tính năng đơn giản như Query Rewrite, prepare/intent rewrite:)

### 4.1. Sliding Window (Đơn giản nhất)

```python
# Chỉ giữ N lượt gần nhất
MAX_HISTORY = 5
recent_history = chat_history[-MAX_HISTORY * 2:]  # N lượt = 2N messages
```

### 4.2. Query Rewrite (Hiệu quả nhất)

Dùng 1 lần gọi model rẻ để viết lại câu hỏi, không cần gửi history cho LLM chính.

```python
# Bước 1: Rewrite (model rẻ, ~100 tokens)
rewrite_prompt = f"""
Dựa vào lịch sử chat, viết lại câu hỏi cuối cùng thành câu hoàn chỉnh,
đầy đủ ngữ cảnh, không cần lịch sử để hiểu.

Lịch sử:
User: Chính sách bảo hành là gì?
Bot: Bảo hành 12 tháng cho sản phẩm điện tử...
User: Thời hạn đó có áp dụng cho hàng nhập khẩu không?

Câu hỏi viết lại:"""

# Output: "Thời hạn bảo hành 12 tháng có áp dụng cho hàng nhập khẩu không?"

# Bước 2: Dùng câu rewrite để retrieve
chunks = vector_db.search(rewrite_output)

# Bước 3: Gửi lên LLM — chỉ chunks + câu rewrite, KHÔNG cần history
messages = [
    {"role": "system", "content": f"Tài liệu: {chunks}"},
    {"role": "user", "content": rewrite_output}
]
```

### 4.3. Hybrid: Tóm tắt + Lượt gần nhất

```python
# Tóm tắt lịch sử cũ + giữ 1-2 lượt gần nhất
messages = [
    system_prompt,                                        # 500 tokens
    {"role": "user", "content": f"Tóm tắt: {summary}"},  # 100 tokens
    *last_2_messages,                                     # ~800 tokens
    {"role": "user", "content": current_q},               # 50 tokens
]
# Tổng: ~1,450 tokens thay vì 7,050 tokens
```

### 4.4. Lọc history (Giữ phần có số liệu)

Chỉ giữ câu trả lời AI chứa số liệu, mốc, điều kiện — bỏ câu xã giao.

```python
def has_numbers_or_conditions(text):
    import re
    if re.search(r'\d+', text):        # Có số: 12 tháng, 500K...
        return True
    if any(w in text.lower() for w in ['nếu', 'khi', 'áp dụng']):
        return True
    return False
```

```
Lịch sử gốc (10 messages = 4,000 tokens):
  User: "Bảo hành bao lâu?"
  AI:   "12 tháng cho điện tử, 6 tháng phụ kiện."    ← GIỮ (có số)
  User: "Có freeship không?"
  AI:   "Có, miễn phí cho đơn trên 500K."            ← GIỮ (có số)
  User: "Cảm ơn"
  AI:   "Không có gì!"                                ← BỎ
  User: "Đơn tôi 400K thì sao?"
  AI:   "Đơn dưới 500K sẽ tính phí ship 30K."        ← GIỮ (có số)

Sau lọc (5 messages ≈ 800 tokens):
  "Bảo hành: 12 tháng điện tử, 6 tháng phụ kiện."
  "Freeship trên 500K."
  "Đơn 400K: phí ship 30K."
```

### 4.5. Cache câu trả lời

```python
# Nếu user hỏi lại hoặc câu hỏi gần giống → cache hit
cache_key = hash(normalized_query + top_chunk_ids)
if cached := redis.get(cache_key):
    return cached  # Không tốn token
```

## 5. So sánh chi phí

Giả sử 1,000 lượt chat/ngày, dùng GPT-4o:

| Chiến lược | Tokens/lượt | Tokens/ngày | Chi phí/ngày |
|---|---|---|---|
| Gửi full history | 7,050 | 7M | ~$17.5 |
| Sliding window (3 lượt) | 4,050 | 4M | ~$10.0 |
| Tóm tắt + 2 lượt | 1,450 | 1.45M | ~$3.6 |
| **Query rewrite + chunks** | **2,600** | **2.6M** | **~$6.5** |
| Query rewrite + model rẻ | 2,600 | 2.6M | **~$0.5** |

> **Khuyến nghị production**: Query rewrite (model rẻ/local) + Retrieve chunks + LLM mạnh

## 6. Flow hoàn chỉnh với History

```
User query + Chat History
        │
        ▼
   ┌──────────────┐
   │ Query Rewrite │  ← Dùng history để viết lại câu hỏi
   │ (model rẻ)    │     Output: câu hỏi hoàn chỉnh, đầy đủ ngữ cảnh
   └──────┬───────┘
          ▼
   ┌──────────────┐
   │   Retrieve    │  ← Dùng câu đã rewrite để search Vector DB
   │   top-K       │
   └──────┬───────┘
          ▼
   ┌──────────────┐
   │  Build Prompt │  ← System + Retrieved chunks + Câu hỏi rewrite
   │              │     KHÔNG cần gửi history lên LLM chính
   └──────┬───────┘
          ▼
   ┌──────────────┐
   │   LLM Call    │  ← Model mạnh (GPT-4o, Claude...)
   └──────┬───────┘
          ▼
   ┌──────────────┐
   │   Response    │  ← Trả lời + trích dẫn nguồn
   │  + Citations  │
   └──────────────┘
```

## 7. KV Cache — Ghi chú

**KV Cache** là kỹ thuật tối ưu phía server (GPU), giúp tái sử dụng phép tính attention đã tính trước đó khi sinh token mới.

- **Tự host model**: KV Cache giúp tăng tốc inference, tiết kiệm GPU compute
- **Dùng API (OpenAI, Claude...)**: KV Cache không giảm chi phí token — bạn vẫn bị tính tiền đầy đủ cho mọi token gửi vào, dù server có cache hay không
- **OpenAI Prompt Caching**: Giảm 50% giá input tokens nếu prefix giống nhau, nhưng chỉ áp dụng cho prefix

**Kết luận**: KV Cache giúp server tính nhanh hơn, nhưng vẫn cần tối ưu token ở phía application.

## 8. Tóm tắt

| Giai đoạn | Giải pháp | Token |
|---|---|---|
| Query Rewrite | Model rẻ/local, chỉ 1 câu | ~100 tokens |
| Retrieve | Vector search + Rerank | Không tốn API |
| LLM Call | Chỉ chunks + câu rewrite (không history) | ~2,600 tokens |
| **Tổng mỗi lượt** | | **~2,700 tokens** |
