上下文工程精要:Context Engineering的艺术与实践
2026/4/30大约 7 分钟
上下文工程精要:Context Engineering的艺术与实践
适读人群:有1-5年Java开发经验,想向AI工程师方向转型的开发者 阅读时长:约16分钟 文章价值:① 建立Context Engineering的完整认知框架 ② 掌握上下文管理的核心技术手段 ③ 理解为什么上下文工程比Prompt Engineering更重要
有一次,我帮一个朋友看他们的AI问答系统,发现一个奇怪的现象:
同样一个问题——"我们的退款政策是什么"——问AI,有时候答得很准,有时候答得不知所云。
我看了看代码,发现问题:每次对话,他们都把所有相关文档全部塞给AI。知识库里有200个文档,每次调用把所有文档内容全部放进context,token爆了,重要的反而被淹没了。
我跟他说了一句话,他记了很久:
"Prompt Engineering是告诉AI怎么做,Context Engineering是决定AI用什么来做。后者决定了上限,前者决定了下限。"
什么是Context Engineering
Context Engineering(上下文工程)是指:设计、管理和优化送给LLM的所有信息的工程实践。
它包括:
- 放什么进context(哪些信息)
- 放多少进context(token预算)
- 怎么放(格式、顺序、结构)
- 什么时候更新context(动态管理)
核心问题:Context爆炸
长对话、大文档、多工具……context会越来越长,超出模型的context window,或者成本爆炸。
几种典型的Context爆炸场景:
| 场景 | 问题 | 影响 |
|---|---|---|
| 长对话 | 对话历史无限增长 | 超出context window,早期信息丢失 |
| 大文档RAG | 全文档塞入context | Token浪费,重要信息被稀释 |
| 多工具调用 | 工具结果不断累积 | Context污染,模型注意力分散 |
| 多轮Agent | 中间步骤全部保留 | Context几十KB,性能下降 |
解决方案:五种Context管理策略
代码实战
策略一:对话历史滑动窗口
@Component
@RequiredArgsConstructor
public class SlidingWindowConversationManager {
private static final int MAX_TURNS = 10; // 最多保留10轮
private static final int MAX_TOKENS_APPROX = 4000; // 历史区域最多4000 tokens
private final Map<String, Deque<ConversationTurn>> sessions = new ConcurrentHashMap<>();
/**
* 获取对话历史(经过滑动窗口处理)
*/
public List<Message> getHistoryMessages(String sessionId) {
Deque<ConversationTurn> history = sessions.getOrDefault(
sessionId, new ArrayDeque<>());
// 按token估算截断(粗略:每个中文字约1.5 token)
List<ConversationTurn> trimmed = trimByTokenBudget(
new ArrayList<>(history), MAX_TOKENS_APPROX);
return trimmed.stream()
.flatMap(turn -> Stream.of(
new UserMessage(turn.getUserMessage()),
new AssistantMessage(turn.getAiResponse())
))
.collect(Collectors.toList());
}
public void addTurn(String sessionId, String userMsg, String aiResponse) {
sessions.computeIfAbsent(sessionId, k -> new ArrayDeque<>());
Deque<ConversationTurn> history = sessions.get(sessionId);
history.addLast(new ConversationTurn(userMsg, aiResponse, LocalDateTime.now()));
// 超过最大轮数,移除最旧的
while (history.size() > MAX_TURNS) {
history.pollFirst();
}
}
private List<ConversationTurn> trimByTokenBudget(List<ConversationTurn> turns, int budget) {
// 从最新往最旧方向选,选到预算满为止
List<ConversationTurn> result = new ArrayList<>();
int tokenCount = 0;
for (int i = turns.size() - 1; i >= 0; i--) {
ConversationTurn turn = turns.get(i);
int turnTokens = estimateTokens(turn.getUserMessage() + turn.getAiResponse());
if (tokenCount + turnTokens > budget) break;
result.add(0, turn);
tokenCount += turnTokens;
}
return result;
}
private int estimateTokens(String text) {
// 粗略估算:中文1.5/字,英文0.25/字
return (int) (text.length() * 1.2);
}
}策略二:对话摘要压缩
当历史很长时,不是截断,而是让AI把旧对话压缩成摘要:
@Component
@RequiredArgsConstructor
public class ConversationSummarizer {
private final ChatClient chatClient;
private final StringRedisTemplate redisTemplate;
private static final String SUMMARY_KEY = "conversation:summary:";
private static final int SUMMARIZE_THRESHOLD = 8; // 超过8轮触发摘要
/**
* 智能对话历史管理:超长时自动摘要压缩
*/
public ConversationContext getContext(String sessionId, List<ConversationTurn> allHistory) {
if (allHistory.size() <= SUMMARIZE_THRESHOLD) {
// 历史不长,直接用
return ConversationContext.builder()
.recentTurns(allHistory)
.build();
}
// 分成"旧历史"和"近期历史"
int recentCount = 4; // 保留最近4轮完整历史
List<ConversationTurn> oldHistory = allHistory.subList(0, allHistory.size() - recentCount);
List<ConversationTurn> recentHistory = allHistory.subList(
allHistory.size() - recentCount, allHistory.size());
// 旧历史:先查Redis缓存,没有就重新生成摘要
String summary = getSummary(sessionId, oldHistory);
return ConversationContext.builder()
.historySummary(summary)
.recentTurns(recentHistory)
.build();
}
private String getSummary(String sessionId, List<ConversationTurn> oldHistory) {
String cacheKey = SUMMARY_KEY + sessionId;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
// 生成摘要
String historyText = oldHistory.stream()
.map(t -> "用户:" + t.getUserMessage() + "\nAI:" + t.getAiResponse())
.collect(Collectors.joining("\n---\n"));
String summary = chatClient.prompt()
.system("你是对话摘要专家,请将对话历史压缩成简洁摘要,保留关键信息、用户偏好和重要决定。")
.user("请压缩以下对话历史:\n\n" + historyText)
.call()
.content();
// 缓存摘要,1小时有效
redisTemplate.opsForValue().set(cacheKey, summary, Duration.ofHours(1));
return summary;
}
}策略三:动态RAG检索(核心)
不是把所有文档全塞进去,而是根据当前问题动态检索最相关的内容:
@Service
@RequiredArgsConstructor
@Slf4j
public class DynamicContextBuilder {
private final VectorStore vectorStore;
private final ChatClient chatClient;
// Token预算配置
private static final int SYSTEM_BUDGET = 500;
private static final int HISTORY_BUDGET = 2000;
private static final int RAG_BUDGET = 3000;
private static final int OUTPUT_RESERVE = 1500;
/**
* 为一次AI调用构建最优Context
*/
public OptimizedContext buildContext(
String sessionId,
String userQuery,
ConversationContext conversationContext) {
// 1. 分析查询意图,决定检索策略
QueryIntent intent = analyzeIntent(userQuery);
// 2. 动态检索(根据意图调整检索参数)
List<String> ragDocs = retrieveRelevantDocs(userQuery, intent);
// 3. Token预算管理:各部分限额
String systemPrompt = buildSystemPrompt(intent);
String historyText = buildHistoryText(conversationContext, HISTORY_BUDGET);
String ragText = trimToTokenBudget(ragDocs, RAG_BUDGET);
log.debug("Context构建完成:system={}tokens, history={}tokens, rag={}tokens",
estimateTokens(systemPrompt),
estimateTokens(historyText),
estimateTokens(ragText));
return OptimizedContext.builder()
.systemPrompt(systemPrompt)
.historyText(historyText)
.ragContext(ragText)
.estimatedTokens(estimateTokens(systemPrompt + historyText + ragText + userQuery))
.build();
}
private QueryIntent analyzeIntent(String query) {
// 简单规则分类,复杂场景可以用LLM分类
if (query.contains("退款") || query.contains("投诉")) return QueryIntent.CUSTOMER_SERVICE;
if (query.contains("价格") || query.contains("费用")) return QueryIntent.PRICING;
if (query.contains("技术") || query.contains("如何")) return QueryIntent.TECHNICAL;
return QueryIntent.GENERAL;
}
private List<String> retrieveRelevantDocs(String query, QueryIntent intent) {
// 根据意图决定检索数量和相似度阈值
int topK = switch (intent) {
case TECHNICAL -> 5; // 技术问题需要更多参考
case CUSTOMER_SERVICE -> 3;
default -> 3;
};
double threshold = 0.75; // 相似度阈值,低于此不使用
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(threshold)
).stream()
.map(Document::getContent)
.collect(Collectors.toList());
}
private String trimToTokenBudget(List<String> docs, int budget) {
if (docs.isEmpty()) return "";
StringBuilder sb = new StringBuilder("【参考文档】\n");
int used = 20;
for (String doc : docs) {
int docTokens = estimateTokens(doc);
if (used + docTokens > budget) {
// 超预算:截断这篇文档
int remaining = budget - used;
if (remaining > 100) {
int charLimit = (int) (remaining / 1.2);
sb.append("- ").append(doc, 0, charLimit).append("...\n");
}
break;
}
sb.append("- ").append(doc).append("\n");
used += docTokens;
}
return sb.toString();
}
private int estimateTokens(String text) {
return (int) (text.length() * 1.2);
}
}策略四:使用Spring AI的内置Advisor管理上下文
Spring AI 1.0提供了内置的对话历史管理机制:
@Service
@RequiredArgsConstructor
public class SpringAiContextService {
private final ChatClient.Builder chatClientBuilder;
private final ChatMemory chatMemory; // Spring AI内置的对话记忆
/**
* 利用Spring AI内置的MessageChatMemoryAdvisor管理对话历史
*/
public ChatClient buildContextAwareChatClient() {
return chatClientBuilder
.defaultAdvisors(
// 内置的对话历史管理Advisor
new MessageChatMemoryAdvisor(chatMemory),
// 内置的RAG Advisor
new QuestionAnswerAdvisor(vectorStore,
SearchRequest.defaults().withTopK(4))
)
.build();
}
/**
* 带完整上下文的对话
*/
public String chatWithFullContext(String sessionId, String userMessage) {
return buildContextAwareChatClient()
.prompt()
.user(userMessage)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call()
.content();
}
}一个工程实践清单
做Context Engineering,我建议每次上线前做这个检查:
□ System Prompt是否精简,没有废话?(每100个system tokens = 每次调用的固定成本)
□ 对话历史是否有长度限制?(防止对话越来越长导致爆context)
□ RAG检索是否设置了相似度阈值?(不要把低相关文档也塞进去)
□ 是否做了Token预算分配?(各部分有上限)
□ 长对话是否有摘要机制?(不是截断,是压缩)
□ Context中是否有过期/无关信息?(定期清理)一套好的Context Engineering,能让同样的LLM输出质量提升30-50%,同时把API成本降低20-40%。
这不是玄学,是工程。
