从技术债转码到通用 Coding Agent:一套可落地的架构与生产化路径

10 分钟阅读
·

本文是「Agent 开发实践与思考」系列第 10 篇。系列目录:

从技术债转码开始,而不是从通用 Agent 开始

2024 年重新做研发助手时,我没有先选开放式需求开发,而是先选了技术债治理。原因很现实:旧代码在仓库里,目标框架和组件库有明确边界,团队也能为一部分任务提供迁移规则。相比「给我做一个新功能」,这类任务的输入更可观察,因而更适合检验模型与 Agent 是否能进入工程流程。

当时主要尝试了两类工作。第一类是 Vue 业务页面和共享资产迁移到 React;第二类是把 Native 页面改造成动态化页面。两者的目标技术栈不同,验证条件也不同,但最后暴露的是同一类问题:模型并不缺少把模板改成 JSX、把 UI 结构改成目标 DSL 的能力;真正难的是识别历史系统的行为边界,并证明迁移没有把这些边界弄丢。

这也是本文的叙事起点。先拆专业领域的转码 Agent 为什么需要特殊架构,再讨论这些能力如何抽象为通用 Coding Agent。若直接从「模型、文件系统、shell」讲通用架构,读者很难理解为什么一个看似简单的代码编辑任务,最后会需要状态机、补丁协议、验证契约、权限系统和生产治理。

1500 行与 4000 行:问题不在上下文长度

在 Native 页面动态化的尝试中,我采用过 planning-execute 的方式:先让模型分析原页面并形成计划,再按计划生成目标代码、执行验证。对于大约 1500 行的 Activity,这种方式可以完成主要功能迁移,页面结构、常规交互和常用组件映射基本能跑通,UI 样式仍需要人工调整。

这个结果很容易让人得出一个乐观结论:只要上下文窗口更长、模型更强,就能自然迁移更大的页面。继续尝试接近 4000 行的页面后,这个结论站不住了。功能开始遗漏;一些组件虽然生成了 className,却没有对应样式资产;一次通过率下降,人工反复介入;通过 MCP 检索组件库时,也会召回相似但不正确的组件或过期 API。

这些问题同时出现,说明瓶颈不是「模型少看了几百行」。一个任务单元里混进了太多不同性质的责任:UI 翻译、状态迁移、样式资产、组件库约束、页面流程、埋点、权限和运行时环境。模型即使记住更多代码,也不会自动知道哪些行为是不可丢失的,哪些只是历史绕过,哪些需要先问人再动。

同样的情况出现在 Vue 转 React。v-model、插槽和 computed 的语法映射并不难;难的是旧组件可能在提交成功后关闭弹窗、刷新父列表、上报埋点、清空跨页缓存。把模板改成 JSX、把 $emit('success') 改成 onSuccess,不代表这些行为仍在。TypeScript 与构建都通过,也不代表迁移完成。

旧代码不是规格,转码不能只有两个结论

历史代码通常同时包含四种内容:正常业务逻辑、为兼容旧系统保留的行为、绕开历史问题的临时处理,以及真实缺陷。转码任务常被描述为「保持行为一致」,但这句话本身没有给出判定标准。

例如,一个 Vue 弹窗提交后刷新列表,可能是业务流程要求,也可能是旧状态管理造成重复请求后的补偿。一个页面写死币种或日期格式,可能是遗漏的国际化技术债,也可能是特定地区的合规展示。逐行搬运会复制技术债;擅自清理「看起来不合理」的代码又会改变业务。

因此,转码 Agent 不能只在「保留」和「改掉」之间选择,它必须支持第三种一等结果:证据不足,等待确认。这不是模型能力不足时的借口,而是工程输入本来不完整时唯一诚实的系统行为。后文的状态机、验证和人机协作,都是围绕这个结果建立的。

技术债治理与普通 Coding 任务的差异

普通 Coding Agent 的典型任务是修复一个 issue、补一个测试或实现一个局部需求。它的目标通常是以有限时间和风险完成当前变更。技术债治理的目标不同:它要在持续交付中替换运行时、组件契约和工程组织方式,不仅让新代码运行,还要让旧依赖以可证明的方式退出。

维度 普通需求或缺陷任务 Vue 转 React / Native 动态化转码
首要目标 完成当前功能或修复 在业务不回退下减少旧栈依赖
工作单元 issue、函数或功能切片 资产契约、调用方与依赖子图
输入 需求、报错、测试与现有实现 旧实现、目标框架、兼容要求与退出条件
最大风险 当前路径直接回归 语法完成但隐式行为丢失,双栈长期耦合
验收 需求满足、测试与评审通过 行为等价、平滑切换、旧依赖可证明清零
回退 revert 当前提交 路由、流量、适配层与必要的数据兼容回切
关键指标 成功率、返工率、人工介入 旧依赖净减少、适配层寿命、未验证契约数量

转码 Agent 不是普通 Agent 加几条「注意迁移」的提示词。它的专业性不来自模型会多少框架术语,而来自任务边界、验证方式和退出条件。下面先从一个专业 Agent 的第一版开始拆。

第一版:只读分析与迁移说明

我最早的做法,是把迁移规则不断塞进系统提示词:v-model 要改成受控组件、不要修改接口字段、保留 class、弹窗关闭后刷新列表、删除 Vue 依赖前确认没有引用。每次失败就增加一条。很快,提示词变得很长,却没有更稳定。

问题不在于这些规则是否正确,而在于它们没有说明作用对象、前置条件和验收方式。「删除 Vue 依赖」对已经替换完成的孤立组件成立,对仍被 Vue 页面使用的共享资产却不成立;「保留样式」也不等于原样复制依赖旧 DOM 结构的 CSS 选择器。

后来的改法是把一次迁移拆成两个阶段。第一阶段只读仓库,第二阶段才允许写业务代码。第一阶段的交付物不是一段自由分析,而是一份可被下一轮 Agent、测试和人工评审共同使用的迁移说明。它把当前任务的事实、假设、验证方式与未知项固定下来。

迁移说明的结构

迁移说明可以用 JSON、YAML 或数据库记录实现,关键不在格式,而在字段稳定、可检查。下面是一个简化的 Vue 组件迁移说明。

{
  "unitId": "legacy-order-table",
  "source": {
    "framework": "vue2",
    "paths": [
      "src/components/LegacyOrderTable.vue",
      "src/components/LegacyOrderTable.less"
    ]
  },
  "target": {
    "framework": "react18",
    "paths": ["src/components/OrderTable/index.tsx"]
  },
  "entryPoints": ["src/pages/orders/index.vue"],
  "callers": [
    "src/pages/orders/index.vue",
    "src/pages/refund/index.vue"
  ],
  "contract": {
    "props": ["data", "loading", "page", "pageSize"],
    "events": ["change", "row-click"],
    "slots": ["status", "operation"],
    "refMethods": ["reload", "reset"],
    "stateOwner": "caller"
  },
  "behaviors": [
    "分页变化后触发列表请求",
    "点击操作列打开详情抽屉",
    "提交成功后调用 reload",
    "无权限时隐藏批量操作"
  ],
  "dependencies": {
    "services": ["src/services/orders.ts"],
    "styles": ["src/styles/table.less"],
    "tracking": ["order_table_click"],
    "permissions": ["order.batch.update"]
  },
  "verification": [
    "pnpm typecheck",
    "pnpm test -- OrderTable",
    "e2e:order-list",
    "visual:order-list"
  ],
  "openQuestions": [
    "reload 是否总是重置到第一页"
  ]
}

这份说明不是为了把自然语言变得形式化,而是为了把不确定性放进系统。openQuestions 不为空时,编排器可以拒绝进入高风险写入阶段;verification 为空时,系统可以把任务降级为候选补丁;refMethods 存在时,不能只迁移组件本体,必须检查调用方;styles 存在时,样式不能被当作实现结束后顺手处理的附属物。

影响图:文件不是迁移单元

迁移说明的输入来自影响分析。它的目标不是列出「相关文件」,而是建立足以决定任务边界的影响图。图中的节点可以是文件、组件、路由、接口、样式文件、埋点事件和权限码;边也不应只有 import,还包括事件订阅、字符串路由、接口 URL、缓存 key、动态组件名与 ref 调用。

订单列表页面的迁移影响图:路由、页面容器、组件、服务、样式、埋点、ref 方法与权限的依赖关系

静态 import 可以通过 AST 或语言服务获得;组件名、事件名和接口字段可以通过精确搜索获得。动态路由、全局事件总线、运行时组件拼装和埋点消费方,往往需要调用样本、运行日志或浏览器网络记录补充。因此影响图不是一次扫描后的真相,而是一组带证据等级的假设。Agent 应记录每条边来自 AST、文本匹配、运行时抓包还是人工确认;没有证据来源的边,不应被伪装成确定事实。

共享资产先迁什么

共享资产的迁移常常决定了项目是否能收敛。一个 Vue 表格可能被十个页面使用,直接按目录翻译通常有两种坏结果:新组件只满足第一个页面的调用方式,或者为了兼容所有不确定场景,做出一个什么都转发的桥接层。前者把问题推给后续页面,后者把双栈耦合变成新的长期债务。

正确的起点是收集调用契约:每个调用方传了哪些 props、监听了哪些事件、是否通过 ref 调用内部方法、插槽是否依赖作用域数据、组件内部是否隐藏埋点、权限、缓存或默认请求。React 版本的第一目标不是「设计得更优雅」,而是覆盖已经被证据支持的稳定契约。少数特例可以进入 render prop、适配器或人工决策,但不能因为少数特例而让所有能力继续隐式存在。

语义先于框架 API

从 Vue 到 React 最容易演示的是 API 对照表:v-model 对应 valueonChange,插槽对应 children 或 render prop,watch 对应 useEffect。这种映射只在局部情况下成立。

watch 可能是在计算派生值、校正输入、发请求、建立订阅,或者补偿一个历史 bug。这些职责在 React 中不应该都写成 useEffect:派生值通常应在渲染过程中计算,用户动作应留在事件处理函数中,请求应进入统一的数据请求边界,外部订阅才是 effect 的典型用途。同样,若调用方依赖插槽作用域数据,render prop 更接近原有契约;若只是表格单元格配置,结构化列定义通常更容易验证。

专业转码 Agent 的价值不在于背诵框架映射,而在于先解释旧机制承担的职责,再选择目标机制。这个能力决定它是在迁移语法,还是在迁移系统行为。

从一次调用到工程状态机

有了迁移说明后,下一步不是直接执行,而是建立状态机管理跨文件、跨轮次和跨会话的变更。最小 ReAct 循环只能做到模型调用工具、读取结果、再决定下一次调用;它无法表达「此时禁止写入」「这次失败应该回到调查」「这个问题必须人工裁定」。对技术债治理,这些状态不是产品 UI,而是工具权限与任务恢复能力。

技术债转码 Agent 的任务状态机:从 Initialize 到 Deliver,含 NeedsDecision 与 Approval 分支

NeedsDecision 是关键状态。它表示系统没有足够证据继续,不应被视为失败,也不应由模型用更多自然语言填平。任何涉及接口语义、权限、数据迁移、未覆盖关键路径或多种合理架构方案的任务,都可能进入这个状态。

phase 是权限开关

每个 phase 对应不同工具集。Investigate 只能使用读取、搜索、依赖分析和基线检查;Plan 可以写迁移说明,但不能改业务代码;Implement 才能打开当前单元的有限写权限;Verify 应拒绝新的编辑,除非状态机显式退回 ImplementDeliver 只能生成报告、草稿 PR 或审批请求。

这种设计比在 prompt 中写「请先分析再修改」可靠得多,因为模型无法绕过一个未注册的写工具。下面是一个更完整的任务状态示例。

{
  "taskId": "migrate-legacy-order-table",
  "repo": "webapp",
  "baseRevision": "a1b2c3d",
  "phase": "implement",
  "riskLevel": "medium",
  "currentUnit": "LegacyOrderTable",
  "completedUnits": ["OrderStatusTag"],
  "allowedPaths": [
    "src/components/LegacyOrderTable",
    "src/components/OrderTable",
    "src/pages/orders"
  ],
  "evidence": [
    {
      "claim": "调用方使用 reload 方法刷新列表",
      "source": "src/pages/orders/index.vue:108-112",
      "kind": "code",
      "status": "supported"
    }
  ],
  "editBudget": {
    "maxFiles": 8,
    "maxChangedLines": 500,
    "maxToolCalls": 60
  },
  "verification": {
    "baseline": "passed",
    "typecheck": "pending",
    "componentTest": "pending",
    "visualDiff": "pending"
  },
  "openQuestions": []
}

任务状态不是给模型自由书写的长日志,而是编排器维护的权威记录。模型会话可以丢失、压缩或换模型;任务状态、工作区、diff 和验证记录不能丢失。预算也应由编排器执行,例如限制当前单元的修改文件数、修改行数、工具调用数、单命令时长与总任务时长。预算耗尽时保存已完成单元和现有证据,进入 needs_review,而不是为了完成任务无限循环。

失败分类决定下一步

一次测试失败并不总是代码错误,一次工具失败也不总是应该重试。把所有 stderr 原样交给模型,常见结果是模型继续修改代码,试图掩盖环境或基线问题。更合理的做法是让工具与编排器先完成分类。

错误类型 例子 系统动作
EDIT_CONFLICT 旧文本不唯一或文件版本变化 重新读取、重新定位,连续冲突后升级
SYNTAX_ERROR JSX 或类型语法错误 退回当前实现单元
CONTRACT_DRIFT 调用方类型或事件契约不匹配 回到影响分析与迁移说明
STYLE_REGRESSION class 映射丢失或视觉 diff 超阈值 纳入样式单元,重新实施
COMPONENT_API_MISS 组件库 API 不存在或版本不匹配 查询版本化知识,无法确认则升级
BASELINE_FAILURE 修改前测试已经失败 记录基线,不把失败归因给补丁
ENVIRONMENT_FAILURE 依赖下载、测试环境不可用 有限重试,超过阈值升级
POLICY_DENIED 路径、命令或网络权限被拒绝 停止并申请授权
UNVERIFIABLE 无测试且无法构建受控验证 输出风险,等待人工决策

错误码不是工程上的装饰,它让 Agent 有机会采用不同恢复策略。网络超时应幂等重试,路径越权应立即停止,契约漂移应回到调查。这几类失败的处理方式完全不同。

工具、补丁与验证:执行层的真实难点

一个 Coding Agent 的工具不需要无限增加,工具越多,选错工具、填错参数和处理错误返回的概率也越高。但工具必须能提供稳定的事实边界。基础工具通常包括目录与文件查找、文本与符号搜索、局部读文件、受限写入、运行命令、读取 git 状态和统一 diff;转码任务还需要抽取组件契约、查找调用方和样式依赖、读取组件库版本、采集 DOM/网络/截图等能力。

工具输出应是机器可消费的结构。搜索结果要有文件、行号与有限上下文;读取结果要有文件版本;命令结果要区分退出码、stdout、stderr、超时和资源耗尽;写入结果要返回新版本与统一 diff。如果工具只返回一段自然语言,模型很难根据失败类型稳定恢复。

大文件编辑:模型应返回编辑意图

假设一个文件有 3000 行,模型只读取了其中 80 行,需要修正一个 effect。让模型返回完整新文件会带来四个风险:输出成本高,截断和无关格式变化更多;模型可能改写没有读过的区域;全文覆盖可能覆盖开发者或其他 Agent 的修改;代码审查只能看到巨大重排,真实语义修改被噪声淹没。

因此默认协议应该是局部、带前置条件的编辑意图。模型通过 tool call 给出结构化参数,执行器负责定位、冲突检查、原子写入和生成 diff。模型的普通文本回复不参与文件写入,也不应该从 Markdown 代码块中猜测补丁。

{
  "tool": "replace_in_file",
  "arguments": {
    "path": "src/pages/orders/index.tsx",
    "expectedVersion": "sha256:9f3c...",
    "oldText": "useEffect(() => {\\n  fetchOrders();\\n}, [filters]);",
    "newText": "useEffect(() => {\\n  if (!filtersReady) return;\\n  fetchOrders();\\n}, [filters, filtersReady]);",
    "replaceAll": false
  }
}

执行器收到请求后,不应直接调用字符串替换。它至少要检查路径归一化后仍在工作区与本任务白名单内、当前文件版本等于 expectedVersionoldText 恰好命中一次、替换后的内容可以编码且未超出大小限制。写入应先发生在临时文件,再原子替换原文件,最后生成统一 diff 并记录调用结果。

任一前置条件不满足时,工具返回 EDIT_CONFLICT,附带当前版本、命中次数和锚点附近的有限上下文,但不应擅自挑选一个「看起来相似」的位置继续修改。expectedVersion 是长会话和并发编辑的底线:模型读取文件后、写入文件前,开发者或其他 Agent 都可能改过它;没有版本前置条件,旧上下文迟早会写回新工作区。

编辑工具应构成协议族

不同编辑形式适合不同风险等级。通用 Agent 至少应提供以下能力。

编辑工具 适用场景 定位与校验 不适用场景
精确文本替换 已读到唯一的小片段 文件版本加唯一旧文本 重复片段或格式化后不稳定
锚点区间替换 已知函数、类或 JSX 节点 AST/符号定位与节点指纹 文件无法解析或跨多个语义节点
结构化 AST 编辑 import、函数参数、属性等规则变换 parser、类型信息与语法树重写 依赖业务意图的跨文件重构
创建文件 新组件、测试、配置 文件不存在与目录白名单 覆盖同名旧文件
全量覆盖 生成文件或获批的大规模重写 显式审批与 diff 限额 现存大业务文件的常规修改

对 TypeScript、Java、Kotlin 等可解析语言,可以优先使用 AST 或语言服务定位。例如模型希望在 OrderList.handleSubmit 中插入埋点,工具先根据符号找到函数,再校验函数签名和节点摘要是否仍与模型读取时一致,最后在节点范围内应用编辑。AST 解决的是定位稳定性,不会替模型判断应该在成功、开始还是失败分支里埋点,业务语义仍需要影响分析与验证。

多文件修改需要事务意识

「修改接口定义,再更新所有调用方」是常见任务。如果第一步成功、第二步失败,工作区会停在不可构建状态。本地交互式 Agent 可以让开发者自行处理,但生产系统不能假装半完成状态不存在。

比较实用的做法是把一组相关编辑放进临时工作树或临时分支,每一步仍产生 diff,但只有通过当前单元的最低验证后,状态机才把它标记完成。严重失败时可以回滚当前单元的所有编辑,而不是让后续 Agent 在半成品上继续猜。跨 tool call 的全局数据库事务通常不现实,因为构建、测试和模型调用都是长时间外部操作;工作树、提交点和明确的单元边界已经足够提供工程上的事务感。

补丁冲突的恢复协议

补丁无法应用时,不能直接退化成全文件覆盖。安全的恢复循环应该是:工具返回冲突类别、当前版本、附近上下文和当前 diff;模型重新读取该符号或更小范围,不复用旧上下文;模型生成新的局部编辑意图;再次冲突时,编排器检查是否存在人工并发修改、格式化器重写或任务边界错误。

同类冲突达到阈值后,任务应暂停并升级人工。这个阈值不应由模型自行决定,而是任务策略的一部分。例如同一文件连续两次 EDIT_CONFLICT,就停止自动写入。持续重试只会消耗 token,并增加覆盖人工修改的风险。

命令执行也要分级

给模型一个 shell 不等于让它拥有 Unix 的全部能力。命令应按副作用和外部影响分级。

级别 示例 默认策略
L0:只读 grepgit diffgit log、读取锁文件 自动执行
L1:受限本地写入 局部编辑、生成测试、格式化指定文件 自动执行并记录 diff
L2:本地验证 typecheck、单测、构建指定模块 自动执行,设置资源与时长上限
L3:外部访问 下载依赖、访问测试服务、创建草稿 PR 白名单或任务审批
L4:不可逆或高影响 删除大量文件、修改锁文件、发布、强推、数据迁移 必须人工确认

命令策略还要检查解释器、工作目录、环境变量和网络出口。只根据命令字符串匹配危险词并不充分,一个脚本文件、环境变量或包管理器生命周期钩子也可能扩大副作用。因此运行命令的执行环境同样重要。

补丁之后先看 diff,再做验证

写入成功不等于任务完成。一个稳定补丁循环是:读取证据,提交局部编辑意图,工具应用补丁并返回 diff,检查修改范围、文件数和行数预算,运行最快的语法或类型验证,再决定是否执行更昂贵的测试。格式化工具应在局部修改后运行,但不应在第一次修改后格式化整个仓库,否则无关变化会混进 diff,削弱评审与回退能力。

测试通过后仍应再次读取 diff,因为测试可能没有覆盖修改范围外的偶然改动。这个「补丁—diff—验证—记录」闭环,比模型一次输出多漂亮的代码更重要。

验证是主流程,而不是生成后的附属动作

代码生成往往只需要几秒,验证一个补丁可能需要十分钟甚至更久:读取调用方、跑类型检查、启动环境、登录有权限的页面、观察网络请求、控制台和埋点。这种成本不对称会诱发错误产品方向:系统不断生成看起来合理的补丁,人工承担最后的大量鉴别工作。那不是提高效率,只是把编码成本转移为 review 与排障成本。

验证必须进入 Agent 的主状态机,不是生成结束后的可选按钮。不同验证层回答的问题不同,低层成功不能代替高层结论。

层次 回答的问题 典型手段 不能证明什么
语法与类型 代码能否解析、依赖是否匹配 formatter、lint、typecheck、build 用户流程与运行时行为
模块契约 已知输入输出是否符合约定 单测、组件测试、契约测试 多页面流程与权限时序
用户流程 用户动作是否经过正确路径 集成测试、E2E、浏览器自动化 线上真实流量与长期性能
运行行为 请求、埋点、错误与性能是否符合预期 网络比对、控制台、截图、灰度观测 所有未覆盖业务规则
架构收敛 旧依赖是否真正退出 依赖扫描、import 规则、资产引用数 新架构一定更合理

类型检查通过,不能证明弹窗提交后刷新了列表;截图相似,不能证明请求参数没有变化;单测全绿,也不能证明旧框架依赖已经清零。对于 Vue 转 React,视觉回归尤其重要,它补的是 DOM 结构变化但用户体验退化的盲区。对于 Native 动态化,页面快照、关键事件与网络序列同样可以提供较强的行为证据。

验证也需要契约

只写「跑测试」没有可执行性。每个迁移单元应带一份验证契约,声明要跑什么、通过条件是什么、失败如何归类。

unit: LegacyOrderTable
checks:
  - name: typecheck
    command: pnpm typecheck
    pass: exit_code == 0
  - name: component
    command: pnpm test -- OrderTable
    pass: exit_code == 0
  - name: order-list-flow
    command: pnpm e2e -- order-list
    pass: exit_code == 0
  - name: network-baseline
    command: pnpm verify:network -- order-list
    pass: no_unexpected_request
  - name: legacy-reference
    command: rg "LegacyOrderTable" src
    pass: no_matches

通用 Agent 不一定能自动推导所有验证项。它可以从 package.json、CI 配置、测试目录和历史 PR 中生成候选项,但候选项是否足以作为验收标准,需要仓库规则或人工确认。自动发现命令,不等于自动理解业务正确性。

上下文工程:证据密度优于长度

窗口变长后,一个常见误区是把整个仓库塞进 prompt。这会降低关键信息密度,也让模型把无关代码、历史兼容和过期规则混在一起。对 Agent 而言,上下文应按阶段构造:调查阶段需要入口、调用方、依赖、测试与基线;规划阶段需要迁移说明、规则、候选方案与未确认项;实施阶段需要当前单元、精确代码区间、编辑约束和相关类型;验证阶段需要 diff、验证契约和失败分类;交付阶段需要已验证证据与风险清单。

上下文不是一个不断追加的聊天记录,更像按任务状态拼装的工作包。可以把输入分成五类:不变操作边界,例如允许目录和策略版本;当前任务的权威状态,例如 phase、预算、基线与审批;代码证据,例如局部文件、调用方、测试与 diff;领域知识,例如接口契约、权限码与组件库版本;会话摘要,例如已经排除的假设与待办。

前两类优先级最高,也应尽量稳定。稳定前缀有助于 prompt caching,频繁变化的工具输出、时间戳和临时进度不应放在消息最前面。这既是 token 优化,也是让模型区分规则、事实与猜测的方式。

组件库知识必须带版本

2024 年遇到组件库 MCP 召回不准时,问题不只是检索没做好。相似组件很多,同名 API 在不同版本可能存在差异,文档、示例和真实安装版本也可能不一致。因此组件库知识不能只是一段说明文本。

每个可复用知识条目至少要带来源框架与目标框架版本、组件库名称与版本范围、适用与不适用条件、已验证调用样例、推荐验证方式、失效时间或最后验证时间。检索时先按框架和版本做资格过滤,再做关键词与语义的混合召回,最后重排并设拒答阈值。这里的拒答不是拒绝执行任务,而是拒绝把低置信度经验当作默认规则写入 prompt。经验库里同样要保留反例,否则它会变成一堆过期的复制粘贴方案。

人机协作:人工不是最后的兜底

在工程系统里,人不应该只在 Agent 完全失败后才出现。更合理的触发条件是证据不足或决策多解。

情况 为什么不能自动继续 人的输出应是什么
代码、测试、文档互相冲突 没有单一可信规格 裁定行为基线
接口字段含义不明确 代码无法推出业务语义 补充契约或示例
多种架构方案都可行 需要长期维护取舍 设计决策记录
关键路径没有验证 无法证明行为等价 补用例或接受明确风险
单元连续验证失败 任务粒度或影响分析可能错误 重划边界与计划
需要改数据语义或路由 影响范围超出当前授权 变更评审与兼容策略
diff 越过允许路径 任务范围已经漂移 扩大范围或拒绝变更

人工裁定不能只留在聊天记录中,它应沉淀为规则、测试、契约或经验条目。否则下一次迁移会再次触发同一个问题,团队成员会变成无法扩展的规则解释器。

专业转码 Agent 的完整闭环

到这里,可以把技术债转码 Agent 的最小闭环概括为八步:固定基线、依赖版本和已有失败;只读扫描入口、调用方、资产、样式、接口、权限与埋点;形成影响图与迁移说明,显式保留未确认项;按资产契约和依赖图切分可验证单元;在单元级审批后开放有限写权限;使用局部、带版本前置条件的补丁修改代码;按验证契约运行静态、流程、运行与架构检查;灰度切换,确认旧引用清零后再注销旧资产与适配层。

每一步都有明确输入、输出与失败出口。这才是专业领域 Coding Agent 的架构,而不是一个把 prompt 写得足够长的代码翻译器。

从专业 Agent 走向通用 Coding Agent

专业转码 Agent 的边界相对明确:输入框架、目标技术栈与迁移单元都比较清楚,验收目标至少可以定义为行为等价与旧依赖收敛。通用 Coding Agent 面对的是任意 issue,因此新增了几个根本问题。

第一个问题是任务分解。转码 Agent 往往由人先给出页面、组件或迁移批次,通用 Agent 需要自己判断一个 issue 应拆成哪些单元。第二个问题是规格发现,转码有旧实现可以作为部分证据,新功能经常没有足够规格,甚至没有测试。第三个问题是权限模型,转码可以预先禁止改接口、改路由或改数据层,通用任务可能合理地要求任何一种修改。第四个问题是经济模型,转码通常是项目制任务,通用 Coding Agent 会成为开发者日常工具,成本、并发与限流都需要长期治理。

专业 Agent 的状态机、补丁协议、验证、权限和工作区隔离都可以复用;但通用 Agent 不能只扩大允许目录和工具列表,因为它失去了专业场景提供的先验。通用化后,最难的不是补更多工具,而是在先验更少时约束任务分解、规格发现和权限扩大。

三层循环

通用运行时可以拆成三层循环。

通用 Coding Agent 的三层循环:Plan Loop、Task Loop 与 Tool Loop

工具循环处理一次读文件、搜索或补丁;任务循环处理一个有完成条件的单元,例如「为某接口补充空值处理和测试」;计划循环处理多个单元之间的依赖、范围与优先级。三层循环不意味着必须有三个模型,而是不能把「列计划」「改一个函数」「执行一次 grep」混成同一种状态。工具循环因工具结果结束,任务循环因验证结果结束,计划循环因所有单元交付、范围变化或需要用户澄清结束。

规格缺失必须被识别

对没有充分验收条件的任务,最危险的行为是模型补全自己的假设,然后把假设写成代码。通用 Agent 应在规划阶段主动检查目标用户与入口、输入输出契约、相关测试和设计资料、是否允许改变接口/数据结构/性能特征、以及成功后应运行哪些验证。

如果关键问题没有答案,任务状态应进入 needs_spec。它可以输出少量聚焦问题,但不应继续高影响写入。「先问清楚」并不降低 Agent 的价值,它避免了把一份不完整需求变成一份错误但可编译的实现。

六层通用架构

从专业转码实践可以抽象出一个通用架构。它不是按产品界面划分,而是按职责与故障边界划分。

通用 Coding Agent 的六层架构:任务编排、上下文与知识、策略与权限、模型决策、工具执行、验证与证据

任务编排层决定任务是否能恢复;上下文层决定模型是否看到了正确证据;策略层决定模型判断错误时能造成多大影响;模型层负责不确定性最高的推理与选择;工具层把选择落实为结构化副作用;验证层决定系统是否有资格宣布任务完成。这六层不是可选的产品功能列表,而是对应六类不同的失败模式。

从转码能力到通用能力

专业转码能力 通用 Agent 中的抽象能力
影响图分析 任务依赖分析与代码导航
迁移说明 任务契约、实施计划与交接状态
组件契约 API、模块和行为契约
语义迁移 需求意图到实现策略的映射
样式与行为比对 多层验证与运行时观测
适配层退出条件 架构收敛与技术债规则
灰度切换 可回退交付与发布治理
经验库 版本化规则、示例与失败案例库

专业 Agent 不是通用 Agent 的低配版本,它是在一个约束更强的领域中,先验证了通用架构需要哪些骨架。通用化后,最难的不是让模型连续调用更多工具,而是让每次假设、编辑、验证与范围扩大都有可追溯的状态和证据。

文件一致性与并发

expectedVersion 可以防止旧上下文覆盖同一文件的新内容,但它解决不了跨文件语义冲突。两个 Agent 分别改了 a.tsb.ts,两个文件版本检查都可能通过,组合后类型却不兼容。因此通用系统至少还需要独立 worktree 或临时分支、写前检查工作区是否存在当前任务之外的未提交修改、单元完成后运行增量 typecheck 或受影响模块检查,以及对高耦合资产、路由和接口契约采用逻辑租约。

大规模转码的并发尤其不能只看文件锁。十个互不依赖的叶子页面可以并行;共享表格组件与使用它的页面不应同时被不同 Agent 改写。并发度由依赖图和验证能力决定,不是由模型并发额度决定。PR 基线变化后,系统还应重新计算 diff 并重跑受影响检查,而不是直接自动 rebase 后合并。

通用工具失败语义

工具接口除了成功返回,还必须定义失败语义。

类别 含义 默认处理
TRANSPORT_ERROR 网络、服务或临时超时 幂等有限重试
VALIDATION_ERROR 参数 schema、路径或格式非法 停止,修复调用方式
BUSINESS_ERROR 版本冲突、符号不存在、条件不满足 重新读取与重新规划
RESOURCE_ERROR 内存、磁盘、配额或时长耗尽 保存状态,停止并升级
POLICY_ERROR 权限、审批、网络策略拒绝 停止并申请授权
AMBIGUOUS_RESULT 多个匹配或搜索结果无法判定 缩小范围或请求人工判断

如果工具只返回 success: false 和一段字符串,模型很容易把所有错误当成可修复的代码错误。结构化错误码不能保证模型做对,但它让编排器可以在模型之前执行正确路由策略。

生产化:运行平面与治理平面

到这里的 Agent 仍可以在单用户工作区中运行。生产环境还需要额外的运行平面和治理平面,关心的不是「模型能不能再快一点」,而是系统能否持续、安全、可审计地服务多个仓库和团队。

生产环境的运行平面与治理平面:API 网关、持久化编排器、临时执行环境池与审计密钥服务

持久化工作流与幂等副作用

生产服务不能把任务状态绑定在 HTTP 连接、某个进程内存或某个 Pod 上。执行节点会宕机,队列会重试,模型调用会超时,CI 可能在几十分钟后才返回。编排器需要持久化任务、阶段、工具调用、工作区引用、审批、模型请求和验证结果;恢复节点根据状态继续,而不是从头再向模型描述一次任务。

真正需要幂等和恢复的是外部副作用,例如创建工作树、创建分支、触发 CI、创建 PR 和发送通知。每个副作用都应有操作 ID。网络超时后重试 create_pr 时,系统先按任务 ID 查询是否已经创建,不能在不确定第一次是否成功时直接再创建一个 PR。

编排器维护权威状态,包括 taskId、租户、仓库、基线 revision、策略快照、审批记录、工作区/分支/PR/CI 引用、工具结果、diff、验证证据、模型版本和工具 schema 版本。执行器可以维护完整上下文、AST 缓存、语义索引与搜索缓存,它们丢失后可以重建。若把权威状态放在执行器内存里,重启后最危险的不是任务失败,而是系统不知道自己已经写过哪些文件、发过哪些外部请求。

隔离工作区与可复现环境

Agent 会执行仓库中的安装、构建和测试脚本,这些脚本本身是不可信输入。即使模型完全可靠,postinstall、生成器、测试和第三方依赖也可能读取环境变量、访问网络或消耗资源。因此生产执行环境不能复用应用服务进程,也不应复用工程师个人机器。

一个任务应在短生命周期工作区运行:从指定 commit 检出,在受限容器或虚拟机中执行,按需挂载短期凭据,结束后销毁文件系统。包管理器、编译器和镜像缓存需要与工作目录、密钥和用户数据隔离,否则一个仓库的构建产物可能污染另一个仓库。

每个任务还应记录环境指纹,包括基础镜像、Node/JDK/编译器与包管理器版本、锁文件 hash、环境变量白名单、网络策略、依赖缓存版本和测试服务版本。转码任务尤其依赖这些信息:同一份 Vue 或 Native 工程在不同组件库版本下,可能类型通过但运行时行为不同。没有环境指纹,系统无法区分补丁问题和环境漂移。

资源、身份与密钥

每个任务应限制 CPU、内存、磁盘、进程数、单命令时长、总任务时长、网络出口与并行度。这不是为了机械压低成本,而是防止死循环测试、异常构建或模型生成的递归脚本占满执行集群。超限结果必须结构化返回,例如 RESOURCE_EXHAUSTEDCOMMAND_TIMEOUT,不能把资源耗尽描述成代码错误并诱导模型继续修改代码。

生产 Agent 经常要访问 Git、制品库、CI、内部文档和问题系统。最危险的做法是给所有任务挂一个万能机器人 token。更合理的模型是三类身份分离:用户身份决定谁能发起与审批,服务身份负责调度,任务身份只获得完成当前动作所需的最小权限。只读代码解释任务不应拥有写分支权限,创建草稿 PR 的任务不应拥有合并、发布或读取其他仓库密钥的权限,运行测试的容器不应继承用户的长期访问令牌。

密钥不能进入模型上下文、工具日志、shell 历史或 diff。执行器可以用工作负载身份向密钥服务换取短期 token,并将 token 绑定到任务、工作区、动作范围与过期时间。还要定义模型供应商边界:哪些代码、日志、路径和 issue 内容允许发到外部模型,哪些必须内部部署或先脱敏;数据分级、租户隔离和留存期限应是策略输入,不是开发者临时约定。

模型网关、预算与降级

生产中的模型不是稳定函数:模型版本会更新,限流、超时和格式漂移会发生,长上下文成本会快速累积。固定使用一个模型、一个 prompt 和一个工具 schema,会让系统失去质量与成本控制。

模型网关至少应按任务风险、语言、上下文长度和预算选择模型;固定每个任务内的模型、规则和工具版本;控制 token、轮次、时间和金额预算;对临时不可用做有限幂等重试;校验工具调用 JSON schema;并在预算耗尽时输出已验证证据、进入 needs_review。搜索摘要和文件分类等低风险工作可以使用成本较低的模型,跨模块规划和高风险编辑需要能力更强的模型。

主模型不可用时,低能力模型可以继续做只读分析,但不应自动接管高风险重构。降级时,自动化等级与可用权限也应同步下降。否则「服务可用」会掩盖「任务风险不可接受」。

可观测性、审计与告警

生产事故后,「Agent 已完成」是最没有用的记录。系统需要恢复一条变更的因果链:谁发起了任务,基线是什么,模型读过哪些证据,提出过什么假设,调用了哪些工具,哪些策略允许或拒绝过操作,最终写了什么 diff,运行过哪些验证,谁批准了 PR。

记录类型 主要内容 用途
业务追踪 时长、成功率、人工升级、PR 合入、回退与反馈 判断产品价值
技术追踪 模型调用、工具调用、退出码、镜像、资源、队列与重试 定位运行故障与成本热点
安全审计 身份、授权、密钥、网络、文件写入、外部调用与审批 合规与追责

三类记录应通过 taskIdtraceIdworkspaceIdpolicyVersion 关联。日志还要分级,代码片段、命令输出和测试日志可能包含用户数据或密钥,不能为了调试把原始内容无限期写入可检索日志。

告警也不应只看服务 5xx。更有意义的信号包括补丁冲突率突然上升、某个模型版本后的验证通过率下降、某仓库的超时与资源耗尽增加、平均修改范围持续超过预算、审批拒绝率异常升高,以及相同错误在多个任务中反复出现。这些信号分别可能指向工具协议、模型升级、环境退化、任务拆分或策略配置问题。

PR、CI 与发布治理

生产 Agent 最自然的交付边界通常是草稿 PR,而不是直接写主干。PR 应同时提供机器可读和人可读信息:改动摘要、影响范围、关键假设、验证结果、未验证项、风险级别、回退方式以及完整审计记录入口。

agent_task_id: migrate-legacy-order-table
base_revision: a1b2c3d
head_revision: e4f5g6h
risk_level: medium
verification_status: partial
verification_report: artifact://task/123/verification
rollback_plan: revert-pr
unverified_items:
  - 订单详情页埋点不在当前任务范围

评审者不必相信 Agent 的自然语言总结,而可以直接检查证据、diff 和验证产物。CI 触发也要防回环:Agent 创建的提交可能触发 issue bot、格式化 bot 或新的 Agent 任务,所有自动事件应携带来源与任务 ID,编排器应拒绝由自身提交递归触发的同类任务。自动修复 CI 时,还应限制最大轮数和允许命令,防止系统在「修一个失败,引入另一个失败」中持续消耗资源。

上线前不能只展示几个成功案例。应从历史 issue、已合入 PR、典型转码单元与故障案例中建立离线集,每条样本保留基线、验收命令和人工标注的不可修改边界。评估至少分能力评估、过程评估和工程结果评估:能否找到正确代码并通过目标验证,是否遵守路径/命令/预算/审批策略,PR 是否被接受、人工修改量和上线后回归是否改善。

模型、工具或规则升级应走灰度:先只读分析或在影子工作区生成补丁,比较搜索路径、修改范围与验证结果;再开放草稿 PR;最后才对明确目录、明确语言、明确测试基线的低风险任务开放自动提交。系统还应能够快速回切模型、prompt、工具与策略版本,不能在效果波动后只靠紧急修改系统提示词止血。

结论:受控变更,而不是更快的补丁

2024 年的转码实践让我改变了一个看法。Coding Agent 的瓶颈不主要是模型能否一次生成足够长的代码,真正的瓶颈是系统能否把大问题拆成可验证的工程单元,能否把不确定性写进状态,把权限放进程序,把验证放进主流程,把人工判断沉淀为下一次可执行的规则和证据。

模型、文件系统和命令执行构成最小闭环。专业技术债治理要求在这个闭环之上增加影响图、迁移说明、契约卡片、版本化知识、灰度切换与旧债注销;通用 Coding Agent 则进一步面对任务分解、规格发现、并发一致性、长期成本和生产治理。

如果只记一件事,我会选择这一句:**Coding Agent 不是更快地生成补丁,而是让每次工程变更都留下范围、依据、约束、验证和未解决风险。**没有这些,系统只是把代码写得更快;有了这些,它才开始成为研发流程中可以被信任的一部分。


1819 字 · 165 段落
xi ming

Written by xi ming You should follow him on Github

评论与讨论