LangChain
LangChain是一個以 LLM (大語言模型)模型為核心的開發(fā)框架,LangChain的主要特性:
- 可以連接多種數(shù)據(jù)源,比如網(wǎng)頁鏈接、本地PDF文件、向量數(shù)據(jù)庫等
- 允許語言模型與其環(huán)境交互
- 封裝了Model I/O(輸入/輸出)、Retrieval(檢索器)、Memory(記憶)、Agents(決策和調度)等核心組件
- 可以使用鏈的方式組裝這些組件,以便最好地完成特定用例。
圍繞以上設計原則,LangChain解決了現(xiàn)在開發(fā)人工智能應用的一些切實痛點。以 GPT 模型為例:
- 數(shù)據(jù)滯后,現(xiàn)在訓練的數(shù)據(jù)是到 2021年9月。
- token數(shù)量限制,如果讓它對一個300頁的pdf進行總結,直接使用則無能為力。
- 不能進行聯(lián)網(wǎng),獲取不到最新的內容。
- 不能與其他數(shù)據(jù)源鏈接。
另外作為一個膠水層框架,極大地提高了開發(fā)效率,它的作用可以類比于jquery在前端開發(fā)中的角色,使得開發(fā)者可以更專注于創(chuàng)新和優(yōu)化產品功能。
1、Model I/O
LangChain提供了與任何語言模型交互的構建塊,交互的輸入輸出主要包括:Prompts、Language models、Output parsers三部分。
1.1 Prompts
LangChain 提供了多個類和函數(shù),使構建和使用提示詞變得容易。Prompts模塊主要包含了模板化、動態(tài)選擇和管理模型輸入兩部分。其中:
1.1.1 Prompt templates
提示模版類似于ES6模板字符串,可以在字符串中插入變量或表達式,接收來自最終用戶的一組參數(shù)并生成提示。
一個簡單的例子:
const multipleInputPrompt = new PromptTemplate({
inputVariables: ["adjective", "content"],
template: "Tell me a {adjective} joke about {content}.",
});
const formattedMultipleInputPrompt = await multipleInputPrompt.format({
adjective: "funny",
content: "chickens",
});
console.log(formattedMultipleInputPrompt);
// "Tell me a funny joke about chickens.
同時可以通過 PipelinePrompt將多個PromptTemplate提示模版進行組合,組合的優(yōu)點是可以很方便的進行復用。比如常見的系統(tǒng)角色提示詞,一般都遵循以下結構:{introduction} {example} {start},比如一個【名人采訪】角色的提示詞:
使用PipelinePrompt組合實現(xiàn):
import { PromptTemplate, PipelinePromptTemplate } from "langchain/prompts";
const fullPrompt = PromptTemplate.fromTemplate(`{introduction}
{example}
{start}`);
const introductionPrompt = PromptTemplate.fromTemplate(
`You are impersonating {person}.`
);
const examplePrompt =
PromptTemplate.fromTemplate(`Here's an example of an interaction:
Q: {example_q}
A: {example_a}`);
const startPrompt = PromptTemplate.fromTemplate(`Now, do this for real!
Q: {input}
A:`);
const composedPrompt = new PipelinePromptTemplate({
pipelinePrompts: [
{
name: "introduction",
prompt: introductionPrompt,
},
{
name: "example",
prompt: examplePrompt,
},
{
name: "start",
prompt: startPrompt,
},
],
finalPrompt: fullPrompt,
});
const formattedPrompt = await composedPrompt.format({
person: "Elon Musk",
example_q: `What's your favorite car?`,
example_a: "Telsa",
input: `What's your favorite social media site?`,
});
console.log(formattedPrompt);
/*
You are impersonating Elon Musk.
Here's an example of an interaction:
Q: What's your favorite car?
A: Telsa
Now, do this for real!
Q: What's your favorite social media site?
A:
*/
1.1.2 Example selectors
為了大模型能夠給出相對精準的輸出內容,通常會在prompt中提供一些示例描述,如果包含大量示例會浪費token數(shù)量,甚至可能會超過最大token限制。為此,LangChain提供了示例選擇器,可以從用戶提供的大量示例中,選擇最合適的部分作為最終的prompt。通常有2種方式:按長度選擇和按相似度選擇。
按長度選擇:對于較長的輸入,它將選擇較少的示例來;而對于較短的輸入,它將選擇更多的示例。
...
// 定義長度選擇器
const exampleSelector = await LengthBasedExampleSelector.fromExamples(
[
{ input: "happy", output: "sad" },
{ input: "tall", output: "short" },
{ input: "energetic", output: "lethargic" },
{ input: "sunny", output: "gloomy" },
{ input: "windy", output: "calm" },
],
{
examplePrompt,
maxLength: 25,
}
);
...
// 最終會根據(jù)用戶的輸入長度,來選擇合適的示例
// 用戶輸入較少,選擇所有示例
console.log(await dynamicPrompt.format({ adjective: "big" }));
/*
Give the antonym of every input
Input: happy
Output: sad
Input: tall
Output: short
Input: energetic
Output: lethargic
Input: sunny
Output: gloomy
Input: windy
Output: calm
Input: big
Output:
*/
// 用戶輸入較多,選擇其中一個示例
const longString =
"big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else";
console.log(await dynamicPrompt.format({ adjective: longString }));
/*
Give the antonym of every input
Input: happy
Output: sad
Input: big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else
Output:
*/
按相似度選擇:查找與輸入具有最大余弦相似度的嵌入示例
...
// 定義相似度選擇器
const exampleSelector = await SemanticSimilarityExampleSelector.fromExamples(
[
{ input: "happy", output: "sad" },
{ input: "tall", output: "short" },
{ input: "energetic", output: "lethargic" },
{ input: "sunny", output: "gloomy" },
{ input: "windy", output: "calm" },
],
new OpenAIEmbeddings(),
HNSWLib,
{ k: 1 }
);
...
// 跟天氣類相關的示例
console.log(await dynamicPrompt.format({ adjective: "rainy" }));
/*
Give the antonym of every input
Input: sunny
Output: gloomy
Input: rainy
Output:
*/
// 跟尺寸相關的示例
console.log(await dynamicPrompt.format({ adjective: "large" }));
/*
Give the antonym of every input
Input: tall
Output: short
Input: large
Output:
*/
1.2 Language models
LangChain支持多種常見的Language models提供商(詳見附錄一),并提供了兩種類型的模型的接口和集成:
- LLM:采用文本字符串作為輸入并返回文本字符串的模型
- Chat models:由語言模型支持的模型,但將聊天消息列表作為輸入并返回聊天消息
定義一個LLM語言模型:
import { OpenAI } from "langchain/llms/openai";
// 實例化一個模型
const model = new OpenAI({
// OpenAI內置參數(shù)
openAIApiKey: "YOUR_KEY_HERE",
modelName: "text-davinci-002", //gpt-4、gpt-3.5-turbo
maxTokens: 25,
temperature: 1, //發(fā)散度
// LangChain自定義參數(shù)
maxRetries: 10, //發(fā)生錯誤后重試次數(shù)
maxConcurrency: 5, //最大并發(fā)請求次數(shù)
cache: true //開啟緩存
});
// 使用模型
const res = await model.predict("Tell me a joke");
取消請求和超時處理:
import { OpenAI } from "langchain/llms/openai";
const model = new OpenAI({ temperature: 1 });
const controller = new AbortController();
const res = await model.call(
"What would be a good name for a company that makes colorful socks?",
{
signal: controller.signal, //調用controller.abort()即可取消請求
timeout: 1000 //超時時間設置
}
);
流式響應:通常,當我們請求一個服務或者接口時,服務器會將所有數(shù)據(jù)一次性返回給我們,然后我們再進行處理。但是,如果返回的數(shù)據(jù)量很大,那么我們需要等待很長時間才能開始處理數(shù)據(jù)。
而流式響應則不同,它將數(shù)據(jù)分成多個小塊,每次只返回一部分數(shù)據(jù)給我們。我們可以在接收到這部分數(shù)據(jù)之后就開始處理,而不需要等待所有數(shù)據(jù)都到達。
import { OpenAI } from "langchain/llms/openai";
const model = new OpenAI({
maxTokens: 25,
});
const stream = await model.stream("Tell me a joke.");
for await (const chunk of stream) {
console.log(chunk);
}
/*
Q
:
What
did
the
fish
say
when
it
hit
the
wall
?
A
:
Dam
!
*/
此外,所有的語言模型都實現(xiàn)了Runnable 接口,默認實現(xiàn)了invoke
,batch
,stream
,map
等方法, 提供了對調用、流式傳輸、批處理和映射請求的基本支持
1.3 Output parsers
語言模型可以輸出文本或富文本信息,但很多時候,我們可能想要獲得結構化信息,比如常見的JSON結構可以和應用程序更好的結合。LangChain封裝了一下幾種輸出解析器:
名稱 | 中文名 | 解釋 |
---|---|---|
BytesOutputParser | 字節(jié)輸出 | 轉換為二進制數(shù)據(jù) |
CombiningOutputParser | 組合輸出 | 組合不同的解析器 |
CustomListOutputParser | 自定義列表輸出 | 指定分隔符并分割為數(shù)組格式 |
JsonOutputFunctionsParser | JSON函數(shù)輸出 | 結合OpenAI回調函數(shù)格式化輸出 |
OutputFixingParser | 錯誤修復 | 解析失敗時再次調用LLM以修復錯誤 |
StringOutputParser | 字符串輸出 | 轉換為字符串 |
StructuredOutputParser | 結構化輸出 | 通常結合Zod格式化為JSON對象 |
一個自定義列表的解析器案例:
...
const parser = new CustomListOutputParser({ length: 3, separator: "\n" });
const chain = RunnableSequence.from([
PromptTemplate.fromTemplate(
"Provide a list of {subject}.\n{format_instructions}"
),
new OpenAI({ temperature: 0 }),
parser,
]);
/* 最終生成的prompt
Provide a list of great fiction books (book, author).
Your response should be a list of 3 items separated by "\n" (eg: `foo\n bar\n baz`)
*/
const response = await chain.invoke({
subject: "great fiction books (book, author)",
format_instructions: parser.getFormatInstructions(),
});
console.log(response);
/*
[
'The Catcher in the Rye, J.D. Salinger',
'To Kill a Mockingbird, Harper Lee',
'The Great Gatsby, F. Scott Fitzgerald'
]
*/
一個完整的Model I/O案例:將一個國家的信息:名稱、首都、面積、人口等信息結構化輸出
2、Retrieval
一些LLM應用通常需要特定的用戶數(shù)據(jù),這些數(shù)據(jù)不屬于模型訓練集的一部分??梢酝ㄟ^檢索增強生成(RAG)的方式,檢索外部數(shù)據(jù),然后在執(zhí)行生成步驟時將其傳遞給 LLM 。LangChain 提供了 RAG 應用程序的所有構建模塊,包含以下幾個關鍵模塊:
2.1 Document loaders
Document loaders可以從各種數(shù)據(jù)源加載文檔。LangChain 提供了許多不同的文檔加載器以及與對應的第三方集成工具。下圖中,黃色顏色代表Loaders對應的npm第三方依賴庫。
返回的文檔對象格式如下:
interface Document {
pageContent: string;
metadata: Record<string, any>;
}
2.2 Document transformers
加載文檔后,通常需要進行數(shù)據(jù)處理,比如:將長文檔分割成更小的塊、過濾不需要的HTML標簽以及結構化處理等。LangChain提供了許多內置的文檔轉換器,可以輕松地拆分、組合、過濾和以其他方式操作文檔。
其中:
- RecursiveCharacterTextSplitter除了可以按指定分隔符進行分割外,還支持根據(jù)特定于語言的語法分割文本,比如:JavaScript、Python、Solidity 和 Rust 等流行語言,以及 Latex、HTML 和 Markdown。
- 當處理大量任意文檔集合時,簡單的文本分割可能會出現(xiàn)重疊文本的文檔,CharacterTextSplitter可以用元數(shù)據(jù)標記文檔,從而解決矛盾來源的信息等問題。
- 當提取 HTML 文檔以供以后檢索時,我們通常只對網(wǎng)頁的實際內容而不是語義感興趣。HtmlToTextTransformer和MozillaReadabilityTransformer都可以從文檔中剝離HTML標簽,從而使檢索更加有效
- MetadataTagger轉換器可以自動從文檔中提取元數(shù)據(jù),以便以后進行更有針對性的相似性搜索。
一個簡單文本分割示例:
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const text = `Hi.\n\nI'm Harrison.\n\nHow? Are? You?\nOkay then f f f f.
This is a weird text to write, but gotta test the splittingggg some how.\n\n
Bye!\n\n-H.`;
const splitter = new RecursiveCharacterTextSplitter({
separators: ["\n\n", "\n", " ", ""], //默認分隔符
chunkSize: 1000, //最終文檔的最大大小(以字符數(shù)計),默認1000
chunkOverlap: 200, //塊之間應該有多少重疊,默認200
});
const output = await splitter.createDocuments([text]);
2.3 Text embedding models
文本嵌入模型(Text embedding models)是用于創(chuàng)建文本數(shù)據(jù)的數(shù)值表示的模型。它可以將文本轉換為向量表示,從而在向量空間中進行語義搜索和查找相似文本。LangChain嵌入模型提供了標準接口,可以與多個Language models提供商(詳見附錄一)進行集成。
一個OpenAI的嵌入示例:通常要結合文檔(Document)和向量存儲(Vector stores)一起使用。
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
/* Create instance */
const embeddings = new OpenAIEmbeddings();
/* Embed queries */
const res = await embeddings.embedQuery("Hello world");
/*
[
-0.004845875, 0.004899438, -0.016358767, -0.024475135, -0.017341806,
0.012571548, -0.019156644, 0.009036391, -0.010227379, -0.026945334,
0.022861943, 0.010321903, -0.023479493, -0.0066544134, 0.007977734,
... 1436 more items
]
*/
/* Embed documents */
const documentRes = await embeddings.embedDocuments(["Hello world", "Bye bye"]);
/*
[
[
-0.0047852774, 0.0048640342, -0.01645707, -0.024395779, -0.017263541,
0.012512918, -0.019191515, 0.009053908, -0.010213212, -0.026890801,
0.022883644, 0.010251015, -0.023589306, -0.006584088, 0.007989113,
... 1436 more items
],
[
-0.009446913, -0.013253193, 0.013174579, 0.0057552797, -0.038993083,
0.0077763423, -0.0260478, -0.0114384955, -0.0022683728, -0.016509168,
0.041797023, 0.01787183, 0.00552271, -0.0049789557, 0.018146982,
... 1436 more items
]
]
*/
2.4 Vector stores
Vector stores是用于存儲和搜索嵌入式數(shù)據(jù)的一種技術,負責存儲嵌入數(shù)據(jù)并執(zhí)行向量搜索。它通過將文本或文檔轉換為嵌入向量,并在查詢時嵌入非結構化查詢,以檢索與查詢最相似的嵌入向量來實現(xiàn)。LangChain中提供了非常多的向量存儲方案,以下指南可幫助您為您的用例選擇正確的向量存儲:
- 如果您正在尋找可以在 Node.js 應用程序內運行,無需任何其他服務器來支持,那么請選擇HNSWLib、Faiss、LanceDB或CloseVector
- 如果您正在尋找可以在類似瀏覽器的環(huán)境內存中運行,那么請選擇MemoryVectorStore或CloseVector
- 如果您來自 Python 并且正在尋找類似于 FAISS 的東西,請嘗試HNSWLib或Faiss
- 如果您正在尋找可以在 Docker 容器中本地運行的開源全功能矢量數(shù)據(jù)庫,那么請選擇Chroma
- 如果您正在尋找一個提供低延遲、本地文檔嵌入并支持邊緣應用程序的開源矢量數(shù)據(jù)庫,那么請選擇Zep
- 如果您正在尋找可以在本地Docker 容器中運行或托管在云中的開源生產就緒矢量數(shù)據(jù)庫,那么請選擇Weaviate。
- 如果您已經在使用 Supabase,那么請查看Supabase矢量存儲。
- 如果您正在尋找可用于生產的矢量存儲,不需要自己托管,那么就選擇Pinecone
- 如果您已經在使用 SingleStore,或者您發(fā)現(xiàn)自己需要分布式高性能數(shù)據(jù)庫,那么您可能需要考慮SingleStore矢量存儲。
- 如果您正在尋找在線 MPP(大規(guī)模并行處理)數(shù)據(jù)倉庫服務,您可能需要考慮AnalyticDB矢量存儲。
- 如果您正在尋找一個經濟高效的矢量數(shù)據(jù)庫,允許使用 SQL 運行矢量搜索,那么MyScale就是您的最佳選擇。
- 如果您正在尋找可以從瀏覽器端和服務器端加載的矢量數(shù)據(jù)庫,請查看CloseVector。它是一個旨在跨平臺的矢量數(shù)據(jù)庫。
示例:讀取本地文檔,創(chuàng)建MemoryVectorStore和檢索
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { TextLoader } from "langchain/document_loaders/fs/text";
// Create docs with a loader
const loader = new TextLoader("src/document_loaders/example_data/example.txt");
const docs = await loader.load();
// Load the docs into the vector store
const vectorStore = await MemoryVectorStore.fromDocuments(
docs,
new OpenAIEmbeddings()
);
// Search for the most similar document
const resultOne = await vectorStore.similaritySearch("hello world", 1);
console.log(resultOne);
/*
[
Document {
pageContent: "Hello world",
metadata: { id: 2 }
}
]
*/
2.5 Retrievers
檢索器(Retriever)是一個接口:根據(jù)非結構化查詢返回文檔。它比Vector Store更通用,創(chuàng)建Vector Store后,將其用作檢索器的方法非常簡單:
...
retriever = vectorStore.asRetriever()
此外,LangChain還提供了他類型的檢索器,比如:
- ContextualCompressionRetriever:用給定查詢的上下文來壓縮它們,以便只返回相關信息,而不是立即按原樣返回檢索到的文檔,同時還可以減少token數(shù)量。
- MultiQueryRetriever:從不同角度為給定的用戶輸入查詢生成多個查詢。
- ParentDocumentRetriever:在檢索過程中,它首先獲取小塊,然后查找這些塊的父 ID,并返回那些較大的文檔。
- SelfQueryRetriever:一種能夠查詢自身的檢索器。
- VespaRetriever:從Vespa.ai數(shù)據(jù)存儲中檢索文檔。
針對不同的需求場景,可能需要對應的合適的檢索器。以下是一個根據(jù)通過計算相似度分值檢索的示例:
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { ScoreThresholdRetriever } from "langchain/retrievers/score_threshold";
const vectorStore = await MemoryVectorStore.fromTexts(
[
"Buildings are made out of brick",
"Buildings are made out of wood",
"Buildings are made out of stone",
"Buildings are made out of atoms",
"Buildings are made out of building materials",
"Cars are made out of metal",
"Cars are made out of plastic",
],
[{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
new OpenAIEmbeddings()
);
const retriever = ScoreThresholdRetriever.fromVectorStore(vectorStore, {
minSimilarityScore: 0.9, // Finds results with at least this similarity score
maxK: 100, // The maximum K value to use. Use it based to your chunk size to make sure you don't run out of tokens
kIncrement: 2, // How much to increase K by each time. It'll fetch N results, then N + kIncrement, then N + kIncrement * 2, etc.
});
const result = await retriever.getRelevantDocuments(
"What are buildings made out of?"
);
console.log(result);
/*
[
Document {
pageContent: 'Buildings are made out of building materials',
metadata: { id: 5 }
},
Document {
pageContent: 'Buildings are made out of wood',
metadata: { id: 2 }
},
Document {
pageContent: 'Buildings are made out of brick',
metadata: { id: 1 }
},
Document {
pageContent: 'Buildings are made out of stone',
metadata: { id: 3 }
},
Document {
pageContent: 'Buildings are made out of atoms',
metadata: { id: 4 }
}
]
*/
一個完整的Retrieval案例:從指定URL地址(靜態(tài)網(wǎng)站)中加載文檔信息,進行分割生成嵌入信息并存儲為向量,跟據(jù)用戶的問題進行檢索。(請使用公開信息,防止隱私數(shù)據(jù)泄漏)
3、Chains
Chains是一種將多個組件組合在一起創(chuàng)建單一、連貫應用程序的方法。通過使用Chains,我們可以創(chuàng)建一個接受用戶輸入、使用PromptTemplate格式化輸入并將格式化的響應傳遞給LLM的鏈。我們可以通過將多個鏈組合在一起或將鏈與其他組件組合來構建更復雜的鏈。LangChain中內置了很多不同類型的Chain:
其中:
- LLMChain:最基本的鏈。它采用提示模板,根據(jù)用戶輸入對其進行格式化,然后返回LLM的響應。
- SimpleSequentialChain和SequentialChain:一個調用的輸出用作另一個調用的輸入,進行一系列調用。前者每個步驟都有一個單一的輸入/輸出,后者更通用,允許多個輸入/輸出。
- loadQAStuffChain、loadQARefineChain、loadQAMapReduceChain、loadSummarizationChain和AnalyzeDocumentChain這些是處理文檔的核心鏈。它們對于總結文檔、回答文檔問題、從文檔中提取信息等很有用。
- APIChain:允許使用 LLM 與 API 交互以檢索相關信息。通過提供與所提供的 API 文檔相關的問題來構建鏈。
- createOpenAPIChain:可以僅根據(jù) OpenAPI 規(guī)范自動選擇和調用 API。它將輸入 OpenAPI 規(guī)范解析為 OpenAI 函數(shù) API 可以處理的 JSON 模式。
- loadSummarizationChain:摘要鏈可用于匯總多個文檔,生成摘要。
- createExtractionChainFromZod:從輸入文本和所需信息的模式中提取結構化信息。
- MultiPromptChain:基于RouterChain,從多個prompt中選擇合適的一個,比如定義多個老師的提示。
- MultiRetrievalQAChain:基于RouterChain,從多個檢索器中動態(tài)選擇。
以下是一個從【2020年美國國情咨文】中生成摘要的示例:
import { OpenAI } from "langchain/llms/openai";
import { loadSummarizationChain, AnalyzeDocumentChain } from "langchain/chains";
import * as fs from "fs";
// In this example, we use the `AnalyzeDocumentChain` to summarize a large text document.
const text = fs.readFileSync("state_of_the_union.txt", "utf8");
const model = new OpenAI({ temperature: 0 });
const combineDocsChain = loadSummarizationChain(model);
const chain = new AnalyzeDocumentChain({
combineDocumentsChain: combineDocsChain,
});
const res = await chain.call({
input_document: text,
});
console.log({ res });
/*
{
res: {
text: ' President Biden is taking action to protect Americans from the COVID-19 pandemic and Russian aggression, providing economic relief, investing in infrastructure, creating jobs, and fighting inflation.
He is also proposing measures to reduce the cost of prescription drugs, protect voting rights, and reform the immigration system. The speaker is advocating for increased economic security, police reform, and the Equality Act, as well as providing support for veterans and military families.
The US is making progress in the fight against COVID-19, and the speaker is encouraging Americans to come together and work towards a brighter future.'
}
}
*/
4、GPTs
Open AI最新發(fā)布會,發(fā)布了GPTs相關的功能:用戶可以用自然語言的方式,來構建自己的GPT應用:簡單的比如一個根據(jù)提示詞生成的各種系統(tǒng)角色;或者通過自定義Action實現(xiàn)一些復雜的功能:比如調用第三方API、讀取本地或網(wǎng)絡文檔等。在一定程度上可以不用通過LangChain等編碼來實現(xiàn)增強檢索等,但是LangChain的一些思路和實現(xiàn)還是值得學習和借鑒的,比如LangChain中可以使用本地化部署的LLM和向量存儲等,來解決隱私數(shù)據(jù)泄漏問題。
參考文獻:
https://js.langchain.com/docs/get_started/introduction
一文入門最熱的LLM應用開發(fā)框架LangChain
作者:京東科技 牛志偉文章來源:http://www.zghlxwxcb.cn/news/detail-747391.html
來源:京東云開發(fā)者社區(qū) 轉載請注明來源文章來源地址http://www.zghlxwxcb.cn/news/detail-747391.html
到了這里,關于TS版LangChain實戰(zhàn):基于文檔的增強檢索(RAG)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!