【Dify】エージェントノードが不安定な時の代替策：公開エージェント×API実行

# はじめに
この記事では、Difyのチャットフロー内エージェントノードが不安定で期待通りの回答を得られないときに使えるTipsを紹介します。
Dify上で独自エージェントを作成・公開し、APIキーを用いてHTTPリクエストで呼び出すことで改善するかも、、

今回の手法はチャットフローのエージェントノードの不安定さの解消に役立ちますが、同時に同方法を用いて外部ツールからDifyを呼ぶこともできると思います。

ぜひご参考にしてください。

# Difyについて
[Dify](https://dify.ai/jp)は、ノーコード・ローコードで生成AIアプリ開発を行えるWebアプリです。
事前に用意された**ノード**と呼ばれる塊をドラッグ＆ドロップしてつなげることで、ほぼほぼコード実装を必要とせず生成AIアプリが作れちゃいます。

生成AIアプリとしてRAG（Retrieval Augmented Generation）やAIエージェントなどの作成や、それらを用いた地涌度の高いアプリを簡単に作成することができ、AIアプリのPoCや試作にも使いやすい今注目のサービスです。

サーバー管理はDify上で行ってくれるますし、作成したアプリも簡単にAPI公開することで外部連携もササッとできちゃいます。

# Dify チャットフローでのエージェント活用
Difyで作成できるAiサービスの中にチャットフローというものがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/8eef0e1e-3515-4bfd-8af2-ca90f9e7596e.png)

チャットフローは、機能を持つノード（LLMやプラグインなど）をドラッグ＆ドロップでつなぎ「入力→処理→出力」の流れを定義することで、プログラミングなしで対話型AIアプリやRAGを含む複雑なワークフローを構築できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/56b46326-89ce-4572-9393-c11bdfbfe051.png)
上記がチャットフロー開発画面です。「開始」や「LLM」などがノードと呼ばれるブロックで、今回の例は開始からLLMに処理を渡し、その結果を回答で出力するものです。途中ノードを増やすことで処理を追加・分岐・そのたさまざまなふるまいをさせることができます。
プラグインを追加することで特定の機能を実施してくれるツールも用いることで開発がより柔軟でスピーディーに行うことができます。

## エージェントノード
AIエージェントの詳細は割愛しますが、AIさんが適切なツール（例えばWeb検索やWeb画面操作）を選択・使用して自律的に行動を決定し実行を行うAIシステムです。（ファンクションコーリングなんてもう死語）
従来のプログラム（いったんAI以外）では事前に決まった処理しかすることができませんでした。AIを使用することでまるで人が考えたような柔軟な（まるで知性があるような）対応をすることができるようになりました。

Difyのアプリを開発する中でエージェントノードを用いてAIエージェントをフローの中に組み込むことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/426cf021-4cc4-4f45-b12b-20353da03d90.png)

このようにエージェントノードに必要な設定をすることでエージェント的なふるまいをしてくれます。
今回はWeb検索ツールを3種類（ほぼ同じ機能なのであまり意味ないwww）与えましたが、playwrightを与えるとAIが自動でWeb画面操作をしてくれたりgithubやslackのツールを与えることでPRを自動で書いてくれたりメッセージを回答してくれたりと自動化がはかどります。（はかどるはずです。いうことを聞かないことも多々多々ある。。。）

LLMには**プロンプト**と呼ばれる指示文を与えることで意図した（意図に近い）振る舞いをしてくれます。
今回は例として以下のような指示で始まるプロンプトを作成してみました。
``` text
あなたは、与えられた証券コードに基づいて、株式銘柄の分析を行うAIアシスタントです。以下の手順に従って、分析結果を出力してください。出力は、XMLタグを含まないプレーンテキスト形式でお願いします。
```

では実際に動作させてみましょう。
画面右上のプレビューをクリックし、チャット形式で質問を投げてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/f3f5a3ae-7ad1-4efa-a990-f5b7b709037d.png)

今回はUFJ銀行（8306）の株価でも調べてもらいましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/4b57fb43-0839-4550-8e6c-3ae2d9246d39.png)

### うまくいきませんでした。

デバッグを兼ねてAIがどういう判断をしたか調べてみましょう。
Difyでは以下のように、実行後の処理の流れを追いながらデバッグをすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/2ea9f5cc-b222-4c75-af67-a7f47c74044b.png)

エージェントノードの出力として上記の文章が出力されます。

___

```
提供されているツールは一般的なウェブ検索ツールであり、特定の金融データソースから構造化された情報を直接取得するAPIではありません。
```

調べろ。。

```
指定された証券コードの情報は取得できませんでした。
```

簡単に諦めんな。お前が証券マンなら激詰めされてるぞ

___

ということで、エージェントノードを使用すると意図した結果を得られないことが多々あります。
※エージェントノードには`Maxium Iterations`という何回までツールを用いたふるまいを行うかを指定できます。
が、回数を増やしても処理を終わりきらずに勝手にやめたり検索結果を正しく得られないということが開発中で何度も遭遇してしまいました。

# 本題 - エージェントを別で実装しAPI公開すればよい
ではどうすればよいのでしょうか。
Difyでエージェントを作成し、エージェント機能単体を実装。それを公開しAPI連携でチャットフローから使用すればよいのです。

まずはエージェントを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/8e30b1b7-dc65-4415-814f-b986eedcee97.png)

___

エージェントの作成は以下のような画面で行います。
プロンプトと使用するツールは先ほどと同じもの。変数（入力）を新たに定義します。
先ほどと同じようにUFJの番号を入力すると、、、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/bbef7b1c-2aa6-46b5-8234-0a1743373776.png)
tavely searchというもので検索を実施中です :label:

### エージェントでの回答！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/23c07481-6677-4912-a5c7-2fd7a1df3ee8.png)
ちゃんと調べた内容が出てる

### では公開してみましょう
右上の「公開する」から更新を公開します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/2f0988fb-184d-4dcc-bfb5-4d19d710bf64.png)

次にAPIリファレンスを開きAPIキーを取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/d0b7b1eb-140d-46fa-8cac-d75bef6757b5.png)　
右上からキーを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/3c4e7b5c-4af6-4a18-9389-00d71a9580b8.png)

## チャットフローでの実装
チャットフローに戻り、エージェントノードとの接続を切ります。
新たにHTTPリクエストを追加し、以下のように書きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/42eab531-79c3-4cd3-86f2-087163ca96af.png)

HTTPリクエストする際の各パラメータは以下を参考にしてください。

|公開する → APIリファレンスを開き、、|表示されるドキュメントを元に必要な情報を入力|
|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/051bcb2c-7780-474d-ad01-a82c7b92bd8d.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/57531ed5-b34a-4f24-865c-93457bcc325c.png)|


``` bash
URL：(POSTメソッド)　`https://api.dify.ai/v1/chat-messages`
ヘッダーキー１：`Authorization`: `Bearer {api_key}`
ヘッダーキー２：`Content-Type` : `application/json`

{
  "inputs": {"stock_code": "{{#sys.query#}}"}, // エージェントの引数。作成したエージェントの定義に合う名前・個数にする
  "query": "お伝えした証券コードの内容を検索し、株式情報について回答してください", // ユーザー入力/質問内容
  "response_mode": "streaming", // 以下を参照ください
  "user": "abc-123" // ユーザー識別子
}
```

:::note
response_mode

* `streaming` ストリーミングモード（推奨）、SSE（Server-Sent Events）を通じてタイプライターのような出力を実装します。
* `blocking` ブロッキングモード、実行完了後に結果を返します。（プロセスが長い場合、リクエストが中断される可能性があります） Cloudflareの制限により、100秒後に応答がない場合、リクエストは中断されます。 注：エージェントアシスタントモードではブロッキングモードはサポートされていません
:::


HTTPレスポンスで得られた回答のままでは、その後の処理にて使用しにくいので以下のようにPythonでパース氏回答部分だけを抽出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/08e01943-3431-487b-8001-a4cb535db597.png)

``` python
import json
def main(body: str) -> dict:
    answer = ""
    for chunk in body.split("\n\n"):
        if not chunk.startswith("data:"):
            continue
        raw = chunk[len("data:"):].strip()
        try:
            obj = json.loads(raw)
            if "answer" in obj:
                answer += obj["answer"]
        except json.JSONDecodeError:
            continue

    return {"answer": answer}

```


# 動作比較
以下のようにエージェントノードでは回答が得られなくても自作エージェントに外部リクエストを行うことで検索をしてくれるような結果を得ることができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/49b7cbb7-cbe7-4bd2-a873-64c257cf0b60.png)

### 他の例）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604944/75b10ac2-98c6-4e49-9feb-56834366c606.png)


## 最後に
エージェントノードから得られる回答は、プロンプトによるところが大きいです。
しかし、エージェントノードのプロンプトをいじってみて思うような回答が得られない場合は、外部にエージェントを立てAPIリクエストをしてみてはいかがでしょうか？