1. 带有记忆的会话
1.1. 查询会话历史记录
在利用大模型自身能力进行对话与解答时,最好对用户当前会话的历史记录进行还原,大模型能够更好地联系上下文进行解答。
在langchain chat chat的chat函数中,通过实现langchain框架提供的ChatMemory。就可以建立一个对话记录的缓冲区,随后读取历史会话记录到缓冲区,在对话时作为memory参数传入。
memory = ConversationBufferDBMemory(conversation_id=conversation_id,
llm=model,
message_limit=history_len)
chain = LLMChain(prompt=chat_prompt, llm=model, memory=memory)
而这个history_len,就会在Buffer初始化时,负责查找id为conversation_id最近的history_len的历史会话记录:
例如,如果history_len = 4,那么就查询前四条记录。
1.2. 与大模型进行对话
我将与大模型对话的过程封装起来,作为一个能够使用http协议进行访问的接口。
async def request_llm_chat(ca: LLMChat) -> dict:
"""
生成llm对话请求
包含参数有:
1. query
2. conv_id
3. history_len
4. model_name
5. temperature
6. prompt_name
"""
request_body = {
"query": ca.query,
"conversation_id": ca.conv_id,
"history_len": CHAT_ARGS["history_len"],
"model_name": CHAT_ARGS["llm_models"][0],
"temperature": CHAT_ARGS["temperature"],
"prompt_name": ca.prompt_name
}
return await request(url=CHAT_ARGS["url"], request_body=request_body, prefix="data: ")
这里的参数解释一下:
-
query:用户询问大模型的内容
-
conv_id:会话的id
-
history_len:前向检索的历史会话记录
-
model_name:请求大模型的名字(因为可以同时部署多个大模型进行对话)
-
temperature:LLM采样温度,用于控制文本生成的随机性。这个随机性不宜过高,过高大模型会随性发挥导致回答不准确;也不宜过低,过低大模型不敢做出回答。
-
prompt_name:使用的prompt的模板,例如,with_history的模板如下:
"with_history": 'The following is a friendly conversation between a human and an AI. ' 'The AI is talkative and provides lots of specific details from its context. ' 'If the AI does not know the answer to a question, it truthfully says it does not know.\\n\\n' 'Current conversation:\\n' '{history}\\n' 'Human: {input}\\n' 'AI:',
提示AI,以下是AI之前与人类的对话记录,现在人类又给了一个输入,AI需要根据历史记录,对这个输入进行回答。
1.3. 记忆功能测试
现在我们来看一下这个大模型的记忆如何,这里我简单做了一个“你能记住我的名字吗”的小测试:
正例测试
先告诉大模型:“我的名字是xxx”
再紧接着在同一个conv_id的会话下问他:“我叫什么名字?”
他记住了我的名字。
反例测试
在看完一个正例后,我们再看一个反例:
现在我换了一个新的conv_id(相当于新创建一个会话),再问他我的名字,他就不知道了,说:“请告诉我你的名字。”
2. 与知识库对话
除了利用大模型本身的能力,我们还可以利用向量知识库检索相关的知识,再生成对应的回答。这和大模型依靠自身能力比对起来,优点是回答更加有依据、更加权威,缺点是如果检索不到相关的知识,那么几乎无法回答问题。
2.1. RAG技术的具体应用场景
检索知识库
首先我们开启一个异步操作,对相关知识库进行检索,返回匹配度最高的top k条相关资料。
docs = await run_in_threadpool(search_docs,
query=query,
knowledge_base_name=knowledge_base_name,
top_k=top_k,
score_threshold=score_threshold)
词向量嵌入
其中search_docs依托于不同向量数据库的检索方式,例如在faiss知识库中:
def do_search(self,
query: str,
top_k: int,
score_threshold: float = SCORE_THRESHOLD,
) -> List[Tuple[Document, float]]:
embed_func = EmbeddingsFunAdapter(self.embed_model)
embeddings = embed_func.embed_query(query)
with self.load_vector_store().acquire() as vs:
docs = vs.similarity_search_with_score_by_vector(embeddings, k=top_k, score_threshold=score_threshold)
return docs
我们先通过词嵌入模型(我们实验和部署时使用的词嵌入模型是bge-large-zh这个模型,由BAAI提供:BAAI/bge-large-zh · Hugging Face)将用户的输入嵌入为词向量,随后直接调用langchain封装好的函数检索top_k个相似度大于等于阈值的词向量。
随后我们把文档的内容拼接到上下文中:
context = "\\n".join([doc.page_content for doc in docs])
最终通过Langchain提供的与大模型对话的接口进行对话:
chain = LLMChain(prompt=chat_prompt, llm=model)
# Begin a task that runs in the background.
task = asyncio.create_task(wrap_done(
chain.acall({"context": context, "question": query}),
callback.done),
)
知识库问答需不需要带历史会话信息
经过我的实验,最好是不要带。因为优先级上历史会话信息会高于知识库检索出来的知识。用户之前的对话记录可能会影响到知识的客观性(也就是说大模型因为用户的对话舍弃了在回答中包含从知识库中检索出来的信息)。
2.2. Prompt工程
由于知识的客观性,我们需要让大模型尽可能客观地总结提炼知识,而非随意发挥:
"default":
'<指令>根据已知信息,简洁和专业的来回答问题。如果无法从中得到答案,请说 “根据已知信息无法回答该问题”,'
'不允许在答案中添加编造成分,答案请使用中文。 </指令>\\n'
'<已知信息>{{ context }}</已知信息>\\n'
'<问题>{{ question }}</问题>\\n',
例如此处建立的prompt模板就是要求大模型不得随意编造发挥,只能根据检索出来的知识进行回答。
而这个Prompt的模板格式,严格遵守了**(<指令>,(<问题>,<回答>))**的QA方式,其中<回答>是需要大模型给出的。
2.3. 知识库问答测试
存在相关文档
我们先来看一个能够检索得到相关文档/知识的例子:
可以看到大模型结合了知识库检索出来的所有相关知识进行了回答。
不存在相关文档
当然,知识库中可能不具有和用户输入比较相近的知识。但我们总不能不让大模型做出回复,所以此时我们只能转为依靠大模型自身能力作出解答这一模式:
3. 与搜索引擎进行对话
大模型也可以借助网络上能够检索到的一些信息进行解答。不过这里我的服务器代理还没配置好,查询搜索引擎时会出现连接超时的情况:
因此这一部分先放在这,等我把服务器配好再说。
4. 混合机制的对话
4.1. 为什么要把机制混合起来
经过微调的大模型的能力和训练时使用的语料/数据集的规模和质量显著相关。而知识库的知识也不能穷尽这个世界上所有有关易学的知识。搜索引擎检索到的内容也是鱼龙混杂,质量也可能不够高。这三个对话机制单拎出来,可能生成的回答质量都难以得到保证。
但这三种机制,恰好又是可以互为补充的:大模型首先自身要具备对专业性知识的理解和认知基础才能更好地运用知识库中检索出来的知识;知识库可以作为外接的知识来源提升大模型认知的深度和广度;而搜索引擎则能够更好地检索相关专业社区(专业的研究中心、论坛),为前二者提供更多补充。
因此我就想,能不能把这三种机制混合起来,让通过专业数据集进行训练的大模型在基于已有的认知基础上,把知识库和搜索引擎检索出来的知识系统地、有逻辑地整理成一个更加专业的回答?以此来显著提升回答的质量?
4.2. MTPE与CoT:渐增式文本生成
MTPE is short for Multi-Turn Prompt Engineering,这是LLM诞生后催生的一种提示工程技术(中文名可以翻译为多步提示工程)。他是基于CoT这一idea的技术,而后者是LLM领域内的一篇经典论文提出的:
Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
这篇有谷歌的AI团队发表的论文中首先提到了一个简单的示例:
- 在Standard Prompting技术中,大模型有时会给出错误的解答,例如23-20+6=27这种错误。
- 在CoT技术中,大模型会详细给出每个问题的推理以及计算过程,且正确率大大提升。
这是怎么做到的?
CoT:引导式文本生成
这篇论文解释了"思维链提示"(chain-of thought prompting)对LLM尤其是大型LLM的提升作用。
思维链(Chain of Thought)是一种提示工具,用于帮助语言模型进行复杂的推理和思考过程。它通过引导模型逐步解决问题,以一系列连贯的步骤展示推理的思路和逻辑关系。
思维链提示的基本思想是将推理过程分解为多个步骤,并在每个步骤中指导模型逐步进行推理。每个步骤都通过自然语言描述,使模型能够理解和执行每个推理阶段所需的操作。
具体而言,思维链提示通常由多个中间步骤组成,每个中间步骤都解释了问题的一个方面或子问题。模型需要根据前一个步骤的结果和当前问题的要求来推断下一个步骤。通过这种逐步推理的方式,模型可以逐渐获得更多信息,并在整个推理过程中累积正确的推断。
这里Google团队给出了一张图,在GSM8K标准的benchmark中,CoT技术的Sr(Solve Rate)提升至57%,远超微调的GPT3以及标准提示工程技术,略胜之前历史最佳的表现。
在进行思维链提示时,需要将思维过程、推理过程告知大模型,随后大模型生成解答:
例如,在StrategyQA类下的问题:是还是否:梨会沉到水里吗?
引导过程如下:
-
为了解决这个问题,我们需要明确一个物理上的公式:
$$ F_{buoyant} = \rho_{liquid} \cdot g \cdot V_{displaced} $$
-
当物体完全沉到水中时,有:
$$ F_{buoyant} = \rho_{liquid} \cdot g \cdot V_{item} $$
-
因为物体完全沉到水中,所以浮力小于等于重力,也就是说:
$$ F_{buoyant} ≤ G_{item} $$
-
我们知道物体的重力等于质量乘以重力加速度g,也就是:
$$ G_{item} = m_{item} \cdot g $$
-
不等式两边展开得到:
$$ \rho_{liquid} \cdot g \cdot V_{item} ≤ m_{item} \cdot g $$
-
g≥0,两边约去,又因为:
$$ m_{item} = \rho_{item} \cdot V_{item} $$
因此我们得到:
$$ \rho_{liquid} \cdot V_{item} ≤ \rho_{item} \cdot V_{item} $$
-
物体体积大于0,所以可以约去,最终我们得到:
$$ \rho_{liquid} ≤ \rho_{item} $$
则物体会沉于水中,反之不会。
-
现在请利用你的知识,获取梨和水的密度进行比较,根据第7.步的结论,给出回答
最终,大模型根据他的知识,得出梨会漂浮在水上的结论。并且他列出了获取梨的密度的过程以及与水的密度比较的过程。
MTPE:CoT的具体应用
多步提示工程(Multi-turn Prompt Engineering)是指在人机交互或人工智能系统中,设计和使用一系列连贯的提示(prompts)来引导用户或AI通过多个步骤完成任务或获取信息的过程。这种方法在自然语言处理(NLP)和对话系统设计中尤其重要,因为它可以帮助系统更有效地理解和响应复杂的用户需求。
在设计多步提示时,需要考虑以下几个方面:
- 上下文管理:系统需要能够理解和跟踪对话的上下文,以便提供相关的后续提示。
- 用户意图识别:系统需要准确识别用户的意图,并根据意图提供相应的提示。
- 灵活性和适应性:系统应该能够适应用户的不同回答,并灵活调整提示策略。
- 用户体验:设计提示时要考虑用户体验,确保对话流畅自然,避免让用户感到困惑或沮丧。
假设我们现在已经获得了大模型分别利用自身能力、知识库、搜索引擎给出的解答,现在我们就可以构造出一个Prompt模板,来进行多步提示:
"mix_chat": """
你是一个知无不言言无不尽的易学(周易)专家。先前我问了你一个易学方面的问题,你通过自身的能力、知识库检索以及搜索引擎检索的方式,给出了三个答案。\\n
现在请你将这三个答案系统地整理成一篇针对原始问题的中文回答。\\n
请你直接了当地给出回答,请一定不要添加任何诸如:\\n
'根据您的要求,我将这三个答案系统地整理成一篇针对原始问题的中文回答。'、'好的,以下是xxx的回答’、'非常抱歉,我刚刚没有理解您的问题。'、'根据您的对话和我所掌握的资料,我为您整理了以下回答'等\\n
这样与实际答案无关紧要的话!\\n
此外,给出回答时,你一定不要重复原始问题!请务必记住,任何与答案正文无关的文本,均不要出现在回答中。\\n
原始问题:\\n
{question}\\n
先前与你的对话如下:\\n
{history}\\n
你通过自身能力给出的解答如下:\\n
{answer1}\\n
你通过知识库检索给出的解答如下:\\n
{answer2}\\n
你通过搜索引擎检索给出的解答如下:\\n
{answer3}\\n
现在请你给出针对原始问题的中文回答。\\n
AI:
"""
- 首先,我们告知大模型他的认知基础:
你是一个知无不言言无不尽的易学(周易)专家。
- 其次,我们告知大模型上下文的metaData:
先前我问了你一个易学方面的问题,你通过自身的能力、知识库检索以及搜索引擎检索的方式,给出了三个答案。
- 随后,我们规定,大模型给出的回答中,不得出现令实际用户迷惑的语句:例如
'根据您的要求,我将这三个答案系统地整理成一篇针对原始问题的中文回答。'
,因为用户原始的问题并没有**“三个问题”**。 - 我们把之前大模型三种机制下给出的解答附在对话中:
原始问题:\\n {question}\\n 先前与你的对话如下:\\n {history}\\n 你通过自身能力给出的解答如下:\\n {answer1}\\n 你通过知识库检索给出的解答如下:\\n {answer2}\\n 你通过搜索引擎检索给出的解答如下:\\n {answer3}\\n
- 最终,要求大模型给出解答:
现在请你给出针对原始问题的中文回答。\\n
这就是一个MTPE在混合机制对话中的实际应用。
4.3. 接口实现
async def request_mix_chat(mc: MixChat) -> BaseResponse:
"""
混合对话
1. 先请求大模型以自身能力给出解答
2. 再请求大模型检索知识库给出解答
3. 最后请求大模型通过询问搜索引擎给出解答
4. 最终将解答融合在一起返回
"""
# 先请求大模型以自身能力给出解答
llm_response = await request_llm_chat(LLMChat(query=mc.query, conv_id=mc.conv_id))
if not llm_response["success"]:
return BaseResponse(code=200, message="大模型请求失败", data={"error": f'{llm_response["error"]}'})
# 请求知识库
kb_response = await request_knowledge_base_chat(KBChat(query=mc.query, conv_id=mc.conv_id))
if not kb_response["success"]:
return BaseResponse(code=200, message="知识库请求失败", data={"error": f'{kb_response["error"]}'})
# 请求搜索引擎
search_response = await request_search_engine_chat(SEChat(query=mc.query, conv_id=mc.conv_id))
if not search_response["success"]:
return BaseResponse(code=200, message="搜索引擎请求失败", data={"error": f'{search_response["error"]}'})
# 生成prompt
prompt = get_mix_chat_prompt(question=mc.query, history=await gen_history(mc.conv_id),
answer1=llm_response["data"]["text"], answer2=kb_response["data"]["answer"],
answer3="")
# 请求大模型
response = await request_llm_chat(LLMChat(query=prompt, conv_id=mc.conv_id, prompt_name=mc.prompt_name))
# 确保问题和回答原子性地入库
with record_lock:
add_record_to_conversation(mc.conv_id, mc.query, False)
add_record_to_conversation(mc.conv_id, response["data"]["text"], True)
return BaseResponse(code=200, message="混合对话请求成功", data={"answer": response["data"]["text"]})
- 首先,请求大模型以自身能力给出解答
- 请求大模型根据知识库的知识给出解答
- 请求大模型根据搜索引擎的检索结果给出解答
- 根据prompt模板生成多步prompt
- 请求大模型根据该上下文信息综合性地给出解答
- 返回该最终解答
4.4. 混合机制对话测试
这里我问了大模型一个周易中“逆向格义”这个概念相关示例的问题:
他给出的解答如下:
历史上有哪些在日常生活中运用逆向格义的例子?
逆向格义是一种思维方式,它强调从相反的角度来理解事物,从而得出更深刻的理解和解决问题的方法。在周易中,逆向格义被认为是一种有效的思维方式,可以帮助人们更全面地理解事物。以下是几个关于逆向格义的例子:
1. 卦象分析:在周易中,每个卦象都代表了一种特定的元素、方向和能量。通过逆向格义,我们可以从相反的角度来分析卦象,从而得出更全面的含义。例如,乾卦代表天、刚强、积极等,如果我们将乾卦的内涵 reversed,它就变成了坤卦,象征地、柔弱、消极。这种分析方法可以帮助我们更全面地理解卦象所代表的意义。
2. 爻辞解读:在周易的卦辞中,每一卦的爻(即阳爻和阴爻)都有特定的含义。通过逆向格义,我们可以从相反的角度来解读爻辞,从而得出更深刻的理解。例如,在一卦中,阳爻表示事物的发展,阴爻表示事物的衰退。如果我们将这个观念 reversed,那么阳爻就代表了事物的衰退,阴爻就代表了事物的发展。这种解读方法可以帮助我们更全面地理解爻辞所要传达的信息。
3. 人事决策:在周易预测中,我们常常需要根据卦象来判断事物的发展趋势和解决方案。通过逆向格义,我们可以从相反的角度来分析卦象所代表的意义,从而得出更全面的解决方案。例如,如果卦象显示事物发展趋势不利,那么我们可以考虑采取相反的行动,如增加投资、增加风险等,从而避免不利的结果。
4. 互传:在周易中,互传是一种特殊的卦象组合,它反映了事物之间的相互影响和变化。通过逆向格义,我们可以从互传中找到事物发展中的矛盾和问题,从而找到更好的解决方案。例如,在互传中,两个卦象相互影响,一个卦象代表了正面的力量,另一个卦象代表了负面的力量。我们可以通过逆向格义来理解互传中的矛盾,并找到解决问题的方法。
这些例子说明,逆向格义在周易中的运用可以帮助我们更全面地理解事物的发展趋势和变化,从而更好地解决问题。这种思维方式不仅限于周易,还可以应用于其他领域,帮助我们更好地理解问题并找到更好的解决方案。
而中间过程给出的解答如下:
其中第一段是大模型自身能力的,第二段是知识库的(搜索引擎在我做实验的时候还不可用)
大模型发现知识库的知识偏向于商业应用而非周易,因此大部分的解答来自于第一段。当知识库的知识与对话的topic相关性更大之后,将会更多的融合知识库的知识到最终回答中。
4.5. 微调大模型与MTPE技术的结合
当我们把MTPE技术应用到微调后的大模型上,增益将远胜于之前:
第一幅图是大模型自身能力和检索知识库之后分别给出的回答,而第二幅图是结合MTPE技术后大模型给出的最终回答。后者在概念的介绍、逆向格义在易学和相关文化领域中的应用的描述更加清晰、系统、详细。并且还有总结,相比于前者更加饱满,有逻辑性。