LLM 多轮对话状态管理:从无状态 API 到有状态会话
LLM 多轮对话状态管理:从无状态 API 到有状态会话

一、大模型 API 的无状态困境:上下文窗口的有限性与会话连续性
大模型的 Chat API 本质上是无状态的——每次请求都需要发送完整的对话历史。这种设计简化了服务端实现,但给后端架构带来了两个核心挑战:一是上下文窗口有限(GPT-4o 约 128K token),长对话的历史消息会超出窗口限制;二是每次请求发送完整历史的 Token 成本随对话轮次线性增长,一个 20 轮的对话,最后一轮的输入 Token 可能是第一轮的 10 倍。
多轮对话状态管理的核心目标是:在有限的上下文窗口内,保留对当前对话最有价值的信息,同时控制 Token 消耗。这涉及消息压缩、摘要替换、关键信息提取和会话持久化四个关键机制。
二、多轮对话状态管理的架构设计
多轮对话状态管理分为三层:会话存储层(持久化对话历史)、上下文窗口管理层(控制发送给模型的消息量)和状态抽象层(提取和压缩关键信息)。
flowchart TB
A[用户消息] --> B[会话状态管理器]
B --> C[加载会话历史]
C --> D[上下文窗口管理]
D --> E{历史消息是否超出窗口?}
E -->|否| F[直接拼接完整历史]
E -->|是| G[消息压缩策略]
G --> H[策略 1: 早期消息摘要替换]
G --> I[策略 2: 关键信息提取]
G --> J[策略 3: 滑动窗口截断]
H --> K[压缩后的上下文]
I --> K
J --> K
K --> L[组装 Prompt]
F --> L
L --> M[调用 LLM API]
M --> N[模型回复]
N --> O[更新会话历史]
O --> P[持久化存储]
subgraph 状态抽象层
Q[用户意图追踪]
R[实体信息提取]
S[对话目标状态]
end
Q --> D
R --> D
S --> D
上图展示了从用户消息到模型回复的完整流程。上下文窗口管理是核心环节——当历史消息超出窗口时,需要选择合适的压缩策略。三种策略各有适用场景:摘要替换适合长对话的知识保留,关键信息提取适合结构化数据的追踪,滑动窗口适合短期对话的快速截断。
三、生产级实现:多轮对话状态管理器
// ConversationStateManager.java — 多轮对话状态管理器
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
import java.util.concurrent.*;
// 对话消息
record ChatMessage(
String role, // system / user / assistant
String content,
long timestamp,
int tokenCount
) {}
// 会话状态
class ConversationState {
private final String sessionId;
private final List<ChatMessage> history = new ArrayList<>();
private final Map<String, String> extractedEntities = new ConcurrentHashMap<>();
private String conversationGoal;
private int totalTokensUsed = 0;
ConversationState(String sessionId) {
this.sessionId = sessionId;
}
void addMessage(ChatMessage message) {
history.add(message);
totalTokensUsed += message.tokenCount();
}
List<ChatMessage> getHistory() {
return Collections.unmodifiableList(history);
}
int getHistoryTokenCount() {
return history.stream().mapToInt(ChatMessage::tokenCount).sum();
}
void setEntity(String key, String value) {
extractedEntities.put(key, value);
}
Map<String, String> getEntities() {
return Collections.unmodifiableMap(extractedEntities);
}
}
// 会话状态管理器
class ConversationStateManager {
private final Map<String, ConversationState> sessions = new ConcurrentHashMap<>();
private final LLMClient llmClient;
private final SessionStore sessionStore; // 持久化存储(Redis/DB)
private final int maxContextTokens;
ConversationStateManager(LLMClient llmClient, SessionStore sessionStore,
int maxContextTokens) {
this.llmClient = llmClient;
this.sessionStore = sessionStore;
this.maxContextTokens = maxContextTokens;
}
// 处理用户消息:加载历史 → 压缩上下文 → 调用模型 → 更新状态
// 设计意图:将上下文管理逻辑封装在管理器中,
// 调用方无需关心消息压缩和 Token 控制
ChatMessage processMessage(String sessionId, String userMessage) {
ConversationState state = getOrCreateSession(sessionId);
// 添加用户消息到历史
int userTokens = estimateTokens(userMessage);
state.addMessage(new ChatMessage("user", userMessage,
System.currentTimeMillis(), userTokens));
// 提取实体信息(如用户名、日期、订单号等)
extractEntities(state, userMessage);
// 管理上下文窗口
List<ChatMessage> context = manageContextWindow(state);
// 组装 Prompt 并调用 LLM
String assistantReply = llmClient.chat(context);
// 添加助手回复到历史
int replyTokens = estimateTokens(assistantReply);
ChatMessage replyMessage = new ChatMessage("assistant", assistantReply,
System.currentTimeMillis(), replyTokens);
state.addMessage(replyMessage);
// 持久化会话状态
sessionStore.save(sessionId, state);
return replyMessage;
}
// 上下文窗口管理:当历史超出窗口时进行压缩
// 设计意图:优先保留最近的消息和关键信息,
// 对早期消息进行摘要替换
private List<ChatMessage> manageContextWindow(ConversationState state) {
List<ChatMessage> history = state.getHistory();
int totalTokens = state.getHistoryTokenCount();
if (totalTokens <= maxContextTokens) {
return new ArrayList<>(history); // 未超限,直接使用
}
List<ChatMessage> compressed = new ArrayList<>();
int reservedTokens = maxContextTokens;
// 保留系统消息
for (ChatMessage msg : history) {
if ("system".equals(msg.role())) {
compressed.add(msg);
reservedTokens -= msg.tokenCount();
}
}
// 保留最近的消息(占 60% 的窗口空间)
int recentBudget = (int) (reservedTokens * 0.6);
List<ChatMessage> recentMessages = getRecentMessages(history, recentBudget);
compressed.addAll(recentMessages);
// 对早期消息生成摘要(占 30% 的窗口空间)
int summaryBudget = (int) (reservedTokens * 0.3);
List<ChatMessage> earlyMessages = getEarlyMessages(history, recentMessages.size());
if (!earlyMessages.isEmpty()) {
String summary = summarizeMessages(earlyMessages, summaryBudget);
compressed.add(1, new ChatMessage("system",
"[对话摘要] " + summary, System.currentTimeMillis(),
estimateTokens(summary)));
}
// 注入提取的实体信息(占 10% 的窗口空间)
if (!state.getEntities().isEmpty()) {
String entityContext = "已知信息: " + state.getEntities().toString();
compressed.add(1, new ChatMessage("system", entityContext,
System.currentTimeMillis(), estimateTokens(entityContext)));
}
return compressed;
}
// 消息摘要:调用 LLM 将多条消息压缩为摘要
// 设计意图:保留对话的核心信息,而非逐字保留每条消息
private String summarizeMessages(List<ChatMessage> messages, int maxTokens) {
String messageText = messages.stream()
.map(m -> m.role() + ": " + m.content())
.reduce("", (a, b) -> a + "\n" + b);
String prompt = String.format(
"将以下对话历史压缩为不超过 %d token 的摘要,保留关键信息和决策点:\n%s",
maxTokens, messageText
);
return llmClient.summarize(prompt, maxTokens);
}
// 实体提取:从用户消息中提取结构化信息
// 设计意图:将对话中的关键信息持久化,
// 即使原始消息被压缩,实体信息仍然保留
private void extractEntities(ConversationState state, String message) {
// 简单规则提取(生产环境可用 NER 模型替代)
extractPatterns(state, message);
}
private void extractPatterns(ConversationState state, String message) {
// 提取订单号
var orderMatcher = java.util.regex.Pattern.compile("订单号[::]?\\s*(\\w+)")
.matcher(message);
if (orderMatcher.find()) {
state.setEntity("order_id", orderMatcher.group(1));
}
// 提取日期
var dateMatcher = java.util.regex.Pattern.compile("(\\d{4}[-/]\\d{2}[-/]\\d{2})")
.matcher(message);
if (dateMatcher.find()) {
state.setEntity("mentioned_date", dateMatcher.group(1));
}
}
private int estimateTokens(String text) {
return (int) Math.ceil(text.length() / 2.0);
}
private ConversationState getOrCreateSession(String sessionId) {
return sessions.computeIfAbsent(sessionId, id -> {
ConversationState stored = sessionStore.load(id);
return stored != null ? stored : new ConversationState(id);
});
}
private List<ChatMessage> getRecentMessages(List<ChatMessage> history, int budget) {
List<ChatMessage> recent = new ArrayList<>();
int used = 0;
for (int i = history.size() - 1; i >= 0; i--) {
ChatMessage msg = history.get(i);
if (used + msg.tokenCount() > budget) break;
recent.add(0, msg);
used += msg.tokenCount();
}
return recent;
}
private List<ChatMessage> getEarlyMessages(List<ChatMessage> history, int recentCount) {
int earlyEnd = history.size() - recentCount;
return earlyEnd > 0 ? history.subList(0, earlyEnd) : Collections.emptyList();
}
}
四、边界分析与架构权衡
多轮对话状态管理在生产落地中需要正视以下 Trade-off:
摘要质量与 Token 节省的矛盾。摘要越短,节省的 Token 越多,但信息损失也越大。一个 500 token 的摘要可能丢失用户在早期对话中提供的关键约束条件。建议摘要长度控制在原始消息的 20-30%,并优先保留"决策点"和"约束条件"而非闲聊内容。
实体提取的精度。基于正则的实体提取精度有限,无法处理口语化表达(如"上周三的那个单子")。NER 模型精度更高,但增加了推理延迟和部署成本。建议先用规则覆盖高频模式,再逐步引入 NER 模型处理复杂表达。
会话持久化的性能。每次对话轮次都需要持久化会话状态,在高并发场景下可能成为瓶颈。Redis 适合短期会话(TTL 1 小时),数据库适合长期会话。建议热数据存 Redis,冷数据异步落库。
适用边界:多轮对话状态管理最适合客服机器人、销售助手等长对话场景。对于单轮问答(如搜索、翻译),不需要状态管理,直接调用 API 即可。
五、总结
LLM 多轮对话状态管理,将无状态的 Chat API 扩展为有状态的会话系统。核心架构:会话存储层持久化历史,上下文窗口管理层控制 Token 消耗,状态抽象层提取关键信息。落地建议:第一,采用"摘要 + 最近消息 + 实体信息"的三段式上下文管理,平衡信息保留和 Token 控制;第二,实体提取优先使用规则,逐步引入 NER 模型;第三,热数据存 Redis,冷数据异步落库。关键原则:上下文窗口是稀缺资源——每一行发送给模型的消息都应该有存在的价值,冗余信息不仅浪费 Token,还会干扰模型的推理质量。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)