如果你平时会用大模型写代码大概率经历过这种烦躁想让 AI 跨文件推测一次改动它却在无限开放的对话里越猜越偏重复粘贴文件又要手动挑拣稍不注意就把几十份代码一股脑丢进对话框半天只等来一段无关建议。我最近把这些碎片化的工作流收敛成一个小工具代号context-mode。这个名字看起来朴素但它确实改变了我跟模型交互的方式在把本地代码喂给 AI 之前先按预设模式重新组装上下文。简单说它就是“上下文开关”——按需决定模型看到哪些文件、以什么顺序、占多大 token 预算。适合经常在多个项目之间切换、又不想每次手工整理粘贴的人。无论你用商业大模型接口还是本地开源模型只要能读文本这套思路基本都成立。1. 这个项目到底想解决什么问题1.1 上下文困境给模型太多与太少都不行大语言模型不是数据库。它的注意力机制决定了“上下文越长信息密度往往越低”。我一开始的思路很简单把整个仓库塞进去模型总该明白了吧结果就是对话窗口被几百个文件占满真正关键的调用链反而被埋掉回答变成“正确但没用”的废话。反过来只丢一个文件也不行。模型缺少关联模块的接口定义、缺少业务规则、缺少测试样例它很容易顺着那个文件里的局部逻辑“自由发挥”生成一段看起来通顺、实际上跟整体设计打架的代码。就像给一个实习生看一个项目 5 秒还不如给他明确相关的三个函数。context-mode要解决的正是这个矛盾怎么选、怎么排、怎么控制长度让模型真正看到它该看到的那块上下文。1.2 从“一刀切”到“按需装载”项目最核心的设计点是“模式”概念。每个模式就是一套独立的上下文装载规则。动手写工具之前我先想清楚自己平时跟 AI 打交道的几种场景补一块逻辑、审查一版改动、整理文档、排查线上异常它们的侧重点完全不同。与其用一个万能配置处理所有场景不如用配置文件描述多个模式然后在同一个命令行入口里切换。下面是我常用的几组模式你可以把它当作参考模板模式名适用场景主要加载内容预算建议debug排查运行异常当前报错堆栈、相关日志、核心调用链3000-5000code修改现有实现入口文件、关联模块、测试样例6000-8000review代码评审变更 diff、接口签名、调用方4000-6000docs文档写作同类文档模板、示例代码3000-5000预算这列不是拍脑袋而是经验值。模型上下文窗口看起来很大但输出本身也要占 token推理阶段如果上下文接近上限不仅速度下降还容易出现“后面的内容记不住”的情况。给每个模式设一个宽松但可控的预算效果往往会好很多。2. 整体架构与设计取舍2.1 配置驱动把上下文组装策略当成项目配置来管第一版我图省事把规则直接写在 Python 代码里。结果是每次调整优先级都要改代码、重启、重新测试非常痛苦。后来我改成“配置驱动”所有模式都写进一个 TOML 或 YAML 文件代码只负责解释和执行。这样做有一个明显的好处配置可以放入版本管理团队里所有人都能共用一套上下文策略。新人接手项目时不需要从零摸索“哪些文件该给模型看”直接跑context-mode build --profile code就能得到一致的上下文。下面是一份精简配置[profile.code] roots [src, tests] include [src/**/*.py, tests/**/*.py] exclude [**/__pycache__/**, **/*.pyc, src/legacy/**] priority [src/app.py, src/model.py, tests/test_app.py] budget_tokens 8000 max_file_chars 6000include只负责圈定候选文件exclude负责过滤噪音priority决定文件在输出中的先后顺序。特别说明一下顺序问题模型对上下文开头的敏感度通常比较高所以我会把“当前要改的入口文件”放在最前面把“可参考的相似实现”往后放。2.2 三级加载路径直读、扫描、语义召回真正实现的时候我把加载逻辑拆成三层每一层的可靠性和成本都不一样第一层是直读。用户明确指定某个文件或某段剪贴板内容直接进上下文。这种方式的确定性最高几乎不会出错也最适合处理“我就想让你看看这个报错”的场景。第二层是扫描。根据include和exclude规则遍历项目目录批量抽取候选文件。这是最常用的一层覆盖了“改不动某个模块时把相关代码一起带上”的需求。第三层是语义召回。当项目变成一个几千文件的巨型仓库时单纯按路径扫描已经不现实因为即使排除掉无关文件候选仍然太多。这时候我会建立一个轻量索引按关键词或向量相似度挑出与当前问题相关的片段只把这些片段拼进上下文。这三层不是三选一而是叠加。直读结果永远保留扫描结果作为骨干语义召回结果作为补充。加载成本从低到高所以处理顺序也应该是先直读、再扫描、最后召回。2.3 为什么我不完全依赖 AI 产品自带的“文件”功能可能有人会觉得现在很多 AI 编码工具本身就能手动选择文件何必自己写一套我个人的体验是手动选择在单文件场景下很好用但一旦涉及“跨五个文件定位一条调用链”手选就非常笨重而且这些工具对 token 预算的控制往往是黑盒我很难知道哪些文件已经被截断、截断到什么程度。context-mode做的事情不是替代那些入口而是把“选上下文”这件事提前到本地、自动化、可审计。我看到输出文件就知道模型到底看到了什么配置在版本管理里团队评审时也能 review。这种透明感是我决定自建它的根本原因。3. 关键实现细节3.1 文件筛选白名单、黑名单与优先级排序筛选逻辑看起来简单真正做起来有细节。路径匹配我一开始用字符串in判断结果src/domain和src/domain_model互相干扰后来全部改成 glob 匹配。from pathlib import Path def should_include(path: Path, include_globs, exclude_globs, max_file_chars: int) - bool: try: if path.stat().st_size max_file_chars: return False except OSError: return False if any(path.match(p) for p in exclude_globs): return False return any(path.match(p) for p in include_globs)要注意path.match默认不看隐藏文件也不跟随符号链接。如果你确实需要把.env.example或某个软链目录带进去需要单独处理。扫描出来的文件最终要排序先按priority列表的先后顺序没在优先级列表里的文件按相对路径字典序排在后面。这样做的好处是当预算不足时我只需要从排序末尾开始丢文件高优先级内容始终安全。3.2 token 预算换算与截断策略token 预算不是一个精确数字但我们要有估算方法。最简单实用的近似公式是英文按 4 个字符折算 1 个 token中文按 1 个汉字折算 1 个 token。我用一个粗粒度函数def estimate_tokens(text: str) - int: # 估算就好不用精确到分词器级别 return len(text) // 3 1这个函数对中英文混合内容不算准但作为预算判断足够。真正执行时我会先算固定开销输出模板的标题、文件路径、分隔符以及 AI 对话开头那段“请基于上下文回答”的指令。这些都要从配置预算里扣掉剩下的才是给文件内容的配额。截断时我坚持一条原则不要从字符中间切断。代码块被切成半个函数模型读起来非常痛苦。我按行截断保留文件的开头部分和结尾部分中间用一行注释替代。开头通常包含导入和主逻辑结尾通常包含收尾和测试用例中间细节丢了反而没那么影响主线。如果某个文件实在太小不值得截断就直接跳过它并打一条日志。3.3 增量缓存与语义召回第一次跑全量扫描时我明显感觉到效率问题。一个中等仓库有几百个文件每次都要重新读取、重新编码、重新判断交互响应慢得没法用。后来我引入了 mtime 缓存记录每个文件的最后修改时间只有 mtime 变化的文件才重新读取和组装。这一步让重复构建从秒级降到毫秒级。对于语义召回我只对新增或修改的文件更新索引不会每次都全量重建。这里还有个小坑索引不能全局统一最好按模式分开。code模式下关注接口和实现docs模式下关注注释和示例两者的索引维度天然不一样。全局索引会把相关性算偏。3.4 输出模板让模型知道每块内容出处的关键很多 AI 提示词失败不是因为模型能力而是因为上下文里没有出处信息。直接把一堆文件内容拼起来丢给模型模型分不清哪段是接口定义、哪段是历史废弃代码、哪段是测试样例。我最后的输出模板长这样 src/app.py (lines 12-88) 用户提供的具体实现代码... src/model.py (lines 1-120) 模型相关定义... tests/test_app.py (lines 1-80) 测试用例摘要...每个文件块都带上路径和行号范围模型在回复里引用位置时会更准确。我试过去掉这段模板同样内容的效果差了一大截。后来我把模板固定成内置常量不允许模式配置随意覆盖避免有人把关键分隔符删掉。4. 完整实操从零搭一个最小context-mode4.1 最小工程结构我建议不要一开始追求大而全先搭一个能跑通的最小闭环。工程目录大概这样context-mode/ context_mode/ __init__.py config.py loader.py tokenizer.py build.py profiles/ code.toml debug.toml docs.toml main.pyconfig.py负责读取 TOML 配置并校验字段loader.py负责扫描文件和生成文本块tokenizer.py负责 token 估算build.py负责把多个文本块按预算拼成最终上下文。main.py只是命令行入口。为什么拆这么细因为后续扩展很容易新增一个模式只是加 TOML 文件新增一种加载方式只改loader.py不会牵一发动全身。4.2 实现目录扫描与文件过滤扫描逻辑在loader.py里。先递归遍历所有文件再用should_include过滤def scan_files(root: Path, profile): files [] for p in root.rglob(*): if p.is_file() and should_include(p, profile.include, profile.exclude, profile.max_file_chars): files.append(p) return filesrglob(*)会遍历所有子目录和文件遇到权限错误时需要捕获异常并继续。排序部分我先把priority列表转成路径映射再按映射顺序排def order_files(files, priority): rank {str(p): i for i, p in enumerate(priority)} return sorted(files, keylambda p: (rank.get(str(p), len(rank)), str(p)))这里稍微注意一下路径匹配的统一性不要把./src/app.py和src/app.py混在一起排序否则同一个文件会出现两份。4.3 配置解析与 token 控制配置解析不复杂但校验不能省。budget_tokens必须大于模板开销include不能为空priority里的路径应该属于include范围否则 warn 一下。token 控制的关键循环是这样def build(profile, extra_files): blocks [render_explicit_file(f) for f in extra_files] quota profile.budget_tokens - TEMPLATE_TOKENS for path in order_files(scan_files(root, profile), profile.priority): block render_file(path) estimated estimate_tokens(block) if quota 0: break if estimated quota: block clamp_block_by_lines(block, quota) quota - estimate_tokens(block) blocks.append(block) return render_template(blocks)clamp_block_by_lines保留文件头尾的固定行数中间用省略标记代替。这里要注意截断之后还应该再估算一次 token因为原始估算和截断后的实际值可能有偏差。4.4 命令行入口与日常接入main.py用 Python 标准库里的argparse就行不需要引第三方依赖import argparse def main(): parser argparse.ArgumentParser() parser.add_argument(--profile, defaultcode) parser.add_argument(--extra, actionappend, default[]) args parser.parse_args() ...日常使用里我总结出三种接入方式。第一种是最直接的运行命令把输出重定向到context.md然后手动复制内容粘贴给 AI。第二种是绑定 IDE 快捷键让命令自动把生成结果写入系统剪贴板省掉打开文件的步骤。第三种是脚本化在调用模型 API 的脚本里先执行context-mode build --profile code把生成的文本拼到系统提示词里。不管用哪种方式我都建议不要直接让生成的上下文进入终端执行环境。它只是提示词材料不是可运行命令安全边界要保持住。4.5 一个最小验证例子我用一个小仓库做过验证结构大概是src/ app.py model.py tests/ test_app.py配置里priority写的是src/app.py优先tests/test_app.py次之src/model.py再次之。跑一次build输出里先看到入口文件再看到测试描述最后才是模型定义。因为预算充足三个文件都能完整保留。如果我把预算压到 2000 token输出就会变成入口文件完整测试文件被截断成头尾摘要模型定义被完全丢弃。这种可预期性正是配置驱动的价值。5. 踩坑与排查实录5.1 文件扫不出来被隐藏规则吃掉第一次用的时候我想把仓库里所有 Markdown 文档都塞给docs模式配置写的是**/*.md。结果发现有一个目录下的文档始终没出现。排查了很久原因是那个目录里有一份嵌套的 ignore 文件把下级文件标成忽略而我的 glob 库默认遵守这套忽略规则。解决方式是在调试模式下打印匹配过程遍历到哪个目录、哪个文件、命中了哪条规则全打出来。另外如果符号链接目录需要被扫描显式开启跟随选项否则永远扫不到真实文件。5.2 单文件太大把整个上下文预算耗尽有一回我处理数据分析相关的项目仓库里躺着一个 3MB 的 JSON 导出文件。它不在 exclude 里于是被完整读入一次吃掉两万多 token后面所有源码全被截断。模型输出的内容明显“失真”因为它只能看到那个大 JSON完全看不到业务代码。后来我在配置里加了max_file_chars和per_file_max_lines两个上限。超过 6000 字符的文件不直接跳过而是先做行级压缩NaN 之类的大块数据只保留统计摘要前 20 行保结构、后 20 行保尾部中间用一行注释代替。这个改动之后大文件从“上下文杀手”变成了“可读摘要”。5.3 中文文件乱码、BOM 和编码问题Windows 环境下写配置文件很容易踩编码坑。有些文件带 BOM读出来第一个字符是肉眼看不见的\ufeff有些历史项目还是 GBK 编码直接按 UTF-8 读就会抛异常。我的处理方案是统一使用utf-8-sig作为默认编码它会自动去掉 BOM。读取时如果遇到UnicodeDecodeError不要直接让整个流程崩溃而是记录警告、跳过该文件并继续。上下文构建不是数据库导入少一个文件可以接受整个流程挂掉才是最浪费时间的。5.4 模型回复变差时的排查顺序context-mode并不是“接了它模型一定变好”。我用时间长了发现偶尔效果反而变差这时候不能盲改提示词得按固定顺序排查现象先查什么常见原因模型回答跟上下文无关输出文件里是否有重复片段同一个文件被多个模式重复拼入关键文件被截断看日志里的 token 分配某个大文件占满预算后面文件被丢弃上下文里出现旧代码检查 mtime 缓存文件内容变了但 mtime 没变回复引用错位置看输出模板是否有路径和行号模板分隔符被配置覆盖其中重复片段这个问题最隐蔽。早期版本我把priority里的文件和include扫描结果都加入列表没有去重导致入口文件出现两次模型读到一个文件两个版本回复自然混乱。后来我在order_files里按绝对路径去重问题消失。固定了这个排查顺序之后我现在每天接新仓库的第一件事不是调模型参数而是在终端跑一次context-mode --debug看看自己准备投喂的上下文到底长什么样。等这个输出干净了模型基本上不会太离谱。