Ohhnews

分类导航

$ cd ..
DZone Java原文

为什么我不希望LLM生成Java业务逻辑

#llm#java#代码生成#业务逻辑#领域特定语言

一个 Pull Request 到了。几百行 Java 代码,实现新的折扣规则:阶梯式阈值、一个区域例外,还有某个谁都解释不清的忠诚度层级设置。它能编译,测试也通过。这是一个 LLM 用了大约四十秒写出来的。问题是:谁来审查它?

规则的所有者在商业运营部门。她知道哪些客户应该拿到折扣,也知道那个区域例外为什么存在,但她读不了 Java。能读 Java 的人并不知道阈值是否合理。他能做的,只是检查代码看起来是否合理,因为这是他唯一有能力检查的事。于是,实际发生的审查,并不是真正重要的审查。

这就是我一直念念不忘的问题,而且它与模型好不好完全没有关系。

这不是在争论模型够不够好

大多数针对生成的代码的反对意见,都集中在“能力”上:模型臆造了一个 API;某个边界情况搞错了;写出的东西在正常路径上能跑,一到生产环境就崩。我觉得这些论点没有说服力,因为它们会过期。模型会变得更好。任何建立在今日错误率之上的立场,都有保质期。三年前持这种观点的人,如今大多已经退让了。

更持久的问题不一样。它不是模型写得好不好,而是它所写出来的东西被允许说什么。一个从不犯错的模型,你把 Java 交给它,它依然可能输出 Runtime.getRuntime().exec(...)。不是因为恶意或困惑——而是因为这句话在它被要求使用的语言里,本来就是可用的。能力与授权是两条独立的轴,提升前者对后者毫无作用。

“用 Java 写”这个授权,比任何人真正想给的都大得多

想一想,当你要求用 Java 实现一条折扣规则时,你实际授权了什么。你授权了文件系统访问、网络套接字、反射、创建线程、执行进程。你授权了类路径上的每一个类,包括那些连接数据库、支付供应商和密钥管理器的类。你还授权了在运行时加载新代码。没有人打算授予这些。它们是随语言免费附赠的,就像一把房门钥匙也顺带能打开储物间。

这个任务可能只需要六个操作——查一笔订单、计算总额、检查客户层级、应用折扣、记录决策、批准或拒绝——而你交出去的语言,包含着 Java 所包含的一切。

任务实际所需的权威,与语言所赋予的权威,两者之间的差距,就是全部问题所在。无论模型是否可信,这个差距都存在。无论有没有人会利用它,它都存在。这个差距非常大,而且你在 Pull Request 里看不见它。

常规防护措施其实是“拒绝列表”

标准的应对手段都有一个共同的形状:在提示词里告诉模型不要碰文件系统;审查生成的代码;运行静态分析并标记危险调用;在带受限安全策略的沙箱中运行。

这些方法都要求你,在一个实际上不可枚举的“可能发生之事”的空间里,列出“决不允许发生之事”。你是在为一种通用目的语言写拒绝列表。你必须想到 exec,然后想到反射调用 exec,然后想到某个依赖替你调用外部命令,然后再想到下一个。

这个教训我们在安全领域很早就学过了,而且已经得到定论:允许列表胜过拒绝列表,因为允许列表是有限的,而且是你自己写的。可不知为什么,一谈到生成的代码,我们又拿起了拒绝列表。

收缩语言,而不是收缩模型

另一种做法是:不再约束一种强大的语言,而是提供一种很小的语言。给模型的词汇表里,只包含该领域实际拥有的操作——比如前面那六个——别无其他。这不是“受限的 Java”,而是一种完全不同的、更小的语言,其全部词汇是你们的团队事先用 Java、有意识地写下的一份清单。

生成的业务逻辑因此会像这样:LOAD_ORDERORDER_TOTALREJECTAPPROVE 并不是这种语言的一部分,而是有人决定暴露出来的 Java 类。Order 是一个 Java 对象,程序可以持有、传递它,却永远无法窥视其内部——这里没有 purchase.customer.account.balance,只有这个领域自己选择的那些操作。

这带来两点改变,而第二点比第一点更重要。

表面那一层:危险程序不再是被禁止,而是根本无法表达。如果模型输出 DELETE_ALL_ORDERS,没有任何东西会基于策略去拒绝它。这个名字什么都不是。程序无法编译,理由和一个拼写错误无法编译一样。这里没有拒绝列表,因为没有什么是需要拒绝的。

不那么明显的一层:商业运营经理能读懂上面的程序。她能告诉你阈值是否正确,拒绝理由是否符合合同要求,一次批准是否应当被记录。审查于是回到了规则所有者手中。这正是文章开头缺失的那种审查。无论对生成的 Java 做多少静态分析,都换不来这种审查。

一种小语言还会悄悄带来另一个好处。由于没有数据结构、只有一个全局作用域、没有 null,而且编译器拒绝运行在变量被赋值之前就读取它的程序,整整一大类细微的错误根本无处栖身。不是“被捕获”,而是“不存在”。

代价是什么,又买不到什么

我不会只列优点就让你相信这个论点,所以这里也说说账单。

你必须设计词汇表。 得有一个人坐下来,决定这个领域有 ORDER_TOTALCUSTOMER_RISK,而没有其他四十种东西。这是实打实的工作,要在生成第一行代码之前,由一位懂这个领域的人完成。如果你团队里没人能写出这张清单,这个办法帮不了你。它只会让你看到“清单并不存在”。发现这一点也是有价值的,但那不会是一个愉快的早晨。

复杂算法仍然留在 Java 里。 业务规则本质上也是算法,它们应当属于小语言,这正是重点。但路径优化、评分模型,任何有真正计算含量的东西,都应该放到小语言所调用的某个函数后面。判断信号通常是:你想构造一个数据结构,或者想有一个能从三处调用的辅助函数。这两种情况都意味着你已经走出业务逻辑,应该退回去。

边界约束的是“命名”,不是“行为”。 这是人们容易忽略的限度;把它夸大了,正是这个想法被否定的方式。一个你暴露出去的函数,可以做 Java 能做的任何事。RUN_SHELL_COMMAND 完全是一个可注册的操作。词汇表只能窄到你所选择的那些操作那么窄;选得不好,你就会恰恰碰到本想避开的敞口。

目前还没有资源限制。 生成的程序仍然可能永远循环下去。这是一个缺口,而不是一项决策:解释器逐条语句执行程序,因此增加一个步数预算或截止时间是小改动而非重新设计,等有人需要时会加上去。在那之前,不可信输入需要和任何不可信工作负载一样的隔离措施。

你得到的,比“安全”更窄,却比听上去更有用:生成的程序所能命名的事物的集合是有限的、被写下来的,而且可以在任何东西生成之前由人类审查。

什么时候我仍然会写 Java

如果这件事真的是计算性的,那就写 Java。如果是一次性、下周就会被删掉的东西,就用最顺手的工具——Java、Python、一段 shell 脚本——让模型去写,不要为一个只能存活几天的东西去构建词汇表。如果规则变化太快,词汇表还没来得及定型就过时了,那么这种开销收不回成本。

如果你的业务逻辑已经由能够读懂它、理解它、并对它的正确性负责的人来审查——也许你并没有这个办法要解决的问题。很多团队都没有。但如果你正打算让模型用 Java 写业务规则,请先问我开头那个问题,因为答案通常会让人不舒服。总有人要批准那个 Pull Request。他是知道规则是否正确的那个人吗?如果不是,那这种语言就太大了。

我一直在沿着这些思路构建一门小语言:BUBAS,一门面向领域专家的编排语言,内嵌于 Java。上面的例子就是真实的 BUBAS。但这个想法并不依赖我的实现——关键在于你交出去的语言有多大,而你完全可以按自己的方式把它缩得更小。

本文表达的观点归 DZone 贡献者所有。