stMind

about Tech, Computer vision and Machine learning

ollama runを追え

ollamaは各種LLMをローカルで実行できるCLIツールで、以下のコマンドが用意されています。

Available Commands:
  serve       Start ollama
  create      Create a model from a Modelfile
  show        Show information for a model
  run         Run a model
  pull        Pull a model from a registry
  push        Push a model to a registry
  list        List models
  ps          List running models
  cp          Copy a model
  rm          Remove a model
  help        Help about any command

ここでは、LLMモデルを実行するrunコマンドについて、 ollama run llama3.1とした場合の実行パスをトレースしてみようと思います。

概要

ここではollamaのv0.3.5を使います。 ollamaではCLI作成ライブラリとしてcobraを利用しています。cmd/cmd.goに各コマンドが記述されていて、NewCLIメソッドでrunCmdが作成されます。runCmd実行前(PreRunE)には、checkServerHeartbeatメソッドでサーバー起動を確認、起動してなかった場合はエラー終了します。RunHandlerメソッドが処理本体になっていて、入力されたプロンプトに対して、LLMが生成した回答をコンソール出力するまでのパスを見ていきます。

RunHandlerメソッドでプロンプト入力からLLMが回答を生成するまで

RunHandlerはinteractiveかnot interactiveのどちらかで実行されます。ollama run lllama3.1の場合はinteractiveがtrueで、ollama run llama3.1 helloのようにモデル名のあとにプロンプトを与えた場合にはinteractiveがfalseとなります。

RunHandlerの最初にinteractive := trueと設定していて、プロンプトを与えた場合には以下のコードでプロンプトがあるかを判定して、interactive = falseと設定し直しています。args[1:]がモデル名以降の引数で、promptsとして保持しています。

prompts := args[1:]
// prepend stdin to the prompt if provided
if !term.IsTerminal(int(os.Stdin.Fd())) {
    in, err := io.ReadAll(os.Stdin)
    if err != nil {
        return err
    }

    prompts = append([]string{string(in)}, prompts...)
    opts.WordWrap = false
    interactive = false
}
opts.Prompt = strings.Join(prompts, " ")
if len(prompts) > 0 {
    interactive = false
}

指定したモデルがpullされてなかった場合には、次にpullが行われます。nameがモデル名で、clientのShowメソッドでモデルに関する情報を取得します(ollama show llama3.1とした場合と同じ)。モデルがなければse.StatusCodeが404となるので、PullHandlerでモデルをpullする処理が実行されます。

name := args[0]
info, err := func() (*api.ShowResponse, error) {
    showReq := &api.ShowRequest{Name: name}
    info, err := client.Show(cmd.Context(), showReq)
    var se api.StatusError
    if errors.As(err, &se) && se.StatusCode == http.StatusNotFound {
        if err := PullHandler(cmd, []string{name}); err != nil {
            return nil, err
        }
        return client.Show(cmd.Context(), &api.ShowRequest{Name: name})
    }
    return info, err
}()
if err != nil {
    return err
}

RunHandlerの最後で、interactiveの場合にはgenerateInteractive、not interactiveの場合にはgenerateでLLMから生成された回答を出力します。ここでは、interactiveの動作を追いかけるので、generateInteractiveを見ていきます。

generateInteractiveでは、ターミナルからの入力に対して、/listや/loadなどのコマンドかプロンプトかをswitch caseで判定します。その後、プロンプトの場合には、LLMに対して送信するメッセージを作成し、LLMから生成された回答を受け取ってターミナルに出力します。これはchatメソッドの中で実行されます。また、作成したメッセージと回答を履歴として保持したメッセージに更新して、次の入力を待ちます。

if sb.Len() > 0 && multiline == MultilineNone {
    newMessage := api.Message{Role: "user", Content: sb.String()}

    if opts.MultiModal {
        msg, images, err := extractFileData(sb.String())
        if err != nil {
            return err
        }
            // clear all previous images for better responses
        if len(images) > 0 {
            for i := range opts.Messages {
                opts.Messages[i].Images = nil
            }
        }

        newMessage.Content = msg
        newMessage.Images = images
    }

    opts.Messages = append(opts.Messages, newMessage)

    assistant, err := chat(cmd, opts)
    if err != nil {
        return err
    }
    if assistant != nil {
        opts.Messages = append(opts.Messages, *assistant)
    }

    sb.Reset()
}

最終的にollama run llama3.1で以下のようにやりとりが出来ることになります。

$ ollama run llama3.1
>>> hello
Hello! How are you today? Is there something I can help you with, or would you like to chat?