1. 先说项目背景:TaskOS与Context管理的关系
1.1 TaskOS是个什么系统
TaskOS是我们内部做的一个任务型Agent编排系统,负责把用户的一句话需求拆解成可执行的任务链,再调度多个模型和工具去完成。它面向的是重流程、多步骤、长耗时的自动化任务,比如批量数据处理、多阶段报告生成、跨系统的信息采集与整理。“Gas Town 2026”是当前迭代的版本代号,立项一个多月,核心目标只有一个:让长任务跑得更稳、更久,不要在十几轮工具调用后突然断掉。
所谓“断掉”,绝大部分不是模型能力不够,也不是下游工具挂了,而是context满了。任务跑到一半,上下文窗口被历史记录撑爆,模型开始遗忘前面的任务目标,甚至直接触发API的400错误,整个会话报废。这类问题在TaskOS里出现得极其频繁:调tool、读文件、写代码、看日志,每轮循环都会往窗口里堆内容。Context大小就是Agent的“有效工作记忆”,记忆失守,再聪明的模型也白搭。
这篇文章我想把Gas Town 2026里做Context管理的整套思路、设计选型和踩坑过程完整复盘一遍。适合正在做Agent编排、任务自动化、多轮对话应用的人参考,尤其是被超长上下文反复困扰的团队。你不需要是算法专家,但至少要明白token、窗口、上下文这几个词的含义,后面每个环节我都会拆开讲。
1.2 为什么Context管理成了主要矛盾
TaskOS早期设计时,其实没有太在意上下文问题。第一个版本直接用单轮prompt,用户说什么就执行什么,任务一长就拆成多个独立步骤,步骤与步骤之间通过JSON传数据。这种方式在20轮以内的短任务里完全够用,但任务链一拉长,缺陷全暴露了。
第一个暴露的问题是状态丢失。比如用户让TaskOS“先分析销售数据,再根据结果生成一份周报邮件”,第二步如果和第一步完全独立执行,模型根本看不到第一步的分析结论,只能靠我们手工把数据摘出来再拼进新Prompt。数据一多,要么塞不下,要么把无关信息全塞进去,模型的推理质量直线下降。
第二个问题是多轮工具调用时模型“忘记”自己在干什么。任务跑了很久、调了几十次工具之后,你问它“当前任务目标是什么”,它的回答经常是片面的,因为模型能看到的信息已经超出了有效关注范围。这非常反直觉:不是信息不存在,而是信息太多,模型抓不住重点。我们真正要处理的不是单纯增加窗口长度,而是让有效信息始终处于“够用又精简”的状态。
第三个问题更直接:成本。LLM API按token计费,上下文越长,每一轮调用都在为历史买单。一个跑满50轮的任务,历史token可能达到十几万甚至几十万,光是重复发送的历史信息就让成本翻了几十倍。Gas Town 2026里我们统计过,优化前约37%的调用成本消耗在无用历史上,钱花得毫无价值。
这三件事叠加,Context管理就从“可做可不做”变成了“必须死磕”的核心模块。模型本身的能力反倒成了次要因素,真正决定Agent实际体验的,是它每时每刻能看到什么、看不到什么。
1.3 目标与边界:我们想解决什么
Gas Town 2026迭代前,我们给Context管理定了三条清晰目标,后续所有设计都不偏离这些约束。
第一条是长期稳定不崩。最少支撑100轮工具调用不触发超长报错,理想状态是200轮。多步骤任务链、数据爬取任务、批量文档处理对这一点要求极高,否则任务跑到一半就废了。
第二条是有效信息不丢。压缩不能把任务目标、用户硬性要求、关键数据结论丢掉。我们宁可接受“模型因为看到信息较少而说不知道”,也不能接受“模型因为上下文被截断而编造一个错误结论”。
第三条是成本可控。目标是把单位有效任务消耗的token降低50%以上,让长任务的单次运行成本下降到可接受范围。
边界也划得很清楚:不做通用记忆系统,不解决跨用户、跨任务的长期知识沉淀问题,那是知识库该管的事。我们只专注单个任务实例内部的上下文存活期管理,让一个任务从开始到结束,始终以清醒的状态工作。
2. Context管理的底层逻辑与方式论
2.1 弄清楚窗口里的token是怎么被消耗的
动手之前,先弄清楚钱花在哪儿。我们抽了几十条典型任务的日志,把每次调用的token构成拆开看,消耗大头集中在三块。
第一块是系统提示词。TaskOS的系统提示词包含角色设定、工具说明、输出格式规范,项目早期写到了近3000个token。如果每轮都原样发送,50轮就是15万token的纯开销。市面上几乎所有Agent框架都有这个问题:系统提示词越长,每轮调用的固定成本越高。
第二块是工具调用记录。调API、查数据库、执行脚本,每一步都有输入、输出、返回值、执行状态。这些记录对模型来说是必要的,但很多是我们完全不处理的原始数据。我们遇到过最夸张的任务,一次数据库查询返回了大量数据,模型当然用不完,但那些数据差点把窗口直接撑爆。
第三块是会话历史。用户说的话、模型生成的内容、内部日志逐轮累积、永不减少。如果没有管理策略,这部分就像慢性病,不疼但会要命。
知道了消耗结构,就能对症下药。我们的核心策略是:固定开销压缩,动态开销分级,高价值内容沉淀,低价值噪音丢弃。
2.2 把上下文当作预算资源来规划
Gas Town 2026里,核心思路是给整个Context定义一套“预算体系”:预设清晰的区域划分边界,超过边界就执行清理或压缩。这有点像给系统做内存管理,但托底策略不同——内存管理靠淘汰,Context管理靠压缩,因为历史信息偶尔还要用。
以某旗舰模型的128K窗口(实际上下文上限为1048576 tokens,量化思路一致)为例,我们设计的分配模板大致长这样:
| 区域 | 预算 | 内容与规则 |
|---|---|---|
| 系统提示词区 | 3000 tokens | 角色、工具说明、输出规范,不允许压缩 |
| 任务元数据区 | 2000 tokens | 当前总目标、已完成步骤、待办步骤、关键锚点,动态更新 |
| 短期操作历史区 | 16000 tokens | 最近3~5轮工具调用、模型输出、用户最新指令,超额则滚动转出 |
| 长期摘要区 | 8000~20000 tokens | 早期历史的压缩摘要,按时间线组织,供按需检索 |
| 输出预留区 | 1000 tokens | 最终答案生成余量,绝对不能占满 |
这样划分之后,整个窗口就不再是一个“什么都能往里塞的大口袋”,而是一块块有明确用途的容器。每一轮循环开始前,Context Manager会重新核算各区域占用,如果短期操作历史区快满了,就把最旧的那几轮滚动到长期摘要区做压缩。
这里有个必须强调的经验:输出预留区常常被忽略,但它是所有区域里优先级最高的。模型输出过程中被强行截断,比上下文超长更隐蔽也更难恢复,因为任务往往已经执行了一大半,最后一步却被切断。
2.3 什么时候该压缩:三个触发条件
压缩不能随便做,做早了丢信息,做晚了没意义。我们设计了三个触发条件,任一满足就进入压缩流程。
第一个是滚动窗口阈值。短期操作历史区满了,这是最常见的情况。比如我们设定最多保留5轮工具调用,第6轮开始前,第1轮就得转出去。
第二个是关键字触发。有些信息我们知道它绝对不能丢,比如用户的核心诉求、明确的数字约束、审批通过的状态。我们把这些信息做成“关键锚点”,无论上下文怎么压缩都必须保留,写入任务元数据区。如果新对话里出现新的关键锚点,即便窗口还没满,也主动做一次重排,调整锚点优先级。
第三个是预算越界预警。每次调用前统计当前Context的估算token总量,如果剩余预算低于总窗口的15%,就提前做一次压缩,而不是等API报错了再处理。这里要注意:压缩动作本身也需要模型参与、消耗token和时间,所以必须留出操作余量,否则会陷入“压缩到一半又超限”的死循环。
这三种触发逻辑听起来简单,实际实现时细节非常多,下一部分讲架构落地时我会展开。
3. 分层Context管理架构的实战落地
3.1 三层Context模型的设计
Gas Town 2026的Context管理模块最终落地成三层结构:工作层、摘要层、归档层。
工作层(Working Context)是最靠近模型的一层,内容直接出现在每一轮对话的messages数组里。这一层包含系统提示词、任务元数据、最近几轮操作历史。特点是体积小、实时性强、全部可见。
摘要层(Summary Context)不直接出现在模型消息里,由Context Manager维护,是一份经过压缩的任务描述。摘要层会被定期重写:每完成一个重要步骤,摘要层就更新一次,把已完成的事情从工作层转移到摘要层,同时更新任务进度。摘要层本身也有大小限制,超过限制时必须做二次压缩,把更早的信息进一步提炼。
归档层(Archive Context)是最底层的完整记录,不被截断。所有原始工具调用、原始输出、模型中间推理都会完整归档到数据库里。模型平时不接触这层数据,但当摘要层信息不完整,或者用户追问某个细节时,Context Manager会从归档层检索相关内容,拼装成临时上下文回填到摘要层或工作层。
这层设计本质上是把“大窗口问题”转换成“存储、摘要、检索”三个独立问题:存储交给数据库,摘要交给LLM,检索交给Embedding或关键词索引。每个问题都有成熟方案,而不是把宝全押在长度上。
3.2 工具调用与Context Manager的接口设计
TaskOS原本的工具调用是直接把输出拼进prompt,这在分层架构里行不通。我们做了一次重构:给每个工具调用增加“上下文边界声明”,Context Manager根据声明决定输出内容该进工作层、摘要层还是归档层。
举个例子,查询数据库的工具增加了三个可配置参数:max_result_rows控制最多返回多少行,max_result_length控制单次返回的最大字符数,summary_mode控制在返回前是否先让模型做一次自动摘要。这三个参数配合使用,就能避免“一次查询返回超大数据量”的灾难。
Context Manager对外暴露的核心接口是这样的:
class ContextManager: def __init__(self, max_working_tokens=20000, max_summary_tokens=20000): self.working = WorkingContext(max_tokens=max_working_tokens) self.summary = SummaryContext(max_tokens=max_summary_tokens) self.archive = ArchiveContext() def add_user_message(self, text): self.extract_anchors(text) # 提取关键锚点 self.check_budget() # 预算检查 self.working.add_message("user", text) def add_tool_result(self, tool_name, result): if not self.tool_needs_full_output(tool_name): result = self.compress_tool_result(result) # 按声明压缩 self.working.add_message("tool", result) self.archive.store_tool_result(tool_name, result) # 归档原始结果 def add_assistant_message(self, text): self.working.add_message("assistant", text) self.archive.store_assistant_message(text) def build_messages(self): if self.working.is_over_budget(): self.run_compaction() messages = [] messages.append(self.working.system_prompt()) messages.append(self.working.task_metadata()) messages.extend(self.working.recent_history()) return messages def run_compaction(self): old_turns = self.working.pop_oldest_turns(n=3) summary_update = self.summarize(old_turns) # 用LLM压缩 self.summary.append_update(summary_update) self.update_task_metadata()这套接口设计重点关注一件事:工作层内容永远是精简的、结构化的、处于控制之下的。工具层要想清楚自己的输出有没有必要都进模型视野,没必要的直接归档。
3.3 Compaction与摘要重写的实操细节
Compaction是整个模块里最需要谨慎的部分。我们写了两版、推倒重写了两版,最后总结出几个稳定可靠的方案。
第一次尝试最简单:满了就把最早的消息扔掉。效果非常差,因为最早的消息里往往带着用户最初的任务目标。后来改成“最早的消息先压缩再丢弃”,效果好很多,压出来的摘要能保住主干信息。再后来发现,摘要本身质量是另一个坑:直接把几轮对话丢给模型让它“总结一下”,效果一般,压缩出来的摘要过度概括,丢了有用的中间数据。
最终我们设计了一套结构化摘要模板,把总结限制在四个板块:任务目标与约束、当前进度、关键数据与工具输出要点、下一步待办。每轮压缩都往模板里填内容,第二次压缩时在旧模板基础上增补,而不是重新生成。这个写法的好处是摘要始终围绕任务推进来组织,模型恢复上下文时不需要重新理解,可以“续读”而不是“重读”。实测下来,结构化模板的摘要有效恢复率(模型能从摘要中提取核心信息继续完成任务的比例)提升了约30%。
Compaction执行时机也要讲究。不能安排在模型生成输出过程中,必须安排在每轮循环的开始、下一轮请求发出前。否则压缩动作和模型输出会混在一起,既浪费token又把时序搞乱。我们的实现是在build_messages里先检查短期区占用,超过阈值先执行compaction,确认成功后再拼装messages。
这里有一个很值得分享的细节:Compaction的标准不是“摘要写得好不好”,而是“压缩后的内容能不能支撑后续任务继续推进”。最开始我们追求摘要的语句通顺、逻辑完整,后来发现完全跑偏了。任务型场景里,摘要的第一优先级是把“任务目标、进度、关键数据、下一步”这四件事讲清楚,文笔优美毫无意义。
3.4 锚点提取与任务元数据的动态维护
任务元数据区是三层架构里最容易做坏的部分。一开始我们把用户第一条消息当作金科玉律,后面发现用户的需求是不断补充、调整的。Gas Town 2026改成了“滚动提取”:每轮用户消息都提取候选锚点,与现有锚点比对,出现不一致或新增时再更新任务元数据。
锚点本身也分级。一级锚点是硬性约束,比如“必须用表格输出”“预算不超过5万”“数据截止到上月底”,这类信息在任何压缩中都不能丢。二级锚点是中间结论,比如“区域A的销售额下降了12%”,这类信息会随着任务推进被更新或替换。三级锚点是临时细节,比如某一步工具调用的具体参数,这类信息随滚动窗口自然淘汰即可。
任务元数据区每次更新都会重写成一条结构化的“任务快照”:
当前目标:分析销售数据并生成周报 已完成:数据加载、缺失值统计、季度聚合 进行中:区域对比分析 待办:生成汇报PPT大纲 硬性约束:输出必须包含表格,截止日期为本月底 关键数据:Q2整体销售额环比下降8%;华东区域下降幅度最大(12%)这条快照无论上下文怎么压缩,都必须完整出现在系统提示词后面。它就像任务执行过程中的“推进中枢”,模型每次读到这里都能迅速恢复全局认知。我们内部管这个叫“给模型递小抄”,效果比任何提示词技巧都直接。
4. 实际场景效果:跑了两个月,数据怎么样
4.1 一个真实任务的全流程拆解
光讲设计有点虚,拿一个具体任务跑一遍全流程。任务内容:用户丢给TaskOS一个销售数据集,要求“分析各季度销售趋势,找出下滑最严重的区域,并给一份包含可视化建议的汇报PPT大纲”。
TaskOS把这个任务拆成四个阶段:数据预处理、趋势分析、区域对比、报告生成。每个阶段包含多次工具调用:预处理阶段调了数据加载、缺失值统计、列类型转换三个工具;趋势分析阶段调了季度聚合、趋势线拟合、输出图表三个工具;区域对比阶段调了分类汇总、Top-K筛选两个工具;报告生成阶段调用一次生成大纲、一次格式校验。合计12次工具调用、14轮消息。
在没有分层Context管理之前,这类任务大概在第8轮左右出现明显上下文退化,第11轮几乎必出问题。数据加载工具返回的原始DataFrame样本很长,趋势分析和聚合结果又是大段表格,每轮还带着完整系统提示词,窗口很快就被撑满了。
在Gas Town 2026的分层架构下,这个任务的数据表现:单次任务总token消耗从优化前约460万降到了约130万,降低约71%;完成时间从平均22分钟缩短到9分钟;全程零超限报错。任务进行到中后段的“区域对比”阶段时,模型还能准确说出第一阶段发现的缺失值比例,说明关键锚点和进度摘要起了实际作用。
这个案例说明一个朴素的道理:大模型不是记不住东西,是没有人帮它筛选该记什么。Context Manager本质上就是帮模型做“有效记忆”的管家。
4.2 迭代过程中的调优经验
上线之后,实际跑出来的问题和设计时预想的不完全一样。最典型的几个坑记录下来:
第一个坑是锚点提取时机太早。最初在用户第一条消息里提取任务目标,用户需求不完整时锚点很容易缺失,后面补充需求时锚点就废了。改滚动提取之后,每轮都提取候选锚点,与现有锚点比对后再更新,准确率提高了很多。
第二个坑是过度压缩。有版本把摘要上限压得非常低,结果模型经常“断片”,在任务中途突然无法理解业务背景。后来把摘要区上限提高,哪怕多花一点token,也保证摘要信息完整。对Agent来说,一个完整准确的摘要,比省那几十个token重要得多。
第三个坑是工具输出压缩的粒度。一开始统一把所有工具输出都压成长摘要,结果预算分析这类工具在后续问题中被反复引用,长摘要无法追溯原始明细。后来改成“保留头部+尾部+整体Summary”的组合格式:前几条元素、后几条元素、整体统计摘要同时保留。绝大多数情况下模型不需要完整原始内容,但要查细节也有入口。
这些调优经历总结成一句话:Context管理是“按需保留”的艺术,不是“尽可能压缩”的艺术。压缩的终极目标不是省钱,是让模型在合适的时候看到合适的信息。
4.3 触发阈值和预算分配的实际参数
很多朋友问我要具体参数,这里把几个核心数值列出来供参考。我们所有参数都是基于对几十条真实任务日志的分析后调整出来的,不同业务场景可能需要微调。
短期操作历史区:我们设为16000 tokens,约等于5轮完整工具调用。这个数值不能太大,太大容易让摘要层形同虚设;也不能太小,太小模型没法在当前步骤内保持足够的上下文连贯性。
输出预留区:我们按最终回答篇幅估算,固定预留1000 tokens。任务进入报告生成阶段时,Context Manager会自动降低工作层预算,把空间让给输出区,防止最后一步被截断。
预算越界预警阈值:剩余预算低于总窗口15%时触发一次主动压缩。这个15%是经验值,既要保证压缩动作有足够执行空间,又不能等到窗口真正满了才动手。
这些数值不必照搬,但有一个心法必须掌握:先看日志、统计分布、再定阈值,而不是拍脑袋设一个数然后祈祷它够用。我们第一版就是拍脑袋设的,后来统计了几十条真实任务才发现真正的瓶颈在哪。
5. 常见问题与排查技巧实录
5.1 高频报错:上下文超长的完整排查路径
Agent类应用遇到最多的报错就是下面这类:
api error: 400 this model's maximum context length is 1048576 tokens. however, your messages resulted in 1049580 tokens.报错信息本身很明确:prompt token加output token超过了模型最大上下文。但很多人排查时容易犯一个错:想通过“删掉一点历史消息”来临时解决,这是治标不治本。
我们的排查顺序是固定的四步。第一步:把最近一次调用的完整messages导出,统计每部分的token占用量,定位是系统提示词、工具输出还是会话历史导致的超限。第二步:检查是否触发过预算越界预警,没触发就说明统计口径有bug,预警条件没生效。第三步:追查工具输出是否绕过了切割逻辑,比如某些工具返回二进制或超长文本,token化方式和预期不同。第四步,也是最重要的一步:处理完本次报错后,从机制上找根因补漏洞,而不是只删掉这一次的超限消息。
另外有个反直觉的点:很多模型的“最大上下文长度”包含输出token。输出预留区太小不会直接报上下文超长,但会导致输出过程中被截断,返回值出现finish_reason=length。这类错误隐蔽且恶劣,因为任务已经执行了一大半,结果在最后生成阶段被切断,会话状态很难恢复。
5.2 本地推理环境的KV Cache与模型初始化问题
TaskOS一部分任务跑在本地推理环境,Ollama和llama.cpp都用过。这里有一个错误案例非常典型:
failed to load model. failed to initialize the context: failed to allocate buffer这条报错里的context和LLM的context不是一回事,它指的是推理引擎的KV Cache缓冲区。核心原因是模型配置的context_size超过了机器显存或内存能分配的量。解决思路直接:调低num_ctx,或者换显存更大的机器。
但这里有个大坑:num_ctx调低之后,Agent任务的可用窗口同步变小。云端模型能跑50轮的逻辑,本地模型可能十几轮就满了。我们的经验是:本地推理方案初次设置时,num_ctx不要超过物理内存的25%。比如32GB内存的机器先设8192,实测稳定后再往上加。别迷信“显卡有24GB显存就设32768”这类想法,KV Cache的占用比想的大得多,推理速度也会随context增长急剧劣化。
这条经验想说明的是:context管理不是云端模型的专属问题,本地部署时它更致命,因为你没法靠“换个更大的API模型”来兜底。
5.3 部署调试环境里两个“同名不同义”的干扰项
排查过程中还遇到过两个与LLM无关、但名字里都带“context”的坑,记录下来免得后人踩。
第一个是Docker镜像拉取失败:
error response from daemon: get "https://registry-1.docker.io/v2/": context deadline exceeded这里的context是网络请求上下文,超时了。处理方式:配置registry mirror、加大docker timeout、或者换网络。真正要提醒的是:这类问题出现的时间和LLM任务报超长的时间往往挨得很近,日志排查时不要混淆,先分清是模型层的问题还是部署环境的问题。
第二个是浏览器调试跨域问题:
has been blocked by cors policy: the request client is not a secure context这是前端调用TaskOS API时报的,浏览器安全策略把HTTP环境当作非安全上下文,直接拦了跨域请求。localhost调试没问题,但一旦用局域网IP访问就会出现。解决方法是给调试环境套一层HTTPS代理,这是最省事的办法。
这两个插曲看起来和主话题无关,但我真心建议做Agent系统的团队把这类“同名不同义”的坑记录在案。它们非常分散注意力,排查时会让你在错误方向浪费大量时间。
5.4 问题速查表
| 报错信息 | 实际原因 | 排查方向 | 推荐处理 |
|---|---|---|---|
| maximum context length is ... tokens | Prompt+输出超出窗口上限 | 统计各部分token构成 | 定位超限区域,压缩或归档对应内容 |
| finish_reason=length | 输出被中途截断 | 检查输出预留区 | 动态调整预留空间,降低工作层预算 |
| failed to initialize the context | KV Cache分配失败 | 检查显存/内存占用 | 调低num_ctx,或升级硬件 |
| context deadline exceeded | 网络请求超时 | 检查Docker/API调用链路 | 配置镜像加速或增加超时时间 |
| not a secure context | 浏览器安全策略拦截 | 检查访问协议 | 本地调试套HTTPS代理 |
这张表建议直接贴在团队wiki里,省得每次遇到类似问题都要重新分析一遍。
从Gas Town 2026立项到现在,我最深的体会是:做一个Agent系统,真正决定成败的往往不是模型选型,也不是Prompt写得多花哨,而是Context这一亩三分地管不管得住。模型是能力上限,Context是发挥下限。管不好Context,再强的模型也只会在长任务里逐渐失忆、逐渐胡言乱语、逐渐把任务搞砸。
最后分享一个我们一直在用的实用心法:每次给模型发消息前,都问自己三个问题。第一,这条消息里哪些信息是模型此刻完成当前步骤必需的?第二,哪些信息可以留到以后需要时再检索?第三,哪些信息是纯粹的噪音、可以直接删掉?把这三个问题养成习惯,不但在系统设计里管用,自己写Prompt、调试Agent的时候,也不会再犯“一股脑全塞进去”的毛病。
后续我们计划在归档层引入向量检索,进一步降低摘要层的压力,让长期历史不再以线性摘要的方式存在,而是变成可随时按需调取的“外部记忆”。这条路还在尝试中,等跑出更多数据了再来分享。