必须用数据库或Redis按conversation_id持久化存储对话历史,存role、content、created_at等结构化字段,调用API前按时间升序拼装messages并严格校验权限与token限制。

PHP里怎么存多轮对话的上下文
靠全局变量或单次请求生命周期内的数组根本不行——用户刷新页面、换设备、甚至只是等几秒再发消息,上下文就断了。必须把对话历史落盘,最稳妥的是用数据库按会话ID存,Redis 适合高频读写但得配好过期策略。
关键不是“能不能存”,而是“存什么、怎么取、谁来清理”:
-
session_id或前端传来的conversation_id必须作为主键,不能依赖 PHP 的$_SESSION,因为 AI 接口常走无状态 API(比如前后端分离) - 每条记录至少含:
role("user"或"assistant")、content(原始文本)、created_at(时间戳,用于截断老消息) - 别存完整 JSON 字符串进一个字段——后期想查某轮是否含敏感词、统计 token 消耗、做向量检索都很难
- MySQL 建议用
JSON类型字段存结构化内容,或拆成messages表 + 外键关联conversations表
调用大模型 API 前怎么拼装上下文
OpenAI、Qwen、DeepSeek 等主流接口都要求 messages 是数组,顺序不能乱,且有长度/Token 限制。PHP 不是 Python,没有内置的 tokenizer,所以不能只看字符数,得用对应模型的 token 计算逻辑(或保守截断)。
实操要点:
立即学习“PHP免费学习笔记(深入)”;
- 从数据库查出该
conversation_id的最近 N 条(比如 10 条),按created_at升序排列,确保system→user→assistant交替出现 - 如果配置了
system角色提示词,必须放在messages[0],且只放一次;重复插入会导致模型忽略或报错"system message must be first" - 用
array_slice()截断前若干条时,别用array_reverse()再翻转——直接ORDER BY created_at ASC查出来就是正确顺序 - 上线前务必压测:当某次请求携带 20 轮对话、每轮平均 300 字,token 超限后 API 返回
"context_length_exceeded",这时要主动丢弃最老的几轮,而不是让整个请求失败
如何防止上下文膨胀和越权访问
用户 A 的对话历史被用户 B 通过改 conversation_id 参数看到,或者一条对话滚到几千条记录拖垮数据库——这类问题在真实项目里爆发过多次。
防御动作必须硬编码进逻辑层:
- 每次查询
messages前,先查conversations表确认user_id匹配当前登录态(比如$_SESSION['user_id']或 JWT 解析出的uid),不匹配直接 403 - 对每个
conversation_id设置最大轮数(如 50 轮),插入新消息前用SELECT COUNT(*)判断,超了就DELETE ... ORDER BY created_at ASC LIMIT X清理最旧的 - Redis 方案中,不要用
conversation_id作 key 直接SET整个数组——改用conversation:{id}:messages加conversation:{id}:meta分离存储,方便单独设 TTL - 日志里打点记录每次上下文加载的条数和总 token 估算值,方便后续发现异常增长
为什么不能用 $_SESSION 存对话历史
因为 $_SESSION 绑定的是 PHP 的 session ID,而 session ID 默认靠 Cookie 传递。用户禁用 Cookie、用 cURL 测试、前后端跨域、甚至某些 CDN 会剥离 Cookie —— 这些场景下 $_SESSION 就失效了。
更麻烦的是:session 文件或 Redis 中的 session 数据不会自动关联用户账号,一旦用户登出再登录,旧 session 还在,但没人清理,变成僵尸上下文。
所以实际项目里,只要涉及“用户可长期续聊”,就必须用显式 conversation_id + 用户身份双校验,哪怕多一次数据库查询也得做。
token 截断逻辑、角色顺序、权限校验——这三个地方漏掉任何一个,上线后都会变成半夜告警的根源。



















