早期讨论 Prompt Engineering 时,有一种常见判断:随着大语言模型能力增强,提示词技巧会逐渐不重要。这个判断看起来很合理,搜索引擎也经历过类似过程。早期用户需要记住通配符、特殊操作符、link、define 等检索语法,后来搜索引擎变聪明了,普通自然语言也能得到不错的结果。
但大语言模型(Large Language Model,LLM)的应用开发没有沿着这条路完全前进。提示词没有消失,反而扩展成了一个更大的工程问题:Context Engineering,也就是上下文工程。
Prompt Engineering 关注“怎么问”;Context Engineering 关注“模型在下一步推理前应该看到什么”。这两者不是并列关系,而是包含关系:提示词只是上下文的一部分,真正决定模型行为的,是整个 context window 中的指令、历史、知识、工具描述、工具返回结果、状态和记忆。
Context Engineering 解决什么问题
一个 LLM 调用可以粗略看成一个无状态函数:
output = LLM(context)
模型权重在一次推理过程中不会改变。它能用来生成答案的信息,主要来自两个地方:
- 训练阶段已经写入参数的知识和能力;
- 推理时放进上下文窗口的内容。
Context Engineering 的核心任务,就是在调用模型前,把“刚好需要的信息”组织进有限的上下文窗口,让模型能够完成下一步任务。
可以把一次 LLM 应用调用拆成下面的结构:
flowchart LR
U[用户输入] --> B[上下文构建器]
subgraph Sources[上下文来源]
P[系统指令 / 任务指令]
H[历史对话 / 当前状态]
K[外部知识库 / RAG]
M[长期记忆]
T[工具定义 / 工具返回结果]
S[少样本示例]
end
Sources --> B
B --> W[Context Window]
W --> L[LLM 推理]
L --> O[回答 / 工具调用 / 下一步动作]
Prompt Engineering 主要处理图里的指令部分,Context Engineering 还要处理其他来源:哪些历史要保留,哪些知识要检索,哪些工具要暴露,工具结果如何压缩,长期记忆何时写入,冲突信息如何消解。
两者的差异可以这样理解:
| 维度 | Prompt Engineering | Context Engineering |
|---|---|---|
| 主要对象 | 提示词、指令、问题表达方式 | 完整上下文窗口 |
| 典型内容 | system prompt、user prompt、少样本示例 | 指令、历史、状态、RAG 文档、工具定义、工具结果、记忆、输出结构 |
| 目标 | 让模型更好理解任务 | 让模型在下一步推理时看到正确、足够、不过量的信息 |
| 主要风险 | 指令不清、格式不稳、约束不足 | 信息缺失、无关信息过多、上下文冲突、记忆污染、成本失控 |
| 工程重点 | 写提示词 | 选择、写入、压缩、隔离、排序和复用上下文 |
上下文窗口不是“能塞多少塞多少”的垃圾桶。窗口越长,成本越高,模型也越容易被无关信息干扰。Context Engineering 要处理的矛盾是:信息太少,模型缺依据;信息太多,模型被噪声带偏;信息有冲突,模型不知道该信哪一个。
用 LLM OS 理解 Context Engineering
Andrej Karpathy 曾把 LLM 类比为一种新的操作系统。这个类比不是严格的系统定义,但很适合帮助工程师理解 LLM 应用的开发边界。
传统操作系统里,应用程序通过系统调用使用内核能力;LLM 应用里,开发者通过推理接口、函数调用和工具协议使用模型能力。对应关系大致如下:
| 操作系统概念 | LLM 应用中的对应物 | 说明 |
|---|---|---|
| Kernel(内核) | 模型架构和模型权重 | Transformer 结构、参数权重、推理能力 |
| System Call(系统调用) | 推理 API、Function Calling、工具调用 | 应用通过接口请求模型生成文本或决定动作 |
| Shell(命令解释器) | Chat UI、CLI、对话前端 | 用户与模型交互的入口 |
| Library(库函数) | LangChain、LlamaIndex、向量数据库、模板引擎、微调工具 | 封装常见能力,降低应用开发成本 |
| File System(文件系统) | 外部知识库、文件、数据库、向量索引 | 存放模型当前窗口外的信息 |
| RAM(内存) | Context Window | 模型本轮推理真正可见的信息 |
| Application(应用程序) | Agent、Copilot、智能客服、代码助手 | 基于模型和工具构建的上层应用 |
从这个角度看,Context Engineering 类似于 LLM OS 上的“用户态程序开发”。它通常不修改模型权重,也不改变模型架构,而是通过构造上下文、调用工具和组织外部资源来控制模型行为。
flowchart TB
subgraph LLMOS[LLM OS 类比]
K[模型权重 / Kernel]
API[推理接口 / 工具调用]
CW[Context Window / RAM]
end
App[LLM 应用 / Agent] --> CE[Context Engineering]
CE --> CW
CW --> API
API --> K
K --> API
API --> App
传统软件工程可以粗略分成几个阶段:
| 阶段 | 主要编程对象 | 典型产物 |
|---|---|---|
| Software 1.0 | 人写的代码 | Java、Go、Python、C++ 服务 |
| Software 2.0 | 训练得到的模型权重 | 分类模型、推荐模型、视觉模型 |
| Software 3.0 | 可被上下文控制的神经网络 | Agent、Copilot、智能工作流 |
Software 1.0 里,开发者要理解 CPU、内存、磁盘 IO、网络、线程模型,因为代码最终运行在操作系统上。Software 3.0 里,LLM 成为应用运行时的重要执行环境,开发者也要理解上下文窗口、token 预算、KV Cache、工具调用、长上下文注意力衰减等机制。
一个典型例子是 KV Cache(Key-Value Cache,键值缓存)。大模型推理时,如果多次请求拥有相同前缀,服务端可以复用前缀部分的计算结果,降低 TTFT(Time To First Token,首个 token 延迟)和输入 token 成本。Manus 的 Agent 实践中就强调:尽量保持上下文前缀稳定,让系统提示、工具定义等固定内容不要频繁变化。以 Claude Sonnet 的价格为例,缓存输入 token 和未缓存输入 token 的价格可能相差 10 倍,前者约 0.30 美元 / 百万 token,后者约 3 美元 / 百万 token。
这不是传统意义上的“优化代码循环”或“减少数据库查询”,而是面向 LLM 推理机制的优化。上下文怎么排、哪些内容放前缀、哪些内容放后缀,都会影响成本和延迟。
上下文处理不当会出现什么问题
上下文质量差时,模型不一定会直接报错,更常见的是生成看似合理但错误的结果。长上下文尤其容易出现四类问题。
| 问题 | 含义 | 典型表现 | 工程处理思路 |
|---|---|---|---|
| Context Poisoning(上下文中毒) | 错误信息进入上下文并被后续轮次继续引用 | 幻觉内容被写入记忆,后续回答持续基于错误前提 | 写入记忆前校验;给知识标来源和时间;允许回滚 |
| Context Distraction(上下文干扰) | 上下文太长或信息太杂,模型注意力被分散 | 明明模型会做,看到大量无关材料后反而答错 | 控制 token 预算;只检索必要片段;把目标放在靠近末尾的位置 |
| Context Confusion(上下文混淆) | 多余内容被模型错误使用 | 暴露了太多工具,模型选择了错误工具 | 工具按任务隔离;精简工具描述;增加调用约束 |
| Context Clash(上下文冲突) | 上下文内部存在互相矛盾的信息 | 新旧规则同时出现,模型无法判断采用哪个版本 | 明确优先级;保留版本号;在构建阶段消解冲突 |
一个客服 Agent 可以同时遇到这四类问题:
flowchart TD
A[用户询问退款规则] --> B[检索知识库]
B --> C{上下文质量}
C -->|旧政策被召回| P[Context Clash]
C -->|错误总结写入记忆| X[Context Poisoning]
C -->|召回 20 段无关材料| D[Context Distraction]
C -->|同时暴露退款、物流、营销、风控工具| F[Context Confusion]
P --> R[回答采用错误政策]
X --> R
D --> R
F --> R
这也解释了为什么“上下文越长越好”并不成立。长窗口提供了更多容量,但不会自动带来更可靠的推理。工程上要做的是把有限注意力用在最关键的信息上。
Context 由哪些内容组成
从内容类型看,LLM 应用中的上下文通常可以分成三大类:Instructions、Knowledge、Tools。
| 类型 | 包含内容 | 关键点 |
|---|---|---|
| Instructions(指令) | 系统提示、任务说明、输出格式、少样本示例、工具描述 | 决定模型应该如何行动 |
| Knowledge(知识) | RAG 文档、业务规则、事实数据、用户画像、长期记忆 | 给模型提供外部事实依据 |
| Tools(工具结果) | API 返回值、代码执行结果、搜索结果、数据库查询结果 | 注意是工具返回结果,不是工具定义;工具定义更接近指令 |
这里容易混淆的是 Memory(记忆)。LLM 本身通常是无状态的,所谓记忆是应用层保存并在后续调用中重新注入上下文的外部状态。
短期记忆和长期记忆
| 记忆类型 | 范围 | 常见实现 |
|---|---|---|
| Short-term Memory(短期记忆) | 当前会话或当前任务线程 | 历史对话、scratchpad、任务状态、最近工具调用结果 |
| Long-term Memory(长期记忆) | 跨会话、跨任务保留 | 用户画像、偏好、历史任务经验、长期知识库 |
长期记忆还可以继续拆成三类:
| 长期记忆类型 | 含义 | 示例 |
|---|---|---|
| Semantic Memory(语义记忆) | 对事实和概念的记忆 | 用户是素食者;用户常用 Java;某业务规则的定义 |
| Episodic Memory(情景记忆) | 对过去事件和经历的记忆 | 用户上次让 Agent 生成了一份周报;某次任务中哪个工具调用失败 |
| Procedural Memory(程序性记忆) | 对执行任务规则和方法的记忆 | “生成摘要必须少于 100 字”;“处理工单前先查客户等级” |
在人类身上,程序性记忆类似“会骑自行车”;在 Agent 里,它更常体现为任务提示词、工作流规则或工具调用策略的更新。生产系统里很少在运行时直接修改模型权重,更常见的做法是修改外部提示词、配置、规则和记忆库。
Context Engineering 的四类核心操作
Context Engineering 不只是把内容拼接成一个大字符串。更合理的视角是把它看成一条流水线:写入、选择、压缩、隔离。
flowchart LR
U[用户输入] --> W[Write Context<br/>写入外部状态]
W --> Store[(状态库 / 文件系统 / 向量库 / 记忆库)]
Store --> S[Select Context<br/>选择相关信息]
S --> C[Compress Context<br/>压缩与重排]
C --> I[Isolate Context<br/>按任务隔离]
I --> CW[Context Window]
CW --> L[LLM]
L --> A[回答或工具调用]
A --> W
Write Context:把信息写到窗口外
Writing context 指的是把对未来有用的信息保存到上下文窗口之外。因为 context window 有长度限制,不能把所有历史永久放在窗口里,必须有外部存储承接状态。
常见写入对象包括:
| 写入对象 | 用途 |
|---|---|
| Scratchpad | 保存当前任务中间推理、计划、临时结果 |
| State | 保存任务进度、表单状态、工作流节点 |
| Long-term Memory | 保存跨会话可复用的用户偏好、事实和经验 |
| File System | 保存大体量、结构化、可被 Agent 反复读写的任务材料 |
长期记忆写入通常不能简单地“看到什么就存什么”。更安全的方式是让模型先抽取候选记忆,再经过规则或人工确认后写入。
sequenceDiagram
participant U as 用户
participant A as Agent
participant L as LLM
participant M as 记忆库
U->>A: 我以后都用中文回答,摘要不超过100字
A->>L: 从对话中抽取可长期保存的偏好
L-->>A: 语言=中文;摘要长度<=100字
A->>A: 校验是否允许写入
A->>M: 保存为长期偏好
可以用结构化格式保存记忆,避免后续拼接时出现歧义:
memory_type: procedural
scope: summarization_task
content:
language: zh-CN
max_length_chars: 100
source: user_explicit_instruction
updated_at: 2026-06-07
对于长任务 Agent,文件系统也可以作为外部化上下文。完整网页、下载文件、代码仓库、长日志不适合全部塞进窗口,可以保存成文件,让 Agent 在需要时读取局部内容。这样比过早做不可逆摘要更安全,因为摘要一定会丢信息,而某些细节可能在十几步之后才变关键。
Select Context:从大量候选信息中选出必要部分
Selecting context 是从工具、知识库、历史、长期记忆中挑选本轮推理所需信息。RAG(Retrieval-Augmented Generation,检索增强生成)里的 Retrieval 就是典型的选择过程。
一个 RAG 选择链路通常包含这些步骤:
flowchart LR
Q[用户问题] --> QA[查询改写 / Query Augmentation]
QA --> R1[向量召回]
QA --> R2[关键词召回]
QA --> R3[图谱召回]
R1 --> Merge[合并候选]
R2 --> Merge
R3 --> Merge
Merge --> Rank[重排序]
Rank --> Budget[按 token 预算截断]
Budget --> Ctx[进入上下文]
选择阶段要同时避免两种错误:
| 错误 | 后果 |
|---|---|
| 少选 | 缺少关键事实,模型只能猜 |
| 多选 | 无关材料进入窗口,增加成本并干扰注意力 |
Agent 场景里,失败操作有时也应该保留。例如某个工具参数调用失败,保留简短错误信息可以阻止模型重复犯同样错误。关键是“保留有用失败”,而不是把完整堆栈和所有日志全塞进去。
较好的失败记录可以长这样:
已尝试:
- tool: search_order
- args: {"phone": "138****0000"}
- result: failed
- reason: phone is not a supported lookup key; use order_id or user_id instead
它告诉模型哪里错了、下一步怎么避开,但没有浪费大量 token。
Compress Context:压缩、摘要和重排
长会话和长任务会快速消耗上下文窗口。工具调用 50 次之后,即使每次只返回几百 token,也会形成很长的轨迹。压缩是必需手段,但它有损。
常见压缩策略包括:
| 策略 | 做法 | 风险 |
|---|---|---|
| 滑动窗口 | 只保留最近 N 轮 | 早期关键约束可能丢失 |
| 全局摘要 | 把历史压成一段总结 | 摘要模型可能遗漏细节 |
| 分层摘要 | 按阶段总结,再汇总阶段摘要 | 实现复杂,摘要误差会累积 |
| 结构化状态 | 把任务进展写成字段 | 需要提前设计 schema |
| 文件外置 | 大材料放文件,窗口只放索引和引用 | Agent 需要学会何时读文件 |
压缩最好由应用层控制,而不是完全交给模型服务自动处理。因为业务系统最清楚哪些字段不能丢,比如订单号、合规限制、用户明确偏好、失败工具调用原因。
还有一个实用技巧:把当前目标和待办事项反复放到上下文靠近末尾的位置。长上下文模型常有“中间迷失”问题,靠近开头和结尾的信息更容易被利用。对于长任务 Agent,可以在每轮工具调用后更新一段简短任务状态:
当前目标:生成一份不超过 100 字的中文摘要。
已完成:
- 已读取用户上传文档
- 已提取 3 个关键观点
待办:
- 合并重复观点
- 输出最终摘要
约束:
- 不超过 100 字
- 不添加文档外信息
这相当于用自然语言调节模型注意力,让模型持续对齐任务目标。
Isolate Context:上下文隔离
Context isolation 指的是让不同任务、不同子 Agent、不同工具只看到必要信息。它和软件工程中的信息隐藏、最小权限原则很像。
多 Agent 系统尤其需要隔离。一个负责写代码的 Agent 不一定需要看到用户的支付信息;一个负责查订单的 Agent 也不应该看到代码仓库的全部内容。
| 场景 | 隔离方式 | 目的 |
|---|---|---|
| 多 Agent 协作 | 每个 Agent 独立工具集和上下文窗口 | 避免互相干扰 |
| 敏感数据处理 | 只暴露脱敏字段 | 降低隐私泄露风险 |
| 工具调用 | 按任务开放工具,不一次性暴露全部工具 | 减少工具误选 |
| 长任务拆分 | 每个阶段只保留阶段目标和必要状态 | 降低上下文复杂度 |
工具越多,模型选择错误工具的概率越高。隔离不是为了“藏信息”,而是为了让模型在更小、更明确的决策空间里行动。
面向 LLM Context 的工程实践
Context Engineering 最终要落到工程实现。一个最小可用的上下文构建器,可以按这样的思路组织:
def build_context(user_id: str, session_id: str, user_input: str) -> list[dict]:
# 1. 固定前缀:尽量稳定,便于 KV Cache 复用
system_prompt = load_stable_system_prompt()
tool_specs = load_stable_tool_specs(task="customer_support")
# 2. 短期状态:当前会话、任务进度、最近失败
recent_history = load_recent_messages(session_id, limit=8)
task_state = load_task_state(session_id)
recent_failures = load_recent_tool_failures(session_id, limit=3)
# 3. 长期记忆:只选择与本轮任务相关的记忆
memories = retrieve_user_memories(user_id, query=user_input, top_k=5)
# 4. 外部知识:RAG 检索 + 重排序 + token 预算控制
docs = retrieve_documents(query=user_input, top_k=20)
docs = rerank(user_input, docs)[:5]
# 5. 组装上下文:目标和约束放在靠近末尾的位置
return [
{"role": "system", "content": system_prompt},
{"role": "system", "content": format_tools(tool_specs)},
{"role": "system", "content": format_memories(memories)},
{"role": "system", "content": format_docs(docs)},
*recent_history,
{"role": "system", "content": format_task_state(task_state, recent_failures)},
{"role": "user", "content": user_input},
]
这个伪代码里有几个关键点:
- 固定内容放前面,并保持稳定,减少 KV Cache 失效;
- 动态内容按相关性选择,不把所有历史和所有记忆都塞进去;
- RAG 结果经过重排序和 token 预算裁剪;
- 当前目标、约束、任务状态靠近末尾,降低长上下文遗忘;
- 工具定义按任务加载,不给模型暴露无关工具。
让上下文前缀稳定
很多 LLM 服务会把工具定义、系统提示序列化到上下文前部。如果每轮都改变这些内容,后续 token 的 KV Cache 也会失效。
实践中可以这样做:
| 做法 | 作用 |
|---|---|
| 系统提示固定化 | 避免每轮生成随机前缀 |
| 工具定义顺序稳定 | 保持序列化结果一致 |
| 动态时间、请求 ID 放到后部 | 防止前缀因小字段变化失效 |
| 不需要的工具用 mask,而不是删除定义 | 减少缓存失效,也避免历史工具调用引用不到定义 |
| 工具 schema 使用确定性序列化 | 避免 JSON 字段顺序变化 |
“Mask, Don't Remove”在 Agent 里很重要。历史轨迹里可能已经出现某个工具调用,如果后续上下文里完全删除工具定义,模型会看到过去调用过但当前不存在的工具,容易产生混淆。更稳的做法是保留稳定工具定义,通过运行时约束禁用某些工具。
少样本示例要避免模式锁死
少样本示例可以帮助模型理解格式,但示例过于单一时,模型会模仿上下文里的固定模式,即使当前任务已经不适合那种模式。
例如上下文里连续出现十次“先搜索再总结”的 action-observation 轨迹,模型可能在不需要搜索时仍然调用搜索工具。这属于 Context Confusion 的一种表现。
处理方式不是完全不用示例,而是控制示例数量和多样性:
| 问题 | 调整方式 |
|---|---|
| 示例过多 | 只保留与当前任务最相关的 1~3 个 |
| 示例格式过于固定 | 引入不同任务类型的示例 |
| 历史轨迹诱导错误动作 | 对历史动作做摘要,只保留结果和约束 |
| 模型重复失败动作 | 保留失败原因,明确禁止重复 |
为 LLM 设计数据结构
传统 API 主要面向人类开发者和程序调用方,LLM 时代还要考虑“模型是否容易理解”。MCP(Model Context Protocol,模型上下文协议)的意义不只是统一工具调用格式,也在于让工具以模型可理解的方式暴露能力。
一个不友好的工具描述可能长这样:
{
"name": "exec",
"description": "execute operation",
"parameters": {
"type": "object",
"properties": {
"x": {"type": "string"}
}
}
}
模型不知道这个工具适合什么任务,也不知道 x 应该填什么。更好的描述要说明使用场景、参数含义和限制:
{
"name": "get_order_status",
"description": "根据订单 ID 查询订单当前状态。只用于用户询问物流、支付、退款进度等订单相关问题。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单 ID,通常由 16 到 20 位数字组成"
}
},
"required": ["order_id"]
}
}
RAG 文档也一样。面向 LLM 的知识库不应该只有 PDF 截图或杂乱 HTML。更适合模型处理的格式通常具备这些特点:
| 文档特征 | 原因 |
|---|---|
| Markdown 标题层级清晰 | 方便切片和语义定位 |
| 表格结构规范 | 便于模型比较字段 |
| 每段有来源和更新时间 | 方便处理版本冲突 |
| 规则写成显式条件 | 减少模型自行推断 |
长页面提供 llms.txt 或 Markdown 入口 | 方便 Agent 抓取和导航 |
网站过去主要面向人类浏览器和搜索引擎优化,LLM Agent 会让“面向模型可读”变成新的工程要求。一个购物网站、文档站或 SaaS 平台,未来可能同时服务人类用户和其他 Agent。数据结构、工具接口、文档格式都需要考虑模型消费。
什么时候不能只靠 Context Engineering
Context Engineering 能显著改善 LLM 应用行为,但它不是万能补丁。
| 情况 | 更合适的处理方式 |
|---|---|
| 模型基础能力不足 | 换更强模型、微调、训练专用模型 |
| 任务要求强确定性 | 用传统代码、规则引擎、数据库约束 |
| 知识库质量很差 | 先治理数据源,再做 RAG |
| 上下文存在大量冲突 | 建立版本、优先级和审核机制 |
| 成本对 token 极敏感 | 缩短链路、缓存结果、拆小模型任务 |
| 涉及安全和隐私边界 | 做权限控制、脱敏和审计,不能只靠提示词约束 |
如果业务规则能用代码明确表达,就不要强行交给模型推断。LLM 适合处理开放表达、模糊理解、多步骤规划和自然语言交互;确定性校验、金额计算、权限判断仍然应该放在可靠的软件系统里。
一套可落地的 Context Engineering 检查清单
构建 LLM 应用时,可以按下面的清单检查上下文设计:
| 检查项 | 需要确认的问题 |
|---|---|
| 上下文来源 | 指令、历史、知识、工具、记忆分别来自哪里 |
| 写入策略 | 什么信息能写入长期记忆,谁来确认,如何删除 |
| 选择策略 | RAG、历史、记忆按什么规则进入窗口 |
| 压缩策略 | 哪些信息可摘要,哪些字段不能丢 |
| 隔离策略 | 不同 Agent、工具、任务是否只看到必要信息 |
| 位置策略 | 稳定前缀、动态内容、当前目标分别放在哪里 |
| 缓存策略 | 前缀是否稳定,工具定义是否频繁变化 |
| 冲突处理 | 新旧规则、不同来源信息冲突时谁优先 |
| 可观测性 | 是否记录最终上下文、token 数、召回文档、工具调用和错误 |
| 评测方式 | 是否用真实任务集评估回答质量、工具成功率和成本 |
Context Engineering 的本质,是把“给模型什么输入”从临时手工技巧变成可设计、可评估、可调试的工程系统。Prompt 仍然重要,但只写好 Prompt 已经不够。真正可用的 LLM 应用,需要围绕上下文窗口管理知识、状态、工具、记忆和成本。
参考资料:
- A Survey of Context Engineering for Large Language Models:https://arxiv.org/abs/2507.13334
- Context Engineering Guide:https://docs.google.com/document/u/0/d/1JU8w-E4LlseFZm-ag22GSBU5A2rp2nb7iFGBNAbFL7k/mobilebasic
- Context Engineering for Agents:https://rlancemartin.github.io/2025/06/23/context_engineering/
- Context Engineering for AI Agents: Lessons from Building Manus:https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus
- How Long Contexts Fail:https://www.dbreunig.com/2025/06/22/how-contexts-fail-and-how-to-fix-them.html
- 12 Factor Agents:https://github.com/humanlayer/12-factor-agents