引子
向量數(shù)據(jù)庫其實最早在傳統(tǒng)的人工智能和機器學(xué)習(xí)場景中就有所應(yīng)用。在大模型興起后,由于目前大模型的token數(shù)限制,很多開發(fā)者傾向于將數(shù)據(jù)量龐大的知識、新聞、文獻、語料等先通過嵌入(embedding)算法轉(zhuǎn)變?yōu)橄蛄繑?shù)據(jù),然后存儲在Chroma等向量數(shù)據(jù)庫中。當(dāng)用戶在大模型輸入問題后,將問題本身也embedding,轉(zhuǎn)化為向量,在向量數(shù)據(jù)庫中查找與之最匹配的相關(guān)知識,組成大模型的上下文,將其輸入給大模型,最終返回大模型處理后的文本給用戶,這種方式不僅降低大模型的計算量,提高響應(yīng)速度,也降低成本,并避免了大模型的tokens限制,是一種簡單高效的處理手段。此外,向量數(shù)據(jù)庫還在大模型記憶存儲等領(lǐng)域發(fā)揮其不可替代的作用。
由于大模型的火熱,現(xiàn)在市面上的向量數(shù)據(jù)庫眾多,主流的向量數(shù)據(jù)庫對比如下所示:
向量數(shù)據(jù)庫 | URL | GitHub Star | Language |
---|---|---|---|
chroma | https://github.com/chroma-core/chroma | 7.4K | Python |
milvus | https://github.com/milvus-io/milvus | 21.5K | Go/Python/C++ |
pinecone | https://www.pinecone.io/ | ? | ? |
qdrant | https://github.com/qdrant/qdrant | 11.8K | Rust |
typesense | https://github.com/typesense/typesense | 12.9K | C++ |
weaviate | https://github.com/weaviate/weaviate | 6.9K | Go |
表格引用自:一文全面了解向量數(shù)據(jù)庫的基本概念、原理、算法、選型
本文重點圍繞向量數(shù)據(jù)庫Chroma的使用和實戰(zhàn),主要包括以下內(nèi)容:
- Chroma設(shè)計理念
- Chroma常見概念(數(shù)據(jù)集,文檔,存儲,查詢,條件過濾)
- Chroma快速上手
- Chroma支持的Embeddings算法
- 實戰(zhàn):在Langchain中使用Chroma對中國古典四大名著進行相似性查詢
Chroma快速上手
設(shè)計理念
Chroma的目標(biāo)是幫助用戶更加便捷地構(gòu)建大模型應(yīng)用,更加輕松的將知識(knowledge)、事實(facts)和技能(skills)等我們現(xiàn)實世界中的文檔整合進大模型中。
Chroma提供的工具:
- 存儲文檔數(shù)據(jù)和它們的元數(shù)據(jù):store embeddings and their metadata
- 嵌入:embed documents and queries
- 搜索: search embeddings
Chroma的設(shè)計優(yōu)先考慮:
- 足夠簡單并且提升開發(fā)者效率:simplicity and developer productivity
- 搜索之上再分析:analysis on top of search
- 追求快(性能): it also happens to be very quick
目前官方提供了Python和JavaScript版本,也有其他語言的社區(qū)版本支持。
完整Demo
首先需要Python環(huán)境(Chroma官方原生支持Python和JavaScript,本文用Python做示例)
pip install chromadb
直接運行如下代碼,便是一個完整的Demo:
import chromadb
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(name="my_collection")
collection.add(
documents=["This is a document about engineer", "This is a document about steak"],
metadatas=[{"source": "doc1"}, {"source": "doc2"}],
ids=["id1", "id2"]
)
results = collection.query(
query_texts=["Which food is the best?"],
n_results=2
)
print(results)
上面的代碼中,我們向Chroma提交了兩個文檔(簡單起見,是兩個字符串),一個是This is a document about engineer,一個是This is a document about steak。若在add方法沒有傳入embedding參數(shù),則會使用Chroma默認(rèn)的all-MiniLM-L6-v2 方式進行embedding。隨后,我們對數(shù)據(jù)集進行query,要求返回兩個最相關(guān)的結(jié)果。提問內(nèi)容為:Which food is the best?
返回結(jié)果:
{
'ids': [
['id2', 'id1']
],
'distances': [
[1.5835548639297485, 2.1740970611572266]
],
'metadatas': [
[{
'source': 'doc2'
}, {
'source': 'doc1'
}]
],
'embeddings': None,
'documents': [
['This is a document about steak', 'This is a document about engineer']
]
}
結(jié)果顯示,兩個文檔都被正確返回,且id2由于是steak(牛排),相關(guān)性與我們的提問更大,排在了首位。還打印了distances。
簡單,易理解。
數(shù)據(jù)持久化
Chroma一般是直接作為內(nèi)存數(shù)據(jù)庫使用,但是也可以進行持久化存儲。
在初始化Chroma Client時,使用PersistentClient:
client = chromadb.PersistentClient(path="/Users/yourname/xxxx")
這樣在運行代碼后,在你指定的位置會新建一個chroma.sqlite3文件。
這個sqlite3的數(shù)據(jù)庫里包含的表如下圖,從中可以窺見一部分Chroma的數(shù)據(jù)存儲思路:
Chroma Client還支持下面兩個API:
client.heartbeat() # returns a nanosecond heartbeat. Useful for making sure the client remains connected.
client.reset() # Empties and completely resets the database. ?? This is destructive and not reversible.
此外,Chroma還支持服務(wù)端,客戶端模式,用于跨進程通信。詳見:
https://docs.trychroma.com/usage-guide#running-chroma-in-clientserver-mode
數(shù)據(jù)集(Collection)
collection是Chroma中一個重要的概念,下面的代碼和注釋簡單介紹了collection的主要功能和使用方法。
collection = client.get_collection(name="test") # Get a collection object from an existing collection, by name. Will raise an exception if it's not found.
collection = client.get_or_create_collection(name="test") # Get a collection object from an existing collection, by name. If it doesn't exist, create it.
client.delete_collection(name="my_collection") # Delete a collection and all associated embeddings, documents, and metadata. ?? This is destructive and not reversible
collection.peek() # returns a list of the first 10 items in the collection
collection.count() # returns the number of items in the collection
collection.modify(name="new_name") # Rename the collection
collection支持傳入一些自身的元數(shù)據(jù)metadata:
collection = client.create_collection(
name="collection_name",
metadata={"hnsw:space": "cosine"} # l2 is the default
)
collection允許用戶自行切換距離計算函數(shù),方法是通過設(shè)置cellection的metadata中的“hnsw:space”:
collection = client.create_collection(
name="collection_name",
metadata={"hnsw:space": "cosine"} # l2 is the default
)
Distance | parameter | Equation |
---|---|---|
Squared L2 | 'l2' | $d = \sum\left(A_i-B_i\right)^2$ |
Inner product | 'ip' | $d = 1.0 - \sum\left(A_i \times B_i\right) $ |
Cosine similarity | 'cosine' | $d = 1.0 - \frac{\sum\left(A_i \times B_i\right)}{\sqrt{\sum\left(A_i^2\right)} \cdot \sqrt{\sum\left(B_i^2\right)}}$ |
文檔(Document)
在上面的Demo中,我們使用了默認(rèn)的add函數(shù)。
def add(ids: OneOrMany[ID],
embeddings: Optional[OneOrMany[Embedding]] = None,
metadatas: Optional[OneOrMany[Metadata]] = None,
documents: Optional[OneOrMany[Document]] = None) -> None
除此之外,你還可以有如下傳參:
- ids: 文檔的唯一ID
- embeddings(可選): 如果不傳該參數(shù),將根據(jù)Collection設(shè)置的embedding_function進行計算。
- metadatas(可選):要與嵌入關(guān)聯(lián)的元數(shù)據(jù)。在查詢時,您可以根據(jù)這些元數(shù)據(jù)進行過濾。
- documents(可選):與該嵌入相關(guān)聯(lián)的文檔,甚至可以不放文檔。
示例:
collection.add(
embeddings=[[1.2, 2.3, 4.5], [6.7, 8.2, 9.2]],
documents=["This is a document", "This is another document"],
metadatas=[{"source": "my_source"}, {"source": "my_source"}],
ids=["id1", "id2"]
)
簡單查詢
輸入文檔內(nèi)的文本進行相似性查詢,可以使用query方法
collection.query(
query_embeddings=[[11.1, 12.1, 13.1],[1.1, 2.3, 3.2], ...],
n_results=10,
where={"metadata_field": "is_equal_to_this"},
where_document={"$contains":"search_string"}
)
若想要通過id查找,可以使用get方法
collection.get(
ids=["id1", "id2", "id3", ...],
where={"style": "style1"}
)
與此同時,你可以定制返回結(jié)果包含的數(shù)據(jù)
# Only get documents and ids
collection.get({
include: [ "documents" ]
})
collection.query({
queryEmbeddings: [[11.1, 12.1, 13.1],[1.1, 2.3, 3.2], ...],
include: [ "documents" ]
})
條件查詢
Chroma 支持按元數(shù)據(jù)和文檔內(nèi)容過濾查詢。
where 字段用于按元數(shù)據(jù)進行過濾
{
"metadata_field": {
<Operator>: <Value>
}
}
支持下列操作操作符:
$eq
- equal to (string, int, float)
$ne
- not equal to (string, int, float)
$gt
- greater than (int, float)
$gte
- greater than or equal to (int, float)
$lt
- less than (int, float)
$lte
- less than or equal to (int, float)
# is equivalent to
{
"metadata_field": {
"$eq": "search_string"
}
}
where_document 字段用于按文檔內(nèi)容進行過濾
# Filtering for a search_string
{
"$contains": "search_string"
}
使用邏輯運算符
可以在查詢條件中使用邏輯運算符
{
"$and": [
{
"metadata_field": {
<Operator>: <Value>
}
},
{
"metadata_field": {
<Operator>: <Value>
}
}
]
}
{
"$or": [
{
"metadata_field": {
<Operator>: <Value>
}
},
{
"metadata_field": {
<Operator>: <Value>
}
}
]
}
使用in/not in
in將返回metadata中包含給出列表中屬性值的文檔:
{
"metadata_field": {
"$in": ["value1", "value2", "value3"]
}
}
not in則與其相反:
{
"metadata_field": {
"$nin": ["value1", "value2", "value3"]
}
}
更新文檔
帶上ids,其他參數(shù)和add方法類似
collection.update(
ids=["id1", "id2", "id3", ...],
embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2], ...],
metadatas=[{"chapter": "3", "verse": "16"}, {"chapter": "3", "verse": "5"}, {"chapter": "29", "verse": "11"}, ...],
documents=["doc1", "doc2", "doc3", ...],
)
刪除文檔
提供ids,還允許附帶where條件進行刪除
collection.delete(
ids=["id1", "id2", "id3",...],
where={"chapter": "20"}
)
Chroma Embeddings算法
默認(rèn)Embeddings算法
Chroma默認(rèn)使用的是all-MiniLM-L6-v2模型來進行embeddings
官方預(yù)訓(xùn)練模型
你也可以直接使用官方預(yù)訓(xùn)練的托管在Huggingface上的模型
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('model_name')
The all-* models where trained on all available training data (more than 1 billion training pairs) and are designed as general purpose models. The all-mpnet-base-v2 model provides the best quality, while all-MiniLM-L6-v2 is 5 times faster and still offers good quality. Toggle All models to see all evaluated models or visit HuggingFace Model Hub to view all existing sentence-transformers models.
選擇非常多,你可以點擊官網(wǎng)查看每種預(yù)訓(xùn)練模型的詳細(xì)信息。
https://www.sbert.net/docs/pretrained_models.html
其他第三方Embeddings算法
你還可以使用其他第三方模型,包括第三方平臺,例如:
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key="YOUR_API_KEY",
model_name="text-embedding-ada-002"
)
其他包括Cohere,HuggingFace等。
自定義Embeddings算法
你甚至可以使用自己的本地Embeddings算法,Chroma留有擴展點:
from chromadb import Documents, EmbeddingFunction, Embeddings
class MyEmbeddingFunction(EmbeddingFunction):
def __call__(self, texts: Documents) -> Embeddings:
# embed the documents somehow
return embeddings
實戰(zhàn):在Langchain中使用Chroma對中國古典四大名著進行相似性查詢
很多人認(rèn)識Chroma是由于Langchain經(jīng)常將其作為向量數(shù)據(jù)庫使用。不過Langchain官方文檔里的Chroma示例使用的是英文Embeddings算法以及英文的文檔語料。官方文檔鏈接如下:
https://python.langchain.com/docs/modules/data_connection/vectorstores.html?highlight=chroma
既然我們是華語區(qū)博客,這本篇文章中,我們就嘗試用中文的語料和Embeddings算法來做一次實戰(zhàn)。
先貼上完整代碼,我們再來逐步解釋:
from langchain.document_loaders import TextLoader
from langchain.embeddings import ModelScopeEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma
import chardet
# 讀取原始文檔
raw_documents_sanguo = TextLoader('/Users/rude3knife/Desktop/三國演義.txt', encoding='utf-16').load()
raw_documents_xiyou = TextLoader('/Users/rude3knife/Desktop/西游記.txt', encoding='utf-16').load()
# 分割文檔
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
documents_sanguo = text_splitter.split_documents(raw_documents_sanguo)
documents_xiyou = text_splitter.split_documents(raw_documents_xiyou)
documents = documents_sanguo + documents_xiyou
print("documents nums:", documents.__len__())
# 生成向量(embedding)
model_id = "damo/nlp_corom_sentence-embedding_chinese-base"
embeddings = ModelScopeEmbeddings(model_id=model_id)
db = Chroma.from_documents(documents, embedding=embeddings)
# 檢索
query = "美猴王是誰?"
docs = db.similarity_search(query, k=5)
# 打印結(jié)果
for doc in docs:
print("===")
print("metadata:", doc.metadata)
print("page_content:", doc.page_content)
準(zhǔn)備原始文檔
我下載了三國演義和西游記的全文本txt,作為我們的知識庫,兩個文本都在1.5MB左右。
在這里還遇到一個小插曲,本以為下載下來的文本時UTF-8編碼,代碼寫成了encoding='utf-8'
,結(jié)果TextLoader怎么讀取都報編碼錯誤,用眼睛也沒法一下子判斷是什么編碼,問了GPT,可以用Python的chardet編碼庫判斷。如果你也遇到同樣的問題,可以也嘗試用該方法獲取編碼。
import chardet
def detect_file_encoding(file_path):
with open(file_path, 'rb') as f:
result = chardet.detect(f.read())
return result['encoding']
file_path = '/Users/rude3knife/Desktop/三國演義.txt'
encoding = detect_file_encoding(file_path)
print(f'The encoding of file {file_path} is {encoding}')
# 輸出
The encoding of file /Users/yangzhendong/Desktop/三國演義.txt is UTF-16
分隔文檔
通常來說文檔都是很大的,比如名著小說,法律文檔,我們通過langchain提供的CharacterTextSplitter來幫我們分割文本:
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
embedding
我們選擇魔搭平臺ModelScope里的通用中文embeddings算法(damo/nlp_corom_sentence-embedding_chinese-base)來作為我們的embedding算法。他有768維的向量。
為啥要選擇魔搭而不選擇Huggingface,因為...在代碼里跑Langchain,連魔搭平臺比較快,連不上Huggingface的原因你懂得。而且魔搭畢竟是達摩院的,自家人平臺的還得支持一下。
query
將兩個文檔準(zhǔn)備好后,我們進行提問,“美猴王是誰?” 要求返回5個相似答案。下面的返回的答案,可以看到,5個文檔都是取自西游記.txt中的文本。
==========
metadata: {'source': '/Users/yangzhendong/Desktop/西游記.txt'}
page_content: 美猴王一見,倒身下拜,磕頭不計其數(shù),口中只道:“師父,師父!我弟子志心朝禮,志心朝禮!”祖師道:“你是那方人氏?且說個鄉(xiāng)貫姓名明白,再拜。”猴王道:“弟子乃東勝神洲傲來國花果山水簾洞人氏?!弊鎺熀攘睿骸摆s出去!他本是個撒詐搗虛之徒,那里修什么道果!”猴王慌忙磕頭不住道:“弟子是老實之言,決無虛詐。”祖師道:“你既老實,怎么說東勝神洲?那去處到我這里,隔兩重大海,一座南贍部洲,如何就得到此?”猴王叩頭道:“弟子飄洋過海,登界游方,有十?dāng)?shù)個年頭,方才訪到此處。”祖師道:“既是逐漸行來的也罷。你姓什么?”猴王又道:“我無性。人若罵我我也不惱,若打我我也不嗔,只是陪個禮兒就罷了,一生無性。”祖師道:“不是這個性。你父母原來姓什么?”猴王道:“我也無父母?!弊鎺煹溃骸凹葻o父母,想是樹上生的?”猴王道:“我雖不是樹上生,卻是石里長的。我只記得花果山上有一塊仙石,其年石破,我便生也。”祖師聞言暗喜道:“這等說,卻是個天地生成的,你起來走走我看?!焙锿蹩v身跳起,拐呀拐的走了兩遍。
==========
metadata: {'source': '/Users/yangzhendong/Desktop/西游記.txt'}
page_content: 太宗更喜,教:“光祿寺設(shè)宴,開東閣酬謝?!焙鲆娝搅⒃陔A下,容貌異常,便問:“高徒果外國人耶?”長老俯伏道:“大徒弟姓孫,法名悟空,臣又呼他為孫行者。他出身原是東勝神洲傲來國花果山水簾洞人氏,因五百年前大鬧天宮,被佛祖困壓在西番兩界山石匣之內(nèi),蒙觀音菩薩勸善,情愿皈依,是臣到彼救出,甚虧此徒保護。二徒弟姓豬,法名悟能,臣又呼他為豬八戒。他出身原是福陵山云棧洞人氏,因在烏斯藏高老莊上作怪,即蒙菩薩勸善,虧行者收之,一路上挑擔(dān)有力,涉水有功。三徒弟姓沙,法名悟凈,臣又呼他為沙和尚。他出身原是流沙河作怪者,也蒙菩薩勸善,秉教沙門。那匹馬不是主公所賜者。”太宗道:“毛片相同,如何不是?”三藏道:“臣到蛇盤山鷹愁澗涉水,原馬被此馬吞之,虧行者請菩薩問此馬來歷,原是西海龍王之了,因有罪,也蒙菩薩救解,教他與臣作腳力。當(dāng)時變作原馬,毛片相同。幸虧他登山越嶺,跋涉崎嶇,去時騎坐,來時馱經(jīng),亦甚賴其力也。”
==========
metadata: {'source': '/Users/yangzhendong/Desktop/西游記.txt'}
page_content: 第七十回 妖魔寶放煙沙火 悟空計盜紫金鈴
卻說那孫行者抖擻神威,持著鐵棒,踏祥光起在空中,迎面喝道:“你是那里來的邪魔,待往何方猖獗!”那怪物厲聲高叫道:“吾黨不是別人,乃麒麟山獬豸洞賽太歲大王爺爺部下先鋒,今奉大王令,到此取宮女二名,伏侍金圣娘娘。你是何人,敢來問我!”行者道:“吾乃齊天大圣孫悟空,因保東土唐僧西天拜佛,路過此國,知你這伙邪魔欺主,特展雄才,治國祛邪。正沒處尋你,卻來此送命!”那怪聞言,不知好歹,展長槍就刺行者。行者舉鐵棒劈面相迎,在半空里這一場好殺:
棍是龍宮鎮(zhèn)海珍,槍乃人間轉(zhuǎn)煉鐵。凡兵怎敢比仙兵,擦著些兒神氣泄。大圣原來太乙仙,妖精本是邪魔孽。鬼祟焉能近正人,一正之時邪就滅。那個弄風(fēng)播土?;释?,這個踏霧騰云遮日月。丟開架子賭輸贏,無能誰敢夸豪杰!還是齊天大圣能,乒乓一棍槍先折。
==========
metadata: {'source': '/Users/yangzhendong/Desktop/西游記.txt'}
page_content: 菩薩引眾同入里面,與玉帝禮畢,又與老君、王母相見,各坐下,便問:“蟠桃盛會如何?”玉帝道:“每年請會,喜喜歡歡,今年被妖猴作亂,甚是虛邀也。”菩薩道:“妖猴是何出處?”玉帝道:“妖猴乃東勝神洲傲來國花果山石卵化生的。當(dāng)時生出,即目運金光,射沖斗府。始不介意,繼而成精,降龍伏虎,自削死籍。當(dāng)有龍王、閻王啟奏。朕欲擒拿,是長庚星啟奏道:‘三界之間,凡有九竅者,可以成仙。’朕即施教育賢,宣他上界,封為御馬監(jiān)弼馬溫官。那廝嫌惡官小,反了天宮。即差李天王與哪吒太子收降,又降詔撫安,宣至上界,就封他做個‘齊天大圣’,只是有官無祿。他因沒事干管理,東游西蕩。朕又恐別生事端,著他代管蟠桃園。他又不遵法律,將老樹大桃,盡行偷吃。及至設(shè)會,他乃無祿人員,不曾請他,他就設(shè)計賺哄赤腳大仙,卻自變他相貌入會,將仙肴仙酒盡偷吃了,又偷老君仙丹,又偷御酒若干,去與本山眾猴享樂。朕心為此煩惱,故調(diào)十萬天兵,天羅地網(wǎng)收伏。這一日不見回報,不知勝負(fù)如何?!?==========
metadata: {'source': '/Users/yangzhendong/Desktop/西游記.txt'}
page_content: 行者道:“實不瞞師父說,老孫五百年前,居花果山水簾洞大展英雄之際,收降七十二洞邪魔,手下有四萬七千群怪,頭戴的是紫金冠,身穿的是赭黃袍,腰系的是藍田帶,足踏的是步云履,手執(zhí)的是如意金箍棒,著實也曾為人。自從涅脖罪度,削發(fā)秉正沙門,跟你做了徒弟,把這個金箍兒勒在我頭上,若回去,卻也難見故鄉(xiāng)人。師父果若不要我,把那個《松箍兒咒》念一念,退下這個箍子,交付與你,套在別人頭上,我就快活相應(yīng)了,也是跟你一場。莫不成這些人意兒也沒有了?”唐僧大驚道:“悟空,我當(dāng)時只是菩薩暗受一卷《緊箍兒咒》,卻沒有什么松箍兒咒?!毙姓叩溃骸叭魺o《松箍兒咒》,你還帶我去走走罷?!遍L老又沒奈何道:“你且起來,我再饒你這一次,卻不可再行兇了。”行者道:“再不敢了,再不敢了。”又伏侍師父上馬,剖路前進。
卻說那妖精,原來行者第二棍也不曾打殺他。那怪物在半空中,夸獎不盡道:“好個猴王,著然有眼!我那般變了去,他也還認(rèn)得我。這些和尚,他去得快,若過此山,西下四十里,就不伏我所管了。若是被別處妖魔撈了去,好道就笑破他人口,使碎自家心,我還下去戲他一戲?!焙醚郑绰栮庯L(fēng),在山坡下?lián)u身一變,變成一個老公公,真?zhèn)€是:
總結(jié)
目前向量數(shù)據(jù)庫在AI中的應(yīng)用越來越重要,但很多廠商更傾向于將向量數(shù)據(jù)庫隱藏在產(chǎn)品內(nèi)部,用戶感知不到很多向量數(shù)據(jù)庫的使用細(xì)節(jié)。但大模型的學(xué)習(xí)終究是建立在開源代碼之上的,學(xué)習(xí)Chroma可以讓我們快速了解向量數(shù)據(jù)庫的基本原理,也有利于我們未來更好地理解大模型。
參考
https://guangzhengli.com/blog/zh/vector-database/#特征和向量
https://luxiangdong.com/2023/09/19/emb/#/Redis-VSS演示
https://blog.csdn.net/engchina/article/details/131868860
https://github.com/chroma-core/chroma文章來源:http://www.zghlxwxcb.cn/news/detail-746050.html
https://docs.trychroma.com/文章來源地址http://www.zghlxwxcb.cn/news/detail-746050.html
到了這里,關(guān)于向量數(shù)據(jù)庫Chroma極簡教程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!