原创 孔某人 2025-01-17 15:26 北京
反思LLM应用的框架与中间件设计,以Memory场景为例
本文说的框架是指LLM应用的策略架构,有些需求有独立的中间件方案,他们在设计上面对的需求是类似的。
1、问题是什么?
本文讨论的是模型层之上的方案设计,能够选择的方案是各种微调类方案和上层的workflow和agent方案设计。
在应用层来说,即使忽略开发成本,整个方案也经常要在几个方面进行平衡:结果符合期望率(质量)、延迟、推理成本。单纯的堆方案的复杂度,可以提高符合期望率,但会增加推理成本,并经常会增加延迟。
现有的框架方案基本上解决的是baseline质量方案的开发成本问题,每个方案再加上模型选择基本上大致锁定了结果符合期望率。然后就总能听到一些声音说:如何优化延迟?如何优化推理成本?但大多数时候基本上得不到什么符合提问者期望的回答。其实“提这种问题”就是“这个提问者在LLM应用层技术上不够专业”的一个强特征。
为什么我们现在看不到一个全面的框架?为什么这些框架很难去做成本或延迟优化呢?
1.1、特化
为什么我们现在看不到一个全面的框架?
在我看来,Python+各种开源库本身就是一个框架,而且它还不错。只是它不太符合很多试图寻找框架的人的希望,究其原因,应该还是希望寻找“更加低开发成本”、“能够帮他们解决一些他们自己也没想明白的问题”的方案。
这两点其实还是能够被满足的,就是我们现在看到的这些中间件方案,但问题是:它们不通用。每个领域大概都需要一些不同的方案,就好像你不可能拿一个电商系统去做公司ERP一样。现在的各种中间件产品有它们适合的场景,但我们还需要很多其他场景的方案,而不是削足适履,把别的领域的需求硬套在这些框架上。也许我们除了Dify这样的产品外,还需要至少7种不同的中间件框架来满足各种各样的场景。
1.2、反封装性
为什么这些框架很难去做成本或延迟优化呢?
归根结底,优化成本或者延迟是一个反封装的任务,每个局部做的不够好,所以才有优化的空间。这可以推广到所有跨组件都存在的成本性metric上。
但并不是因为每个组件本身实现的不够好,很多时候是在一个具体的场景下,还有更好的实现方案,但在另外的场景下则可能是完全不同的选择。
理论上可以把所有可以优化的地方都实现并放在接口中,让调用方根据场景来榨取最后的一点成本和延迟优化空间,但这会导致每个局部组件的封装复杂度爆炸。某种意义上这几乎等同于直接使用Python+各种库的复杂度。
只有说一个设计既能够屏蔽内部的复杂度,又在各种场景下表现都足够好,不会让人想要去介入其中来优化各种地方,此时才是一个优秀而又普适的解耦设计。
2、以Memory为例
在本文写作时,我正好有了一个不太老生常谈的素材来讨论这方面的问题,就是LLM应用中的Memory。所以正好一起来谈下。我之前从模型层的角度讨论了Memory,具体见 展望LLM应用的Memory技术发展 。本节则主要从应用层的角度进行讨论。
还得再强调一下:并没有一种具体的技术叫做LLM应用的Memory,它跟Agent一样是一类很模糊很模糊的概念,某种意义上它描述的是一种理想,而不是某种已经达到60分的技术方案。
2.1、已有的Memory组件方案
根据我的粗略调研,目前Memory方案可以分为大致以下几种:
完全的非结构化,直接把所有信息放入llm的context
最基本的按条记忆,具体存储结构类似List[String],类似于ChatGPT的memory
Graph Memory,主要用于存储图结构的信息
基于Struct的半结构化记忆,结构类似Map[KeyString, Union[String, Struct]],主要用于有明确业务语义字段的场景
按我的命名标准,后三种我都称为半结构化的Memory,差别是它们的结构化程度不同。之所以不叫(完全)结构化,是因为它们中仍然保留了可以填入任何语义内容的String,这部分信息并没有被结构化。
所有的半结构化Memory方案都需要为其设计的Memory读写方案,一般是一个较短的LLM workflow。
为什么不选择最简单的完全非结构化,是因为:
目前的long context成本还较高,重复计算成本也较高
目前LLM对long context的处理能力还有限,包括硬性的context token上限,以及long context下的效果下降。
所以后续所有的Memory方案都是在如何丢弃信息,并对信息进行标准化的方式上进行优化。
2.2、列表性Memory
列表性的Memory是最简单的半结构化Memory方案,它相对于后续两种方式也普适各种场景。它说白了就是从历史信息中,提取出需要记忆的部分,并在需要的时候进行选择性召回。这就是我们在ChatGPT memory上看到的。
但这种方式只适合事实记忆性的场景,对于可能变化的信息,就开始力不从心。所以现在的列表性Memory方案开始支持时间相关的结构化信息,例如memory创建时间、过期时间等等。这些时间方面的信息都是基本结构化的,并会在Memory读写策略中进行使用。
但列表性Memory在识别同类信息和同类信息的不同时间版本存在困难,这需要对于memory内容语义的理解,而这恰恰是列表性memory没有处理的。所以当更新一个旧的memory条目,或者是更新了旧memory条目中的部分信息时麻烦就开始显现。
列表性Memory还有个问题是,对于业务逻辑中需要区分的具体业务字段不能直接处理。只能对全表使用LLM进行扫描,或者在一些预计算的Index帮助下进行剪枝。
2.3、基于Struct的半结构化Memory
基于Struct的半结构化Memory则是针对于业务字段的具体需求进行特化,在memory写入时就将信息投影到预先设定的key上。这样在查询时只需要从对应的key进行检索,或者对于key进行语义检索,而不必对于整个Memory库进行遍历。
这种方式似乎更符合传统软件开发的思路,但问题是它就不需要时间信息了么?以及语义的匹配能够做到唯一么?
从完备的角度上来说,Struct中的每个Value也需要记录相关的时间信息,对于改写、有效时间等进行标记。以及同一个Value下可能会关联多条信息,将它们单纯的拼合为一个String并不是一个完美的设计。所以每个Value自己大概需要是一个列表性Memory才更普适一些。但方案复杂度就上升了。
这还没完,对于语义不能唯一匹配到一个key的情况,树结构的逻辑结构可能并不适合,也许我们需要一种基于Tag标签的逻辑结构进行关联,同一个memory可以以不同强度关联到多个位置上。这时候这个Struct结构实际上就变成了一个分级标签树。进一步来说,它是【一个】对于memory库的索引,对于同一个memory库可以有多个不同的索引。
还要在吹毛求疵的话,我们还需要一个独立的others列表memory来关联无法与已有key匹配的内容。实际上当我们踏入Struct半结构化的时候,就注定会扩展到所有可以被使用的数据结构上,它与列表性memory并不是互斥的。
考虑到实际应用场景,还有一个需求是在系统生命周期中,对于新key的不断挖掘,以及与系统逻辑的共同迭代。只要业务逻辑上可以更好地处理某个特征,且系统的边际迭代收益超过成本,那么系统就会不断扩展。
某种意义上这就像是一个传统的数据库结构,只是读写、挖掘和部分字段都是语义的,这就像是一个LLM时代的智能关系数据库。
2.4、Memory框架的使用成本
上述的提到的功能想做都可以做,但问题是,这里大量的语义操作都需要依赖于LLM,而且还不能是太差的LLM。
一般来说,Memory的写操作对于结果符合期望率的要求是相对于业务流程本身的一个环节来说更高的。单次请求犯错了只会影响一次请求,但如果Memory的写操作犯错了,那么会影响后续对于该memory信息的所有操作。
这几乎意味着我们需要在memory写操作上使用不差于业务流程本身基线LLM模型的模型。甚至由于Memory框架的设计而可能需要在一次memory操作中调用多次LLM。而这都导致:Memory框架的推理成本变高,延迟增加。
而现阶段的很多LLM应用的推理成本和延迟成本都是其要优化的主要成本,所以这就更导致使用一个通用的Memory框架变得很鸡肋。
如果我们能够去为每次LLM调用或者每个基本操作的workflow去微调一个小模型的话,是有可能解决这些问题的。但合成数据的成本和开发成本都是较高的,而且还是跟目标场景的数据相关的,这都使得其开发成本无法被分摊。
2.5、Memory组件的本质功能
站在框架使用者的角度,对于Memory组件的功能期望其实也是很复杂的。虽然使用者觉得都是记忆功能,但这些需求实际上可能是:
通过示例学习新能力,举一反三。
从事实中推理出相关信息,例如:用户说他的学生如何如何,那么他可能是一个老师等等。
这些实现方和使用方对于memory理解的差异目测还会存在很长一段时间。
从实现来说,半结构化的Memory本质上只有存储结构、读写策略,两部分。在理想情况下,存储结构可以由框架使用者进行配置,这比较类似于目前基于Struct的半结构化的思路。但一个问题是,读写策略到底是不是业务层的业务逻辑的一部分?
不少人会直觉认为读写策略应该是通用的,不是业务逻辑的一部分。但这只有在它的方案很完美的时候才成立,即:准确率很高,成本很低,就像我们使用一个编程语言内嵌的字典类型一样。很明显,现在的Memory框架还不满足这点,或者说是目前的LLM还做不到这点。
现在的Memory组件的内部折中对于应用的总体效果影响很大,而且很明显Memory组件的开发方做不出一个通用的方案来自动适配所有情况。所以目前Memory相关的语义操作应该被合并到业务层一起进行联合优化设计,这也是目前大多数LLM应用的方案所趋向的。
看起来,Memory组件也是需要按场景进行特化的,同场景内可以复用。在LLM能力和成本大幅改进之前,可能我们还看不到通用方案普适各种场景的可能性。这也佐证了我上文的观点:下一个Memory方面的突破可能来自于模型层能力的提升,只依靠模型的提升能够更scale的解决这些问题。
2.6、其他需求
把视角从Memory中抽离出来,其他方面的需求看起来似乎也适用于上述的结论。我们可以做出一些通用性的组件,但它们的使用成本较高。在具体场景会出现一些针对性的设计,其成本明显更优。
3、解耦的设计
虽然上述分析看起来很悲观,但并不是完全没有可以通用的方案。
一个优秀的通用中间件方案需要满足:在符合期望率、推理成本、延迟等多方面,在足够多的场景下都能做到帕累托最优。至少要让大部分使用者不会去想如何进入内部进行定制优化的程度,就像大部分开发者不会去想要自己定制一个CPU、定制一个操作系统一样。
3.1、智能语义函数
而似乎有一种设计符合上述要求,就是我之前提过的“智能函数”。叫做语义函数,是为了与前LLM时代的AutoML进行区分。
智能函数这个叫法不是站在实现方的角度,而是从需求方的角度来说的。需求方希望有某种实现方案,能够执行一些语义操作,并维持其符合期望率、推理成本和延迟等都最优。这个智能函数应该是可以自学习的,即在系统执行的过程中,根据处理的数据和后续反馈来优化其效果和成本。
这种方案是可以实现的,单个智能函数的功能控制在目前prompt和微调能实现的范围内,调用方在持续使用过程中反馈一些标注结果来指导智能函数内部的优化。
在这个场景下,任务的难度是确定的,完全由初始配置和后续提供的数据就能全部反映。而大多数时候调用方并不关注其内部实现方案的选择,只要其成本和符合期望率是帕累托最优的即可。只有希望从这个系统中提取一些有价值的中间特征,或跟其他组件共同优化的时候,才会涉及到对其内部方案的干涉,而这类需求占比不高。
这个抽象提供的价值并不是单纯的自动微调或者自动prompt优化,而是基于已有信息的最佳方案选择、自动构建和自动评估和切换的能力。
假设已经有了一个这样的智能函数API,那么通用的Memory框架似乎就成为了可能,每个语义操作都可以在保证符合结果期望率的情况下自动优化成本和延迟。只不过这时候各种Memory框架自己已经变得太薄,更多成为一种设计模式,可以被加入到各种应用的框架设计中。
3.2、其他
还有其他候选么?目前我也没有想到,读者有兴趣可以思考和讨论这个问题。
交流与合作
如果希望和我交流讨论,或参与相关的讨论群,或者建立合作,请私信联系,获取联系方式请点击 -> 联系方式。
本文于2025.1.17首发于微信公众号和知乎,知乎链接:
https://zhuanlan.zhihu.com/p/18902715026