FEATURED · 精选文章

Scroll项目:让Agent用代码管理上下文的工程实践

发布时间 / 2026/8/31 8:22:19
来源 / 创域科博编辑部
栏目 / 资讯中心
Scroll项目:让Agent用代码管理上下文的工程实践 最近在梳理 Agent 工程的落地细节时我越来越强烈地感觉到一个矛盾模型能力在快速进步但很多复杂任务最后不是“模型不够聪明”而崩掉的而是“上下文管不住”而崩掉的。比如一个智能客服 Agent用户前面聊了订单信息中间问售后流程最后又绕回订单状态。如果你把全部对话原文都塞进模型上下文很快会遇到几个现实问题token 上限触顶、关键信息被淹没、单次请求成本直线上升。手工让模型总结摘要又经常丢细节订单号错一位、售后状态记反处理起来非常头疼。最近看到阿里开源的 Scroll 项目核心思路让我觉得值得认真聊一聊与其让模型把上下文“背在脑子里”不如让模型自己写代码来管理上下文。换句话说Scroll 不是给模型换一个更大的窗口而是给模型配一个“记事本管理员”让记忆从模型的隐性能力变成显性的、可编程的工程能力。这篇文章会围绕这个思路展开先讲清楚它到底解决了什么问题再拆解它的核心原理然后给出一个概念级 Demo 和工程落地建议最后聊一聊哪些场景适合它、哪些场景不建议盲目上。1. 为什么上下文会成为 Agent 的真正瓶颈很多开发者第一次接触 Agent 时会觉得“上下文”就是 prompt 里那几段文字不够就继续拼。但实际跑一个稍复杂的任务就会发现上下文管理是整个系统最容易出问题的地方而且出问题的方式非常隐蔽。第一个问题是物理上限。每个模型的上下文窗口都有明确长度多轮对话、工具调用结果、中间脚本输出、参考文件内容都会挤占空间。一个看起来不太复杂的跨天任务可能在十几个来回之后就无路可退只能截断历史。而截断是粗暴的它不会替你分辨哪些信息重要哪些不重要。第二个问题是信息衰减。即使上下文长度没有超限模型也不一定真的“看”了所有内容。研究表明模型对长文本中部的信息往往敏感度更低这可能和注意力分布有关。放到 Agent 场景里如果订单号出现在第十二轮、售后记录出现在第二十轮到第三十轮时模型大概率会凭“印象”编一个答案而不是回头去翻准确信息。第三个问题是成本。大模型调用费用和 token 数量直接挂钩而且大多数模型的 attention 计算会随序列长度上升。每轮对话都重复处理全部历史意味着同一个事实会被反复计费、反复计算。上下文越长单次请求的耗时和费用都同步上涨这在生产环境里是非常现实的约束。第四个问题是状态一致性。一个复杂任务通常会被拆成多个子步骤步骤之间的中间状态靠什么传递如果只靠对话历史每一步都可能成为污染源。前一步的误解读、截断、或模型随机生成的一句话都可能带偏后面的所有决策。这本质上已经不是模型智力问题而是系统设计问题。所以我的判断是上下文不应该只被当成 prompt 拼料它更接近一个外部系统的状态。我们要解决的是状态如何存储、读取、更新、压缩和审计。Scroll 的核心就是把这一层用代码显式表达出来。2. Scroll 的思路从“硬记”到“编码”在 Scroll 之前开发者处理长上下文主要有四种方案每一种都有明显缺陷。方案一是窗口截断。超过窗口就删最早的内容实现最简单代价是信息丢失不可控。方案二是模型摘要。把历史对话交给模型压成一段话再继续跑。摘要适合“大概意思保留”的场景但缺少精确性而且每次摘要都是不可逆操作一旦摘要错了原始信息已经回不来。方案三是 RAG 检索把文档切块向量化按相似度找回。RAG 在开放域知识问答里很有用但在强状态、强顺序、强关系的任务里表现不稳定比如订单状态流转、多轮表单填写用户问“刚才那个订单怎么样了”语义相似度并不足以精确找回“刚才”对应的实体。方案四是外部状态。人手工设计 JSON 或数据库结构Agent 读写这些结构。这个方案更接近工程化但状态结构是预设的一旦任务超出预设Agent 就不知道该怎么更新状态了。Scroll 的思路更进了一步状态结构不靠人预先写死而是由模型在运行时生成代码来定义和维护。模型既是任务执行者也是状态管理器的编写者。它可以根据当前任务需要生成一段更新状态文件或数据库的代码代码执行后外部状态就变成了一个真实存在的、可查询、可回滚、可审计的工程产物。这个思路把上下文管理从“模型的隐性行为”变成了“显式代码”。原来的问题是模型面对一串越来越长的历史它的注意力是概率性的可能漏看可能记错。新方案是历史的关键信息被结构化保存到外部模型每一轮只需要基于一小段“种子上下文”做决策需要细节时可以通过代码去查就像人做项目时不用把整个对话背下来而是随时翻看自己的笔记和表格。这个转变非常关键它意味着上下文长度不再是任务复杂度的直接函数。我们用一个生活中的类比帮助理解。会议长达两小时如果要求你全部记住所有数字、日期和结论你一定崩溃。但如果你安排一个助理在旁边做结构化笔记会议结束时拿着笔记做总结事情就变得可控。Scroll 所做的是让模型自己扮演那个助理并且用代码把“做笔记”这件事做得可执行、可追溯而不是靠模型临场发挥。3. 核心原理模型、代码执行器与外部状态的三层协作从架构角度看Scroll 的设计可以抽象成三个组件模型、代码执行器、外部状态存储。三者的关系不是串行调用而是一个迭代循环。先说外部状态存储。它可以是本地文件、JSON、SQLite、数据库或对象存储关键特征是状态独立于模型上下文存在。模型不在了、会话重启了、换一个模型厂商了状态还在。这个特性对于生产 Agent 尤其重要因为会话可能跨小时甚至跨天模型实例可能发生切换。再看代码执行器。它负责运行模型生成的代码并限制代码的权限。模型生成的代码不是直接执行在宿主机上的而是放进一个受控环境比如沙箱容器或子进程。代码能访问什么路径、能调用哪些 API、执行时长上限是多少都由执行器的策略控制。这个设计是工程安全的关键因为模型生成代码本身是一件动态的事情不能完全信任它的每一次输出。最后是模型。它在这个循环里承担两个职责第一基于当前任务目标和种子上下文决定下一步需要什么信息第二生成一段代码把需要更新的信息写进外部状态。模型不再是所有信息的载体它只需要在每一个决策点读取一小段关键状态其他信息都放在外部。整个迭代循环大致是第一步观察。模型拿到任务目标、会话阶段、上一步输出后的状态摘要。第二步规划。模型判断当前这一步需要哪些新信息有没有需要保留到后续步骤的关键数据。第三步生成代码。模型生成一段代码用来更新外部状态比如新增一条记录、修改订单字段、追加文件索引。第四步执行。代码执行器在沙箱中运行这段代码并返回执行结果。第五步提取种子。执行完成之后系统把外部状态压缩成一个短小的“种子上下文”比如最新状态快照加上未完成任务清单。第六步推理。模型基于种子上下文继续回答或调用工具进入下一轮循环。这个循环有一个很明显的优势模型上下文里需要携带的内容不再是全部历史而是“决策所需的最小集”。它让上下文长度趋于稳定而不是随着对话轮次线性膨胀。同时因为每一步状态变更都落在了外部存储里任何一步出错都能回溯理论上甚至可以回滚到上一个版本。这是传统“把所有东西塞进窗口”的模式完全做不到的。4. 为什么“写代码”比“写摘要”更适合复杂任务有人会问模型直接写一段摘要不是更省事吗为什么非要生成代码去管理状态这里面的差别恰好是 Scroll 这类方案真正的价值所在。先看摘要的本质。摘要是一个高度压缩过程它把一组事实映射成一段自然语言而这个映射是有损的。当摘要只有两三句话时它可能保留“订单已退款”这个结论但丢掉了“退款金额 88.5 元、原支付渠道 13 号账单、用户要求开具电子发票”这些次级但重要的细节。更麻烦的是摘要是一次性生成过程无法在事后局部修正。想让摘要多保留一个字段只能重新生成而重新生成可能改变其他内容。代码的本质则完全不同。代码不再是一次性压缩而是一种可重复执行的状态转换。模型生成的结构化操作可以只更新一个字段其他字段原样保留。例如# 生成一段更新订单状态的代码 def update_order(orders, order_id, new_status, note): for order in orders: if order[order_id] order_id: order[status] new_status order[updated_at] 2025-01-20 14:30:00 if note: order[notes] note return order return None这段代码执行后只有指定订单的位置被修改其他所有数据都不受影响。这是摘要类方案做不到的精细度。再看可测试性。代码可以被静态检查、单元测试、在测试环境跑一遍确认没有破坏数据结构后才进入生产。摘要无法做形式化验证只能靠人肉眼判断。对于金融、客服、数据分析这类对准确度要求高的场景代码的验证能力是很大的优势。还有可审计性。代码有明确的执行记录谁在什么时间修改了哪个状态字段都能通过日志还原。摘要则是黑盒你只有最终一段文字无法知道它基于哪些原始信息得出的结论。从工程角度看摘要适合“传递语义”的场景代码适合“维护状态”的场景。Scroll 把状态维护这份工作完全代码化等于让 Agent 拥有一个可以随时增删改查的“结构化记忆库”。这才是它区别于传统提示词工程的核心。传统提示词工程是让人去适配模型的输入输出Scroll 是让模型去创建和维护外部系统的状态两个方向完全不同。5. 概念级实现搭建一个由模型管理上下文的 Agent 骨架下面我会给出一个概念级 Demo用来演示“模型生成代码管理上下文”这个核心循环。需要先说明以下代码不是某个具体 SDK 的官方调用方式也不是 Scroll 的真实 API只是为了讲清楚设计模式而写的示意实现。真实项目中请以对应官方仓库和文档为准。5.1 环境准备本文示例使用 Python 3.10 及以上版本不需要安装第三方框架。我们会用标准库完成文件读写和子进程执行。所有演示均在本地完成不含任何线上接口调用。建议先创建一个实验目录mkdir scroll-demo cd scroll-demo目录结构如下scroll-demo/ ├── main.py ├── context/ │ └── state.json └── sandbox_runner.py5.2 状态文件context/state.json状态文件是 Agent 的外部记忆仓库。初始状态下它只包含一个空的任务记录列表{ current_task: 处理用户订单查询与售后引导, customer: { user_id: u_1024, name: 李明, recent_orders: [] }, history_log: [], open_questions: [] }这个文件相当于 Agent 的“笔记本”。每一轮之后代码更新这个文件下一轮模型基于更新后的文件做决策而不是重新阅读整段对话。5.3 沙箱执行器sandbox_runner.py模型生成的代码不应该直接运行在宿主环境里。为了演示我们用一个子进程来执行模型生成的脚本并设置超时时间。这里的核心思路是代码最多运行 10 秒超出即终止避免模型生成死循环。# 文件路径scroll-demo/sandbox_runner.py import subprocess import sys import tempfile import os TIMEOUT_SECONDS 10 def run_generated_code(code: str, state_file: str) - str: 在受限子进程中执行模型生成的代码并传入状态文件路径。 # 把模型生成的代码包装成一个可脚本化的临时文件 wrapper f import json import os state_file {state_file!r} def load_state(): with open(state_file, r, encodingutf-8) as f: return json.load(f) def save_state(state): with open(state_file, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) {code} with tempfile.NamedTemporaryFile(w, suffix.py, deleteFalse, encodingutf-8) as f: f.write(wrapper) tmp_path f.name try: result subprocess.run( [sys.executable, tmp_path], capture_outputTrue, textTrue, timeoutTIMEOUT_SECONDS, cwdos.path.dirname(os.path.abspath(state_file)), ) if result.returncode ! 0: return f执行失败: {result.stderr} return result.stdout except subprocess.TimeoutExpired: return f执行超时{TIMEOUT_SECONDS}秒 finally: os.unlink(tmp_path)这个执行器做的事情很简单把模型生成的代码片段包装成一个临时 Python 脚本传入 state_file 路径然后在一个新子进程中运行。子进程不继承当前进程的全局变量天然形成了一层基础隔离。5.4 主循环main.py主循环模拟的是“模型生成代码 → 执行器运行 → 状态更新 → 提取种子上下文”这个过程。为了让示例不依赖外部 API我们用一个模拟函数代替模型推理。你可以在真实环境里把 mock_model_generate_code 替换成任意模型的接口调用。# 文件路径scroll-demo/main.py import json from sandbox_runner import run_generated_code STATE_FILE context/state.json def load_state(): with open(STATE_FILE, r, encodingutf-8) as f: return json.load(f) def save_state(state): with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) def mock_model_generate_code(seed_context: str, state: dict) - str: 模拟模型生成的状态管理代码。真实环境请替换为 LLM 接口调用。 # 这里返回一段写死的代码用于演示状态更新流程。 return order_data { order_id: A10086, product: 智能门锁, price: 899.0, status: 已付款, logistics: 待发货 } state load_state() state[customer][recent_orders].append(order_data) state[history_log].append({ step: 用户输入订单号A10086系统查询到订单信息, context_summary: 订单A10086已付款等待发货 }) state[open_questions] [用户之后可能追问发货时间] save_state(state) print(状态已更新新增订单A10086) def extract_seed_context(state: dict) - str: 把外部状态压缩成下一轮模型的种子上下文。 customer state.get(customer, {}) recent_orders customer.get(recent_orders, []) order_lines [ f订单{o[order_id]}: {o[product]}状态{o[status]} for o in recent_orders ] return ( f当前任务{state.get(current_task, )}\\n f用户{customer.get(name, )}\\n f近期订单\\n \\n.join(order_lines) \\n f待跟进问题{state.get(open_questions, [])} ) def agent_loop(round_count1): state load_state() for _ in range(round_count): seed_context extract_seed_context(state) print( 本轮种子上下文 ) print(seed_context) print() code mock_model_generate_code(seed_context, state) print( 模型生成的代码 ) print(code) print() result run_generated_code(code, STATE_FILE) print( 执行结果 ) print(result) print() state load_state() if __name__ __main__: agent_loop()这段代码的关键设计有两点。第一模型和状态文件之间通过代码解耦代码执行成功后状态文件才发生变化。第二每一轮结束后的 seed_context 是压缩的它只包含当前任务、用户信息和近期订单摘要不包含完整对话历史。运行方式python main.py预期输出会依次显示种子上下文、模型生成的代码、执行结果。程序结束后state.json 里会多出一条订单记录history_log 里会增加一行操作日志。这个示例虽然简单但已经体现了整个思路的基本形状。你可以把它理解成一个最小可运行的 Scroll 模式骨架。继续扩展时最值得替换的部分就是 mock_model_generate_code把它变成真实模型调用并让模型根据当前任务动态生成更新代码。6. 运行结果与效果验证示例跑通后验证点主要有三个状态文件是否按预期更新、种子上下文是否保持精简、历史日志是否可追溯。运行前先确认 state.json 中 recent_orders 是空数组。运行python main.py后再打开 state.json能看到类似下面的内容{ current_task: 处理用户订单查询与售后引导, customer: { user_id: u_1024, name: 李明, recent_orders: [ { order_id: A10086, product: 智能门锁, price: 899.0, status: 已付款, logistics: 待发货 } ] }, history_log: [ { step: 用户输入订单号A10086系统查询到订单信息, context_summary: 订单A10086已付款等待发货 } ], open_questions: [ 用户之后可能追问发货时间 ] }如果每一步都成功你会看到三个明确信号。第一个信号是状态文件发生了预期变更。新增订单信息出现在 recent_orders 中说明模型生成的代码被正确执行并且状态是可持久化的。第二个信号是种子上下文输出非常短。回到终端可以看到打印的 seed_context 只有几行但 state.json 里存了完整订单对象。这说明模型只需要依赖一个紧凑摘要就能继续推进任务而不必把所有原始对话重新读一遍。第三个信号是 history_log 的存在。无论后续出了什么问题我们都能通过这个日志了解“这个状态是哪个步骤写入的”这一点在传统对话系统中很难实现。如果运行失败优先检查三个位置。第一看终端输出的错误信息。如果提示ModuleNotFoundError: sandbox_runner说明 main.py 和 sandbox_runner.py 不在同一目录或者你从其他目录运行了命令。第二如果提示 JSON 解析错误说明 state.json 的格式被破坏请检查文件是否被其他程序改写并确认逗号、引号是否完整。第三如果执行超时说明模型生成的代码可能存在死循环需要在沙箱执行器中进一步收紧超时时间并限制循环次数。7. 真实场景下的使用边界与风险概念 Demo 跑通之后更重要的是一盆冷水这个方案并不是“银弹”在生产环境里它有几个非常现实的风险点。第一个风险是代码安全性。模型生成的代码天然不可完全信任。它可能因为出错而删除文件也可能因为被注入恶意指令而执行危险操作。所以在真实项目里代码执行器必须做多层防护使用独立容器或虚拟机运行、以最小权限账号执行、禁止网络访问、限制文件系统可写范围、严格控制依赖库。不要在图省事的情况下直接把模型生成的代码exec在当前进程里这等于把 Agent 的完整权限交给一个概率模型。第二个风险是状态一致性。多轮任务中状态文件可能被多个环节并发读写。如果没有锁或版本号机制两个步骤同时写同一个 JSON 文件后写入方可能覆盖前写入方的数据。生产环境建议使用带版本号的存储或者直接使用数据库表记录状态变更每次更新都是插入新版本而不是原地覆盖。第三个风险是审计困难。虽然代码执行日志比摘要好追踪但状态文件本身仍然可能被直接刷写。如果状态文件可以被人工编辑而你没有办法区分这次修改是模型生成代码造成的还是人工调试造成的审计链路就断了。建议给状态变更记录加上批次号每次执行模型生成的代码时把代码内容本身也存到审计表里。第四个风险是场景错配。如果任务只是简单的单轮问答比如“帮我写一封邮件”引入“模型写代码管理上下文”反而增加了复杂度响应的首字延迟也会变高。这个方案适合的是复杂多步、长会话、需要精确记忆的任务不适合高频低延迟的轻量场景。安全方面也要特别提醒在涉及用户数据、订单信息、支付记录等敏感数据时必须在合规前提下进行数据脱敏和权限控制不能让 Agent 的状态文件变成敏感信息的裸奔仓库。所有状态读写都应当经过授权检查关键操作要有审计记录并且生产环境必须遵守最小权限原则。8. 常见问题与排查思路问题现象可能原因排查方式解决方案模型生成的代码频繁语法错误模型缺少状态文件结构的足够描述查看错误日志中 Python traceback在 prompt 中附上 state.json 的 schema 示例并要求先输出 JSON 校验结果代码执行超时模型生成死循环或执行了资源消耗过高的操作查看沙箱超时日志和资源占用缩短超时时间限制循环次数复杂数据处理交给独立任务队列状态文件被清空或字段丢失模型代码里出现全量覆盖赋值对比最近一次审计日志中的代码内容沙箱中禁止直接写整个 state.json只能通过白名单函数更新种子上下文仍然太长状态对象嵌套过深、冗余字段太多检查 extract_seed_context 输出对状态做精简只保留当前步骤必需字段多轮后出现重复写入状态查询未做幂等控制查看 history_log 中同订单是否出现多次在状态更新代码中加入订单号去重逻辑跨会话恢复失败会话 ID 没有绑定到状态文件检查状态文件命名和每次请求参数为每个会话维护独立状态目录并增加会话 ID 校验这些问题的共性在于一旦你接受“模型用代码维护上下文”这个前提所有传统分布式系统的基础问题都会找上门来。所以它不是一个可以偷懒的技术方案它只是把问题从模型层转移到了工程层而工程层的问题是你可以用成熟手段解决的。9. 最佳实践与工程建议结合上面的思路这里整理几条实践建议适合从 Demo 走向生产的开发者参考。第一给 Agent 设计一个“迷你文件系统”。不要把所有状态塞进一个巨型 JSON。更好的做法是分目录管理比如context/memory/{会话ID}/下面放订单、对话摘要、工具调用记录、用户偏好等独立文件。模型生成代码时也更容易定位到目标文件而不是频繁加载全量状态。第二为状态更新定义白名单接口。不要让模型直接写原生文件操作代码而是给它一组预设函数比如add_order(order_data)、update_status(order_id, new_status)、append_log(entry)。模型只需要做“填空式”调用大大降低生成代码出错概率。这也是很多实际项目比完全自由生成更稳妥的原因。第三每一轮变更都写审计日志。审计日志不只是记录“改了什么”还要记录“为什么改”。做法是让模型在生成代码前先输出一句意图描述代码执行后系统把这句意图和实际代码一起存下来。这样后续复盘时你能理解每一步的动机。第四状态需要定期压缩。虽然外部状态不会占模型窗口但文件本身会越来越大。可以设置一个压缩策略当状态文件中某类历史记录超过 N 条时把超过部分归档到冷存储只保留最近 N 条在当前状态文件里。这样可以避免每次加载文件、解析文件的开销越来越大。第五给代码执行器加“人在回路”的开关。对于关键状态变更比如退款、删除用户数据、修改金额不要直接执行模型代码。让模型先输出一个变更计划人工确认后再执行。这种开关可以做成默认关闭、按需开启但对高风险操作建议强制开启。第六前后端都要考虑回滚。状态文件建议使用版本化命名比如state_0001.json、state_0002.json每次更新都生成新版本。出问题时可以快速回退到上一个版本而不需要从日志里手工重建状态。这个成本很低收益却很直接。第七不要忽略向模型传输的 schema 信息。你可以在 prompt 中带一段精简的字段说明让模型知道哪些字段必须保留哪些字段可以丢弃。这个看似细节实际上可以显著减少模型生成错误代码的概率。10. 总结与后续学习方向Scroll 这类方案带来的最大启发不是某个参数或某个 API而是思路上的转变把模型上下文从“一次性提示文本”变成“外部系统状态”让模型用代码来维护记忆。它让上下文管理变得可扩展、可追踪、可回滚也把 Agent 的可靠性问题从概率层拉回到工程层。接下来你可以做的第一件事是把上面的 Demo 跑通再看清楚种子上下文、状态文件、代码执行器这三个角色之间的关系。然后试着把 mock_model_generate_code 换成真实模型调用给它一个具体任务观察模型会不会按预期生成状态更新代码在哪些地方会出错。这一步跑完再考虑是否引入沙箱容器、数据库存储、审计服务。如果做的是复杂 Agent、长时间运行的任务型系统或者客服、数据分析、自动化运营这类强状态场景这个思路非常值得深入实验。如果项目只是一个简单问答工具可以暂时不用引入这套复杂度。判断标准很简单你的任务是不是依赖跨多轮的精确记忆如果是那 Scroll 这条路就值得继续走。建议先把状态文件和审计日志放进本地 Git 仓库每跑一轮都看一眼 diff你会非常直观地看到“上下文”到底是怎么被模型管理起来的。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻