Điểm mấu chốt: cái hiển thị cho người dùng và cái gửi lên model là hai thứ khác nhau. DB lưu đầy đủ vĩnh viễn; prompt chỉ lấy ra một tập con.
Lược đồ tối thiểu: bảng conversations (id, user_id, title, created_at) và bảng messages (id, conversation_id, role, content, model, prompt_version, input_tokens, output_tokens, created_at). Lưu thêm model và prompt_version ngay trên từng message — sau này điều tra chất lượng bạn sẽ cần biết câu trả lời đó do cấu hình nào sinh ra.
Lấy ra để gửi lên model: system prompt + N lượt gần nhất, cộng thêm bản tóm tắt các lượt cũ nếu hội thoại dài. Chốt chặn phải tính bằng token, không phải bằng số lượt — mười lượt ngắn khác hẳn mười lượt có dán log lỗi vào.
const budget = MODEL_CONTEXT - RESERVED_FOR_OUTPUT - countTokens(systemPrompt)
const recent = takeFromEndUntilBudget(messages, budget)Những điểm hay sai:
- Giữ nguyên cặp tool call và tool result. Cắt ngang một cặp làm hỏng định dạng và provider trả lỗi.
- Không lưu context RAG đã chèn vào message. Lưu id của tài liệu, dựng lại khi cần — nếu không, DB phình lên và bạn gửi lại nguyên khối tài liệu ở mỗi lượt.
- Đừng để hội thoại kéo dài vô hạn. Khi vượt ngưỡng, đề xuất người dùng mở hội thoại mới; vừa rẻ hơn vừa cho câu trả lời tập trung hơn.