芥末
发布于 2025-09-04 / 0 阅读
0
0

从 Prompt Engineering 到 Context Engineering:LLM 应用的上下文工程方法

早期讨论 Prompt Engineering 时,有一种常见判断:随着大语言模型能力增强,提示词技巧会逐渐不重要。这个判断看起来很合理,搜索引擎也经历过类似过程。早期用户需要记住通配符、特殊操作符、linkdefine 等检索语法,后来搜索引擎变聪明了,普通自然语言也能得到不错的结果。

但大语言模型(Large Language Model,LLM)的应用开发没有沿着这条路完全前进。提示词没有消失,反而扩展成了一个更大的工程问题:Context Engineering,也就是上下文工程。

Prompt Engineering 关注“怎么问”;Context Engineering 关注“模型在下一步推理前应该看到什么”。这两者不是并列关系,而是包含关系:提示词只是上下文的一部分,真正决定模型行为的,是整个 context window 中的指令、历史、知识、工具描述、工具返回结果、状态和记忆。

Context Engineering 解决什么问题

一个 LLM 调用可以粗略看成一个无状态函数:

output = LLM(context)

模型权重在一次推理过程中不会改变。它能用来生成答案的信息,主要来自两个地方:

  1. 训练阶段已经写入参数的知识和能力;
  2. 推理时放进上下文窗口的内容。

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 EngineeringContext 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},
    ]

这个伪代码里有几个关键点:

  1. 固定内容放前面,并保持稳定,减少 KV Cache 失效;
  2. 动态内容按相关性选择,不把所有历史和所有记忆都塞进去;
  3. RAG 结果经过重排序和 token 预算裁剪;
  4. 当前目标、约束、任务状态靠近末尾,降低长上下文遗忘;
  5. 工具定义按任务加载,不给模型暴露无关工具。

让上下文前缀稳定

很多 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 应用,需要围绕上下文窗口管理知识、状态、工具、记忆和成本。

参考资料:


评论