`Agent.find`で`assume_model_exists`が伝播しないバグを修正

crmne/ruby_llm

Agent.findでチャット履歴を再ロードした際、クラス設定のassume_model_exists: trueが引き継がれずModelNotFoundErrorが発生するバグが修正されました。新たにapply_assume_model_existsメソッドを導入し、findsync_instructions!llm_chat_forの各パスで一貫してフラグを伝播するようになりました。

背景

assume_model_exists: trueは、モデルレジストリに登録されていないカスタムモデルを使用するための設定です。クラスレベルでmodel 'gpt-6.7', provider: :openai, assume_model_exists: trueと宣言しても、Agent.find経由で既存のチャットを再開しようとするとModelNotFoundErrorが発生するという問題が#679で報告されました。

問題の根本は、assume_model_existsトランジェントなattr_accessorとして定義されていることにあります。ActiveRecordがデータベースからレコードをロードする際、このフィールドはデータベースには保存されないためnilfalse相当)にリセットされます。その後apply_configurationto_llmを呼び出すと、Models.resolveがレジストリ未登録モデルの解決を試みて例外を投げます。

create/create!で使われるwith_rails_chat_record**chat_kwargsをレコードのコンストラクタに展開するため同問題は発生しませんでしたが、findパスにはその相当処理がなく、findsync_instructions!(ID渡し)の両方で同様の抜け漏れがありました。

技術的な変更

apply_assume_model_existsという専用メソッドを追加し、chat_kwargsからassume_model_existsの値をActiveRecordオブジェクトに注入する処理を一箇所に集約しました。

追加されたメソッド (lib/ruby_llm/agent.rb):

def apply_assume_model_exists(chat_object)
  return unless chat_kwargs.key?(:assume_model_exists) &&
                resolved_chat_model &&
                chat_object.is_a?(resolved_chat_model)

  chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
end

このメソッドは3つの条件を満たす場合のみ動作します。chat_kwargsassume_model_existsキーが存在し、resolved_chat_modelが解決されており、かつchat_objectがそのモデルのインスタンスである場合に限り、値を書き込みます。

呼び出し箇所の追加:

# find パス
def find(id, **kwargs)
  # ...
  record = resolved_chat_model.find(id)
  apply_configuration(record, input_values:, persist_instructions: false)
  # (空行追加のみ、apply_assume_model_existsはllm_chat_for内で適用)
  record
end

# sync_instructions! パス
def sync_instructions!(chat_or_id, **kwargs)
  # ...
  record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
  apply_assume_model_exists(record)  # 追加
  # ...
end

# llm_chat_for(new(chat:) パスを含む)
def llm_chat_for(chat_object)
  apply_assume_model_exists(chat_object)  # 追加
  chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
end

llm_chat_forへの追加により、既存のActiveRecordオブジェクトをchat:引数としてAgent.newに渡すパス(例: MyAgent.new(chat: Chat.find(id)))も同時に修正されています。

テストは3つのシナリオを網羅しています。find直接呼び出し、sync_instructions!へのID渡し、そして再ロードしたレコードをnew(chat:)に渡すケースです。いずれもnot-a-real-modelという実在しないモデル名とassume_model_exists: trueの組み合わせで、例外が発生しないことを検証しています。

設計判断

apply_assume_model_existsとして処理を単一メソッドに集約する方式が採用されました。

findsync_instructions!llm_chat_forの各コードパスに個別にインライン代入を書く方法も取れましたが、それは同一ロジックの散在を招きます。専用メソッドにすることで、将来同様のパスが追加された際の適用漏れを防ぎつつ、ガード節による早期リターンで副作用の発生条件を明示しています。

また、chat_kwargs.key?(:assume_model_exists)という存在チェックにより、フラグが未指定の場合はレコードに一切触れない設計になっています。falseを代入するケースも含めて意図的に制御されており、「値がない」と「falseが設定されている」を区別します。

まとめ

この修正は、create系とfind系でチャットオブジェクトの初期化経路が異なることに起因した非対称性を解消したものです。apply_assume_model_existsの導入により、ActiveRecordロード後のトランジェント属性の欠落を補填する処理が統一され、既存チャットを再開する際のModelNotFoundErrorが防がれます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
3ba32317

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

記事全体の構成が「総論→各論→結論」の3部構成に沿っており、リード文、背景、技術的変更、設計判断、まとめの各要素が明確に配置されています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```ruby:path/to/file.rb)およびGitHubのIssue/PRへのリンク記法([#679](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

ActiveRecordの挙動や`attr_accessor`の特性など、専門知識を持つエンジニアを対象とした適切な技術レベルで記述されています。

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

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

各セクション、各パラグラフが「総論→各論」の構成になっており、トピックセンテンスが段落の冒頭に置かれているため、非常に読みやすい構造です。1段落1トピックの原則も守られています。

Diff内容との照合 ⚠ WARNING

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

引用されているコードはDiffと一致していますが、`find`メソッドのコードブロックの提示方法が若干紛らわしいです。`find`メソッド自体の変更は空行追加のみであり、実際の修正は`find`から呼び出される`llm_chat_for`内にありますが、コードブロックのコメントで補足する形式は読者の誤解を招く可能性があります。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`attr_accessor`、`transient`、`ModelNotFoundError`などの技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

バグの根本原因(ActiveRecordロード時にtransientな属性が失われること)と、それを解決するためのアプローチ(専用メソッドを導入し、複数のコードパスで呼び出すこと)が技術的に正確に説明されています。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の発生源、修正内容、関連Issue番号など)は、提供されたPRのDescriptionやDiff内容によって裏付けられており、ハルシネーションは検出されませんでした。「設計判断」セクションも、コードの形式から妥当に推論できる範囲の内容です。

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

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

PR番号(#680)とIssue番号(#679)が正確に記載されています。

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

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

記事のタイトルはPRの主題「`assume_model_exists`が伝播されないバグの修正」を正確に反映しています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれない外部知識(バージョンのサポート状況、リリース日程など)の記述はなく、提供された情報に忠実です。

時間表現の正確性 ✓ PASS

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

時間表現に関する記述はなく、事実を歪曲するような表現は見られません。