昨天一天没陪儿子玩,成效显著,AIlice终于如愿支持自扩展了---她自己可以完成外部环境交互模块的构建,并且立即加载使用!而今天统计了下代码量,竟然还在2300行左右,可以说是相当的轻量级了.
AIlice的构建初衷是基于开源模型构建一款Agent框架,要足够的轻,从而开发者能清楚的明白系统整体的运转,从而具有足够可控性;同时要足够灵活,在一个相当泛化的框架基础上再做特化,即应用层agents.另一方面,我非常希望未来的agents能一次性帮我调研一个学术方向,它自己去收集文献阅读文献,回头再在对话里教给我.这就要求agents具有长时间针对一个主题做深入调研的能力,这种任务需要大量的计算时间,用眼下的商业LLM显然不合适,因而这就要求有一个充分强大的开源LLM做保障,当然这还没有.但我觉得,眼下这个时代,构建任何软件系统都要面向未来,我们的时间轴在加速,你无法预期明天是不是就会出来一个开源的GPT4级别LLM,而你知道,这早晚会发生.所以,AIlice就这样被做出来了.她尚且无法帮我调研一个学术方向,但能针对一个主题做长时间的探索了,而且具有极佳的编程/脚本执行能力,在自我扩展做上后,我想,是时候让她出来面对世界了.
接下来我还会加上多模态支持,届时AIlice的基本框架就差不多完成了,代码量会控制在3k行之内.
我们来罗列一些典型的用例.我经常用这些例子来对开发中的AIlice做测试,确保性能的稳定.但即便如此,执行结果仍然和你选择的模型,所用的代码版本,甚至是测试时间有关(chatgpt疑似在高负荷下会降智,当然开源模型没这个问题,它们的智力是一直低).另外,AIlice是一个基于多智能体对话协作的Agent,这里作为用户的你,也是智能体之一(不承认?),所以,当它需要额外信息时,会来询问你,你充分详细的信息是她成功的关键.另外,当任务执行的不尽人意,你也可以告诉她正确的做法,她会回头纠正的.
最后一个需要注意的地方是,AIlice目前没有运行时间控制机制,因此她可能陷入死循环或长期运行,在使用商业LLM运行时你需要关注她的运行.
我们来简单解释一下几条基本原则.从最显而易见的层面说,高度动态的提示构建会让agent更不容易陷入死循环,来自外界的各种新变量会不断冲击LLM使其避免进入那个坑. 进一步说,每次推理中,向LLM充分输入当前可用的信息可以对它的输出产生巨大的改善,比如在自动编程里,来自解释器或命令行的错误信息会辅助LLM不断修改代码最终达到正确结果.最后,动态的提示构建中,提示中的新信息还可能来自其他Agent,这起到了一种串联推理计算的作用,使得系统的计算机制更复杂,变化万端,产生更丰富的行为.
分离计算任务,从现实的角度说,是因为我们只有有限的context window.我们无法预期在一个几千token的window里完成一项复杂任务.这时,如果能把复杂任务分解,每个子任务都在有限的资源内解决,就是美好的结局了.传统的计算模式里,我们一直在利用这一点,但在LLM为核心的新计算里,这一点不易实现,最简单的问题就是,如果一个子任务失败了,整个任务都有失败的风险.递归则更难实现:你怎么确保在每次调用里LLM都解决了一部分子问题,而不是把包袱都丢给下一级调用呢?我们在AIlice里用IACT架构解决了第一个问题,第二个问题原则上也不难解决,但估计需要更聪明的LLM.
第三个原则是目前大家都在做的事情,让多智能体交互合作完成更复杂的任务.这条原则的实现事实上解决了前面所说的子任务失败问题.多智能体协作对于agents运行中的容错性至关重要,事实上这可能是新计算范式和传统计算的最大区别之一:传统计算精确无误,仅通过单向发起交流(函数调用)来分配子任务,而新计算范式里错误百出,需要计算单元之间双向的交流来纠错.这会在下文关于IACT框架的介绍里详细说明.
通过基本计算单元(其实就是各种功能的agents,类比于传统计算中的函数)的层层调用来将计算任务分而治之.
所以,用户输入的文本指令被作为一种程序而执行,分解成各种子程序而由不同的agents分别解决,这就是AIlice的基本架构.接下来我们详细解释这里的基本计算单元到底是什么样的.
一个自然的想法是通过LLM与外部调用者和外围模块进行多轮对话,在基本计算单元中解决特定问题(如信息检索、文档理解等)。我们临时将这个计算单元称为“函数”。然后,类比传统计算,我们允许函数互相调用,并最终添加线程的概念来实现多代理交互。然而,我们可以拥有比这更简单和更优雅的计算模型。
关键在于封装LLM推理的“函数”实际上可以被调用和返回多次。一个具有coder功能的“函数”在编程过程中遇到不清晰的需求时,可以暂停工作并向其调用者返回查询语句。如果调用者对答案仍然不清楚,它将继续询问下一个更高级别的调用者。这个过程甚至可以一直延伸到最终用户的聊天窗口。当添加新信息时,调用者将通过传入补充信息重新激活coder的执行过程。可以看出,这个“函数”不是传统的函数,而是一个可以被多次调用的对象。LLM的高智能性使得这种有趣的特性成为可能。您也可以将其看作由调用关系串联在一起的agents,其中每个agent可以创建和调用更多的子agents,并且还可以与其调用者对话以获取补充信息或报告进度。在AIlice中,我们将这个计算单元称为“AProcessor”。其代码位于core/AProcessor.py中(其实它就是我们说的agent)。
由于我对学术文献的无知(看在上帝的份上,我都要做个AI来帮自己读文献了),尚不知晓是否有人做过这种东西,所以就给它起了一个名字: IACT. 这个架构在实践里表现很不错,agents之间的调用机制确保了agent可以把任务动态的分解成子任务去让更动agents解决, 而调用者和被调者间的对话机制使得计算有了超强的容错性:子agent失败了,它的调用者会去提醒他;调用者提供的任务描述有问题,被调者会反馈问题给调用者. 甚至它还缓解了context window资源受限问题:一个agent失忆了,但别的agent还记得,就可以提醒他.基本计算单元:LLM和解释器的太极图
原谅我最终还是止不住痒用了这个民间科学爱好者最喜欢的词汇.但考虑到胡渊鸣大佬也用了并且感觉很好,我想那没事了.
接下来,我们将详细介绍AProcessor内部的结构。AProcessor内部是一个多轮对话。定义AProcessor功能的“程序”是一个提示生成机制,它从对话历史中生成每一轮对话的提示。对话是一对多的。在外部调用者输入请求后,LLM将与外围模块进行多轮对话,LLM以各种语法形式输出函数调用,系统调用外围模块生成结果并将结果放入回复消息中。最终,LLM得到答案并回应外部调用者,结束此次调用。但由于对话历史仍然保留,调用者可以再次调用以继续执行更多任务。
我们要介绍的最后一部分是LLM输出的解析模块。实际上,我们将LLM的输出文本视为半自然语言和半形式语言的“脚本”,并使用简单的解释器来执行它。我们可以使用正则表达式来表示精心设计的语法结构,将其解析为函数调用并执行。在这种设计下,我们可以设计更灵活的函数调用语法形式,比如具有某个固定标题(如“UPDATE MEMORY: .....”)的部分,也可以直接解析出并触发执行一个动作。这种隐式函数调用无需让LLM意识到其存在,只需让其严格遵循某种格式约定即可。对于最geek的可能性,我们留有余地。这里的解释器不仅可以使用正则表达式进行模式匹配,其Eval函数还是递归的,因而可以用于真正复杂的语法解析和动作执行。我们不知道这将用于什么,但留下一个酷炫的可能性似乎也不错,对吧?因此,在AProcessor内部,计算由LLM和解释器交替完成,它们的输出是彼此的输入,形成一个循环。这就是为啥本节标题叫这玩意儿的原因.
目前AIlice要有正常的表现,仍然需要gpt4级别的模型.不过前文也说了,这不是我们担心的问题.agents所需要的LLM并不需要海量的知识储备,而只需要常识和强大的reasoning能力.我乐观的预期在不远的将来我们会有一个这样的轻量级LLM出现(事实上,眼下的Open-Orca/Mistral-7B-OpenOrca就是一个命令理解能力极好的开源小模型,这也是我这种乐观的支柱之一).
AIlice的编程能力不错,在用户参与下可以轻松完成形形色色的编程小任务.将来的进化方向是通过更好的长短期记忆机制和针对大工程的经验注入,实现大软件工程的实现能力.我想,基于IACT这不是太难的事情.
调查功能还比较初级,只能针对一些简单主题,通过大量网络搜索和页面浏览收集信息整理出结果,还不能做到调研一个领域这种夸张的事情. 这个我最早想要的功能目前发展滞后.我们需要一种对大量文本形成一致理解的机制(在long context window LLM这种方案之外),我们总不能把所有的东西都放入context,也没有那么大显存去做这件事.这方面将来会继续想办法.