做了几年一线开发,我的工作流里最常出现的尴尬场景,就是AI辅助编程时它“断片”。键盘敲得飞快,模型却完全不记得自己半小时前提过的架构方案,对我刚从配置文件里复制出来的关键内容也只回一句“根据当前信息无法确认”。后来我认真复盘,发现根子不在某个具体工具上,而在整个项目从头到尾没有人认真管理过“上下文”这一层。
所谓context-mode,我理解成一套专门管理上下文状态的机制:用哪些方式收集项目信息、按什么节奏更新、以哪种粒度和顺序注入给模型,让AI或任何依赖上下文的工具始终保持对项目全貌的正确理解。它比“多贴几段代码”这件事系统得多,稳定得多。
这篇文章是我把这套机制从零落地的完整笔记,包括目录设计、脚本实现、实测参数和一堆踩过的坑。适合正在用AI辅助写代码、做代码评审,或者折腾Agent、自动化流程的开发者和技术团队,尤其是那些一天要和AI对话几十次的重度用户。看完你可以直接照抄一套适合自己的context-mode工作流。
1. 为什么需要context-mode:从一次重构事故说起
1.1 那次“上下文丢失”事故
上个月我接手一个老中后台项目,Vue加Node,二十多万行代码。我打算让AI助手协助做模块拆分。第一轮对话我把目录结构、核心接口、数据库关系全部贴进去,它很快指出了几个模块之间的循环依赖,方案基本靠谱。第二轮我让它动手改第一个模块时,它突然开始推荐“新建一个service层把逻辑全部下沉”,而这正是我在第一轮明确否掉的方向。我把之前的结论重新贴一次,它道歉,然后接着改,改到第三个文件,又把一个已经标记废弃的接口拿来当主链路。来回折腾了大半天,进度比我自己动手还慢。
这事的根源不是模型智商,而是我没有为它建立一套持续、稳定、正确的上下文来源。对话窗口里散落着原始文档,我自己手动补充的信息又支离破碎,它每一次回答都在做盲人摸象。后来我仔细想了想,如果我在一开始就把项目的关键事实写进一个独立的、结构化的快照文件,每次对话都从这个文件开始,事情完全不会走到这一步。
1.2 三个真实痛点:丢失、过时、失焦
这次事故背后其实藏着三个典型痛点。
第一是丢失。切换会话窗口或者上下文一长,前面敲定的技术决策会被挤出有效窗口,模型就“失忆”了。丢失的还不只是决策,还有我们对某些代码“为什么写成这样”的原因记忆,这些东西散落在聊天记录里,别人根本看不到。
第二是过时。我给它看的项目信息是静态的,可代码每天都在变。上午导出的依赖关系,下午局部就失效,快照一旦过期,误导比没有更可怕。模型拿着昨天的接口定义去改今天的代码,报错是必然的。
第三是失焦。为了让AI理解一个模块,我把整个项目说明、几十个目录结构全塞给它,结果它被大量无关信息干扰,反而抓不住重点,回答里充满泛泛而谈的“最佳实践”。这种回答看着正确,实则完全没有针对我们的代码库。
这三个痛点表面上是模型能力不足,实际上全是工程问题。代码有Git管版本,依赖有锁文件管一致性,但“项目上下文”这个最宝贵的资产,绝大多数团队都是即用即扔、无人维护的状态。
1.3 context-mode不是玄学,是一种工程手段
我给自己定的目标,是把上下文这件事变成一套有章法的机制,核心是三个词:可快照、可分级、可轮换。
可快照,指的是在关键节点把项目状态提炼成一份结构化、可阅读的文本,而不是散落各处的原始文件。可分级,指不同任务只注入对应粒度的上下文,避免上下文堆成信息垃圾场。可轮换,指窗口紧张的时候,用更精炼的摘要替换掉冗长的原始内容,同时保住最重要的决策记录。
这套思路听起来简单,落地才知道细节特别多。谁生成快照、快照粒度多大、多久更新一次、轮换时先砍谁,每个问题不试几轮根本拿不准。好在这些坑我都替你踩了一遍。
2. 核心机制拆解:context-mode的三块基石
2.1 上下文快照:给项目拍一张“可以阅读的CT”
Git保存的是代码变更,上下文快照保存的是“项目当前的可读状态”。它和Git diff完全是两码事:diff只告诉差异,快照要告诉模型系统全貌。我试过很多格式,最后固定成下面这种Markdown结构:
# 项目快照 - 订单中台 (2025-06-28) ## 技术栈与架构 - Vue3 + Pinia + Node/NestJS + PostgreSQL - 模块边界:gateway / order / payment / user ## 核心数据流 用户下单 -> gateway鉴权 -> order创建订单 -> payment发起支付 -> 支付回调 -> order更新状态 -> event-bus通知user模块 ## 关键决策日志 - [2025-06-20] 放弃MySQL分表方案,改用PostgreSQL分区表 - [2025-06-25] 所有对外接口统一加request_id中间件 ## 当前已知技术债 - payment模块超时重试逻辑有bug,重试次数硬编码为3 - order模块的领域事件未落库,重启会丢事件 ## 活跃分支与待办 - feat/refund 正在开发退款流程 - 本周待办:补order模块单元测试这样一份快照,模型读完就能建立系统心智模型。快照怎么生成?我把它分成两条腿走:机器抽取加人工维护。机器抽取负责客观事实,比如目录树、函数列表、最近提交记录;人工维护负责主观判断,比如决策日志、技术债、下一阶段目标。两者缺一不可,纯自动生成的快照没有灵魂,纯手写的又跟不上代码变化速度。
2.2 分级注入:把“全量上下文”改成“按需上下文”
很多人的第一反应是“让AI看全部信息不是更好吗”,实测下来完全不是。模型对上下文里内容的注意力不是均匀分配的,塞入大量低质量、低相关信息,反而会稀释关键信号的权重。我给上下文做了四个级别,按任务定制注入:
| 级别 | 内容 | 典型任务 |
|---|---|---|
| L0 项目全貌 | 架构图、技术栈、核心数据流、决策日志 | 架构评审、新需求规划 |
| L1 模块详情 | 模块接口、数据模型、依赖方向 | 功能开发、跨模块改造 |
| L2 文件级 | 具体函数签名、配置项、局部算法 | 修BUG、代码审查 |
| L3 对话级 | 本轮对话记录、即时提问 | 交互式问答、调试过程 |
我自己的习惯:做一个新功能,注入L0加L1;改一个已知文件的BUG,注入L0的摘要加L2;纯粹问一个配置项含义,L2就够。这个道理跟给设计师看需求是一样的——你只会给他需求文档和相关设计规范,绝不会把公司考勤制度也一起递过去。
2.3 轮换压缩:让窗口预算花在刀刃上
现在主流模型的上下文窗口动辄几十万token,听起来很大,真用起来还是不够。我建议把窗口预算分成三块:快照与参考资料占40%,历史对话占30%,留给模型生成回复的空间占30%。后两者被占满后,模型生成的回答质量会肉眼可见地下降,甚至出现自相矛盾。
轮换压缩的核心是分层保底。优先级永远是:L0快照大于L1模块详情大于L2文件级大于L3对话历史。窗口一紧张,先从L3旧对话开始折叠,把多轮问答压成一段事实摘要;还不够,再把L2的原文替换成函数签名和关键注释。
我实测了一个数字:一个三万行代码的项目,L0快照控制在3000到5000个token以内最舒服,L1模块详情单次控制在8000到12000之间,超过这个量,模型对细节的把握就开始飘。记住一个原则:快照越小越容易被完整吸收,压缩轮换的最终目标不是“塞得下”,而是“看得懂”。
3. 实操记录:一套可复现的落地配置
3.1 目录与命名:先立规矩再写代码
我把这套东西放在项目根目录的.context/文件夹里,结构如下:
.context/ ├── snapshots/ │ ├── latest.json │ └── archive/ │ ├── 20250628-1015-order-refund.json │ └── 20250628-1530-payment-test.json ├── rules/ │ ├── global.md │ └── code-style.md ├── logs/ │ └── decisions.md └── inject/ └── templates/ ├── feature.md ├── bugfix.md └── review.md命名规范一开始我就踩过坑。最早我图省事,文件名就叫snapshot.json,每次直接覆盖,结果两周后想追溯“当时的架构方案是什么样”完全查不到。后来改成时间戳-任务标签.json的格式,加进archive目录,这才把历史决策留下来了。别小看这步,没有归档的快照,时间一长就变成了不可信的一堆过期文件。
3.2 快照脚本:自动化生成与人工校准
我写了一个Python补充脚本,做三件事:遍历src目录生成树状结构并抽取文件头部注释,跑git log获取近期提交,扫描代码里的TODO和FIXME标记,最后汇总进快照的自动采集区。
#!/usr/bin/env python3 """生成项目上下文快照的辅助脚本(自动采集部分)""" import subprocess from pathlib import Path def build_tree(root: Path, prefix: str = "") -> list[str]: lines = [] for item in sorted(root.iterdir()): if item.name.startswith((".git", ".context", "node_modules")): continue if item.is_dir(): lines.append(f"{prefix}{item.name}/") lines.extend(build_tree(item, prefix + " ")) else: lines.append(f"{prefix}{item.name} ({item.stat().st_size}B)") return lines def git_log(limit: int = 15) -> str: result = subprocess.run( ["git", "log", "--oneline", f"-{limit}"], capture_output=True, text=True ) return result.stdout def scan_todos(root: Path) -> list[str]: todos = [] for f in root.rglob("*.{ts,js,vue,sql}"): try: lines = f.read_text(errors="ignore").splitlines() except Exception: continue for num, line in enumerate(lines, 1): if "TODO" in line or "FIXME" in line: todos.append(f"{f.relative_to(root)}:{num} {line.strip()[:80]}") return todos[:20] if __name__ == "__main__": base = Path(".") print("# 自动采集区") print("## 目录结构(简版)") print("\n".join(build_tree(base)[:40])) print("## 近期提交") print(git_log()) print("## TODO/FIXME") print("\n".join(scan_todos(base)) if scan_todos(base) else "无")脚本的输出再和人工维护的决策日志拼接到latest.json里。脚本不追求完美,它追求的是提供关于当前状态的客观锚点。最核心的决策日志,我每周五下午花十五分钟手动更新一次,记录本周踩过的最大的坑和定下来的关键决定。这种频率不高,但足够维持快照的含金量。
3.3 参数调优:实测比较稳的配置组合
落地过程里我做了几组对照测试,配置参数对效果的影响很大。我把验证过的组合整理成一张表:
| 配置项 | 我的实测值 | 说明 |
|---|---|---|
| 快照更新频率 | 代码活跃期每次commit后刷新;稳定期一天一次 | 频率太高浪费,太低失真 |
| L0快照大小 | 3000-5000 token | 超过这个量,模型开始忽略细节 |
| 注入位置 | 放在system提示词的首部 | 放在用户消息开头容易被后续淹没 |
| 快照格式 | Markdown表格加短段落 | 避免大段无结构文字 |
| 轮换阈值 | 上下文用量达70%时启用压缩 | 提前压缩比爆窗后补救稳定 |
注入位置这个发现很关键。我一开始把快照拼在用户消息的开头,结果模型有时候会忽视它,直接把后续新增的对话当最高优先级。后来我换成放在系统提示词里,并明确写一句“以下内容是项目当前状态的权威描述,任何回答必须以此为基准”,效果立刻不一样,模型两个小时内再没有出现过“忘记项目架构”的情况。
另外,我会在每次对话前做一次“三件事校验”:确认快照是最新的,确认本次任务对应的注入级别,确认模型复述一遍它理解的现状。第二步和第三步我用一个简单的Prompt模板固定下来:
请先基于我提供的项目快照,用不超过五条要点复述你对当前项目状态的理解,然后回答我的问题。模型如果复述出的状态跟我理解的不一致,那说明快照写得不到位,而不是AI理解力差,我会先去补快照。这套流程跑了两周之后,AI参与重构的返工率体感下降了五成以上。
4. 常见问题与排查技巧实录
4.1 “明明更新了快照,AI还在用旧信息”
这个坑我遇到过不止一次。脚本跑完了,latest文件也变了,但对话里的模型还是拿着旧信息回答。排查下来有两个原因:一是我把快照放进了用户消息的末尾,模型在处理长对话时对早期内容衰减严重;二是注入时把新快照和旧快照同时塞了进去,模型自己选择了置信度更高的旧版本。
解决办法很直接:每次对话只注入当前版本的快照,archive里的历史快照绝不加入正常对话,只在排查问题需要追溯时单独使用。同时把快照放到系统提示词的首部,并在快照开头写上一行更新时间。
> 快照生成时间:2025-06-28 15:30 > 系统提示:以下描述为唯一权威现状,若与对话历史冲突,以本快照为准。这一行字加上去之后,模型用旧信息的现象几乎消失。它相当于给上下文里所有冲突信息加了一个仲裁规则,让模型明确知道该信谁。
4.2 上下文窗口被撑爆,直接报错
我的一个自动化任务在跑批量代码审查时,连续注入了二十多个文件的完整内容,结果窗口直接撑爆,任务崩溃。这个问题的根源是我没有在脚本里做Token预算控制。
现在我的做法是:每个文件内容在注入前先估算token数,超限的文件一律先做压缩,只保留函数签名、导出接口和关键注释。整个批量任务有一个总预算线,一旦逼近上线,自动跳过那些低优先级的检查项,优先保证核心模块的覆盖。预算表长这样:
| 阶段 | 预期消耗 | 实际控制 |
|---|---|---|
| L0快照 | 4000 | 不超过5000 |
| 单文件L2内容 | 1000-2000 | 超过3000先压缩 |
| 对话历史 | 按轮数递增 | 超过预算后折叠为摘要 |
| 模型回复余量 | 30% | 低于20%立刻停手 |
4.3 注入多了反而被带跑偏
还有一次特别有意思,我给AI塞了一份大而全的快照,里面包含了整个平台的模块清单和规划路线图。AI在回答一个具体的订单状态机问题时,突然引入了“用户增长模块”的设计理念,还建议我用事件溯源重构状态机。纯属被无关上下文带偏了。
从那以后,我每次不只注入L0,还加一段“本次聚焦范围”的标记,用分隔线把本次任务相关的部分单独框出来。
## 本次任务聚焦范围 - 只关注 order 模块的状态机实现 - 涉及文件:src/modules/order/state-machine.js - 暂不考虑支付、退款、用户模块的交互这个聚焦标记帮我解决了80%的带偏问题。AI拿到这个信息后,即使快照里有其他模块的内容,也会主动忽略。
4.4 团队协作:大家的“上下文”版本不一致
有段时间我和同事各自维护自己的快照,内容对不上。我按我的架构理解让AI生成代码,同事按他的理解评审,两边经常在关键设计上吵起来。后来我们在快照文件的顶部增加了一个owner字段和last review字段,每次改动都要过PR评审,而且只有一个人拥有“写过快照的权限”。
快照文件不再是个人私藏,而是像代码一样纳入版本管理。现在任何改动都有历史记录,谁在什么时间更新过哪条决策,Git日志清晰可查。团队协作下的上下文管理,本质不是技术问题,而是流程问题。
5. 最后分享几点体会
这套context-mode跑顺之后,我最大的感受是:它改变的其实不只是AI的输出质量,也在逼着我重新梳理自己对项目的理解。每当我写不出来一条清晰的决策日志时,往往不是语言组织问题,而是我自己也没想清楚这个设计到底为了什么。
如果你也想试,我的建议是别一上来就追求完美的自动化。先手工建一个snapshot文件夹,用最简单的Markdown写一份L0快照,手动跑三天,等你觉得“每次对话前看一眼快照”已经成了肌肉记忆,再上脚本、再定分级、再做轮换。从小闭环滚起来,比一次规划一个大系统稳得多。
最后再分享一个小技巧:很多编辑器现在都支持用户级的代码片段或快捷指令,我把“输入快照信息并让模型复述理解”做成了一个快捷命令,一键触发。实测下来,这个动作让每次AI对话的跑偏率降得最明显。比起研究各种花哨的提示词工程,先把上下文本身管好,收益反而来得直接。