スキルファーストアーキテクチャ
ThemisはすべてのAIエージェント機能を3つのレイヤーに構造化しています:スキル(ドメイン知識)、出力ツール(構造化コントラクト)、ワークフロー(ライフサイクルオーケストレーション)。スキルが基盤であり、エージェントを効果的にする判断力、推論力、専門知識を持っています。
3つのレイヤー
+--------------------------------------------------+
| LAYER 1: SKILL |
| Owns: persona, process, judgment, domain knowledge |
| Sources: file-based (.claude/skills/, lib/skills/) |
| DB-backed (Skill model, 3 scopes) |
| prompts (app/prompts/) |
+--------------------------------------------------+
|
agent calls tool
|
v
+--------------------------------------------------+
| LAYER 2: OUTPUT TOOL |
| Owns: structured data contract, side effects |
| Lives in: app/services/*_tool_builder.rb |
| Built with: ClaudeAgentSDK.create_tool() |
+--------------------------------------------------+
|
returns result
|
v
+--------------------------------------------------+
| LAYER 3: WORKFLOW |
| Owns: lifecycle orchestration only |
| Lives in: app/services/workflows/ |
| Target: under 50 lines |
+--------------------------------------------------+
レイヤー1:スキル
スキルはすべての判断を担います。エージェントに_何を_すべきか、_どのように_推論すべきか、_いつ_ツールを使うべきかを伝えます。Themisは3つのスキルソースをサポートしており、それらが連携してエージェントに包括的なドメイン知識を提供します。
ファイルベーススキル(コードベース)
SKILL.mdマニフェストを持つmarkdownファイルとしてリポジトリにチェックインされたスキルです。2つのディレクトリが異なる目的を果たします:
.claude/skills/— Themis内部スキル。アーキテクチャガイド、コーディング規約、レビュー手法、インテグレーションヘルパー。Themisリポジトリに留まり、Claude Agent SDKによって自動検出されます。lib/skills/— ポータブルスキル。コード生成時にターゲットプロジェクトのワークツリーにコピーされ、エージェントがクロスプロジェクトの標準を持ち運べるようにします(例:Railsの規約やセキュリティチェックリストを含むcode-quality/)。
各スキルはSKILL.md(YAMLフロントマター + markdown)とオプションの補足ファイルを含むディレクトリです:
.claude/skills/understanding-themis/
SKILL.md # 名前、説明、コンテンツを含むマニフェスト
HOTWIRE.md # 補足リファレンス(オプション)
DBバックスキル(Skillモデル)
Active Storageファイル添付を持つデータベースに保存されたユーザー作成スキルです。Web UIまたはチャット中のエージェントツールを通じて管理されます。3つのスコープが可視性を制御します:
| スコープ | 所有者 | 閲覧可能 | ユースケース |
|---|---|---|---|
| システム | 管理者 | 全ユーザー、全スペース | 組織全体の標準 |
| スペース | スペース | すべてのスペースメンバー | チーム固有の知識 |
| 個人 | ユーザー | 所有者のみ | 個人の好みとワークフロー |
DBスキルは各エージェント実行前にSkillExtractorによってディスクに抽出され、アトミックなディレクトリ置換でスコープごとにキャッシュされます。エージェントは同じ.claude/skills/ディレクトリ規約を通じてそれらを発見します。
SkillExtractor.prepare_for_agent(space:, user:)
→ Skill.available_for(user, space)をクエリ
→ キャッシュディレクトリに抽出(system / space / personal)
→ SDKオプション用のadd_dirs配列を返す
エージェントはSkillToolBuilderツール(create_skill、update_skill、list_my_skills)を通じて、会話中に独自の個人スキルを作成・更新することもできます。
エージェントプロンプト
ワークフロー固有の指示を提供する静的および動的プロンプトファイルです:
- 静的プロンプト(
.md) — スキルがランタイムコンテキストを必要としない場合。例:レビュープロセス、品質基準、判定基準を含むpr_review.md。 - 動的プロンプト(
.md.erb) — ランタイムデータを注入するERBテンプレート。例:base_agent.md.erbは space ごとのコンテキスト(エージェント ID、利用可能なチャネルなど)をレンダリングします。
プロンプトはapp/prompts/にあります。PromptLoader.load("name")(静的)またはPromptLoader.render("name", locals)(動的)で読み込みます。
プロンプトはプロセスと判断を定義しますが、出力フォーマットは記述しません — その責任は出力ツールスキーマにあります。
スキルがエージェントに届く方法
ChatJob / ChannelMentionJob
│
├─ System prompt ← PromptLoader (app/prompts/)
│
└─ add_dirs ← SkillExtractor
├─ File-based skills (.claude/skills/)
└─ DB skills (extracted to cache)
├─ system/
├─ {space_id}/space/
└─ {space_id}/personal/{user_id}/
Claude Agent SDKはadd_dirsのSKILL.mdファイルをスキャンし、エージェントに自動的に利用可能にします。スキルはfeature_skills設定でスペースごとにトグル可能です。
レイヤー2:出力ツール
出力ツールはエージェントとシステム間の構造化コントラクトを定義します。エージェントにパース可能なテキストを生成させる(脆弱な)代わりに、型付き引数で呼び出すツールを提供します。
ツールを呼び出し元に接続するのは、ツールを定義した後の次のステップです。各エージェントの呼び出し元(フルエージェント、Web/API チャット、メッセージング)は、ツールカタログ経由でツールグループのセットにオプトインします — 新しい呼び出し元にツールを追加するのは4ファイルにまたがる編集ではなく、宣言的な1回の変更です。
ツールビルダーはapp/services/*_tool_builder.rbにあり、ClaudeAgentSDK.create_tool()を使用します:
class PRReviewToolBuilder
def self.build_submit_review_tool(review:, space:)
ClaudeAgentSDK.create_tool(
"submit_review",
"Submit your completed code review.",
{
type: "object",
properties: {
verdict: { type: "string", enum: %w[APPROVE REQUEST_CHANGES COMMENT] },
summary: { type: "string", description: "Markdown review summary" },
comments: {
type: "array",
items: {
type: "object",
properties: {
path: { type: "string" },
line: { type: "integer" },
body: { type: "string" }
},
required: %w[path line body]
}
}
},
required: %w[verdict summary]
}
) do |args|
# 副作用:GitHubに送信、レビューレコードを更新
end
end
end
スキーマがフォーマット仕様です。エージェントはツールリストでそれを確認し、何を生成すべきか正確に把握します。出力フォーマットの指示にプロンプトのバジェットを浪費する必要はありません。
現在のツールビルダー
目的別にグループ化。すべてのツールは ツールカタログ 経由でエージェント呼び出し元に接続されます。
ワークフロー出力コントラクト
| ビルダー | 主要ツール | 目的 |
|---|---|---|
PRReviewToolBuilder | get_pr_info, get_pr_diff, get_pr_comments, get_ci_status, submit_review | PRレビューワークフローの出力 |
CodeGenerationResultToolBuilder | submit_code_generation_result | コード生成からのPRメタデータ |
AutomationToolBuilder | skip_message | 自動化のスキップ判断 |
トリガー(ファクトリー接続)
| ビルダー | 主要ツール | 目的 |
|---|---|---|
PRReviewTriggerToolBuilder | trigger_pr_review | チャット/メンションからPRレビューをエンキュー |
CodeGenerationToolBuilder | trigger_code_generation | チャット/メンションからコード生成をエンキュー |
データアクセス
| ビルダー | 主要ツール | 目的 |
|---|---|---|
GithubToolBuilder | get_pr_info, get_pr_diff, get_pr_comments, get_ci_status, list_pull_requests, post_pr_comment | チャット/メンションコンテキストでのGitHub直接APIアクセス |
ChatHistoryToolBuilder | search_conversations, recall_conversation | オンデマンド会話履歴 |
RepoSearchToolBuilder | resolve_repo_path | ローカル git ワークツリーの参照 |
ThemisQueryToolBuilder | query_themis_data | Themis DBクエリ(editable_by? ゲート) |
GoogleDriveProxyToolBuilder | プロキシされたGoogle Drive読み取りツール | ユーザーごとのOAuthスコープ Drive アクセス |
副作用
| ビルダー | 主要ツール | 目的 |
|---|---|---|
SentryToolBuilder | update_sentry_issue | Sentry ステータス + アサインメント |
MemoryToolBuilder | save_memory, delete_memory | ユーザーごとのメモリストア |
チャット UX
| ビルダー | 主要ツール | 目的 |
|---|---|---|
AskUserQuestionHook (PreToolUse) | AskUserQuestion (ネイティブ組み込み) | 構造化された確認質問 — MCP ツールとして構築せず、フックで介入 |
FileToolBuilder | create_file | エージェント生成のファイルダウンロード |
ShowWidgetToolBuilder | show_widget | サンドボックス化されたHTMLウィジェット(D3、Mermaid、SVG) |
ShowChartToolBuilder | show_chart | 構造化された Chart.js レンダリング |
ImageGenerationToolBuilder | generate_image | Gemini 画像生成 |
リソース管理
| ビルダー | 主要ツール | 目的 |
|---|---|---|
SkillToolBuilder | 13 ツール — CRUD、ファイル操作、チェックアウト/チェックイン | エージェント駆動のスキル管理 |
AutomationChatToolBuilder | create_automation, update_automation, list_my_automations, delete_automation | エージェント駆動の自動化管理 |
レイヤー3:ワークフロー
ワークフローは薄いグルーです。レコードの作成、エージェントの起動、エラー処理、ステータスの更新を行います。判断はゼロ、パースもゼロであるべきです。
すべてのワークフローはWorkflows::BaseWorkflowを継承し、#executeを実装します。基底クラスは#run_agent(prompt:, system_prompt:, model:, max_turns:)を提供します。
module Workflows
class FeatureWorkflow < BaseWorkflow
def execute(input:)
record = FeatureRecord.create!(input: input, status: "running")
begin
result = run_agent(
prompt: build_prompt(input),
system_prompt: PromptLoader.load("feature_name")
)
record.complete!(result)
rescue => e
record.fail!(e.message)
raise
end
end
end
end
ワークフローで正規表現のパース、JSON抽出、ビジネスロジックを行っている場合、何かが間違ったレイヤーにあります。
判断フレームワーク
| 質問 | 回答 | レイヤー |
|---|---|---|
| 判断、推論、またはドメイン知識を伴うか? | スキルに移動 | スキル |
| 構造化データ交換を定義するか、副作用を生むか? | 出力ツールにする | 出力ツール |
| レコードのライフサイクル、エラーリカバリ、オーケストレーションを管理するか? | ワークフローに残す | ワークフロー |
| LLMの自由テキストを構造化データにパースしているか? | 間違っている | 出力ツールにリファクタリング |
| 出力フォーマットについてのプロンプト指示を書いているか? | ツールスキーマが処理すべき | 出力ツールにリファクタリング |
| ワークフローが50行を超えているか? | 何かが間違ったレイヤーにある | 監査して再配分 |
新しい機能の追加
ステップ1:スキルを書く
目的に応じてスキルの配置場所を決定します:
- エージェントプロンプト(
app/prompts/) — ワークフロー固有の指示。静的には.md、動的コンテキストには.md.erbを使用。 - コードベーススキル(
.claude/skills/) — ワークフロー間で共有される再利用可能なドメイン知識。 - DBスキル — UIを通じて管理されるユーザー設定可能な知識。
焦点:ペルソナ、プロセス、判断基準、ドメイン知識。出力フォーマットは記述しないでください。
ステップ2:出力コントラクトをSDKツールとして定義する
app/services/feature_tool_builder.rbを作成します。ツールスキーマがエージェントが生成するものを定義します。ハンドラーが副作用を実行します。
class FeatureToolBuilder
def self.build_submit_tool(record:, space:)
ClaudeAgentSDK.create_tool(
"submit_result",
"Submit your analysis results.",
{ type: "object", properties: { ... }, required: %w[...] }
) do |args|
# 副作用を実行、確認を返す
end
end
end
ステップ3:ワークフローを薄いグルーとして書く
app/services/workflows/feature_workflow.rbを作成します。レコードの作成、プロンプトの構築、run_agentの呼び出し、エラー処理、ステータスの更新のみを行うべきです。目標は50行未満です。
ステップ4:ジョブに接続する
app/jobs/feature_job.rbを作成します。ジョブがオプション(モデル、MCPサーバー、ツール、スキルディレクトリ)を構築し、ワークフローをインスタンス化してexecuteを呼び出します。mcp_servers / allowed_tools リストを手で組み立てる代わりに、ツールカタログを使ってツールグループにオプトインしてください。
class FeatureJob < ApplicationJob
def perform(record_id)
record = FeatureRecord.find(record_id)
options = build_options(record)
workflow = Workflows::FeatureWorkflow.new(options: options)
workflow.execute(record: record)
end
end
アンチパターン
| アンチパターン | なぜ間違っているか | 修正方法 |
|---|---|---|
| エージェントの自由テキストからJSONをパース | 脆弱:markdownフェンス、余分なテキスト、フォーマットの変動で壊れる | 型付き引数を持つ出力ツールを定義 |
| 出力フォーマットについてのプロンプト指示 | エージェントが無視する可能性のあるルールにトークンバジェットを浪費。コントラクトの重複 | ツールスキーマがフォーマット |
| ワークフロー内のビジネスロジック | オーケストレーションをドメインロジックに結合。ワークフローが肥大化 | 判断はスキルに、データコントラクトはツールに移動 |
| ERBなしの巨大なプロンプト | ランタイムコンテキスト(ユーザー設定、プロジェクト設定)を注入できない | 動的セクションにはlocalsを持つ.md.erbを使用 |
| ワークフローごとに複数の出力ツール | どのツールを呼ぶかエージェントが混乱する | ワークフローごとに1つの主要出力ツール |
| 複雑なビジネスロジックを持つツールハンドラー | テストが困難、インフラストラクチャに密結合 | ハンドラーをシンプルに:検証、副作用、確認 |
ワークフロー成熟度
成熟度は、各ワークフローがスキル、出力ツール、オーケストレーションをどれだけクリーンに分離しているかを追跡します。ツール接続は成熟度レベルに関わらず、すべてのワークフローで ツールカタログ 経由で統一されています。
| ワークフロー | 成熟度 | 出力コントラクト |
|---|---|---|
| PRReview | 高 | submit_review SDKツールがGitHub送信を処理 |
| Mention | 高 | エージェントがMCPツールを直接使用(GitHub、Linearコメント) |
| Automation | 高 | skip_messageツールでスキップ判断。AutomationMessageDeliveryServiceで配信 |
| CodeGeneration | 中 | submit_code_generation_resultツールは存在。PRメタデータは部分的にパース |
成熟度レベル:
- 高 — プロンプトが判断を担い、ツールがコントラクトを担い、ワークフローは薄いライフサイクルグルー。
- 中 — パターンに部分的に準拠。一部パースまたはフォーマット指示が残存。
- 低 — 判断、パース、オーケストレーションがワークフローに混在。