提示词工程与上下文工程
这里是第五章「怎么做得可靠」的第一章。前四章解决了"能干活":模型接上了工具、跑起了自主循环、长出了文档能力、说起了通用协议。但把一个 Agent 从"能跑"带到"可靠",第一站不在模型、也不在工具,而在一个容易被忽视的…
这里是第五章「怎么做得可靠」的第一章。前四章解决了"能干活":模型接上了工具、跑起了自主循环、长出了文档能力、说起了通用协议。但把一个 Agent 从"能跑"带到"可靠",第一站不在模型、也不在工具,而在一个容易被忽视的地方——上下文窗口。进入模型窗口的每个 token 都是设计对象:系统提示词、会话历史、工具定义、工具结果、检索内容、长期记忆,什么进、进多少、放哪里、留多久,每一项都是显式的工程决策。本章按"从部分到整体"的顺序展开:前半章打磨提示词这个组分——它是成本最低的杠杆,五个已验证的模式、一张反模式清单、一套工程化管理方法;后半章升级到上下文这个整体——建立七组分的总账本,理解注意力经济学,掌握预算分配与压缩策略。你会看到前沿从业者口中的关键词正在从 Prompt Engineering 换成 Context Engineering,这一章把两者一次讲透。
1. 先祛魅:提示词的边界
提示词工程被两种极端声音夹击:"会写提示词就能年薪百万"和"提示词工程已死"。真相在中间:它是每个 Agent 开发者的基本功,但重要性远不如后半章的上下文工程——因为提示词只是上下文的一部分。这一部分给出可复制的工程方法,而非玄学话术。
第 5 章讲过系统提示词的约束力来自后训练的统计倾向。由此推出提示词工程的三个边界认知:
- 它调的是概率分布,不是逻辑电路。同一提示词跑两次结果可能不同(第 4 章采样随机性)
- 上限被模型能力锁定。提示词无法让 4B 模型做出 70B 的推理深度——它是"发挥模型现有的能力",不是"创造新能力"
- 它是最便宜的杠杆。改一行文字就能试验,迭代成本秒级——这是为什么值得先优化提示词再动其他层(第 9 章调试心法的"行为层")
所以提示词工程的正确定位:用最低成本逼近模型当前能力上限的方法论。
2. 五个已被验证的模式
2.1 角色设定(Persona)
你是一位资深数据库管理员,专精 PostgreSQL 查询优化。
你习惯先确认表结构和数据量,再给出索引建议。
有效的原理:角色词汇关联着训练语料中的专业语境(DBA 的讨论风格、术语密度),设定角色 = 把模型拉进高相关专业文本的分布区域。要点:角色的价值在"行为暗示"而非头衔——"习惯先确认表结构"比"资深专家"有用。
2.2 Few-shot 示例(少样本学习)
第 4 章说过:模型对示例的模仿力远超对形容词的理解力。这是最强力的模式:
将用户评论分类为:正面 / 负面 / 中性。
评论:物流很快,包装也仔细
输出:正面
评论:东西还行,就是客服半天不回
输出:中性
评论:用了一周就坏了,退货还各种扯皮
输出:负面
评论:{user_input}
输出:
三个工程要点:
- 示例要覆盖边界情况(上面第三条带了点混合情绪,教模型判断主导倾向),只给简单正例学不到判别力
- 示例的格式就是输出格式——模型会精确模仿包括空格和标点的模式
- 示例数量 2~5 个通常饱和,堆 20 个不如挑 3 个有代表性的
2.3 思维链(CoT)
第 4 章详细讲过原理(生成即上下文,中间结论做脚手架)。工程上的两种用法:
# 显式触发(提示词版)
请一步步分析,先列出已知条件和约束,再推导结论。
# 结构化触发(更强:把思考过程格式化)
请按以下步骤处理:
1. 复述任务目标
2. 列出所需信息与已有信息
3. 给出推理过程
4. 输出最终答案(以 "答案:" 开头)
结构化版本的好处:第 4 步给了稳定的输出锚点,方便下游解析(第 6 章的接口思维)。另注意:简单任务不要加 CoT——"今天星期几"式的直接问答,加"一步步思考"反而增加啰嗦和出错面。
2.4 结构化模板(XML/Markdown 分区)
## 任务
{任务描述}
## 输入
{数据}
## 约束
- {约束1}
- {约束2}
## 输出格式
{格式说明}
原理:分区标签帮助模型(和读代码的你)区分指令、数据、约束的边界——结构化本身就是一种提示。对长提示词的效果显著(第 4 章的中段遗忘问题也靠分区缓解)。
2.5 分而治之(任务分解)
第 6 章埋过的原则:别让一次调用做所有事。
❌ "分析这篇文章的情感、摘要、关键词、可读性评分"
✅ 四次调用,每次一个明确任务,各自校验各自的输出
每加一个任务,其他任务的输出质量都在掉(注意力被稀释)。多任务合并省下的是调用费,赔上的是质量——这笔账多数情况不划算。
3. 反模式清单
比"该做什么"更有教育意义的是"常见的错":
| 反模式 | 症状 | 修正 |
|---|---|---|
| 形容词堆砌 | "要专业、简洁、生动、深入浅出" | 给示例(2.2),形容词无锚点 |
| 负面指令过多 | "不要啰嗦不要跑题不要编造" | 换成正向指令(第 5 章:肯定句是强约束) |
| 威胁模型 | "答错你会被惩罚" | 无统计依据;改为展示"好答案"长什么样 |
| 过度礼貌客套 | "拜托了,非常感谢" | 无害但浪费 token;指令清晰度不升反降 |
| 万能长提示词 | 一个 prompt 服务 20 个场景 | 分场景拆分(职责越少越可靠) |
| 隐藏关键约束在中间 | 长文中间一句话带过要求 | 首尾放置(第 4 章位置敏感) |
4. 提示词的工程化管理
提示词是代码——就该用代码的方式管理。
4.1 模板分离(第 5 章的进阶版)
from jinja2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined) # 变量缺失时报错而非静默空串
CLASSIFY_PROMPT = env.from_string("""
将用户评论分类为:正面 / 负面 / 中性。
评论:物流很快,包装也仔细
输出:正面
评论:{{ example_neg }}
输出:负面
评论:{{ user_input }}
输出:
""")
def classify(user_input: str, example_neg: str) -> str:
prompt = CLASSIFY_PROMPT.render(
user_input=user_input, example_neg=example_neg)
... # 调用模型
StrictUndefined 是血泪经验:默认配置下变量拼错会静默渲染成空串,提示词悄悄坏掉而你毫无察觉。
4.2 版本化与评测
提示词改了怎么知道变好还是变坏?——没有评测集的提示词优化是玄学(第 15 章的预告):
prompts/
├── classify_v1.py
├── classify_v2.py # 改动记录在 docstring
└── evalset.jsonl # 20~50 条标注样本
最低可行流程:固定一组测试输入 + 期望输出,每次改提示词跑一遍对比准确率。20 个样本就能拦住大部分"负优化"。
5. 站位:提示词技巧的黄昏
一个值得诚实讨论的趋势:提示词技巧的半衰期越来越短。
- 第 4 章讲过:指令遵循能力随模型代际提升,老技巧在贬值("让我们一步步思考"在强模型上已无明显增益——它已经默认会思考)
- 结构化输出(第 6 章)接管了格式约束——曾经的"输出 JSON 的 12 种咒语"已无必要
- 提示词正在从"面向模型的咒语"演变为"面向系统的配置"——角色定义、边界声明、工具说明,本质是系统设计
但三个东西不会贬值:清晰的任务定义、好的示例、以及知道什么时候不该用提示词解决(该用工具的用工具、该用代码的用代码、该改模型的换模型)。
本章后半的 Context Engineering 就是这个演进的方向:提示词只是上下文的一个组分,真正的设计对象是进入模型窗口的一切——提示词工程的主场正在让位,上下文才是真正的战场,窗口里每一个 token 的进出,都是设计决策。
6. 视角升级:上下文的总账本
2024 年后,前沿从业者的共识悄悄换了词:从 Prompt Engineering 到 Context Engineering。因为提示词只是冰山一角——真正决定 Agent 表现的,是进入上下文窗口的一切:系统提示词、历史、工具定义、工具结果、检索内容、记忆。本章后半就讲怎么系统地设计这条"总装流水线"。
先建立总账本。Agent 每轮请求时,上下文窗口的构成:
┌─────────────────────── 上下文窗口(如 32K)───────────────────────┐
│ │
│ ① 系统提示词(常驻) ~500-2000 tok │
│ ② 工具定义 schema(常驻) ~100-200 tok × 工具数 │
│ ③ Skill 索引(常驻) ~10 tok × 技能数 │
│ ④ 长期记忆注入(每轮变化) ~200-500 tok │
│ ⑤ 会话历史(逐轮增长) 无上限 ← 最危险的增长项 │
│ ⑥ 工具结果(随轮次累积) 单次可达数万 tok ← 第二危险 │
│ ⑦ 当前输入 + 生成空间 动态 │
│ │
└────────────────────────────────────────────────────────────────────┘
Context Engineering 的定义就一句话:对上述每一项,有意识地决定"放什么、放多少、放多久"。前面的章节都在做局部优化(第 6 章裁历史、第 7 章控工具数、第 10 章省 skill 索引、第 15 章控记忆注入)——本章把它们装进一个统一框架。
7. 注意力经济学:两条核心原则
7.1 每个 token 都在付费——以"注意力学"计价
第 4 章讲过 lost in the middle:上下文越长,中段信息被忽略的概率越高。推论:
窗口里每个 token 不只花你的钱,还在稀释其他所有 token 的注意力。
这是 Context Engineering 的第一性原理。它推翻"上下文越多越好"的直觉:塞进去的无关内容不是"无害的冗余",是主动的干扰。有个被广泛引用的比喻:上下文不是硬盘(存得越多越好),是 CPU 的寄存器——每个位置都该放当前要用的东西。
7.2 结构决定命运——位置敏感的排版
第 4 章的位置规律升格为设计规则:
窗口开头(高注意力)→ 身份、目标、核心约束
窗口结尾(高注意力)→ 当前任务、最新指令、格式要求
窗口中间(低注意力)→ 详细背景、参考材料、历史细节
实操含义:系统提示词放头、当前用户输入和"格式要求"放尾,中间塞"软性内容"。这也是为什么第 6 章的摘要压缩块放在 system 之后历史之前——摘要降权到中段,最新的原文交互留在尾部。
8. 各组分的经营策略汇总
把散落各章的策略收进统一账本(含本章新增的细化):
| 组分 | 策略 | 章节 |
|---|---|---|
| 系统提示词 | 精简;首尾放关键规则;运行时变量放尾部 user 消息(保缓存) | 第 5 章 |
| 工具定义 | 按职责装配 5~8 个;schema 描述精炼 | 第 7 章 |
| Skill 索引 | 一行索引常驻;正文按需加载 | 第 10 章 |
| 长期记忆 | top-k 限定 + 相似度阈值 + "以最新为准"声明 | 第 15 章 |
| 会话历史 | 三级裁剪:截断 → 摘要压缩 → 检索外挂 | 第 6 章 |
| 工具结果 | 写入前截断(而非事后裁剪)——见下文 | 本章 |
| 检索文档 | 分块 + 相关性排序 + 两端放置 | 第 1、9 章 |
重点展开工具结果——Agent 上下文管理的最大盲区:
9. 工具结果:最大的隐形杀手
第 8 章提过"工具结果永久留在 messages 里"。算一笔具体的账:
Agent 跑一个 8 轮任务,每轮调用 read_webpage(3000 字符 ≈ 1500 tok)
→ 第 8 轮请求时,上下文里有 8 × 1500 = 12000 tok 的历史工具结果
→ 其中第 1~5 轮的结果大概率已无用处(占 7500 tok)
→ 它们不只是占空间:还在稀释模型对当前任务相关内容的注意力
三种对策,按彻底程度排序:
对策一:入口截断(第 7 章原则的重申)——工具返回前就压到最小必要信息。
对策二:结果替换——历史轮次的工具结果,只留摘要:
def compact_tool_results(messages: list, keep_last: int = 2) -> list:
"""把旧轮次的工具结果替换为占位摘要"""
tool_result_positions = [
i for i, m in enumerate(messages)
if m.get("role") == "tool"
]
for i in tool_result_positions[:-keep_last] if keep_last else []:
original = messages[i]["content"]
messages[i] = {
**messages[i],
"content": f"(历史工具结果,已省略 {len(original)} 字符)",
}
return messages
注意边界:必须保留成对结构(第 6 章的裁剪边界),只改 content 不删消息,tool_call_id 配对不破。信息论的权衡:旧工具结果以"被压缩"的形式存在(可追溯),注意力被释放给当前任务。
对策三:外置存储 + 按需引用(终极方案)——工具结果不进 messages,存外部(文件/向量库),上下文里只放"句柄":
tool result → 存为 artifact_001.txt,messages 里只有:
"结果已存储。需要时可调用 read_file('artifact_001.txt') 查看细节"
模型需要时会主动去取。这是"寄存器 vs 硬盘"的彻底贯彻,代价是多一跳工具调用。生产 Agent(如 Claude Code 的长会话)都在往这个方向演进。
10. 预算分配:一个实操框架
给一个可以直接抄的分配模板(以 32K 窗口、长任务 Agent 为例):
class ContextBudget:
"""上下文预算表:各组分的配额与实时水位"""
def __init__(self, window: int = 32768):
self.window = window
# 配额(留 20% 给生成空间)
usable = int(window * 0.8)
self.quota = {
"system": 1500, # 系统提示词
"tools": 800, # 4 个工具 × ~200
"memory": 400, # 记忆注入
"history": usable - 2700, # 弹性大头
"generation": int(window * 0.2),
}
def check(self, current_usage: dict) -> list[str]:
"""返回超支组件清单(第 6 章估算函数的水位应用)"""
violations = []
for key, used in current_usage.items():
if used > self.quota.get(key, float("inf")):
violations.append(key)
return violations
超支时的处置动作(对应第 6 章的三级策略):history 超了先压缩、再截断;memory 超了降 top-k;tools 超了是架构问题(该拆 Agent 或上 Skill)。
预算思维的价值不在数字精确,在于"每组分有水位警报"——没有预算表的 Agent,第一次知道上下文出问题的时候就是任务失败的时候。
11. 什么时候该"满窗口"
平衡一下:上下文不是越小越好。三种情况值得花 token:
- 关键约束的重复:长任务里,把核心目标每 10 轮重申一次(锚定注意力)——不是浪费是投资
- few-shot 示例:3 个好示例的 600 token 往往比省下它换来 10% 的格式错误率划算
- 任务必需的背景:该给的全给。砍背景省下的 token 会以模型瞎猜的形式加倍偿还
判断标准始终是那一条:这个 token 是在帮助当前任务,还是在稀释注意力。
12. 分界线:说清楚事 vs 装对东西
最后一图流总结本章前后两半的关系:
Prompt Engineering(本章前半) Context Engineering(本章后半)
───────────────────── ─────────────────────────
设计对象:单条指令的质量 设计对象:窗口里的一切
关心:怎么"说"一件事 关心:什么该"进"窗口
静态:写好一个 prompt 动态:每轮实时组装
类比:写好一句广告词 类比:设计整个货架的陈列
提示词工程是必要条件(说清楚事),上下文工程是决定性因素(装对东西)。当 Agent 出问题,先查窗口(上下文工程),再改措辞(提示词工程)——顺序反了事倍功半。
本章小结
- 定位:用最低成本逼近模型当前能力上限;调的是分布不是电路
- 五模式:角色(行为暗示)、few-shot(最强力)、CoT(复杂任务结构化)、模板分区、任务分解
- 反模式核心:形容词无锚点,示例才有;负面指令弱于正向;约束放首尾
- 工程化:模板分离(StrictUndefined)、版本化、最小评测集
- 趋势:咒语在贬值,系统化在升值;不变的是清晰的任务定义和好示例
- 统一账本:上下文七组分(系统/工具/技能/记忆/历史/工具结果/当前输入),每组分的进出都是设计决策
- 第一性原理:token 不只花钱,还稀释其他 token 的注意力——上下文是寄存器不是硬盘
- 位置规律:核心约束放首尾,背景放中段
- 工具结果三对策:入口截断 → 历史结果替换(保配对)→ 外置存储+句柄
- 预算表:各组分配额+水位警报;没有预算的 Agent 只能被动等死
- 与提示词工程的分界:说清楚事 vs 装对东西
上下文是"每一轮装什么",还有一层更大的结构在决定"整个系统怎么搭"——脚手架工程。
下一篇:14 · 循环、图与骨架工程 —— 模型只是发动机,真正决定 Agent 上限的是围绕它的工程骨架:工具、循环、反馈、错误恢复的系统设计。