DSLs Enable Reliable Use of LLMsDSL 让 LLM 的可靠使用成为可能
LLMs generate code incredibly fast, but to ensure they generate exactly what is intended, they need clear boundaries. Abstractions and Domain-Specific Languages (DSLs) provide a strong harness that guides LLMs right from the start. The example of Tickloom - a domain model and DSL for illustrating distributed system behavior - shows how we can use an LLM as a partner to iteratively build a DSL and as a natural language interface to use it. Such a DSL can act as the key source of truth for software systems in the world of LLMs.LLM 生成代码的速度极快,但要确保生成的代码恰好符合预期,就需要清晰的边界。抽象和领域特定语言(DSL)提供了一种强有力的约束,从一开始就引导 LLM。Tickloom 的例子——一个用于说明分布式系统行为的领域模型和 DSL——展示了我们如何将 LLM 作为伙伴,以迭代方式构建 DSL,并将其作为自然语言接口来使用。在 LLM 的世界里,这样的 DSL 可以成为软件系统的关键真相来源。
14 July 20262026年7月14日
Contents目录
- The Limits of Upfront Specification预先规范的限制
- Design Is Discovered Through Implementation设计通过实现被发现
- Domain Abstractions and DSLs领域抽象与 DSL
- Why DSLs work so well with LLMs为什么 DSL 与 LLM 配合得这么好
- Example: Using LLMs to generate diagram rich powerpoint presentations示例:使用 LLM 生成包含丰富图表的 PowerPoint 演示文稿
- Building the Semantic Model构建语义模型
- Example: Tickloom — a semantic model for distributed systems示例:Tickloom——分布式系统的语义模型
- Even good abstractions help — without a DSL即使是好的抽象也有帮助——无需 DSL
- Example: Building a DSL for testing distributed system scenarios示例:为测试分布式系统场景构建 DSL
- Two phases working with LLMs与 LLM 协作的两个阶段
- The DSL as the Source of TruthDSL 作为真相来源
Modern LLMs possess an incredible capability. They can generate large amounts of code, and sometimes entire systems, from just a high-level natural language description. An important assumption here is that the 'intent' of what needs to be built is well articulated, using precise words that LLMs can map to coding building blocks. However, there are two important points worth noting: the limits of upfront specification, and how design is discovered through implementation. 现代 LLM 拥有令人难以置信的能力。它们可以根据一个高层级的自然语言描述,生成大量代码,有时甚至是整个系统。这里有一个重要的假设:需要构建的“意图”被很好地阐明了,使用了精确的词语,LLM 可以将这些词语映射到编码构建块。然而,有两点值得注意:预先规范的限制,以及设计是如何通过实现被发现的。
The Limits of Upfront Specification
Building large systems involves a great many small design decisions, and these cannot all be known in advance or driven entirely from a high-level spec. A specification is at best a starting hypothesis: the real constraints, trade-offs, and edge cases are discovered iteratively, as we proceed with the implementation. We discussed this at length in an earlier article, where we called it Upfront Specification Impossibility. The point is not that specs are worthless, but that the first one is a hypothesis to be revised, never a finished blueprint. 构建大型系统涉及大量小的设计决策,这些决策不可能全部预先知道,也不能完全由高层级的规范驱动。规范充其量只是一个初始假设:真正的约束、权衡和边界情况是在实现过程中迭代发现的。我们在之前的一篇文章中详细讨论过这一点,称之为“预先规范不可能性”。关键不在于规范没有价值,而在于第一个规范是一个需要修正的假设,而不是一个完成的蓝图。
The natural response is to iterate: refine the spec, generate code, review what comes back, and feed what we learn into the next round. That loop works well when each round produces a small, reviewable change.自然的回应是迭代:细化规范,生成代码,审查返回的结果,并将学到的知识反馈到下一轮。当每一轮产生一个小的、可审查的变更时,这个循环运转良好。
Design Is Discovered Through Implementation
Reviewing code, particularly while we are still discovering the design, is not the same as writing it. While reviewing the generated code, we review through the chunks validating if it maps to our intent and looking for possible pitfalls. But reviewing rarely forces us to wrestle with the design decisions. Writing code, by contrast, forces us to think through concrete decisions—such as where a responsibility belongs or what boundaries should be exposed so the design can be extended further. It is in making those decisions that a design most fully reveals itself. 审查代码——尤其是在我们仍在发现设计的过程中——与编写代码是不同的。在审查生成的代码时,我们审查各个块,验证它是否映射到我们的意图,并寻找可能的陷阱。但审查很少迫使我们与设计决策抗争。相比之下,编写代码迫使我们思考具体的决策——比如一个职责应该属于哪里,或者应该暴露哪些边界以便设计可以进一步扩展。正是在做出这些决策的过程中,一个设计最充分地展现了自己。
The programming language and paradigm we code in shapes the design insight we get. A functional design approach or an object-oriented design approach reveals different aspects of the design, along with idioms and patterns that are natural to the paradigm. 我们编码所使用的编程语言和范式塑造了我们获得的设计洞察。函数式设计方法或面向对象设计方法揭示了设计的不同方面,以及该范式自然的惯用模式和模式。
So where do LLMs fit in? I see LLMs playing two roles. They are a great help while we shape the design and its vocabulary, acting as brainstorming partners to help us explore the design space and discover the right abstractions. Once the vocabulary is established, LLMs work as an excellent natural language interface to it. 那么 LLM 的角色是什么呢?我认为 LLM 扮演两个角色。在我们塑造设计及其词汇时,它们作为头脑风暴伙伴,帮助我们探索设计空间并发现正确的抽象,是极大的帮助。一旦词汇建立起来,LLM 就成为一个优秀的自然语言接口。
Domain Abstractions and DSLs
A useful way to frame this is through Domain Driven Design. Its core insight is building a shared conceptual model of the domain in code, and then using that model - which DDD calls a Ubiquitous Language - both to evolve the codebase and to give the team a vocabulary to think and communicate in. Often, it is highly effective to build a domain specific language on top of that model: a constrained syntax for expressing the domain's concepts and operations. Seen this way, most development is the process of building a domain model and using it to evolve the system. The LLM plays two distinct roles depending on whether the domain model already exists. In this article, I will focus on how Domain-Specific Languages (DSLs) work with LLMs. 一个有用的框架是领域驱动设计。其核心洞察是在代码中构建一个共享的领域概念模型,然后使用该模型——DDD 称之为通用语言——既用于演化代码库,也赋予团队思考和沟通的词汇。通常,在该模型之上构建一个领域特定语言是非常有效的:一种用于表达领域概念和操作的受限语法。从这个角度看,大部分开发都是构建领域模型并使用它来演化系统的过程。根据领域模型是否已经存在,LLM 扮演两个不同的角色。在本文中,我将重点讨论领域特定语言(DSL)如何与 LLM 协作。
Why DSLs work so well with LLMs
It is a common experience that DSLs work well with LLMs. PlantUML, Mermaid, and Graphviz are domain specific languages for visual modeling; SQL is a DSL for querying databases; Kubernetes YAML is a DSL for describing cloud infrastructure. These are not general-purpose programming languages — they are deliberately constrained, designed to express a narrow set of concepts in one domain. And it is no surprise that LLMs are remarkably good at generating Mermaid diagrams, SQL queries, or Kubernetes manifests from a plain English description. 一个常见的经验是 DSL 与 LLM 配合得很好。PlantUML、Mermaid 和 Graphviz 是用于可视建模的领域特定语言;SQL 是用于查询数据库的 DSL;Kubernetes YAML 是用于描述云基础设施的 DSL。这些不是通用编程语言——它们被有意地约束,设计用于表达一个领域中狭窄的一组概念。毫不奇怪,LLM 非常擅长根据简单的英语描述生成 Mermaid 图表、SQL 查询或 Kubernetes 清单。
My observation is that DSLs make LLMs more reliable because they respond so well to a few in-context examples. A general-purpose language like Java offers lots of valid ways to express the same intent. A DSL strips the variation away. Giving the model a few examples is enough to reliably generate the correct syntax. It's worth noting that frontline models are already heavily exposed to PlantUML or Java fluent interfaces during training, so they aren't starting from scratch. It will be curious to see how smaller, more constrained models perform when tasked with a truly novel DSL. 我的观察是,DSL 使 LLM 更可靠,因为它们对几个上下文示例的反应非常好。像 Java 这样的通用语言提供了多种有效的方式来表达相同的意图。DSL 消除了这种变异性。给模型几个示例就足以可靠地生成正确的语法。值得注意的是,前沿模型在训练期间已经大量接触了 PlantUML 或 Java 流畅接口,因此它们不是从零开始。当面对一个真正新颖的 DSL 时,更小、更受限的模型表现如何,将是一件有趣的事情。
For an agent — an LLM running in an autonomous generate-and-check loop rather than a single shot generation — there is one more benefit. A DSL almost always ships with a deterministic validator: a parser, a JSON schema, a type checker, or a compiler. The agent can generate a candidate, run it past the validator, and repair it from the error, all without a human in the loop. Crucially, the errors are phrased at the level of the domain — “you cannot select an action before choosing a client” — rather than as a stack trace buried deep in generated code. DSL's toolset itself acts as an excellent harness. We will see it concretely in the Tickloom examples below, where the DSL's grammar is enforced by the host language's compiler and the resulting runs are checked automatically. 对于 agent——一个在自主生成-检查循环中运行而不是单次生成的 LLM——还有一个好处。一个 DSL 几乎总是附带一个确定性验证器:解析器、JSON 模式、类型检查器或编译器。Agent 可以生成一个候选,将其传递给验证器,并根据错误进行修复,整个过程无需人工介入。关键是,错误的表述是在领域层面——“你不能在选择客户端之前选择操作”——而不是作为埋在生成代码深处的堆栈跟踪。DSL 的工具集本身就是一个优秀的约束框架。我们将在下面的 Tickloom 示例中具体看到这一点,其中 DSL 的语法由宿主语言的编译器强制执行,并且生成的运行结果会被自动检查。
It is important to note that this is not a one-size-fits-all solution. The advantage holds while the DSL stays small and constrained enough that a few in-context examples can convey its usage. There is also a real upfront cost in designing and maintaining the language and its semantic model. The payoff is therefore concentrated in well-factored, genuinely constrained DSLs backed by a validator. 需要注意的是,这并不是一个放之四海而皆准的解决方案。当 DSL 保持足够小且受限,以至于几个上下文示例就能传达其用法时,优势才成立。设计和维护语言及其语义模型也有真正的初始成本。因此,回报集中在那些经过良好分解、真正受限且由验证器支持的 DSL 上。
Example: Using LLMs to generate diagram rich powerpoint presentations
LLMs make it really easy to build custom tools. While teaching distributed systems, I frequently need to create presentations which mostly have diagrams explaining distributed operations in a cluster. UML Sequence diagrams have been great for that but showing a full sequence diagram while explaining the flow of messages through the cluster is not very useful. I needed a tool to show a sequence diagram step by step in a powerpoint presentation. With the help of LLMs I was able to build a tool which processes a YAML describing the presentation structure with references to the PlantUML diagrams and generates a powerpoint presentation. The PlantUML diagrams are marked with steps, and the tool generates a separate slide for each step. This made it really easy to create diagram-rich presentations.LLM 使得构建自定义工具变得非常容易。在教授分布式系统时,我经常需要创建演示文稿,其中大部分是解释集群中分布式操作的图表。UML 序列图对此很好,但在解释消息流经集群时显示完整的序列图并不是很有用。我需要一个工具,可以在 PowerPoint 演示文稿中逐步显示序列图。借助 LLM,我能够构建一个工具,它处理描述演示文稿结构(包含 PlantUML 图表的引用)的 YAML,并生成 PowerPoint 演示文稿。PlantUML 图表带有步骤标记,该工具为每个步骤生成单独的幻灯片。这使得创建包含丰富图表的演示文稿变得非常容易。
This prompt generates the following PlantUML code with step markers:这个提示生成了以下带有步骤标记的 PlantUML 代码:
@startuml
actor Alice
box "Cluster" #lightblue
participant athens
participant byzantium
participant cyrene
end box
'[step]
Alice -> athens: "title", "After Dawn"
'[step]
athens -> athens: save()
note right of athens
state:
title: After Dawn
end note
'[step]
athens -[#red]x byzantium: "title", "After Dawn"
'[step]
athens -> cyrene: "title", "After Dawn"
note right of cyrene
state:
title: After Dawn
end note
'[step]
athens -> athens: isQuorumReached()
'[step]
athens --> Alice: Success
@enduml
I used it to create a series of slides in a powerpoint presentation. For doing that, I developed a small YAML specification to describe the presentation structure and the diagrams to be used in each slide. This allowed me to use LLMs to create presentations describing complex distributed systems concepts, without having to manually create animations on the slides. An example prompt to generate a slide YAML spec is as simple as following. 我使用它来创建 PowerPoint 演示文稿中的一系列幻灯片。为此,我开发了一个小的 YAML 规范来描述演示文稿结构以及每张幻灯片中使用的图表。这使我能够使用 LLM 创建描述复杂分布式系统概念的演示文稿,而无需手动创建幻灯片上的动画。一个用于生成幻灯片 YAML 规范的示例提示简单如下。
This generates a slide spec YAML like following:这将生成如下幻灯片规范 YAML:
- slide:
title: "Quorum Write Example"
diagram: "quorum-write"
It's important to note that even if the prompt is saying create a slide YAML, it's not any random YAML spec. Because the tool to generate the powerpoint presentation and the YAML specification understood by the tool is used as a context in the prompt, the LLM is able to generate the correct YAML spec which can be directly used by the tool to generate the powerpoint presentation. 需要注意的是,即使提示说的是创建幻灯片 YAML,它也不是任何随机的 YAML 规范。因为用于生成 PowerPoint 演示文稿的工具以及工具理解的 YAML 规范被用作提示中的上下文,LLM 能够生成正确的 YAML 规范,该规范可以直接被工具用来生成 PowerPoint 演示文稿。
The full YAML spec can be viewed at this Github repo完整的 YAML 规范可以在 Github 仓库中查看
Notice that the LLM played two different parts in this single example. First it was a co-designer — helping shape the step-marked PlantUML extension and the slide YAML on top of existing PlantUML tooling. Then, once that small DSL existed, it became the natural-language interface that turns an English request into a valid spec. We will come back to this division of labour at the end of the article. 注意,在这个单一的例子中,LLM 扮演了两个不同的角色。首先它是共同设计者——帮助塑造带步骤标记的 PlantUML 扩展以及基于现有 PlantUML 工具的幻灯片 YAML。然后,一旦那个小的 DSL 存在了,它就变成了自然语言接口,将英语请求转化为有效的规范。我们将在文章末尾回到这种分工。
Building the Semantic Model
The example we covered in the previous section was straightforward. The YAML was used as a carrier syntax, and I process its parsed syntax tree directly, effectively using the syntax tree itself as my Semantic Model (though this couples the syntax to the execution semantics). But in more complex domains, like distributed systems, we need more complex semantic models to represent the concepts in the domain and the design decisions we have made in the codebase. Let's look at an example based on a small framework I built to quickly build and test distributed systems. 上一节中的示例很简单。YAML 被用作载体语法,我直接处理其解析后的语法树,实际上将语法树本身作为我的语义模型(尽管这将语法与执行语义耦合在一起)。但在更复杂的领域,比如分布式系统,我们需要更复杂的语义模型来表示领域中的概念以及我们在代码库中做出的设计决策。让我们看一个基于我构建的一个用于快速构建和测试分布式系统的小框架的例子。
Example: Tickloom — a semantic model for distributed systems
Implementing distributed systems such as quorum-based key-value stores or consensus protocols like Raft and Paxos is a daunting task. Even if implementation is guided incrementally through prompts, specifications, or carefully constructed .md skill files, the asynchronous runtimes still expose an overwhelming space of possible implementation decisions. Threading models, networking patterns, storage coordination, retry behavior, and timing semantics all remain entangled within the generated code. The problem is not merely code generation complexity, but verification complexity. The resulting state space created by all possible interleavings across thread scheduling, network delays, process pauses, and clock skew becomes so large that systematically reviewing and validating correctness across all interacting behaviors is nearly impossible. This is the reason we see that Jepsen tests find bugs even in the most battle-tested distributed systems. 实现分布式系统,例如基于仲裁的键值存储或像 Raft 和 Paxos 这样的共识协议,是一项艰巨的任务。即使实现是通过提示、规范或精心构建的 .md 技能文件增量指导的,异步运行时仍然暴露出一个压倒性的实现决策空间。线程模型、网络模式、存储协调、重试行为和时序语义都仍然纠缠在生成的代码中。问题不仅仅是代码生成的复杂性,而是验证的复杂性。由线程调度、网络延迟、进程暂停和时钟偏移的所有可能交错产生的状态空间变得如此之大,以至于系统地审查和验证所有交互行为的正确性几乎是不可能的。这就是为什么 Jepsen 测试即使在经过最严格考验的分布式系统中也能发现错误的原因。
This is exactly where a semantic model is beneficial. Tickloom is a small framework I built
to construct and test distributed algorithms. Its abstractions are not a generic runtime;
they are a set of design decisions about how a distributed process behaves. Every
node runs in a single-threaded tick loop: each call to tick() advances a
logical clock by one and processes pending work in a fixed, deterministic order (network,
then message bus, then process, then storage). Time is measured in ticks, not milliseconds.
Messages are plain Java records. Coordination across replicas is expressed through a
Replica base class that already knows about peers, broadcasts, and quorums. 这正是语义模型有益的地方。Tickloom 是我构建的一个用于构造和测试分布式算法的小框架。它的抽象不是一个通用的运行时;它们是一组关于分布式进程行为的设计决策。每个节点运行在一个单线程的 tick 循环中:每次调用 tick() 将逻辑时钟向前推进一个单位,并以固定的确定性顺序处理待处理的工作(先网络,然后消息总线,然后进程,然后存储)。时间以 tick 为单位,而不是毫秒。消息是普通的 Java 记录。跨副本的协调通过一个 Replica 基类来表达,该类已经知道对等节点、广播和仲裁。
Threading, timing, network delivery are no longer open questions to be re-decided in every prompt. What remains for the algorithm author is the actual protocol logic. A quorum replica, for instance, is just a set of message handlers expressed in the framework's vocabulary.线程、时序、网络传递不再是需要在每个提示中重新决定的开放问题。留给算法作者的是实际的协议逻辑。例如,一个仲裁副本只是一组用框架词汇表达的消息处理器。
Because the framework supplies the vocabulary — Replica, quorumRequest
,
countResponseIf, MessageType, Handler — a
prompt can stay at the level of the protocol rather than the plumbing: 因为框架提供了词汇——Replica、quorumRequest、countResponseIf、MessageType、Handler——一个提示可以停留在协议层面而不是管道层面:
This high level description produces code like following: 这个高层级的描述产生如下代码:
@Override
protected Map<MessageType, Handler> initialiseHandlers() {
return Map.of(
LWWMessageType.CLIENT_SET_REQUEST, this::handleClientSetRequest,
LWWMessageType.CLIENT_GET_REQUEST, this::handleClientGetRequest,
LWWMessageType.INTERNAL_SET_REQUEST, this::handleInternalSetRequest,
LWWMessageType.INTERNAL_GET_REQUEST, this::handleInternalGetRequest,
LWWMessageType.INTERNAL_SET_RESPONSE, this::handleInternalSetResponse,
LWWMessageType.INTERNAL_GET_RESPONSE, this::handleInternalGetResponse
);
}
private void handleClientGetRequest(Message message) {
var req = deserializePayload(message.payload(), ClientGetRequest.class);
var internalReq = new InternalGetRequest(req.key());
this.<InternalGetResponse>quorumRequest(LWWMessageType.INTERNAL_GET_REQUEST, internalReq)
.countResponseIf(r -> true) // any response is fine, just need a majority
.send()
.whenComplete((responses, error) -> {
if (error != null) {
send(createMessage(message.source(),
message.correlationId(),
new ClientGetResponse(req.key(), null, false),
LWWMessageType.CLIENT_GET_RESPONSE));
return;
}
byte[] highestValue = null;
long highestTimestamp = -1;
for (InternalGetResponse r : responses.values()) {
if (r.value() != null && r.timestamp() > highestTimestamp) {
highestTimestamp = r.timestamp();
highestValue = r.value();
}
}
boolean found = highestValue != null;
send(createMessage(message.source(),
message.correlationId(),
new ClientGetResponse(req.key(), highestValue, found),
LWWMessageType.CLIENT_GET_RESPONSE));
});
}
source来源
The semantic model itself acts as a context. The prompt names concepts that exist as concrete types in the codebase, so the LLM is not inventing a threading model or a networking layer — it is filling in protocol logic against a fixed, well-understood substrate. 语义模型本身作为上下文。提示命名了在代码库中作为具体类型存在的概念,因此 LLM 不是在发明线程模型或网络层——它是在固定的、被充分理解的基底上填充协议逻辑。
Even good abstractions help — without a DSL
A DSL is one end of the spectrum, and it is not easy to
build. Before reaching for a language of your own it is worth noticing that a clean set of
abstractions is already a lighter version of the same idea — and, just as the framework
supplied the vocabulary for the quorum store above, a library's named types and methods are
themselves a vocabulary the model can be grounded in. Tickloom's semantic model is really
just four such seams — Process/Replica for compute and message
handling, Network for communication, Storage for persistence, and
a logical Clock for time — and that decomposition does most of the work with no
new syntax at all. DSL 是频谱的一端,构建起来并不容易。在着手构建自己的语言之前,值得注意,一组清晰的抽象已经是同一个想法的轻量级版本——就像框架为上述仲裁存储提供词汇一样,库的命名类型和方法本身就是模型可以立足的词汇。Tickloom 的语义模型实际上只有四个这样的接缝:Process/Replica 用于计算和消息处理,Network 用于通信,Storage 用于持久化,以及一个逻辑 Clock 用于时间——这种分解在没有新语法的情况下完成了大部分工作。
This is why abstractions, not just DSLs, pair well with LLMs.
A prompt to “implement Raft as a Tickloom Replica” has limited state space to explore.
The existing QuorumReplica can be used in context as a worked example. 这就是为什么抽象(而不仅仅是 DSL)与 LLM 配合得很好的原因。一个像“将 Raft 实现为 Tickloom Replica”这样的提示只有有限的状态空间需要探索。现有的 QuorumReplica 可以作为工作示例在上下文中使用。
Example: Building a DSL for testing distributed system scenarios
Implementing the algorithm is one thing; exercising it is another.
The subtle bugs in distributed systems live in specific orderings: a write that
replicates to one node before a reader's quorum shifts, a partition that heals at just the
wrong moment, two coordinators whose clocks have drifted apart. Writing such a scenario
directly against the testkit involves juggling futures, and
manual tick() loops. Here is a clock-skew scenario written that way: 实现算法是一回事;运行它是另一回事。分布式系统中的微妙错误存在于特定的顺序中:一个写入在读者仲裁转移之前复制到一个节点,一个分区在错误的时间愈合,两个协调器的时钟发生了偏移。直接针对测试工具编写这样的场景涉及处理 futures 和手动 tick() 循环。以下是一个用这种方式编写的时钟偏移场景:
Cluster cluster = new Cluster()
.withProcessIds(Arrays.asList(ATHENS, BYZANTIUM, CYRENE))
.useSimulatedNetwork()
.build(QuorumReplica::new);
cluster.start();
try {
cluster.tickUntil(cluster::areAllNodesInitialized);
cluster.setTimeForProcess(ATHENS, 1000L);
cluster.setTimeForProcess(BYZANTIUM, 2000L);
QuorumReplicaClient alice = cluster.newClientConnectedTo(ALICE, ATHENS, QuorumReplicaClient::new);
QuorumReplicaClient bob = cluster.newClientConnectedTo(BOB, BYZANTIUM, QuorumReplicaClient::new);
QuorumReplicaClient reader = cluster.newClientConnectedTo(READER, ATHENS, QuorumReplicaClient::new);
TickCompletableFuture<SetResponse> bobWrite = bob.set(KEY.getBytes(StandardCharsets.UTF_8), "B".getBytes(StandardCharsets.UTF_8));
cluster.tickUntilComplete(bobWrite);
TickCompletableFuture<SetResponse> aliceWrite = alice.set(KEY.getBytes(StandardCharsets.UTF_8), "A".getBytes(StandardCharsets.UTF_8));
cluster.tickUntilComplete(aliceWrite);
TickCompletableFuture<GetResponse> read = reader.get(KEY.getBytes(StandardCharsets.UTF_8));
cluster.tickUntilComplete(read);
assertEquals("B", new String(read.getResult().value(), StandardCharsets.UTF_8));
} finally {
cluster.close();
}
The intent — “Bob writes through Byzantium, Alice writes through Athens, a reader sees Bob's value because Byzantium's clock was ahead” — is buried under mechanics. This is also a hard to verify code: there are dozens of incidental decisions (when to tick, how to encode bytes, which factory overload to call) for an LLM to get subtly wrong, and a reviewer must check every one of them. 意图——“Bob 通过 Byzantium 写入,Alice 通过 Athens 写入,一个读者看到 Bob 的值因为 Byzantium 的时钟更快”——被机械细节掩盖了。这也是一个难以验证的代码:有几十个偶然的决策(何时 tick,如何编码字节,调用哪个工厂重载)可能让 LLM 产生微妙的错误,而审查者必须检查每一个。
So on top of the semantic model I built an internal DSL whose vocabulary is the vocabulary of the scenario itself — servers, clients, who is connected to whom, what each client does, and which faults are in effect while it does it. The same scenario becomes: 所以在语义模型之上,我构建了一个内部 DSL,其词汇就是场景本身的词汇——服务器、客户端、谁与谁连接、每个客户端做什么,以及在处理过程中哪些故障生效。同样的场景变成:
Scenario<QuorumReplicaClient> scenario =
QuorumStepBuilder.scenario("LWW lost update via server clock skew")
.servers(ATHENS, BYZANTIUM, CYRENE)
.clients(ALICE, BOB, READER)
.client(ALICE).connectedTo(ATHENS)
.client(BOB).connectedTo(BYZANTIUM)
.given(g -> g.serverTimeAt(ATHENS, 1_000L)
.serverTimeAt(BYZANTIUM, 2_000L))
.steps(s -> {
s.client(BOB).writes(KEY, "B").expectSuccess();
s.client(ALICE).writes(KEY, "A").expectSuccess();
s.client(READER).reads(KEY)
.expectResponse(v -> "B".equals(v));
});
The DSL is a thin, declarative surface that compiles down to a pure intermediate
representation — a Scenario made of Steps, where each step carries
an Action (a read or write) and optional ClusterEvents (faults
like partitions and message delays). Faults read as English too:
partition(BYZANTIUM).from(CYRENE), reconnect(BYZANTIUM), delay(INTERNAL_SET_REQUEST).from(ATHENS).to(BYZANTIUM,
CYRENE).byTicks(100). The grammar is enforced by the type system through progressive
interfaces — you cannot declare a step before the topology, or an action before selecting a
client — so whole classes of malformed scenarios simply do not compile.
Because the DSL is an internal DSL built in Java, the host compiler validates the grammar for free, and a malformed
generation comes back as a compile error pinned to exactly the illegal step rather than a
runtime surprise.这个 DSL 是一个薄的声明式表面,编译成一个纯粹的中间表示——一个由 Step 组成的 Scenario,其中每个 Step 携带一个 Action(读取或写入)和可选的 ClusterEvents(像分区和消息延迟这样的故障)。故障读起来也像英语:partition(BYZANTIUM).from(CYRENE),reconnect(BYZANTIUM),delay(INTERNAL_SET_REQUEST).from(ATHENS).to(BYZANTIUM, CYRENE).byTicks(100)。语法通过渐进式接口由类型系统强制执行——你不能在拓扑之前声明一个步骤,也不能在选择客户端之前声明一个动作——因此整个类别的畸形场景根本不会编译。因为该 DSL 是用 Java 构建的内部 DSL,宿主编译器免费验证语法,一个格式错误的生成会以编译错误的形式返回,精确地定位到非法的步骤,而不是运行时意外。
Once the DSL exists, a natural language description of a failure scenario maps almost directly onto it. A prompt like: 一旦 DSL 存在,一个失败场景的自然语言描述几乎直接映射到它上面。一个像这样的提示:
yields a scenario that stays entirely within the DSL's constrained vocabulary: 产生一个完全停留在 DSL 约束词汇内的场景:
Scenario<QuorumReplicaClient> scenario =
QuorumStepBuilder.scenario("Non-linearizable quorum read")
.servers(ATHENS, BYZANTIUM, CYRENE)
.clients(WRITER, ALICE, BOB)
.client(WRITER).connectedTo(ATHENS)
.client(ALICE).connectedTo(BYZANTIUM)
.client(BOB).connectedTo(BYZANTIUM)
.steps(s -> {
// Writer sets the key to VOLD initially and it replicates fully
s.client(WRITER).writes(KEY, VOLD).expectSuccess();
// Writer updates to VNEW, but replication from Athens is delayed
s.client(WRITER).writes(KEY, VNEW)
.whileClusterEvent(delay(QuorumMessageTypes.INTERNAL_SET_REQUEST)
.from(ATHENS).to(BYZANTIUM, CYRENE).byTicks(100))
.expectSuccess();
// Alice reads through Byzantium. Force quorum to include Athens by partitioning Cyrene.
// She will read VNEW from Athens.
s.client(ALICE).reads(KEY)
.whileClusterEvent(partition(BYZANTIUM).from(CYRENE))
.expectResponse(v -> VNEW.equals(v));
// Bob reads later through Byzantium. Force quorum to include Cyrene (and exclude Athens).
// The delayed VNEW replication hasn't arrived at Cyrene, so Bob reads VOLD.
s.client(BOB).reads(KEY)
.whileClusterEvent(reconnect(BYZANTIUM))
.whileClusterEvent(partition(BYZANTIUM).from(ATHENS))
.expectResponse(v -> VOLD.equals(v));
});
ScenarioResult result = scenario.run();
Because the surface is so small — and the space of valid code it can generate is so much smaller than the space of valid Java programs — the LLM has very little room to hallucinate, and a reviewer can read the result as a description of an experiment rather than as code to be audited line by line. Even if LLM hallucinates, the internal DSL will fail to compile allowing LLM to correct the errors made. 因为表面如此之小——它可以生成的有效代码空间比有效 Java 程序的空间要小得多——LLM 几乎没有空间产生幻觉,审查者可以将结果作为实验描述来阅读,而不是需要逐行审计的代码。即使 LLM 产生幻觉,内部 DSL 将无法编译,允许 LLM 纠正所犯的错误。
Two phases working with LLMs
A pattern emerges from the examples discussed above: in every one of them the LLM was useful in two quite different ways. 从上面讨论的例子中浮现出一个模式:在每一个例子中,LLM 都以两种截然不同的方式发挥作用。
The first phase is designing the abstraction or DSL itself. Here the LLM is best treated as a brainstorming partner rather than a code generator. As argued at the start of this article, the design decisions that make up a semantic model cannot all be specified upfront — we discover the constraints, trade-offs, and edge cases as we implement them. So this phase is inherently iterative and feedback-driven: you propose a structure, try it against a real case, see where it turns out awkward, and feed what you learned back into the next round. The LLM speeds that loop up — it sketches alternatives, critiques a design, ports an idea from one language to another — but you stay firmly in the driver's seat, because these are exactly the decisions you need to understand and own. The structures that make a DSL pleasant to use, like progressive interfaces that make an illegal scenario fail to compile or a semantic model kept separate from the builder that produces it, are the kind of thing you converge on by iterating, not by writing a specification and generating code. 第一个阶段是设计抽象或 DSL 本身。这里,LLM 最好被当作头脑风暴伙伴而不是代码生成器。正如本文开头所论证的,构成语义模型的设计决策不能全部预先指定——我们在实现过程中发现约束、权衡和边界情况。因此,这个阶段本质上是迭代和反馈驱动的:你提出一个结构,在真实案例中尝试,发现哪里变得笨拙,然后将学到的知识反馈到下一轮。LLM 加速了这个循环——它勾勒替代方案,批评设计,将想法从一种语言移植到另一种——但你牢牢地坐在驾驶座上,因为这些正是你需要理解并拥有的决策。使 DSL 使用起来愉悦的结构,比如使非法场景无法编译的渐进式接口,或者与生成它的构建器分离的语义模型,都是通过迭代趋近的,而不是通过编写规范和生成代码。
The second phase begins once the abstraction or DSL is in place. Now what the LLM does changes: it becomes a natural-language interface to what you have built. The prompts in this article are examples — “implement a quorum store as a Tickloom Replica”, “write a scenario reproducing the DDIA §10.6 read”, “create a slide YAML for this diagram”. In each case the English description maps almost directly onto the vocabulary you defined, and the LLM is a dependable generator precisely because the abstraction supplies both the context that grounds the prompt and the harness that checks the result. 第二个阶段从抽象或 DSL 就位时开始。现在 LLM 的作用发生了变化:它成为你所构建的东西的自然语言接口。本文中的提示就是例子——“将仲裁存储实现为 Tickloom Replica”,“编写一个复现 DDIA §10.6 读取的场景”,“为这个图表创建一个幻灯片 YAML”。在每种情况下,英语描述几乎直接映射到你定义的词汇,而 LLM 是一个可靠的生成器,正是因为抽象提供了约束提示的上下文和检查结果的约束框架。
The DSL as the Source of Truth
There is a growing trend of treating prompts as the primary source of truth. A well-designed DSL fundamentally changes this dynamic. One of the key advantages I observe of working with DSLs is that the generated program itself often becomes the artifact that humans maintain. Because a DSL is dense, expressive, and largely free of incidental boilerplate, it captures the essential intent of the solution in a form that remains readable long after generation. If an LLM generates a Tickloom failure scenario from a natural language request, the resulting scenario is already expressed in the vocabulary of the domain. If the scenario needs to change next month, there is no need to recover the original prompt and regenerate everything. The DSL has enough context for LLM to know the intent and work with it. The enduring asset is not the prompt, but the DSL and the semantic model. 有一种日益增长的趋势,将提示视为主要真相来源。一个精心设计的 DSL 从根本上改变了这种动态。我观察到使用 DSL 的一个关键优势是,生成的程序本身往往成为人类维护的产物。因为 DSL 紧凑、表达力强,并且基本没有偶然的样板代码,它以生成后仍然可读的形式捕获了解决方案的基本意图。如果 LLM 从自然语言请求生成了一个 Tickloom 失败场景,生成的场景已经用领域的词汇表达。如果下个月场景需要修改,无需恢复原始提示并重新生成所有内容。DSL 拥有足够的上下文让 LLM 知道意图并与之协作。持久的资产不是提示,而是 DSL 和语义模型。
Acknowledgments致谢
I would like to thank Martin Fowler and Rebecca Parsons for their valuable feedback and suggestions. 感谢 Martin Fowler 和 Rebecca Parsons 提供的宝贵反馈和建议。
Significant Revisions重要修订
14 July 2026: published2026年7月14日:发布

