
实际使用大模型写代码时结果质量的瓶颈常常不在模型本身而在上下文管理。同一个开发需求如果上下文组织得好模型能记住项目结构、代码风格、约束条件和未完成进度如果组织得差模型会在几轮对话之后重复造函数、改错文件甚至自相矛盾。阿里 Scroll 这一名字所代表的思路就是像管理一卷长卷轴一样管理模型看到的上下文内容太长就压缩旧对话就摘要关键信息单独固定让模型每次读到的都是一份最新、最完整、最省 token 的代码写作简报。这里不复制 Scroll 的内部实现而是把它背后的上下文滚动管理思想拆成可落地的工程方法并用一个最小 Python 示例演示如何给模型写代码工具加上上下文管理能力。1. 模型写代码时上下文为什么会“散”掉1.1 上下文窗口大不代表模型记住了所有内容大模型的上下文窗口决定了单次请求能塞进多少 token但窗口大和真正“记住”是两回事。很多模型对一段长文本的注意力并不是均匀分布的。当消息列表里堆满数十轮对话、多个代码文件、反复贴过的报错信息时模型更容易关注最近几条消息和最后出现的代码片段而最早写入的硬性约束反而会被弱化。这和人类的记忆规律类似第一页的规则和最后一页的修改要求同时摆到桌面临时记忆会优先处理最后一页。 在代码生成场景下这个问题被放大因为代码文件本身 token 成本高几段函数就能占满几千 token。窗口一旦逼近上限系统最简单的处理办法就是从头部截断于是最开始的“返回格式必须为 JSON”“不要修改现有表结构”这类关键约束会被直接切掉。模型并没有犯错它只是根本没有看到这些内容。所以判断一个上下文方案是否有效不能只看上下文窗口的数值要看真正进入模型输入的有效上下文占比。下表是一个简化判断上下文窗口塞入内容有效上下文8k40 轮对话 5 个代码文件末尾 2 轮对话 最后 1 个文件32k大量重复贴代码中间和开头信息密度低容易被忽略128k结构化槽位 压缩摘要 最近窗口关键约束在 system 层代码细节在尾部后者的有效上下文不一定比前者大但信息密度更高。Scroll 这类方案要解决的正是如何把上下文从“流水账”变成“高密度简报”。1.2 代码生成场景对上下文的特殊要求普通问答中模型忘记前文用户重问一次即可。代码生成不是这样需求是累积的。第一轮说“用 Python 写一个读取 CSV 并返回 DataFrame 的函数”第二轮说“统一返回 JSON”第三轮说“不要用 pandas”。如果模型没有在第三轮记住第一轮的 CSV 需求和第二轮的 JSON 约定它就会写一个返回 DataFrame 的 pandas 函数与后续要求冲突。代码生成场景中模型真正需要持续记住的内容包括五类当前任务目标要实现什么功能最终交付形态是什么。硬性约束接口格式、数据库结构、命名规则、禁止使用的依赖。依赖关系当前项目使用哪些框架、哪些版本、哪些关键 API。代码风格函数名风格、异常处理方式、目录组织方式。进行状态已经完成哪些模块正在处理哪个问题上一步报了什么错。这些信息如果只散落在历史消息里模型每次都要在大量文本中重新“找”。Scroll 的管理方式则不同先把这些信息提取成固定槽位再让模型每次请求都看到槽位。1.3 Scroll 的核心思路不是截断而是滚动重组Scroll 强调的滚动不是简单把旧内容删掉而是对上下文做分层处理。可以把它理解成一个三层结构固定层任务、约束、依赖、风格、进度、最近错误始终存在不随对话轮次消失。滚动窗口层最近几轮未压缩的对话和代码片段保留细节。摘要层窗口之前的历史被压缩成摘要提供背景但不占用大量 token。每次模型请求前系统把这三层拼成新的 messages。模型看到的不再是一长串聊天记录而是一份格式稳定的“项目状态简报 最近变更”。这正是 Scroll 这一类工具的核心价值让模型在长时间写代码过程中始终有一个稳定、可复用、可更新的工作记忆。这里要注意上下文管理不是模型能力之外的多余动作而是模型能力的一部分。 一个模型参数不变只要输入结构变化输出质量可能差别很大。Scroll 的做法相当于把工程经验固化到 prompt 组织方式中。2. 从 Scroll 思路抽象出的上下文管理链路2.1 四个阶段收集、筛选、压缩、恢复如果把 Scroll 的管理思想落地到代码中可以拆成四个阶段。第一阶段是收集。系统要记录用户消息、模型回复、工具调用结果、文件变更、报错信息。很多上下文丢失问题根源是根本没有把文件变更收集进来。模型已经生成了新代码但上下文管理器没有更新下一轮继续使用旧代码片段。第二阶段是筛选。不是所有内容都值得放进槽位。用户偶然说的一句“可能这样写吧”不代表最终决策而“接口必须返回 code、message、data”就是硬约束。筛选的目标是识别出会影响后续代码结果的高价值信息。第三阶段是压缩。对超出窗口的旧对话要生成摘要。压缩不是简单截断而是保留结论、放弃过程。比如弹了好几轮“版本不兼容”的报错摘要应该写成“已确认 pandas 与当前 Python 版本不兼容改用 pyspark”而不是保留几段报错日志。第四阶段是恢复。在发起新请求前把固定槽位、摘要、最近窗口重新组合成 messages。恢复阶段最容易被忽视因为代码里往往只维护一个 history 数组然后直接把它塞给模型。正确的做法是先检查槽位是否过期再检查摘要是否需要更新最后结合最近历史生成完整输入。这个链路可以用一句话概括收集要全筛选要准压缩要狠恢复要稳。2.2 用“槽位”固定不能丢的信息槽位是上下文管理器最重要的数据结构。它是一组固定字段每个字段保存一个类别的关键信息。模型每次看到槽位就知道当前项目的整体状态。下面是一个适合代码生成场景的 JSON 示例{ version: 1, task: 实现用户模块的注册和登录接口, constraints: [ 所有接口返回统一格式 { code, message, data }, 密码使用 bcrypt 加密, 不要修改现有数据库表结构 ], dependencies: [ flask, flask-sqlalchemy, bcrypt ], style: [ 函数名使用 snake_case, 服务层异常统一抛 AppException ], progress: [ 已完成用户模型定义, 正在编写注册接口, 登录接口尚未开始 ], last_error: register 接口在 bcrypt 校验时报错TypeError: password must be a string }这个 JSON 的价值在于稳定。无论对话进行到第几轮只要任务没有变化constraints里的内容就必须原样出现在下一轮 prompt 中。模型不需要从历史里回忆不需要靠运气它每次都直接看到这一组事实。更新槽位时要小心约束通常是只增不减的已确认的决策不要随意删除progress需要根据最近输出推进last_error要更新为最新一次错误否则模型会反复修复旧问题。2.3 滑动窗口与滚动摘要怎么配合滑动窗口和滚动摘要不是二选一而是互补关系。滑动窗口保留最近 N 轮完整消息保证模型能拿到具体代码和最近对话。如果只保留摘要模型会失去细节比如不知道上一次生成的函数具体长什么样。滚动摘要则负责把窗口之前的内容浓缩成几条结论保证旧信息不会彻底消失。举个例子。假设这是第 10 轮对话窗口中原本有 9 轮消息。系统可以保留最后 4 轮完整内容把前 5 轮压缩成摘要“已完成用户模型定义确认使用 SQLAlchemy排除了 Django ORM。” 然后放到 system prompt 中。这种配合方式避免了两个极端只截断第 1 轮的需求约束会被切掉模型不知道最初任务。只摘要模型只看到结论不知道最近函数体的具体实现改代码容易改偏。Scroll 的思路里滑动窗口管的是“最近发生了什么”滚动摘要管的是“之前决定了什么”。两者叠加才能同时满足细节和背景需要。3. 实现一个最小上下文管理器3.1 环境准备与依赖选型下面用 Python 实现一个最小可运行的上下文管理器。它不绑定具体产品使用 OpenAI 兼容接口可以对接阿里云百炼、OpenAI 或其他兼容服务。先创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate pip install openai python-dotenv在项目目录中创建.env文件OPENAI_API_KEYyour_api_key OPENAI_BASE_URLhttps://dashscope.aliyuncs.com/compatible-mode/v1 MODEL_NAMEqwen-plus这里用阿里云百炼的兼容模式只作示例。实际项目里MODEL_NAME要换成自己账号可用的模型名OPENAI_BASE_URL也要根据服务商调整。环境变量方式是为了避免把密钥写进代码。3.2 定义上下文结构体上下文管理器的核心是一个CodeContextdataclass。它保存槽位信息和历史消息。from dataclasses import dataclass, field from typing import List dataclass class CodeContext: task: str constraints: List[str] field(default_factorylist) dependencies: List[str] field(default_factorylist) style: List[str] field(default_factorylist) progress: List[str] field(default_factorylist) last_error: str history: List[dict] field(default_factorylist) def to_slots(self) - dict: return { task: self.task, constraints: self.constraints, dependencies: self.dependencies, style: self.style, progress: self.progress, last_error: self.last_error, } def add_message(self, role: str, content: str) - None: self.history.append({role: role, content: content})to_slots负责生成固定槽位 JSONadd_message负责累积对话历史。这个结构虽然简单但已经是上下文管理的骨架。后面所有压缩和重组逻辑都围绕它展开。3.3 实现滚动压缩滚动压缩的目标是当历史消息 token 数超过阈值时把旧消息压缩成摘要只保留最近几条完整消息。这里先写一个 token 估算函数。实际生产环境建议使用模型分词器示例中先用字符长度近似def estimate_tokens(text: str) - int: return int(len(text) / 1.5)压缩函数如下def compress_history( history: List[dict], max_window_tokens: int 2000, keep_last: int 4 ): total_tokens sum( estimate_tokens(msg[content]) for msg in history ) if total_tokens max_window_tokens: return history, old_messages history[:-keep_last] recent_messages history[-keep_last:] source \n.join( f{msg[role]}: {msg[content]} for msg in old_messages ) summary summarize_with_rules(source) new_history [ {role: system, content: f历史摘要{summary}} ] recent_messages return new_history, summary这里keep_last是保留最近几轮完整消息的数量。max_window_tokens是历史消息允许的最大 token 数。需要说明摘要本身也会占 token生产环境要把摘要 token 纳入总上下文预算。summarize_with_rules可以先用规则实现def summarize_with_rules(source: str) - str: lines source.strip().splitlines() important [] keywords (约束, 决定, 要求, 确定, 完成, 错误) for line in lines: if any(k in line for k in keywords): important.append(line.strip()) if not important: important lines[:3] return .join(important[:5])规则摘要适合快速验证思路但它不够精准。生产环境更推荐让模型生成摘要例如把 source 传给同一个模型要求返回 3 到 5 条结论。这样摘要质量更高但会多一次模型调用需要做好成本和耗时的平衡。3.4 把上下文管理器接到模型调用链路上接下来要生成发给模型的 messages。核心思路是把固定槽位作为 system 消息放在最前面把压缩后的历史放到后面。import json from openai import OpenAI import os client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL) ) def build_messages(ctx: CodeContext, max_window_tokens: int 2000): slots ctx.to_slots() slots_text json.dumps(slots, ensure_asciiFalse, indent2) history, _ compress_history(ctx.history, max_window_tokens) system_prompt ( 你是资深软件开发工程师。请基于固定槽位和最近历史完成代码任务。\n 固定槽位中的约束必须严格遵守如果槽位与历史冲突以槽位为准。\n f项目状态{slots_text} ) messages [{role: system, content: system_prompt}] messages.extend(history) return messages调用模型时只需要传入build_messages的结果def chat(ctx: CodeContext, user_message: str, max_window_tokens: int 2000) - str: ctx.add_message(user, user_message) messages build_messages(ctx, max_window_tokens) resp client.chat.completions.create( modelos.getenv(MODEL_NAME), messagesmessages, temperature0.3 ) reply resp.choices[0].message.content ctx.add_message(assistant, reply) return reply这里的调用链路已经形成了一个最小闭环用户发送新消息写入ctx.history。build_messages生成固定槽位和压缩后的历史。模型返回回复。回复写入历史供下一轮使用。这套实现的价值在于即使需求发生多轮变化只要槽位更新正确模型就不会丢掉最关键的约束。4. 关键参数不能拍脑袋设置4.1 参数含义与默认值上下文管理不是写完代码就结束参数决定了效果。下面几个参数是常见项目中必须校准的。参数含义示例默认值max_context_tokens单次请求总上下文上限模型窗口的 60%max_window_tokens未压缩历史消息的 token 上限2000keep_last压缩时保留最近多少条完整消息4summary_lines摘要最多保留多少条结论5slot_max_tokens固定槽位文本的 token 上限1000max_window_tokens调小了历史会被频繁压缩prompt 变小成本降低但最近上下文可能被过早摘要。调大了模型能看到更多细节但总上下文可能超过窗口导致系统退化为简单截断。keep_last同理保留消息越多摘要触发越晚但近窗口占用越大。4.2 调参影响速查表参数调小的影响调大的影响适用场景max_window_tokens摘要更频繁细节丢失响应快细节更丰富prompt 变大成本高小任务用小值大型重构用大值keep_last模型只见最近几条消息容易丢上下文模型能看到较多历史但摘要被推迟简单改文件用 2跨文件修改用 6 或更多summary_lines摘要简洁但结论可能不足摘要完整但占用槽位空间需要保留多个决策时调大slot_max_tokens槽位精炼但可能写不下依赖槽位冗长挤压历史和摘要空间统一格式字段时尽量压缩一个相对稳妥的起点是把max_context_tokens设为模型窗口的 60%其中max_window_tokens占 30%摘要和槽位占 30%。这样即使某个字段超长也不至于立刻顶到窗口上限。4.3 用最小实验验证参数是否合理参数调完必须有验证。推荐做一个“约束坚持实验”准备一个硬约束例如“所有生成代码禁止使用 requests只能使用 urllib。”然后连续向模型提出 5 个修改要求比如“写一个 get 请求函数”“改成带超时参数”“增加重试逻辑”“改成类方法”“补充异常处理”。每轮都由上下文管理器自动组装 prompt。实验过程中记录两个指标最终代码是否仍然遵守“禁止使用 requests”。每轮 prompt 的 token 数量变化。如果第 5 轮代码不再遵守约束说明槽位没有把约束固定住或者摘要压缩时把约束删掉了。此时应该优先检查build_messages输出的 system 内容。一个符合预期的结果应该是5 轮之后代码仍使用urllib.request并且每轮 prompt token 稳定在一个范围内。如果 token 持续上涨说明压缩函数没有触发或者max_window_tokens设置过高。5. 常见问题与排查路径5.1 关键约束被模型遗忘现象模型前面几轮还遵守“接口统一返回 JSON”后面几轮突然返回纯文本或直接返回 HTML。常见原因有三个约束没有进入固定槽位只在历史消息中出现被压缩或截断。摘要生成时丢弃了约束相关内容。槽位存在但 system prompt 里没有强调“槽位优先于历史”。检查方法打印每次请求的messages确认system中是否包含完整的constraints字段。如果摘要文本里没有约束关键词说明压缩规则需要调整。解决方式把约束复制进独立constraints槽位并在 system prompt 中明确告诉模型“槽位与历史冲突时以槽位为准”。同时摘要函数要优先保留包含“约束”“必须”“禁止”等关键词的句子。5.2 摘要后丢失依赖信息现象模型使用了错误版本的 API例如把sqlalchemy.orm.Session写成sessionmaker的返回值或者把 Python 2 语法带入 Python 3 工程。原因摘要只记录了“已完成数据库模块”没有记录“使用 SQLAlchemy 2.0 版本ORM 模型统一继承Base”。依赖细节在压缩时被当成普通对话丢了。检查方式查看固定槽位的dependencies字段是否准确。代码生成任务中依赖信息不能靠历史摘要推导应该由工程扫描或用户确认后写入槽位。解决方式每次启动任务时要求用户提供或自动扫描依赖文件写入dependencies槽位。压缩摘要时不要把依赖列表并入普通文本而是保持独立字段。5.3 上下文管理本身消耗过多 token 和费用现象每次请求都很大耗时变长费用增加但输出质量没有明显提升。原因压缩函数触发频率过低或者每次压缩都调用模型生成摘要导致多一次完整请求。检查方式为上下文管理器增加日志打印history_tokens、summary_tokens、slot_tokens和compress_count。观察是不是每次请求都触发压缩。解决方式只有max_window_tokens超过阈值才执行压缩。摘要任务可以使用更便宜的模型或者使用规则摘要。生产环境还可以把摘要任务放到异步队列不阻塞主请求链路。5.4 从最终输出倒推的排查链路当模型输出不符合预期时建议按以下顺序排查检查输入 messages。确认 system 槽位是否存在内容是否过期。检查历史消息。确认最近几轮关键输出是否被压缩保留的消息是否切断了上下文。检查摘要文本。确认旧决策是否还在摘要里有没有包含用户本轮最新的修改要求。检查 token 统计。确认 prompt 是否接近窗口上限模型是否因为截断而丢失尾部内容。检查代码文件变更。确认文件修改后上下文管理器是否更新了进度槽位还是继续使用旧文件内容。排查时可以在每次请求前记录一份日志logging.info(slots%s, json.dumps(ctx.to_slots(), ensure_asciiFalse)) logging.info(history_tokens%s, estimate_tokens(str(ctx.history))) logging.info(summary_tokens%s, estimate_tokens(summary))日志是定位上下文问题最有效的工具不要等到模型输出变差才去猜原因。6. 学习环境与生产环境要分开对待6.1 本地实验的快速跑通方式本地验证只需要一个 Python 脚本和一台能调用模型的机器。可以把CodeContext保存成 JSON 文件方便多人测试同一组上下文。python main.py脚本运行后每次输出模型回复前先打印当前messages结构。这样能直观看到槽位、摘要和最近历史拼接后的实际输入。本地阶段不建议直接接入 IDE 插件或代码仓库扫描先把“约束不被遗忘”这条主线跑通。本地环境还可以准备一个迷你测试集写 10 个前后有依赖关系的需求要求模型逐个完成。比如从“创建 Flask 项目结构”到“添加用户表”再到“实现登录接口”最后到“补充单元测试”。每一轮都依赖上一轮的约定能有效检验上下文管理是否工作。6.2 生产环境至少要补上的工程能力本地跑的上下文管理器只是一个核心算法生产环境还需要补充外围工程能力。能力说明配置外置化模型名、窗口阈值、摘要触发比例放到配置中心或环境变量上下文持久化槽位和历史要持久化到数据库避免服务重启后丢失压缩任务异步化历史过多时用队列触发压缩不阻塞用户请求监控指标记录 prompt_tokens、completion_tokens、压缩次数、槽位更新次数版本回滚每次上下文变更保留快照异常时可回到上一版本多模型适配不同模型窗口和分词方式不同参数要按模型配置缓存重复历史摘要结果可缓存减少模型调用成本生产环境尤其要注意 token 预算。模型窗口是硬上限上下文管理器再聪明也不能在有限窗口内塞下无限信息。当项目规模超出单次窗口时应该考虑检索增强而不是继续压缩。6.3 上下文管理落地检查清单上线前可以使用下面这份清单逐项确认任务槽位是否包含当前最核心的目标是否随着需求变化而更新。约束槽位是否会覆盖历史消息是否能在每次请求中直通 model。依赖槽位是否从实际项目依赖中读取而不是依赖模型猜测。代码风格槽位是否明确是否包含命名规范、错误处理方式、目录约定。进度槽位是否在每次模型输出后更新是否记录了“已完成”和“正在做”。最近错误槽位是否只保留最新错误旧错误是否已经被清理。压缩摘要是否有版本记录能否追溯某一条决策是第几轮确定的。每次请求是否有 token 统计日志是否有 prompt 内容留档。当槽位与历史冲突时模型是否会按槽位执行。是否有回滚机制上下文出错时能否恢复上一稳定版本。这份清单也可以用于代码评审把上下文管理器的质量从“能跑”提升到“可维护”。7. 扩展方向让模型写代码从“能用”到“好用”7.1 把上下文管理与检索结合滚动摘要解决的是旧信息保存问题但当代码库很大时所有历史都保存也不现实。更合理的做法是把上下文管理与检索增强生成结合。当用户提到“修改订单模块里的超时任务”时系统不把整个项目历史塞给模型而是先检索订单模块相关的文件、最近的变更记录、相关函数的定义然后把检索结果放入当前上下文。滚动摘要负责保留项目级决策检索负责提供文件级细节。两者是互补关系。Scroll 这类思路如果延伸到 RAG 场景需要额外管理检索结果的生命周期。文件内容被修改后原来检索到的旧代码片段必须失效否则模型会基于过期代码做修改。7.2 从对话上下文升级为仓库上下文对话上下文只是代码生成的一小部分。更完整的上下文应该包括代码仓库的结构哪些文件被修改哪些函数被调用哪些模块是新建的哪些是重命名后的旧模块。大型工程中模型写代码出错往往不是因为它不理解语言而是因为它看不到完整代码依赖图。实现仓库上下文时可以先用工具扫描代码目录生成 AST 索引记录函数签名、类定义、模块依赖关系。然后把这些结构化信息写入槽位。这样模型知道“订单服务依赖支付服务和库存服务”就不会在修改订单服务时随意改动支付接口。从对话上下文升级到仓库上下文是让模型成为一个真正“熟悉项目”的开发者的关键一步。7.3 给新手的练习建议如果刚接触上下文管理不建议直接搭建完整框架。可以先写一个最简单的脚本只做一件事固定一个约束多轮修改代码观察模型是否忘记。具体练习路径如下写一个chat函数直接调用模型不做任何上下文管理。连续向模型发起 8 轮修改请求每一轮都给定不同但相关的代码修改任务。在任务开始时提出硬约束“所有接口返回 JSON 格式”。观察第 8 轮结果是否还遵守约束。实现固定槽位把约束写入 system prompt重复实验。实现滚动摘要重复实验。对比三次实验中最终代码遵守约束的比例。这个练习能直观感受到上下文管理带来的差异。之后再考虑接入文件系统、数据库和模型部署复杂度会自然上升。上下文管理不是模型能力的替代品而是模型能力的一部分。把 Scroll 这类思路内化成自己代码生成工具的基础设施后模型才真正开始像一名能够持续工作、不翻旧账的开发者。