3385 字
17 分钟
大语言模型的多轮对话与上下文管理

最简单的大语言模型 API 调用可以彼此独立:应用提交一段输入,模型返回一段结果,本次请求到此结束。但在聊天助手、客服系统和 Agent 中,用户通常会连续追问:

用户:我正在开发一个 Spring Boot 项目。
AI:你现在主要在解决什么问题?
用户:项目准备接入 Redis。
AI:可以考虑使用 Spring Data Redis。
用户:那连接池怎么配置?

最后一句单独看并不完整。模型只有同时知道 Spring Boot、Redis 和前面的讨论,才能理解“连接池”指什么。

多轮对话的核心并不是让模型永久记住用户,而是让每次推理获得当前任务所需的历史状态。这项工作通常称为会话状态管理(Conversation State Management),进一步延伸后就是上下文工程(Context Engineering)。

模型为什么需要上下文#

每次请求默认相互独立#

一次新的模型请求不会天然拥有上一条独立请求的内容:

from openai import OpenAI

client = OpenAI()

client.responses.create(
    model='gpt-5.6',
    input='我正在开发一个 Spring Boot 项目。',
)

response = client.responses.create(
    model='gpt-5.6',
    input='我刚才说我在开发什么项目?',
)

如果应用没有建立关联,第二次请求就看不到第一次请求。所谓“AI 记住了对话”,更准确地说,是应用或 API 服务为后续请求重新提供了相关上下文。

上下文不等于聊天记录#

上下文是模型在当前一次推理中能够看到的信息集合,可能包括:

  • 应用提供的 Instructions;
  • 当前用户输入;
  • 历史用户消息和模型回答;
  • Function Call 与 Tool Output;
  • RAG 检索片段和文件内容;
  • 用户状态或其他业务数据。

聊天记录主要用于历史展示、搜索和审计;模型上下文只服务于当前推理。用户可以查看全部 300 轮记录,但模型回答第 301 个问题时,未必需要读取全部内容。

Role 则用于标记内容在上下文中的语义:

Role含义
user用户提交的内容
assistant模型此前生成的内容
developer / instructions应用提供的规则和约束

因此,多轮对话不是简单拼接字符串,而是在构造一组有顺序、有角色语义的上下文项。

三种多轮对话方案#

Responses API 可以通过手动传递历史内容、previous_response_id 或 Conversations API 延续状态。三种方案解决的问题相似,但控制边界不同。

手动传递历史消息#

最直观的方式是由应用自己保存消息,并在每次请求时重新提交:

history = [
    {
        'role': 'user',
        'content': '我正在开发一个 Spring Boot 项目。',
    },
    {
        'role': 'assistant',
        'content': '你目前在处理什么问题?',
    },
    {
        'role': 'user',
        'content': '准备接入 Redis。',
    },
]

response = client.responses.create(
    model='gpt-5.6',
    input=history,
)

history.extend(response.output)

这种方式让应用完全决定哪些消息保留、删除、修改或摘要,也便于切换模型和供应商。代价是应用必须自己保存历史,并持续控制上下文长度。

需要注意,手动维护 Responses API 的历史时,应该保存 response.output 中的完整输出项,而不只是 response.output_text。推理模型或工具调用可能产生需要在下一轮继续传递的非文本项。

使用 previous_response_id#

如果不想每次手动拼装历史,可以让当前 Response 关联上一个 Response:

first = client.responses.create(
    model='gpt-5.6',
    instructions='你是一名 Java 后端技术助手,使用中文回答。',
    input='我正在开发一个 Spring Boot 项目。',
)

second = client.responses.create(
    model='gpt-5.6',
    instructions='你是一名 Java 后端技术助手,使用中文回答。',
    previous_response_id=first.id,
    input='项目准备接入 Redis。',
)

third = client.responses.create(
    model='gpt-5.6',
    instructions='你是一名 Java 后端技术助手,使用中文回答。',
    previous_response_id=second.id,
    input='那连接池怎么配置?',
)

应用只需保存最新的 Response ID,就能形成一条 Response Chain。它适合 Demo、短期会话以及不需要复杂上下文策略的应用。

这里有两个容易忽略的细节:

  • 上一轮顶层的 instructions 不会自动传递到下一轮,固定规则应由应用在每次请求中提供;
  • previous_response_id 简化了状态管理,但链中之前的输入 Token 仍然会按输入 Token 计费。

Response 还可以产生分支。例如从同一个 Response 分别生成两个后续回答,可用于“重新生成”或对比不同提示词。数据结构因此更接近树,而不一定永远是一条直线。

使用 Conversations API#

需要跨设备、任务或较长时间维持会话时,可以创建持久化的 Conversation:

conversation = client.conversations.create()

client.responses.create(
    model='gpt-5.6',
    conversation=conversation.id,
    input='我正在开发一个 Spring Boot 项目。',
)

response = client.responses.create(
    model='gpt-5.6',
    conversation=conversation.id,
    input='项目准备接入 Redis,连接池应该怎么配置?',
)

print(response.output_text)

Conversation 是一个长期存在的会话容器,可以保存消息、工具调用、工具输出等 Item。应用不需要自己维护 Response 链,但仍应在本地数据库保存会话归属、展示记录和业务状态。

三种方案可以这样选择:

方案优点更适合
手动传递历史上下文控制能力最高RAG、Agent、跨模型系统
previous_response_id接入简单,代码较少Demo 和简单 Response 链
Conversations API持久化会话容器长期助手、跨设备会话

previous_response_idconversation 不能在同一个请求中同时使用。项目应根据自己的状态模型选择一种延续方式,而不是将两者混合。

长对话如何控制上下文#

为什么不能无限增长#

对话历史、Instructions、工具结果、检索内容以及模型输出都会占用上下文窗口。持续加入全部历史会带来四类问题:

  • 输入 Token 和调用成本不断增长;
  • 响应延迟增加;
  • 无关历史干扰当前任务;
  • 最终超过模型的上下文窗口。

所以多轮对话管理的重点不是“保存所有内容”,而是“选出当前推理真正需要的内容”。

滑动窗口与历史摘要#

最简单的策略是只保留最近若干条消息:

MAX_MESSAGES = 20
recent_messages = history[-MAX_MESSAGES:]

滑动窗口适合短期上下文最重要的客服问答、代码讨论和临时咨询。但它可能删除较早出现的关键事实,例如项目使用的数据库类型。

更常见的做法是组合摘要和近期消息:

历史摘要
+ 最近若干轮原始消息
+ 当前用户输入

例如把多轮对话压缩为:

用户项目采用 Spring Boot 3、PostgreSQL 和 Redis,
部署在 Kubernetes,目前关注接口性能优化。

摘要可以降低上下文占用,但摘要本身也可能遗漏“不允许在生产环境执行 FLUSHALL”之类的重要约束。因此,长期稳定且影响业务决策的信息不应只保存在自然语言摘要里。

结构化长期记忆#

关键状态更适合以结构化数据保存:

conversation_state = {
    'language': 'zh-CN',
    'project': {
        'framework': 'Spring Boot 3',
        'database': 'PostgreSQL',
        'cache': 'Redis',
        'deployment': 'Kubernetes',
    },
    'constraints': [
        '生产环境禁止执行 Redis FLUSHALL',
    ],
}

应用在需要时将这些状态重新注入上下文,不必依赖数百轮原始消息。成熟系统通常会区分:

类型主要作用示例
短期上下文保持当前对话连贯最近 10 轮消息
长期记忆保存稳定的用户或任务状态语言偏好、项目技术栈
外部知识提供可检索的事实资料产品文档、企业知识库

RAG、长期记忆和聊天历史并不是同一种数据,但最终都要回答同一个问题:这一轮模型应该看到什么?

工具输出也要精简#

Function Calling 会把工具调用和结果加入会话状态。后续用户问“那物流到哪里了”时,模型需要知道前面查询的是哪个订单。

但工具不应把完整数据库对象、内部日志和无关字段全部返回给模型:

{
  "order_id": "10001",
  "status": "SHIPPED",
  "latest_location": "上海转运中心"
}

最小化 Tool Output 既能降低 Token 消耗,也能缩小敏感数据泄露和 Prompt Injection 的攻击面。

会话状态如何落地#

用户与会话必须隔离#

一个用户可以同时讨论 Java、数据库和旅行计划,因此不能只按照 user_id 保存一个最新 Response。更合理的关系是一个用户拥有多个 Conversation,每个 Conversation 独立维护状态。

可以至少设计两张表:

ai_conversation 字段含义
id本地会话 ID
user_id所属用户
title会话标题
provider / model模型信息
last_response_id最新 Response ID
provider_conversation_id外部 Conversation ID
created_at / updated_at生命周期信息
ai_message 字段含义
id消息 ID
conversation_id所属会话
roleuser / assistant
content展示内容
response_id对应的模型 Response
created_at创建时间

如果系统包含 Function Calling,还可以单独保存工具名称、参数、结果、Call ID 和执行状态,以便审计与故障恢复。

不要只存在进程内存#

下面这样的字典可以用于本地 Demo:

self.sessions[conversation_id] = response.id

但它不能直接用于生产环境。服务重启会丢失数据,多实例部署时不同实例也无法共享状态。常见的职责分配包括:

数据常见存储位置
产品聊天记录MySQL / PostgreSQL
短期会话状态Redis
用户长期状态业务数据库
模型会话状态Conversations API
可检索记忆向量数据库

前端可以携带本地 conversation_id,但服务端必须检查该会话是否属于当前登录用户。否则攻击者可能通过猜测 ID 访问其他用户的会话,形成典型的 IDOR 越权问题。

一个基础聊天服务#

下面的代码展示了本地会话 ID 与 previous_response_id 的基本组合:

from openai import OpenAI


class ChatService:
    def __init__(self):
        self.client = OpenAI()
        self.sessions: dict[str, str] = {}
        self.instructions = (
            '你是一名软件工程技术助手。'
            '使用中文回答,不确定的信息需要明确说明。'
        )

    def chat(self, conversation_id: str, message: str) -> str:
        request = {
            'model': 'gpt-5.6',
            'instructions': self.instructions,
            'input': message,
        }

        previous_response_id = self.sessions.get(conversation_id)
        if previous_response_id:
            request['previous_response_id'] = previous_response_id

        response = self.client.responses.create(**request)
        self.sessions[conversation_id] = response.id
        return response.output_text

真实项目还需要把 sessions 替换为共享存储,并补充用户鉴权、并发更新、失败重试、幂等和审计。如果同一个会话允许并发发消息,还要避免两个请求同时基于同一个旧 Response 产生意外分支。

数据生命周期与安全边界#

明确谁负责保存什么#

Response 对象默认保存 30 天,可以通过 store=False 禁用这种默认存储。Conversation 对象及其中的 Item 不受该 30 天 TTL 限制,因此选择 Conversations API 时应同时设计删除、归档和用户数据生命周期策略。

流式输出不会改变上述关系。Streaming 解决的是结果如何传输,会话状态解决的是下一轮模型能看到什么。流式响应结束后,应用仍需保存 Response ID、聊天记录或 Conversation 状态。

只提供完成任务所需的数据#

不要因为模型需要上下文,就把用户的全部资料放进 Prompt。过多信息会增加 Token、隐私风险和噪声,还会扩大 Prompt Injection 的影响范围。

尤其不应把以下数据未经处理地写入长期上下文:

  • 密码、API Key 和 Access Token;
  • 身份证号、银行卡等敏感身份信息;
  • 与当前任务无关的订单、地址或聊天记录;
  • 不应被模型或最终用户看到的内部字段。

应用的 Instructions 也不能直接由用户控制:

# 错误:用户内容获得了应用规则的权限
instructions = user_input

开发者指令属于可信的应用策略;用户输入、网页、检索文档和 Tool Output 通常都应视为不可信数据。进入上下文不代表它们拥有修改系统规则的权限。

Context Engineering#

Prompt Engineering 主要关注“指令应该怎么写”,Context Engineering 更关注“模型这一轮应该看到什么”。一个较完整的上下文可以由以下部分组成:

固定应用规则
+ 结构化用户或任务状态
+ 历史摘要
+ 最近消息
+ 相关长期记忆
+ RAG 检索结果
+ 必要的工具结果
+ 当前输入

这不是要求每次都提供所有部分,而是要求应用根据当前任务主动选择。上下文不是越多越好,正确、相关、可信且足够完成任务才是目标。

总结#

多轮对话的本质,是在每次推理时为模型提供必要的历史状态,而不是让模型无限保存所有内容。

手动传递历史拥有最高控制力;previous_response_id 适合快速建立 Response Chain;Conversations API 适合长期存在的会话。无论选择哪一种,生产系统仍需处理会话隔离、上下文窗口、摘要与裁剪、持久化、权限控制和数据生命周期。

随着历史不断增长,Token 数量、上下文窗口和调用成本会成为下一个关键问题。理解这些限制,才能进一步设计可控的长对话系统。

参考资料#

大语言模型的多轮对话与上下文管理
https://lllirunze.cn/posts/conversation-context-management/
作者
柒月の風灬
发布于
2026-09-20
许可协议
CC BY-NC-SA 4.0