本文目录导读:

- 前提:你需要一个AI接口
- 方案一:基于Session的简单对话(适合测试或低并发场景)
- 方案二:基于数据库的持久化对话(适用于生产环境)
- 方案三:基于Redis的缓存对话(高性能,适合实时场景)
- 方案四:压缩Token的优化技巧(大模型API按Token收费)
- 方案五:使用WebSocket实现流式对话(高级)
- 生产环境推荐
在PHP中实现多轮对话,核心在于维护对话上下文,因为HTTP协议本身是无状态的,所以你需要一种机制来识别用户并存储每次对话的“记忆”。
以下是几种主流的实现方案,从简单到复杂排列:
前提:你需要一个AI接口
无论是调用OpenAI、Claude、文心一言、通义千问的API,还是本地部署的大模型,PHP项目只是作为一个“中间人”来转发请求和管理历史记录。
基于Session的简单对话(适合测试或低并发场景)
原理:利用PHP的Session(服务端存储,默认依赖Cookie)来存储对话历史数组。
优点:实现简单,无需数据库。 缺点:数据存储在服务器内存/文件里,不持久化,重启服务器或Session过期后对话丢失。
<?php
session_start();
// 1. 初始化对话历史(如果不存在)
if (!isset($_SESSION['dialogue'])) {
$_SESSION['dialogue'] = [];
}
// 2. 处理用户输入
$userInput = $_POST['message'] ?? '';
if ($userInput) {
// 3. 将用户消息加入历史
$_SESSION['dialogue'][] = ['role' => 'user', 'content' => $userInput];
// 4. 调用AI接口(这里以OpenAI为例简化)
$apiKey = 'your-api-key';
$data = [
'model' => 'gpt-4o-mini',
'messages' => $_SESSION['dialogue'] // 将整个历史作为上下文发送
];
// 使用cURL发送请求
$ch = curl_init('https://api.openai.com/v1/chat/completions');
// ... (cURL配置,设置POST、Header等)
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
$aiReply = $result['choices'][0]['message']['content'];
// 5. 将AI回复加入历史
$_SESSION['dialogue'][] = ['role' => 'assistant', 'content' => $aiReply];
echo json_encode(['reply' => $aiReply]);
}
?>
特性:
- 同一个浏览器、同一个用户在同一个Session内可以连续对话。
- 内存控制:历史越来越多,可能需要限制长度(如只保留最近20条)。
- 不跨设备:用户换浏览器或清除Cookie,对话就断了。
基于数据库的持久化对话(适用于生产环境)
原理:使用MySQL/Redis存储对话历史,通过user_id或session_id加conversation_id来标识不同对话。
优点:持久化、可回溯、支持多设备(用户登录后)、支持多场景(新建对话、历史列表)。 缺点:需要设计数据库表,实现稍复杂。
-- 1. 对话表
CREATE TABLE conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,VARCHAR(255) DEFAULT '新对话',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 消息表
CREATE TABLE messages (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT NOT NULL,
role ENUM('user', 'assistant', 'system') NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);
PHP核心逻辑:
// 假设已登录,$userId 已知
$conversationId = $_POST['conversation_id']; // 前端传过来,或新建
// 1. 如果没有 conversation_id,创建一个新的对话
if (!$conversationId) {
$stmt = $pdo->prepare("INSERT INTO conversations (user_id) VALUES (?)");
$stmt->execute([$userId]);
$conversationId = $pdo->lastInsertId();
}
// 2. 保存用户消息
$stmt = $pdo->prepare("INSERT INTO messages (conversation_id, role, content) VALUES (?, 'user', ?)");
$stmt->execute([$conversationId, $userInput]);
// 3. 从数据库中读取该对话的完整历史
$stmt = $pdo->prepare("SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY id ASC");
$stmt->execute([$conversationId]);
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 4. 发送给AI
$data = ['messages' => $history]; // 包含历史
// ... 调用API
// 5. 保存AI回复
$stmt = $pdo->prepare("INSERT INTO messages (conversation_id, role, content) VALUES (?, 'assistant', ?)");
$stmt->execute([$conversationId, $aiReply]);
前端配合:
- 前端需要保存当前对话的
conversation_id(通常存在LocalStorage或URL参数)。 - 切换对话时,传入对应的ID,后端重新加载历史。
基于Redis的缓存对话(高性能,适合实时场景)
原理:使用Redis的List数据结构存储消息序列,利用Redis的TTL做自动过期。
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$conversationKey = "conversation:" . $sessionId; // 用SessionID或用户ID+时间戳
// 追加用户消息
$redis->rPush($conversationKey, json_encode(['role' => 'user', 'content' => $userInput]));
// 获取最近N条消息作为上下文(常用优化手段)
$messages = $redis->lRange($conversationKey, -20, -1); // 只取最近20条
$context = array_map('json_decode', $messages);
// 调用AI...
// 追加AI回复
$redis->rPush($conversationKey, json_encode(['role' => 'assistant', 'content' => $aiReply]));
// 设置过期时间(1小时无操作自动清理)
$redis->expire($conversationKey, 3600);
注意:Redis断电丢失,如果对话重要需配持久化;或者用作“热点缓存”,同时用MySQL做落盘备份。
压缩Token的优化技巧(大模型API按Token收费)
当对话轮数很多时,历史会变得非常长,直接发过去会:
- 超出模型最大上下文长度(如4k、8k、128k)。
- 消耗大量Token(= 烧钱)。
常见策略:
- 截断(滑动窗口):只保留最近X轮对话(如20轮),压缩**:对话超过一定长度时,自动生成一段摘要,然后用摘要+最近几轮消息作为上下文。
- 遗忘机制:如果用户提到“重新开始”或“清除历史”,清空历史重新发。
- 限制单条消息长度:对每个用户的单次输入做截断(如5000字符),并提示用户“太长,请精简”。
示例:保留最后10轮
$history = ...; // 从数据库或Session获取
if (count($history) > 10) {
// 可以只保留系统提示 + 最新的10条
$history = array_slice($history, -10);
}
使用WebSocket实现流式对话(高级)
如果希望像ChatGPT那样一个字一个字打印(Streaming),纯HTTP + AJAX长轮询体验差,推荐:
- 方案:前端使用 Server-Sent Events (SSE) 或 WebSocket。
- 后端:PHP开启
ob_flush+flush逐字输出,或用Swoole/Workerman的长连接服务接收SSE事件。 - 历史管理:仍由PHP服务端按上述方案管理,与前端流式输出并行。
生产环境推荐
| 需求 | 推荐方案 |
|---|---|
| 简单测试,个人工具 | Session |
| 用户系统,产品级应用 | 数据库(MySQL) |
| 高并发,毫秒级响应 | Redis + MySQL(双写) |
| Token成本敏感 | 滑动窗口 + 摘要 |
| 流式打字效果 | SSE/WebSocket + 方案二/三 |
关键点:无论选择哪个方案,都要记得限制上下文长度(控制Token数量),否则你的服务器和钱包都会被耗尽。