TECH PLAY

サイオステクノロジー(Tech.Lab)

サイオステクノロジー(Tech.Lab) の技術ブログ

610

今号では、複数のファイルをまとめたり、バックアップを取る際によく利用される tar コマンドについて説明します! tar とは tar とは、複数のファイルを 1つのファイル (アーカイブファイル) にまとめたり、まとめたファイルを元に戻す (展開) ための仕組みです。 本来は「まとめる」だけでありファイルの圧縮は行われませんが、オプションを組み合わせることで gzip などの圧縮も同時に行うのが一般的です (tar.gz 形式など)。 基本の書式 アーカイブファイルを作成 tar [オプション] アーカイブファイル名 アーカイブ対象のファイル・ディレクトリ の書式で実行します。 ディレクトリを指定した場合、ひとつのアーカイブファイルにまとめられます。 例: $ tar -cf data.tar /tmp/data/ $ ※各オプションの意味については後述します。 -v オプションを付与することによって、アーカイブされたファイルが表示されるようになります。 例: $ tar -cvf data.tar /tmp/data/ /tmp/data/ /tmp/data/log.1 /tmp/data/log.2 /tmp/data/log.3 /tmp/data/log.4 /tmp/data/log.5 $ アーカイブファイルを展開 tar [オプション] アーカイブファイル名 の書式で実行します。 例: $ tar -xf data.tar $ アーカイブファイルの作成と同様に -v オプションを付与することによって、展開されたファイルが表示されるようになります。 例: $ tar -xvf data.tar /tmp/data/ /tmp/data/log.1 /tmp/data/log.2 /tmp/data/log.3 /tmp/data/log.4 /tmp/data/log.5 $ 展開した後、カレントディレクトリに tmp ディレクトリが表示されていれば成功です。 $ ls data.tar tmp $ ls tmp/data/ log.1 log.2 log.3 log.4 log.5 tar コマンドのオプション tar コマンド の基本的なオプションをご説明します。 ※すべてのオプションはご紹介せず、よく使用されると考えられるものを抜粋しています。 -c アーカイブファイルを作成します。 ※-f オプション (アーカイブファイルを指定) と同時に使用する必要があります。 $ tar -cf data.tar /tmp/data/ -x アーカイブファイルを展開します。 ※-f オプション (アーカイブファイルを指定) と同時に使用する必要があります。 $ tar -xf data.tar -v アーカイブ、もしくは展開されたファイルの一覧など、実行時の詳細な情報を表示します。 -z gzip 形式でアーカイブ、もしくは展開します。 ファイルが圧縮されるため、ファイルサイズを小さくしたい場合に有効です。 アーカイブ時の拡張子は、tar.gz にするのが一般的です。 $ tar -czvf data.tar.gz /tmp/data/ /tmp/data/ /tmp/data/log.1 /tmp/data/log.2 /tmp/data/log.3 /tmp/data/log.4 /tmp/data/log.5 $ $ tar -xzvf data.tar.gz /tmp/data/ /tmp/data/log.1 /tmp/data/log.2 /tmp/data/log.3 /tmp/data/log.4 /tmp/data/log.5 $ アーカイブ後のファイルサイズを確認すると、tar.gz 形式のファイルの方がサイズが小さくなっていることが分かります。 $ ls -l data.* -rw-r--r--. 1 user1 user1 201 1月 13 00:21 data.tar.gz -rw-r--r--. 1 user1 user1 10240 1月 13 00:21 data.tar 補足1:なぜ -f オプションが必須なのか? 上で記載の通り、 -f オプション はアーカイブファイルを指定するオプションですが、なぜこのオプションが必要なのか?について説明します。 tar コマンドのマニュアルを確認すると、-f オプションに関する箇所に下記の記述があります。 -f, --file=ARCHIVE Use archive file or device ARCHIVE. If this option is not given, tar will first examine the environment variable `TAPE'. If it is set, its value will be used as the archive name. Otherwise, tar will assume the compiled-in default. The default value can be inspected either using the --show-defaults option, or at the end of the tar --help output. 上記によると、-f オプションが指定されていない場合、最初に TAPE 変数を参照、次にコンパイル時に読み込まれたデフォルトのデバイスを参照しに行きます。 つまり、-f オプションで明示的にファイルを指定しなければ意図しないファイルやデバイスを参照してしまうことになります。 このような誤動作を避けるために -f オプションが必要となります。 補足2:アーカイブする場合の拡張子は何でも良いか? コマンドの仕組み上、tar -cf や tar -czf でファイルをアーカイブする際のファイル名に決まりはなく、どんな名前 (拡張子) にしても良いです。 しかしながら、 ユーザがアーカイブファイルとひと目で分かるように .tar や .tar.gz としておくのが一般的 であり、推奨されます。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 知っておくとちょっと便利!tar コマンドを使ったファイルのアーカイブ・展開 first appeared on SIOS Tech Lab .
こんにちは。サイオステクノロジーの和田です。前回は こちら でアウトボックスパターンという設計パターンを紹介しましたが、今回はその設計パターンを実現できる River というライブラリを使ってみたので紹介したいと思います。それではいきましょう。 River とは River は Go 言語で書かれた PostgreSQL 専用のジョブキューライブラリです。PostgreSQL をバックエンドとして使用することで、アウトボックスパターンを簡単に実装できます。主な特徴として、Redis や RabbitMQ などの外部ジョブキューを必要とせず River 単体でジョブキューを管理することができます。 ディレクトリ構成 今回作るサンプルアプリは river_examples 配下に、以下のような構成で配置します。 river_examples ├── db │ └── init.sql ├── docker-compose.yml ├── go.mod ├── go.sum ├── handlers │ └── user.go ├── jobs │ └── send_email.go └── main.go 実装 まず、必要なパッケージを 公式 の手順に従ってインストールします。 go get github.com/riverqueue/river go get github.com/riverqueue/river/riverdriver/riverpgxv5 go mod tidy River を動かすためには PostgreSQL が必須なので、Docker Compose で DB を作っていきます。 ここでは、River 本体が使うテーブルのマイグレーションと、今回サンプルで作成するアプリケーションで使うテーブルのマイグレーションを行います。 まず最初にアプリケーションで使うユーザーテーブルを作成するための SQL を作ります。 -- filepath: db/init.sql CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); 次に、postgres 本体とマイグレーション用のコンテナを Docker Compose で作ります。 River にはマイグレーション用の go コマンドがあるので、そちらを利用してマイグレーションを行っています。 # filepath: docker-compose.yml services: db: image: postgres:16-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: password POSTGRES_DB: mydb ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data - ./db/init.sql:/docker-entrypoint-initdb.d/001-init.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U user -d mydb"] interval: 2s timeout: 5s retries: 30 river-migrate: image: golang:1.24 depends_on: db: condition: service_healthy environment: DATABASE_URL: postgres://user:password@db:5432/mydb?sslmode=disable command: - bash - -c - | set -euo pipefail go install github.com/riverqueue/river/cmd/river@v0.28.0 /go/bin/river migrate-up --line main --database-url "$$DATABASE_URL" restart: "no" volumes: pgdata: 以下のコマンドで作成します。 cd river_examples docker compose up -d db docker compose run --rm river-migrate 1. ジョブの定義 続いて、メール送信ジョブを定義します // filepath: jobs/send_email.go package jobs import ( "context" "fmt" "log" "github.com/riverqueue/river" ) // SendEmailArgs はメール送信ジョブの引数 type SendEmailArgs struct { UserID int64 `json:"user_id"` Email string `json:"email"` Subject string `json:"subject"` Body string `json:"body"` } // Kind はジョブの種類を識別する名前を返す func (SendEmailArgs) Kind() string { return "send_email" } // SendEmailWorker はメール送信を実行するワーカー type SendEmailWorker struct { river.WorkerDefaults[SendEmailArgs] } // Work は実際のメール送信処理を実行 func (w *SendEmailWorker) Work(ctx context.Context, job *river.Job[SendEmailArgs]) error { log.Printf("Sending email to %s (UserID: %d)", job.Args.Email, job.Args.UserID) log.Printf("Subject: %s", job.Args.Subject) // ここで実際のメール送信処理を行う // 例:外部のメール配信サービスAPIを呼び出す err := sendEmailViaSMTP(job.Args.Email, job.Args.Subject, job.Args.Body) if err != nil { return fmt.Errorf("failed to send email: %w", err) } log.Printf("Email sent successfully to %s", job.Args.Email) return nil } func sendEmailViaSMTP(email, subject, body string) error { // 実際のメール送信ロジック // ここでは簡略化のため省略 return nil } 2. メイン処理 メイン処理では主に以下を行います。 River クライアントの初期化 River ワーカーの登録 ユーザー登録処理 // filepath: main.go package main import ( "context" "log" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver/riverpgxv5" "river_examples/handlers" "river_examples/jobs" ) func setupRiver(ctx context.Context, dbPool *pgxpool.Pool) (*river.Client[pgx.Tx], error) { workers := river.NewWorkers() // メール送信ワーカーを登録 river.AddWorker(workers, &jobs.SendEmailWorker{}) riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ Queues: map[string]river.QueueConfig{ river.QueueDefault: {MaxWorkers: 100}, }, Workers: workers, }) if err != nil { return nil, err } return riverClient, nil } func main() { ctx := context.Background() // PostgreSQL接続プールの作成 dbPool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/mydb?sslmode=disable") if err != nil { log.Fatal(err) } defer dbPool.Close() // Riverクライアントのセットアップ riverClient, err := setupRiver(ctx, dbPool) if err != nil { log.Fatal(err) } // Riverワーカーを起動 if err := riverClient.Start(ctx); err != nil { log.Fatal(err) } defer riverClient.Stop(ctx) log.Println("River worker started") // アプリケーションのメイン処理 userHandler := handlers.NewUserHandler(dbPool, riverClient) if err := userHandler.RegisterUser(ctx, "test@example.com", "password"); err != nil { log.Fatal(err) } // 動作確認用(ジョブが処理されるまで少し待つ) time.Sleep(2 * time.Second) } 3. ユーザー登録処理 ユーザー登録時に、DB への書き込みとジョブの投入を同じトランザクションで行います // filepath: handlers/user.go package handlers import ( "context" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/riverqueue/river" "river_examples/jobs" ) type UserHandler struct { dbPool *pgxpool.Pool riverClient *river.Client[pgx.Tx] } func NewUserHandler(dbPool *pgxpool.Pool, riverClient *river.Client[pgx.Tx]) *UserHandler { return &UserHandler{ dbPool: dbPool, riverClient: riverClient, } } func (h *UserHandler) RegisterUser(ctx context.Context, email, password string) error { // トランザクション開始 tx, err := h.dbPool.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) // 1. ユーザーテーブルに挿入 var userID int64 err = tx.QueryRow(ctx, ` INSERT INTO users (email, password_hash, created_at) VALUES ($1, $2, NOW()) RETURNING id `, email, hashPassword(password)).Scan(&userID) if err != nil { return fmt.Errorf("failed to insert user: %w", err) } // 2. メール送信ジョブをエンキュー(同じトランザクション内) _, err = h.riverClient.InsertTx(ctx, tx, jobs.SendEmailArgs{ UserID: userID, Email: email, Subject: "Welcome to Our Service!", Body: "Thank you for registering. Please verify your email...", }, nil) if err != nil { return fmt.Errorf("failed to enqueue email job: %w", err) } // 3. トランザクションをコミット if err := tx.Commit(ctx); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func hashPassword(password string) string { // パスワードハッシュ化(今回はそのまま登録) return password } サンプルでは UserHandler を main から呼び出す形にしていて、実行すると users への INSERT と River のジョブテーブルへの INSERT が同一トランザクションで行われ、ワーカーがジョブを処理します。 動作の流れ 実際の処理の流れは以下のようになります。 ユーザー登録リクエストが来る トランザクション開始 ユーザーテーブルに挿入(INSERT) River のジョブテーブルに挿入(InsertTx) トランザクションコミット River ワーカーがジョブテーブルを監視 メール送信ジョブを実行 これにより、ユーザー登録が成功した場合のみメール送信ジョブが確実に実行され、前回説明したアウトボックスパターンが実現できます。 まとめ 今回は、River を使ったサンプルアプリを作ってみました。River を使うことで、 PostgreSQL のトランザクション機能を活用し、アウトボックスパターンを簡単に実装できます。データベースへの書き込みとジョブの投入を単一のトランザクションで行えるため、データの整合性を保ちながら非同期処理を実現できます。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Goのジョブキューライブラリ River 使ってみた first appeared on SIOS Tech Lab .
はじめに 皆さん、こんにちは!PS-SLの織田です。2025年最後のブログ投稿になります。今回は、『 Java言語で学ぶデザインパターン入門 』の第3章を読んだ感想をまとめていきたいと思います。第2章ではAdapterパターンを扱いましたが、第3章ではTemplate Methodパターンについて書かれています。第2章のブログは コチラ からご覧ください。それでは詳しく見ていきましょう! Template Methodパターンとは Template Methodパターンとは「処理の枠組み(テンプレート)を親クラスで定義し、具体的な処理内容はサブクラスに実装させる」パターンです。文字で説明しても何のこっちゃという感じなので、具体的なコードとともに説明していきます。 処理の枠組みを定義する:AbstractDisplay.java public abstract class AbstractDisplay { // open, print, closeはサブクラスに実装をまかせる抽象メソッド public abstract void open(); public abstract void print(); public abstract void close(); // displayはAbstractDisplayで実装してるメソッド public final void display() { open(); for (int i = 0; i < 5; i++) { print(); } close(); } } このクラスはTemplate Methodパターンの核心部分です。全体のおおまかな流れをここで定義しています。一方で、細かな処理については記述されていません。ここで注目すべきはdisplay()メソッドです。このメソッドは次のような処理の流れを定義しています: 1. open()で開始処理を実行 2. print()を5回繰り返し実行 3. close()で終了処理を実行 重要なのは、display()メソッドがfinal宣言されている点です。これにより、サブクラスがこの処理の流れを変更できないようにしています。一方、open()、print()、close()は抽象メソッドとして宣言されており、具体的な処理内容はサブクラスに委ねています。 具体的な実装例1:CharDisplay.java public class CharDisplay extends AbstractDisplay { private char ch; // 表示すべき文字 // コンストラクタ public CharDisplay(char ch) { this.ch = ch; } @Override public void open() { // 開始文字列として"<<"を表示する System.out.print("<<"); } @Override public void print() { // フィールドに保存しておいた文字を1回表示する System.out.print(ch); } @Override public void close() { // 終了文字列として">>"を表示する System.out.println(">>"); } } AbstractDisplayクラスで記述しなかった細かい処理を記述していきます。CharDisplayクラスは、文字を括弧で囲んで表示する実装です。open()で<<を表示し、print()で指定された文字を表示し、close()で>>を表示します。 実行すると次のような出力になります: <<HHHHH>> 具体的な実装例2:StringDisplay.java public class StringDisplay extends AbstractDisplay { private String string; // 表示すべき文字列 private int width; // 文字列の表示幅 // コンストラクタ public StringDisplay(String string) { this.string = string; this.width = string.length(); } @Override public void open() { printLine(); } @Override public void print() { System.out.println("|" + string + "|"); } @Override public void close() { printLine(); } // openとcloseから呼び出されて"+----+"という文字列を表示するメソッド private void printLine() { System.out.print("+"); for (int i = 0; i < width; i++) { System.out.print("-"); } System.out.println("+"); } } おまけでもう1個処理のバリエーションを増やします。StringDisplayクラスは、文字列を罫線で囲んで表示する実装です。printLine()という独自のヘルパーメソッドを使用して、開始と終了時に罫線を表示します。 実行すると次のような出力になります: +-------------+ |Hello, world.| |Hello, world.| |Hello, world.| |Hello, world.| |Hello, world.| +-------------+ 利用例:Main.java public class Main { public static void main(String[] args) { // 'H'を持ったCharDisplayのインスタンスを1個作る AbstractDisplay d1 = new CharDisplay('H'); // "Hello, world."を持ったStringDisplayのインスタンスを1個作る AbstractDisplay d2 = new StringDisplay("Hello, world."); // d1,d2とも、すべて同じAbstractDisplayのサブクラスのインスタンスだから // 継承したdisplayメソッドを呼び出すことができる // 実際の動作は個々のクラスCharDisplayやStringDisplayで定まる d1.display(); d2.display(); } } このクラスはTemplate Methodパターンを利用するクライアント側の実装例です。重要なポイントは、AbstractDisplay型の変数を宣言し、CharDisplayやStringDisplayのインスタンスを代入している点です。 どちらのインスタンスも同じdisplay()メソッドを呼び出していますが、実際の動作(open()、print()、close()の処理内容)は各サブクラスの実装によって異なります。 Template Methodパターンで嬉しいこと 「わざわざ抽象クラスを作る必要があるの?」と思う人がいるかもしれません。CharDisplayやStringDisplayに必要な処理を書けば、たしかにAbstractDisplayは削ることができます。しかし、このパターンを使うことで受けられる恩恵があります。例えば、処理の流れを変更したくなったとします。 ケース1:表示回数を変更したい Template Methodパターンあり: public abstract class AbstractDisplay { // 略 // 表示回数を3回に変更したい public final void display() { open(); for (int i = 0; i < 3; i++) { // この1行だけ変更すれば済む print(); } close(); } } Template Methodパターンなし: public class CharDisplay { // 略 public void display() { System.out.print("<<"); for (int i = 0; i < 3; i++) { // すべてのクラスで変更が必要 System.out.print(ch); } System.out.println(">>"); } } public class StringDisplay { // 略 public void display() { printLine(); for (int i = 0; i < 3; i++) { // こっちのクラスでも変更が必要;; System.out.println("|" + string + "|"); } printLine(); } } Template Methodパターンあり:親クラスの1箇所を修正するだけで、すべてのサブクラスの動作が変わる Template Methodパターンなし:すべてのクラスで同じ修正を繰り返す必要がある ケース2:処理の前後にログを追加したい また、もっと複雑な処理をする際にはより重宝します。例えば、処理の開始と終了時にログを出力したくなったとします。 Template Methodパターンあり: public abstract class AbstractDisplay { // 略 public final void display() { System.out.println("[LOG] 処理開始"); // ログ追加 open(); for (int i = 0; i < 5; i++) { print(); } close(); System.out.println("[LOG] 処理終了"); // ログ追加 } } Template Methodパターンなし: public class CharDisplay { public void display() { System.out.println("[LOG] 処理開始"); // すべてのクラスに追加が必要 System.out.print("<<"); for (int i = 0; i < 5; i++) { System.out.print(ch); } System.out.println(">>"); System.out.println("[LOG] 処理終了"); // すべてのクラスに追加が必要 } } public class StringDisplay { public void display() { System.out.println("[LOG] 処理開始"); // こっちのクラスにも追加が必要 printLine(); for (int i = 0; i < 5; i++) { System.out.println("|" + string + "|"); } printLine(); System.out.println("[LOG] 処理終了"); // こっちのクラスにも追加が必要 } } Template Methodパターンありの場合: ケース1と同様に修正が必要最小限に留まっています。AbstractDisplayにログ出力の処理を追記するだけで済みました。 Template Methodパターンなしの場合:ケース1と同様に、処理の流れが各クラスに分散しているため、同じような修正をすべてのクラスで実施する必要があります。そのため、修正漏れやバグの混入リスクが高まってしまいます 。 ケース3:新しい表示方法を追加したい さらに、新しい表示方法を追加したい場合を考えてみましょう。例えば、XMLタグで囲んで表示するクラスを追加したいとします。 Template Methodパターンあり: public class XmlDisplay extends AbstractDisplay { private String content; private String tagName; public XmlDisplay(String content, String tagName) { this.content = content; this.tagName = tagName; } @Override public void open() { System.out.print("<" + tagName + ">"); } @Override public void print() { System.out.print(content); } @Override public void close() { System.out.println("</" + tagName + ">"); } } Template Methodパターンなし: public class XmlDisplay { private String content; private String tagName; public XmlDisplay(String content, String tagName) { this.content = content; this.tagName = tagName; } public void display() { // 処理の流れを毎回ゼロから実装する必要がある // もし他のクラスと処理の流れを変えたい場合、それも可能になってしまう(一貫性が失われる) System.out.print("<" + tagName + ">"); for (int i = 0; i < 5; i++) { System.out.print(content); } System.out.println("</" + tagName + ">"); } } Template Methodパターンありの場合: 新しいクラスを追加するだけで、既存のコードを全く変更することなく、新しい表示方法を実装できます。しかも、処理の流れ(open() → 5回print() → close())は自動的に継承されるため、一貫性が保たれます。 Template Methodパターンなしの場合:新しいクラスを追加するたびに処理の流れをゼロから実装する必要があり、一貫性を保つのが開発者の責任になってしまいます。 まとめ Template Methodパターンの本質は、「処理の流れの統一」と「変更の容易さ」の実現にあります。処理の枠組みを親クラスで一元管理することで、共通の処理フローを保証しつつ、具体的な実装の詳細は各サブクラスに委ねることができます。 特に重要なのは、以下の3つの利点です: Template Methodパターンの本質は、「処理の流れの統一」と「変更の容易さ」の実現にあります。処理の枠組みを親クラスで一元管理することで、共通の処理フローを保証しつつ、具体的な実装の詳細は各サブクラスに委ねることができます。 特に重要なのは、以下の3つの利点です: 1. 一箇所の変更で全体に影響 :処理の流れを変更したい場合、親クラスの1箇所を修正するだけで、すべてのサブクラスの動作が変わる 2. 一貫性の保証 :すべてのサブクラスが同じ処理の流れを持つことが保証される 3. 拡張の容易さ :新しいサブクラスを追加する際、処理の流れを意識する必要がない 実際の開発では、「処理の流れは共通だが、詳細な処理内容だけが異なる」という場面が発生するかと思います。そんな時、Template Methodパターンはコードの重複を避け、保守性を向上させ、将来の拡張性も確保するというメリットをもたらしてくれます。 それではこの辺りで2025年最後のブログを締めたいと思います。個人的に大変学びの多い一年でした。来年も引き続きデザインパターンをはじめ多くのことを学んでいきたいと思います。それでは、よいお年を! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post デザインパターンのすゝめ ~Template Method~ first appeared on SIOS Tech Lab .
初めに メリークリスマス!!ども!龍ちゃんです。 いや~クリスマスですね。年末ですし、そろそろいろいろな締め作業がありますね。 実は弊社では、裏でこっそりアドベントカレンダーもどきをやっていました。アドベントカレンダーもどきってのは、アドベントカレンダーって「テーマ」があるはずなんですけど、弊社のブログとエンジニアの特徴上、ネタの一貫性を持たせるのが大変だからって理由だったので「フリーテーマ」ですね。というわけで 12 月は毎日投稿されていたって感じです。この時期で投稿記事が爆増えして、ブログの企画をやっている側としてはにっこにこです。 ざっくりと紹介とまとめを書いていこうと思います。ぜひ興味ある記事を見つけてみてね! AI・機械学習関連 12 月は AI 活用の記事が盛りだくさんでした。Azure OpenAI からローカル LLM、エージェント連携まで幅広くカバーしています。 Azure OpenAI 入門:モデルのデプロイと python から API を実行 Azure OpenAI を初めて触る方向けの入門記事です。Azure ポータルでのリソース作成から、モデルデプロイ、Python での API 呼び出しまでを丁寧に解説しています。デプロイの種別(グローバル/リージョン、Standard/バッチ)や TPM、動的クォータといった概念もわかりやすく説明されており、これから Azure OpenAI を始めたい方の最初の一歩に最適です。 AzureOpenAI 入門:JSON 形式のデータを出力させる 前回の続編として、LLM に JSON 形式でデータを出力させる方法を解説しています。「JSON モード」と「構造化出力(Structured Outputs)」の 2 つの方法を比較し、構造化出力がなぜキーを保証できるのか、制約つきデコードという技術まで踏み込んで解説しています。アプリケーションに LLM を組み込む際に必須の知識です。 【v0.4】Ragas の最新アップデート情報と Azure OpenAI GPT-5 での評価方法【2025 年 12 月】 RAG 評価フレームワーク「Ragas」の v0.4.0 アップデート情報と、GPT-5 を使った評価方法を紹介しています。GPT-5 の reasoning_effort パラメータを「minimal」と「high」に変えた場合の評価結果比較も実施。faithfulness スコアに差が出ることが確認でき、用途に応じた使い分け指針が示されています。 【LangChain】SQL Agent で RAG に集計・統計機能を追加する方法 セマンティック検索は「完了率を教えて」「担当者ごとの件数は?」といった集計・統計の質問が苦手です。この記事では、LangChain の SQL Agent を組み合わせてこの限界を突破するハイブリッドアーキテクチャを提案しています。LLM がクエリの意図を判断して適切なツールを選択する実装例も紹介されています。 ノート PC で動くローカル LLM 完全ガイド【2025 年 12 月版】 2025 年 12 月時点でノート PC で動作する主要なローカル LLM を徹底比較した保存版記事です。Gemma 3、Phi-4、Qwen2.5、Llama 3.1、DeepSeek-R1 など各モデルのスペック・特徴・日本語性能を公式ソースに基づいて整理。Ollama などの実行環境や、量子化・メモリ使用量などの導入時注意点まで網羅しています。 【FastAPI】SSE で AI の回答をリアルタイムストリーミングする ChatGPT のように文字が順次表示されるストリーミング体験を FastAPI で実現する方法を解説しています。SSE と WebSocket の違いから始まり、sse-starlette を使った実装、LangGraph からのイベント変換、フロントエンド(React)での受信処理まで、実用的なコード例とともに紹介されています。 もう AI に振り回されない!OpenSpec で実現する予測可能な AI 開発 「Vibe コーディング」の問題点と、その解決策である「仕様駆動開発(SDD)」について詳しく解説した力作記事です。CLAUDE.md の限界から始まり、OpenSpec、Kiro、Spec Kit といった主要 SDD ツールの比較、実際の使い方までを網羅しています。AI に振り回されず、予測可能な開発を実現したい方必読です。 A2A(Agent2Agent)プロトコル入門| MCP との違いと活用メリット Google が発表した AI エージェント間通信プロトコル「A2A」の入門記事です。異なるフレームワークで作られた AI エージェント同士が連携できるようになる仕組みで、MCP との違い(MCP はツール接続、A2A はエージェント間連携)も明確に解説されています。マルチエージェントシステムに興味がある方におすすめです。 Claude Code 関連 Claude Code の機能を深掘りした記事と、AI 開発の新しいパラダイムについて考察した記事が揃っています。 Claude Code Hooks ガイド:AI コーディングを自動化・カスタマイズする方法 Claude Code の「Hooks」機能を詳しく解説した記事です。Hooks は、ツール実行の前後に設定したシェルコマンドを自動実行する機能で、LLM の判断に依存せず確実に実行されるのが特徴です。PreToolUse、PostToolUse など 9 種類のイベント、設定ファイルの優先度、マッチャーの正規表現パターン、そして実践的な設定例(自動フォーマット、センシティブファイル保護、Bash コマンドのロギング)まで網羅しています。 Claude Code Skills の使い方と汎用テンプレート公開 Claude Code に特定タスクの実行方法を教える「Skills」機能のガイドです。Skills は「新しいチームメンバーに渡すオンボーディング資料」のようなもので、プログレッシブ・ディスクロージャーにより必要な情報だけを段階的に読み込みます。description の書き方が成否の 9 割を決めるという重要なポイントも解説されています。Web 開発向けの汎用 Skills テンプレートも公開されています。 GitHub Spec Kit 入門| AI コーディングエージェントで仕様駆動開発を実践する GitHub がオープンソースで公開した「Spec Kit」を実際に Todo アプリで試した体験記です。Specify(仕様作成)→ Plan(技術計画)→ Tasks(タスク分解)→ Implement(実装)の 4 フェーズワークフローを採用し、各フェーズにゲートがあることで品質を担保します。Claude Code、GitHub Copilot、Cursor など 15 以上の AI エージェントに対応しており、「思ってたのと違う」を減らしたい方に最適です。 これからの「バイブコーディング」の話をしよう AI 開発時代における原則の再定義を試みた考察記事です。コードレビュー、DRY 原則、可読性、保守性といった従来の「正しい」原則は、すべて「人間がコードを読み、人間が修正する」という前提から生まれています。AI がコードを書き直す時代には、「入出力が正しければ内部実装は問わない」「一貫性は不要、重複も OK」「保守しない、作り直す」という新しい原則が成立するのでは、という刺激的な提言がされています。 Three.js の Vibe Coding でビジュアルイメージを検討する Gemini と Copilot を使って three.js で Web ビジュアルを作成した実践記事です。「アスタリスクの 3D 版」「ゴリッと拡大したい」といった雰囲気ベースのプロンプトで AI と対話しながら、数ステップでイメージに近いビジュアルを実装していく過程が紹介されています。完成した three.js コード(コピペで動作可能)も公開されており、バイブコーディングの具体的な成功事例として参考になります。 Kubernetes・インフラ関連 Kubernetes のバックアップ・運用に関する記事が充実しています。Velero シリーズは特に読み応えがあります。 初めての Kubernetes バージョンアップ:Kubernetes におけるバックアップの必要性とデータ管理の課題 Kubernetes のバックアップがなぜ必要なのかを解説した入門記事です。バージョンアップ時に失われる可能性のある要素として「クラスターリソースの設計図(Deployment、Service など)」と「アプリケーションデータ(DB 内のデータなど)」の 2 つを挙げ、etcd と PV/PVC の二軸でバックアップを行う重要性を説明しています。Blue/Green デプロイにおけるデータ移行の課題も詳しく解説されています。 Kubernetes バックアップツール「Velero」の概要 Kubernetes のリソースと永続ボリュームを包括的にバックアップできる「Velero」の概要記事です。PV のバックアップ方式(ボリュームスナップショット、ファイルシステムバックアップ、CSI スナップショットデータムーバー)、アーキテクチャ、バックアップ/リストアのワークフロー、そしてデータベースの整合性を確保する Hook 機能について解説されています。他のエンタープライズバックアップソリューション(Cohesity、Commvault、Rubrik)との比較もあります。 Velero のインストールと基本設定 前回の概要編に続き、実際に Velero を導入する手順を解説した実践記事です。学習用環境として Minikube を使用し、バックアップ先に S3 互換ストレージである MinIO を構築します。Velero CLI のインストール、VolumeSnapshotClass の設定、Helm を使った Velero 本体のインストールまで、コマンドと YAML ファイルを交えて丁寧に説明されています。 GitOps だけじゃない!新たな選択肢「HelmOps」とは? Kubernetes へのアプリケーション自動デプロイ手段として、GitOps に代わる「HelmOps」を解説した記事です。GitOps が Git リポジトリを信頼できる情報源とするのに対し、HelmOps は Helm レジストリを信頼できる情報源として扱います。CD プロセスで Git を介さずにアプリケーション更新ができるためシンプルな構成になる反面、環境ごとのカスタマイズが難しいというトレードオフがあります。 【Kong 初心者向け】Canary Release Plugin の使い方 API Gateway の Kong を使って、一部ユーザーにのみ新サービスを公開するカナリアリリースを実現する手順を解説しています。Kong Manager での workspace 作成、service/route 設定、Key Auth・ACL plugin の設定、そして Canary Release Plugin の設定まで、スクリーンショット付きで丁寧に説明されています。Enterprise 機能ですが、Kong Academy の virtual lab で試すことができます。 フロントエンド・開発ツール 初心者向けの入門記事から、実践的な CI/CD 構築まで幅広いトピックが揃っています。 Next.js + Storybook + Playwright を Chromatic でビジュアルテスト自動化する Next.js 14 プロジェクトに Storybook と Playwright E2E テストを導入し、Chromatic でビジュアルテストを自動化する方法を詳しく解説した実践記事です。Storybook のセットアップ、各コンポーネントの Stories 作成例(Button、MessageList、ImportProgress)、Playwright E2E テストの Chromatic 対応、GitHub Actions での自動化設定まで網羅しています。ハマりポイントと解決策も記載されており、導入時に役立ちます。 チンパンジーでもわかる Git/Github【初心者向け】 「サルでもわかる git」がわからなかった筆者が、当時の自分を救うべく書いた超初心者向け Git 入門記事です。グループでレポートを書く例え話から始まり、リポジトリ、クローン、ブランチ、コミット、プッシュ、プルリクエスト、マージといった用語を、大学のグループ課題に例えて説明しています。Git と GitHub の違いも明確に解説されており、これから Git を学ぶ方の最初の一歩に最適です。 UI 基礎:選択式入力の使い分け 入力フォームにおける「ラジオボタン」「チェックボックス」「セレクト」などの選択式 UI コンポーネントの使い分けを整理した記事です。基本ルール(1 件選択で 2〜5 件ならラジオボタン、5 件以上ならセレクト、複数選択ならチェックボックス)から始まり、トグルスイッチ、コンボボックス、デイトピッカーなどの発展的なコンポーネントまで解説。性別、生年月日、都道府県など具体的な事例での使い分けも紹介されています。 Hardhat 3 と Dev Container で Solidity の開発 VS Code の Dev Container を使って Solidity 開発環境を構築し、Hardhat 3 で SmartContract を動かすまでを解説した記事です。devcontainer.json の設定、Nomic Foundation の Solidity 拡張のインストール、Hardhat 3 の初期化、ローカルネットワークでの Contract 実行、Sepolia テストネットへのデプロイまでを順を追って説明しています。Hardhat 3 の keystore 機能を使った秘密鍵の安全な管理方法も紹介されています。 デザインパターン・設計 設計パターンを学ぶシリーズ記事と、マイクロサービスで役立つパターンの解説です。 デザインパターンのすゝめ ~ Iterator パターン編~ 『Java 言語で学ぶデザインパターン入門』第 1 章を読んだ感想をまとめた記事です。Iterator パターンは集合要素を順に指していき処理を繰り返し実行するパターンで、本棚(BookShelf)と本(Book)を例に解説されています。配列から ArrayList への変更時も、Iterator パターンを使っていれば BookShelf クラスだけの修正で済み、Main クラスは変更不要という具体例が示されています。 デザインパターンのすゝめ ~ Adapter パターン編~ Iterator パターン編の続編として、Adapter パターンを解説しています。「すでに提供されているもの」と「本当に必要なもの」のズレを埋めるパターンで、既存クラス(Banner)を新しいインターフェース(Print)で使えるように変換する PrintBanner クラスを例に説明されています。HTML 出力への切り替えなど、将来の拡張でもクライアントコードの修正を最小限に抑えられる利点が紹介されています。 アウトボックスパターンとはなにか マイクロサービスアーキテクチャにおいて、データベースの更新とイベント発行を矛盾なく行うためのアウトボックスパターンを解説しています。ユーザー登録サービスを例に、「DB 書き込み成功・メッセージ送信失敗」「DB 書き込み失敗・メッセージ送信成功」といった不整合が発生するケースを示し、Outbox テーブルを使った解決策を紹介しています。メッセージリレーによる送信と冪等性の考慮についても解説されています。 その他 プロジェクトマネジメント、資格取得、キャリア、コミュニティ運営など、技術以外のトピックも充実しています。 ラスト 1 か月を炎上させないためのプロジェクトマネジメント 初めて PM を担当した筆者が、プロジェクト終盤で多くのバグが発生した経験から学んだ教訓をまとめた記事です。「ウォーキングスケルトン」(フロント・バック・DB が繋がった最小限の動く骨組み)を早期に作ること、「フェイルファスト」の考え方で小さく早く失敗すること、CI/CD と「触れる環境」を早期に整えることの重要性が解説されています。 LT は怖くない!(怖がらない!)LT 実践の心得 5 選 これから LT(ライトニングトーク)で登壇したい方向けの心得を紹介した記事です。「登壇すると決める」「何を伝えるかを決める」「しゃべりすぎない」「スライドに書いていないことはあまりしゃべらない」「楽しむ」の 5 つを中心に、自己紹介は短く、デモは避ける、AI でスライド作成を楽にするなど具体的なコツも紹介されています。 プロジェクト管理入門 – WBS・スケジュール作成 WBS(Work Breakdown Structure)の作成方法からスケジュール作成のポイントまでを解説した記事です。成果物の洗い出し、タスクの分解(1〜5 人日程度の粒度)、依存関係の定義(FS/SS/FF/SF)、バッファ設定、営業日を考慮したスケジュール作成、ガントチャート可視化まで網羅しています。Jira と Notion の比較も掲載されています。 Exchange Online のメール自動転送を制限する Exchange Online でメールの自動転送を制限し、情報漏洩リスクを軽減する方法を解説した記事です。Microsoft 365 Defender ポータルでのアウトバウンドスパムフィルターポリシー設定、特定ドメインのユーザーに対する自動転送完全禁止、または社内ドメインのみへの自動転送許可(リモートドメイン設定)の 2 つのパターンを、スクリーンショット付きで説明しています。 自宅にデジタルサイネージを導入しました Raspberry Pi と MagicMirror2 を使って自宅にデジタルサイネージを導入した体験記です。時計、祝日カレンダー、天気予報、ニュースヘッドライン、YouTube ライブ動画を 1 画面で表示し、さらに洗濯物の取り込みタイミングを判断するための「直近の雨予報モジュール」を Vibe Coding で自作しています。実用的かつ楽しい作品です。 AZ-104 合格体験記:効果的な学習法と予想問題集の活用戦略 Azure Administrator Associate(AZ-104)に合格した筆者が、効果的だった学習方法を紹介した記事です。Udemy の予想問題集を中心に学習すること、試験中に MS Learn を参照できる特性を活かして「調べ方」を鍛えること、間違った問題を徹底的に深掘りすること、比較表や図を自作することなど、具体的なアドバイスが満載です。 地方で IT 技術コミュニティを 1 年間運営して得たもの JAZUG(Japan Azure User Group)静岡支部を立ち上げ、1 年間運営してきた経験をまとめた記事です。「ないなら自分でやってしまえばいい」という発想から始まり、集客の苦労、「現地開催のみ」へのこだわり、運営で得られた技術的視野の広がりやコネクション、「場づくり」スキルの重要性について語られています。 基礎知識ゼロから新卒 1 年|仕事としての技術とマインド 機械工学専攻から未経験で IT エンジニアになった新卒 1 年目の振り返り記事です。学生と社会人のマインドセットの違い、「分からない」を認める勇気、暗記ではなく「調べ方」を知ること、「雑談・相談」の重要性、とにかく手を動かして作りながら学ぶことなど、未経験からエンジニアを目指す方への励みになる内容です。 OSS 鳥観図ワーキングループの活動紹介 OSS 推進フォーラムの「鳥観図ワーキンググループ」の活動を紹介した記事です。OSS 鳥観図は、複雑多岐にわたる OSS を視覚的に俯瞰できるようにまとめたもので、2014 年から毎年更新されています。新規追加・削除・カテゴリ変更の議論プロセスや、OSC・Open Source Summit での宣伝活動、参加方法も紹介されています。 まとめ いかがでしたか?12 月は本当にたくさんの記事が公開されて、企画している側としてはとても嬉しい限りです。 AI・機械学習から Kubernetes、デザインパターン、プロジェクトマネジメントまで、幅広いトピックが揃いました。ぜひ気になる記事があったら読んでみてくださいね! 来年も SIOS Tech Lab をよろしくお願いします! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post SIOS Tech Lab アドベントカレンダー2025まとめ|全34記事を一挙紹介 first appeared on SIOS Tech Lab .
サイオステクノロジーのひろです。今回はAzure OpenAIでスキーマに沿ったJSON形式データを出力することをゴールに、以下の目次で解説していきます。 前回の記事はこちら Azure OpenAI入門:モデルのデプロイとpythonからAPIを実行 LLMの構造化データ出力 LLMは流暢で自然な文章を出力することが可能です。学習した膨大なテキスト情報を使い、ユーザの入力(プロンプト)に沿った文章を出力してくれます。 例えばAzure OpenAIモデルに「こんにちは」というプロンプトを渡すと以下のように返してくれました。 「こんにちは!どのようにお手伝いできますか?」 自然な文章だと思います。 しかしLLMはこのような自然な文章を生成するだけでなく、JSON形式でテキストを出力することができます。 JSON形式で出力させることで、アプリケーションのシステムの一部として組み込むことが容易になります。 AzureOpenAIで可能な出力方法 試しにプロンプトのみでJSON形式で出力してもらいました。 messageは以下の通りです。 python messages=[ { "role": "system", "content": "あなたは優秀なアシスタントAIです。JSON形式で出力してください。", }, {"role": "user", "content": "こんにちは"}, ], 実行結果(返答)はこちらです。 { "message": "こんにちは!何かお手伝いできることがありますか?" } JSON形式でレスポンスをくれました。 この例のようにJSON形式で返してくれる場合もありますが、プロンプトエンジニアリングだけでは確実性に欠けるもので、崩れたJSON形式であったり、JSONの前に「JSON形式で回答します。」という文章が出力されることがあります。 そのためアプリケーションに組み込むにはリスクがあります。 そこで、この次の章ではAzure OpenAIモデルでJSON形式でレスポンスを取得できる2種類の方法をご紹介します。 1つ目:JSONモード Azure OpenAIにはJSONモードが存在します。 JSONモードを使用することで、JSON形式で返答されることが保証されます。 JSONモードを使用するには、AzureOpenAIへのリクエストの中に以下のパラメータを追加し、プロンプトでJSON形式で出力してください等という文章を追加します。 python response_format: { type: "json_object" } これによりJSONモードを使用できます。 注意点 JSONモードを使用するときは、systemプロンプトにJSON形式で出力してください等の文言を入れてください。JSONという文字が含まれない場合、エラーが出ます。 以下はサンプルコードと実行結果です。今回はpythonで用意しています。 また、スキーマを用意し、プロンプトに含めることでスキーマに沿った内容を出力させようとしています。 サンプルコード python import os import json from openai import AzureOpenAI from dotenv import load_dotenv from test_schemas import TestSchemas # .envファイルから環境変数を読み込み load_dotenv() api_key = os.getenv("AZURE_OPENAI_API_KEY") endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") api_version = os.getenv("AZURE_OPENAI_API_VERSION") deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") # Azure OpenAIクライアントの作成 client = AzureOpenAI( api_version=api_version, azure_endpoint=endpoint, api_key=api_key, ) # スキーマのJSON表現を取得 schema_string = json.dumps( TestSchemas.model_json_schema(), indent=2, ensure_ascii=False ) prompt = f""" 以下の文章を分析し、指定されたスキーマに従ってJSON形式で出力してください。 文章: '''私は今週末のテストに備えて早く寝ることにしました。''' スキーマ: '''{schema_string}''' """ response = client.chat.completions.create( messages=[ { "role": "system", "content": "あなたは優秀なアシスタントAIです。日本語でJSON形式に合わせて回答してください。", }, {"role": "user", "content": prompt}, ], response_format={"type": "json_object"}, max_tokens=1000, temperature=0.7, model=deployment, ) print(response.choices[0].message.content) TestSchemasクラス python from pydantic import BaseModel, Field class TestSchemas(BaseModel): title: str = Field(description="文章のタイトル") content: str = Field(description="文章の内容") importance: int = Field( ge=1, le=5, description="文章の重要度を1から5の範囲で評価(5が最も重要)" ) 実行結果 { "title": "テスト準備", "content": "私は今週末のテストに備えて早く寝ることにしました。", "importance": 4 } スキーマに沿ったJSON形式のレスポンスが返ってきていることが確認できました。 ただ、これは成功例で、スキーマに沿ったキーでレスポンスをくれない場合もあります。 このJSONモードはJSON形式であることは保証してくれますが、どんなキーが含まれるかまでは保証してくれません。 プロンプトのみでJSONを指定した場合に比べると形式が保証されているという良い点がありますが、キーが変わる場合があるとなるとアプリケーションに組み込むにはリスクがあります。 この問題を解決するのがもう一つの方法である構造化出力(Structured Outputs)です。 2つ目:構造化出力(Structured Outputs) 構造化出力はJSONモードより後に追加された機能で、JSONモードを強化したバージョンといえると思います。 JSONモードと何が違うかというと、JSONモードではどんなキーが含まれるかという点は保証してくれませんでした。しかし、この構造化出力は指定したスキーマに準拠した回答をさせることが可能です。 JSONモードではプロンプトにスキーマを入力していましたが、response_formatにスキーマを指定することができます。 以下はサンプルコードです。 スキーマはJSONモードのサンプルコードで使用したTestSchemasを使用しています。 python import os import json from openai import AzureOpenAI from dotenv import load_dotenv from test_schemas import TestSchemas # .envファイルから環境変数を読み込み load_dotenv() api_key = os.getenv("AZURE_OPENAI_API_KEY") endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") api_version = os.getenv("AZURE_OPENAI_API_VERSION") deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") client = AzureOpenAI( api_version=api_version, azure_endpoint=endpoint, api_key=api_key, ) schema_string = json.dumps( TestSchemas.model_json_schema(), indent=2, ensure_ascii=False ) prompt = """ 以下の文章を分析し、指定されたスキーマに従ってJSON形式で出力してください。 文章: '''私は今週末のテストに備えて早く寝ることにしました。''' """ response = client.chat.completions.parse( messages=[ { "role": "system", "content": "あなたは優秀なアシスタントAIです。日本語でJSON形式に合わせて回答してください。", }, {"role": "user", "content": prompt}, ], response_format=TestSchemas,#スキーマを指定 max_tokens=1000, temperature=0.7, model=deployment, ) print(response.choices[0].message.content) 実行結果 {"title":"テストに備えるための早寝","content":"私は今週末のテストに備えて早く寝ることにしました。","importance":3} スキーマに沿ったJSON形式で出力されていることが確認できました。 制約つきデコード なぜ構造化出力でキーを保証できるんだろうという点に疑問を持ったので調べてみました。 どうやら制約付きデコードと呼ばれる技術が用いられているようです。 ここでLLMが文章を生成する方法についておさらいしましょう。 LLMは学習したパターンに基づいて、プロンプトに沿った内容の文章を生成してくれます。 まず、文脈に沿った次に来る確率が高い言葉をリストアップし、その中から確率で次の言葉を決定し、これを繰り返すことで文章を生成しています。 以下の図の例で見ますと、「今日は」に続いて、「いい」、「天気」、「寒い」という言葉がリストアップされており、それぞれに確率が存在します。次の単語が「いい」の確率は60%、「天気」の確率は30%、「寒い」の確率は10%という具合です。例では「いい」が選ばれていますね。この選択を繰り返すことで文章を生成しています。 制約つきデコードとは何かというと、リストアップされた言葉のうち、指定された種類の言葉以外が選ばれる確率を制限するというものです。 以下の例では、response_formatとして、titleとcontentというfieldのみ存在するスキーマを渡したとします。 制約付きデコードがない場合は。「{」に続いて、「title」が60%、「key」が30%、「name」が10%という具合で、title以外のキーが選ばれる可能性があります。 制約付きデコードがあれば、「title」以外の言葉の確率が0になるようマスキングされ、titleが選ばれるようになります。 このように制約付きデコーディングが使用されることで、構造化出力で指定されたスキーマのキーが保証されるようになります。 まとめ 今回はAzureOpenAIを使用してJSON形式のデータを出力させる方法についてまとめました。 方法は2種類あり、JSONモードではJSON形式の出力は保証されますが、どんなキーが含まれるかについては保証されません。 対して構造化出力(Structured Outputs)を使用することでキーについても保証されるようになります。 JSON形式で出力させることでアプリケーションに生成AIを組み込みやすくなります。是非活用してみてはいかがでしょうか。 次回はAzure OpenAIのFunctionCallingについて解説します。 参考文献 https://learn.microsoft.com/ja-jp/azure/ai-foundry/openai/how-to/json-mode?view=foundry-classic&tabs=python https://learn.microsoft.com/ja-jp/azure/ai-foundry/openai/how-to/structured-outputs?view=foundry-classic&tabs=python-secure%2Cdotnet-entra-id&pivots=programming-language-python https://openai.com/ja-JP/index/introducing-structured-outputs-in-the-api ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post AzureOpenAI入門:JSON形式のデータを出力させる first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジーの遠藤です。 前回のブログ「 A2A(Agent2Agent)プロトコル入門|MCPとの違いと活用メリット 」では、A2Aプロトコルの概要を解説しました。 今回は、 実際にA2Aプロトコルを使ってマルチエージェントアプリを作ってみた ので、その実装を紹介します。 5秒でわかる:この記事の内容 項目 内容 やったこと A2Aプロトコルで「ライター×校閲者」マルチエージェントアプリを構築 得られるもの A2Aエージェントの実装パターン、Python SDKの使い方 対象読者 A2Aを実際に試したい人、マルチエージェント開発に興味がある人 関連記事:より深く理解するために A2A自体について理解したい方は、前回のブログを先に読むことをおすすめします。 前回ブログ: A2A(Agent2Agent)プロトコル入門|MCPとの違いと活用メリット TL;DR 「ライターエージェント」と「校閲者エージェント」をA2Aで連携させるデモを実装 Python SDK( a2a-sdk )を使えば、数十行でA2Aサーバーが構築可能 Azure OpenAI(または他のLLM)と組み合わせて、実用的なワークフローを構築 こんな人に読んでほしい A2Aプロトコルを実際に動かしてみたい人 マルチエージェントアプリの実装パターンを知りたい人 Python + Azure OpenAIでAIアプリを作っている人 「エージェント間連携って実際どう実装するの?」と思っている人 作るもの:編集部ワークフロー 今回作るのは、 ブログ記事の執筆→校閲を自動化するワークフロー です。 オーケストレーター がライターエージェントにテーマを送信 ライターエージェント がAzure OpenAIを使って初稿を生成 オーケストレーター が初稿を受け取り、校閲者エージェントに送信 校閲者エージェント が誤字脱字・技術的正確性をチェックし、修正案を返す 実装の全体像 Step 1: 環境セットアップ 前提条件 このデモでは uv (Pythonパッケージマネージャー)を使用します。uvの導入がまだの方は、以下の記事を参考にセットアップしてください。 参考: 【Python】uv入門 – pipより高速なパッケージ管理ツール 依存関係のインストール # pyproject.toml [project] name = "a2a-demo" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "a2a-sdk[http-server]>=0.3.0", "openai>=1.0.0", "httpx>=0.28.0", "uvicorn>=0.34.0", "python-dotenv>=1.0.0", ] 環境変数の設定 AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ AZURE_OPENAI_API_KEY=your-api-key AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o AZURE_OPENAI_API_VERSION=2024-02-15-preview Step 2: ライターエージェントの実装 agent.py – LLM呼び出しロジック import os from typing import Literal from openai import AzureOpenAI from pydantic import BaseModel class WriterResponse(BaseModel): """ライターエージェントのレスポンス形式""" status: Literal["input_required", "completed", "error"] = "input_required" content: str class WriterAgent: """ブログ記事の初稿を書くエージェント""" SYSTEM_PROMPT = """あなたは技術ブログのライターです。 指定されたテーマについて、読みやすく分かりやすいブログ記事の初稿を書いてください。 ## 記事の構成 1. 導入(テーマの背景と記事の目的) 2. 本文(技術的な説明、具体例) 3. まとめ(要点の整理) ## 注意事項 - 技術的に正確な内容を心がける - 初心者にも分かりやすい表現を使う - 適切な見出しを付ける - 500〜800文字程度で簡潔にまとめる""" def __init__(self): self.client = AzureOpenAI( azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], api_key=os.environ["AZURE_OPENAI_API_KEY"], api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-15-preview"), ) self.deployment_name = os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"] async def invoke(self, theme: str) -> WriterResponse: """テーマに基づいてブログ記事の初稿を生成する""" if not theme or theme.strip() == "": return WriterResponse( status="input_required", content="記事のテーマを指定してください。", ) response = self.client.chat.completions.create( model=self.deployment_name, messages=[ {"role": "system", "content": self.SYSTEM_PROMPT}, {"role": "user", "content": f"テーマ: {theme}"}, ], temperature=0.7, max_tokens=2000, ) return WriterResponse( status="completed", content=response.choices[0].message.content, ) agent_executor.py – A2Aエグゼキューター ここがA2Aプロトコルの核心部分です。 AgentExecutor を継承して、 execute メソッドを実装します。 """Writer Agent Executor - A2Aプロトコル用のエグゼキューター""" from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import TaskUpdater from a2a.types import Part, TaskState, TextPart from a2a.utils import new_agent_text_message, new_task from writer_agent.agent import WriterAgent class WriterAgentExecutor(AgentExecutor): """ライターエージェントのA2Aエグゼキューター""" def __init__(self): self.agent = WriterAgent() async def execute( self, context: RequestContext, event_queue: EventQueue, ) -> None: query = context.get_user_input() task = context.current_task if not task: task = new_task(context.message) await event_queue.enqueue_event(task) updater = TaskUpdater(event_queue, task.id, task.context_id) # 処理中ステータスを送信 await updater.update_status( TaskState.working, new_agent_text_message("記事を執筆中です...", task.context_id, task.id), ) # ライターエージェントを実行 result = await self.agent.invoke(query) if result.status == "completed": # 成果物として記事を追加 await updater.add_artifact( [Part(root=TextPart(text=result.content))], name="draft_article", ) await updater.complete() else: await updater.update_status( TaskState.input_required, new_agent_text_message(result.content, task.context_id, task.id), final=True, ) ポイント : RequestContext からユーザー入力を取得 TaskUpdater でタスクの状態を更新 結果は Artifact として返す main .py – サーバー起動 """Writer Agent Server - ライターエージェントのA2Aサーバー""" import uvicorn from dotenv import load_dotenv from a2a.server.apps import A2AStarletteApplication from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.tasks import InMemoryTaskStore from a2a.types import AgentCapabilities, AgentCard, AgentSkill from writer_agent.agent import WriterAgent from writer_agent.agent_executor import WriterAgentExecutor load_dotenv() def main(): skill = AgentSkill( id="write_article", name="ブログ記事ライター", description="指定されたテーマでブログ記事の初稿を執筆します", tags=["blog", "writing", "article"], examples=[ "Pythonの非同期処理について記事を書いて", "Dockerの基本概念を解説する記事を書いて", ], ) agent_card = AgentCard( name="Writer Agent", description="技術ブログの初稿を執筆するエージェント", url="http://localhost:10001/", version="1.0.0", default_input_modes=WriterAgent.SUPPORTED_CONTENT_TYPES, default_output_modes=WriterAgent.SUPPORTED_CONTENT_TYPES, capabilities=AgentCapabilities(streaming=False), skills=[skill], ) request_handler = DefaultRequestHandler( agent_executor=WriterAgentExecutor(), task_store=InMemoryTaskStore(), ) server = A2AStarletteApplication( agent_card=agent_card, http_handler=request_handler, ) print("Starting Writer Agent on http://localhost:10001") uvicorn.run(server.build(), host="0.0.0.0", port=10001) if __name__ == "__main__": main() ポイント : AgentSkill でエージェントの能力を定義 AgentCard でメタデータを公開 A2AStarletteApplication でHTTPサーバーを構築 Step 3: 校閲者エージェントの実装 校閲者エージェントも同様の構造です。違いはシステムプロンプトと出力形式です。 agent.py(抜粋) class ReviewerAgent: """ブログ記事を校閲するエージェント""" SYSTEM_PROMPT = """あなたは技術ブログの校閲者です。 与えられた記事を以下の観点でチェックし、修正案を提示してください。 ## チェック項目 1. **誤字脱字**: タイポや文法ミスがないか 2. **技術的正確性**: 技術的な説明が正確か 3. **読みやすさ**: 文章が分かりやすいか 4. **トーン&マナー**: 技術ブログとして適切なトーンか ## 出力形式(JSON) { "issues": ["問題点1", "問題点2", ...], "suggestions": ["改善提案1", "改善提案2", ...], "corrected_article": "修正後の記事全文" }""" 校閲者はJSONで構造化された結果を返し、問題点・改善提案・修正後の記事を明確に分離しています。 Step 4: オーケストレーターの実装 オーケストレーターは、A2Aクライアントとして両エージェントを呼び出します。 """Orchestrator - ライターと校閲者エージェントを連携させるクライアント""" import asyncio from uuid import uuid4 import httpx from a2a.client import A2ACardResolver, A2AClient from a2a.types import MessageSendParams, SendMessageRequest async def get_agent_client(base_url: str) -> tuple[A2AClient, httpx.AsyncClient]: """エージェントのA2Aクライアントを取得する""" httpx_client = httpx.AsyncClient(timeout=httpx.Timeout(120.0)) resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url) agent_card = await resolver.get_agent_card() client = A2AClient(httpx_client=httpx_client, agent_card=agent_card) return client, httpx_client async def send_message(client: A2AClient, message: str) -> str: """エージェントにメッセージを送信して結果を取得する""" payload = { "message": { "role": "user", "parts": [{"kind": "text", "text": message}], "messageId": uuid4().hex, }, } request = SendMessageRequest( id=str(uuid4()), params=MessageSendParams(**payload) ) response = await client.send_message(request) return extract_result_text(response) async def run_editorial_workflow(theme: str): """編集ワークフローを実行する""" print(f"テーマ: {theme}") # Step 1: ライターエージェントに接続 writer_client, writer_http = await get_agent_client("http://localhost:10001") # Step 2: ライターに記事を依頼 draft_article = await send_message(writer_client, theme) print("--- 初稿 ---") print(draft_article) # Step 3: 校閲者エージェントに接続 reviewer_client, reviewer_http = await get_agent_client("http://localhost:10002") # Step 4: 校閲者に記事をレビュー依頼 review_result = await send_message(reviewer_client, draft_article) print("--- 校閲結果 ---") print(review_result) # クリーンアップ await writer_http.aclose() await reviewer_http.aclose() ポイント : A2ACardResolver でAgent Cardを取得 A2AClient でメッセージを送信 結果は Artifact から抽出 Step 5: 実行してみる エージェントを起動(ターミナル2つ) # ターミナル1: ライターエージェント cd demo PYTHONPATH=. uv run python -m writer_agent # ターミナル2: 校閲者エージェント cd demo PYTHONPATH=. uv run python -m reviewer_agent オーケストレーターを実行 uv run python orchestrator/main.py "grpcとはなにか" 実行結果 ============================================================ A2A Protocol Demo: ライター × 校閲者 ワークフロー ============================================================ テーマ: grpcとはなにか [Step 1] ライターエージェントに接続中... ✓ ライターエージェントに接続しました [Step 2] 記事の初稿を依頼中... ✓ 初稿を受け取りました --- 初稿(抜粋) --- # gRPCとはなにか?初心者でもわかる解説 ## 導入:gRPCの背景とこの記事の目的 近年、マイクロサービスやクラウドアプリケーションの普及に伴い、 サービス間の通信技術がますます重要になっています。 そんな中で注目されているのが「gRPC」という通信プロトコルです。 gRPCはGoogleが開発した高速で効率的なRPCフレームワークで、 多くの企業や開発者に支持されています。 ### 双方向ストリーミング gRPCは一方向または双方向のストリーミング通信もサポート。 これにより、リアルタイムのデータ送受信や大容量データの分割送信が可能です。 --- 初稿ここまで --- [Step 3] 校閲者エージェントに接続中... ✓ 校閲者エージェントに接続しました [Step 4] 記事の校閲を依頼中... ✓ 校閲結果を受け取りました --- 校閲結果 --- 【発見した問題点】 1. gRPCはGoogleが開発したオープンソースであることを明記したほうが良い 2. 「gRPCは一方向または双方向のストリーミング通信もサポート」の文が やや簡潔すぎて意味が取りづらい 3. 対応言語をもう少し具体的に挙げると親切 4. 全体的に文章は読みやすいが、技術用語の説明がもう少し丁寧だと初心者に親切 【改善提案】 1. gRPCがオープンソースとして広く利用されていることを明示する 2. ストリーミング通信の種類(クライアント/サーバー/双方向)を 具体的に説明し、用途例を簡単に示す 3. C#, Node.js, PHPなどの対応言語も補足する 4. 専門用語には簡単な注釈を加え、初心者が理解しやすいようにする 【修正後の記事(抜粋)】 ### ストリーミング通信 gRPCは以下の3種類のストリーミング通信をサポートしています。 - クライアントストリーミング:クライアントからサーバーへ連続したデータを送信 - サーバーストリーミング:サーバーからクライアントへ連続したデータを送信 - 双方向ストリーミング:クライアントとサーバーが同時にデータを送受信 これにより、リアルタイムのデータ送受信や大容量データの分割送信が可能になります。 --- 校閲結果ここまで --- ============================================================ ワークフロー完了! ============================================================ 校閲者エージェントが初稿の曖昧な表現を検出し、より具体的な説明に改善してくれました。特に「ストリーミング通信」のセクションでは、3種類のストリーミングを箇条書きで明確に説明する形に修正されています。 Agent Cardを確認してみる 各エージェントのAgent Cardは、以下のURLで確認できます: # ライターエージェント curl http://localhost:10001/.well-known/agent.json | jq # 校閲者エージェント curl http://localhost:10002/.well-known/agent.json | jq { "name": "Writer Agent", "description": "技術ブログの初稿を執筆するエージェント", "url": "http://localhost:10001/", "version": "1.0.0", "skills": [ { "id": "write_article", "name": "ブログ記事ライター", "description": "指定されたテーマでブログ記事の初稿を執筆します" } ], "capabilities": { "streaming": false } } これがA2Aの「発見性」です。クライアントはこのAgent Cardを取得することで、エージェントの能力を動的に把握できます。 まとめ この記事で作ったもの コンポーネント 役割 ライターエージェント テーマを受け取り、ブログ記事の初稿を生成 校閲者エージェント 記事を受け取り、問題点・改善案・修正版を返す オーケストレーター 両エージェントを連携させてワークフローを実行 A2Aの実装パターン エージェントロジック(agent.py) : LLM呼び出しなどのビジネスロジック エグゼキューター(agent_executor.py) : A2Aプロトコルとの橋渡し サーバー(main.py) : Agent CardとHTTPサーバーの定義 まとめ A2Aプロトコルは「エージェントのマイクロサービス化」を実現する有力なアプローチです。MCPでツールを接続し、A2Aでエージェントを連携させる。この組み合わせで、より複雑で柔軟なAIシステムが構築できます。 ぜひ皆さんも試してみてください! 参考リンク 公式ドキュメント A2A Protocol Official Documentation A2A Python SDK A2A Samples ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post A2Aプロトコルでマルチエージェントアプリを作ってみた|ライター×校閲者ワークフロー first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジーの遠藤です。 最近、AIエージェントの話題が増えてきましたね。Claude Code、GitHub Copilot、Cursor… 単体で動くAIツールはたくさんありますが、「複数のAIエージェントを連携させたい」と思ったことはありませんか? そんなニーズに応えるのが、Googleが2025年4月に発表した A2A(Agent2Agent)プロトコル です。今回は、A2Aプロトコルとは何か、MCPとの違い、どんなメリットがあるのかを解説します。 こんな人に読んでほしい AIエージェント開発に興味がある人 マイクロサービスアーキテクチャの経験がある人 MCPを使っていて、次のステップを探している人 複数のAIを連携させたワークフローを構築したい人 A2Aプロトコルとは? 概要 A2A(Agent2Agent)プロトコル は、Googleが2025年4月に発表したAIエージェント間通信のためのオープンプロトコルです。Atlassian、Salesforce、SAP、ServiceNowなど多くのテクノロジーパートナーが参加を表明しており、現在はLinux Foundationに寄贈されています。 簡単に言うと、 異なるフレームワークや言語で作られたAIエージェント同士が、標準化された方法で会話できる仕組み です。 なぜA2Aが必要なのか? 従来、AIエージェントを連携させようとすると、こんな問題がありました: 問題 具体例 フレームワークの壁 LangChainで作ったエージェントとCrewAIのエージェントが連携できない プロトコルの不統一 各社独自の通信方式で互換性がない 発見性の欠如 他のエージェントが何ができるか分からない A2Aはこれらの問題を解決し、 エージェントのマイクロサービス化 を実現します。 A2Aの5つの設計原則 A2Aプロトコルは、以下の5つの原則に基づいて設計されています: 1. エージェントの能力を活かす(Embrace agentic capabilities) エージェント同士がメモリやツールを共有しなくても、自然な形で協調できることを重視しています。各エージェントは独立性を保ちながら連携できます。 2. 既存技術の活用(Build on existing standards) JSON-RPC 2.0 over HTTP(S) SSE(Server-Sent Events) でストリーミング対応 OpenAPIと同様の認証スキーム 特別なインフラは不要で、既存のWebスタックにそのまま統合できます。 3. セキュリティ重視(Secure by default) エンタープライズグレードの認証・認可をサポート。API Key、OAuth2、mTLSなど、OpenAPIと同等のセキュリティスキームに対応しています。 4. 長時間タスクへの対応(Support for long-running tasks) 数秒で終わるタスクから、人間の確認を挟む数時間〜数日のタスクまで柔軟に対応。タスクの状態管理やプッシュ通知もサポートしています。 5. モダリティ非依存(Modality agnostic) テキストだけでなく、画像、音声、動画など様々なデータ形式を扱えます。 A2Aの主要コンポーネント Agent Card(エージェントカード) エージェントの「名刺」のようなものです。 .well-known/agent.json というパスで公開され、以下の情報を含みます: Json { "name": "Writer Agent", "description": "技術ブログの初稿を執筆するエージェント", "url": "http://localhost:10001/", "version": "1.0.0", "skills": [ { "id": "write_article", "name": "ブログ記事ライター", "description": "指定されたテーマでブログ記事の初稿を執筆します" } ], "capabilities": { "streaming": false } } クライアントはこのAgent Cardを取得することで、エージェントが何をできるかを動的に発見できます。 Task(タスク) A2Aでは、エージェント間のやり取りは「タスク」として管理されます。タスクには以下の状態があります: 状態 説明 submitted 受付済み working 処理中 input_required ユーザー入力待ち completed 完了 failed 失敗 cancelled キャンセル 長時間タスクでも、途中経過の確認や再開が可能です。 Message(メッセージ) エージェント間でやり取りするメッセージは、複数の「Part」で構成されます: TextPart : テキストデータ FilePart : ファイル(画像、PDFなど) DataPart : 構造化されたJSONデータ MCPとA2Aの違い 両者は 補完関係 にあります。 比較表 どう使い分ける? MCPを使うべき場面 : エージェントから外部ツール(DB、ファイルシステム、API)にアクセスしたい Claude DesktopやCursor等のMCP対応アプリを使っている A2Aを使うべき場面 : 複数のAIエージェントを連携させたい 専門特化したエージェントを組み合わせたワークフローを構築したい エージェントを「サービス」として公開・発見したい A2Aの活用メリット 1. 専門特化エージェントの組み合わせ 一つの「なんでもできるエージェント」を作るより、専門特化した小さなエージェントを組み合わせる方が効率的です。 例:コンテンツ制作ワークフロー 2. ベンダーロックインの回避 A2Aはオープンプロトコルなので、特定のフレームワークやクラウドベンダーに縛られません。LangChainで作ったエージェント、CrewAIで作ったエージェント、自作エージェントを自由に組み合わせられます。 3. スケーラビリティ エージェントをマイクロサービスとして独立してデプロイできるため、負荷に応じたスケーリングが容易です。 4. 再利用性 一度作ったエージェントを、別のプロジェクトやチームで再利用できます。Agent Cardで能力を公開しているので、発見も簡単です。 A2Aを始めるには A2Aプロトコルは複数の言語で公式SDKが提供されています。プロジェクトに合った言語を選びましょう。 言語 リポジトリ 特徴 Python a2a-python 最も人気。サンプルコードも豊富 JavaScript/TypeScript a2a-js Node.js/ブラウザ両対応。型安全 Java a2a-java エンタープライズ向け .NET a2a-dotnet ASP.NET Core対応 まとめ A2Aプロトコルのポイント AIエージェント間通信の標準プロトコル :異なるフレームワーク・言語のエージェントが連携可能 MCPとは補完関係 :MCPがツール接続、A2Aがエージェント間連携 Agent Cardで動的発見 :エージェントの能力を公開し、自動的に発見・連携 既存Web技術との親和性 :JSON-RPC、HTTP(S)、SSEなど標準技術を活用 次のステップ A2Aの概念は理解できましたか?次回の記事では、 実際にA2Aプロトコルを使ってマルチエージェントアプリを作ってみます 。 具体的には、「ライターエージェント」と「校閲者エージェント」を連携させて、ブログ記事の執筆→校閲を自動化するデモを構築します。 実際に動くものを見ると理解がグッと深まるので、次回もお見逃しなく! 次回ブログ: A2Aプロトコルでマルチエージェントアプリを作ってみた|ライター×校閲者ワークフロー 参考リンク 公式ドキュメント A2A Protocol Official Documentation Google Developers Blog – A2A Announcement A2A Python SDK 関連記事 MCP(Model Context Protocol)公式ドキュメント ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post A2A(Agent2Agent)プロトコル入門|MCPとの違いと活用メリット first appeared on SIOS Tech Lab .
はじめに 本記事はSIOS Tech Labアドベントカレンダー23日目の投稿です。 サイオステクノロジーの曽根田です。 普段はデザイン、フロントエンドコーディングや、CMSのセキュリティ保守の一部対応などを行っています。 近年、CopilotやGeminiなどの生成AIの進化により、Photoshop,Illustratorなどでポチポチ作業する手間を省略して、 “フロントエンドのモックアップで直にビジュアルを作る” ということが手軽にできるようになったと感じています。 この記事ではthree.jsでの実例について書きます。 three.jsって? three.js はブラウザ上で3D表現を実装するのに便利なJavaScriptライブラリです。 ブラウザ上でのリッチな演出やゲーム制作までカバーしています。 公式ドキュメント も充実しています。 アイデアの種 美術展が好きでよく行くのですが、六本木にある 21_21 DESIGN SIGHT というギャラリーのショップで購入した、 アスタリスクを3D化して3Dプリンタで出力したオブジェクトようなもの 。直径は5cmくらいです。机に飾っていたこれがアイデアの種になりました。 このオブジェクトとthree.jsを使って作れそうなイメージとして以下が浮かびました。 htmlの一部にビジュアル要素として埋め込めるcanvas要素 鮮やかな大きな色のエリアが複雑なパターンで動く 動きはゆっくり 3Dにも見えるが2D的でもある。全体像ははっきりわからない 見ているだけで飽きない万華鏡のような動き 完成形のビジュアルイメージ 当初は Blender でこの3Dアスタリスクをモデリングを作成し、それを外部ファイルとしてthree.jsに取り込むやり方を検討したのですが、オブジェクトが幾何学的であれば数学的な計算だけで生成できるはずなので、一旦生成AIのプロンプトのみで作る方法を選びました。 いわゆる”Vibe Coding”のフェーズへ 今回の開発プロセスの特徴は以下のようなものです。 AIとの共同作業: GeminiとCopilotという複数の生成AIをプロンプトで制御し、コードを発展させました。 ナレッジベースの雰囲気コーディング: 過去の three.js ライブラリのコーディング経験や、 Blender などの3Dナレッジを活かした「雰囲気で進めるコーディング」です。 開発フェーズ 1: オブジェクトの基本配置と初期プロンプト 難しいことを考えず、ことばのイメージでやりたいことを伝えます。 以下、かなり指示がフランクですが、平日は生成AIと会話している時間が一番多く、”話が通じる仲間”という感覚なので、自然と砕けた口調になります笑 abstractなローポリゴンのオブジェクトが画面いっぱいに表示され、ゆっくりと回転するような、ウェブサイトのファーストビューイメージを作りたい。three.jsを活用。背景は#feeb0bにしてほしい。ライティングはリアルではなくフラットでうすくグラデーションが掛かっている感じ。 ↓ オブジェクトは添付写真のような、”アスタリスクの3D版”みたいにしてほしい(面ごとに色が違う。色の明度と彩度あげる)。正二十面体のような幾何学図形のそれぞれの面を外側にExtrudeしたような形。カメラワークはもっとゴリっと拡大したい。 バイブコーディングなので ”ゴリッと拡大したい” など、雰囲気主体の擬音も直さずそのまま渡してみますが、なんとなく察してくれます。 上記のプロンプト以外にも細かい指示はいくつか与えていますが省略しています。 途中、エラーで上手く描画されないケースもありますが、そのときは都度修正指示を出します。 ほんの3~4ステップのプロンプトでイメージに近いthree.jsの動きを実装してくれました。3D座標空間でオブジェクトが回転しています。 途中経過のキャプチャ。まだ色々と変。 Extrudeという用語について ちなみに上記プロンプトにある”Extrude”というのはBlenderやMayaなどの3D作成ツールのポリゴンモデリングで使われるコマンドで、”押し出し”を意味します。ビジュアル的には以下のようなイメージです。 Extrudeの図解。選択した面が押し出されています 開発フェーズ 2: 形・色・ライティングの調整 面単位ではなく、ポリゴン毎に色が違っていたり、面が重なってチカチカしてたりするのが作りたいゴールのイメージと異なるので、引き続きプロンプトで修正していきます。 カラーリングだが、ポリゴンごとに変えるのではなく、オブジェクトの同一平面の面は同じ色にする。あと、面が重なっていることでちらつきが発生しているようなので解消してほしい。色の彩度と明度をもっと上げる。 修正後が以下。ちらつきとカラーリングは解消されましたが、 形が気に食わない 。 1つのつながったオブジェクトになっていない気がする。アスタリスクの3D版だけど、一つの多面体の各面を個別にExtrudeして作られたアスタリスク、という感じにできない? あとShadingやライティングの感じもフラットすぎて面白くないので、以下のようなプロンプトで改善を加えてみます。 もっと“照明っぽい”陰影にしたい。flatShadingじゃなくていいのでは。vertexColorsも使えば?あと、スポットライト複数使ってオブジェクトを照らせば、それっぽくなるんじゃない? 面にグラデーションが付き、スポットライトによるライティングで陰影が生まれました。 形はガラッと変わり 、”3D版アスタリスク”ではなくトゲトゲのスターのような形になりましたが、数学的計算によって生成できるシームレスな1つのオブジェクトとなったのでOKと判断しました。 欲しかったのはシームレスなオブジェクトと、色のグラデーションの方です。 最後に、カメラを被写体にぐっと近づけて、起動するたびに毎回異なるカラーリングが施されるようなプロンプトを渡します。 完成版のキャプチャ 望んでいた動くキービジュアル要素が作成できました! 最終成果物:three.jsコード 最終版コードは以下です。1枚のhtmlなのでコピペして保存し、開くだけで動きます。three.jsは実際にブラウザで動かさないとよくわからないと思います。 リロードするたびにランダムにカラーが割り当てられます。3Dオブジェクトは数学的に生成されているので、外部読み込みの3Dファイルは不要です(ただし、オンライン上のCDNのthree.jsライブラリを読み込んで使っています) 一応モバイル対応も考慮しているので、モダンなデバイスならば処理落ちなどせずに動くと思います。 <!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Extruded Poly Star – three.js</title> <style> html, body { height: 100%; margin: 0; } body { background: #feeb0b; /* 指定の背景色 */ overflow: hidden; font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Noto Sans JP", sans-serif; } #hero { position: fixed; inset: 0; } </style> <!-- Import Map(正しい閉じタグ) --> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.1/build/three.module.js" } } </script> </head> <body> <canvas id="hero"></canvas> <script type="module"> import * as THREE from 'three'; // ===== ランダム化ユーティリティ(毎回違う画に) ===== // seed は URL ?seed=12345 で固定可能。未指定なら毎回ランダム。 // FIX: URLSearchParams.get('seed') が null のとき Number(null) は 0 になるので、 // 「seed未指定なのに毎回0固定」というバグを避ける。 const params = new URLSearchParams(location.search); const seedParam = params.get('seed'); const urlSeed = (seedParam !== null && seedParam !== '') ? Number(seedParam) : null; const autoSeed = (crypto && crypto.getRandomValues) ? crypto.getRandomValues(new Uint32Array(1))[0] : Math.floor(Math.random() * 1e9); const SEED = (urlSeed !== null && Number.isFinite(urlSeed)) ? Math.floor(urlSeed) : autoSeed; function mulberry32(a){ return function(){ let t = a += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; } } const rand = mulberry32(SEED); const randRange = (min, max) => min + (max - min) * rand(); console.log('%c[Hero Seed]', 'color:#555', SEED, urlSeed !== null ? '(from URL)' : '(random)'); // ===== 基本セットアップ ===== const canvas = document.getElementById('hero'); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'high-performance' }); // --- DPR 上限(PC/モバイル) + reduced-motion 対応 + 低FPSダウングレード --- const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Windows Phone/i.test(navigator.userAgent) || matchMedia('(pointer: coarse)').matches; const DPR_CAP_DESKTOP = 1.5; const DPR_CAP_MOBILE = 1.25; let dprCap = isMobile ? DPR_CAP_MOBILE : DPR_CAP_DESKTOP; const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; function applyDPR(cap = dprCap) { const target = Math.min(window.devicePixelRatio || 1, cap); renderer.setPixelRatio(target); renderer.setSize(window.innerWidth, window.innerHeight); return target; } let currentDPR = applyDPR(); const scene = new THREE.Scene(); // カメラ:右側を大きく覆うフレーミング const camera = new THREE.PerspectiveCamera(48, window.innerWidth / window.innerHeight, 0.01, 100); // 構図を毎回微妙に変える(少しだけジッター) const camX = -1.0 + randRange(-0.15, 0.15); const camY = 0.12 + randRange(-0.12, 0.12); const camZ = 2.0 + randRange(-0.2, 0.2); camera.position.set(camX, camY, camZ); const lookX = 0.4 + randRange(-0.08, 0.08); camera.lookAt(lookX, randRange(-0.05,0.05), randRange(-0.05,0.05)); renderer.physicallyCorrectLights = true; // ===== ライティング(複数スポットで“照明っぽい”陰影) ===== const amb = new THREE.AmbientLight(0xffffff, 0.08); scene.add(amb); const keyColor = new THREE.Color().setHSL(0.08 + randRange(-0.03,0.03), 0.9, 0.7); const rimColor = new THREE.Color().setHSL(0.58 + randRange(-0.04,0.04), 0.6, 0.7); const fillColor= new THREE.Color().setHSL(0.9 + randRange(-0.03,0.03), 0.8, 0.7); const spotKey = new THREE.SpotLight(keyColor, 40 + randRange(-8,8), 6.0, Math.PI * (0.26 + randRange(-0.03,0.03)), 0.45, 1.0); spotKey.position.set(2.4 + randRange(-0.3,0.2), 3.2 + randRange(-0.3,0.2), 1.8 + randRange(-0.2,0.2)); scene.add(spotKey); const spotRim = new THREE.SpotLight(rimColor, 20 + randRange(-6,6), 6.0, Math.PI * (0.34 + randRange(-0.04,0.04)), 0.5, 1.0); spotRim.position.set(-2.8 + randRange(-0.2,0.2), 1.6 + randRange(-0.2,0.2), -1.6 + randRange(-0.2,0.2)); scene.add(spotRim); const spotFill = new THREE.SpotLight(fillColor, 12 + randRange(-6,6), 6.0, Math.PI * (0.44 + randRange(-0.05,0.05)), 0.8, 1.0); spotFill.position.set(0.0 + randRange(-0.3,0.3), -2.4 + randRange(-0.3,0.3), 2.2 + randRange(-0.3,0.3)); scene.add(spotFill); // ====== 単一メッシュの「多面体→各面を外向きにエクストルードしたスター」生成 ====== const baseRadius = randRange(0.7, 0.95); const base = new THREE.IcosahedronGeometry(baseRadius, 0); // 20面(すべて三角形) function buildExtrudedStarFromTriPoly(geo, extrude = 1.2) { // 非インデックスでも動作 const pos = geo.attributes.position; const idxArray = (geo.index && geo.index.array) ? geo.index.array : (function(){ const a = new Uint32Array(pos.count); for (let i=0;i<pos.count;i++) a[i]=i; return a; })(); const positions = []; const colors = []; const color = new THREE.Color(); // グローバル色相オフセット&勾配方向もランダム const hueShift = randRange(0, 1); const gradDir = new THREE.Vector3(randRange(-1,1), randRange(0.3,1), randRange(-1,1)).normalize(); for (let f = 0; f < idxArray.length; f += 3) { const ai = idxArray[f], bi = idxArray[f+1], ci = idxArray[f+2]; const a = new THREE.Vector3(pos.getX(ai), pos.getY(ai), pos.getZ(ai)); const b = new THREE.Vector3(pos.getX(bi), pos.getY(bi), pos.getZ(bi)); const c = new THREE.Vector3(pos.getX(ci), pos.getY(ci), pos.getZ(ci)); const ab = new THREE.Vector3().subVectors(b, a); const ac = new THREE.Vector3().subVectors(c, a); const n = new THREE.Vector3().crossVectors(ab, ac).normalize(); const centroid = new THREE.Vector3().addVectors(a, b).add(c).multiplyScalar(1/3); const tip = new THREE.Vector3().copy(centroid).addScaledVector(n, extrude); pushTriWithFaceGradient(a, b, c, f); pushTriWithFaceGradient(a, b, tip, f + 1); pushTriWithFaceGradient(b, c, tip, f + 2); pushTriWithFaceGradient(c, a, tip, f + 3); } function pushTriWithFaceGradient(v1, v2, v3, seed) { const arr = [v1, v2, v3]; let minD = Infinity, maxD = -Infinity; const ds = arr.map(v => { const d = v.dot(gradDir); if (d<minD) minD = d; if (d>maxD) maxD = d; return d; }); const span = Math.max(1e-6, maxD - minD); const baseH = (hueShift + ((seed * 19.23) % 360) / 360) % 1.0; const S = 1.0; const L0 = 0.63; for (let i = 0; i < 3; i++) { const f = (ds[i] - minD) / span; // 0..1 const h = (baseH + f * 1.4) % 1.0; // 虹っぽく周回 const L = THREE.MathUtils.clamp(L0 + (Math.cos((f - 0.5) * Math.PI) * 0.22), 0, 1); color.setHSL(h, S, L); positions.push(arr[i].x, arr[i].y, arr[i].z); colors.push(color.r, color.g, color.b); } } const out = new THREE.BufferGeometry(); out.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); out.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); out.computeVertexNormals(); return out; } const starGeom = buildExtrudedStarFromTriPoly(base, 1.0); // ===== マテリアル(“照明っぽい”陰影:PBR マット) ===== const mat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.85, metalness: 0.05, side: THREE.DoubleSide }); const star = new THREE.Mesh(starGeom, mat); // 開始角度&サイズ・位置をランダム化(右寄せは維持) star.scale.setScalar(randRange(2.2, 2.8)); star.position.x = randRange(0.55, 0.9); star.rotation.x = randRange(0, Math.PI*2); star.rotation.y = randRange(0, Math.PI*2); star.rotation.z = randRange(0, Math.PI*2); scene.add(star); // ===== アニメーション(単純なリニア回転) + 低FPSダウングレード ===== const clock = new THREE.Clock(); let rafId = null; // FPS 測定用 let fpsFrames = 0; let fpsStart = performance.now(); let degradeSteps = 0; const MAX_DEGRADE_STEPS = 3; // --- FIX: 乱数は「毎フレーム」じゃなく「起動時に1回だけ」決める(チカチカ防止) --- const rotX0 = star.rotation.x; const rotY0 = star.rotation.y; const rotZ0 = star.rotation.z; const rotXSpeed = randRange(0.015, 0.028); // x軸 [rad/s] const rotZSpeed = -randRange(0.012, 0.020); // z軸 [rad/s](逆方向) function maybeDowngrade() { const now = performance.now(); const elapsed = now - fpsStart; if (elapsed >= 1000) { const fps = fpsFrames * 1000 / elapsed; fpsFrames = 0; fpsStart = now; // 45fpsを下回ったら DPR を 0.25 刻みで下げる(最小 1.0) if (fps < 45 && degradeSteps < MAX_DEGRADE_STEPS && currentDPR > 1.0) { dprCap = Math.max(1.0, +(dprCap - 0.25).toFixed(2)); currentDPR = applyDPR(dprCap); degradeSteps++; // 30fps を著しく下回るならライトも少し弱める if (fps < 30) { spotKey.intensity *= 0.9; spotFill.intensity *= 0.85; } } } } function renderFrame() { renderer.render(scene, camera); } function tick() { if (reduceMotion) { // 動きに弱いユーザーは静止 renderFrame(); return; } const t = clock.getElapsedTime(); star.rotation.x = rotX0 + t * rotXSpeed; star.rotation.y = rotY0; // Yは固定(開始角度は保持) star.rotation.z = rotZ0 + t * rotZSpeed; renderFrame(); fpsFrames++; maybeDowngrade(); rafId = requestAnimationFrame(tick); } // ===== リサイズ対応(DPR も再適用) ===== function onResize() { currentDPR = applyDPR(); camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); } window.addEventListener('resize', onResize); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { if (rafId) cancelAnimationFrame(rafId); } else { tick(); } }); onResize(); tick(); // ====== テスト(DPR上限・reduced-motion・低FPSダウングレードの土台を検証) ====== (function runTests(){ console.groupCollapsed('%c[Hero Tests] DPR cap + reduced-motion + perf', 'color:#111'); try { console.assert(renderer instanceof THREE.WebGLRenderer, 'renderer created'); console.assert(scene && camera && star, 'scene/camera/star exist'); console.assert(star.material instanceof THREE.MeshStandardMaterial, 'using MeshStandardMaterial'); console.assert(star.material.vertexColors === true, 'vertexColors enabled'); // DPR 上限が PC:1.5 / モバイル:1.25 を超えていない const capExpected = isMobile ? DPR_CAP_MOBILE : DPR_CAP_DESKTOP; console.assert(renderer.getPixelRatio() <= capExpected + 1e-6, 'pixelRatio is capped'); // reduced-motion フラグが boolean console.assert(typeof reduceMotion === 'boolean', 'reduceMotion flag present'); // 【追加】seed未指定時に urlSeed が null になる(= 0 固定バグが無い) const hasSeed = new URLSearchParams(location.search).has('seed'); if (!hasSeed) console.assert(seedParam === null, 'no-seed should not default to 0'); // エクストルード済み(頂点数増加) const vcount = star.geometry.getAttribute('position').count; console.assert(vcount > 60, 'geometry extruded (vertex count increased)'); // 回転が進行(reduced-motion でない環境のみチェック) const rx0 = star.rotation.x, rz0 = star.rotation.z; setTimeout(() => { const rx1 = star.rotation.x, rz1 = star.rotation.z; if (!reduceMotion) console.assert(rx1 !== rx0 || rz1 !== rz0, 'rotation progressing over time'); console.log('All tests passed ✅'); console.groupEnd(); }, 350); } catch (e) { console.error('Test failure:', e); console.groupEnd(); } })(); </script> </body> </html> 補足 今回、tech-lab.sios.jpのデザインリニューアルを担当させていただいたのですが、トップページの一部背景に上記のビジュアルを画像として使用しています。 元々、キーのキャラクターであるサイをポリゴン化する、というのもこのthree.jsの実験から着想したアイデアでした。 結局、こちらのサイがメインのキービジュアルという形になりました。 ポリゴン化したサイ デザイン案のバリエーション検討(Adobe DXのキャプチャ) さいごに three.jsはリッチコンテンツ向きである分、敷居が高く、使いどころが難しいように見えるかもしれませんが、ビジュアルイメージ検討のためのツールの1つとして気軽に使うと面白いと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Three.jsのVibe Codingでビジュアルイメージを検討する first appeared on SIOS Tech Lab .
はじめに 前回 は、Kubernetesのリソースと永続ボリュームを包括的に保護できるVeleroについて解説しました。Veleroは、Hook機能を利用してデータベースの整合性を確保できる点や、環境に応じてスナップショットとファイルシステムバックアップを使い分けられる柔軟性が強みです。 本記事では、実際にVeleroを用いたバックアップ環境の構築手順について、インストールと初期設定に絞って解説します。今回は学習用環境としてMinikubeを使用し、バックアップ先にはS3互換ストレージであるMinIOを利用する構成を構築します。 環境構築の前提 kubectlおよびhelmがインストール済みであること 環境情報 kubectl:v1.33.4 minikube:v1.36.0 helm:v4.0.4 MinIO:RELEASE.2025-09-07T16-13-09Z Velero CLI:v1.17.1 Velero:v1.17.1 Kubernetesクラスター Veleroの機能の一つにCSI (Container Storage Interface) を利用したボリュームスナップショットがあります。今回はこれを有効化するため、Minikube起動時にアドオンとドライバを指定します。 $ minikube start --addons=volumesnapshots,csi-hostpath-driver 次に、CSIを利用するためのVolumeSnapshotClassを定義します。 VolumeSnapshotClass.yaml apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata:   name: csi-hostpath-snapclass   annotations:     snapshot.storage.kubernetes.io/is-default-class: "true"   labels:     velero.io/csi-volumesnapshot-class: "true" # Veleroに使わせるためのラベル driver: hostpath.csi.k8s.io deletionPolicy: Delete ストレージクラスを確認し、作成したYAMLを適用します。 $ kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE csi-hostpath-sc hostpath.csi.k8s.io Delete Immediate false 3m33s standard (default) k8s.io/minikube-hostpath Delete Immediate false 3m36s $ kubectl apply -f VolumeSnapshotClass.yaml volumesnapshotclass.snapshot.storage.k8s.io/csi-hostpath-snapclass configured オブジェクトストレージ Veleroはバックアップデータをオブジェクトストレージに保存します。今回はAWS S3の代わりに、Kubernetes内に「MinIO」を構築して利用します。 minio.yaml apiVersion: apps/v1 kind: Deployment metadata:   name: minio   labels:     app: minio spec:   replicas: 1   selector:     matchLabels:       app: minio   template:     metadata:       labels:         app: minio     spec:       containers:       - name: minio         image: minio/minio:latest         args:         - server         - /data         - --console-address         - :9001         env:         - name: MINIO_ROOT_USER           value: "minioadmin"         - name: MINIO_ROOT_PASSWORD           value: "minioadmin"         ports:         - containerPort: 9000         - containerPort: 9001 --- apiVersion: v1 kind: Service metadata:   name: minio   labels:     app: minio spec:   ports:     - port: 9000       name: api     - port: 9001       name: console   selector:     app: minio MinIO用のNamespaceを作成し、デプロイします。 $ kubectl create namespace minio namespace/minio created $ kubectl apply -f minio.yaml -n minio deployment.apps/minio created service/minio created MinIOが立ち上がったら、バックアップ保存用のバケット(minio-bucket)を作成します。ここではminio-clientポッドを起動して操作します。 $ kubectl run minio-client --rm -it --image=minio/mc --restart=Never --command -- /bin/sh -c "  mc alias set myminio http://minio.minio.svc:9000 minioadmin minioadmin;  mc mb myminio/minio-bucket;  mc ls myminio; " If you don't see a command prompt, try pressing enter. mc: Configuration written to `/root/.mc/config.json`. Please update your access credentials. mc: Successfully created `/root/.mc/share`. mc: Initialized share uploads `/root/.mc/share/uploads.json` file. mc: Initialized share downloads `/root/.mc/share/downloads.json` file. Added `myminio` successfully. Bucket created successfully `myminio/minio-bucket`. [2025-12-22 02:01:02 UTC] 0B minio-bucket/ pod "minio-client" deleted Velero CLI(クライアント)が外部からMinIOにアクセスして情報を取得できるように、ポートフォワードを行い、設定を変更します。 $ minikube service minio --namespace=minio --url 表示されたアクセス情報(http://127.0.0.1:39783)はVelero本体のインストールで使用するため、新しいターミナルを開いて後続の手順を実施します。 例 $ minikube service minio --namespace=minio --url 😿 service minio/minio has no node port ❗ Services [minio/minio] have type "ClusterIP" not meant to be exposed, however for local development minikube allows you to access this ! http://127.0.0.1:39783 http://127.0.0.1:42827 ❗ Because you are using a Docker driver on linux, the terminal needs to be open to run it. Velero CLIのインストールと設定 Veleroを操作するためのCLIツールをローカル環境にインストールします。今回は Linux (amd64) 版のバージョンv1.17.1を使用します。 # バイナリのダウンロード $ curl -L -o /tmp/velero.tar.gz https://github.com/vmware-tanzu/velero/releases/download/v1.17.1/velero-v1.17.1-linux-amd64.tar.gz # 解凍 $ tar -C /tmp -xvf /tmp/velero.tar.gz # パスが通っている場所へ移動 $ sudo mv /tmp/velero-v1.17.1-linux-amd64/velero /usr/local/bin/velero # 実行権限の付与 $ chmod +x /usr/local/bin/velero インストールが正常に完了したかバージョンを確認します。 $ velero version --client-only Client: Version: v1.17.1 Git commit: 94f64639cee09c5caaa65b65ab5f42175f41c101 最後に、コマンド補完を有効化しておくと便利です。 $ echo 'source <(velero completion bash)' >>~/.bashrc オブジェクトストレージへの設定 Velero本体をインストールする前に、VeleroがオブジェクトストレージであるMinIOへアクセスするための認証情報を設定します。 オブジェクトストレージへの認証情報 AWS S3互換の認証情報ファイルを作成します。Minioの認証情報(user: minioadmin, password: minioadmin)を使用します。 credentials-velero [default] aws_access_key_id = minioadmin aws_secret_access_key = minioadmin Velero用のNamespaceを作成し、このファイルをKubernetesのSecretとして登録します。 $ kubectl create namespace velero namespace/velero created $ kubectl create secret generic velero-s3-creds \   --namespace velero \   --from-file=cloud=credentials-velero secret/velero-s3-creds created Velero本体のインストール 準備が整いましたので、Helmを使用してVelero本体(サーバー)をクラスターにインストールします。 まずはHelmリポジトリを追加します。 $ helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts $ helm repo update 次に、インストール設定を記述したvalues.yamlを作成します。ここで、VeleroがMinIOにアクセスするために「2.2 オブジェクトストレージ」で取得した1つ目のアクセス情報(http://127.0.0.1:39783)を使用します。 今回使用するvalues.yamlでは、Minikube環境でVeleroのCSIスナップショットとファイルシステムバックアップの両方で動作させるために、以下の設定を行っています。 MinIOはAWS S3 APIと互換性があるため、Velero純正のvelero-plugin-for-awsを初期コンテナ(initContainers)として読み込ませています。 configuration.backupStorageLocationでは、バックアップデータの保存場所を定義します。 今回は保存先として、事前に構築した MinIOを指定しています。 ファイルシステムバックアップを利用するために、CSIを使わずにPodのボリュームを直接ファイルコピーする機能(Node Agent)を有効化しています。Minikube環境でホストパス上のボリュームデータに確実にアクセスできるよう、Node Agentをrootユーザーで実行する設定を加えています。 values.yaml image:   repository: velero/velero   tag: v1.17.1   pullPolicy: IfNotPresent dnsPolicy: ClusterFirst initContainers:   - name: velero-plugin-for-aws     image: velero/velero-plugin-for-aws:v1.13.0     imagePullPolicy: IfNotPresent     volumeMounts:       - mountPath: /target         name: plugins kubectl:   image:     repository: bitnamilegacy/kubectl     tag: 1.33.4 configuration:   # CSIスナップショット機能を有効化   features: "EnableCSI"   backupStorageLocation:   - name: default-backup-storage-location     provider: "aws"     bucket: "minio-bucket" # 保存先のバケット名     default: true     accessMode: ReadWrite     config:       # クラスタ内部(Velero)から見たMinIOのURL       s3Url: http://minio.minio.svc:9000        # クラスタ外部(手元のPC)から見たMinIOのURL       #「2.2 オブジェクトストレージ」で取得した1つ目のアクセス情報(http://127.0.0.1:39783)       publicUrl: http://127.0.0.1:39783       s3ForcePathStyle: "true" # MinIOを使うための設定       region: minio   volumeSnapshotLocation: [] credentials:   useSecret: true   existingSecret: velero-s3-creds upgradeCRDsJob:   automountServiceAccountToken: true rbac:   create: true   clusterAdministrator: true   clusterAdministratorName: cluster-admin serviceAccount:   server:     create: true     automountServiceAccountToken: true backupsEnabled: true snapshotsEnabled: true # ファイルシステムバックアップを行うためのエージェント設定 deployNodeAgent: true nodeAgent:   podVolumePath: /var/lib/kubelet/pods   pluginVolumePath: /var/lib/kubelet/plugins   dnsPolicy: ClusterFirst   podSecurityContext:     runAsUser: 0 livenessProbe:   httpGet:     path: /metrics     port: http-monitoring     scheme: HTTP   initialDelaySeconds: 10   periodSeconds: 30   timeoutSeconds: 5   successThreshold: 1   failureThreshold: 5 readinessProbe:   httpGet:     path: /metrics     port: http-monitoring     scheme: HTTP   initialDelaySeconds: 10   periodSeconds: 30   timeoutSeconds: 5   successThreshold: 1   failureThreshold: 5 metrics:   enabled: true   scrapeInterval: 30s   scrapeTimeout: 10s   serviceMonitor:     autodetect: true     enabled: false   nodeAgentPodMonitor:     autodetect: true     enabled: false   prometheusRule:     autodetect: true     enabled: false 作成したvalues.yamlを指定してHelmインストールを実行します。 $ helm install velero vmware-tanzu/velero \   --namespace velero \   -f values.yaml NAME: velero LAST DEPLOYED: Mon Dec 22 11:15:51 2025 NAMESPACE: velero STATUS: deployed REVISION: 1 DESCRIPTION: Install complete TEST SUITE: None NOTES: Check that the velero is up and running: kubectl get deployment/velero -n velero Check that the secret has been created: kubectl get secret/velero -n velero Once velero server is up and running you need the client before you can use it 1. wget https://github.com/vmware-tanzu/velero/releases/download/v1.17.1/velero-v1.17.1-darwin-amd64.tar.gz 2. tar -xvf velero-v1.17.1-darwin-amd64.tar.gz -C velero-client More info on the official site: https://velero.io/docs インストール完了後、Podが正常に起動しているか確認してください。 $ kubectl get pods -n velero NAME READY STATUS RESTARTS AGE node-agent-vntpw 1/1 Running 0 3m22s velero-6f76558b9f-v529z 1/1 Running 0 3m22s 以上で、Veleroのインストールとオブジェクトストレージへの接続設定は完了です。これでバックアップを取得するための準備完了です。 まとめ 本記事では、KubernetesのバックアップツールであるVeleroの導入を解説しました。 minio.yamlやvalues.yamlの内容は一般的な設定を補完していますが、実際の環境に合わせて微調整が必要な場合は適宜変更してください。 次回以降はバックアップやリストアといった操作に入っていきます。実際にサンプルアプリケーションをデプロイし、バックアップを取得します。ボリュームスナップショットだけでなく、ファイルシステムバックアップも確認します。 参考文献 https://velero.io/docs/v1.17/ https://velero.io/docs/v1.17/contributions/minio/ https://velero.io/docs/v1.17/file-system-backup/           ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Veleroのインストールと基本設定 first appeared on SIOS Tech Lab .
今号では、rsync コマンドを使ったファイル転送の方法について説明します! rsync とは rsync とは、サーバ間でファイルを同期、転送する仕組みです。 用途は ftp/sftp と似ていますが、rsync は 差分転送 という技術でファイルを転送しており、ファイルの変更部分のみを検出して転送します。 そのため、同ファイルを 2回目以降転送する場合は非常に高速に動作し、定期的なバックアップやミラーリングの操作に適しています。 なお、前回ご紹介した sftp コマンドの記事 と同様 Linux や Windows 上のターミナルで rsync コマンドを使う前提で説明します。 基本の書式 ローカル→リモートサーバへファイルを転送 rsync ローカルのファイル リモートのユーザ名@リモートのホスト名:ディレクトリのパス の書式で実行します。 なお、rsync は sftp のように事前に接続を確立しておくのではなく、コマンド実行時点で一時的に接続を確立し、ファイルの転送を行います。 例: $ rsync /tmp/data/1.log ruser@172.30.10.10:/data/ ruser@172.30.10.10's password: $ ※パスワード認証の場合、リモートユーザのパスワードが求められます。 ファイル転送が成功した場合、デフォルトでは何も表示されませんが、-v オプションを付与することによって実行時の詳細な情報が表示されるようになります。 例: $ rsync -v /tmp/data/1.log ruser@172.30.10.10:/data/ sent 40 bytes received 12 bytes 20.80 bytes/sec total size is 0 speedup is 0.00 $ リモート→ローカルサーバへファイルを転送 rsync リモートのユーザ名@リモートのホスト名:ファイルのパス ローカルのディレクトリ の書式で実行します。 例: $ rsync ruser@172.31.7.54:/data/1.log.bkp /tmp/data/ $ -v オプションを付与した場合、下記のような実行結果となります。 $ rsync -v ruser@172.31.7.54:/data/1.log.bkp /tmp/data/ 1.log.bkp sent 43 bytes received 83 bytes 84.00 bytes/sec total size is 0 speedup is 0.00 $ なお、 ファイルのパス 部分にはディレクトリを指定することができ、この場合は当該ディレクトリ配下のすべてのファイルを転送します。 例: (ローカルサーバ側での操作) $ ls /tmp/data/ 1.log 2.log 3.log 4.log 5.log $ rsync -avz /tmp/data/ ruser@172.31.7.54:/data/ sending incremental file list ./ sent 179 bytes received 19 bytes 79.20 bytes/sec total size is 0 speedup is 0.00 $ 例: (リモートサーバ側での操作) $ ls /data/ 1.log.bkp 2.log.bkp 3.log.bkp 4.log.bkp 5.log.bkp (ローカルサーバ側での操作) $ rsync -av ruser@172.31.7.54:/data /tmp/data/ receiving incremental file list data/ data/1.log.bkp data/2.log.bkp data/3.log.bkp data/4.log.bkp data/5.log.bkp sent 28 bytes received 201 bytes 152.67 bytes/sec total size is 0 speedup is 0.00 $ ※各オプションの意味については後述します。 rsync コマンドのオプション rsync コマンド の基本的なオプションをご説明します。 ※すべてのオプションはご紹介せず、よく使用されると考えられるものを抜粋しています。 -a アーカイブモード (ディレクトリ構成、パーミッション、グループなどを保持した状態) で転送します。 ★基本的には付与しておくことを推奨します。 $ rsync -a /tmp/data/1.log 172.30.10.10:/data/ -v 実行時の詳細な情報を表示します。 $ rsync -v /tmp/data/1.log 172.30.10.10:/data/ -r ディレクトリ (ディレクトリ配下のサブディレクトリも) を再帰的に転送します。 ディレクトリごと転送したい場合に使用します。 $ rsync -r /tmp/data/ 172.30.10.10:/data/ -n テスト用コマンド。 実際には変更を加えず、どのような動作になるかだけ表示します。 $ rsync -n /tmp/data/1.log 172.30.10.10:/data/ -z ファイル転送時にデータを圧縮します。 ネットワーク転送速度が遅い環境において、帯域幅を削減したい場合に有効です。 ★ファイルをたくさん転送する場合、付与しておくことを推奨します。 $ rsync -z /tmp/data/*.log 172.30.10.10:/data/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 知っておくとちょっと便利!rsync コマンドを使ったファイル転送 first appeared on SIOS Tech Lab .
こんにちは! 今月も「OSSのサポートエンジニアが気になった!OSSの最新ニュース」をお届けします。 12/8-10、Open Source Summit Japan 2025 が開催されました。 Open Source Summit Japan | LF Events https://events.linuxfoundation.org/open-source-summit-japan/ 「Spiderman」と呼ばれるフィッシングサービスが欧州の銀行、暗号通貨サービスの利用者をターゲットにしているようです。 Spiderman Phishing Kit Mimics Top European Banks With A Few Clicks https://www.varonis.com/blog/spiderman-phishing-kit 12/18、シドニー大学でデータ侵害があり、職員と学生の個人情報等が盗まれたようです。 University of Sydney suffers data breach exposing student and staff info https://www.bleepingcomputer.com/news/security/university-of-sydney-suffers-data-breach-exposing-student-and-staff-info/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【2025年12月】OSSサポートエンジニアが気になった!OSS最新ニュース first appeared on SIOS Tech Lab .
はじめに 皆さんこんにちは。エンジニアの細川です。 皆さんは、登壇してますか? 僕はこんな記事を書いていながら、実はそんなに登壇できていません… 社内でも社外でもまだ数回程度しか登壇していませんが、今回はサイオステクノロジーPS SLのアドベントカレンダー22日目ということで、今後の自分自身への鼓舞も含めてLTについて書いていこうと思います。 コツなどは少しずつ分かってきたような気もするので、これから登壇してみようという方の励みになれば幸いです。 LTとは? LT( Lightning Talk )は、その名の通り稲妻(Lightning)のように素早い、5分程度の短いプレゼンテーションのことです。準備のハードルが比較的低く、気軽に聴きやすいこともあり、勉強会などでもよく行われています。 LTは怖くない! LT含めた登壇を怖いと思っている方は多いのではないでしょうか? 実際僕も、怖いと思っていました。というか今も登壇の前はやっぱり緊張します(笑) しかし、LTは他の登壇形式に比べても自由な雰囲気が強く、初心者の方にも優しい空気感があると思っています。 LTは自由! LTのテーマはかなり自由です。IT分野に絞っても面白い技術の紹介をする方もいれば、失敗談を共有する方、自分の主張を世に知らしめたい方など様々なテーマで発表する方がいます。 有益な情報じゃなくても、内容が薄くても、ちょっとした共感できるめんどくさい作業なんかでもOKです! 登壇するだけで割とほめてくれる 皆さんお分かりの通り、登壇というのはどうしてもハードルが高いものです。そのため、仮に発表で失敗してしまっても登壇するだけでほめてくれるようなことも結構あります。一笑い取れた暁にはもう賞賛の嵐です(笑) また、勉強会などは懇親会もよくセットであると思いますが、その際の話題にもなります。知らない人ばかりの時はむしろ登壇した方がトータルで見ると楽かもしれません。     LTの心得5選 では、LTで登壇するうえで大事な心得を5つ紹介したいと思います。個人的に特に大事な5つに絞ってみました。 1. 登壇すると決める! まず、何よりも大事なのは登壇すると決めて、登壇者として申し込むことです。テーマが決まってなくてもとりあえず申し込みましょう。テーマ決めや発表準備などは期限が迫れば頑張れます 2. 何を伝えるかを決める 次に大事なのは、何を伝えるかを決めることです。テーマやタイトルを決めることはもちろんなのですが、「この発表で何を伝えたいか」を決めましょう。その際に聞き手側のターゲット層まで考えられるとより良いかなと思います。また、その「この発表で伝えたいこと」は繰り返し発表内で伝えられるといいかと思います。 3. しゃべりすぎない!短すぎるくらいでいいかも LTは大体しゃべりすぎてしまいがちです。スライドを作っているときも興が乗って多く作りすぎてしまいがちですが、作りすぎないようにしましょう。よく言われている基準ですが、目安としては1枚1分くらいのつもりで良いと思います。タイトルや目次、自己紹介などを合わせても、5分程度の発表なら多くても10枚いかないくらいかと思います。 運営的にも巻いてくれた方がうれしいはずです!(多分きっと) 4. スライドに書いていないことはあまりしゃべらない スライドに書いていないことはあまりしゃべらない方が良いと思います。もちろんダメということは無いのですが、目から入る情報の方が印象に残りやすく、結構聞き流されることも多いです。 また、登壇中に緊張して頭が真っ白になってもスライドを読むだけなら、発表を耐えきることができます! どうしても細かい内容を伝えたい場合は説明用の超細かいスライドを作って、スライドを後で共有するときに読んでもらいましょう。 5. 楽しむ! 登壇前は緊張で押しつぶされそうになることもありますが、登壇中は楽しみましょう!失敗してもそれはそれで次のLTのネタになるかもしれません(笑) その他の細かいコツ LTを話すうえで特に個人的に大事な心得5選は上記の通りですが、それ以外の細かいコツ的なところも少しまとめてみようと思います。 自己紹介は長くしすぎない LTは全体の発表時間が短いこともあり、自己紹介で時間を使ってしまうと、延びてしまいがちです。情報を詰め込みすぎないようにしましょう。 スライド1枚に詰め込みすぎない 文字は大きめにし、可能な限り図や表などを利用しましょう。 スライドに文字を詰め込みすぎると自分も聞き手の方も読むのに時間がかかり、疲れてしまいます。また、会場で行う場合は後ろの方でも見える程度の文字の大きさにするように頑張りましょう。 想定質問用のスライドを作っておくと良いかも 5分だと伝えきれないことも多いので、質問の余地をあえて残し、その答え用のスライドを作っておくとかなりいいかなと思います。もし質問が出ない場合は最後に「これについてはスライド読んでください。」みたいな感じで後で読んでもらうのもいいかと思います。 リハーサルは、やっておくと安心感 個人的には、短めの発表では原稿を作らないことも多いです。ただ、スライドを読むだけにしてもリハーサルはやっておいた方が安心感があります。どのスライドで何分目安か確かめておくだけでほぼ時間ぴったりで発表を終えることもできます。 ただ、リハーサルに時間をかけすぎて準備のハードルが上がってしまうよりは、何回も登壇する方が良いかなと思っています。 デモは可能な限り避ける デモは結構動きません。また、時間が読みにくいこともあり、可能な限り動画やスクリーンショットで対応する方が良いかと思います。動かないのもネタにできる!という強者の方以外は避けておいた方が無難です。 スライド作成はAIで楽しよう 最近はAIでスライドの準備も楽になりました。 Canva でもAIがサポートしてくれますし、LLMを契約している方は Marp を使えばLLMでもスライドを作成できます。ClaudeとMarpを連携して登壇資料を作る方法は弊社の龍ちゃんが ブログ にまとめてくれているのでぜひそちらもご一読ください。 まとめ LTの心得5選は以下の通り 登壇すると決める! 何を伝えるかを決める しゃべりすぎない!短すぎるくらいでいいかも スライドに書いていないことはあまりしゃべらない 楽しむ! おわりに 今回、LTについて書いてみました。これから登壇しようと考えている方に少しでも役に立てば幸いです。僕自身もこの記事を意識しながら登壇回数を増やしていきたいと考えています。ともに頑張りましょう!今月はサイオステクノロジーでもアドベントカレンダーを実施中です。テーマは「SIOS社員が今年一年で学んだこと」で、ほかにもさまざまな記事が上がっていますので、ぜひ こちら もチェックしてみてください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post LTは怖くない!(怖がらない!)LT実践の心得5選 first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジーの佐藤 陽です。 「SIOS社員が今年一年で学んだこと」のアドベントカレンダー 21 日目です。 私は元々サーバーサイド側のエンジニアでしたが、今年は自身初となる PM(プロジェクトマネージャー)として多く活動した 1 年でした。 この 1 年の経験から、プロジェクトマネジメントにおいて特に重要だと感じたポイントを備忘録も兼ねて紹介します。 はじめに 私はプロジェクトマネージャーとして、とある Web システム開発プロジェクトを担当しました。 Web システムの内容としてはよくあるもので、ここでは仮に「オンライン書店システム」を構築したとします。 概要としては、ユーザーが本を検索・購入できる機能を持ち、管理者が在庫管理や注文処理を行うシステムです。 システムの構成としてはフロントエンドがあり、バックエンドがあり、外部 DB があり、Azure 上にデプロイされるというものです。 はじめに要件定義を行い、設計・開発と順を追って進めていきました。 開発の中でいくつか課題は出たものの、致命的な遅れは発生せず、なんとかバッファを使いながらも予定通り進めていきました。 しかし、プロジェクトの終盤、リリース直前のタイミングにおいて多くのバグが発生し、ラスト 1 ヶ月は非常に高い負荷での対応に追われる形となってしまいました。 このとき、メンバーには非常に負担をかけてしまい、とても反省したポイントです。 そこでこの経験を踏まえて、なぜプロジェクトが最後の 1 ヶ月で非常に高い負荷の状態になってしまったのか、そしてそれを防ぐためにはどうすればよいのかを考えました。 なぜ最後の 1 ヶ月が苦しくなったのか 直接的な要因は、リリース直前のタイミングにおいて多くのバグが発生したことです。 ただし、アプリケーション開発を行うにあたってバグが発生すること自体は、ある程度は避けられないものだとも思います。 そのため、このときに本当に問題となったのは、 「リリース直前のタイミングで、大量の」 バグが見つかってしまった点だと考えました。 なお、バグの主な原因としては以下のようなものがありました。 仕様の解釈ズレ フロントとバックエンドのインターフェース不整合 クラウド(Azure)環境特有の制約 そして、これらのバグがリリース直前まで潜んでしまっていた要因として、次のような点が挙げられます。 フロントとバックエンドを結合した構築が遅れていた クラウド上にアップロードしての動作確認が遅れていた 1 と 2 のような環境が、気軽に触れられる状態になっていなかった それぞれの実装時に、各機能の単体での動作確認などは行っていましたし、コードの品質も一定のレベルでは担保できていたと感じています。 ただし、いざクラウドにデプロイしてみると想定外のエラーが発生しましたし、 フロントとバックエンドとの連携がうまく取れておらず、リクエストやレスポンスに不整合が多く発生しました。 また、実際のアプリケーションを気軽に触れられる環境がなく、 UI を操作しての総合的な動作確認というものが十分に行えていなかった点も大きかったと感じています。 これを踏まえると、今回の記事で伝えたいことは次の 3 点です。 早めにフロントとバックエンドを結合しておきましょう 早めにクラウド環境へデプロイしましょう 誰もが最新の環境を気軽に触れるようにしておきましょう ただ、こういったことは既に色々なところで提唱されているはずだ、と思い少し勉強してみました。 そうすると以下のようなキーワードが引っかかりました。 ウォーキングスケルトン フェイルファスト せっかくなので、これらの考え方について学んだ内容を少し体系的にまとめていきたいと思います。 ウォーキングスケルトンを早く作る 1997 年に Alistair Cockburn によって名づけられたもので、「小さなエンドツーエンド機能を持つ、ごく小さな実装」として定義されています。 本番さながらの構成すべてを作り込む必要はなく、主要なアーキテクチャ要素(フロント、バックエンド、データベース)などが必要最低限に繋がった状態を指します。 スケルトン(骸骨)でありながら、ウォーキング(歩く)ことができる、というイメージと直結しますね。 例えば、上記の「オンライン書店システム」の例でいうと、 フロントエンド:検索画面のみ バックエンド:書籍一覧取得 API のみ データベース:書籍情報テーブルのみ といった主要な要素がひとまずすべて繋がり、「検索画面から書籍一覧を取得して表示できる機能だけが動作する」状態がウォーキングスケルトンにあたります。 機能としては最終想定のものから大きく欠如していますが、主要な要素がすべて繋がって動作している点が重要です。 ここで気になるのは「MVP(Minimum Viable Product)」との違いです。 MVP はユーザーに提供することを前提とした「最小限の製品」です。 つまり、「最小限」といいつつ、ユーザーが目的を達成できるレベルの機能を持ちます。 ここで求められているのは、ユーザーの要望を実現できているか、です。 一方、ウォーキングスケルトンはあくまで内部向けの成果物であり、求められているのは「ちゃんと端から端まで動くか」を確認することです。 ウォーキングスケルトンが作られるのは、プロジェクトのかなり早い段階であるべきと考えられています。 Cockburn 氏の書籍では、以下のように述べられています。 大規模なプロジェクトが進行中でした。それは、リングを介してシステム間でメッセージをやり取りするというものでした。もう一人のテクニカルリーダーと私は、最初の1週間以内にシステム同士を接続し、リングを介して単一のヌルメッセージをやり取りできるようにしようと考えました。こうすることで、少なくともリングは動作するようになりました。 そして、毎週末には、その週にどんな新しいメッセージやメッセージ処理が開発されても、リングが完全な状態を維持し、前週のメッセージをすべて確実に通過させることを義務付けました。こうすることで、システムを制御された方法で拡張し、各チームの同期を維持することができました。 要するに、最初の 1 週間で「動く全体像(骨組み)」を作り、その後も常にリング全体が壊れていないことを保証し続けることで、複数チームが安心して機能追加できる状態を保っていた、という話です。 一度骨組みができたら、そこに少しずつ『肉付け』をしていきます。 具体的には、新しい API や画面を追加していく、既存の機能を拡張していく、といった形になります。 この時重要なのが、「ウォーキングスケルトン全体が常に動作する状態を保つ」ことです。 新しい要素を追加したり、既存の要素を拡張したりするたびに、全体がちゃんと動くかを確認しながら進めていきます。 また、こういったウォーキングスケルトンが存在すると、早めに様々なメンバーに触ってもらうことができ、様々なバグ報告やフィードバックが早期に得られます。 まさにこれがウォーキングスケルトンを用意する最大のメリットで、「いつでも触れる環境が早期から整備されている」ことで「後からまとめて問題が噴き出す」のをある程度防げることです。 また、ここで重要になるのが、「早めに CI/CD 環境を構築しておくこと」だと感じました。 例えば、次のような仕組みをプロジェクト序盤から用意できていると、ウォーキングスケルトンを常に動く状態で保ちやすくなります。 フロントエンドやバックエンドに関して PRがマージされるたびに 自動でビルド・テストが走る クラウド環境に自動でデプロイされる こうしておくと、高い頻度でテストが実施されますし、 誰でも触れる最新の環境がクラウド上に常に用意されるため、色々な人に触ってもらいやすくなります。 ウォーキングスケルトンと CI/CD をセットで早期に整えておくことで、「いつでも動く骨組み」と「いつでも触れる環境」が両立し、結果として後半になってからの問題の噴き出しをかなり抑えられるはずです。 今回のプロジェクトでは、こういった「触れる環境の構築」がだいぶ後回しになってしまい、結果的に「まとめてバグが出る」状況を招いてしまいました。 もし開発のかなり早い段階で「検索画面から書籍一覧を 1 件だけ取得する」程度のウォーキングスケルトンを作っておけていれば、 こうした問題の多くは、ラスト 1 ヶ月ではなく、もっと余裕のあるタイミングで潰せていたはずだと痛感しました。 フェイルファストで早く失敗する フェイルファストは、「失敗するならできるだけ早く、できるだけ小さく失敗しよう」という考え方です。 Google などでも採用されている考え方で、シリコンバレーのスタートアップ企業などで重要視されています。 今回のプロジェクトで言えば、 フロントとバックエンドを結合してみたら意図しない挙動が多く発生した 開発の終盤で結合環境をデプロイして結合テストを落としたら多くのバグが見つかった といった、ラスト 1 ヶ月で実際に発生していた事象を、もっと早い段階から意図的に踏みに行って潰すべきでした。 そして、先ほど紹介した「ウォーキングスケルトン」の手法と「フェイルファスト」のマインドは非常に相性が良いです。 ウォーキングスケルトンを早期に作成し、早めの検証を行うことで、 仕様の解釈ズレやインターフェースの不整合 クラウド環境特有の制約 といった問題に、プロジェクトのかなり早い段階でぶつかり、そして潰しておくことが可能です。 また、ここで見落としがちなのが、チームのマインドセットです。 細かいミスを見つけたときに、それをためらわずに共有できる雰囲気づくりが欠かせません。 例えば、次のような文化が根付いていると理想的です。 バグを報告した人が責められない むしろバグを見つけたら「ありがとう」と感謝される チーム全体で品質向上に取り組む姿勢が共有されている とはいえ、マインドセットだけではなかなか維持できないので、それを後押しするための「仕組み」も用意しておく必要があります。 例えば、バグや問題点を見つけたらすぐに報告できる運用フローを整備しておくことが重要だと考えています。 ポイントは、「迷わず機械的にフローに載せられるようにしておく」ことです。 そのために、GitHub 上にバグ報告用の Issue テンプレートを用意しておき、見つけた人はそれに沿って淡々と記入するだけにします。 その後の優先度付けや担当アサインは PM が引き取り、適切にハンドリングしていきます。 Slack などで都度報告して…となると、「文章をどう書くか」「どう報告したらよいか」といった形で迷いが生じ、 「このくらいのバグなら大きな支障もないし、まあいいか」となってしまうことも無いとは限りません。 まとめ 今回のプロジェクトでは、ラスト 1 ヶ月で多くのバグが発生し、メンバーにも大きな負荷をかけてしまいました。 振り返ってみると、バグが潜伏していたことよりも「バグが発見されるタイミングが遅すぎたこと」が、大きな要因だったと感じています。 ウォーキングスケルトンを早期に作ること フェイルファストの考え方で、小さく早く失敗すること それらを支える CI/CD や「触れる環境」と、バグ報告を歓迎するチーム文化を整えること これらを意識できていればば、ラスト 1 ヶ月の過ごし方は大きく変えられたかもしれません。 自分自身への戒めも込めて、次のプロジェクトでは、今回の反省を活かしていきたいと思います。  ではまた! 参考リンク https://67bricks.com/blog/what-is-a-walking-skeleton-and-why-do-i-need-one https://wiki.c2.com/?WalkingSkeleton https://web.archive.org/web/20080511171042/http://alistair.cockburn.us/index.php/Walking_skeleton https://henko.net/blog/break-down-silos-with-a-walking-skeleton/   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post ラスト1か月を炎上させないためのプロジェクトマネジメント first appeared on SIOS Tech Lab .
はじめに PS-SLの佐々木です。 アドベントカレンダー20日目の記事になります。 今回の記事ではChatGPTやClaudeのようにAIの回答をリアルタイムで表示する方法について解説します。 なぜSSEが必要か LLMを使ったチャットアプリでは、回答生成に数秒〜数十秒かかることがあります。 ユーザー: 「この資料を分析して」 ↓ ... 10秒待機 ... ↓ AI: 「分析結果は...(長文)」 全文が完成するまで何も表示されない のはUXが悪い。ChatGPTのように 文字が順次表示される 体験を実現するには、 SSE(Server-Sent Events)   が有効です。 SSEとWebSocketの違い 項目 SSE WebSocket 通信方向 サーバー → クライアント(単方向) 双方向 プロトコル HTTP 独自プロトコル 再接続 自動 手動実装が必要 実装難易度 低い 高い ユースケース AI回答、通知、ログ チャット、ゲーム AIの回答ストリーミングはサーバー→クライアントの単方向通信なので、SSEで十分です。 実装 1. 必要なライブラリ pip install fastapi sse-starlette sse-starlette はFastAPIでSSEを簡単に扱うためのライブラリです。 2. イベント型の定義 まず、ストリーミングするイベントの型を定義します。 from dataclasses import dataclass, field from typing import Any @dataclass class StreamEvent : """SSEで送信するイベント""" type : str # イベント種別 content: str | None = None # テキストコンテンツ tool: str | None = None # ツール名 result: dict [ str , Any ] | None = None # ツール実行結果 message: str | None = None # エラーメッセージ等 def to_dict ( self ) -> dict [ str , Any ]: """JSON変換用""" data: dict [ str , Any ] = { "type" : self. type } if self.content is not None : data[ "content" ] = self.content if self.tool is not None : data[ "tool" ] = self.tool if self.result is not None : data[ "result" ] = self.result if self.message is not None : data[ "message" ] = self.message return data 3. イベント種別の設計 AIエージェントの処理状況を伝えるため、複数のイベント種別を用意します。 イベント 用途 例 thinking 処理中表示 「分析中…」 text_delta 回答の差分 「結果」「は」「以下」… tool_call ツール呼び出し開始 検索ツール起動 tool_result ツール実行結果 5件ヒット final_answer 最終回答 全文 error エラー タイムアウト等 done 完了 ストリーム終了 4. FastAPIエンドポイント import json from collections.abc import AsyncGenerator from fastapi import APIRouter from sse_starlette.sse import EventSourceResponse router = APIRouter() @router.post( "/chat" ) async def chat ( request: ChatRequest ) -> EventSourceResponse: """SSEでAI回答をストリーミング""" async def event_generator () -> AsyncGenerator[ dict [ str , Any ], None ]: try : # 処理開始を通知 yield { "event" : "thinking" , "data" : json.dumps({ "type" : "thinking" , "content" : "処理中..." }), } # AIエージェントからイベントを受け取りながら送信 async for event in agent.stream(request.message): yield { "event" : event. type , "data" : json.dumps(event.to_dict(), ensure_ascii= False ), } # 完了を通知 yield { "event" : "done" , "data" : json.dumps({ "type" : "done" }), } except Exception as e: yield { "event" : "error" , "data" : json.dumps({ "type" : "error" , "message" : str (e)}), } return EventSourceResponse( event_generator(), media_type= "text/event-stream" , ) ポイント: EventSourceResponse に AsyncGenerator を渡す yield でイベントを送信( event と data のdict) ensure_ascii=False で日本語を正しく送信 5. LangGraphからのイベント変換 LangGraphの astream_events を使ってイベントを取得し、SSE用に変換します。 async def stream ( self, message: str ) -> AsyncGenerator[StreamEvent, None ]: """LangGraphワークフローをストリーミング実行""" initial_state = { "messages" : [HumanMessage(content=message)]} async for event in self._graph.astream_events(initial_state, version= "v2" ): event_type = event.get( "event" ) event_name = event.get( "name" , "" ) event_data = event.get( "data" , {}) # LLMのストリーミング出力 if event_type == "on_chat_model_stream" : chunk = event_data.get( "chunk" ) if chunk and hasattr (chunk, "content" ) and chunk.content: yield StreamEvent( type = "text_delta" , content=chunk.content) # ツール呼び出し開始 elif event_type == "on_tool_start" : yield StreamEvent( type = "tool_call" , tool=event_name, input =event_data.get( "input" , {}), ) # ツール実行完了 elif event_type == "on_tool_end" : output = event_data.get( "output" , "" ) yield StreamEvent( type = "tool_result" , tool=event_name, result=parse_tool_output(output), ) # ワークフロー完了 elif event_type == "on_chain_end" and event_name == "LangGraph" : final_answer = extract_final_answer(event_data) if final_answer: yield StreamEvent( type = "final_answer" , content=final_answer) yield StreamEvent( type = "done" ) フロントエンド実装 JavaScript(fetch API) async function streamChat ( message ) { const response = await fetch ( '/api/chat' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ message }), }); const reader = response. body . getReader (); const decoder = new TextDecoder (); let buffer = '' ; while ( true ) { const { done, value } = await reader. read (); if (done) break ; buffer += decoder. decode (value, { stream : true }); const lines = buffer. split ( '\n' ); buffer = lines. pop () || '' ; for ( const line of lines) { if (line. startsWith ( 'data: ' )) { const data = line. slice ( 6 ); if (data === '[DONE]' ) continue ; try { const event = JSON . parse (data); handleEvent (event); } catch { // パースエラーは無視 } } } } } function handleEvent ( event ) { switch (event. type ) { case 'thinking' : showThinking (event. content ); break ; case 'text_delta' : appendText (event. content ); break ; case 'tool_call' : showToolCall (event. tool , event. input ); break ; case 'tool_result' : showToolResult (event. tool , event. result ); break ; case 'final_answer' : setFinalAnswer (event. content ); break ; case 'error' : showError (event. message ); break ; case 'done' : finishStream (); break ; } } React実装例 const [content, setContent] = useState ( '' ); const [isStreaming, setIsStreaming] = useState ( false ); // SSEイベントハンドラ const handleEvent = ( event: StreamEvent ) => { switch (event. type ) { case 'text_delta' : setContent ( prev => prev + event. content ); break ; case 'final_answer' : setContent (event. content ); break ; case 'done' : setIsStreaming ( false ); break ; } }; 実用的なTips 1. エラーハンドリング async def event_generator (): try : async for event in agent.stream(message): yield format_event(event) except asyncio.TimeoutError: yield format_event(StreamEvent( type = "error" , message= "タイムアウト" )) except Exception as e: logger.error( f"Stream error: {e} " ) yield format_event(StreamEvent( type = "error" , message= str (e))) finally : yield format_event(StreamEvent( type = "done" )) 2. タイムアウト設定 from asyncio import timeout async def event_generator (): async with timeout( 120 ): # 2分でタイムアウト async for event in agent.stream(message): yield format_event(event) 3. ログ出力 本番環境ではイベントログが重要です。 elif event. type == "tool_call" : logger.info( f"[TOOL] {event.tool} called with {event. input } " ) elif event. type == "error" : logger.error( f"[ERROR] {event.message} " ) elif event. type == "done" : logger.info( f"[DONE] Total events: {event_count} " ) 4. CORSの設定 フロントエンドが別オリジンの場合: from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:3000" ], allow_methods=[ "*" ], allow_headers=[ "*" ], ) まとめ 項目 実装 ライブラリ sse-starlette バックエンド EventSourceResponse   +   AsyncGenerator フロントエンド fetch   +   reader.read() イベント設計 処理状況を細かく通知 SSEを使うことで、 LLMの回答がリアルタイムで表示され 、ユーザーは処理状況を把握しながら待つことができます。 参考 FastAPI Streaming sse-starlette LangGraph Streaming ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【FastAPI】SSEでAIの回答をリアルタイムストリーミングする first appeared on SIOS Tech Lab .
AIが生成したコードをレビューせずにマージしていいだろうか。 多くのエンジニアは「ダメに決まっている」と答えるだろう。バグが混入するかもしれない。セキュリティホールが見逃されるかもしれない。既にある関数と同じものを新たに作ってしまっているかもしれない。技術的負債が積み上がるかもしれない。 しかし、少し考えてみてほしい。 コードレビューは、誰のためにあるのか。DRY原則は、何を守っているのか。可読性は、誰が読むことを想定しているのか。技術的負債は、誰が返済するのか。 これらはすべて、 人間がコードを読み、人間が修正する という前提から生まれた概念である。 近い将来、AIがコードを書き、AIが直す時代が来たら、この前提はどうなるだろうか。 私が考えてみたいのは、 人間がコードレビューもしなければ、きれいなコードを意識することもしない、最大限AIを活かすとしたらどうなるか という新しい原則である。 人間がコードをレビューする。人間が一貫性を保つ。人間が保守する。これらはすべて、人間の時間というスケールしないリソースを消費する。 人間をボトルネックにせず、AIによって一定水準のプロダクト提供を最大限スケールさせる。そのための原則を考えたい。 まずは現在「正しい」とされるバイブコーディングの原則を整理し、その前提を疑い、最後に新しい原則を考えてみる。 現在の「正しい」原則 2025年現在、バイブコーディングのベストプラクティスとして広く共有されているのは、以下のようなものである。 仕様書ファースト。 コードを書く前に、何を作るのかを明確にする。PRDやREADMEを最初に作成し、AIと人間の間で「完成像」を共有する。曖昧な指示は曖昧なコードを生む。 テストファースト。 実装の前にテストを書く。AIが生成したコードの正しさを担保する唯一の方法がテストである。人間がコードを全行レビューするのは現実的ではない。 Git管理の徹底。 すべての変更をコミットし、いつでも巻き戻せるようにする。AIが予期せぬ変更を加えたとき、直前の状態に戻せることが安全網になる。 コードレビュー。 AIが生成したコードであっても、マージ前に人間がレビューする。「AIを信じすぎるな」が合言葉である。 一貫性・保守性・可読性。 コードベース全体で一貫したスタイルを保つ。同じことを複数の方法で実装しない(DRY原則)。将来の自分や他のエンジニアが読んで理解できるコードを維持する。 これらは従来のソフトウェア開発で培われた知恵をバイブコーディングに適用したものである。実際、これらを守ることで成功率は大幅に上がる。 しかし、本当にこれでいいのだろうか。 常識を疑う 先に挙げた原則のうち、コードレビュー、DRY、可読性、保守性。 これらはすべて**「人間がコードを読み、人間がコードを修正する」**という前提から生まれている。 AIがコードを書き、AIが直す時代には、この前提を問い直す必要があるのではないか。 コードレビューは誰のためか コードレビューは主に以下の観点で行われる(知識共有や教育という側面もあるが、割愛)。 業務観点:設計書通りに動くか。設計書の考慮漏れはないか。エラーやワーニングが出ていないか。 非業務観点:命名規則に忠実か。スコープは最小限か。長すぎるロジックは分割されているか。共通ロジックは切り出されているか。 ここで区別したいのは、 外部設計レベル と 詳細設計以下 である。 外部設計——APIの設計、インターフェース、モジュール間の依存関係。 これは人間が見るべきだと考える。アーキテクチャの妥当性は、まだ人間の判断が必要な領域である。 しかし、 詳細設計以下——関数の中身、ループの書き方、変数名、インデント。 ここに人間のリソースを割く必要があるだろうか。 「設計書通りに動くか」はテストが検証する。「考慮漏れ」はエッジケースのテストが担保する。「エラーやワーニング」はCIが検出する。非業務観点も、リンターやフォーマッターがチェックする。 実際、写真を入れたら犬か猫か正確に判定するAIがあるとして、中身がブラックボックスだからという理由で使われないことはないだろう。 テストが通っていて、致命的なバグや危険性がなければ問題はない。 AIを最大限活かすなら、人間のリソースはコードの細部のチェックではなく、それを担保する入出力のテストに振ったほうがいいのではないか。 DRY原則は何を守っているのか DRY(Don’t Repeat Yourself)原則は、「同じロジックを複数の場所に書くな」という教えである。なぜか。同じロジックが3箇所にあると、修正時に3箇所すべてを直す必要があり、人間は漏れやすいからだ。 では、修正するのがAIならどうか。「この仕様変更に対応して」と言えば、AIは該当箇所を全部直してくれる。3箇所だろうが10箇所だろうが、全部直せばいいだけの話である。 「AIも見落とすのでは?」という反論があるかもしれない。しかし、仕様変更が入ったら、まずテストを修正し、テストが通るまでAIに修正を依頼すればよいのではないか。 もちろん、DRY原則を守ったコードが理想的ではある。しかし、AIを前提とすると、守っていないことのデメリットは以前より少なくなる。 ゆえに、DRY原則を徹底して守らせるより、テストの充足に力を割いたほうが良いように思える。 可読性は誰が読むことを想定しているのか 「可読性の高いコード」とは、人間が読んで理解しやすいコードである。変数名は意味のあるものに。関数は短く。コメントは適切に。 可読性自体は、AIにとっても重要である。AIも自然言語ベースでコードを解釈するため、意味のある命名やコメントは有効に働く。 ただし、「人間のための可読性」の優先度は下がる。たとえば、インデントが一切ないコードでも、AIは問題なく読める。フォーマットの美しさ、視覚的な整理——そういう「人間の目に優しい」配慮に時間をかける必要性は減っていく。 関数名も、短くて読みやすい <<< 長くても意味ある命名 になるだろう。 技術的負債は誰が返済するのか 技術的負債とは、「今は動くが、将来の修正を困難にするコード」のことである。返済コストは、人間が修正に苦労する時間として計上される。 AIがコードを修正する場合、事情は変わる。ちょっとしたリファクタリングは、頼めばすぐ終わる。古い言語で書かれたシステムを置き換えたいなら、目的とテストケースさえあれば、新たに作り直せばいい。 「壊れたら捨てて作り直す」がAIには低コストで可能である。 少なくとも、今までの技術的負債とこれからの技術的負債は、だいぶ意味合いが変わるように思える。 これからの原則 ここで、AIの発展を前提とした新しい原則を考えてみたい。 この原則の目的は明確である。 人間をボトルネックにせず、AIによって一定水準のプロダクト提供を最大限スケールさせること。 人間がコードをレビューする。人間が一貫性を保つ。人間が保守する。これらはすべて、人間の時間というスケールしないリソースを消費する。AIに任せられる部分はAIに任せ、人間は「何を作るか」「何が正しいか」の定義に集中する。 原則1:入出力が正しければ、内部実装は問わない。 関数の「契約」だけを検証する。テストが通れば、詳細設計以下のコードレビューは不要である。 原則2:一貫性は不要、重複もOK。 同じロジックが3箇所で違う方法で実装されていてもいい。各関数が独立してテストを通るなら、統一する必要はない。 原則3:詳細設計以下はレビューしない、テストを厚くする。 外部設計(API、インターフェース、アーキテクチャ)は人間が見る。しかし関数の中身は見ない。テストが信頼の源泉になる。 原則4:保守しない、作り直す。 技術的負債を「返済」するのではなく、壊れた部分を「作り直す」。保守性を高める努力より、テストを整備して「いつでも作り直せる」状態を維持する。 成立条件 この原則が成立するには、いくつかの条件がある。 テストファーストが絶対。 テストなしにこの原則を適用すると、ただの無法地帯になる。 関数の粒度が適切。 巨大な関数は入出力の検証が困難になる。テスト可能な単位に分割する。 E2Eテストの整備。 個々の関数が正しくても、システム全体の動作は別途検証が必要である。 モデルの継続的な発展。 2025年現在のAIではまだ難しいが、発展が続くことが前提である。 もちろん、安全性が重視されるプロジェクトでは、こうはいかない。銀行の勘定系や医療システムなど、従来のベストプラクティスをすべて適用すべき領域は当然ある。しかし、スタートアップや個人開発など、開発速度がより重要な現場では、この原則のような開発手法に次第になっていくのではないか。 まとめ 本稿では、現在「正しい」とされるバイブコーディングの原則を整理し、その前提を疑い、新しい原則を考えてみた。 コードレビュー、DRY、可読性、保守性——これらはすべて「人間がコードを読み、人間が修正する」という前提から生まれている。AIがコードを書き、AIが直す時代には、この前提を問い直す必要がある。 新しい原則の目的は、人間をボトルネックにせず、AIによって一定水準のプロダクト提供をスケールさせること。人間のリソースはコードの細部ではなく、入出力のテストに振る。 AIがもっと賢くなれば、コードレビューもDRYも可読性も技術的負債も、過去の遺物になるかもしれない。 確かなことは、今まで「正しい」とされてきた原則はすべて、人間の、人間による、人間のための原則だったということである。 これからは、人間のための、AIによる、人間への原則を考える必要があるのではないだろうか。 あとがき タイトルのせいで、論文みたいな口調になってしまいました。 ここのところ、AIを使った開発をしていて、ずっとうっすら違和感を覚えていました。 最近になってそれが、「人間ではなくAIが開発するようになったのに、まだ人間のための手順や原則を守らせている」という違和感だと腹落ちしました。 そしてそれは、いずれやらなくてよくなる過渡期だけのテクニックかもしれない。それを踏まえて考えてみたのが今回の原則です。 調べてみましたが、AI開発においてコードレビューを不要とするような主張は、他に見つかりませんでした。 一番近いのはテスト駆動開発でしょうか。 Anthropicも「テストが通るまでAIに修正させる」というワークフローを推奨しています 。 ただ、そこから一歩踏み込んで「だから詳細設計以下のレビューは要らない」とまで言っている人は、まだいないようです。 このような原則が使われ始めるのはまだ先かもしれませんが、AIの進歩の速さは目覚ましく、来年にはもうこのような開発が行われていてもおかしくないな、と思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post これからの「バイブコーディング」の話をしよう first appeared on SIOS Tech Lab .
ウェブページやアプリケーションの「入力フォーム」についての記事です。 主に、これからUI設計をはじめる方へ向けた内容です。 アンケートや、コンテンツ登録、設定画面など、入力フォームを作成する際に、「ラジオボタン」「チェックボックス」「セレクト」など、各項目をどの方式とするのがよいか迷うことがあります。 今回は「選択式」のUIコンポーネントに注目して、整理してみたいと思います。 なお、各UIコンポーネントの名称や詳細仕様は、開発、制作するシステムによって異なります。 その点については、過去の記事「そのUIどう呼ぶ?チームの認識を揃えるためのコンポーネント名称整理」を御覧ください。 https://tech-lab.sios.jp/archives/48327 基本偏 まず、下記の3つのコンポーネントで、多くのケースをカバーできます。 選択できる数が「1件」で、選択肢が「2〜5件程度」の場合は『ラジオボタン』 選択できる数が「1件」で、選択肢が「5件以上」の場合は『セレクト』 選択できる数が「複数」であれば『チェックボックス』 ラジオボタン セレクト チェックボックス 選択できる数 1件 1件 複数 選択肢の数 2〜5件程度 5件程度以上 – 選択肢が2件(2択)の場合は、次のように内容を考慮して、コンポーネントを選びます。 「on/off、有効/無効、有/無、はい/いいえ、必要/不要」のように、端的な排他の選択の場合は「ラジオボタン」か「チェックボックス」のどちらかを利用します。 2択でも、色を選択する場合など、選択肢それぞれに固有の意味がある場合は「ラジオボタン」です。 ラジオボタン チェックボックス 2択で、排他的な選択肢の場合 2択だが、選択肢に固有の意味がある場合 (チェックボックスは利用しない) 発展編 ユーザー層が広い場合は「基本編」のUIコンポーネントで構成するのがよいでしょう。 業務システムなど、操作に慣れたユーザーが利用する場合は、以下のコンポーネントも利用することで、操作が効率的になる場合があります。 複数選択のセレクト 選択可能な数が複数の場合は、「チェックボックス」がよいですが、選択肢が多く小さなスペースに収めたい場合は、「セレクト」を複数選択可能(multiple)な状態にして、利用するとよいでしょう。 ただし、HTML標準の複数選択可能なセレクトで実装した場合、モバイル(スマートフォン等)のブラウザでは、複数選択できることが分かりやすい表現になっていますが、デスクトップ(パソコン)のブラウザでの表示は、分かりにくい場合が多いです。 これを対応する場合は、既存のUIライブラリを利用して、セレクトの選択肢部分にチェックボックスを表示させると、良いでしょう。 次の図は「MUI」の例です。 https://mui.com/material-ui/react-select/#checkmarks コンボボックス(オートコンプリート、データリスト) 選択以外に、テキスト入力(自由入力)をあわせたい場合はコンボボックスです。 テキストを入力すると、選択肢が絞り込まれて表示されます。 なお、コンボボックスやオートコンプリートと呼ばれるコンポーネントでは、自由入力を受け付けないバリエーションもあります。 複数の項目選択を選択できるバリエーションもあります。 また、入力中の選択肢の絞り込みは、前方一致の場合の他に、部分一致の場合もあります。 デイトピッカー 日付を入力する場合、デイトピッカーにすることで、テキストでの入力に加えて、カレンダーから日付を選択して、入力させることができます。 カレンダー形式は、一覧性や、俯瞰しやすいため、直感的に素早く入力できる場合があります。 トグル(スイッチ) スマートフォンの登場により、よく目にするようになったのが「トグル(スイッチ)」。 見た目がキャッチーなので、つい配置したくなるコンポーネントですが、注意が必要です。 チェックボックスと同じく、on/off(有効/無効)を切り替えるものですが、トグルは基本的に情報入力フォームでは利用しません。 即時反映を期待させるUIであるため、「保存」や「登録」ボタンのある入力フォームの中に混ぜると、ユーザーが混乱します。(いつ反映されたか分からない) トグルは、主に設定画面や表示切り替えにて利用します。 事例編 最後に「会員登録」フォームを例に、各入力情報に対してどの入力形式が適しているか考えてみたいと思います。 性別 「ラジオボタン」が良いでしょう。 小さいスペースに収めたい場合は「セレクト」に。 生年月日 「生年月日」は、何年もさかのぼることが多く、記憶している数字列をそのまま入力するので、デイトピッカーではなく、セレクト、または、テキストフィールドが向いています。 テキストフィールドとする場合、「年」「月」「日」と分けたほうが親切ですが、業務システムなど、何度も利用する場合は「年月日」をまとめるほうがよいでしょう。 予定日、実施日、希望日など 生年月日と異なり、予約日やイベントの日付を指定する場合は、今日を起点として予定日が近いケースが多く、カレンダー形式で予定を想像しながら入力できるように「デイトピッカー」を利用して、テキスト入力とピッカー(選択)入力両方を可能とするのが良いでしょう。 ピッカーの場合、最短で2クリック(2タップ)で入力が完了する場合もあります。 都道府県 47都道府県から1つ選ぶ場合「セレクト」が良いでしょう。 工夫としては、「東北」「近畿」など、選択肢をグループに分けると項目を見つけやすくなります。(HTML標準では「optgroup」) また、選択肢は北から南に並んでおり、ユーザーは上から選択肢をだどるのが一般的ですが、デフォルトを愛知県と三重県の間にしておくと、南の地域へのスクロール量が減り、ユーザー全体を平均して効率化が期待できます。 上記は、あくまで基礎的な内容です。 想定するユーザーや、分野、情報量、画面上の流れなど、場合によって適したUIコンポーネントは変わります。 具体的なケースに合わせて、ユーザーが入力しやすい画面をつくりましょう。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post UI基礎:選択式入力の使い分け first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジーの遠藤です。 クラウドAPIを使わず、手元のノートPCでLLMを動かしたいと思ったことはありませんか? 2025年現在、軽量で高性能なモデルが続々と登場し、一般的なノートPCでも実用的なLLMを動かせるようになりました。本記事では、ノートPCで動作する主要なローカルLLMモデルを公式ソースをもとに徹底比較し、導入時の注意点まで解説します。 ローカルLLMとは?なぜ今注目されているのか ローカルLLMとは、クラウドサービスを介さず、自分のPC上で直接動作させる大規模言語モデルのことです。利用することで以下のようなメリットを得ることが出来ます。 プライバシー保護 : データが外部サーバーに送信されない オフライン利用 : インターネット接続なしで利用可能 コスト削減 : API利用料が発生しない カスタマイズ性 : ファインチューニングや独自の設定が可能 レイテンシ : ネットワーク遅延がない 必要なスペック目安 ローカルLLMを動かすために必要なスペックは、モデルサイズによって大きく異なります。 モデルサイズ 最低メモリ(RAM/VRAM) 推奨CPU/GPU 用途 1B〜2B 4GB 8世代以降のIntel Core / Apple M1 簡単なチャット、軽い要約 3B〜4B 8GB RTX 3060 / Apple M2 一般的な質問応答、コード補助 7B〜8B 16GB RTX 4060 / Apple M2 Pro 本格的な開発支援、文章生成 14B 32GB RTX 4090 / Apple M3 Max 高度な推論、複雑なタスク CPUのポイント : AVX2/AVX512命令セットに対応したCPU(Intel 第11世代以降、AMD Zen4以降)が推奨されます。これらの命令セットはLLMの行列演算を高速化します。 主要モデル比較一覧 2025年12月時点で、ノートPCで動作する主要なローカルLLMを公式情報をもとにまとめました。 軽量モデル(1B〜4B) モデル パラメータ コンテキスト長 ライセンス 特徴 Gemma 3 4B 4B 128K Gemma License マルチモーダル対応、140言語サポート Gemma 3n E4B 8B(実効4B) 32K Gemma License 超省メモリ設計、3GBで動作可能 Phi-3-mini 3.8B 4K/128K MIT 数学・推論に強い、合成データで学習 SmolLM2 1.7B – Apache 2.0 11兆トークンで学習、命令追従性が高い Qwen2.5 0.5B〜3B 131K Apache 2.0 29言語対応、構造化データに強い TinyLlama 1.1B 2K Apache 2.0 Llama 2互換、3兆トークンで学習 中規模モデル(7B〜8B) モデル パラメータ コンテキスト長 ライセンス 特徴 Llama 3.1 8B 8B 128K Llama License Meta製、幅広いエコシステム Qwen2.5 7B 7.6B 131K Apache 2.0 日本語含む29言語、JSON出力対応 Mistral 7B 7B 32K Apache 2.0 高効率、商用利用可能 DeepSeek-R1 8B 8B(蒸留版) 128K MIT 推論特化、RL学習 大規模モデル(14B〜24B) モデル パラメータ コンテキスト長 ライセンス 特徴 Phi-4 14B 16K MIT STEM・推論でトップクラス性能 Mistral Small 3.1 24B 128K Apache 2.0 RTX 4090単体で動作、マルチモーダル Qwen2.5 14B 14B 131K Apache 2.0 バランスの取れた性能 各モデル詳細解説 Gemma 3(Google DeepMind) Googleが開発した軽量オープンモデルで、Geminiと同じ技術基盤を持ちます。 公式スペック パラメータ: 1B / 4B / 12B / 27B コンテキスト長: 128Kトークン ライセンス: Gemma License(商用利用可、要規約確認) 主な特徴 マルチモーダル対応 : テキストと画像の両方を入力可能(4B以上) 140言語サポート : 日本語を含む多言語に対応 効率的なアーキテクチャ : ラップトップでのデプロイを想定した設計 ベンチマーク(4Bモデル) ベンチマーク スコア MMLU(5-shot) 59.6 GSM8K(8-shot) 38.4 HumanEval 36.0 参考 : Gemma公式サイト Gemma 3n(Google DeepMind) Google I/O 2025で発表された、モバイル・エッジデバイス向けに最適化された最新モデルです。 公式スペック(E4Bモデル) パラメータ: 8B(実効4B相当のメモリ使用量) コンテキスト長: 32Kトークン 最小メモリ: 約3GB ライセンス: Gemma License 主な特徴 超省メモリ設計 : Per-Layer Embeddings(PLE)技術により、8Bパラメータながら4B相当のメモリで動作 MatFormerアーキテクチャ : マトリョーシカ人形のように、大きなモデル内に小さなモデルを内包する設計 マルチモーダル対応 : テキスト、画像、動画、音声の入力に対応 140言語サポート : Gemma 3と同様の多言語対応 ベンチマーク LMArenaスコア: 1300以上(10B未満のモデルで初めて達成) 注目ポイント : ノートPCやモバイルデバイスで高性能なマルチモーダルLLMを動かしたい場合の最有力候補です。 参考 : Gemma 3n公式ドキュメント Phi-4 / Phi-3-mini(Microsoft Research) Microsoftが開発した、合成データを活用した推論特化モデルです。 公式スペック(Phi-4) パラメータ: 14B コンテキスト長: 16Kトークン 学習トークン: 9.8兆トークン ライセンス: MIT 公式スペック(Phi-3-mini) パラメータ: 3.8B コンテキスト長: 4K / 128K ライセンス: MIT 主な特徴 合成データ学習 : 高品質な合成データセットで学習し、教師モデルを上回る性能を実現 STEM特化 : 数学、コーディング、推論タスクで特に高い性能 軽量かつ高性能 : 同サイズのモデルと比較して優れたベンチマーク結果 ベンチマーク比較(Phi-4 vs Phi-3) ベンチマーク Phi-4(14B) Phi-3(14B) MMLU 84.8 77.9 GPQA 56.1 31.2 MATH 80.4 44.6 HumanEval 82.6 67.8 注意点 : 英語中心の学習のため、他言語での性能は低下する可能性があります。 参考 : Phi-4 Technical Report Qwen2.5(Alibaba) Alibabaが開発した多言語対応モデルで、日本語を含む29言語をサポートします。 公式スペック(7Bモデル) パラメータ: 7.61B(非埋め込み層: 6.53B) コンテキスト長: 131,072トークン 出力長: 最大8,000トークン ライセンス: Apache 2.0 主な特徴 29言語対応 : 日本語、中国語、韓国語、英語など幅広くサポート 構造化データ対応 : テーブルやJSONの理解・生成に強い 長文生成 : 8,000トークン以上の長文出力に対応 GQA採用 : Grouped Query Attentionによる効率的な推論 対応言語(一部) 日本語、中国語、英語、韓国語、ベトナム語、タイ語、アラビア語、フランス語、スペイン語、ドイツ語、ロシア語など 参考 : Qwen2.5公式ブログ Llama 3.1(Meta) Metaが開発したオープンソースLLMで、最も広いエコシステムを持ちます。 公式スペック パラメータ: 8B / 70B / 405B コンテキスト長: 128Kトークン ライセンス: Llama License(商用利用可、要規約確認) 主な特徴 幅広いエコシステム : 多くのツールやライブラリが対応 事前学習/指示調整版 : 用途に応じて選択可能 活発なコミュニティ : ファインチューニング済みモデルが豊富 参考 : Llama公式サイト DeepSeek-R1(DeepSeek) 強化学習(RL)を活用した推論特化モデルで、蒸留版が軽量環境で利用可能です。 公式スペック パラメータ: 671B(フル版)、蒸留版: 1.5B〜70B コンテキスト長: 128Kトークン ライセンス: MIT 蒸留モデルラインナップ ベースモデル サイズ Qwen系 1.5B / 7B / 14B / 32B Llama系 8B / 70B 主な特徴 純粋なRL学習 : 教師なし微調整なしで推論能力を獲得 高い推論性能 : 32B蒸留版がOpenAI o1-miniを一部ベンチマークで上回る オープンライセンス : MITライセンスで商用利用可能 参考 : DeepSeek-R1 GitHub SmolLM2(Hugging Face) Hugging Faceが開発した超軽量モデルで、オンデバイス実行に最適化されています。 公式スペック パラメータ: 135M / 360M / 1.7B 学習トークン: 11兆トークン ライセンス: Apache 2.0 主な特徴 超軽量 : 135Mから利用可能で、リソースが限られた環境に最適 11兆トークン学習 : 小型ながら豊富なデータで学習 関数呼び出し対応 : ツール使用のワークフローに対応 ベンチマーク(1.7B Instructモデル) ベンチマーク SmolLM2-1.7B Llama-1B Qwen2.5-1.5B IFEval 56.7 53.5 47.4 GSM8K 48.2 26.8 42.8 参考 : SmolLM2 Hugging Face Mistral Small 3.1(Mistral AI) Mistral AIが開発した中規模モデルで、RTX 4090単体で動作可能です。 公式スペック パラメータ: 24B コンテキスト長: 128Kトークン 推論速度: 150トークン/秒 ライセンス: Apache 2.0 主な特徴 RTX 4090で動作 : 32GB RAM搭載Macでも動作可能 マルチモーダル : テキストと画像の入力に対応 高い汎用性能 : GPT-4o Miniを上回るベンチマーク結果 ベンチマーク ベンチマーク Mistral Small 3.1 Gemma 3 27B GPQA Diamond 45.96% 42.4% HumanEval 88.41% – DocVQA 94.08% – 参考 : Mistral Small 3.1公式 日本語性能について 日本語タスクでの性能は、モデルによって大きく異なります。以下は日本語対応状況の目安です。 モデル 日本語対応 備考 Qwen2.5 ◎ 公式で日本語サポート、29言語対応 Gemma 3 / 3n ○ 140言語対応、日本語も実用レベル Llama 3.1 △ 英語中心、日本語は限定的 Phi-4 / Phi-3 △ 英語中心の学習、他言語は性能低下 Mistral △ 多言語対応だが英語が最も得意 DeepSeek-R1 ○ 中国語・英語中心だが日本語も対応 日本語を重視する場合 : Qwen2.5やGemma 3がおすすめです。 実行環境の選択 ローカルLLMを動かすための主要なツールを紹介します。 Ollama 最も簡単にローカルLLMを動かせるツールです。 # インストール(macOS/Linux) curl -fsSL https://ollama.com/install.sh | sh # モデルのダウンロードと実行 ollama pull gemma3:4b ollama run gemma3:4b 特徴 ワンコマンドでモデル実行 REST API対応 幅広いモデルサポート 参考 : Ollama公式 LM Studio GUIで操作できるデスクトップアプリケーションです。 特徴 直感的なUI Hugging Faceから直接モデルダウンロード チャットインターフェース付属 llama.cpp 最も軽量で柔軟性の高い実行環境です。 特徴 90MB未満の軽量実装 Vulkanサポート CPU最適化 導入時の注意点 ローカルLLMを導入する際に注意すべきポイントをまとめます。 1. メモリ使用量の確認 量子化レベルによってメモリ使用量が大きく変わります。 量子化 メモリ削減率 品質への影響 FP16(なし) – なし Q8 約50% ほぼなし Q4_K_M 約75% 軽微 Q4_0 約75% やや影響あり 推奨 : メモリが限られる場合は Q4_K_M がバランスが良いです。 2. 初回起動時の遅延 モデルの初回ロードには数十秒〜数分かかる場合があります。これはモデルをメモリに展開する時間です。一度ロードすれば、以降の応答は高速になります。 3. 発熱と消費電力 LLM推論はCPU/GPUに高負荷をかけるため、ノートPCでは以下に注意してください。 冷却 : 長時間使用時は冷却パッドの使用を推奨 電源 : バッテリー駆動では性能が制限される場合あり サーマルスロットリング : 高温時に自動的に性能が低下 4. ストレージ容量 モデルファイルは数GB〜数十GBのサイズがあります。 モデルサイズ 目安容量(Q4量子化) 1B〜2B 1〜2GB 7B〜8B 4〜6GB 14B 8〜10GB 70B 40〜50GB 5. 生成内容の信頼性 ローカルLLMも、クラウドLLMと同様に以下の制限があります。 ハルシネーション : 事実と異なる情報を生成する可能性 知識のカットオフ : 学習データ以降の情報は持っていない バイアス : 学習データに含まれるバイアスを反映 重要 : 生成された内容は必ず検証してから使用してください。 6. ライセンスの確認 モデルによってライセンスが異なります。商用利用を検討する場合は必ず確認してください。 ライセンス 商用利用 代表的なモデル MIT ○ Phi-4, DeepSeek-R1 Apache 2.0 ○ Qwen2.5, Mistral, SmolLM2 Llama License △(条件付き) Llama 3.1 Gemma License △(条件付き) Gemma 3 用途別おすすめモデル 最後に、用途別のおすすめモデルをまとめます。 用途 おすすめモデル 理由 とにかく軽く動かしたい SmolLM2 1.7B 超軽量、4GBメモリで動作 日本語チャット Qwen2.5 7B 公式日本語サポート コード補助 Phi-4 / DeepSeek-R1 STEM・コード特化 画像も扱いたい Gemma 3 4B / Gemma 3n マルチモーダル対応 省メモリでマルチモーダル Gemma 3n E4B 3GBメモリで画像・音声対応 バランス重視 Llama 3.1 8B エコシステムが充実 最高性能(ノートPC限界) Mistral Small 3.1 RTX 4090で動作、高性能 まとめ 2025年12月時点で、ノートPCでも実用的に使えるローカルLLMが多数存在します。 選び方のポイント スペック確認 : 自分のPCのメモリ・GPUに合ったモデルサイズを選ぶ 用途の明確化 : コード、日本語、マルチモーダルなど、目的に合ったモデルを選ぶ ライセンス確認 : 商用利用する場合は必ずライセンスを確認 量子化の活用 : メモリが足りない場合は量子化版を検討 まずはOllamaをインストールして、Gemma 3 4BやQwen2.5 7Bあたりから試してみることをおすすめします。 参考リンク Ollama公式 Gemma公式 Gemma 3n公式ドキュメント Phi-4 Technical Report Qwen2.5公式 Llama公式 DeepSeek-R1 GitHub Mistral Small 3.1 SmolLM2 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post ノートPCで動くローカルLLM完全ガイド【2025年12月版】 first appeared on SIOS Tech Lab .
こんにちは、サイオステクノロジーの遠藤です。本ブログはSIOS Tech Labアドベントカレンダー18日目の投稿です。 今回は、RAG評価フレームワークである Ragas の最新バージョン v0.4.0 について紹介します。 さらに、GPT-5の reasoning_effort パラメータを使って、 minimal(高速モード)とhigh(推論重視モード) での評価結果の違いも検証しました。 Ragasのv0.4.0で何が変わったの? Azure OpenAI ServiceのGPT-5でRagasを使いたい! GPT-5のreasoning_effortで評価結果はどう変わるの? といった方は、ぜひ最後までご覧ください! はじめに Ragasって何?という方は以下の記事をご覧ください! 【初心者向け】RAG評価フレームワーク Ragasを必要最低限で使ってみる 今回のv0.4.0では、特に 最新のOpenAIモデル対応 が目玉となっています。 手元の環境では ragas==0.4.0 のバージョンを利用して検証を行っています。 v0.4.0の主な変更点 公式のリリースノート によると、v0.4.0では以下の変更が加えられました。 GPT-5/o-seriesモデルのサポート 今回の目玉となる変更点です。 GPT-5およびo-seriesモデル が正式にサポートされ、 temperature や top_p パラメータの制約が自動的にハンドリングされるようになりました。 これまでo1-previewなどのモデルを使おうとすると、パラメータの制約によりエラーが発生することがありましたが、v0.4.0ではこれが解消されています。 注意点 : GPT-5は temperature=1 のみをサポートしています。Ragasは内部でtemperatureを変更しようとするため、 bypass_temperature=True オプションを使用してこれを防ぐ必要があります。 from langchain_openai import AzureChatOpenAI from ragas.llms import LangchainLLMWrapper llm = AzureChatOpenAI( azure_deployment="gpt-5-deploy", temperature=1, # GPT-5は1のみサポート model_kwargs={"reasoning_effort": "high"}, ) # bypass_temperature=TrueでRagasによるtemperature上書きを防ぐ evaluator_llm = LangchainLLMWrapper(llm, bypass_temperature=True) 統一されたLLMプロバイダーサポート instructor.from_provider への移行により、複数のLLMプロバイダーとの互換性が向上しました。 これにより、Azure OpenAI、OpenAI、その他のプロバイダーを統一的に扱えるようになっています。 モジュラーなメトリクスアーキテクチャ 評価メトリクスの多くが標準化された BasePrompt パターンを使用するようにリファクタリングされました。 これにより、メトリクスの拡張性が向上し、カスタムメトリクスの作成がより簡単になっています。 GPT-5のreasoning_effortについて GPT-5では reasoning_effort パラメータにより、推論の深さを制御できます。 パラメータ値 説明 minimal 最小限の推論。高速だが精度は低い可能性 low 軽量な推論 medium バランスの取れた推論(デフォルト) high 深い推論。時間はかかるが精度が高い可能性 今回の検証では、 minimal と high の2つのモードで評価結果を比較してみました。 事前準備 Azure OpenAI Serviceの設定 今回はAzure AI Foundry経由でデプロイしたGPT-5を利用します。 以下のモデルをデプロイしておいてください。 モデル 用途 GPT-5 評価用のChatモデル text-embedding-3-small Embeddingsモデル パッケージのインストール 必要なパッケージをインストールします。 pip install ragas==0.4.0 langchain-openai python-dotenv 実装 それでは、実際にGPT-5を使ってRagasで評価を行っていきます。 環境変数の設定 まずは環境変数を設定します。 import os from dotenv import load_dotenv load_dotenv() AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY") GPT5_API_VERSION = os.getenv("GPT5_API_VERSION") GPT5_DEPLOYMENT = os.getenv("GPT5_DEPLOYMENT") EMBEDDINGS_API_VERSION = os.getenv("EMBEDDINGS_API_VERSION") EMBEDDINGS_DEPLOYMENT = os.getenv("EMBEDDINGS_DEPLOYMENT") .envファイルには以下の内容を記載しておきます。 AZURE_OPENAI_ENDPOINT=https://{resource-name}.openai.azure.com/ AZURE_OPENAI_API_KEY=your-api-key GPT5_API_VERSION=2025-01-01-preview GPT5_DEPLOYMENT=gpt-5-deploy EMBEDDINGS_API_VERSION=2023-05-15 EMBEDDINGS_DEPLOYMENT=text-embedding-3-small-deploy LLMとEmbeddingsの設定 GPT-5を評価用のLLMとして設定します。 ポイントは以下の2点です: temperature=1 を設定(GPT-5は1のみサポート) bypass_temperature=True でRagasによる上書きを防ぐ reasoning_effort で推論の深さを制御 from langchain_openai import AzureChatOpenAI, AzureOpenAIEmbeddings from ragas.llms import LangchainLLMWrapper from ragas.embeddings import LangchainEmbeddingsWrapper def create_llm(reasoning_effort: str) -> LangchainLLMWrapper: """指定したreasoning_effortでLLMを作成""" llm = AzureChatOpenAI( azure_endpoint=AZURE_OPENAI_ENDPOINT, api_key=AZURE_OPENAI_API_KEY, api_version=GPT5_API_VERSION, azure_deployment=GPT5_DEPLOYMENT, temperature=1, # GPT-5は1のみサポート model_kwargs={"reasoning_effort": reasoning_effort}, ) # bypass_temperature=Trueでtemperatureの上書きを防ぐ return LangchainLLMWrapper(llm, bypass_temperature=True) def create_embeddings() -> LangchainEmbeddingsWrapper: """Embeddingsモデルを作成""" embeddings = AzureOpenAIEmbeddings( azure_endpoint=AZURE_OPENAI_ENDPOINT, api_key=AZURE_OPENAI_API_KEY, api_version=EMBEDDINGS_API_VERSION, azure_deployment=EMBEDDINGS_DEPLOYMENT, ) return LangchainEmbeddingsWrapper(embeddings) 評価データの準備 評価用のデータセットを準備します。 v0.2以降と同様に、 EvaluationDataset と SingleTurnSample を使用します。 今回は、RAGシステムの評価を想定して 就業規則PDF から10件の質問を選定しました。 各質問には正解( reference )、検索されたコンテキスト( retrieved_contexts )、RAGの回答( response )を設定しています。 from ragas import EvaluationDataset, SingleTurnSample # 就業規則から抽出したコンテキスト CONTEXTS = { "試用期間": [ "(試用期間)第6条 労働者として新たに採用した者については、採用した日から3か月間を試用期間とする。", "前項について、会社が特に認めたときは、試用期間を短縮し、又は設けないことがある。", "試用期間は、勤続年数に通算する。" ], "労働時間": [ "(労働時間及び休憩時間)第18条 1週間の所定労働時間は、2025年1月1日を起算日として、2週間ごとに平均して、1週間当たり40時間とする。", "1日の所定労働時間は、7時間30分とする。", "始業・終業の時刻及び休憩時間は、次のとおりとする。始業 午前9時00分、終業 午後17時30分、休憩時間 12時00分から13時00分まで" ], "家族手当": [ "(家族手当)第33条 家族手当は、次の家族を扶養している労働者に対し支給する。", "18歳未満の子 1人につき 月額 40000円", "65歳以上の父母 1人につき 月額 50000円" ], # ... 他のカテゴリも同様に定義 } # RAGの回答をシミュレート(一部に余計な情報を含める) SIMULATED_RESPONSES = { "試用期間は何か月ですか?": "試用期間は採用した日から3か月間です。試用期間は勤続年数に通算されます。", "18歳未満の子を扶養している場合の家族手当はいくらですか?": "18歳未満の子を扶養している場合、1人につき月額40,000円の家族手当が支給されます。なお、最近の物価上昇を受けて今後増額される可能性があります。", # ← コンテキストにない情報を追加 # ... } # 評価サンプルの作成 samples = [] selected_questions = [ {"question": "試用期間は何か月ですか?", "ground_truth": "試用期間は採用した日から3か月間です。", "context_key": "試用期間"}, {"question": "1日の所定労働時間は何時間ですか?", "ground_truth": "1日の所定労働時間は7時間30分です。", "context_key": "労働時間"}, {"question": "18歳未満の子を扶養している場合の家族手当はいくらですか?", "ground_truth": "18歳未満の子を扶養している場合、1人につき月額40,000円の家族手当が支給されます。", "context_key": "家族手当"}, # ... 合計10件 ] for q in selected_questions: sample = SingleTurnSample( user_input=q["question"], retrieved_contexts=CONTEXTS[q["context_key"]], response=SIMULATED_RESPONSES[q["question"]], reference=q["ground_truth"], ) samples.append(sample) eval_dataset = EvaluationDataset(samples=samples) ポイント : 一部の回答には意図的に「コンテキストにない情報」を含めています(例:「今後増額される可能性があります」)。 これにより、 faithfulness (忠実性)メトリクスが正しく機能しているかを確認できます。 評価の実行 メトリクスを設定し、評価を実行します。 import time from ragas import evaluate from ragas.metrics import ( Faithfulness, ResponseRelevancy, ContextPrecision, ContextRecall ) def run_evaluation(reasoning_effort: str, dataset, embeddings): """指定したreasoning_effortで評価を実行""" # LLMの作成 evaluator_llm = create_llm(reasoning_effort) # メトリクスの設定 metrics = [ Faithfulness(llm=evaluator_llm), ResponseRelevancy(llm=evaluator_llm, embeddings=embeddings), ContextPrecision(llm=evaluator_llm), ContextRecall(llm=evaluator_llm) ] # 評価の実行(時間計測) start_time = time.time() results = evaluate(dataset=dataset, metrics=metrics) elapsed_time = time.time() - start_time return results.to_pandas(), elapsed_time # 共通のEmbeddingsを作成 embeddings = create_embeddings() # minimal(高速モード)で評価 df_minimal, time_minimal = run_evaluation("minimal", eval_dataset, embeddings) # high(推論重視モード)で評価 df_high, time_high = run_evaluation("high", eval_dataset, embeddings) 結果と考察 minimal vs high 比較結果 実際にGPT-5の reasoning_effort を変えて、就業規則データセット(10件)で評価を行った結果を紹介します。 処理時間の比較 モード 処理時間 minimal 76.37秒 high 94.83秒 差分 +18.46秒(24.2%増) 評価スコアの比較 メトリクス minimal high 差分 faithfulness 0.8500 0.8833 +0.0333 answer_relevancy 0.8290 0.8386 +0.0095 context_precision 0.6917 0.6917 ±0.0000 context_recall 1.0000 1.0000 ±0.0000 考察 1. faithfulness(忠実性)に差が出た high モードでは faithfulness のスコアが 0.85から0.88に向上 しました。 今回のテストデータでは、一部の回答に意図的に「コンテキストにない情報」を含めています。 例えば、家族手当の質問に対する回答に「最近の物価上昇を受けて今後増額される可能性があります」という推測を追加しました。 high モードはこのような不正確な情報をより厳密に検出できる傾向が見られました。 2. context_recallは両モードで完璧 context_recall は両モードとも1.0000でした。 これは、正解(ground_truth)に含まれる情報がすべてコンテキストから取得できていることを意味します。 就業規則という明確なソースがあるため、このような結果になったと考えられます。 3. 処理時間とのトレードオフ high モードは処理時間が 約24%増加 しますが、 faithfulness のスコアが向上する傾向が見られました。 用途 推奨モード 開発・デバッグ時の簡易評価 minimal 本番環境のRAG品質評価 high 大量データの評価 minimal + サンプリング まとめ 今回はRagas v0.4.0の主な変更点と、Azure OpenAI ServiceのGPT-5を使った評価方法を紹介しました。 ポイントとしては: GPT-5/o-seriesモデルのサポート により、最新のLLMを使った評価が可能に GPT-5は temperature=1のみ サポートのため、 bypass_temperature=True が必要 reasoning_effort パラメータで推論の深さを制御できる high モードは処理時間が約1.2倍だが、faithfulnessのスコアが向上する傾向(0.85→0.88) 用途に応じて minimal と high を使い分けるのがおすすめ 今後もRagasの進化に注目していきたいと思います。 ではまた! 参考 Ragas公式ドキュメント Ragas GitHub リリースノート 【初心者向け】RAG評価フレームワーク Ragasを必要最低限で使ってみる ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【v0.4】Ragasの最新アップデート情報とAzure OpenAI GPT-5での評価方法【2025年12月】 first appeared on SIOS Tech Lab .
「Azure」上に、Elasticsearch、Kibana、Fleet Serverを含むElastic Stackの検証環境を「Self-Managed」方式で構築するための詳細な手順書(ホワイトペーパー: Azure上へのElastic検証環境構築手順書(Self-Managed版) )を公開しました。 ホワイトペーパーのダウンロードはこちらから なぜSelf-Managedなのか? Elasticの環境をAzure上に構築する方法には、主に4つの選択肢があります。Elastic Cloud HostedやServerlessといったマネージドサービスを利用すれば、手軽に環境を用意できます。しかし、本手順書が取り上げるSelf-Managedは、Azure上に仮想マシン(VM)を作成し、その上に自前でElasticsearchなどをインストールする方式です 。 この方式の最大のメリットは、 「細かい設定が可能」 という点です 。特定のセキュリティ要件や複雑なネットワーク構成、またはカスタマイズされた運用ポリシーを検証したいエンジニアにとって、この自由度は大きな魅力となります。 本手順書で実現できること 本書は、VMの作成から始まり、Elastic Stackの主要コンポーネントのインストールと設定、さらには運用に欠かせない機能の構築までを網羅しています。 システム・ネットワーク構成の定義: Elasticsearchノード(es01-vm, es02-vm, es03-vm)3台、Kibana(kibana-vm)、Fleet Server(fleet-server-vm)1台ずつの計5台のVM構成と、プライベートIPアドレス中心のネットワーク構成を明確に示しています 。 VM作成からElasticsearchインストール: Azure PortalでのVM作成手順(リソースグループ、ネットワーク、セキュリティ設定など)を画面キャプチャ付きで詳細に解説 。Elasticsearchのコアな設定ファイルである elasticsearch.yml の編集内容についても、クラスター構成( sios-dx1 )やSSL/TLSセキュリティ設定を含めて具体的な記述例を示しています 。 セキュリティの確保: ElasticsearchとKibana間の通信暗号化のために、自己認証局(CA)を用いた証明書と秘密キーの生成手順を詳しく説明 。また、Bastion経由での安全な接続設定やNetwork Security Group(NSG)のポートルール設定(Elasticsearch: 9200-9300、Fleet Server: 8220、Kibana Nginx Proxy: 443/80)も網羅しています 。 KibanaとNginxの設定: Kibanaのインストール後、外部Webブラウザからのアクセスを可能にするため、Nginxをリバースプロキシとして導入し、HTTPS通信(443ポート)を内部のKibana(5601ポート)へルーティングする設定を解説しています 。さらに、Let’s EncryptのCertbotを使ったサーバー証明書の発行手順も収録 。 運用に役立つ機能の構築: Snapshot(バックアップ): Azure Blob Storageを利用したスナップショットリポジトリの作成方法、アクセスキーの設定、および定期的なスナップショット作成のためのポリシー設定(ILM)手順を詳しく解説 。 Fleet Serverのインストール: Elastic Agentの一元管理に不可欠なFleet Serverの導入手順を、Kibana画面からの設定とVM上でのコマンド実行の両面から手順化しています 。 アップグレード検証: v9.2.0で構築した環境を、最新版のv9.2.1へアップグレードする手順を、ノードタイプごとの推奨順序に従って解説しており、ダウンタイムを最小限に抑えるためのノウハウも含まれています 。 VMの構築から、きめ細かなElasticsearchの設定、Kibanaのセキュリティ強化、そして無停止アップグレードのノウハウまで、詳細な手順をこの一冊に凝縮しました 。Elastic Stackのセルフマネージド環境の構築・運用標準化に向けた詳細なテクニカル資料として、ぜひダウンロードし、貴社の検証プロジェクトにお役立てください。 ホワイトペーパーのダウンロードはこちらから 併せてチェック :実践的な技術情報を発信中「Elastic Portal ブログ」 「Elastic Portal」内の技術ブログでは、今回ご紹介した検証環境の構築にとどまらず、Elastic Stackを最大限に活用するための最新情報を随時公開しております。ぜひご覧ください。ブログは こちら から。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Azure上にセルフマネージドなElastic検証環境を構築!詳細な手順書を公開 first appeared on SIOS Tech Lab .
こんにちは。サイオステクノロジーの和田です。アドベントカレンダー17日目です。今回はアウトボックスパターンという設計パターンを学んだので、紹介したいと思います。まず初めにアウトボックスパターンの説明をしてから、その設計を使った具体的なサービス例、そして冪等性の考慮について書いていきます。それではいきましょう。 アウトボックスパターンとは アウトボックスパターンとはマイクロサービスアーキテクチャにおいて、データベースの更新と「イベント発行(メッセージ送信要求)」を矛盾なく行うための設計パターンです。マイクロサービスでは、あるサービスが自身のデータベースを更新し、その変更をイベントとして他のサービスに通知するという処理がよくあります。 アウトボックスパターンでは、送信したいイベント(= メッセージの内容)を Outbox テーブル に書き込み、ユーザーデータの更新と同一トランザクションでコミットします。実際のメッセージブローカーへの送信は後段のリレーが行うため、「DB に書けたのにイベントが失われる / DB に書けていないのにイベントだけ流れる」といった不整合を避けられます。 例:ユーザー登録サービス ここでは具体例として、Web サービスの 新規ユーザー登録 を例に考えます。 ユーザーが登録ボタンを押した際に、システムでは以下の 2 つを行う必要があります。 「ユーザーデータベース」にユーザー情報(ID、メールアドレス、パスワード等)を書き込む。 「メール配信サービス」に対して、「ユーザー登録が完了した」というメッセージをメッセージブローカーに送信する(登録完了メールや認証メールを送るため)。 この 2 つの処理を、アプリケーションが順番に実行しようとすると、以下のような問題が発生する可能性があります。 ケース 1:DB 書き込み成功、メッセージ送信失敗 ユーザーデータベースへの保存は成功したが、その直後にメッセージブローカーがダウンしてしまった。 結果 ユーザーのアカウントは作成されたが、登録完了メール(または認証メール)が届かない。ユーザーはログインの手順を完了できず、サービスを利用開始できない。 ケース 2:DB 書き込み失敗、メッセージ送信成功 何らかの理由(メールアドレスの重複など)でデータベースへの書き込みが失敗(ロールバック)したが、そのあとにメッセージだけ送信されてしまった。 結果 データベースにユーザーは存在しない(登録できていない)のに、「登録ありがとうございます」というメールだけがユーザーに届いてしまう。ユーザーがメール内のリンクをクリックしても、アカウントが存在しないためエラーとなる。 このように、どちらのケースの場合でも結果に不整合が生じてしまいます。 解決策:アウトボックスパターン このような問題に対する解決策として使えるのがアウトボックスパターンです。アウトボックスパターンでは「外部への送信そのもの」ではなく「送信したい内容を Outbox に永続化するところまで」を、データベースへの書き込みと同じトランザクションで処理することで問題を解決します。具体的には以下の流れで実現します。 1. ユーザー登録サービスの処理 ユーザー登録サービスではユーザーテーブルへの書き込みと同時に、送信したいメッセージ(メール送信依頼)を Outbox テーブル と呼ばれる特別なテーブルに書き込みます。 ここで重要なのは、ユーザーテーブルへの書き込みと Outbox テーブルへの書き込みを単一のデータベーストランザクションで実行することです。 トランザクションの結果 トランザクションが成功した場合 ユーザーデータと送信すべきメッセージの両方がデータベースにコミットされます。 トランザクションが失敗した場合 何らかのエラー(DB エラーやバリデーションエラーなど)があれば、両方の書き込みがロールバックされます。 この結果、データベースへのユーザー登録が成功したならば、送信すべきメッセージも必ず DB 内に保存されている状態が保証され、データの整合性を担保することができます。 一方で、メッセージブローカーへの反映はリレーのタイミングに依存するため、通知は(多くの場合)最終的整合になります。つまり、登録完了直後に必ずメール送信が開始されるとは限らず、多少の遅延が起こり得ます。 2. メッセージのリレー 実際の送信ではメッセージリレーと呼ばれるアプリケーションとは別の独立したプロセスがメッセージの送信を行います。 このリレーが Outbox テーブルを定期的に監視(ポーリング)し、未送信のメッセージを見つけた場合はメッセージブローカーへ送信します。送信が完了したら Outbox テーブルの該当のレコードを削除(または送信済みステータスに変更)します。 監視の方法はポーリング以外にも、CDC(Change Data Capture)などで変更を検知する構成もあります。 3. メールの配信 最後にメールを送信するプロセスがメッセージブローカーからメッセージを受け取り、実際にメールの送信を行います。 冪等性の考慮 アウトボックスパターンを実装する際には、 冪等性 についても考慮する必要があります。冪等性とは、 同じ操作を何度実行しても、結果が 1 回実行した場合と同じになる性質 のことです。 メッセージの重複送信の可能性 メッセージリレーが Outbox テーブルからメッセージを読み取り、メッセージブローカーに送信した後、送信済みのマークを付ける前にクラッシュした場合、同じメッセージが再度送信される可能性があります。 例えば、ユーザー登録のケースでは以下のような問題が発生する可能性があります メッセージリレーがメッセージブローカーにメール送信依頼を送信 メッセージブローカーへの送信は成功 Outbox テーブルの更新前にメッセージリレーがクラッシュ リレーが再起動し、同じメッセージを再送信 ユーザーに登録完了メールが 2 通届いてしまう 今回の例のようなメール送信の場合は、2 通届いても致命的ではないケースもあります。しかし、他のストレージにデータを保存する処理などを行う場合は冪等性を確実に実装する必要があります。 典型的な対策としては、各メッセージに一意な eventId (メッセージ ID)を付与し、受信側で「処理済み eventId」を保存して重複を検知・無視する方法があります。 まとめ 今回はアウトボックスパターンという設計パターンについて紹介しました。アウトボックスパターンを使うことで、マイクロサービス間でのデータの整合性を担保しやすくなり、「データは保存されたのに通知が届かない」といった事象を避けられます。その結果、より堅牢なシステムを構築できます。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post アウトボックスパターンとはなにか first appeared on SIOS Tech Lab .