Skip to content

Latest commit

 

History

History
397 lines (219 loc) · 23.5 KB

File metadata and controls

397 lines (219 loc) · 23.5 KB
title 上下文工程实战指南:让 Agent 少犯蠢的工程方法论
description 深入解析 Context Engineering 核心概念,涵盖静态规则编排、动态信息挂载、Token 预算降级、按需加载策略及长任务上下文持久化,帮助开发者构建高信噪比的 Agent 上下文供给系统。
category AI 应用开发
head
meta
name content
keywords
Context Engineering,上下文工程,Agent,LLM,RAG,Prompt Engineering,Compaction,Sub-agent

同样的模型,同样的 Agent 框架,为什么有的人跑起来很稳,你的一跑就开始迷路?

它会重复调用工具,查了一堆没用的信息,最后还输出一段看起来很像结论、实际根本跑不通的东西。

很多时候,问题不在模型,而是出在上下文。Agent 每次调用 LLM 前,窗口里到底塞了什么,塞得干不干净,顺序对不对,工具描述够不够清楚,都会直接影响最后表现。

这篇文章聊 Context Engineering。说白了,就是怎么给 Agent 准备一套高质量的上下文供给系统。

文章比较长,接近 6000 字。看完你大概能搞清楚几件事:

  1. 为什么上下文会决定 Agent 表现
  2. Context Engineering 和 Prompt Engineering 到底差在哪
  3. 静态规则、动态信息、Token 预算、按需加载怎么落地
  4. Compaction、结构化笔记、Sub-agent 怎么解决长任务上下文问题

同样的 Agent,为什么表现差这么多

先看一个很常见的电商售后场景。

用户发来一句话:

“我上周买的耳机右耳没声音了,怎么处理?”

如果 Agent 拿到的上下文很少,它大概率会这么回:

User: 我上周买的耳机右耳没声音了,怎么处理?
Model: 抱歉给您带来不便。请问您购买的是哪款耳机?订单号是多少?能否描述一下具体故障表现?

这段回复不能说错,但它像一个刚上岗的客服新人,只会照着流程追问。代码逻辑没问题,LLM 调用也没问题,就是没有主动整合信息。

换一个上下文充足的版本。

在调用 LLM 之前,系统先把该查的信息查出来:

  • 查订单系统,定位到上周购买记录:索尼 WH-1000XM5,3 月 25 日下单
  • 查保修状态,发现还在 7 天无理由退换期内
  • 查历史工单,发现用户是老客户,之前没有售后纠纷
  • 挂载 create_return_ordercheck_inventory 工具

这时候 Agent 就可以这样回复:

“您好,查到您 3 月 25 日购买的索尼 WH-1000XM5,目前还在退换期内。我这边直接帮您发起换货申请,仓库显示同款有库存,预计 2-3 天寄出新品。需要我操作吗?”

差距一下就出来了:前一个 Agent 在要信息,后一个 Agent 在解决问题。

Agent 的很多失败,根子都在上下文。 上下文不够,模型再强也只能猜;上下文给对了,中等水平的模型也能把任务做下去。

Context Engineering 到底在做什么

它和 Prompt Engineering 差在哪

Tobi Lutke 对 Context Engineering 有个说法:

the art of providing all the context for the task to be plausibly solvable by the LLM

意思是:给 LLM 提供足够上下文,让这个任务在模型能力范围内“有可能被解决”。

这里的关键词是 plausibly——它不是说上下文给够了模型就一定能解决,而是强调如果没有这些上下文,任务压根就不具备可解条件。

很多文章会把 Context Engineering 和 Prompt Engineering 混着讲,但这两个东西关注点不一样。

Prompt Engineering 更关心指令怎么写,比如措辞、顺序、格式、语气。

Context Engineering 关心的是这轮调用前,模型窗口里应该放哪些信息,以什么结构放,什么时候放,什么时候撤掉。

下面这张图来自 Anthropic 官方博客,对比很直观:

Prompt engineering vs. context engineering

如果把 Prompt Engineering 比成“告诉厨师这道菜怎么做”,那 Context Engineering 更像是给厨师准备厨房:食材在哪、刀具在哪、调料怎么分类、火候参考在哪里。

Prompt vs Context 工程维度对比

我更喜欢另一个类比:Context Engineering 是 LLM 的内存管理。

LLM 的上下文窗口就是一块有限内存。Context Engineering 管的是这块内存里装什么、换出什么、什么时候读、什么时候写。

窗口满了,就要淘汰内容。这和操作系统里的页面置换有点像,比如 LRU、优先级策略。后面讲 Token 降级时,也是在处理这个问题。

它具体管哪些东西

拆开看,Context Engineering 至少管六块。

System Prompt

这是静态规则,比如 .cursorrules.claude/rulesAGENTS.md 这类文件。里面一般会放角色设定、目标、约束、执行流、输出格式。

这些内容决定了 Agent 做任务时的基本边界。

User Prompt

用户输入的业务数据和指令。

这部分看起来简单,但真实项目里经常会混着自然语言、业务字段、历史状态、附件内容,处理不好就会污染上下文。

Memory

记忆系统分短期和长期。短期记忆一般是 Session 内的滑动窗口,长期记忆通常是核心事实提取后写入向量数据库,后续按需检索。

RAG & Tools

RAG 负责检索外部文档,把相关内容塞进上下文;Tools 负责把可调用工具的描述、参数格式、调用结果挂载进去。

RAG 可以看作 Context Engineering 的一种实现。它回答的是:检索什么、怎么检索、结果怎么放进上下文。

Structured Output

结构化输出也属于上下文的一部分,比如 JSON Schema、function call 的返回结构。

它会影响下游系统怎么解析,也会影响后续 Agent 链路怎么衔接。很多人写 Agent 时会忽略这块,最后解析阶段一堆脏活。

Token 优化

摘要压缩、历史剔除、Context Caching 都属于这里,目标很简单:保留信息完整度,同时控制 Token 消耗。

上下文窗口(Context Window)= LLM 的工作记忆

Context Engineering 怎么落地

先把静态规则写清楚

静态规则可以理解成 Agent 的“出厂设置”。

现在比较常见的做法,是用结构化 Markdown 写系统提示词。不要把所有东西揉成一大段,而是拆成角色、目标、约束、执行流、输出格式。

比如一个故障排查 Agent,可以这样写:

## 角色

你是一个后端服务故障排查专家,擅长通过日志和监控数据定位问题根因。

## 约束

- 只调用必要的工具,不重复调用相同逻辑的工具
- 发现关键信息时立即停止搜索,输出结论
- 优先使用实时数据而非历史推断

## 执行流

1. 查监控指标(CPU/内存/网络)
2. 查对应时间范围的日志
3. 如发现异常调用链,追踪上下游依赖
4. 输出结构化报告:问题描述 → 根因 → 建议修复方案

## 输出格式

使用 JSON,包含字段:incident_summary, root_cause, evidence, recommendation

这些规则可以固化到 .cursorrulesAGENTS.md 文件里。

现在模型越来越强,对 Prompt 细节没以前那么敏感了。但结构化规则依然值得做。它的价值不只是提升模型表现,还方便团队维护。

一个团队里,如果每个人都靠口头经验写 Agent 规则,后面一定会乱。

动态信息别一股脑塞进去

上下文窗口不是垃圾桶,很多 Agent 失败不是信息不够,而是塞了太多无关信息。

动态挂载主要看两块:第一块是工具懒加载,也就是 Tool Retrieval。

当 Agent 面对大量 MCP 工具时,把所有工具描述一次性塞进去,既浪费 Token,也会增加误调用概率。

更合理的做法是:先通过向量检索找出当前任务最相关的 Top-5 工具定义,再挂载进去。

这和人查手册差不多。你不会把整本手册背下来,而是先翻到相关章节。

不过这里也有个现实限制。Anthropic 更强调在设计阶段就精简工具集,别把工具集合做得过度膨胀。工具太多,后面再做检索也只是补救。

第二块是动态记忆和 RAG。

短期记忆可以用滑动窗口管理,长期事实通过向量数据库检索。API 报错日志、工具返回结果这类 Observation,最好先让 LLM 做一次摘要,只把关键信息写回上下文。原始日志洪流直接塞进去,很容易把模型淹没。

Token 不够时要会降级

长任务跑到后面,窗口一定会紧张,这时候不能靠感觉删内容,得有优先级。

上下文 Token 预算的三级淘汰策略

优先级 内容 处理方式
低优先级(可折叠) 早期对话历史 AI 摘要压缩
中优先级(可精简) RAG 检索的背景资料 二次裁剪,保留核心段落
高优先级(绝对保护) System Constraints、当前核心工具描述 永不丢失,确保逻辑一致性

低优先级内容可以折叠,比如早期对话历史不一定要保留原文,压缩成摘要就行。中优先级内容可以精简,比如 RAG 检索出来的资料没必要整段保留,可以二次裁剪,只留和当前任务直接相关的片段。高优先级内容不能丢,System Constraints、当前核心工具描述、关键任务目标这些一旦丢了,Agent 很容易开始乱跑。

大规模并发场景里,还可以配合 Context Caching。相同的 System Prompt 不用每次重复加载,可以降低首 Token 延迟和推理成本。

上下文为什么会失效

很多人直觉上会觉得:窗口越大,塞的信息越多,模型应该表现越好。实际不是这样——上下文存在边际收益递减,塞过头之后效果还可能变差。

原因和 Attention 机制有关。Transformer 里,每个 Token 都要和上下文里的其他 Token 计算注意力关系。n 个 Token 会产生 n² 量级的注意力计算。

当上下文从 1K 扩展到 100K Token,问题不只是“信息被稀释”这么简单。

真正麻烦的是,模型要在更多 Token 之间判断哪些相关、哪些不相关。上下文越长,噪声越多,信号越难被挑出来。

这就是 Context Rot,也就是上下文腐化。

随着上下文 Token 总量增加,模型整体的信息回忆能力会下降。和它相关的,还有 Lost in the Middle 问题:模型对上下文中间位置的信息记忆更弱,对开头和结尾更敏感,整体呈 U 型分布。

这两个现象都说明一件事:上下文不是越长越好。还有一个训练层面的原因。

模型的 Attention 模式主要是在相对短的文本序列上学出来的。互联网文本的平均长度远低于现在一些模型支持的上下文窗口。

这意味着模型处理超长依赖时,学习经验本来就不足。位置编码外推能力也有限。虽然有 Position Encoding Interpolation,比如基于 RoPE 的 YaRN、NTK-aware Interpolation,用来缓解长序列外推问题,但精度损失不会完全消失。

工程上别迷信窗口大小,不同模型的衰减曲线不一样,有些退化平缓,有些退化很陡,具体阈值要靠实测。但有一点可以确定:上下文必须当作有限资源来管,真正要找的是高信噪比平衡点,而不是把窗口塞满。

怎么构建有效上下文

System Prompt 别写成两种极端

System Prompt 常见两个问题。

第一个是过度设计。有些工程师会把大量 if-else 逻辑硬塞进 Prompt,试图精确控制 Agent 的每一步,结果是 Prompt 又长又脆弱,像纸片房——维护成本很高,遇到没见过的边缘情况,模型照样会跑偏。

第二个是过度抽象。只写一句“你要做一个有帮助的助手”,模型拿不到足够决策依据,要么不停追问用户,要么输出和业务预期偏得很远。

比较好的状态是:具体到能引导行为,抽象到能覆盖常见变化。

Anthropic 工程博客里提到过一个词,叫 Goldilocks zone,也就是刚刚好的区域。

上下文工程过程中的系统提示

实操上可以这么做:先用最小 Prompt 测基线表现,再根据 failure case 一条一条补规则,不要第一天就试图穷举所有情况。Anthropic 把这件事叫 Calibrating the system prompt——System Prompt 应该像一个持续调校的参数,不应该是一份写完就不再动的配置文档。每发现一个 failure case,就补一条清楚的规则,然后重新测试。

工具描述要先讲边界

工具定义写得好不好,直接决定 Agent 会不会选错工具。

一个好的工具描述要回答两个问题:什么时候该调用?什么时候不该调用?如果一个工具描述连人类工程师都看不出该不该用,Agent 也一定会犯错。

最常见的坑,是做一个“大而全”的工具。比如 manage_database,里面同时包含建表、查数据、删数据、备份、导出五个能力。Agent 选择工具时会犹豫,填参数时也容易被一堆无关字段干扰。

很多人觉得工具描述越详细越好,其实重点不是面面俱到,而是边界清楚。做到两条就行:一个工具只做一件事,参数描述里给格式示例。这两条做到,误调用率通常会明显下降。

Few-shot 示例别堆太多

Few-shot prompting 很有用,但很多人用法不对。典型错误是往 Prompt 里塞几十个 edge case,试图覆盖所有规则,结果模型可能过度拟合示例表面的写法,反而忽略真正该学的处理逻辑。

更稳的做法是选 3-5 个多样化的典型示例,也就是 canonical examples。“Canonical” 的意思不是把所有边缘情况列全,而是每个示例能代表一类标准场景。对模型来说,示例像一张图——它展示的是“什么情况该用什么策略”,不是“这个输入必须对应这个输出”。

运行时上下文怎么检索

预检索为什么不够

传统 AI 应用常用预检索,也就是在调用 LLM 之前,先通过 Embedding 相似度找出最相关的上下文,然后一次性塞进 Prompt。

简单问答场景里,这套机制还挺好用,但到了复杂 Agent 任务里,它会暴露问题。

预检索拿到的是“调用前看起来相关”的信息,但 Agent 执行过程中会不断发现新线索,而这些线索在预检索时根本还不存在。

Just-in-Time 按需加载

Just-in-Time 的思路是:不要一开始就装载所有可能相关的信息。

Agent 运行时先维护轻量级引用,比如文件路径、数据库查询、Web 链接。真正需要时,再通过工具动态拉取数据。

Claude Code 就是很典型的例子。它分析大型代码库时,不会把所有文件都塞进上下文,而是先通过目录结构、文件名、搜索命令定位目标,再用 headtailgrep 这类方式逐步读取。

Agent 像人一样靠文件名和目录结构理解信息位置,靠文件大小和时间戳判断优先级,而不是上来就把全部内容吞进去。

这里有个很容易被忽略的点:元数据本身也是信息。

tests/test_utils.pysrc/core_logic/test_utils.py 语义就不一样。光看路径,Agent 就能判断它们大概率服务于不同目的。

Anthropic 把这种方式叫 Progressive Disclosure,也就是渐进式披露。

Agent 不是一次性拿到所有上下文,而是通过一轮轮探索逐渐理解任务。文件大小暗示复杂度,时间戳暗示相关性,目录结构传递语义。

但按需加载也有代价:它比预检索慢,而且需要工程师提供好用的导航工具,比如 glob、grep、tree。

如果导航工具不好用,或者导航启发式规则写得差,Agent 很容易追进死胡同,浪费上下文和调用次数。

所以 Just-in-Time 不是“不预处理”,恰恰相反,它对工具集和导航策略要求更高。

更现实的方案通常是混合策略:确定性高的静态知识可以预检索,运行中动态发现的信息再按需拉取。

Claude Code 也是这个思路:CLAUDE.md 文件可以预加载,但具体文件内容靠 Agent 运行时探索。

不同场景的选择也有规律。代码库分析、信息检索这种探索空间大、动态内容多的任务,更适合以 Just-in-Time 为主;法律文书审阅、财务报表分析这种上下文稳定、动态内容少的任务,更适合预检索加少量运行时补充。

长任务里,上下文怎么撑住

长任务上下文持久化:抵抗腐化的三大武器

Compaction:窗口快满时压缩历史

Agent 如果要连续跑几个小时,处理很多轮迭代,只靠普通上下文管理是不够的,它需要跨窗口持久化。

Compaction 就是常见做法:当上下文快满时,把历史内容交给 LLM 总结,然后用摘要开启一个新的上下文窗口继续跑。

Claude Code 的思路是:把历史消息交给模型做摘要,保留架构决策、未解决 Bug、关键实现细节,丢掉冗余工具调用结果。然后 Agent 拿着压缩后的上下文,再加上最近访问的 5 个文件,继续工作。

难点在取舍:保留太多压缩没意义,保留太少关键上下文丢了。

比较实际的做法是:拿复杂 Agent 轨迹反复调压缩 Prompt。先保证重要信息别漏,再逐步删掉冗余内容。

这不是一次能写准的。还有一个更轻量的压缩方法:清理工具结果。

工具已经调用过,结果也被消化了,后面就没必要保留完整原始输出。Anthropic 的 Developer Platform 已经把这个做成了原生功能。

Structured Note-taking:让 Agent 记笔记

Structured Note-taking 是另一种长任务处理方式。让 Agent 把关键进展写到外部文件里,比如 NOTES.md。上下文重置后,再读取这些笔记继续工作。

这和人类工程师写 to-do list、技术备忘很像。Claude Code 在长任务里会自动维护 to-do list。自定义 Agent 也可以在项目根目录维护 NOTES.md,里面记录当前进度、已知问题、下一步计划。

一个很有意思的例子是 Claude 玩 Pokemon。在数千轮游戏步骤里,Agent 自己维护了数值追踪,比如“过去 1234 步我在 1 号道路训练皮卡丘,已升 8 级,距离目标还差 2 级”。

它还自发建立了地图、成就清单、战斗策略笔记。上下文重置后,这些笔记还能被重新读取,所以它才能跨几个小时持续推进游戏。

Anthropic 在 Sonnet 4.5 发布时,也推出了 Memory Tool 公开测试版,用文件系统持久化的方式让 Agent 建立跨会话知识库。

Sub-agent:别让一个 Agent 扛所有状态

Sub-agent 架构的思路很直接:别让一个 Agent 扛完整项目状态。具体来说,就是把专门任务拆给专业化子 Agent,主 Agent 负责分配任务和汇总结果。

每个子 Agent 可以自己探索大量上下文,可能是几万个 Token。但返回给主 Agent 的,只是一段 1000-2000 Token 的高密度摘要。

这样主 Agent 的上下文会干净很多——详细搜索过程被隔离在子 Agent 里,主 Agent 只处理分析和决策。

Anthropic 在《How we built our multi-agent research system》里讲过这个模式。相比单 Agent,它在复杂研究任务上有明显质量提升。

三种方式可以这么选:

技术 适用场景
Compaction 需要持续对话的长流程,重点是保持上下文连贯
Note-taking 迭代式开发、有清晰里程碑、多步推进的任务
Sub-agents 复杂研究、需要并行探索、最终要汇总结果的任务

落地 Context Engineering 会用到哪些工具

方法讲完,工程工具也顺一下。

  • 编排框架:LangChain、LangGraph 这类框架,主要负责 Agent 的控制流、状态管理和循环调度
  • 数据框架:LlamaIndex 更偏 RAG,负责数据摄取、索引构建和检索优化
  • 向量数据库:Pinecone、Weaviate、Chroma、Qdrant 这类工具,负责 Embedding 存储和语义搜索
  • 通信协议:MCP(Model Context Protocol)解决的是工具怎么标准化接入宿主程序的问题,经常被类比成 AI 应用里的 USB-C。Anthropic 发布的 MCP 基于 JSON-RPC 2.0,定义了 Tools(可执行函数)、Resources(只读数据)、Prompts(可复用模板)三类标准原语
  • Memory 产品:Mem0、LETTA(原 MemGPT)、ZEP 这类产品,主要做 Agent 记忆层,通常在向量库之上封装记忆写入、检索、遗忘这些生命周期管理能力

真正落地时,要盯住什么

Context Engineering 最重要的判断其实很简单:Agent 的大多数失败,不在模型智商,而在上下文精度。

过去大家更关心“这句 Prompt 怎么写”,现在更该关心的是:什么信息,以什么格式,在什么时机进入窗口。

模型能力还会继续变强,但注意力有限这个约束不会消失——窗口再大,塞一堆噪声进去,模型一样会变笨。

上下文是系统输出,不是静态配置。

每次 LLM 调用前,你都在组装一个动态上下文,这个组装逻辑本身就是工程重点。

改一个检索策略,换一种摘要方式,调整工具 Schema 的挂载顺序,效果差别可能比换模型还大。

高信噪比比高信息量更重要。

上下文长度不决定效果,Dex Horthy 的 40% 阈值实验也说明塞满窗口不如只放必要信息。真正要找的是让模型做出正确决策所需的最小高密度信息集。

长任务里,上下文一定会腐化。

Compaction、结构化笔记、Sub-agent 分层,要组合起来用,才能让上下文在长时间运行里不变质。

先从最简单的方案跑通。

Anthropic 反复强调过一句话:do the simplest thing that works。过度设计的上下文系统,和上下文不足一样危险。

Guide 见过不少团队,连基线都没跑通,就开始做记忆分层、复杂检索、长期状态管理,最后调试成本比收益还高。先跑通,再加复杂度。

上下文给对了,中等模型也能做出复杂任务。上下文给烂了,再贵的模型也会输出一坨看起来很像答案的噪声。

延伸阅读