独自開発の言語をAIに理解させる

# はじめに
HIKKYでAIエンジニアとして、開発や業務効率化をやっているFukuroと申します！

この記事は**VR法人HIKKY Advent Calendar 2024** の 9日目の記事です。
明日は@subesubeManjuGani さんの【「webサイトにアニメーションは不要」を、ユーザーに合わせて叶える】です

https://qiita.com/advent-calendar/2024/hikky

# 背景
HIKKYでは[**Vket Cloud**](https://cloud.vket.com/)というWEBブラウザ上で動く独自のVRエンジンを開発しており、
そのエンジン上で動くHeliScript(以下HS)という独自のスクリプト言語を開発しています。

https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_overview.html

https://qiita.com/shirapy/items/02acd23c37d112402f7d

HSは情報も少なく、**ChatGPT**や**Claude**などのAIサービスの中に情報がなく、ちゃんと理解して返答をしてくれない問題があります。

>VketCloudのHeliscriptついて教えてください
>>申し訳ありませんが、私の知識の範囲（2023年10月時点）では、VketCloudのHeliscriptに関する具体的な情報がありません。HeliscriptがVketCloudで使用されるスクリプト言語やツールである可能性がありますが、詳細についてはVketCloudの公式サイトや最新のドキュメントをご確認いただくことをお勧めします。
ChatGPT o1-preview

逆に検索した情報を元に返答できるモデルであればそれなりに答えてくれます
(ChatGPT-4o等)

AIを使った業務改善を推進していくにあたり、HSのコードや情報を出せるようにする必要がありました。

# 実験
## RAG(Retrieval-Augmented Generation)
まず真っ先に思い浮かぶのがRAGを使った方法です。
RAGとは、AIによるテキスト生成に、外部情報の検索を組み合わせることで、回答精度を向上させる技術を指します。
これによって、最新の情報について答えさせたり、ハルシネーションを抑制できたりします。

ではRAGを用いて以下のプロンプトで生成させてみましょう
>heliscriptで岩を上から落とすギミックを作ってください

```heli
//コンポーネント
component FallingRock
{
    //岩の初期位置
    private float initialPositionY;
    //岩の落下速度
    private float fallSpeed = 10.0f;
    //岩の最低位置
    private float groundLevel = 0.0f;

    //岩オブジェクトの参照を格納する変数
    private hsItem rock;

    //コンストラクタ関数：ワールド入場時に一度だけ実行されます
    public FallingRock()
    {
        //岩オブジェクトを取得
        rock = hsItemFind("Rock");
        //岩の初期位置を取得
        initialPositionY = hsItemGetPositionY(rock);
    }

    //アップデート関数：毎フレーム実行されます
    public void Update()
    {
        //岩の現在の位置を取得
        float currentY = hsItemGetPositionY(rock);

        //岩が地面に到達していない場合は落下させる
        if (currentY > groundLevel)
        {
            hsItemSetPositionY(rock, currentY - fallSpeed * hsTimeDeltaTime());
        }
        else
        {
            //岩が地面に到達したら初期位置に戻す
            hsItemSetPositionY(rock, initialPositionY);
        }
    }
}
```
一見問題なさそうなコードが出力されました。
しかし、コード内で使っている`hsItemSetPositionY`や`hsItemFind`は実際に存在しない関数であり、このコードは動作しません。

## 全部ぶち込む
さて、RAGでは厳しいことが分かりました。ではどうやって生成させたらいいでしょうか？
問題の本質はHSの知識をAIが把握していないことにあります。全部知っておかないといけない知識が０の状態なので、RAGで断片の情報を与えても正しい回答はできません。
「じゃあ、**全部の情報をプロンプトに入れてしまえば答えられるのでは？**」と考えました。

まず、公開されているHSのマニュアルをテキスト化します。この時点で文字数が大体60,000文字あり、ChatGPTだとコンテキスト長の問題で厳しいため、Claudeを採用しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/940599/73f97146-9559-2b6c-1f7c-764728f10df0.png)
Projectを作成し、テキスト化したマニュアルを全部knowledgeに入れます

それでは回答させてみましょう。質問はRAGと同じです
>heliscriptで岩を上から落とすギミックを作ってください

回答
```
component RockDrop
{
    // 制御する岩のItem
    Item    m_Field;
    // 岩のノード名
    string  m_RockNodeName;
    // 岩の初期位置
    Vector3 m_InitialPosition;
    // 岩が落下中かどうか
    bool    m_IsFalling;
    // 落下のクールダウン時間
    float   m_CooldownTime;
    // 前回の落下からの経過時間
    float   m_ElapsedTime;

    // コンストラクタ
    public RockDrop()
    {
        // Fieldアイテムを取得
        m_Field = hsItemGetSelf();
        
        // 岩のノード名を設定
        m_RockNodeName = "Rock";
        
        // 初期位置を保存
        m_InitialPosition = makeVector3(0.0f, 10.0f, 0.0f); // 高さ10mの位置から落とす
        
        // 落下フラグを初期化
        m_IsFalling = false;
        
        // クールダウン時間を5秒に設定
        m_CooldownTime = 5.0f;
        m_ElapsedTime = m_CooldownTime;

        // 岩の初期設定
        InitializeRock();
    }

    // 岩の初期設定
    void InitializeRock()
    {
        if (m_Field !== null) {
            // 岩を初期位置に配置
            m_Field.SetRotateNode(m_RockNodeName, makeVector3(0.0f, 0.0f, 0.0f));
            
            // 物理演算を無効化して位置を固定
            m_Field.SetPhysicsEnable(m_RockNodeName, false);
            
            // 岩を表示
            m_Field.SetShowNode(m_RockNodeName, true);
        }
    }

    // 岩を落とす
    void DropRock()
    {
        if (m_Field !== null && !m_IsFalling) {
            // 岩を初期位置に移動
            m_Field.SetRotateNode(m_RockNodeName, makeVector3(0.0f, 0.0f, 0.0f));
            
            // 物理演算を有効化して落下させる
            m_Field.SetPhysicsEnable(m_RockNodeName, true);
            
            // 落下フラグを設定
            m_IsFalling = true;
            
            // 経過時間をリセット
            m_ElapsedTime = 0.0f;
        }
    }

    // 毎フレーム実行
    public void Update()
    {
        // クールダウン時間を更新
        m_ElapsedTime += hsSystemGetDeltaTime();

        // 落下中で、かつ一定時間経過したら岩をリセット
        if (m_IsFalling && m_ElapsedTime >= m_CooldownTime) {
            // 物理演算を無効化
            m_Field.SetPhysicsEnable(m_RockNodeName, false);
            
            // 落下フラグをリセット
            m_IsFalling = false;
            
            // 初期位置に戻す
            m_Field.SetRotateNode(m_RockNodeName, makeVector3(0.0f, 0.0f, 0.0f));
        }
    }

    // ノードがクリックされたときの処理
    public void OnClickNode(int NodeIndex)
    {
        string clickedNodeName = m_Field.GetNodeNameByIndex(NodeIndex);
        
        // クリックされたノードが岩の場合、落とす
        if (clickedNodeName == m_RockNodeName && !m_IsFalling) {
            DropRock();
        }
    }
}
```

素晴らしいです！いくつか不足している箇所（位置と落下速度の初期化部分）がありますが、実際に動作するコードが出力されました。

# まとめ
必要な情報をすべて入力に投げることで独自言語でも回答ができることが分かりました。
また、同じくコンテキスト長が大きいGeminiなどでも回答させることが出来ました。
（Geminiは入力できるファイル数に制限があるので一つのファイルにまとめるなどの工夫が必要です）
今回はClaudeのProjects機能を使って回答させましたが、API経由でやる場合は料金がかかってしまうのが欠点ですね。この辺はうまく使い分けていきたいところ。

**結論：コンテキスト長は正義**