DeepSeek API 会话记忆实现:从问题复现到代码落地

0 阅读

问题现象:AI 为什么「失忆」了

很多开发者第一次调用 DeepSeek API 时会遇到这样的困惑:第一轮问「北京今天天气怎么样」,AI 回答得挺好;紧接着追问「那上海呢」,AI 却一脸茫然,仿佛刚才的对话从未发生。

这不是 bug,而是大模型 API 的默认行为。我们用 APIFOX 做个对比实验:

第一轮请求(正常)

{
  "messages": [
    {"role": "system", "content": "你是一个 helpful 的助手"},
    {"role": "user", "content": "推荐一本 Python 入门书"}
  ]
}

AI 返回:《Python编程:从入门到实践》是一本经典选择……

第二轮请求(异常)

{
  "messages": [
    {"role": "system", "content": "你是一个 helpful 的助手"},
    {"role": "user", "content": "作者是谁"}
  ]
}

AI 返回:请问您指的是哪本书的作者?

问题很明显:第二轮请求里完全没有提到「Python 入门书」,AI 自然不知道我们在问什么。

根源:大模型的「无状态」设计

DeepSeek API 本质上是一个 HTTP 接口,每次请求都是独立的。服务器不会为你的对话开辟一块内存空间,也不会自动关联你的上一次提问。

这种设计有几个现实考量:

  • 成本:为每个用户维护会话状态需要大量服务器资源
  • 并发:无状态架构更容易水平扩展,应对高并发
  • 灵活性:开发者可以自定义记忆策略,比如只保留最近 3 轮、或者按 token 数截断

代价就是:记忆功能必须由调用方实现。

实现原理:messages 列表的完整传递

解决方案很简单:每次请求时,把历史对话全部塞进 messages 数组。

正确的第二轮请求应该是这样:

{
  "messages": [
    {"role": "system", "content": "你是一个 helpful 的助手"},
    {"role": "user", "content": "推荐一本 Python 入门书"},
    {"role": "assistant", "content": "《Python编程:从入门到实践》是一本经典选择……"},
    {"role": "user", "content": "作者是谁"}
  ]
}

注意数组里的第三项:我们必须把 AI 的第一轮回答原样传回去,角色标记为 assistant。这样 DeepSeek 才能理解「作者是谁」是在追问那本书。

格式规范

messages 数组必须满足以下规则:

要求 说明
顺序严格交替 user → assistant → user → assistant,不能连续两个 user
以 system 开头(可选) 如果有系统提示词,必须放在最前面
以 user 结尾 最后一个必须是用户的新问题
内容原样保留 不要修改历史对话的原文,包括 AI 的废话

APIFOX 实操验证

打开 APIFOX,在之前的 DeepSeek 接口配置基础上,修改 Body 部分:

步骤 1:构造完整上下文

先手动模拟一轮对话。在「前置操作」里添加一个「自定义脚本」,或者直接编辑 Body:

{
  "model": "deepseek-chat",
  "messages": [
    {"role": "system", "content": "你是资深 Python 开发者"},
    {"role": "user", "content": "解释下 Python 的 GIL"},
    {"role": "assistant", "content": "GIL(Global Interpreter Lock)是 CPython 解释器的全局锁……"},
    {"role": "user", "content": "这对多线程有什么影响"}
  ],
  "stream": false
}

步骤 2:发送并观察响应

点击发送后,AI 的回答应该是这样的:

由于 GIL 的存在,同一时刻只有一个线程能执行 Python 字节码。这意味着……

关键词「由于 GIL 的存在」说明 AI 成功关联了上文。如果它重新解释「什么是 GIL」,说明你的 messages 顺序有问题。

步骤 3:流式输出同样适用

把 stream 改为 true,messages 的格式不变。只是响应变成逐字推送,需要前端做累积显示。

Python 代码封装示例

实际项目中,你需要在服务端维护每个用户的对话历史。这里给一个最小可用的 Flask 示例:

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

# 简易内存存储:生产环境请用 Redis
user_sessions = {}

DEEPSEEK_API_KEY = "your-api-key"
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"

@app.route('/chat', methods=['POST'])
def chat():
    user_id = request.json.get('user_id')
    new_message = request.json.get('message')
    
    # 获取或初始化该用户的会话历史
    if user_id not in user_sessions:
        user_sessions[user_id] = [
            {"role": "system", "content": "你是一个 helpful 的助手"}
        ]
    
    # 追加用户新消息
    user_sessions[user_id].append(
        {"role": "user", "content": new_message}
    )
    
    # 调用 DeepSeek API
    resp = requests.post(
        DEEPSEEK_URL,
        headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}"},
        json={
            "model": "deepseek-chat",
            "messages": user_sessions[user_id],
            "stream": False
        }
    )
    
    result = resp.json()
    assistant_reply = result['choices'][0]['message']['content']
    
    # 关键:把 AI 回复也存进历史
    user_sessions[user_id].append(
        {"role": "assistant", "content": assistant_reply}
    )
    
    return jsonify({"reply": assistant_reply})

if __name__ == '__main__':
    app.run(debug=True)

测试方式:

curl -X POST http://localhost:5000/chat \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_001", "message": "你好"}'

curl -X POST http://localhost:5000/chat \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_001", "message": "刚才说了什么"}'

第二次请求应该返回「你好,我是……」之类的内容,证明 AI 记得第一轮对话。

避坑指南

1. 顺序错误是最常见的坑

// 错误:连续两个 user
[
  {"role": "user", "content": "你好"},
  {"role": "user", "content": "天气怎么样"}
]

// 错误:assistant 在 user 前面
[
  {"role": "assistant", "content": "你好"},
  {"role": "user", "content": "你好"}
]

DeepSeek 会返回 400 错误,提示 messages 格式非法。

2. 上下文太长会爆 token

DeepSeek 的上下文窗口是 64K,但超过 8K 后价格会上涨。实际建议:

  • 保留最近 10-20 轮对话
  • 或者按 token 数截断,保留 4000 token 左右
  • 超长对话可以考虑摘要压缩:把早期对话让 AI 总结成一段背景描述

3. 敏感信息别往上下文里塞

如果你实现了「查看历史」功能,注意用户可能在对话中输入过密码、身份证号。要么做前端脱敏,要么定期清理历史记录。

4. 多用户必须隔离

上面的 Flask 示例用 user_id 做 key,生产环境请换成 Redis 或数据库,并且注意:

  • 不要用 IP 地址做 key(NAT 环境下多个用户共享 IP)
  • 设置合理的过期时间(比如 30 分钟无操作清空)
  • 考虑用户主动「清空对话」的功能

5. 系统提示词别放中间

有些开发者想动态修改系统提示,结果把新的 system 消息插到数组中间。这会导致 API 报错,或者 AI 行为异常。系统提示只能出现在 messages[0]。

速查表

场景 操作
开启新对话 只传 system + 用户第一条消息
继续对话 追加历史:user → assistant → user(新)
清空记忆 丢弃该用户的 messages 数组,重新初始化
控制长度 截断数组,保留尾部 N 条,或按 token 数裁剪
多用户隔离 用 user_id 做 key,分别存储
调试格式问题 打印完整 messages,检查角色交替和顺序

下一步

会话记忆搞定后,你的 AI 助手已经具备了基础对话能力。接下来可以思考:

  • 如何让 AI 调用外部工具(function calling)
  • 如何实现 RAG,让 AI 基于私有知识库回答
  • 如何优化首 token 延迟和流式体验

这些我们会在后续篇章继续展开。