ADR 0012: corespine の LLM シームを採用 — create_message / ProviderResponse に代えて chat / ChatCompletion を採用
LLMProvider Protocol をファミリー共有コア(corespine)へ移動し、OpenAI chat-completions 形式を採用する。corespine の RateLimitedProvider によるオプションのアクティブ TPM スロットルを追加する。
ステータス: accepted · 日付: 2026-06-20
不変の記録。ドリフト追跡の対象外(
coversなし)。編集ではなく、置き換え(supersede)で更新すること。
関連: 0009(フレームワーク/依存関係ポリシー) および 0011(python-project-standard の採用)。 rag-spine 0.3.0 で出荷済み。
背景
RAGSpine は一貫して、すべての外部 SDK を型付き Protocol の背後に隔離し、SDK ゼロのオフラインコアとデフォルトの MockProvider を備えてきた(ADR 0005 /
0009)。LLMProvider シームは ragspine の内部
(agent/llm_provider.py)に、ragspine 固有のメソッドとして定義されていた:
def create_message(*, system, messages, tools) -> ProviderResponse: ...2 つの圧力が重なった:
- 兄弟パッケージのファミリーが、いまや同じプリミティブを共有している。 ragspine、
spineagent(マルチエージェントオーケストレーション)、そして 薄い共有コアであるcorespineのすべてが、単一のプロバイダー契約を通じて LLM と対話する必要がある。パッケージごとに独自のプロバイダー Protocol を重複させれば、契約と テストシームが分断されてしまう。corespine は、まさにこうしたドメイン中立なプリミティブのために意図的に薄く保たれた ファミリーの共有コアである。 - 独自の
create_message(*, system, messages, tools) -> ProviderResponseという形式は、理由もなく非標準だった。 システムプロンプトを別引数として切り出し、ProviderResponseという独自のエンベロープを 発明したことは、実際の SDK(そしてすべてのアダプター作者のメンタルモデル)がすでに話している、 広く理解されている OpenAI chat-completions 形式から乖離していた — 何の利点もない摩擦である。
これとは別に、レートリミットへの防御手段は SDK の受動的な max_retries による 429 バックオフだけであり、
これは制限に達した後に反応するものだった。契約上の TPM 上限を下回り続けたいクライアントには、
ライブラリ内に使える調整手段がなかった。
決定
LLMProvider Protocol を corespine へ移動し、OpenAI chat-completions 形式を採用する。
ragspine は corespine>=0.1.0 へのハードなランタイム依存(唯一の非オプション依存)を持ち、
このシームを agent/llm_provider.py から再エクスポートするため、既存のインポート箇所
(from ragspine.agent.llm_provider import LLMProvider, MockProvider, AnthropicProvider)は
そのまま動作し続ける。具体的には:
- 単一のメソッドは
chat(messages: list[dict[str, Any]], *, tools: list[dict[str, Any]] | None = None) -> ChatCompletionになる。システムプロンプトは単に先頭の{"role": "system", ...}メッセージであり、別引数ではない。 ProviderResponseは corespine のChatCompletionエンベロープに置き換えられる:choices(各要素はcontent+tool_callsを持つResponseMessageをラップしたChoice)に加えてusage、model、id— すべて frozen dataclass であり、標準的な chat-completions 形式である。AnthropicProviderは引き続きanthropicSDK を遅延インポートするが、OpenAI 形式のmessages/toolsを Anthropic Messages API にマッピングし、レスポンスをChatCompletionにマッピングし直すようになる。ProviderErrorはcorespine.CorespineErrorのサブクラスになる。- オフラインの
MockProviderとツール使用ループ(provider.chat(messages, tools=tools))は 同時に更新される。捏造防止ガードは手を付けない(このガードは数値についてプロバイダーの文章を 一切信用していなかった)。
オプションのアクティブ TPM スロットルを追加する。 新しい環境変数 RAGSPINE_TOKENS_PER_MINUTE(int、
デフォルト 0 = オフ)により、build_provider は構築したプロバイダーを corespine の
RateLimitedProvider でラップする — これはスライディングウィンドウ方式のソフトリミットで、ウィンドウに
予算がないときは chat 呼び出しの前にブロックする。これは SDK の受動的な max_retries を補完するもの:
一方は上限を事前に下回り続け、もう一方は超過からの回復を担う。
検討した代替案(却下)
- 独自の
create_message/ProviderResponseシームを ragspine 内に維持する。 却下 — ファミリー全体で契約が分断され、不必要に非標準のままになる。このシームは教科書どおりの ドメイン中立プリミティブであり、共有コアに属するべきものである。 - シームを ragspine 内に定義し、兄弟パッケージが ragspine に依存する。 却下 — 依存の方向が逆転してしまう(RAG パッケージが汎用エージェントパッケージの依存先になってしまう)。 corespine は共有プリミティブの合意された集約先であり、矢印は corespine に向かって入る。
- レートリミットを SDK の
max_retriesだけに頼る。 却下 — 受動的なバックオフでは、クライアントを 契約上の TPM 上限以下に保つことはできない。2 つのメカニズムは補完関係にあり、冗長ではない。
帰結
- カスタムプロバイダーに対する破壊的変更。
create_message(*, system, messages, tools) -> ProviderResponseを実装していた者は、chat(messages, *, tools=None) -> ChatCompletionへ移行しなければならない(dataclass 群はcorespineから、またはragspine.agent.llm_providerの再エクスポート経由でインポートする)。拡張ポイント ガイドには、新しい形式での OpenAI の実例が掲載されている。 - ragspine は非オプション依存をちょうど 1 つ獲得する(
corespine>=0.1.0、それ自体のdependenciesは空)。オフラインファーストで重い SDK を持たないデフォルトパスは維持される。 - プロバイダー契約はいまや Spine ファミリー全体で共有されるため、あるパッケージ向けに書かれた プロバイダーはすべてのパッケージで動作する。
- 新しい設定面は
RAGSPINE_TOKENS_PER_MINUTEの 1 つだけであり、それ以外はすべて内部的な変更である。
ADR 0011: python-project-standard の採用 — src/ レイアウトへ移行し、残りは維持
ハウス標準を統治哲学として採用し、境界の明確な唯一の構造的不変条件(src/ragspine/ レイアウト)を整合させ、文書化された 4 つの乖離を受け入れる。
ADR 0013: Dify ワークフロー YAML → 純 Python コンパイラ + 静的最適化アドバイザー
Dify ワークフローの .yml を、脱 Dify 化された IR(parse → IR → codegen)を経由してフレームワーク非依存の命令型 Python にコンパイルし、8 つのルールからなる静的最適化アドバイザーを備える — PyYAML は新設の [dify] extra の背後に置かれ、完全同期で、コアへの新規依存はゼロ。