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 ListChatMessage history new ArrayList(); private final MapString, 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(); } ListChatMessage getHistory() { return Collections.unmodifiableList(history); } int getHistoryTokenCount() { return history.stream().mapToInt(ChatMessage::tokenCount).sum(); } void setEntity(String key, String value) { extractedEntities.put(key, value); } MapString, String getEntities() { return Collections.unmodifiableMap(extractedEntities); } } // 会话状态管理器 class ConversationStateManager { private final MapString, 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); // 管理上下文窗口 ListChatMessage 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 ListChatMessage manageContextWindow(ConversationState state) { ListChatMessage history state.getHistory(); int totalTokens state.getHistoryTokenCount(); if (totalTokens maxContextTokens) { return new ArrayList(history); // 未超限直接使用 } ListChatMessage 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); ListChatMessage recentMessages getRecentMessages(history, recentBudget); compressed.addAll(recentMessages); // 对早期消息生成摘要占 30% 的窗口空间 int summaryBudget (int) (reservedTokens * 0.3); ListChatMessage 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(ListChatMessage 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 ListChatMessage getRecentMessages(ListChatMessage history, int budget) { ListChatMessage 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 ListChatMessage getEarlyMessages(ListChatMessage 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还会干扰模型的推理质量。
LLM 多轮对话状态管理:从无状态 API 到有状态会话
发布时间:2026/6/11 1:00:57
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 ListChatMessage history new ArrayList(); private final MapString, 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(); } ListChatMessage getHistory() { return Collections.unmodifiableList(history); } int getHistoryTokenCount() { return history.stream().mapToInt(ChatMessage::tokenCount).sum(); } void setEntity(String key, String value) { extractedEntities.put(key, value); } MapString, String getEntities() { return Collections.unmodifiableMap(extractedEntities); } } // 会话状态管理器 class ConversationStateManager { private final MapString, 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); // 管理上下文窗口 ListChatMessage 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 ListChatMessage manageContextWindow(ConversationState state) { ListChatMessage history state.getHistory(); int totalTokens state.getHistoryTokenCount(); if (totalTokens maxContextTokens) { return new ArrayList(history); // 未超限直接使用 } ListChatMessage 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); ListChatMessage recentMessages getRecentMessages(history, recentBudget); compressed.addAll(recentMessages); // 对早期消息生成摘要占 30% 的窗口空间 int summaryBudget (int) (reservedTokens * 0.3); ListChatMessage 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(ListChatMessage 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 ListChatMessage getRecentMessages(ListChatMessage history, int budget) { ListChatMessage 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 ListChatMessage getEarlyMessages(ListChatMessage 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还会干扰模型的推理质量。