【Prompt flow入門①】Prompt flowの作成・実行・カスタマイズ方法

# まえがき

今回は入門ということで、以下の2つのことをまとめています。

- Prompt flowの作成～実行
- 簡易的なタスクをPrompt flowで実装


Prompt flowの便利機能には今回触れていません。次回以降まとめていきます。
以下のような便利機能を試していく予定です。

- マネージドオンラインエンドポイントの作成
- Variant
- Bulktest
- ベクトルDBやSerp APIとの連携

# リソース構築

## 事前作成が必要なもの

- Azure OpenAI
    - gpt-35-turbo
- Machine Learning Studioのワークスペース


Azure OpenAIを作成し、上記モデルをデプロイします。エンドポイントとキーをコピーしておきます。
また、下記URLからワークスペースを作成しておきます。

https://ml.azure.com

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7fbc3411-2bea-6da1-9418-ce1b6a8608ce.png)


## Prompt flowとAzure OpenAIの接続

**Connenctions**タブへ移動し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/80ce31d9-4c69-ed7d-b4b8-94f0ef53361c.png)

**Azure OpenAI**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/f3210595-9f1f-e085-0d47-623d7cd19bd9.png)

名前・APIキー・エンドポイントを入力し、**Save**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/a1cba470-5eda-30a9-f65b-91165778c500.png)




## Prompt flowの作成

左側のメニューから**Prompt Flow**をクリックし、**Create**をクリックし、**Standard flow**を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/1fc1ab7e-f213-b96b-73a9-ac561e4d64ee.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b1278ab3-ba95-ff87-e96e-df311db04973.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/55f5c3c9-9731-7c1b-2b4d-edf5579767f8.png)



# 初期状態のPrompt flowを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/af35a2c1-fd0c-6790-2e1f-da17ca72d536.png)


リソース作成直後の状態を確認してみます。画面左側を見てみると、inputとoutputが定義されています。

- ```inputの定義```：textという変数名でHello World!が格納されます
- ```outputの定義```：output_promptという変数名でecho_my_prompt.pyの実行結果(return値)が格納されます


画面右側を見ると、「**入力～出力の間でデータに対してどのような処理を実行するか** 」が可視化されています。
次に、hello_promptとecho_my_prompt.pyの詳細を確認してみます。画面をスクロールすると定義が確認できます。




- ```hello_prompt```
    - サンプルプロンプトが定義されています
    - ${inputs.text}の値がプロンプト内の{{text}}で展開されています
    - ここには先ほど確認した「Hello World!」という文字列が展開されることになります


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/bed898f2-ab07-f907-6b5c-06232988e570.png)



- ```echo_my_prompt.py```
    - promptflowパッケージからデコレーター:toolをインポートしています
    - デコレーターで修飾した関数をPrompt flowのツールとして利用可能になります
    - 関数の引数はhello_promptの出力となっています


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/04d6178b-69c9-b7eb-0dab-5170d287eb84.png)



# 初期状態のPrompt flowを実行

実行準備は２つです。

- ランタイムを作成する
- Azure OpenAIに接続する（上記手順で実施済み）


## ランタイムの作成

まずは、コンピューティングリソースを作成します。**Add runtime**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/63654384-a9ee-63df-73ad-7f9ba5be8f78.png)

**Compute instance runtime**を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/d543a04e-918d-f0cf-fcfb-751d915691f7.png)

**Create Azure ML compute instance**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/29f59ae8-a231-f2e1-671d-53d728706d66.png)

名前を付けてVMのサイズを選択し、**Create**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/f78a64ab-d2f4-0d81-c39b-c108a2f1d7a6.png)


作成されるのを待ちます。
作成後、ランタイムの名前を付け、先ほど作成したコンピューティングリソースを選択すると「認証が必要です」と表示がでます。
**Authenticate**をクリックし、サインイン画面が表示されたのでAzureアカウントでサインインすると認証はOKとなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/01040e05-4b3e-1b20-494e-d715b4a14ede.png)

**Create**→**Confirm**とクリックし、ランタイムを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/45b1a892-e8a5-8cc6-4b0c-846859fcb9b8.png)


## 実行してみる

画面右上にある**Run**ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/57e58d70-42b1-6060-28c9-21c76985f5af.png)

するとフローが実行され、実行が終わるとステータスが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/073238e6-2d95-47fb-4ea8-1956ec6645cb.png)


## 詳細を確認してみる

hello_promptにカーソルを持っていくと、**Details**というボタンが表示されるのでクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/49c29582-c493-8dab-47c5-8b01c15915f6.png)

inputとoutputの情報が展開されますが、画面を右に移動できない(?)のでDetailsの表示が切れてしまっています、、:frowning2:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/f43e61d9-7015-f9bc-5622-3888ca6ab435.png)

hello_promptの定義に詳細情報が表示されているのでこちらから確認してみます。今回のinputは「Hello World!」という文字列です。ちゃんとhello_promptに入力されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/87c0ba99-2820-298c-2fc2-7b35bac66650.png)


echo_my_promptも同じ要領で確認可能です。


# Prompt flowのカスタマイズ

今回は簡易的に以下のようなタスクを例にフローを作成してみます。

- タスク：
    - Function calling用の関数のメタデータをJSON形式で生成する
- フロー：
    - ①ユーザの入力からJSON形式の文字列を出力
    - ②PythonでJSON形式の文字列をパースして出力


出力をFunction callingで利用するのであれば文字列でいいので②は必要ありませんが、Prompt flowのお勉強という名目なので無駄な処理を足しています。

**最終的に出来上がったフロー**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/14db13c6-a150-ec37-c6bc-31d97a0b4917.png)



**入出力のイメージ**

```sh:
# 入力
社内情報を検索したい。

# 出力
{
    "name": "search_file",
    "description": "社内情報を検索する",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "ファイル検索のクエリ"
            },
            "source":{
                "type": "string",
                "enum": ["Fileserver, Database"]
            }
        },
        "required": ["query", "source"]
    }
}
```




## LLMの追加

さきほどの初期状態のフローですが、Azure OpenAIが登場していません。文字列を入力して、その文字列をそのまま出力しています。
ここにAzure OpenAIを組み込むことで、入力から何らかの文字列を生成することが可能になります。

**LLM**ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b3dee563-7229-65dc-6d33-7848c64331be.png)

すると、画面左側に詳細な定義情報、画面右側にコンポーネントが追加されます。後ほどこれを編集していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/fd39e23b-e3b4-b2ac-8ddd-1b62d908a315.png)

## inputとプロンプトの変更

inputを変更します。名前を**user_input**にし、値を**Slackに任意のメッセージを送りたい**としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/cc914735-6261-c331-8982-a33a5e434962.png)


hello_promptを編集し、JSON文字列生成用にします。
ペンマークを押して名前を**json_prompt**に変更し、プロンプトを書き換えました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/cab95dab-090f-8f64-fa63-4ece0f3ae896.png)


<details>
<summary>書き換え後のプロンプト</summary>

```sh:
system:
- あなたはJSONを出力するAIです。
- ユーザの入力に適切に答えられるよう、ステップバイステップで慎重に考えることができます。

user:
# ゴール
- ユーザの入力に対して、JSON形式の出力を行うこと

# 出力例
\```  (バックスラッシュは必要ありません。)
{
    "name": "search_file",
    "description": "社内情報を検索する",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "ファイル検索のクエリ"
            },
            "source":{
                "type": "string",
                "enum": ["Fileserver, Database"]
            }
        },
        "required": ["query", "source"]
    }
}
\```  (バックスラッシュは必要ありません。)

# 条件
- JSONはマークダウン形式のコードブロックとして出力すること

入力:{{text}}
```

</details>

chatタイプのモデルを使用するときは、以下のような書式にする必要があります。

```sh:
systems:
#システムメッセージ

assistant:
#モデルの返答

user:
#ユーザの入力
```


修正後の定義を確認します。inputsの変数名はtextのままで、Valueを```${inputs.user_input}```に変更しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/821ae378-82a1-161d-87e7-f768126f3642.png)


## LLMの編集

「**json_promptの出力をLLMに渡して出力を生成する**」というフローを作成していきます。
手順はさきほどと大体同じですが、LLMの場合はモデルの情報を設定する必要があります。

- (名前を変える)
    - json_generatorとしました。
- モデルの情報を編集
    - どのリソースのどのモデルを使うか指定
    - その他パラメータを指定
- Promptを定義する
    - 変数展開のみのプロンプトを記述
- Inputsを編集する
    - inputsを追加する際は、**Validate and parse input**をクリックする必要があります
    - さきほど定義した**json_prompt**の出力を指定


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/f863d7ec-f70a-a604-b6cf-3d6605324e5f.png)


## JSONをパースする関数を作成・outputを変更

モデルの出力を整形するための関数を作成します。処理の中身は単純で、必要な情報を抽出してPython内で扱える辞書型に変換するだけです。

- 「\```生成されたJSON文字列```」の形式にマッチする部分を探す
- マッチした部分を取り出し、JSONとしてパース


```py:
from promptflow import tool
import json
import re
from typing import Any, Union, Dict, List

@tool
def parse_json(input1: str) -> Union[Dict, List, Any]:
    extracted_str = _extract_code(input1)
    return json.loads(extracted_str)


def _extract_code(text: str) -> str:
    triple_match = re.search(r'```(?:\w+\n)?(.+?)```', text, re.DOTALL)
    return triple_match.group(1).strip()
```


フローの定義の編集については、先ほどと大体同じ手順です。

- 名前を変える
- コードを修正する
- inputsのValueを変更する


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/1e4c6be6-983d-1b2e-cacb-d6f371700590.png)


さらに、outputを編集する必要があります。関数名を変更したため、変更後の関数の出力を参照するように変更します。(自動で反映されていました。自動反映されていない場合もあった気が、、)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/c844c0a3-2f7a-0565-806c-fa320a2b0f7a.png)


## 実行してみる

**Run**ボタンをクリックし、実行ステータスをチェックします。
すべて```Completed```となっているのでよさそうですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b61d6eb6-8aa1-0a38-e28a-83dab9b0da05.png)


出力(outputsの中身)を確認してみると以下のような結果となりました。
JSON形式で出力してくれています。また、「Slackにメッセージを送りたい」と入力しただけですが、チャネル名に該当する引数も必要だと判断してくれています。

```sh:
# 入力
Slackに任意のメッセージを送りたい。

# 出力
{
    "description": "Slackにメッセージを送信する",
    "name": "send_slack_message",
    "parameters": {
      "properties": {
        "channel": {
          "description": "送信するチャンネル名",
          "type": "string"
        },
        "message": {
          "description": "送信するメッセージ",
          "type": "string"
        }
      },
      "required": [
        "message",
        "channel"
      ],
      "type": "object"
    }
}
```

# まとめ

Prompt flow内でPythonの関数を簡単に使用できるのはめちゃくちゃ使える機能だと思いました。
モデルの出力整形やエラーハンドリング、その他様々な処理を簡単に組み込めますね。

主観ですが、LLMアプリケーションを開発する上で以下2つは重要な要素だと考えています。
- ①プロンプトに埋め込むためのデータの収集・加工
- ②プロンプトエンジニアリング

Python関数とPrompt flowの便利機能を組み合わせることで①②の実装が楽になりそうです。
また、管理・評価の面に関しても機能が充実してそうなので次回は便利機能を色々と試していきますー。



# 参考

上位概念から丁寧にまとめられているため非常に参考になります。

https://qiita.com/nohanaga/items/7c8b797f20ab8a3d5c92#azure-ai-studio