# OpenAI Prompt 經驗
[toc]
## 基本用法
> 以 Azure OpenAI 服務為例
> 程式語言以 Python 為例
> 閃亮亮的 Azure 官網教學:https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line&pivots=programming-language-python
### Prerequisite
1. **獲取Azure OpenAI Key:**
Azure Portal $\rightarrow$ Azure OpenAI $\rightarrow$ `your openai service` $\rightarrow$ `Keys and EndPoint` (in menu bar) $\rightarrow$ copy `KEY1` or `KEY2`(both are able)

2. **請確認已部署好的 GPT 模型:**
* Azure Portal $\rightarrow$ Azure OpenAI $\rightarrow$ `your openai service` $\rightarrow$ `Model deployments` (in menu bar) $\rightarrow$ `Manage Deployments`
* 將 `部署名稱` 和 `模型名稱` 都記錄下來
* 如下圖

3. 下載 [Python OpenAI Package](https://pypi.org/project/openai/)
```sh
pip install openai
```
### Simple Example
#### Step 1: Import Package
```python=
import openai
```
#### Step 2: Set OpenAI Configuration
因為使用 Azure OpenAI 服務,所以要設定 `api_base`、`api_key`、`api_version`、`api_type`,如下:
```python=+
openai.api_base = f"https://{your openai service name}.openai.azure.com"
openai.api_key = "<your openai key>"
openai.api_version = "2023-05-15"
openai.api_type = "azure"
```
#### Step 3: 建立 Message
`ChatCompletion` 有特定的 Message 格式,如下:
```python=+
[
{'role': 'system', 'content': '<your chatbot prompt>'},
{'role': 'user', 'content': '<your query or message>'},
{'role': 'assistant', 'content': '<your chatbot response>'},
...
]
```
* Example:
```python=+
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Knock knock."},
{"role": "assistant", "content": "Who's there?"},
{"role": "user", "content": "Orange."},
]
```
* `role`:代表該 Message 的角色,有 `system`、`user`、`assistant` 三種
* 其他詳細資訊請參考 [ChatCompletion](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models) or [Chat completions API](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
> The `system message` can be used to prime the assistant with different personalities or behaviors.
> [name=Offical Website]
#### Step 4: 呼叫 OpenAI API
```python=+
chat_completion = openai.ChatCompletion.create(
deployment_id='你的部署名稱',
model='你的模型名稱',
messages=messages,
temperature=0.7,
max_tokens=1024,
n=1)
```
* 詳細參數請參考 [Chat completions API](https://platform.openai.com/docs/api-reference/chat/create)
#### Step 5: 回應範例 `chat_completion`
```json=
{
"id": "chatcmpl-7UkgnSDzlevZxiy0YjZcLYdUMz5yZ",
"object": "chat.completion",
"created": 1687563669,
"model": "你的模型名稱",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Orange who?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 39,
"completion_tokens": 3,
"total_tokens": 42
}
}
```
## 先烈們的經驗
### 現實與理想的差距
> 以下為我們在使用 OpenAI 的過程中,所遇到的問題與解決方法
1. 我想要用OpenAI讀取文件,請他回答內容
* **現實:** OpenAI 並 **==不會去讀取文件==**,他只會回答你的問題
* **解決方法:** 先將文件讀取為文字檔(txt, md etc.),並且將文字檔結合你的問題,讓 OpenAI 回答你的問題
* **範例:**
* 文件:`test.md`
* 文件內容:
epc-r7200是研華的產品,他的規格如下:...(略)
* 問題:`epc-r7200是什麼?`
* 結合後的問題:`epc-r7200是什麼?\n <請自己複製貼上test.md的內容(請自己手動key進來)>`
* 再把結合後的問題丟給 OpenAI,他就會回答你了
2. 啊OpenAI的回答都不是我想要的
* **現實:** 很明顯 OpenAI 就不知道你的問題,它可能沒有看過你的文件,所以他回答的內容都不是你想要的==
* **解決方法:** 這時候就要使用課金大法,自己建立一個資料庫,並從資料庫中找到合適的資料來源
* **大家的問題:** 啊要怎麼建立資料庫????????????(╯°Д°)╯︵ /(.□ . \)
==請接續看下一節==
### 啊我不知道要怎麼建立資料庫
我們偉大的 Microsoft Azure 大大提供了一個神奇的服務:[**Azure Cognitive Search**](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search),他可以幫助我們建立一個資料庫,並且可以透過 query 來搜尋資料庫中的資料,我們再自己將搜尋出來的資料丟入prompt讓 OpenAI 可以回答我們的問題(如上類似讀取文件的方法)
> [**Azure Cognitive Search**](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search) 詳細的介紹請自己去看連結的內容
#### Step 1: 建立資料庫
1. 首先請確認你已經先建立好一個 Azure Cognitive Search 服務
2. 在 Azure Cognitive Search 服務中,我們是透過指定 `Index` 和提供 `Query` 來搜尋資料庫中的資料
* `Index`:一個 Azure Cognitive Search 服務中可以有多個 `Index`,每個 `Index` 都是一個資料庫。
* `Query`:透過 `Query` 來搜尋 `Index` 中的資料,一般來講是輸入關鍵字,因為此搜尋演算法是使用 `TF-IDF`,所以輸入的關鍵字越精準,搜尋出來的資料越準確。
* 一個 Cognitive Search 服務中可以有多個 `Index`,每個 `Index` 中可以有多個 `Document`(在 value 裡),每個 `Document` 中可以有多個 `Field`,如下圖:

#### Step 2: 建立 `Index` 的方法:
1. 在 Azure Cognitive Search 服務中,點選 `Indexes` $\rightarrow$ `Add Index` 新增一個 `Index`
> 如何在 Azure 上面怎麼建立 `Index` 請參考 [Create an index in Azure Cognitive Search](https://learn.microsoft.com/zh-tw/azure/search/search-how-to-create-search-index?tabs=portal)
* 在建立 `Index` 的時候,請思考好 `Index` 的 `Feild` 要怎麼設定,因為 `Feild` 的設定會影響到 `Query` 的搜尋結果
* 每個 `Field name` 都有不同的權限需要設定,以下圖為例:
- 當我們在搜尋 `Index` 的資料時,輸入的 `Query` 只會針對 `Field name` 為 `title`、`content`、`heading` 的內容進行搜尋(`Searchable` 被勾選起來),而其他 `Field name` 的內容則不會被納入搜尋範圍。
- 而以我們目前所建立的架構來看,當我們的資料庫回傳候選資料後,我們只會將 `markdown` 的資料丟給 OpenAI,而 `sourcefile` 則是用來記錄該資料的來源,方便我們可以直接從 `Blob Storage` 中提取原始資料,方便後續優化時的判斷(如他到底找的東西是對的還是錯的)或是作為測試人員的參考。

* 而 `Index` 也有其他的 `Configuration` 可以設定,如 `Scoring Profiles`、`Semantic configurations` 等等,這些都是可以依照自己的需求來設定,但因為我們還沒試過這些設定,也不清楚其運作流程,所以這裡就不討論ㄌ。
2. 確定我們的資料來源型態,如 `PDF`、`Word`、`txt`、`md` 等等
* 為何需要先確認資料來源呢?
- 因為不同的資料來源,可能會使我們獲取文本的方式不同,如 `PDF` 需要使用 `Document Intelligence` 服務,而 `txt`、`md` 則可以直接在本地端手動讀取。
- 如果你還是很懶的話,可以直接到[Azure OpenAI Studio](https://oai.azure.com/portal/)上的聊天裡上傳資料,他會自動幫你建立 `Index` ,並將資料同步上傳到 `Blob Storage`,但是這樣的話,你就沒辦法自己控制 `Index` 的內容了。
3. 在獲取完文本資料並進行資料切割後,我們需要將資料上傳到 `Index`
* 問題來了,**為啥要切割資料呢?**
- 因為根據不同的 OpenAI Model 有不同的 `max_tokens` 限制,像是最常見的 `gpt-3.5-turbo` 最多只能接受 4096 tokens,所以我們需要將資料切割成小塊,並附在 `prompt` 中作為資料來源,才能讓 OpenAI 能夠回答我們的問題。
- 以 **4096 tokens** 為例,會先預留 **1024 tokens** 供 prompt 和論壇問題或是顧客的問答使用,而剩下的 tokens 則是用來回答問題和資料來源的扣打使用。
- 在我們的架構下,我們以最多 **1000 tokens** 為單位進行文本切割,並且將切割後的資料上傳到 `Index` 中。
- 而每個單位在 `Index` 中,就是一個 `Document`。
> 需注意,`Index` 中的實際 `Document` 數量不宜太多(之前忘記在哪看到,數量太多會導致資料搜尋表現變差)
* 而至於為啥我們**使用 `markdown` 作為資料來源的型態**呢?
- 因為一般預設的切割方式是直接使用 token 數作為切割單位,但這樣會導致切割後的資料可能會將一個句子切割成兩半或是切割在列表中間等等,而這就會導致 OpenAI 回答不準確或是回答不出來。
- 所以我們希望我們切割出來的文本可以最大化的保留句子、段落的完整性,因此我們使用 `markdown` 作為資料來源的型態,並且在切割時,使用以下優先度作為切割的單位,這樣就可以盡量確保切割出來的文本都是完整的句子。:
> `markdown` 的 **`heading`** 作為切割單位 > **`\n`** 作為切割單位 > **token 數**作為切割單位
* 如果資料是 Markdown 的話,可以參考[這個程式碼](https://github.com/shuxian12/Text-Segmentation-and-Data-Crawling/blob/main/text_spliter.py),我們使用的 Markdown 規範為 `ATX`
4. `Index` 的簡易範例程式碼(也可以手動上傳):
> ==以下程式碼僅供參考==,或是可以直接看 [code](https://github.com/shuxian12/Text-Segmentation-and-Data-Crawling/blob/main/prepdocs.py)
> 請自行先下載azure的套件,大部分都是分開下載的
* 匯入套件:
```python=
from azure.core.credentials import AzureKeyCredential
from azure.storage.blob import BlobServiceClient
from azure.search.documents import SearchClient
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
SearchableField,
SearchIndex,
SimpleField,
)
```
* 新增 `Index`:
```python=
def create_search_index():
index_client = SearchIndexClient(endpoint=f"https://{'your search service name'}.search.windows.net/",
credential=AzureKeyCredential("your search service key"))
index = SearchIndex(
name="your index name",
fields=[
# 如果之後想使用 Semantic Search,可被搜尋的欄位記得要加 analyzer_name,記得要上 Portal 設定此功能的權限
# key=True 代表此欄位為唯一值,不可重複,在上傳資料時一定要有此欄位
SimpleField(name="id", type="Edm.String", key=True),
SearchableField(name="title", type="Edm.String", analyzer_name="en.microsoft"),
SearchableField(name="content", type="Edm.String", analyzer_name="en.microsoft"),
SimpleField(name="category", type="Edm.String", filterable=True, facetable=True),
SimpleField(name="sourcepage", type="Edm.String", filterable=True, facetable=True),
SimpleField(name="sourcefile", type="Edm.String", filterable=True, facetable=True),
]
)
index_client.create_index(index)
```
* 上傳 `Index` 的範例格式:
> 除了 `id` 外,其他欄位都不是必須的,請依照自己的需求來設定。
```json
{
"id": "create a unique id for each document by yourself",
"title": "file name without file type", // ex: test
"content": "content of the file",
"sourcefile": "file name", // ex: test.md
}
```
* 上傳 `Index`:
> 也可以將所有資料以上述的格式寫成一個 `json` 檔,手動上傳(在 Portal)
```python=
def index_sections(sections):
search_client = SearchClient(endpoint=f"https://{'your search service name'}.search.windows.net/",
index_name="your index name",
credential=AzureKeyCredential("your search service key"))
i = 0
batch = []
for s in sections:
batch.append(s)
i += 1
# 應該也是可以一個一個上傳,但沒試過,我們也沒試過全部一起上傳
if i % 1000 == 0:
results = search_client.upload_documents(documents=batch)
batch = []
if len(batch) > 0:
results = search_client.upload_documents(documents=batch)
```
* 上傳到 `Blob Storage`:
```python=
def upload_blobs(upload_file_path):
blob_service = BlobServiceClient(account_url=f"https://{'your storage name'}.blob.core.windows.net", credential="your storage key")
blob_container = blob_service.get_container_client("your container name")
if not blob_container.exists():
blob_container.create_container()
with open(upload_file_path, "rb") as data:
blob_container.upload_blob(blob_name, data, overwrite=True)
```
#### Step 3: 很棒,你應該差不多建立完一個すごい資料庫了
1. 現在你可以在 Azure Cognitive Search 服務中,點選 `Indexes` $\rightarrow$ `your index name` $\rightarrow$ `Search explorer` 來測試你的資料庫是否建立成功
2. `Query` 可以直接打 `*` 即可搜尋所有資料,也可以輸入關鍵字來搜尋資料,ㄜㄏㄜㄏ
### Do Re Mi So, 是時候來試試 OpenAI 了
> 以下為我們在使用 OpenAI 的過程中,所遇到的問題與解決方法
1. 或許你會問,啊如果我都有資料了,要怎樣下 `Prompt` 才會讓回答達到我們的期望?
* 想太多了,最簡單的依舊是 **==課金大法==**,直接把模型開到 **`GPT4`** 他就比較不會回覆北七的問答了。
2. 資料庫都建好了,所以我要怎麼設計一個 `Prompt`?
* 這個問題很難回答,因為每個人的需求都不一樣(做人真是困難 ==;)
* 我們的 `Prompt` 設計:
- 簡單的來講一共可以分為三個部分:
1. **`System Message`**:這部分是用來告訴我們偉大的GPT他要幹嘛,通常有兩種方式可以嘗試。
1. **角色扮演**:舉個例,告訴 `GPT`『嘿👋,你現在是個研華公司的客服,請你只依照顧客提供的問題和資料來源,回答問題』(當然不會打那麼少我只是舉個例而已,請發揮天馬行空,把他當成一張白紙,寫出你對這個角色的想像)
2. **角色扮演佐條列式設定**:這裡的`角色扮演`與上述的不太一樣,比較像是你希望他以怎樣的態度進行回答,屬於較為單純的人設,而`條列式設定`則是告訴他你希望他回答的內容要包含哪些東西,如:
+ 回傳的格式:如 回傳表格時,請使用 `html` 的表格格式
+ 是否要引用資料來源,如果要的話,要引用哪些欄位
c.g. 請依照以下方法進行回覆:your answer [ref.md]...(略)
+ 要用什麼語言回答 $\rightarrow$ 像是你可以叫他依照問題的語言回答,**but 對 就是 but**,事情就不是你想像的那麼簡單。
`GPT3.5`頗笨,你叫他回答繁中,他就用簡中回你,真滴很棒🙃。`GPT4` 就比較沒有這個問題,但我也不能跟你保證
> 上述這些都是要自己去嘗試的,因為我說的在你的case底下也不一定是對的。
2. **`User Question`**:這應該就沒啥問題,就把使用者的問題丟進去就好了。
3. **`Data Source`**:比起純文本範例,**`Markdown`** 可能會更適合,因為 `Markdown` 提供文本更清晰明確的**結構和格式**。尤其如果當你需要請 `GPT` 從資料來源擷取**特定資料**,如標題或是連結或是**文本較長**,`Markdown`會是更適合的選擇。
- 但凡事都有例外,如果你的**資料特別單純,或是長度不長**,純文本會更適合模型理解。
- 簡易範例:
```python=
msg =
'''
<your system message>
Question:
<your user question>
Data Sources:
[info.md]:# 標題\n## 副標題 ...
[info2.md]:# 標題\n## 副標題 ...
'''
```