AI编程上下文工程实战:三层架构让模型视野精准对焦
2026/9/10 10:19:20 网站建设 项目流程

最近这半个月,我一直在折腾一个叫context-mode的思路。起因很普通:我把日常工作里大部分“问代码”的活交给了 AI 编程助手,结果它在一个 8 万行左右的项目里给我表演了十几次一本正经的胡说八道——引用不存在的接口、把旧实现当成新实现、修一个 bug 又带出两个新 bug。起初我也以为是模型不够聪明,后来把对话记录翻出来逐条排查,才发现问题根本不在于模型理解能力,而在于我喂给它的上下文是乱的。

所以我真正想聊的不是“怎么调教大模型”,而是把它背后的逻辑抽成一套可以复用的模式:如何让 AI 在正确的时间、看到正确范围的代码。context-mode 这个名字就是这么来的,它解决的问题也很具体——你能不能让 AI 的“视野”像相机一样,自动对焦到当前问题,而不是整个模糊背景。这篇文章适合正在用 AI 写代码但总觉得“效果不稳定”的人,也适合想自己写 IDE 插件、CLI 工具,想认真搞一搞上下文工程的朋友。

1. 一个让我忍无可忍的场景:AI 在正确的代码里找不到正确的上下文

1.1 一次典型的翻车现场

当时我在改一个多人协作的后端服务。有个需求是排查“用户登录 token 在哪些地方做了二次校验”,我随手在 IDE 里打开了某个 Controller 文件,然后把问题丢给 AI。它很快给出一段看起来非常完整的回答,说校验逻辑在AuthController.refresh里,还附了代码片段。

可实际上,真正的校验早就在AuthService的实现类里做完了,Controller 只是层壳。AI 为什么答错?因为它能看到的“当前上下文”就是我当时打开的那个 Controller 文件,而真正承载业务逻辑的 service 文件压根不在它的视野里。它不是在推理,而是在拿着半张地图画路线图。

我后来复盘时发现,这个场景太常见了。现在的 AI 编程工具普遍会收集 IDE 里“当前打开文件”“最近编辑文件”这类信息,但你打开哪个文件往往取决于你手指的操作习惯,而不是当前问题的答案藏在哪里。上下文收集和问题意图之间出现了错位,输出质量立刻崩盘。

1.2 上下文给错,比模型不够强更致命

要理解为什么“上下文错了”会这么致命,得先知道大模型的工作方式。模型一次能处理的内容是有限的,专业说法叫上下文窗口,比如 128K tokens 或 200K tokens。听起来很多,可一个中大型代码仓库动辄几十万行,根本塞不完。就算塞得下,也存在两个被反复验证过的现象:

  • 大海捞针效应:当一段关键信息被埋在一大堆无关文本中间时,模型很可能直接漏看。信息存在,但它“没注意到”。
  • 位置偏好:模型对上下文开头和结尾的内容记得更牢,中间部分容易稀里糊涂。

我习惯用一个生活类比来解释:你叫了一个刚入职的新人帮你排查生产事故,然后丢给他一个几万文件的代码仓库,也没告诉他该看哪几个文件。他会怎么做?大概率是打开最近改过的几个文件、项目入口文件,然后开始猜。AI 也一样,它手里只有几块拼图,自然会脑补出完整图画。

所以问题的关键不是“往窗口里塞了多少信息”,而是“塞进去的信息跟当前问题是否相关”。而相关性这件事,靠自动收集很难做对,因为它本质上是意图判断。

1.3 大多数人的两种做法,和我选择的不同思路

我观察过身边同事使用 AI 编程助手的习惯,基本可以分成两类:

做法典型表现结果
依赖插件自动抓取当前文件“我就打开那个文件问那个问题”跨文件、跨模块的问题频繁翻车
一股脑把整个仓库塞进去“你不是窗口大吗,都给你看”token 消耗飙升,模型注意力被噪声稀释,回答开始“东拉西扯”
手动复制粘贴关键代码每次问之前先翻代码复制准确率最高,但效率太低,多轮对话下来体力活太重

这三种做法的问题,要么是上下文太窄,要么是太宽,要么是太费人工。我想要的是一种显式的上下文模式——在开始一次 AI 会话之前,先明确告诉系统“这一轮你要看哪一层代码”,就像手机里的勿扰模式、飞行模式一样,不同场景切换不同配置。

context-mode的核心就是给 AI 编程引入“模式切换”的概念:全局模式、会话模式、聚焦模式。后面我会把每一层拆开讲。

2. 三层上下文架构:全局、会话、聚焦

2.1 全局模式(Global):让 AI 先懂项目底细

第一层叫全局模式,解决的是“AI 不懂这个项目的基本盘”的问题。

很多 AI 回答不靠谱,不是因为它不理解语法,而是因为它完全不了解这个项目的技术约束和业务约定。比如:这个项目实际上用的是 TestNG 而不是 JUnit 5;所有对外接口的返回结构必须包一层Result<T>;数据库连接不能直接裸用,必须走代理层。这些信息如果 AI 不知道,它写出来的代码再“正确”也是废的。

所以在我的方案里,每个项目根目录都维护一份CONTEXT.md,相当于这个项目的“体检报告”。里面写五类东西:

  • 技术栈和关键依赖版本,以及哪些依赖被禁用了
  • 目录结构说明,哪里放 controller、service、dao、工具类
  • 业务领域名词表,比如“订单”在这个系统里特指什么状态机
  • 编码规约,比如异常处理方式、日志规范、命名习惯
  • 踩坑记录,比如“这个模块的缓存曾经出现过脏数据,改这里必须清缓存”

有人可能会问,为什么不自动生成这份文档?我尝试过,自动生成的CONTEXT.md内容很快会失真,它会混入一堆废弃模块的描述,甚至把已经删掉的老架构写进去。我后来选择人工维护,每次改架构时顺手更新几行,代价很小,但收益极高——AI 每次回答都基于一份稳定的项目事实,而不是靠猜。

2.2 会话模式(Session):让 AI 看到“眼前正在发生什么”

第二层是会话模式,解决的是“AI 看不到你当前正在做的事”的问题。

这一层负责收集和当前这一次开发会话直接相关的动态信息,比如:

  • 当前 git 分支、本次改动涉及的文件列表(git diff --name-only
  • 当前打开的文件、光标停留的位置
  • 最近一次构建报错或测试失败的输出
  • 和当前改动相关的接口签名或函数调用链

适用场景很明确:开发一个新接口、修复一个 bug、做一轮代码评审。我实测下来,git diffgit status的输出是所有动态信息里性价比最高的,因为它精确地反映了“你自己刚改了什么”,AI 结合这些信息能快速理解你的意图。

但这里有个坑:自动收集不能无脑收。我最初的做法是把所有打开过的文件都放进上下文,结果 AI 老是看到一堆无关的测试代码和构建脚本,反而把它们当成主逻辑。后来我加了两条规则:一是文件必须被激活超过 1 秒、或者被编辑过才进入候选池;二是只向依赖关系的上游扩展一层。宁可让它少看,也不要让它看错。

2.3 聚焦模式(Focus):手动 pin 才是真正的杀手锏

第三层叫聚焦模式,也是我认为最核心的一层,解决的是“AI 永远猜不准你到底想让它看什么”的问题。

自动收集做得多好,都取代不了“人工指定”。原因很简单:AI 没有读心术,它不知道你现在真正纠结的是哪一行。比如你正在排查一个诡异的跨服务调用问题,真正有价值的上下文不是整个 service 文件,而是某一个方法里的那几行远程调用代码,外加一个报错堆栈。

聚焦模式的操作方式非常直接:选中一段代码,按快捷键把它 pin 到上下文池;或者直接把报错堆栈喂进去;也可以用 @ 语法显式引用某个函数名,让系统去源码里找到这个函数并拉进来。被 pin 的内容拥有最高的优先级,不会被后面的自动收集内容挤掉。

一个让我印象特别深的例子:有次排查一个空指针,没开聚焦模式之前,AI 一直围绕当前方法里的判空逻辑打转,建议我加各种 if 判断。我把堆栈里第 55 行和调用链上两个方法都 pin 进去之后,它立刻反应过来——空值根本不是当前方法产生的,而是上游某个接口在特定条件下返回了 null。问题瞬间定位。这个案例让我确信:焦点信息手动 pin 一次,胜过自动收集一整天。

2.4 三种模式怎么组合

这三种模式不是三选一,而是分层组合。我把常见场景和推荐配置整理成了下面这张表:

场景组合方式典型上下文内容
新人熟悉项目整体架构全局CONTEXT.md、目录树、核心业务流程图
开发一个独立功能全局 + 会话项目约定 + 当前分支改动 + 相关接口定义
排查疑难 bug全局 + 会话 + 聚焦项目约定 + git diff + 报错堆栈 + 手动 pin 的代码
多轮重构全局 + 聚焦项目约定 + 待重构模块的关键方法

在我实际使用的过程中,百分之八十的任务都只需要“全局 + 会话”就够,只有遇到真正棘手的问题才手动打开聚焦模式。这样既不会让操作变得繁琐,又保留了一个随时可以深度干预的入口。

3. 把上下文塞进窗口之前,我先学会了做取舍

3.1 token 预算是一道必答题

构建 context-mode 时,我绕不开一个现实问题:模型的上下文窗口是有限的,而代码对 token 的消耗比自然语言快得多。我实测下来的经验公式大致是:Java、TypeScript 这类强类型语言,平均每一行代码会消耗 6 到 8 个 tokens;中文自然语言大约每 100 个字符消耗 50 到 90 个 tokens;英文则要便宜一些。

算一笔账:一个 1000 行的 Java 文件,光源码就要 6000 到 8000 tokens。如果一次会话里既要贴 5 个这样的文件,又要保留几轮对话历史,还要给模型的输出预留空间,128K 的窗口看着很宽裕,实际可用空间可能只有 30K 到 60K。一旦超过这个安全线,回答质量就会明显下降——不是报错,而是开始遗忘。

所以我在 context-mode 里对所有进入上下文的条目做了一层结构化封装,类似这样:

{ "path": "src/main/java/com/example/OrderService.java", "startLine": 128, "endLine": 156, "priority": "high", "source": "focus-pin", "timestamp": 1717000000 }

每条上下文都带上文件路径、起止行、优先级和来源。这样既方便压缩模块做决策,也让 AI 能分辨“哪段是用户手动指定的重点,哪段只是自动收集的辅助材料”。

3.2 压缩的三个层次

在 token 不够用的时候,我按下面三个层次做裁剪,顺序不能乱:

  1. 先裁剪无信息量内容:注释、import 块、空行、没有改动过的样板代码,这些对解决问题几乎没有帮助,直接去掉。
  2. 再做摘要化:对于确实相关、但整体太长的方法,保留函数签名、关键判断分支和返回值逻辑,其余部分压缩成两三句话的功能说明。比如一个 80 行的OrderService.submitOrder方法,压缩后可能只剩签名加核心状态流转的几行。
  3. 最后淘汰低价值条目:维护一个“优先级 + FIFO”的队列,低优先级且长时间没被引用的条目,不直接扔进回收站,而是降级成一行路径加摘要。万一后面用到,可以按路径快速找回。

这里的关键原则是:宁可让 AI 看到一小块“局部的完整”,也不要让它看到一大片“模糊的全部”。局部完整能推理,模糊全部只会让它自由发挥。

3.3 自动收集时的噪声治理

自动收集最怕的是“无意识噪声”。在开发 context-mode 的过程中,我花了不少时间清理两类噪声。

第一类是无关文件混入。自动扫描当前工作区时,测试文件、构建脚本、自动生成的 DTO、package-lock.json 这类东西都会进来。它们看起来“都在项目里”,但跟当前问题关系可能为零。我的解决办法是维护一个文件白名单,只收集 controller、service、repository 这类真正的业务代码层;同时给自动收集器增加一个“引入理由”字段——它必须说明“为什么这个文件与当前问题相关”。理由牵强的自动条目,权重会大幅降低。

第二类是旧的会话信息残留。多轮对话之后,前面几轮的内容可能已经过时了,比如 AI 上一轮让你改了 A 文件,这一轮你要改的其实是 B 文件。如果没有清理机制,A 文件的内容会一直占着上下文窗口。我的方案是:超过 N 轮未被再次引用的自动收集条目,自动触发一次“压缩总结”,把它的摘要保留,原始内容释放掉。

4. 实测对比:同一批 issue,开与不开 context-mode 差多远

4.1 我不想只凭感觉说话

光说“效果变好了”没有说服力,所以我专门做了一组对比测试。测试项目就是我自己维护的那个 8 万行左右的后端服务,模型用的是同一个 API,prompt 也保持一致性。我从真实任务池里挑了 5 个问题,包括:新增一个分页查询接口、修复一个空指针异常、重构一个工具函数、排查一个事务不生效的问题、给旧的订单模块补单元测试。

对照组使用默认模式,也就是只让 AI 看到“当前打开文件 + 所有对话历史”;实验组采用 context-mode 的推荐配置,默认开全局 + 会话,遇到卡壳再手动加聚焦。

4.2 数据结果

指标对照组实验组
一次通过的可用率32%71%
平均往返次数(解决一个问题)4.2 次2.1 次
单任务平均 token 消耗118K96K
人工纠错时间每次超过 20 分钟基本只要复核一次

有两个数据很有意思。第一,实验组多花了“收集上下文”的功夫,但总 token 消耗反而下降了 18% 左右。原因是无效往返少了,模型不再反复问“你这个文件在哪”“能贴一下那个方法吗”,省下来的 token 远大于收集成本。

第二,一次通过率从 32% 涨到 71%,剩下的 29% 里有一半还是因为需求本身表述模糊,不完全怪上下文。说明只要把视野喂对了,AI 编程的可靠性就能从“偶尔翻车”提升到“基本可用”的级别。

4.3 一个值得仔细看的 case

这里我拆解一下那个空指针异常的 case,因为它是体现聚焦模式价值的最佳例子。

现象是:某个接口在特定参数下会抛出空指针,堆栈指向OrderService.getOrderDetail方法的第 55 行。对照组里,AI 反复围绕第 55 行做文章——检查order对象是不是 null、要不要加判空条件、是不是并发问题。我按它说的改了两次,问题依然存在。

切换到 context-mode 之后,我做了一个关键动作:把堆栈完整 pin 进聚焦池,再把调用链上OrderApiOrderQueryHandler两个方法也 pin 进去。AI 看到完整调用链之后,只用了一轮就定位到真正的原因:OrderQueryHandler在某些条件下返回了一个空对象,而这个空对象是从外部接口透传进来的,OrderService里怎么判空都没用,因为问题根本不在这一层。

这个场景让我意识到,context-mode本质上不是在“帮模型变聪明”,而是在“帮模型节省排除干扰的时间”。手动 pin 一个调用链,等于告诉它“别猜了,问题就在这条路上”。

5. 落地过程中踩过的三个大坑

5.1 全量塞入:窗口没爆,效果先崩了

我最早犯的错误,是把“上下文窗口大”误当成“可以什么都往里塞”。有一段时间我为了省事,干脆让脚本把整个 src 目录下所有文件都转成上下文,丢给长窗口模型处理。

结果非常惨。AI 开始大量引用无关内容,最典型的一次是它在回答业务逻辑问题时,引用了测试类里 mock 出来的假数据,还一本正经地标注了出处。我冷静下来分析原因:不是长窗口模型不能处理长文本,而是当大量低相关度文本混入时,模型的注意力会被稀释,真正关键的信息反而沉底了。这和让一个新人一下子读 50 个文件,他大概率什么都记不住是一样的道理。

后来我把策略改成了“先给目录树,再按需深入”:初始只给文件结构概览,AI 可以根据目录结构主动要求查看具体文件,或者由收集器根据问题关键词做一次检索。效果立刻恢复。

5.2 自动收集有时聪明得过了头

自动收集还有一个让人哭笑不得的问题,就是它会把“表面相关”的文件当成“深度相关”的文件收集进来。

举个例子:我的 Python 项目里有两个同名但不同用途的config.py,一个在业务模块,一个在测试工具模块。自动收集器通过关键词匹配,把测试工具里的ConfigParser当成了业务配置,推荐给 AI。结果 AI 给出的方案里全是从测试配置模块里读取参数,直接跑不起来。

这个问题的根源是同名文件和模糊路径带来的歧义。解决方式是我在收集器里强制使用模块全限定名(比如module.core.events而不是events),同时要求收集器给出“为什么收集这个文件”的理由,并展示在界面上。用户一眼就能看出哪个收集不合理,可以一键移除。这时候我又想起那句话:自动化的前提是保留人工可监督的通道,否则就是盲飞。

5.3 模式切换把用户 pin 的数据给弄丢了

这是我踩过最难受的坑,因为它属于工程状态管理的低级失误。

当时我实现了“全局 / 会话 / 聚焦”三种模式的互斥切换:从聚焦模式切回会话模式的时候,我写了一段重置逻辑,把整个上下文池清空了。结果用户手动 pin 过的代码片段、前几轮费劲贴进去的堆栈信息,全部消失。当时在项目里跑了一上午的排查,所有手动焦点全部丢失,只能重新来一遍。

后来我把架构改成了“模式只影响自动收集策略,不影响用户静态数据”。具体做法是把上下文池拆成两个独立的区域:动态区负责会话和自动收集,静态区负责用户手动 pin 的内容,两者互不干扰。切换模式时,只改动态区的收集规则,静态区原封不动。这个改动之后,再也没有出现过焦点丢失的情况。

6. 从零复刻一套 context-mode,最少只需要半小时

6.1 不写代码的极简版

很多人一听到“上下文工程”就觉得很高级,其实最低成本的方案只需要三样东西:一个文件、一条命令、一个习惯。

第一步,在项目根目录创建一个CONTEXT.md,花十分钟写上十条项目事实:技术栈、目录结构、接口返回格式、数据库访问方式、常见坑位。这相当于给 AI 一份“项目入职培训手册”。第二步,每次开始 AI 会话时,在第一条消息里让 AI 读取这个文件。第三步,在涉及具体改动时,运行一次git statusgit diff,把输出贴进对话。

这里有一个可以直接抄走的 Prompt 模板:

先阅读项目根目录的 CONTEXT.md,理解项目背景和约定。 然后结合我提供的 git diff 内容分析问题。 除非 AI 明确需要,否则不要使用我没提到的其他文件。

别小看这三步,它已经覆盖了全局模式 + 会话模式的核心能力。我身边很多同事在没引入任何插件的情况下,靠这个模板就把 AI 回答的可用率拉高了一大截。

6.2 用脚本自动化一半

如果你不想每一次都手动贴git diff,可以写一个不到 50 行的小脚本,自动组装上下文块。大致思路是:

  • 收集当前分支名、最近 5 条 commit message
  • 收集git diff --stat和具体git diff内容
  • 收集当前光标所在文件的函数名(可以用 LSP 的textDocument/documentSymbol接口拿)
  • 把以上内容拼接成一段纯文本,复制到剪贴板

比如这样的一个片段:

#!/bin/bash echo "## Branch: $(git branch --show-current)" echo "## Recent commits:" git log --oneline -5 echo "## Changed files:" git diff --name-only echo "## Diff content:" git diff --stat

这段脚本跑完,复制输出到对话里,AI 就已经掌握当前会话最关键的动态信息了。纯手工版和半自动化版之间的差距,主要是省掉了每次敲 git 命令的体力活。再往后,如果你想更进一步,就可以考虑在 IDE 里写一个插件,监听文件切换事件和光标移动事件,把收集逻辑做成常驻服务。这时候 LSP 的能力就可以补进来了:跳转到定义、查找引用、列出某个函数的所有调用方,这些符号级别的信息比简单的字符串匹配准确得多。

6.3 下一步可以怎么扩展

做到这里,基础版 context-mode 已经能为你所用。如果你想让它在更多场景下发挥作用,可以从三个方向继续扩展。

第一个方向是语义检索。本地起一个 Embedding 模型,把项目源码切成代码块并向量化,每次遇到问题时,先用当前错误信息做语义召回,把相关的代码块自动拉进聚焦池。这个思路能弥补关键词匹配的不足,但它需要额外维护一套向量索引,适合项目规模更大、模块关系更复杂的场景。

第二个方向是跨会话沉淀。把每次排查过程中 AI 发现的“坑位”自动追加到CONTEXT.md里。比如这次发现某个模块不能用 MyBatis 的二级缓存,下次直接写进项目事实,避免后续再踩。这样时间越长,CONTEXT.md越值钱,AI 回答的本地化程度也会越来越高。

第三个方向是适配不同模型。不同模型的上下文窗口差异很大,有些只有 32K,有些到了 1M。对于窗口大的模型,可以适当放宽自动收集的阈值;对于窗口小的模型,则要更激进地做压缩和摘要。把上下文组装策略和模型能力解耦,会让这套模式更耐打。

最后说一点个人体会。context-mode这个项目做下来,我对 AI 辅助开发的认知改变挺大的:过去我总觉得“效果不好就换更强的新模型”,现在我会先检查“我给的上下文是不是配得上这个模型的水平”。真正好用的 AI 编程,不是靠玄学调提示词碰运气,而是把“它每天能看到什么”当成工程来设计和维护。一个小技巧是,每次切换模式时,我心里会默念一遍“我现在只让它看这一层”,多花十秒想清楚边界,AI 给出的答案质量就会有肉眼可见的差异。如果你也在折腾 AI 编程助手,或者想做一个自己的上下文管理工具,希望这篇文章能给你提供一条值得试探的路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询