面向LLM的App架构——技术维度

发布时间:2023年12月23日

这是两篇面向LLM的大前端架构的第二篇,主要写我对LLM辅助开发能力的认知以及由此推演出的适合LLM辅助开发的技术架构。

LLM之于代码

商业代码对质量的要求其实对LLM是有点高的。主要是输入准确度、输出准确度(这个是绝大部分人质疑的点)、知识新鲜度和上下文长度。
现有的玩票类的代码辅助看起来非常酷炫(也证明了LLM的潜力,特别是chatdev),放到商业环境下还是不太行。Copilot简单使用了相似的线上代码,所以可用性好很多,但是LLM的参与度非常低。AutoDev、UnitMesh做了很多东西,源码上看prompt有些简单,没有真的用过,不敢评价。
简单解释一下我觉得有挑战的这几个方面:

  • 输入准确度,要保证完备且精准的把需求写进prompt。而且要把边界条件、异常处理都加上,这个就比较离谱了,因为大部分人是做不了这么详细的技术设计的,或者能细致如斯那离真正代码就太近了,不太值得费心思用LLM
  • 输出准确度,这个好理解,想办法规避幻觉。不论是代码本身层面的还是业务逻辑层面的
  • 知识新鲜度,LLM对新知识的追加是非常困难的,而且不天然具备新知识比旧知识权重高的特点。对于大前端这种人均轮子王的技术栈,很难给出足够现代的代码
  • 上下文长度,即便是最新的gpt4,面对轻松松百万行的代码处理也是有心无力的,更不要说超长上下文带来的lost in context问题。就算是想办法把业务上下文处理掉,组件、内部基建的上下文是极难搞定的

修改架构以适应LLM

核心思路:严格的SOLID和代码拆解,减少单次生成范围;不造轮子;让LLM做准确度要求不高的工作。

流程

编码只是整个开发过程的一部分,基于前面的分析,这部分提效最困难。而开发过程的其他部分反而有非常适合LLM的,有的简单调整也可以快速应用LLM。单次开发流程大概是需求分析->技术设计->写代码->自测->CR->bugfix->上线。

需求分析

主要是把业务正常、异常情况,潜在扩展点,非业务逻辑类的需求(性能、安全性等)列清楚,并且确定业务归属的子域(DDD)。
需要做的根据需求文档做问答、对上下文进行常见的扩写(异常情况、扩展点、其他要求)非常适合LLM。但是确定业务归属不太合适,不过也不太值得用LLM。

技术设计

确定模块、分层、用的技术点、核心复杂度的拆解和设计、风险评估、扩展点实现方式等。
需要做的有不少是搜索(retrieve)类的工作:技术点、风险评估、核心复杂度的拆解方式、设计模式,是非常适合LLM的,如果能对接搜索引擎的就基本完美了。
识别核心复杂度是个分类任务,还不需要特别新的知识,这个也非常适合LLM。
还有另一个大部分工作是应用规则的:模块、分层、细节设计、任务拆解和依赖梳理、相关代码梳理,这些对LLM比较难。

写代码

见下文

自测

主要是手点和UT,手点没办法,UT见下文

CR

CR其实要确定三件事是做到位的:技术使用、业务合理、多端一致。

  • 技术CR,主要就是看遵循习俗、规范。只要不是有特别多内部基建和奇异写法,LLM的知识和理解力是完全胜任的
  • 业务CR。这个需要有完整的业务定义的,仅靠当前的技术评审文档是不够的。和代码合到一起,上下文会非常爆炸,不太行
  • 多端一致。这个其实完全可以在写代码的时候靠翻译,我们落地效果非常好。如果是硬对齐,是需要LLM分别解释代码到一个中间态,然后再用LLM做中间态的一致性判断的。这里面又涉及到了不同端基建行为的不一致,所以问题会比较多

bugfix

由于技术带来的bug一般都问题不大,由业务逻辑带来的bug一般都解不了。

上线

这部分主要是merge代码处理冲突。这个上下文会非常长,但是如果能有业务解释文档(prompt历史记录)做背景,解冲突可能会很好用。没试过,不敢保证。

新App

基建

简单一句,尽量用最流行的开源库。用hook、字节码、fork来做底层修改,但是保证api与公有知识一致。这样LLM生成的代码能非常简单的适配。
即便做不到这个,那一定要有(智能)规则来做转化,这个转换可以也利用LLM。
如果没有流行开源库,那一定按照广为流传的标准定义或者计算机基本原理中的术语(不是黑话)来定义接口。LLM的理解和近迁移还是相对厉害的,如果复杂任务+中远迁移是强模所难。

UI/业务组件

要么能用规则说明白设计稿与组件的对应关系,让LLM一次性直接生成使用组件的代码。要么就是生成原始代码,再利用RAG相反的思路去做组件替换。
前者没有试过,我司用的figma,导出的设计稿并不能包含组件信息(虽然在figma页面上能看到)。后者逻辑上可行,还没来得及试。

分层

一定要严格、规整的MVX分层,严格、规整的数据驱动(或者状态机),保证每一层的关注点是独立的,且上一层的代码只需要理解和依赖下一层。规避任何context、单例、全局变量等不受控的信息扩散。保证任何一层的代码生成只需要当层的业务描述和下一层的接口声明,且接口必须明确变化和不变字段。

分块

分块是为了尽可能减少单模块的上下文需求和迭代所需要的历史代码。
几个关键点:

  • 平衡模块大小和通信,降低大小核心目的是控制上下文长度。如果通信过多的话,上下文会比业务逻辑描述还要更麻烦。
  • M层用DDD拆分,按业务逻辑边界拆开
  • V层用视图位置拆分,按设计稿边界拆开
  • X层如果是VM的话,按V层拆分;如果是P层的话,按M层拆分。这个比较麻烦,需要实践中规范SOP。MVVM和MVI(redux)应该是更合理的选型

函数式

分块的粒度最终会是方法,不会是类。因为类会有成员变量,对生成准确性影响会比较大,所以要尽可能规避。而最好的办法就是函数式。

打包/组件化

幻觉是不可避免的,人和IDE一定是在相当长时间内作为纠错机制存在的。虽然已经有利用编译错误进行编译错误修复的功能,但是,这个的正确与否是极为客观的,修复也是极为客观的(语法规则)。而一般情况下业务的正确与否还是需要人来校准的,否则就可以通过细化步骤完全规避幻觉了(在用LLM的过程中发现,任务的核心复杂度严重影响最终结果可用性,细化步骤+ReAct来把单任务的核心复杂度降低,并且利用前序消息做chat历史效果好很多)。
那么,快速试错就是开发过程中最重要的了。换句话说,打包速度、小粒度快速运行会变得极为重要。打包又要成显学了,就…。所以,MVVM这种能把业务逻辑、交互逻辑都以UT方式验证的模式会比其他MVX模式要更适合。任何可以快速预览的UI框架也都更优于需要打包的框架。

UT

对于相对完整和独立的代码,LLM是可以给出完整的UT的。如果是白盒补UT,边界搞得比较准。但是,有一些算法会明显被网络上不靠谱但错的很普遍的代码误导,出问题。
再叠加验证速度很可能成为开发效率瓶颈,应该更多的考虑黑盒的TDD,甚至case本身会是非常好的提示词,能非常准确的描述需求。从需求到UT这个还没找到靠谱的prompt,直给的prompt效果不好,能想到的多步骤是需求->简单代码框架(翻译)->边界条件(联想)->UT(翻译)这个路径,但是我司并不写UT,暂未实践。

迭代

迭代的挑战是远高于一次性生成的,之前所以image2code的项目都选择性的无视了这个点,导致不太能落地,沦为KPI项目。
代码由于大部分源自于生成,迭代方式一定会有大量变化。无非是两种可能性:每次都重新生成和每次都带上老代码。
重新生成就必须把历史(上次)prompt和人工修改点记录下来,那么commit的规范应该是生成(记录代码和prompt)+fix bugs。这样的下次生成最理想情况下(LLM的温度和topxxx都是最稳定的那档)是可以直接重新生成+cherry-pick来形成下一个迭代结果的。但注意,很可能代码生成是一连串对话的结果,所以可能对话导出+单独文件是更明智的选择,比如codexxx.kt有一个配套的codexxx.chat.log来记录完整prompt过程。
带上次代码,这部分没有持续试过,但简单尝试发现很难正确描述代码。或者说很难把修改点让LLM弄明白。Chat效果相对好一些,不过也不是很乐观。

风格一致性和协作

这里是不是真的需要我是有一些怀疑的。如果所有代码都是生成的,那么协作规模应该是相对小的,即便是大团队,边界也会更加明确,人的作用也更多的是cr和流程管理,所以由大规模协作带来的风格一致性、规范等等问题会不会就不需要了,只要保证模型能迭代就ok了。
但是一致性带来的技术收敛就没有了,对hook、依赖管理会有更高的要求。

老App

老代码会希望演变到新App描述的状态,或者说把屎山变成稍微有条理的代码。有一些额外的工作可以试着用LLM,也有一些大概率当前的LLM不能胜任。从大到小大概是这些工作。这部分很多来自我自己的知乎回答。

模块化和业务分割

受困于上下文长度和代码理解的精准程度,这种偏全局的工作很难直接利用LLM。但是在原有静态分析工具的基础上,把结果规范化之后让LLM做全局分析应该是问题不大的。因为静态分析之后做模块化和拆分其实是根据依赖做最大流最小割,以及对代码做分类,这两个上下文要求就小很多了,分类还是LLM最强的能力之一。

整体分层

分层比单纯模块化需要更多的业务知识,是根据业务特征把分好的模块归入不同的层级。毕竟对于IDaaS和支付公司来说登录是完全不同的业务定位,代码所处的层级也是完全不同的。这个其实不太需要LLM,有个正经业务架构师正经分析一下业务情况,给一个原则性指导文件就好。但是,让LLM根据这个指导文件来指导后续代码的实施还是很有价值的,至少是可以做一个文件问答,让每个人都能更好的理解其中的原理。

冗余

发现和去掉冗余/重复代码也是去屎山的重要步骤。对于完全基于LLM的App来说,这个也是非常重要的,因为LLM上下文天然决定了除非用非常强的RAG和大量反反复复的chat,否则一定会出现大量重复或极小差异的代码。
这部分直接裸用LLM不行,但是用LLM做标注、embedding后做相似度应该是可以的,只不过做全代码聚类的成本比较高,需要有的放矢的查和改。如果能fine tune embedding,直接对代码表意,能去掉标注步骤,可以真的做全代码的聚类和修改。

局部分层

就是把BBM搞成合理分层的MVX。如果是想真的把MVX分好,这个难度很大,LLM不太能理解分层的清晰边界是啥;但是,如果是人明确规则,比如buildview、setdata、updateconstraints,这种明确的代码特征,那非常好用。有了这些方法,其实人再做分层也没那么麻烦了。
主要还是困于LLM应用原则(中距离迁移)的能力并不是很强。

老代码迁移

迁移到函数式

这个相对简单,一般这个量级的代码对gpt4来说问题不大。函数式也是一个通用概念。任务本身难度没那么大。

迁移到更通用的库

完全看现有库是不是接口非常离谱。如果老库只是实现非常蹩脚,但是还是使用业界相关命名,迁移效果非常好。如果老库取的是Eva这种不着调的名字,那很可能会大规模跑偏。因为被激活的包含了完全不同领域的部分。

性能问题

看性能来源是什么:

  • 全局设计问题,这种通常是业务设计问题,LLM不太行
  • 局部设计问题,这种通常是使用的技术不太对或者实现方式不合理。如果是成熟(老)技术,那问题不大,如果最好的解法是新技术可能会有点问题

梳理

或者说与代码对话。
全局搜索类的靠RAG是非常值得做的。我自己实现了一版半,效果还是可以的。如果不考虑成本,把能上的手段都上了,把embedding的ft也做了,应该会非常惊艳。
局部分析类的直接把代码丢给GPT4也是OK的,只是代码最好和正常思路是一致的,要不然代码中命名、逻辑激活的常见思路和实际情况的差异会带来大量的幻觉。

总结

LLM对于开发来说,当前情况下最大的贡献点在于非代码部分和最细节的代码部分。为了适配LLM的能力水平,流程需要识别出适合LLM的点,并且规范化输入输出,尽可能交给LLM;代码需要极限的拆解,让每一块代码都更聚焦;人需要增强cr能力和表达能力,充分利用好工具;IDE需要更好的集成和基于prompt的协作支持。

文章来源:https://blog.csdn.net/pouloghost/article/details/135133576
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。