Contents

AI Native 开发环境:让 Agent 完成开发、发布与排错

让 Agent 修改代码,通常很快就能得到结果。但让它把一个需求做到上线,过程会长得多:服务在哪里创建,接口怎样接入,代码怎样发布,测试请求会打到哪个环境,失败以后又该去哪里找原因。

这些步骤过去常常依靠开发者对团队系统的熟悉程度。Agent 接手以后,原来藏在操作习惯里的知识,需要变成它能够找到的说明、能够调用的工具,以及能够判断的执行结果。本文以我们的后端开发环境为例,介绍这条链路如何组织。

1. 从一次服务开发开始

假设需要增加一项异步处理能力:客户端提交任务,服务返回任务标识;任务在后台执行,客户端随后查询状态和结果。这里用简化的任务接口贯穿全文,便于说明开发、测试和排错之间的关系。

如果只看业务代码,这个需求可以拆成提交、执行和查询几个部分。但要交付它,Agent 还需要回答一串问题:已有服务能否承接,是否需要新的存储,如何部署一个可调试版本,以及怎样确认后台任务确实完成。

这些问题对应着不同的系统。代码仓库保存实现和接口定义,流水线生成可部署的制品,研发平台维护服务与发布记录,运行环境提供实际实例,日志和数据库则保存执行过程与业务状态。

Agent 从代码仓库进入流水线和运行环境,再通过接口、日志和业务状态判断结果
图 1:一次开发任务涉及的系统与反馈路径

图中的关键是反馈路径。Agent 发起测试后,应当拿到实际响应;请求失败后,应当能沿着这次请求找到日志与状态。否则它只能不断修改代码,却无法判断改动是否解决了问题。

这里讨论的 AI Native 开发环境,就是围绕这条工作链路组织开发设施。下面从服务创建开始,逐步补齐其中的环节。

2. 新服务怎样进入开发环境

2.1. 先找到业务归属

Agent 接到需求后,直接生成一个新服务很容易。更早需要确定的是:这项能力是否已经有负责的服务。

例如,任务状态已经由任务服务维护,查询接口也应当从这里读取。如果为了增加一个接口,又在调用方建立一份任务状态,两个服务就都能修改同一件事情。后续遇到重试、取消和状态同步时,复杂度会迅速增加。

我们在仓库中维护能力归属的说明,把业务职责、接口定义和客户端入口关联起来。Agent 先据此找到候选服务,再读取实际接口和实现。同步调用使用已有的生成客户端,异步协作沿用已有的事件协议。只有在状态归属、故障隔离或独立扩展上确实出现新边界时,才创建服务。

这份说明更接近索引。它帮助 Agent 找到代码,具体接口仍以当前定义为准。业务演进以后,只更新一篇概括性的文档而不核对实现,也会把 Agent 引向错误的位置。

2.2. 模板需要包含哪些内容

确定要创建服务后,下一个问题是让它符合整个仓库的运行约定。

我们使用统一的 Kitex 服务模板。Kitex 是内部 RPC 框架,服务通过接口描述文件定义请求、响应和方法,再生成服务端与客户端代码。模板最初只包含健康检查,业务逻辑按需增加。

模板同时放入配置加载、服务发现、请求上下文传递、日志、指标和优雅退出等基础能力。这样,后续代码调用其他服务时,环境标记和追踪信息能够继续向下传;实例停止时,也能按统一方式结束正在处理的请求。

新服务分别完成代码侧接入与平台登记,发布时由服务名和构建绑定关联
图 2:代码生成、平台登记和首次发布之间的关系

服务目录生成以后,还需要接入仓库的构建工作区、代码生成、监控清单和模块流水线。这些内容不都在服务自己的目录里,创建指引必须把它们列出来。否则很容易得到一个本地可以编译、流水线却找不到的服务。

2.3. 代码存在以后,还要登记运行目标

代码模板解决了服务怎样运行,研发平台还需要知道它是谁、允许部署到哪里。

平台登记服务名称、用途、构建绑定和环境声明。构建绑定把逻辑服务与对应的代码位置、流水线和制品仓库关联起来;环境声明则描述这个服务有哪些可发布目标。二者分别回答“构建什么”和“发布到哪里”。

因此,创建代码、登记服务、创建运行实例是三个不同的动作。服务登记完成后可以出现在目录中,此时仍可能没有任何实例。流水线完成构建并提交首次发布后,平台才按受信任的运行模板创建相应资源。

数据库、缓存和公网入口也需要各自的申请过程。创建一个空服务,不应当顺带开出它暂时用不到的资源。对 Agent 来说,这个区分还会直接影响后续判断:目录中能够查到服务,只证明登记存在;能否调用,需要等实例与访问路径都准备好。

3. 缩短每一轮构建与发布

3.1. 先把流水线组织成明确的依赖关系

服务写好后,Agent 会进入反复修改和验证的过程。每一轮都要等待构建与部署,等待时间会累积成整个需求的交付时间。

我们的流水线先根据变更确定受影响的模块,再生成相应的构建和部署任务。业务目录映射到它所属的服务;共享库、代码生成与公共契约的变化,通过依赖声明触发使用它们的模块。这里需要维护真实的依赖关系,只看文件所在目录会漏掉下游影响。

变更分析生成受影响模块的检查与构建任务,通过后登记制品并请求平台部署
图 3:按受影响模块生成任务,按依赖关系推进发布;图中两类服务仅为示意

各模块可以独立构建。一个服务的部署等待它所需的检查和制品;如果多个服务必须配套发布,则明确声明部署顺序。GitLab 的 needs 可以表达任务依赖,使已经满足条件的任务提前开始,避免等待没有关系的任务。GitLab 文档介绍了这种组织方式。

我们还把动态流水线展开为直接执行的任务,减少只负责继续创建子流水线的中间层。它不会缩短编译本身,但可以减少调度和等待,也更容易从结果中找到真正失败的服务。

3.2. 缓存究竟复用了什么

只减少构建范围还不够。如果每次构建都重新下载依赖、重新编译没有变化的包,仍然会浪费大量时间。

这里需要区分三类东西。第一类是项目源代码和本次输出,必须对应本次构建输入;第二类是依赖下载与编译缓存,可以按内容复用;第三类是已经生成的不可变制品,可以在满足发布条件后进入其他环境。

不同构建复用同一 Runner 上的 BuildKit 缓存,远端保存可导出的构建层,部署复用不可变制品
图 4:构建缓存与制品复用发生在不同位置

我们的 Runner,也就是执行流水线任务的机器,会复用持久的 BuildKit 构建器。Go 模块、编译缓存和 Node 依赖下载缓存挂载在其中,下一次任务能够继续使用。Dockerfile 中依赖声明与经常变化的业务源码分开复制,可以减少缓存失效范围。Docker 的缓存说明解释了缓存层与缓存挂载的区别。

本机缓存和远端缓存也有边界。我们将可导出的构建层保存到镜像仓库,便于更换 Runner 后恢复;缓存挂载中的包下载和编译数据,则仍依赖对应的构建器。换一台机器以后,不能因为远端存在构建缓存,就假定所有本机缓存都已恢复。

工作区同样按用途拆开。构建任务需要业务源码,部署与汇总任务往往只需要少量脚本和声明,可以使用稀疏检出。旧源码、旧输出和本次制品必须正确清理;不能为了保留缓存,顺带把上一次构建结果当成本次输入。

3.3. 用一次实际构建定位最长路径

做过这些优化以后,流水线仍然可能慢。2026 年 9 月的一次发布,从流水线创建到最后一个任务完成,约为 6 分 50 秒。沿着实际完成路径拆开,时间分布如下。

一次发布约 410 秒,其中构建 317.1 秒、部署 57.7 秒;构建内检查路径为 257.4 秒
图 5:一次流水线的实际耗时;上方时间条按比例绘制,下方为构建内的并行关系
环节耗时
创建至开始构建22.8 秒
镜像构建任务317.1 秒
构建后的排队1.0 秒
部署任务57.7 秒
汇总与收尾11.2 秒

其中,构建任务约占总时间的 77%。进一步查看构建日志,最慢的是 257.4 秒的检查路径:代码规范检查约 30 秒,独立类型检查约 51 秒,数据库测试准备与测试执行约 176 秒。

这次依赖安装已经命中缓存,源码准备也只有约 3 秒。继续压缩依赖下载,无法解决主要等待。测试配置为了避免共享数据库中的任务被其他测试消费,关闭了文件间并行;这个设置同时影响到了不访问数据库的纯逻辑测试。测试框架的阶段统计又显示,模块导入占用了大量被追踪的执行时间。

由此可以确定下一步的优化方向:把纯逻辑测试与共享数据库测试分开,核对模块加载开销;需要并行运行数据库测试时,先解决数据和后台 worker 的隔离。这些是这次诊断提出的后续工作,不能把预期节省的时间写成已经取得的结果。Vitest 性能文档也建议先分析各阶段耗时,再选择优化方式。

还有一个容易算错的地方:当时的编译与进程验收已经和检查并行执行,且更早完成。将所有任务的耗时相加,会重复计算并行部分。决定等待时间的是依赖链中最后完成的路径,优化一个提前完成的任务,不一定能让整条流水线更早结束。

3.4. 后续环境复用已经验证的制品

测试环境验证完成后,同一份不可变制品继续进入预发布和生产环境。这里的制品,是已经构建并登记的镜像等部署产物;摘要用来确认内容没有变化。

在上述案例中,后续两个环境的构建任务分别用了 5.9 秒和 6.6 秒,日志确认它们复用了已有镜像。这两个数字只包含对应的构建任务,部署、验证和观察仍然需要时间。

复用带来的另一个作用是版本一致:已经验证的内容与随后发布的内容可以对应起来。如果每到一个环境都重新构建,还需要解释新的产物与上一环境验证的产物是否相同。配置也是部署输入,我们的标准服务将配置随镜像交付,修改配置同样需要重新构建并发布。

至此,Agent 能够较快地获得一个可运行版本。接下来还要解决一个问题:它发出的测试请求,是否真的在验证这个版本。

4. 给并行开发提供可确定的测试环境

多个 Agent 共用一个开发环境时,实例更新会相互覆盖。一个 Agent 刚刚发布,另一个 Agent 又替换了下游服务;前者随后看到的响应,已经对应另一组代码。

我们为开发中的变更提供个人环境。这里的“个人”指独立的运行目标和环境身份,同一开发者的不同任务也需要按实际环境区分。测试请求携带环境信息,入口、RPC 客户端和异步消息继续传递这个信息,使后续调用进入相应的服务实例。

测试请求经共享入口进入个人任务服务和个人 worker,环境信息沿 RPC 和消息传递;数据仍需单独隔离
图 6:运行路由的隔离与测试数据的隔离

对异步任务来说,入口请求返回以后工作还没有完成。环境信息需要进入消息,并在消费时恢复;消费者和队列的组织也要与路由约定一致。否则请求虽然进入了个人服务,任务仍可能被共享环境的 worker 执行。

并非每个依赖都需要复制一套。允许复用的依赖可以按明确规则访问共享环境,但本次正在验证的服务应当确认有对应实例,不能在目标缺失时无声地切到另一份实现。

运行环境隔离也不自动带来数据隔离。两个实例如果连接同一个数据库,使用同一批任务记录,依然可能相互影响。测试需要专用账号或数据范围;后台扫描与消费任务时,也要限制在相应范围内。涉及全表扫描、共享队列等场景,可能需要独立的库、队列或消费者配置。

因此,一次测试至少要明确两件事:请求经过哪些版本,以及它会读写哪一组数据。只在地址中换一个环境参数,还不足以回答这两个问题。

5. 每轮上线要验证到哪一步

5.1. 从接口定义生成调用入口

环境准备好后,Agent 需要构造请求。如果每次都临时拼路径、参数和鉴权头,测试本身也容易出错。

我们把接口定义作为调用代码的来源。网关接口生成 TypeScript 客户端,内部 RPC 使用对应的调用工具和测试用例。Agent 可以从修改过的方法找到已有测试,沿用其参数、身份和环境选择方式。

这解决了“怎样正确地调用”,业务是否正确仍要由测试说明。生成的契约探针能够检查接口定义和请求结构,一次预期的参数拒绝也能验证边界行为;它们各自证明的范围都有限。

5.2. 为改动选择有业务含义的断言

回到任务示例。提交接口返回成功,可能仅表示任务已受理;如果要验证整个功能,还需要查询任务状态,并检查最终结果。

测试使用专用身份提交任务,按返回标识查询,在限定时间内确认完成并检查结果,最后清理测试数据
图 7:异步任务的业务测试;接口与状态为简化示意

一个有意义的用例需要把预期写清楚:提交结果属于当前测试账号,查询返回同一个任务,任务在限定时间内进入预期终态,输出满足业务条件。失败和取消是不同结果,等待超时也需要单独报告,不能一直轮询下去。

如果这次修改的是任务权限,就补充另一个账号不能读取该任务的验证;如果修改的是重试逻辑,就验证相同请求不会产生重复任务。测试范围随着实际变化扩大。普通处理逻辑修复,可以先运行受影响的用例;只有接口定义变化时,才需要重新生成对应客户端和契约检查。

我们不会要求每次改几行业务代码,都把全部服务、所有环境和所有测试重新跑一遍。发布前的必要检查必须保留,额外验证则应当针对本次可能破坏的行为。这样既能较快获得反馈,也能知道每项检查为什么存在。

测试数据本身也是测试设计的一部分。创建的数据需要能够识别并清理,清理失败要留有记录。涉及扣费、发送通知或删除业务对象的接口,应当有专门的测试条件与授权,不能直接把生产用户当作测试样本。

5.3. 部署完成与线上验证分开记录

本地测试和开发环境测试通过以后,还需要确认生产环境运行的是预期内容。

平台发布检查会核对发布目标、声明的镜像和实例就绪情况。我们的 release verify 还会检查这条发布是否仍是当前发布,避免拿已经被替换的旧记录证明当前状态。它的覆盖范围不包括业务接口、日志、监控,也不等于逐个容器的实际镜像摘要核验。

发布受理、平台执行、部署核对和业务验证各自产生不同的结果,客户端超时后查询原操作
图 8:发布状态与业务验收分别推进,超时后先确认原操作

因此,一轮上线至少要把部署结果和一次针对改动的真实请求接起来。请求需要使用正确的入口、身份和环境,检查业务字段与结果;发现异常时,再扩大到日志、监控或实际容器版本的核对。对异步任务,线上验证同样要继续观察任务终态,提交成功还不够。

结果中可以分别记录“部署已核对”“提交接口通过”“任务执行失败”。这些状态放在一起,Agent 才知道下一步是检查发布,还是进入业务排错。缺失的检查保留为未验证,不应当被一个总的成功状态覆盖。

还有一种情况是客户端等待超时。发布可能仍在平台执行,重复创建新的发布会使判断更复杂。我们保留原操作标识,先查询原发布或按同一意图重试;平台根据幂等记录恢复结果。命令停止等待,也不意味着服务端任务已经取消。

6. 验证失败后怎样找到原因

6.1. 先保留这次失败的上下文

假设提交接口正常,但任务迟迟没有完成。Agent 如果只得到“测试失败”,很难确定应该从哪里开始。

测试输出需要保留环境、请求时间、接口、任务标识、预期结果和实际结果。能够取得请求追踪标识时,也应当一并保留。这些字段把一次失败限制到具体调用与时间范围,后续查询才能有明确目标。

失败请求提供时间、环境和任务信息,日志解释执行过程,数据库确认业务状态,按发现的问题继续查队列或外部结果
图 9:从一次失败请求逐步缩小排查范围

同步接口报错时,先沿请求查入口和被调用服务的日志。任务停留在某个状态时,先读取负责该状态的服务数据库,再根据记录中的执行阶段查相应日志、队列或租约。错误率上升、延迟变高这类整体问题,则更适合先看指标趋势,再选取具体请求分析。

这些入口不是每次都必须查一遍。它们分别回答不同的问题,下一次查询由上一份结果决定。

6.2. 日志、数据库和指标各自说明什么

日志记录服务在某个时刻做了什么。通过任务标识,可以看到入口是否接收、消息是否发送、worker 是否开始执行,以及下游调用在哪里失败。跨服务使用一致的追踪字段,比单纯增加日志数量更有用。

数据库保存业务状态。例如任务记录已经存在,但仍处于等待状态,排查就应当继续进入分发与消费;任务已经标记完成,客户端却一直得到旧状态,则需要核对查询条件、账号范围、缓存或读取路径。

查库应当使用状态所属服务的数据库和明确条件。下面是说明查询范围的示意 SQL,表名与字段并非项目实际结构:

SELECT status, updated_at, error_code
FROM task_records
WHERE task_id = :task_id
  AND owner_id = :test_user_id
LIMIT 1;

参数来自本次测试结果。只读取排查需要的列,限制结果数量,并设置查询超时。我们的生产查询入口会设置只读事务与超时,但包装脚本的限制和数据库账号本身的权限仍是两件事;如果底层账号拥有写权限,只读规则还需要在凭据与服务端授权层继续收紧。

指标更适合判断影响范围:错误是否集中在某个接口,延迟是否从上线后开始上升,任务积压是否持续增加。没有采样、采集失败和数值为零要分别展示,Agent 才不会把“没有读到数据”解释成“没有问题”。

6.3. 修复以后,回到原来的失败用例

假设日志和状态共同表明,任务停在了消费者读取之后、业务执行之前。Agent 修改相应代码后,应当首先重跑原来的任务用例,确认同样的输入能够进入预期终态,再补充与修复直接相关的边界验证。

保留原用例的输入条件、环境和断言,可以让前后结果形成可比较的记录。若每次排错都换一个账号、另一套环境和另一种请求,即使后来成功,也很难说明原问题已经解决。

7. 权限怎样贯穿整个过程

7.1. 把员工身份带到操作入口

前面的步骤要连起来,Agent 必须能够读取必要信息、调用测试接口和发起已经授权的发布。但如果每个开发环境都直接持有集群、云资源和生产数据库的管理凭据,权限也会随工具一起扩散。

我们的做法是让员工通过统一身份登录,Agent 使用相应的员工工具入口。发布命令提交服务、环境和已经登记的制品,由研发平台解析实际运行目标并执行。CI 负责检查、构建、登记与请求发布,运行资源的选择和修改集中在平台。

Agent 通过员工身份调用平台,CI 使用受限构建身份;平台校验操作并访问运行资源,数据库查询走单独受控入口
图 10:身份、操作授权与基础设施凭据的分工

这样,Agent 不需要根据集群名称猜测部署目标,也不需要为了发布一个服务取得任意集群操作权限。平台保存服务与目标的对应关系,并在服务端检查调用者、制品来源、环境门禁和操作状态。

真实操作人也要随请求进入审计记录。流水线机器人可以执行操作,但记录应当能够回答是谁发起、发布了什么、进入哪个环境,以及最后得到什么结果。代码作者、最近一次修改者和本次发布发起者不能混为一谈。

7.2. 不同操作采用不同边界

权限如果只分成“普通用户”和“管理员”,往往会遇到两个问题:日常工作频繁等待管理员,或者为了让工作继续,把过多权限一次性开放。

我们按操作拆开这件事。在平台的权限设计中,已验证员工可以执行符合门禁的发布和回退;服务资料维护、负责人管理、资源纳管和付费资源审批则有各自的限制。负责人用于表达责任关系,不必自动成为所有日常发布的唯一操作者。

操作需要明确的范围
查看服务、日志与指标可见服务与环境、查询时间、返回数量和字段。
查询业务数据数据所属服务、可读表与字段、账号范围、只读与超时限制。
发布和回退目标服务、环境、登记制品、发布前置条件与本次授权。
开通付费资源可申请模板、价格范围、审批人和实际开通结果。
管理权限与资源归属单独的管理权限及审计记录。

付费资源仍然经过申请、审批和开通。报价成功只得到成本信息,申请成功只代表申请已经受理,后续步骤各自留下状态。Agent 可以推进它有权完成的部分,并明确指出下一步需要什么条件。

服务端必须执行这些检查。页面隐藏按钮、Skill 写一句“不要越权”,都不能替代接口授权。数据库也一样:把查询工具描述为只读,并不能自动降低其底层账号权限。

平台能力和当前任务授权还需要分开。员工身份具备某项操作权限,只代表系统允许这个身份发起操作;Agent 在当前任务中是否应当执行,还取决于用户授权的服务、环境与动作。只读诊断任务中发现了解决办法,也不能据此直接修改生产数据。

8. 让 Agent 找得到、接得上这些能力

当工具逐渐齐全,新的问题是 Agent 怎样知道什么时候使用哪一个。把全部说明塞进每次对话,会让上下文很快被大量细节占满;只提供一组工具名称,又不足以理解它们的前置条件和结果含义。

我们把稳定的仓库约束放在 AGENTS.md,把具体工作拆成服务创建、接口测试、发布和线上排查等 Skill。每份 Skill 提供入口和判断顺序,再指向更详细的说明与现有脚本。例如,服务创建指引先要求确认业务归属;接口测试指引根据网关或内部 RPC 选择测试入口;排查指引根据错误表现选择日志、状态或指标。

实际调用由 CLI、测试工具和受控查询入口完成。Agent 可以在终端运行平台 CLI,由 CLI 通过 HTTP 接口访问研发平台。页面与 CLI 使用同一套服务端权限和发布记录,人打开平台页面时,也能继续查看 Agent 发起的那次操作。

例如,服务概览返回当前环境、可用制品与运行信息;发布命令返回发布记录;日志查询接收服务、环境和时间范围,返回限定数量的日志。工具用结构化结果区分成功、权限不足、前置条件不满足、数据不可用与结果未知。长任务返回可以继续查询的操作记录,临时断开后仍能确认进展。

服务、环境、制品、发布记录和本次测试任务之间的对应关系,也需要随开发任务保存。恢复工作时,Agent 先查询这些对象的当前状态,再决定继续等待、执行测试还是进入排错。这样,一次任务可以跨越多轮对话,而不必重新猜测上一次究竟发布到了哪里。

在这层设计里,说明文件负责告诉 Agent 怎样工作,工具负责执行动作,平台负责校验并保存执行状态。仅仅增加一段 Prompt,无法补出缺失的发布接口或数据库访问入口;仅仅增加一个接口,也不能让 Agent 自动理解什么结果才算完成。

这套环境把一次开发拆成了可以持续推进的步骤:创建符合约定的服务,用可复用的构建过程生成制品,在确定的环境中运行测试,发布后核对真实请求,失败时沿请求和状态继续排查。

接下来仍然需要完善的是业务验收。发布工具能够统一检查版本和实例,不同业务的正确结果却各不相同:任务需要执行完成,文件需要能够读取,通知需要实际送达。把这些条件写成可重复运行、能够管理测试数据的用例,Agent 才能逐步减少对人工补充判断的依赖。

与本文相关的存储和运行环境问题,另见Sandbox 存储性能优化与Sandbox 环境持久化;服务拆分和环境选路的变化,见后端架构演进。