生成AIを信用しすぎてハルシネーションに引っかかった話

# はじめに

皆さんは日々の業務でAIを利活用しておりますでしょうか。

生成AIの進化は凄まじく、プログラミングの経験が浅い人でも、簡単なアプリケーションくらいなら作れてしまうほどです。すごい時代ですよね。
私自身、要件定義は自身で行い、コーディングはAIに任せるような場面も多いです。

しかし、生成AIを扱う上で常に意識すべきことは、「AIは常に完璧ではない」ということです。
AIの精度は日々進化を続けていますが、それでもハルシネーションが完全に改善できているわけではありません。
> ハルシネーションとは？
ハルシネーション（Hallucination）とはもともと「幻覚、幻影」という意味です。 生成AIがユーザーの質問に対して、事実とは異なる情報を利用して回答を生成することを指し、「もっともらしいうそ」といった意味合いで、こう呼ばれるようになりました。

そのため、AIが生成したコードをそのまま使用すると、予期せぬエラーに遭遇することがあります。特に厄介なのは、AIのコードを丸写しにした場合、エラーが発生しても原因の特定が難しく、解決に膨大な時間を要することがあるのです。

この記事では、外部APIの実装を例に、私の経験したハルシネーションでの失敗談について記していきます。

# 目次
[失敗談](#失敗談)
[どうやって解決した？](#どうやって解決した？)
[ハルシネーションの発生ポイント](#ハルシネーションの発生ポイント)
[最後に](#最後に)
[参照](#参照)

# 失敗談

とある開発案件で外部APIを実装する機会があり、
そこで一時期「おしゃべりひろゆきメーカー」で一世を風靡した、テキストを音声に変換する「CoeFont」というAPIを利用しました。
[CoeFontのサイト](https://coefont.cloud/)

こちらのAPI導入の実装コードを、生成AIの中で最も信頼しているClaude3.5Sonnetに下記のようなプロンプトをぶん投げ、Node.jsで実装してもらいました。

※以下のプロンプトやコードは実際の実装時のものとは異なります。

```
下記はCoeFontAPIのAPIドキュメントになります。
https://docs.coefont.cloud/
こちらを参考に、APIの実装コードをNode.jsで作成してください。
```
で、返ってきたコードがこちらになります。
```js
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');

class CoeFontAPI {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseURL = 'https://api.coefont.cloud/v1';
  }

  async getVoices() {
    try {
      const response = await axios.get(`${this.baseURL}/voices`, {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      });
      return response.data;
    } catch (error) {
      console.error('Error fetching voices:', error);
      throw error;
    }
  }

  async synthesize(text, voiceId, outputPath) {
    try {
      const response = await axios.post(`${this.baseURL}/synthesis`, {
        text: text,
        voiceId: voiceId
      }, {
        headers: { 'Authorization': `Bearer ${this.apiKey}` },
        responseType: 'arraybuffer'
      });

      fs.writeFileSync(outputPath, response.data);
      console.log(`Audio saved to ${outputPath}`);
    } catch (error) {
      console.error('Error synthesizing speech:', error);
      throw error;
    }
  }

  async uploadAudio(filePath) {
    try {
      const form = new FormData();
      form.append('file', fs.createReadStream(filePath));

      const response = await axios.post(`${this.baseURL}/audio_uploads`, form, {
        headers: {
          ...form.getHeaders(),
          'Authorization': `Bearer ${this.apiKey}`
        }
      });
      return response.data;
    } catch (error) {
      console.error('Error uploading audio:', error);
      throw error;
    }
  }
}

// 使用例
async function main() {
  const apiKey = 'YOUR_API_KEY';
  const coefont = new CoeFontAPI(apiKey);

  try {
    // 利用可能な音声一覧を取得
    const voices = await coefont.getVoices();
    console.log('Available voices:', voices);

    // 音声合成
    await coefont.synthesize('こんにちは、世界', 'VOICE_ID', 'output.wav');

    // 音声ファイルのアップロード
    const uploadResult = await coefont.uploadAudio('path/to/your/audio.wav');
    console.log('Upload result:', uploadResult);
  } catch (error) {
    console.error('Error:', error);
  }
}

main();
```
はい、ぱっと見よさそうなコードに見えますが、これでは残念ながら機能しません。
下記のようなエラーがサーバーで発生してしまいました。
```
[500]Internal Server Error
```
その後エラーコードを投げ、何度も実装を試みますが状況は変わらず、、
生成AIの力だけでは解決に至れず、手詰まりとなってしまいました。

# どうやって解決した？
はい、とにかく**公式ドキュメントを読み込みました。**
生成AIと違って、公式は嘘を付きませんからね。
公式ドキュメントに目を通したことで返されたコードの不完全生に気づき、結果的に期限内にAPIの実装を納品することができました。

公式ドキュメントは分かりにくいものも多いのですが、こちらの公式は親切に参考例のコードが書いてありました。
それがこちらです。
```js
const axios = require('axios')
const crypto = require('crypto')
const fs = require('fs')

const accessKey = 'QY4Tuiug6XidAKjDS5zTQHGSI'
const accessSecret = '62A03zCgPflc3NZwFHliphpKFt4tppOpdmUHgqPR'
const text = 'これはテストです。'
const data = JSON.stringify({
  'coefont': '2b174967-1a8a-42e4-b1ae-5f6548cfa05d',
  'text': text
})
const date = String(Math.floor(Date.now() / 1000))
const signature = crypto.createHmac('sha256', accessSecret).update(date+data).digest('hex')

axios.post('https://api.coefont.cloud/v2/text2speech', data, {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': accessKey,
      'X-Coefont-Date': date,
      'X-Coefont-Content': signature
    },
    responseType: 'stream'
  })
  .then(response => {
    response.data.pipe(fs.createWriteStream('response.wav'))
  })
  .catch(error => {
    console.log(error)
  })

```

はい、こちらのコードと公式の内容を注意深く読み込むと、生成AIのコードの不完全性が明らかになってきました。
直すべき箇所は何点かありますが、主なポイントは下記の2つです。

# ハルシネーションの発生ポイント

### エンドポイントが違う
公式のエンドポイントはこのように記載があります。
```
https://api.coefont.cloud/v2/text2speech
```
対して、Claudeの作成したコードのエンドポイントはこのようになっています。
```
https://api.coefont.cloud/v1
```
はい、これではエンドポイントにアクセスできずAPIを叩けない為、500エラーになってしまいますね。
末尾が`v1`となっているので、少し昔のナレッジを参照してしまっているのでしょうか...？

### リクエストヘッダーが違う
公式ドキュメントを見ると、リクエストヘッダーの`Content-Type`、`Authorization`、`X-Coefont-Date`、`X-Coefont-Content`の4つのキーが、APIを叩く上で必須項目であることが分かります。
![SCR-20240825-pjqa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/01c1280d-abbe-fc64-5c2b-7de230309ccb.png)
しかし生成AIの提案したコードを見てみると、
```js
  async getVoices() {
    try {
      const response = await axios.get(`${this.baseURL}/voices`, {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      });
      return response.data;
    } catch (error) {
      console.error('Error fetching voices:', error);
      throw error;
    }
  }
```
このようにヘッダーで`Authorization`しか定義しておらず、不完全なコードであることが分かりますね。

このように、一見しっかりしたコードに見えて、実は重要な部分で不備のあるコードが提供されていたことが分かりました。
「こんな基本的なことを見落とすなんて」と思うかもしれませんが、基礎的な部分だからこそ「AIがこのような初歩的なミスを犯すはずがない」という慢心が自分の中にあり、確認を怠ってしまった為に原因解決に時間を要してしまいました。

# 最後に
今回は自分が生成AIを信用しすぎるあまりにハルシネーションを見落とし、解決に時間がかかってしまったのが原因でした。
生成AIは基本的にすごく優秀なのですが、優秀であるが故にこちら側で間違いを認識しずらい部分があるので、
AIに頼りきりではなく、プログラミング知識（今回のユースケースの場合はフレームワークやRESTful APIの概念など）についての最低限の理解を持つことが極めて重要だと改めて実感しました。

# 参照
https://usknet.com/dxgo/contents/dx-technology/what-is-hallucination/#:~:text=%E3%83%8F%E3%83%AB%E3%82%B7%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%EF%BC%88Hallucination%EF%BC%89%E3%81%A8%E3%81%AF,%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82