エクセルでローカルLLM

エクセルでローカルLLMをやってみました！
.NET（C#）でxllをつくってVBAから使います。
LLamaSharpというのを使っています。
完成したのを以下です。

https://github.com/msmsrep/ExcelAddinLLM


# 準備
## 環境
- windows11
- NVIDIA GeForce RTX 3060 VRAM 12GB
- excel2019 64bit
- .net8 vscode

## ライブライの準備など
- dotnet new classlib -n ExcelAddinLLM
- dotnet add package ExcelDna.Addin
  [Excel-DNA](https://excel-dna.net/)を使ってxllにします
- dotnet add package LLamaSharp --version 0.19.0
  [LLamaSharp](https://scisharp.github.io/LLamaSharp/0.19.0/QuickStart/)でLLamaを使います
- dotnet add package LLamaSharp.Backend.Cuda12 --version 0.19.0
  CPUだけなどでこれは変える必要があります

## 使用するモデル
モデルは以下を使用してみました。
- [**Llama-3-ELYZA-JP**](https://note.com/elyza/n/n360b6084fdbd)
  - [elyza/Llama-3-ELYZA-JP-8B-GGUF](https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF/tree/main)

## cudaを使えるようにする
- [CUDA Toolkit](https://developer.nvidia.com/cuda-downloads)をインストールしておく必要があるようでした

# コード内容
## Excel-Dnaでの呼び出し

Excel-Dnaはデバッグとかするための準備が少し必要です。

- [Excel-DNAを使って.NETでエクセルアドイン（.xll）をつくる](https://qiita.com/msms/items/7de38ab10799af195c62)
- [Excel-DNAで非同期処理の関数をつくる](https://qiita.com/msms/items/0978934282c0067e6450)
- [dllファイルが見つからない](https://qiita.com/msms/items/a807ca397237cbb54230#dll%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84)

```ExcelAddonFunctions.cs
using ExcelAddinLLM.Utils;
using ExcelDna.Integration;

namespace ExcelAddinLLM;

public static class ExcelAddonFunctions
{
    private static readonly object ReturnNA = new();
    [ExcelFunction(Description = "ローカルLLM")]
    public static string SayLLM(string targetValue, int row, int col)
    {
        try
        {
            var functionName = nameof(SayLLM);
            var parameters = new object[] { targetValue, row, col };
            var result = AsyncTaskUtil.RunTask(functionName, parameters, async () =>
              {
                  return await LLamaAddin.LLamaChatAsync(targetValue, row, col);
              });
            if (result.Equals(ReturnNA))
                return ExcelError.ExcelErrorNA.ToString();
            if (result.Equals(ExcelError.ExcelErrorNA))
                return "Busy...";
            else
                return "true";
        }
        catch (Exception ex)
        {
            return ex.ToString();
        }
    }
}
```

## LLamaSharpの処理
Exampleから[Instruct executor - basic](https://scisharp.github.io/LLamaSharp/0.19.0/Examples/InstructModeExecute/)の内容を使っています。

```LLamaAddin.cs
using LLama.Common;
using LLama;
using ExcelDna.Integration;

namespace ExcelAddinLLM;

public static class LLamaAddin
{
    public static async Task<string> LLamaChatAsync(string inputText, int row, int col)
    {
        ExcelAsyncUtil.QueueAsMacro(() =>
         {
             var cellRef = new ExcelReference(row - 1, row - 1, col - 1, col - 1);
             cellRef.SetValue("");
         });
        try
        {
            DirectoryInfo DirectoryInfo = new(Path.GetDirectoryName(ExcelDnaUtil.XllPath)
                                        ?? new(AppDomain.CurrentDomain.BaseDirectory));
            string modelPath = Path.Combine(DirectoryInfo.FullName, @"Llama-3-ELYZA-JP-8B-q4_k_m.gguf");
            var prompt = @"以下はタスクを記述した指示である。要求を適切に完了する応答を書きなさい。";
            prompt += inputText;
            var parameters = new ModelParams(modelPath)
            {
                ContextSize = 1024,
                GpuLayerCount = 20
            };
            using var model = LLamaWeights.LoadFromFile(parameters);
            using var context = model.CreateContext(parameters);
            var executor = new InstructExecutor(context);
            var inferenceParams = new InferenceParams() { MaxTokens = 600 };
            var resultText = "";
            await foreach (var text in executor.InferAsync(prompt, inferenceParams))
            {
                resultText += text;
                ExcelAsyncUtil.QueueAsMacro(() =>
                {
                    var cellRef = new ExcelReference(row - 1, row - 1, col - 1, col - 1);
                    cellRef.SetValue(resultText);
                });
            }
            return "true";
        }
        catch (Exception ex)
        {
            return ex.ToString();
        }
    }
}
```

## vbaから呼び出す
ggufなど必要なファイルはxllと同じ場所に保存しておきます。
引数は以下のように指定します。
(LLMに聞きたい内容, 出力行,出力列)

```vb
Sub llm()

    Debug.Print Application.Run("SayLLM", ActiveSheet.Range("B3").Value, 3, 4)

End Sub
```

# 完成！
![LLMのgif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2383487/1e2139ea-3206-a9b5-5807-717e9275fae0.gif)



# その他
## DeepSeekできなかった
DeepSeekの日本語学習したggufを使ってみましたができなかった。
Excel-DnaじゃなくてLLamasharpだけのチャットになら使えました。
- [DeepSeek-R1-Distill-Qwen-14B/32B_cyberagent](https://x.com/CyberAgent_PR/status/1883783524836413468?)
  - [mmnga/cyberagent-DeepSeek-R1-Distill-Qwen-14B-Japanese-gguf](https://huggingface.co/mmnga/cyberagent-DeepSeek-R1-Distill-Qwen-14B-Japanese-gguf/blob/main/cyberagent-DeepSeek-R1-Distill-Qwen-14B-Japanese-Q4_K_M.gguf)

## llamasharpのバージョン
LLamaSharpの最新バージョンは0.21.0ですが、ExcelDnaだと0.19.0でしか使えませんでした。
dllファイルが2つから4つに変わっているようですが、ちゃんとコピーしてるのにダメでした。

## モデルがよみこめない
ggufを保存しているパスに日本語があると読み込めないようです。
なにか変換とかできるのかは分からない。

## 参考
- [日本語のローカルLLM（文章生成AI）をWindowsで動かす](https://qiita.com/kenta1984/items/7233f8ec9d256f4fa4f7)
- [「GPT-4」を上回る日本語性能のLLM「Llama-3-ELYZA-JP」を開発しました](https://note.com/elyza/n/n360b6084fdbd)
- [llama.cppでGGUF形式のモデルファイルを読み込み、チャットする](https://note.com/kohya_ss/n/nd80b439d1a27)

## Excel-DNAの他
- https://github.com/Excel-DNA/ExcelDna
- [Excel-DNAを使って.NETでエクセルアドイン（.xll）をつくる](https://qiita.com/msms/items/7de38ab10799af195c62)
- [Excel-DNAで非同期処理の関数をつくる](https://qiita.com/msms/items/0978934282c0067e6450)
- [ExcelからSQLiteを使う時の方法として](https://qiita.com/msms/items/a807ca397237cbb54230)