我一直有个心得AI 编码助手其实很挑“工作现场”。模型本身的能力差距没你想的大真正拉开体验的是你能不能在一个可控的范围内给它喂正确的信息。这套可控范围的做法我习惯把它叫做 context-mode也就是上下文模式。它不是某个工具里现成的按钮而是一套围绕 AI 编码助手的上下文管理方案谁来提供信息、提供多少、保留哪些历史、允许输出多长全部用可切换的模式来约束。这篇文章适合正在用 AI 做开发、但经常被“模型聊久了就忘事”“上下文不够用”“输出跑偏”折腾的同学也适合想做 agent 或插件开发的人参考。看完之后你可以照着搭一套把 token 预算、历史摘要、项目扫描都梳理清楚的最小可用系统。1. context-mode 到底在解决什么问题1.1 AI 的“工作现场”就是上下文先说一个我在每个新项目里都会遇到的场景。模型刚开始工作时明明很“灵”能记住你们定义的函数名会照着项目已有的目录结构来提建议可只要连续对话几轮或者项目里塞进来一个比较大的模块它就开始变得很像一个拿到半张地图的临时工——回答开始泛化调用名写错风格跟前文不统一。原因不是模型失效而是它的上下文被用光了真正重要的约束、刚跑出来的报错、团队规范被中间大段闲聊和无关文件挤出了窗口。“上下文”在这里指的是每次请求时发给模型的那一堆文字相当于 AI 临时“打开的工作现场”。人类打开 IDE 时能看到整个项目但模型只能看到我们放进上下文窗口里的内容。窗口有硬性上限短则几千 token长则十几万 token但不论多长都架不住“什么都往里塞”。context-mode 的核心就是把“现场长什么样”从失控变成可控。1.2 我给 context-mode 定下的三种模式我最终实现的 context-mode并不是一个单纯改了改提示词模板而是通过配置文件驱动的状态机。默认有 light、standard、deep 三种模式分别针对不同任务类型。它们之间不是“多了一句口号”的区别而是三个维度的完整切换token 预算、项目信息扫描深度、历史保留策略。模式典型场景输入预算项目扫描深度历史保留策略light快速问答、生成提交信息、解释报错约 2000 token只给目录树和必要依赖信息最近 1 轮 摘要standard日常开发、单文件改动约 8000 token目录树 与本次改动直接相关的文件最近 3 轮 摘要deep跨模块重构、接口梳理、全局排查约 16000 token核心源码 测试 文档尽量全量完整会话历史这些数值不是固定的我自己的项目里会按规模调整但配置逻辑是通用的每一档只改三件事token 预算、扫描范围、历史保留。这三件事直接影响模型“读到什么、记住什么、能写多复杂”。拿 deep 模式举例它不是为了把上下文塞满而是为了长任务下保证不丢关键信息而 light 模式则是刻意让模型“少读一点”让它在短平快任务里不被无关文件干扰。1.3 为什么坚持“模式”而不是“自动压缩”现在很多 AI 助手都提供自动压缩功能聊到一定长度后把之前的对话压成一段摘要继续往下走。我实验过一段时间后还是决定在自己的工作流里降低自动压缩的优先级。自动压缩的出发点是好的但系统替你做决策时会悄悄丢掉关键信息。它可能保留了你们对某个按钮交互的详细讨论却把你上一条命令里“不要修改公共接口”这样的硬约束丢掉了。手动切换 context-mode看起来笨拙却换来了可预期性你清楚每一档会拼出什么类型的输入你清楚在哪一步模型会丢掉旧内容你可以在任务开始前先锁定模式。当然我也加了自动降级提醒但它只做提醒不做静默修改。上下文的所有变化都要可见后面查日志、回溯问题才做得到。这套思路贯穿了后续所有设计。2. 拆开上下文四类信息和预算分配逻辑2.1 一次请求里上下文由哪四类信息组成要实现 context-mode第一步不是写代码而是搞清楚每次请求中到底有哪些内容会占 token。我拆过一段时间的日志后把上下文分成四类系统提示词告诉模型“你是谁、遵循什么规范”例如“你是资深后端工程师”“项目统一使用切片编程风格”“所有 API 必须走网关”。项目信息目录结构、关键配置、目标文件、相关签名、编译期装配信息。会话历史过去几轮的用户消息和助手消息模型要靠它理解“我们刚才聊到哪了”。工具执行结果终端输出、测试日志、文件搜索结果、语法诊断、LSP 返回的错误信息。这四类内容像拼图一样拼进同一个窗口。context-mode 做的事就是为每一块规定上限和优先级。拿 standard 模式举例我会在上下文中给四个区块分配大概的比例内容块输入预算占比说明系统提示词约 10%固定在最前面优先级最高不能压缩项目信息约 35%投入产出比最高但要裁剪会话历史约 35%只保留与当前任务相关的部分工具执行结果约 20%保留最新报错和测试输出即可这个比例不是绝对的。工具结果如果特别重要比如编译报错是当前唯一线索我会把历史的比例临时调低把报错放大。记住一个原则哪一块信息在决定这次任务的成败哪一块就是老大。2.2 准确估算 token别用“字数除以 2”做预算的前提是算得准。估算 token 这事用“一个汉字约等于 2 个 token”之类的经验值很容易翻车。中文语义下一个汉字可能被切成一个到三个 token代码里一个短函数名可能只占一个 token一行很长的字符串又可能被切得非常碎。最靠谱的做法是用模型对应的 tokenizer。以 OpenAI 系模型为例用 tiktoken 库就能直接拿到准确数字import tiktoken def count_tokens(text: str, model: str gpt-4o) - int: try: enc tiktoken.encoding_for_model(model) except KeyError: enc tiktoken.get_encoding(cl100k_base) return len(enc.encode(text)) if __name__ __main__: sample 把这段文字作为样例看看切出来多少 token。 print(count_tokens(sample))注意不同厂商的模型 tokenizer 不同甚至同一家的不同版本也可能不同。Claude、Llama、Gemini 各有自己的分词方案拿 OpenAI 的编码去估算别家模型预算一定会漂移。我的习惯是把所有准备进入模型的内容合并成一段文本再用对应模型的计数器统计最后拆回到四个区块里。每个区块超了预算就在组装前先做裁剪而不是等模型收到请求后傻乎乎地截断。2.3 预算不足时按什么顺序丢弃信息context-mode 中最核心的不是技术而是取舍。当仓库很大、历史很长、预算有限时我遵循一套明确的优先级从最高到最低项目硬性规则命名规范、框架选型、接口约束、不允许使用的库。当前报错和测试失败信息这些是当下最直接的上下文。准备修改的文件内容模型要改的是它它不进场就全靠猜。与目标文件有依赖关系的文件调用方、被调用方、类型定义。目录结构和全局配置让模型明白“东西大概在哪”。对话历史里与当前任务无关的部分。通用知识排序算法、常见设计模式这类模型已经知道的内容能不占就不占。这个顺序最反直觉的地方是很多人习惯把对话历史完整保留却忽略了报错信息。实际上模型的长处恰恰是通用知识它缺的从来都是“你项目的本地事实”。所谓上下文优化就是让本地事实尽可能多、通用废话尽可能少。2.4 切换模式的三种触发方式模式不能只靠手动切否则很容易忘了切。我在实现里做了三种触发路径手动切换在终端敲ctx --mode deep或者通过 IDE 快捷键切换。适合任务开始前先锁定模式。自动降级在组装上下文时估算 token如果超过当前模式预算的 90%就自动降一档并向用户打印一行提示“context-mode: 8.4K 8K已切换至 light”。任务类型预判通过文件变更范围判断这次任务是否涉及跨模块比如同时改动了service/order.py和dal/order.go就自动进入 deep只改了一个页面组件就退回 standard。手动为主自动为辅。自动预判不能太激进我做批处理迁移时会把模式固定为 deep防止中途为了省 token 自动切走。判断规则宁可保守不要自作聪明。3. 实操从 0 到 1 搭一套最小可用工作流3.1 先定义配置一个 JSON 文件管住所有模式要落地第一步是把所有模式规则从代码里抽出来放到独立配置文件里。我的做法是在项目根目录建.ctx/modes.json随仓库一起管理。这样换电脑、加队友、换模型都能复制同步。我给出一个可以直接动手改的示例{ default_mode: standard, window_size: 32000, reserved_output_tokens: 2400, modes: { light: { description: 快速问答、生成提交信息, budget_tokens: 2000, system_prompt_tokens: 500, repo_scan_depth: 1, history_rounds: 1, history_summary: true }, standard: { description: 日常开发、单文件改动, budget_tokens: 8000, system_prompt_tokens: 800, repo_scan_depth: 2, history_rounds: 3, history_summary: true }, deep: { description: 跨模块重构、接口梳理, budget_tokens: 16000, system_prompt_tokens: 800, repo_scan_depth: 3, history_rounds: 99, history_summary: false } } }说明几个字段。window_size是模型真实上下文窗口reserved_output_tokens是为模型输出预留的空间所以任何模式的budget_tokens最大不能超过window_size - reserved_output_tokens。如果窗口是 32000预留 2400那输入预算理论上限是 29600我给 deep 设 16000 是在这个范围内故意留了余量。repo_scan_depth我约定1 代表只给目录树2 代表目录树加与目标文件直接相关的源码3 代表核心源码、测试和文档全量扫描。3.2 写一个上下文构造器按模式拼装请求配置文件有了还需要一个程序把所有内容按预算拼起来。下面是一段能跑通思路的 Python 示例import json from pathlib import Path import tiktoken def count_tokens(text: str, enc) - int: return len(enc.encode(text)) def truncate_to_tokens(text: str, limit: int, enc) - str: tokens enc.encode(text) if len(tokens) limit: return text return enc.decode(tokens[:limit]) class ContextBuilder: def __init__(self, config_path: str .ctx/modes.json, model: str gpt-4o): raw json.loads(Path(config_path).read_text()) self.window_size raw[window_size] self.reserved raw[reserved_output_tokens] self.modes raw[modes] try: self.enc tiktoken.encoding_for_model(model) except KeyError: self.enc tiktoken.get_encoding(cl100k_base) def build(self, mode: str, system_prompt: str, project_sections: list[str], history: list[str], tool_logs: list[str]) - str: cfg self.modes[mode] sections { system_prompt: system_prompt, project_info: \n\n.join(project_sections), history: \n\n.join(history[-cfg[history_rounds] * 2:]), tool_logs: \n\n.join(tool_logs[-5:]), } ordered_keys [system_prompt, project_info, tool_logs, history] final_parts [] used 0 for key in ordered_keys: limit max(0, cfg[budget_tokens] - used) piece truncate_to_tokens(sections[key], limit, self.enc) final_parts.append(piece) used count_tokens(piece, self.enc) return \n\n.join(final_parts)这段代码的思路很简单按优先级顺序把四个区块塞进去前面的区块先吃预算后面的区块剩多少吃多少。如果project_info特别长会先被压短而system_prompt永远在最前面、最优先不会被压缩到消失。真实工程里还可以加缓存、加并发锁、加摘要替换但骨架就是这样的模式不同等于换了一组预算和扫描参数拼装逻辑完全复用。3.3 一定要给输出和模型行为留余地第一次实现时我犯过一个低级错误把输入预算顶到了窗口的 95%。结果模型每次生成到一半就断了或者输出显得畏畏缩缩好像知道自己的“字数不够了”。后来我把reserved_output_tokens单独列为配置项才想明白上下文窗口是输入和输出共用的物理空间模型生成答案也要占 token。建议是至少预留 10%~30% 给输出。按 32000 窗口来算reserved_output_tokens我取 2400偏向保守。如果预判这次任务会产生长输出比如让模型生成一整个模块代码我会把预留值临时调高到 5000同时把模式从 standard 切到 deep。否则一次长代码生成就可能输送到一半被窗口卡死。另外不要小看格式开销。一次请求里还有角色标记、分隔符、系统消息结束符这些隐式 token它们不在你的业务文本里但会真实计入统计。预留的空间能把这类隐形开销一并兜住。3.4 加日志把上下文使用变成可分析的数据模式切了、上下文拼了、请求发出去了然后呢如果不记录你又回到了“靠感觉调参数”的老路。我在每次请求前后都埋点输出一行 JSON 日志ctx --mode standard --log .ctx/request.log一条日志大概长这样2025-06-18 10:21 modestandard input5140 output1820 ctx_used70% resultsuccess filesrc/service/user.py 2025-06-18 10:25 modedeep input11800 output2300 ctx_used74% resulttype_error filesrc/service/order.py字段分别是发生时间、当前模式、输入 token、输出 token、上下文占用率、任务结果、涉及文件。攒两周数据后你可以快速看出几个问题哪种模式下出错的概率更高哪个文件的扫描成本最离谱哪类任务总是触达预算导致降级这些数据反哺配置模式参数就不必再靠猜。4. 常见问题与排查实录4.1 deep 模式反而变笨多半是无关信息污染有人问过我为什么切换到 deep 模式后模型表现反而更差我遇到过的最常见原因不是模型不够强而是上下文被无关信息污染了。deep 模式扫描范围更大会把大量老代码、旧规范、废弃接口一并塞进去。模型读多了陈旧风格自然被带着跑偏。排查思路很简单先跑一个 light 模式的同一请求如果输出反而更稳定那说明问题不在“信息不够”而在“噪声太多”。解决办法是在 deep 模式下增加裁剪约束例如只扫描 import 路径上能到达的文件忽略测试夹具、生成目录和第三方代码。后来我甚至给扫描器加了一块规则与目标文件没有依赖关系的文件默认不进入上下文。4.2 切换模式后模型像失忆了有一段时间我设置了history_rounds2后模型连刚才讨论过的 API 名字都记不住。查日志发现我的“轮”数取错了对象直接把最近的 2 条消息当成了 2 轮实际上那可能只是 AI 的一条回复加你的一条追问根本不够表达一段完整对话。正确的做法是把“轮”定义为“用户消息 助手消息”这个完整配对。同时在切换模式时把更早的历史压缩成摘要放到历史区块的最前面。比如先写一行summary当前任务修复用户列表接口 500 错误已确认问题在分页参数类型/summary再放最近 3 轮完整对话。这样即使只保留少量历史模型对任务背景仍有基本盘。deep 模式之所以效果好也就是因为它把这条摘要拉得更长、更接近原文。4.3 token 估算和厂商扣费对不上预估 token 和实际扣费不一致也是常见问题。刚开始我用 OpenAI 的 cl100k_base 编码估算所有模型结果在部分模型上偏差最高到 20%。原因是不同模型的词汇切分表不同直接套用必然不准。更稳妥的做法是每次请求完成后从 API 返回的 usage 字段里读实际 token 数把它回写到日志和本地缓存里。用真实数据反推和校正比任何经验公式都可靠。如果厂商不直接返回 token 明细可以用“字符数 × 系数”先粗估再在多次请求后得到该模型的实际系数。把系数写进配置而不是硬编码在代码里后续校准才不麻烦。千万别在“差不多够”的边缘反复试探超了预算截断伤害的是整段输出。4.4 给新手的避坑清单最后整理几条实操心得都是我用坏了无数个 session 换来的模式数量不要超过 4 个。三四档足够太多档会让人懒得切最终又回到“一刀切”。配置文件必须集中不要散落在代码各处的 magic number 里。改参数只改 JSON代码只负责执行。自动降级只能提示不能静默。所有上下文变化都要能被日志看到否则出了问题根本无从定位。不要迷信长窗口。窗口再大也装不下整个仓库更装不下“所有信息都重要”这种幻觉。每接入一个新模型先花十分钟跑一组 token 估算对照把你的模式预算重新标定一遍不要沿用旧参数。对我而言context-mode 最后的形态就是一种对上下文的预算管理明白哪些内容值得占内存、哪些内容可以舍弃、哪些历史需要保留这本身就是 AI 辅助开发的核心能力。它不是什么高深算法但跑顺之后整个工作流的稳定性会有明显提升。