2.3 上下文工程——99 的人忽略的成本与质量平衡

2 阅读17分钟

模块二:AI 协作方法论 | 第03讲:上下文工程——99% 的人忽略的成本与质量平衡

项目:VibeNote 智能笔记 · 技术栈:Next.js / React / TypeScript / Tailwind
一句话:上下文不是「堆料」,是「调度」;调得好,质量上去、成本下来。
本讲和上一讲是组合拳:Workflow 决定「何时提供何种材料」,上下文工程决定「材料本身是否干净」。


一、开场:为什么「上下文越多越好」是伪命题

你在工具链文章里会看到一条被反复验证的判断:很多问题表面像「模型不够强」,根子上是规格不清、目标不明、验收标准缺失。上下文(context)就是把规格与目标送进模型工作记忆里的载体。于是很多人走向另一个极端:把所有文件、所有历史对话都塞进去,以为「多就是稳」。

这与参考实践 vibe-coding-methodology.md 里的经验相反:作者刻意强调稳定优先、小步慢跑,原因之一就是上下文过长会导致模型迷路。再结合核心概念文章对 Agent 的描述——Agent 的价值在于维持上下文、调用工具并连续完成任务——你会意识到:上下文不是无限仓库,而是昂贵的工作记忆

本讲目标:建立一套「成本—质量」权衡框架,让你在给 VibeNote 加功能时,知道什么时候该 @ 整个目录,什么时候只该贴二十行关键代码。

我想再强调一个容易被忽略的事实:上下文质量会直接决定你对 AI 的信任曲线。一开始你可能觉得「多塞点更稳」,等你吃过几次张冠李戴的亏,又会走向「什么都不信」。这两种极端都不健康。我们要的是可解释的稳定:你能说清楚「我为什么带这几份材料」,而不是玄学祈祷。


二、上下文到底是什么:别把它混同于「聊天记录」

在工程语境里,我建议你把上下文拆成三类东西:

  1. 决策上下文:为什么要这么做、非目标是什么、验收标准是什么。
  2. 系统上下文:目录结构、技术栈、约束、风格、关键接口。
  3. 任务上下文:本次要改的路径、相关类型、报错栈、复现步骤。

聊天记录往往三类混在一起,噪声极高;上下文工程要做的是分区投放,让模型优先看到决策与系统,其次才是任务细节。

flowchart TB
  D[决策上下文] --> P[模型注意力]
  S[系统上下文] --> P
  T[任务上下文] --> P
  N[噪声: 无关日志/过期对话] --> X[稀释注意力]
  X --> P

三、上下文窗口:硬上限与「软退化」

模型的上下文窗口(context window)是硬上限;超过就要截断、摘要、换模型或拆任务。更重要的是软退化:即便没触顶,上下文里无关段落越多,模型越容易「看错重点」——表现为张冠李戴 import、改错文件、把 A 需求的假设带到 B 需求。

对 VibeNote 这种全栈项目,软退化常见触发点包括:

  • 把整页 pnpm build 日志不加筛选贴进去
  • 把无关页面的组件一起 @
  • 把三个月前的对话原样复制

经验法则:先写「我将提供以下片段,其余请不要假设」,再贴最小相关片段。


四、Token 成本:钱包与延迟

除了质量,上下文还直接关联费用与响应时间。个人项目可能不在意几美分,但一旦你养成「全仓库 @codebase」的习惯,你会遇到两件事:迭代变慢、账单变丑。企业场景里,这还会变成合规与审计成本(敏感信息更容易被误贴)。

所以建议你在团队里约定:默认最小上下文原则;只有定位不清时才扩大范围。

flowchart LR
  C[上下文体积] --> Q[质量: 先升后降]
  C --> L[延迟与费用: 单调上升]
  Q --> O[最优点: 充分且干净]

曲线的直觉是:干净的小上下文往往打败肮脏的大上下文


五、主动上下文 vs 被动上下文

这是 03-toolchain-frameworks.md 里一条特别好用的主线:被动上下文往往比按需检索更稳——因为很多东西不是「会搜就行」,而是「每轮都在眼前」才不容易漏。

被动上下文(Passive)

例如 AGENTS.md.cursorrules、CLAUDE.md、放在仓库里的栈约定。特点是:高频、稳定、短。适合「我们的 Next.js 用 App Router」「禁止客户端暴露密钥」。

主动上下文(Active)

例如你在输入框里 @file@folder,或粘贴报错。特点是:与当前任务强相关、可变

反模式:把本该被动的长规范每次手动粘贴——既浪费 token,又容易版本不一致。


六、分形文档结构:让上下文「自我更新」

vibe-coding-methodology.md 给了一套很「程序员浪漫」的做法,我建议你认真试试:

  • 根目录主 md:任何功能、架构、写法更新,工作结束后同步子文档。
  • 每个文件夹一个极简说明(三行内)+ 文件列表(名字、地位、功能);文件开头声明「目录变化请更新我」。
  • 每个源码文件头三行:input / output / pos(地位),并声明「我被更新要同步注释与目录 md」。

它的工程意义是:把上下文维护从「心血来潮」变成「修改即触发」。AI 改文件时,如果规则写得好,会顺带把文档涟漪更新,避免「代码是新的、文档是旧的」这种最 poison 的上下文。

flowchart TD
  F[修改代码文件] --> H[更新文件头三行注释]
  H --> M[更新所属目录 README]
  M --> R[必要时更新根架构文档]

七、如何为 VibeNote 组织项目上下文:分层建议

Layer 0:团队共识(被动)

  • 技术栈、包管理器、目录约定、环境变量命名、禁止事项。

Layer 1:模块地图(被动 + 轻量)

  • app/ 负责路由与页面;components/ 负责可复用 UI;lib/ 负责纯逻辑与数据访问。

Layer 2:任务包(主动)

  • 只带相关页面、相关 server action、相关类型定义。

Layer 3:运行时证据(主动)

  • 精简报错、网络请求失败信息、复现数据。

再补一层「产品上下文」(可选 Layer 2.5):当功能涉及文案、权限提示、空状态插图时,把用户可见字符串的约束写清楚,比贴 UI 代码更省 token。很多 UI bug 本质是状态机缺状态,不是 CSS。


八、Cursor 的 @file、@folder、@codebase:使用策略

@file

最常用、最可控。适合:单点修改、对照实现、贴类型定义。

@folder

适合:一个功能竖切(例如 components/editor/)。注意 folder 越大,噪声越大;可配合子目录拆分。

@codebase

适合:你不知道改哪里,需要全局检索定位。代价最高,建议先让模型输出「候选文件列表」,再缩小到 @file。

实践口诀:能 file 不 folder,能 folder 不 codebase。


九、可运行示例:估算「提示文本」的体量(字符统计)

下面脚本不依赖模型,只帮你养成习惯:贴之前先看看自己塞了多少字。

// scripts/context-size.ts
// 运行: npx tsx scripts/context-size.ts "你的粘贴内容"

const text = process.argv.slice(2).join(" ") || "";
const han = (text.match(/[\u4e00-\u9fff]/g) ?? []).length;
const ascii = text.replace(/[\u4e00-\u9fff]/g, "").length;

console.log({
  chars: text.length,
  approxHan: han,
  nonCjkChars: ascii,
  roughTokensGuess: Math.ceil(text.length / 3),
});

roughTokensGuess 很粗,但足够你在团队里做「别超过 X」的纪律训练。


十、上下文裁剪清单(贴之前逐条勾)

  1. 这条信息能否压缩成链接/文件路径而不是全文?
  2. 这段日志能否只保留首尾各 30 行?
  3. 这条需求是否重复了 AGENTS.md 已有内容?
  4. 是否存在过期信息(旧分支、旧 API)?
  5. 是否包含密钥/手机号/内部域名?(必须脱敏)

你可以把清单打印成便签贴在显示器边。它看起来很土,但比「收藏一百篇提示词文章」更能降低事故率。上下文工程的一部分,就是对抗你自己的懒惰与侥幸心理:你知道该脱敏,但手快会忘;清单是外置大脑。


十一、与课程原稿的关系

课程内容/2.3 上下文越多越好吗?99%的人都忽略了成本问题.md 原稿偏模板化;本讲把它落到:窗口、成本、主动/被动、分形文档、Cursor 策略五条可操作轴。你可以把原稿当目录,把本讲当说明书。


十二、VibeNote 场景:三类任务的上下文配方

A. 新增 Route Handler

被动:栈与安全约束。主动:相关 lib/ 方法、已有类似 route、环境变量名。避免:整个 app/

B. 调整编辑器交互

被动:组件风格约定。主动:NoteEditor 文件、键盘事件相关 util。避免:所有页面组件。

C. 修复构建失败

主动:第一条报错栈 + 相关文件片段。避免:把全量 node_modules 相关抱怨贴进来。


十三、上下文与 Workflow 的接口(承接第02讲)

第02讲强调 Plan/Review/Implement/Verify。上下文工程在 Plan 阶段就要回答:我需要哪些证据与哪些地图。Review 阶段则要回答:哪些上下文会误导实现


十四、常见反模式

  1. 复制整个聊天:历史噪声会把当前任务冲散。
  2. 多任务混聊:A、B 需求共用一个 thread。
  3. 只给结论不给证据:模型只能猜。
  4. 过度依赖 @codebase:成本高且易引入无关文件。
  5. 文档与代码长期分裂:分形结构就是为了治这个。

十五、进阶:什么时候该「换会话」而不是「继续堆」

当你发现模型开始引用不存在文件、或把旧方案当现行方案,通常说明会话上下文已经脏了。此时更有效的是:新开对话,携带「摘要 + 当前分支 + 相关文件」三件套,而不是继续往上滚。


十六、再谈被动上下文:为什么 Vercel 团队说「评测更稳」

03-toolchain-frameworks.md 提到 AGENTS.md 这类被动上下文在评测中更稳,本质是:减少触发不确定性。Skills/工具如果未被可靠触发,会带来「以为带了,其实没带」的隐性问题;被动挂载则更像编译期的 include——更笨,但更稳。


十七、给 VibeNote 的「上下文预算」建议(团队可改成表)

任务类型建议主动片段数是否启用 codebase
小改 UI1~3 个文件
新 API2~5 个文件
定位神秘 bug先 codebase 出清单,再收敛短暂启用
大重构分阶段会话 + 目录级 @谨慎

十八、可运行示例 2:从目录生成「文件夹 README 骨架」

配合分形文档,你可以用脚本生成待填骨架(Node 16+)。

// scripts/gen-folder-readme.mjs
import fs from "node:fs";
import path from "node:path";

const target = process.argv[2] || "components";
const lines = [];
lines.push(`# ${target}/`);
lines.push("");
lines.push("三行架构说明:(用一句话说明这个目录负责什么边界)");
lines.push("");
lines.push("## 文件列表");
for (const name of fs.readdirSync(target)) {
  if (name === "README.md") continue;
  const p = path.join(target, name);
  const stat = fs.statSync(p);
  lines.push(`- \`${name}\` — ${stat.isDirectory() ? "目录" : "文件"}:职责待补`);
}
fs.writeFileSync(path.join(target, "README.md"), lines.join("\n"), "utf8");
console.log("written:", path.join(target, "README.md"));

运行:node scripts/gen-folder-readme.mjs components


十九、上下文「蒸馏」:把 10 屏聊天压成 10 行摘要

建议你养成一个习惯:每个里程碑结束,写一段不会过期的摘要放进 docs/sessions/2026-03-xx.md,结构固定为:

  • 目标一句话
  • 关键决策三 bullet(含「不做什么」)
  • 触碰的文件列表
  • 未解决问题与下一步

这段摘要会成为下一次会话最贵的资产:它比整段聊天便宜得多,但信息密度更高。参考方法论强调用文档持久化关键对话与技术决策,这就是落地版。


二十、与「规范是新的源代码」的咬合

核心概念文章列表里提到:当 AI 大规模承担实现,规范会越来越像驱动产出的源头文件。上下文工程在这里的角色是:把规范放在正确层级——稳定规范进被动上下文;临时约束进当次工单。否则你会看到一种怪现象:规范写得很全,但模型每次都没读到,因为只躺在 Notion 里。


二十一、VibeNote 实战:一次「上下文过多」的复盘剧本

假设你在做「AI 改写」功能,第一次对话里你 @ 了整个 app/、贴了 200 行日志、还复制了另一项目的代码。模型很可能输出「看起来很专业」的通用方案:新依赖、全量重构、顺便改布局。根因不是坏,是注意力被噪声带走

第二次你改成:被动上下文声明栈与密钥规则;主动只 @ NoteEditor + 一个已有 route.ts 示例;日志只保留首行错误类型与对应文件路径。通常第二次会更像「你的项目」。


二十二、上下文与安全的交界:你必须红线的三件事

  1. 密钥与 token:永远不进聊天全文;用占位符 + 环境变量名。
  2. 用户数据:脱敏到「结构相同、内容不同」即可。
  3. 内网地址:改成 https://internal.example 这类占位。

上下文工程不是纯技术问题,一次粘贴就可能把事故贴进模型提供方日志


二十三、什么时候该用「外部记忆」而不是「对话记忆」

长对话不是数据库。需要跨周复用的决策,请放进仓库:issue、ADR、docs/。外部记忆的好处是可检索、可 diff、可 code review。对话记忆的好处是快——但快不等于可靠。


二十四、Cursor 组合技:@Docs + @file 的分工

如果你使用官方文档索引(不同版本功能略有差异),经验是:语法/框架行为问 @Docs,项目细节问 @file。把两者混在一个问题里时,写清楚边界:「官方推荐怎么做」与「我们项目当前代码长什么样」分别要什么。


二十五、分形结构的「最小可行版」

如果你觉得三行注释 + 目录 README 太重,可以先只做两层:

  1. docs/ARCHITECTURE.md 维护模块地图
  2. 每个大目录一个 README.md 只列文件职责

等团队习惯后,再加文件头三行。上下文工程最怕一次性上太重导致没人维护


二十六、对比表:主动检索 vs 被动挂载

维度主动检索(搜/问)被动挂载(规则/AGENTS)
稳定性依赖触发正确
token 成本按需每轮固定
适合内容临时、长尾高频、短、硬约束
风险「没带技能」「规则太长」

二十七、再补一张图:上下文生命周期

stateDiagram-v2
  [*] --> Collect: 收集证据
  Collect --> Trim: 裁剪噪声
  Trim --> Bind: 绑定到任务
  Bind --> Use: 模型推理
  Use --> Archive: 蒸馏进文档
  Archive --> [*]

二十八、FAQ

Q:@codebase 是不是越新越强就越该常用?
A:越强越要克制,否则你会用算力买噪声。

Q:上下文要不要包含测试代码?
A:要,当你改动会影响行为时;但要精确定位到相关 *.test.ts,别把整个测试目录塞进去。

Q:中文注释会不会干扰模型?
A:不会本质干扰,但要统一语言策略:公共规则里写清「注释中文/英文」即可。

Q:我能不能让模型自己总结上下文?
A:可以,但要做「二次校验」:模型摘要可能漏掉关键约束。最稳的是摘要后你再补三条硬约束(安全/兼容/不改动范围)。

Q:分形文档会不会变成形式主义?
A:会,如果没人 review。把它绑定到 PR:改目录必须同步 README,一条机器人评论就能救。

Q:小团队只有一个人也要做吗?
A:更要。你未来的自己会感谢现在留下的地图;否则三周后你会在「我当初为什么这样写」里浪费时间。

Q:上下文要不要包含 package.json?
A:不必整文件;通常贴 dependencies 里与任务相关的几行即可,或说明 Next/React 版本号。


二十八(续)、把「噪声」定义清楚:五类高频噪声清单

  1. 重复规则:已在 AGENTS.md 写过却又粘贴一遍。
  2. 过期 diff:来自旧分支的代码片段。
  3. 情绪性文字:长篇抱怨对定位没有信息增益。
  4. 无关页面:为了「显得完整」而 @ 的全站组件。
  5. 巨型日志:未裁剪的 trace。

二十九、与无效调试循环的关系(预告第05讲)

参考方法论描述过:项目变大后,如果把报错随手丢给模型而不先分析,容易进入「按了葫芦起了瓢」的循环。上下文工程在这里的作用是:把根因证据组织成模型可消费的形态,而不是把混乱原样放大。换句话说,调试不是「更多上下文」,而是更准的上下文


三十、团队练习:「上下文审计」15 分钟

挑一个真实 issue,三个人分别独立列出「应携带的文件清单」,再对比差异。你会惊讶地发现:大家对系统边界的理解不一致,而这正是返工源头。把差异合并进 docs/ARCHITECTURE.md,比多写十个 Prompt 更有用。


三十一、本讲小结

  • 上下文是昂贵的工作记忆,不是堆料比赛。
  • 主动 vs 被动要想清楚:稳定短规则应被动挂载。
  • 分形文档解决「代码新、文档旧」的慢性中毒。
  • Cursor:@file 优先,@codebase 用于定位后迅速收敛。
  • 会话脏了要会「换盆」:新会话 + 蒸馏摘要,而不是无限往上卷。

把小结再压成一句行动:每次贴上下文前,先问「这条信息会改变决策吗?」不改决策的,别贴。


三十二、延伸:RAG/知识库与「仓库内文档」怎么选

很多团队会问:要不要上向量检索?对 VibeNote 这种代码仓库型项目,我的务实建议是:先把仓库内文档写短、写准、写进分支。RAG 解决的是「海量外部知识」,而你们最大的痛点通常是「地图不在仓库里」。当文档真的不够用时,再引入检索;否则你会用复杂系统替代本该有的纪律。

再补一句落地判断:如果你连自己该读哪三个文件都说不清,RAG 只会让你更自信地读错。先把「文件地图」写进仓库,比先上向量数据库更划算。


三十三、延伸:多文件修改时如何防止上下文「串线」

当你必须同时修改 5 个文件时,不要在一个消息里扔 5 个互不关联的问题。更好的做法是:一次会话只解决一个竖切目标,或者要求模型输出「文件 A 完成后再处理 B」的检查点。上下文工程不仅是「放什么」,还包括对话节奏:节奏错了,材料再全也会串。


三十四、把「上下文」写进 Definition of Done

建议你给团队加一条简单 DoD:合并前确认相关 README/ADR 已同步。这不是为了写论文,是为了让下一次 AI 协作更便宜。上下文工程最终会变成组织习惯;习惯比工具贵,但也更持久。

如果你用的是 Pull Request 模板,也可以加一项:「本次变更是否需要更新架构文档/目录 README?」——勾「否」要给出理由。理由本身也是信息,能防止想当然。


三十五、思考题

  1. 回顾你最近一次超长对话:其中真正必要的片段占比多少?你会如何改写?
  2. 你的项目里哪些信息应该从「每次粘贴」迁移到「被动上下文」?迁移的触发条件是什么?
  3. 分形文档结构你最怕的维护成本是什么?有没有折中版(只要求两层)?

附加题:为 VibeNote 写一个「上下文配方」模板(Feature/Bugfix 各一份),下次直接复制填空。

再想一想:如果你只能减少一种噪声,你会先减哪一种?为什么它对当前项目伤害最大?


三十六、下讲预告

下一讲进入规则文件与复用模板AGENTS.md.cursorrulesCLAUDE.md 各自解决什么问题,如何把「好规范」写成智能体成功率的核心变量,并借鉴 Addy Osmani「如何为 AI 智能体编写好的规范」那条线的思路,把 VibeNote 的规则库搭起来。你可以提前整理一份清单:哪些规则你一周要重复讲三次以上——它们最值得进规则文件。


参考阅读reference/practice/vibe-coding-methodology.md(分形文档、上下文过长风险);reference/articles/03-toolchain-frameworks.md(被动上下文 vs Skills);课程内容/2.3 上下文越多越好吗?99%的人都忽略了成本问题.md

最后一句话:别用更大的窗口逃避更清晰的地图。窗口会越来越大,但你的工程纪律不该越来越松。

如果你读完只记住一件事:记住最小充分上下文——它会让你少花钱、少返工、少熬夜。真的够了,去练一周比再看十篇文章重要;练完记得复盘,把浪费的 token 记下来,下次就少交学费,划算得很,别偷懒。