当我们把一个 AI 代理丢进一个要忙上好几天的软件项目里,它真的能像一支靠谱的工程团队那样,一点点稳步推进吗?还是每次“上岗”都要从头摸索,甚至把上一个班次辛苦搭好的东西搞崩?这篇文章围绕这个问题,介绍 Anthropic 团队是如何为长时间运行的编码代理设计“工作制度”和工具链,让它们像轮班工程师一样接力协作、持续交付可用的功能,下面内容翻译自 Anthropic 博客文章《Effective harnesses for long-running agents》。


代理在跨越多个上下文窗口时依然面临诸多挑战。我们从人类工程师的日常工作中汲取灵感,为长时间运行的代理设计了一套更有效的“工作约束(harness)”。

随着 AI 代理能力不断增强,开发者越来越希望它们能承担复杂任务,这类任务往往需要持续工作数小时甚至数天。然而,要让代理在多个上下文窗口之间持续、稳定地推进进度,依然是一个悬而未决的问题。

长时间运行代理的核心难题在于:它不得不在离散的会话中工作,而每一个新会话开始时,对之前发生的事情一无所知。想象一个软件项目,一群工程师轮班上岗,但每个新来的工程师都对上一班做了什么毫无记忆。因为上下文窗口是有限的,而且大多数复杂项目根本不可能在单次窗口内完成,所以代理必须有一种机制,把一次次编码会话串联起来。

为此,我们提出了一个两部分组成的方案,让 Claude Agent SDK 能够在大量上下文窗口中依然高效工作:

  • 一个在第一次运行时负责初始化环境的 初始化代理(initializer agent)

  • 一个在后续每次会话中负责做增量改动、并为下一次会话留下清晰痕迹的 编码代理(coding agent)

相关代码示例可以在配套的 quickstart 仓库中找到:
https://github.com/anthropics/claude-quickstarts/tree/main/autonomous-coding

长时间运行代理的问题

Claude Agent SDK 是一个功能强大的通用代理框架,擅长编码,也支持其他需要模型使用工具来收集上下文、规划和执行的任务。它具备诸如上下文压缩(compaction)这样的上下文管理能力,使代理可以在不耗尽上下文窗口的情况下完成任务。从理论上讲,在这样的设置下,代理应该可以在非常长的时间里持续产出有用的工作成果。

但仅靠压缩远远不够。即便是像 Opus 4.5 这样的前沿编码模型,在 Claude Agent SDK 上循环运行多个上下文窗口,如果只给它一个非常笼统的高层提示,比如“做一个 claude.ai 的克隆版”,依然很难真正构建出一款生产级的 Web 应用。

Claude 在实践中的失败,大致表现为两种模式。

  • 第一种失败模式,是试图“一口气做完所有事”——本质上就是想一蹴而就把整个应用实现出来。这往往会导致模型在实现过程中耗尽上下文,在功能只实现了一半、文档也没写清楚的状态下就被迫结束会话。下一次会话开始时,代理不得不靠猜来推断之前发生了什么,并花费大量精力先把应用重新弄到能正常跑起来的状态。即便有上下文压缩,这种问题仍然会出现,因为压缩并不总是能把完全清晰的指令传给下一次会话。

  • 第二种失败模式则更常出现在项目后期。当已经实现了一部分功能之后,后来的代理实例往往“看一眼”当前的项目状态,觉得“好像差不多了”,就直接宣布项目已经完成。

把问题拆开来看,可以分成两部分:

  • 首先,我们需要在一开始就搭建一个初始环境,为提示中提到的所有功能打好地基,让代理能够逐步、按功能地推进实现。

  • 其次,我们需要在每一次会话中都要求代理只做增量式改动,同时保证在本次会话结束时把环境收拾干净。这里的“干净状态”,指的是那种可以直接合并到主干分支的代码:不存在严重 bug,结构清晰、文档健全,一个新的开发者接手时,可以立刻着手开发新功能,而无需先清理各种历史遗留问题。

在内部实验中,我们使用了一个由两部分组成的解决方案:

  1. 初始化代理:第一次会话使用一个专门的提示,请模型搭建初始环境,包括:生成一个 init.sh 脚本、创建一个记录代理工作日志的 claude-progress.txt 文件、以及一个包含初始文件的 git 提交。

  2. 编码代理:之后的每一次会话,都要求模型在此基础上做增量改动,并留下结构化的进度更新。

关键的洞见在于:必须想办法让代理在一个全新的上下文窗口中,也能快速理解当前项目的状态。这一点是通过 claude-progress.txt 文件配合 git 历史记录来实现的。这些实践的灵感,来自我们对优秀软件工程师日常工作方式的观察。

环境管理

在更新后的 Claude 4 提示工程指南中,我们分享了一些多上下文窗口工作流的最佳实践,其中包括一种结构化的“外壳”,会在“第一次上下文窗口使用一个不同的提示”。这个“不同的提示”,要求初始化代理搭建好环境,为后续的编码代理提供它们有效工作所需的所有上下文。下面,我们会更深入地介绍这种环境中的几个关键组成部分。

功能列表

为了解决代理“一次性做完所有事”或过早认为项目已经完成的问题,我们会让初始化代理根据用户最初的提示,写出一份尽可能全面的功能需求文件。在 claude.ai 克隆项目的例子中,这份功能列表包含了 200 多个功能,例如:“用户可以打开一个新的对话框,输入问题,按下回车,并看到 AI 的回复”。这些功能在一开始全部标记为“失败(failing)”,这样后续的编码代理就能清楚地看到完整功能应该包含什么。

{
    "category": "functional",
    "description": "New chat button creates a fresh conversation",
    "steps": [
      "Navigate to main interface",
      "Click the 'New Chat' button",
      "Verify a new conversation is created",
      "Check that chat area shows welcome state",
      "Verify conversation appears in sidebar"
    ],
    "passes": false
  }

我们会要求编码代理修改这份文件时,只能调整 passes 字段的状态。同时,我们给出非常强烈的说明,比如:“不允许删除或随意修改测试,因为那会导致功能缺失或存在 bug 而不自知。” 经过一番实验,我们最终选择了使用 JSON 来存储这些测试条目,因为相较于 Markdown,模型更不容易不恰当地改写或覆盖 JSON 文件。

渐进式推进

在有了上述初始环境的支撑之后,下一轮编码代理就会被要求:一次只处理一个功能。事实证明,这种渐进式方法对于克服代理“想一次做太多事”的倾向至关重要。

在渐进式工作模式下,还有一点非常重要:每次代码修改之后,模型都必须把环境整理到一个干净的状态。在实验中,我们发现,要诱导这种行为的最佳方式,是让模型在完成工作后:

  • 用有描述性的提交信息把进度提交到 git;

  • 在进度文件中写出本次改动的总结。

这样,模型就可以利用 git 来回滚错误的代码改动,并恢复到一个可以正常工作的代码库状态。

这些做法也显著提高了效率,因为代理无需再去猜测之前发生了什么,也不用浪费时间反复修复最基础的运行问题。

测试

我们观察到的最后一种主要失败模式,是 Claude 倾向于在缺乏充分测试的情况下就把某个功能标记为“已完成”。如果没有明确要求,Claude 往往只会做一些代码改动,并做少量的单元测试或者在开发服务器上用 curl 等方式进行测试,但没有真正验证整个功能能否端到端正常工作。

在构建 Web 应用的场景下,当我们明确要求 Claude 使用浏览器自动化工具,并像真实用户那样做端到端测试时,它在功能验证上的表现整体是不错的。

为 Claude 提供此类测试工具极大提升了整体表现,因为代理可以主动发现并修复那些仅从代码层面不易识别的 bug。

当然,问题并未完全消失。例如,Claude 的视觉能力和浏览器自动化工具本身都有局限,这使得某些类型的 bug 很难被发现。举例来说,Claude 无法通过 Puppeteer MCP 看到浏览器原生的弹窗,这就导致依赖这些弹窗的功能更容易出现问题。

快速上手项目

在上述机制都就位的前提下,每一个编码代理在开始工作时,都会先按照一系列步骤来“摸清情况”,这些步骤看起来很基础,但实际上非常有用:

  1. 运行 pwd,确认当前工作目录。代理只能编辑这个目录中的文件。

  2. 阅读 git 日志和进度文件,了解最近做了哪些工作。

  3. 阅读功能列表文件,选择优先级最高且尚未完成的功能来实现。

这种做法可以在每次会话中帮 Claude 节省一些 token,因为它不需要再花时间“摸索如何运行应用”。同时,我们也会让初始化代理编写一个可以启动开发服务器的 init.sh 脚本,并在实现每个新功能之前,先运行一次基础的端到端测试。

以 claude.ai 克隆项目为例,这意味着代理每次都会先启动本地开发服务器,再通过 Puppeteer MCP 打开一个新对话、发送一条消息并接收回复。这样可以尽快发现应用是否被上一轮改动弄坏,并立刻修复已有的 bug。如果代理在没有做这一步检查的情况下就开始实现新功能,很可能只会让问题变得更糟。

在这一整套流程下,一次典型的会话会从下面这样的助手消息开始:

[Assistant] 我先搞清楚当前项目的状态。
[Tool Use] <bash - pwd>
[Tool Use] <read - claude-progress.txt>
[Tool Use] <read - feature_list.json>
[Assistant] 让我先看一下 git 日志,了解最近的工作。
[Tool Use] <bash - git log --oneline -20>
[Assistant] 接下来我检查一下是否有 init.sh 脚本可以用来重启服务。
<启动开发服务器>
[Assistant] 很好!现在我会打开应用,确认核心功能是否仍然正常工作。
<测试基础功能>
[Assistant] 根据刚才的验证测试,我可以看到核心功能运行良好:聊天核心流程、主题切换、会话加载以及错误处理都工作正常。接下来我会更系统地阅读 tests.json 文件,确定下一步应该实现哪些功能。
<开始实现新的功能>

代理的失败模式与解决方案

下表总结了长时间运行的 AI 代理中,四种常见的失败模式以及相应的解决思路:

问题初始化代理的行为编码代理的行为
Claude 过早宣布整个项目已经完成。建立一份功能列表文件:基于输入规格,生成一份结构化 JSON,列出端到端功能描述。在每次会话开始时读取功能列表文件,只选择一个具体功能开始实现。
Claude 把环境留在一个有 bug 或缺乏文档的状态。初始化一个 git 仓库并写入进度说明文件。每次会话开始时,阅读进度说明文件和 git 提交日志,并在开发服务器上跑一次基础测试,捕捉未记录的 bug;会话结束时提交一次 git 记录并更新进度说明。
Claude 过早把功能标记为完成。建立一份功能列表文件。对所有功能做自我验证,只有在经过充分测试之后,才把功能标记为“通过(passing)”。
Claude 需要花时间摸清楚如何运行应用。编写一个可以启动开发服务器的 init.sh 脚本。每次会话开始时先阅读 init.sh

未来工作

本研究展示了一种可行的长时间运行代理“外壳”方案,使模型能够在跨越多个上下文窗口时持续做出渐进式的、有条不紊的改动。但仍然存在许多开放问题。

最突出的一个问题是:究竟是单一的通用编码代理在各种场景下表现更好,还是通过多代理架构——比如专门的测试代理、质量保障代理或代码清理代理——在软件开发生命周期的不同阶段分别发力,会带来更好的整体效果?从直觉上看,后者似乎是有潜力的。

此外,本文所展示的示例主要面向全栈 Web 应用开发。未来的一个方向,是把这些经验推广到其他领域。很可能,这些方法中的一部分或全部都可以迁移到其他长时间运行的代理任务中,比如科学研究、金融建模等。

本文翻译自 Anthropic 博客文章《Effective harnesses for long-running agents》,作者为 Justin Young,原文及代码示例可参考 GitHub 仓库:https://github.com/anthropics/claude-quickstarts/tree/main/autonomous-coding。