文档指引架构
代码生成越来越容易,但系统边界、主流程、非目标和验收标准必须先被写清楚。
我在 NeoCode 的第二阶段写过一份总结,标题很普通,叫 Second Milestone Summary。
但现在回头看,那应该是我第一次比较明确地意识到:这个项目的问题已经不只是“代码能不能继续写出来”,而是“系统边界有没有先想清楚”。
当时我写下了一段反思:我们一直在往前做,也做出来了不少东西,但很多时候还是偏“先实现、再整理”。短期看,这种方式推进很快;但一旦进入多人协作、模块并行开发、接口开始互相咬合的时候,问题就会暴露出来。
主流程虽然大概知道,但没有被足够明确地固定下来;模块边界在开发过程中不断变化,大家理解也不完全一致;文档更多像补充说明,而不是先于实现存在的约束;很多讨论会自然滑到细节实现,反而把更上层的系统设计稀释掉了。
在 AI 编程时代,代码实现本身变快了,但系统并不会因为代码变多而自动变清楚。
甚至很多时候,AI 越能写代码,系统越容易走向另一种混乱:这里补一层,那里兼容一下,先让它跑起来,先把异常兜住,先多加一个状态,先多留一个扩展点。
最后功能是有了,代码也变多了,但系统不一定真的变好。
文档指引架构,而不是文档反映架构。
还有许老师说过的话:
好的架构做减法,功能做乘法。
文档理解不应该成为“事后总结”
代码写完了,功能做出来了,再把它整理成文档。文档的作用是告诉别人:这个项目现在有什么、怎么用、怎么配置、遇到问题怎么排查。
这种文档当然有价值。README、使用手册、接口说明、配置指南,都很重要。
但在 NeoCode 里,我后来发现,只把文档当成“事后总结”是不够的。
因为当系统复杂度上来以后,文档如果只是在追着代码跑,它就很难约束代码。代码里已经长出了某种结构,文档只能把它描述出来;模块边界已经被实现耦合住了,文档只能解释为什么现在是这样;接口已经互相咬合了,文档只能补充注意事项。
这时文档更像“现状说明书”,而不是“架构约束”。
问题是,AI 编程会让“先实现、后整理”的代价变得更大。
因为 AI 很擅长顺着当前代码继续补东西。你告诉它这里有问题,它会补一层兼容;你告诉它那里可能失败,它会加一个兜底;你告诉它未来可能要扩展,它会提前抽象一层。
这些改动单独看都合理,但组合起来以后,系统可能会越来越臃肿。
这就是我在第二阶段总结里写的那句话:AI 编程更需要“先想清楚再生成”。现在代码实现本身越不难,真正稀缺的是架构判断、模块边界、文档表达和抽象取舍。
文档应该指引架构,而不是反映架构
架构文档应该是一种“实现前的约束”,它不是代码写完后的说明书,而是系统开始生长前的边界定义。
一个好的架构文档,至少应该先回答这些问题:
- 主流程是什么?
- 当前已经落地的是什么?
- 目标态是什么?
- 哪些只是未来方向,不能写成现状?
- 哪些是当前阶段明确不做的?
- 模块之间怎么分工?
- 边界和桥接点在哪里?
- 什么行为必须经过统一入口?
- 验收标准是什么?
- 风险和回滚方式是什么?
这些问题看起来不像代码,但它们决定了代码会怎么长。
如果没有文档先行,AI 和人都会倾向于从局部问题出发:这里报错了就修这里,这里缺功能就补这里,这里未来可能扩展就加抽象。这样短期很快,但主流程会越来越模糊。
反过来,如果文档先定义了主流程、边界、非目标和验收标准,后面的实现就会有方向。
这就是我说的:
文档指引架构,而不是文档反映架构。
文档不是把已经长出来的系统拍一张照片,而是告诉系统应该往哪里长、哪些地方不能长、哪些地方暂时不应该长。
AI 编程更需要文档先行
传统开发里,“先写代码再整理文档”的问题已经存在。但在 AI 编程里,这个问题会被放大。
因为 AI 的优势是生成速度。
它可以很快生成模块、接口、适配层、兜底逻辑、错误处理、状态机和测试样例。它也很容易给出一个看起来完整的方案:Scheduler、Worker、Coordinator、Retry、Cancel、State Store、Dependency Resolver、Result Merge……
这些词都很工程化,也都可能有价值。
但问题是:当前阶段真的需要它们吗?
AI 不会天然帮我们收敛系统。很多时候,它更像是在当前代码和当前 prompt 的方向上继续做加法。只要你让它“完善一下”,它就会补更多东西;只要你让它“考虑边界情况”,它就会加更多分支;只要你让它“设计得完整一点”,它就会加更多模块。
所以在 AI 编程里,文档先行的意义更大。
文档要先告诉 AI,也告诉团队:
- 当前阶段的目标是什么;
- 什么是最小闭环;
- 什么是不做;
- 哪些路径不能绕过;
- 哪些接口必须复用;
- 哪些状态不能新增;
- 哪些能力只是未来目标;
- 怎么判断真的完成。
否则 AI 会非常努力地把代码写多,但不一定把系统写清楚。
我其他的博客中有一句话:
AI 可以快速生成代码,但不会自动生成边界。边界必须先由人和文档定义。
架构做减法:先决定不做什么
“架构做减法”是我在 NeoCode 里最重要的收获之一。
以前我会觉得,架构设计是把系统设计得完整一点:多考虑一些模块,多留一些扩展点,多准备一些未来能力。
但后来我发现,对一个正在快速变化的项目来说,架构更重要的是先做减法。
减法不是少做功能,而是挡住当前阶段不该进入系统的复杂度。
比如 SubAgent / DAG 这条线。
一开始 DAG 看起来很自然:复杂任务可以拆成多个节点,节点之间有依赖关系,多个 SubAgent 可以并发执行,最后再汇总结果。这个方向长期看不是错的,但在当时的 NeoCode 里,单 Agent 主链路、工具调用、权限边界、状态回灌、测试验证都还没有完全稳定。
如果这时候直接引入 DAG、Scheduler、多 Worker 和复杂结果合并,就会让系统状态迅速膨胀。
所以后来我们收敛到 inline SubAgent:先让主 Agent 可以显式调用一个受控子任务,子任务在边界内执行,再把结构化结果回灌给主 Agent。
这就是一次架构减法。
不是因为 DAG 没价值,而是因为它不应该早于最小可验证闭环。
Hooks 也是类似。
理论上,Hooks 可以开放很多能力:command、http、prompt、agent,甚至允许用户在各种生命周期点上执行外部逻辑。但如果一开始就全部开放,系统会立刻面临命令沙箱、HTTP allowlist、环境变量泄露、prompt 注入、循环调用、预算控制、repo trust 等一堆问题。
所以 P6-lite 阶段选择 builtin-only,不开放 external hooks。
这也是架构减法。
它不是说 external hooks 永远不做,而是说当前阶段先把安全、可解释、可配置、可测试的最小闭环做好。
再比如飞书接入。
最短路径可能是:飞书 Adapter 收到消息,直接调用 Runtime。这样也许能更快跑通。但长期看,飞书就会变成一条绕过 Gateway 的特殊路径。
所以更合理的约束是:Feishu Adapter 作为 Gateway 外部 Client,只通过 Gateway 协议进入系统,不直接调用 Runtime,不把飞书逻辑塞进 Gateway 内部。
这还是架构减法。
减掉的是“快速直连”的诱惑,保留下来的是统一入口。
不是功能少做,而是先挡住不该进入当前阶段的复杂度。
功能做乘法:能力复用
架构做减法的目的,不是让系统永远少做功能,而是为了让后续功能可以做乘法。
Gateway 就是最典型的例子。
如果没有 Gateway,TUI、Web、Desktop、Feishu、Runner 都需要各自对接 Runtime,各自处理认证、事件、状态、权限、错误和恢复。每增加一个入口,都要重复实现一套逻辑。
但有了 Gateway,入口就可以变成复用同一套能力的不同控制面。
Terminal 可以用,Web 可以用,Desktop 可以用,Feishu 可以用,Runner 也可以通过 Gateway 接入。每个入口不需要重新发明 Agent,只需要接入同一套协议和事件流。
这就是功能做乘法。
ToolManager 也是一样。
如果 filesystem、bash、Todo、MCP、SubAgent、ask_user 都各自散落在 Runtime、TUI、Adapter 里,功能越多,安全边界越乱。
但如果所有模型可调用能力都统一经过 Tools / ToolManager,再经过权限、sandbox、facts 和结果回灌,那么每新增一个工具,都能复用同一套执行和审计路径。
这也是功能做乘法。
Runtime Loop 也是一样。
当 Runtime 的上下文构建、模型调用、工具执行、结果回灌、完成判断这些主链路稳定之后,Skills、Hooks、HITL、SubAgent 才能围绕它扩展,而不是侵入它、复制它、绕过它。
功能增长不应该靠复制路径,而应该靠复用边界。
架构越清楚,后续功能越容易增长;架构越混乱,功能越多越难维护。
当前态和目标态必须分清
这是我在 Second Milestone Summary 里写到的另一个问题:我们有时会一边讨论“现在代码是什么”,一边讨论“未来理想应该是什么”,但没有明确拆开。
这个问题非常隐蔽。
文档看起来很完整,但它可能混合了三种东西:
- 当前已经实现的能力;
- 正在开发但还不稳定的能力;
- 未来希望达到的目标态。
如果这三种东西写在一起,后果会很麻烦。
读文档的人会以为未来目标已经实现;写代码的人会在“先落地”还是“先为未来预留”之间摇摆;评审时也很难判断某个 PR 是在修当前问题,还是在提前实现目标态。
我现在觉得,架构文档里最应该明确区分:
Current State:现在是什么
Target State:未来希望是什么
Non-goals:当前明确不做什么
尤其是 Non-goals。
很多时候,Non-goals 比 Goals 更能约束架构。
比如:
- 当前不做 DAG Scheduler;
- 当前不开放 external hooks;
- 当前 Feishu 不直接调用 Runtime;
- 当前 Skill 不授予工具权限;
- 当前不把 Runner 设计成暴露本机入站端口;
- 当前不让模型自然语言 final 直接等于完成。
这些“不做”会让团队更容易形成共同语言,也让 AI 生成代码时不至于无限扩展。
所以我现在越来越觉得:
当前态和目标态混在一起,是架构文档里最隐蔽的风险。
流程先于细节
Issue #235 里还有一个判断:先把主流程串起来,比先做细节更重要。
比如一个 Agent 任务到底怎么跑:
用户输入
-> Gateway / TUI 进入
-> Runtime 构建上下文
-> Provider 返回工具调用或文本
-> ToolManager 执行工具
-> Permission / Sandbox 约束执行
-> ToolResult 回灌 Runtime
-> Facts / Todo / Snapshot 更新
-> Acceptance 判断继续或完成
-> Gateway / TUI / Feishu 展示结果
这条主流程如果不清楚,后面的功能都会乱长。
飞书接入可能直接找 Runtime;Todo 可能只变成 UI 状态;SubAgent 可能绕过 ToolManager;Hooks 可能接管终态决策;TUI 可能把被 Runtime 拦截的 final 当成正常完成;文档可能把未来目标写成当前能力。
所以主流程不是“画给别人看的图”,而是所有模块协作的约束。
我后来觉得,一个项目在扩展前,至少要先把主流程写到大家都能复述。
只有主流程清楚,细节才是顺着长出来的。
怎么写架构文档
如果让我写一个架构设计 Issue 或文档,我会按这个结构来:
## Background
为什么要做这件事?现在遇到的问题是什么?
## Current State
当前已经实现了什么?哪些路径是真实存在的?哪些行为已经稳定?
## Target State
目标态是什么?希望系统最终变成什么样?
## Non-goals
当前阶段明确不做什么?哪些能力暂缓?哪些复杂度不能提前进入?
## Main Flow
主流程是什么?从入口到 Runtime、Tools、State、Output 的链路如何流动?
## Boundaries
哪些模块能调用哪些模块?哪些调用必须禁止?哪些状态由谁维护?
## Design
当前阶段的最小设计是什么?为什么不是更复杂的方案?
## Risks
风险是什么?可能在哪些地方失控?
## Acceptance Criteria
怎么证明它真的完成?需要哪些测试、日志、状态、截图或文档?
这个模板不是为了形式完整,而是为了逼自己把“当前态、目标态、非目标、主流程、边界和验收”说清楚。
尤其是 Non-goals 和 Acceptance Criteria。
Non-goals 决定系统不会乱长;Acceptance Criteria 决定系统不会只靠模型说“完成了”。
现在回头看,文档先行、架构减法、功能乘法并不是三件分开的事。
文档先行,是为了提前定义边界。
架构减法,是为了把不该进入当前阶段的复杂度挡在外面。
功能乘法,是边界稳定之后自然发生的结果。
如果没有文档,架构很容易只剩下实现现状;如果没有减法,系统会在 AI 的加速下越来越臃肿;如果没有稳定边界,功能增长就只能靠复制路径,而不是复用能力。
所以它们其实是一条链:
文档先行
-> 定义边界和非目标
-> 架构先做减法
-> 主流程稳定
-> 功能复用边界
-> 能力做乘法
AI 可以生成代码,但人必须先写清楚边界
AI 编程让实现变快了。
这当然是好事。没有 AI,很多功能我可能根本推不动,很多文档、Issue、PR、测试也很难这么快整理出来。
但 AI 越能写代码,人越不能放弃架构判断。
因为系统不会因为代码变多而自动变好。
如果没有文档先行,AI 会顺着现状继续加代码;如果没有架构减法,复杂度会提前进入系统;如果没有主流程和边界,功能会越做越散;如果没有验收标准,模型会很容易自己宣布完成。
文档不是代码写完后的说明书,而是系统开始生长前的约束。
架构做减法,不是少做功能,而是让功能能在稳定边界里做乘法。
AI 可以生成代码,但人必须先写清楚边界。
(完)
评论
评论组件加载中…