Madogiwa Blog

主に技術系の学習メモに使っていきます。

RubyでAIエージェントを作るgem Rixieを作った💎

最近個人開発でRixieというAIエージェントオーケストレーション用のRuby gemを作ってgemとして公開しました。

github.com

Rixieを作った動機はシンプルで、 PythonのLangChainやLlamaIndexのようなエージェントフレームワークをRubyで使いたかったのですが、 既存の選択肢はRailsに依存していたり、機能が限定的だったりと自分のユースケースにフィットするものなかったのと、 単純に自分でAIエージェントの中身の理解のためにAIエージェントフレームワークを作ってみたかったので今回作ってみました。

Railsに依存しないスタンドアロンなgemとして、 セッション管理・ツール実行・複数のエージェント戦略・MCPサポートを一通り揃えています。

動作確認環境: Ruby 3.4 / rixie 0.1.0

本題

概念階層:Session → Task → Run → think

Rixieの実行モデルは4層の階層で整理されています。

Session          # 会話全体を管理。複数のchatをまたいで履歴を保持
└── Task         # 1つのゴールを担当。Strategyを持つ
    └── Run × N  # Agent#thinkを1回呼び出す単位
        └── think (llm_call × N)  # LLMを呼び、ツールを実行し、ループする

Session#chatを呼ぶたびに1つのTaskが生成され、TaskはStrategyに従ってRunを何回実行するかを決めます。RunはAgentにthinkを依頼し、Agentはツール呼び出しが完了するまでLLMとのループを回します。

セットアップと基本的な使い方

以下の通り、gem "openai"や機能によっては追加で必要なライブラリが必要なことはありますが、 基本的にはRubyの標準機能の範囲内で動くように作っています。

# Gemfile
gem "rixie"
gem "openai"     # OpenAI / OpenAI互換エンドポイントを使う場合
gem "nokogiri"   # Fetch・WebSearchツールを使う場合
gem "cli-ui"     # CLIを使う場合

実際に使うときは以下のような感じで設定し、Rixie::Sessionを生成してRixie::Session#chatでLLMを呼び出します。

require "rixie"

Rixie.configure do |config|
  config.default_provider = "openai"
  config.default_model    = "gpt-4.1-mini"
end

session = Rixie::Session.new(instructions: "You are a helpful assistant.")
puts session.chat("Rubyの良いところを教えてください")
# => "Rubyの良いところは..."

OpenAI互換エンドポイント対応

BuiltinのプロバイダはOpenAIですが、register_providerでGitHub ModelsやOllamaなどのOpenAI互換エンドポイントを登録できます。

Rixie.configure do |config|
  # GitHub Models
  config.register_provider("github",
    adapter:  :openai,
    base_url: "https://models.github.ai/inference",
    api_key:  ENV["GITHUB_TOKEN"]
  )

  # Ollama(ローカル)
  config.register_provider("ollama",
    adapter:  :openai,
    base_url: "http://localhost:11434/v1",
    api_key:  "ollama"
  )
end

# GitHubModelsを使う場合
session = Rixie::Session.new(
  instructions: "You are a helpful assistant.",
  provider: "github",
  model: "openai/gpt-4.1-mini"
)

Strategy(エージェント戦略)の切り替え

Session#chatstrategy:引数で実行戦略を切り替えられます。

# Simple(デフォルト): 1つのAgentループで完結するタスク向け
session.chat("今日の東京の天気は?")

# PlanExecute: まず計画を立て、各ステップを順番に実行する複雑なタスク向け
session.chat(
  "Ruby 3.xの新機能を調査してレポートにまとめて",
  strategy: Rixie::Strategy::PlanExecute.new
)

# ReAct: Thought → Action → Observationのサイクルを明示するデバッグ・ステップ確認向け
session.chat(
  "東京の人口を調べて2倍にした数を教えて",
  strategy: Rixie::Strategy::ReAct.new
)

デフォルトでは上記の3つを用意していますが、run(task:, listener:) を実装したクラスを渡すだけで独自のストラテジーを作れます。Run を複数作って task.runs に積み、run.execute(listener:) を呼ぶだけです。

以下は「同じ入力を2つのエージェントに投げて最初に返った方を採用する」例です(シンプルな実装例として直列で書いています)。

class BestOfTwoStrategy
  def run(task:, listener:)
    candidates = 2.times.map do
      run = Rixie::Run.new(
        user_input: task.user_input,
        agent:      task.agent,
        context:    task.context
      )
      task.runs << run
      run.execute(listener:)
      run.output
    end

    # 長い方を「より詳細な回答」として採用する例
    candidates.max_by(&:length)
  end
end

session.chat("Rubyの魅力を教えて", strategy: BestOfTwoStrategy.new)

組み込みツール群

以下のような組み込みツールを用意してるのでWebサーチのようなRAG的な機能や、HILのような機能も実装することができます。

ツール 説明
Tool::WebSearch DuckDuckGo Liteで検索(nokogiriが必要)
Tool::Fetch URLを取得してテキスト抽出(SSRF対策あり)
Tool::WikipediaSearch Wikipedia検索(言語指定可)
Tool::Calculator 四則演算・べき乗など。手書きパーサでevalを使わない実装
Tool::CurrentTime 現在時刻をISO 8601形式で返す
Tool::FileRead ファイル読み込み(root_dirサンドボックス内のみ)
Tool::FileList globパターンでファイル一覧を取得
Tool::FileSearch regexでファイル横断grep
Tool::HumanInput エージェントからユーザーへの質問(Human-in-the-loop)
session = Rixie::Session.new(
  instructions: "You are a research assistant.",
  tools: [
    Rixie::Tool::WebSearch,
    Rixie::Tool::Calculator,
    Rixie::Tool::FileRead.with(root_dir: "/path/to/project")
  ]
)

MCPサポート

HTTPベースだけですがMCPもサポートしています。

require "rixie/mcp"

mcp = Rixie::MCP::Http::Client.new(url: "http://localhost:8000/mcp")

session = Rixie::Session.new(
  instructions: "You are a helpful assistant.",
  tools: mcp.tools + [Rixie::Tool::Calculator]
)

ストリーミング

Session#liveでトークンをリアルタイムに受け取れます。返り値はEnumeratorで、パターンマッチでイベントを振り分けます。

session.live("最新のRubyリリースを調べて").each do |envelope|
  case envelope.event
  in Rixie::Event::Token[delta:]
    print delta; $stdout.flush
  in Rixie::Event::ToolCallStart[tool_call:]
    puts "\n[#{tool_call.name} を呼び出し中...]"
  in Rixie::Event::Finished[content:]
    puts "\n#{content}"
  else
  end
end

マルチエージェントオーケストレーション

SessionRixie::Toolとしてラップすることで、エージェントを組み合わせ、コンテキスト節約や専門的なエージェントに作業を委譲させたりといったマルチエージェントオーケストレーションを構築できます。 (ツールベースの非決定論的なマルチエージェントオーケストレーションではなく決定論的なワークフローのようなものを組みたい場合には前述のStrategyを利用することで実現できます。)

research_agent = Rixie::Session.new(instructions: "You are a research specialist.")
write_agent    = Rixie::Session.new(instructions: "You are a technical writer.")

orchestrator = Rixie::Session.new(
  instructions: "研究と執筆を調整してレポートを作成してください。",
  tools: [
    Rixie::Tool.new(
      name:         "research",
      description:  "トピックを調査して調査結果を返す",
      input_schema: {type: "object", properties: {query: {type: "string"}}, required: ["query"]},
      call: ->(args) { research_agent.chat(args["query"]) }
    ),
    Rixie::Tool.new(
      name:         "write",
      description:  "トピックに基づいてレポートを書く",
      input_schema: {type: "object", properties: {topic: {type: "string"}}, required: ["topic"]},
      call: ->(args) { write_agent.chat(args["topic"]) }
    )
  ],
  parallel_tool_calls: true
)

おわりに

まだv0.1.0で荒削りな部分もありますが、個人サービスで利用している範囲ではありますが、RailsなしのRubyスクリプトやCLIツールからAIエージェントを動かしたいときには使いやすいなぁというのと、自身でAIエージェント系のフレームワークを自作してみて理解が深まったので作ってみてよかったです💎

VSCodeでインタプリタを適切に選択してもpythonの自動補完が表示されない事象の解決方法メモ

VSCode上のMicrosoft公式のPython及びPylanceを使ったpython開発環境でpythonのメソッド等のインラインでの補完が動かず、いろいろ検索するとインタプリタを適切なものに選択すると言うのが結構多く出てきましたが、それでは解決しなかったので、色々試して動くようになったので解決方法をメモ📝

github.com

github.com

結論は以下の設定を.vscode/settings.json等に入れてあげれば解決しました。

{
  "editor.quickSuggestions": {
    "other": "on" // 文字列およびコメント外での補完表示の有効化
  }
}

なんかglobalなVSCodeの設定が、知らず知らずのうちにこの辺の設定を上書きしてしまっていたのかも? 🤔

GitHub Copilot CLI向けの自作プロンプトをpluginとしてGitHubリポジトリで配布するメモ📝

はじめに

以下のリポジトリでGitHub Copilot CLI向けの自作プロンプトをpluginとしてGitHubリポジトリで配布してみたので、その手順をまとめました!

github.com

最近GitHub Copilot CLIを色々使っているのですが、CladeCodeのpluginのようにGitHugb Copilot CLIにもpluginの機能があり、plugin等を配布しているAwesome GitHub Copilotというリポジトリもあるのですが、、、、

GitHub Copilot CLIはまだPublic Previewということもあり、公式ドキュメント等でまだ作り方や配布方法が詳しく記載されていない様なので、awescome-copilotの中身を参考にしつつ、実際に自分で以下にcopilot cliのplugin等を公開するリポジトリを作ってみたので方法をメモしておきます📝

本題

pluginを配布するリポジトリ構成

最低限必要なのは.github/plugin/marketplace.jsonと、plugins/配下にpluginの定義をMarkdownで書いたファイルを置くだけの様です。

root
├── .github/
│       └── plugin/
│               └── marketplace.json
└── plugins/
    └── plugin-name/  # プラグインの本体
        ├── .github/
        │      └── plugin/
        │              └── plugin.json  # プラグインのメタ情報
        ├── skills/ # プラグインで配布するスキル (あれば)
        │       └── skill-name/
        │               └── SKILL.md
        ├── agents/  # プラグインで配布するエージェント(あれば)
        ├── instructions/  # プラグインで配布する命令書 (あれば)
        ├── README.md  # プラグインの説明やインストール方法など
         ...             # その他(例: ドキュメント、サンプルコードなど)

この構成だけで「プラグインの中身」としてCopilot CLIが認識してくれるようでした🎉

ファイルのサンプル

特に重要なのは.github/plugin/marketplace.jsonと、plugins/xxx/.github/plugin/plugin.jsonの2点です。 この2点をもとにGitHub Copilot CLIが「このリポジトリはプラグイン集で、plugins/配下のディレクトリはそれぞれプラグインとして認識する」という挙動になる模様 👀

.github/plugin/marketplace.json

{
  "name": "my-agents-collection",
  "metadata": {
    "description": "A collection of prompts for AI agents that I have created.",
    "version": "0.0.1",
    "pluginRoot": "./plugins"
  },
  "owner": {
    "name": "your name",
    "email": "your email"
  },
  "plugins": [
    {
      "type": "plugin",
      "name": "plugin-name",
      "description": "A plugin that provides ...",
      "path": "./plugins/plugin-name"
    }
  ]
}

plugins/plugin-name/.github/plugin/plugin.json

{
  "name": "plugin-name",
  "description": "A plugin that provides ...",
  "version": "0.0.0",
  "author": {
    "name": "Your Name"
  },
  "repository": "https://github.com/owner/repo",
  "license": "MIT"
}

marketplace登録とインストール

リポジトリをmarketplaceとして登録すると、そこからプラグインを以下のようにinstallできるようになります📥

# コレクション(リポジトリ)をmarketplaceに追加
copilot plugin marketplace add owner/repo

# 例: プラグインをインストール
copilot plugin install plugin-name@repo

ちなみに試してないですがprivateなリポジトリもサポートしているようです 👀

github.com

おわりに

とりあえず手探りではありますが、上記のような構成でリポジトリを作成し、marketplaceに登録することで、GitHub Copilot CLI向けのプラグインを配布できるようになりました🎉

プロンプト周りを再利用したり、チームとして共有したりするのに便利そうなので汎用性の高いプロンプトや、特定の用途に特化したプロンプトなどはpluginとしてまとめて配布できると良さそうですね 👍

参考リンク

`strict_loading_mode=:n_plus_one_only`をグローバルに設定してRails標準機能でN+1を検知するメモ📝

はじめに

Railsのstrict_loadingは「関連を意図せず遅延ロードしてない?」を検知できる便利機能ですが、Rails v8から strict_loading_mode をグローバル設定できて、しかも :n_plus_one_only が選べる のを最近知りました📝

Allow to configure strict_loading_mode globally or within a model.
Defaults to :all, can be changed to :n_plus_one_only.
https://github.com/rails/rails/blob/8-0-stable/activerecord/CHANGELOG.md

以下の記事で以前書いた時にはstrict_loading_mode = trueしかグローバルに設定できなかったので導入を躊躇っていたのですが、n_plus_one_onlyなら現実的に運用できそうだったので、導入してみたメモ📝

madogiwa0124.hatenablog.com

strict_loading_mode=:n_plus_one_onlyをグローバルに有効化する

以下の通り、development/test環境で有効化しました。

# config/environments/development.rb
config.active_record.strict_loading_by_default = true
config.active_record.strict_loading_mode = :n_plus_one_only
# config/environments/test.rb
config.active_record.strict_loading_by_default = true
config.active_record.strict_loading_mode = :n_plus_one_only

これだけで、development/test環境で関連の遅延ロードがN+1的に発生した場合に例外が発生するようになります🎉

またTIPSですが、consoleではstrict_loadingによるN+1の検知を無効化したい場合には以下のようにapplication.rb等で無効化してあげると良いかと思います。

# config/application.rb
console { ApplicationRecord.strict_loading_by_default = false }

おわりに

Rails 8でn_plus_one_onlyモードをグローバルで適用できるようになり、strict_loadingの運用がかなり現実寄りになった気がします。

Rails標準機能でN+1を検知できるのはありがたいですね🙏

参考リンク

DevcontainerのChrome DevTools MCPをコンテナでinstallしたChromiumに接続する方法メモ📝

DevcontainerでChrome DevTools MCPを使うときにホスト側のChromeデバッグモードで起動してipアドレス指定でホスト側もChromeに接続したり、UbuntuベースのDevcontainer環境にGoogle Chromeを入れるの、リポジトリや権限周りでつまずきやすくめどくさいなぁと思っていたのですが、

多少ChromiumChromeの互換性の問題が出たとしてもコンテナ内で完結できるメリットもありそうに感じ、Chrome DevTools MCPをコンテナ内でinstall済みのChromiumに接続できないかと思い試したら出来たのでメモしておきます📝。

本題

Devcontainer上でChrome DevTools MCPはインストール済みなことが前提で、主に必要な対応は以下でした。

  • Devcontainerイメージにchromiumを追加
  • start-chromium-mcp.shChromium(headless)を--remote-debugging-port=9222で起動し、chrome-devtools-mcp--browser-urlで接続
  • devcontainer.jsonMCPサーバー設定をスクリプト実行に変更(ホストChrome依存の設定を撤廃)

これにより、ブラウザ起動からMCP接続までをDevcontainer内で完結でき、ホスト側のChromeを立ち上げたりしなくてもMCP経由でAI AgentがChrome DevToolsにアクセスしてくれるようになりました。

実装例・コード

1) Dockerfile(Chromiumの導入)

# .devcontainer/Dockerfile(抜粋)
RUN apt-get update && apt-get install -y \
    chromium
  • Devcontainer(Debian/Ubuntu系)を想定し、chromiumをAPTで追加しています。

2) DevcontainerのMCP設定(スクリプトに委譲)

// .devcontainer/devcontainer.json(抜粋)
{
  "customizations": {
    "vscode": {
      "settings": {
        "mcp.servers": {
          "io.github.ChromeDevTools/chrome-devtools-mcp": {
            "type": "http",
            "config": {
              // Google Chrome DevToolsをChromiumで使うための設定
              "command": "/app/.devcontainer/scripts/start-chromium-mcp.sh",
              "env": {}
            }
          }
        }
      }
    }
  }
}

コンテナ上のchromiumをurl指定で起動してMCPに設定する/app/.devcontainer/scripts/start-chromium-mcp.shを呼び出します。

3) 起動スクリプトChromium + MCP

# .devcontainer/scripts/start-chromium-mcp.sh
#!/usr/bin/env bash
set -e

PORT=9222

# Chromium が未起動なら起動
if ! curl -sf http://127.0.0.1:${PORT}/json/version > /dev/null; then
  chromium \
    --headless \
    --no-sandbox \
    --disable-gpu \
    --remote-debugging-port=${PORT} \
    about:blank \
    >/tmp/chromium.log 2>&1 &

  # DevTools endpoint 起動待ち
  for i in {1..20}; do
    if curl -sf http://127.0.0.1:${PORT}/json/version > /dev/null; then
      break
    fi
    sleep 0.2
  done
fi

# UUID を直接扱わず browser-url で接続
exec npx chrome-devtools-mcp@latest \
  --browser-url http://127.0.0.1:${PORT}

おわりに

多少ChromiumChromeの互換性の問題があるかもしれませんが、自分のローカルでは今のところはいい感じっぽい?ので、これで使ってみて様子見してみようと思います🫡

参考リンク

qiita.com

github.com

2025年振り返り📝

今年も一年が終わるということで今年も振り返ってみる。

今年の振り返り

アウトプット

BLOG

BLOGは、今年1年で20記事書いていた。色々とあり6月ぐらいから更新が滞ってしまった。。。

pickup

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

公開したツールとか

今年は独自のUIライブラリを作った。

github.com

あと独自の認証ライブラリをマルチスコープ機能とか入れたりRails標準ぽいcookie + DBセッション形式に直したりした。

github.com

github.com

OSS活動

今年はRailsにCSPのhash-sourceをサポートするPRと、

github.com

stylelint-config-recommended-vueにstylelint-config-recommended v16対応を入れたPRを作成しました。

github.com

ref: 今年のPRリスト

インプット

今年も、去年に引き続き100冊以上読んでいて136冊でした。100冊以上本を読むのは大変かなとも思ったのですが、小説も結構読んでいたのと、割と習慣化されてきていてそこまで無理せずに今年も読めて良かったなと思います。

印象に残ってる本をピックアップして載せておきます。

技術書

面接対策というよりかはオートコンプリートやWebクローラ等々、よくあるシステムの構成の型が知れて勉強になった。

技術キャリアでのリーダーシップについての本、やはりプロダクトとアーキテクチャと組織はセットだなぁという感想になった。

AIを使ったコーディング周りの本、プロンプトテクニックというよりも人間にとって分かりやすいコードベースはAIにとっても分かりやすいだろうなと感じた。

プロンプトテクニックを超えて、エンジニアリングとして実際にLLMをサービス導入する際のテクニックや考え方が学びだった。

去年の目標と結果

目標 メモ 結果
モダンフロントエンドの技術的なキャッチアップ 独自UIライブラリも作れたし一定達成できたということで ⚪︎
OSS活動もうちょっと頑張りたい 数は少ないがCHANGELOGに乗る対応を複数PR出せたということで ⚪︎
人生における楽しみを増やす 散歩とかしか趣味を増やしたりはできなかった。
心身ともに穏やかに健康で過ごす 色々あったが、まぁ穏やかに過ごせたと言えば過ごせた ⚪︎

今年の目標

今年はとりあえず平和に穏やかに心身ともに健康で過ごせれば良しということで。

おわりに

今年は仕事もプライベートもバタバタした1年だったが、 来年は今年よりバタバタしそうなので何とか心身ともに健康で乗り切りたい。

eslint-plugin-unicornの導入してみたので設定方法とかメモ📝

eslint-plugin-unicornを個人のプロジェクトに導入してみたので導入方法とかについてメモ📝

eslint-plugin-unicornとは?

eslint-plugin-unicornはモダンなJavaScript/TypeScriptの書き方を促進するためのESLintプラグインで、ESLintには無いモダンな記法やファイル名に関する静的解析なども提供しているプラグインです。

github.com

導入方法

1. インストール

# pnpmを使う場合
pnpm add -D eslint-plugin-unicorn

# npmを使う場合
npm install --save-dev eslint-plugin-unicorn

2. ESLint設定ファイルの構成

最近のESLintフラットコンフィグ形式(ESLint v9.0.0以降)を使った設定方法ですがeslint.config.jsに以下のように追加します。以下は推奨ルールをデフォルトで設定する例です。

import unicorn from "eslint-plugin-unicorn";

export default [
  // 他のプラグインやルール設定
  unicorn.configs.recommended
];

これでeslint-plugin-unicornのオススメルールが有効になった状態になります🦄

主要なルールと特徴

eslint-plugin-unicornには多数のルールがありますが、特に気になったものをいくつか紹介します。

全てのルールは以下を参照してください。

https://github.com/sindresorhus/eslint-plugin-unicorn?tab=readme-ov-file#rules

モダンなAPIの使用を促進するルール

  • prefer-array-flat: ネストした配列をフラット化する際にArray#flat()を使用するよう促します
  • prefer-string-slice: 非推奨のString#substr()String#substring()の代わりにString#slice()を使用するよう促します
  • prefer-string-replace-all: 正規表現のグローバルフラグを使った置換よりもString#replaceAll()を使用するよう促します

コードの品質向上に関するルール

  • no-useless-undefined: 不要なundefinedの使用を禁止します
  • prefer-ternary: シンプルなif-else文の代わりに三項演算子を推奨します
  • explicit-length-check: 配列やオブジェクトの長さを明示的に確認することを強制します

ファイル名規則に関するルール

  • filename-case: ファイル名に一貫した命名規則を適用します(camelCase、PascalCaseなど)

一部のルールをカスタマイズする

基本的にESLintのその他のルールと同様ですが、すべてのルールがすべてのプロジェクトに適していると言うわけではないので、プロジェクトの特性に合わせて一部のルールを調整することができます。私は以下のような調整を行いました。

{
  rules: {
    // ファイル名の規則をカスタマイズ
    "unicorn/filename-case": [
      "error",
      {
        cases: {
          // プロジェクトで許可する命名規則
          camelCase: true,  // myComponent.js
          pascalCase: true, // MyComponent.js
        },
      },
    ],
    // 略語を許可(prevent-abbreviationsは厳しすぎる場合が多い)
    "unicorn/prevent-abbreviations": "off",
  }
}

まとめ

eslint-plugin-unicornは、モダンなJavaScript/TypeScriptコードにいい感じに矯正してれるのと、ファイル名のルールとかカスタムルールとか作ってましたがデフォルトで提供されてるのもいい感じだなと思いました!

ESLintにはsuppress ruleの仕組みも導入されたのでとりあえず既存のプロジェクトにサクッと導入してみて一旦suppressしておき徐々に対応していくような進め方も良さそうですね🦄

eslint.org