长上下文模型的工程机会——100K Token 能做什么新事情
长上下文模型的工程机会——100K Token 能做什么新事情
适读人群:AI 应用工程师 / 对大模型工程实践感兴趣 | 阅读时长:约16分钟 | 核心价值:100K Token 带来的真实工程机会,以及哪些是伪需求的诚实判断
100K Token 这个东西,刚出来的时候我兴奋了大概一周。
我的第一反应是:完美!以后不用做 RAG 了,把整个知识库塞进去就行。然后我试了,发现这个想法幼稚得可笑——不是技术上不可行,而是成本、速度、和实际效果都有问题。
冷静下来之后,我开始更实际地思考:100K Token 真正开启了哪些新的工程可能?哪些是我以前做不了的事,现在可以做了?
这两年我做了一些尝试,有真的有价值的,也有感觉是伪需求的。今天都说出来,不帮你制造幻觉。
先说一个基本认知:长上下文不是免费的
在讨论"能做什么"之前,要先理解代价:
成本:Token 越多,费用越高。100K Token 的调用,成本可能是 4K Token 调用的几十倍。
速度:上下文越长,首 Token 延迟越高,用户等待时间越长。100K 上下文的首 Token 延迟,通常在 5-15 秒甚至更长。
准确性衰减:当前大多数模型在超长上下文时,"中间部分"的内容容易被忽略("Lost in the Middle"问题)。把 100K Token 全部放进去,不代表模型会同等关注所有内容。
所以正确的问题不是"100K Token 能放什么",而是"什么场景值得付出这些代价"。
机会1:整个代码库的分析
这是我觉得最有工程价值的应用。
传统的代码 AI 助手,受限于 4K/8K 上下文,每次只能看一个文件或几个文件。但很多代码问题是跨文件的:这个接口在哪里实现?这个变量是从哪里传进来的?修改这个函数会影响哪些地方?
有了 100K Token,可以把整个中小型项目的代码一次性放进去分析。
我做了一个内部工具:"代码变更影响分析"。给定一个 PR 的 diff,分析这个改动可能影响哪些模块、有没有潜在的 bug、有没有违反已有的设计模式。
@Service
public class CodebaseAnalysisService {
@Autowired
private ChatClient chatClient;
@Autowired
private GitService gitService;
@Autowired
private TokenCountService tokenCountService;
/**
* 分析 PR 的变更影响
* 这个功能在 4K Token 时代做不了
*/
public ChangeImpactAnalysis analyzePrImpact(String prNumber) {
PullRequest pr = gitService.getPr(prNumber);
// 获取 PR 涉及的文件及其上下文
List<String> changedFiles = pr.getChangedFiles();
// 构建分析上下文
StringBuilder contextBuilder = new StringBuilder();
// 加入 PR diff
contextBuilder.append("## PR 变更内容\n");
contextBuilder.append(pr.getDiff());
contextBuilder.append("\n\n");
// 加入被修改文件的完整内容
contextBuilder.append("## 被修改文件\n");
for (String file : changedFiles) {
String content = gitService.getFileContent(file);
contextBuilder.append(String.format("### %s\n```\n%s\n```\n\n", file, content));
}
// 加入相关联的文件(被修改文件 import 的或 import 了修改文件的)
contextBuilder.append("## 相关联文件\n");
Set<String> relatedFiles = gitService.getRelatedFiles(changedFiles);
for (String file : relatedFiles) {
String content = gitService.getFileContent(file);
// 相关文件只加入关键部分(接口定义、方法签名)而非完整内容
// 节省 token 空间
String skeleton = extractCodeSkeleton(content);
contextBuilder.append(String.format("### %s\n```\n%s\n```\n\n", file, skeleton));
}
// 检查 token 数量,如果超限就裁减
String context = contextBuilder.toString();
int tokenCount = tokenCountService.count(context);
if (tokenCount > 80000) { // 留 20K token 给 prompt 和 response
context = truncateContext(context, 80000);
}
String prompt = String.format("""
你是一个资深工程师,请分析以下代码变更的影响。
%s
请分析:
1. 这个变更可能影响哪些功能模块?
2. 有没有潜在的 bug 或边界情况没有处理?
3. 有没有遗漏的测试用例?
4. 有没有违反代码库中已有的设计模式?
5. 综合建议
请给出具体的代码位置和原因。
""", context);
String analysis = chatClient.prompt()
.user(prompt)
.call()
.content();
return ChangeImpactAnalysis.builder()
.prNumber(prNumber)
.analysis(analysis)
.tokenUsed(tokenCount)
.build();
}
/**
* 提取代码骨架(只保留类名、方法签名、注释)
* 在不需要完整实现时,节省 token
*/
private String extractCodeSkeleton(String code) {
// 简化实现:去掉方法体
// 实际实现需要用语法分析
return code.replaceAll("\\{[^}]{50,}\\}", "{ ... }");
}
}这个工具在我们团队实际用起来了。Code Review 时,把 PR 号扔进去,几秒内得到一份影响分析报告,作为 reviewer 的辅助参考。
不能替代人工 review,但能显著提高 review 效率,特别是大 PR。
实际价值判断:有价值,但要控制成本。 一次分析可能用 50K-80K token,成本不低。我们只在关键 PR(涉及核心逻辑的修改)上用,不是每个 PR 都跑。
机会2:长文档的深度对话
RAG 解决的是"从大量文档里找相关内容"的问题,但它有个弱点:如果问题需要综合整篇文档的多处内容来回答,检索出来的片段可能不够。
比如:"这份合同在第 3 条和第 15 条之间有没有矛盾?"这种问题,检索出两个片段,但模型看不到完整上下文,可能回答不准确。
100K Token 的使用场景:把整个文档放进去,让模型做全局分析。
@Service
public class DocumentDeepAnalysisService {
@Autowired
private ChatClient chatClient;
@Autowired
private DocumentStorage documentStorage;
/**
* 对单个长文档进行深度分析
* 适合需要全局理解的问题
*/
public DocumentAnalysisResult analyzeDocument(String documentId,
String analysisTask) {
Document document = documentStorage.load(documentId);
// 粗略估算 token 数(1 字符约 1.5 token,中文约 2 token/字)
int estimatedTokens = (int) (document.getContent().length() * 2);
if (estimatedTokens > 100000) {
// 文档太长,不适合全文放入,改用 RAG 策略
return DocumentAnalysisResult.tooLong(
"文档超过 100K Token,建议换用 RAG 方式");
}
// 文档长度在 100K 以内,直接放入
String prompt = String.format("""
以下是完整的文档内容,请仔细阅读后回答问题。
文档内容:
%s
分析任务:%s
""", document.getContent(), analysisTask);
// 这个调用会很慢,预计 5-20 秒
String result = chatClient.prompt()
.user(prompt)
.call()
.content();
return DocumentAnalysisResult.builder()
.documentId(documentId)
.task(analysisTask)
.result(result)
.tokensUsed(estimatedTokens)
.build();
}
/**
* 混合策略:根据问题类型决定用全文还是 RAG
*/
public String intelligentQuery(String documentId, String question) {
QuestionType questionType = classifyQuestion(question);
return switch (questionType) {
case GLOBAL_ANALYSIS -> {
// "总结"、"有没有矛盾"、"整体结构"等问题,需要全文上下文
yield analyzeDocument(documentId, question).getResult();
}
case LOCAL_LOOKUP -> {
// "第几条说了什么"、"某个术语的定义"等问题,RAG 更快更省
yield ragService.query(documentId, question);
}
case HYBRID -> {
// 既需要局部检索又需要全局判断
// 先 RAG 找相关内容,再把相关内容 + 文档结构放入上下文
String relevantContent = ragService.retrieveRelevant(documentId, question);
String documentOutline = documentStorage.getOutline(documentId);
yield queryWithPartialContext(question, relevantContent, documentOutline);
}
};
}
private QuestionType classifyQuestion(String question) {
// 简单规则分类
if (question.contains("矛盾") || question.contains("冲突") ||
question.contains("总结") || question.contains("整体") ||
question.contains("全文") || question.contains("综合")) {
return QuestionType.GLOBAL_ANALYSIS;
}
if (question.contains("第几条") || question.contains("定义") ||
question.contains("列出所有")) {
return QuestionType.LOCAL_LOOKUP;
}
return QuestionType.HYBRID;
}
}机会3:多轮对话历史的完整保留
传统聊天机器人,上下文窗口有限,超出部分会被截断或压缩。用户会发现"AI 忘记了之前说过的事情"。
有了 100K Token,可以保留完整的对话历史,让用户体验到真正连续的对话。
@Service
public class LongContextChatService {
@Autowired
private ChatClient chatClient;
@Autowired
private ConversationHistoryStore historyStore;
/**
* 保留完整历史的对话
* 关键:需要动态管理上下文,防止无限增长
*/
public String chat(String conversationId, String userMessage) {
List<ConversationTurn> history = historyStore.loadFull(conversationId);
// 估算当前历史的 token 数
int historyTokens = estimateTokens(history);
// 如果加上新消息超过安全上限(留出 10K 给 response)
if (historyTokens + estimateTokens(userMessage) > 90000) {
// 启动历史压缩:把旧的对话历史总结成摘要
history = compressHistory(conversationId, history);
}
// 构建消息列表
List<Message> messages = buildMessages(history, userMessage);
String response = chatClient.prompt()
.messages(messages)
.call()
.content();
// 保存这轮对话
historyStore.append(conversationId, userMessage, response);
return response;
}
/**
* 历史压缩策略:
* 保留最近 20 轮完整对话
* 把更早的对话总结成摘要
*/
private List<ConversationTurn> compressHistory(String conversationId,
List<ConversationTurn> history) {
int keepRecent = 20;
if (history.size() <= keepRecent) return history;
List<ConversationTurn> toCompress = history.subList(0, history.size() - keepRecent);
List<ConversationTurn> toKeep = history.subList(history.size() - keepRecent, history.size());
// 用 AI 总结需要压缩的部分
String summaryPrompt = String.format("""
请将以下对话历史总结成简洁的要点,保留关键信息和用户的核心需求:
%s
""", formatHistory(toCompress));
String summary = chatClient.prompt()
.user(summaryPrompt)
.call()
.content();
// 用摘要替换早期历史
List<ConversationTurn> compressed = new ArrayList<>();
compressed.add(ConversationTurn.systemNote(
"以下是之前对话的摘要:" + summary));
compressed.addAll(toKeep);
// 保存压缩后的版本
historyStore.saveCompressed(conversationId, compressed);
return compressed;
}
}实际价值判断:对某些场景很有价值,但不是所有场景。
我发现,大多数用户的对话不超过 20 轮就结束了,4K Token 的历史窗口其实够用。但在一些特定场景——比如持续几天的项目咨询、复杂的法律文件审查——完整历史保留的价值很大。这类场景值得用 100K Token。
哪些是伪需求(诚实地说)
伪需求1:把整个知识库放进去取代 RAG
我最开始的想法,事实证明不可行:
- 成本高得离谱。几百篇文档放进去,一次查询可能用掉几十万 token
- 速度慢。100K Token 的调用,首 token 延迟经常超过 10 秒
- 质量不一定更好。RAG 可以精确检索最相关的内容,长上下文把不相关的内容也塞进去,反而可能干扰模型
RAG + 长上下文是互补的,不是替代关系。
伪需求2:用来存储用户的个性化信息
有人想把用户的所有历史行为、偏好都放到上下文里。这在概念上有吸引力,但实际上:
- 用户的历史数据量随着使用增长,很快就超过 100K Token
- 对很多查询来说,大多数历史信息是不相关的,放进去是噪声
- 正确做法还是用数据库存储,查询时按需检索
伪需求3:放入大量示例来 few-shot 学习
有人想放几十个示例来指导 AI 的输出格式。但实际上,3-5 个精心挑选的示例效果不比 50 个随机示例差,还省了大量 token。质量比数量重要。
工程上的关键实现:Token 计数
使用长上下文的最大工程挑战是:你需要在发请求之前准确估算 token 数,避免超限。
@Service
public class TokenCountService {
// 使用 tiktoken 的 Java 移植库
private final Encoding encoding;
public TokenCountService() {
this.encoding = Encodings.newDefaultEncodingRegistry()
.getEncoding(EncodingType.CL100K_BASE); // GPT-4 用的编码
}
/**
* 计算文本的 token 数量
*/
public int count(String text) {
return encoding.countTokens(text);
}
/**
* 计算消息列表的 token 数量
* 注意:每条消息有额外的格式 token(约 4 个)
*/
public int countMessages(List<Message> messages) {
int total = 3; // 对话结构的固定开销
for (Message msg : messages) {
total += 4; // 每条消息的额外 token
total += count(msg.getContent());
}
return total;
}
/**
* 在给定 token 预算内,保留尽可能多的内容
* 策略:优先保留最近的内容
*/
public String truncateToTokenLimit(String text, int maxTokens) {
int currentTokens = count(text);
if (currentTokens <= maxTokens) return text;
// 按段落分割,从后往前保留
String[] paragraphs = text.split("\n\n");
StringBuilder result = new StringBuilder();
int usedTokens = 0;
for (int i = paragraphs.length - 1; i >= 0; i--) {
int paragraphTokens = count(paragraphs[i]);
if (usedTokens + paragraphTokens <= maxTokens) {
result.insert(0, paragraphs[i] + "\n\n");
usedTokens += paragraphTokens;
} else {
// 超出预算,在这里截断
result.insert(0, "[...早期内容已截断...]\n\n");
break;
}
}
return result.toString().trim();
}
}100K Token 是一个工具,不是魔法。它开启了一些以前做不了的事情,但用它需要付出成本,而且不是所有问题都适合用它解决。
理解它的边界,找到它真正合适的场景,是工程师的工作。
