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 Userinterface UserCardPropsconst statusColorMapexport function UserCardexport 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。 .picDSL 用来验证片段模型是否能脱离 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 的基础设施,而不是一个单独的编译器实验。
或许您还需要下面的文章: