今天是4月的第二周,自三月中旬中止毕业设计以来,做了不少有益的热情洋溢的尝试,虽然最后没有做出非常有用的成果,但尝试本身已经带来了许多有益的经验,因此本着学习要闭环的态度在此记录.
论文循环修正系统--人或机器文本的识别分类器设计
这个项目是因为在初次答辩撰写论文时,发现网上的论文写作自动化流程,实验之后发现效果很好,但是还不够好,能通过知网检测,却不能通过维普的测试(ai率50%以上)于是萌生一种朴素的控制循环改善办法:通过论文中提到的检测算法本地检测论文中是ai的部分,然后语言模型进行改写.简单的两个部分的负反馈跟踪控制系统.然而实现起来却没那么简单.
首先是首次进行了完整研究流程的实践.我的毕设更像是一个给老师接的私活,是一个有设计文档和甲方要求的工程项目.而开展研究是另一回事,可能以后我都没有机会再踏入校园了,因此也不会有什么学术上的指导.阅读论文是一种特别的阅读体验,和之前看过的任何文字组织类型都不同,一方面具有学术行文风格和总结性,有点像教科书,另一方面又主题单一,流程清晰,具有很强的实践指导性,像是实验报告.借助目前的ai确实能很快整理和梳理论文要点,汇总以梳理各个论文之间的关系,就好像自己在编排一本只属于自己的关于特定问题的书一样,这个过程也有点像linux终端里的编译/调用工具链,为了特定目的将一个个专精小的工具以服务于此目的的方式组合排列在一起,在此过程中一边沿着多篇论文的方向推导,一边总结着多篇论文的共通性,比如其中的假设和数学建模方法等等.通过这些归纳演绎方法,能够初步确定一个自己要做的问题解决纲领.
工程实践上,在这样的开发中首次尝试了无编码开发--纯自然语言prompt引导模型:创建文档,实施计划,编写关键模块,复核审查与更新计划的循环.从MVP开始,以TDD为依赖,演变到SDD/DDD的文档约束开发流程,被证明至少是部分有效的.也暴露出现有模型/agent平台设计的仍然不足够,对跨多轮对话的架构记忆漂移和幻觉导致的错误引用和重复实现问题,以及单次对话的重点转移问题仍然对于整个工程实现来说危害巨大,也证明人在软件开发,起码是像自然语言处理这样复杂程度的任务上已经是不可或缺的了(对这个复杂度的估计描述:以我的能力和api速度,时间上大概需要3h以上的心流工作的程度).对我来说,有纸笔在键盘面前写写画画,这个介质引起的发散思维模式和记录的形式仍然是任何软件,任何在键盘鼠标上的工作都不能替代的.至少无纸化对我来说不成立.同时我也体会到数学建模在工程实现上的显著指导作用,不只是算法的抽象,也是在架构设计和开发流程上,应用控制论模型和假设检验思想,数学思路可以为工程实现提供清晰可靠,恰到好处,理论严谨的指导.
全栈开发的系统流程设计与实践
清明节前,在一次简短的交流中,有人向我透露在清明节学习网站制作的可能.哦,我的生活里已经很久没有和活生生的人说话了,这可是相当难得.于是我立马开始准备教程,并且应用刚在上一个项目中学到的一切,与之前全栈编程的经验.也正好趁此机会把自己的博客网站更新一次,改掉原来那些不太令我满意的设计.由于在这方面已经有过多次的完整经验,因此这个项目主要是从理论出发的验证性实践,而不是在实践中摸索理论.我认为一个完整的网页开发应该包括design--develop--deploy的主要流程,在design里有意识的引入数学建模和艺术设计环节,分别对应着后端算法,数据结构抽象与前端视觉效果.对于主要是静态展示的博客网站来说,数学方面倒是没什么做的,不过明确集合特征(比如那些matadata)对UI设计的组件选择还是很有指导意义.这一次的UI真的使用手绘对包括动画效果在内的转场做了细致到每个组件的设计.实际上就结果来看确实是比上一次更满意的.
这次因为涉及到大量动画的同步,在被ai误导着实现了简陋的自制状态机管理.但是很快就沿着需求出发回到了成熟的gsap动画引擎状态机.UI上虽然离初始设计有简化,但是在实现过程中也有随着平面设计点线面填充原则的创新.对平面设计理论的学习算是一个小插曲,不过确实对改善视觉效果有很大帮助.实现上沿用了毕设项目的VUE+TS,相比之前博客的纯js+react要现代和先进很多.也采用了类似的分层文件结构.不过暴露出VUE页面组件单组件过大的问题,这对于LLM不是好事,REACT的模块化结构会做的更好,更偏函数而不是页面,但是性能方面确实会稍微差一些,相比于之前一直用的庞大nextjs+react全栈框架,这次也算是实践了另一种轻量高性能的选择.总体上还是比较令人满意的.
对编程语言本身分析与辅助性软件开发--css内联提示器
但是在定制界面的过程中,最麻烦也不能让ai代劳的就是css编辑了.css其实是一大堆预定义,不用导入,接受可变数量多种类型参数的函数集合而成的声明式语言.为什么至今没有人实现内联提示呢,要是有像clangd那样的静态分析提示,应该会好写很多,本着这样的想法我开始编写css-inlayhint,内联提示器.
这个项目的创新之处在于首次真正分析源码,参考大型成熟开源项目,这里用到的是llvm的clangd模块,用流行的概念来说就是distill吧.ai不能自动完成的原因是,它面对极度复杂,交叉依赖的大型cpp项目完全是无能为力.只能依赖clangd自己的静态分析,从关键词查找与筛选开始,沿着调用链分析inlayhint的实现用到的每一个关键函数具体实现,以及如何把这些函数组织起来构成完整功能.不过倒是可以在找到相关函数之后利用ai分析,以及探讨分析方向,总结分析成果.我想以后可能的成熟项目源码功能学习可能也会沿用这一思路.这一阶段的产出主要是对源码的分析文档,以及借鉴性的整体架构设计.
对代码本身的分析也是第一次做.相比于第一个的自然语言分析,代码作为一种语言的分析要简单得多,因为其本身就是人工设计的语言体系,具有AST结构,语法清晰(虽然在库里比如mdn-data并不能完整清晰的直接访问),语义也都有标准定义.但是设计一个分析器不像自然语言那样依赖统计,而是基于符号的精确解析.这里的困难点主要不在于数学,而在于一个合理的工程结构,从clangd的学习文档里能学到许多有益的思路,但是针对css这一语言的特殊性,以及单一工具并没有llvm那样的历史负担,要做的工程适配还有很多.架构设计在实现中从三层演变到五层,最后到六层.不过这些架构设计是值得的,在不断演进和增加的需求中,几乎只有核心算法层需要变动,边缘的适配层保持了良好的解耦和模块化职责,使得代码增加的同时,维护量几乎保持不变.这次的项目我放弃了纯黑箱的cli无编码编程,转而回到最初的ai编辑器辅助设计,llm从助手降级为伪代码编译器的角色,从一开始就依赖TDD写测试样例固定行为,并在我觉察到上下文处理能力不足时及时进行cleancode重构,过长过大的文件进行模块化拆分,以及总是对实际运行代码进行人工逐行审查.这些尝试都被证明是有益的,代码的改动量先变大后变小,大概可以可视化成纺锤形/橄榄球型的分布,在最后加功能时几乎只用添加而几乎不用修改已经写好的部分,这是高内聚低耦合的体现.我认为在很长一段时间里这都会成为对质量有保证的项目的一种模式.这次实践中,证明测试代码虽然逻辑重复,但是用于观测的代码量比实际执行功能的代码量只多不少.对测试案例的选取也是需要工程经验确定测试边界的.我为这次的情景尝试构建了笛卡尔测试样例自动构建器,思想上就像之前自然语言处理时的自动特征以及参数寻找优化一样.不过构建器本身也会出错,人工的逐行验收(略读)仍然不可缺少.
控制论视角的通用性补充
控制论就像万能锤子,学会之后看什么都是钉子.凡是有循化结构,必然让我立马联想到控制论.对llm的应用也是如此,工程准则 human in the loop 就明确表达了这一点,整个工作流是在一个loop之中.llm在编码工作中作为被控对象,依赖prompt与spec(doc)进行前馈控制,而由人类审查与TDD多层结构的测试用例进行反馈控制,最终目的仍然是快准稳,即要求可行代码的尽可能快产出,准确在工程可接受误差内达到目的,同时保证长时间和各种边缘情况下的稳定性.沿着这个思路很容易就会想到如今的那些声称要替代人类的编码agent,只是他们失败了,人类进行代码层面的反馈是不可或缺的,失去这一信号,结果总是振荡很大(幻觉漂移),同时伴随着难以收敛到期望结果,而一个不稳定的系统是工程上不能接受的.
沿着控制论思考的不只有我一个人,但是在阅读任何材料和工程实现之前,我想自己就这个问题做一些已有知识上的思考和总结.对于控制系统来说,首先要了解其特性和类别,就类agent循环过程而言,我认为这是一个离散的时变系统.输入的信号是编码意图,输出的就是代码.稳定输出在这里意味着代码在工程上在可接受的范围内,允许一定的实现不同.控制系统的分析主要分为频域和时域,虽然有硬套概念的嫌疑,但是我认为这其中存在着某种相似,只是基于自己的数学直觉有了以下的想法.对于输入信号,也就是编码意图,会受到随着时间演进脑海中冒出的新想法,或是结构表现出的未预料的特征,或是市场需要,或者单纯只是计划中的演进,总归是要产生变化.那么这种变化就是有频率和幅度的,因此可以进行傅立叶分解,可以进入频域分析.为什么LLM -> 代码执行 -> 报错信息 -> LLM这样的回路在实践中会产生振荡较大,迟迟不能满足工程要求,甚至发散走向彻底的架构灾难呢,我认为是因为这种过程中存在着相当大的相位滞后,同时增益不加控制,很容易就进入奈奎斯特稳定判据的发散状态,相位裕度小.因此对于简单的CRUD尚且可以完成,但是一旦进入比如前后端协调,复杂算法设计,大型代码库高耦合项目修改就会频繁出错.而为什么人类进入这个循环能达到改善这一状况呢?实践中我们会尝试回退,减少修改(一次只修改一段或是一个文件),清空上下文,或是阻止趋势.实际上,这可以归为对编码幅度和趋势的控制,也就是PD前馈比例微分,通过观察LLM改写的趋势,我们能在它不对劲的时候及时纠正,也能减少修改的程度,PD控制的引入大大提高了相位裕度,因此系统得以在即使是频繁修改中也变得稳定.在这样的离散宽频域(并非稀疏)系统中,修改的频率并不是固定的几个点,而是存在多种无法预测的可能.依靠采样反馈也因此受到采样定理的约束,其中观测频率由人类的水平,包括理解水平,精力水平等等个人化的因素决定.因此对llm的迭代速度实际上存在某种取决于人类的约束.下一代agent如果存在,也许就要向这个方向改良.现在我看到有引入多agent协作,无论是中心化形式还是蜂群分布式,虽然在各种评测成绩上有所提高,但实际在面向大众生活的长期软件项目中的效用仍然存疑,这些方法本身诞生的时间也就最多几个月而已.类似在时域中对应的工程指标也比较好找.比如上升时间可以理解为到达MVP的时间,峰值时间是那个代码最混乱复杂的时间,或者说修改量/修改速度最大的时间,同理可以定义超调.调节时间则是到达最后可交付稳定产物的时间,稳态误差则是距离需求的偏差程度,可以描述为可以接受的bug或者为了省事暂时忽略的边缘情况.PD控制,在这里的效果近似于大增益纠正与增加阻尼比.以及探讨一下为什么TDD在agent流程中成为广泛运用的结构.实际上测试用例在起到一种非线性调节作用,给出的是二值判断,而不断增量增加,固定已有行为的测试用例,实际上类似于积分控制.结果上来说,减少甚至是消除稳态误差也确实是其目的.然而这会不会有导致相位裕度减少呢?实际上编码时,引入TDD流程似乎确实也增加了开发时间,我没有同时间的同项目对照实验,但是相较过去的编码过程确实要耗费更久时间.但是导致数学比喻意义上的发散似乎是难以想象的.至于能控性与能观性的分析,似乎是更抽象的领域,涉及对模型黑盒行为的本质探讨,目前可解释性仍然是学术研究的热门方向,进展每天都有,我也不好说什么结论,不过或许是可以做一些分解得到能控或者能观的部分.