☰
context-mode 详解:给AI批处理工具加上上下文记忆
2026/10/20 4:13:26 网站建设 项目流程

最近我在折腾个人知识库里的文本清洗和摘要任务,手头攒了一大堆零散的调研笔记,想批量交给AI工具做整理。一开始我写了个非常简单的脚本,把每一段文本都当成完全独立的条目去处理,跑了两天就发现结果格外割裂——同一页笔记的上半段和下半段,居然会得出互相矛盾的摘要。后来我加了一个叫 context-mode 的开关,让每个处理单元都能感知到前一个单元产生的中间结果,整个输出质量才总算稳定下来。这篇就把我对 context-mode 的理解、设计、实现和踩坑过程完整聊一遍,适合正在做数据管道、写AI辅助脚本,或者单纯想给你的命令行工具加一点“记忆”的读者。

1. 先聊清楚:context-mode到底是什么

1.1 一句话定义与对照

context-mode 指的是在处理任务时,把任务之外的关联信息一并带入处理过程的运行模式。核心动作是“带上下文”,它和最常见的无状态模式互为对照。无状态模式里,每一个任务都像第一次见面,脚本、模型、工具对之前发生过什么一无所知;context-mode 里,每个任务都能“翻以前的老账”,前一阶段的结论会直接影响当前阶段的理解和输出。

我习惯用一个生活化的类比来解释:无状态模式像你在路边随机找人问路,每问一个人都要从头描述“我从哪来、要去哪”;context-mode 则像家庭医生,他手里有你历年的病历和过敏史,你说一句“最近咳嗽”,他能立刻把背景串起来,而不是让你把五年的病史复述一遍。

这个模式在软件工程里还有一堆相近的说法:带状态处理、会话保持、记忆机制、上下文感知。落到实际代码里,核心不过就是:额外维护一份数据结构,在每次任务开始前把它塞进处理输入,在任务结束后把本次结果写回去。

1.2 三种常见形态

形态实现方式典型场景
显式上下文任务启动时明确传入一份资料单条日志诊断时附加完整堆栈信息
会话上下文同一会话内自动累积历史记录连续对话机器人、客服脚本
全局上下文读取固定不变的配置或知识库带固定人设的AI助手、统一风格改写

显式上下文最容易被理解,你的脚本启动参数里加一个--context-file,把资料读进来,拼在提示词前面,完事。会话上下文则需要维护一个会话ID和对应的历史列表,每一次交互结束都追加记录。全局上下文则更像是“环境变量”,内容基本固定,但会持续影响每一次处理。

这三种形态在同一个工具里往往同时存在。我做的 ctx-tool 里,全局上下文保存了输出格式要求,会话上下文保存了前面每一段文本的摘要,显式上下文则用于临时指定某条背景资料。

1.3 适用与不适用的判断标准

我判断要不要用 context-mode 只有一个标准:任务之间是否存在状态依赖。

如果每一条处理对象是完全独立的,比如把一百张图片转成黑白图,那么无状态模式最合适,简单、快、不会出幺蛾子。但如果结果依赖前面发生了什么,或者需要保持一致的口径和风格,甚至需要多条记录拼成一个完整结论,那就该上 context-mode。

依赖程度还可以分等级:轻度依赖(只需要刚才那一条结果)、中度依赖(需要最近N条)、重度依赖(需要从开头到现在的全部信息)。不同等级对应的实现复杂度完全不同,别一上来就无脑做全量历史。

2. 为什么我非要给工具加context-mode:无状态处理的三个真实痛点

2.1 语义断裂,前后结果互相矛盾

这是我踩到的第一个坑,也是最直观的一个。

我清洗的笔记里有一页是某个项目的前期调研,前半段写着“方案A初选通过,性能表现优秀”,后半段则补充说“方案A因成本问题最终被否决”。因为我的脚本是逐行独立处理,前半段被摘要成了“推荐方案A”,后半段被摘要成了“方案A不可行”,两条结论放在知识库里,谁看谁懵。

无状态模式根本没有“整体观”,它像盲人摸象,每次只摸到一块。一旦原始数据的语义跨段落分布,独立处理就必然产生断裂。这不是模型能力的问题,而是信息输入结构的问题——你压根没给它看另一只“手”摸到的部分。

2.2 重复描述消耗时间和上下文预算

如果不做上下文传递,那你只能把所有背景都重复塞进每一条任务里。我在给AI工具写批量改写需求时,每一条提示词都要把“你是专业编辑、统一术语、输出Markdown、不要解释过程”这些规则重写一遍。刚开始还能忍,到了几十条之后,人就开始烦躁了。

更麻烦的是,这部分重复内容会持续占掉模型的输入长度预算。假设你单次允许的输入上限是8000字,背景说明就占去1500字,真正留给待处理文本的空间只剩八成。数据规模一大,这一笔开销是纯浪费。

2.3 下游处理需要连贯口径

除了语义一致性,还有一个容易被忽略的点:风格和术语的连贯性。

我给一段长文档做统一风格改写时,如果每段无状态处理,第一段你可能用“用户画像”这个说法,第二段可能就漂移成“客户肖像”,术语来回跳。开了 context-mode 后,第一段里定下来的措辞、语气、结构会延续到后面,整体看起来才像同一批人写的。

2.4 引入后的代价与权衡

不过我得把话说全,context-mode 不是免费的午餐。它的代价至少体现在四方面:存储成本(要维护历史记录)、计算成本(上下文越长处理越慢)、调试难度(结果不仅取决于当前输入还取决于历史状态)、隐私风险(背景信息被持久化存放)。

所以我的习惯是:默认 plain,需要时才开 context。工具默认跑在无状态模式,用户明确知道后面有上下文依赖,才手动切换。这把决定权留给使用者,而不是让程序偷偷记住一切。

3. 手写一个带context-mode的小工具

3.1 工具要做什么

为了让这个概念落地,我写了一个名为 ctx-tool 的命令行小工具。它的功能很简单:读取一个文本文件,把里面每一行当作一个条目,逐条生成摘要。支持两种运行模式:

  • --mode plain:无状态,每条独立生成摘要。
  • --mode context:上下文模式,生成摘要时带入上一条的摘要结果,让结论衔接。

之所以用这个例子,是因为它足够小,却覆盖了 context-mode 的全部关键环节:上下文初始化、读取、写入、持久化、模式切换。

3.2 目录和配置结构

工具采用零依赖文件存储,整个项目结构很简单:

ctx_tool/ ├── ctx_tool.py └── sessions/ └── ctx_store.json

ctx_tool.py是全部逻辑所在,sessions/ctx_store.json是上下文存储文件。会话文件用 JSON 格式,因为结构清晰、直接能用文本编辑器打开检查,调试起来特别方便。

配置文件里面保存这几项:

{ "session_id": "20250512-093011", "context": "", "updated_at": 1747020611.0, "turn_count": 0 }

3.3 核心实现:上下文存储层

我先把存储层抽出来,单独做成一个类,方便后续扩展。它负责加载、保存、读取和更新上下文。

import json import os import time class ContextStore: def __init__(self, path): self.path = path self.data = self._load() def _load(self): if not os.path.exists(self.path): return { "session_id": "", "context": "", "updated_at": 0.0, "turn_count": 0, } with open(self.path, "r", encoding="utf-8") as f: return json.load(f) def save(self): self.data["updated_at"] = time.time() self.data["turn_count"] = self.data.get("turn_count", 0) + 1 with open(self.path, "w", encoding="utf-8") as f: json.dump(self.data, f, ensure_ascii=False, indent=2) def set_context(self, new_context): self.data["context"] = new_context self.save() def get_context(self): return self.data.get("context", "") def reset(self): self.data["context"] = "" self.data["turn_count"] = 0 self.save()

save方法每次写入都会更新时间戳并递增轮次,这两个字段后面在“过期策略”里会起到大作用。上下文本身就是一个字符串字段,简单直接。

3.4 核心实现:提示构造与主流程

有了存储层,接下来就是构造提示词和主流程。这里我为了演示,用一个mock_summarize函数代替真正的大模型调用,重点展示 context-mode 的数据流。

import argparse def build_prompt(text, use_context, store): ctx = store.get_context() if use_context else "" if ctx: return ( "上一条处理结果:\n" + ctx + "\n\n当前待处理文本:\n" + text + "\n\n请参考上一条结果,结合当前文本生成摘要。" ) return "当前待处理文本:\n" + text + "\n\n请直接生成摘要。" def mock_summarize(prompt): # 演示用:从提示词里直接抠出当前待处理文本,模拟“摘要过程” # 真实项目里这里会替换成大模型接口调用 if "当前待处理文本:" in prompt: return prompt.split("当前待处理文本:")[-1].split("\n\n请")[0][:20] return "EMPTY" def main(): parser = argparse.ArgumentParser(description="ctx-tool") parser.add_argument("--mode", choices=["plain", "context"], default="plain") parser.add_argument("--file", required=True, help="输入文本文件") parser.add_argument("--session", default="ctx_store", help="会话名称") args = parser.parse_args() store_path = f"sessions/{args.session}.json" store = ContextStore(store_path) with open(args.file, "r", encoding="utf-8") as f: blocks = [line.strip() for line in f if line.strip()] for i, block in enumerate(blocks): prompt = build_prompt(block, args.mode == "context", store) result = mock_summarize(prompt) print(f"[{i + 1}/{len(blocks)}] {result}") if args.mode == "context": store.set_context(result) if __name__ == "__main__": main()

主流程的逻辑非常直白:循环逐条读文本,构造提示词,拿到结果,如果是 context-mode 就把结果写回上下文存储。

3.5 实际运行效果对比

用一个四行的测试文件跑一下,效果差异立刻就看出来了。

输入内容:

方案A初选通过 方案A成本过高 方案B进入候选 最终选择方案B

plain 模式下,输出是四条孤零零的摘要,互相没有关系,甚至会出现“方案A通过”和“方案A成本过高”这种并列的矛盾信息。

context 模式下,因为每一条摘要都会拼在下一个 prompt 里,结果会自然衔接成一条逻辑线:“方案A初选通过,但成本过高;随后方案B进入候选;最终选择方案B”。这才是笔记摘要真正该有的样子。

3.6 为什么用文件做存储

ctx-tool 里我选文件系统做上下文存储,而不是引入数据库或缓存服务,原因很现实:零依赖、肉眼可查、随手可改。个人工具的场景里,打开 JSON 文件就能看到当前记忆内容,出问题直接用编辑器修,调试效率极高。

但这也意味着它不适合多人并发和分布式场景。如果你要做一个多进程共享上下文的生产工具,建议换成 SQLite 或单独的存储服务,并加上适当的锁机制。这个问题我在第五节还会展开聊。

4. context-mode的三件核心细节:合并、裁剪与过期

4.1 上下文合并:追加还是覆盖

上下文写入时,最常见的抉择是:每一次新结果进来,应该覆盖旧的,还是追加到历史里?

覆盖策略最简单,内存和存储开销最低,但它会让上下文丢失“过程信息”。如果最终结果是“选择方案B”,你只存这五个字,那后面如果再处理“备选方案C”,它就完全不知道之前还考虑过方案A和B。

追加策略更稳,把每一轮结果都保留下来,形成一条完整历史链,但风险是疯狂膨胀。处理几百条文本后,上下文会变成一篇超长文档,既占空间又可能超过处理工具的长度限制。

我的折中方案是:上下文存储改造成结构化列表,而不是单一字符串。每一轮都追加一条记录,并同时维护一个摘要字段。

{ "session_id": "20250512-093011", "history": [ "方案A初选通过", "方案A成本过高", "方案B进入候选" ], "summary": "方案A初选通过但成本过高,方案B进入候选", "updated_at": 1747020611.0, "turn_count": 4 }

这样既保留了完整过程,又能在上下文过大时只把summary带进请求,兼顾质量与资源。

4.2 上下文裁剪:三种策略对比

不管怎么做,上下文总会有过长的一天。我实操下来,裁剪策略大致有三类:

策略实现方式优点缺点
滑动窗口只保留最近N轮记录实现简单,连续任务稳定早期重要信息全丢
摘要压缩老记录合并成一个整体摘要信息密度高,省空间摘要过程本身有损
重要度筛选按关键词或评分保留部分记录适合检索式任务打分规则难设计

滑动窗口适合处理前后依赖不强、只有短期依赖的任务。摘要压缩适合长文档场景,先让模型把前50条压成200字摘要,后面再处理时带上这200字就够用。重要度筛选更精细,但需要根据业务设计评分逻辑,比如包含关键实体的记录优先保留,我做知识库梳理时常用它。

实际参数设置上,我建议先估算再拍板。一个我一直在用的粗略公式:

合理上下文长度 ≈ 单条平均输入长度 × 需要依赖的轮数 × 1.5冗余

比如单条笔记平均500字,任务需要依赖最近10轮,那上下文长度定在7500字左右比较合理。太小会丢失覆盖范围,太大则白白消耗资源。

4.3 过期与失效:别让旧上下文毒害新任务

上下文如果永远不过期,迟早会污染新的任务。最典型的情况:你上周处理了一篇关于前端框架的笔记,这周开始处理完全无关的财务数据,结果模型还带着前端术语去理解财务文本。

我引入了两个机制解决这个问题。

第一个是 TTL(Time To Live),按时间过期。每次任务开始前,检查updated_at距当前时间是否超过了设置的过期秒数,超了就自动清空上下文。

ttl_seconds = 3600 # 1小时过期 if time.time() - store.data.get("updated_at", 0) > ttl_seconds: store.reset()

第二个是会话隔离,按场景切换。处理不同主题时,或手动指定一个全新的 session 名称,让旧上下文彻底不参与新任务。ctx-tool 里面直接用--session参数解决,不指定就用默认会话,不同会话之间物理隔离。

4.4 TTL参数怎么定才合适

TTL 定得太短,会话做到一半上下文突然被清空;定得太长,又起不到防污染作用。我的经验值是:TTL = 预计完成整个批次所需时间的1.5倍。

假设你要处理一份100行的文档,估计全部跑完要40分钟,那 TTL 就设60分钟。这样正常情况下永远不会中途过期,一旦任务意外中断、隔夜再跑,旧上下文就直接作废,反而符合预期。这个倍数留够余裕,又不会拖沓。

5. 我把context-mode用进真实流程后遇到的五个问题

5.1 上下文串台:不同任务互相污染

第一个让我头疼的问题,是多个任务共用同一个默认会话,导致上下文互相串台。我一开始偷懒没指定 session,结果处理完笔记再处理邮件时,邮件摘要里冒出来一堆笔记里的术语。

解决方式很简单:每个独立任务都分配一个唯一 session_id,例如用“日期+序号”作为会话名。我后来干脆在脚本里加了一段自动生成ID的逻辑,参考当前时间戳和进程号,让每次启动默认使用新会话,避免误用。

5.2 JSON缓存文件被并发写坏

当我把 ctx-tool 从单进程扩展到多进程并行处理文本时,立刻遇到了json.decoder.JSONDecodeError。原因很清楚:两个进程同时打开同一个会话文件并写入,内容互相覆盖,JSON 结构整个坏掉。

修复方式是“临时文件 + 原子替换”:先把新内容写到一个临时文件,写完之后通过os.replace原子性地替换旧文件。这样任何时刻读取者看到的是完整版,不会读到半个文件。

import tempfile def safe_save(store): data = json.dumps(store.data, ensure_ascii=False, indent=2) fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(store.path)) with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(data) os.replace(tmp_path, store.path)

如果并发进程更多,建议再套一层文件锁,比如fcntl或filelock库,确保同一时刻只有一个进程在写。

5.3 上下文无限膨胀导致工具直接拒绝

还有个高频问题:处理长文档时,上下文越攒越长,最终超过模型或工具的单次输入上限。表现为请求直接被拒,报错信息通常是 too long。

解决办法是双层防线:首先给上下文设置最大长度,超了就走裁剪策略;其次对历史记录做摘要压缩。我的实践是:完整保留最近10轮,10轮之前的内容在每天首次运行时压缩成一条summary,保证整体上下文长度恒定的同时不丢失太早期的主线信息。

5.4 旧上下文过期但没清掉

这个问题的表现最隐蔽:新任务看起来运行正常,但结果老是“莫名受到上周的影响”。排查半天才发现是 TTL 判断逻辑写错了位置——我在任务跑完之后才检查时间戳,跑之前没检查,等于旧上下文从没被清过。

后来我把过期检查挪到了主循环开头,也就是每次构造 prompt 之前都跑一次 TTL 判断,问题立刻消失。

5.5 敏感内容残留在本地缓存

最后一类问题跟安全相关。当批量处理含个人信息或未发布内容的文件时,context-mode 会把原始片段留在本地 JSON 里,这算是一个隐患。

我的处理原则是:上下文里不存原文,只存脱敏后的摘要;任务结束后提供清理机制,把会话文件删除;如果确实需要保留信息,先做字段级脱敏再写入。ctx-tool 里加了一个--wipe参数,执行后清空 sessions 目录下指定对话的全部记录。

为了速查,我把这些问题收进一张表:

问题现象根本原因解决方案
上下文串台多个任务共用同一会话每次任务分配唯一 session_id
JSON 文件损坏多进程并发覆写临时文件 + os.replace 原子替换
请求超长被拒上下文无限增长最大长度 + 摘要压缩
旧上下文残留TTL 检查时机错误每次构造输入前检查时间戳
敏感信息残留原文写入缓存只存摘要,支持 wipe 清理

6. 一些实话和建议

image

整个 context-mode 做下来,我最大的体会是:它本质上是一种“外部化状态”的工程实践。把中间结果从内存里搬出来,落成文件、数据库或缓存,让每一次任务都能往前追溯。这思路不新鲜,但放到现在的AI辅助处理和批处理脚本里,价值反而被放大了。

我个人现在的习惯是:默认无状态,遇到下面几种情况再开 context-mode——输入片段之间有语义牵连、需要保持风格和术语一致、下游结论依赖多条中间结果。而且我会刻意控制上下文的规模,让它保持“刚够用”的状态,而不是把能塞的历史全部塞进去。上下文太多和太少,都是错。

最后分享一个调试小技巧:在写入上下文时给内容加一个时间戳前缀,例如[ctx@20250512-093011]。这样哪天输出结果奇怪了,打开日志一看就能确认当前处理到底引用的是哪一轮的记录,排错效率至少翻倍。这个小习惯我一直在用,也非常推荐你试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询