做 Zettelk 时,我总觉得自由意味着拥有更多选项。
页面可以使用 SPA 路由,可以显示悬浮预览和内容图谱;文章可以连接图片、标签和双链;系统还可以接入 MCP 与 LLM。每增加一种能力,项目似乎就离“理想的数字花园”更近一步。
但选项没有自动变成自由。它们先变成了一连串需要持续作出的决定:移动端要保留哪些入口,图谱如何布局,标签怎样统一,图片由谁管理,组件在什么时机加载,一个未来也许有用的功能是否值得现在维护。
每个选择都合理,所有选择放在一起却占据了写作本身。
后来 mdout 主动删除许多能力,把产品压缩到少数命令和一条朴素管线:Markdown 输入,内容检查,静态 HTML 输出。它能做的事情少了,我反而更容易开始写,也更有把握让站点长期运行。
这段经历迫使我修正一个直觉:自由并不总是来自更多可能。有时,真正阻止行动的恰恰是每一步都必须重新面对所有可能。
好的约束所做的,是把那些反复出现的临场判断提前变成清楚、可检查、可以修改的边界。它减少了一部分选项,也把注意力释放给更重要的决定。
选项也会形成债务#
我们很容易把自由理解为“我随时可以选择”。如果一个工具提供十种配置,似乎就比只提供两种配置更自由;一种语言允许更多隐式行为,似乎就比规则严格的语言更灵活;一个 Agent 能访问所有文件和工具,似乎就比权限受限的 Agent 更强大。
但每个尚未决定的选项,都会在未来某个时刻要求判断。
配置需要组合,组合需要测试,兼容关系需要维护,异常路径需要处理。今天保留的可能性,会变成明天的决策债务。它不一定出现在代码行数里,却会出现在每次修改前的犹豫中:改变这里,会不会影响那个没人记得的模式?
因此,选项数量和行动自由并不是同一件事。
如果边界清楚,我可以快速行动,因为知道哪些事情不会发生。如果边界模糊,我看起来拥有更多选择,却需要先证明每一步都不会破坏未知的依赖。
类型把决定变成可以检查的形状#
第一次学习类型系统时,我也把类型看成限制。它告诉我哪些值不能放在一起,哪些函数不能调用,哪些程序甚至没有运行的机会。动态语言看起来更自由,因为许多决定可以推迟到运行时。
后来我逐渐理解,类型真正有价值的地方不是禁止,而是表达。
假设一个界面只用 string | null 表示加载结果。null 可能意味着尚未开始、正在加载、没有数据或者请求失败。类型给了实现很大的自由,却把解释负担留给每一个调用者。
如果状态被明确表示为:
NotLoaded
Loading
Loaded(Data)
Failed(Error)类型数量增加了,允许随意表达的空间缩小了,但每个状态可以做什么变得清楚。界面不再需要猜测 null,编译器也能提醒还有哪个分支没有处理。
这种约束没有替我作出产品决定。它只是要求决定留下可以检查的形状。
类型系统因此像一种提前进行的讨论:这个值可能处于哪些状态,谁负责处理失败,什么组合根本没有意义。讨论一旦进入类型,后来使用它的人就不必在每个调用点重新发明答案。
所有权让责任有了位置#
C 给程序员很大的资源控制权。申请的内存何时释放,文件何时关闭,锁何时归还,都可以根据需要决定。这种控制有时不可替代,但它也意味着每一条退出路径都必须正确承担清理责任。
RAII 减少了这种自由。资源获取与对象生命周期绑定,离开作用域时由析构过程完成清理。资源一旦被正确封装,程序员就不需要在每一个返回点重复写释放逻辑。
失去的是“随时决定是否清理”的选项,获得的是在异常、提前返回和重构之后仍能预测资源行为的能力。
这揭示了约束的另一个作用:
约束不是取消责任,而是让责任有明确归属。
当所有权模糊时,每个参与者都可能以为别人会处理;当所有权清楚时,局部代码就可以放心组合。自由不再是“谁都可以做”,而是“我知道自己负责什么,也知道什么不需要由我担心”。
产品边界也是一种类型#
mdout 不提供 CMS、在线编辑器、封面图片系统、复杂发布状态机或自动翻译。它也没有为了减少外部依赖而重新实现 Markdown 解析、模板、搜索索引和开发服务器。
这些限制并不说明那些功能没有价值。它们只是回答了一个更具体的问题:哪些问题属于这个产品?
mdout 的契约可以被压缩为:
Markdown
↓
内容检查与静态构建
↓
HTML边界清楚以后,很多决定自然消失。文章不需要经过数据库状态迁移,生产页面不依赖在线运行时,构建结果可以直接检查,日常工作可以围绕少数命令完成。
一个产品的边界很像它的类型。边界越模糊,使用者越需要猜测它会接受什么输入、维护什么状态、承担什么责任。边界越明确,产品越容易与其他工具组合,也越容易被替换。
删除功能不是目的。目的是避免让一个系统承担它无法长期兑现的承诺。
Agent 的边界不能只写在提示词里#
LLM 让约束问题变得更直接。
一个只生成文字的模型即使答错,影响通常停留在对话里。一个能够调用工具的 Agent 可以读取文件、执行命令、修改仓库、控制浏览器和访问外部服务。能力从“建议”变成“行动”之后,边界就不能只靠一句“请小心”。
约束可以分成不同层次:
自然语言要求
↓
结构化输出与 Schema
↓
工具和目录白名单
↓
重要操作前确认
↓
差异、测试与验收条件“不要修改无关文件”是一条有用的提示,但只允许写入工作区是更可靠的系统边界。要求 Agent 认真检查是一种期待,提交前展示 diff、运行测试则留下了可以验证的证据。提醒它不要发送敏感信息不如从权限上阻止访问不需要的数据。
这些机制不是因为模型特别不值得信任。任何能够独立行动的主体都需要责任边界。只不过模型可以快速生成看似合理的下一步,更容易让人忽略行动背后的权限和后果。
能力越强,边界越不能只存在于提示词里。
不是所有约束都产生自由#
我曾经做过一个 AI 标签工具,并规定每篇文章恰好拥有五个标签。这个规则清楚、一致、可以自动检查,也很容易交给模型执行。
但它回答的是“怎样让标签整齐”,没有证明“五个标签”真的改善了写作或阅读。为了满足固定数量,系统可能需要制造边缘标签;为了维护标签池,我又引入别名、分类和自动扩展。一个执行得很好的约束,也可能在优化错误的目标。
因此,清晰和严格还不够。能够产生自由的约束通常还需要满足几个条件:
- 它服务一个真实、明确的问题。
- 使用者能够理解它为什么存在。
- 它在接近风险的地方被执行。
- 限制强度与潜在后果相称。
- 新证据出现时,它可以被修改。
- 必要的例外有明确而可审计的路径。
相反,如果一条规则无法解释、无法质疑、无法退出,只要求服从,那么它不是自由的基础,而是控制本身。
好的约束会减少不重要的选择,同时保留修改约束的能力。
自由不是随时决定一切#
类型系统减少了非法状态,RAII 减少了失去所有者的资源,产品边界减少了无止境扩张的问题空间,Agent 权限减少了不可逆操作的范围。它们的共同点不是规则更多,而是把决定放到了更适合被理解和检查的位置。
我现在不再追求一个没有约束的系统。那样的系统看起来开放,实际却会把复杂度分散给每一次使用、每一个调用者和每一个未来的维护者。
我更希望边界是可见的:知道它在哪里,为什么存在,保护了什么,以及什么证据足以让我修改它。
自由不是每一刻都面对所有可能,而是在可信的边界内行动,不必反复担心脚下是否会突然消失。
约束真正带来的自由,不是替我决定一切,而是让我把有限的判断力留给那些仍然必须由我决定的事情。