Skip to content

问题:AI 记不住上一句 ​

与 AI 大模型的交互本质是无状态的,每一次请求响应都是相互独立的,AI 大模型本身没有真正的会话记忆能力。

课程里的例子很直观:先问"12 个苹果,3 个人怎么均分",得到"每人 4 个";接着问"那 2 个人呢?",模型却反问"你是指哪两个人呢?"

原因:第二次请求里只有"那 2 个人呢?"这一句话,服务端根本不知道前文。

json
"messages": [
  { "role": "system", "content": "你是一名可爱的AI助手……" },
  { "role": "user", "content": "那2个人呢?" }
]

方案:会话历史滚雪球 ​

把之前所有对话都塞进 messages 一起发过去,模型就"看起来记得"了。

json
"messages": [
  { "role": "system", "content": "你是一名可爱的AI助手……" },
  { "role": "user", "content": "12个苹果,3个人怎么均分?" },
  { "role": "assistant", "content": "嘻嘻,12个苹果分给3个人,每个人可以分到 **4个苹果** 哦~" },
  { "role": "user", "content": "那2个人呢?" }
]

这样模型就能正确回答"每人 6 个苹果"。每多聊一轮,这个列表就长一截,所以叫"滚雪球"。

代码实现 ​

1. 每轮把一问一答都存进 session_state ​

python
# 保存用户输入的提示词
st.session_state.messages.append({"role": "user", "content": prompt})
...
# 保存大模型返回的结果
st.session_state.messages.append({"role": "assistant", "content": full_response})

IMPORTANT

必须把 assistant 的回复也存下来。只存用户的问题是不行的:模型看到的是"我问了 3 个问题",却看不到自己答过什么,前后逻辑会断裂。

2. 发请求时带上全部历史 ​

python
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. 顺序为什么是这样的 ​

text
messages[0]  system     设定身份
messages[1]  user       第1轮提问
messages[2]  assistant  第1轮回答
messages[3]  user       第2轮提问
...
最后一条     user       本轮提问(在调用前已经 append 进去)

调用前先 append 用户的问题,所以列表最后一条正好是本轮提问,不用再单独拼一次。

代价与优化方向 ​

问题说明
Token 越来越贵每轮都要重发全部历史,Token 消耗随对话轮数增长
超出模型上下文上限历史太长会超过模型的最大上下文长度,需要截断

常见的优化方向(超出本章范围,知道有这回事即可):只保留最近 N 轮对话、或者让模型把早期对话压缩成一段摘要。

TIP

这也解释了为什么"会话记忆"这件事必须由客户端负责:模型是无状态的,记忆只能由调用方保存并在每次请求时重新提交。本章后面的 会话管理 就是把这份历史存到文件里,实现"关掉网页也能接着聊"。

相关 ​

练习题 ​

一、知识回顾(读完直接做下面的实践题) ​

  1. 与 AI 大模型的交互本质是无状态的:每一次请求响应相互独立,模型本身没有真正的会话记忆能力
  2. 实现会话记忆的方案叫会话历史滚雪球(把之前对话都带上)
  3. 发请求时把历史"摊开"接在 system 后面的写法:messages=[{"role": "system", ...}, *st.session_state.messages]
  4. 列表解包符号是 *,作用是把列表里的元素逐个展开
  5. system 消息必须放在最前面,而且只放一条
  6. 调用前先 append 用户的问题,所以列表最后一条正好是本轮提问
  7. 只存用户问题、不存 assistant 回复会怎样:模型看不到自己答过什么,前后逻辑会断
  8. 滚雪球的两个代价:每轮 Token 越来越贵、可能超出模型的上下文长度上限

二、裸写题 ​

  • - 提示(先自己想,实在想不出再点开)

    一级 · 思路:发出去的是"system + 全部历史 + 本轮提问",历史就是那个越来越长的列表 二级 · 方法:列表 append + 列表拼接([system消息] + 历史列表) 三级 · 骨架:messages = [{"role": "system", "content": "……"}, ____history]

    - 参考答案(做完再点开)

    python
    history = []
    
    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]

    - 参考答案(做完再点开)

    python
    import 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 能答对了