
使用NestJS和Prisma構建REST API:身份驗證
本系列我們主要關注以下兩個模型:
GPT-3.5- Turbo
:用于文本生成、對話系統和語言理解等任務。text-embedding-ada-002
:用于文本嵌入(Text Embedding)任務,將文本轉換成高維向量表示。API Key
在開始使用 OpenAI API之前,首先需要前往 Azure 或 OpenAI 官網申請 API 密鑰(不同的服務提供商申請流程略有不同)。但是由于&*%@的原因……,總之這個過程沒有統一完備的解法和攻略,需要大家各顯神通。
為了確保API密鑰的安全性,我們首先應當學會將其儲存在環境變量中,而不是直接寫入源代碼。將API密鑰直接硬編碼在代碼中是非常不安全的做法,會增加密鑰泄露的風險。
右鍵點擊”此電腦”,選擇”屬性”。在彈出窗口中,點擊”高級系統設置”。接著,在”系統屬性”對話框中,依次點擊”高級”→”環境變量”。在這里,我們可以為系統或當前用戶新建”OPENAI_API_KEY”的環境變量,并將OpenAI API密鑰字符串作為變量值進行存儲。
OpenAI 提供了一個Python庫(https://github.com/openai/openai-python)。安裝之前,請確保你的 Python 版本是 3.7.1 或更高版本。
pip install openai
調用OpenAI’s (Python) API
下面我將以?GPT-3.5-turbo
?模型為例,展示調用API的代碼。需要注意,不同的服務提供商(Azure和OpenAI)調用API的方式略有不同,下文會分別講解。調用OpenAI API的計費取決于調用的模型類型以及消耗的Token數量,一般按照每百萬或每千個Tokens進行結算。作為參考,根據OpenAI官方文檔:對于英文文本,1 個Token大約為 4 個字符或 0.75 個單詞。
之前我們已經將API KEY儲存在環境變量中,而dotenv
是一個用于加載環境變量的Python庫。利用下面的代碼,我們從環境變量中獲取OpenAI API密鑰,并將其設置為全局變量,以便在后續的OpenAI API調用中安全使用。
import openai
import os
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
openai.api_key = os.getenv('OPENAI_API_KEY')
接下來,我們定義一個函數get_completion_openai
。
它有兩個參數:prompt
表示輸入文本,model
表示要使用的模型,默認為gpt-3.5-turbo
。
import os
import openai
import json
def get_completion_openai(prompt, model="gpt-3.5-turbo"):
openai.api_key = os.getenv('OPENAI_API_KEY')
messages = [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": prompt
}
]
response = openai.ChatCompletion.create(
model=model,
messages=messages,
temperature=0
)
return response.choices[0].message['content']
在這段代碼中,messages
列表中的每個字典代表一個對話消息,其中包含兩個鍵值對:
"role"
:在 OPENAI API 中,提供了三種角色,允許與 LLM 進行不同類型的交互。而"role"
參數指定了消息的角色,可以是 “system” ,”user”或”assistant “。"content"
:包含了消息的內容。在上面的代碼中,第一個"content"
是系統(system)的介紹性消息,第二個"content"
是用戶(user)輸入的內容。“system” 角色:
“user” 角色:
“assistant” 角色:
response = openai.ChatCompletion.create(……)
這一行使用 OpenAI API向指定的 GPT 模型發送請求。
model
參數指定了要使用的模型名稱messages
參數傳遞了之前創建的 messages 列表temperature=0
設置確保模型的輸出是確定的。temperature
接近 1 時,模型會產生更多出人意料的、新穎的內容(但我們做實證研究中的文本分析,為了確保結果是可復現的,一般這個參數都要設置為0)。在openai.ChatCompletion.create(……)
上述示例代碼中沒有列出,但也同樣常用的其他參數包括:
max_tokens
n
調用Azure OpenAI API
Azure 與 OpenAI 的調用 API 的方式略有不同。我們首先需要創建一個 Azure 訂閱(Subscription),以及相應的資源組(Resource Group)和部署(Deployments),三者缺一不可。在 Azure 門戶中創建上述資源后,你將獲得一個終結點 URL(Endpoint) 和一個訪問密鑰(API Key),這兩者是調用API的必備信息。
注意:Deployments雖然可以自定義,但最好不要亂起名,用什么模型起什么名,比如用gpt-35-turbo,就取gpt-35-turbo的部署名。不然部署的模型多了容易亂成一鍋粥。
在獲取了Endpoint和API Key之后,我們用以下代碼進行調用。
首先,讓我們看一下Azure官方提供的使用Azure OpenAI API的示例代碼。類似地,我們需要在環境變量中設置并獲取”AZURE_OPENAI_API_KEY”和”AZURE_OPENAI_ENDPOINT”。
import os
import openai
openai.api_type = "azure"
openai.api_base = os.getenv("AZURE_OPENAI_ENDPOINT")
openai.api_key = os.getenv("AZURE_OPENAI_API_KEY")
openai.api_version = "2023-05-15"
response = openai.ChatCompletion.create(
engine="gpt-35-turbo", # engine = "deployment_name".
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Does Azure OpenAI support customer managed keys?"},
{"role": "assistant", "content": "Yes, customer managed keys are supported by Azure OpenAI."},
{"role": "user", "content": "Do other Azure AI services support this too?"}
]
)
print(response)
print(response['choices'][0]['message']['content'])
我們在Azure官方提供的代碼的基礎上,進行修改,封裝成一個函數:
import os
import openai
def get_completion_azure(prompt, model="gpt-35-turbo"):
openai.api_key = os.getenv("AZURE_OPENAI_API_KEY")
openai.api_base = os.getenv("AZURE_OPENAI_ENDPOINT")
openai.api_type = "azure"
openai.api_version = "2023-05-15"
response = openai.ChatCompletion.create(
engine = model,
temperature = 0,
messages = [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": prompt
}
]
)
return response['choices'][0]['message']['content']
注意,OpenAI對于gpt-3.5-turbo
的引用是gpt-3.5-turbo
,而Azure是gpt-35-turbo
,這個地方比較細節,容易出錯……
在設置好上面的get_completion_openai
和get_completion_azure
函數后,我們就可以在各種場景下靈活應用OpenAI API 。
以下是一個基礎的API調用示例:
get_completion_openai("TextMatters是一個專注于基于文本分析的金融會計領域實證研究的微信公眾號,請你為它撰寫一個30字以內的簡短介紹。")
以上API調用代碼會返回以下結果:
'TextMatters是專注于金融會計領域實證研究的微信公眾號,致力于通過文本分析揭示財務報告中的信息和趨勢,為投資者和研究者提供有益的見解和分析。關注TextMatters,了解最新的研究成果和行業動態。'
Prompt最初是為下游任務而設計的一種任務專屬的輸入形式或模板,在ChatGPT引發大語言模型新時代后,Prompt成為與大模型交互輸入的代名詞。因此,我們通常將提供給大模型的輸入稱為Prompt。
Prompt Engineering是指設計和調整Prompt的過程。正如吳恩達本人所強調,通過正確地使用和優化提示詞,我們才能夠充分發揮LLM的潛力,指導它按照我們的意圖生成所需輸出。
上一章講到的調用API的函數,我們需要在本章進行復用。因此我們在Python中運行以下代碼,定義函數get_completion
:
def get_completion(prompt, model="gpt-3.5-turbo"):
openai.api_key = os.getenv('OPENAI_API_KEY')
messages = [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": prompt
}
]
response = openai.ChatCompletion.create(
model=model,
messages=messages,
temperature=0
)
return response.choices[0].message['content']
吳恩達和 Iza Fulford給出了設定Prompt的兩條基礎原則。它們分別是:
在撰寫指令時,明確并具體的Prompt是十分重要的。在具體的應用過程中,原則1可以體現為以下幾種Prompt實踐。
(1)使用分隔符清楚地表示輸入的不同部分
我們可以使用分隔符清晰地劃分輸入的不同部分,這有助于模型準確理解輸入的結構。分隔符可以是各種形式的,比如雙引號""
、三引號'''
,尖括號<>
、標簽<tag></tag>
或者冒號:
。
示例代碼:
text = f"""
您應該通過提供盡可能清晰和具體的指令來表達您希望模型執行的任務。這將引導模型朝著期望的輸出方向發展,并減少接收到無關或不正確響應的幾率。不要混淆寫清晰提示與寫簡短提示。在許多情況下,更長的提示為模型提供了更多的清晰度和上下文,這可能導致更詳細和相關的輸出。
"""
prompt = f"""
把用三個反引號括起來的文本總結成一句話。
``{text}
``
"""
# 調用函數
response = get_completion(prompt)
print(response)
以上代碼的輸出結果為:
提供清晰和具體的指令可以引導模型朝著期望的輸出方向發展,減少接收到無關或不正確響應的幾率。
(2)設定結構化的輸出
當我們調用API與模型進行交互時,為了獲得更有用的結果,有時候可以要求它提供結構化輸出,例如JSON或HTML格式的數據。
示例代碼:
prompt = f"""
生成三本虛構中文書籍標題,包括它們的作者和流派。
以JSON格式提供,包含以下鍵:
book_id,title,author,genre。
"""
# 調用函數
response = get_completion(prompt)
print(response)
以上代碼的輸出結果為:
```json
[
{
"book_id": 1,
"title": "夢境之城",
"author": "張小明",
"genre": "奇幻"
},
{
"book_id": 2,
"title": "時間之門",
"author": "王小紅",
"genre": "科幻"
},
{
"book_id": 3,
"title": "幽靈之謎",
"author": "李大山",
"genre": "懸疑"
}
]
```
(3)要求模型檢查是否滿足條件
在處理任務時,我們經常會面對各種假設或條件,有些條件可能并非總是被滿足。這種方法可以幫助我們在開始任務之前排除一些可能導致錯誤結果的情況。如果發現條件不滿足,模型會及時指出并停止執行后續的完整流程。
示例代碼:
text = f"""
今天陽光明媚,鳥兒在歡唱。今天是去公園散步的好日子?;▋菏㈤_著,樹木在微風中輕輕搖曳。人們出門在外,享受著美好的天氣。
"""
prompt = f"""
您將獲得由三個引號界定的文本。如果其中包含一系列指令,請按以下格式重新編寫這些指令:
步驟1 - ...
步驟2 - …
…
步驟N - …
如果文本中不包含一系列指令,則簡單寫為“未提供步驟”。
'''{text}'''
"""
# 調用函數
response = get_completion(prompt)
print(response)
以上代碼的輸出結果為:
未提供步驟。
(4)Few-shot prompting
Few-shot prompting 的核心思想是提供一個簡短的提示或示例,然后讓模型根據這些提示進行推理和生成,指導它按照我們的意圖生成所需輸出。
示例代碼:
text = f"""
龍騰虎躍鬧春意
"""
prompt = f"""
您的任務是以對聯的格式回答問題。
對聯是中國傳統文化中的一種形式,通常由兩句相對、呼應或對仗的詩句構成。這兩句詩句往往在意義、韻律或結構上相呼應,構成了一種文學藝術形式。
例如:
“春風吹綠柳,夏雨潤花枝?!?br />
下面三引號中的內容是你需要回答的問題:
'''{text}'''
"""
# 調用函數
response = get_completion(prompt)
print(response)
以上代碼的輸出結果為:
鳥語花香迎新春
我們可以在Prompt中先要求語言模型自己嘗試解決這個問題,思考出自己的解法,然后再與提供的解答進行對比,判斷正確性。在具體的應用過程中,原則2可以體現為以下幾種Prompt實踐。
(1)指定完成任務所需的步驟
在Prompt中明確指定語言模型需要執行的操作步驟,以便引導模型有條不紊地進行推理和生成。
示例代碼:
text = f"""
畢竟西湖六月中,風光不與四時同。\
接天蓮葉無窮碧,映日荷花別樣紅。
"""
prompt = f"""
執行以下操作:
1-請描述下面用三個反引號括起來的古詩文本的意思。
2-找出其中的提到的顏色。
3-找出顏色對應的物體。
4-輸出一個 JSON 對象,其中包含以下鍵:顏色,物體。
請用換行符分隔您的答案。
Text:
``{text}
``
"""
# 調用函數
response = get_completion(prompt)
print(response)
輸出結果為:
1- 這首古詩描述了西湖六月的景色,與其他季節不同。蓮葉碧綠無窮,荷花在陽光下映出別樣的紅色。
2- 綠色、紅色
3- 蓮葉、荷花
4-
```json
{
"顏色": ["綠色", "紅色"],
"物體": ["蓮葉", "荷花"]
}
```
(2)在指導模型得出結論之前讓它自行思考問題
當我們要求語言模型判斷數學問題的解答是否正確,但僅僅提供問題和解答是不夠的,過于匆忙可能導致錯誤的判斷。我們應該在Prompt中鼓勵模型首先嘗試自己解決問題,思考出自己的解決方案。這種方式能夠讓模型有足夠的時間去理解問題,從而提高解決問題的準確性。
我們接下來展示一個初始Prompt被不斷迭代優化的過程,包括:添加長度限制;提取信息并結構化輸出。
text = f"""
Author: Chen Yi-Chun,Hung Mingyi,Wang Yongxiang \
Journal: Journal of Accounting and Economics \
Year: 2018 \
Abstract: We examine how mandatory disclosure of corporate social responsibility (CSR) \
impacts firm performance and social externalities. Our analysis exploits China’s 2008 mandate \
requiring firms to disclose CSR activities, using a difference-in-differences design. \
Although the mandate does not require firms to spend on CSR, we find that mandatory CSR \
reporting firms experience a decrease in profitability subsequent \
to the mandate. In addition, the cities most impacted by the disclosure mandate \
experience a decrease in their industrial wastewater and SO2 emission levels. \
These findings suggest that mandatory CSR disclosure alters firm behavior and \
generates positive externalities at the expense of shareholders.
"""
prompt = f"""
請你用中文概括下面論文摘要中的核心結論。
``{text}
``
"""
response = get_completion(prompt)
print(response)
輸出結果為:
這段論文摘要的核心結論是:強制性披露企業社會責任(CSR)對公司績效和社會外部性產生影響。研究發現,雖然強制性CSR披露并不要求公司在CSR方面進行支出,但披露CSR活動的公司在法規實施后經歷了盈利能力下降。此外,受披露要求影響最大的城市在工業廢水和二氧化硫排放水平上也出現下降。這些發現表明,強制性CSR披露改變了公司行為,并在股東的代價下產生了積極的外部性影響。
可以看到,這個回復還是太長了,沒有起到概括的作用。因此我們可以在Prompt中添加輸出長度限制。
prompt = f"""
請你用中文概括下面論文摘要中的核心結論。長度限制到20字以內。
``{text}
``
"""
response = get_completion(prompt)
print(response)
輸出結果為:
強制CSR披露影響企業績效和社會外部性,減少盈利,改善環境。
最后,我們進一步優化prompt,讓它提取我們感興趣的特定信息并生成結構化輸出。
prompt = f"""
針對三個反引號括起來的文本,請你提取下列信息并組織成Json格式輸出:
作者,期刊,年份,核心結論(20字以內)
``{text}
``
"""
response = get_completion(prompt)
print(response)
輸出結果為:
```json
{
"Author": "Chen Yi-Chun, Hung Mingyi, Wang Yongxiang",
"Journal": "Journal of Accounting and Economics",
"Year": "2018",
"Core Conclusion": "Mandatory CSR disclosure affects firm performance and social externalities."
}
```
當我們讓語言模型描述一個實際上不存在的產品或概念時,它可能會憑空捏造出貌似合理的細節,自行”補全”這個虛構的事物,一本正經地向我們提供這些編造的”知識”。這種現象被稱為”幻覺”(Hallucination),是語言模型的一大局限性和缺陷。
雖然我們在前面的內容中學習了如何設定準確合適的Prompt來指導模型的輸出方向,但我們仍然需要警惕幻覺現象可能帶來的問題和風險。
text = f"""
用50字向我描述小米公司最新開發的Mate60手機的特點。
"""
prompt = f"""
請你用專業的語言回答下列三個反引號括起來的問題
Text:
``{text}
``
"""
# 調用函數
response = get_completion(prompt)
print("prompt")
print(response)
輸出結果為:
小米公司最新開發的Mate60手機具有高性能處理器、創新設計、多攝像頭系統、大容量電池和快速充電技術等特點。
讀取數據集
我們先讀入數據集,再在DataFrame中選取名為'Text'
這一列,即包含產品評論文本內容的那一列。
import pandas as pd
amazon = pd.read_excel('./amazon_review.xlsx')
amazon_review = amazon['Text'].tolist()
# 取前5條數據
amazon_review = amazon_review[:5]
我們定義函數get_setiment
,它接受兩個參數:
amazon_review
是一個字符串,表示待分析的產品評論文本model
是一個可選參數,默認值為"gpt-3.5-turbo"
代表使用OpenAI的gpt-3.5-turbo模型在函數內部,我們設計了一個prompt,明確指示模型對給定的產品評論進行情感分類,并僅輸出”positive”或”negative”兩個詞匯作為結果,確保輸出的準確性。
import openai
import os
# 設定情感分類相關的OpenAI API調用函數
def get_setiment(amazon_review, model="gpt-3.5-turbo"):
openai.api_key = os.getenv('OPENAI_API_KEY')
prompt = f"""
What is the sentiment of the following product review,
which is delimited with triple backticks
Give your answer as a single word, either "positive" or "negative".
Review text: ``{amazon_review}
``
"""
messages = [
{
"role": "user",
"content": prompt
}
]
response = openai.ChatCompletion.create(
model=model,
messages=messages,
temperature=0
)
return response.choices[0].message['content']
#運行函數
setiment = {}
for text in amazon_review:
setiment[text] = get_setiment(text)
sentiment_df = pd.DataFrame(setiment.items(), columns=['Review', 'Sentiment'])
輸出結果整理如下。從分類結果來看,大型語言模型展現出了非常優秀的性能,模型能夠精準地區分出正面(positive)和負面(negative)兩種情感。
在這一小節,我們將嘗試設定一個Prompt來分析評論本文的情感類型。我們期望模型可以識別出評論作者表達的情感,并將其整理成一個不超過五個項目的列表。
prompt = f"""
Identify a list of emotions that the writer of the \
following review is expressing. Include no more than \
five items in the list. Format your answer as a list of \
lower-case words separated by commas.
Review text: '''{amazon_review[0]}'''
"""
response = get_completion(prompt)
print(response)
輸出結果如下:
satisfied, pleased, impressed, grateful, content
識別憤怒
prompt = f"""
Is the writer of the following review expressing anger?\
The review is delimited with triple backticks. \
Give your answer as either yes or no.
Review text: '''{amazon_review[0]}'''"""
response = get_completion(prompt)
print(response)
輸出結果如下:
No
文本嵌入(Text embeddings)是一種將文本轉換為向量表示(Vector Representation)的技術(更加通俗地說,嵌入就是將人類可讀的文字變成模型可讀的數字)。其核心思想是捕捉單詞或短語之間的語義,并將其表示為向量空間中的距離。
換句話說,語義上相似的單詞在嵌入空間中會有更接近的向量表示,而語義上不相關的單詞則會有較遠的向量表示。
常見的嵌入模型包括Word2Vec、GloVe、BERT等。
*下文提到的內容具有一定時效性,建議定期跟蹤OpenAI API官方文檔,更新信息。
2024 年年初,OpenAI API發布了全新的V3嵌入模型?text-embedding-3-small
?和?text-embedding-3-large
。
截至目前(2024年3月),OpenAI 提供的嵌入模型主要包括:
text-embedding-ada-002
text-embedding-3-small
text-embedding-3-large
考慮到使用較大的嵌入會消耗更多的計算、內存和存儲,OpenAI 現在允許用戶通過API 傳遞維度參數來縮短嵌入維度,且不會使嵌入失去其代表概念的屬性。根據下圖官方測試中提供的信息,兩個V3嵌入模型即使在縮短嵌入維度后的表現仍然高于ada-002。
因此綜上,如果只是用于金融/會計學術領域的文本研究,我強烈建議無條件使用text-embedding-3-small
模型。
下列代碼中,我們定義了一個函數?get_embedding_v1(text_to_embed)
,該函數接受一個字符串參數?text_to_embed
,該字符串是要嵌入的文本。函數內我們調用了?openai.Embedding.create()
?方法,發起一個 API 請求。
import json
import openai
import os
import time
def get_embedding(text_to_embed):
openai.api_key = os.getenv('OPENAI_API_KEY')
response = openai.Embedding.create(
model= "text-embedding-3-small",
input=[text_to_embed]
)
try:
embedding = response["data"][0]["embedding"]
except:
embedding = None
time.sleep(2)
print("error")
pass
if embedding:
with open("./mydata_embedding.json", "a+") as f:
json.dump({text_to_embed: embedding}, f)
f.write("\n")
return embedding
上述代碼的響應是一個 JSON 對象,我們用embedding = response["data"][0]["embedding"]
提取其中的嵌入,它是一個長度(維度)為 1536 維的向量。我們將文本及其對應的嵌入向量寫入一個名為 mydata_embedding.json
的 JSON 文件中。
我們可以在一個簡單文本上,運行上述函數:
get_embedding("Harry potter, the boy who lived.")
得到嵌入返回的向量:
[-0.029458941891789436,
0.04615234211087227,
-0.0445975624024868,
0.033100392669439316,
0.015087478794157505,
-0.061004556715488434,
-0.005027454812079668,
...
-0.01764467917382717,
-0.02884521335363388,
-0.021930545568466187,
-0.011804034002125263,
...]
我們看一下這個向量的維度:
print(len(get_embedding("Harry potter, the boy who lived.")))
會得到嵌入維度:
1536
通常,使用更大的嵌入向量會消耗更多的計算、內存和存儲資源。上文提到,OpenAI 現在允許在兩個 V3 嵌入模型上通過 API 傳遞維度參數來縮短嵌入維度(ada-002
不支持維度調整)。
具體實現代碼如下,我們只需在 response 中添加對維度參數的要求:
response = openai.Embedding.create(
model= "text-embedding-3-small",
input= [text_to_embed],
dimensions = 512
)
此時維度從1536維下降至512維:
print(len(get_embedding("Harry potter, the boy who lived.")))
返回值:
512
文本嵌入為機器學習模型和其他算法執行各種下游任務(如聚類或檢索)提供了基礎。包括但不限于:
本節進一步展示一個簡化后的下游任務:計算文本相似度。通過將文本轉換成嵌入向量,我們可以利用向量空間中的距離或相似度度量來比較它們之間的相似性。
作為一個示例,我同時獲取了哈利波特和指環王主角們的姓名文本嵌入。通過比較這些嵌入向量之間的距離或相似度,我們可以探索這些主角之間在文本語境中的相似性或相關性。
# 哈利波特人物
harry_potter = get_embedding("Harry potter")
ron_weasley = get_embedding("Ron Weasley")
hermione_granger = get_embedding("Hermione Granger")
# 指環王人物
gandalf = get_embedding("Gandalf the Grey")
frodo = get_embedding("Frodo Baggins")
legolas = get_embedding("Legolas")
我們使用scipy
庫中的distance.cosine
函數來計算哈利波特與其他人物之間的余弦距離。
# 計算哈利到各個人的距離
from scipy.spatial import distance
print(distance.cosine(harry_potter, ron_weasley))
print(distance.cosine(harry_potter, hermione_granger))
print(distance.cosine(harry_potter, gandalf))
print(distance.cosine(harry_potter, frodo))
print(distance.cosine(harry_potter, legolas))
上述代碼輸出結果如下。我們可以看出,同一故事的角色之間的余弦距離(= 1-余弦相似度)更小,這表明這些角色在語義上更為相似或者相關。
0.4510517158795876
0.46182439707308476
0.613488967442906
0.6424822454338903
0.6501364169064245
為了更清晰地展示嵌入的空間距離,我們利用主成分分析將文本的高維向量表示降維至二維空間,并將其呈現在二維圖表中。結果顯示,同一系列或同一故事中的角色之間的文本嵌入更相似,在圖表中呈現為更接近的空間距離。
本文章轉載微信公眾號@TextMatters