从形式定义生成代码

在过去的几周里,我深入研究了 盲意提示协议(BMPP)的协议开发后,我发现了一些有趣的观察。

在开发 vibelang-rsbmpp-agents-rs 和 BMPP 的形式文法 规范 时,我亲身体验到了 Rust 的设计哲学为何能更好地与大型语言模型互动。

这是我第一次认真进行“Vibecoding”,因为我一直认为 LLM 是特定任务的好助手,需要相当多的人类监督,所以我通常避免大量生成;通常我会在某个模块范围内使用 LLM 并进行相对测试。这是我第一次尝试一个相当大的项目,目标是完全用 LLM 编写它

1、类型安全 + AI 指导

Rust 著名的类型系统和借用检查器不仅仅是防止内存安全问题的工具——它们是AI 助手(除了人类)的优秀老师。当 LLM 生成 Rust 代码时,编译器成为一个非常详细的反馈机制,引导 AI 和开发者走向正确的解决方案。这尤其真实,因为我的项目性质也涉及编写转换器,所以 让 Rust 代码生成 Rust 代码(来自代理工作流程)。另一个使环境更加受控的有趣特性(根据我的经验,LLMs 在适当的护栏下表现更好)是该项目涉及结构化生成,因此程序应该在机器可读格式上运行,以便与自然语言交互。

这是我过去两年在 LLM 上的经验中得出的一些基本规则,我决定在这个为期三周的 Vibecoding 会话中坚持这些规则:

  • 最多使用两个不同的 LLM 来比较输出,在这种情况下是 Claude Sonnet 4 和 Gemini 2.5 Pro 通过 Perplexity UI。
  • 定义一个会话为一天的代码(大约一个宏任务,如“实现解析器”或“实现代码生成”,通常需要 4 到 9 小时)。
  • 在每个会话结束时删除或压缩上下文。这是因为如果上下文在多个功能上变得过于冗长,根据我的经验,它会失去清晰度。
  • 其他属于秘密配方的部分 🍝,比如在每次少样本交互中提供代码库的哪一部分作为上下文。
  • 不使用“高级”代理功能,如子代理和其他商业平台提供的额外功能。

结果对我来说相当令人惊讶,因为我总是被我试图解决的问题的严重性压得喘不过气来,没有 LLM 我可能根本不会开始这样的尝试,现在它已成为定义代理协调的一种框架(请 kindly 给 GitHub 仓库点赞 ⭐ 以继续跟踪)。

2、我的 BMPP 开发流程:案例研究

让我带你了解一下我在构建 BMPP 协议栈(从 Vibelang 到运行时)时所使用的流程:

2.1  结构良好的提示:起步良好

当我需要为 BMPP 的自然语言注释语法构建解析器时,我从简单的提示开始,例如:

“这是一个期望语法的例子 ~ BMPP 负载的一些示例 ~,请提供 BNF、EBNF 格式和 PEST 格式的正式语法”

这使得可以比较不同的输出并开始解析过程。一般来说,语法大约有 90% 的预期功能是正确的(有一些陷阱使得这个探索变得有趣)。我尽量简化示例语法,避免任何对 LLM 来说难以做出的假设。

例如,在 BSPL 中,括号 (...) 的使用表示“函数调用”,像 Pack(....),其中标识符为 Pack 的协议从嵌套协议内部调用;由于 BMPP 需要自然语言注释,并考虑到括号是解释事物的自然方式,我决定将其用于注释目的。这使得 LLM(以及偶然的解析器)很难接受,因为区分“用于协议调用的括号”和“用于注释的括号”需要更复杂的模式匹配)在遇到函数或注释时难以接受,这使得过程陷入术语的混乱。结果是输出中出现了更多的复杂性来应对误解。当这种情况发生时,我只需删除上下文并重新建立一个更具体的重点,尝试避免导致嘈杂结果的决策。

在原型定义语法之后,我进入了解析器。~ ... ~ 中的部分是为了简洁而省略的,目的是展示提示的结构:

“为这个 EBNF 语法定义一个样板解析器,带有语义注释 … ~ 对 BSPL 和我希望 Vibelang 的一些功能如何添加的持续描述 ~ 这里是一些 BMPP 负载示例: DirectedProtocol <Protocol>("protocol with valid direction") { ... }. 解析器应处理语义标签如 <Protocol>, <Agent>, <Action> 和括号中的自然语言描述 ("...") ~ 对 BSPL 语法特征的详细描述以及如何在协议基础上分部分添加注释 ~".

经过这次快速实验后,我决定开始一个更广泛的上下文定义,这个新提示花费了几周时间研究 BSPL,即 BMPP 所基于的协议;所以我尝试将所有内容整合在一起(语法定义和解析器),花了 2/3 小时以一种 LLM 不会假定有关协议的错误知识的方式编写(即使提供了整个 BSPL 文献)。于是,我开始了为期三周的 Vibecoding 冒险。

经过上述一些调整后,LLM 生成了坚实的初始代码,利用了 Rust 的模式匹配解析(使用 peg 库)。但这只是开始,一些功能缺失,一些标签和语法痕迹(如复合语法 <Tag>("some description"))被定义得相当繁琐,试图在所有情况下查找实际的单词 "Tag" 而不是一般地查找 "<...>" 中的标签位置,后面跟着括号。我必须明确写出一些我期望在解析器中看到的标记。我花了一些时间忽略这些缺失的标记,并试图告诉 LLM 重写模块,却没有注意到测试是被编写成忽略这些缺失的,即使在语法中定义了它们。

这是一次关于提示和上下文工程的伟大课程,问题是:我学到的东西 对于我在这个项目中需要实现的目标是否相关,还是只是在一年后不太相关的说法和技术?我得到了安慰的想法,了解 LLM 如何接收输入和推理可能是一种长期的知识,所以我接受了压力和潜在的时间损失。与学习编程语言相比,从我的角度来看风险更大,即使学习习惯也能让你更好地理解其他语言。直觉是,即使有时相当依赖于习惯(考虑你选择哪个 LLM 以及上下文如何定义,还更多地依赖于你在那一周/一天的特定时刻的语言表达能力),与 LLM 一起练习就像思维的体操。它仍然是不同类型的搜索。

2.2 通过编译器反馈进行迭代优化

这就是 Rust 的魔法真正闪耀的地方。我的编辑器直接突出显示了 Rust Analyzer 的错误,大多数都是微不足道的,当错误太多时,我会尝试在 LLM 中解决它们:大部分时候响应是正确的,但大约有 25% 的情况:

  • 回答过时了,无法让 LLM 通过指向正确资源来纠正它,即使使用推理也无法改善。这是由上下文架构解决的问题,你的空间(提示历史的集合)将需要越来越长的指令,例如:“始终检查 X 的最新版本 API”,“使用这些示例而不是那些”,等等。这对于单个功能的有限范围已经很重要,我可以想象人们试图在项目级别甚至 LLM 级别解决这个问题。
  • 回答撒谎并重复相同的代码带有错误。
  • 回答撒谎并使代码变得更糟,可能是通过回忆不同的例子,你可以看到几乎所有东西的风格变化。如果 LLM 选择了你之前生成的代码的风格,这在一段时间后会得到缓解。

这些都是要避开的行为,我认为 Vibecoder 的一种冲浪技能是平衡提示,以避免陷入这些陷阱,这会使你的功能螺旋下降到纯粹的熵和随机猜测的标记。

2.3 编译器作为教师和共同教师

不过大多数时候,粘贴到提示中的错误信息提供了有用的修正,并避免了一些需要去阅读文档、阅读示例、阅读 Rust 社区中任何帖子的相关时间。这是对伟大的 Rust 错误消息系统的最佳陈述!LLM 可以轻松地从编译器那里获取一些踢打 🙏

这部分认知已经被卸载,我很高兴,但再次,我是在学习新的东西,还是仅仅利用我已经知道的东西?我在 Rust 方面的经验在多年的时间里相对较长,即使没有预先的技能,这种事也会像这样发生吗?

2.4 Vibecoding 循环的实际应用

总结一下,我的典型开发周期变成了:

  1. 调整提示:始终阅读答案末尾的总结段落中提供的文档,可能会发现某些决定完全错误,无需尝试代码。
  2. 获取编译器反馈:Rust 告诉我们哪里出错了。
  3. 向 AI 报告:“编译器说:不能作为可变借用,因为它也被不可变借用”(“计算机说不”的那种)。
  4. 迭代:LLM 提供针对性修复,检查修复,重新运行测试。
  5. 编写测试:“为协议因果关系检查生成测试”。始终完整地阅读测试,比代码更重视测试。在重构的后期阶段再关注代码,当测试保持不变且代码被更仔细地阅读并可能优化时。
  6. 重复:每个循环都会变得更加精细
  7. 尽量保持在循环中并跟踪主要决策,很可能你需要在选择的不同分支上重复该过程。

这就是大多数 AI 编码公司正在尝试复制的过程。目前,对于我来说,为了使这种自动化有效,需要考虑太多的自定义调整和细微差别,但系统总是在不断改进。放弃一些对提示后的上下文调整的直接控制,可能会增加发生 2 中列出的情况的风险。

2.5 当 LLM 遇到其极限时

总的来说,这个过程让我想起了我尝试阅读 AlphaGo 的工作原理的时候;需求、功能和你的决定可能会生成相当大的决策树;上下文和提示工程,正确的提示,在适当的时间点的一些经验性基础规则可以大大修剪掉许多无用的分支。你使用 LLM 的经验为工具可以生成的最佳代码的概率提供了权重。

如果有一个要点:在这次编码实验中,大多数 LLM 失败并不是关于 Rust 的复杂性 —— 而是关于问题规范。对于像我这样的哲学爱好者来说,这令人欣慰。

在 BMPP 开发过程中,我遇到了一些 AI 无法修复的问题。归根结底,这些是因为模糊的提示或缺少人类读者可能认为显而易见的信息。

一个极端的例子:

❌ 模糊的提示:“让这个解析器更快”

  • LLM 尝试随机优化
  • 关于借用值的编译器错误到处都是
  • 没有明确的前进路径,即使在提示更明确的情况下,推理也会陷入垃圾输出的死胡同

✅ 具体提示:“通过为标识符实现零拷贝字符串切片来优化这个解析器,同时仅对语义注释保留拥有字符串”

  • LLM 明白了确切的权衡
  • 编译器验证了方法
  • 清洁、快速的代码出现

测试测试测试,一开始要仔细阅读测试胜过代码。

2.6 类型驱动开发

在构建 BMPP 的语义注释系统时,Rust 的类型系统引导 AI 向正确的抽象发展。我也帮助我更好地构建了问题(我最初对解析器了解有限):

#[derive(Debug, Clone)]  
pub struct SemanticAnnotation {  
    pub meaning: String,  
    pub constraints: Vec<Constraint>,  
}  

#[derive(Debug, Clone)]  
pub enum Constraint {  
    TypeConstraint(TypeInfo),  
    ValueConstraint(String),  
    ReferenceConstraint(String),  
}

AI 自然地围绕这些类型构建,文档越详尽,任何解释上的偏差都可能产生,这在可解释性方面看起来很有潜力。从这些抽象类型中,我引导编码进入更实用的类型,最终映射到协议中存在的类型。这是对协议定义过程的一个很好的见解,从不可用的高抽象到代码中的可用类型。

3、默认内存安全

在其他语言中,AI 生成的代码经常有微妙的内存问题。安全漏洞随处可见。我真的不知道使用非类型语言的开发人员如何信任上述过程。Rust 提供的形式安全性是唯一能说服我进入这种尝试的单一功能,使 AI 生成的代码可用。我也在 Python 中尝试过,即使广泛使用类型注解和 mypy,但这个过程比 Rust 的过程要累得多,也许是因为 Python 中各种代码样式的多样性没有那么严格控制。这让我想到两个随 Rust 生态系统提供的强大工具:cargo fmtcargo checkrustfmtclippy。我可以看到这些如何集成到自动反馈循环中。

4、BMPP 故事

使用这种 Rust + AI 工作流的结果是:

  • vibelang-rs: 完整的含义类型提示实现。
  • bmpp-agents-rs: 一个最小的协议编排框架,用于查询 LLM。
  • BMPP 语法: 基于 BSPL 的带语义注释的复杂规范。基于交互编程的安全分布式计算,具有形式规范。

并且:

  • 更高的协议合规率,LLM 现在可以更一致地读取/写入 BSPL(查看我的 结构化生成的比较测试台 以了解商业模型)
  • 基于交互编程的复杂交互安全定义,保证并行执行和死锁避免的形式证明
  • 不同组织维护的不同 LLM 系统现在可以在不同种类的系统下互操作

5、在 Rust 中 Vibecoding 的关键要点

  1. 从精确的、领域特定的和功能特定的提示开始,涉及你代码库中的少数模块。
  2. 让编译器通过详细的错误信息教 AI,并控制命名等样式问题以避免混淆和注意力分散
  3. 快速迭代 —— 尽快让 Rust 编译允许的速度,保持上下文更新:始终提供范围细节或尝试你心中目标的功能,这些功能可能对其他人来说并不明显。
  4. 当 AI 卡住时,专注于问题规范,这可能是因为提示树中上游的某个决定导致流程陷入僵局。
  5. 利用 Rust 的类型系统 引导 AI 向正确的抽象:LLM 倾向于选择其训练数据中编码的最流行模式,但这并不意味着如果推理提示过程的正确分支被正确引导,就没有什么可以生成的。

Rust 不妥协的正确性和 AI 创造性解决问题的结合创造了一种开发体验,既极其高效可靠,只要对问题形成和运行测试投入大量关注。

对于正确性至关重要的复杂系统,如分布式协议,这种配对不仅是方便的——它是改变游戏规则的。


原文链接:Generate code from formal definitions: Rust and Vibecoding

汇智网翻译整理,转载请标明出处