LEVEL 52 QUEST
대화 히스토리 & 컨텍스트 관리
대화 저장, 토큰 관리, 장기 메모리, 멀티테넌트 분리
대화 저장 DB 설계
conversations & messages 테이블
AI 대화를 체계적으로 저장하려면 대화방(conversation)과 메시지(message)를 분리하여 설계합니다. SaaS에서는 테넌트(고객사) 분리도 필수입니다.
-- 대화방 테이블
CREATE TABLE conversations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL, -- SaaS 고객사 ID
user_id INT NOT NULL, -- 최종 사용자 ID
title VARCHAR(200), -- 대화 제목 (첫 질문 기반 자동 생성)
summary TEXT, -- 대화 요약 (장기 메모리용)
total_tokens INT DEFAULT 0, -- 누적 토큰 사용량
status ENUM('active','archived','deleted') DEFAULT 'active',
metadata JSON, -- 추가 정보 (agent 타입, 모델 등)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_tenant_user (tenant_id, user_id),
INDEX idx_updated (updated_at)
);
-- 메시지 테이블
CREATE TABLE messages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversation_id BIGINT NOT NULL,
role ENUM('user','assistant','system','tool') NOT NULL,
content TEXT NOT NULL,
token_count INT DEFAULT 0,
tool_name VARCHAR(100), -- 도구 호출 시 도구 이름
tool_args JSON, -- 도구 호출 파라미터
cost_usd DECIMAL(10,8) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
INDEX idx_conv_created (conversation_id, created_at)
);
컨텍스트 윈도우 관리
토큰 수 제한의 중요성
AI API는 최대 컨텍스트 크기가 정해져 있습니다. 대화가 길어지면 오래된 메시지를 잘라내야 합니다. 단순히 자르는 것이 아니라 전략적으로 관리해야 합니다.
| 전략 | 방법 | 장점 | 단점 |
|---|---|---|---|
| 슬라이딩 윈도우 | 최근 N개 메시지만 유지 | 구현 쉬움 | 오래된 맥락 유실 |
| 토큰 카운트 | 총 토큰 수 기준으로 잘라냄 | 정확한 비용 관리 | 구현 복잡 |
| 요약 + 최근 | 오래된 대화 요약 + 최근 메시지 | 맥락 보존 + 비용 절감 | 요약에 추가 API 비용 |
/**
* 대화 컨텍스트 매니저
* 토큰 한도 내에서 최적의 메시지 구성
*/
class ConversationManager {
private PDO $db;
private int $maxContextTokens;
public function __construct(PDO $db, int $maxContextTokens = 4000) {
$this->db = $db;
$this->maxContextTokens = $maxContextTokens;
}
/** 새 대화 시작 */
public function createConversation(int $tenantId, int $userId): int {
$stmt = $this->db->prepare("
INSERT INTO conversations (tenant_id, user_id) VALUES (?, ?)
");
$stmt->execute([$tenantId, $userId]);
return (int)$this->db->lastInsertId();
}
/** 메시지 저장 */
public function addMessage(
int $convId, string $role, string $content,
float $cost = 0
): void {
$tokens = $this->estimateTokens($content);
$stmt = $this->db->prepare("
INSERT INTO messages (conversation_id, role, content, token_count, cost_usd)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$convId, $role, $content, $tokens, $cost]);
// 대화방 토큰 합계 업데이트
$stmt = $this->db->prepare("
UPDATE conversations
SET total_tokens = total_tokens + ?
WHERE id = ?
");
$stmt->execute([$tokens, $convId]);
// 첫 메시지면 제목 자동 생성
if ($role === 'user') {
$this->autoSetTitle($convId, $content);
}
}
/**
* 컨텍스트 윈도우에 맞는 메시지 목록 반환
* 전략: 요약(있으면) + 최근 메시지 (토큰 한도 내)
*/
public function getContextMessages(int $convId): array {
$messages = [];
$usedTokens = 0;
// 1. 대화 요약이 있으면 먼저 포함
$stmt = $this->db->prepare("SELECT summary FROM conversations WHERE id = ?");
$stmt->execute([$convId]);
$summary = $stmt->fetchColumn();
if ($summary) {
$summaryTokens = $this->estimateTokens($summary);
$messages[] = [
'role' => 'system',
'content' => "[이전 대화 요약]\n{$summary}"
];
$usedTokens += $summaryTokens;
}
// 2. 최근 메시지를 역순으로 가져와서 토큰 한도 체크
$stmt = $this->db->prepare("
SELECT role, content, token_count
FROM messages
WHERE conversation_id = ?
ORDER BY created_at DESC
");
$stmt->execute([$convId]);
$recentMessages = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($usedTokens + $row['token_count'] > $this->maxContextTokens) {
break; // 토큰 한도 초과 → 여기서 중단
}
$recentMessages[] = [
'role' => $row['role'],
'content' => $row['content'],
];
$usedTokens += $row['token_count'];
}
// 역순으로 가져왔으니 다시 시간순으로 정렬
$messages = array_merge($messages, array_reverse($recentMessages));
return $messages;
}
/** 토큰 수 추정 (한국어: 문자 수 / 1.5 근사) */
private function estimateTokens(string $text): int {
// 한국어는 글자당 약 1.5~2 토큰
// 영어는 단어당 약 1.3 토큰
$koreanChars = preg_match_all('/[\x{AC00}-\x{D7A3}]/u', $text);
$totalChars = mb_strlen($text);
$englishChars = $totalChars - $koreanChars;
return (int)($koreanChars * 1.7 + $englishChars * 0.4);
}
/** 첫 메시지 기반 제목 자동 생성 */
private function autoSetTitle(int $convId, string $firstMessage): void {
$stmt = $this->db->prepare("
SELECT title FROM conversations WHERE id = ?
");
$stmt->execute([$convId]);
if (empty($stmt->fetchColumn())) {
$title = mb_substr($firstMessage, 0, 50);
$stmt = $this->db->prepare("UPDATE conversations SET title = ? WHERE id = ?");
$stmt->execute([$title, $convId]);
}
}
}
🔒
여기까지는 미리보기입니다
대화 히스토리 & 컨텍스트 관리
무료 가입하면 이어서 볼 수 있고, 강의를 완료할 때마다 XP와 레벨이 쌓입니다.
Google로 3초 만에 시작 →🧵 Threads로 시작무료 공개 강의 둘러보기 (Lv.1~3)무료 가입하면 이어서 볼 수 있고, 강의를 완료할 때마다 XP와 레벨이 쌓입니다.