Summary
精读 Anthropic 工程博客两篇:《Code execution with MCP》与《Effective harnesses for long-running agents》。前篇讲用代码执行给上下文瘦身(渐进披露、沙箱内处理数据、Skills 固化),后篇讲长程 agent 跨 session 的交接方案(initializer + coding agent、特征清单、git/progress、端到端测试)。两篇共同指向一条演进线:agent 从 LLM + 工具走向 LLM + harness。

把这两篇放在一起读,是因为它们前后只隔三周(2025-11-04 与 2025-11-26),讲的恰好是同一件事的两面:

  • 前一篇解决”工具定义和中间结果把上下文啃光”的问题,办法是让 agent 别再直接发 tool call,改成写代码去调用工具;
  • 后一篇解决”每个新窗口都从零开始”的问题,办法是把记忆挪到模型脑子外面去。

两篇合起来,正好画出一条线:agent 正从 LLM + 工具 走向 LLM + harness。


上篇 · Code execution with MCP:把上下文省在两个口子上

原文:https://www.anthropic.com/engineering/code-execution-with-mcp Code execution with MCP: Building more efficient agents(2025-11-04,作者 Adam Jones、Conor Kelly)

一个被规模放大的老毛病

MCP 自 2024 年 11 月发布后,很快成了连接 agent 与外部系统的既成标准,社区攒了几千个 server。工具一多,两个平时不显眼的成本就被放大了。

一是工具定义全量预加载。绝大多数 MCP client 的做法是,把拿到的所有工具定义一次性塞进上下文。接几千个工具,就是几十万 token——模型还没读到用户那句请求,预算已经烧掉一大截。

二是中间结果必须穿过模型。原文举了个例子:让 agent”把 Google Drive 里的会议记录下载下来,附加到 Salesforce 的线索上”。第一步取回来的整段记录要进上下文,第二步写回时又把整段记录抄进参数里——同一份文本走了两遍。一场 2 小时的销售会议,可能就多出 5 万 token;文档再大一点直接撑爆窗口,工作流当场断掉。而且模型在两次调用之间搬运大块数据时,抄错的概率也不低。

要理解这两个毛病,得先看清 MCP 的调用链路:模型 ↔ MCP client(跑在 harness 里)↔ MCP server。client 通过 tools/list 从 server 拿到工具定义,塞进模型上下文;模型决定”要哪个工具、传什么参数”,但吐出的只是一个调用意图,真正执行、路由、再把结果回传的是 client。也就是说,client 并不理解业务,它只负责搬运,它自己的动作不占一点上下文。真正进上下文的是这条链路的两个端点:注入进去的工具定义、回传出来的结果。

解法:把 MCP server 暴露成代码 API

思路是把工具从”直接暴露的调用语法”换成”文件系统上的代码”:给每个 MCP server 生成一个目录,目录下每个工具对应一个 TypeScript 文件,文件里是一个薄封装函数,函数体转调 MCP client。比如 servers/google-drive/getDocument.ts 对应 Google Drive 的取文档工具,参数和返回值用 interface 标出类型。

agent 发现工具的方式也随之改变:先列出 servers/ 看有哪些 server,再按需读它要用的那几个工具文件,理解接口后直接写代码调用。前面那个”Drive 转 Salesforce”的例子,就成了一行取值、一行写回的普通代码。

这样只加载当前任务需要的定义。原文给的数据是 token 从 15 万降到 2000,省 98.7%。Cloudflare 有类似实践,把它叫作 Code Mode。核心洞察一句话:LLM 擅长写代码,就该让它用这个强项去对接 MCP。

这里最容易理解岔的一点,是别以为”把 MCP client 挪出了上下文”。client 从来没进过上下文——它从头到尾都在外面那侧做搬运:连 server、拉定义、路由、执行、把结果带回来,这套过程本身不产生 token。真正占着上下文预算的是另外两样东西:

  • 静态的:harness 把 client 拿到的全部工具定义一次性注入模型上下文;
  • 动态的:每个工具返回的结果要回传进上下文。

所以这次改动划的不是 client 的位置,是”什么碰上下文”的口子:

  • 定义,从”全量预加载”收成”按需读”。模型不再面对一张一次全塞进来的工具总表,而是先看到一棵文件树,需要哪个才读哪个的 schema。
  • 结果,从”过一遍模型”收成”留在沙箱”。中间数据在代码里流转,只有显式 log 或 return 的那点东西回到上下文。
  • 模型侧的动作也跟着变:以前是”在一堆现成定义里挑一个,吐一个 tool call”,现在是”先浏览文件树找到要的,写代码引用它”。挑,从”眼前全摆着挑”变成”需要了才去读”——省的就是这一口。

执行端始终没变,还是那个 client;变的是谁触发它(tool call 变成模型写的代码)、在哪跑(模型循环里变成沙箱)、什么回上下文(全部结果变成显式输出)。

四条收益

渐进式披露(progressive disclosure)。模型很会逛文件系统,工具摆成文件,就能按需读定义,而不是一次性全读。另一个办法是在 server 上挂一个 search_tools,让它带 detail level 参数(只给名字 / 名字加描述 / 带 schema 的完整定义),模型按需索取。

要提醒的是,这是”少读”而不是”不读”。只要工具定义进上下文就烧 token,工具一多照样会涨。它做的是把量级从 O(全部工具) 压到 O(当前用到的几个),不是把账结清。

上下文高效的结果处理。大数据集先在执行环境里过滤、聚合、连接,再把结果交给模型。原文的例子是张一万行的表格:按状态筛出待处理订单、只打印条数和前几行供审阅,模型的上下文里最终只有五行,而不是一万行。

这背后的道理其实是软件工程里的数据局部性——谁要算,就在数据附近算,别让它跨边界来回搬。放在这里,“远端”就是那个又贵又远、带宽还小的模型,沙箱是”本地”。数据、能力、私有内容都留在本地,只把显式 log 或 return 的部分交出去。

更强的控制流。循环、条件、错误处理、轮询等待,直接用代码写,不必在 agent 循环里反复”调工具—sleep—调工具”。原文举的例子是轮询 Slack 频道等一条部署完成的消息:写成代码就是一个 while 循环,等到了就退出。

这里省下的其实是”中间轮次的首 token 延迟”。先看 agent 的”一轮”是什么:一轮 = 一次完整的模型调用 + 它产出的那个动作。传统逐步走,完成一件”查一下、再写回”的事要好几个轮次:

轮1: 模型(用户请求 + 全部工具定义) → 想 → 吐 tool_call(取文档) ← 付一次 TTFT
执行: client 调 server
轮2: 模型(上文 + 文档全文) → 想 → 吐 tool_call(写回) ← 又付一次 TTFT
执行
轮3: 模型(上文 + 全部) → 吐最终回答 ← 再付一次 TTFT

TTFT(time-to-first-token)是每次模型调用里,从 prompt 送进去到吐出第一个字的等待。它主要花在 prefill——把整个输入上下文过一遍,输入越长等得越久;之后 token 是流式出来的,单个很快。所以每轮最贵、最容易被感知的停顿,就在开头这一下。

要命的是三条:每轮都是把模型从头跑一遍,不是接着上一轮跑,所以”想下一步、决定发哪个 tool call”这件事每轮都要重新生成一次;每轮开头都要付一次 TTFT;而且上下文越堆越多,prefill 越来越长,一轮比一轮慢。所谓”中间轮次”,就是那些不产出最终答案、只为推进流程的轮次(上面轮 1、轮 2)——它们本身不给用户任何东西,却各自白付一次”重启生成”的等待。

把确定性的循环、条件、轮询写成一段代码交给沙箱一次跑完,就只付一次”我写这段代码”的调用(一次 TTFT + 生成代码的 token),后面的 while 循环、分支、多次工具调用全在沙箱里本地跑,不再一步步回模型。只有当代码需要”再想想”时才回模型开新的一轮——省掉的就是那些”每步都回模型、每步都从头重启一次生成”的 TTFT。

前提是分支条件能在沙箱里确定地算出来;要靠模型理解来判的分支(比如”这条评论是不是负面”),还是得回模型。说白了,条件树是死板的,但换来了时间——这正是 workflow(代码定路径)与 agent(模型定路径)的分界。

不过这条收益很容易被讲过头,它其实很局限:

  • 只吃确定性那一块。能在沙箱里不带判断算完的步骤才搬得走;一旦分叉要靠模型理解,还是得回模型付那一轮 TTFT。省的是”可代码化的中间轮次”,不是”中间轮次”。
  • 它要求你提前知道路径。得在写代码那一刻就想清楚 loop 什么、判断什么、失败怎么办。可 agent 的价值恰在路径事先不知道——真能提前写全,它就不是 agent 而是 workflow 了。写死得越多,省得越多,也越像套壳脚本。
  • 出错要回模型,等于白省。代码跑挂、撞上没预料到的分支,还是得把错误抛回模型重来,这一轮反而比原来多绕一步。
  • TTFT 本身没多贵,prompt caching 一上,重复前缀的 prefill 还会大幅变快。长任务里占大头的,往往是模型真要想的 token,不是每轮开头那一下。
  • 它没动根本的那几个模型轮次。写代码、读结果做判断、出最终答案,这几轮照付;省下的只是纯机械往返,天花板就是那些机械步骤原本占的比例。

所以更准确的说法是:这是一条渐进原则,不是一个解法——把能搬走的机械步骤尽量搬出模型循环,模型只留不可削减的判断。天真的版本会退化成 workflow;有意义的版本是”该确定的地方确定,该交给模型的地方不省”。

隐私保护。中间结果默认留在执行环境,只把显式 log 或 return 的部分交给模型;你不想共享的数据可以在工作流里流转,而从不进入模型上下文。更敏感的场景,harness 还能自动对 PII 做 token 化——真实值在 client 层查表还原,模型全程只看到 [EMAIL_1] 这类占位符。比如把一张表里的客户联系方式导入 Salesforce,模型眼里是 token 化的字段,真值从表格流到 CRM,全程没经过模型。这等于顺手拿到一条数据边界:可以确定地说”哪些数据从没到过模型那里”。

状态持久化与 Skills

带文件系统访问的代码执行,还顺手解决了状态问题:中间结果落盘,下一个 session 能接着跑。agent 可以先把查到的线索写成 CSV,之后换个窗口再从文件读回来接着处理。

更值得注意的是:跑通的代码可以被存成可复用函数,加一个 SKILL.md 就成了结构化 skill。也就是说,agent 不只是”用现成的工具”,它开始自己攒工具箱——把自己验证过的解法固化成高一层的能力。这一步比省 token 更有长期价值。

代价

代码执行不是白拿的。要跑 agent 生成的代码,就得有安全的执行环境:沙箱、资源限制、监控。这些基础设施直接调用工具时并不需要,是实打实的运维和安全开销。省下的 token、延迟、工具组合能力,得跟这份成本一起权衡。

放到更长的线索上看,这份代价指向一条演进:agent 正从 LLM + 工具 走向 LLM + harness。简单的工具调用撑不起长程任务、coding 任务,沙箱、权限控制、token 预算这些能力只能由 harness 来兜。而一旦允许模型写代码,攻击面就从”它能调哪些工具”扩到”它能跑什么代码”——安全边界必须由 harness 守住。这也正是下篇的入口。

沙箱的权限模型:从「减法」到「加法」

「安全边界必须由 harness 守住」这句话,说起来轻巧,做起来有两种截然不同的思路。Cloudflare 那篇讲 Code Mode 的文章(2025-09-26)把这层哲学讲得比 Anthropic 原篇更透,值得补一句。

传统沙箱走的是减法。先给一个能力齐全的环境——有网络、有文件系统、有 shell——然后一层层往里加限制:关端口、上 seccomp 过滤系统调用、防火墙只放行几个域名、挂载只读目录。本质是「全都能干,我把危险的收走」。

这套的问题在于,它的正确性依赖你把危险项穷举干净。漏了一个出口,就是漏洞;而危险的面几乎无穷。边界还模糊:防火墙想判断「这条流量到底是不是这个 agent 合法需要的」很难,模型那侧也在猜「我这么写会不会被拦」——两边都在跟一个说不清的规则较劲。

加法模型反过来:沙箱启动时几乎什么都没有。Cloudflare 的 Code Mode 里,沙箱默认无网络(globalOutbound: null,fetch() 直接抛错),能力一个一个显式授予——要它能查 GitHub,就给它一个已经鉴权好的 client binding,除此之外什么都没有。默认拒绝,白名单是有限的、明确的、可审计的。

为什么说这是哲学转变,而不是小优化:

  • 提问方式反过来了。减法问的是「怎么阻止这东西做得太多」,加法问的是「我们到底希望它能做什么」。后者才是能给出确定答案的问题——前者的答案永远是不完整的。
  • 边界从模糊变精确。网络过滤要靠猜「哪条流量合法」;binding 就是「这个 JS 接口允许被调」,调别的直接不存在。模型不用猜,supervisor 也不用判别。
  • 安全不再靠行为约束,而靠架构。用 Cloudflare 的原话,是 enforce security through architecture rather than behavior——不是祈祷模型别写坏代码,而是坏代码在物理上就没有那个能力。
  • 顺带一个副产品:能力由 binding 给出去,binding 背后的 token 存在 supervisor 那侧,AI 写的代码根本没机会把密钥写出来泄漏——这是减法模型里很难彻底解决的问题。

不过这个保证有个必须说清的边界:它只覆盖「你没授予的能力」。一旦你给了一个 read-write 的 workspace binding,它照样可以把工作区里的文件全删——这段代码确实是坏的,而且它有能力。加法模型不阻止这个,它只是把风险面收敛到「你明确授予的、能列举、能审计、能单独设权限的那几项能力」上。所以准确的说法是:不是让坏代码无害,而是让坏代码能造成的伤害严格等于你主动发出去的那几张牌,一张不多。跟减法比,区别在于漏掉的牌是零张,而不是「我尽量都收干净了」。

这其实就是最小权限原则(principle of least privilege),只是传统实现起来太费劲,大家退化成「先给全再收」。而 isolate 这种便宜到可以每次从零起的运行时,才第一次让真正的「默认零权限」变得不麻烦——这也解释了为什么 Code Mode 和 Cloudflare 的 Agents SDK 敢把「每个 agent 一套独立沙箱」当默认姿势。


下篇 · Effective harnesses:跨 session 的长程 agent 怎么不失忆

原文:https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents Effective harnesses for long-running agents(2025-11-26)

问题:每换一个窗口,就失忆一次

agent 只能在离散的 session 里工作,一个新的 session 对之前发生的事毫无记忆。项目一复杂,一个上下文窗口装不下,活就得跨很多个窗口干——而每个新窗口都从头开始。原文把它比作一支轮班施工的工程队:每一班工人上岗时,都对上一班干了什么一无所知。

Claude Agent SDK 自带 compaction 这类上下文管理能力,理论上可以让 agent 一直干下去。但光靠压缩不够。原文说,即便用 Opus 4.5 在 SDK 里循环,只丢一句”给我建一个 claude.ai 的克隆”,它也做不出一个能上生产级的 web app。

两个典型的翻车方式

  1. 想一口气做完整个应用。结果实现做到一半,上下文就耗尽了,留下一个半成品,而且没有任何文档。下一个 session 只能靠猜上一班干了什么,把大量时间花在”重新让基础应用跑起来”上。compaction 也不总能把这个交接传清楚。
  2. 后期误判完工。项目做得差不多了,新一班的 agent 看到”看起来挺完整”,就宣布大功告成——其实还有一堆特征没做。

这两个坑指向同一条设计原则:把项目拆小,让每个 session 只推进一点点,并且每次都留下一个”干净状态”——干净状态的标准,是这份代码看起来像能直接合进主干的:没有重大 bug、有条理、有文档。

拆成两段:initializer 与 coding agent

方案是两段式。第一个 session 交给 initializer agent,用一段专门的 prompt,把初始环境搭好:一个 init.sh 脚本、一个 claude-progress.txt(记录 agent 干过什么)、以及一个初始的 git commit。之后每个 session 交给 coding agent,只做增量推进,并留下结构化的更新。

关键,是要让 agent 在一个全新的窗口里,能快速读懂”现在是什么状态”。它靠两样东西:claude-progress.txt 和 git history——灵感就来自真实工程师每天上班做的事。

这里有个常被误读的点:所谓”两个 agent”,其实只是初始 prompt 不同,system prompt、工具集、harness 完全一样(原文脚注专门讲了这点)。别把它想成两套系统。

环境管理,三块拼图

特征清单(feature list)。让 initializer 把用户最初那句 prompt,展开成一份完整的特征需求文件——claude.ai 克隆这个例子里有 200 多个特征。这份清单一开始把所有条目标成 failing,等于给后续每一个 agent 一份”完整功能长什么样”的底稿。规则很硬:只允许改 passes 字段,并用很重的措辞警告”删改测试是不可接受的”。原文特意选了 JSON 而不是 Markdown,因为模型更不容易乱改、覆盖 JSON。

增量推进。每个 session 只做一个特征。做完要留下干净状态:把进度提交进 git(写描述性的 commit message),同时在 progress 文件里写总结。这样一来,坏的改动可以用 git revert 回滚,恢复到一个可用状态;二来省掉了猜”上一班干了啥”的时间。

测试。这是最容易漏的一环。不给明确提示时,Claude 会跑单元测试、curl 一下 URL,就以为通过了,但它识别不出”端到端其实没通”。明确要求它用浏览器自动化工具、像真人用户一样端到端验证之后,表现大幅提升——Puppeteer MCP 截下来的图,能发现光看代码看不出来的 bug。

需要澄清一个容易混的点:这里的”测试”不是项目收尾时做一次验收,而是贯穿每一班、每个特征的动作。它落在两处——每做完一个特征,agent 要自己端到端验证过才敢把清单里那条标成 passing(这正是防上面第二个翻车方式);每个新 session 开工,先跑 init.sh 里那条基础端到端测试,确认应用没被上一班搞坏,再动手。原文说的”最后一个失败模式”,指的是清单上最后一个容易漏的坑,不是时间上最后的测试。

测试也不是没有遗留问题——视觉和浏览器自动化本身有局限。比如 Puppeteer 看不到浏览器原生的 alert 弹窗,所以依赖弹窗的功能更容易带 bug。

每班开工前先做的几件事

每个 coding agent 开工,先做几件基本功:pwd 看工作目录;读 git log 和 progress 文件;读特征清单,挑出优先级最高、还没完成的那个特征。initializer 还应该写好 init.sh,让它能起开发服务器、并跑一遍基础的端到端测试。agent 每次开工先跑它,确认应用没被上一班搞坏,再动手改。以 claude.ai 克隆为例,这条基础测试就是:用 Puppeteer 起一个新对话、发一条消息、收到回复。

失败模式对照表

原文明列了四条”失败模式 → 解法”:

  1. 过早宣布整个项目完工 → initializer 生成特征清单;coding agent 开工先读清单,挑一个特征开工。
  2. 留下带 bug、没文档的环境 → 建初始 git 仓库 + progress 文件;开工读 progress 和 git log、跑基础测试;收工写 commit 和 progress 更新。
  3. 过早把特征标为完成 → 特征清单 + self-verify,仔细测过才标 passing。
  4. 得花时间搞清楚怎么把应用跑起来 → 写 init.sh;开工先读它。

还没答的问题

原文也留了开放项:到底该做一个”通用的单一 coding agent”,还是搞多 agent 架构,作者还没定;测试 agent、QA agent、清理 agent 这类专用 agent,可能在各自子任务上更强。另外,这套方案是针对全栈 web 开发优化的,未来想推广到科研、金融建模等其他长程任务。


串起来:同一枚硬币的两面

两篇讲的其实是同一件事的两面。

前篇把上下文当稀缺预算,在两个口子上节流——静态的工具定义、动态的中间结果——并把重数据推出上下文之外:沙箱里先处理、文件系统落盘、跑通的代码固化成 Skills。后篇则把状态推出上下文之外:特征清单、git history、progress 文件、能跑起来的 init.sh,让失忆的下一班能接手。

共同点很清楚:把”记忆”和”状态”从模型脑子里挪到外部可读可写、可回滚的载体上,模型只负责增量推进。清单和 transcript 都不是目的,真正要守住的是”状态可交接”。

这也是那条演进线的落点:简单工具调用撑不起长程任务,沙箱、权限控制、上下文预算、跨 session 交接,都得由 harness 来兜。LLM + 工具 正在变成 LLM + harness。