LlamaIndex是一个基于LLM的数据处理框架,在RAG领域非常流行,简单的几行代码就能实现本地的文件的对话功能,对开发者提供了极致的封装,开箱即用。
本文以官方提供的最简单的代理示例为例,分析LlamaIndex在数据解析、向量Embedding、数据存储及召回的整个源码过程。
通过学习框架的源码也能让开发者们在实际的企业大模型应用开发中,对RAG有一个更清晰的了解和认知。
本次选用的技术组件:
官方代码示例如下:
# 1.构建向量数据库存储对象实例
vector_store = ElasticsearchStore(
index_name="my_index",
es_url="http://localhost:9200",
)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# 加载本地的数据集
documents = SimpleDirectoryReader('data').load_data()
# 构建索引
index = VectorStoreIndex.from_documents(documents,storage_context=storage_context)
# 服务对象,构建query引擎
service_context = ServiceContext.from_defaults(llm=OpenAI())
query_engine = index.as_query_engine(service_context=service_context)
# 问问题
resp=query_engine.query("住院起付线多少钱?")
# 响应答案
print(resp)
在数据处理的过程中,主要包含几个核心的步骤:
SimpleDirectoryReader
是llamaindex提供的一个基于文件夹的读取器类,会根据文件夹中的文件扩展后缀类型自动加载数据
主要支持的文件数据类型如下:
DEFAULT_FILE_READER_CLS: Dict[str, Type[BaseReader]] = {
".hwp": HWPReader,
".pdf": PDFReader,
".docx": DocxReader,
".pptx": PptxReader,
".ppt": PptxReader,
".pptm": PptxReader,
".jpg": ImageReader,
".png": ImageReader,
".jpeg": ImageReader,
".mp3": VideoAudioReader,
".mp4": VideoAudioReader,
".csv": PandasCSVReader,
".epub": EpubReader,
".md": MarkdownReader,
".mbox": MboxReader,
".ipynb": IPYNBReader,
}
class SimpleDirectoryReader(BaseReader):
"""Simple directory reader.
Load files from file directory.
Automatically select the best file reader given file extensions.
Args:
input_dir (str): Path to the directory.
input_files (List): List of file paths to read
(Optional; overrides input_dir, exclude)
exclude (List): glob of python file paths to exclude (Optional)
exclude_hidden (bool): Whether to exclude hidden files (dotfiles).
encoding (str): Encoding of the files.
Default is utf-8.
errors (str): how encoding and decoding errors are to be handled,
see https://docs.python.org/3/library/functions.html#open
recursive (bool): Whether to recursively search in subdirectories.
False by default.
filename_as_id (bool): Whether to use the filename as the document id.
False by default.
required_exts (Optional[List[str]]): List of required extensions.
Default is None.
file_extractor (Optional[Dict[str, BaseReader]]): A mapping of file
extension to a BaseReader class that specifies how to convert that file
to text. If not specified, use default from DEFAULT_FILE_READER_CLS.
num_files_limit (Optional[int]): Maximum number of files to read.
Default is None.
file_metadata (Optional[Callable[str, Dict]]): A function that takes
in a filename and returns a Dict of metadata for the Document.
Default is None.
"""
supported_suffix = list(DEFAULT_FILE_READER_CLS.keys())
//....
总共支持了16个文件数据类型,整理到表格如下:
文件类型 | 依赖组件 | 说明 |
---|---|---|
hwp | olefile | |
pypdf | ||
docx | docx2txt | |
pptx、pptm、ppt | python-pptx、transformers、torch | 用到一些模型,对数据进行理解、提取 |
jpg、png、jpeg、 | sentencepiece、transformers、torch | 用到一些模型,对数据进行理解、提取 |
mp3、mp4 | whisper | 用到一些模型,对数据进行理解、提取 |
csv | pandas | |
epub | EbookLib、html2text | |
md | 无 | 本地流直接open,读取文本 |
mbox | bs4、mailbox | |
ipynb | nbconvert |
整个Reader类的UML类图设计如下:
所有文件数据类型的Reader,通过load_data
方法,最终得到该文档的Document
对象集合,Document
类是LlamaIndex框架的处理文档的核心类对象,从该类的结构设计来看,我们可以总结一下:
最终构建完成所有的Document信息后,我们可以看到下面一个结构信息
本示例程序,使用的是一个PDF文件,由于我们并未指定分割等策略,LlamaIndex对于PDF文件是以Page为单位,进行切割,最终将所有的Document对象存储进入向量数据库
当本地数据集处理完成,得到一个Document
集合的时候,此时,这需要构建向量数据库的索引,主要是包含几个过程:
text-embedding-ada-002
模型),将Document对象集合中的text文本,进行向量化处理并赋值Document
集合的对象值(text、embedding、metadata)存储进入向量数据库在LlamaIndex创建ES的向量索引结构中,初始情况下,核心字段也是前面我们提到的Document
类中的几个核心字段(id、embedding、content、metadata),如下图:
但是在Document对象遍历结束后,在数据存储阶段,考虑到元数据的信息,LlamaIndex会扩充metadata元数据的字段,如下图:
元数据信息会将文档的信息提取出来,包括页码、文件大小、文件名称、创建日期等等信息
最终在本地数据集的情况下,LlamaIndex创建的ES数据索引结构最终就会变成下面这种结构形式:
{
"mappings": {
"properties": {
"content": {
"type": "text"
},
"embedding": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "cosine"
},
"metadata": {
"properties": {
"_node_content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"_node_type": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"creation_date": {
"type": "date"
},
"doc_id": {
"type": "keyword"
},
"document_id": {
"type": "keyword"
},
"file_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"file_path": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"file_size": {
"type": "long"
},
"file_type": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"last_accessed_date": {
"type": "date"
},
"last_modified_date": {
"type": "date"
},
"page_label": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"ref_doc_id": {
"type": "keyword"
}
}
}
}
}
}
数据Index定义完成,Document对象存储进入向量数据库,此时,我们的数据集结构如下:
在获取答案的过程中,主要包含几个核心的步骤:
首先,在RAG的查询阶段,不管是使用那个向量数据库,根据数据库的类型,将用户的query语句进行Embedding后,再构建数据库的查询条件,如下图:
这里面会包含几个核心的参数:
VectorStoreQuery
类结构定义如下:
@dataclass
class VectorStoreQuery:
"""Vector store query."""
# knn搜索的查询Embedding浮点型数组
query_embedding: Optional[List[float]] = None
# knn搜索的top k取值
similarity_top_k: int = 1
doc_ids: Optional[List[str]] = None
node_ids: Optional[List[str]] = None
query_str: Optional[str] = None
output_fields: Optional[List[str]] = None
embedding_field: Optional[str] = None
mode: VectorStoreQueryMode = VectorStoreQueryMode.DEFAULT
# NOTE: only for hybrid search (0 for bm25, 1 for vector search)
alpha: Optional[float] = None
# metadata filters
filters: Optional[MetadataFilters] = None
# only for mmr
mmr_threshold: Optional[float] = None
# NOTE: currently only used by postgres hybrid search
sparse_top_k: Optional[int] = None
# NOTE: return top k results from hybrid search. similarity_top_k is used for dense search top k
hybrid_top_k: Optional[int] = None
根据query的条件,会从向量数据库中召回获取得到topk的TextNode数组,如下:
最终召回到引用文档内容后,剩下的就是构建整个大模型对话的Prompt工程,来看看LlamaIndex的基础Prompt是如何构建的
partial_format
方法获取得到一个基础的Prompt模版信息,内容如下:
'Context information is below.
---------------------
{context_str}
---------------------
Given the context information and not prior knowledge, answer the query.
Query: {query_str}
Answer: '
这里有两个核心的参数:
context_str
: 从向量数据库召回查询的知识库引用文本数据上下文信息,从模版的设定也是告诉大模型基于知识信息进行回答query_str
:用户提问的问题而最终的context_str信息,我们可以看到,如下图:
我们的问题是:住院起付线多少钱?
从最终knn检索召回的文档片段来看,精准的找到了知识库的引用内容,此时,交给大模型进行回答,获取我们想要的答案结果。
好了,本文从LlamaIndex给我们提供的基础的示例程序,基于Basic RAG的基础架构来分析数据的处理、召回响应等过程,我们可以看到LlamaIndex框架给了我们一个很好的处理流程,从这里我们可以总结如下:
在昨天员外分享的《TorchV的RAG实践分享(1)——RAG的定位、技术选型和RAG技术文章目录》一文中介绍了TorchV的由来,也分享了我们的几个基线产品和应用架构的方向,我们想的是在创业的过程中,将我们自己的一些产品理念、技术心得都通过公众号发文的方式分享出来,更多的和行业内的专家们共同交流,这对我们自己也是一种提升和锻炼,也期待和客户一起共创成长,逐步把产品打磨好。
在目前大模型应用技术架构中,通过召回上下文来回答用户的问题是解决大模型当下的幻觉问题最靠谱/经济实惠的一种解决方案,RAG检索增强技术在整个LLM技术架构体系中的作用越来越明显。而检索召回和用户的query问句的质量则直接关系到最终大模型的生成结果。在向量数据库基础设施普及的今天,仅仅通过语义搜索召回已经无法满足企业级的需求,大家发现传统的搜索技术(基于关键词、词频等相关性的搜索)的作用也显得尤为重要,混合检索也成为了目前在RAG的技术架构中的主流检索方式,混合检索通过扬长避短的方式,在不同的业务应用场景中形成了很好的互补,对于不同的业务场景需求中,可以更灵活的进行配置满足业务需要,是RAG技术架构体系中非常重要的重要一环。
本文中所提到的混合检索主要是两种搜索技术的结合,主要如下:
本文针对ElasticSearch中间件来实现整个外部知识库向量的存储和计算,在RAG技术架构中的混合检索进行探索和分析,结合我们自己的实际业务情况,如何通过底层的技术驱动,完善我们的产品设计,改善整个产品流程。
整篇文章主要包括:
在介绍ElasticSearch的混合检索之前,我们需要先简单回顾ElasticSearch这个中间件如何在目前AI技术场景的落地情况
在目前的RAG大模型技术架构体系中,向量Vector
技术已经作为大模型外挂知识库的非常重要的技术栈,向量的核心对于数据的表征(Embedding)然后执行相似度(Similarity)计算。2023年随着大模型技术的发布带火了非常多的向量数据库,特别是LangChain、llama_index等LLM数据应用框架的发布,包括:Milvus
、Qdrant
、Pinecone
、Chroma
等等专业的向量数据库中间件。向量数据结构的存储与计算可以说是当前做大模型应用的基建产品了,就好像传统软件工程中的数据库一样。
而对于ElasticSearch而言也同样如此,对于之前使用ElasticSearch中间件的开发人员而言,可能对于向量数据的存储和计算是比较陌生的,在传统软件工程用ES来储存搜索主要还是基于关键词搜索技术(BM25、TF-IDF)等实现,本质还是基于文本的精确匹配。而在最近ES组件发布的版本来看,特别是ES 8.0版本发布对于KNN算法搜索的优化支持来看,AI大模型这场技术革命风暴,似乎也不想袖手旁观。
我们选择ElasticSearch作为TorchV的基础RAG架构组件也是出于以下几个方面考虑:
在ElasticSearch的目前的版本中,要使用向量实现存储和计算对于开发者使用上非常简单,开发者在定义ES的索引结构时,定义向量字段类型dense_vector
,并且自定义向量维度dims
(最大维度不超过4096(自8.x版本开始)),如下索引结构:
PUT test-001
{
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text" : {
"type" : "keyword"
}
}
}
}
在执行搜索时则可以通过k-最近邻(KNN)搜索找到与查询向量最近的K个向量结果值来获取结果,通过相似度值来衡量获取文档片段。
GET test-001/_knn_search
{
"knn": {
"field": "my_vector",
"query_vector": [0.3, 0.1, 1.2],
"k": 10,
"num_candidates": 100
},
"_source": ["name", "date"]
}
而我们在前面提到,混合检索(语义搜索+相关性搜索)是目前做RAG的非常重要文档召回技术手段,纯KNN搜索并不能完全满足业务的需求,因此在当前的RAG技术架构体系中,ES在保持传统相关性搜索的基础上,增加对语义搜索的技术支持就显得很有冲击力,毕竟在向量搜索火爆之前,ES作为搜索引擎的老大哥,在企业级的产品应用体系中,应用范围还是非常广泛的。
在做混合检索时,我们会接触到两类算法,需要对算法有一个基础了解,这有助于我们在应用产品的技术体系中做决策:
KNN算法:k近邻算法,是机器学习算法中一种基本分类和回归方法。在给定的一个数据集中,对于新的数据实例,找到与该实例最邻近的k个实例,这k个实例的多数属于某个分类。
这就像你在一个陌生的城市,你可能会问周围的k个人哪家餐馆最好。如果大多数人都推荐同一家餐馆,那么你可能会选择去那家餐馆。
而我们在选择餐馆的过程中,每一个餐馆会有非常多的维度来描述这个餐馆的信息,包括:地理位置、菜系、价格、环境、口味等等,这一系列参数属性就是特征工程,目前的向量Embedding模型用来对一段文本进行Embedding,其实就是对于该文本内容的的特征信息进行提取描述。
这个时候,你会根据你自己的诉求,对于餐馆的不同特征要求,最终选择你要去哪家餐馆吃饭。
在Elasticsearch中,KNN搜索主要通过使用向量相似度(特征空间中的两个实例点间的距离可以反映出两点间的相似程度)进行度量,文档根据向量数据集与查询向量的相似度进行排名。每个文档的 _score
将从相似度中得出,以确保分数为正并且分数越高对应于越高的排名。
ES目前主要提供了三种度量的标准供我们选择(考虑到本文是基于es,因此也只对该三种度量标准做介绍,对于其它的向量计算距离的方式,开发者可以自行搜索了解)
score
计算方式为:1 / (1 + l2_norm(query, vector)^2)
_score
计算为 (1 + dot_product(query, vector)) / 2
_score
计算为 (1 + cosine(query, vector)) / 2
我们在开发RAG的大模型应用产品中,通常会将外部的知识库通过chunk分段存储处理,对于用户的query,通过Embedding模型进行表征为向量后,与chunk片段的向量进行距离计算,此时作为距离度量的方式考虑,那么根据实际的业务场景,就可以考虑上面的三种类型中的一种。
一般默认选择cosine余弦相似度进行计算召回,主要考虑:
而ES自8.0版本发布后,同样也提供了对KNN搜索的优化,主要提供了两种策略:
script_score
函数扫描每个匹配文档计算向量距离获取文档结果集,这会导致搜索速度缓慢(大数据集的应用场景下)。开发者在选择KNN搜索的算法策略时,可以根据自己的实际业务需要进行抉择。
ES自5.0版本之后,针对文档的相关性评分机制默认采用了BM25相似度算法(之前是基于TF-IDF),BM25全称Okapi BM25。Okapi 是使用它的第一个系统的名称,即Okapi信息检索系统,于 20 世纪 80 年代和 1990 年代在伦敦城市大学实施。 BM则是best matching的缩写。
因此对于词的相关性检索方案,我们对于TF-IDF和BM25也需要有一个基础的了解。
TF-IDF(Term Frequency-Inverse Document Frequency):词频-逆文档频率是一个常用于信息检索和文本挖掘的权重计算方法,函数公式如下:
主要由两部分组成:
n
,文本的总词条数为m
,则词频TF为n/m,也就是词频,比如一个单词:旅游
在我们的一篇文档中出现了4次,而我们的文档总共包含的词条数量是100,那么词频的值就是4/100
。词频越高,说明这个词在文档中越重要。TF-IDF就是将这两个值相乘,得到的结果就是一个词的权重,这个权重可以用来表示这个词对于文档的重要性,也可以用来比较不同文档的相似性。
TF-IDF在实践的发展中会存在一些问题:
这些问题都在BM25中得到了改进,BM25的词频部分使用了一个饱和函数,使得词频达到一定值后,增加词频对于最终得分的影响会变小。同时,BM25还考虑了文档长度的影响,通过一个归一化因子来调整不同长度文档中的词频。这使得BM25在处理词频未饱和和文档长度问题时,比TF-IDF有更好的性能。
BM25 的计算公式:
和TF-IDF的计算公式相比,BM25的公式着实有点吓人,不过其实我们关注几个核心的参数即可。
对于BM25算法在ElasticSearch中的应用公式和参数变量说明,可以参考这篇文章《BM25 算法及其变量》,这里我们只关心几个核心的参数
1.2
。较低的值导致较快的饱和,较高的值导致较慢的饱和。0.75
。这个参数值也是针对上面我们提到TF-IDF在文档长度未考虑的情况下一个加权计算,当然默认值0.75
是官方基于大量的数据实验得到的一个值,在默认场景下都会有较好的效果,我们可以不用调整。如果我们的默认检索效果不佳,应该从其它方面来考虑优化,这个后面我们再说在理解了算法、es中间件之后,结合实战+Score分值的计算使用过程,包括配合ES的Explain接口,讲清楚Score的计算规则,原理
在前面了解了ES的整个检索Score算法介绍之后,其实对于文本内容的检索召回Score分值计算,就比较清晰了,先说结论:
ElasticSearch在使用KNN+BM25检索的混合检索分值Score计算公式是:knn_score+bm25_score
使用ES混合检索的语法如下:
POST image-index/_search
{
"query": {
"match": {
"title": {
"query": "mountain lake",
"boost": 0.9
}
}
},
"knn": {
// 字段
"field": "image-vector",
// 输入向量
"query_vector": [54, 10, -2],
// k值
"k": 5,
// 每个分片要考虑的最近邻居候选数。不能超过 10,000
"num_candidates": 50,
// 加权参数值
"boost": 0.1,
// 档被视为匹配所需的最小相似度,配合filter使用,提高检索效率
"similarity": 0.7,
// 过滤条件
"filter": {
"term": {
"file-type": "png"
}
}
},
"size": 10
}
query
部分的检索所代表的是BM25算法的Score计算分值召回,而knn
部分的检索所代表的则是语义向量空间的距离Score分值,最终的结果值相加后倒排的一个文档列表结果集
score=
match_score*0.9 + knn_score*0.1
对于BM25算法的检索分值计算,开发者可以使用Explain API来查看整个Score
的计算过程,整个计算过程就和BM25算法公式那样,如下图:
BM25算法会将用户输入的match参数,计算每一个分词的score分值,最终加起来,得到一个总的分值score数据,对于每一个分词,都可以通过该接口查看到完整的计算过程,是非常方便的开发者进行理解的。
在这里进行BM25计算时,我前面提到BM25算法可能存在检索不到最终我们说期望的文本,会有一些其它参数影响最终效果,并非需要更改算法中的k1和b这两个参数,主要考虑如下:
对于KNN的检索分值计算,就非常的简单了,开发者在构建用户索引的时候,选择具体的向量距离类型,es在计算knn的时候,就会根据其算法进行计算
PUT my-index-2
{
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 1024,
// 选择类型,cosine、dot_product、l2_norm
"similarity": "cosine"
}
}
}
}
选择不同的类型, 就是单纯的向量距离计算了,按公式套用就可以了。
不过值得注意的是,对于使用最多的cosine
的文档 _score
计算为 (1 + cosine(query, vector)) / 2
。
当我们使用混合检索的时候,有一些注意事项值得我们关注:
filter
过滤参数会提高检索的效率,但是提高检索效率的同时,由于总是会计算召回文档进行相似度计算,所以可以配合similarity
来一起使用。对于混合检索,我们在算法层面有了直接的了解后,最终在产品层面会影响一些设计。
1、混合检索的权重设置:在上面的score分值计算公式中,我们其实知道es最终是通过bm25*boost
+knn*boost
,那么这个boost
则可以影响我们最终的内容,因为并不是所有的客户和业务场景都适合knn检索,可能在其他关键的场景中,关键词检索会更适合(比如一些利用大模型做一些异步的任务提取,报告输出等等业务场景),我们在产品设计中则可以根据不同的客户诉求以及业务诉求,就可以设置这个boost
来影响最终的召回结果天平,从而改善我们的产品效果。
在我们的TorchV的产品设计中,我们设计了一个alpha
参数值,取值范围在0-1之间,具体来说:
alpha = 1
:完全基于向量的搜索,也就是KNN近邻搜索alpha = 0
:完全基于关键词的搜索,基于ES的BM25算法检索2、在BM25算法的场景中,分词是非常重要的一个特性,对于不同的行业客户,词库的收集建立对于产品应用的提升肯定是会有质的提升,也是每个公司做RAG产品的核心竞争力。
3、持续运营能力的重要性,RAG问答检索功能在技术架构迭代优化上是一个方面,但是运营能力同样重要,哪怕是ChatGPT4,在针对特殊的数据文件,如果数据混乱,知识库质量不高,那么同样回答准确率不会很好的,这在我们和客户进行沟通交流的同时,虽然RAG可能会给客户眼前一亮的感觉,但是持续的提升他的能力,发挥更大的作用,产品的持续运营能力是必不可少的。
好了,全文完.
在不久前苹果官方开源发布了针对Apple Silicon 芯片优化的 MLX 深度学习框架,该框架可以简化研究人员在 Mac、iPad、iPhone 平台设计和部署模型的过程。
MLX的主要特性包括:
mlx.nn
和 mlx.optimizers
等更高级别的包,其 API 紧密遵循 PyTorch,以简化构建更复杂的模型。项目地址:https://github.com/ml-explore/mlx
而在今天的X上看到Apple开发者分享说可以在32GB的M1设备上使用MLX框架对Mistral 7B(或者llamA)等模型进行微调(Fine-tune)
看到官方的例子,我的电脑正好是M1 32GB的配置,就把代码跑来试试看
首先代码下载下来,地址:https://github.com/ml-explore/mlx-examples/tree/main/lora
安装依赖:
pip install -r requirements.txt
下载Mistral-7B(14.48GB大小)的模型并解压
curl -O https://files.mistral-7b-v0-1.mistral.ai/mistral-7B-v0.1.tar
tar -xf mistral-7B-v0.1.tar
将下载下来的模型文件进行转换,执行convert.py
文件, 命令如下:
# 转换命令
python convert.py \
--torch-model <path_to_torch_model> \
--mlx-model <path_to_mlx_model>
# 转换
python convert.py \
--torch-model mistral-7B-v0.1 \
--mlx-model mistral-7b-v0.1-mlx
两个主要的参数:
mistral-7B-v0.1
mistral-7b-v0.1-mlx
通过命令转换后,转换的目录文件会有三个文件,如下图:
将模型下载转换完成后,接下来就可以使用官方提供的lora.py
进行微调(Fine-tune)了,先来看数据集:
训练的数据集是1000行,主要的格式:
微调目标是得到一个能够将自然语言句子转换为SQL
{
"text": "table: 1-1000181-1\ncolumns: State/territory, Text/background colour, Format, Current slogan, Current series, Notes\nQ: Tell me what the notes are for South Australia \nA: SELECT Notes FROM 1-1000181-1 WHERE Current slogan = 'SOUTH AUSTRALIA'"
}
数据集的格式很清晰:
table: 表名称
columns: 列名称
Q: 用户问题
A: SQL语句
在第一次train的过程中,直接使用demo中的命令:
python lora.py --model <path_to_model> \
--train \
--iters 600
运行了大概10分钟后,程序就异常退出了,提示内存不足。
从图中可以看出,在声明内存的过程中,出现了异常,无法开辟新内存空间,并且每秒的Tokens数量也很感人😭
在看了官方的针对内存的issues建议后,发现有几个参数是影响着内存使用的:
--batch-size
使用较小的批量大小。 默认值为 4,因此将其设置为 2 或 1 将减少内存消耗。 这可能会减慢速度,但也会减少内存使用。--lora-layers
进行微调。 默认值为 16,因此您可以尝试 8 或 4。这会减少反向传播所需的内存量。 如果您使用大量数据进行微调,它还可能会降低微调模型的质量。根据官方的建议,那么修改train参数,如下:
python lora.py \
--model mistral-7b-v0.1-mlx \
--train \
--batch-size 1 \
--lora-layers 4
按这个命令执行后,在我的M1设备上执行的还比较快,每秒的Tokens数量平均上110左右
而Loss的值如下:
Iter | Loss |
---|---|
1 | 2.265 |
200 | 1.516 |
400 | 1.380 |
600 | 1.350 |
800 | 1.325 |
train完成后,会在本地默认生成一个权重文件adapters.npz
测试结果:
python lora.py --model mistral-7b-v0.1-mlx \
--adapter-file adapters.npz \
--num-tokens 50 \
--prompt "table: 1-10015132-16
columns: Player, No., Nationality, Position, Years in Toronto, School/Club Team
Q: What is terrence ross' nationality
A: "
Loading pretrained model
Total parameters 7243.436M
Trainable parameters 1.704M
Loading datasets
Generating
table: 1-10015132-16
columns: Player, No., Nationality, Position, Years in Toronto, School/Club Team
Q: What is terrence ross' nationality
# 大模型输出
A: SELECT Nationality FROM 1-10015132-16 WHERE Player = 'Terrence Ross' blowing off the rosshill. SELECT Nationality FROM 1-10015
从结果看,SQL的前半部分写对了,并且也识别出了字段、where条件,但是后面的句子好像就不太对了
我怀疑是在train时,参数--lora-layers 4
的问题,这时,我将改参数改为8,在train一次
python lora.py \
--model mistral-7b-v0.1-mlx \
--train \
--adapter-file adapters_2_8_1.npz \
--batch-size 2 \
--lora-layers 8
而Loss的值如下:
Iter | loss |
---|---|
1 | 2.348 |
200 | 1.392 |
400 | 1.293 |
800 | 1.213 |
1000 | 1.233 |
之后,同样的命令,再来看效果:
python lora.py --model mistral-7b-v0.1-mlx \
--adapter-file adapters_2_8.npz \
--num-tokens 50 \
--prompt "table: 1-10015132-16
columns: Player, No., Nationality, Position, Years in Toronto, School/Club Team
Q: What is terrence ross' nationality
A: "
Loading pretrained model
Total parameters 7243.436M
Trainable parameters 1.704M
Loading datasets
Generating
table: 1-10015132-16
columns: Player, No., Nationality, Position, Years in Toronto, School/Club Team
Q: What is terrence ross' nationality
A: SELECT Nationality FROM 1-10015132-16 WHERE Player = 'Terrence Ross' SELECT Nationality FROM 1-10015132-16 WHERE
看效果好像在SQL语句中,比上面的效果稍微要好一点了?但是结果还是不对。
效果并没有达到预期,我觉得主要是可能有几个方面的原因:
--lora-layers
的问题,默认是16,虽然我最后改成了8,但是从官方给出的说明来看,该参数会影响质量我将参数--lora-layers
修改为16进行了尝试,跑不了,可能还是我的内存太低了😭,那我只能加数据集了
修改了data目录下的wikisql.py文件,将数据集下载整理的总体数量上升到10000,代码:
if __name__ == "__main__":
datanames = ["train", "dev", "test"]
sizes = [56355, 8421, 15878]
for dataname, size in zip(datanames, sizes):
len(WikiSQL(dataname)) == size, f"Wrong {dataname} set size."
# Write the sets to jsonl
import json
train, dev, test = load()
# 此处原train参数是1000,我改成5000
datasets = [
(train, "train", 10000),
(dev, "valid", 1000),
(test, "test", 1000),
]
for dataset, name, size in datasets:
with open(f"data/{name}.jsonl", "w") as fid:
for e, t in zip(range(size), dataset):
# Strip the <s>, </s> since the tokenizer adds them
json.dump({"text": t[3:-4]}, fid)
fid.write("\n")
修改数据集后,在train过后,得到一个新的权重文件,命令:
python lora.py \
--model mistral-7b-v0.1-mlx \
--train \
--adapter-file adapters_2_8_1.npz \
--batch-size 2 \
--lora-layers 8
loss的train过程分值变化:
Iter | loss |
---|---|
1 | 2.348 |
200 | 1.472 |
400 | 1.410 |
600 | 1.387 |
800 | 1.360 |
1000 | 1.349 |
再来看看我们的promt得到的结果:
从结果来看,SQL语句的语法好像并没有什么大的问题,只是结果没有达到预期,可能还是得从数据集及相关参数找一下原因。
虽然运行的结果还没有完全达到预期,但是在MAC上通过Apple推出的MLX深度学习框架进行Fine-ture的技术方案是可行的。
这也为以后大模型的训练、生态发展提供了另外一种可能性。
包括我们应用开发者在做RAG的过程中,和数据进行对话的场景随着业务的深入肯定会触及,而对模型进行微调是不可避免的。
在源码编译安装时,如果没有指定OpenSSL那么在使用时会出现一些异常,解决方案:
1、更新yum软件包
yum update
yum install openssl-devel bzip2-devel libffi-devel
2、下载最新的OpenSSL源码,解压并编译
cd /usr/src
wget https://ftp.openssl.org/source/openssl-1.1.1q.tar.gz --no-check-certificate
解压OpenSSL包并安装
# 解压
tar -xzvf openssl-1.1.1q.tar.gz
cd openssl-1.1.1q
# 编译
./config --prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared zlib-dynamic
make
# 安装
make install
3、验证版本
> openssl version
OpenSSL 1.1.1q 5 Jul 2022
> which openssl
/usr/bin/openssl
4、下载Python的源码包,解压并安装
## 解压
tar -xzf Python-3.11.6.tgz
cd Python-3.11.6
# 编译(指定python3的目录和openssl模块)
./configure --prefix=/mnt/python/python3 --with-openssl=/usr
# 安装
sudo make
sudo make install
5、生成软链
系统中可能已经存在python3的命令,删除重新命名即可
在上面我们指定安装目录在/mnt/python/python3
下,所以可以直接创建软链
sudo ln -s /mnt/python/python3/bin/python3.11 /usr/bin/python3
后来,就基于自己的想法,整理的了要重新整理博客的需求,列了一个思维导图,如下图:
主要从以下几个方面考虑:
肯定是基于目前已经开放流行的博客框架进行改造,这样能够快速的搭建完成,而且无需考虑页面布局的情况
这里列的大部分我都是用过
这里选择Jekyll 主要原因有几个:
第二个方面考虑的因素是首页的内容,像VuePress和VitePress默认的首页内容其实是非常简洁的,我觉得更适合产品的展示,不适合博客,当然你也可以花时间改造,或者选择一个很棒的主题进行替换,这里主要是时间不够,就没有选择这个,并非所他们不好。而我所考虑的是:
以下就是该博客的首页,非常符合我的诉求
接下来就是考虑博客框架一个基础的功能,这里从个人的诉求,列了以下的要求:
移动端时代,这是一个非常重要的特性,有时候在发公众号文章时可以在底部配置原文链接,对于读者来说可以无差别阅读
可以提供站内搜索的功能,不管是构建本地博客索引还是使用外部的实现,例如《Final.激活Knife4j官网的文档搜索功能》提到的algolia都是可以的
不管是分类还是日期归档,都是必须的功能
评论算是一个个人诉求吧,并非强制,当然如果有默认提供那更好,本站点基于giscus实现
快速分享到各大社交平台,非常实用的功能
该功能我想目前各个框架都支持,使用百度统计或者Google Analytics应该都非常方便
根据个人喜好提供
个人的博客,我觉得整体的大纲内容不必太多,只需要关注写作内容即可,能够将写作内容能够快速的索引到并且给读者一个清晰的结构,就可以了
所以我的博客大纲主要是四个: 主页、标签、归档、关于
本站的博客基于Jekyll的chirpy主题实现,该主题满足了博主的所有诉求,非常棒,希望你也能够喜欢!!!
本站源码:https://github.com/xiaoymin/xiaoymin.github.io
]]>大模型正为我们带来前所未有的技术革新,而用好大模型也是有一定技巧的。
本文主要分享5种实用的Prompt
对话提示框架,结合自己的实际需求,让你能够灵活使用大模型!
RTF(Role-Task-Format)框架是一个非常简单通用的Prompt提示框架,我们和任意大模型对话场景下都可以使用该规范进行改进输出
主要优点:
示例1: 给出一份Python语言的学习清单
Role:指定大模型角色为Python布道师
Task:Python语言的学习从基础到进阶清单列表
Format: 以表格的形式返回
在实际工作的任务中,我通过优化Prompt工程,对于我们的产品改善,对于回答的内容改善也非常明显!
在我司给宁波天一阁开发的AI讲解产品中,我们提供和大模型对话的RAG产品,将天一阁的相关知识导入到系统,借助大模型进行讲解回答
对比以下两个Prompt的区别:
原Prompt:
基于以下已知信息,简洁和专业的来回答天一阁相关的的问题。
如果无法从中得到答案,请说 "根据已知信息无法回答该问题" 或 "没有提供足够的相关信息",不允许在答案中添加编造成分,答案请使用中文。
问题:
{}
已知内容:
{}
改进后的Prompt:
你是宁波天一阁的历史研究员,基于以下已知信息,简洁和专业的来回答天一阁相关的的问题。
如果无法从中得到答案,请根据根据实际回答,不要臆测内容,否则请说 "根据已知信息无法回答该问题" 或 "没有提供足够的相关信息",不允许在答案中添加编造成分,答案请使用中文。
问题:
{}
已知内容:
{}
改进后,大模型回答更加拟人化,如下图:
通过这种模式来逐步改善大模型的推理能力,非常适合一些复杂的任务处理。
例如:
而要使用这种模式,只需要在末尾添加”让我们逐步思考”即可。
Task-任务
差不多RTF
框架中的Format
有异曲同工之妙,一个是格式的约束,而这里的约束可以是任意方面,比如回答的内容(特定领域)、字数限制等等方面该框架主要适合:
示例:
示例:
密度链模式Prompt
是Salesforce、麻省理工学院和哥伦比亚大学的研究人员推出的一种新提示,它非常的高效,使用递归来创建越来越好的输出的提示,与普通提示生成的 GPT-4 摘要相比,它生成的摘要更加密集且更适合人们理解。
这种模式在RAG工程中非常实用,想想看你的客户上传的文档知识库(PDF/WORD)都是长篇的步骤性的文档,而在RAG召回送给大模型的Context上下文又受限于大模型的Token限制,为了更好的回答用户提问的问题,对于上传的知识库做密度链模式的摘要总结,然后索引整个文章内容召回是非常有必要的,最终能够非常精准的回答用户的问题。
适合:
密度链模式的Prompt
如下:
文章: {ARTICLE}
您将为上述文章生成越来越简洁、实体密集的摘要。
重复以下2个步骤5次:
- 步骤1:从文章中识别出先前生成的摘要中缺少的 1-3 个信息实体(以“;”分隔)。
- 步骤2:写一个新的、长度相同的、更密集的摘要,其中涵盖先前摘要中的每个实体和细节以及缺失的内容实体。
缺少的实体是:
- **相关的:**与主要故事相关,
- **具体的:**描述具体而简洁(5个字或更少),
- **新颖的:**不在之前的摘要中
- **务实的:**存在于文章中
- **任何地方:** 位于文章中的任何位置
**指南:**
- 第一个摘要应该较长(4-5句,约80个词),但非常不具体,除了标记为缺失的实体外,几乎没有包含其他信息。使用过度冗长的语言和填充词(例如,“本文讨论”)以达到约80个词。
- 让每个词都有意义:重新撰写前一个摘要以改善流畅性,并为额外的实体腾出空间。
- 利用融合、压缩和删除诸如“文章讨论”的无信息短语,腾出空间。
- 摘要应变得非常密集而简洁,但又是自包含的,例如,不需要阅读文章就能容易理解。
- 缺失的实体可以出现在新摘要的任何位置。
- 永远不要删除前一个摘要中的实体。如果无法腾出空间,就添加更少的新实体。
请记住,对于每个摘要都使用相同数量的词。
以JSON格式回答。JSON应该是一个字典列表(长度为5),其中键是“Missing_Entities”和“Denser_Summary”。
关于密度链模式的Prompt论文可以参考:https://arxiv.org/pdf/2309.04269.pdf
或者微信公众号回复”cod”获取文件
Prompt
对话提示框架在大模型领域中是非常重要的一环,不管你是在直接使用大模型,还是在做RAG领域的产品开发,Prompt
的重要程度都是无可替代的。
希望大家能根据本文列出的这5种Prompt
框架进行举一反三,多多实践~
对于Prompt
工程技术细节,可以阅读员外的这两篇文章:
在前几天的文章中,我分享说在RAG领域,很多都是工程上的实践,做AI大模型应用的开发其实Java也能写,那么本文就一个Java开发者的立场,构建实现一个最基础的大模型应用系统。
而大模型应用系统其实在目前阶段,可能应用最广的还是RAG领域,因此,本文也是通过在RAG领域的基础架构下,来实现应用的开发,主要需求点:让大模型理解文本(知识库)内容,基于知识库范围内的内容进行回答对话
而基于知识库的回答会帮助我们解决哪些问题呢?
在本文中,你将学习到:
考虑到作者也是Java开发者,因此本文所选择的技术栈以及中间件也是Java人员都耳熟能详的,主要技术栈如下:
1、开发框架:Spring Boot
、Spring Shell
(命令行对话)
Java开发者对于Spring Boot的生态应该是非常熟悉的,而选择Spring Shell
工具包主要是为了演示命令行的交互问答效果,和本次的技术无太大关系,算是一个最小雏形的产品交互体验。
2、HTTP组件:OkHTTP
、OkHTTP-SSE
此次我们选择的大模型是以智谱AI开放的ChatGLM系列为主,因此我们需要HTTP组件和商业大模型的API进行接口的对接,当然开发者如果有足够的条件,也是可以在本地部署开源大模型并且开放API接口进行调试的,这个并不冲突,本文只是为了方便演示效果,所以使用了智谱的大模型API接口,而智谱AI注册后,默认提供了一个18元的免费Token消费额度,因此接口的API-Key只需要注册一个即可快速获取。
3、工具包:Hutool
非常好用的一个基础工具包组件,封装了很多工具类方法,包含字符、文件、时间、集合等等
本文会使用到Hutool
包的文本读取和切割方法。
4、向量数据库:ElasticSearch
向量数据库是RAG应用程序的基础中间件,所有的文本Embedding向量都需要存储在向量数据库中间件中进行召回计算,当然在Java领域并没有类似Python中numpy
这类本地化工具组件包,即可快速实现矩阵计算等需求(PS:最近Java21的发布中,不仅仅只是虚拟线程等新特性,提供的向量API相信在未来AI领域,Java也会有一席之地的
),所以选择了独立部署的中间件。
本文选择ElasticSearch可能对于Java开发人员也是比较熟悉的一个组件,毕竟ES在Java领域用途还是非常广的,只是可能很多开发者并不知道ElasticSearch居然还有存储向量数据的功能?
对于向量数据库中间件的选择,目前市面上有非常多的向量数据库,包括:Milvus
、Qdrant
、Postgres(pgvector)
、Chroma
等等,Java开发者可以在熟悉当前流程后,根据自己的实际需求,选择符合企业生产环境的向量数据库。
5、LLM大模型:ChatGLM-Std
为了演示方便,本文直接使用开放API接口的商业大模型,智谱AI提供的ChatGLM-Std
在RAG检索增强生成领域中,最简单的核心处理流程架构图如下:
该架构图图是一个非常简单的流程图,在RAG领域中其实有非常多的处理细节,当我们深入了解后就会知道
我们后续根据该图来进行Java编码实现。
在RAG应用工程领域,其实整个程序的处理包含两部分:
Embedding
模型处理,然后通过查询向量数据库(ElasticSearch
)进行相似度计算获取和用户问题最相似的知识库段落内容,获取成功后,构建Prompt
,最终发送给大模型获取最终的答案。PDF
/Word
/Text
等等),提取文本数据后进行分割处理,最终通过向量Embedding
模型将这些分割后的段落进行向量化,最终向量数据存储在基础设施向量数据库组件中,以供后续的问答流程使用。从图中我们可以知道,在我们所需要的大模型处于什么位置,以及它的作用,主要是两个模型的应用:
Prompt
送给大模型以获取最终的答案,问答大模型在这里充当的角色是理解我们送给他的内容,然后进行精准回答我们理解了基础的架构流程,接下来就是编码实现了
Java:JDK 1.8
ElasticSearch:7.16.1
对于ElasticSearch
的安装,可以通过docker-compose
在本地快速部署一个
编写docker-compose.yml
配置文件,当前部署目录建data
文件夹挂载数据目录
version: "3"
services:
elasticsearch:
image: elasticsearch:7.16.1
ports:
- "9200:9200"
- "9300:9300"
environment:
node.name: es
cluster.name: elasticsearch
discovery.type: single-node
ES_JAVA_OPTS: -Xms4096m -Xmx4096m
volumes:
- ./data:/usr/share/elasticsearch/data
deploy:
resources:
limits:
cpus: "4"
memory: 5G
reservations:
cpus: "1"
memory: 2G
restart: always
启动Es:docker-compose up -d
先来看整个程序的应用效果,通过Spring Shell环境下,程序启动后,如下图所示:
程序启动后,在命令行终端,我们可以看到一个可交互的命令行,此时,我们可以通过add
和chat
两个命令完成图1中的整个流程
先使用add
命令加载文档,在data
目录下分别存储了001.txt
、002.txt
两个文件,通过命令加载向量处理,如下图:
当日志显示保存向量成功后,此时,我们即可以通过chat
命令进行对话了,我们先来看看002.txt
的文本主要说了什么内容?
data目录下的文本,开发者在调试时可以自己随意添加,网上随便找的文章都可以
文章内容是一篇非常具有代表性的时政人物介绍新闻,那么我们就根据该文章的内容进行问答!
问题1:苏州2022年全市的GDP是多少?
问题2:吉林省宣传部部长现在是谁?
通过第一个问题,你是否可以发现问题呢?,如果你问ChatGPT一样的问题,它能准确回答吗?
以下是ChatGPT
的回答
通过对比ChatGPT
,开发者应该能看到一个基础的对比效果,主要体现:
进行问答体验后,我们来看具体的Java代码实现。
新建Spring Boot项目,工程目录如下:
GitHub:https://github.com/xiaoymin/LlmInAction/tree/master/llm_chat_java_hello
从上文的RAG流程图中,我们知道了主要分两个步骤来实现,分别是数据的向量处理和问答
由于是通过Spring Shell
进行实现,因此这里我也分开,主要实现了两个Command
命令:
add file名称
来实现文档的向量化流程加载处理,数据的处理开发者在实际的生产过程中可以通过定时任务、MQ消息等方式进行异步处理。chat 问题
即可在Spring Shell的命令行终端进行对话,可以问data目录下相关的问题为了方便后续的处理,程序启动时即会自动构建向量数据库的索引集合,代码如下:
/**
* 初始化向量数据库index
* @param collectionName 名称
* @param dim 维度
*/
public boolean initCollection(String collectionName,int dim){
log.info("collection:{}", collectionName);
// 查看向量索引是否存在,此方法为固定默认索引字段
IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(IndexCoordinates.of(collectionName));
if (!indexOperations.exists()) {
// 索引不存在,直接创建
log.info("index not exists,create");
//创建es的结构,简化处理
Document document = Document.from(this.elasticMapping(dim));
// 创建
indexOperations.create(new HashMap<>(), document);
return true;
}
return true;
}
Es中的Index的Mapping结构如下:
开发者需要注意vector字段,字段类型时dense_vector
,并且指定向量维度为1024
向量维度的长度指定是和最终向量Embedding模型息息相关的,不同的模型有不同的维度,比如ChatGPT的向量模型维度是1536,百度文心一言也有368的,因此根据实际情况进行选择。
而这里因为我们选择的是智谱AI的向量模型,该模型返回的维度为1024,那么我们在向量数据库的维度就设置为1024
首先是add
命令实现文档的向量化过程处理,代码如下:
@Slf4j
@AllArgsConstructor
@ShellComponent
public class AddTxtCommand {
final TxtChunk txtChunk;
final VectorStorage vectorStorage;
final ZhipuAI zhipuAI;
@ShellMethod(value = "add local txt data")
public String add(String doc){
log.info("start add doc.");
// 加载
List<ChunkResult> chunkResults= txtChunk.chunk(doc);
// embedding
List<EmbeddingResult> embeddingResults=zhipuAI.embedding(chunkResults);
// store vector
String collection= vectorStorage.getCollectionName();
vectorStorage.store(collection,embeddingResults);
log.info("finished");
return "finished docId:{}"+doc;
}
}
我们完全按照图1RAG的流程架构图进行代码的变现,主要的步骤:
1、加载指定的文档,并且将文档内容进行分割处理(按固定size大小进行分割处理),得到分割集合chunkResults
,代码如下:
@Slf4j
@Component
@AllArgsConstructor
public class TxtChunk {
public List<ChunkResult> chunk(String docId){
String path="data/"+docId+".txt";
log.info("start chunk---> docId:{},path:{}",docId,path);
// 读取data目录下的文件流
ClassPathResource classPathResource=new ClassPathResource(path);
try {
// 读取为文本
String txt=IoUtil.read(classPathResource.getInputStream(), StandardCharsets.UTF_8);
//按固定字数分割,256
String[] lines=StrUtil.split(txt,256);
log.info("chunk size:{}", ArrayUtil.length(lines));
List<ChunkResult> results=new ArrayList<>();
//此处给每个文档一个固定的chunkId
AtomicInteger atomicInteger=new AtomicInteger(0);
for (String line:lines){
ChunkResult chunkResult=new ChunkResult();
chunkResult.setDocId(docId);
chunkResult.setContent(line);
chunkResult.setChunkId(atomicInteger.incrementAndGet());
results.add(chunkResult);
}
return results;
} catch (IOException e) {
log.error(e.getMessage());
}
return new ArrayList<>();
}
}
2、将分块的集合通过智谱AI提供的向量Embedding
模型进行向量化处理,代码实现如下:
/**
* 批量
* @param chunkResults 批量文本
* @return 向量
*/
public List<EmbeddingResult> embedding(List<ChunkResult> chunkResults){
log.info("start embedding,size:{}",CollectionUtil.size(chunkResults));
if (CollectionUtil.isEmpty(chunkResults)){
return new ArrayList<>();
}
List<EmbeddingResult> embeddingResults=new ArrayList<>();
for (ChunkResult chunkResult:chunkResults){
//分别处理
embeddingResults.add(this.embedding(chunkResult));
}
return embeddingResults;
}
public EmbeddingResult embedding(ChunkResult chunkResult){
//获取智谱AI的开发Key
String apiKey= this.getApiKey();
// 初始化http客户端
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(20000, TimeUnit.MILLISECONDS)
.readTimeout(20000, TimeUnit.MILLISECONDS)
.writeTimeout(20000, TimeUnit.MILLISECONDS)
.addInterceptor(new ZhipuHeaderInterceptor(apiKey));
OkHttpClient okHttpClient = builder.build();
EmbeddingResult embedRequest=new EmbeddingResult();
embedRequest.setPrompt(chunkResult.getContent());
embedRequest.setRequestId(Objects.toString(chunkResult.getChunkId()));
// 智谱embedding模型接口
Request request = new Request.Builder()
.url("https://open.bigmodel.cn/api/paas/v3/model-api/text_embedding/invoke")
.post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), GSON.toJson(embedRequest)))
.build();
try {
Response response= okHttpClient.newCall(request).execute();
String result=response.body().string();
ZhipuResult zhipuResult= GSON.fromJson(result, ZhipuResult.class);
EmbeddingResult ret= zhipuResult.getData();
ret.setPrompt(embedRequest.getPrompt());
ret.setRequestId(embedRequest.getRequestId());
return ret;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
3、向量处理成功后,我们即可将向量数据存储在向量数据库中间件(ElasticSearch
)中,调用vectorStorage.store
处理,代码如下:
public void store(String collectionName,List<EmbeddingResult> embeddingResults){
//保存向量
log.info("save vector,collection:{},size:{}",collectionName, CollectionUtil.size(embeddingResults));
List<IndexQuery> results = new ArrayList<>();
for (EmbeddingResult embeddingResult : embeddingResults) {
ElasticVectorData ele = new ElasticVectorData();
ele.setVector(embeddingResult.getEmbedding());
ele.setChunkId(embeddingResult.getRequestId());
ele.setContent(embeddingResult.getPrompt());
results.add(new IndexQueryBuilder().withObject(ele).build());
}
// 构建数据包
List<IndexedObjectInformation> bulkedResult = elasticsearchRestTemplate.bulkIndex(results, IndexCoordinates.of(collectionName));
int size = CollectionUtil.size(bulkedResult);
log.info("保存向量成功-size:{}", size);
}
至此,整个文本数据的Embedding处理就完成了。
数据处理完成后,接下来我们需要实现问答chat
命令,来看代码实现:
@AllArgsConstructor
@Slf4j
@ShellComponent
public class ChatCommand {
final VectorStorage vectorStorage;
final ZhipuAI zhipuAI;
@ShellMethod(value = "chat with files")
public String chat(String question){
if (StrUtil.isBlank(question)){
return "You must send a question";
}
//句子转向量
double[] vector=zhipuAI.sentence(question);
// 向量召回
String collection= vectorStorage.getCollectionName();
String vectorData=vectorStorage.retrieval(collection,vector);
if (StrUtil.isBlank(vectorData)){
return "No Answer!";
}
// 构建Prompt
String prompt= LLMUtils.buildPrompt(question,vectorData);
zhipuAI.chat(prompt);
// 大模型对话
//return "you question:{}"+question+"finished.";
return StrUtil.EMPTY;
}
}
Chat
命令主要包含的步骤如下:
1、将用户的问句首先通过向量Embedding模型转化得到一个多维的浮点型向量数组,代码如下:
/**
* 获取句子的向量
* @param sentence 句子
* @return 向量
*/
public double[] sentence(String sentence){
ChunkResult chunkResult=new ChunkResult();
chunkResult.setContent(sentence);
chunkResult.setChunkId(RandomUtil.randomInt());
EmbeddingResult embeddingResult=this.embedding(chunkResult);
return embeddingResult.getEmbedding();
}
2、根据向量数据查询向量数据库召回相似的段落内容,vectorStorage.retrieval
方法代码如下:
public String retrieval(String collectionName,double[] vector){
// Build the script,查询向量
Map<String, Object> params = new HashMap<>();
params.put("query_vector", vector);
// 计算cos值+1,避免出现负数的情况,得到结果后,实际score值在减1再计算
Script script = new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, "cosineSimilarity(params.query_vector, 'vector')+1", params);
ScriptScoreQueryBuilder scriptScoreQueryBuilder = new ScriptScoreQueryBuilder(QueryBuilders.boolQuery(), script);
// 构建请求
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(scriptScoreQueryBuilder)
.withPageable(Pageable.ofSize(3)).build();
SearchHits<ElasticVectorData> dataSearchHits = this.elasticsearchRestTemplate.search(nativeSearchQuery, ElasticVectorData.class, IndexCoordinates.of(collectionName));
//log.info("检索成功,size:{}", dataSearchHits.getTotalHits());
List<SearchHit<ElasticVectorData>> data = dataSearchHits.getSearchHits();
List<String> results = new LinkedList<>();
for (SearchHit<ElasticVectorData> ele : data) {
results.add(ele.getContent().getContent());
}
return CollectionUtil.join(results,"");
}
这里主要利用了ElasticSearch
提供的cosineSimilarity
余弦相似性函数,计算向量得到相似度的分值,分值会在区间[0,1]之间,如果无限趋近于1那么代表用户输入的句子和之前我们存储在向量中的句子是非常相似的,越相似代表我们找到了语义相近的文档内容,可以作为最终构建大模型Prompt的基础内容。
向量矩阵的计算除了余弦相似性,还有IP点积、欧几里得距离等等,根据实际情况选择不同的算法实现。
3、向量召回Top3得到相似的语义文本内容后,我们就可以构建Prompt
了,并且发送给大模型,Prompt
如下:
public static String buildPrompt(String question,String context){
return "请利用如下上下文的信息回答问题:" + "\n" +
question + "\n" +
"上下文信息如下:" + "\n" +
context + "\n" +
"如果上下文信息中没有帮助,则不允许胡乱回答!";
}
而在构建Prompt
时,我们可以遵循一个最简单的框架范式,RTF框架(Role-Task-Format):
4、最后是调用大模型,实现sse流式调用输出,代码如下:
public void chat(String prompt){
try {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(20000, TimeUnit.MILLISECONDS)
.readTimeout(20000, TimeUnit.MILLISECONDS)
.writeTimeout(20000, TimeUnit.MILLISECONDS)
.addInterceptor(new ZhipuHeaderInterceptor(this.getApiKey()));
OkHttpClient okHttpClient = builder.build();
ZhipuChatCompletion zhipuChatCompletion=new ZhipuChatCompletion();
zhipuChatCompletion.addPrompt(prompt);
// 采样温度,控制输出的随机性,必须为正数
// 值越大,会使输出更随机,更具创造性;值越小,输出会更加稳定或确定
zhipuChatCompletion.setTemperature(0.7f);
zhipuChatCompletion.setTop_p(0.7f);
EventSource.Factory factory = EventSources.createFactory(okHttpClient);
ObjectMapper mapper = new ObjectMapper();
String requestBody = mapper.writeValueAsString(zhipuChatCompletion);
Request request = new Request.Builder()
.url("https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke")
.post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody))
.build();
CountDownLatch countDownLatch=new CountDownLatch(1);
// 创建事件,控制台输出
EventSource eventSource = factory.newEventSource(request, new ConsoleEventSourceListener(countDownLatch));
countDownLatch.await();
} catch (Exception e) {
log.error("llm-chat异常:{}", e.getMessage());
}
}
SSE流式的调用我们使用了okhttp-sse
组件提供的功能快速实现。
好了,整个工程层面的Java代码实现就已经全部完成了。
以上就是本片分享的全部内容了,通过Java开发语言,实现一个最小可用级别的RAG大模型应用!相信你看完本文后,也能够对AI大模型应用的开发有一个基本的了解。
如果你也在关注大模型、RAG检索增强生成技术,欢迎关注我,一起探索学习、成长~!
本文代码Github:https://github.com/xiaoymin/LlmInAction
]]>首先,非常感谢关注Knife4j项目的朋友,该公众号应该是今年开始,对于开源项目Knife4j的更新都在此公众号进行了第一时间的发布更新,包括该项目的迭代、想法、实践等等内容,包括最近Knife4j的付费产品Knife4jInsight的推出,虽然更新的不是很频繁,但对于还是要给自己一直坚持的事情做输出点赞的(感动了自己😹)。对于写公众号而言,对于文章的发表,如果有人持续关注的话,会是一种正向的鼓励~!
而现在,考虑到我实际工作中的内容,目前工作核心是在做大模型RAG(检索增强生成)方面的开发工作,主要还是以Java技术栈进行产品的功能迭代开发,Python语言则更多的偏向底层大模型方面,包括大模型(LLM)的训练、微调、数据标注、向量Embedding等领域,工程上,业务产品测的逻辑实现、编排,接口开发等等功能还是以Java语言实现为主,并非使用当下最火的LangChain、LlaMaIndex等Python框架,我在昨天看一本书《Java开发之道》里面提到,开发者在实际开发过程中,在产品或项目开发周期结束后,需要善于总结自己的工作内容,我觉得以Blog或者视频的形式进行输出最好,这样才能更快速的成长。
我回想起这么多年的工作,对于一个领域,不管是技术层面,或者是业务层面,似乎一直是缺少总结性的输出的,或者说做的都不是太深入。在早些时候,我和我的老大哥员外说,这么多年做的东西不管是产品、还是项目,都不太能拿的出手,而随着时间越久,这种深深的挫败感就从内心油然而生,每每想起,都自感恼火😫。主要原因还是我们做的东西都太表面了,在技术层面,我们很多技术都是浮于表面,并没有做持续深度的精进,产品层面,一个像样的产品是经过千锤百炼的打磨的,细扣每一个细节,把同类竞品比下去,也许产品上面一个看似不起眼的优点,都能把同类的竞品给比下去,而要做到产品上的优势体现,技术人员的投入是成正相关的。项目就更不用说了,交钥匙工程(内部代号,意指项目做完就验收了,至于使用如何,功能好不好用从来没care过,业主也不care)的项目做的已经数都数不过来了。
而最近我和我的老伙计员外已经一头扎进了AI大模型、RAG(Retrieval Augmented Generation-检索增强生成)这个赛道,每天讨论的都是大模型、RAG等相关内容,然后产品的技术栈开发又是Java为主,Python为辅,在Java语言这个领域开发AI应用,好像目前在RAG这个领域并没有相关的技术文档、博客输出,基本80%都是Python,我在看了LangChain、LlaMaIndex等Python框架处理开发大模型应用(主要是RAG)时,除了在本地加载大模型外,很多其实都是工程方面的知识,并非一定要使用Python语言来进行开发,而我最近实际工作中又在做这个,因此,希望能够通过以Java+Python两种语言的形式,通过学习大模型、RAG等新型领域的知识,甚至是学习Python语言,将自己学到的内容以及一些工作中的实践通过文章的方式进行输出分享,更多的还是会将内容应用的实际的产品中,也希望在这个领域做一个深耕,不管是技术上还是业务领域,大模型这一波,我觉得每一个开发者都应该持续保持关注。
在AI大模型、RAG这个赛道,我们要做的事情:
技术:深挖AI大模型、RAG领域的技术细节(应用工程领域),从思想、代码、架构等领域都需要花120%的投入,知其所以然!
☑️ 考虑到我并非搞算法的人员,底层的大模型算法等内容,我觉得对于我自己的要求而言,做到知道、了解即可,甚至有必要的话,搞一张4090(太贵了,还没舍得买)的卡跑跑预训练的模型也是有必要的!
☑️ RAG领域在工程层面的知识所涉及的面也是足够广的,对于做应用开发者而言,也并非一朝一夕就能全盘了解其中的细节,需要做的就是两字:“深耕”
☑️ 技术层面有时候是需要较真一下的,解决关键的核心技术问题是对竞对产品的致命打击
产品:坚持价值输出导向,倾听客户的意见,坚决反对交钥匙工程
☑️ 技术上输出最终都会落地到实际应用产品的开发上,而产品需要坚持的事情则是要尊重用户,技术测则需要尊重产品
☑️ 价值输出导向是永恒的真理,客户付费买单,最终看中的也是这个产品所带来的价值,或者说能够给客户带来收益(赚钱才是硬道理)
☑️ 做顺势而为的事情,而大模型这一波技术浪潮,我觉得做这方面相关的内容或者产品技术探索,就是顺势了
这个公众号之前都是以Swagger/OpenAPI/Knife4j等领域相关的名称,因为一直在维护开源项目Knife4j,主要还是和接口相关的,就一直用了这些名称!思来想去,公众号的性质也是个人性质,以后更多的还是以个人的一些想法、工作实践等内容为主进行输出,就干脆换回之前一直用的网络昵称名称吧:八一菜刀
🆕 名称含义(该公众号之前注册时也是用的这个名称,相当于恢复出厂设置了)
八一:字面意思一致
菜刀:作者故乡来自湖南省桑植县,贺龙元帅故乡,贺老总两把菜刀闹革命是旗帜,以此纪念,同时也有时刻思念故乡之意!
目前看来,主要两个方面:
结合我最近的工作内容,我总结了一部分的大纲内容(后面随着学习的深入可能会扩展更多),如下图:
在技术层面,我愿称这方面的技术合集为”大模型应用工程技术“。
为什么这么说呢?主要几个方面:
而在”大模型应用工程技术“,我初步列了一个大纲(后面随着学习的深入可能会扩展更多)
目前包含的几个方面:
这些都只是AI大模型、RAG等领域的冰山一角,千里之行,始于足下,作为技术人员,那么就从现在开始,一步步学起来吧~
如果你最近也在做RAG领域相关的技术研究或者产品开发,欢迎关注、沟通交流合作!!!
]]>主要还是聚焦在Knife4j这个开源项目上,然后将自己的一些想法进行输出,并将一些在单体工具组件中无法解决落地的需求场景,共同灌注在这个新的产品中。
今天,Knife4jInsight平台版-MVP(Minimum Viable Product)最小可行性版本v1.0.0终于来了
Knife4jInsight是简单、方便的OpenAPI接口规范文档聚合开放平台!
产品地址:http://knife4j.net
在很多年前,我的工作中的老大哥卢员外(微信公众号:土猛的员外
),那时候我们经常讨论如何创造产品、一个公司的产品及商业模式要如何保持市场竞争力,多年过去了,令我印象最深刻的就是三级火箭理论
以360的产品三级火箭为例:
360的第一级火箭是免费杀毒工具。它利用这级火箭打破了持续10年的杀毒软件市场三国鼎立的局面,成为用户量最大的安全工具
360的第二级火箭是从免费杀毒工具变为安全网络平台,进而推出360安全浏览器和360安全网址导航
360的第三级火箭就是它最终承载的商业闭环,从安全浏览器和网址导航的广告收入,获得企业的经营利润
在迄今为止,我给Knife4j造了一些生态组件,主要如下:
将三级火箭理论应用到开源项目Knife4j上面,到今天为止,我觉得算是勉强完成了第一级别的火箭路程,我也希望能够将这个项目一直维护下去,按照这个产品理论去执行,算是一种人生经历。而Knife4jInsight平台版本的诞生,我觉得是时候去落地一些商业化的场景了
我不确定现在三级火箭理论是否已经过时,但创造更好的产品一直是每个技术人应该追求的目标
如果将开源项目Knife4j比做一次创业,那这正是一次践行实战之旅,做商业化的场景需求落地,从这个产品本身而言我觉得有几个好处:
该产品主要功能定位:
给产品取名是一件令人头痛的事情,从目前的功能定位来看,可能将该产品命名为Knife4jCloud
可能更合适一些,cloud意为云数据中心,将Knife4j界面功能提供的数据整合到云上,进行统一处理。
但我还是更钟意Knife4jInsight
,主要有几层含义:
Insight
有洞察之意,对于聚焦在API接口领域而言,提供对OpenAPI接口的全方位洞察、了解Cloud
,这为以后产品的新功能扩展迭代奠定基调哪怕目前Knife4jInsight
还没有达到产品名所定位的寓意高度,但也这驱使我们努力向前,为客户创造更有价值的功能。
技术架构图如下:
技术架构平台的定位是开放平台和接口文档管理平台进行职责区分:
在Knife4jInsight的前期,我们着重先把OpenAPI接口文档平台的功能做好,因为产品依靠开源项目Knife4j
起家,这是该产品的本职工作.
在功能架构中,我们加入了一些未来产品要加入的功能,虽然目前MVP版本并未实现,但会在迭代Knife4j开源版本的同时,保持对该版本的升级迭代
功能架构图如下:
在功能上,主要是三大块的功能:
平台的网关鉴权,通过实现Apache APIXIS的鉴权插件,植入到网关组件中,此时所有开放平台的网关入口流量,都会通过该插件与Knife4jInsight中的开发密钥进行联动,实现接口的鉴权。
Knife4jInsight
版本是商业化产品,但是我想既然面对的主要群体都是开发者,虽然是平台,但也更多的是工具,为开发者提供方便的工具
也思考了良久,最终产品价格定价在49.9元,主要是软件license的价格
主要体现在:
在目前Knife4jInsight在线版本中,可以在线体验,付费后不限Namespace、ApiRegister的数量
以Docker镜像提供交付,开发者可以将该版本独立部署在私有环境,保证企业数据安全
购买的License是永久期限使用,没有时间限制
License限定部署域名(最大支持5个域名/ip授权)
License限定平台更新周期,平台免费更新期限1年
即自购买该license后,Knife4jInsight在之后1年内的任何版本更新,都可以使用该license进行免费更新,超过期限后的新版本,则需要重新购买license
有任何技术问题可通过社区issue、交流群找到作者进行沟通反馈,或者通过邮箱:
xiaoymin@foxmail.com
与作者取得联系
Knife4jInsight提供了在线版本,域名:https://console.knife4j.net
开发者可以在线试用,及完成license的购买行为
目前是Knife4jInsight的MVP版本,该产品还在发展中,我给该产品规划了roadmap,主要如下:
如果您有好的想法或者建议,可以通过在开源项目Knife4j中提issues或者discussions进行反馈
功能 | 进度 | 发布日期 | 发布版本 |
---|---|---|---|
平台管理OpenAPI数据源接口文档自动i18n,支持中英双语 | 待开发 | - |
- |
微服务OpenAPI规范数据源自动注册上报 | 待开发 | - |
- |
整合开源swagger-ui组件,平台中可进行OpenAPI规范接口设计 | 待开发 | - |
- |
打通开源注册中心(Nacos\Eureka\Consul等等),获取服务中的OpenAPI数据源 | 待开发 | - |
- |
产品首页:http://knife4j.net
产品试用:https://console.knife4j.net
期待Knife4j和Knife4jInsight齐头并进,创造更好的产品服务!!!
]]>