第2037篇:上下文工程——LLM长对话的信息管理策略
2026/4/30大约 5 分钟
第2037篇:上下文工程——LLM长对话的信息管理策略
适读人群:需要处理多轮对话和长上下文的工程师 | 阅读时长:约17分钟 | 核心价值:掌握上下文窗口的高效利用方法,让LLM在长对话中始终有效
"多轮对话做到第10轮,AI就开始变傻了。"
这是一个高频投诉。前几轮说得好好的,随着对话拉长,AI开始忘事,或者回答开始偏离主题。
这不是模型变笨了,是上下文管理出了问题。
为什么对话越长模型越差
LLM有固定大小的上下文窗口(比如8192个token)。多轮对话中,每次发送请求,都要带上完整的历史对话:
第1轮:[system prompt + turn1]
第2轮:[system prompt + turn1 + turn2]
第3轮:[system prompt + turn1 + turn2 + turn3]
...
第10轮:[system prompt + turn1 + ... + turn10] ← 可能快超限了两个问题:一是超出上下文窗口,旧的内容被截断,模型"忘记"了;二是注意力稀释,即使没超窗口,太长的上下文也会让模型对早期内容的注意力下降。
策略1:滑动窗口
最简单的方案,只保留最近N轮对话:
@Service
@RequiredArgsConstructor
public class SlidingWindowConversation {
private static final int MAX_TURNS_TO_KEEP = 10; // 保留最近10轮
private static final int MAX_TOKENS_BUDGET = 6000; // 预留给历史对话的token预算
private final Map<String, Deque<ConversationTurn>> conversations =
new ConcurrentHashMap<>();
/**
* 添加新一轮对话,维持滑动窗口
*/
public void addTurn(String sessionId, String userMessage, String assistantResponse) {
conversations.computeIfAbsent(sessionId, k -> new ArrayDeque<>());
Deque<ConversationTurn> history = conversations.get(sessionId);
history.addLast(new ConversationTurn(userMessage, assistantResponse));
// 保持窗口大小:按轮数限制
while (history.size() > MAX_TURNS_TO_KEEP) {
history.removeFirst();
}
// 还要按token数限制(更精确)
trimByTokenBudget(history);
}
private void trimByTokenBudget(Deque<ConversationTurn> history) {
int totalTokens = history.stream()
.mapToInt(turn -> estimateTokens(turn.getUserMessage()) +
estimateTokens(turn.getAssistantResponse()))
.sum();
while (totalTokens > MAX_TOKENS_BUDGET && !history.isEmpty()) {
ConversationTurn removed = history.removeFirst();
totalTokens -= estimateTokens(removed.getUserMessage()) +
estimateTokens(removed.getAssistantResponse());
}
}
/**
* 获取对话历史,格式化为可以发送给LLM的消息列表
*/
public List<Message> getMessages(String sessionId, String systemPrompt) {
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
Deque<ConversationTurn> history = conversations.getOrDefault(
sessionId, new ArrayDeque<>());
for (ConversationTurn turn : history) {
messages.add(new UserMessage(turn.getUserMessage()));
messages.add(new AssistantMessage(turn.getAssistantResponse()));
}
return messages;
}
private int estimateTokens(String text) {
return text.length() / 3; // 粗估:中文约1.5字/token,英文约4字/token
}
}策略2:对话摘要压缩
比滑动窗口更优雅的方式:把旧的对话摘要成简短的"记忆",保留语义但大幅减少token:
/**
* 摘要式对话压缩
* 当历史超过阈值时,把早期对话摘要成一段总结
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SummarizedConversationManager {
private final ChatClient chatClient;
private final ConversationRepository conversationRepo;
private static final int SUMMARY_THRESHOLD_TURNS = 8; // 超过8轮就开始摘要
/**
* 智能构建对话历史:早期对话摘要 + 近期对话原文
*/
public List<Message> buildMessagesWithSummary(
String sessionId, String systemPrompt, String currentUserMessage) {
ConversationSession session = conversationRepo.findBySessionId(sessionId)
.orElseGet(() -> ConversationSession.builder().sessionId(sessionId).build());
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
// 如果有历史摘要,先加入
if (session.getSummary() != null) {
messages.add(new SystemMessage(
"以下是此前对话的摘要(用于保持上下文连贯性):\n" + session.getSummary()));
}
// 加入最近的几轮完整对话
List<ConversationTurn> recentTurns = session.getRecentTurns(5);
for (ConversationTurn turn : recentTurns) {
messages.add(new UserMessage(turn.getUserMessage()));
messages.add(new AssistantMessage(turn.getAssistantResponse()));
}
// 加入当前用户消息
messages.add(new UserMessage(currentUserMessage));
return messages;
}
/**
* 在对话轮次超过阈值时触发摘要压缩
*/
public void maybeCompress(String sessionId) {
ConversationSession session = conversationRepo.findBySessionId(sessionId)
.orElse(null);
if (session == null || session.getTotalTurns() < SUMMARY_THRESHOLD_TURNS) {
return;
}
// 取出最早的一批对话(不在近期5轮内的)
List<ConversationTurn> olderTurns = session.getOlderTurns(5);
if (olderTurns.isEmpty()) return;
// 用LLM压缩
String conversationText = olderTurns.stream()
.map(t -> "用户:" + t.getUserMessage() + "\nAI:" + t.getAssistantResponse())
.collect(Collectors.joining("\n\n"));
String summarizationPrompt = """
请将以下多轮对话摘要成一段简洁的背景信息(200字以内)。
摘要需要包含:用户的主要需求/问题、已达成的共识、重要的上下文信息。
对话内容:
%s
""".formatted(conversationText);
try {
String newSummary = chatClient.prompt()
.user(summarizationPrompt)
.call()
.content();
// 如果已有摘要,和新摘要合并
if (session.getSummary() != null) {
newSummary = "之前:" + session.getSummary() +
"\n后续:" + newSummary;
}
session.setSummary(newSummary);
session.removeOlderTurns(5); // 删除已摘要的旧对话
conversationRepo.save(session);
log.debug("对话压缩完成,会话: {}", sessionId);
} catch (Exception e) {
log.warn("对话摘要失败(会话: {}): {}", sessionId, e.getMessage());
}
}
}策略3:显式记忆管理
为重要信息建立"记忆库",不依赖对话历史:
/**
* 显式对话记忆管理
* 主动提取和存储关键信息,不只依赖历史消息
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ExplicitMemoryManager {
private final ChatClient chatClient;
private final MemoryRepository memoryRepo;
/**
* 从每轮对话中提取关键信息,存入记忆库
*/
public void extractAndStore(String sessionId, String userMessage, String response) {
String extractionPrompt = """
从以下对话中提取值得记住的关键信息(如:用户偏好、重要事实、已确认的决定)。
如果没有值得记录的信息,返回"无"。
用户:%s
AI:%s
提取格式:- 关键信息描述(类型:偏好/事实/决定)
""".formatted(userMessage, response);
try {
String extracted = chatClient.prompt()
.user(extractionPrompt)
.call()
.content();
if (!extracted.equals("无") && !extracted.isEmpty()) {
memoryRepo.save(Memory.builder()
.sessionId(sessionId)
.content(extracted)
.timestamp(LocalDateTime.now())
.build());
}
} catch (Exception e) {
log.debug("记忆提取失败: {}", e.getMessage());
}
}
/**
* 为新的用户请求注入相关记忆
*/
public String buildMemoryContext(String sessionId) {
List<Memory> memories = memoryRepo.findBySessionId(sessionId);
if (memories.isEmpty()) return "";
String memoryText = memories.stream()
.map(Memory::getContent)
.collect(Collectors.joining("\n"));
return "\n\n关于用户的记忆信息:\n" + memoryText;
}
}综合方案:分层上下文管理
/**
* 整合所有策略的上下文管理器
*/
@Service
@RequiredArgsConstructor
public class LayeredContextManager {
private final ExplicitMemoryManager memoryManager;
private final SummarizedConversationManager summaryManager;
/**
* 为每次请求构建最优的上下文
* 层次:系统提示 → 长期记忆 → 对话摘要 → 近期对话 → 当前请求
*/
public List<Message> buildContext(
String sessionId,
String systemPrompt,
String currentMessage) {
// 1. 系统提示 + 长期记忆
String enrichedSystemPrompt = systemPrompt +
memoryManager.buildMemoryContext(sessionId);
// 2. 基于摘要的历史
List<Message> messages = summaryManager.buildMessagesWithSummary(
sessionId, enrichedSystemPrompt, currentMessage);
return messages;
}
/**
* 对话完成后的维护工作
*/
public void postTurnMaintenance(
String sessionId, String userMessage, String response) {
// 提取记忆
memoryManager.extractAndStore(sessionId, userMessage, response);
// 检查是否需要压缩
summaryManager.maybeCompress(sessionId);
}
}上下文管理的本质是用有限的窗口空间,装下最重要的信息。滑动窗口简单但会真正丢失信息;摘要压缩保留语义但额外增加LLM调用;显式记忆管理最精准但维护成本最高。
根据场景选择合适的策略,或者像分层方案一样组合使用,是工程上成熟的做法。
