拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

像管理代码一样管理AI上下文:context-mode实战指南

像管理代码一样管理AI上下文:context-mode实战指南

如果你也和我一样,每天在 AI 编程助手前面反复粘贴项目背景、技术栈约定、文件路径,然后过十分钟发现它又开始答非所问,那这篇内容应该能帮你省下不少时间。我最近把一套叫context-mode的上下文管理模式,做成了一个小工具,专门用来解决“AI 记不住上下文”这个老毛病。它的核心思路很简单:把上下文当成代码一样管理——分文件、定作用域、按优先级合并、能回滚、能复用,而不是每次开新会话都从零开始口述一遍。

这篇文章会完整拆解我为什么做这个东西、它解决了什么问题、核心设计怎么来的、关键代码怎么落地,以及我在实际使用三个月之后踩过的坑。无论你是在折腾 AI 辅助编程,还是纯粹对“如何管理给模型的信息”这件事感兴趣,都可以参考我的方案,然后自己做一套顺手的东西出来。

1. 为什么要把上下文管理当成一个正经工具来做

1.1 我在 AI 编程里反复踩的“失忆”问题

先说说我最初遇到的场景。我每天的工作流里,有一大半时间是在和 AI 助手结对写代码。它的使用方式很简单:开新会话,粘贴需求,它写代码,我 review,再让它改。问题就出在“开新会话”这几个字上。

每次新开一个会话,我都得重新告诉它:项目的技术栈是什么、代码入口在哪、数据库模型放在哪个目录、接口返回结构有什么约定、哪些文件改不得。这些话我一天要说上好几遍,而且说得并不短。哪怕我很有耐心地把项目背景写清楚,聊到第二轮第三轮,它还是会出现一种很典型的情况:用错了接口名、改了不该改的公共模块、或者把上一次会话里已经否定的方案又重新提出来。

最让我崩溃的一次是,我让 AI 优化一个 FastAPI 接口的性能,第一轮给足了背景信息,它也给出了正确的优化思路。结果我在同一个会话里接着问了几个不相关的问题,把它的注意力带偏之后,再让它继续优化,它居然开始引用一个早就不存在的旧路由名称。我回头翻聊天记录才明白:最初的背景说明早就滚出了它的上下文窗口,模型只能靠当前这段对话里的零散信息去猜。

这个经历让我意识到一件事:不是 AI 变笨了,是我的输入组织方式有问题。它的上下文窗口是有限的,如果我不能把最重要的信息稳定地放在合适的位置,它就会在长对话里慢慢“失忆”。

1.2 不加管理的上下文为什么会越写越偏

这里有个概念叫上下文漂移(context drift),我是在反复翻车之后才真正理解它的。简单说,模型在长对话里的注意力会被最近的几轮消息牵引,最初设定的规则如果一直没有被强化,它的影响力就会越来越弱。就像你给一个新同事入职当天讲了一遍项目概况,他没记住,后面干了两周,所有操作都基于头一天的错误理解,又没人及时纠正,于是越偏越远。

还有一个更隐蔽的问题:中间环节的“错误假设”。比如 AI 在某个瞬间以为某个配置文件是放在根目录的,后面所有回答都会默认这个结构。如果你没有在每一轮对话里把正确路径重新贴一遍,它根本不会意识到自己错。所以很多人的体感是“AI 聊天可以,干活不靠谱”,本质上是因为干活需要的上下文比较长,而大多数人根本没在管理这个东西。

我在意识到这一点之后,开始尝试各种土办法:把项目背景写在一个固定的笔记里,开新会话时手动复制粘贴;把常用说明做成占位符,写进我的输入法快捷短语;甚至试过给 AI 助手添加一个固定的“开场白”模板。这些办法都有点用,但都很零碎。真正让我下决心做context-mode的,是一个常见的需求组合:我需要同时维护多个项目,每个项目有自己独立的背景知识,而我又经常在项目之间来回切换。指望靠手动复制粘贴来管理这些内容,既不现实,也一定会出错。

1.3 context-mode 到底要管哪些东西

我最终设计的context-mode,并不是简单地把“整段聊天记录保存下来”,而是把需要交给 AI 的信息分成三类来管理。

第一类是稳定的背景,包括项目描述、技术栈、代码目录结构、接口风格约定、团队偏好等等。这类信息变化很慢,一个月可能都不会变一次。第二类是可变的目标,就是当前正在做的具体任务,比如“优化 /api/tasks 接口的响应时间”,以及验收标准,“QPS 提升 20%,不能改变返回结构”。这类信息可能几个小时就要换一次。第三类是临时的经验,是我在项目过程中总结出来的一些教训,比如“这个模块有历史包袱,不要动”“那个接口的字段命名容易混淆,记得看清楚”。

这三类信息如果混在一起,AI 就会被大量无关的历史信息干扰。context-mode做的事情,就是把它们分到不同的作用域里,按规则组装,每次只给模型当前最需要的那一份。

2. context-mode 的核心设计:上下文种子、作用域与合并策略

2.1 上下文种子:把背景、约束、偏好固化成文件

我把基本的管理单元叫“上下文种子”,英文就是 context seed。为什么叫种子?因为和种子的性质很像:体积很小,但包含了一棵完整植物的所有关键信息。只要在每次新会话时把它种下去,后面长出来的对话就是稳定、符合预期的。

一个种子就是一个 TOML 文件,里面用[context]字段组织内容。下面是我全局配置的一个简化例子:

# ~/.config/context-mode/base.toml [meta] name = "global-default" [context] persona = "你是一名资深 Python 后端工程师。回答问题直接,先给结论,再解释原因。" style = "使用中文回答。代码使用 Python 3.11+ 语法,必要时给出完整可运行示例。" output_format = """ 1. 先总结本次需要的改动; 2. 按文件分组列出具体修改点; 3. 不要输出与任务无关的内容; 4. 长段代码直接给出文件路径和关键片段,不要贴全文。 """

这个文件只放“我对所有项目通用”的偏好,不会出现任何具体项目的信息。真正属于某个项目的内容,必须放到那个项目自己的目录里去。

2.2 作用域与优先级:全局、项目、任务怎么叠加

context-mode把上下文分成三个作用域:全局、项目、任务。对应关系如下:

作用域存放位置内容举例合并优先级
全局~/.config/context-mode/base.toml角色设定、通用回答风格、输出格式要求最低
项目<项目根>/.context/project.toml项目描述、技术栈、代码结构、禁止事项中
任务<项目根>/.context/tasks/<任务名>.toml本次目标、验收标准、关注点最高

优先级的设计逻辑很直白:任务级信息离“当下”最近,最应该被模型重视;项目级信息是任务发生的环境;全局信息则是兜底的通用偏好。当它们之间出现冲突时,优先级高的覆盖优先级低的。

举个例子。全局种子说“使用中文回答”,项目种子说“代码注释使用中文,对外文档使用英文”,任务种子说“本次只需要写代码注释,不需要写文档”。合并之后,模型实际接受的指令就是:本次用中文写代码注释,文档部分忽略。三级叠加之后,每一条都是具体的、可执行的,而不是一堆互相矛盾的要求。

我特别强调一点:不要把项目信息写进全局文件。很多人在实际使用中最容易犯的错,就是图省事,在全局配置里塞了几个项目的描述,结果切换项目时 AI 脑子里全是上一个项目的背景,输出直接跑偏。全局文件就只能放“放之四海而皆准”的东西。

2.3 合并策略:两条上下文冲突时听谁的

合并不是简单地把三段文本拼起来,而是要做“字段级别的合并”。同一字段存在多份时,高优先级覆盖低优先级;不同字段之间则直接叠加保留。

我简化一下合并的规则:先按优先级对种子排序,然后遍历每个种子的[context]字段。如果某个键是之前出现过的,用新的值替换;如果没有出现过,就直接追加。这样最终产出的是一段无重复、无矛盾的完整提示词。

再补充一类特殊字段:文件引用指令。我从实践里发现,与其把整个 README 或整个代码文件的内容塞进上下文,不如告诉模型“你应该先读哪些文件”,让 AI 编程助手自己去读。这就是种子里include和exclude两个字段的价值。

# .context/project.toml [context] project_description = """ 当前项目是一个基于 FastAPI 的任务调度服务。 技术栈:Python 3.11 / FastAPI / Redis / PostgreSQL。 代码入口在 app/main.py,自定义业务逻辑集中在 app/services/ 下。 """ include = [ "app/routes/tasks.py", "app/services/scheduler.py", "README.md", ] exclude = [ "migrations/", "tests/temp/", ]

合并时exclude优先于include:即使某个文件同时出现在两个列表里,只要被 exclude 命中,模型就不该去碰它。这个规则的现实意义很大,比如你不想让 AI 在重构任务里去读迁移文件,几万个文件扫描起来费 token 不说,还容易产生危险的建议。

3. 手把手实现一个可用的 context-mode 命令行工具

3.1 技术选型:为什么用 Python + Click

做这个工具,我第一反应是选 Python,理由很实际:核心逻辑只是“读 TOML、合并字典、拼字符串、复制到剪贴板”,这种 IO 加文本处理的任务,Python 写起来最快。Python 3.11 以后标准库自带tomllib,解析 TOML 不再需要第三方依赖,这又少了一个安装负担。命令行框架我用的是 Click,它已经非常成熟,写子命令、处理参数、输出帮助信息都很顺手,比手撸argparse干净得多。

如果你问我为什么不用 Node 或 Go,我的答案也简单:它们都完全能做,但不是最省事的。Node 那边你需要额外引toml解析库和剪贴板库;Go 编译出来虽然是个零依赖的二进制,很酷,但开发调试成本比 Python 高。这种小工具,开发效率才是第一位的。

唯一需要注意的是 Windows 环境的剪贴板操作,原生 API 在不同版本上有些不一致。我的处理方案是直接用pyperclip库,它内部帮你适配了各种平台,实际用下来很稳。项目源码的结构我放在下面,方便你直接参考。

3.2 目录结构与核心代码

我的项目目录大概是这样的:

context-mode/ ├── ctx/ │ ├── __init__.py │ ├── loader.py │ ├── merger.py │ └── cli.py └── pyproject.toml

loader.py负责按作用域加载所有种子文件,核心代码如下:

# ctx/loader.py from pathlib import Path import tomllib from dataclasses import dataclass @dataclass class ContextSource: scope: str # global / project / task priority: int # 合并优先级,数值越大越靠前 path: Path # 源文件路径 data: dict # 解析后的 TOML 数据 class ContextLoader: def __init__(self, project_root: Path | None = None): self.project_root = project_root or Path.cwd() self.global_dir = Path.home() / ".config" / "context-mode" self.task_name = None def set_task(self, task_name: str): self.task_name = task_name def load_all(self) -> list[ContextSource]: sources = [] pair_list = [ ("global", 1, self.global_dir / "base.toml"), ("project", 2, self.project_root / ".context" / "project.toml"), ] if self.task_name: task_path = ( self.project_root / ".context" / "tasks" / f"{self.task_name}.toml" ) pair_list.append(("task", 3, task_path)) for scope, priority, path in pair_list: if path.exists(): with open(path, "rb") as fp: data = tomllib.load(fp) sources.append( ContextSource( scope=scope, priority=priority, path=path, data=data, ) ) return sources

merger.py负责合并去重,输出最终的纯文本提示词:

# ctx/merger.py from ctx.loader import ContextSource def merge_context(sources: list[ContextSource]) -> str: # 1. 按优先级排序 ordered = sorted(sources, key=lambda s: s.priority) # 2. 字段级合并:相同键用高优先级覆盖 merged: dict[str, str] = {} for src in ordered: ctx = src.data.get("context", {}) for key, value in ctx.items(): if isinstance(value, str): merged[key] = value.strip() # 3. 组装 include/exclude 为指令 has_include = any( "include" in src.data.get("context", {}) for src in ordered ) instructions = [] for src in ordered: ctx = src.data.get("context", {}) if ctx.get("exclude"): instructions.append( "禁止查看或修改以下路径:" + "、".join(ctx["exclude"]) ) if has_include and ctx.get("include"): instructions.append( "建议优先阅读以下文件:" + "、".join(ctx["include"]) ) # 4. 拼装为分段提示词 blocks = [] for key, value in merged.items(): if key in ("include", "exclude"): continue block = f"【{key}】\n{value}" blocks.append(block) if instructions: blocks.append("【文件范围】\n" + "\n".join(instructions)) return "\n\n".join(blocks)

cli.py则是命令入口:

# ctx/cli.py import click import pyperclip from pathlib import Path from ctx.loader import ContextLoader from ctx.merger import merge_context @click.group() def ctx(): """context-mode: 像管理代码一样管理 AI 上下文。""" @ctx.command() @click.argument("task", required=False) def use(task): """切换上下文模式。指定 task 表示只加载当前任务上下文,不指定则只加载全局+项目上下文。""" loader = ContextLoader(project_root=Path.cwd()) if task: loader.set_task(task) sources = loader.load_all() if not sources: click.echo("没有找到任何上下文种子文件,请先创建 base.toml 或 .context/project.toml") return text = merge_context(sources) pyperclip.copy(text) click.echo(f"已合并 {len(sources)} 个上下文源并复制到剪贴板。") for s in sources: click.echo(f" - [{s.scope}] {s.path}") @ctx.command() def status(): """查看当前目录下能加载到哪些上下文源。""" loader = ContextLoader(project_root=Path.cwd()) sources = loader.load_all() if not sources: click.echo("当前目录未发现上下文种子。") return for s in sources: ctx_keys = list(s.data.get("context", {}).keys()) click.echo(f"[{s.scope}] {s.path}") click.echo(f" 包含字段: {', '.join(ctx_keys)}") if __name__ == "__main__": ctx()

这套代码已经够我日常使用了。use命令负责把合并好的上下文复制进剪贴板,我直接粘贴给 AI 助手就行;status命令帮我快速确认当前项目里到底配了哪些种子,排查问题时会用。

3.3 配置模板与使用示例

用一个实际的终端会话演示整个过程,会更清楚。

$ cd ~/work/task-scheduler $ ctx status 当前目录未发现上下文种子。

第一次进来,项目还没有任何配置。我手动建一个:

$ mkdir -p .context/tasks $ vim .context/project.toml $ vim .context/tasks/perf-fix.toml

然后切到任务上下文:

$ ctx use perf-fix 已合并 3 个上下文源并复制到剪贴板。 - [global] /home/me/.config/context-mode/base.toml - [project] /home/me/work/task-scheduler/.context/project.toml - [task] /home/me/work/task-scheduler/.context/tasks/perf-fix.toml

此时剪贴板里已经有了一段完整的分段提示词,大致长这样:

【persona】 你是一名资深 Python 后端工程师。回答问题直接,先给结论,再解释原因。 【style】 使用中文回答。代码使用 Python 3.11+ 语法,必要时给出完整可运行示例。 【output_format】 1. 先总结本次需要的改动; 2. 按文件分组列出具体修改点; 3. 不要输出与任务无关的内容; 4. 长段代码直接给出文件路径和关键片段,不要贴全文。 【project_description】 当前项目是一个基于 FastAPI 的任务调度服务。 技术栈:Python 3.11 / FastAPI / Redis / PostgreSQL。 代码入口在 app/main.py,自定义业务逻辑集中在 app/services/ 下。 【task_focus】 本次任务只关注 /api/tasks 接口的响应时间优化。 【文件范围】 建议优先阅读以下文件:app/routes/tasks.py、app/services/scheduler.py、README.md 禁止查看或修改以下路径:migrations/、tests/temp/

把这段内容直接贴给 AI 助手作为新会话的第一条消息,刚才那个“失忆”的问题基本就不会再出现了。这是我实际使用中效率提升最明显的地方:原来每次开新会话要花三五分钟重新交代背景,现在一条命令加一次粘贴,十秒钟搞定。

4. 实际使用三个月后的踩坑记录与细节调优

4.1 上下文越堆越长:token 预算与裁剪策略

用了一段时间之后,我发现了新的问题:种子文件写得越来越多,导出的上下文越来越长。最开始只有三四段,后来项目级描述写了大量细节,任务级又追加了一堆过程记录,导致单次导出的 token 数量从大约 2000 涨到了 8000 多。

模型处理超长输入时,一是费用变高,二是注意力会被稀释,中间或者后面的内容容易被忽略。我实测下来,超过 4000 token 之后,AI 对尾部禁令的遵守程度明显下降。于是我做了三个调整。

第一个调整是给上下文设预算线。我在use命令里加了一个粗估 token 数的方法,超过 3500 token 就警告。估算方法很简单:中文按 1 个字约等于 0.6 到 1 个 token 来算,英文按 4 个字符约等于 1 个 token 来算,不求精确,只要大概量级对就行。

第二个调整是把“临时事实”和“稳定背景”分开。所谓临时事实,比如某次调试中发现的问题、某个接口当前的异常表现,这类信息不应该沉淀在 project.toml 里,否则它会随着时间越积越多,还会误导后续的任务。我把它放到当前任务的文件里,任务结束就清理。

第三个调整是用 include 指令替代直接贴代码内容。过去我会把某个核心模块的代码全文粘到种子里,现在只写一行“建议优先阅读 app/services/scheduler.py”,让 AI 助手自己去读文件。这个改变极大节省了 token,同时模型的准确率反而更高了,因为它是从完整的源码文件里获取信息,而不是从我截取的一段代码里。

4.2 多任务并行时的作用域污染:一次完整的排查链路

多项目并行使用时,我碰到过一个特别隐蔽的问题,完整的排查思路写下来,也许能帮你省几个小时。

现象是一个已经配置好的项目 A,切到项目 B 之后,让 AI 助手完成任务,它的回答里还频繁引用项目 A 的文件路径和模块名。第一反应我以为是 AI 助手缓存的问题,重开会话也没解决。

排查的第一步,我先用ctx status查看项目 B 当前加载了哪些上下文源。结果发现项目 B 的.context目录压根是不存在的,也就是说它没有自己的项目种子。第二步,我直接把剪贴板里的合并文本贴到一个文本编辑器里,从头读了一遍。这里关键信息出现了:project_description的内容写的竟然是项目 A 的介绍。

为什么会这样?因为当项目 B 没有自己的 project.toml 时,ContextLoader会静默跳过项目作用域,只加载全局种子。而全局种子里的项目描述,是我当初犯懒,把项目 A 的信息顺手写进了全局 base.toml。于是项目 B 拿到的上下文里,项目背景完全是错的,模型自然按项目 A 的语境来回答。

根因查清楚之后,修复很直接。我把全局 base.toml 还原成只放通用角色和输出偏好,把项目 A 的描述迁移到它自己的.context/project.toml,同时给use命令加了一个启动校验:执行时如果项目根目录下没有 project.toml,就打印一行提醒,而不是静默加载全局配置完事。

这个坑给了我一个很深的教训:上下文工具的作用域设计得再合理,使用者的纪律跟不上,一样白搭。全局文件是一个“公共空间”,绝不允许被某个具体项目的内容污染。后来我又补了一个ctx clear命令,在切出某个项目前把当前上下文语境清掉,避免旧项目的残留信息影响新项目的会话。

4.3 与 AI 编程助手配合时的输出格式约束

还有一类问题,和工具无关,但和提示词内容强相关。很多 AI 助手在回答代码问题时,习惯先解释一堆原理、再给优化建议、最后才写代码。对于学习来说这很好,但对于干活来说,它会让 review 成本变得很高。

我的解决办法是在全局种子里增加一段输出格式要求,也就是前面那个output_format字段的来源。加了之后,效果非常明显:回答从“大段说明 + 尾部代码”变成了一种更紧凑的结构,先是结论,然后是每个文件的改动点清单,长代码只给文件路径和关键片段。这个格式是我实测下来最适合进入代码 review 流程的样式。

这里有个细节值得说一下:格式约束一旦写进全局种子,就不建议在任务级种子反复重复。我见过有人每个任务文件里都写一遍“请先总结再解释”,既费 token,又容易和全局配置冲突。正确做法是全局只写一份,项目或者任务有特殊展示需求时,才用高优先级覆盖它。

5. 再往后走:扩展方向与落地前的几个问题

5.1 我这个工具可能的扩展方向

context-mode目前已经很够我用,但它的设计留了不少扩展空间。

一个比较自然的方向是支持多角色上下文。现在种子里的 persona 是固定的,但实际开发中,同一个项目可能需要切换不同的视角:让 AI 以架构师身份做模块设计、以测试身份列测试用例、以运维身份检查部署脚本。可以把这些角色拆成不同的种子文件,通过一个ctx use architect perf-fix这样的参数同时指定角色和任务。

另一个方向是自动检测项目类型并生成种子。比如检测到目录里有pyproject.toml且包名是 FastAPI 相关,就自动把常见的 Python 后端背景填进去。这个能降低初次配置成本,前提是把自动生成的内容和手工维护的内容明确分开,否则自动化写出来的东西质量不可控。

还可以做 git 分支联动。比如切到feature/optimize-tasks分支时,自动激活perf-fix任务上下文;切回main时自动清除。目前我用的是手动执行ctx use,自动化之后会更顺滑,适合重度使用者。

5.2 团队落地前,我建议你先想清楚这些问题

如果你想把这个思路引入团队,我认为有几个问题应该提前有答案,否则很容易变成新的形式主义。

第一是版本控制和安全边界。种子文件一定会想放进 git 仓库,方便团队成员共享。但种子文件里一旦出现数据库地址、密钥、内部服务名,它就会成为一次安全事故。我的做法是在README里显式约定:所有敏感配置一律用环境变量插值,比如连接串写成${DATABASE_URL},文件和 TOML 里不允许出现真实的密钥;如果你准备长期给团队用,还可以加一个 CI 检查,扫描种子文件的敏感模式。

第二是种子文件的维护责任。谁来维护全局 base.toml?项目描述更新了谁去改 project.toml?如果不指定责任人,这个文件很快又会变成一个谁都在写、谁也不负责的垃圾堆。我这边是跟架构评审走:项目级种子变更必须走一次代码评审,时机就是项目结构大调整的那一次提交。

第三是模型能力边界。不同的 AI 助手模型对上下文长度和指令遵循能力不一样,哪怕你的工具能输出一万字,模型未必能全部有效利用。所以,把种子文件当代码来收拢,尽量控制在 3000 token 以内的做法是最稳的。

我个人用下来的体会是,context-mode最大的价值不是这个工具本身有多厉害,而是它逼着我把“到底应该让 AI 知道什么”这件事想清楚了。过去我总觉得上下文乱是模型的问题,现在回头看,大部分情况是输入方自己没有管理好。如果你也想试一试,我建议先别急着写代码:把你平时反复粘贴给 AI 的那几段话收集起来,整理成三个文件,手动合并一周。如果发现真的能省时间,再照着这篇文章的思路把它做成工具也不迟。

返回列表