はじめに
前回は、Sayane(紗綾音)を使って、ローカルProfileからChatGPT / Claude向けのプロンプトを生成する最小フローを紹介しました。
第1回で扱った中心は、次の流れです。
Sayane Profile
↓
Prompt IR
↓
Adapter
↓
ChatGPT / Claude などのtarget向け出力
今回は、この中の Sayane Profile と Prompt IR に焦点を当てます。
なぜ、最初からChatGPT用プロンプトやClaude用プロンプトを書かないのか。なぜ、ProfileとPrompt IRを分けるのか。なぜ、Adapterという変換層を置くのか。
この記事では、Sayaneの設計意図を、実装者向けに整理します。
GitHubはこちらです。
https://github.com/zyx-corporation/sayane
この記事で扱うこと
この記事では、次の点を扱います。
Sayane Profileの役割
Prompt IRの役割
ProfileとPrompt IRを分ける理由
Adapterとの責務分離
targetごとにcompileする設計の意味
逆に、この記事ではCandidate、lineage、RDE評価の詳細には入りません。これらは後続回で扱います。
なぜプロンプトを直接書かないのか
LLMをひとつだけ使うなら、直接プロンプトを書いても大きな問題はありません。
たとえば、ChatGPT向けに次のようなシステムプロンプトを書くことはできます。
あなたはソフトウェアアーキテクトです。
回答は日本語で、簡潔かつ具体的にしてください。
Pythonではテスト容易性を重視してください。
これは単純で、すぐ使えます。
しかし、複数のLLMを使い分けると、直接プロンプト方式は少しずつ苦しくなります。
ChatGPTにはChatGPT向けの形式がある。ClaudeにはClaude向けの渡し方がある。Geminiやlocal-openwebuiでは、さらに違う制約があります。CursorやMCP経由で使う場合も、プロンプトの置き場所や呼び出し方が変わります。
このとき、すべてのtargetに対して直接プロンプト文字列を管理し始めると、次のような問題が起きます。
同じ内容を何度も書くことになります。targetごとの差分が見えにくくなります。あるtarget向けの修正が、他のtargetへ反映されなくなります。どれが正本なのか分からなくなります。
つまり、プロンプト文字列そのものを正本にすると、複数LLM運用では破綻しやすくなります。
そこでSayaneでは、プロンプト文字列ではなく、Profile を正本にします。
Sayane Profileとは何か
Sayane Profileは、ユーザーやプロジェクトの文脈を保持するローカルの正本です。
Profileには、たとえば次のような情報が入ります。
identity: 名前、役割、所属、肩書きなど
voice: 文体、トーン、説明の好み
values: 重視する価値観や判断基準
policy: 応答方針、禁止事項、優先事項
context: プロジェクトや作業に関する文脈
概念的には、Profileは「LLMにそのまま投げる文字列」ではありません。
Profileは、ユーザーやプロジェクトについての構造化された文脈です。
このProfileを、ChatGPT向け、Claude向け、Gemini向けなどに変換して使います。
ここで重要なのは、Profileがtarget非依存であることです。
Profileは、ChatGPTのためだけの設定ではありません。Claudeのためだけの設定でもありません。あくまで、ローカルに置かれた文脈の正本です。
Profileは何を持ち、何を持たないべきか
Profileには、長期的に再利用したい文脈を入れます。
たとえば、次のような情報はProfileに向いています。
ユーザーの基本的な役割
回答の言語設定
文体や説明粒度の好み
プロジェクトの基本方針
開発時に重視する設計原則
繰り返し使う制約条件
一方で、Profileに入れない方がよいものもあります。
一時的な会話の流れ、まだ検証していない仮説、その場だけの感情、一回限りの作業指示、長期的に採用するか分からない表現。こうしたものをすぐProfileに入れると、文脈がノイズで膨らみます。
Profileは、何でも入れるメモ帳ではありません。
Profileは、長期的に使う文脈の正本です。
この区別が、後続回で扱うCandidateやlineageにつながります。取り込んだ文脈をいきなりProfileへmergeせず、候補として扱う理由もここにあります。
Prompt IRとは何か
Prompt IRは、Prompt Intermediate Representationの略です。日本語では、プロンプト中間表現と呼べます。
Profileは、ユーザーやプロジェクトの文脈を保持する正本です。しかし、Profileをそのまま各LLMに渡すわけではありません。
まず、ProfileをPrompt IRへ変換します。
Prompt IRは、targetに依存しないプロンプトの内部表現です。
つまり、ChatGPT用のJSONでも、Claude専用の形式でもありません。Adapterが後からtarget別の出力へ変換するための中間構造です。
Prompt IRを挟むことで、Profileと各targetの形式を直接結びつけずに済みます。
これは、ソフトウェア設計ではよくある分離です。
内部の正本と外部への出力形式を分ける。内部モデルとプレゼンテーションを分ける。ドメインモデルとAPIレスポンスを分ける。
Sayaneでは、それをLLM向けプロンプト生成に適用しています。
ProfileとPrompt IRを分ける理由
ProfileとPrompt IRを分ける理由は、主に3つあります。
第一に、Profileを安定させるためです。
Profileはローカルの正本なので、できるだけtargetの都合に引きずられない方がよい。ChatGPTの形式が変わったからといって、Profileそのものを大きく変える設計にはしたくありません。
第二に、targetごとの違いをAdapter側へ閉じ込めるためです。
ChatGPT、Claude、Gemini、local-openwebuiでは、期待される入力形式や自然な構造が違います。その違いをProfileへ直接混ぜると、Profileが肥大化し、見通しが悪くなります。
第三に、テストしやすくするためです。
ProfileからPrompt IRへの変換をテストする。Prompt IRからtarget別出力への変換をテストする。このように分けることで、問題が起きたときに、どの層で壊れているのかを見つけやすくなります。
これは、CLIツールとしてもライブラリとしても重要です。
Adapterの責務
Adapterの責務は、Prompt IRをtarget別の形式へ変換することです。
Adapterは、Profileの正本を直接編集しません。Profileの意味そのものを勝手に変えるべきでもありません。
Adapterがやるべきことは、targetごとに必要な形式へ整えることです。
たとえば、ChatGPT向けにはmessages形式が自然かもしれません。Claude向けには別の構造が必要かもしれません。local-openwebui向けには、より単純なテキスト形式が扱いやすいかもしれません。
Adapterは、targetの都合を吸収する境界です。
この境界を置くことで、ProfileはProfileとして保ち、targetの変化にはAdapter側で対応できます。
同一Profileから複数targetへ出力する
Sayaneの基本的な使い方は、同じProfileからtargetだけを変えてcompileすることです。
sayane compile --target chatgpt
sayane compile --target claude
サンプルProfileを明示する場合は、次のようにします。
sayane compile --target chatgpt --profile examples/profiles/minimal.yaml
sayane compile --target claude --profile examples/profiles/minimal.yaml
ここで行っていることは、同じProfileを別々のtargetへ変換することです。
この設計では、正本はProfileにあります。
ChatGPT向け出力も、Claude向け出力も、正本そのものではありません。あくまでProfileから生成されたtarget別の成果物です。
この考え方は、ソースコードとビルド成果物の関係に近いです。
ソースコードが正本で、ビルド成果物はtargetごとに生成される。同じように、SayaneではProfileが正本で、各LLM向けプロンプトは生成物です。
Markdown contextとの関係
Sayane Profileは、単体のYAMLだけで完結するとは限りません。
context/ 配下にMarkdown文脈を置き、それをProfileの文脈として扱うことができます。
~/.sayane/
profiles/
default/
sayane.profile.yaml
context/
project.md
coding-policy.md
ai-handoff.md
概念的には、Profileが文脈の中心であり、Markdown contextが補助的な文脈として接続されます。
新しいMarkdownファイルを追加した場合は、必要に応じてindexを更新します。
sayane storage index
その後、compileします。
sayane compile --target chatgpt
この仕組みにより、長いプロジェクト文脈や作業方針を、Profile本体にすべて詰め込まず、Markdownとして管理できます。
設計上の注意点
ProfileとPrompt IRを分ける設計には、利点があります。一方で、注意点もあります。
まず、Profileに何でも入れすぎると、compile結果が重くなります。LLMにはトークン制限があるため、文脈は多ければよいわけではありません。
次に、Prompt IRが複雑になりすぎると、Adapterの実装が難しくなります。中間表現は、表現力と単純さのバランスが必要です。
また、Adapterがtargetごとの最適化をやりすぎると、Profileの意図から離れる危険があります。Adapterは出力形式を整える層であり、Profileの意味を勝手に書き換える層ではありません。
Sayaneでは、このあたりの境界をできるだけ明確に保つことを重視しています。
今回のまとめ
今回の記事では、Sayane ProfileとPrompt IRの設計を説明しました。
Sayane Profileは、ユーザーやプロジェクトの文脈の正本です。
Prompt IRは、targetに依存しないプロンプト中間表現です。
Adapterは、Prompt IRをChatGPT、Claude、Gemini、local-openwebuiなどのtarget別出力へ変換する層です。
重要なのは、プロンプト文字列そのものを正本にしないことです。
複数LLM時代には、プロンプトをコピペで運ぶよりも、Profileを正本として管理し、必要なtargetへcompileする方が扱いやすくなります。
Sayaneは、そのための小さな基盤です。
連載予告
この連載では、Sayaneの設計と使い方を、実装者向けに順番に整理していきます。
第1回では、Sayaneをインストールし、ローカルProfileからChatGPT / Claude向けプロンプトを生成する最小フローを扱いました。
第2回である本記事では、Sayane ProfileとPrompt IRの設計を説明しました。
次回、第3回では、Adapterの作り方を扱います。同じPrompt IRから、targetごとの出力形式をどう生成するのか。ChatGPT向け、Claude向け、local-openwebui向けの違いをどこで吸収するのかを、実装方針として整理します。
その後は、Candidate / evaluate / lineageによる文脈更新管理、MCP ServerとしてCursorから使う方法、Markdown / Obsidian / Gitを使った文脈運用、RDE的な評価をどのように実装へ落とすかを順に扱う予定です。