🏠

《当代码可以被无限生成… 我病了吗?》

Prompt → Context → Rule → Harness,AI Coding 的工程化之路

我一直想写一篇关于 AI Coding 的文章。

去年做相关项目时,写过怎么写提示词,写了一半就搁置了。后来转向上下文工程,又开始研究上下文组织方式对模型生成效果的影响……再后来,就没有后来了。

大模型时代的技术迭代太快了。Prompt、Agent、Context、Spec、Skills、Harness……一个个新词不断冒出来,它们都在做同一件事:把我们正在做的事情,定义清楚,让它变得标准、可复用。

但我用 AI Coding 越深,一种奇怪的感觉就越强烈:

敲代码确实越来越轻松了,因为不用怎么敲了。但检查代码的负担,却越来越重了。

以前写代码是有节奏的:先把需求想清楚,理解上下文,在脑子里搭好方案,再一行行敲、查文档、等编译、做验证、转测。一套流程走完,其实是很放心的,可以非常自信的说一定没问题,就算有问题也不会怀疑是自己的问题。

但现在的工作流变成了这样:

需求来了 → 理解一下上下文 → 丢给 Agent → Agent 生成代码 → 我们去验证

可这个验证,成本可能很高。Agent 的改动范围可能很大,逻辑散、边界模糊,有时候很难一眼看出改了啥。虽然有理有据,但不是自己写的代码,我相信现阶段绝大多数人还是会再验证一遍。

于是我最清晰的感受是:

我对业务需求的理解,变弱了。

更准确地说,我对「我提交的代码」的理解程度变弱了。因为把更多时间花在了和 Agent「辩论」以及验证上:不断完善上下文边界、修正提示、重新生成,直到所有验证通过……整个过程里,越来越只关注「能不能跑通」这个结果,却慢慢忽略了业务需求本身的逻辑和边界。

模型写的代码确实比自己写的更快、更好、更标准。但有时候会问自己:

我真的读懂了这段牛逼代码了吗?

我能说清楚这里的业务逻辑吗?

往后系统升级的时候,我还记得这段代码为什么这么实现吗?

之前在前团队做 AI Coding 时有个经历:当时要写一个很繁琐的工程化校验逻辑,大多是正则表达式和类型判断。于是定好大体的框架后,就交给模型补全完整代码了。说实话,绝大多数场景下都没有问题,但不巧后台协议某些字段类型需要前端来兼容一下,恰巧我当时又在休假,同事排查起来就说非常困难,尤其是那些晦涩的正则和类型判断。

最后,他只能重构了一版。

而我细想,其实也不知道模型在那里面到底做了多少边界处理。毕竟,我只定了架构,细节实现,都是模型生成的。

那么当代码可以被无限生成、无限重构,我们,又该站在哪里?

这篇文章想通过这一年的技术演变,结合自己之前的工作经验,来写写自己的理解与看法。


Prompt:把话说清楚

去年做 Vibe Coding 相关工作,就是通过自然语言描述需求,调用大模型自动生成可落地的应用。

当时遇到最苦恼的问题:同一个模型,换一种说法,结果差很多。

Prompt 的质量直接决定了生成结果的质量。

而又因为这个原因,我们越发想往 Prompt 里塞更多内容,希望模型记住需求、记住约束、记住规范,让输出更稳定、更可控、更符合预期。

我们可能会这么做:

于是 Prompt 越写越长,越来越像一份前端百科全书……又或者是一份疑难杂症的病历,对症下药。

问题接踵而至:

问题 表现
遗忘 模型在多轮交互中丢失先前的约束条件
质量 生成代码存在命名不规范、逻辑缺失等 badcase 反复出现
失智 prompt 中明确写了不要 xxx,反而出现的概率更大了
脆弱 改一个措辞就能导致整个系统崩溃

这些问题的根源,基于一个看似合理实则脆弱的假设:只要提供足够的上下文,AI 就能完美工作。

但事实并非如此。完美提示词是一种幻觉,工程问题只有最佳实践,没有最优解。

Prompt Engineering 迭代下来的最佳实践技巧包括:调整温度、结构化 Prompt(XML)、System Prompt、任务分解、提供 few-shot、订目标再探索结果。

这些东西有用,但需要理解本质:大模型是模式匹配,本质上是概率问题。

模型在生成每个 token 时,都在做概率选择,在当前上下文下预测下一个 token。提示词工程做的事,是在概率空间里提供方向,把模型输出的概率分布往特定方向推。

日常 Chat 中,很少需要考虑这些,让模型优化一下,它就能生成一段不错的提示词。

但提示词工程的边界非常清晰:它解决的是表达问题,而不是信息问题。

举个例子:让模型生成一个表单页面,表单组件用我自己写的私有组件库。提示词写得再漂亮,它也不会知道怎么用,只会去它训练数据里找一个类似的出来,然后编造一个结果给你。

graph LR
    A[用户意图] --> B[Prompt Engineering]
    B --> C{模型能理解吗?}
    C -->|训练数据里有| D[正确输出]
    C -->|训练数据里没有| E[幻觉/编造]
    style E fill:#f9d0d0
    style D fill:#d0f9d0
    
Prompt Engineering 的信息边界

Prompt 只能调整「怎么说」,解决不了「模型不知道」的问题。这就引出了下一层。


Context:把书准备好

上下文工程的出现,恰好解决了提示词工程信息缺失的问题。

核心逻辑很简单:在合适的时机、以合适的方式,给模型注入它不知道的信息,让模型能理解新事物、新规则。

在 Agent 处理任务的流程中,上下文的范围远比提示词更宽:历史对话记录、工具调用结果、私有文档、组件库说明、接口规范,这些都是模型训练数据里没有的东西;还有代码上下文、业务需求文档,甚至实时的系统状态。所有能影响模型决策的信息,都是上下文的一部分。

最早做上下文工程时,踩过很多坑。比如把所有文档一股脑塞进模型的上下文窗口,结果要么超出窗口限制,要么信息混乱,模型抓不到重点。

后来对上下文内容做了拆分:在每次 Agent Loop 流程中,动态构建上下文,基于 Query 的内容筛选关联上下文。单次模型调用中,只提供 Query 涉及的最大相关内容。

效果还挺好,上下文大小减少了很多,模型输出的内容也更稳定了。

之前看过一篇文章说,单次模型调用 prompt 长度超过 3000 tokens 后,模型降智的曲线会变得更加陡峭。所以不必各种吹嘘无限上下文、超长上下文——如果 1M 的上下文都能超,多多少少在 Agent 设计上有些问题。

RAG:检索增强生成

因为上下文窗口大小的限制,RAG in Context 成了不少工程实践的选择:在构建上下文窗口时,通过 RAG 检索外部知识库内容,再塞入上下文。

但 RAG 的召回稳定性是个老问题。之前做特定领域场景的 RAG 解决方案时,遇到过很多问题:文档怎么切分、切分策略怎么定,检索用什么方式,向量数据库里应该包含哪些内容……

分享几个实践经验,如果你的 RAG 是应用在某个特定领域(不是泛化的),可以试试:

flowchart LR
    A[用户 Query] --> B[向量检索]
    B --> C[召回 Top-K Chunks]
    C --> D[Rerank 重排序]
    D --> E[筛选最相关内容]
    E --> F[注入 Context Window]
    F --> G[模型生成]
    H[知识库] --> I[文档切分]
    I --> J[向量化入库]
    J --> B
    style D fill:#fff3cd
    style E fill:#d4edda
    
RAG 检索增强生成流程

RAG 解决的是上下文超长、内容无法有效获取的问题。在这个检索增强生成的流程中,当一个 Query 需要依赖外部知识库时,我们只把相关内容放进上下文窗口。

Skills:渐进式披露

这和 Skills 的渐进式披露本质是一样的:不是把所有信息一开始就全给模型,而是按需、分层地给,做好精确的上下文管理。

Skills 的信息分为两类:

这样做有两个好处:一是节省 context window,把宝贵的空间留给真正有用的信息;二是减少噪音,信息越少越精准,模型做决策的干扰越少。

一句话总结:给地图,不给百科全书。

flowchart TB
    subgraph 静态上下文
        A1[技能名称]
        A2[简要描述]
        A3[触发条件]
    end
    subgraph 动态上下文
        B1[完整指令]
        B2[示例代码]
        B3[工具定义]
    end
    A1 & A2 & A3 --> C{模型判断
是否需要?} C -->|是| B1 & B2 & B3 C -->|否| D[跳过,节省窗口] style 静态上下文 fill:#e8f4fd style 动态上下文 fill:#fff3cd
Skills 渐进式披露机制

但即便做好了上下文工程,还是会陷入新的问题:信息给对了,Prompt 也写得很好,模型生成的代码还是会出错。

因为上下文解决的是「输入问题」,没解决「执行问题」。模型在连续生成、多步操作的过程中,依然会跑偏、会出错,甚至悄悄修改不该改的逻辑,而我们往往要到最后验证时才能发现。

之前做 AI Coding 项目时,我们在 Agent 最后做了很多 Babel 检查来保证输出结果稳定,这和现在 Harness 中的结果控制不谋而合。但这还远远不够。


Rule:把流程把控好

这里本来想写 Agent 的,但 Agent 本质就是个 Loop,没什么太多可说的。这里主要讲 Spec 和 Rule 这两个在 Harness 前夜出现的词。

我们用 AI 写代码时,很容易陷入一个误区:只关心 AI 能不能把代码写出来,却很少想——AI 写出来的东西,怎么在我们的工程体系里管得住。

这也是之前做 AI Coding 反复踩过的坑:Agent 乱改接口、代码风格不统一、明明只写一个交互它非要顺手多做一堆,最后全是隐患。

问题不在模型强不强,在于没有给它明确的约束。Rule 和 Spec 就是用来解决这件事的,也是整个 Harness 体系的地基。

Rule:先学会不越界

Rule 的核心就一件事:把「不能干什么」写成机器能看懂、能校验的边界。

不同工具做法其实差不多:Codex 会读 AGENTS.md 做长期指导,Claude 用 CLAUDE.md 做默认约束,本质都是 Rule。现在很多项目也都会在 rules 目录下加上规范约束,Agent 启动就自动加载。

再配上 Linter、Babel 这类工具,把规则变成硬性检查。Agent 生成代码后,工具自动扫一遍:命名不规范、动了不该动的目录、语法有问题,直接打回去重改,直到合规为止。

这种方式比靠 Prompt 约束靠谱得多。

flowchart LR
    A[Agent 生成代码] --> B[Linter/Babel 检查]
    B --> C{是否合规?}
    C -->|否| D[打回重改]
    D --> A
    C -->|是| E[通过,进入下一步]
    F[Rule 定义] --> G[AGENTS.md / CLAUDE.md]
    G --> H[Agent 启动自动加载]
    H --> A
    style D fill:#f9d0d0
    style E fill:#d0f9d0
    
Rule + Linter 闭环检查流程

举个实际的例子。我在项目的 rules 里定义了一条约束:源文件名必须用 kebab-case(全小写,单词用 - 连接),禁止 PascalCase 或 camelCase。

// 错误
ModulePanelHeader.vue
UserProfile.ts

// 正确
module-panel-header.vue
user-profile.ts

Agent 启动时自动加载这条规则,生成文件时就会遵守。这比在 Prompt 里写「请用 kebab-case 命名文件」靠谱得多——Prompt 里的约束模型随时可能忽略,但 Rule 是每次任务都会带上来的硬上下文。

Rule 是红线,工具是监督员,从根本上避免 Agent 犯高代价的低级错误。

Spec:别让 Agent 跑偏

比较久之前用的了,现在我自己也不怎么用这个了,但在写 prompt 的时候会下意识地按照这种方式去提问,效果会好很多。

有了 Rule 守住底线,还需要 Spec 定清楚目标。

有一点很重要:Spec 不是更长的 Prompt,而是变更范围的纪律。

一份能用的 Spec,必须说清五件事:

  1. 这次要解决什么
  2. 这次不解决什么
  3. 允许改哪些文件、哪些逻辑
  4. 哪些接口、契约绝对不能动
  5. 做到什么样才算完成

给 Agent 派任务前,先写一段极简 Spec,不用写长,把范围和验收条件卡死。

比如:

只改登录页的提示文案,不动登录接口,不改其他页面样式,改完能正常渲染就算过。

边界一清楚,Agent 就不会自作主张「多做事」,更聚焦。

graph TB
    subgraph Spec 五要素
        A[要解决什么]
        B[不解决什么]
        C[允许改什么]
        D[绝对不能动什么]
        E[完成标准]
    end
    A --> F[聚焦目标]
    B --> F
    C --> G[控制范围]
    D --> G
    E --> H[明确验收]
    F & G & H --> I[Agent 不跑偏]
    style A fill:#d4edda
    style B fill:#f9d0d0
    style D fill:#f9d0d0
    style E fill:#fff3cd
    
Spec 五要素结构

简单说:Rule 管边界,Spec 管目标。

但只靠这两个还不够。要让整个过程稳定可控,还需要 Loop 把流程跑完。

Loop:闭环执行

有了 Rule 和 Spec,Agent 才能在操场内有计划地干活。而 Loop 就是让 Agent 能稳定可控地干活,控制 Agent 执行任务的节奏。现在基本上所有的 Coding Agent 背后跑的都是 ReAct 模式:Reasoning + Acting,想一步,做一步。

流程是这样的:模型先看当前的上下文,想一下该做什么(Reasoning),然后调一个工具去执行(Acting),拿到工具返回的结果后再想下一步该做什么。如此循环,直到任务完成。

flowchart LR
    A[观察上下文] --> B[思考:下一步该做什么]
    B --> C[行动:调用工具]
    C --> D[观察:拿到结果]
    D --> E{任务完成?}
    E -->|否| B
    E -->|是| F[输出结果]
    style B fill:#d4edda
    style C fill:#fff3cd
    
ReAct 模式:Reasoning + Acting

举个例子:你让 Agent「修一下登录页的报错」。它不会一口气生成一大段代码,而是先读一下项目结构,发现登录页在 src/pages/login.tsx,再读这个文件,发现报错来自一个接口调用,然后去看接口定义,最后才动手改代码。每一步都是「看到什么,想一下,做一步」。

这和早期那种「一次性把需求丢给模型,然后开始许愿」完全不同。ReAct 模式让 Agent 有了纠偏的机会:每一步都能根据最新的信息调整方向,而不是一把梭。

当然,光有 Loop 不够。每一步之间还需要外部校验(Linter、测试)和状态记录,这些就是前面 Rule 和后面 Harness 负责的事情了。

对日常用 Claude Code 或 Codebuddy IDE 这些工具来说,我们不需要太关注 Loop 的内部实现,Agent 会自己跑这套流程。但理解它的运作方式,能帮我们写出更好的 Spec:把任务拆得足够小、边界足够清晰,Agent 在每一步 Reasoning 时才不容易跑偏。而对于 Rule 来说,每个项目会有自己的一些 rule 约束,这些 rule 约束会被带到 Agent 调用模型的提示词内,也就是会做出 Prompt 的约束。


Harness:模型之外的一切

前面聊了 Rule、Spec、Loop,分别解决边界、目标、执行流程的问题。放在一起,基本能让 Agent 少出错。

但少出错和稳定交付之间,还隔着一整套工程系统。

前面提到过,之前在 Agent 末尾加的那些 Babel 检查,现在回头看,只是 Harness 的冰山一角。检查只管最后一步的输出,真正要管住的,是 AI 从接到任务到交出结果的每一步。

这就是 Harness Engineering 要做的事。

先说清楚几个概念

LLM、推理模型、Agent、Harness,这几个词经常混着用,但它们处在不同层次:

graph TB
    subgraph 层次关系
        A[LLM
预测下一个 token] --> B[推理模型
多想几步,多校验几次] B --> C[Agent
控制循环:看什么、做什么、何时停] C --> D[Harness
工程系统:上下文、工具、状态、权限] end style A fill:#e8f4fd style B fill:#d4edda style C fill:#fff3cd style D fill:#f9d0d0
LLM → 推理模型 → Agent → Harness 层次关系
概念 本质 类比
LLM 预测下一个 token 的引擎 会写代码的工程师
推理模型 多花算力、多想几步的 LLM 做事前先想清楚的习惯
Agent 围绕模型的控制循环 处理任务的节奏和步骤
Harness 模型之外的一切工程系统 工位、工具链、规范、权限和验收机制

用一个类比把层次串起来:LLM 是一个会写代码的工程师,推理模型是他做事前先想清楚的习惯,Agent Loop 是他处理任务的节奏,Harness 就是团队给他配的工位、工具链、代码规范、仓库权限和验收流程。

一个再厉害的工程师,扔到陌生项目里,不给仓库权限、不告诉团队规范、不接 CI/CD,他也写不出能上线的代码。AI 也一样。

Mitchell Hashimoto 说过一句话:「你不是模型,那你就是 Harness。」对于我们做应用的工程师来说,模型层面能做的事越来越少,能拉开差距的,就是外面这层系统。

同一个模型,为什么效果差这么多

这是我一直在想的问题。

同样说一句「修一下这个问题,顺便看看代码为什么报红了」,有的 Agent 噼里啪啦回一大段解释,看起来很努力,结果改错了文件。有的 Agent 会先默默扫一遍仓库结构,读 README 和配置文件,跑一遍测试命令定位报错,最后给一个能直接合并的 diff。

模型是同一个,差别在哪?在 Harness。

自己也有过类似体验。同样的模型,在普通聊天界面里让它改 bug,它给我解释了一大堆原理,代码贴出来还跑不通。但在 IDE Agent 里,同样的指令,它会自己去读文件、跑测试、定位报错,最后给出能直接用的 diff。模型没变,变的是它跑在什么样的系统上。

所以 Harness 不是一个架子,也不是把 Rule、Spec、Loop 捆在一起就行了。它是一套运行时系统,负责上下文组装、工具调用、权限校验、状态持久化、出错恢复。简单说,模型之外跟执行有关的事,都归它管。

引导与控制

Harness 对模型的管理分两个阶段:生成之前引导,生成之后控制。

生成之前,给模型看该看的东西。架构文档(AGENTS.mdCLAUDE.md)、编码规范、类型定义,这些都属于引导。给模型看的是地图,不需要百科全书:告诉它项目长什么样、规矩是什么、哪些路不能走。

但文档不是越长越好。60 行以内的 AGENTS.md 比几百行的长文档有用得多。信息越精准,模型做决策时的噪音越少。

生成之后,用工具去校验结果。单元测试、类型检查、Linter、Hooks,都属于控制。这些工具的输出需要针对模型优化。比如 Linter 报错不要只说「line 42: error」,而要说「line 42: 变量名应该用 camelCase,当前是 snake_case,建议改为 userName」。模型拿到后者,一次就能改对。

一前一后配合:引导减少犯错概率,控制兜住漏网之鱼。

这个结构乍一看和模型训练里的前向传播、反向传播有点像,都是「一前一后形成回路」。但本质不同:反向传播真的在「学习」,梯度回传会改变模型权重;Harness 的控制只是「纠错重试」,模型还是那个模型,只是拿到了新的上下文信息再来一次。

flowchart LR
    A[架构文档 / 编码规范 / 类型定义] -->|引导| B[模型生成代码]
    B -->|校验| C[测试 / Linter / Hooks]
    C -->|不通过| B
    C -->|通过| D[合并]
    style A fill:#d4edda
    style C fill:#fff3cd
    
引导 + 控制

零信任

看到这里,你大概已经感觉到了:Harness 的任务,从来不是让 AI 更自由,而是让人更放心。

对我们来说,AI 一直都是一个黑盒,一个抽奖游戏。AI 写的每一行代码都需要被检查,不然不敢动。

这一小节标题叫零信任,说的是:在做 Agent 的时候,如果想保证结果稳定,不能只靠上下文。想做到 100% 正确,单纯靠模型能力,完全不可能。

那就工程化。在全链路内增加工程化校验,校验输出结果。但这也只能保证格式正确,不能保证逻辑正确。

先锁好门,再递工具。

比如之前做 Vibe Coding 时遇到的问题:我们的渲染引擎只支持两层数组,但模型生成页面配置 JSON 时不知道这个限制,偶尔会输出三层嵌套,直接解析失败。光在 Prompt 里写「不要超过两层」没用,模型过几轮就忘了。

如果任何环节都没有 AI 的参与,这个约束约定一下就好了。但现在大多数代码都是 AI 生成的,比如写个生成 Schema 的 Skills,让模型生成表单 Schema,它可能就不知道嵌套层数的限制。因此必须在工程环境上做好这个校验:定义一份 JSON Schema,直接把 items.type 不允许为 array 写死。模型输出之后跑一次校验,发现嵌套了就打回,把校验错误信息连同原始输出一起喂回去,让模型重新生成。这一步不需要人参与,Harness 自动完成。

flowchart LR
    A[模型输出] --> B[JSON Schema 校验]
    B -->|格式错误| C[错误信息 + 原始输出]
    C --> D[模型重新生成]
    D --> B
    B -->|校验通过| E[进入下一步]
    style B fill:#fff3cd
    style C fill:#f9d0d0
    
格式校验与自动重试

同样的思路可以推广到更多场景:

这些校验保证的都是「格式层面」的正确。逻辑对不对,还得靠测试用例和人工 review。但至少,格式层面的问题不需要人操心了,Harness 帮你挡掉。

光有拦截还不够,全程都得留痕。每一步的状态都被完整记录,形成可追溯的会话转录本。哪怕会话突然崩了,下次打开也能从断点接着干。

出了问题能追根溯源,好的实践也能沉淀成新的规则。用得越久,规则越准。

AI 越能干,缰绳越得勒紧

这是我对 Harness 最大的感受。

很多人觉得模型越强,需要的约束应该越少。恰恰相反。模型越强,它能做的事越多,失控的后果也越严重。一个只能写 hello world 的模型,放不放约束差别不大。但一个能重构整个项目、能执行任意命令的模型,如果不加约束,一次失控就可能造成不可逆的损害。

我们在业务层做的事情,本质是在产品自带的 Harness 之上再加一层约束。团队的代码规范、项目的目录约定、业务逻辑的边界,这些都是我们自己的 Harness。AGENTS.md 里的每一行,对应的可能就是一次历史踩坑。

说到底:模型决定你能做到多好,Harness 决定你最差不会差到哪去。


回到工程落地

聊了这么多,最后说说在实际工作中怎么看待这件事。

真实的软件开发,只有一小部分是在「生成下一段代码」。更多精力花在这些地方:

这些事,人类工程师再熟悉不过了。我们常觉得「写代码很累」,大部分累不是在想算法,而是这些碎的、脏的、但绕不过去的脑力劳动。回想一下自己一天的工作,真正在「写逻辑」的时间可能不到两成,剩下全是这些活。

一个好的 Coding Harness,就是把这些活接过去。它替你导航仓库、组织上下文、编排工具调用、管理状态恢复,你专注在业务判断上。

我自己现在接到需求的时候,因为在新团队,对业务理解和代码仓库都还在熟悉阶段,都会直接让模型先帮我找到这个需求要改动的地方在哪里,以及关联的是哪些东西,这样能快速有一个需求的全貌。

所以同一个模型,放在普通聊天界面里只能做简单的问答,放在一个做过上下文管理、工具编排、状态恢复的系统里,能完成复杂的多步骤工程任务。差距不在模型,在 Harness。

这也是为什么越来越多的 AI Coding 产品在 CLI 化。CLI 天然离终端近、离文件系统近、离开发者的真实工作流近。模型很重要,但产品形态决定了它能做什么事情。任务一旦进入真实仓库,结果差距就逐渐从模型能力转移到模型外面的那套系统工程上。

工程师的角色正在从「写代码」转向「设计写代码的系统」。我们写的 AGENTS.md、配置的 Hooks、定义的 Rules,做的就是 Harness Engineering。

说到这里,想聊聊之前做的 D2C(Design to Code)。

当时做的是基于 Figma 生成落地应用。流程是这样的:Figma 设计稿进来,模型先打标(识别组件类型、层级关系),然后工程化转标(把标注结果转成中间结构),基于 HTML 工程化出 React 代码框架,再让模型填充业务逻辑,最后跑一轮工具校验。整套流水线跑下来,还需要一份庞大的规范文档告诉模型该怎么写、不该怎么写。

这套方案能跑通,但中间环节很重。模型打标、工程化转标、基于 HTML 再出 React 框架,每一步都有信息损耗,每一步都可能出错。而且整条流水线是独立的,和业务工程是割裂的。模型不知道你项目里用的是什么组件库、什么目录结构、什么命名规范,它只看到一张设计稿和一份通用规范文档。生成出来的代码,往好了说能跑,往差了说和项目风格格格不入,还是要人花时间改。

即便是一次性的页面,这条链路我认为也不轻松,中间步骤的维护成本不低,一旦设计稿的结构稍有变化,流水线就容易断。

而我看来,D2C 应该回到工程环境本身。把 D2C 的能力沉淀成 Skills,这些 Skills 跑在 IDE Agent 里,天然就能利用 Harness 的能力——它知道你的项目结构、你的组件库、你的代码规范,生成出来的代码本来就是贴合项目的。中间不需要打标、转标、再转框架,省掉了那些信息损耗的环节。

flowchart LR
    A[传统 D2C] --> B[打标 → 转标 → 出框架 → 填逻辑]
    B --> C[通用规范文档]
    C --> D[生成通用代码]
    E[D2C as Skills] --> F[IDE Agent + Harness]
    F --> G[项目上下文 + 组件库 + 规范]
    G --> H[直接生成贴合项目的代码]
    style A fill:#f9d0d0
    style E fill:#d4edda
    
传统 D2C vs D2C as Skills

说白了,传统 D2C 是给模型一份说明书让它闭卷考试,D2C as Skills 是让模型坐在你的工位上开卷答题。同一个模型,后者的输出质量会好得多,因为 Harness 帮它补齐了所有它不知道的东西。

我自己最近在试的一个思路叫 D2A(Design to Anything)。做法很简单:通过 Figma API 拿到设计稿的 Design Token(颜色、字号、间距、组件结构),做一层压缩把冗余信息去掉,然后直接丢给 IDE Agent。不打标、不转标、不出中间框架,就一层。模型拿到的是精简过的设计意图,再结合当前项目的上下文(组件库、目录结构、代码规范),直接生成高可用的代码。

flowchart LR
    A[Figma API] -->|获取| B[Design Token]
    B -->|一层压缩| C[精简设计意图]
    C --> D[IDE Agent]
    D -->|结合项目上下文| E[生成代码]
    E --> F[Playwright 视觉校验]
    F -->|通过| G[交付]
    F -->|不通过| H[截图差异 + 错误信息]
    H --> D
    style B fill:#e8f4fd
    style D fill:#d4edda
    style F fill:#fff3cd
    
D2A:一层压缩 + Playwright 视觉校验

整条链路只有一次信息转换,损耗降到最低。模型不需要理解 HTML 再翻译成 React,它直接从设计意图出发,在你的项目环境里写代码。

生成之后还有一步:启动 Playwright 做视觉校验。代码跑起来之后,Playwright 打开页面截图,和设计稿做像素级对比(或者和基线截图对比)。如果布局、间距、颜色有差异,把截图和差异信息回传给 Agent,让它基于视觉反馈修正代码。这一步不需要人参与,整个链路闭环。

这个 Skill 现在还在打磨,但方向上我觉得是对的:把中间环节砍到最少,让 Harness 做它擅长的事。Playwright 的加入,等于给 Agent 加了一双「眼睛」,它不再只能靠静态规则校验,还能看到真实的渲染结果。

但其实我认为,AI 时代下,Figma 不会是最终形态,未来的设计产品会有很大的变革。当然,AI 时代下的设计师也不会再追着那 1px 的 100% 还原了吧。

但写到这里,回头看看自己这一年的状态,又会冒出另一个问题。


向前看

前面聊的都是现状和经验。接下来想说一件正在酝酿的事:AI 时代的组件库建设。

传统组件库的边界

先说个观察。我们现在的组件库,不管是内部的还是开源的,设计目标都很明确:给开发者用。开发者看文档、查 API、引入组件、传 props、调事件。整条链路都是「人对组件」。

这没问题。但放到 AI Coding 的场景里,这条链路有个隐性的断裂:AI 写代码时,它怎么知道该用哪个组件?怎么知道这个组件的 props 约束?怎么知道组件之间的组合规则?

答案是:大多数时候,它不知道。

在之前做 AI Coding 出码开发的时候,就做过组件库的接入,当时遇到的最大问题就是:

模型能猜,能从训练数据里扒拉出一份类似的用法。但它不知道你项目里私有的 ProTable 和 Ant Design 的 Table 有什么区别,不知道 BizFormonSubmit 回调里要做哪些校验,不知道 AuthButton 在没有权限时应该渲染成什么。拿 Login 组件来说,它不知道登录态需要什么参数,只会按照自己的想法来,userName 和 password。

于是只能把这些组件信息塞进上下文:组件文档、API 说明、使用示例。但这些东西都没有现成的,都需要重新生成。

而站在现在这个点,我们设计的组件库本身就可以自带一份 AI 可读的完整描述。

组件 Schema:一份结构化的组件百科

这个想法的起点很直接:能不能让每个组件在构建阶段就自动生成一份结构化的描述,这份描述足够完整,既能给 Agent 用来理解和组装组件,也能直接渲染成给人看的组件库文档。

关键点在「自动生成」这四个字上。我们不需要手动维护一份额外的 schema,而是定义好 schema 的结构规范,在组件库构建时,让模型自动从源码、类型定义、使用示例中提取信息,生成这份描述。当然,这是理想状态。也可以通过项目结构、项目描述来辅助生成更加精确的内容。

这是一个未来要定义的标准 schema 结构,还没想好。但设想一下,一个组件的 schema 大概长这样:

{
  "name": "ProTable",
  "category": "数据展示",
  "description": "带分页、排序、筛选的企业级表格,支持远程数据源",
  "importPath": "@biz/pro-table",
  "props": {
    "columns": {
      "type": "Column[]",
      "required": true,
      "description": "列配置,定义表格的每一列"
    },
    "dataSource": {
      "type": "Record[]",
      "required": true,
      "description": "表格数据源"
    },
    "pagination": {
      "type": "PaginationConfig | false",
      "default": "{ pageSize: 20 }",
      "description": "分页配置,设为 false 关闭分页"
    }
  },
  "constraints": {
    "dataSource 超过 100 条时必须启用 remote 模式",
    "columns 不支持嵌套超过 3 层"
  },
  "composableWith": {
    "FilterPanel": "筛选面板,需要和 ProTable 的 filters 联动",
    "SearchBar": "搜索栏,需要通过 onSearch 回调触发表格查询"
  },
  "antiPatterns": [
    "不要在 columns 里写业务逻辑,应该用 render 函数",
    "不要直接修改 dataSource 引用,应该通过 setState 更新"
  ],
  "scenarios": [
    "商品管理列表:支持搜索、筛选、批量操作",
    "订单详情表格:展示固定数据,支持展开查看子订单"
  ]
}

这份 schema 覆盖了一个组件的全部维度:props 定义、插槽、事件、用法场景、约束条件、组合规则、反面模式。它既是一份完整的组件文档的数据源,也是 Agent 理解和组装组件的全部输入。

换句话说,只要有了这份 schema,就能:

同一份数据,三个消费方。这就是 schema 的价值:一份投入,多处产出。

构建:模型生成,人审校

刚才说了,schema 是构建时模型自动生成的,不是手写的。整个链路大概是这样的:

flowchart LR
    A[组件源码] --> D[模型提取]
    B[类型定义 / TypeScript] --> D
    C[JSDoc / 注释] --> D
    E[已有使用示例] --> D
    F[人工补充的业务上下文] --> D
    D --> G[生成 Schema]
    G --> H{人工审校}
    H -->|通过| I[入库]
    H -->|不通过| J[修改 + 标记问题]
    J --> G
    style D fill:#d4edda
    style H fill:#fff3cd
    style I fill:#e8f4fd
    
Schema 生成链路

模型从四个来源提取信息:

此外,还需要人补充一些模型提取不到的业务上下文:比如「这个组件在哪个业务线用得最多」「和哪个后端接口强绑定」「有什么历史遗留的限制」。

生成之后,走一轮人工审校。审校通过就入库,不通过就标记问题点,修改后重新生成。

这个流程的好处是:schema 的生成成本被摊到了构建环节,组件每次发版,schema 自动更新。不需要专门维护一份文档,也不需要担心文档和代码不同步。

消费:一份 schema,处处可用

schema 入库之后,它就不再属于某个特定工具,而是一份通用的组件知识资产。任何需要理解组件的场景,都能消费这份 schema。

IDE Agent 场景:Agent 接到页面需求时,从 schema 注册表中检索相关组件,按约束生成代码。Agent 不需要猜这个组件怎么用,schema 里有完整的 props 定义、使用场景和组合规则。

CLI 场景:在终端里敲一行命令,就能查到某个组件的用法、示例、注意事项。不需要打开浏览器翻文档,schema 就是文档的数据库。

D2C 场景:设计稿进来,Agent 从 schema 中匹配组件,按组合规则组装。不需要维护一份额外的「设计组件到代码组件」的映射表,schema 里的 category 和 composableWith 字段天然承担了这个职责。

新成员 onboarding:新人加入团队,不需要先啃几百页的文档站。把 schema 注入到 AI 助手里,直接问「我要做一个商品列表页该用哪些组件」,AI 基于真实的 schema 数据回答,比看文档高效得多。

graph TB
    A[组件 Schema 注册表] --> B[IDE Agent
自动生成组件代码] A --> C[CLI 工具
终端查询组件用法] A --> D[D2C
设计稿自动映射组件] A --> E[AI 助手
新人问答 / 方案推荐] A --> F[文档站
自动渲染组件文档] style A fill:#d4edda
一份 schema,处处可用

核心思路是:schema 是唯一的真实来源。文档站、Agent 上下文、CLI 工具、D2C 映射,都从同一份 schema 派生出来。不会出现「文档上写的和代码里实现的不一样」这种老问题,因为文档就是从代码生成的。

Agent First:不只是组件库

组件 Schema 只是其中一个切面。更宏观地看,AI 时代下,工程里的一切都应该变得更加 Agentic。

这也是我到新部门工作感受最深的一点:所有的工程设计都要 Agentic。

这不是一句口号,而是一个设计原则:在写代码、搭架构、做工程决策的时候,多想一步——Agent 能不能理解?AI 能不能利用?

几个具体的例子。

项目结构设计。传统的项目结构是给人导航的:src/pagessrc/componentssrc/utils,开发者靠经验和约定找到想改的文件。但 Agent 导航项目的方式和人不一样,它更依赖结构化的描述文件。所以新项目里,除了目录本身,还会维护一份结构描述:每个目录干什么、每个模块的职责边界、模块之间的依赖关系。这份描述写在 AGENTS.md 里,Agent 启动时自动加载,不需要自己猜。

接口契约前置。以前前后端协作是「人对人」:前端看接口文档,后端写接口文档,中间经常对不上。新项目里,接口契约不再只是一份给人看的文档,而是一份机器可读的 JSON Schema 或 TypeScript 类型定义。Agent 读到这份类型定义,就知道请求长什么样、返回什么结构、哪些字段必填。前后端的 AI 都在同一份契约上工作,对不上的概率大幅降低。

错误处理标准化。以前每个模块自己定义错误码和错误信息,格式五花八门。Agent 处理报错时,需要理解「这个报错是什么意思、该从哪里排查」。新项目里,错误处理统一收敛:错误码、错误信息、排查建议、关联文档链接,全部结构化。Agent 拿到报错信息,不需要猜,直接知道该干什么。

构建产物多态。前面提到的组件 Schema 就是典型例子。构建时不止产出 JS bundle,还产出 AI 可消费的 schema、人可读的文档、校验可用的类型定义。同一份源码,一次构建,多种产物。Agent 用 schema,开发者用文档,工程系统用类型定义,各取所需。

flowchart LR
    A[组件源码] --> B[构建流程]
    B --> C[JS Bundle
给运行时] B --> D[组件 Schema
给 Agent] B --> E[类型定义
给工程系统] B --> F[文档站
给开发者] style B fill:#d4edda style D fill:#fff3cd
一次构建,多种产物

配置即约束。传统项目里,很多工程约束是靠人遵守的:命名规范、目录约定、提交格式。新项目里,这些约束全部写成可执行的规则:ESLint 配置、Husky hooks、CI 检查。Agent 不需要「记住」这些规则,工具会自动替它检查。这其实就是前面聊的 Harness 思路在项目维度的落地。

回头看我正在做的新项目,整个架构设计就是按这个思路来的。从项目初始化的第一天起,就不只是「给人用」,而是同时考虑「给 Agent 用」。目录结构、类型定义、错误处理、构建流程,每一层都多想了一步:AI 能不能从这些信息里理解项目的全貌?

以前我们写代码,心里只有一个读者:下一个看这段代码的人。现在多了一个读者:AI。而且这个读者可能比下一个同事来得更早、看得更多。

代码写得好不好,以前看人好不好懂。以后还要看 AI 好不好懂。

我病了吗

回到开头那个问题。

当 AI 席卷各行各业,焦虑和亢奋让人矛盾激昂。

我病了吗?

没有。只是刚好站在一个时代切换的路口:从人写代码,到 HITL in AI(Human-in-the-Loop)。

之前面试时面试官说了一句话,我记了很久:「AI 时代不分什么前端、后端……不要给自己加前缀,我们都是工程师」(大概这个意思)。

技术边界正在被打破,Agent 在重塑我们的角色。但工程师的核心在于解决问题的能力,而非某种编码能力。

工具在变,瓶颈在变,价值在变。但真正不变的是:人对业务的理解、对逻辑的判断力。这些是 AI 永远无法生成的东西。

对个人来说,在 AI 时代站稳脚跟的关键,需要更加专注于文档的沉淀,或者说数字资产的积累。不管大模型、Agent 未来怎么迭代,只要我们有自己的文档沉淀、经验积累和业务认知,这些独一无二的数字资产,就是我们始终坚挺的底气。

加油!

graph LR
    A[代码 / 文档 / 测试用例] -->|AI 能生成| C[需要人验证]
    B[业务理解 / 逻辑判断力 / 经验沉淀] -->|AI 无法生成| D[验证的底气]
    style A fill:#e8f4fd
    style B fill:#d4edda
    
AI 能生成 vs AI 无法生成