课程0基础Agent开发课 / Agent基础 / 上下文工程-Context-Engineering比Prompt工程更重要
— 14 min read

上下文工程-Context-Engineering比Prompt工程更重要

*Context Engineering 全景图 — Context Window 的六大组成部分及其管理策略*

上下文工程:Context Engineering 比 Prompt 工程更重要

1.1 为什么 Prompt 工程不够用:LLM 的决策依据是什么

Context Engineering 全景图
Context Engineering 全景图 — Context Window 的六大组成部分及其管理策略

在理解上下文工程之前,需要先想清楚 LLM 是基于什么来生成输出的。

LLM 唯一能依赖的,是它在推理时刻能"看到"的所有信息——也就是上下文窗口(Context Window)里的全部内容。模型没有什么神奇的"内部理解"能力,它的每一个输出 token,都是基于当前 context 里的信息通过概率计算得出的。

这个认识非常重要:如果 context 里的信息不够好——关键信息缺失、重要信息被埋在中间被忽略、工具描述模糊导致模型选错工具——不管 System Prompt 写得多漂亮,输出质量都会差。

Prompt Engineering 的局限在哪里

Prompt Engineering 把注意力集中在 System Prompt 的措辞和结构上:如何表达角色、如何设定规则、如何引导思维链。这在单次对话的场景下效果显著。

但一个运行中的 Agent,它的 context 里有六种东西:System Prompt 只是其中一种,而且通常是 token 占比最少的那种。工具定义、对话历史、RAG 检索结果、工具执行结果——这些内容加起来可能占 context 的 80%。把全部精力放在 20% 的内容上,优化空间天然有限。

上下文工程的本质

上下文工程(Context Engineering)是一个更宏观的概念:系统性地设计和管理"什么信息、以什么格式、出现在 context 的什么位置",使得模型在推理时刻能得到最高质量的决策依据。

它包含 Prompt Engineering,但远不止于此。一个掌握上下文工程的工程师,关注的不是某句话怎么措辞,而是整个 context 的信息架构:哪些内容是噪音需要过滤、哪些内容需要放在最显眼的位置、如何在有限窗口里最大化信息价值。


本章介绍 Context Engineering 的概念、核心挑战,以及在 Agent 工程化过程中的四个关键实践。

1.2 Prompt Engineering 是 Context Engineering 的子集

很多人把 Prompt Engineering 当成 Agent 开发的核心技能,觉得把 System Prompt 写得足够好就够了。这个认知是不完整的。

Prompt 只是 context window(上下文窗口,LLM 每次推理时能"看到"的最大文本量,以 token 为单位;token 是模型处理文本的基本单元,大约每个汉字或英文单词算 1-2 个 token)里的一部分内容。一个 Agent 的 context 通常包含:

  • System Prompt(角色定义和规则)
  • 对话历史
  • 工具定义
  • RAG 检索到的文档
  • 工具执行结果
  • 用户当前输入

Prompt Engineering 主要优化第一项。但一个运行中的 Agent,context 里绝大多数 token 都是后几项。花了大力气优化 System Prompt,但如果对话历史太长挤掉了关键信息,或者工具描述写得模糊让模型调错工具,效果依然会很差。

Context Engineering 的定义:设计和管理"什么信息在什么时候以什么格式出现在 context window 里"的工程实践。

两个 Agent 使用同一个模型,使用效果差异悬殊的根本原因,往往不在 Prompt 的措辞有多精妙,而在于对整个 context 的设计是否清晰、合理。

1.3 Context 的完整构成

Context Window

System Prompt
角色 + 规则 + 约束

对话历史
多少轮?是否压缩?

工具定义
名称 + 描述 + 参数

RAG 检索结果
相关文档片段

工具执行结果
Observation

用户当前输入

影响模型行为基调

影响上下文连贯性

影响工具选择准确性

影响知识准确性

影响下一步决策

触发当前推理

这六个部分,每一个都在争夺有限的 context window 空间,都在影响模型的输出质量。

1.4 核心挑战:Lost in the Middle

2023 年 Stanford 的研究《Lost in the Middle》发现了一个重要现象:当相关信息被放在 context 的中间位置时,模型对它的关注度显著下降。

这张图示意性地展示了模型注意力和信息位置的关系:

code
注意力
  ^
  |  *                               *
  |    *                           *
  |      *                       *
  |        *                   *
  |          * * * * * * * * *
  +-----------------------------------> 位置
  开头                              结尾

开头和结尾的信息被模型"记住"得更好,中间的内容容易被忽略。

这个发现直接影响了 Context Engineering 的很多实践决策。

1.5 实践一:System Prompt 的设计

System Prompt 是整个 context 的"基调设定",它出现在最前面,权重最高。一个常见的错误是把所有规则塞进去,结果又臭又长,模型根本没法全部遵守。

python
# 差的写法:所有内容堆在一起,没有优先级
bad_system_prompt = """
你是一个代码审查助手。你需要检查代码质量、安全问题、性能问题、风格问题、
文档完整性、测试覆盖率、依赖管理、版本兼容性、错误处理、日志记录、
数据库查询优化、缓存使用、API 设计、并发安全、内存管理...(后面还有200行)
"""

# 好的写法:层次清晰,优先级明确
good_system_prompt = """
你是 Acme 公司的代码审查专家。

## 核心职责
审查 Python 代码,重点关注三个维度:安全性、可维护性、性能。

## 审查优先级
1. 高危(必须修改):SQL 注入、敏感数据明文、未验证输入
2. 中危(建议修改):函数超过 50 行、缺少错误处理、重复代码
3. 低危(可选优化):变量命名、注释质量

## 输出格式
按优先级分组,每条问题包含:位置、问题描述、修改建议。

## 工具使用原则
- 需要查看文件时,用 read_file
- 需要搜索代码模式时,用 search_code
- 不确定语义时,先读代码再判断,不要猜测
"""

关键原则:核心规则放前面,优先级要明确,工具使用原则要具体。

1.6 实践二:工具描述的质量决定调用准确率

工具描述是决定模型能否正确调用工具的关键因素。模糊的工具描述会直接导致工具调用错误率上升。

python
# 差的工具描述:模糊,没有边界
bad_tool = {
    "name": "search",
    "description": "搜索信息",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {"type": "string"}
        }
    }
}

模型看到这个描述,完全不知道"搜索信息"是什么意思,不知道什么时候该用,不知道怎么构造 query。

python
# 好的工具描述:具体、有边界、有示例
good_tool = {
    "name": "search_codebase",
    "description": """在项目代码库中搜索代码模式或文本。

    适用场景:
    - 查找某个函数或类的所有调用点
    - 搜索特定的代码模式(如未处理的异常)
    - 查找某个变量或常量的定义

    不适用场景:
    - 不要用于读取文件内容(用 read_file)
    - 不要用于搜索互联网(用 web_search)

    示例:
    - 查找所有 SQL 拼接:query="cursor.execute" + "%" 或 query="f'SELECT"
    - 查找函数定义:query="def process_payment"
    """,
    "parameters": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "搜索关键词或正则表达式,支持精确匹配和模糊搜索"
            },
            "file_pattern": {
                "type": "string",
                "description": "限定搜索范围,如 '*.py' 或 'src/payment/**'"
            }
        },
        "required": ["query"]
    }
}

工具描述的核心原则

  1. 说清楚"什么时候用"和"什么时候不用"
  2. 给出具体的使用示例
  3. 参数描述要包含格式要求和边界条件

1.7 实践三:对话历史的管理策略

对话历史是 context 里增长最快的部分。一次复杂任务可能有几十轮工具调用,每轮都往 context 里加内容。不加管理,很快就会撑爆 context window。

有三种常见策略:

python
from openai import OpenAI
from typing import List, Dict

client = OpenAI()

# 策略一:截断(简单粗暴,丢失信息)
def truncate_history(messages: List[Dict], max_messages: int = 20) -> List[Dict]:
    """只保留最近 N 轮对话"""
    system_messages = [m for m in messages if m["role"] == "system"]
    non_system = [m for m in messages if m["role"] != "system"]

    if len(non_system) > max_messages:
        non_system = non_system[-max_messages:]

    return system_messages + non_system

# 策略二:滑动窗口(保留首尾,压缩中间)
def sliding_window_history(
    messages: List[Dict],
    keep_first: int = 3,
    keep_last: int = 10,
    max_total: int = 20
) -> List[Dict]:
    """保留开头的关键上下文和最近的对话"""
    system_messages = [m for m in messages if m["role"] == "system"]
    non_system = [m for m in messages if m["role"] != "system"]

    if len(non_system) <= max_total:
        return system_messages + non_system

    # 保留开头(任务背景)和结尾(最近上下文)
    preserved = non_system[:keep_first] + non_system[-keep_last:]
    return system_messages + preserved

# 策略三:摘要压缩(保留信息,减少 token)
def summarize_history(messages: List[Dict], keep_recent: int = 5) -> List[Dict]:
    """将较早的对话历史压缩成摘要"""
    system_messages = [m for m in messages if m["role"] == "system"]
    non_system = [m for m in messages if m["role"] != "system"]

    if len(non_system) <= keep_recent + 5:
        return system_messages + non_system

    # 需要压缩的部分
    to_summarize = non_system[:-keep_recent]
    recent = non_system[-keep_recent:]

    # 用 LLM 生成摘要
    summary_response = client.chat.completions.create(
        model="gpt-4o-mini",  # 用小模型降低成本
        messages=[
            {
                "role": "system",
                "content": "将以下对话历史压缩成简洁摘要,保留关键决策和重要发现。"
            },
            {
                "role": "user",
                "content": str(to_summarize)
            }
        ]
    )
    summary = summary_response.choices[0].message.content

    # 用摘要替换原始历史
    summary_message = {
        "role": "system",
        "content": f"[历史对话摘要]:{summary}"
    }

    return system_messages + [summary_message] + recent

三种策略的选择依据:

  • 截断:适合对连续性要求不高的简单任务
  • 滑动窗口:适合需要保留任务起点上下文的场景
  • 摘要压缩:适合长时间运行的复杂 Agent,保留信息量最大,但有额外的模型调用成本

1.8 实践四:RAG 结果的质量控制

RAG 检索到的文档会大量占用 context。但检索到的内容不一定都有用,质量差的 RAG 结果反而会干扰模型。

python
def prepare_rag_context(query: str, documents: list, max_chars: int = 3000) -> str:
    """准备 RAG 上下文,控制质量和长度"""
    if not documents:
        return ""

    # 过滤低相关性文档(假设 documents 包含相关性分数)
    relevant_docs = [
        doc for doc in documents
        if doc.get("relevance_score", 0) >= 0.6
    ]

    if not relevant_docs:
        return ""

    # 按相关性排序,最相关的放最前面(利用 Lost in the Middle 效应)
    relevant_docs.sort(key=lambda x: x.get("relevance_score", 0), reverse=True)

    context_parts = []
    total_chars = 0

    for doc in relevant_docs:
        content = doc["content"]
        source = doc.get("source", "未知来源")

        # 控制总长度,重要的放前面,不重要的截断
        if total_chars + len(content) > max_chars:
            remaining = max_chars - total_chars
            if remaining > 200:  # 至少 200 字才加入
                content = content[:remaining] + "..."
            else:
                break

        context_parts.append(f"[{source}]\n{content}")
        total_chars += len(content)

    return "\n\n---\n\n".join(context_parts)

1.9 把所有实践整合起来

python
def build_agent_context(
    user_input: str,
    conversation_history: list,
    retrieved_docs: list,
    tool_results: list
) -> list:
    """构建完整的 Agent context"""

    # 1. System Prompt 放最前面(高权重位置)
    system_prompt = good_system_prompt

    # 2. 管理对话历史(控制长度)
    managed_history = sliding_window_history(
        conversation_history,
        keep_first=2,
        keep_last=8
    )

    # 3. 准备 RAG 上下文
    rag_context = prepare_rag_context(user_input, retrieved_docs)

    # 4. 构建 messages
    messages = [{"role": "system", "content": system_prompt}]

    # 5. 如果有 RAG 结果,在历史之前注入(靠近开头,提高关注度)
    if rag_context:
        messages.append({
            "role": "system",
            "content": f"相关参考资料:\n{rag_context}"
        })

    # 6. 加入对话历史
    messages.extend(managed_history)

    # 7. 用户当前输入放最后(高权重位置)
    messages.append({"role": "user", "content": user_input})

    return messages

注意信息的位置安排:System Prompt 在最前,RAG 结果紧随其后,用户输入在最后。重要信息不放中间。

1.10 Context Engineering 是 Agent 工程化的核心能力

高质量 Agent 的构建,通常需要在以下几件事上花费大量精力:

  • 把工具描述打磨到能准确引导模型选择正确工具
  • 设计对话历史的滑动窗口策略,确保关键任务背景不被挤掉
  • 控制 RAG 结果的质量和位置,最相关的文档放最前面
  • System Prompt 保持简洁,核心规则清晰,不堆砌

这些都不是 Prompt Engineering。这是对整个 context 的系统性设计。

一个 Agent 的输出质量,取决于模型在生成回答那一刻,它的 context window 里有什么,位置怎么排,格式是否清晰。Context Engineering 就是工程化地解决这个问题。

Prompt 工程是起点,Context Engineering 是终点。当 Agent 从 demo 走向生产,会发现大部分真实问题都出在 context 的设计上,而不是 Prompt 的措辞上。

本页目录