Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Go StudyでAIエージェントを実装しました

はじめに

こんにちは!サーバーサイドエンジニアの西村です。

弊社では技術顧問のtenntennさんを招いてGo StudyというGoの勉強会を月1で開催しています。

tenntennさんに解説してもらったり、実際に手を動かしたり、わいわいスレでみんなでわいわい(質問・感想投げ)しながら実施しています。

前回は4月に開催した「GoでMCPサーバーを実装する」を記事にしましたが、今回は5月に開催した内容の「GoでAIエージェントを実装する」という内容を紹介します。

gollemを読んでみる

今回、gollem をつかってAIエージェントを作ります。

こちらにある通り、geminiやopenai、caudeといったpackageが存在するので、これらのLLMを利用できます。

gollem/examples/tools/main.goを軽くみてみる

※開発が活発なのでコードが頻繁に更新されており、内容が異なる場合がありますがご了承ください。

こちらに簡単なサンプルコードがあるので、どのようにしてAIエージェントを組み立てるのかをみていきます。

大まかな流れは下記で、全体の流れをみながらがわかりやすいと思うので、具体的なコードの上にコメントで関数毎の補足を書いています。

①Geminiクライアントの初期化

②toolを作成する

③agentを作成して上で作成したtoolを登録する

④agent.Excuteメソッドで、指定されたプロンプトを使用してエージェントタスクを実行

func main() {
    ctx := context.Background()

    // ①Geminiクライアントの初期化
    client, err := gemini.New(ctx, os.Getenv("GEMINI_PROJECT_ID"), os.Getenv("GEMINI_LOCATION"))
    if err != nil {
        log.Fatal(err)
    }

    // ②toolを作成する
    tools := []gollem.Tool{
        &AddTool{},
        &MultiplyTool{},
    }

    // ③agentを作成して上で作成したtoolを登録する
    agent := gollem.New(client,
        gollem.WithTools(tools...),
        // ③-1: gollemエージェントからのシステムプロンプトをセットする。
        gollem.WithSystemPrompt("You are a helpful calculator assistant. Use the available tools to perform mathematical operations."),
        // ③-2: メッセージ用のコールバック関数。LLM から生成されたテキストメッセージを受信した際に呼び出される。
        gollem.WithMessageHook(func(ctx context.Context, msg string) error {
            log.Printf("🤖 %s", msg)
            return nil
        }),
        // ③-3: ここにはコールバック関数をセットする。toolを実行する前に呼び出される。
        gollem.WithToolRequestHook(func(ctx context.Context, tool gollem.FunctionCall) error {
            log.Printf("⚡ Using tool: %s", tool.Name)
            return nil
        }),
    )

    query := "Add 5 and 3, then multiply the result by 2"
    log.Printf("📝 Query: %s", query)

    // ④agent.Excuteメソッドで、指定されたプロンプトを使用してエージェントタスクを実行。セッション状態を内部で管理しているので手動での履歴管理無しに連続した会話が可能。
    if err := agent.Execute(ctx, query); err != nil {
        log.Fatal(err)
    }

    log.Printf("✅ Calculation completed!")
}

実際にAIエージェントを作ってみる

準備

mkdir gollemsample
cd gollemsample
go mod init gollemsample

mainファイル

nilだけ返すrun関数と、main関数を下記のように実装しておきます。

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/m-mizutani/gollem"
    "github.com/m-mizutani/gollem/llm/gemini"
    "google.golang.org/api/option"
)

func main() {
    ctx := context.Background()
    if err := run(ctx); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(ctx context.Context) error {
    return nil
}

OpenAIのクライアントを初期化

今回はOpenAIを利用しようと思います。

run関数の中でOpenAIのクライアントを初期化します。

また、OPENAI_API_KEYをこちらのページで作成し、.envファイルに設定してください。

import "github.com/m-mizutani/gollem/llm/openai"

func run(ctx context.Context) error {
     err := godotenv.Load()
     if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
      }
 
      // OpenAIのクライアント初期化
      client, err := openai.NewClient(os.Getenv("OPENAI_API_KEY"))
      if err != nil {
          return fmt.Errorf("failed to create OpenAI client: %w", err)
       }
      return nil
}
OPENAI_API_KEY=xxxxx

エージェント作成、実行

run関数の中で初期化したエージェントを実行します。

func run(ctx context.Context) error {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
    }
    client, err := openai.New(ctx, os.Getenv("OPENAI_API_KEY"))
    if err != nil {
        return fmt.Errorf("failed to create OpenAI client: %w", err)
    }

    // エージェントの初期化
    agent := gollem.New(client)
    // 一旦プロンプトを"こんにちは"でベタ書きし、エージェント実行
    prompt := "こんにちは"
    err = agent.Execute(ctx, prompt)
    if err != nil {
        return fmt.Errorf("failed to send prompt to agent: %w", err)
    }

    return nil
}

LLMからのレスポンスを出力

WithMessageHook関数をNew関数の第二引数に渡し、msg(LLMからのレスポンス)を出力します。

func run(ctx context.Context) error {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
    }

    client, err := openai.New(ctx, os.Getenv("OPENAI_API_KEY"))
    if err != nil {
        return fmt.Errorf("failed to create OpenAI client: %w", err)
    }

    agent := gollem.New(client,
        // WithMessageHookをNew関数の第二引数に渡し、msg(LLMからのレスポンス)を出力する
        gollem.WithMessageHook(func(ctx context.Context, msg string) error {
            fmt.Printf("agent: %s\n", msg)
            return nil
        }))

    prompt := "こんにちは"
    err = agent.Execute(ctx, prompt)
    if err != nil {
        return fmt.Errorf("failed to send prompt to agent: %w", err)
    }

    return nil
}

ここまでの全体のコードはこちら

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/joho/godotenv"
    _ "github.com/joho/godotenv"
    "github.com/m-mizutani/gollem"
    "github.com/m-mizutani/gollem/llm/openai"
)

func main() {
    ctx := context.Background()
    if err := run(ctx); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(ctx context.Context) error {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
    }
    client, err := openai.New(ctx, os.Getenv("OPENAI_API_KEY"))
    if err != nil {
        return fmt.Errorf("failed to create OpenAI client: %w", err)
    }
 
    agent := gollem.New(client,
        gollem.WithMessageHook(func(ctx context.Context, msg string) error {
            fmt.Printf("agent: %s\n", msg)
            return nil
        }))
     
   prompt := "こんにちは"
    err = agent.Execute(ctx, prompt)
    if err != nil {
        return fmt.Errorf("failed to send prompt to agent: %w", err)
    }

    return nil
}

実行

gollemディレクトリにて下記を実行すると、

go run .

このような返事が返ってきました!

agent : こんにちは!どんなご用件でしょうか?お手伝いできることがあれば教えてください。
agent : ありがとうございます!ご配慮いただき嬉しいです。私はAIですので、皆さんからの質問や相談、文章作成、翻訳、学習サポート、情報提供など、さまざまなご要望に対応できます。

もし何かご相談や確認したいことがあれば、遠慮なくご質問ください。たとえば:

- 調べものや最新情報の検索
- 日本語や英語の添削や翻訳
- レポートやメール、スピーチの作成支援
- プログラミングやITに関する疑問
- 資格試験や学習のアドバイス
- 日常生活の悩み相談 など

どうぞお気軽にご利用ください!

ベタ書きプロンプトからユーザー入力でLLMに渡す

さきほどまでは prompt := "こんにちは" とベタ書きプロンプトでしたが、こちらをユーザー入力を渡すようにします。

func run(ctx context.Context) error {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
    }

    client, err := openai.New(ctx, os.Getenv("OPENAI_API_KEY"))
    if err != nil {
        return fmt.Errorf("failed to create OpenAI client: %w", err)
    }
    agent := gollem.New(client,
        gollem.WithMessageHook(func(ctx context.Context, msg string) error {
            fmt.Printf("agent:  %s\n", msg)
            return nil
        }))

  // ユーザーからの入力文字を渡す
    fmt.Print("> ")
    scanner := bufio.NewScanner(os.Stdin)
    if !scanner.Scan() {
        if err := scanner.Err(); err != nil {
            return fmt.Errorf("failed to read input: %w", err)
        }
    }
 
    err = agent.Execute(ctx, scanner.Text())
    if err != nil {
        return fmt.Errorf("failed to send prompt to agent: %w", err)
    }

    return nil
}

ここまでだと1ラリーで終了してしまいます。

会話のラリーを続ける

for文を入れて会話のラリーを続けます。

func run(ctx context.Context) error {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
    }

    client, err := openai.New(ctx, os.Getenv("OPENAI_API_KEY"))
    if err != nil {
        return fmt.Errorf("failed to create OpenAI client: %w", err)
    }
    agent := gollem.New(client,
        gollem.WithMessageHook(func(ctx context.Context, msg string) error {
            fmt.Printf("agent:  %s\n", msg)
            return nil
        }))

   // for 文でagentの実行(agent.Excute())周りを囲み、会話を続けられるようにする
    for {
        fmt.Print("> ")
        scanner := bufio.NewScanner(os.Stdin)
        if !scanner.Scan() {
            if err := scanner.Err(); err != nil {
                return fmt.Errorf("failed to read input: %w", err)
            }
            break
        }
        prompt := strings.TrimSpace(scanner.Text())
        err = agent.Execute(ctx, prompt)
        if err != nil {
            return fmt.Errorf("failed to send prompt to agent: %w", err)
        }
    }
    return nil
}

会話のラリーができるようになりました!

全体のコード

package main

import (
    "bufio"
    "context"
    "fmt"
    "log"
    "os"
    "strings"

    "github.com/joho/godotenv"
    _ "github.com/joho/godotenv"
    "github.com/m-mizutani/gollem"
    "github.com/m-mizutani/gollem/llm/openai"
)

func main() {
    ctx := context.Background()
    if err := run(ctx); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(ctx context.Context) error {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf(".env ファイルを読み込めませんでした: %v", err)
    }

    client, err := openai.New(ctx, os.Getenv("OPENAI_API_KEY"))
    if err != nil {
        return fmt.Errorf("failed to create OpenAI client: %w", err)
    }
    agent := gollem.New(client,
        gollem.WithMessageHook(func(ctx context.Context, msg string) error {
            fmt.Printf("agent:  %s\n", msg)
            return nil
        }))

    for {
        fmt.Print("> ")
        scanner := bufio.NewScanner(os.Stdin)
        if !scanner.Scan() {
            if err := scanner.Err(); err != nil {
                return fmt.Errorf("failed to read input: %w", err)
            }
            break
        }
        prompt := strings.TrimSpace(scanner.Text())

        err = agent.Execute(ctx, prompt)
        if err != nil {
            return fmt.Errorf("failed to send prompt to agent: %w", err)
        }
    }
    return nil
}

最後に

こんな感じでわいわい手を動かしながらGoやその時気になった話題を元に勉強会を開いてます。

また、A関連の話でいうと、弊社ではこちらの記事にあるように「AI委員会」があり、気になったAI情報を交換したり、いち早くツールを導入するために動いてくださったりするので、早い段階で会社でさまざまなAIツール使えるようになってとても助かっています。

そんな弊社が気になった方はぜひカジュアル面談にお越しください!o(^o^)o

open.talentio.com