Piece:将 Coding Agent 的局部构建反馈提速 10x

TL;DR: https://github.com/phodal/piece ,在线预览在:https://phodal.github.io/piece/

最近几周,我一直被卷在 Qoder IDE/Desktop 团队的 996 节奏里。直到这个周末,才终于抽出一段完整时间,尝试把一个反复冒出来的想法做成原型:

如果代码越来越多是由编码智能体生成和修改的,那么我们的工程反馈系统是不是也应该重新设计?

这里说的反馈系统,包含编辑、类型检查、编译、构建、预览、测试和回滚。过去这些反馈大多围绕文件、模块或项目展开;而在智能体式编码(Agentic Coding)的工作流里,AI 实际修改的单位往往更小:一个函数、一个 React 组件、一个接口、一个类,甚至是一段 JSX 结构。

所以我真正关心的问题是:

当编码智能体修改的不再总是文件,而是文件内部的语义片段时,构建系统能不能也理解这些片段?

换句话说,我们需要从基于文件的构建继续往前走一步,进入片段感知构建反馈

引子:长上下文与长文件

两三年前,当编码智能体刚开始变得可行时,由于上下文窗口有限,我们一直在强调要控制单文件的代码行数,让 AI 更容易理解和修改代码,避免超出上下文。

现在情况已经发生了变化。头部模型已经开始支持更长的上下文窗口,AI 处理长文件、长上下文和复杂代码片段的能力明显增强。与此同时,代码库里也越来越容易出现几千行的代码文件,尤其是在前端项目里。再加上 AI 反复修改之后,往往会产生大量冗余代码,进一步导致文件行数暴增。一个文件达到 5k、10k 行,甚至更多,已经不再是一个特别罕见的现象。

这并不意味着我们应该接受巨型文件,也不意味着文件拆分不再重要。文件拆分依然承担着命名空间、架构边界、团队协作、代码所有权、评审粒度和可测试性的作用。

但有一个变化正在发生:

文件仍然是人类协作的稳定边界,却不一定仍然是最自然的最小反馈单位。

过去我们拆文件,很大程度上是为了让人更容易理解、维护和协作。现在编码智能体可以在更长的文件里定位结构、替换局部、生成补丁,文件内部的结构片段开始变得越来越重要。

这件事可以先用一张图压缩出来:文件不会消失,只是它不再天然等于最小反馈范围。

传统构建反馈的边界

很多现代构建系统早就不是简单的“全项目重编译”。Gradle 有任务、输入、输出和构建缓存;Vite、Webpack、Turbopack 这类前端工具有模块图、热更新和增量更新;TypeScript、Kotlin、Go 也都有各自的增量编译、类型检查和缓存机制。

所以问题并不是“现有构建系统不够聪明”。

更准确地说,问题在于:

大多数开发反馈系统的变更入口,仍然主要围绕文件、模块、目标或任务展开。

在传统编程方式里,这个假设很自然。我们通常通过有意义的文件名组织代码,以便让人理解领域概念。比如一个 UserService,我们会把它放在 user_service.ts 里;一个 LoginForm,我们会把它放在 login_form.tsx 里。

文件既是存储单位,也是阅读单位,还是很多构建工具感知变化的入口。一个文件变了,构建系统、语言服务、预览系统和测试系统再根据各自的依赖图去传播影响。

但到了编码智能体时代,这个边界开始变得不够细。

编码智能体的编辑方式:从文件到语义片段

AI 修改代码的方式,和人类过去修改代码的方式并不完全一样。

  • 人类通常是打开一个文件,理解上下文,然后在脑子里维护文件级别的结构。我们会关心这个文件叫什么、这个文件属于哪个目录、这个文件表达的领域概念是什么。
  • 编码智能体更常见的工作方式是:先定位一个目标区域,再对某个函数、组件、接口、类,甚至某一段 JSX 结构做局部替换。

它不一定在意“这个文件作为整体表达什么”,而是更关心“我要修改的目标片段在哪里,它依赖什么,修改后会影响谁”。换句话说,编码智能体实际操作的单位, 很多时候并不是文件,而是文件内部的结构片段。这里我把这样的结构片段称为语义片段,后文简称片段

一个片段可以是: 函数、 React 组件、类、JSX 代码块、DSL 代码块等,还可以是一个更细粒度、但仍然具备语义边界的代码片段

片段不是简单的字符串片段,也不只是 AST 节点。一个可用于构建反馈的片段,至少应该包含几类信息:

  • 身份标识:它是谁,比如 DashboardPage.tsx#UserCard
  • 结构范围:它在源码中的位置和结构边界。
  • 对外形态:它对外暴露的名字、签名、props、类型结构或导出形态。
  • 依赖关系:它依赖谁、谁依赖它,包括类型依赖、运行时依赖、外部依赖和未知依赖。
  • 反馈目标:它能不能被局部预览、局部验证、局部缓存或局部回滚。

于是,新的问题就变成了:

如果编码智能体的修改天然是片段级别的,那么构建反馈系统是否也应该理解片段?

也就是说,我们不一定每次都把整个文件作为最小反馈单元,而是可以把一个长文件切成多个可识别、可追踪、可替换、可预览的语义片段。

文件仍然存在,但片段开始接住变化。

一个简单的例子

假设我们有一个很典型的 TSX 文件:

  • interface User
  • interface UserCardProps
  • const statusColorMap
  • export function UserCard
  • export function OtherCard

在传统视角里,它们共同属于同一个文件:DashboardPage.tsx。只要这个文件变了,很多工具首先看到的就是 DashboardPage.tsx 发生了变化。

但如果通过 TypeScript Compiler API、Tree-sitter 或语言服务去看,我们看到的就不只是一个文件,而是一组有结构的声明:类型声明、常量声明、函数组件、类、依赖关系等

也就是说,DashboardPage.tsx 在文件系统里仍然是一个文件,但在构建反馈系统眼里,它完全可以被理解成一组更细粒度的语义单元。

这就是片段感知构建的第一层抽象:

文件是存储边界,但不一定是反馈边界。

如果编码智能体只是修改了 OtherCard 里的 JSX,那么理论上我们不一定需要重新理解整个 DashboardPage.tsx。如果编码智能体修改了 User 这个接口, 那么它影响的可能不是运行时代码,而是依赖这个类型的 props、函数签名和类型检查结果。如果编码智能体修改的是 statusColorMap ,那么受影响的就是运行时依赖它的组件。

如果编码智能体修改的是模块级副作用、全局 CSS、装饰器、环境声明、聚合导出、动态导入或初始化顺序相关代码,那就不能乐观地认为它只影响局部。这个时候系统应该回退到文件级,甚至项目级。

于是,构建反馈系统真正需要回答的问题就不再只是:

这个文件有没有变?

而是:

哪个片段变了? 它的对外形态有没有变? 它影响了哪些片段? 哪些预览目标需要更新? 哪些构建产物可以继续复用? 哪些变化不能证明局部安全,必须回退?

如果把这些问题展开,就会发现它已经不是一个简单的 AST 切分问题,而是一个构建计划问题。

系统要从变化片段出发,继续判断对外形态、依赖关系、受影响目标、最小闭包和最终反馈动作。

和现有系统的关系

片段感知构建很容易被误解成“重新发明热更新、增量编译或构建缓存”,但它并不替代这些系统。热更新关注模块替换,增量编译关注最小重算, Bazel、Gradle 关注任务、目标和产物缓存,语言服务关注编辑态语义理解,测试选择关注如何少跑测试。片段感知构建要做的, 是把这些能力继续推进到文件内部的语义单元上。

它真正想回答的是:

当编码智能体修改一个函数、组件、接口或 JSX 代码块时,系统能不能生成一个结构化的更新计划,而不是只告诉下游“某个文件变了”?

这个更新计划需要说明:哪些片段发生变化,哪些片段被影响,哪些预览目标需要刷新,哪些产物可以复用,哪些产物必须失效,以及为什么需要回退。

所以,片段感知构建的价值不在于“又做了一个构建器”,而在于重新定义智能体式编码下的最小反馈单位。

如果说无组件架构重新划分的是组件的运行边界,那么片段感知构建重新划分的就是代码变化的反馈边界。文件继续负责存储,语义片段接住变化,构建器仍然负责生成产物。

一次片段变更应该如何被处理?

一次编码智能体修改后的反馈过程,可以简化成一条链路:

提取片段清单 → 对比历史快照 → 找到变化片段 → 检查对外形态 → 传播影响 → 构造最小闭包 → 经过安全边界 → 生成反馈计划 → 更新预览与构建产物

这里的核心不是解析器,而是更新计划。解析器只能告诉我们“结构在哪里”,真正困难的是:片段身份是否稳定、对外形态如何比较、 类型依赖和运行时依赖如何区分、未知依赖如何处理、最小闭包能否独立运行、副作用会不会破坏局部性,以及什么时候必须回退。

这些问题,才是片段感知构建真正有意思的地方。

实现原型

代码:https://github.com/phodal/piece

一开始我只是想做一个面向 AI 长文件的结构化构建系统:把单个巨型 TSX 文件拆成可预览、可缓存、可重构的语义切片。当时的方式大概是这样:

/goal 实现一个生产级的片段编译器:
面向 AI 长文件的结构化构建系统,把单个巨型 TSX 文件拆成可预览、可缓存、可重构的语义切片。

整体流程:

长 TSX 文件
  -> 增量解析器
  -> 声明级片段清单
  -> 片段关系图
  -> 预览目标解析器
  -> 闭包构造器
  -> 虚拟模块
  -> 构建引擎
  -> 预览 / 构建产物

在经历了几个 /goal 的代码之后,当前的实现里:

  • TS/JS 主要走 TypeScript AST。
  • React 预览会围绕组件片段构造预览目标。
  • Kotlin 回到 Kotlin/JVM 的 PSI / Analysis API。
  • Go 则把编译反馈交给 go build / go test
  • .pic DSL 用来验证片段模型是否能脱离 React 继续成立。

也就是说,Piece 更像是在把不同语言工具链里的事实,收敛成同一套可以追踪、失效和反馈的对象。最后架构图大概是这样:

flowchart TB
    A["片段清单<br/>声明级结构清单"]
    B["片段关系图<br/>类型 / 运行时 / 外部 / 未知依赖"]
    C["快照协调器<br/>变化 / 脏标记 / 受影响"]
    D["安全边界<br/>局部反馈 or 文件级 / 项目级回退"]
    E["闭包构造器<br/>最小可运行闭包"]
    F["虚拟模块<br/>闭包模块 + 预览入口"]
    G["构建引擎<br/>esbuild / Vite / Webpack / go build / test runner"]
    H["预览 / 构建产物<br/>局部预览与缓存产物"]
    A --> B --> C --> D
    D -->|局部安全| E --> F --> G --> H
    D -->|不安全| G --> H
    I["上一次快照"] --> C
    J["产物缓存"] --> C
    C --> K["更新计划<br/>受影响目标<br/>复用 / 失效产物<br/>回退原因"]
    K --> H

这里有一个容易被忽略的点:片段感知构建不能假装所有变化都可以局部执行。它必须先经过安全边界。能证明局部安全,才继续收敛闭包;不能证明,就回到文件级或项目级。

结语:文件之后的反馈单位

从人的编程习惯来看,文件仍然会长期存在。它是命名空间,是目录结构,也是团队协作时最容易达成共识的边界。只是到了智能体式编码时代, 文件已经不再总是最自然的工作单位。

AI 更像是在文件内部移动,找到一块结构,替换一块结构,然后等待系统告诉它:这次修改有没有破坏周围的东西。

所以,我更倾向于把片段感知构建看成下一阶段 AI IDE 的基础设施,而不是一个单独的编译器实验。


或许您还需要下面的文章:

关于我

Github: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806