问题:AI 记不住上一句
与 AI 大模型的交互本质是无状态的,每一次请求响应都是相互独立的,AI 大模型本身没有真正的会话记忆能力。
课程里的例子很直观:先问"12 个苹果,3 个人怎么均分",得到"每人 4 个";接着问"那 2 个人呢?",模型却反问"你是指哪两个人呢?"
原因:第二次请求里只有"那 2 个人呢?"这一句话,服务端根本不知道前文。
"messages": [
{ "role": "system", "content": "你是一名可爱的AI助手……" },
{ "role": "user", "content": "那2个人呢?" }
]方案:会话历史滚雪球
把之前所有对话都塞进 messages 一起发过去,模型就"看起来记得"了。
"messages": [
{ "role": "system", "content": "你是一名可爱的AI助手……" },
{ "role": "user", "content": "12个苹果,3个人怎么均分?" },
{ "role": "assistant", "content": "嘻嘻,12个苹果分给3个人,每个人可以分到 **4个苹果** 哦~" },
{ "role": "user", "content": "那2个人呢?" }
]这样模型就能正确回答"每人 6 个苹果"。每多聊一轮,这个列表就长一截,所以叫"滚雪球"。
代码实现
1. 每轮把一问一答都存进 session_state
# 保存用户输入的提示词
st.session_state.messages.append({"role": "user", "content": prompt})
...
# 保存大模型返回的结果
st.session_state.messages.append({"role": "assistant", "content": full_response})IMPORTANT
必须把 assistant 的回复也存下来。只存用户的问题是不行的:模型看到的是"我问了 3 个问题",却看不到自己答过什么,前后逻辑会断裂。
2. 发请求时带上全部历史
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system_prompt},
*st.session_state.messages # 把历史消息一条条"摊开"接到后面
],
stream=True
)| 写法 | 含义 |
|---|---|
[A, *B] | 列表解包:把 B 里的元素逐个展开,和 A 拼成一个新列表 |
| 结果 | [system消息, 历史user, 历史assistant, ..., 本轮user] |
WARNING
system 消息必须放在最前面,且只放一条。它负责设定身份和行为准则,位置错了模型的行为约束会失效。
3. 顺序为什么是这样的
messages[0] system 设定身份
messages[1] user 第1轮提问
messages[2] assistant 第1轮回答
messages[3] user 第2轮提问
...
最后一条 user 本轮提问(在调用前已经 append 进去)调用前先 append 用户的问题,所以列表最后一条正好是本轮提问,不用再单独拼一次。
代价与优化方向
| 问题 | 说明 |
|---|---|
| Token 越来越贵 | 每轮都要重发全部历史,Token 消耗随对话轮数增长 |
| 超出模型上下文上限 | 历史太长会超过模型的最大上下文长度,需要截断 |
常见的优化方向(超出本章范围,知道有这回事即可):只保留最近 N 轮对话、或者让模型把早期对话压缩成一段摘要。
TIP
这也解释了为什么"会话记忆"这件事必须由客户端负责:模型是无状态的,记忆只能由调用方保存并在每次请求时重新提交。本章后面的 会话管理 就是把这份历史存到文件里,实现"关掉网页也能接着聊"。
相关
- Apifox 测试大模型接口(无状态与滚雪球的概念演示)
- 实战-AI智能伴侣-会话管理
练习题
一、知识回顾(读完直接做下面的实践题)
- 与 AI 大模型的交互本质是无状态的:每一次请求响应相互独立,模型本身没有真正的会话记忆能力
- 实现会话记忆的方案叫会话历史滚雪球(把之前对话都带上)
- 发请求时把历史"摊开"接在 system 后面的写法:
messages=[{"role": "system", ...}, *st.session_state.messages] - 列表解包符号是
*,作用是把列表里的元素逐个展开 system消息必须放在最前面,而且只放一条- 调用前先 append 用户的问题,所以列表最后一条正好是本轮提问
- 只存用户问题、不存 assistant 回复会怎样:模型看不到自己答过什么,前后逻辑会断
- 滚雪球的两个代价:每轮 Token 越来越贵、可能超出模型的上下文长度上限
二、裸写题
- 提示(先自己想,实在想不出再点开)
一级 · 思路:发出去的是"system + 全部历史 + 本轮提问",历史就是那个越来越长的列表 二级 · 方法:列表
append+ 列表拼接([system消息] + 历史列表) 三级 · 骨架:messages = [{"role": "system", "content": "……"}, ____history]- 参考答案(做完再点开)
pythonhistory = [] def add(role, content): """往历史里追加一条消息""" history.append({"role": role, "content": content}) def messages_for(question): """本轮要发给模型的完整 messages:system 在最前,后面接全部历史 + 本轮提问""" return [{"role": "system", "content": "你是小甜甜"}] + history + [{"role": "user", "content": question}] # 第一轮 print(messages_for("12个苹果,3个人怎么均分?")) add("user", "12个苹果,3个人怎么均分?") add("assistant", "每个人可以分到 4 个苹果") # 第二轮:历史已经攒了 2 条 msgs = messages_for("那2个人呢?") print(f"\n第二轮要发 {len(msgs)} 条:") for m in msgs: print(" ", m["role"], ":", m["content"]) # 观察结论:同一个问题,轮次越靠后,发出去的消息条数越多 —— 这就是"滚雪球"- 提示(先自己想,实在想不出再点开)
一级 · 思路:界面渲染用这份列表,发给模型也用这份列表——同一份数据 二级 · 方法:
st.session_state.messages.append(...)两次(user 一次、assistant 一次) 三级 · 骨架:messages=[{"role": "system", "content": system_prompt}, *st.session_state.messages]- 参考答案(做完再点开)
pythonimport os import streamlit as st from openai import OpenAI st.set_page_config(page_title="AI智能伴侣", page_icon="🤖", layout="wide") st.title("AI智能伴侣") system_prompt = "你是一名非常可爱的AI助理, 你的名字叫小甜甜, 请你使用温柔可爱的语气回答用户的问题" if "messages" not in st.session_state: st.session_state.messages = [] for message in st.session_state.messages: st.chat_message(message["role"]).write(message["content"]) client = OpenAI(api_key=os.environ.get("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com") prompt = st.chat_input("请输入您要问的问题") if prompt: st.chat_message("user").write(prompt) st.session_state.messages.append({"role": "user", "content": prompt}) response = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": system_prompt}, *st.session_state.messages, # 关键:把历史一条条摊开接在后面 ], stream=False, ) reply = response.choices[0].message.content st.chat_message("assistant").write(reply) st.session_state.messages.append({"role": "assistant", "content": reply}) # 验证:先问"12个苹果,3个人怎么均分",再问"那2个人呢" —— 这次 AI 能答对了
