2.1 RAG:知识,记忆与技能库
2.1.1 RAG简介
RAG的核心思想就是将传统的语言生成模型(如GPT系列)与一个检索系统相结合,在处理一个输入时,RAG首先使用检索系统从一个大规模的文档集合中检索出相关的文本片段,然后将这些检索到的文本作为额外的上下文信息输入到生成模型中,以此来生成更加丰富和准确的输出。说白了就是给LLM一个字典让它查。
RAG往往是结合向量数据库,但是对于简单应用而言,只需要JSON格式文件就能实现,甚至能轻易指向其他格式文件(如图像)。注意:这里的embedding
是text embedding
,即句向量,需要和词向量分开。句向量是将整个句子映射到向量空间,而计算句子语义相似度最常见的方式则是计算其text embedding
之间的余弦相似度。而这种语义相似度除了可以进行文本检索以外,也可也用于识别用户意图,进行情感分析等等。
2.1.2 聊聊《Generative Agents》的检索函数
RAG的检索函数不一定只算语义相似度,可以有其他实现形式。比如《Generative Agents》里记忆流的检索函数。其检索函数的要素有三点,即[时效性,重要性,相关性]
。时效性代表记忆最近被检索出的时间,重要性表明Agents对该记忆事件的重要性评估(一般是结合人设让LLM给出个分数,或许也可以用一个小模型做评分),相关性就是语义相似度。
检索记忆时,不再是按照语义相似度进行排序。而是在对三个特征进行最小最大归一化之后,计算一个综合评分,然后取Top k
:
Xnorm=(X−Xmin)/(Xmax−Xmin)
score=a∗recency+b∗importance+c∗relevance
详见原文:
arxiv.org/pdf/2304.03442.pdfarxiv.org/pdf/2304.03442.pdf
2.1.3 RAG的语义孤立问题
RAG同样会出现一些问题。除了chunk本身就会丢失一些上下文以外(可以用交叠上下文来缓解),chunk的语义本身也是“孤立”的。这里是指:一段chunk中的文本可能需要chunk外的内容来帮助理解。例如有三个chunk分别为:
胡桃和宵宫是好朋友。
宵宫是“长野原烟花店”的现任店主。
胡桃是璃月“往生堂”第七十七代堂主。
对于一个通用LLM而言,如果它拿到的最相似匹配是“胡桃和宵宫是好朋友”,那其实LLM并不知道胡桃和宵宫是谁,并不一定能答好这个问题。这是因为:要理解一个知识,就需要理解其上层知识。在这个例子里,即“胡桃和宵宫到底是谁”。
针对于这个问题,向大家介绍一个比较简单的,面向原生JSON格式文件的优化方法,类似于分层检索:通过将知识库分层,首先在较高层次上对用户的查询进行理解和定位,然后再在更具体的层次上检索细节信息。这样一来,每次RAG时会拿到多级的语义,既有宏观认识也有最匹配用户Query的内容,或许能改善RAG效果。
对于上面的例子,其两级组织格式就应该是:
顶层知识:宵宫是“长野原烟花店”的现任店主。胡桃是璃月“往生堂”第七十七代堂主。
↓指向
子知识:胡桃和宵宫是好朋友。
如此一来,每次检索到最相似的匹配是“胡桃和宵宫是好朋友。”时,就必然会附带上对胡桃和宵宫的简介。需要指出的是,由于没有向量数据库等高效存储检索系统,这并不是一种生产环境适用的高效方法。只适用于小型项目,如独立游戏。
2.1.4 另一种方法
我之前也有看到过另外一种做法:即按照语义去切分段落。合并语义上相关联的段落,然后对段落生成一个描述句,以该描述句来生成嵌入向量。即以段落描述句的嵌入向量为键,段落的完整内容为值。我们可以简单对比一下这两种方法:
分层检索通过建立层级结构,使得检索能够从大范围的上下文逐步深入到具体细节,保持了信息之间的关联性,但可能会因为结构的复杂性和维护难度而影响检索效率。语义切分合并则侧重于将语义相似的内容动态组织在一起,通过为每个内容块生成描述性的句子来创建索引,这样可以提高检索效率,但有时可能会因为合并不当而丢失关键上下文。
实际上,这两种方法可以结合起来使用。例如,可以先通过分层检索方法构建一个宏观的知识框架,然后在具体的层级内使用语义切分合并方法来进一步细化信息组织。这样,既保留了层级结构带来的上下文关联优势,又能够利用语义相似度进行高效检索。
2.2 CoT:问题分解与推理
2.2.1 CoT简介与两个例子
CoT让LLM将一个复杂问题分解为级联的子问题,并依次进行顺序处理,可以显著提升LLM处理较复杂的性能。CoT既可以用在单次内容生成里,也可用在一条内容生成的Pipeline里。一般的例子的是用CoT做数学题,但大家可能看腻了:
我这里倒是想给出一个基于想法进行风格化回复的例子,其广义上也是CoT的思路,并且很好理解,希望能让大家耳目一新。
直接让LLM做角色扮演对话会出现一些问题:例如,LLM喜欢抽取Prompt的要素而非好好说话;或是不怎么利用Prompt的要素而说一些无关紧要的话;又或是LLM干脆不知所措,说一些莫名其妙的话。我认为问题在于:LLM是需要“思考”后才能好好说话的,角色扮演(风格化对话)对于LLM其实是一个复杂问题。
因而可以将CoT的思想应用于风格化对话生成任务中,把对话生成分解为两个步骤:首先生成角色的内心思考内容,然后基于这些内容构建角色的回复。这样可以确保对话既符合角色的人设,又能够自然地融入到对话的流程中,经简单的主观测试,回复效果的确有提升。请大家重点关注下图左侧的生成部分(红框圈起来的部分):
具体的Prompt是这样组织的,应该比较好理解。请重点关注两段Prompt之间是如何联动的,即第一段生成的结果如何被嵌入到第二段Prompt当中:
thought_prompt = f"""
角色名称:{self.name}
初始记忆:{self.seed_memory}
当前心情:{self.mood}
任务:根据角色当前的相关记忆,相关知识,对话上下文进行分析,基于角色第一视角进行思考,给出角色的心理反应对和相关事件的判断。
字数限制:不超过100字。
<<<
相关记忆:“{memory['description']}”
相关知识:“{knowledge_text}”
对话上下文:
{self.language_style}
{context}
>>>
请仅返回{self.name}第一人称视角下的思考内容,不要添加额外信息或格式。
"""
response_prompt = f"""
角色名称:{self.name}
初始记忆:{self.seed_memory}
当前心情:{self.fsm.mood}
任务:基于角色的思考内容和对话上下文进行回复。
字数限制:不超过100字。
<<<
思考内容:“{thought}”
对话上下文:
{self.language_style}
{context}
>>>
请在思考内容和对话上下文的基础上,以{self.name}的身份回复。不要扮演其他角色或添加额外信息,不要添加其他格式。
"""
CoT相关内容详见原文:
Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
2.2.2 CoT变种
CoT的两个经典变种是ToT和Self-consistency。ToT是CoT的一个扩展,它不仅仅以线性的方式构建思维链,而是创建一个更为复杂的树形结构来进行推理。Self-consistency是另一种变种,强调在推理过程中保持一致性,会对语言模型进行多次采样,生成多个推理路径。然后再对不同推理路径的生成结果,基于投票选择最一致的答案输出。关于LM采样的概念,可以学习一下贪心解码,束搜索,Top-k ,Temperature等。
2.3 意图识别与执行
2.3.1 函数调用的例子
意图识别与执行是指让LLM基于上下文从一组备选项中选择合适的类别,根据需要填入相应的参数,并进行格式化输出的能力。
意图识别与执行包括工具使用,行为状态切换,理解环境反馈等(环境反馈的一个例子是:LLM生成的代码运行失败,Agent根据代码解释器返回的相关错误信息进行修改)。
函数调用是工具使用的一个典型例子。以OpenAI对话接口的函数调用功能Function Calling
为例。Function Calling
的工作方式是:首先,我们提供一个工具函数或外部API的接口描述,包括其用途和参数;然后,根据用户的查询(Query),让LLM生成一个格式化的函数调用。假设一个函数描述大概长这个样子,其描述了一个用于检索wiki百科的函数接口:
{
"name": "use_wiki",
"description": "检索wiki百科以补充不了解的知识",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "需要检索的事物"
}
},
"required": ["query"]
}
}
用户只需要在上下文中描述一个具体任务,如果LLM认为需要调用相关函数,就有可能会触发函数调用功能。在这个例子中,LLM会解析用户的查询,并生成一个格式化的调用请求,例如use_wiki(query="特定主题的关键词")
。
2.3.2 状态切换的例子
意图识别与执行不止是函数调用。例如:在上下文中给定LLM一些情绪种类描述和情绪程度说明,要求LLM分析一段用户输入的情绪种类和情绪程度数值,并格式化地返回。又例如:要求LLM分析上下文中给出的一些状态和场景,并要求LLM根据场景决定其中的角色应该如何行动。这一类问题也算是意图识别与执行,这种能力往往作为某种中间步骤被使用。其准确数学模型应该是马尔可夫决策过程(MDP)。
意图识别与执行是构建LLM Agent时的一个重要能力,因为它允许模型进行决策并生成可操作的输出,这也是建立其数据通路与行动框架的基础。
这里给出一个最简单的用Prompt切换状态的例子,仅作示范:
任务:推理角色的下一个心情应该是什么。
<<<
角色当前心情:{self.mood}
观察到的事件:{trigger}
角色的想法:{thought}
可能的心情列表:{self.mood_list}
>>>
现在请根据角色目前的心情,观察到的事件和角色想法,从可能的心情列表中选择一个心情。例如:{self.mood_list[0]}。
心情可以是不变的。精确地输出心情名称,不要进行额外的输出。
2.4 数据通路与行动框架
数据通路通常指的是在Agent内部信息流动的机制,包括感知环境的输入、处理这些输入的中间步骤,以及生成输出或行动的最终步骤。行动框架则指的是Agent决策的算法和策略,它定义了Agent如何根据输入的数据和内部状态来选择行动。
两者共同定义了Agent到底以什么规则在对应的环境下行动。这里给出一些经典的框架图供参考:
Conceptual framework of LLM-based agent with three components: brain, perception and action.
generative agent architecture.
举个具体例子。《Voyager:LLM 驱动的具身终身学习智能体》用LLM来自我掌握技能和发现新事物,该智能体的数据通路与行动框架可以概括为以下几个构件:
- 自动课程(Automatic Curriculum):为智能体提供一系列逐步增加难度的任务,鼓励多样化行为和施加约束的指令,以促使其不断学习和进步。
- 迭代提示机制(Iterative Prompting Mechanism):用于引导智能体迭代和提升自身的技能,并引入环境反馈,程序解释器的执行错误,自我验证等。
- 技能库(Skill Library):用于存储智能体掌握的各种技能。通过存储成功解决任务的行动程序来逐渐构建技能库。
Agent会试图解决由自动课程提出的越来越难的任务,不断地生成代码和接受反馈,自我纠错和自我验证。直到自我验证模块确认任务完成后,就把生成的代码技能添加到技能库。后续就可以使用技能库里的相关技能来辅助完成相关新的任务。
总结一下,其完成“自我进化”的行动要素就是三点:
- 保持Agent持续活动,不断地接受任务要求,进行学习尝试,进行自我验证。
- 自我验证成功后,把学习成果添加到知识库中。
- Agent后续的活动可以以某种方法,使用之前添加到知识库的知识。
仅以此作为例子描述什么是数据通路与行动框架,详见原文:
CharacterEval: A Chinese Benchmark for Role-Playing Conversational Agent Evaluation
2.5 SFT简介:参数级别支撑
SFT即监督微调(Supervised Fine-Tuning)。SFT的一般目的是强化通用LLM在某个垂直任务上的能力,但也会出现“灾难性遗忘”这样的问题。
全参微调非常吃显存和计算资源,因而后续出现了各种各样的低资源微调方法。全参微调、Prompt Tuning、LoRA、P-Tuning等都在SFT这个范围里面。
最简单的SFT就是针对{prompt,response}
的单轮问答pair
进行微调,其数据格式组织如:
{
"prompt": "<prompt text>",
"response": "<response text>"
}
SFT和RAG往往是被二选一的,也常被横向比较各自的优劣。但如果用对齐的RAG格式问答对去微调LLM,或许也是可行的。即用微调增强LLM通过{检索内容+上下文}来生成内容的能力。
2.6 多模态简介
多模态LLMs不仅能够理解和生成文本,还能够理解和生成与其他模态相关的信息,如图像。以GPT-4V为例: