魯迅說沒說?大模型來搞定…

你好,我是郭震
這篇文章來自Zilliz江浩,一篇關於大模型向量資料庫實戰教程,推薦學習。
2024年B站最火的RAG影片是什麼?
那一定是up主“阿威的衝浪日記”釋出的《我宣佈向量資料庫才是查詢魯迅說沒說的最優解法》,B站播放破百萬,是今年最火的RAG影片。
而這背後的技術支撐,來自開源向量資料庫Milvus。
B站影片連結:
https://www.bilibili.com/video/BV1ZmBQYjEea/?share_source=copy_web&vd_source=7c31b4c90861db40c5b220f32fbc4899
01.
這真的是魯迅說的嗎?
“橫眉冷對千夫指,俯首甘為孺子牛。”——《自嘲》
“小的時候,不把他當人;大了以後,也做不了人。”——《熱風・隨感錄二十五》
“從來如此,便對嗎?”——《狂人日記》
這些都是魯迅先生的名言。
但:自己因為天太冷不想去廁所,於是就“看夜半無人時,即從視窗潑下去”;卻在看到別人溜到樓下牆角來小便的時候,就會用橡皮筋和紙團做成彈弓,朝著人家屁股就是“嗖”地一下。
這樣生動鮮活的,也是魯迅先生。
作為一個在課本中讓無數中國學生為之膽怯的存在,長大後,某時某刻,我們總會與魯迅先生擲地有聲的話語驀然相逢,總能激起靈魂中難以言喻的震顫與共鳴。
但是,網上逐漸出現了很多偽魯迅語錄,比如“學醫救不了中國人”,“抓捕周樹人,和我魯迅有什麼關係”,流傳之廣以至於很多人都以為魯迅真的說過這些話。
於是,出現了一個神器——北京魯迅博物館資料查詢線上檢測系統,可以鑑定某句話究竟是不是魯迅說的。但是,這個網站有個很大的問題,只要輸錯了一個字,就找不到對應的原文
要解決這個問題,向量資料庫的語義搜尋就派上用場了。如果我們將魯迅作品集向量化,儲存到向量資料庫中,然後透過模糊搜尋某條據說是魯迅說過的話,最後透過大模型組織語言輸出回答,就能知道這句話是不是出自魯迅之口。
下面,就讓我們一起探索如何用向量資料庫辨別魯迅說沒說過。
02.
準備工作
首先安裝向量資料庫 Milvus。Milvus 支援本地,Docker 和 K8s 部署。本文使用 Docker 執行 Milvus,所以需要先安裝 Docker Desktop。MacOS 系統安裝方法:Install Docker Desktop on Mac (https://docs.docker.com/desktop/install/mac-install/),Windows 系統安裝方法:Install Docker Desktop on Windows(https://docs.docker.com/desktop/install/windows-install/)
然後安裝 Milvus(Milvus 版本:>=2.4.0)。下載安裝指令碼:

curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh

執行 Milvus:

bash standalone_embed.sh start

安裝依賴:

!pip install pymilvus==2.4.7 

'pymilvus[model]'

 torch

下載魯迅作品集(https://github.com/sun510001/luxun_dataset),在原資料的基礎上,設定以下欄位:'book'、'title'、'author'、'type'、'source'、'date'和'content',但是因為在原資料的基礎上整理,有些欄位的值為空。整理後的資料請見魯迅作品資料集(https://github.com/BushJiang/LuXun_dataset),它的格式如下:

        {

'book'

'偽自由書'

'title'

'最藝術的國家'

'author'

'魯迅'

'type'

''

'source'

''

'date'

''

'content'

'我們中國的最偉大最永久,而且最普遍的“藝術”是男人扮女人... 

        }, 

        {

                '

book

': '

偽自由書

', 

                '

title

': '

王道詩話

', 

                '

author

': '

魯迅

', 

                '

type

': '', 

                '

source

': '', 

                '

date

': '', 

                '

content

': '

《人權論》是從鸚鵡開頭的,據說古時候有一隻高飛遠走的鸚哥兒... 

        }, 

        ...

]

03.
向量化文字:字數太多怎麼向量化
首先定義函式 vectorize_query 把文字向量化的函式。

import torch

import json

from pymilvus import DataType, MilvusClient

from pymilvus.model.hybrid import BGEM3EmbeddingFunction

# 將輸入的文字向量化

def vectorize_query(query , model_name = 

'BAAI/bge-small-zh-v1.5'

):

# 檢查是否有可用的CUDA裝置

    device = 

'cuda:0'if

 torch.cuda.is_available() 

else'cpu'
# 根據裝置選擇是否使用fp16

    use_fp16 = device.startswith(

'cuda'

)

# 建立嵌入模型例項

    bge_m3_ef = BGEM3EmbeddingFunction(

        model_name=model_name,

        device=device,

        use_fp16=use_fp16

    )

# 把輸入的文字向量化

    vectors = bge_m3_ef.encode_documents(query)

return

 vectors

下一步就是把魯迅作品集向量化了。但是,魯迅作品集中的“content”欄位的值,就是一篇文章。有的文章字數多達幾萬字,用幾百維的向量根本無法表達文章的語義細節。怎麼辦?就像前面說的,既然全文字數太多,我們就把文章切成幾塊,對每個塊再做向量化,這個操作叫做“分塊”。
下面將介紹三種常見的分塊方法,並且比較基於它們的向量搜尋和 RAG 響應有什麼區別。
方法一:根據固定字數分塊
最簡單的分塊方法是 fixed_chunk(固定分塊),是按照字數分塊,比如每隔150個字就分割一次。
優點:足夠簡單粗暴
缺點:經常在句子中間分割,導致句子不連貫,語義的完整性被破壞。
在此基礎上,我們嘗試了方法二:根據標點符號分割。
優點:可以基於相對完整的語義進行分析。
缺點:塊與塊之間仍然是相互獨立的,缺少關聯。打個比方,如果看《生活大爆炸》這樣的單元劇,我們跳著看也沒關係,不影響理解劇情。但是如果看《天龍八部》這樣的連續劇,上一集講的還是段譽為救鍾靈去萬劫谷拿解藥,下一集他就瞬移到了少室山,用六脈神劍大戰慕容復。我們會一頭霧水,這中間到底發生了什麼?
如何完善:連續劇的開頭有“前情提要”,結尾有“下集預告”。同樣,為了保證塊與塊之間語義的連貫,我們也要設計一個“重疊”部分,讓下一個塊的開頭部分,重複上一個塊的結尾部分。具體操作中,我們可以使用 LlamaIndex[^2] 庫輕鬆實現這種分塊方法—— semantic_chunk 。
方法三:根據句子分塊
對於上面的分塊結果,你可能還不滿意。雖然它根據標點符號分割,但是並不一定在句號處分割,無法保證句子的完整性。比如,對於這句話 我們中國的最偉大最永久,而且最普遍的“藝術”是男人扮女人。這藝術的可貴,是在於兩面光,或謂之“中庸”—男人看見“扮女人”,女人看見“男人扮”。可能分割成 我們中國的最偉大最永久,而且最普遍的“藝術”是男人扮女人。這藝術的可貴 和 是在於兩面光,或謂之“中庸”—男人看見“扮女人”,女人看見“男人扮” 兩個塊。
為了解決這個問題,又誕生了一種分塊方法,它根據句子而不是字數分割,也就是說,根據“。”、“!”和“?”這三個表示句子結束的標點符號分割,而不會受到字數的限制。但是,這種分割方式怎麼實現重疊的功能呢?這也簡單,把整個句子作為重疊部分就行了,叫做“視窗句子”。這種分塊方法叫做 sentence_window。
比如,對於句子 ABCD,設定視窗大小為1,表示原始句子的左右各1個句子為“視窗句子”。分塊如下:
第一個句子:A。視窗句子:B。因為第一個句子的左邊沒有句子。
第二個句子:B。視窗句子:A 和 C。
第三個句子:C。視窗句子:B 和 D。
第四個句子:D。視窗句子:C。因為最後一個句子的右邊沒有句子。
前面兩種分塊方法,都是對 chunk 欄位向量化。而這種分塊方法,除了對 chunk 欄位(也就是原始句子)向量化外,還會把視窗句子作為原始句子的上下文,以元資料的形式儲存在檔案中。
原始句子用來做向量搜尋,而在生成回答時,視窗句子和原始句子會一起傳遞給大模型。這樣做的好處是,只向量化原始句子,節省了儲存空間。提供視窗句子作為原始句子的上下文,可以幫助大模型理解原始句子的語境。
04.
建立向量資料庫
文字分塊完成,接下來就是文字向量化,匯入向量資料庫了,這部分你應該比較熟悉了,我直接給出程式碼。
定義函式 vectorize_file,向量化 json 檔案中指定的欄位。

def vectorize_file(input_file_path, output_file_path, field_name):

# 讀取 json 檔案,把chunk欄位的值向量化

    with open(input_file_path, 

'r'

, encoding=

'utf-8'

) as file:

        data_list = json.load(file)

# 提取該json檔案中的所有chunk欄位的值

        query = [data[field_name] 

for

 data 

in

 data_list]

# 向量化文字資料

    vectors = vectorize_query(query)

# 將向量新增到原始文字中
for

 data, vector 

in

 zip(data_list, vectors[

'dense'

]):

# 將 NumPy 陣列轉換為 Python 的普通列表

        data[

'vector'

] = vector.tolist()

# 將更新後的文字內容寫入新的json檔案

    with open(output_file_path, 

'w'

, encoding=

'utf-8'

) as outfile:

        json.dump(data_list, outfile, ensure_ascii=False, indent=4)

為了比較 RAG 使用不同分塊方法的效果,我們把三個分塊檔案全部向量化。
# 向量化固定分塊的檔案

vectorize_file(

'luxun_sample_fixed_chunk.json'

'luxun_sample_fixed_chunk_vector.json'

'chunk'

# 向量化透過標點符號分塊的檔案

vectorize_file(

'luxun_sample_semantic_chunk.json'

'luxun_sample_semantic_chunk_vector.json'

'chunk'

# 向量化透過句子分塊的檔案

vectorize_file(

'luxun_sample_sentence_window.json'

,

'luxun_sample_sentence_window_vector.json'

'chunk'

接下來建立集合。為了能夠在同一個集合中區分三種分塊方法的搜尋結果,我們設定引數 partition_key_field 的值為 method,它表示採用的分塊方法。Milvus 會根據 method 欄位的值,把資料插入到對應的分割槽中。打個比方,如果把集合看作一個 excel 檔案,partition (分割槽)就是表格的工作表(Worksheet)。一個 excel 檔案包含多張工作表,不同的資料填寫在對應的工作表中。相應的,我們把不同的資料插入到對應分割槽中,搜尋時指定分割槽,就可以提高搜尋效率。
# 建立集合

from pymilvus import MilvusClient, DataType

import time
def create_collection(collection_name):

# 檢查同名集合是否存在,如果存在則刪除
if

 milvus_client.has_collection(collection_name):

print

(f

'集合 {collection_name} 已經存在'

)

        try:

# 刪除同名集合

            milvus_client.drop_collection(collection_name)

print

(f

'刪除集合:{collection_name}'

)

        except Exception as e:

print

(f

'刪除集合時出現錯誤: {e}'

)

# 建立集合模式

    schema = MilvusClient.create_schema(

        auto_id=False,

        enable_dynamic_field=True,

# 設定partition key

        partition_key_field = 

'method'

,

# 設定分割槽數量,預設為16

        num_partitions=16,

        description=

''

    )

# 新增欄位到schema

    schema.add_field(field_name=

'id'

, datatype=DataType.VARCHAR, is_primary=True, max_length=256)

    schema.add_field(field_name=

'book'

, datatype=DataType.VARCHAR, max_length=128)

    schema.add_field(field_name=

'title'

, datatype=DataType.VARCHAR, max_length=128)

    schema.add_field(field_name=

'author'

, datatype=DataType.VARCHAR, max_length=64)

    schema.add_field(field_name=

'type'

, datatype=DataType.VARCHAR, max_length=64)

    schema.add_field(field_name=

'source'

, datatype=DataType.VARCHAR, max_length=64)

    schema.add_field(field_name=

'date'

, datatype=DataType.VARCHAR, max_length=32)

    schema.add_field(field_name=

'chunk'

, datatype=DataType.VARCHAR, max_length=2048)

    schema.add_field(field_name=

'window'

, datatype=DataType.VARCHAR, default_value=

''

, max_length=6144)

    schema.add_field(field_name=

'method'

, datatype=DataType.VARCHAR, max_length=32)

    schema.add_field(field_name=

'vector'

, datatype=DataType.FLOAT_VECTOR, dim=512)

# 建立集合

    try:

        milvus_client.create_collection(

            collection_name=collection_name,

            schema=schema,

            shards_num=2

        )

print

(f

'建立集合:{collection_name}'

)

    except Exception as e:

print

(f

'建立集合的過程中出現了錯誤: {e}'

)

# 等待集合建立成功
while

 not milvus_client.has_collection(collection_name):

# 獲取集合的詳細資訊

        time.sleep(1)

if

 milvus_client.has_collection(collection_name):

print

(f

'集合 {collection_name} 建立成功'

)

    collection_info = milvus_client.describe_collection(collection_name)

print

(f

'集合資訊: {collection_info}'

)
collection_name = 

'LuXunWorks'

uri=

'http://localhost:19530'

milvus_client = MilvusClient(uri=uri)

create_collection(collection_name)

把資料插入到向量資料庫。

from tqdm import tqdm

# 資料入庫

def insert_vectors(file_path, collection_name, batch_size):

# 讀取和處理檔案

    with open(file_path, 

'r'

) as file:

        data = json.load(file)

# 將資料插入集合
print

(f

'正在將資料插入集合:{collection_name}'

)

    total_count = len(data)

# pbar 是 tqdm 庫中的一個進度條物件,用於顯示插入資料的進度

    with tqdm(total=total_count, desc=

'插入資料'

) as pbar:

# 每次插入 batch_size 條資料
for

 i 

in

 range(0, total_count, batch_size):  

            batch_data = data[i:i + batch_size]

            res = milvus_client.insert(

                collection_name=collection_name,

                data=batch_data

            )

            pbar.update(len(batch_data))

# 驗證資料是否成功插入集合
print

(f

'插入的實體數量: {total_count}'

)

# 設定每次插入的資料量

batch_size = 100

insert_vectors(

'luxun_sample_fixed_chunk_vector.json'

, collection_name, batch_size)

insert_vectors(

'luxun_sample_semantic_chunk_vector.json'

, collection_name, batch_size)

insert_vectors(

'luxun_sample_sentence_window_vector.json'

, collection_name, batch_size)

建立索引。我們使用倒排索引,首先建立索引引數。

index_params = milvus_client.prepare_index_params()
index_params.add_index(

# 指定索引名稱

    index_name=

'IVF_FLAT'

,

# 指定建立索引的欄位

    field_name=

'vector'

,

# 設定索引型別

    index_type=

'IVF_FLAT'

,

# 設定度量方式

    metric_type=

'IP'

,

# 設定索引聚類中心的數量

    params={

'nlist'

: 128}

)

接下來建立索引。

milvus_client.create_index(

# 指定為建立索引的集合

    collection_name=collection_name,

# 使用前面建立的索引引數建立索引

    index_params=index_params

)

驗證下索引是否成功建立。檢視集合的所有索引。

res = milvus_client.list_indexes(

    collection_name=collection_name

)

print

(res)

返回我們建立的索引 ['IVF_FLAT']。再檢視下索引的詳細資訊。

res = milvus_client.describe_index(

    collection_name=collection_name,

    index_name=

'IVF_FLAT'

)

print

(res)

返回下面的索引資訊,表示索引建立成功:

{

'nlist'

'128'

'index_type'

'IVF_FLAT'

'metric_type'

'IP'

'field_name'

'vector'

'index_name'

'IVF_FLAT'

'total_rows'

: 0, 

'indexed_rows'

: 0, 

'pending_index_rows'

: 0, 

'state'

'Finished'

}

接下來載入集合到記憶體。
print

 (f

'正在載入集合:{collection_name}'

)

milvus_client.load_collection (collection_name=collection_name)

驗證下載入狀態。
print

 (milvus_client.get_load_state (collection_name=collection_name))

如果返回 {'state': <LoadState: Loaded>},說明載入完成。接下來,我們定義搜尋函式。
先定義搜尋引數。

search_params = {

# 度量型別
'metric_type'

'IP'

,

# 搜尋過程中要查詢的聚類單元數量。增加nprobe值可以提高搜尋精度,但會降低搜尋速度
'params'

: {

'nprobe'

: 16}

}

再定義搜尋函式。還記得前面我們在建立集合時,設定的 partition_key_field 嗎?它會根據 method 欄位的值,把資料插入到相應的分割槽中。而搜尋函式中的 filter 引數,就是用來指定在哪個分割槽中搜索的。

def vector_search(

    query, 

    search_params,

limit

,

    output_fields,

    partition_name

):

# 將查詢轉換為向量

query_vectors = [vectorize_query(query)[

'dense'

][0].tolist()]

# 向量搜尋

res = milvus_client.search(

    collection_name=collection_name,

# 指定查詢向量

    data=query_vectors,

# 指定搜尋的欄位

    anns_field=

'vector'

,

# 設定搜尋引數

    search_params=search_params,

# 設定搜尋結果的數量
limit

=

limit

,

# 設定輸出欄位

    output_fields=output_fields,

# 在指定分割槽中搜索

    filter=f

'method =='{partition_name}''

)

return

 res

再定義一個列印搜尋結果的函式,方便檢視。
# 列印向量搜尋結果

def print_vector_results(res):

# hit是搜尋結果中的每一個匹配的實體

    res = [hit[

'entity'

for

 hit 

in

 res[0]]

for

 item 

in

 res:

print

(f

'title: {item['title']}'

)

print

(f

'chunk: {item['chunk']}'

)

print

(f

'method: {item['method']}'

)

print

(

'-'

*50)   

05.
呼叫大模型的 API
建立向量資料庫這部分想必你已經輕車熟路了,下面我們來完成 RAG 應用的最後一個部分:生成。我們要把搜尋到的句子傳遞給大模型,讓它根據提示詞重新組裝成回答。
首先,我們要建立一個大模型的 api key,用來呼叫大模型。我使用的是 deepseek。為了保護 api key 的安全,把 api key 設定為環境變數“DEEPSEEK_API_KEY”。請把 <you_api_key> 替換成你自己的 api key。

import os

os.environ[

'DEEPSEEK_API_KEY'

] = <you_api_key>

然後,再從環境變數中讀取 api key。

deepseek_api_key = os.getenv(

'DEEPSEEK_API_KEY'

)

deepseek 使用與 OpenAI 相容的 API 格式,我們可以使用 OpenAI SDK 來訪問 DeepSeek API。
# 安裝 openai 庫

pip install openai

接下來建立 openai 客戶端例項。
# 匯入openai庫

from openai import OpenAI

# 匯入os庫

import os

# 建立openai客戶端例項

OpenAI_client = OpenAI(api_key=deepseek_api_key, base_url=

'https://api.deepseek.com'

)

根據 deepseek api 文件的說明,定義生成響應的函式 generate_responsemodel 是我們使用的大模型,這裡是 deepseek-chattemperature 決定大模型回答的隨機性,數值在0-2之間,數值越高,生成的文字越隨機;值越低,生成的文字越確定。
# 定義生成響應的函式

def generate_response(

        system_prompt, 

        user_prompt, 

        model, 

        temperature

    ):

# 大模型的響應

    response = OpenAI_client.chat.completions.create(

        model=model,

        messages=[

# 設定系統資訊,通常用於設定模型的行為、角色或上下文。

            {

'role'

'system'

'content'

: system_prompt},

# 設定使用者訊息,使用者訊息是使用者傳送給模型的訊息。

            {

'role'

'user'

'content'

: user_prompt},

        ],

# 設定溫度

        temperature=temperature,  

        stream=True

    )

# 遍歷響應中的每個塊
for

 chunk 

in

 response:

# 檢查塊中是否包含選擇項
if

 chunk.choices:

# 列印選擇項中的第一個選項的增量內容,並確保立即重新整理輸出
print

(chunk.choices[0].delta.content, end=

''

, flush=True)

響應函式接收的引數中,system_prompt 是系統提示詞,主要用於設定模型的行為、角色或上下文。你可以理解為這是系統給大模型的提示詞,而且始終有效。我們可以使用下面的提示詞規範大模型的響應:

system_prompt = 

'你是魯迅作品研究者,熟悉魯迅的各種作品。'

user_prompt 是使用者提示詞,是使用者發給大模型的。大模型會在系統提示詞和使用者提示詞的共同作用下,生成響應。使用者提示詞由查詢句子 query 和向量資料庫搜尋到的句子組成。對於 fixed_chunksemantic_chunk,我們需要獲取 chunk 欄位的值。對於 sentence_window,我們需要獲取 window 欄位的值。定義下面的函式可以幫助我們方便獲取想要的值。

def get_ref_info (query, search_params, 

limit

, output_fields, method):

    res = vector_search (query, search_params, 

limit

, output_fields, method)

for

 hit 

in

 res[0]:

        ref_info = {

'ref'

: hit[

'entity'

][

'window'

if

 method == 

'sentence_window'else

 hit[

'entity'

][

'chunk'

],

'title'

: hit[

'entity'

][

'title'

]

        }

return

 ref_info

最後,針對不同的分塊方法,獲取對應的響應。
for

 method 

in

 chunk_methods:

print

(f

'分塊方法: {method}'

)

# 獲取參考資訊

    ref_info = get_ref_info(query, search_params, 

limit

, output_fields, method)

# 生成使用者提示詞

    user_prompt = (

        f

'請你根據提供的參考資訊,查詢是否有與問題語義相似的內容。參考資訊:{ref_info}。問題:{query}。\n'

        f

'如果找到了相似的內容,請回復“魯迅的確說過類似的話,原文是[原文內容],這句話來自[文章標題]”。\n'

        f

'[原文內容]是參考資訊中ref欄位的值,[文章標題]是參考資訊中title欄位的值。如果引用它們,請引用完整的內容。\n'

        f

'如果參考資訊沒有提供和問題相關的內容,請回答“據我所知,魯迅並沒有說過類似的話。”'

)

# 生成響應

    generate_response(system_prompt, user_prompt, model, temperature)

print

(

'\n'

 + 

'*'

 * 50 + 

'\n'

)

好啦,一切準備就緒,讓我們看看使用不同分塊方法的 RAG,究竟有什麼區別。
RAG響應效果對比
先看第一句話,“世上本沒有路,走的人多了,也便成了路。”

分塊方法: fixed_chunk

魯迅的確說過類似的話,原文是“的人多了,也便成了路。 一九二一年一月。”,這句話來自《故鄉》。

**************************************************
分塊方法: semantic_chunk

魯迅的確說過類似的話,原文是“跨過了滅亡的人們向前進。什麼是路?就是從沒路的地方踐踏出來的,從只有荊棘的地方開闢出來的。以前早有路了,以後也該永遠有路。人類總不會寂寞,因為生命是進步的,是樂天的。昨天,我對我的朋友L說,“一個人死了,在死者自身和他的眷屬是悲慘的事,”這句話來自《六十六生命的路》。

**************************************************
分塊方法: sentence_window

魯迅的確說過類似的話,原文是“我想:希望本是無所謂有,無所謂無的。 這正如地上的路;其實地上本沒有路,走的人多了,也便成了路。 一九二一年一月。”,這句話來自《故鄉》。

fixed_chunk 選手雖然給出了原文,但是遺憾的是不夠完整。semantic_chunk 選手沒有搜尋到原文,可能是因為這個塊的前半部分和查詢句子的語義相差較遠,這也反應了分塊對搜尋結果的影響,但是給出的句子語義也和原文類似,算是意外收穫。而 sentence_window 選手,則給出了標準答案。
當然,sentence_window 的高效背後是有代價的。你可以比較下這三種分塊方法向量化後的檔案,luxun_sample_fixed_chunk_vector.json 的大小是11.5MPa,luxun_sample_semantic_chunk_vector.json 增加到了16.1MPa,而 luxun_sample_sentence_window_vector.json 則達到了49.2MPa,是前兩者的3到4倍。
再來看看第二句,“我家牆外有兩株樹,一株是棗樹,還有一株也是棗樹。”

分塊方法: fixed_chunk

魯迅的確說過類似的話,原文是“在我的後園,可以看見牆外有兩株樹,一株是棗樹,還有一株也是棗樹。這上面的夜的天空,奇怪而高,我生平沒有見過這樣的奇怪而高的天空。他彷彿要離開人間而去,使人們仰面不再看見。然而現在卻非常之藍,閃閃地䀹著幾十個星星的眼,冷眼。他的口角上現出微笑,似乎自以為大有深意,而將繁霜灑在我的園裡的野花草上。我不知”,這句話來自《秋夜》。

**************************************************
分塊方法: semantic_chunk

魯迅的確說過類似的話,原文是“在我的後園,可以看見牆外有兩株樹,一株是棗樹,還有一株也是棗樹。這上面的夜的天空,奇怪而高,我生平沒有見過這樣的奇怪而高的天空。他彷彿要離開人間而去,使人們仰面不再看見。然而現在卻非常之藍,閃閃地䀹著幾十個星星的眼,冷眼。他的口角上現出微笑,”,這句話來自《秋夜》。

**************************************************
分塊方法: sentence_window

魯迅的確說過類似的話,原文是“在我的後園,可以看見牆外有兩株樹,一株是棗樹,還有一株也是棗樹。 這上面的夜的天空,奇怪而高,我生平沒有見過這樣的奇怪而高的天空。”,這句話來自《秋夜》。

**************************************************

三位選手表現差不多,sentence_window 選手給出的原文更精準。
最後來看看第三句,“猛獸總是獨行,牛羊才成群結對。”

分塊方法: fixed_chunk

魯迅的確說過類似的話,原文是“獸是單獨的,牛羊則結隊;野牛的大隊,就會排角成城以御強敵了,但拉開一匹,定只能牟牟地叫。”,這句話來自《春末閒談》。

**************************************************
分塊方法: semantic_chunk

據我所知,魯迅並沒有說過類似的話。

**************************************************
分塊方法: sentence_window

魯迅的確說過類似的話,原文是“猛獸是單獨的,牛羊則結隊;野牛的大隊,就會排角成城以御強敵了,但拉開一匹,定只能牟牟地叫。”,這句話來自《春末閒談》。

**************************************************

fixed_chunk 選手雖然搜尋結果包含了無關內容,但是大模型從中篩選出了合適的句子。semantic_chunk 選手搜尋到的句子並沒有被大模型採納。sentence_window 選手仍然不負眾望,給出了標準答案,還能提供“視窗句子”作為上下文,在 RAG 應用中,把上下文句子一起傳遞給大模型,能讓大模型更好地理解句子的語義,作出更好的回答。請為 sentence_window 選手的精彩表現鼓掌。
06.
探索
其實,RAG 的響應和很多因素相關,你可以多多嘗試,看看結果有什麼不同。比如,修改 vector_search 函式的 limit 引數,讓向量資料庫多返回幾個句子,增加命中機率。或者增加 generate_response 函式的 temperature 引數,看看 RAG 的響應如何變化。還有提示詞,它直接影響大模型如何回答。
另外,你還可以基於本應用,開發其他功能,比如魯迅作品智慧問答功能,解答關於魯迅作品的問題。或者魯迅作品推薦功能,輸入你想要閱讀的作品型別,讓 RAG 為你做推薦。玩法多多,祝你玩得開心。
07.
藏寶圖
老規矩,推薦一些資料供你參考。
  • ChunkViz(https://chunkviz.up.railway.app/)是一個線上網站,提供分塊視覺化功能。
  • 想了解 RAG 更多有趣應用,可以看看這個影片:當我開發出史料檢索RAG應用,正史怪又該如何應對?(https://www.bilibili.com/video/BV1da4y1k78p/?spm_id_from=333.337.search-card.all.click&vd_source=ad92e3138da83a643ab3f5883c7664c7)。想了解更多技術細節,看這裡:揭秘「 B 站最火的 RAG 應用」是如何煉成的
  • 想了解更多分塊技術,可以閱讀檢索增強生成(RAG)的分塊策略指南(https://zilliz.com.cn/blog/guide-to-chunking-sreategies-for-rag) 從固定大小到NLP分塊 – 文字分塊技術的深入研究(https://safjan.com/from-fixed-size-to-nlp-chunking-a-deep-dive-into-text-chunking-techniques/)兩篇文章。
參考
[^1]: 魯迅作品集資料基於 luxun_dataset ,增加了一些欄位。luxun_sample.json 為魯迅部分作品,方便試用。luxun.json 為完整的魯迅作品集。
[^2]: LlamaIndex 是一個用於構建帶有上下文增強功能的生成式 AI 應用的框架,支援大型語言模型(LLMs)。
程式碼檔案連結: https://pan.baidu.com/s/16nSrOh7jM0c6naB0JgsUhQ?pwd=1234 提取碼: 1234
特別感謝B站up主阿威的衝浪日記的貢獻!
以上全文1萬4千+字如果這篇文章覺得對你有用,可否點個關注。給我個三連擊:點贊、轉發和在看。若可以再給我加個⭐️,謝謝你看我的文章,我們下篇再見!

相關文章