RubyLLM::Prompt クラスの抽出で Agent 外からテンプレートが利用可能に

crmne/ruby_llm

RubyLLM では、プロンプトテンプレートのパス解決と ERB 描画が Agent クラスに閉じていたが、今回の PR で RubyLLM::Prompt クラスへ切り出し、Agent を継承しなくても同一規約でテンプレートを描画できるようになった。

背景

Agent に埋め込まれていたプロンプト描画ロジックは、外部から再利用したいケースに対して壁となっていました。Issue #667 で指摘されたように、app/prompts/ 配下の ERB テンプレートを直接呼び出す手段がなく、利用者は同様のパス解決や ERB 処理を自前で実装する必要がありました。

この制約は、Agent を継承しないシンプルな RubyLLM.chat 呼び出しでもプロンプト管理の一貫性を求めるユーザーにとって不便でした。そのため、テンプレート解決とレンダリングを公開 API として提供する方向で設計が検討されました。

技術的な変更

RubyLLM::Prompt クラスが新規追加され、プロンプトのルート解決、パス構築、ERB 描画を単一責務で担います。クラスは root メソッドで Rails 環境かカレントディレクトリの app/prompts を基準にし、render メソッドでローカル変数を渡してテンプレートを評価します。

# frozen_string_literal: true

require 'erb'
require 'pathname'

module RubyLLM
  # Renders ERB prompt templates from the prompts directory.
  class Prompt
    attr_reader :name, :path

    def initialize(name)
      @name = name.to_s
      @path = self.class.root.join(
        @name.end_with?('.txt.erb') ? @name : "#{@name}.txt.erb"
      )
    end

    def render(**locals)
      raise PromptNotFoundError, "Prompt file not found: #{@path}" unless File.exist?(@path)

      ERB.new(File.read(@path)).result_with_hash(locals)
    end

    def self.render(name, **locals)
      new(name).render(**locals)
    end

    def self.root
      if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
        Rails.root.join('app/prompts')
      else
        Pathname.new(Dir.pwd).join('app/prompts')
      end
    end
  end
end

RubyLLM モジュールにも render_prompt ヘルパーが追加され、内部的に Prompt.render へ委譲します。

def render_prompt(name, **locals)
  Prompt.render(name, **locals)
end

Agent クラス内の render_prompt 実装は大幅に簡素化され、ファイルパス計算と ERB 評価の部分が RubyLLM::Prompt に委譲されました。削除前後のコードは以下の通りです。

def render_prompt(name, chat:, inputs:, locals:)
  path = prompt_path_for(name)
  unless File.exist?(path)
    raise RubyLLM::PromptNotFoundError,
          "Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
  end

  resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
  ERB.new(File.read(path)).result_with_hash(resolved_locals)
end
def render_prompt(name, chat:, inputs:, locals:)
  resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
  RubyLLM.render_prompt("#{prompt_agent_path}/#{name}", **resolved_locals)
end

また、prompt_path_forprompt_rootprompt_root(Rails/Dir 判定)といったプライベートメソッドは削除され、パス解決は RubyLLM::Prompt.root が一元管理します。

設計判断

内部実装を公開 API として切り出す方針が採用されました。新しい設定キーを増やす代わりに、既存の Prompt 名前空間に機能を集中させることで、後方互換性とコードベースのシンプルさを両立させています。Agent はクラス名ベースのスコーピングとランタイムコンテキスト解決のみを保持し、具体的なテンプレート処理は Prompt に委譲します。

この設計は 単一責務の原則 に沿い、プロンプト管理を独立したコンポーネントとしてテストしやすくしています。テスト (spec/ruby_llm/prompt_spec.rb) でも Prompt.root のモック化や一時ディレクトリでの実ファイル検証が行えるため、将来的な拡張やバグ修正が局所化しやすくなります。

まとめ

今回の変更により、RubyLLM::Prompt.render('friend', name: 'Andrey') のように、Agent を継承しなくても同一のテンプレート規約でプロンプトを利用できるようになりました。機能は Prompt クラス に集約され、Agent からは委譲のみを残すことで拡張性と保守性が向上しています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
eca4b4f8

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文→背景→技術的な変更→設計判断(任意)→まとめの3部構成が明確に存在し、各部が要点を説明している。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きシンタックスハイライトは正しい形式だが、GitHubリンク記法に余分なスペースがあり `[ #667 ]` や `[ #675 ]` と記述されている。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的な Ruby/Rails の知識を前提とした記述で、初心者向けの余計な説明はない。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論パラグラフ→各論→結論の構成になり、段落は1トピック・6文未満で、トピックセンテンスが先頭に配置されている。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは Diff に示された追加・変更内容と完全に一致し、ファイル名も正しい。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Prompt、ERB、Pathname、PromptNotFoundError などの用語は PR と一致し、誤用はない。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明は PR の目的・変更点と整合しており、因果関係も正しく記述されている。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張は PR タイトル、Description、Diff、追加されたテストコードで裏付けられている。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #675、Issue 番号 #667、ファイルパスなどの数値・固有名詞は正確である。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR タイトルの内容を日本語で適切に要約している。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンやリリーススケジュール等、PR に記載されていない外部知識は一切含まれていない。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現は使用されておらず、PR の記述と矛盾もない。