本文是「Agent 开发实践与思考」系列第 10 篇。系列目录:
- 2024
- 2025
- 01-28 长任务不失忆:上下文压缩、交接文档与子任务隔离
- 02-25 MCP 用了三个月:工具标准化之后 Agent 设计变了什么
- 03-25 拆解 Coding Agent:为什么”写代码 + 文件系统”是通用 Agent 的内核(本篇)
从技术债转码开始,而不是从通用 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 调用。
静态 import 可以通过 AST 或语言服务获得;组件名、事件名和接口字段可以通过精确搜索获得。动态路由、全局事件总线、运行时组件拼装和埋点消费方,往往需要调用样本、运行日志或浏览器网络记录补充。因此影响图不是一次扫描后的真相,而是一组带证据等级的假设。Agent 应记录每条边来自 AST、文本匹配、运行时抓包还是人工确认;没有证据来源的边,不应被伪装成确定事实。
共享资产先迁什么
共享资产的迁移常常决定了项目是否能收敛。一个 Vue 表格可能被十个页面使用,直接按目录翻译通常有两种坏结果:新组件只满足第一个页面的调用方式,或者为了兼容所有不确定场景,做出一个什么都转发的桥接层。前者把问题推给后续页面,后者把双栈耦合变成新的长期债务。
正确的起点是收集调用契约:每个调用方传了哪些 props、监听了哪些事件、是否通过 ref 调用内部方法、插槽是否依赖作用域数据、组件内部是否隐藏埋点、权限、缓存或默认请求。React 版本的第一目标不是「设计得更优雅」,而是覆盖已经被证据支持的稳定契约。少数特例可以进入 render prop、适配器或人工决策,但不能因为少数特例而让所有能力继续隐式存在。
语义先于框架 API
从 Vue 到 React 最容易演示的是 API 对照表:v-model 对应 value 和 onChange,插槽对应 children 或 render prop,watch 对应 useEffect。这种映射只在局部情况下成立。
watch 可能是在计算派生值、校正输入、发请求、建立订阅,或者补偿一个历史 bug。这些职责在 React 中不应该都写成 useEffect:派生值通常应在渲染过程中计算,用户动作应留在事件处理函数中,请求应进入统一的数据请求边界,外部订阅才是 effect 的典型用途。同样,若调用方依赖插槽作用域数据,render prop 更接近原有契约;若只是表格单元格配置,结构化列定义通常更容易验证。
专业转码 Agent 的价值不在于背诵框架映射,而在于先解释旧机制承担的职责,再选择目标机制。这个能力决定它是在迁移语法,还是在迁移系统行为。
从一次调用到工程状态机
有了迁移说明后,下一步不是直接执行,而是建立状态机管理跨文件、跨轮次和跨会话的变更。最小 ReAct 循环只能做到模型调用工具、读取结果、再决定下一次调用;它无法表达「此时禁止写入」「这次失败应该回到调查」「这个问题必须人工裁定」。对技术债治理,这些状态不是产品 UI,而是工具权限与任务恢复能力。
NeedsDecision 是关键状态。它表示系统没有足够证据继续,不应被视为失败,也不应由模型用更多自然语言填平。任何涉及接口语义、权限、数据迁移、未覆盖关键路径或多种合理架构方案的任务,都可能进入这个状态。
phase 是权限开关
每个 phase 对应不同工具集。Investigate 只能使用读取、搜索、依赖分析和基线检查;Plan 可以写迁移说明,但不能改业务代码;Implement 才能打开当前单元的有限写权限;Verify 应拒绝新的编辑,除非状态机显式退回 Implement;Deliver 只能生成报告、草稿 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
}
}执行器收到请求后,不应直接调用字符串替换。它至少要检查路径归一化后仍在工作区与本任务白名单内、当前文件版本等于 expectedVersion、oldText 恰好命中一次、替换后的内容可以编码且未超出大小限制。写入应先发生在临时文件,再原子替换原文件,最后生成统一 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:只读 | grep、git diff、git 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 不能只扩大允许目录和工具列表,因为它失去了专业场景提供的先验。通用化后,最难的不是补更多工具,而是在先验更少时约束任务分解、规格发现和权限扩大。
三层循环
通用运行时可以拆成三层循环。
工具循环处理一次读文件、搜索或补丁;任务循环处理一个有完成条件的单元,例如「为某接口补充空值处理和测试」;计划循环处理多个单元之间的依赖、范围与优先级。三层循环不意味着必须有三个模型,而是不能把「列计划」「改一个函数」「执行一次 grep」混成同一种状态。工具循环因工具结果结束,任务循环因验证结果结束,计划循环因所有单元交付、范围变化或需要用户澄清结束。
规格缺失必须被识别
对没有充分验收条件的任务,最危险的行为是模型补全自己的假设,然后把假设写成代码。通用 Agent 应在规划阶段主动检查目标用户与入口、输入输出契约、相关测试和设计资料、是否允许改变接口/数据结构/性能特征、以及成功后应运行哪些验证。
如果关键问题没有答案,任务状态应进入 needs_spec。它可以输出少量聚焦问题,但不应继续高影响写入。「先问清楚」并不降低 Agent 的价值,它避免了把一份不完整需求变成一份错误但可编译的实现。
六层通用架构
从专业转码实践可以抽象出一个通用架构。它不是按产品界面划分,而是按职责与故障边界划分。
任务编排层决定任务是否能恢复;上下文层决定模型是否看到了正确证据;策略层决定模型判断错误时能造成多大影响;模型层负责不确定性最高的推理与选择;工具层把选择落实为结构化副作用;验证层决定系统是否有资格宣布任务完成。这六层不是可选的产品功能列表,而是对应六类不同的失败模式。
从转码能力到通用能力
| 专业转码能力 | 通用 Agent 中的抽象能力 |
|---|---|
| 影响图分析 | 任务依赖分析与代码导航 |
| 迁移说明 | 任务契约、实施计划与交接状态 |
| 组件契约 | API、模块和行为契约 |
| 语义迁移 | 需求意图到实现策略的映射 |
| 样式与行为比对 | 多层验证与运行时观测 |
| 适配层退出条件 | 架构收敛与技术债规则 |
| 灰度切换 | 可回退交付与发布治理 |
| 经验库 | 版本化规则、示例与失败案例库 |
专业 Agent 不是通用 Agent 的低配版本,它是在一个约束更强的领域中,先验证了通用架构需要哪些骨架。通用化后,最难的不是让模型连续调用更多工具,而是让每次假设、编辑、验证与范围扩大都有可追溯的状态和证据。
文件一致性与并发
expectedVersion 可以防止旧上下文覆盖同一文件的新内容,但它解决不了跨文件语义冲突。两个 Agent 分别改了 a.ts 和 b.ts,两个文件版本检查都可能通过,组合后类型却不兼容。因此通用系统至少还需要独立 worktree 或临时分支、写前检查工作区是否存在当前任务之外的未提交修改、单元完成后运行增量 typecheck 或受影响模块检查,以及对高耦合资产、路由和接口契约采用逻辑租约。
大规模转码的并发尤其不能只看文件锁。十个互不依赖的叶子页面可以并行;共享表格组件与使用它的页面不应同时被不同 Agent 改写。并发度由依赖图和验证能力决定,不是由模型并发额度决定。PR 基线变化后,系统还应重新计算 diff 并重跑受影响检查,而不是直接自动 rebase 后合并。
通用工具失败语义
工具接口除了成功返回,还必须定义失败语义。
| 类别 | 含义 | 默认处理 |
|---|---|---|
TRANSPORT_ERROR |
网络、服务或临时超时 | 幂等有限重试 |
VALIDATION_ERROR |
参数 schema、路径或格式非法 | 停止,修复调用方式 |
BUSINESS_ERROR |
版本冲突、符号不存在、条件不满足 | 重新读取与重新规划 |
RESOURCE_ERROR |
内存、磁盘、配额或时长耗尽 | 保存状态,停止并升级 |
POLICY_ERROR |
权限、审批、网络策略拒绝 | 停止并申请授权 |
AMBIGUOUS_RESULT |
多个匹配或搜索结果无法判定 | 缩小范围或请求人工判断 |
如果工具只返回 success: false 和一段字符串,模型很容易把所有错误当成可修复的代码错误。结构化错误码不能保证模型做对,但它让编排器可以在模型之前执行正确路由策略。
生产化:运行平面与治理平面
到这里的 Agent 仍可以在单用户工作区中运行。生产环境还需要额外的运行平面和治理平面,关心的不是「模型能不能再快一点」,而是系统能否持续、安全、可审计地服务多个仓库和团队。
持久化工作流与幂等副作用
生产服务不能把任务状态绑定在 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_EXHAUSTED 或 COMMAND_TIMEOUT,不能把资源耗尽描述成代码错误并诱导模型继续修改代码。
生产 Agent 经常要访问 Git、制品库、CI、内部文档和问题系统。最危险的做法是给所有任务挂一个万能机器人 token。更合理的模型是三类身份分离:用户身份决定谁能发起与审批,服务身份负责调度,任务身份只获得完成当前动作所需的最小权限。只读代码解释任务不应拥有写分支权限,创建草稿 PR 的任务不应拥有合并、发布或读取其他仓库密钥的权限,运行测试的容器不应继承用户的长期访问令牌。
密钥不能进入模型上下文、工具日志、shell 历史或 diff。执行器可以用工作负载身份向密钥服务换取短期 token,并将 token 绑定到任务、工作区、动作范围与过期时间。还要定义模型供应商边界:哪些代码、日志、路径和 issue 内容允许发到外部模型,哪些必须内部部署或先脱敏;数据分级、租户隔离和留存期限应是策略输入,不是开发者临时约定。
模型网关、预算与降级
生产中的模型不是稳定函数:模型版本会更新,限流、超时和格式漂移会发生,长上下文成本会快速累积。固定使用一个模型、一个 prompt 和一个工具 schema,会让系统失去质量与成本控制。
模型网关至少应按任务风险、语言、上下文长度和预算选择模型;固定每个任务内的模型、规则和工具版本;控制 token、轮次、时间和金额预算;对临时不可用做有限幂等重试;校验工具调用 JSON schema;并在预算耗尽时输出已验证证据、进入 needs_review。搜索摘要和文件分类等低风险工作可以使用成本较低的模型,跨模块规划和高风险编辑需要能力更强的模型。
主模型不可用时,低能力模型可以继续做只读分析,但不应自动接管高风险重构。降级时,自动化等级与可用权限也应同步下降。否则「服务可用」会掩盖「任务风险不可接受」。
可观测性、审计与告警
生产事故后,「Agent 已完成」是最没有用的记录。系统需要恢复一条变更的因果链:谁发起了任务,基线是什么,模型读过哪些证据,提出过什么假设,调用了哪些工具,哪些策略允许或拒绝过操作,最终写了什么 diff,运行过哪些验证,谁批准了 PR。
| 记录类型 | 主要内容 | 用途 |
|---|---|---|
| 业务追踪 | 时长、成功率、人工升级、PR 合入、回退与反馈 | 判断产品价值 |
| 技术追踪 | 模型调用、工具调用、退出码、镜像、资源、队列与重试 | 定位运行故障与成本热点 |
| 安全审计 | 身份、授权、密钥、网络、文件写入、外部调用与审批 | 合规与追责 |
三类记录应通过 taskId、traceId、workspaceId 与 policyVersion 关联。日志还要分级,代码片段、命令输出和测试日志可能包含用户数据或密钥,不能为了调试把原始内容无限期写入可检索日志。
告警也不应只看服务 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 不是更快地生成补丁,而是让每次工程变更都留下范围、依据、约束、验证和未解决风险。**没有这些,系统只是把代码写得更快;有了这些,它才开始成为研发流程中可以被信任的一部分。

