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_tokens20000, max_summary_tokens20000): self.working WorkingContext(max_tokensmax_working_tokens) self.summary SummaryContext(max_tokensmax_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(n3) 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 models maximum context length is 1048576 tokens. however, your messages resulted in 1049580 tokens.报错信息本身很明确prompt token加output token超过了模型最大上下文。但很多人排查时容易犯一个错想通过“删掉一点历史消息”来临时解决这是治标不治本。我们的排查顺序是固定的四步。第一步把最近一次调用的完整messages导出统计每部分的token占用量定位是系统提示词、工具输出还是会话历史导致的超限。第二步检查是否触发过预算越界预警没触发就说明统计口径有bug预警条件没生效。第三步追查工具输出是否绕过了切割逻辑比如某些工具返回二进制或超长文本token化方式和预期不同。第四步也是最重要的一步处理完本次报错后从机制上找根因补漏洞而不是只删掉这一次的超限消息。另外有个反直觉的点很多模型的“最大上下文长度”包含输出token。输出预留区太小不会直接报上下文超长但会导致输出过程中被截断返回值出现finish_reasonlength。这类错误隐蔽且恶劣因为任务已经执行了一大半结果在最后生成阶段被切断会话状态很难恢复。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 ... tokensPrompt输出超出窗口上限统计各部分token构成定位超限区域压缩或归档对应内容finish_reasonlength输出被中途截断检查输出预留区动态调整预留空间降低工作层预算failed to initialize the contextKV Cache分配失败检查显存/内存占用调低num_ctx或升级硬件context deadline exceeded网络请求超时检查Docker/API调用链路配置镜像加速或增加超时时间not a secure context浏览器安全策略拦截检查访问协议本地调试套HTTPS代理这张表建议直接贴在团队wiki里省得每次遇到类似问题都要重新分析一遍。从Gas Town 2026立项到现在我最深的体会是做一个Agent系统真正决定成败的往往不是模型选型也不是Prompt写得多花哨而是Context这一亩三分地管不管得住。模型是能力上限Context是发挥下限。管不好Context再强的模型也只会在长任务里逐渐失忆、逐渐胡言乱语、逐渐把任务搞砸。最后分享一个我们一直在用的实用心法每次给模型发消息前都问自己三个问题。第一这条消息里哪些信息是模型此刻完成当前步骤必需的第二哪些信息可以留到以后需要时再检索第三哪些信息是纯粹的噪音、可以直接删掉把这三个问题养成习惯不但在系统设计里管用自己写Prompt、调试Agent的时候也不会再犯“一股脑全塞进去”的毛病。后续我们计划在归档层引入向量检索进一步降低摘要层的压力让长期历史不再以线性摘要的方式存在而是变成可随时按需调取的“外部记忆”。这条路还在尝试中等跑出更多数据了再来分享。