github源码:github.com/easonchanga…
gitee源码:gitee.com/hairstyleea…
欢迎大家关注仓库,后续将更新更多的技术demo。
Intro
不知道大家想过大模型是如何记住和我们的对话的
大家好,最近在开发过程中,遇到了一些与大模型(LLM)调用中多轮对话相关的问题,今天就跟大家分享一下多轮对话的实现原理。
了解训练与推理
大模型主要分为两个阶段:
- 训练
- 推理
通俗点讲,训练就是通过已有的数据(如文本,图片,音频,视频等),更新模型的参数。
推理,就是利用模型已有的参数,依据用户的输入,得到输出。此过程不会更新模型的参数。
这一过程,可以极度简化为一个一元函数,如下。
因此,简化后的训练和推理如下:
- 训练,通过已有的数据,这里是(x, y),来求参数a和b。
- 推理,利用训练中求得的a和b,通过新的输入x,计算y。
日常生活中,当我们使用大模型聊天类APP时,其实就是使用的推理过程。我们输入的文字,就相当于一元函数的x,大模型回答我们的,就相当于一元函数的y。
我之前说了什么?
如果说推理过程是利用已有的参数进行输出,并不会更新参数,按理说模型不会记住我们之前提及的信息。我们通过python,两次询问tongyi模型来验证这种情况。
第一次提问,我们告诉模型,我的名字是坤,看看模型是否可以记住。
# 第一次提问
import os
import dashscope
# 第一次提问问题
question = '我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。'
messages = [
{'role': 'user', 'content': question}
]
response = dashscope.Generation.call(
# 请求模型需要的api-key信息,需到官网申请,然后添加到环境变量 DASHSCOPE_API_KEY 中
api_key=os.getenv('DASHSCOPE_API_KEY'),
model="qwen-plus",
messages=messages,
result_format='message'
)
print(f"我的第一次提问: {question}")
print(f"模型第一次回答: {response.output.choices[0].message.content}")
'''
代码输出:
我的第一次提问: 我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。
模型第一次回答: 你好,坤!从你的介绍中可以看出你是一位多才多艺的艺人,不仅擅长唱歌、跳舞和说唱,
还喜欢打篮球。《ji你太美》是你的代表作品之一。希望你能继续努力,
在自己喜欢的领域里不断进步,为粉丝带来更多优秀的作品!
如果你有任何问题或需要帮助,欢迎随时告诉我。
'''
现在,我们进行第二次提问。询问模型我的名字,看看模型能否根据第一次提问进行回答。
# 第二次提问
import os
import dashscope
# 第二次提问问题
question = '我的名字是什么?'
messages = [
{'role': 'user', 'content': question}
]
response = dashscope.Generation.call(
# 请求模型需要的api-key信息,需到官网申请,然后添加到环境变量 DASHSCOPE_API_KEY 中
api_key=os.getenv('DASHSCOPE_API_KEY'),
model="qwen-plus",
messages=messages,
result_format='message'
)
print(f"我的第二次提问: {question}")
print(f"模型第二次回答: {response.output.choices[0].message.content}")
'''
代码输出:
我的第二次提问: 我的名字是什么?
模型第二次回答: 您好!您没有提供具体的名字信息,所以无法知道您的名字。
如果您有其他问题或需要帮助,请随时告诉我。
'''
如上,通过两次对话,第二次提问模型并不知道第一次提问的内容。那我们使用的LLM聊天软件,是如何记住我们上一次的回答的呢?
我们问一下ChatGPT,可以看到,GPT在我们第二次提问“我的名字是什么时”,准确给出了答案。这是怎么实现的?答案就是上下文管理
让模型知道我说了啥——上下文管理
简而言之,上下文管理就是在每一次与模型的对话中,告诉模型我之前说了什么。
实现的方式可以非常地简单粗暴。我们定义一个数组,用来保存我们的提问和模型的回答。不断将新的内容,添加到数组的尾部。在每一次请求时,带上历史对话数组,和本次的提问。这样,模型就能根据历史对话数组知道我之前说了啥以及模型自己之前说了啥。这就实现了一个简易版的上下文管理。
上下文管理python实现
第一次提问
我们改写一下之前的代码。用一个 history_message数组,按顺序保存自己的提问和大模型的回答。在新的提问时将问题追加到 history_messa尾部,再将整个数组发给大模型,让模型能够感知到对话的上下文。
# 上下文管理
import os
import dashscope
# 定义历史对话数组
history_message=[]
question = '我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。'
# 这里传入的问题是一个字典,是tongyi规定的api格式,可以暂时不管。
first_question = {'role': 'user', 'content': question}
# 将新的提问添加到历史对话数组,喂给模型
history_message.append(first_question)
response = dashscope.Generation.call(
# 请求模型需要的api-key信息,需到官网申请,然后添加到环境变量 DASHSCOPE_API_KEY 中
api_key=os.getenv('DASHSCOPE_API_KEY'),
model="qwen-plus",
messages=history_message,
result_format='message'
)
# 拷贝历史数组,这里是发送给模型的完整信息
send_to_model_messages = history_message.copy()
# role是模型默认的角色,可以暂时不管
first_answer = {'role': 'assistant', 'content': response.output.choices[0].message.content}
# 将模型回答,按tongyi格式规定后,添加到历史对话数组
history_message.append(first_answer)
print(f"我的第一次提问: \n{question}")
print("---")
print(f"第一次对话传递给模型的完整信息:\n{send_to_model_messages}")
print("---")
print(f"模型第一次回答: \n{response.output.choices[0].message.content}")
print("---")
print(f"模型第一次回答后历史对话数组: \n{history_message}")
第一次提问的输出如下。
我的第一次提问:
我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。
---
第一次对话传递给模型的完整信息:
[{'role': 'user', 'content': '我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。'}]
---
模型第一次回答:
你好,坤!很高兴认识你。听起来你是一位多才多艺的艺人,不仅擅长唱歌、跳舞和说唱,还热爱篮球。你的代表作《鸡你太美》也受到了很多人的喜爱。继续加油,追求自己的梦想!如果你有任何问题或需要帮助,随时告诉我。
---
模型第一次回答后历史对话数组:
[{'role': 'user', 'content': '我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。'}, {'role': 'assistant', 'content': '你好,坤!很高兴认识你。听起来你是一位多才多艺的艺人,不仅擅长唱歌、跳舞和说唱,还热爱篮球。你的代表作《鸡你太美》也受到了很多人的喜爱。继续加油,追求自己的梦想!如果你有任何问题或需要帮助,随时告诉我。'}]
可以看到,“模型第一次回答后历史对话数组”,中包函两个元素。第一个是我们的提问,第二个是本次提问后模型的回答。
第二次提问
现在,我们进行第二次提问,询问模型“我的名字是什么”。注意,这里需要将“我的名字是什么”这个问题追加到history_message数组的尾部,再将history_message传递给大模型。
# 第二次提问
import os
import dashscope
question = '我的名字是什么?'
second_question = {'role': 'user', 'content': question}
# 将第二次提问添加到数组尾部
history_message.append(second_question)
response = dashscope.Generation.call(
# 请求模型需要的api-key信息,需到官网申请,然后添加到环境变量 DASHSCOPE_API_KEY 中
api_key=os.getenv('DASHSCOPE_API_KEY'),
model="qwen-plus",
# 一股脑传入数组
messages=history_message,
result_format='message'
)
send_to_model_messages = history_message.copy()
# role是模型默认的角色,可以暂时不管
second_answer = {'role': 'assistant', 'content': response.output.choices[0].message.content}
# 将模型回答,按tongyi格式规定后,添加到历史对话数组
history_message.append(second_answer)
print(f"我的第二次提问: \n{question}")
print("---")
print(f"第二次对话传递给模型的完整信息:\n{send_to_model_messages}")
print("---")
print(f"模型第二次回答: \n{response.output.choices[0].message.content}")
print("---")
print(f"模型第二次回答后历史对话数组: \n{history_message}")
第二次提问的回答如下。
我的第二次提问:
我的名字是什么?
---
第二次对话传递给模型的完整信息:
[{'role': 'user', 'content': '我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。'}, {'role': 'assistant', 'content': '你好,蔡徐坤!很高兴认识你。作为一名练习生,你的努力和才华值得肯定。《Ji你太美》这首歌也展现了你的音乐风格和个人魅力。希望你在未来的演艺道路上继续加油,为粉丝带来更多优秀的作品!\n\n如果你有其他问题或需要帮助,随时告诉我哦!'}, {'role': 'user', 'content': '我的名字是什么?'}]
---
模型第二次回答:
你的名字是坤,也就是蔡徐坤。你是一位多才多艺的艺人,喜欢唱跳、RAP和篮球,并且有代表作《Ji你太美》。如果你还有其他问题或需要帮助,随时告诉我哦!
---
模型第二次回答后历史对话数组:
[{'role': 'user', 'content': '我的名字是坤,练习时长两年半,喜欢唱跳rap篮球,代表作ji你太美。'}, {'role': 'assistant', 'content': '你好,蔡徐坤!很高兴认识你。作为一名练习生,你的努力和才华值得肯定。《Ji你太美》这首歌也展现了你的音乐风格和个人魅力。希望你在未来的演艺道路上继续加油,为粉丝带来更多优秀的作品!\n\n如果你有其他问题或需要帮助,随时告诉我哦!'}, {'role': 'user', 'content': '我的名字是什么?'}, {'role': 'assistant', 'content': '你的名字是坤,也就是蔡徐坤。你是一位多才多艺的艺人,喜欢唱跳、RAP和篮球,并且有代表作《Ji你太美》。如果你还有其他问题或需要帮助,随时告诉我哦!'}]
重点关注“第二次对话传递给模型的完整信息”部分,说明第二次提问传递给大模型的 histroy_message完整数据依次包函三个元素:1)第一次提问;2)第一次回答;3)第二次提问。因此,这里实际上是把整个对话历史传递给了大模型。
从“模型第二次回答后历史对话数组”输出可以看到,大模型回答第二次提问后,答案又被追加到了history_message数组的尾部。如果还有后续提问,可以继续将问题追加到 history_message,再发送给大模型。
我们通过一个数组实现了非常朴素的多轮对话原型,这个原型,也正是阿里通义模型给出的多轮对话示例的原理。参见官方文档
进一步思考
我们实现了一个简易的上下文管理,那是否还可以更近一步?
(1)token限制
这里不妨粗略地将token数理解为字符数(token与字符数成正比,但一般不等于字符数)。
也就是说,大模型对可以处理的字符数是有限制的。比如一个支持4096的token数的大模型,随着对话轮次的增加,token数会超过上限,这样大模型将无法正常工作。
同时,很多大模型都是按token收费的,更多的token,意味着花更多的钱(哭)。
如何优化呢?这里分享两个思路:
- 设置一个变量k,只保留最近k次的对话,丢弃之前的信息。
- 对历史对话信息进行摘要提取或概括,再我们保存历史对话数组时对提问和回答进行概括,提取关键信息。这一步其实也可以通过大模型来完成。这样,我们保存的数组信息密度就会更高,token数会更低。
(2)微调(Fine-tuning)
微调实际超出了上下文管理的范畴,本质属于再次训练模型,只不过进行细微的训练,更新少量、部分参数。微调可以更新模型的参数,使得模型“记”住我的名字。
结语
总结来说,通过合理的历史对话存储和传递机制,我们可以在一定程度上解决模型“无记忆”的局限,使得模型能够在多轮对话中保持连贯性。如果你有其他想要补充或修改的部分,随时告诉我!
参考: