让大模型编写 DSL:用类型系统和编译器压住幻觉

2026-09-16 22 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

让大模型生成领域专用语言(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 循环。一个简单流程如下:

  1. 把需求、允许使用的 API 和目标文件交给模型;
  2. 保存模型输出;
  3. 执行 npm run check
  4. 如果失败,把源码和编译器诊断发回模型;
  5. 要求模型只修复相关错误,不修改 DSL 定义;
  6. 设置最大重试次数,仍然失败则转人工处理。

可以使用这样的修复提示词:

你正在修复一个由模型生成的 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 作者,但编译器应该坐在它旁边。把生成能力与确定性的类型检查、策略验证和人工审批组合起来,才可能让“模型写配置”从演示走向可靠的工程流程。


相关推荐