从“一句命令”到“会记事的助手”:我为什么坚持做一个本地可切换的 context-mode
先交代一下背景:我最近在折腾一个叫context-mode的小项目。说白了,它就是一个带“上下文模式”切换能力的本地 AI 对话工具,让我在同一个终端里,既能快速闲聊、又能切到代码审查模式、还能切到长文档分析模式,而且每种模式下的对话记忆、系统提示词、token 预算都互相独立。项目跑起来之后,身边好几个同事都来问我怎么做的,因为大家普遍被大模型对话“串味”的问题折磨得够呛——聊着聊着,模型突然忘了之前在干嘛,或者前一秒还在帮你写代码,下一秒就变成日常唠嗑的语气,非常难受。
这个项目的核心价值,其实不是“又一个聊天机器人”,而是解决大模型应用里最容易翻车的一个细节:上下文的管理方式。很多人以为把对话历史一股脑塞给模型就行,但实际跑过生产环境的人都知道,token 会爆、指令会漂移、风格会错乱、记忆会污染。而 context-mode 用一种很朴素的方式把这些问题拆开了:给每一类任务一个独立的上下文空间,再给每个空间配上专属的“行为准则”和“记忆窗口”。适合谁参考呢?我觉得主要是三类人:正在用 API 做个人工具的开发者和技术爱好者、需要把大模型接入工作流的效率控、以及想搞懂 prompt 工程和上下文管理到底怎么落地的学习者。这篇文章我尽量不写废话,把我从设计到实现到踩坑的全过程都摊开讲。想看结论直接拉到最后,想自己复刻一遍的,建议从头读。
1. 先搞明白:context 为什么会“乱”,mode 到底在管什么
1.1 大模型上下文管理的三个死穴
在讲 context-mode 之前,必须先搞清楚我们到底在解决什么问题。大模型对话里的 context,不是简单的一段文本,而是“系统提示词 + 历史消息 + 用户当前输入 + 可能的检索结果”拼起来的一个大包裹。这个包裹决定了两件事:模型知道自己在干什么,以及模型能回忆起什么。但在实际使用中,这个包裹往往会在三个地方出问题。
第一是指令漂移。同一个上下文里,如果用户一会儿让它扮演翻译、一会儿让它写代码、一会儿闲聊,系统提示词如果又是固定不变的,模型很容易被后面进来的信息带偏。我见过最夸张的一次,系统提示词明明写着“你是代码审查助手”,结果聊了二十轮后,模型开始教我怎么做饭。第二是token 预算失控。历史消息是无限累积的,但模型的上下文窗口是有限的。哪怕你现在用的是 128K 的模型,塞满之后要么报错,要么被强行截断,而截断的往往是“最早的系统指令”,于是模型彻底失忆。第三是记忆污染。这是最隐蔽的一个问题——前一个任务的对话残留,会影响后一个任务的输出质量。比如你刚聊完一个悲伤的故事,接着让它写代码注释,它可能会在注释里带上奇怪的语气。
这三个问题,本质上都是“把不同任务的上下文混在一起”导致的。所以 context-mode 的思路很直接:别混,分开管。每一个模式就是一个独立的上下文容器,互不干扰。
1.2 模式切换不是“换 prompt”,而是换一整套对话环境
很多初学者会以为,context-mode 就是“切换一下系统提示词”。这个理解不对。模式切换至少涉及四个层面的联动变化,缺一个都会让整个系统的行为变得别扭。
- 系统提示词:每个模式有一套独立的 system prompt,这没什么好说的。
- 历史消息列表:切到代码模式时,不应该看到闲聊模式的对话历史,否则模型还得从一堆无关信息里“找重点”,既浪费 token 又容易误判。
- 记忆窗口大小:闲聊模式可能只需要记住最近 6 轮,但长文档分析模式可能需要记住最近 30 轮,甚至需要额外的固定摘要。
- 行为参数:代码模式的 temperature 我习惯调低到 0.2,让输出更稳定;闲聊模式则可以调到 0.8,让回复更活跃。如果这些参数跟着模式一起变,体验会好很多。
所以我在设计 context-mode 的时候,把它定义成了一套“会话环境快照”机制。切换模式,等于把当前这套对话环境整体存档,再立即载入另一套完整的环境。存档之间互不影响。这个设计思路,类比一下生活场景就很好理解:你在家里是家庭成员模式,在公司是员工模式,切换的不是“说话的措辞”,而是你整个角色、记忆重心和行为准则。context-mode 就是给大模型做了一个“多重人格切换器”,而每一重人格都拥有独立的记忆档案。
2. 模式分层、记忆窗口与 token 预算:先把设计图纸画清楚
2.1 三种模式足够覆盖 90% 的日常需求:闲聊 / 代码 / 文档
在设计模式分类的时候,我一开始列了七八种模式,比如“翻译模式”“写作模式”“周报模式”“数据分析模式”,但后来发现模式越多,维护成本越高,而且用户(也就是我自己)根本记不住什么时候该用哪个。最后我收敛成三种基础模式,再通过每种模式内部的 prompt 变量去适应用途。
- chitchat(闲聊模式):负责日常对话、头脑风暴、概念解释。特点是历史窗口短、温度高、即时性强。
- code(代码模式):负责写代码、查 bug、做代码审查、解释实现原理。特点是历史窗口中等、温度低、系统提示词里有严格的输出格式约束。
- doc(文档分析模式):负责读长文、总结要点、抽取信息、对比修订。特点是历史窗口长、需要支持分段注入、答案要求带引用定位。
三种模式的区分,本质上是对“记忆和时间尺度”的区分。闲聊是短时记忆,代码是工作记忆,文档分析是长期记忆。这样分层的好处是,你的 token 预算可以按需分配,而不是永远用同一套窗口硬扛。
2.2 记忆窗口的确定:不是拍脑袋,是算出来的
每个模式我把 max_context_pairs 设成多少,背后是有计算逻辑的。核心公式其实就一条:预估每轮对话 token 数 × 要保留的轮数 ≤ 你希望留给模型生成的剩余窗口。
拿 doc 模式举例。假设我用的是 32K 窗口的模型,系统提示词约 600 token,参数和返回格式约束约 100 token,单轮用户提问平均 400 token,单轮模型回复平均 800 token。那我如果保留 30 轮历史,历史消息就是 30 × 1200 = 36000 token,已经超了。所以要么降低保留轮数,要么对历史做压缩。我实测下来,doc 模式保留 20 轮 + 一个独立的“文章摘要块”是性价比最高的组合。摘要块是额外的:每当对话超过 10 轮,就调用一次模型,把当前文章的核心观点压缩成 500 token 的摘要,后续轮次只携带摘要和最近 10 轮对话。
那为什么不是单纯保留最近 20 轮呢?因为长文档分析有个特性——用户可能会在第 15 轮突然问“那第一部分提到的那个数据是多少”,如果早被挤掉了,模型就只能瞎编。摘要块的加入,相当于给模型留了一份“压缩过的长期笔记”,既控住了 token,又保住了关键信息的可追溯性。
2.3 每个模式独立一套配置,别手软
这是我从实际教训里提炼出来的建议。早期我把 temperature、max_tokens 这些参数做成了全局配置,切换模式只换 prompt 跟历史。结果就是:闲聊模式下模型太死板,代码模式下模型发挥又不稳定。后来我学乖了,每个模式一个配置字典,切换时全量加载。
配置结构我用的是 JSON 文件存储,放在项目根目录的 modes/ 下面,每个模式一个文件。比如 code.json 长这样:
{ "name": "code", "description": "代码审查与开发助手", "system_prompt": "你是一名资深软件工程师,擅长代码审查、Bug 定位和实现方案设计。回答必须给出具体代码或明确建议,禁止空泛描述。", "temperature": 0.2, "max_tokens": 1500, "max_context_pairs": 15, "enable_summary": false, "context_separator": "\n--- CODE CONTEXT ---\n" }每个字段的设定我都有具体考量。temperature 设 0.2,是为了减少代码生成时的随机性,宁可保守也不花哨;max_tokens 设 1500 是因为代码回复通常较长,但又不能长到把后续对话的窗口全占了;max_context_pairs 设 15 是因为代码任务里“刚才定义的那个变量”很重要,太短会失忆,太长则成本太高。至于 enable_summary 为什么是 false,因为代码讨论过程中的信息大多是过程性的,压缩后反而丢失关键变量名和调用关系,保留原始轮次更靠谱。
3. 从零实现一个带 context-mode 的本地 AI 对话助手
3.1 技术选型:Python + 命令行,够用且好维护
我最终选了 Python + 命令行交互的方式来实现这个项目,没有上 Web 界面,也没有用重型框架。理由很朴素:这个项目最核心的价值是“上下文管理模式”本身,UI 只是附庸。用命令行,省掉前后端联调的时间,而且方便我后续拿它做自动化脚本调用。对于本地的个人工具,快和轻就是第一优先级。
对话模型我用的是 OpenAI 兼容的 API 接口,这样好处是后续可以在不同模型服务商之间无缝切换。不过 context-mode 的核心逻辑其实跟具体模型无关——它做的事情只是“拼装不同模式的上下文包”,模型本身只负责根据这个包生成回复。所以就算你不用 OpenAI,换成任何支持 chat completion 格式的模型(比如本地跑的 llama.cpp、Ollama 等等),这套代码框架也完全能跑。
项目文件结构我保持得很克制:
context-mode/ ├── main.py ├── modes/ │ ├── chitchat.json │ ├── code.json │ └── doc.json ├── sessions/ │ └── (运行时自动生成,按模式分区存储) ├── utils/ │ ├── context_manager.py │ ├── token_counter.py │ └── llm_client.py └── requirements.txt这个结构有几个刻意的设计。sessions 目录按模式分区存存储,每个模式一个子目录,每个会话一个 JSON 文件。这样即使用户切到别的模式聊了半天,再切回来,原模式的历史记录依然原封不动。utils 里单独拆了 token_counter.py,虽然代码只有几十行,但独立成模块的意义是方便我未来替换成更精确的 tiktoken 计算。
3.2 核心代码实现:模式管理器与上下文构建流程
main.py 里最核心的是ContextManager类。它负责所有模式的加载、切换、记忆增删和上下文构建。我把它的核心流程写在这里,你可以直接抄:
import json import os class ContextManager: def __init__(self, modes_dir="modes", sessions_dir="sessions"): self.modes_dir = modes_dir self.sessions_dir = sessions_dir self.current_mode = None self.mode_configs = {} self.session_history = {} self.load_modes() def load_modes(self): for fname in os.listdir(self.modes_dir): if fname.endswith(".json"): mode_name = fname.replace(".json", "") with open(os.path.join(self.modes_dir, fname), "r", encoding="utf-8") as f: self.mode_configs[mode_name] = json.load(f) def switch_mode(self, mode_name): if mode_name not in self.mode_configs: raise ValueError(f"未知模式: {mode_name}") self.current_mode = mode_name if mode_name not in self.session_history: self.session_history[mode_name] = [] self.load_persisted_session(mode_name) def add_message(self, role, content): if self.current_mode is None: raise RuntimeError("请先切换到某个模式") self.session_history[self.current_mode].append({ "role": role, "content": content }) self.trim_history_if_needed() self.persist_session() def get_prompt_messages(self): if self.current_mode is None: raise RuntimeError("请先切换到某个模式") config = self.mode_configs[self.current_mode] messages = [{"role": "system", "content": config["system_prompt"]}] history = self.session_history[self.current_mode] if config.get("enable_summary", False) and len(history) > 10: messages.extend(history[-10:]) else: messages.extend(history[-config["max_context_pairs"]:]) return messages def trim_history_if_needed(self): config = self.mode_configs[self.current_mode] max_pairs = config["max_context_pairs"] max_msgs = max_pairs * 2 history = self.session_history[self.current_mode] if len(history) > max_msgs: excess = len(history) - max_msgs del history[:excess] def persist_session(self): mode_dir = os.path.join(self.sessions_dir, self.current_mode) os.makedirs(mode_dir, exist_ok=True) session_file = os.path.join(mode_dir, "current.json") with open(session_file, "w", encoding="utf-8") as f: json.dump(self.session_history[self.current_mode], f, ensure_ascii=False, indent=2) def load_persisted_session(self, mode_name): session_file = os.path.join(self.sessions_dir, mode_name, "current.json") if os.path.exists(session_file): with open(session_file, "r", encoding="utf-8") as f: self.session_history[mode_name] = json.load(f)这段代码有几个细节值得讲。get_prompt_messages里我用了history[-config["max_context_pairs"]:],注意是负索引,意思永远截取列表末端的 N 条消息,这样最早的消息会被自动丢弃,而最近的消息永远完整保留。enable_summary逻辑有点特殊:当模式开启了摘要功能(比如 doc),并且历史消息超过 10 条,就只保留最近 10 条原始记录,摘要数据会额外放在 system prompt 里。但这个版本的代码为了可读性,摘要块的生成逻辑我没贴全,后面实操里我再展开讲。
3.3 实操中的关键动作:切换指令与消息轮转的联动
在主循环里,我设计了一套命令语法。普通输入会当作对话内容发给模型,而以/开头的输入会被识别为控制指令。常用的几个指令:
/mode code:切换到代码模式/mode doc:切换到文档分析模式/new:清空当前模式的历史会话/stats:查看当前模式的 token 占用情况/exit:退出程序
这套指令设计有个容易被忽略的点:切换模式后,用户的无意识对话会被记录到新模式底下。比如用户在 chitchat 模式说了句“今天天气不错”,然后敲/mode code切到代码模式,再问“刚才那句你听懂了吗”,模型是听不懂的,因为新模式的历史列表里根本没有那句话。这种“跨模式记忆的中断”不是 bug,而是预期行为。但它很容易让用户产生困惑,所以我在切换模式时,会额外打印一行提示,告诉用户当前模式的记忆范围和已保留的历史轮数,让“串味”变成一件透明的事情。
消息轮转联动这里也有一个容易被忽视的细节:用户发送消息之后,需要把用户消息和模型回复都记录到历史里,但记录的顺序必须是“先用户、后助手”。我在main.py主循环里是这么实现的:
user_input = input("你: ") if user_input.startswith("/"): handle_command(user_input) continue cm.add_message("user", user_input) assistant_reply = llm_client.chat(cm.get_prompt_messages()) cm.add_message("assistant", assistant_reply) print("助手:", assistant_reply)先 add 用户消息,再调模型,再把模型回复 add 进去,顺序不能反。如果先调模型再记录用户消息,后续轮次模型会看不到“这次它是在回答哪个问题”,长期运行后上下文关联会乱掉。这个顺序问题,是我查了很久才发现的小坑——当时日志里模型的回复越来越答非所问,最后发现是加历史时把顺序搞反了。
4. 实战截图式的核心循环:token 计数、摘要压缩与控制台体验
4.1 token 估算:不引入重依赖也能算个八九不离十
很多教程会直接让你引入 tiktoken 做 token 精确计算,但对于一个本地工具来说,经常是“杀鸡用了牛刀”。我的token_counter.py里用了一个估算函数,对中英文混合场景准确率在 90% 以上,够用了:
def estimate_tokens(text: str) -> int: # 中文约占1.5~2 token/字,英文约占0.3 token/字符 chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff') other_chars = max(0, len(text) - chinese_chars) return int(chinese_chars * 1.8 + other_chars * 0.35) + 2为什么要单独区分中英文?因为 OpenAI 系模型的 tokenizer 对中文是按字形切分的,一个汉字通常对应 1.5~2 个 token,英文则是按字母组合切分。很多项目直接用 len(text) 估算,在中文场景下会严重低估,导致上下文“悄悄超载”。这个函数虽然不精确,但在提示“当前上下文是否快满了”这个场景下,足够给出有效的预警。
在每次对话开始前,我会统计整个 prompt 的估算 token 数,如果超过模式配置里设定的告警阈值(比如窗口的 70%),就打印一条警告,提示用户考虑/new清空或者切换模式。这一步对生产体验至关重要——模型表现变差,很多时候不是模型变笨了,而是你的上下文塞得太满,重要信息被稀释了。
4.2 doc 模式的高级玩法:让“长文总结”不丢关键信息
这是整个项目里头最需要动脑子的部分。doc 模式如果只是简单地保留更多轮历史,堆到 2 万 token 甚至更多,不是不行,但成本和速度都很难看。所以我给 doc 模式额外设计了一个轻量摘要机制。
每当 doc 模式的历史轮数达到 10 的倍数时,系统会额外调用一次模型,生成一条“动态摘要”:
summary_prompt = """请用不超过300字总结当前这篇文章的以下信息: 1. 文章主题与研究背景 2. 核心论点与论据 3. 文中提到的关键数据或引用 4. 尚未讨论完的遗留问题 当前对话历史: {history} """ summary = llm_client.chat([{"role": "system", "content": summary_prompt}])生成的摘要,会被注入到下一次请求的 system prompt 末尾。这样模型在回答后续问题时,手里既有摘要提供的全局视角,又有最近 10 轮原始对话提供的细节线索。我实测下来,这个方案比“保留 30 轮原始对话”在同样 token 预算下,能多记住约 40% 的关键信息。代价是每 10 轮会额外花一次模型调用的费用,但相比上下文超载导致胡言乱语,这点成本完全值得。
4.3 控制台体验的细节:状态可视化是刚需
因为这是个命令行工具,所以“用户当前处于哪个模式、这个模式能记住多少轮”必须显眼地展示出来,否则用户很快就会迷失。我专门在输入框上方搞了个状态栏,每次对话后刷新:
当前模式: [code] | 历史消息: 12/15 轮 | 上下文估算: 3.2/8K token | 温度: 0.2这行信息的价值在实操中非常大。因为我发现,当用户(包括我自己)知道当前的记忆边界在哪里,就不容易产生不切实际的期待。比如你看到只剩 3 轮的记忆空间了,就不会要求模型“根据我们两天前聊的方案继续优化”,而是主动补一句简短的背景概述,人机协作反而更顺畅。
控制台模式还让我养成了一个习惯:每次都尽量把指令写完整,用/mode code开头,而不是一上来就直接问。因为一旦你明确切了模式,系统 prompt 就会自动带上“你是一名资深软件工程师”的约束,回复质量立刻上了一个台阶。如果没有这个机制,同一个模型在面对“帮我看看这段代码”时,可能输出科普式回答,也可能输出实操型回答,全看它的随机发挥,很不稳定。
5. 常见问题与排查:那些让我凌晨三点还在调 bug 的坑
5.1 上下文“串味”:为什么切了模式,模型还记得旧对话
这是我被问过最多的问题。现象很典型:用户从 chitchat 模式切到 code 模式,结果模型回了一句“刚才我们聊的那个话题真有意思,回到你的代码问题上……”。明明系统已经切了模式,为什么模型还带着上一段记忆?
排查顺序如下,你按这个顺序基本五分钟内能定位:
- 检查切换时是否真的重置了 session_history 的当前指针。我的代码里,每个模式有自己的历史列表,但如果你用的是单一列表,切换时没有切换存储区,就会串。
- 检查系统提示词里是否写了“根据上文对话”之类的话。如果 system prompt 暗示模型去参考上下文,模型会更倾向于翻旧账。
- 检查模型请求参数里,有没有把多个模式的 history 一起拼进去。我之前犯过的一个错是在拼 prompt 时用了全局变量而不是
self.current_mode对应的列表。 - 最后一个隐蔽的问题:复用了同一个 LLM client 的长连接,但某些服务端会缓存上下文。这个基本只能通过换 client 实例来验证。
5.2 token 超限后“静默失忆”:截断模型的隐形行为
我遇到过一种特别迷惑的情况:明明没有报错,模型却忘记了系统提示词里的要求。排查到最后发现,是因为消息历史太长,被 API 服务端静默截断了,而且截断的算法是先丢最前面的消息——也就是 system prompt。所以你辛辛苦苦写的“你是一个资深工程师”,在 token 超限那一刻就没了。
解决方案就是我在 4.1 里说的 token 预警机制。并且在代码里加了一个硬性保护:构建 prompt 前,如果估算 token 数超过配置的 max_context_tokens,就直接拒绝发送本次请求,并提示用户先/new清空历史,而不是产生一次注定很差的请求。
5.3 模式配置改完不生效:配置文件缓存的血泪教训
开发过程中我发现一个特别容易踩的坑:改了 modes/code.json 里的 temperature,但实际请求带过去的还是旧参数。原因是我在程序启动时一次性 load 了所有配置到内存,之后修改 JSON 文件不会触发重新加载。当时我一度以为是 API 没生效,排查到差点去翻服务商的文档,最后发现是本地缓存。
好,那这个问题我是怎么处理的?其实方案也简单,我给加载逻辑加了个中间检查项:每次调用时判断文件的修改时间。实现上不那么优雅,大意就是if os.path.getmtime(json_path) > self.loaded_time: 重载配置。后来我又试过直接把配置改成数据库存,但最终还是觉得 JSON + 修改时间检测更适合个人工具,毕竟没有引入额外依赖,改配置也直观。
5.4 摘要模式和原始历史的取舍:什么时候该开,什么时候该关
我给每个模式都设置了enable_summary,但并不是所有模式都适合开摘要。最初我把所有模式都开了,结果闲聊模式出问题了——聊到第 10 轮就开始做“摘要”,反而把生动具体的上下文压缩成了干巴巴的骨架,模型回复的质感变得很僵硬。
所以现在的建议非常明确:只有文档分析这类“信息密度高、逻辑链条长、话题单一”的任务适合开摘要;闲聊和代码这些“语言风格本身就是上下文一部分”的任务,宁可多花 token 保留原始历史,也绝不要用摘要压缩。代码任务里一个变量名的丢失,比省那几百 token 的代价大十倍。
6. 运行实测:三个模式下的典型对话流与真实表现
6.1 chitchat 快速闲聊:短平快,不占资源
我实测的对话流是这样的。进入程序后,默认模式是 chitchat,我直接问了一个开放性问题:“用一句话解释什么是递归?”
模型输出:“递归就是你打开一扇门,里面还是一扇门,只不过每次打开门,门上的锁都小了一点。直到有一天,锁不见了,而你发现自己已经站在房间里了。”
这个回答明显是温控调高之后的效果,有创造力、不干瘪。和 code 模式下同一个问题的回答对比一下就很有意思。在 code 模式下我问同样的问题,输出是:“递归是一种函数调用自身的编程技巧。必须注意设置终止条件,否则会栈溢出。示例:def fact(n): return 1 if n <= 1 else n * fact(n-1)。”
同一个模型,两种股气儿。这就是模式化上下文最大的价值——不是换了模型,而是换了“说话方式和工作方式”。靠什么换的?就是那套精心设计的 system prompt 和 temperature。
6.2 code 模式长对话:连续三轮代码审查不跑偏
这是我最看重的一个场景。我贴了一段有 bug 的 Python 代码进 code 模式,让它审查。第一轮它指出了两个逻辑问题并给了修复建议。第二轮我继续追问“如果输入是负数,你那个修复方案还会出问题吗”,它基于第一轮讨论的上下文做出了正确的推演——它记得“刚才的修复方案是什么”。第三轮我再让它“把最终修正后的完整代码输出”,它给出的版本同时包含了两轮讨论中的修复点,没有遗忘,也没有把无关内容混进来。
这个场景如果放到旧的“全局上下文”架构里,一旦中途夹几句聊天,代码审查的连续性就被打断了。而在 context-mode 的 code 模式里,由于记忆窗口只属于代码任务,所以整个讨论就像和一个“只专注代码的同事”在对话。
6.3 doc 模式长文分析:总结、追问、溯源三连
doc 模式的实测里,我喂了一篇 8000 字左右的技术文章。第一轮让它总结核心论点,模型给了 5 个要点。第二轮我追问“第三个论点提到的实验数据是多少”,模型准确输出了数字,因为最近轮次里有原文。第三轮我让它“找出文章中支撑第一个论点的两个例子”,模型在摘要和最近上下文的配合下,给出了定位准确、边界清晰的回答。
最让我意外的是第四轮:我故意切换到了 chitchat 模式聊了十个回合,再切回 doc 模式,直接问“刚才那篇文章的第二个论点是什么”,模型依然能回答上来。因为 doc 模式的独立记忆被完整保留着,没被 chitchat 的对话冲掉。这个能力的本质就是“独立记忆区”——它把大模型的单线程上下文,拆成了多线程并行存储,每个线程各司其职。
7. 独立记忆区的价值与局限:context-mode 到底解决了什么
7.1 记忆文件的持久化:关掉程序,重新打开,记忆还在
这是 context-mode 项目的另一个核心设计:所有模式的对话历史都通过persist_session落盘成 JSON 文件。这意味着我关掉终端,甚至重启电脑,再打开程序并切到 code 模式,昨天聊到一半的代码审查上下文还能继续沿用。
这个特性在真实工作中非常实用。白天我可能在分析一篇技术文档(doc 模式),晚上有人让我帮忙看代码(code 模式),第二天早上想继续整理文档分析的思路时,只需要启动程序、切到 doc 模式,一切都还在。如果你的工作涉及“多任务并行 + 随时切换再切回”,这一点体验上的提升是质的飞跃。
不过也要说实话,持久化存储有个注意点:随着历史积累,文件会越来越大。我当前模式的历史轮数最大 30 对,一个文件通常不超过 200KB,个人工具完全没压力。但如果想做成多人使用的服务,就得换数据库存,并且要做会话隔离和过期清理。这是 context-mode 当前的一个边界。
7.2 什么时候 context-mode 会失灵:三个已知的边界
写到这里我必须补一句实话,context-mode 不是万能药。它有几个边界情况,我用下来发现处理得并不完美。
一是跨模式的信息关联。比如用户在 chitchat 里提到“今天的心情影响了我写代码”,然后在 code 模式里说“帮我写个心情日记程序”。模型不知道这个需求背后的情绪背景,因为它看不到另一个模式的记忆。这个在现有架构下确实没法很好解决,除非我做全局摘要或者跨模式检索。
二是模式数量膨胀后的管理开销。我早期测试时每个模式都有一大堆自定义 prompt 和参数,改起来疼得要命。后面收敛到三种模式才舒服了。模式不是越多越好,每多一个,就多一份维护成本。
三是模型窗口本身的硬件天花板。无论怎么优化组合,都是在一个固定的窗口内做分配。如果真的需要百万 token 级别,那得走检索增强(RAG)路线,仅仅靠模式化分配是不够的。
7.3 后续可以怎么扩展:从个人工具到团队工具的想象空间
这个项目如果只是想自己用,现在这个规模刚刚好。但如果想让同事也用起来,我会考虑做几件事。
首先是把模式配置做成共享的,做一个简单的配置拉取机制,让团队每个人都用同样的代码审查规范、同样的文档总结模板。其次是把记忆区从本地文件挪到中心化存储,这样同事之间可以共享同一个会话——比如我在 code 模式里审查到一半,同事接过去继续看。最后是给每个模式加一个“可引用外部知识库”的开关,当用户触发某个关键词时,自动去检索内部 Wiki 并把结果注入到上下文里。这些方向都不难,但工作量不小,我准备有空再慢慢折腾。
回到最初的想法:context-mode 本质上是一个“让大模型按需记忆、按模式工作”的容器。它不是模型的一部分,而是模型外面那一层我们完全能控制的东西。这层控制权,才是本地工具相对直接用公网聊天网站最大的优势。跑完整个项目,我最深的体会就是,上下文管理真的是大模型应用工程的“水电煤”,平时不显山不露水,但出了问题,整个系统都在遭殃。如果这篇文章能帮你少踩几个我在深夜踩过的坑,那我就没白写。最后再分享一个小技巧:给“模式切换”这种指令动作加上明显的视觉反馈(状态栏高亮、切换提示文本),能极大降低对话漂移带来的心理不适感,别问我怎么知道的。