上下文工程:窗口、预算、记忆与检索 本节摘要:提示工程只是子集,上下文工程(Context Engineering)才是整盘棋。提示是你敲进去的一段字符串,而上下文是塞进模型窗口的一切东西:系统指令、检索到的文档、工具定义、对话历史、少样本示例,外加提示本身。2026 年最好的 AI 工程师其实是上下文工程师,他们决定什么放进窗口、什么留在外面、按什么顺序排。本节带你吃透上下文窗口的稀缺性:窗口虽大但注意力不随长度线性增长,「lost-in-the-middle」效应让位于中间的信息召回率掉 1020%;六类上下文组件如何竞争同一份 token
本节摘要:提示工程只是子集,上下文工程(Context Engineering)才是整盘棋。提示是你敲进去的一段字符串,而上下文是塞进模型窗口的一切东西:系统指令、检索到的文档、工具定义、对话历史、少样本示例,外加提示本身。2026 年最好的 AI 工程师其实是上下文工程师,他们决定什么放进窗口、什么留在外面、按什么顺序排。本节带你吃透上下文窗口的稀缺性:窗口虽大但注意力不随长度线性增长,「lost-in-the-middle」效应让位于中间的信息召回率掉 10~20%;六类上下文组件如何竞争同一份 token 预算;历史压缩、相关性过滤、工具裁剪、递归摘要四大压缩策略;短/长/情景三类记忆的时间尺度;以及一套从零实现的动态上下文装配管线——按查询意图选工具、按相关度选文档、按 lost-in-the-middle 重排序。
对应原课程:Phase 11 · Lesson 05 ·
context-engineering(原英文phases/11-llm-engineering/05-context-engineering/docs/en.md)。
阅读完本节,你应当能够:
Claude Opus 4.7 有 200K token 窗口(测试版 1M)。GPT-5 是 400K。Gemini 3 Pro 是 2M。Llama 4 号称 10M。这些数字听起来吓人,直到你真把它填满。
一个编程助手的真实账本:系统提示 500 token;50 个工具的定义 8000 token;检索到的文档 4000 token;对话历史(10 轮)6000 token;当前用户查询 200 token;生成预留(最大输出)4000 token。合计 22700 token——只占 128K 窗口的 18%。
但注意力不随上下文长度线性扩展。一个塞满 128K token 的模型要付出平方级注意力开销(标准 Transformer 是 O(n²),多数生产模型用了高效注意力变体)。更要命的是召回精度会退化。「大海捞针」(Needle in a Haystack)测试表明,模型很难找到放在长上下文中间的信息。Liu 等人(2023)的实验显示,LLM 在长上下文开头和结尾的信息上召回准确率接近完美,但放在中间(40%70% 位置)的信息,准确率掉 1020%。这种「中间遗忘」效应因模型而异,但所有当前架构都中招。
实践教训很直接:有 200K token 可用,不代表用满 200K token 是有效的。精心挑选的 10K token 上下文,常常胜过一股脑倒进去的 100K token。上下文工程就是在窗口内最大化信噪比的学科。
你放进窗口的每一个 token,都挤掉了一个本可以承载更相关信息的 token。每条无关的工具定义、每段陈旧的对话、每个答非所问的检索块,都在让模型在这项任务上变差一点点。
把上下文窗口想成 RAM,而不是磁盘。它快、直接可访问,但有限。你装不下所有东西,必须做选择。
每个组件都在抢空间。多加工具定义,历史就少一截;多塞检索上下文,少样本就没位置。上下文工程就是在这份预算里做分配、让任务表现最大化的艺术。
上下文工程里最重要的实证发现。模型对上下文开头和结尾的信息注意力更强,中间信息得到的注意力分数更低、更容易被忽略。
Liu 等人(2023)系统测试了这一点:把一份相关文档混在 20 份无关文档里,放在不同位置,测量答题准确率。当相关文档放在最前或最后时,准确率 8590%;放在**中间(20 份里的第 10 位)**时,掉到 6070%。
这有直接的工程含义:
系统提示:设定角色、约束、行为规则。放在最前,跨轮次不变。Claude Code 的系统提示连同工具定义和行为指令大约 6000 token。要紧凑——系统提示里的每个字,在每次 API 调用里都要重复一遍。
工具定义:每个工具增加 50200 token(名字、描述、参数 schema)。50 个工具按 150 token 算就是 7500 token,对话还没开始就没了。动态工具选择——只放进与当前查询相关的工具——能省下 6080%。
检索上下文:来自向量数据库、搜索结果、文件内容的文档。检索质量直接决定响应质量。糟糕的检索比没有检索更糟——它用噪声填满窗口,还会主动误导模型。
对话历史:之前每一轮的用户消息与助手响应,随对话长度线性增长。一轮 200 token 的对话,50 轮就是 10000 token 的历史,其中大部分对当前查询无关。
少样本示例:展示期望行为的输入输出对。两三个精挑细选的示例,带来的质量提升往往胜过几千 token 的指令——但它要占空间。
生成预留:为模型响应保留的 token。把窗口塞到顶,模型就没地方作答了。至少预留 2000~4000 token 给生成。
历史摘要:与其逐字保留所有旧轮次,不如定期总结对话。「我们讨论了 X,决定了 Y,用户想要 Z」这 100 token,可以替代 2000 token 的十轮原文。当历史超过阈值(如 5000 token)就触发摘要。
相关性过滤:把每个检索文档按当前查询打分,丢弃低于阈值的。检索回 10 个块只有 3 个相关,就扔掉另外 7 个。3 个高度相关的块胜过 10 个平庸块。
工具裁剪:分类查询意图,只放进与意图匹配的工具。代码问题不需要日历工具,日程问题不需要文件系统工具。这能把工具定义从 8000 token 压到 1000。
递归摘要:对超长文档分阶段摘要。先总结每一节,再总结这些摘要。一份 50 页文档变成 500 token 的精华,抓住要点。
上下文工程横跨三个时间维度。
短期记忆(Short-term Memory):当前对话。直接存在上下文窗口里,随每轮增长,靠摘要与截断管理。
长期记忆(Long-term Memory):跨对话持久的事实与偏好。「用户偏好 TypeScript」「项目用 PostgreSQL」。存在数据库里,会话开始时检索注入。Claude Code 用 CLAUDE.md 文件存这个,ChatGPT 的 Memory 功能也是。
情景记忆(Episodic Memory):可能相关的过往交互。「上周二我们在认证模块修过一个类似的 bug」。存成嵌入,当当前对话与某个历史情景相似时检索回来。
关键洞见:不同查询需要不同上下文。静态系统提示 + 静态工具 + 静态历史,是浪费。最好的系统会按查询动态装配上下文。
这就是好 AI 应用和顶级 AI 应用的分水岭。模型是同一个,上下文是差异点。
我们把上述概念落到一个可运行的 Python 管线:一个token 计数器 + 一个预算管理器 + lost-in-the-middle 重排 + 对话历史压缩器 + 动态工具选择器 + 装配引擎。完整代码见原课程 code/context_engineering.py,这里给出关键骨架。
你管不了你测不了的东西。先做个简单计数器(用空格分词近似,精确值依赖分词器)。
import json from collections import OrderedDict def count_tokens(text): if not text: return 0 return int(len(text.split()) * 1.3) # 英文近似;中文按字数 ×1.5 更接近实际 def count_tokens_json(obj): return count_tokens(json.dumps(obj, ensure_ascii=False))
核心抽象。一个预算管理器追踪每个组件用了多少 token,并强制上限。
class ContextBudget: def __init__(self, max_tokens=128000, generation_reserve=4000): self.max_tokens = max_tokens self.generation_reserve = generation_reserve self.available = max_tokens - generation_reserve self.allocations = OrderedDict() def allocate(self, component, content, max_tokens=None): tokens = count_tokens(content) # 单组件上限:超过就截断到目标词数 if max_tokens and tokens > max_tokens: words = content.split() target_words = int(max_tokens / 1.3) content = " ".join(words[:target_words]) tokens = count_tokens(content) # 总预算上限:超了就再截断到剩余可用量 used = sum(self.allocations.values()) if used + tokens > self.available: allowed = self.available - used if allowed <= 0: return None, 0 words = content.split() target_words = int(allowed / 1.3) content = " ".join(words[:target_words]) tokens = count_tokens(content) self.allocations[component] = tokens return content, tokens def remaining(self): return self.available - sum(self.allocations.values()) def report(self): total = sum(self.allocations.values()) lines = [f"上下文预算报告(窗口 {self.max_tokens:,} token)", "-" * 50] for comp, tok in self.allocations.items(): pct = tok / self.max_tokens * 100 lines.append(f" {comp:<22} {tok:>6} token ({pct:>5.1f}%) {'#' * int(pct/2)}") lines += ["-" * 50, f" {'已用':<22} {total:>6} token ({total/self.max_tokens*100:.1f}%)", f" {'生成预留':<22} {self.generation_reserve:>6} token", f" {'剩余':<22} {self.remaining():>6} token"] return "\n".join(lines)
实现重排策略:最重要的项放最前和最后,最不重要的放中间。
def reorder_lost_in_middle(items, scores): # 按分数降序 paired = sorted(zip(scores, items), reverse=True) sorted_items = [item for _, item in paired] if len(sorted_items) <= 2: return sorted_items # 偶数位取前半,奇数位取后半并反转,拼起来 first_half = sorted_items[::2] # 分数 1、3、5…… second_half = sorted_items[1::2] # 分数 2、4、6…… second_half.reverse() return first_half + second_half
效果:分数最高的项落在序列首位,第二高落在末位,第三高落在次首位,以此类推——把高分项推到两端,低分项留在中间。
把旧轮次摘要掉,腾出预算。
class ConversationManager: def __init__(self, max_history_tokens=5000): self.turns = [] self.summaries = [] self.max_history_tokens = max_history_tokens def add_turn(self, role, content): self.turns.append({"role": role, "content": content}) self._compress_if_needed() def _compress_if_needed(self): total = sum(count_tokens(t["content"]) for t in self.turns) while total > self.max_history_tokens and len(self.turns) > 4: old = self.turns[:2] self.summaries.append(self._summarize_turns(old)) self.turns = self.turns[2:] total = sum(count_tokens(t["content"]) for t in self.turns) def _summarize_turns(self, turns): parts = [] for t in turns: c = t["content"] if len(c) > 100: c = c[:100] + "..." parts.append(f"{t['role']}: {c}") return "此前对话: " + " | ".join(parts) def get_context(self): parts = [] if self.summaries: parts.append("[对话摘要]") parts.extend(self.summaries) parts.append("[近期对话]") for t in self.turns: parts.append(f"{t['role']}: {t['content']}") return "\n".join(parts)
阈值触发、每次压缩最早两轮、保留最近四轮——这样既省 token 又保住上下文连续性。生产中 _summarize_turns 通常换成一次真正的 LLM 摘要调用。
只放进与当前查询相关的工具。先分类意图,再过滤。
TOOL_REGISTRY = { "read_file": {"tokens": 120, "categories": ["code", "files"]}, "write_file": {"tokens": 150, "categories": ["code", "files"]}, "search_code": {"tokens": 130, "categories": ["code"]}, "run_command": {"tokens": 140, "categories": ["code", "system"]}, "create_calendar_event": {"tokens": 180, "categories": ["calendar"]}, "list_emails": {"tokens": 160, "categories": ["email"]}, "send_email": {"tokens": 200, "categories": ["email"]}, "web_search": {"tokens": 140, "categories": ["research"]}, "query_database": {"tokens": 170, "categories": ["code", "data"]}, "generate_chart": {"tokens": 190, "categories": ["data"]}, } def classify_intent(query): q = query.lower() intent_kw = { "code": ["code", "function", "bug", "error", "file", "implement", "debug", "test"], "calendar": ["meeting", "schedule", "calendar", "appointment", "event"], "email": ["email", "mail", "send", "inbox", "message"], "research": ["search", "find", "what is", "how does", "explain", "look up"], "data": ["data", "query", "database", "chart", "graph", "sql"], } scores = {i: sum(1 for kw in kws if kw in q) for i, kws in intent_kw.items()} scores = {i: s for i, s in scores.items() if s > 0} if not scores: return ["code"] # 默认回退到代码类 m = max(scores.values()) return [i for i, s in scores.items() if s >= m * 0.5] def select_tools(query, token_budget=2000): intents = classify_intent(query) relevant, total = {}, 0 for name, tool in TOOL_REGISTRY.items(): if any(cat in intents for cat in tool["categories"]): if total + tool["tokens"] <= token_budget: relevant[name] = tool total += tool["tokens"] return relevant, total
一句「修一下 auth.py 的 bug」,只会带回 read_file/write_file/search_code/run_command,把无关的日历、邮件、图表工具全过滤掉。
把所有零件接起来。给定一个查询,动态装配最优上下文。
class ContextEngine: def __init__(self, max_tokens=128000, generation_reserve=4000): self.budget = ContextBudget(max_tokens, generation_reserve) self.conversation = ConversationManager(max_history_tokens=5000) self.system_prompt = ( "你是一个有用的 AI 助手。你可以使用代码编辑、文件管理、" "网页搜索、数据分析等工具。按任务选用合适工具,简洁准确。" ) self.knowledge_base = [ "项目用 PostgreSQL 16 + pgvector 存储嵌入。", "认证由 Supabase Auth 处理,使用 JWT token。", "前端用 Next.js 15 App Router 构建。", "API 限流为每用户每分钟 100 次请求。", "测试覆盖率要求所有新模块高于 80%。", ] def assemble(self, query): self.budget = ContextBudget(self.budget.max_tokens, self.budget.generation_reserve) self.budget.allocate("system_prompt", self.system_prompt, max_tokens=1000) # 动态选工具 tools, _ = select_tools(query, token_budget=2000) self.budget.allocate("tools", json.dumps(list(tools.keys()), ensure_ascii=False), max_tokens=2000) # 检索 + 相关性过滤 + lost-in-the-middle 重排 rel = score_relevance(query, self.knowledge_base) threshold = 0.1 relevant = [d for d, s in zip(self.knowledge_base, rel) if s >= threshold] if relevant: dscores = [s for s in rel if s >= threshold] reordered = reorder_lost_in_middle(relevant, dscores) self.budget.allocate("retrieved_context", "\n".join(reordered), max_tokens=3000) # 历史 hist = self.conversation.get_context() if hist.strip(): self.budget.allocate("conversation_history", hist, max_tokens=5000) # 当前查询 self.budget.allocate("user_query", query, max_tokens=500) return self.budget def chat(self, query): self.conversation.add_turn("user", query) budget = self.assemble(query) response = f"[对 {query[:50]}…… 的响应]" self.conversation.add_turn("assistant", response) return budget
💡 设计要点:每次
assemble都重建一次预算对象,确保报告反映这一次查询的分配,而不是跨查询累加。装配顺序本身就是优先级声明——系统提示最先(占住首位),用户查询靠后(占住末位)。
Claude Code 用分层方式管理上下文。系统提示含行为规则和工具定义(约 6K token);打开文件时把内容注入;搜索时把结果加进来;旧轮次被摘要;CLAUDE.md 提供跨会话的长期记忆。
关键的工程决策是:Claude Code 不会把你整个代码库倒进上下文。它按需检索相关文件——这就是上下文工程在产品里的落地。
Cursor 把整个代码库索引进嵌入。你输入查询时,它用向量相似度检索最相关的文件和代码块,只把那些片段放进上下文窗口。一个 50 万行的代码库,被压缩成 5~10 个最相关的代码块。
模式很清楚:全部嵌入,按需检索,只放要紧的。
ChatGPT 把用户偏好与事实存成长期记忆。每次对话开始,相关记忆被检索并放进系统提示。「用户偏好 Python」只要 5 token,却省下了跨对话反复用几百 token 重新说明的开销。
检索增强生成(RAG)就是被正式化的上下文工程。与其把知识塞进模型权重(训练)或系统提示(静态上下文),不如在查询时检索相关文档、注入上下文窗口。整个 RAG 管线——分块、嵌入、检索、重排——存在的全部理由,就是解决一个问题:把正确的信息放进上下文窗口。下一节我们专门讲它。
本节产出两个可复用文件(位于原课程 outputs/):
prompt-context-optimizer.md:一个元提示,审计一套上下文装配策略并给出优化建议。喂给它你的系统提示、工具数量、平均历史长度、检索策略,它就能定位 token 浪费、提出改进。skill-context-engineering.md:一个决策框架,按任务类型、上下文窗口大小、延迟预算,帮你设计上下文装配管线。Python 代码(code/context_engineering.py)是一条独立装配管线,把模拟响应换成真实 API 调用即可投产,预算管理、重排、压缩、工具选择逻辑均无需修改。
token 浪费探测器:给 ContextBudget 加一个检测器,标记占用预算超过 30% 的组件,并按组件类型给出专属压缩建议(摘要历史、裁剪工具、重排文档)。
语义去重:为检索上下文实现语义去重。若两份文档相似度超过 80%(按词重叠或嵌入余弦相似度),只保留分数更高的那个。测量这能回收多少 token 预算。
上下文回放工具:给定一段对话转录,让它通过 ContextEngine 回放,逐轮可视化预算分配的变化。画出每个组件随时间的 token 占用曲线,找出从哪一轮开始上下文被压缩。
优先级工具选择器:把二元的「纳入/排除」改成给每个工具按与当前查询的相关度打分,按降序纳入直到工具预算耗尽。对比纳入 5、10、20、50 个工具时的任务表现。
多策略压缩器:实现三种压缩策略(截断、摘要、关键句抽取),在 20 份文档上做基准。测量压缩率与信息保留(压缩后的版本是否仍包含查询的答案)之间的权衡。
下一节,我们正式进入 RAG——把本节的「按需检索相关上下文」升级为一条完整的「分块-嵌入-检索-生成」管线,让模型用上你自己语料库里的知识。