前言
我用 Copilot 最深的一个感受是,同一个模型、同一个工具,在不同项目里的表现差异会非常大。
有时候它能很快补出符合项目风格的代码,函数命名、错误处理、测试写法都接近现有实现。有时候它又会写出一段看起来能跑,但和项目结构完全不搭的通用代码。这个差异一开始很容易被归因到模型不稳定,后来我发现,很多问题其实出在上下文。
Copilot 不是在真空里写代码。它会根据当前文件、打开的文件、工作区索引、你显式引用的文件、聊天历史、项目自定义指令、工具执行结果这些信息生成回答。上下文给得准确,它更容易沿着项目原有方式写;上下文混乱或者缺失,它就会回到通用模式。
我现在会把这件事称为上下文工程。它不是多写几句提示词,也不是把整个项目都丢给 AI,而是有意识地管理 Copilot 能看到什么、应该遵守什么规则、当前任务的边界在哪里、哪些文件才是判断依据。
会写 prompt 只是第一步。真正用好 Copilot,需要把上下文当成工程材料来整理。

一、先知道 Copilot 看到了什么
很多人刚开始用 Copilot 时,会默认它理解整个项目。这个假设很危险。Copilot 在不同模式下获取上下文的方式不完全一样。补全、Chat、Edits、Agent 模式都会使用上下文,但它们不会无条件理解整个仓库里每个文件。
在 VS Code 里,一次 Copilot 请求通常会组合多层信息。系统会带上基础规则,你的工作区可能带有自定义指令,当前聊天里有用户输入和历史对话,编辑器会提供当前文件、选中代码、打开文件、工作区索引、显式引用的文件或符号,工具调用也可能把搜索结果、终端输出、测试结果带回来。
这就解释了一个现象:你问 Copilot 一个业务问题,它不一定知道你指的是哪个模块;你只写一句帮我实现保存逻辑,它可能会自己猜数据库结构、错误处理和返回格式。不是它故意乱写,而是它没有拿到足够明确的项目约束。
我会把上下文分成三类。
| 上下文类型 | 来源 | 适合解决的问题 |
|---|---|---|
| 隐式上下文 | 当前文件、选中代码、打开的编辑器、工作区索引 | 补全当前函数、解释当前代码、做小范围修改 |
| 显式上下文 | #file、#folder、#codebase、符号引用、拖拽文件 |
分析指定文件、跨模块改造、排查调用链 |
| 规则上下文 | .github/copilot-instructions.md、prompt files、workspace settings |
固定项目规范、测试要求、提交风格、架构边界 |
这三类上下文要配合使用。只靠隐式上下文,Copilot 容易猜;只靠显式引用,使用成本又太高;只写自定义规则,不提供当前任务文件,它仍然不知道要改哪里。比较好的方式是让项目长期规则沉淀在指令文件里,让当前任务通过显式引用补足文件和目标。
我现在提问前会先确认三件事:当前文件是不是任务入口,相关类型和测试有没有进入上下文,项目规则有没有被 Copilot 读取。这个检查比直接写长 prompt 更重要。
二、打开文件只是开始,显式引用更可靠
以前我经常靠打开文件来给 Copilot 补上下文。比如写 OrderService.ts 时,同时打开 Order.ts、OrderRepository.ts 和 OrderService.test.ts。这样 Copilot 在补全时有机会参考相邻文件里的类型、测试和调用方式。
这个习惯仍然有用,尤其对代码补全很有帮助。你在写一个新方法时,旁边打开的测试文件可能会影响它生成的函数签名和返回结构;打开同类 service 文件,它也更容易模仿项目已有写法。
但 Chat 场景里,我不会只依赖打开文件。更稳的方式是显式引用。
比如要让 Copilot 分析订单保存逻辑,我会这样写:
text
请基于 #file:src/services/OrderService.ts 和 #file:src/repositories/OrderRepository.ts 分析订单保存流程。
重点看三件事:
1. 保存前做了哪些校验。
2. 数据库写入失败时怎么返回错误。
3. 有没有可能重复创建订单。
先分析,不要修改代码。
这里我没有让 Copilot 猜我要看哪些文件,而是直接把关键文件放进问题里。它回答时就会更接近当前项目,而不是给一段通用订单逻辑。
如果不确定文件在哪,可以用 #codebase 或工作区搜索能力先让它定位。
text
#codebase 帮我找到项目里处理订单状态流转的入口文件。只列文件路径和主要职责,先不要写实现方案。
找到入口后,再把具体文件加入下一轮对话。这样比一上来就让它全项目分析更容易控制结果。
显式上下文还有一个好处:审查时更容易追溯。你知道它参考了哪些文件,也知道它没有参考哪些文件。AI 写错时,可以回头判断是规则没说清,还是关键文件没给到。
三、项目规则要写进仓库
临时 prompt 适合描述当前任务,项目长期规则不适合每次重复输入。比如 TypeScript 项目不能用 any,测试统一用 Vitest,API 返回结构必须包含 code、message 和 data,错误处理统一走 AppError,这些规则每次都写一遍很浪费,也容易漏。
我会把这类规则放进 .github/copilot-instructions.md。
markdown
# Copilot Instructions
## 技术栈
- 使用 TypeScript 严格模式。
- 前端使用 React 函数组件。
- 测试使用 Vitest。
- API 请求统一走 `src/lib/apiClient.ts`。
## 代码约束
- 不要生成 `any`。
- 不要引入新的状态管理库。
- 不要绕过现有权限校验。
- 新增工具函数要放在 `src/utils` 或对应模块目录下。
## 错误处理
- 业务错误使用 `AppError`。
- API 返回结构必须包含 `code`、`message` 和 `data`。
- 数据库写入失败时不能直接返回原始异常。
## 验证方式
- 修改业务逻辑后补充或更新测试。
- 优先运行和当前模块相关的测试。
这个文件的作用不是让 Copilot 一次性变得完美,而是减少它偏离项目风格的概率。每次 Chat、Edits 或 Agent 任务都能参考这些规则,很多重复沟通会被省掉。
我会把自定义指令控制在清晰、稳定、长期有效的范围里。不要把临时需求写进去,比如今天要改登录页、这次要加导出按钮,这些应该放在当前 prompt 里。指令文件更适合写项目级约束。
一个好的 .github/copilot-instructions.md 通常包含这些内容。
| 内容 | 是否适合写进指令文件 | 原因 |
|---|---|---|
| 技术栈和框架版本 | 适合 | 长期稳定,影响生成代码 |
| 命名规范和目录结构 | 适合 | 能减少错误路径和风格漂移 |
| 测试框架和运行命令 | 适合 | 能让 Copilot 生成更可验证的代码 |
| 错误处理约定 | 适合 | 项目差异很大,必须明确 |
| 当前 sprint 的临时需求 | 不适合 | 容易过期 |
| 某个 bug 的临时排查结论 | 不适合 | 应该放在当前会话或 issue 里 |
| 特定文件的修改要求 | 不适合 | 应该在 prompt 里显式引用 |
如果项目比较大,我还会把更具体的任务规则拆成 prompt files 或 skills。简单规则放在自定义指令里,复杂任务用专门文件承接。比如代码审查、单元测试生成、数据库迁移检查,都可以各自有一份更详细的说明。
四、注释不是装饰,是局部上下文
很多时候 Copilot 生成代码不准,是因为当前文件里缺少意图说明。类型能告诉它数据长什么样,函数名能告诉它大概做什么,但业务规则和边界条件通常藏在人的脑子里。
这时候注释很有用。这里说的注释不是写一堆废话,而是在即将生成代码的位置,把函数目标、输入、输出和边界写清楚。
比如下面这个注释就比简单写一个函数名有效。
ts
/**
* 根据订单金额、优惠券和会员等级计算最终支付金额。
*
* 规则:
* 1. 订单金额必须大于 0。
* 2. 优惠券只能抵扣商品金额,不能抵扣运费。
* 3. 会员折扣在优惠券之后计算。
* 4. 返回值保留两位小数。
* 5. 任何校验失败都返回 AppError,不直接抛原始异常。
*/
function calculatePayableAmount(input: CheckoutPriceInput): Result<number> {
// ...
}
Copilot 看到这种注释时,生成结果会更接近业务规则。它不需要猜优惠券和会员折扣谁先执行,也不会随手把错误直接 throw 出去。
我会把注释分成三类使用。
第一类是规则注释,放在函数或复杂逻辑前面,说明业务处理顺序。
第二类是边界注释,放在容易出错的位置,说明为什么不能用更简单的写法。
第三类是迁移注释,放在临时兼容逻辑旁边,说明这个逻辑什么时候可以删除。
比如:
ts
// 这里保留旧字段读取,是为了兼容 1.4.0 以前导出的 JSON。
// 新版本保存时只写 `displayName`,不再写 `name`。
const displayName = raw.displayName ?? raw.name ?? '';
这种注释对人有用,对 Copilot 也有用。下次让它改导入逻辑时,它能知道这段兼容代码不能随手删。
不要把注释写成命令清单,也不要写成过度抽象的设计口号。注释越贴近当前业务,越能帮 Copilot 生成符合项目预期的代码。
五、Chat 里要把任务边界说清楚
Copilot Chat 最容易出问题的地方,是用户只写目标,不写边界。
比如:
text
帮我重构这个模块。
这个提示词太宽。Copilot 可能会改目录、抽函数、换依赖、补测试,也可能顺手调整一堆无关代码。最后你看 diff 时,会发现自己不知道哪些改动应该保留。
我更愿意这样写:
text
请基于 #file:src/auth/tokenService.ts 重构 token refresh 逻辑。
目标:
- 把重复的过期时间判断抽成独立函数。
- 保留现有接口返回结构。
- 不修改数据库字段。
- 不引入新依赖。
验证:
- 改完后说明需要运行哪些测试。
- 先输出修改计划,不要直接改文件。
这个提示词没有追求复杂,但它给了 Copilot 需要的边界。目标是什么,不能动什么,验证方式是什么,先做计划还是直接修改,都说清楚了。
我会把 Chat 提问固定成一个结构。
text
上下文:
我正在处理哪个模块,相关文件是什么。
目标:
这次要完成什么。
约束:
哪些内容不能修改,哪些风格必须保留。
输出:
先分析、先计划、直接给代码、还是生成测试。
验证:
需要运行哪些测试,或者如何确认结果。
这个结构适合绝大多数工程任务。写熟以后,提问反而会更快,因为不需要每次临时组织语言。
如果任务很复杂,我会先让 Copilot 做 plan,再让它改代码。先计划的好处很直接:方向不对时,可以在最小成本下纠正。
text
请先给出三步以内的修改计划。确认计划前不要改文件。
这句话能避免 Copilot 一上来就进入执行。尤其是多文件修改,先计划再执行更稳。
六、Copilot Edits 要小步使用
Copilot Edits 很适合处理多文件修改。比如把一个工具函数抽出去,给多个调用点补错误处理,把组件文案迁移到 i18n,把几个相似测试统一成同一种写法。
但 Edits 也容易让 diff 变大。你给它一个很宽的目标,它可能一次修改十几个文件。看起来效率很高,实际上审查成本也会马上上来。
我会给 Edits 设置一个很明确的范围。
text
把当前组件里的日期格式化逻辑抽到 `src/utils/date.ts`。
只修改:
- 当前组件
- 新增的 date 工具文件
- 当前组件对应的测试
不要修改样式,不要调整组件结构。
这种写法能让它集中在一个小任务里。改完以后先看 diff,再决定是否继续下一步。
我一般会把一个大改动拆成几轮 Edits。
| 大任务 | 拆分后的 Edits 任务 |
|---|---|
| 重构登录模块 | 先抽 token 工具函数,再改调用点,再补测试 |
| 国际化页面文案 | 先迁移一个组件,再迁移同类组件,最后整理缺失 key |
| 优化列表性能 | 先减少重复请求,再拆分页状态,再补 loading 和空状态 |
| 调整错误处理 | 先统一 service 返回,再改 UI 展示,再补异常测试 |
这种拆法和 PR 拆分很像。AI 可以帮助改代码,但人仍然要控制变更颗粒度。越是大项目,越要避免一次性让 Edits 改太多文件。
Edits 生成结果后,我会重点看三类问题。
第一,是否改了任务范围外的文件。
第二,是否引入了新的依赖或新的架构选择。
第三,是否把异常路径写得过于理想化。
AI 生成的正常路径通常还可以,边界条件更需要人工检查。
七、上下文工程要覆盖测试和验证
很多人给 Copilot 上下文时,只给实现文件,不给测试文件。这样生成出来的代码可能能跑,但不一定符合项目验证方式。
如果我正在改业务逻辑,会主动把测试文件放进上下文。比如:
text
请基于 #file:src/services/orderService.ts 和 #file:src/services/orderService.test.ts 修改订单取消逻辑。
目标:
- 已支付订单不能直接取消。
- 未支付订单取消后释放库存。
- 保持现有测试风格。
先说明需要新增哪些测试用例。
这里测试文件的作用很大。它告诉 Copilot 当前项目怎么 mock、怎么断言、怎么命名 case。相比让它自己发明测试结构,参考现有测试更稳。
我还会让 Copilot 先生成测试清单,而不是直接写代码。
text
先列出这个改动需要覆盖的测试场景,不要写实现。
这样可以先检查它有没有理解业务边界。测试场景都不对,代码大概率也会偏。
上下文工程不应该只围绕生成代码,也要围绕验证代码。一个好的 prompt 应该包含验证方式,比如运行哪条测试命令、检查哪个页面、看哪个接口返回。否则 Copilot 很容易停在代码能写出来这一层。
八、团队里要把上下文沉淀成规范
个人使用 Copilot 时,很多上下文可以放在脑子里。团队使用时就不能这样。每个人给 Copilot 的规则不一样,生成出来的代码风格也会飘。
我会建议团队至少沉淀三类文件。
第一类是仓库级 Copilot 指令。放在 .github/copilot-instructions.md,写技术栈、目录结构、测试要求、错误处理和代码风格。
第二类是任务级 prompt files。比如代码审查、生成测试、排查构建失败、写发布说明,都可以各有一份固定提示。这样团队成员不需要每次重新写。
第三类是专项 skills 或 agent 说明。比如安全审查、数据库迁移检查、前端组件规范、接口兼容性检查,这些任务可以比普通自定义指令更详细。
团队上下文文件不要写成很长的制度文档。它们要让 Copilot 能用,也要让开发者愿意维护。太长、太抽象、太过期的规则,会比没有规则更麻烦。
我会定一个简单的维护原则:每次发现 Copilot 重复犯同一种错误,就考虑把规则写进上下文文件。比如它总是引入 any,就把 TypeScript 约束写进去;它总是忘记补测试,就把测试要求写进去;它总是改错目录,就把目录规则写进去。
上下文工程最后会变成团队工程的一部分。它不是某个人写 prompt 的技巧,而是项目如何向 AI 表达自己的结构和规则。
总结
GitHub Copilot 的使用效果,很大程度上取决于上下文质量。模型能力当然重要,但在真实项目里,Copilot 更常见的问题是缺少项目规则、缺少关键文件、缺少测试约束、缺少明确的任务边界。
我现在会按这几个层次整理上下文:
- 当前任务用显式引用锁定文件和目录。
- 项目长期规则放进
.github/copilot-instructions.md。 - 复杂任务先让 Copilot 输出计划。
- 多文件修改用 Edits 小步推进。
- 测试文件和验证命令一起进入上下文。
- 团队高频任务沉淀成 prompt files、skills 或 agents。
把这些习惯建立起来以后,Copilot 的表现会更接近真实项目需要。它生成的代码不会突然变得百分百正确,但会少很多通用代码,少很多风格漂移,也少很多不该出现的改动。
上下文工程的目标不是让 AI 替你理解项目,而是让项目用更清楚的方式告诉 AI:这里怎么写代码,哪里不能乱动,改完以后怎么验证。这个能力会越来越重要。