先说个我天天遇到的痛点:在终端里用 AI 编码助手的时候,它总是答非所问。你明明在某个项目的分支上改了三个文件,AI 却以为你在写另一个老代码,开口就是“这个 module 不存在”。问题十有八九不是模型不行,而是它缺乏上下文。我后来写了一个小工具,专门处理这种事,名字就叫 context-mode。它的核心功能很简单:把“当前工作环境里到底发生了什么”收集成一个结构化、可直接喂给 AI 的上下文块,同时支持不同的“模式”——日常开发、代码评审、写文档各自用不同的采集策略。
这篇文章会从为什么需要它开始,讲清楚它背后是怎么设计的,再给出一份最小可用版本的实现,最后把我踩过的一些坑直接列出来。适合谁看?如果你是重度依赖 AI 辅助编码、又经常在终端和编辑器之间切换的开发者,这篇文章应该能帮你省掉很多重复贴代码、解释项目背景的时间。
1. 为什么需要 context-mode:问题拆解与方案对比
1.1 终端场景里的“上下文断裂”
先还原一下实际工作流。我在终端里敲命令,查 git log,跑测试,然后打开一个 AI 对话窗口问问题。这个过程中,AI 其实完全看不见我在干什么。它只知道我手动贴进去的那几行代码、那一句话。可我问的问题往往隐含了大量背景:当前分支修的是什么、刚才测试报了什么错、这个文件处于什么状态。
这种“上下文断裂”在纯终端场景里尤其明显。在 IDE 里,插件还可以拿到打开的文件、选中的代码、项目的语言服务信息。但在终端里,很多 AI 助手只是把 stdin 里的文本和你的问题拼在一起,剩下的全靠猜。
我印象很深的一次:我在重构一个订单模块,把订单状态的枚举从 int 改成字符串常量。我让 AI 帮我生成迁移脚本,结果它按照旧枚举值输出了一堆switch (status)。不是它笨,而是它完全不知道我刚把枚举定义改了。真正的问题是我没有给够上下文,而不是它不会写。
后来我开始尝试手动把上下文塞给 AI:复制项目结构、粘贴 git diff、把关键文件整个糊进去。好一点,但很痛苦。项目一大,复制的东西又长又乱,AI 抓不住重点,我发消息前还要自己先删减一遍。做得多了,我就意识到需要有个东西来标准化这个过程。
1.2 常见方案与对比:手动粘贴、全局提示词、context-mode
我的习惯是把一个方案放进对比表再决定方向。这里我也把几种常见做法列一下,方便你看清我为什么最终写了新工具。
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 手动复制粘贴 | 准确,自己知道当前重点 | 繁琐、易漏、文件多时很累 | 短会话、一次性问答 |
| 固定全局提示词 | 一次配置,通用性强 | 无法感知当前目录、分支、文件状态 | 通用知识问答 |
| IDE 插件自动注入 | 智能,能拿编辑器内部状态 | 绑定编辑器,终端里没法用 | 主要在 IDE 内工作 |
| context-mode | 终端无关、可配置、可切换模式、可组合 | 需要自己维护配置 | 重度终端用户、多项目切换 |
全局提示词是我最早用的方式。我在 AI 工具的设置里写了一大段:“你是一个资深后端工程师,请给出简洁的代码方案……” 这套东西解决了一部分问题,比如风格偏好、输出语言。但它对“当前项目”完全无感。你的提示词是死的,而上下文是活的。真正值钱的上下文不是通用规则,而是这一个小时里你在哪个仓库、动过哪几个文件、git 状态是什么。
context-mode 想要填的,正是这个“活的上下文”。它不替代模型,也不替代 IDE,它在中间做一层收集和整理:把散落在 git、文件系统、shell 历史里的状态收集起来,变成一个结构清晰的文本块。你再决定要不要把这个文本块发给 AI。
1.3 我为什么把它做成本地命令行工具
第一版我考虑过做成 VS Code 插件,因为插件拿编辑器状态最容易。但实际用了几天就放弃了:我并不是整天泡在 IDE 里,很多时候我在 SSH 到远程机器、在 tmux 里切来切去、在 Neovim 里改代码。插件只能覆盖一块,命令行工具却能覆盖所有场景。
做成 CLI 还有一个额外好处:它可以被管道化、脚本化、打包进工作流。比如我可以用一个 shell alias 快速生成上下文块,也可以把一个关键的上下文块保存成文件,下次接着用。它不绑架你的工具链,你可以只把它当作一个“上下文生成器”来使,后面接任何 AI 工具都行。
另一个理由是调试方便。页面里的插件出了问题,你得开 DevTools;CLI 出了问题,直接跑一次就知道,输出对不对一眼就能看到。
2. 核心设计与关键实现
2.1 整体流程:收集、压缩、注入、切换
context-mode 的工作流程可以拆成四段。
第一段是收集。它需要把当前环境里的关键信息读出来:当前目录名、项目根目录、git 分支、最近一条提交、未提交的改动文件列表、最近执行的命令、指定目录下的文件结构。收集阶段的关键是“知道什么值得收”。
第二段是压缩。原始信息可能很庞大,比如一个仓库有几千个文件,git diff 可能有几百行。如果全量塞进上下文,信息量大了反而会让 AI 抓不住重点。压缩阶段会按配置把内容裁剪、摘要、优先级排序,最后生成一个不超过指定规模的文本块。
第三段是注入。context-mode 本身不负责跟模型交互,它只负责把生成好的上下文块交给用户。你可以让它输出到屏幕、复制到剪贴板、写进指定文件,或者直接拼进一个提示词模板里。
第四段是切换,也是“mode”这个名字的由来。不同工作场景需要不同的收集策略。日常开发时我要看分支状态和最近改动;代码评审时我要看 git diff 和关键文件;写文档时我要看目录结构和 README。如果每次手动去调配置,就没有意义。所以 context-mode 把一组采集规则打包成一个“模式”,一条命令切过去。
2.2 上下文来源:哪些信息值得收集
实际采集的时候,我总结出最有用的几类信息,按优先级从高到低排列:
- git 状态:当前分支、当前 commit、工作区改动列表、diff。代码上下文里最重要的一项。
- 项目基本信息:项目名、目录结构、README 摘要。AI 需要知道“这个项目是干什么的”。
- 正在编辑的文件:在当前编辑器里打开的文件路径,以及这些文件的部分内容。如果做不到自动获取,至少可以收集最近修改的文件列表。
- 最近执行的命令:shell 历史里与项目路径相关的最近命令,比如
pytest、npm run build。这能帮 AI 推测用户正在做什么。 - 用户自定义补充:比如一个
CONTEXT_NOTES.md,里面写清楚项目的设计约束、术语、要避免的坑。这个非常重要,相当于给 AI 一本手册。
我见过有人把整个项目文件全部塞进上下文,效果反而很差。信息熵太高,AI 不知道你要问什么。真正有效的做法是:让上下文反映“你此刻在做什么”,而不是“这个项目的全部事实”。
2.3 Token 预算与截断策略:怎么防止上下文爆掉
模型都有限制,少则几千 token,多则十几万。但即便上下文窗口很大,塞满冗余信息也会影响回答质量,还增加延迟和费用。所以 context-mode 需要一个 token 预算。
我常用一个估算公式:英文大约 4 个字符算 1 个 token;中文因为不像英文有空格分词,保守一点按 2 个字符算 1 个 token。这个估算不用精确,正常误差在 20% 以内,对裁剪判断已经够用。
假设我给 context-mode 设定的上下文预算上限是 6000 个字符(不是 token)。接下来需要做一个分配:
| 内容类型 | 分配字符数 | 说明 |
|---|---|---|
| 项目基本信息 | 800 | 项目名 + 目录结构 + README 摘要 |
| git 状态 | 1500 | 分支、最近提交、改动文件列表、关键 diff |
| 最近修改文件 | 2500 | 最多 5 个文件,每个文件最多截取 500 字符 |
| shell 历史 | 700 | 最近 10 条相关命令 |
| 用户补充说明 | 500 | CONTEXT_NOTES 精简版 |
这里我把“最近修改的文件内容”放在了核心位置,因为编码问题时 AI 最需要看到的就是正在改的代码。如果字符数超过预算,优先裁掉的是目录结构里不相关的部分,然后是无关文件的正文,最后才动 git diff。
一个我摸索出来的技巧是:把上下文的开头和结尾留给最重要的信息。模型对开头和结尾的内容通常更敏感,中间部分容易被忽略。所以 project 信息和 git 状态放在最前面,最近文件的代码内容放在最后面。结构大概就是这样:
项目基本信息 git 状态 最近执行的命令 用户补充说明 最近修改文件内容2.4 context 的“模式”到底是什么:多配置管理与快速切换
一开始我并没有“模式”这个概念,只有一个默认配置。后来发现不是所有场景都需要同样的内容。比如我自己写博客时不需要 git diff,更想要当前文章目录里有哪些未发布的草稿;代码评审时则不需要 shell 历史,更需要完整的 diff。
于是我引入了一个很朴素的概念:每个模式就是一份 YAML 配置。配置里写清楚要收集哪些来源、每个来源的参数、输出格式、字符上限。切换模式只是切换配置文件,并不需要重新设计逻辑。
通常我会配置这么几个模式:
dev:日常开发。收集 git 状态、最近修改文件、shell 历史。review:代码评审。收集 git diff、最近提交、关键文件的改动内容。doc:写文档。收集目录结构、README、特定目录下的新文件。
模式之间可以快速切换,也可以叠加。比如ctx --mode review --extra note表示在 review 基础上额外加入用户备注。这比给一个工具堆无数个开关参数要清晰得多。
到这里,项目的整体设计就算清楚了。接下来我会带你直接看一个可运行的版本。
3. 从零搭建一个 context-mode 的最小可用版本
3.1 环境准备与项目结构
我的实现用的是 Python,因为它在文本处理、子进程调用上都很省事,而且任何类 Unix 环境基本都有。如果你更习惯 Node,也可以参考同样的设计思路,语言本身不是重点。
项目结构我保持得很小:
context-mode/ ctx.py # 主入口 config.yaml # 默认配置 modes/ review.yaml dev.yaml依赖方面,我只用了 Python 标准库,没有第三方依赖。subprocess拿来调 git 命令,os和pathlib拿目录信息,yaml这种需求我甚至可以手写一个极简解析器,或者直接用 JSON 配置。这里为了省事,我用 JSON 格式来演示,避免引入 PyYAML。实际上 JSON 也是有效的 YAML 子集,配置体验差别不大。
3.2 核心代码:目录快照与上下文打包
主程序的核心逻辑并不复杂,就是三步:读配置、跑采集函数、把结果拼成文本。
我简化后的ctx.py大概是这样的:
#!/usr/bin/env python3 import json import os import subprocess import sys def run_cmd(cmd): try: result = subprocess.run( cmd, shell=True, capture_output=True, text=True, timeout=5 ) return result.stdout.strip() except subprocess.TimeoutExpired: return "" def collect_git_info(): git_info = {} git_info["branch"] = run_cmd("git branch --show-current") git_info["last_commit"] = run_cmd( "git log -1 --pretty=%h%n%s" ) git_info["changed_files"] = run_cmd( "git diff --name-only HEAD" ).splitlines() return git_info def collect_file_snapshot(file_list, max_files=5, max_chars=500): snapshot = [] for path in file_list[:max_files]: if not os.path.isfile(path): continue try: content = open(path, "r", encoding="utf-8", errors="ignore").read(max_chars) except OSError: continue snapshot.append(f"### {path}\n{content}") return "\n\n".join(snapshot) def build_context(config): sections = [] git_info = collect_git_info() sections.append("## Git 状态") sections.append( f"分支: {git_info['branch']}\n" f"最近提交: {git_info['last_commit']}" ) sections.append("## 最近改动文件") sections.append("\n".join(git_info["changed_files"])) changed_files = git_info["changed_files"] if changed_files: sections.append("## 文件内容快照") sections.append( collect_file_snapshot( changed_files, max_files=config.get("max_files", 5), max_chars=config.get("max_chars_per_file", 500), ) ) return "\n\n".join(sections) if __name__ == "__main__": mode = "dev" if len(sys.argv) > 1: mode = sys.argv[1] config_path = f"modes/{mode}.json" with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) context = build_context(config) print(context)这段代码当然不是完整产品,但骨架已经出来了。关键是collect_git_info和collect_file_snapshot这两个函数:前者拿到了“当前在做什么”的基础信息,后者把最近改过的文件内容截取出来。对于绝大多数编码问答场景,这两个信息就能覆盖掉一大半背景。
如果你想更准确地读取“当前打开的文件”,可以结合编辑器命令。比如 Neovim 可以用nvim --headless "+echo expand('%:p')" +qa拿到当前窗口文件路径。VS Code 则可以直接从~/.config/Code/User/workspaceStorage下的文件里读,但比较麻烦。我的建议是先不追求拿到所有信息,把 git 状态和最近改动文件做好,已经够用。
3.3 配置文件的字段设计与参数计算
配置文件的核心字段我设计成三类:sources、limits、output。
以dev.json为例:
{ "sources": { "git": { "branch": true, "last_commit": true, "changed_files": true, "diff": false }, "file_snapshot": { "enabled": true, "max_files": 5, "max_chars_per_file": 500 }, "shell_history": { "enabled": false, "tail": 10 } }, "limits": { "max_total_chars": 6000, "priority_order": ["git", "file_snapshot", "shell_history"] }, "output": { "format": "markdown", "prefix": "下面是当前项目的上下文信息,请基于此回答问题:" } }这里的max_total_chars可以按我之前说的字符估算公式反推。假设你用的模型上下文窗口是 8000 token,你希望留给输出的空间不要少于一半,那上下文最多占 4000 token。按中文 2 字符 1 token 估算,是 8000 字符。但为了防止模型在长上下文下仍表现下降,我会再打个七折,结果大约是 5600 字符。你在配置里填 6000 就已经是比较稳妥的值。
priority_order字段处理的是超限时的裁剪顺序:先裁掉 shell_history,再裁文件正文,最后保 git 信息。这个顺序来自我自己的经验,git 状态是判断“当前做什么”的最强信号,一般不能丢。
diff字段我默认关掉,因为git diff可能非常长。需要看具体改动时才在 review 模式里打开,并单独限一个字符数。这是上下文管理里最需要克制的部分——diff 看着很有用,但动辄几百行,很容易挤占给文件正文的空间。
3.4 集成到常用编辑器与 Shell
最小版本能跑之后,最重要的是把它嵌进日常工具链。我自己做了三处集成。
第一处是 shell alias。我在.zshrc里加了这样一行:
alias ctx='python3 ~/context-mode/ctx.py'然后在需要的时候执行:
ctx dev | clipcopyclipcopy是 macOS 上的剪贴板命令,Linux 可以换成xclip -selection clipboard。这样上下文块就进了剪贴板,直接粘贴到 AI 工具的对话框里。为了避免每次都要敲两个命令,我还留了一个快捷脚本ctxc,生成之后自动复制。
第二处是 Neovim。我更习惯先按一个键把上下文块塞进 buffer,再看情况发送。映射如下:
nnoremap <leader>cc :r!python3 ~/context-mode/ctx.py dev<CR>这一条命令会在光标下面插入当前上下文的文本块。我可以在里面删删改改,去掉不想发给 AI 的部分,然后再整体复制。
第三处是 VS Code 的 tasks。我在.vscode/tasks.json里加了一个 shell 任务,生成的上下文输出到终端面板里。用 IDE 的朋友也可以把这个工具接成自定义命令,虽然不如原生插件直接拿编辑器状态方便,但至少能拿到 git 和文件系统的上下文。
集成做好之后,我的日常流程变成了:切到要讨论的代码区域,按一下快捷键生成上下文,丢给 AI。之前那种反复解释“我这个项目是干嘛的、分支是什么、我改了哪些文件”的唠叨话,基本可以省掉。
4. 实操中的常见问题与排查技巧
4.1 上下文缺失或采集不完整
第一种常见情况是“明明当前目录是项目根目录,但 git 采集结果为空”。这通常不是工具的问题,而是 git 版本或配置的问题。新版 git 出于安全考虑,如果某个目录的 owner 与当前用户不一致,会拒绝执行 git 命令并提示“detected dubious ownership”。这种时候先跑一次git config --global --add safe.directory '<项目路径>',然后再试。
还有一种情况是文件读取为空。我会优先检查文件编码。有些旧项目是 GBK 编码,Python 用utf-8读会直接报错或乱码。我在代码里加了errors="ignore",保证至少还能拿到一部分内容。更稳妥的做法是把可疑文件转换成 UTF-8,或者在配置里加一个encoding字段,按项目指定。
另外,如果你的changed_files列表里出现的是deleted状态的文件,os.path.isfile会返回 False,文件内容快照会跳过它们。这是合理的,删除的文件本来也没有内容可以取。但你需要在上下文里保留这个文件名,并标注一下状态是“已删除”,这样 AI 才能理解这里少了一个文件。
4.2 隐私与敏感信息泄露风险
这是我在真实使用中最警惕的问题。context-mode 会把 .env、密钥文件、内部 URL 一股脑收集进来吗?如果我没做过滤,答案是“会”。第一次我把整个项目路径下的文件内容塞进去,结果 .env 里的数据库密码也被塞进了剪贴板,差点顺手发出去。从那以后我加了一条硬规则:文件采集默认排除.env、*.pem、*.key、*secret*等模式。
配置文件里可以加一个exclude_patterns字段:
{ "file_snapshot": { "exclude_patterns": [ ".env", "*.pem", "*.key", "**/node_modules/**", "dist", "build" ] } }另外记住一件事:context-mode 本身不发送数据,它只是把上下文打印到屏幕或剪贴板。但如果你复制之后不小心粘贴到了公共对话里,责任不在工具,而在操作习惯。我的做法是养成了发送前检查一遍上下文块的习惯,特别是看到 git 信息或文件内容里出现了奇怪字段时,宁可删掉一部分再问。
4.3 大仓库性能问题
在一些大仓库里,git diff --name-only HEAD可能非常慢,或者返回几千个文件,直接把上下文爆掉。这时候需要两个机制。
第一个是限制文件数量。我在collect_file_snapshot里只取前 5 个文件,但如果 changed files 有 300 个,这 5 个并不一定是重点。更合理的做法是按修改时间排序,把最近修改的文件排在前面。实现上可以用os.path.getmtime对文件列表排序。
第二个是忽略路径。仓库里常见的vendor、node_modules、dist目录应该默认排除,否则目录结构截图都会非常臃肿。我在收集目录结构时,会先读项目根目录里的.gitignore,然后用fnmatch或pathspec做匹配。最小版本里实现一个简单版就够了,真正要处理复杂规则时可以引入 git 命令git ls-files来获取被跟踪的文件列表,这样天然忽略未跟踪的垃圾文件。
如果仓库真的太大,还有一个思路是加一个缓存层。比如把 git 状态和目录结构缓存到/tmp/ctx_<项目hash>.json,每 30 秒刷新一次。这样你连续多次生成上下文时,不会每次都执行完整的 git 子进程,响应会快很多。
4.4 与不同 AI 工具的兼容性调优
不同 AI 工具对提示词的格式要求不一样,这是我最开始没注意到的。有的工具自动带 system prompt,你再塞一个prefix进去反而啰嗦;有的工具对 Markdown 结构敏感,喜欢把##开头的段落语义化处理,导致上下文块被当成多个独立对话,而不是一段连贯文本。
我的解决方案是让output.format可配置,提供markdown、plain、json三种格式。plain模式去掉所有 Markdown 标题,只保留换行;json模式把上下文块的每个 section 变成一个对象,方便程序化拼接。
另外,如果你用的是支持“附加文件”功能的 AI 工具,可以把 context-mode 输出到一个.context.md文件,然后把这个文件作为附件传上去。这比粘贴大段文本的效果要更稳定,因为附件形式不会破坏模型对原始文本段的识别。
我还做了一件提升兼容性的小事:把上下文块的最后一行固定加上一句提示,例如“请基于上面内容回答我的问题”。因为有些模型的注意力会集中在用户消息的末尾,这句显式指令能让它更清楚上下文块是背景资料而不是待处理代码。实测下来,加了这一行之后答非所问的概率明显降低。
最后还有一个反直觉的经验:上下文块不一定越长越好。我最早总想把所有内容塞满,后来发现压缩到只包含“项目信息 + git 状态 + 最近改动的文件”,反而答得最准。因为模型不会被无关代码干扰,也不会把精力浪费在理解一堆冗余信息上。context-mode 的意义不在于“无脑收集”,而在于帮你做到“该给的都给了,不该给的一点都不给”。
如果你也想复刻这套东西,我建议从最简单的 git + 文件快照开始跑,别一上来就追求完整的目录结构、shell 历史、编辑器联动。先把最核心的上下文链路打通,用上一周,再按你实际遇到的痛点去加新的采集源。毕竟,工具永远是长在自己的工作流里的,别人的完美配置照搬过来,多半会水土不服。