一座没有共同语言的塔:用六个计算思想理解领域建模
一封让我卡住的简报
那天早上,我打开自己搭的雷达简报,第一条重点关注是 Simon Willison 发布的 sqlite-utils 4.0 更新说明。
这是一次完全正常的运行。抓取没有失败,评分函数照常返回了数字,模板渲染得干干净净,卡片上还贴心地写了一句推荐理由。系统里没有任何一处报错。
问题只是:我根本不想读它。
我最近在做的事情跟 sqlite 的命令行参数毫无关系,而系统之所以把它推到最前面,唯一的依据是我在关注名单里写过 Simon 的名字。那一刻我意识到,我没法把这件事描述成一个 bug。因为要报一个 bug,你得先能说清楚正确的行为是什么,而我说不清楚。
我在搭的东西叫个人信息中枢。听起来是个再普通不过的软件项目:从各处收集信息,判断什么值得注意,整理成雷达简报,再把有长期价值的内容沉淀成知识。真正动手之后我才发现,最让我不安的从来不是某段代码写错了,而是一些看上去最基本的问题,我自己也答不上来。
比如,“雷达”到底是什么?
它是一个抓文章的程序,一个推荐系统,一份定期生成的报告,还是我和外部世界之间的一层注意力过滤器?这四种说法会导出四种完全不同的系统。
再比如,“重点关注”是什么意思?
是这篇文章来自我关注的人,还是它跟我眼下正在啃的问题有关?我把 Simon 加进名单,是否等于他每一条 sqlite-utils 更新都该顶上首位?显然不是。可我从来没有把这句“显然不是”写进任何地方——没写进文档,没写进代码,也没写进给代理的提示词。关注一个人、关注一个主题、对一篇具体文章感兴趣,是三种不同的关系,而我的系统把它们压成了同一个字段。
还有更深的一层。一条信息从网页进入系统之后,究竟经历了什么?原始文章、抓取结果、证据、候选条目、推荐理由、简报卡片、用户反馈、长期知识——它们看上去都在谈论“同一条信息”,实际上根本不是同一种东西。它们由不同环节产生,可信程度不同,更新方式不同,出错代价也不同,本来应该由不同模块负责。
平时这些问题会被代码盖住。数据库里加一张表,前端里加一张卡片,提示词里补两句话,功能就跑起来了。有了编程代理之后更是如此:只要我把需求说得足够像一个任务,系统很快就会变化。按钮消失了,标题改好了,报告重新跑出来了,我甚至来不及问自己刚才那一步意味着什么。
速度越快,另一件事反而越刺眼:代码已经替我做了一大堆决定,而我还没有为这些决定建立一套稳定的语言。
这正是领域建模要解决的问题。它不是教我们把现实里的名词都翻译成 Java 类,也不是要求每个项目都配一套庞大的 DDD 仪式。它对付的是一个更朴素、也更难的问题:
当一个系统不断变化时,我们怎样让人、代码和 AI 仍然在谈论同一个世界?
这篇文章不打算从“实体、值对象、聚合根”开始。我们换一条路走:把领域建模当成六个我们其实很熟悉的计算问题——分区、样例运行、事件序列、类型、读写分离、多尺度地图。它们分别回答六个问题:
- 一个大世界应该怎样切成若干可以独立理解的小世界?
- 在归纳规则之前,怎样先把真正发生过的故事讲明白?
- 怎样按时间顺序理解系统,而不是只盯着静态名词?
- 怎样让同一个词在同一个边界内只有一种可执行的含义?
- 怎样区分事实本身、改变事实的入口,以及给不同场景看的投影?
- 怎样让不同角色在不同缩放级别上看懂同一套系统?
把这六件事想通,DDD 就不再是一堵术语墙。它更像一门地图学:地图从来不等于领土,但没有地图,越来越快的交通工具只会让我们更快地迷路。
塔还在上升,语言却在消失
Armin Ronacher 在 The Tower Keeps Rising 里借用了巴别塔的故事。他真正关心的不是砖,是共同语言。
传统的软件开发有很多摩擦。你要改别人负责的存储层,通常得先读代码、问背景、解释方案、等待评审。我们习惯把这些统统归为低效,盼着有工具把它们干掉。但摩擦里并不全是浪费,其中有一部分做的是同步理解:我把你脑中的模型装进我的脑子,你也在解释的过程中重新检查那个模型是不是还成立。
编程代理会大幅削掉这种摩擦。一个人让代理加认证,另一个人让代理换缓存,第三个人让代理重做数据库。每一项改动单独看都合理,代码能编译,测试全绿。过去大家必须先交谈才能继续施工,现在每个人身边都站着一个不知疲倦的翻译兼施工队,哪怕彼此早就不再共享同一张建筑图,塔仍然在往上长。
这跟经典的巴别塔故事恰好相反。故事里,人们失去共同语言之后,工程停了;在 AI 辅助的软件里,共同语言消失之后,工程不会停。没有立刻发生的故障,才是最危险的地方。
用分布式系统来看这件事会更清楚。
假设团队里每个人脑中都有一份“系统模型副本”。开会、评审、结对、文档、争论,就是副本之间的同步协议。过去代码修改的吞吐量比较低,同步协议虽然笨重,大体还跟得上。现在代理把写代码的速度提高了一个量级,而人的理解带宽并没有同倍增长。于是写入速率超过了同步速率,系统进入一种奇怪的“最终一致”状态——只不过这个“最终”可能永远不会到来。
此时积累的不是我们通常说的技术债,而是理解债。
一项 2026 年的早期研究把“理解债”描述成团队实际掌握的知识与维护系统所需知识之间不断扩大的差距;另一篇讨论“认知债务与意图债务”的文章再往前推了一步,把两件事分开:一是人们不再理解系统,二是当初为什么这样设计压根没有被外化记录。需要说清楚的是,这些仍然是正在形成中的研究概念,不能凭几篇论文就宣布定论。但它们确实准确说出了很多 AI 编程者已经能感受到的东西:代码的健康,不等于系统的可理解性。
测试能告诉我们那个按钮还能点,却告诉不了我们“重点关注”这个概念已经在三个模块里长出了三种含义。类型检查能证明字段存在,却证明不了 source 在一个地方指原始网站、在另一个地方指作者、在第三个地方指证据类型。而代理最擅长的事情之一,就是为每一处局部歧义写出能跑通的适配代码——于是歧义不再当场爆炸,它被封存进了系统。
领域建模的价值,就是为这种高速变化配一套理解同步协议。
DDD 不是 OOP 的豪华装修
很多人第一次碰 DDD,都会冒出一种熟悉感:这不就是 Java OOP 吗?
实体、值对象、仓储、工厂、领域服务、聚合根——看上去确实像一整套面向对象设计模式。大量教程也是从一个 Order 类讲起,接着讨论 OrderItem 该放哪儿、Repository 接口属于哪一层。学到最后,人很容易留下一个印象:DDD 就是把普通 CRUD 写得更讲究一点。
这印象不能说全错,但它把屋顶当成了地基。
OOP 首先关心的是程序组织:状态和行为怎样封装,对象之间如何协作,依赖如何替换。DDD 首先关心的是知识组织:业务里的词到底是什么意思,哪些规则必须同时成立,同一个词在哪些场景下会换意思,谁有权改变什么,以及这些边界怎样进入软件。
两者可以合作,但不在同一层。
可以把 OOP 想成建筑材料。类、接口、继承、组合,是砖、钢筋和梁。DDD 更像城市规划:这里为什么划成住宅区,那里为什么是港口,两块区域之间怎样通行,消防通道不能被什么占住。规划当然可以用钢筋混凝土实现,也可以用木材;同样,领域模型可以用对象实现,也可以用函数、状态机、数据库约束、事件流、策略表,甚至一份进了版本控制的规则文件。
反过来也成立:一个项目里到处是漂亮的类,并不说明它有清晰的领域模型。一个叫 UserManager 的类可以同时接待几十种互相矛盾的“用户”;一个巨大的 Article 对象可以一人分饰四角——原始网页、推荐候选、知识条目、前端卡片。它们在语法上完全合格,在语义上是一团雾。
DDD 真正经久不衰的部分,主要来自它的战略设计:
- 用“通用语言”让业务讨论和软件表达尽量共用同一套词;
- 用“限界上下文”承认大型系统不可能只有一个全球统一模型;
- 用上下文之间的关系说明模型怎样翻译、依赖和协作;
- 让关键业务规则成为模型的中心,而不是散落在控制器、脚本和提示词里。
至于实体、值对象、聚合这些战术模式,它们在某些场景下很有用,但绝不是学习领域建模的唯一入口。对一个个人信息中枢来说,最先要搞清楚的通常不是“推荐条目是不是聚合根”,而是“推荐条目和原始证据究竟是不是同一种东西”。前一个问题问早了,后一个问题决定整个系统会不会长期混乱。
第一个计算思想:分区,让不同的世界不必强行统一
计算机面对一个巨大问题时,最常用的招数之一是分而治之。
快速排序不会试图在一次比较里解决整个数组,它只负责把元素扔到基准的左右两边,剩下的交给更小的自己。操作系统把进程关进各自的地址空间;网络被切成自治系统和子网;数据库用分区限制一次操作必须关心的数据量。分区不是为了否认事物之间有联系,而是为了压缩每一次推理需要装进脑子的范围。
领域建模里的“限界上下文”,本质上就是一种分区算法。
Martin Fowler 对 Bounded Context 的概括很直接:大型模型很难保持内部统一,所以要把它拆进不同边界,并且说清楚边界之间的关系。边界内部,语言必须一致;跨过边界时,允许同一个现实对象拥有不同模型,但你必须知道怎么翻译。
这一点相当反直觉。人们通常认为,系统设计得好,就应该为“文章”“用户”“项目”各建一个终极模型,然后全系统共享。实际情况恰恰相反:越想造出全局唯一的大模型,模型越容易长成一只谁也不敢动的巨兽。
拿“文章”来说。
在采集上下文里,文章是一个网络资源:URL、抓取时间、正文哈希、作者、发布时间、抓取状态。
在雷达上下文里,它是一个待评估候选:与当前关注主题的相关性、信息新颖度、行动价值、为什么值得注意,以及是否应该进入本期简报。
在知识上下文里,它可能根本不再是一篇文章,而是若干条可以被核验的主张、支撑它们的证据,以及长期知识候选。
在发布上下文里,它又变成一件内容产品:标题、摘要、排版、栏目、发布日期、目标读者。
现实中也许只有一个网页,但系统里不该只有一个包打天下的 Article。这些模型的生命周期根本不同:原始网页可以永久保留,雷达评分会随当前目标漂移,知识主张需要审查,发布稿可以反复改写。把它们塞进同一张表,等于要求一张地图同时精确显示地质构造、地铁换乘、学区边界和外卖配送时间。
一张地图当然画得下所有图层。只是画完之后,没人还能用它找路。
所以限界上下文的关键不是画几个方框,而是做出三类决定。
第一,什么语言只在这个边界内成立?
“重点”在雷达上下文里的意思是高相关、高价值、值得占用注意力;在任务管理上下文里,“重点”大概是指高优先级、必须尽快执行。它们最好不要共用一个含义模糊的 priority 字段,否则某天一定会有人把待办事项的紧急程度算进文章排序。
第二,谁拥有事实?
作者姓名和文章发布时间,应该来自原始来源或经过核验的权威记录;“我为什么可能感兴趣”是雷达的推断;“我明确表示不感兴趣”是用户反馈事实。三者的权威来源完全不同,不能因为它们最后显示在同一张卡片上,就让前端卡片反过来成为它们的主人。
第三,边界之间怎样交流?
采集上下文可以把“观察到一篇新文章”交给雷达,但不替雷达决定它值不值得推荐;雷达可以把“这篇文章被明确否定”交给偏好上下文,但不能把一条短期反馈直接改写成永久人格标签;知识上下文可以引用原文证据,却不该把雷达生成的推荐理由冒充成原作者的观点。
好的分区不是让模块彼此失忆,而是让每一次跨界都变成一次显式翻译。
第二个计算思想:先跑一个样例,再概括规则
程序员很容易从名词开始建模。
看到“个人信息中枢”,我们立刻会列出 User、Source、Article、Knowledge、Report。这跟做数学题先设未知数很像,动作很自然。但现实领域里的难点往往不藏在名词里,而藏在动词和顺序里。
一篇文章被抓下来之后,是先判断来源可信度,还是先判断主题相关性?推荐理由由谁产生?当我说“我对 sqlite-utils 4.0 不感兴趣”时,系统应该降低对 Simon 的整体权重,降低对这一类产品更新的权重,还是只否定这一条内容?如果他下一篇写的是 AI 编程中的认知债务,还要不要推给我?
光画出五个名词,一个都答不了。
这时候最有用的动作不是继续往上抽象,而是运行一个具体样例。就像算法课上的手工跟踪:给定一组输入,把每一步的状态变化都写下来,一直写到结果出现。很多错误只有在真跑一遍时才会现形,你在纸上盯着伪代码看半小时都看不出来。
Domain Storytelling 把这个做法变成了一种非常轻的协作方法。它要求参与者讲一个具体故事,并用最简单的句法记录:
谁,做了什么,作用于什么,又把什么交给了谁。
不急着画条件分支,不急着归纳所有例外,先讲最常见的那一个场景。官方指南建议从“80% 情况”和顺利路径开始,因为过早抽象拿到的往往是既错误又无用的规则。故事讲完之后,再回头追问遗漏和例外。
于是我给那封让我卡住的简报讲了个故事:
- 采集器发现 Simon 发布了一篇 sqlite-utils 4.0 的文章。
- 采集器保存原始链接、作者、时间和正文证据。
- 相关性评估器读取我当前的关注主题、进行中的项目和既往反馈。
- 它判断这篇文章主要是工具版本更新,与我眼下的核心问题联系较弱。
- 简报生成器把它放进普通新增,而不是重点关注。
- 我查看简报,明确表示对这类内容不感兴趣。
- 偏好记录器保存这次反馈,连同它的适用范围。
- 下一次评估器用上这条反馈,但仍然保留 Simon 其他主题进入重点关注的可能。
故事一写出来,原本糊在一起的概念自己就分开了。
“关注 Simon”是一个来源发现策略,不是无条件推荐策略。
“不喜欢这篇版本更新”是一条带范围的反馈,不等于永久拉黑作者。
“重点关注”是简报编排的结果,不是文章自身的固有属性。
“为什么值得注意”是系统结合我的处境生成的解释,不是原文摘要。
这就是故事的力量:它逼着抽象去接受一个真实案例的检验。而我在第三步和第四步之间卡住的地方,正是我系统里真正缺的那一环——现实中那台评估器压根没有读取“我当前在做什么”,它只读了名单。
初学 DDD 最容易犯的错,是想先把全部概念读懂再动手。更好的顺序恰好倒过来:先找一个最近真的让你困惑的案例,从头到尾讲一遍,再从故事里提取名词、规则和边界。领域知识不是从词典里推导出来的,它往往藏在三类追问里——“接下来呢”“你凭什么这么判断”“如果失败会怎样”。
第三个计算思想:把静态世界展开成事件序列
如果说 Domain Storytelling 像逐帧回放一段录像,那么 EventStorming 就是把许多段录像的关键帧铺在同一条长时间轴上。
EventStorming 最初就是为了快速探索复杂业务领域而设计的协作建模方法。它的起点既不是数据库表,也不是类,而是已经发生、并且对领域有意义的事件。事件一般用过去式表达,因为它描述的是一个已经成立的事实。
对雷达来说,下面这几句话的差别很要紧:
- “抓取文章”是命令,表示某个角色希望系统做什么;
- “文章已抓取”是事件,表示某件事确实发生了;
- “如果来自关注作者,就进入相关性评估”是策略;
- “本期重点关注已生成”是另一个事件;
- “用户已否定推荐”又会触发新的策略。
为什么要分得这么细?
因为静态数据会把因果关系吃掉。
我那次事故在数据库里最后只剩一行:
1 | article_id = 42 |
这行数据告诉我现在是什么,不告诉我为什么变成这样。它回答不了:最初是不是被错误列成了重点?是哪一条证据导致了推荐?我否定的是作者、主题还是这篇文章?反馈发生在报告发布之前还是之后?下一次评估应该复用其中哪一部分?
事件时间轴会把这些因果重新展开:
1 | 发现来源 |
当这条时间轴摆在面前,人们通常会开始吵架。“生成候选”和“评估相关性”是不是同一步?“编入重点关注”之前需不需要先验证那条解释?用户反馈应该直接改评分,还是先进入一份可审查的偏好记录?这些争论不是会议噪声,它们本身就是模型成形的过程。哪怕只有你一个人,跟自己吵完这一轮,模型也已经不一样了。
事件还有个额外的好处:它天生适合和 AI 合作。
代理很擅长根据已有模式补全流程,也很擅长悄悄把歧义填平。如果我只说“优化雷达推荐”,它多半会直接去改一个评分函数;如果我给出事件序列、每个事件的输入证据、触发者和后续策略,它就很难再把来源发现和兴趣判断混成一团。
需要澄清的是,用事件思考不等于必须采用事件溯源,也不意味着系统里非得部署消息队列。EventStorming 首先是一种思考工具。在纸上画流程图不代表生产环境必须装一个工作流引擎;在墙上贴事件卡片,也不意味着每张卡片都要变成一个 Kafka topic。先用事件发现领域,要不要用事件架构实现,是后面才需要做的工程决定。
第四个计算思想:类型不是字段形状,而是可执行的含义
静态类型系统最迷人的地方,不是它能拦住你把字符串当整数传,而是它能一次性排除掉一整类“本来就不该存在的程序”。
如果金额和温度在程序里都只是 double,编译器没有任何理由阻止我们把 37.2 元加到 28 摄氏度上。给它们不同的类型,等于告诉系统:底层表示虽然长得一样,但它们属于不同的语义世界。
领域语言应该发挥同样的作用。
在个人信息中枢里,下面这些东西都可能包含一段文字加一个链接:
- 原始材料:外部世界实际发布的内容;
- 证据摘录:能够支持某项判断的可追溯片段;
- 系统推断:模型根据证据得出的解释;
- 推荐理由:系统针对当前用户处境给出的价值判断;
- 知识主张:准备长期保存、可被检索和更新的陈述;
- 展示投影:为某个界面而组织的文本。
如果它们全都叫 content,那就相当于把人民币、摄氏度和公里一起声明成 double。短期非常方便,长期却完全没办法表达合法操作的边界:推荐理由可以被误写回权威事实,展示摘要可以被当成原文证据,旧投影可以覆盖更新的规范记录。而这些错误发生的时候,同样不会有任何一个测试变红。
DDD 说的“通用语言”,不是整理一张术语表就算完事。它要求语言进入对话、进入代码、进入规则,最好还能进入校验。
比如,可以给几个核心词写上带行为的定义:
来源:系统观察外部信息的入口。加入来源只提高“被发现”的概率,不直接提高“被推荐”的等级。
候选:已被发现、等待评估的内容。候选还不是简报条目,更不是知识。
重点关注:在某一期简报、某个用户处境下,被判断为值得优先占用注意力的候选。它是一次有时效的决策结果,不是内容的永久标签。
证据:可回溯到来源、用于支持事实或判断的材料。证据不等于结论。
投影:从规范数据派生、为特定消费场景优化的表示。投影可以随时重建,它不拥有被投影的事实。
反馈:用户对一次具体推荐或结果的明确评价。反馈必须保留作用对象和适用范围,不能自动膨胀成对整类内容的永久判断。
接着写“不变量”,也就是系统不管怎么演化都不能破坏的规则:
- 任何重点关注都必须能说明它与当前目标的关系,不能只依赖作者在关注名单里。
- 任何长期知识主张都必须保留可追溯证据,不能只引用模型生成的摘要。
- 展示投影不得成为规范事实的反向写入口。
- 用户否定单篇文章时,系统不得自动把该作者永久降权。
- 当推荐依据已经过期时,旧的重点等级必须能够被重新计算。
这些句子比“采用 DDD 架构”有用得多。后者是一句愿望,前者可以直接变成测试、数据库约束、写入权限,以及交给代理时的验收条件。
第一条不变量,恰好就是我那个早晨真正缺的东西。它不需要任何新框架,一条断言就能表达。
所谓共同语言,说到底不是大家背下同一份词典,而是大家开始用同一组词,拒绝同一批非法状态。
第五个计算思想:把真相、写入和阅读分开
图书馆的书架不是书目检索页面,银行账本不是 App 上那条资产曲线,厨房里的原料也不是端上桌的菜。一个系统之所以需要多种表示,是因为“保持事实正确”和“让某个使用场景更顺手”本来就是两类目标,它们的优化方向经常相反。
在软件架构里,CQRS 用一条很清楚的原则表达这种不对称:改变数据的命令与读取数据的查询,可以使用不同模型。简单系统完全不必上一整套 CQRS 基础设施,但“写模型和读模型不必长得一样”这个思想,几乎适用于所有信息系统。
个人信息中枢尤其需要这种区分。
它的一端是权威事实与受控写入:一次用户明确反馈、一项项目的当前状态、一篇文章的原始证据、一条审查通过的长期知识。这些内容需要知道由谁产生、依据什么、何时更新,以及哪些规则允许它改变。
另一端是消费投影。同一组规范信息,可以被投影成雷达简报、今日摘要、项目页、搜索结果、时间线、手机卡片,或者一个供代理读取的上下文包。每个投影都可以为了阅读效率改变结构,甚至容忍短暂延迟,但它不能悄悄变成第二份真相。
很多信息中枢最后会乱,不是因为数据太少,而是因为写入路径太多。
一个前端页面能直接改数据库;一个同步脚本也能改;一个聊天代理把对话摘要存进自己的记忆;另一个自动化又拿旧页面反向覆盖知识库。每个模块都觉得自己只是“顺手更新一下”,整个系统却逐渐拥有了好几份互相竞争的权威版本。而这类冲突通常不会立刻报错,它们只是让你在三个月后发现某条记录莫名其妙地退回到了旧值。
解法不是把所有东西塞进一张超级表,而是把三件事说清楚。
权威源在哪里?
对于外部文章,原始发布页面和保存下来的证据是事实来源;对于个人偏好,用户的明确反馈记录是事实来源;对于项目状态,经过控制的项目写入流程是事实来源。
谁可以写?
读取可以很宽,写入必须经过受控入口。而且写入动作要表达意图,比如“记录用户对这次推荐的否定反馈”,而不是笼统的“更新 preference 字段”。前者把业务含义留在了系统里,后者把意义丢给调用者自行解释——半年后没人解释得清。
消费者看什么?
雷达页面不需要读取所有原始事实,它需要一份为简报优化的投影;聊天代理也不该自己维护一份长期事实库,而应该通过稳定的读取接口取得当前上下文。投影错了,可以从权威数据重建;权威数据错了,必须走纠正流程。
这也解释了为什么“删掉页面上一块多余内容”和“改变领域模型”不是一回事。界面可以不再显示“查看原始证据”或“问 OpenClaw”,但原始证据的可追溯性和 OpenClaw 的职责边界仍然存在。删除一个消费入口,不该顺手删掉系统内部的证据链;反过来,保留证据链也不意味着每张卡片都得把所有元数据摊给用户看。
把真相和视图分开,我们才可能既保持系统严谨,又让界面清爽。
第六个计算思想:为同一座城市准备不同缩放级别的地图
你要从上海去杭州,一张全国地图很有用;进了地铁站,全国地图帮不上任何忙;要修某栋楼里的水管,地铁图更是彻头彻尾的错误工具。它们都没画错,只是尺度不对。
软件模型也有缩放级别。
C4 模型 用“上下文、容器、组件、代码”四个层次表达软件结构。它的重点不在于凑齐四张图——官方自己就说过,多数团队只用系统上下文图和容器图就够了。真正重要的是那个前提:不同问题需要不同尺度,别指望用一张巨图回答所有问题。
对个人信息中枢,至少值得保留三种视角。
第一张是世界关系图:用户、外部来源、OpenClaw、个人信息中枢、网站和其他消费端之间是什么关系?谁负责理解和编排,谁保存规范事实,谁只负责展示?这张图回答“系统存在于什么环境中”。
第二张是领域边界图:采集、雷达评估、偏好反馈、知识沉淀、项目事实和发布投影分别属于哪个上下文?它们之间怎样传递事件和数据?这张图回答“我们把世界切成了哪些可管理的小世界”。
第三张是关键故事图或事件图:一篇文章如何从被发现走到进入简报,再到用户反馈影响下一次排序?这张图回答“一个具体变化如何穿过系统”。
真要落实现细节,再往下放大到组件、接口和数据结构。不要一上来就画类图,因为类图回答的是“代码怎样组织”,而你此刻可能还没搞清楚“哪些事实应该归谁所有”。
地图上还缺一样东西:为什么当初选了这条路。
这可以用一份很短的架构决策记录,也就是 ADR 来保存。它不需要写成长篇论文,只要记下五件事:
- 当时面对什么问题;
- 考虑过哪些主要方案;
- 最后选择了什么;
- 为什么这样选;
- 代价是什么,以及将来什么条件出现时可以推翻它。
比如:
决定:关注名单只参与候选发现,不直接决定重点等级。
原因:用户关注某位作者,不代表对他每一类输出都感兴趣;重点等级必须结合当前目标、主题相关性和既往反馈。
代价:评估流程更复杂,可能漏掉少数意外有价值的内容。
复议条件:若长期观察显示相关性模型召回率过低,就调整发现与排序之间的权重。
几个月之后,我们多半不会记得某个 if 语句为什么存在。ADR 让意图不必依赖人的记忆,它相当于给代码提交附上一份“决策的源代码”。
回到那封简报:推荐其实是一个探索问题
现在可以回头解决开篇那个具体错误了。
“关注谁”和“推荐什么”之所以容易混为一谈,是因为它们属于两个不同阶段。
关注名单解决的是搜索空间问题。互联网的信息是无限的,任何系统都不可能读完,所以它得先挑一批高概率有价值的来源。Peter、Simon、Armin、Karpathy、Dwarkesh、Ethan Mollick、赫拉利、Vitalik,可以分别作为亲自构建、连接与访谈、现实应用、文明与制度等方向的观察窗口。
但来源只是入口,不是结论。
推荐排序解决的是另一个问题:有限注意力怎么分配。即使一个来源整体优质,它每一次的输出也未必和你当前的目标有关。把来源信誉直接当成文章相关性,就像因为一家餐馆口碑很好,就认定菜单上每道菜都适合今天的你——哪怕你正在发烧,它照样给你端来一份冰镇生腌。
从算法角度看,雷达同时面对两件事:
- 利用:优先挑选已经确定与当前项目高度相关的内容;
- 探索:留一小部分陌生、但可能拓宽视野的内容,防止系统越走越窄。
这和多臂老虎机问题有相似之处。只做利用,系统会被自己的历史偏好锁死,你会越来越只看到自己已经同意的东西;只做探索,你每天都会收到一堆无关的惊喜。一个成熟的雷达不该把所有内容都往同一个“重点关注”里挤,而应该明确分层:
- 重点关注:与当前目标有明确联系,值得现在就投入注意力;
- 扩展关注:当前联系较弱,但可能带来认知增量;
- 普通新增:来自已知来源,保留可检索性,不主动占用注意力;
- 过滤:重复内容、纯版本通告、与当前上下文无关,或已被明确否定。
到这一步,相关性不再是文章身上的一个固定分数,而是一个带上下文的函数:
1 | 本期价值 |
这个式子不必真的用数字实现。它更重要的作用是让人知道“重点”到底由哪些理由组成,从而能够指出某次推荐究竟错在哪一项。
我说“这篇文章跟我有什么关系”,其实不是在质疑来源可信度,而是在指出“当前目标相关性”几乎为零;我说“这个我就更不感兴趣了”,说明的是纯工具版本更新的可行动性太低。如果模型只保存一个总分,它永远学不到这种差异——它只会知道我打了个低分,然后开始怀疑 Simon 这个人。只有当领域模型保存了反馈的对象、原因和范围,系统才可能真正改进。
这就是领域建模和推荐算法相遇的地方。算法负责计算,领域模型决定究竟在计算什么。
这些镜头怎样拼成一套体系
到这里可以回答那个问题了:有没有一套现成体系?
有,但它不是一本书里从第一章走到最后一章的单线流程,更像一组彼此衔接的镜头。
| 你要回答的问题 | 最适合的工具 |
|---|---|
| 一件事在现实中到底怎样发生? | Domain Storytelling |
| 关键变化按什么时间顺序发生? | EventStorming |
| 哪些词在什么范围内保持一致? | 通用语言与术语表 |
| 一个大系统应该怎样切开? | 限界上下文与 Context Map |
| 权威事实、写入动作和阅读投影怎样分离? | CQRS 思想、受控 Writer、Projection |
| 不同角色怎样看到合适尺度的结构? | C4 模型 |
| 为什么当初做出这个设计? | ADR |
| 怎样证明模型没有被代码破坏? | 示例、验收测试与不变量检查 |
DDD 更像这套体系的操作系统。它提供“模型、语言、边界”三项核心原则;Domain Storytelling 和 EventStorming 帮我们从真实活动里发现模型;C4 帮我们表达技术结构;ADR 保存选择背后的意图;测试和写入约束让语言不只停留在文档里。
这些工具不需要全部上场。
如果一个小工具只有两张表、一个使用者、几个简单 CRUD,那么清晰的命名加几条测试就够了。硬塞进聚合、事件总线和十层目录,只会制造仪式感。领域建模的复杂度应该由理解风险决定,不该由项目的技术时髦程度决定。
那什么时候值得认真建模?看四个信号:
- 同一个词在不同对话里反复出现,每次意思却略有不同;
- 一项局部修改经常意外影响别的功能;
- 你能让 AI 做出功能,却很难解释它为什么应该这样工作;
- 系统开始保存长期事实,错误写入的代价高于重新生成一个页面。
个人信息中枢几乎同时踩满这四条。它连接许多来源,横跨多个生活与工作领域,还要区分原始证据、推断、记忆和展示。它复杂不是因为用户多,而是因为“意义的种类”多。
一次六十分钟的轻量建模
如果 DDD 曾经让你觉得抽象,最好的解药不是重啃一遍厚书,而是拿一个刚刚发生的真实问题,做一次六十分钟练习。
我用的例子就是开篇那个:为什么雷达把我不感兴趣的文章放进了重点关注。
第一步:只选一个具体故事
不要讨论“推荐系统应该如何设计”这种大问题。写下一次真实经历:
雷达发现 Simon 的 sqlite-utils 4.0 更新,把它列成重点;我看到之后表示不感兴趣,希望系统以后别再把“关注作者”直接等同于“文章与我相关”。
好故事要包含具体对象、发生顺序和结果。它不必代表所有情况,只要足够真实。
第二步:用“谁做了什么”复述一遍
先把参与者列出来:外部作者、采集器、相关性评估器、简报生成器、用户、偏好记录器。
然后逐句追问:
- 谁发现了这篇文章?
- 谁保存原始证据?
- 谁决定它进入候选池?
- 谁判断它与当前目标相关?
- 谁决定它在本期简报里的位置?
- 用户的否定反馈写到了哪里?
- 下一次谁读取这条反馈?
如果某一句话的主语说不清,通常意味着责任边界有问题;如果一个主语包办了所有动作,通常意味着模块过度集中。
第三步:把关键事件放上时间线
只写已经发生的事实:
1 | 文章已发现 |
再在事件之间补上命令和策略。这一步先别急着讨论数据库字段,先确认因果链本身对不对。
第四步:圈出有争议的词
最值得定义的,通常不是大家都懂的词,而是大家以为彼此都懂的词。
在这个故事里,至少要定义“关注”“候选”“相关”“重点”“反馈”和“偏好”。每个定义最好各配一个正例和一个反例:
正例:关注 Simon 使系统更容易发现他的新文章;
反例:关注 Simon 不代表他的每篇文章都进入重点。
正例:用户说“这个 sqlite 工具升级与我无关”,形成一条针对内容类型的负反馈;
反例:这不等于用户永远不想看 Simon 关于 AI 编程的文章。
反例是领域语言的单元测试。没有反例的定义,通常只是听起来正确。
第五步:画边界和所有权
把词放回它们所属的上下文:
- 来源与抓取属于采集;
- 相关性和重点等级属于雷达评估;
- 明确的用户评价属于偏好反馈;
- 原文证据和长期主张属于证据与知识治理;
- 页面卡片属于展示投影。
然后给每一类事实标出唯一权威来源。凡是出现两个模块都能“顺手改一下”的地方,都要停下来设计受控写入。
第六步:写三到五条不变量
不变量不求多,只求能挡住真实错误:
- 关注作者不能单独构成重点推荐理由;
- 重点条目必须附带与当前目标相关的解释;
- 用户反馈必须记录作用对象和适用范围;
- 原始证据与模型推断必须在数据上可区分;
- 删除展示组件不得删除规范证据。
下一步,让 AI 把这些规则转成测试、schema 校验和写入权限。到这里,领域语言就从白板走进了系统。
第七步:留一页可以继续更新的地图
最终产物不需要是一份几十页的架构文档。对个人项目来说,一页往往就够:
1 | 目标:雷达只把“对当前我有价值”的内容放进重点关注。 |
这页地图不是一次性文档。每发现一个新歧义,就更新语言;每做一次会影响未来的取舍,就补一条 ADR;每当一种错误可以被明确表达,就补一个不变量测试。
AI 在模型里该扮演什么角色
AI 很适合参与领域建模,但不适合独自拥有领域语言。
它可以读大量代码和文档,找出同一个概念的不同叫法;可以把一段对话转成候选事件;可以为不变量生成测试;可以拿着一个真实故事不停追问遗漏;还可以在每次修改前说明这次会跨越哪些边界。
但模型最终表达的是人的意图和责任分配。代理没法仅凭代码判断,“个人信息中枢应该拥有规范事实,OpenClaw 只负责理解、对话与编排”到底是一项刻意设计,还是偶然长成的现状。缺少明确约束时,它会倾向于挑眼前最省事的路径:在对话系统里再存一份事实,直接改一个投影文件,或者为了让测试通过复制一套数据。
更可靠的协作方式,是让 AI 在四个阶段分别承担不同角色。
发现阶段:做采访者
给它一个真实故事,让它只负责追问:
- 下一步发生什么?
- 谁做出这个决定?
- 依据来自哪里?
- 哪一步失败代价最大?
- 同一个词是否在两个地方含义不同?
不要一上来就让它“设计架构”。过早索要方案,你得到的往往是它从训练语料里拼出来的一套标准答案,看起来很专业,跟你的系统没什么关系。
建模阶段:做反例生成器
给它候选定义,让它去找能把定义搞崩的情形。
比如“关注作者发布的内容都应进入重点关注”,反例马上就来了:工具版本公告、与当前工作无关的访谈、重复转载。反例会逼着模型把“来源发现”和“相关性判断”拆开。
实现阶段:做约束执行器
把边界、不变量和写入权限写进任务合同。要求代理在修改前指出受影响的上下文,在修改后用测试证明自己没有越权。这样它交付的就不只是代码,同时也在维护模型。
演化阶段:做同步助手
代码改完之后,让它检查术语、事件、决策记录和投影是否需要跟着更新。目的不是制造文档负担,而是避免人类的地图与机器的领土长期分叉。
AI 最有价值的用法,不是替我们取消思考,而是把隐含的思考变成可以检验的对象。
五种看起来很专业、其实容易失败的做法
领域建模常常失败,原因通常不是团队不努力,而是建模活动与真实决策脱了节。
一、从术语大全开始。 团队花几天定义“用户、内容、标签、来源”,却一条完整故事都没讲过。这样的词典很快会退化成同义词解释,因为没有动作和冲突,词的边界根本显不出来。改法:每个重要定义至少绑定一个真实故事和一个反例。
二、追求一套全球统一模型。 大家希望 Person、Article、Project 在全系统只有一个标准结构,结果模型不断吸收各上下文的需求,最后每个字段都可空,每个状态都得加注释。改法:允许同一现实对象在不同上下文拥有不同模型,只对跨界翻译达成明确合同。
三、把图画完就算完成。 图非常漂亮,却不影响代码、写入路径和测试。三个月后系统已经变了,图还停在第一次讨论的样子。改法:图只保存稳定边界和关键故事;规则进入不变量测试;重大取舍进入 ADR;容易重建的细节不必手工维护。
四、把所有模式一次装上。 刚学 DDD 就上聚合根、仓储、领域事件、命令总线、事件溯源和 CQRS。复杂度先于问题到达,团队开始为框架服务。改法:从语言和边界开始,只有当某种模式能消除一个已经观察到的风险时才引入它。
五、让 AI 根据代码反推唯一真相。 代码记录了系统现在怎样运行,不一定记录它本来应该怎样运行。有些结构是历史包袱,有些复制是临时迁移,有些绕路是上一轮错误留下的疤。改法:让 AI 把代码当作证据之一,而不是唯一权威;用用户意图、领域故事、规范事实和决策记录一起判断。
怎样知道自己真的理解了一个领域
理解不是能背出术语定义,而是面对变化时能做出稳定判断。
可以用六个问题自测:
- 故事:我能不能不用技术名词,把一件关键事情从头到尾讲清楚?
- 语言:关键名词是否都有正例、反例和适用范围?
- 边界:同一个词跨过边界时,我知不知道它的含义发生了什么变化?
- 权威:对每一项重要事实,我能否指出谁拥有它、谁可以改变它?
- 约束:我能否说出三件系统绝对不该允许的事情?
- 演化:新需求到来时,我能否判断它改变的是事实、规则、流程,还是仅仅一个展示投影?
答不出来,再多的类图也只是装饰。
反过来,如果它们都答得出来,实现未必需要多复杂。一个 Markdown 文件、一组 JSON Schema、几个受控写入脚本、一套验收测试,同样可以承载一个清晰的领域模型。模型的成熟度不取决于框架数量,取决于关键含义有没有被稳定地保存下来。
共同语言是一种基础设施
回看软件史,生产力工具每往前一步,人们都要重新发现一次协调问题。
高级语言让一个人能写更多代码,于是模块化变得重要;开源和分布式协作扩大了参与范围,于是版本控制和代码评审变得重要;云计算让部署变快,于是可观测性和基础设施即代码变得重要。今天,代理让“产生修改”变得极其便宜,我们就需要为“保持共同理解”建一套新的基础设施。
领域建模不是要阻止塔继续上升,也不是要求每块砖放上去之前先开一次大会。它要做的事情只有一件:让施工速度和理解同步速度重新对上。
对个人开发者来说,“共同”也不只意味着多人团队。今天的你、三个月后的你、同时工作的几个代理、后台自动化,还有将来接手这套系统的模型,都是不同的参与者。只要知识需要跨越时间和进程,共同语言就有价值。
所以领域建模的最小起点,既不是读完 DDD,也不是选一个框架。它是一句可以写在每个复杂需求前面的问题:
谁在什么情况下,依据什么事实,做出什么决定,并把结果写到哪里?
然后继续追问:
这个结果是谁的真相,谁的投影?哪个词可能在另一个边界里换了含义?什么事情无论如何都不应该发生?
只要你能把一个真实案例回答清楚,你就已经在建模了。
一开始,地图一定很粗。有些边界画错了,有些词还会再改,有些规则只能等下一次事故才浮出来。这不可怕。算法从来不是一次猜中答案,它是把问题组织成一个可以不断改进的过程。
真正危险的是另一件事:塔一直在上升,而我们再也没有一套语言,能说清楚自己正在建什么。
延伸阅读
- Armin Ronacher:The Tower Keeps Rising
- Martin Fowler:Bounded Context
- Domain Storytelling:Quick-Start Guide
- EventStorming:官方网站与入门资源
- C4 Model:Diagrams
- Microsoft Azure Architecture Center:CQRS Pattern
- Muhammad Ovais Ahmad:Comprehension Debt in GenAI-Assisted Software Engineering Projects
- Margaret-Anne Storey:From Technical Debt to Cognitive and Intent Debt