跳转至

第 4 讲:版本管理 (2):从文件快照、变更身份到 Agent 时代的协作与工程起步

导读

如果今天从第一性原理重新设计版本管理系统,我们会把什么作为基本对象?当主要使用者从“单线程”的人类变成能够秒级产出、并行复制的 AI Agent,过去习以为常的工具前提又有哪些需要推倒重想?

本讲承接上一讲对状态保存与并行开发的讨论,从软件工程制品(Artifact)之间的可追踪性(Traceability)切入,剖析为什么把项目装进目录树只是历史延续下的工程便利,以及 Git 的有向无环图快照模型在表达“持续演化的同一项改动”时的局限。

由此,我们将沿着 Jujutsu(jj)引入 Change ID、工作区自动快照与可保存冲突状态(First-class Conflicts)的抽象演进,一路推演到多 Agent 团队中的 git worktree 本地隔离,并探讨从类似多版本并发控制(MVCC)的分叉推进转向事前互斥加锁的并发策略。

最终,这些围绕状态、历史、隔离与整合的思考将落脚到课程实验一“会成长的个人助手”:理解工具背后的工程智慧,学会复用版本管理抽象,并以最小可行产品(MVP)和架构决策记录抵御早期 Prompt 锚定带来的技术债。

机制总览

重写一项改动时,人仍把它看成同一件事,但具体快照或父提交可能已经改变。jj 分开表示这种持续身份和精确版本。

jj 用两个标识区分改动与版本(据本讲绘制)
jj 用两个标识区分改动与版本(据本讲绘制) 下载 PDF 原图

读图:机制与适用边界

change ID X 关联重写前后的两个版本。由 commit C 修订或 rebase 到 C\(\prime\),commit ID 改变,而 change ID 通常保持;两者仍对应具体快照。

稳定的 change ID 有助于跟踪演化,但不替代 commit ID 对具体版本的定位,也不表示改动在任何操作下都永远保持身份。

一堆文件,够不够表示一个项目

上一讲我们学习了如何用版本控制系统保存项目状态、支撑并行开发。进入本讲,我们要从第一性原理重新追问两个根本问题:如果今天从头设计一套版本管理系统,什么才应该是被管理的基本对象?如果软件开发的主要执行者从人类变成了可以快速工作、并行复制的 Agent,过去习以为常的工具假设有哪些必须推倒重来?这些思考不能停留在空谈,而要落脚到具体的工程实践中。围绕课程实验“会成长的个人助手”,我们将审视传统目录树与文件接口的工程便利与历史局限,剖析软件工程制品之间的隐藏关联,探讨软件形态在 AI 时代的演进方向。

从实验一出发:会成长的个人助手

在探讨版本管理系统的底层抽象之前,我们需要一个真实的工程靶场。课程实验一提出了制作“会成长的个人助手”这一需求。面对时髦的 AI 概念,初学者很容易陷入堆砌炫酷功能的误区,或者不加约束地把模糊需求直接丢给大模型。我们需要先明确该实验的核心起点,以及拿什么标准验收“成长”。

从实验一出发:会成长的个人助手(官方课件第 1 页)
从实验一出发:会成长的个人助手(官方课件第 1 页) 查看原图

实验一针对“会成长的个人助手”给出了明确的目标定位:项目应当以“能帮上自己”为切实起点,优先解决自己的日常高频痛点,而不是追求表面上看起来像下一代产品。围绕这一起点,“成长”被赋予了明确的工程验收标准——“这次纠正,下次还能生效”。例如,在日常整理专业课资料、自动填写含学号与简历的文档表格、关联邮箱草拟回复或办理教务大厅事务时,助手不仅要能打通流程,还要能在你指出“不要把报名截止时间当成活动开始时间”后,将这条纠正保存为可阅读、可修改的脚本、技能或记忆,并在后续新会话处理其他通知时真正生效。

为什么要布置这样一个介于明确与不明确之间的实验?讲义提醒我们,如果直接把宽泛的需求丢给 AI Agent 去自由发挥,极大概率会得到一团看似能跑但内部混乱的 AI slop(低质量生成物)。当后续需求变更或出现冲突时,Agent 会在代码泥潭中反复试错、大量消耗 token,重现《人月神话》所描述的软件焦油坑。亲历这种失控并借助 AI 极强的重构能力重新组织架构,正是理解软件工程价值的必经之路。

软件工程制品:项目里究竟装了什么

当着手构建个人助手并尝试摆脱“焦油坑”时,我们首先要看清一个软件项目内部到底包含哪些要素。日常开发中,我们习惯打开文件夹查看源码,但这只是表象。从脑海中的最初想法到最终可运行、可维护的系统之间,究竟生成并沉淀了哪些不同层次的对象?

软件工程制品:项目里究竟装了什么(官方课件第 2 页)
软件工程制品:项目里究竟装了什么(官方课件第 2 页) 查看原图

在软件工程中,开发过程所形成并维护的各类对象统称为软件工程制品(Artifact)。一个完整的个人助手项目除给出具体实现的 Code(也称 Impl,即代码实现)外,还包含多个上游与外围制品:Intent(意图)用于记录我们最初想做什么产品、要达到什么效果;Spec(规范)说明系统具体应当满足什么要求,涵盖架构选择、模块边界与接口定义;此外还有测试(Test)、配置文件、运行期维护的记忆(Memory)以及说明文档。文件接口之所以成功,是因为人类能用编辑器直观读写,编译器、搜索和版本管理工具也能直接接手。

课堂提示我们关注 README 文档在制品体系中的真实角色。许多开发者习惯让 AI 生成冗长且充满套话的 README,但这反而掩盖了核心信息。一份优秀的 README 应当简明扼要地传达项目的核心 Intent、产品效果(例如放两张关键截图)以及最顶层的关键设计。只要原始 Intent 足够清晰,哪怕提出点子的人不了解具体数据库选型或高并发细节,具备架构与编码能力的合作者也能顺着 Intent 补齐 Spec 与 Impl。

目录树的盲区与软件可追踪性

既然文件接口对人类阅读和现有工具链如此方便,为什么我们还要质疑“一堆文件够不够表示一个项目”?当个人助手的需求发生演进时,单纯把文件分门别类放进目录树,会暴露出什么结构性缺陷?对照需求、规范、实现与测试之间的网状关联,可以看出单纯的文件容器丢失了哪些关键信息。

目录树的盲区与软件可追踪性(官方课件第 3 页)
目录树的盲区与软件可追踪性(官方课件第 3 页) 查看原图

“把项目放在文件里”首先是因为现有文件系统和工具生态提供了便利,而数据库等其他存储形式对人类阅读和传统文本工具不够友好。然而,项目本身并不天然是一棵目录树,文件结构尤其不擅长直接表达制品之间的网状关系——即软件可追踪性(Software Traceability)。可追踪性关心的是能否识别并持续维护从需求到设计、实现、验证,再从代码反查回需求的双向联系。例如图中“记住用户纠正过的偏好”这一需求,同时关联着具体的规格说明、更新记忆的函数以及几组测试用例。当用户新增“某些偏好仅在特定时间段有效”的要求时,我们不仅需要知道哪个文件变了,还必须追踪哪些设计假设受影响、哪些实现要调整、哪些测试需补充。

回溯到 1950 至 1960 年代,早期操作系统只提供文件系统而尚未发明关系型数据库,程序员将项目装进目录树只是历史延续下的路径依赖。这种“将错就错”在演进中极易产生技术债(Technical Debt,即因早期权宜或不一致导致未来修改需额外付出的代价)。例如课堂所举的例子:你让 AI 将代码底层的数据结构从链表(linked list)优化为树(tree),AI 迅速改完了代码,但某份 Markdown 文档里依然写着“使用 linked list 管理数据”。由于缺乏强制关联机制,聊天记录消失后,后续维护者与 AI 将无从判断究竟是代码正确还是文档正确。

将错就错之后:AI 时代的软件形态设想

既然目录树无法天然维护制品间的双向关联,为什么工业界没有抛弃文件,反而围绕它构建了越来越庞大的生态?如果未来人类不再逐行手写代码,软件的底层容器与交互方式会发生怎样的范式转移?跳出围绕文本文件积累的路径依赖,我们可以推演 AI 时代软件形态的变革方向。

将错就错之后:AI 时代的软件形态设想(官方课件第 4 页)
将错就错之后:AI 时代的软件形态设想(官方课件第 4 页) 查看原图

从早期的源码压缩包,到集中式的 SVN,再到分布式的 Git,基于文件的工具链蓬勃发展。因为全套 UNIX 文本工具、编译器与人类习惯都建立在文件之上,人类在工程上只能“将错就错”,靠需求编号、文档链接、提交说明和测试命名手工修补可追踪性。由此可以进一步推演:在 AI 时代,当 Agent 成为软件的主要生产者和使用者时,“软件”的组织形态可能会发生根本性变化,需求、证据、实现及其相互关系将有机会比“这段文字放在哪个文件里”更直接地进入系统的基本模型。

顺着这一推论,老师在课堂上提出了一个面向未来的方向性设想——repo.db。如果将文档切片、代码实体、测试用例及其相互关系显式表示为带环图(Graph)中的实体(Entity)与关系(Relation),并存入数据库 repo.db,虽然人类难以用传统命令行工具直接维护这样的图数据库,但这一设想希望引入一致性校验作为强化学习的反馈信号,训练 AI 维持制品间的同步,目标是做到“文档写链表、代码实现就保持链表,文档要求测 A/B/C、测试用例就覆盖 A/B/C”(仅靠存成图结构或模型训练本身并不会自动保证一致,仍依赖明确的约束检查与反馈闭环)。进一步借用数据库的视图(View)概念,当我们想做一小步修改(如调整幻灯片样式或修一个局部 bug)时,AI 可以从庞大的图中动态抽取相关制品,按需生成仅针对当前任务的交互看板,并在修改后将系统从一致状态 V0 推进到下一个一致状态 V1。

软件可追踪性(Software Traceability)

软件可追踪性是指识别并持续维护软件工程制品(需求、规格说明、代码实现、测试用例、配置与文档等)之间关联关系的能力。它不仅回答“哪个文件发生了改动”,更用于正向追踪某项需求如何落地为设计与测试,以及反向核查某段代码究竟服务于哪项原始意图、受哪些设计假设约束。

从经典计算机科学概念汲取创新灵感

课堂关于 repo.db 的设想并非凭空捏造,而是直接复用了经典系统课程的核心抽象:用数据库中的实体(Entity)与关系(Relation)表达制品图谱,用视图(View)隔离局部修改所需的最小上下文,再结合 Git“小步前进、保持状态一致”的版本演化哲学。即使未来不用手写 SQL,扎实的底层概念依然是重新设计 AI 时代工具链的基石。

文件目录的现实便利与图仓库的适用前提

现有文件系统与目录树在当前人类编辑器、编译器和 Git 生态中依然是直接可用的工程载体,因此实验一推荐用 Markdown 加目录结构管理个人数据库,方便直接观察和调整。而 repo.db 图结构仓库则需要配套的制品关系提取、一致性检查器与视图生成工具,适合作为面向全 Agent 生产流程的探索方向。

术语消化:软件项目组织与核心概念对照

术语 解决的问题 机制与课程关系
软件工程制品(Artifact) 界定软件开发过程中产生并需维护的各类对象 涵盖 Intent(意图)、Spec(规范)、Impl/Code(实现)、测试与文档等,是项目管理的实质内容
软件可追踪性(Traceability) 消除需求、文档、代码与测试在演进中的脱节 建立并维护制品间的双向网状关联,评估需求变更的影响范围并防止认知不一致
技术债(Technical Debt) 解释早期权宜设计或未同步修改为何拖慢后续开发 当代码结构已改动(如链表改树)而文档未同步,或早期误解被固化进实现时,后续修改需付出额外代价
视图(View) 在复杂系统的小步修改中屏蔽无关的上下文干扰 借用数据库概念,由 AI 从底层制品图中动态抽取与当前修改任务相关的切片,生成可视化交互看板

本章小结

  • 本章从实验一“会成长的个人助手”切入,指出项目开发应以“能帮上自己”和“纠正后下次生效”为起点,警惕无约束生成带来的 AI slop。
  • 随后我们剖析了软件项目中的各类制品(Intent、Spec、Code、Test 等),揭示出用目录树组织项目只是历史延续下的工程便利,它丢失了制品间的可追踪性(Traceability)。
  • 最后,我们探讨了在 AI 时代将制品关系显式存入图数据库并按需生成视图的未来设想。

从快照理解分叉、合并与第一性原理学习法

在理解了文件系统只是软件制品的一种存储容器之后,我们需要进一步审视当前最主流的版本管理工具 Git 究竟在文件之上建立了怎样的抽象模型。当多个开发者或智能体从同一个项目状态出发并行工作时,各自演进的快照历史如何组织成有向无环图?两条分叉的开发路线在重新汇合时,合并与变基分别代表了怎样的历史构造策略?面对课堂上未曾详尽展开的工程概念,我们又该如何打破被动等待的应试习惯,从第一性原理出发主动构造实验场景,建立起属于自己的系统理解?

Git 快照模型、有向无环图与并行整合策略

当项目采用文件集合作为底层载体时,版本控制系统需要回答两个基本问题:如何记录某一时刻项目的完整状态,以及如何表达状态随时间的演进关系。我们需要理清 Git 管理的核心对象层次与小步提交推进模式,并对比多人在同一快照上并行开发时,Merge 与 Rebase 两条整合路径在历史图结构上的差异。

Git 快照模型、有向无环图与并行整合策略(官方课件第 5 页)
Git 快照模型、有向无环图与并行整合策略(官方课件第 5 页) 查看原图

Git 的核心管理对象包括文件内容、目录树、快照(Snapshot)以及快照之间的历史关系。一次 commit(提交)不仅指向当时的文件目录树状态,还包含作者、提交说明以及指向父提交(Parent Commit)的指针,父子指针将整个项目的演进历史连接成一张有向无环图(Directed Acyclic Graph,简称 DAG)。单线开发表现为从快照状态 A 出发,完成一项功能修改并附带形如 feat: ... 的提交说明,推进到新状态 B。当不同开发者基于同一快照 A 分别开发时,历史发生分叉。为整合分叉成果,Merge A, B -> C 会创建一个同时连接两条历史路线的新提交 C;而 Rebase(变基)则是将一条路线上的修改依次重放到新的起点之上,使这些改动看起来像是在新基础上顺序完成的。

讲义与课堂口述进一步剖析了两种整合路径的工程细节与冲突本质。假设 A 先后修改了文档与测试(形成两个快照增量),B 先后添加了功能与测试。若采用 Merge 汇合,原有的提交 ID 均得以保留,但若发生冲突并由 A 动手解决,B 所写代码行在表面归属追溯上可能变成由 A 提交。若采用 Rebase,系统会尝试将 B 的功能增量与测试增量依次搬运到 A 的最新提交之后以维持线性历史。默认情况下,Git 仅在重放遇到文本或目录树冲突时暂停并要求人工介入;即使文本无冲突地重放成功,若 A 先前加入了与该功能不兼容的测试或改了隐含约定,未显式配置 git rebase --exec 运行测试时 Git 不会自动拦截,但后续测试流水线仍会失败。同时,重写基底会改变父指针并为每一步生成全新的 commit ID。此外,讲义提醒我们区分文本冲突与语义冲突:例如同一配置项原为“立即保存”,一边改为“手动确认后保存”,另一边改为“每分钟自动保存”,工具虽能报错文本分歧,但最终保留何种行为必须回到需求本身;即便两处修改位于不同文件而能被工具自动合并,组合后的系统行为仍可能违背双方原有的设计假设。

打破“学生思维”与第一性原理学习法

理解了分叉与变基的机制后,一个现实的教学问题随之浮现:为什么许多计算机专业的学生在日常学习中对 rebase 和 merge 依然感到陌生?对照常见的被动学习心态,我们可以看清课堂教学边界与真实软件工程世界之间的差距,从等待划重点转向主动构建理解。

打破“学生思维”与第一性原理学习法(官方课件第 6 页)
打破“学生思维”与第一性原理学习法(官方课件第 6 页) 查看原图

不少同学对版本控制操作的陌生,源于长期应试形成的“学生思维”——高度依赖外部划定的范围,认为“老师不教的就是我不需要会的”,“考试不考的就是我不用学的”。大学与工程学习的目的则是基于第一性原理(First Principles,即回归事物最基本的条件与核心问题进行推导的思维方法)形成自己的理解。

课堂上曾展示过一条很有代表性的学生留言:学生时代大多数人独自写作业,很少与他人合作,因此不熟悉 rebase 和 merge 很正常。这一解释虽符合个人经历,却把“眼前没用到”等同于“不需要知道”。一旦走出课堂进入开源社区,几乎每个成熟项目的根目录下都有一份 CONTRIBUTING.md(贡献者指南),其中往往明确规定了分支管理规范,例如要求贡献者在提交合并请求前必须在自己的分支上执行 rebase 以保持主干线性历史。面对这类课堂未曾手把手讲授的概念,第一性原理学习法要求我们先追问“这个机制试图解决什么问题”,随后在本地建立一个包含两三个提交的最小实验仓库,亲自制造分叉、触发冲突并执行历史重写,通过观察每一步留下的对象状态来验证自己的解释。这也直接引出了下一章的追问:既然我们在变基时真正关心的是“搬运修改”,为什么 Git 却总在迫使我们操作快照提交?

Git 的 DAG 快照模型与小步推进

Git 并不以补丁链表作为底层存储,而是将每次提交保存为目录树的完整快照,并通过指向父提交的指针构成有向无环图(DAG)。工程实践中提倡 A + feat -> B 的小步原子提交:每次只推进一个明确的意图。这不仅让代码审查与历史阅读更加清晰,也为后续分支合并、变基重放以及故障回溯提供了粒度合理的稳定锚点。

自动合并通过不等于语义兼容

切忌将合并冲突仅仅简化为同一文件同一行的文本重叠。即使两名开发者分别修改了不同文件,使得 Git 能够毫无报错地自动完成 Merge 或 Rebase,两路代码在逻辑上仍可能互相破坏各自的设计假设(例如一方修改了数据格式的隐含约束,另一方新增了依赖旧约束的展示逻辑)。裁决冲突与验证合并结果,最终必须回到产品需求与自动化测试中去检验。

从 CONTRIBUTING.md 开启最小实验学习

在真实开源工程中,Merge 与 Rebase 并无绝对的优劣之分,而是分别服务于“保留真实分叉历史”与“维持线性整洁历史”的不同团队偏好,具体规范通常写在仓库根目录的 CONTRIBUTING.md 中。遇到不熟悉的命令或规范时,不必等待课程安排,直接在空目录中初始化仓库、模拟两条分支的冲突修改并观察历史图的变化,是摆脱应试思维最直接的途径。

术语消化:Git 并行开发与历史整合核心概念对照

术语 解决的问题 机制与课程关系
Commit(提交快照) 记录项目在某一时刻的完整状态及演进上下文 保存目录树快照、作者、说明及父提交指针,将项目演进连接成有向无环图(DAG)
Merge(合并) 将两条分叉的开发路线汇合为一个共同状态 创建同时指向两边历史的双亲提交以保留真实分叉轨迹,但解决冲突时可能改变代码行表面的归属追溯信息
Rebase(变基) 将一条分支上的改动迁移到另一基底以维持线性历史 将一系列提交相对于父节点的增量依次重放到新起点上,会重写历史并生成新的 Commit ID
CONTRIBUTING.md(贡献者指南) 对齐开源项目或团队的协作、分支与提交规范 位于仓库根目录的说明文档,常明确规定使用 Merge 还是 Rebase,是走出课堂接触真实工程规范的入口

本章小结

本章从 Git 的快照与有向无环图(DAG)模型出发,阐明了小步提交如何推进项目状态,并深入对比了并行开发中 Merge(双亲提交汇合历史)与 Rebase(重放增量以保持线性历史)的运作机制,强调冲突裁决不能仅看文本重叠而必须回归需求语义。

同时,本章剖析了被动等待教学的“学生思维”,倡导阅读开源项目的 CONTRIBUTING.md 并利用最小实验场景从第一性原理构建理解,为下章探讨“变更身份”奠定了基础。

把变化的身份纳入版本管理:从 Git 到 Jujutsu

上一节我们梳理了基于快照的分叉与合并机制。然而在日常开发中,当我们在不同分支间挑选改动或将提交序列变基到新起点时,Git 的操作模型常常让人产生一丝概念上的“不和谐”。我们在直觉上关心的是“某次修改(Change)”的流转与修订,尽管 Git 支持操作分支与标签等引用,但在定位具体历史版本时,底层对象标识仍是静态提交快照的哈希值(Commit ID)。只要父节点或元数据发生变化,重写后的提交就会得到全新的 Commit ID,导致同一项改动在工具层面的身份发生断裂。本章将剖析这一抽象错位,并探讨现代版本控制系统 Jujutsu(简称 jj)如何把“随时间演化的改动”提升为一级公民,以及它如何通过工作副本即提交、可保存冲突等机制重塑版本管理体验。

快照标识与修改意图的错位

理解了 Git 的有向无环图快照模型后,我们需要审视一个日常高频场景:当开发者要把某次修改搬运到另一个分支,或者把一整条开发路线变基到新的主干起点时,人类的心智模型与 Git 的底层标识之间究竟存在怎样的张力?这里需要区分两个不同维度的问题:“某一个静态版本的准确状态是什么”,以及“某一项持续演进的修改经历了哪些版本”。

快照标识与修改意图的错位(官方课件第 7 页)
快照标识与修改意图的错位(官方课件第 7 页) 查看原图

Git 在执行 cherry-pick(挑选提交)或 rebase(变基)等操作时,经常需要处理两个 commit 之间的差异(delta,即 change)。虽然 Git 也能操作分支或标签等引用,但在指代具体历史节点时,我们传给工具的核心标识仍然是 commit。这就带来了一处轻微的“不和谐”:开发者嘴上说的是“把这次修改搬到另一个版本上”,心里关心的是 change 本身;但在 Git 的对象模型中,commit 的身份(Commit ID)是由目录树快照、父提交哈希、作者及时间戳等元数据共同哈希计算出来的。一旦换了起点,这处修改在工具眼里还是不是“同一次修改”?

讲义进一步拆解了这一现象:假设开发者A修改了文档并添加了测试,开发者B在另一条线上添加了新特性(Commit C)。当执行 rebase 把 C 搬运到 A 的最新提交之后时,由于父节点发生了改变,Git 必须创建一个全新的提交对象 \(C'\)(重写后的 C),并赋予它一个完全不同的 Commit ID。人在语义上清楚 C 和 \(C'\) 是在做同一件事,但单靠 Commit ID 无法表达这种跨越重写的身份延续。要消除这种错位,就需要把“某项改动随反复修改经历了哪些版本”直接纳入工具的基本设计。

将 Change 提升为一级公民

既然单靠绑定了父节点与元数据的 Commit ID 无法在历史重写后维持同一项修改的身份,那么如果我们重新设计版本管理系统,把“修改(Change)”本身作为一级公民(First-Class Citizen)纳入核心抽象,系统会变成什么样?Jujutsu(jj)通过双标识机制,将“精确版本状态”与“持续演化的改动身份”拆分开来。

将 Change 提升为一级公民(官方课件第 8 页)
将 Change 提升为一级公民(官方课件第 8 页) 查看原图

顺着“Rebase 不应该让同一次 change 失去身份”的推演,便走到了版本控制系统 Jujutsu(简称 jj)的核心设计:引入独立的 change ID 来追踪一项随时间持续演化的改动,同时保留 commit ID 用来标识某一次具体的修订版本。当一个提交被 amend(修订)或 rebase(变基)到新的父节点上时,由于底层内容或父节点改变,它的 commit ID 会发生变化,但它的 change ID 通常保持不变。因此,同一次修改在生命周期中可以先后对应多个不同的修订版本(如从 C 演化为重写后的 \(C'\))。

讲义提醒我们注意底层的实现边界:jj 所说的 change 指的是“随时间演化的提交”,它在数据模型中并不是把系统退回成早期纯补丁文件(diff/patch)的集合,其底层依然完整保存文件目录的快照(Snapshot),且能够兼容并使用现有的 Git 仓库。此外,根据官方术语定义,change ID 是附加在提交上的一项属性;若同一项改动在不同分支或并发操作下被分别重写且未及时汇合,同一个 change ID 也可能同时对应多个可见提交(称为分歧变更,divergent change)。因此,追踪演化过程靠 change ID,而需要精确锁定唯一源码快照时仍应依赖 commit ID。

消除暂存区与一等公民冲突状态

当“随时间演化的 Change”成为核心抽象后,版本管理系统中另外两个长期困扰开发者的痛点——“尚未写完的半成品代码无处安放”以及“变基中途遇到冲突导致整个工作流被迫卡死”——能否顺理成章地一并解决?这正是 jj 的工作副本(Working Copy)模型与一等冲突(First-class Conflicts)机制要解决的问题。

消除暂存区与一等公民冲突状态(官方课件第 9 页)
消除暂存区与一等公民冲突状态(官方课件第 9 页) 查看原图

jj 将未完成工作与冲突状态一并纳入了提交模型。

  • 第一,连独立的暂存区(staging area / index)都不再需要了:在 jj 中,当前工作副本(Working Copy)本身就直接对应一个可以持续修订的 commit;大多数 jj 命令在执行开始时,都会自动把当前工作区的文件变化记录为新的快照并更新这个 commit,使用体验仿佛一直在自动执行 commit + amend。
  • 第二,是将冲突直接存进 commit(即课堂强调的 killer feature):当 rebase 遇到冲突时,操作可以直接完成而不必卡在半途;系统保存的是冲突的逻辑表示,且带着冲突的 commit 依然能继续参与后续的 rebase 或 merge。

课堂上用了一个生动的例子对比 Git 的痛点:假设你正在修改 hello.c,代码改到一半还无法编译,导师突然打来电话要求一分钟内紧急修复一个客户高优 Bug;你不应直接创建正式提交,而是执行 git stash 将当前工作区和暂存区中已跟踪文件的未提交修改临时压栈(git stash 可直接保存已跟踪文件的未暂存修改,无需先执行 git add,但课堂提及放进 index 反映了 Git 用户在工作区、暂存区与 stash 栈之间倒腾的心智负担);结果新 Bug 刚修到一半,导师又来电话插队另一个更紧急的任务,你只能再次 git stash,最后不得不面对多层难管的 stash 栈。而在 jj 中,工作区本身就是已记录的 change,随时切换到新修订即可处理插队任务,无需手动维护 stash 栈。同时,一等冲突设计把“调整历史拓扑结构”和“决定冲突处的最终业务内容”解耦,让“尚未解决”也成为一种可保存、可继续操作的正规状态。

破除定势效应与反向学习法

面对 Jujutsu 这样概念更正交、操作更自然的现代工具,为什么许多经验丰富的资深开发者依然习惯停留在 Git 的繁琐流程中?作为在 AI 时代成长起来的新一代学习者,我们应当采取怎样的技术学习路径?结合认知心理学中的定势效应(Einstellung effect),我们可以看清旧工具的肌肉记忆如何限制技术视野。

破除定势效应与反向学习法(官方课件第 10 页)
破除定势效应与反向学习法(官方课件第 10 页) 查看原图

老师拿自己开玩笑,解释了为什么 AI Native 的新一代有机会超越资深开发者:老一代开发者已经在长期工程实践中形成了坚固的 Git 肌肉记忆(更早一代人则是 SVN 的肌肉记忆)。即便能从第一性原理理解 jj 的设计,往往也没有足够时间去重新训练另一套习惯。2008 年针对国际象棋棋手的眼动实验验证了这种定势效应(Einstellung effect):当棋盘上存在一个熟悉的经典套路和另一个更优的新解法时,哪怕棋手主观上声称自己正在寻找更优解,他们的目光仍围绕旧套路打转。因此,初学者的学习方向可以反过来:先理解更自然的现代抽象,再回望历史。

讲义总结道,具体某一款工具的命令熟练度终究会随技术演进而过时,但对底层设计前提的洞察却能迁移到下一代系统中。Git 诞生之初主要致力于用持久数据结构和合并(merge)解决分布式协作问题,当时并未充分预料到后来社区对高频变基(rebase)和追踪改动身份的强烈需求。同学们趁着尚未被旧工具的妥协设计固化思维,完全可以在 Agent 的辅助下,先从“把 Change 作为一级公民”的现代抽象入手建立直觉,再回过头审视 Git 为什么会有暂存区和变基冲突锁死等历史包袱,从而更高效地掌握软件工程的本质演进规律。

区分 Commit ID 与 Change ID 的职责边界

在 Jujutsu(jj)中,Commit ID 与 Change ID 分别回答两个正交的问题:Commit ID 标识某一次具体修订的精确快照状态(包含文件内容、父提交与元数据),一旦历史被重写(如 amend 或 rebase),就会生成新的 Commit ID;Change ID 则作为提交的一项属性,用于追踪同一项修改意图在反复修订过程中的身份延续。若操作导致同一 Change ID 衍生出多个未合并的可见版本(即 divergent change),精确绑定源码版本时仍须使用 Commit ID。

课堂提示:把异常过程转化为可保存的一等状态

在传统 Git 流程中,变基中途发生冲突被视为一种“异常中断”,开发者被迫进入特殊的 rebase 模式立即排障,否则无法继续调整后续历史。jj 的核心设计启发在于:将“尚未解决的冲突”明确表示为一种可持久化保存在提交内部的逻辑状态(First-class Conflicts)。当原本临时的过程状态变成一等公民后,历史拓扑调整与代码语义裁决得以解耦,系统流程因此大幅简化。

易错辨析:底层依然是快照,可保存冲突不等于自动解决

学习 Jujutsu 时需避免两类常见误解:第一,jj 将 Change 作为一级公民,并不意味着底层退回了 SVN 式的纯补丁(diff/patch)集合,其底层依然保存完整的文件目录快照;第二,jj 支持将冲突直接存入 commit 并允许继续 rebase 或 merge,这只是保存了冲突的逻辑表示以防阻塞历史操作,绝不等于工具能替开发者自动裁决业务语义,最终冲突内容仍需人工或 Agent 后续显式解决。

术语消化:Git 与 Jujutsu 核心版本管理概念对比与机制消化

术语/概念 解决的核心问题 底层机制与课程关系
Commit ID(提交标识) 精确定位某一时刻的代码树状态与历史上下文 基于目录快照、父提交哈希及元数据计算生成;只要父节点或内容在变基、修订中改变,就会生成全新的 Commit ID。
Change ID(变更标识) 在 rebase 或 amend 重写历史后延续同一项改动的身份 作为提交属性追踪随时间演化的同一项修改;重写时 Commit ID 改变而 Change ID 通常延续,底层仍存快照而非纯补丁。
Working Copy Commit(工作副本提交) 消除未完成代码无处安放及多层 git stash 压栈管理难题 将当前工作区直接映射为一个可持续修订的提交,多数 jj 命令执行前自动快照更新该提交,从而省去独立的暂存区(staging area)。
First-class Conflicts(一等冲突状态) 避免 rebase 途中遇到冲突时将整个历史重写流程卡死在半途 将冲突的逻辑表示直接保存在重写后的提交中(写出到文件系统时转化为冲突标记),实现“调整历史拓扑”与“解决冲突内容”的解耦。

本章小结

本章从 Git 执行 cherry-pick 与 rebase 时的概念错位出发,揭示了静态快照标识(Commit ID)无法表达跨历史重写的修改身份延续问题。

通过引入 Jujutsu(jj)的设计哲学,我们看到将 Change 提升为一级公民后,系统不仅能用通常保持稳定的 Change ID(并发重写下亦可能产生分歧版本)追踪持续演化的改动,还能将工作区统一为可持续修订的提交以省去独立暂存区,并将冲突转化为可保存在提交内的一等逻辑状态,从而解耦历史结构调整与冲突内容裁决。

当开发者可以并行复制:从单线程人类到 Agent Worktree

在上一章中,我们看到了 Jujutsu 如何通过引入变更身份和一等冲突状态来优化版本管理抽象。然而,无论是 Git 还是 Jujutsu,它们的底层交互假设很大程度上依然围绕人类开发者展开。当软件生产的主体从单线程的人类变成可以高速运转、随时并行复制的 Agent 团队时,过去依靠单个工作目录配合分支切换和临时暂存的开发模式为什么会迅速失效?面对多个 Agent 同时读写带来的环境踩踏,我们又该如何利用现有工具在本地实现轻量级的并行隔离?本章将从人类与 Agent 的并发特征对比出发,剖析多工作区隔离的工程机制。

单线程人类与单工作区假设

在重新审视版本管理系统的并发设计之前,我们需要先追问一个基本前提:为什么过去几十年里,一个代码仓库默认只对应磁盘上的一个工作目录就足够了?从操作系统的并发与输入输出模型出发,可以看到人类开发者的生理限制如何塑造了传统版本控制工具的日常工作流。

单线程人类与单工作区假设(官方课件第 11 页)
单线程人类与单工作区假设(官方课件第 11 页) 查看原图

人类在操作计算机时类似单线程(Single-Threaded)执行体:每个开发者只有一双手、一张嘴和一对眼睛,即仅有一套输入输出设备。尽管理智上可以同时挂念多项任务,但人在同一时刻与开发环境实际交互的带宽非常有限。围绕这一前提,Git 提供的单工作区模型配合辅助命令已基本够个人使用:当手头任务未完成却遇到紧急插队需求时,可以使用 stash(暂存栈,用于将工作区尚未提交的本地修改临时收起并压栈保存,使工作目录恢复干净以便先处理其他任务的机制)将修改收起;当需要探索不同方案时,可以使用 branch(分支,指向特定提交的可移动引用)切换上下文推进;而在 Jujutsu 中,由于工作副本本身就是可持续修订的提交,这种单人切换甚至更加顺畅。

课堂上用操作系统中的单线程读写与物理屏障做了一个生动的类比:人类编写代码、产生提交是以小时或数十分钟为单位的,一个十几人的模块团队平均也要十分钟左右才产生一次提交。这个速度远慢于工位之间花十几秒口头招呼一声的协调速度,而且紧急任务插队时人类可以拒绝或自行排队,因此 stash 栈通常只有浅浅一层。然而,一旦开发主体变成可并发执行的 Agent,这个低频、单通道的前提就不再成立。

Agent Swarm 带来的并发冲突与节奏突变

当我们将编码任务交给人工智能时,变化的不仅是单个开发者的敲字速度,更是开发者的组织形态。如果我们在终端里同时拉起多个 Agent 组成团队,传统的单工作目录模型会立刻遭遇什么底层危机?高速并行下的文件读写竞态会立即暴露出来。

Agent Swarm 带来的并发冲突与节奏突变(官方课件第 12 页)
Agent Swarm 带来的并发冲突与节奏突变(官方课件第 12 页) 查看原图

从单人开发到 Agent Team(亦称 Swarm,即由多个可并行协同或分工执行任务的智能体组成的开发集群),开发节奏发生了量级变化。首先,单个 Agent 的工作速度显著快于人类,对于范围合适的修改,往往在分钟级甚至数十秒内就能推进到可生成原子提交的程度;其次,Agent 可以随时并行复制。比如在同一个项目中,一个 Agent 负责界面,一个负责数据处理,另一个补充验证。如果它们共用同一个工作目录,Git 为人类设计的单工作区模型便跟不上这个速度:Agent A 刚刚读取或正在修改的文件,可能随即被 Agent B 改写,或者测试正在运行时底层实现就已经发生了变化。因此,需要一起加速的不仅是编码,还有协调、验证与整合。

给每个 Agent 起一个不同的名字,或者在多个终端会话里分别运行 Codex 与 Claude,并不会自动为它们提供独立的开发环境。正如操作系统中多个线程在不加保护的情况下同时读写同一片共享内存会导致数据损坏一样,多个高速运转的 Agent 在同一个工作目录中交错执行文件读写与构建测试,极易读到半成品状态或互相覆盖修改。要让多 Agent 团队正常工作,第一步就是为它们重建文件系统层面的环境隔离。

利用 Git Worktree 实现本地轻量级并行隔离

既然多个 Agent 共用同一目录会互相踩踏,最直观的解决思路是什么?如果不想为每个 Agent 都配置沉重的独立虚拟机和远程仓库同步流程,能否在单机本地实现低开销的工作区隔离?Git 提供的 git worktree 正好用于在单机本地实现低开销的多工作区隔离。

利用 Git Worktree 实现本地轻量级并行隔离(官方课件第 13 页)
利用 Git Worktree 实现本地轻量级并行隔离(官方课件第 13 页) 查看原图

要隔离多个 Agent 的工作环境,最直接的做法是把每个 Agent 当成独立开发者,各自分配一台开发计算机或容器,再通过远程仓库(Remote)互相拉取和推送来同步成果。而为了把这个流程轻量地放到本地,Git 提供了 git worktree(多工作树,允许同一个本地 Git 仓库同时挂载多个独立工作目录的机制)。在多工作树模式下,每个 worktree 都拥有自己独立的工作文件、HEAD(标识当前检出的分支或提交的指针)以及 index(暂存区);同时,它们在底层共享同一个仓库的对象库和大部分分支引用。例如执行 git worktree add -b agent-a ../gense-agent-a 与 git worktree add -b agent-b ../gense-agent-b,即可为两个 Agent 分别创建独立分支与工作目录。这样各自的提交在本地直接可见,无需互相 push 或 pull,但分叉的修改依然需要通过 merge 或 rebase 才会汇合。

课堂演示进一步剖析了 git worktree 的底层实现:打开子工作目录中的 .git,会发现它并不是传统包含 objects 和 logs 的目录,而是一个只有一行 gitdir: 路径指针的普通文本文件,指向主仓库 .git/worktrees/ 下的独立管理目录。这种设计复用了操作系统的文件机制,避免了复制整个对象库。讲义同时提醒,worktree 虽然消除了工作文件互相覆盖的问题,但并没有消除任务间的逻辑依赖:界面等待什么数据格式、记忆模块何时提供接口、双方是否遵循相同的错误处理约定,依然需要显式协调与管理。

Git Worktree 的共享与隔离边界

在使用 git worktree 组织多 Agent 并行开发时,必须准确区分哪些状态是隔离的、哪些是共享的。

每个关联的工作目录都拥有各自独立的工作区文件、独立的 HEAD 指针以及独立的暂存区(index),因此不同 Agent 在各自目录中修改文件、暂存和运行测试不会发生文件级踩踏。

与此同时,所有 worktree 共享底层仓库中的对象库(objects)和大部分引用,新产生的提交在本地直接互相可见,省去了跨仓库网络传输开销。

课堂提示:子工作目录中 .git 文件的底层巧思

在常规 Git 仓库中,根目录下的 .git 是一个存放全部版本对象、日志与配置的文件夹。而在通过 git worktree 创建的子工作目录中,.git 其实是一个纯文本文件,里面仅包含一行形如 gitdir: 的指针,指向主仓库 .git/worktrees/ 下对应的子目录。那个子目录保存着该工作树专属的 HEAD、index 和 logs,而把沉重的历史对象库留给主仓库统一管理。这种轻量级指针机制非常适合在本地快速拉起多个 tmux 会话供不同 Agent 并行工作。

工作区隔离不等于免除合并与接口协调

切勿误以为给每个 Agent 分配了独立的 worktree 就解决了所有并发协作问题。首先,虽然本地提交直接可见、无需执行 push 和 pull,但不同 Agent 在各自独立分支上推进的修改,最终仍然必须通过 merge 或 rebase 才能整合到一起,期间依然可能发生代码冲突。其次,文件隔离无法自动解决跨模块的架构耦合:如果负责界面的 Agent 与负责记忆模块的 Agent 对数据格式或错误处理约定理解不一致,集成时同样会失败。

术语消化:单工作区与多 Agent 隔离开发机制对比

术语 解决的问题 机制与课程关系
git stash 单人单工作区下,手头修改未完成却需临时切换处理紧急任务的上下文保存问题 将当前工作区与暂存区尚未提交的修改收起并压入栈中,使工作目录恢复干净;适用于低频切换的单线程人类,无法支撑多 Agent 同时读写
Agent Swarm (Agent Team) 突破单个人类开发者的带宽限制,实现多任务高速并行推进与交叉审查 由主 Agent 规划调度、多个子 Agent(如在不同 tmux 会话中运行的模型实例)并行执行编码与验证;其分钟乃至秒级的提交速度迫使版本管理重构并发隔离机制
git worktree 避免多个并行 Agent 共用同一目录导致文件互相覆盖,同时免去克隆多份完整仓库与远端同步的开销 为同一个本地仓库挂载多个工作目录,各目录拥有独立的工作区、HEAD 和暂存区,底层共享对象库和大部分引用,提交本地直接可见但汇合仍需 merge 或 rebase
为并行工作的多 Agent 创建本地独立工作树
# 在主仓库目录下,为负责界面的 Agent 创建分支与独立工作目录
git worktree add -b agent-ui ../project-ui

# 为负责记忆模块的 Agent 创建分支与独立工作目录
git worktree add -b agent-memory ../project-memory

# 查看当前仓库关联的所有工作树状态
git worktree list

输入为已初始化的本地 Git 仓库及目标分支名、外部目录路径。执行后,Git 会在父目录下生成 ../project-ui 和 ../project-memory 两个独立目录,分别检出新分支 agent-ui 和 agent-memory。两个 Agent 可分别进入对应目录独立修改文件、运行测试并产生提交;由于底层共享对象库,双方提交无需 push/pull 即可在本地仓库中直接看到,待各自验证通过后再通过 merge 或 rebase 整合。

本章小结

本章探讨了当软件开发主体从单线程人类转变为可并行复制的 Agent 团队时,版本管理在工作区层面面临的挑战与应对机制。

  • 人类受限于单套输入输出设备与较慢的产出节奏,依靠单工作目录配合 branch 和 stash 即可满足日常切换;而 Agent 团队不仅能在分钟级生成原子提交,还能多实例并行运转,若共用同一目录极易引发文件读写竞态。
  • 通过引入 git worktree,我们可以在本地为同一仓库挂载多个拥有独立工作文件、HEAD 和暂存区的工作目录,同时共享底层对象库与大部分引用。
  • 这消除了本地并行开发的文件踩踏与远端同步开销,但分支合并与跨模块接口约定仍需显式协调,这也引出了下一章关于事后合并与事前加锁的并发策略权衡。

先分开做再合并,还是先上锁:面向 Agent 的并发控制重构

前面我们借助 git worktree 为并行工作的 Agent 提供了隔离的本地目录,但这只是把“单人开发者”模式复制了多份。当十几个甚至几十个同模型克隆体 Agent 能够以秒或分钟级速度产生提交时,沿用人类时代“先各自修改、事后合并解决冲突”的流程会面临高频冲突与返工。如果执行者变成不会“被开除或坐牢”的 Agent,历史追溯与并发协调的目标会发生什么变化?在多版本事后合并与事前互斥加锁之间,我们应当如何借用操作系统与数据库的经典理论重新权衡?

从追责到恢复:重审版本控制的人类学前提

为什么传统版本控制如此强调小步快照与责任追溯?在阅读本页前,我们需要先审视工具背后的人类学假设:人类的生产速度以小时计,彼此之间存在沟通损耗(communication gap),且沟通过后仍会误解和遗忘。当执行主体替换为无须承担人事责任的 Agent 时,历史记录的核心价值会发生怎样的转移?

从追责到恢复:重审版本控制的人类学前提(官方课件第 14 页)
从追责到恢复:重审版本控制的人类学前提(官方课件第 14 页) 查看原图

正因为人的生产效率有限、沟通效率低且容易出错,工程实践才发展出小步提交快照的规范,并配套了 bisect 与 blame 等机制。git bisect 利用一组可重现的好坏判断,通过二分查找逐步缩小引入问题的提交范围;git blame 则把当前代码行关联到相应的历史修改与作者,用来找回当时的开发上下文。对此,老师提出了一个具启发性的设想:既然没有哪个 Agent 能被开除或坐牢,我们能否放弃以追责为目的的 blame,只要求系统把问题修好并向前推进?这与 NASA 航空安全报告系统(ASRS)的思路相通——该系统通过自愿、保密、非惩罚性的报告收集航空安全隐患,目标不在惩罚责任人,而在搞清楚哪次变化破坏了什么、如何防止错误重演。

在人类团队中,找责任人往往是为了唤醒当事人的记忆,回忆“那天早会讨论了什么需求、当时为什么觉得这个决定合理、现在什么前提变了”。而同模型的 Agent 本质上是思维高度趋同的克隆体,它们没有绩效考核的恐惧,也不靠个人回忆恢复上下文。因此课堂提示我们,在 Agent 协作中,保留历史快照依然不可或缺——方向走错时回退到旧快照的代价远低于在错误方向上硬修;但提交历史的记录重点可以从“谁写错了”转向“哪次修改破坏了什么系统不变量、留下了什么教训(lessons)以及如何快速恢复”。

乐观合并与悲观加锁:把协调放到什么时候

明确了历史用于恢复与推进之后,接下来的核心问题是:多个 Agent 并发修改同一项目时,协调应当发生在修改之后还是修改之前?我们可以借用数据库与操作系统中的并发控制理论,对比 Git/Jujutsu 的现有模式与课堂提出的“主从 Agent 加锁”实验路线。

乐观合并与悲观加锁:把协调放到什么时候(官方课件第 15 页)
乐观合并与悲观加锁:把协调放到什么时候(官方课件第 15 页) 查看原图

为此,老师提出了“主 Agent 规划,子 Agent 上锁开干”的并发协调构想:主 Agent 负责把任务拆解成边界明确的部分;子 Agent 负责获取互斥锁(Mutex Lock,即约定同一时刻只允许一个参与者修改指定范围内状态的同步机制),把活干完并清理干净现场后再解锁。这与现有版本控制形成了鲜明的策略对照:Git 与 Jujutsu 允许各分支在各自历史版本上先行工作、汇合时再检测并处理冲突;课堂借用数据库中的多版本并发控制(MVCC,Multi-Version Concurrency Control,通过保留数据的多个版本为并发读写提供对应版本快照,常与锁或乐观冲突检测结合使用)来类比这种“保留多版本、让工作先推进再处理版本关系”的思路。而互斥锁则属于事前协调的悲观并发控制,在修改前先约定谁能访问共享状态,把事后的合并冲突转化为事前的排队等待。

为什么人类时代偏爱乐观策略,而 Agent 时代值得探索悲观加锁?讲义与课堂口述给出了清晰的速度对比:人类写代码以小时为单位,早会花半小时划分模块后,各自修改一整天也很难高频撞车;即便偶发冲突,人也能手动解决。但 Agent 生产修改的速度快了几个数量级,若主 Agent 协调稍有不慎,并发任务会频繁触发冲突与回滚。此时,如果主 Agent 能够继续把不依赖当前锁定资源的工作分配给其他子 Agent,那么对高冲突共享资源采用“先拿锁再修改”的悲观策略,就能在遵守锁协议的参与者和锁覆盖范围内,避免并发双写带来的合并冲突与返工。

锁的边界与协议:操作系统与数据库理论的回归

“一把大锁保平安”听起来简单直接,但在真实的软件工程项目中,给资源上锁绝非调用一个系统函数那么轻松。运用《操作系统》与《数据库》课程中的基本原理,我们可以看清 Agent 互斥加锁方案在锁粒度、状态读取顺序与协议遵守上的工程边界。

锁的边界与协议:操作系统与数据库理论的回归(官方课件第 16 页)
锁的边界与协议:操作系统与数据库理论的回归(官方课件第 16 页) 查看原图

要让“上锁把活干完”真正成立,必须明确三个工程边界。

  • 第一,锁保护的不是孤立的文件,而是一件事所依赖的共享状态;如果只锁住当前正在写入的文件,就可能漏掉其他 Agent 正在修改的跨文件接口。
  • 第二,课件明确了正确的加锁操作序列约定:上锁 \(\rightarrow\) 读取最新状态 \(\rightarrow\) 修改 \(\rightarrow\) 验证并留下可恢复的快照 \(\rightarrow\) 解锁。
  • 第三,这种机制与数据库中的咨询锁(Advisory Locks,一种不由存储引擎强制拦截底层读写、而是依赖所有参与访问的应用程序自觉遵守加锁协议的显式锁机制)完全一致:所有参与的 Agent 必须严格遵守同一套协议,且锁本身只能保证互斥,不会自动让错误的业务修改变正确。

讲义提醒我们,一旦走上加锁这条设计路线,操作系统和数据库里的经典难题就会全部浮现。首先是粒度权衡:锁住整个仓库虽然能保护所有跨文件不变量,却会把多 Agent 并行退化为串行;只锁单个文件虽灵活,但当接口定义与调用方分别位于两个文件且必须同步更新时,单文件锁就会漏掉跨文件依赖。其次是时序与异常:以两个任务同时修改“个人助手的记忆格式”为例,后一个拿到锁的 Agent 必须先同步并重新读取最新状态,不能沿用等待锁之前缓存在上下文里的旧内容(如同多线程编程中线程被唤醒后寄存器里的旧值已经失效);此外,当某个任务同时需要两项资源时如何预防死锁,以及 Agent 意外崩溃停止时如何超时回收锁,都是在工程落地时必须设计的机制。

加锁修改的五步原子协议

在多 Agent 悲观并发控制设想中,子 Agent 操作共享资源必须严格遵循五步协议:

  • 第一,向协调层申请并获取目标范围的互斥锁;
  • 第二,拿锁后立即重新读取仓库的最新状态,丢弃等待期间的任何旧缓存;
  • 第三,在隔离环境中完成代码与文档修改;
  • 第四,运行自动化测试验证并生成可恢复的提交快照;
  • 第五,释放互斥锁。

缺少“拿锁后重读”或“解锁前验证”,锁机制就无法保证系统状态的一致演进。

课堂提示:非惩罚性历史与 ASRS 的工程启示

NASA 航空安全报告系统(ASRS)之所以能成功收集大量隐患,是因为它用保密和非惩罚机制换取了真实的故障上下文。课堂借此说明:当执行者变成同模型克隆体 Agent 时,“追责到人”失去了管理学意义。保留版本快照与提交说明,不再是为了证明谁写错了代码,而是为了在方向走偏时能低成本回退,并把“哪次变更破坏了什么假设”转化为后续 Agent 可直接读取的历史教训。

MVCC 类比边界与事前加锁的适用条件

第一,将 Git/Jujutsu 的分叉开发类比为数据库的多版本并发控制(MVCC),重点在于“保留多个版本、让并发工作先基于各自版本推进”,但数据库 MVCC 主要为读写提供一致的版本快照且可与锁结合,Git 的合并操作也不具备数据库事务的 ACID 属性。第二,事前加锁与事后合并各有适用条件:当任务边界清晰、跨模块耦合低时,分叉开发后合并的吞吐量更高;当多个高速 Agent 频繁修改同一核心接口或配置时,对特定共享范围采用咨询锁协议能有效减少冲突返工。

术语消化:面向 Agent 的并发控制与历史追溯核心术语

术语 解决的问题 机制与课程关系
git bisect / git blame 在长历史或多人协作中定位引入缺陷的提交及代码行的原始修改上下文 bisect 通过可重现的好坏判断二分缩小提交范围;blame 关联代码行与历史提交。在 Agent 时代,其重心从人事追责转向故障恢复与提取教训
多版本并发控制 (MVCC) 避免读写操作互相阻塞,允许多个并发任务在各自版本上先行推进 数据库中通过保留数据的多个版本为并发操作提供对应快照(可配合锁或乐观冲突检测);课堂借“保留多版本以支持并发推进”类比 Git/Jujutsu“先分叉修改、汇合时再处理版本关系”的策略
互斥锁 (Mutex Lock) 防止多个并发执行体同时修改同一份共享状态而引发写写冲突 约定同一时刻仅允许一个参与者进入临界区修改状态,把事后冲突转化为事前等待;但需权衡锁粒度、死锁预防与异常崩溃时的锁回收
咨询锁 (Advisory Locks) 在底层存储不强制拦截读写的场景下,通过应用层约定实现协作互斥 源自数据库的显式锁机制,要求所有参与访问的 Agent 自觉遵守“上锁\(\rightarrow\)重读最新状态\(\rightarrow\)修改\(\rightarrow\)验证留快照\(\rightarrow\)解锁”的完整协议

本章小结

本章重审了版本控制从“面向人类”到“面向 Agent 集群”的演进逻辑。

  • 由于 Agent 无须承担人事追责且思维趋同,版本历史的价值应如 NASA 安全报告系统般,从 blame 追责转向记录教训与快速恢复。
  • 在并发协调上,除了沿用类似 MVCC 的“先分叉修改、事后合并”策略外,课堂提出了“主 Agent 规划、子 Agent 事前获取咨询锁”的悲观并发构想,并结合操作系统与数据库原理剖析了锁粒度、拿锁后重读与跨文件不变量等核心工程边界。

人多以后,困难转移到了协作:AI 斩杀线与团队管理

无论我们把版本控制系统从 Git 升级到 Jujutsu,还是为并行工作的 Agent 引入 worktree 与互斥锁,版本控制终究只是工具。当局部编码能力被大模型高速替代,软件工程真正的瓶颈便暴露在团队与进度管理之上。为什么参与者变多以后,局部的代码增长不再等同于项目向可用状态推进?在 AI 能力边界不断推移的背景下,团队协作的复杂性究竟来自哪里,而版本控制又为我们组织人类或 Agent 团队留下了哪些跨越工具生命周期的第一性原理?本章将从 AI 斩杀线出发,剖析团队失调的结构性根源。

AI 斩杀线与软件工程的核心壁垒

在讨论了多种面向人类与面向 Agent 的并发版本控制方案后,我们需要跳出工具细节,审视一个更根本的生存问题:如果 AI 编写局部代码的速度和质量持续跃迁,软件工程师的核心价值还剩什么?沿着技术演进规律,我们可以看清岗位边界如何变化,以及为什么团队与进度管理成了当前尚未被覆盖的壁垒。

AI 斩杀线与软件工程的核心壁垒(官方课件第 17 页)
AI 斩杀线与软件工程的核心壁垒(官方课件第 17 页) 查看原图

版本控制只是工具,软件工程真正的困难在于进度与团队管理。课堂将 AI 的能力边界比作一条不断移动的“斩杀线”(即一旦某项工作能被机器稳定地 100% 替代,仅提供该局部能力的岗位就会失去原有价值的临界线)。这一推演建立在摩尔定律与 Scaling Law(规模定律,即模型性能随计算量、数据量和参数规模增长而持续提升的经验规律)继续生效的前提上:模型会变得更强、更小、更快。如果未来一句话就能直接生成满足全部要求的高质量产品,传统软件工程就会消亡;正如部分基础会计或数学推导工作也正面临机器能力跃迁带来的重新定价。

讲义提醒我们,具体哪类工作在何时被覆盖仍需实践检验,但眼下的现实非常清晰:老板随口提出“做一个国民级社交应用”,AI 还无法一步到位直接交付。需求澄清、把宏大目标拆解给十个子团队、定义模块接口并验收集成结果,这些把局部能力组织成完整产品的管理工作,正是当前斩杀线尚未触及、也最稀缺的工程能力。

沟通关系膨胀与工程失调的四类病灶

既然团队管理如此关键,为什么把大任务分给多个人或多个 Agent 后,项目推进反而常常变慢?即便每个参与者都觉得自己把分内工作想得很清楚,集成时依然状况百出。結合《人月神话》的沟通模型与四类典型场景,可以拆解“我的部分已经好了”这句常见汇报背后的协作失调。

沟通关系膨胀与工程失调的四类病灶(官方课件第 18 页)
沟通关系膨胀与工程失调的四类病灶(官方课件第 18 页) 查看原图

当团队人数 n 增加时,若成员两两之间都可能需要直接沟通,潜在沟通关系数将按 n(n-1)/2 呈平方级上升:3 人是 3 条,5 人是 10 条,6 人是 15 条,10 人则达到 45 条。除了沟通边数激增,课件还归纳了导致项目停滞的四类病灶:

  • 第一,目标不一致,有人急于上线,有人趁机重构,双方都认为自己在帮项目;
  • 第二,依赖在等待,前端等接口、后端等需求,全员忙碌但关键路径未动;
  • 第三,完成的定义(Definition of Done,即团队对任务达到何种可验收状态才算结束的统一标准)不同,有人指代码能跑,有人指测试通过或可部署;
  • 第四,责任落在缝隙里,各模块都有人负责,跨模块问题却无人认领。

讲义给出一个具体例子:前端已经能展示模拟数据,后端也通过了自身测试,但双方对字段含义和失败情况的理解不一致,代码量虽在增长,离可用产品的距离却没有同速缩短。此时再多几个 Git 提交也无法自动排定优先级或填补分工缝隙,必须依靠清晰的架构接口和组织分工来裁剪不必要的直接协调。

超越具体工具:版本控制留下的第一性原理

既然工具不能自动解决团队分歧,我们前面花费大量篇幅剖析 Git、Jujutsu 与并发锁机制,对团队协作究竟有什么长远价值?当我们面对全新的多 Agent 协作系统时,应该从版本控制的历史中带走什么?把版本管理的具体操作抽象出来,就能得到组织项目演化的基本原则。

超越具体工具:版本控制留下的第一性原理(官方课件第 19 页)
超越具体工具:版本控制留下的第一性原理(官方课件第 19 页) 查看原图

项目从共同起点分叉出多条路线进行探索,在过程中保留历史与教训,最终将成果汇合——这一演化结构揭示了版本控制留给我们的核心启发:一个项目不能只被看作“当前状态”的静态集合,而是一个持续演化的过程。无论具体技术如何更替,第一性原理始终会留存——系统必须能够保存当前可靠状态、看到开发历史并记住教训,同时支持管理多个并行的探索分支,并设计出让它们重新汇合的机制。

在未来的开发流程中,记录历史教训的工作也许会部分交由 Agent Memory(智能体长期记忆机制,即跨会话保存经验、规则与上下文的存储与检索组件)来承担。但讲义强调,究竟什么内容值得记住、哪条经验已经过时、以及如何用历史教训指导下一步决策,仍然需要工程设计者来定义。理解了状态、历史、隔离与整合,我们就拥有了驾驭下一代协作系统的底层直觉。

把项目视为持续演化的过程而非静态快照

如果只把版本控制当作记几个终端命令,换一套工具或进入多 Agent 时代就得从零学起。版本控制真正蒸馏出的工程智慧在于四件事:保存当前可工作的状态、记录走到当前状态的历史与教训、提供隔离机制允许不同方向的并行探索、以及设计验证与合并机制将探索成果重新汇合。这套结构同样适用于组织人类团队与 Agent 集群。

把自己当成主 Agent 练习任务分解

课堂提示:想要在毕业前越过 AI 斩杀线、锻炼稀缺的团队管理能力,不必等到真正带领几十人的团队。现在就可以把自己当成主 Agent(包工头),手下调度两三个并行工作的编码 Agent:尝试由你来划分模块边界、对齐接口规范、规定何时提交快照、何时运行测试与合并,在实战中体会进度管理与集成协调。

切勿将潜在沟通边数等同于实际沟通耗时

公式 n(n-1)/2 计算的是无组织结构下 n 个成员两两之间“潜在的沟通关系边数”,绝不等于每个团队实际耗费的沟通时间都严格服从该公式。优秀的软件工程恰恰通过分层架构、明确定义的模块接口以及团队负责人(Team Leader)的层级分解,大幅裁剪掉不需要直接发生的两两协调边。

术语消化:团队协作与版本控制哲学的核心概念消化

术语 解决的问题 机制与课程关系
AI 斩杀线 评估哪些软件工程岗位价值会因模型能力跃迁而归零 当局部编码任务被模型 100% 覆盖时,工程重心被迫上移至需求拆解、接口协调与团队进度管理
潜在沟通关系边数 度量多人协作时两两直接协调带来的结构性复杂度膨胀 按 n(n-1)/2 随人数平方级增长,揭示为何必须通过清晰接口和模块划分来减少直接沟通
完成的定义 (Definition of Done) 消除团队成员对“任务已做完”的验收标准分歧 明确区分“代码写完”“测试通过”与“可集成部署”,防止局部完工却无法汇合成可用产品
Agent Memory 在跨会话与持续迭代中为智能体保留开发经验与历史教训 承接版本控制“记住教训”的职责,但何种信息值得存入、何时失效仍需工程师设计规则
\[ C(n) = \frac{n(n-1)}{2} \]

其中 n 表示团队中需要相互协调的成员数量(n 为大于等于 2 的正整数),C(n) 表示无层级约束下两两之间潜在的双向沟通关系边数。该公式仅反映潜在协调通道的组合增长趋势,实际工程中可通过架构解耦与组织分工裁剪边数。

本章小结

本章跳出版本控制的具体命令,探讨了 AI 时代软件工程的核心挑战。

  • 随着模型能力沿 Scaling Law 推进,单纯的局部编码面临“斩杀线”威胁,而应对平方级潜在沟通复杂度、解决目标分歧、依赖阻塞、完成定义不一与责任缝隙的团队管理能力愈发稀缺。
  • 版本控制留给团队协作的第一性原理,正是保存状态、铭记教训、支持分叉探索与设计汇合机制。

Git 还可以记录小朋友怎样解题:跨界复用版本管理抽象

理解了版本管理系统如何保存当前状态、保留演进历史、隔离并合并多条探索路线之后,我们能否跳出“代码仓库”的刻板印象,把这套抽象迁移到业务系统的架构设计中?当产品需要在服务器端保存小学生解汉诺塔题目的探索过程并由 AI 助教提供个性化点评时,为什么不必机械套用大模型给出的“API 应用 + 关系型数据库 + 对象存储”常规套路,而可以直接复用 Git 的版本演化结构?本章通过剖析解题过程与版本历史的同构性,展示如何用第一性原理识别现成工具的抽象能力。

从增值服务需求到教科书式数据库架构

软件工程教学常面临一个现象:一听到“在服务器端保存用户数据”,开发者或大模型就会条件反射式地启动全套企业级存储组件。如果要为一个在线编程与解题平台设计付费增值功能,真正需要持久化和展示的核心数据结构究竟是什么?它的存储开销与 AI 推理开销相比处于什么量级?

从增值服务需求到教科书式数据库架构(官方课件第 20 页)
从增值服务需求到教科书式数据库架构(官方课件第 20 页) 查看原图

回到课堂实例 Pico Playground:小学生在面对带有额外限制的汉诺塔(Tower of Hanoi)考级题目时“一片哀嚎”。课堂口述补充了这道题的难度所在:它要求所有圆盘只能沿着单一方向移动,不能折返;例如三个圆盘时,最大的圆盘不能一步移到目标柱,而必须在其余小盘的配合下向右走一步、再走一步,通过递归与数学归纳法才能求得最优移动方案。为了把纯粹的练习场变成能盈利的产品,自然的方向是提供付费增值服务:在服务器端保存小朋友的做题记录,并引入蒸馏了老教师经验的 AI 助教做个性化点评。当把这项需求交给 GPT 时,它给出了标准的教科书式架构:一个 API 应用、一个关系数据库、一个对象存储,需要异步点评时再加后台任务进程;数据库存关系和记录,大课程包与完整轨迹放对象存储,任务量小时先用数据库任务表。

这套由 GPT 生成的方案部件齐全、四平八稳,放在任何传统软件工程课程里都算得上标准答案。课堂口述指出,这种设计忽略了两个关键事实:第一,调用大模型生成一次 AI 助教点评所消耗的 Token 成本,远远高于把其余文本数据持久化下来的存储开销,底层数据库的压缩率与高并发性能在项目早期根本不是瓶颈;第二,它没有看穿“小朋友反复尝试解题的过程”在数据结构上究竟长什么样,从而错失了直接复用现成抽象的机会。

解题轨迹与版本历史的同构:用 Git 替代自建历史表

既然常规的“关系型数据库 + 对象存储”在项目早期显得笨重,我们应该如何重新审视小学生做汉诺塔题目的数据特征?对照上一章提炼的版本管理要素(状态快照、父子连接、分叉探索、差异比较),可以看到解题动作序列如何与 Git 的底层机制形成一一对应。

解题轨迹与版本历史的同构:用 Git 替代自建历史表(官方课件第 21 页)
解题轨迹与版本历史的同构:用 Git 替代自建历史表(官方课件第 21 页) 查看原图

老师由此提出了一个 GPT 未曾想到的跨界架构构想:直接用 Git 记录小朋友怎样解题。首先,汉诺塔解题如果只保存最终盘面,老师或家长根本看不出孩子在哪里卡住;必须把尝试的动作序列保存成逐行文本,才能回放和比较“怎么做出来的”。一旦表示为逐行文本,Git 的三项核心能力便刚好契合需求:

  • 第一是树状的版本历史(Tree-shaped History),孩子做到哪一步遇到困难、回退到之前某个状态重新尝试,都会作为提交(Commit)与分支(Branch)留存下来;
  • 第二是免费的差异比较(Diff,即计算相邻或指定版本之间文本行增删变化的机制),能直接展示孩子在收到助教提示前后到底修改了哪些步骤;
  • 第三是提交注释关联,利用 git notes(一种在不改变原提交文件内容和哈希标识的前提下,为指定提交附加额外元数据或说明文本的 Git 机制),可以把 AI 助教的点评后补绑定到当时的具体提交上。

课堂口述提到,这一灵感源自 Overleaf 支持将论文项目导出为 Git 仓库的体验:教师可以把学生的论文仓库克隆到本地,直接让本地 Agent 批改各个 .tex 文件并提交。同理,在 Pico Playground 中,每当学生点击运行、进入空闲(idle)状态或切换页面时,系统都可以自动执行一次 Git 提交;申请 AI 助教时,点评可以直接关联或分叉在对应提交上。这一架构构想也有明确的职责边界:Git 适合管理点击运行或暂停时的“有意义的解题版本”,而每一次鼠标移动的细粒度轨迹仍适合单独的事件日志;动作文本的业务含义以及“哪一步值得表扬”仍需应用层解释;用户资料、课程关系与统计查询也依然由合适的数据库承担。

从需求结构出发识别可复用的工具抽象

看到“服务器端保存数据”就立即搭建关系数据库与对象存储,是典型的套路化反映。工程设计应先剖析业务数据的内在结构:小学生解汉诺塔题目的过程包含“推进步骤、卡壳回退、换思路分叉重试、提示后对比修改、定点附加点评”,这与版本控制系统的“提交快照、回滚、分支、diff 比较、notes 附加元数据”在结构上高度同构。识别出这种同构性,就能直接借用 Git 成熟的树状历史管理能力,免去在关系数据库里自行设计和维护复杂历史表结构的早期负担。

课堂提示:结合成本结构判断早期架构重心

在带有大模型能力的生成式应用中,架构权衡的成本结构发生了显著变化。课堂口述特别算了一笔经济账:学生点击“我要助教”时,后台调用大模型(如 DeepSeek)进行推理和点评所消耗的 Token 成本,远远高于在磁盘上持久化几行代码或动作序列的存储成本。在项目最早期,目录格式、提交说明格式以及数据的使用方式都还在探索中,优先利用文件系统和 Git 仓库快速跑通版本演化与助教点评闭环,等数据模式收敛或真正遇到性能瓶颈时再迁移到数据库,是更务实的工程节奏。

Git 存储解题轨迹的职责边界与外部组件配合

用 Git 存储汉诺塔解题轨迹并非要替代所有关系型数据库或应用层逻辑,实际落地时需要划清三道边界。第一,区分数据粒度:点击运行或空闲时的“有意义解题尝试版本”适合交给 Git 管理,而高频的“每一次鼠标移动”更适合写入独立的事件流日志;第二,Git 只负责保存逐行动作文本与计算文本行级 diff,至于动作是否合法、哪一步体现了递归思想并值得表扬,必须由上层应用程序配合模型来解释;第三,用户账户、付费账单、课程关联与全局统计查询,依然需要关系型数据库等适合的存储组件配合。

术语消化:汉诺塔解题追踪需求与 Git 机制及外部存储的职责映射

术语或机制 解决的问题 机制与课程关系
动作序列逐行文本 只存最终盘面无法看出孩子在哪里卡住、如何一步步推导 将每次移动圆盘的动作按行记录为文本文件,使解题过程转化为可被版本控制工具追踪、回放与按行比对的制品
树状版本历史 (Commit & Branch) 孩子解题中途受阻回退、从旧状态换思路重新尝试的轨迹难以在线性表中直观表达 在点击运行或空闲时自动生成 Git 提交,回退重试自然形成父子关系与分叉分支,直接复用 Git 的有向无环图历史结构
git diff 需要直观对比孩子在收到老师或 AI 提示前后,具体调整了哪些解题步骤 直接复用 Git 内置的文本差异计算能力,免费获得相邻尝试或提示前后两个版本之间的动作增删对比,无需自写比对算法
git notes AI 助教异步生成点评后,若直接修改原提交会改变 Commit ID 并破坏历史标识 利用 Git 的附加说明机制将助教评语后补绑定到指定 Commit 对象上,在不篡改原提交内容与哈希值的前提下实现精准关联
事件日志与关系数据库 高频细粒度交互记录(如每次鼠标移动)以及用户账户、课程关系、统计查询不适合放进 Git 划清架构边界:Git 仅接管“有意义的解题版本演化”,细粒度行为流交由独立事件日志,结构化业务关系与统计仍交给数据库
将汉诺塔解题尝试与助教点评映射到 Git 操作的伪代码示例
# 1. 学生第1次点击运行:将当前动作序列保存为逐行文本并自动提交
cat << 'EOF' > moves.txt
move disk 1 from A to B
move disk 1 from B to C
move disk 2 from A to B
EOF
git add moves.txt
git commit -m "attempt: try moving disk 2 after disk 1 reaches C"

# 2. 学生请求 AI 助教:先记录发起请求时的提交 SHA,待异步生成点评后按该 SHA 挂载 git notes(避免等待期间产生新提交导致 HEAD 漂移挂错版本)
attempt_commit=$(git rev-parse HEAD)
git notes add -m "助教点评:前两步正确遵守了单向移动限制,但此时盘2在B、盘1在C,下一步需先将盘1移回A才能继续推进。" "$attempt_commit"

# 3. 学生根据提示修改方案并再次运行:形成新提交,并用 git diff 免费对比提示前后的变化
cat << 'EOF' > moves.txt
move disk 1 from A to B
move disk 1 from B to C
move disk 2 from A to B
move disk 1 from C to A
EOF
git commit -am "attempt: move disk 1 back to A after tutor hint"
git diff HEAD~1 HEAD

输入为学生在前端触发的“运行尝试”动作序列与后台 AI 助教生成的点评文本;条件是每次有意义的尝试都被序列化为按行分隔的 moves.txt。代码演示了如何通过自动 git commit 记录探索快照、在发起异步请求时用 git rev-parse HEAD 锁定目标提交 SHA、待点评返回后用 git notes add 准确挂载到该次尝试上而不改变原提交 ID,以及用 git diff 检验提示前后的步骤变化。

本章小结

本章通过 Pico Playground 小学生单向汉诺塔解题记录与 AI 助教点评的案例,展示了如何将版本管理的核心抽象跨界复用到业务系统架构中。

面对大模型给出的“API + 关系数据库 + 对象存储”常规方案,我们从需求结构出发,识别出“解题探索、回退重试、提示前后对比、定点点评”与 Git 的“树状提交历史、分支、git diff、git notes”之间的同构性,并划清了有意义解题版本与细粒度鼠标日志、应用层语义解释及业务数据库之间的职责边界。

从零开始,你就是包工头:技术债、MVP 与早期 Prompt 的锚定效应

在前一节中,我们借助版本管理的底层抽象,重新审视了汉诺塔做题轨迹的存储架构,发现熟悉的工程工具可以替代沉重的通用数据库套件。现在,视角从局部架构设计正式转入课程的第一个实验项目:从零构建一个“会成长的个人助手”。为什么从零开始做一个新产品,往往比参与一个已经成型的开源项目更棘手?当开发者化身为指挥大语言模型与 Agent 的“包工头”时,如何避免早期开放式 Prompt 的盲目生成演变为沉重的技术债,并用最小可行产品(MVP)与留痕历史稳步推进系统演进?

从零开始的鸿沟与包工头准则

当我们将版本控制与团队协作的理念落到自己的项目上时,第一条行动原则是:任何时候开始动手都比不开始强。但在 AI 能够以每秒数百个 token 编写代码的今天,开发者的核心角色已经转变为项目的“包工头”:决定先做哪一块、定义各模块如何衔接、验收交付物是否真正可用。为什么在空目录里启动新项目,比在成熟工程里修补功能更容易失控?

从零开始的鸿沟与包工头准则(官方课件第 22 页)
从零开始的鸿沟与包工头准则(官方课件第 22 页) 查看原图

从零开始做产品的核心难点在于需求尚未对齐。成熟开源项目已经固定了大量工程约束:目标用户是谁、代码如何分层、什么叫向下兼容、测试流水线如何运行,开发者只需沿着既有轨道做局部修改。而在空项目中,意图(Intent)、规格(Spec)与实现(Impl)之间存在巨大的鸿沟(intent-spec-impl gap)。在缺乏约束的情况下,任何过早“确定”下来的设计,都很可能把早期对需求的误解直接固化进代码实现中。为此,课件提出了软件工程抵御变化的核心准则:“没有什么是不能变的”。所谓以不变应万变,落到软件架构上,本质就是检验系统能否以最小的改动代价抵抗未来需求的变化。

讲义以实验一“会成长的个人助手”为例具体展开了这种鸿沟:所谓“成长”,究竟是指记住静态事实、修正用户偏好,还是学会执行新任务?助手何时应当主动行动,记错了又该如何纠正?如果一开始就不假思索地把所有记忆数据结构设计为“永久有效”,等后续真正用起来才发现大量偏好具有严格的时间和场景限制,早期那个看似确定的数据表结构就会把需求误解锁死在系统底层。因此,包工头不能指望在开工前猜中所有未来需求,而应围绕已经看见的变化划分模块边界,让记忆策略或输入方式能够独立替换,而不必推倒重写整个系统。

Vibe Coding 的陷阱与决策记录

在明确了“没有什么是不能变的”这一准则后,我们来看如果不加约束地直接让 AI 凭感觉编程(Vibe Coding),项目会遭遇怎样的演化困境。许多开发者习惯在聊天窗口里与模型反复拉锯,直到代码勉强跑通,却忽略了对话上下文的易逝性。当聊天记录消失、需求再次变更时,为什么仅靠留存下来的代码文件不足以支撑项目的持续迭代?

Vibe Coding 的陷阱与决策记录(官方课件第 23 页)
Vibe Coding 的陷阱与决策记录(官方课件第 23 页) 查看原图

盲目 Vibe Coding 往往会陷入三阶段死循环:第一阶段,开发者通过反复与 AI 对话,好不容易让程序满足了当前需求;第二阶段,随着会话关闭或更换设备,当时的聊天记录消失,仓库里只剩下最终生成的代码以及大量凑数、冗余的 AI 劣质产物(slop);第三阶段,当需要修改功能或排查问题时,由于缺乏上下文,开发者或新的 Agent 只能回到原点,把当初踩过的坑和犯过的错误再踩一遍。这也印证了版本管理在 AI 时代的完整内涵:不仅要记住现在、记住历史,还要记住教训。代码本身只能记住最终的“结果”,而架构决策记录(Architecture Decision Records, ADR,即记载关键架构选择、背景原因及被否决方案的轻量级文档)则用来记住“为什么这样做、哪些路走不通”。

结合课堂讲义,哪怕是在局部修改中——比如让 Agent 把某处底层实现从链表(linked list)替换为树结构以优化性能——如果只改了代码而没有同步更新设计说明,或者丢失了当时的讨论背景,接手项目的后续 Agent 看到代码与旧文档冲突时,根本无法判断究竟谁是正确的。因此,在动手让 Agent 写代码之前,必须先建立空仓库并规划好留痕机制:把讨论出的核心意图、被证伪的错误尝试和模块边界沉淀为版本库里的决策记录,防止项目在多轮生成后退化为无人能懂的焦油坑。

用最小可行产品(MVP)验证核心假设

既然直接把宏大而模糊的需求丢给 AI 会产生大量无法维护的劣质代码,作为“包工头”应当如何迈出第一步?答案不是闭门造车写出几万字的设计文档,而是尽快做出一个能放进真实场景检验的最小系统。这就需要理解最小可行产品(MVP)的工程定义,以及在动手编码前必须回答的验证问题。

用最小可行产品(MVP)验证核心假设(官方课件第 24 页)
用最小可行产品(MVP)验证核心假设(官方课件第 24 页) 查看原图

产品开发最大的风险,是做出一个没人想要的东西(building something nobody wants)。为了规避这一风险,Eric Ries 提出的最小可行产品(Minimum Viable Product, MVP)方法论要求构建一个立即能使用、尽管功能尚不完备的版本,帮助开发者在实际使用中看清未来潜在的需求变化,并评估当前软件设计的合理性。在决定“先验证什么”时,核心原则是:开工前必须先挑选出一个最值得验证的核心假设,再动手制作恰好能验证该假设的最小版本,用尽可能少的工程投入换取经过真实检验的认知。

讲义进一步给出了运用 MVP 的实操标准:在写代码前,先明确写下“这个版本要验证什么”以及“出现什么结果就说明我们想错了”。以个人助手实验为例,第一版绝不必包揽邮件收发、日程管理、教务系统自动化和全部长期记忆。你可以只挑一个每天都会遇到的高频痛点——比如记录一条学习资料归档偏好,在下一次同类任务中调用它,并允许手动纠正。哪怕界面极其简陋,只要能亲自用起来,原先隐藏在“更聪明、更主动”这类笼统形容词背后的真实分歧(究竟是每天定时提醒,还是发现遗漏时才提示;究竟是永久记住偏好,还是需要自动遗忘一次性指令)就会立刻暴露出来,从而在系统规模还很小的时候指导规格修正。

早期 Prompt 的锚定效应与技术债

理解了 MVP 的起步策略后,我们结合课堂现场的实时演示,剖析为什么项目启动阶段的第一次提问(早期 Prompt)具有决定性的破坏力或建设力。课堂演示中,老师将实验一包含五项能力的开放式需求文档直接喂给 Agent,模型以每秒约 300 个 token 的速度快速生成了一个庞大的系统。这种“一步到位”的生成方式是如何制造技术债的?我们又该如何利用版本历史支持架构重构?

早期 Prompt 的锚定效应与技术债(官方课件第 25 页)
早期 Prompt 的锚定效应与技术债(官方课件第 25 页) 查看原图

项目从零演化时遵循两条关键的工程规律。

  • 第一,早期的 Prompt 对项目走向的影响最大。每一次在提示词或早期代码中写下的“确定”设计,都会在后续开发中产生强烈的锚定效应,直接演变为潜在的技术债(Technical Debt,指为了短期快速交付或受限于早期认知而采取的次优设计,导致未来修改系统时必须额外付出的返工成本)。
  • 第二,必须通过自动提交与推送(Commit & Push)留下可回溯的完整历史,让真正可用的最小版本反过来帮助开发者重新整理和优化架构设计,而不是被早期的临时决定永久绑架。

课堂演示生动揭示了这一机制的运作过程:老师在课堂上解释这一现象时推測,大语言模型在 Self-Attention 机制下会关注提示词里的每一个词,经对齐训练后表现出极强的顺从倾向(课堂戏称为“Prompt 的舔狗”)。当把讨论稿式的实验要求直接交给它时,模型立即按传统的业务逻辑系统套路,过度设计出入口层、编排层、能力层和数据层,不仅创建了关系数据库 assistant.db 来存放个人资料、邮件归档与草稿,甚至凭空编写了“提交人民奖学金申请”“准备退课”等硬编码业务功能,而且全程只做了一次或者干脆没有做 Git 提交。对于早期个人助手 MVP 而言,像这样过早引入数据库 Schema,后续每次需求调整都要迫使 Agent 执行复杂的数据库迁移(Database Migration);反之,若早期仅采用直观的 Markdown 目录结构配合共享云盘传递 .eml 邮件与解析草稿,人类直接在文件夹里就能调整结构,待数据格式收敛后再让 Agent 提炼数据库也完全来得及。正如 Martin Fowler 在技术债象限(Technical Debt Quadrant)中所分析的,即便早期认真设计,随着实践认知的深入,旧结构也会落后于新理解。因此,只有坚持小步前进、自动提交留痕,明确写出第一版的可用标准与可变边界,才能在 AI 时代真正掌控软件工程的演进方向。

早期 Prompt 的锚定效应与技术债象限

在从零启动项目时,第一次输入给 Agent 的 Prompt 往往会成为后续所有代码生成与上下文讨论的默认前提。由于模型倾向于严格迎合字面指令,开放式文档中的举例极易被固化为僵硬的数据库表结构与分层架构。同时,根据 Martin Fowler 的技术债象限理论,技术债不仅源于草率敷衍,即便团队早期深思熟虑,当真实使用带来新认知后,原有的合理设计也会自然沦为阻碍修改的技术债。因此早期设计必须保持轻量,明确区分核心验证目标与允许变化的边界。

针对特定个人助手 MVP:文件目录与云盘流水线为何优于早期建库

在课堂演示的个人助手早期探索阶段,相比于上来就建立 assistant.db 并固化 Schema,直接使用 Markdown 文件目录配合共享云盘是更具韧性的特定 MVP 方案。例如邮件处理流水线可将收取的 .eml 原始文件放入同步云盘,旁边直接存放 Agent 生成的分类标签与回复草稿文件,供下一级流水线读取。这样既省去了早期频繁执行 Database Migration 的沉重负担,又允许开发者在文件管理器中直观调整目录结构、直接观察 Agent 读写了哪些记忆,待业务规则稳定后再平滑迁移至数据库。

切勿将 MVP 误解为缺乏设计的粗糙半成品

最小可行产品(MVP)绝不等于不经思考、随便让 AI 生成一个充满劣质代码(slop)的半成品。Eric Ries 提出 MVP 的核心在于“经过验证的认知(Validated Learning)”:在写下第一行代码或第一句 Prompt 之前,包工头必须事先定义清楚“当前版本要验证哪一个核心假设”以及“观察到什么具体结果就证明假设错误”。没有明确证伪标准和自动版本留痕的快速堆砌,只会加速项目滑入难以维护的软件焦油坑。

术语消化:从零构建项目的核心工程概念消化

术语 解决的问题 机制与课程关系
意图-规格-实现鸿沟 (Intent-Spec-Impl Gap) 空项目缺乏既有工程约束,模糊想法极易被模型用默认套路错误填满 通过拆解小步目标、明确接口边界与验收条件,防止早期对需求的误解被直接固化进底层数据结构
技术债 (Technical Debt) 早期固化的次优设计或落后于新认知的旧结构,导致后续修改代价倍增 结合 Martin Fowler 技术债象限,识别早期 Prompt 锚定效应带来的架构僵化,用模块解耦与重构偿还债务
架构决策记录 (ADR) 聊天记录消失后只剩代码,后续维护者与 Agent 无法得知设计背景而重踩旧坑 在版本库中显式记录“为什么这样做、哪些方案走不通”,与代码快照配合实现记住现在、历史与教训
最小可行产品 (MVP) 避免投入大量资源与 Token 后,做出一套无人需要或偏离真实痛点的庞大系统 开工前先定义核心验证假设与证伪标准,用诸如 Markdown 目录加云盘的最小闭环获取真实使用反馈

本章小结

  • 本章围绕实验一“会成长的个人助手”,阐述了开发者作为 Agent “包工头”的工程方法。
  • 从零做产品面临巨大的 Intent-Spec-Impl 鸿沟,直接向模型倾倒开放式需求会因早期 Prompt 的锚定效应催生过度设计与沉重的技术债。
  • 遵循“没有什么是不能变的”准则,开发者应借助最小可行产品(MVP)验证核心假设,并配合自动提交与架构决策记录(ADR)留存演化历史与教训,让真实使用反馈驱动架构持续重构。

总结与延伸

回顾本讲,我们完成了一次从底层抽象到高层工程实践的贯穿式推演。在方法论层面,版本管理的核心价值被提炼为九个字:“记住现在、记住历史、记住教训”。

我们首先认识到,现有文件系统虽对人类编辑器和 UNIX 工具极其友好,却丢失了需求(Intent)、规范(Spec)、实现(Impl)与测试之间的网状可追踪性(Traceability),为此,课程进一步探讨了面向 AI 的 repo.db 图结构仓库与动态视图(View)。

回到现有工具,Git 通过有向无环图(DAG)组织快照,支持分支、标签及 merge、rebase 等历史操作;但在变基或修订时,与具体内容和父节点绑定的 Commit ID 无法延续“同一次修改”的身份。

Jujutsu(jj)在底层依然保存文件快照,同时将演化中的修改作为一级公民,用通常保持稳定的 Change ID(并发重写未汇合时也可能产生分歧版本 divergent change)追踪改动身份,用 Commit ID 精确锁定具体快照,配合工作副本(Working Copy)命令级自动快照以及可保存、可继续变基的一等冲突状态,理顺了版本演化的基本模型。

在面向 Agent 团队的实际应用中,开发者的并发规模与速度发生了量级跃迁。我们可以利用 git worktree 为同一本地仓库挂载多个拥有独立工作区、HEAD 与暂存区(index)但共享底层对象库(objects)和大部分引用的目录,防止多 Agent 互相踩踏文件;也可以跳出类似数据库 MVCC 的“先各自修改再合并”范式,让主 Agent 规划调度、子 Agent 按咨询锁(Advisory Lock)协议先上锁、同步最新主干状态、修改验证并推进主干后再解锁。

在业务架构中,面对小学生汉诺塔解题轨迹与助教点评这类带有回退、分叉与比对特征的需求,直接复用 Git 的树状历史、git diff 与 git notes,比套用“API + 关系数据库 + 对象存储”更贴合数据的版本演化结构。

在课程脉络上,本讲不仅深化了上一讲的版本控制基础,更直接为实验一“会成长的个人助手”及下一讲的软件架构设计铺平了道路。理解了早期 Prompt 对后续代码生成的强烈锚定作用,作为“包工头”的我们就不会在起步时盲目生成庞杂代码。针对个人资料与邮件处理这类特定早期场景,先用直观的 Markdown 目录与云盘构建最小可行产品(MVP),明确核心假设与证伪标准,并通过自动提交与架构决策记录(ADR)留存演化轨迹,正是我们在面对《人月神话》所述的 \(n(n-1)/2\) 潜在沟通边复杂度与 AI 时代“斩杀线”时,能够以不变应万变的工程基石。

学习练习

用下面的延伸练习检查自己能否应用本讲方法。

练习 1:最小场景验证:Git 与 Jujutsu 的变基、变更身份及冲突状态对比

在本地初始化一个启用 Jujutsu(与 Git 共存,如 jj git init --colocate)的最小测试仓库。构造一条基础提交 A(含配置文件 config.txt 写明保存策略为 immediate),从 A 分叉出两条路线:路线 1 提交 B 将配置改为 manual_confirm;路线 2 连续提交 C1(改为 auto_every_minute)和 C2(新增独立的 README.md 说明)。

先在传统 Git 分支模型下尝试将路线 2 rebase 到路线 1 上,记录其中断行为与 Commit ID 变化;随后重置并在 jj 模型下执行相同的 rebase 操作,观察 jj log 中 Change ID 与 Commit ID 的变化规律,以及冲突状态如何被保存在提交中而不阻塞后续 C2 的重放,最后手动消解冲突并结合需求说明裁决理由。

验收标准:

  1. 提交实验过程命令序列与前后对比的 git log --graph / jj log 输出截图或文本记录;
  2. 准确指出 C1、C2 在变基前后 Commit ID 发生了改变而 Change ID 保持延续,并说明 jj 底层依然存储快照而非纯补丁;
  3. 展示带着冲突标记的提交状态及后续成功消解冲突的最终快照,附 100 字以内的说明区分“工具保存冲突状态”与“人工/Agent 完成语义冲突裁决”。

练习 2:多 Agent 本地并行隔离与咨询锁协议原型实验

围绕一个包含共享接口定义文件 memory_schema.md 与两个独立模块目录 ui/、storage/ 的小型代码库,使用 git worktree 创建两个并行的子工作目录 ../wt-agent-ui 与 ../wt-agent-storage。

首先检验两个子目录中的 .git 文件内容,验证其与主仓库的共享与隔离边界;接着编写一个轻量级的本地咨询锁(Advisory Lock)协调脚本(基于锁文件记录持有者、锁定范围与获取时间,不自动超时抢占锁以免旧持有者继续写入造成双写),模拟两个并行任务同时申请修改 memory_schema.md 及其对应模块时,“先上锁 -> 将当前 worktree 同步到共同权威分支(如 main)的最新提交 -> 修改并运行接口校验 -> 提交并推进权威分支 -> 释放锁”的完整串行整合流程。

验收标准:

  1. 提供 git worktree list 输出及子工作区 .git 文本文件中 gitdir: 指向的核查记录,准确说明各 worktree 独立拥有工作区、HEAD、暂存区而共享对象库与大部分引用;
  2. 协调脚本能够阻止第二个任务在未获锁前修改共享接口文件,并要求获锁者在锁内先同步共同权威分支的最新提交(仅重读各自独立 worktree 的本地文件无法看到另一分支的提交),且在未确认旧进程退出前不自动删除锁;
  3. 两个 worktree 的修改在锁内依次验证并合入本地权威分支,全程无需远端 push/pull 且通过接口一致性检查。

练习 3:个人助手 MVP 启动包与架构决策记录(ADR)设计

针对课程实验一“会成长的个人助手”,拒绝一次性生成包含全套数据库与复杂四层架构的套路化方案,从你日常最真实的一个高频痛点(例如:单类课程资料归档检索,或单类通知的偏好纠正闭环)出发,建立一个受 Git 管理的 MVP 启动仓库。用纯文本/Markdown 目录结构替代早期关系型数据库,编写首份精炼的 README.md、第一份架构决策记录 docs/adr/0001-mvp-scope-and-storage.md 以及一条可自动验证“这次纠正,下次生效”的最小闭环脚本或测试规约。

验收标准:

  1. 仓库根目录 README.md 在 300 字以内清晰界定核心 Intent、当前 MVP 唯一验证的假设以及“出现什么结果说明想错了”的证伪标准;
  2. ADR 文档明确记录为何在当前特定 MVP 阶段选择直观的文件目录而非过早引入数据库 Schema,并列出未来触发架构演进的条件;
  3. Git 历史中至少包含两次原子提交:一次为初始 MVP 实现,另一次模拟用户发出纠正指令后规则/记忆文件的更新提交,且验证脚本能证明新会话读取该提交状态后不再重犯同类错误。

拓展阅读

下面列出本讲的原始讲义、课件和相关项目,便于回看案例、查阅代码并继续实践。