让大模型生成领域专用语言(DSL)并不难,难的是确保结果真的可执行。模型可能写出不存在的属性、拼错资源类型,或者生成语法正确但违反业务约束的配置。Typed Domain Grounding 提供了一条务实路径:不要让模型直接在缺少约束的文本空间里自由发挥,而是把 DSL 嵌入 TypeScript、Kotlin 等主流类型化语言,让编译器成为生成流程中的验证器。
文章通过 kUML 基准和基础设施即代码示例讨论了这种方法,并强调 generate-compile-repair,也就是“生成—编译—修复”循环。关键变化不是期待模型永远一次写对,而是建立一个成本低、反馈明确、可以重复执行的纠错闭环。
文本 DSL 为什么容易诱发幻觉
传统 DSL 往往依赖文档、示例和运行时错误来约束作者。人类开发者可以搜索文档、理解上下文,模型却很容易把相似领域中的概念混在一起。例如,一个基础设施配置生成器可能产生以下问题:
- 使用
eu-central-9之类不存在的区域; - 把
public写成publick,解析器直到部署时才报错; - 为生产环境选择被策略禁止的实例规格;
- 引用已经废弃或根本不存在的资源属性;
- 输出格式合法,但资源之间的依赖关系不成立。
如果 DSL 只是字符串,很多错误只能交给自定义解析器或真实后端发现。此时反馈通常较晚,而且错误信息未必适合模型理解。
Typed Domain Grounding 的思路是把可用概念编码成类型:合法区域使用联合类型,资源结构使用接口,互斥配置使用可辨识联合,构造过程通过普通函数完成。模型仍然生成代码,但它能生成什么,开始受到编译器约束。
这并不意味着类型系统能够判断所有事实。编译器擅长发现结构、名称和类型错误,却不知道某个云区域当前是否有容量,也无法自动证明配置满足企业安全制度。它解决的是“可静态表达的约束”,不是整个部署正确性问题。
可以这样实践:把 IaC 声明放进 TypeScript
下面是一个可直接运行的最小项目。这里假设我们正在设计一个内部部署 DSL,模型只负责生成 src/generated.ts。示例没有绑定真实云厂商,目的是展示如何把区域、环境和规格限制交给 TypeScript 检查。
创建项目文件:
mkdir typed-dsl-demo
cd typed-dsl-demo
mkdir src
cat > package.json <<'EOF'
{
"name": "typed-dsl-demo",
"private": true,
"scripts": {
"check": "tsc --noEmit",
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.6.0"
}
}
EOF
cat > tsconfig.json <<'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"strict": true,
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
EOF
cat > src/generated.ts <<'EOF'
type Region = "eu-central-1" | "us-east-1" | "ap-southeast-1";
type Size = "small" | "medium" | "large";
type BaseService = {
name: string;
region: Region;
replicas: 1 | 2 | 3 | 4 | 5;
};
type ServiceSpec =
| (BaseService & {
env: "dev" | "staging";
size: Size;
})
| (BaseService & {
env: "prod";
size: "medium" | "large";
});
function defineService(spec: ServiceSpec): ServiceSpec {
return spec;
}
export const service = defineService({
name: "checkout-api",
env: "prod",
region: "eu-central-1",
size: "medium",
replicas: 3
});
console.log(JSON.stringify(service, null, 2));
EOF
npm install
npm run check
npm run build
node dist/generated.js
成功后会输出一份经过类型检查的部署声明。如果把生成内容改成下面这样:
export const service = defineService({
name: "checkout-api",
env: "prod",
region: "eu-central-9",
size: "small",
replicas: 3
});
编译器会指出区域不属于 Region,同时生产环境不能使用 small。这类诊断比“部署失败”更早出现,也比只返回一个布尔值更适合作为模型的修复上下文。
真实项目中还可以继续加入:
- 使用品牌类型约束资源 ID、CIDR 和镜像引用;
- 用构造函数隐藏危险默认值;
- 把合规策略编码为不同环境的联合类型;
- 由经过测试的渲染器把类型化对象转换成 Terraform、Kubernetes YAML 或内部 API 请求;
- 对无法静态检查的规则追加策略引擎和部署前探测。
不要让模型同时生成类型定义和业务配置,否则它可能通过修改类型来“修复”自己的错误。更安全的做法是由平台团队维护 DSL API,只允许模型修改受限的生成文件。
把编译错误变成模型的修复任务
类型化嵌入真正有价值的地方,是可以建立 generate-compile-repair 循环。一个简单流程如下:
- 把需求、允许使用的 API 和目标文件交给模型;
- 保存模型输出;
- 执行
npm run check; - 如果失败,把源码和编译器诊断发回模型;
- 要求模型只修复相关错误,不修改 DSL 定义;
- 设置最大重试次数,仍然失败则转人工处理。
可以使用这样的修复提示词:
你正在修复一个由模型生成的 TypeScript 部署声明。
约束:
- 只能修改 src/generated.ts 中 defineService(...) 的参数对象。
- 不得修改类型、函数签名、tsconfig.json 或 package.json。
- 不得添加 any、类型断言、@ts-ignore 或 @ts-expect-error。
- 保留用户要求的服务名称和生产环境。
- 输出完整的 src/generated.ts,不要解释。
编译器诊断:
{{COMPILER_ERRORS}}
当前文件:
{{CURRENT_SOURCE}}
循环控制本身可以保持简单。下面的脚本骨架负责运行编译器并限制尝试次数;其中调用模型的部分需要替换为团队实际使用的 API 或代理程序:
#!/usr/bin/env bash
set -euo pipefail
for attempt in 1 2 3; do
echo "Type-check attempt: $attempt"
if npm run check >compiler.log 2>&1; then
echo "Generated DSL passed type checking."
exit 0
fi
cat compiler.log
echo "Invoke your model here with compiler.log and src/generated.ts."
# 例如:python repair_with_model.py compiler.log src/generated.ts
done
echo "Generation still fails after 3 attempts; human review required."
exit 1
生产环境还应保存每轮输入、模型输出、编译器错误和最终差异。这样不仅能审计自动修改,也能统计哪些类型设计经常让模型困惑。若某类错误反复出现,问题可能不在模型,而在 DSL API 过于隐晦。
文章提到的 kUML 基准可以看作这一方法的评估场景:重点不只是比较模型第一次生成了多少正确文本,还要观察类型约束和修复循环能否把失败样本收敛成可验证结果。没有具体评测数据时,不应假设这种方案必然优于所有专用解析器;它的优势主要来自成熟编译器、清晰诊断和现成开发工具链。
采用前要划清验证边界
Typed Domain Grounding 能减少一类幻觉,但不能把模型输出自动变成可信部署。落地时建议检查以下事项:
- 类型是否由可信代码维护:模型不应有权限放宽类型或加入逃生口;
- 是否禁用绕过机制:CI 应拒绝
any、忽略注释和不受控的类型断言; - 是否还有语义验证:配额、权限、成本、网络连通性和实时资源状态通常需要额外检查;
- 渲染器是否经过测试:类型化对象正确,不代表转换后的 YAML、SQL 或 Terraform 一定正确;
- 修复次数是否有限制:无限循环既浪费成本,也可能掩盖需求本身的矛盾;
- 高风险操作是否需要审批:删除数据库、开放公网访问和修改身份策略不应仅凭编译通过就执行。
适合优先尝试的场景,是结构稳定、非法状态可以大量编码进类型、并且已有成熟编译工具链的内部 DSL。相反,如果规则主要依赖实时外部状态,或者语言允许大量动态扩展,仅靠类型化嵌入的收益会较小。
大模型可以成为下一位 DSL 作者,但编译器应该坐在它旁边。把生成能力与确定性的类型检查、策略验证和人工审批组合起来,才可能让“模型写配置”从演示走向可靠的工程流程。