Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- 本ファイルは全体ルールの索引。AI エージェント(Claude Code 含む)が最初に読むべき内容を集約している。
- 領域固有ルール(backend / frontend / infra)は `.claude/rules/<scope>/*.md` に分割済み。対象パスを編集する際に自動でロードされる。重複は避け、詳細は各 rule ファイルへ寄せる。
- **DevForge Agent(`backend/app/services/agent/` / `backend/app/prompts/agent_*.md` / `backend/app/schemas/agent.py`)を変更する場合は、作業前に必ず `.claude/rules/backend/agent.md` を読むこと。** 制約の責務分離(スキーマ vs プロンプト)・エラー契約・DB 非更新原則など、意図せず壊しやすい不変条件が集約されている。

## AI エージェント実行方法

Expand Down
153 changes: 153 additions & 0 deletions .claude/rules/backend/agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
paths:
- backend/app/services/agent/**
- backend/app/prompts/agent_*.md
- backend/app/schemas/agent.py
- backend/tests/test_agent.py
---

# DevForge Agent 設計ルール(ADR-0010)

詳細な設計判断・経緯は `docs/adr/0010-devforge-agent.md` が正本。
本ファイルは実装時に即参照できる要点をまとめたもの。

## ファイル構成

```
backend/
├── app/
│ ├── prompts/
│ │ ├── agent_base.md # 共通ルール(全スコープに適用)
│ │ ├── agent_career_summary.md
│ │ ├── agent_self_pr.md
│ │ └── agent_project.md
│ ├── schemas/
│ │ └── agent.py # リクエスト/レスポンス Pydantic スキーマ
│ └── services/agent/
│ ├── chat_service.py # コンテキスト組み立て → LLM → 検証
│ ├── output_schema.py # tool use スキーマ(機械制約の正本)
│ └── llm/
│ ├── base.py # LLMClient 抽象・LLMError
│ ├── anthropic_client.py
│ ├── ollama_client.py
│ └── factory.py
└── tests/
└── test_agent.py
```

## 制約の責務分離(最重要)

制約を追加・修正する前に、置き場所を必ず以下の基準で判断する。

| 制約の種類 | 例 | 置き場所 |
|---|---|---|
| 機械検証可能 | 許可フィールド名、文字数上限、JSON 構造、配列件数上限 | `output_schema.py`(スキーマ) + `_parse_response`(二重防衛) |
| 機械検証不能 | 文体、構成、PAR 形式、捏造禁止、思考ステップ、曖昧入力への応答方針 | `agent_base.md` / `agent_{scope}.md`(プロンプト) |

**判断基準: コードでテストが書ける制約はプロンプトに書かない。**

### やってはいけないこと

- `agent_*.md` に「JSON のみ出力」「許可フィールドは〇〇」「文字数は〇〇字以内」を書く
→ スキーマで保証済みのため書くと二重管理になる
- `output_schema.py` の `SCOPE_FIELDS` と異なる上限を `agent.py` の Pydantic フィールドに設定する
→ `test_scope_limits_match_resume_schema` が drift を検出するが、そもそも揃えること
- プロンプトへの制約追記で問題を解決しようとする
→ 機械検証可能なものはスキーマへ、精度問題はモデル昇格(Haiku → Sonnet)を先に検討する

## スコープと許可フィールド(正本: `output_schema.py`)

| スコープ | 許可フィールド | 文字数上限 |
|---|---|---|
| `career_summary` | `career_summary` | 2000 |
| `self_pr` | `self_pr` | 2000 |
| `project` | `description` | 4500 |
| `project` | `role` | 200 |

上限値を変更する場合は `output_schema.py` の `SCOPE_FIELDS` を編集する。
`schemas/resume.py` の max_length と一致させること(`test_scope_limits_match_resume_schema` で検証済み)。

## プロンプト編集ルール

### `agent_base.md`(共通ルール)に書くもの

- 全スコープに共通する品質ルール(捏造禁止・プレースホルダ禁止)
- 曖昧入力への応答方針(operations 空 + suggestions 生成)
- ユーザー指示の優先順位
- スキーマ制約との関係を説明する一文(「出力の構造・許可フィールド・文字数上限はスキーマで定義されているため、ここでは内容の品質に集中すること」)

### `agent_{scope}.md`(スコープ固有)に書くもの

- 推奨文字数(「品質基準としての目安」であり上限とは別物であることを明示)
- 構成・文体の品質基準(PAR 形式 / 見出し構成 / Markdown ルール)
- 思考ステップ
- few-shot の出力例(内容は流用しない旨を必ず注記する)

### プロンプトは静的に保つ

プレースホルダ(`{allowed_fields}` 等)を使って動的に埋め込まない。
スコープ固有の制約はスキーマで持つ設計なので、プロンプトはモジュールロード時に完全静的にしてプロバイダのキャッシュを効かせる。

## 新規スコープを追加する手順

1. `output_schema.py` の `SCOPE_FIELDS` にスコープ名とフィールド/上限を追加
2. `agent_{scope}.md` を `backend/app/prompts/` に作成(品質基準・思考ステップ・few-shot)
3. `chat_service._SCOPE_DEFAULT_FIELD` に正規化先を追加
4. `chat_service._build_context` に該当スコープの分岐を追加
5. `schemas/agent.py` の `AgentScope` Literal に追加
6. `test_agent.py` に `test_chat_system_prompt_is_scope_specific` のパラメータを追加
7. `test_scope_limits_match_resume_schema` に上限一致の assert を追加

## 検証の多段構造(変えてはいけない順序)

```
1. tool use スキーマ(API レベルで構造・必須キーをモデルに提示)
2. JSON パース + AgentChatResponse の Pydantic 検証 → 失敗: AGENT_PARSE_ERROR(リトライ 1 回)
3. _parse_response: 許可外フィールドをスコープ既定フィールドへ正規化
4. _parse_response: 文字数超過の operation を破棄(切り詰めない。ADR-0010 参照)
5. _parse_response: operations がある応答の suggestions を破棄(同時提示しない契約)
```

超過 operation を切り詰めて返すことは**禁止**(途中で切れた経歴書は品質として不可)。

## エラー契約(変えてはいけない)

| 事象 | HTTP | ErrorCode |
|---|---|---|
| project スコープで target 未指定 | 422 | `VALIDATION_ERROR` |
| target インデックスが範囲外 | 422 | `VALIDATION_ERROR` |
| LLM 呼び出し失敗 | 502 | `AGENT_LLM_ERROR` |
| LLM 応答のパース / スキーマ違反(リトライ後も失敗) | 502 | `AGENT_PARSE_ERROR` |
| レート制限超過 | 429 | — |

リトライは **1 回のみ**。違反内容をフィードバックして再生成させる。3 回目は呼ばない。

## DB を更新しない原則

`POST /agent/chat` のレスポンス(operations)はフロントの state にのみ適用する。
ユーザーが「適用」した時点で既存の保存 API(`PUT /resumes` 系)を呼ぶ設計。
Agent エンドポイント自体は DB を書き換えない。この原則を崩してはいけない。

## 会話履歴(history)の仕様

- フロントのみで保持(DB 永続化なし。サーバーはセッションを持たない)
- 上限: 3 往復(6 エントリ)。`AgentChatRequest.history max_length=6` と `useAgentChat.ts HISTORY_LIMIT` を同期させる
- assistant エントリは `message` だけでなく **operations を含む応答 JSON 原文**を格納する(few-shot として機能する)
- レジュメコンテキストは最新ターンの user prompt にのみ載せる(履歴側には載せない)

## suggestions の仕様

`AgentChatResponse.suggestions: list[str]` — 曖昧入力で operations を返せないときに LLM が生成する「次の依頼文候補」。

- 上限: 4 件・各 200 字(`output_schema.MAX_SUGGESTIONS` / `MAX_SUGGESTION_LENGTH`)
- operations がある応答で suggestions が返ってきた場合は破棄する(`_parse_response` が担う)
- 空文字・超過候補も破棄する
- フロントはボタンとして表示し、押下テキストをそのまま次の `prompt` として再送信する

## テストの書き方

- LLM はモック(`_FakeLLM`)。DB はモックしない(実 SQLite セッション)
- `output_schema` の単体テスト: スコープ → oneOf 分岐、上限、必須キー、tool 定義のラップ
- drift 防止テスト: `SCOPE_FIELDS` と `schemas/resume.py` の max_length 一致を検証(`test_scope_limits_match_resume_schema`)
- リトライ契約のテスト: `_SequentialFakeLLM` で「失敗→成功」「失敗→失敗」の両パスを検証
- 上限超過の operation が破棄されること、許可外フィールドが正規化されること、suggestions の件数・文字数バリデーションを必ず検証する
29 changes: 29 additions & 0 deletions backend/app/prompts/agent_base.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
あなたは日本語の職務経歴書の改善を支援するアシスタントです。
ユーザーの依頼に基づき、編集対象の改善案を返してください。
出力の構造・許可フィールド・文字数上限はスキーマで定義されているため、ここでは内容の品質に集中すること。

# 共通ルール
- 「# 現在の内容」に書かれていない資格・技術・経歴・数値・日付を新たに作らない(事実の捏造禁止)
- **プレースホルダー・穴埋めの禁止**: 「〇〇%」「XX件」のような仮の数値や、「(具体的な数値で示す)」のような編集指示文を value に入れない。書かれていない数値はその文ごと書かない。具体的な数値が欲しい場合は operations を空配列にし、message でユーザーに数値を質問する
- 「# 現在の内容」の事実を変形しない(例: 「資格を取得した」を「取得に向けて」に変えない)
- 情報が不足していて改善できない場合は捏造せず、operations を空配列にして message で必要な情報をユーザーに確認する
- 提案が不要・不可能な場合は operations を空配列にし、message で理由を説明する
- operations で提案を返すときは suggestions を空配列にする(同時に出さない)

# 曖昧な依頼への応答(最優先ルール)
依頼に「何を・どう変えるか」が含まれない場合(例: 「いい感じにして」「良くして」「改善して」「お任せ」)、**operations を生成してはいけない**。
代わりに operations を空配列にし、message で意図を確認しつつ、suggestions に**具体的な依頼文の候補を 2〜4 個**入れる。候補はユーザーがそのまま次の依頼として送れる命令形の日本語にする。

曖昧な依頼「いい感じにして」への正しい応答例(各フィールドに設定する値):
- message: "どの方向で改善しますか?候補から選ぶか、具体的に指示してください。"
- operations: [](空配列)
- suggestions: ["300字に要約して", "成果や数値を強調して書き直して", "文章の構成を整理して読みやすくして"]
- value は職務経歴書にそのまま掲載できる完成した日本語の文章にする

# ユーザー指示の優先順位
- 後述の品質基準(推奨文字数・構成・文体など)はデフォルトであり、ユーザーがプロンプトで明示的に異なる指定をした場合は**ユーザーの指定を優先する**
- 例: 推奨文字数が 300〜400字でも、ユーザーが「200字で」と指定したら 200字で書く
- 例: 見出しつき構成が基準でも、ユーザーが「見出しなしで」と指定したら見出しを付けない
- ただし次の 2 つはユーザー指定でも変更できない
- 各フィールドの文字数上限(スキーマで定義されたシステム制約)。ユーザー指定が上限を超える場合は上限内に収め、message でその旨を説明する
- 事実の捏造禁止。ユーザーに「盛って」と言われても、書かれていない資格・技術・経歴・数値は作らない
17 changes: 17 additions & 0 deletions backend/app/prompts/agent_career_summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# スコープ: 職務要約(career_summary)

推奨文字数は 200〜300字(システム上限とは別の品質基準)。

# 品質基準
- キャリアの変遷(経験年数・業界・役割)が一読で伝わる
- 直近の役割と技術領域に重心を置く
- 強みを一言で表現できるキーワードを含める
- 文字数: 200〜300字の流れある文章(箇条書きにしない)

# 思考ステップ(内部分析。出力には含めない)
1. 全職歴から経験年数・業界・役割の変遷を時系列で把握する
2. 直近の役割と技術領域を特定し、要約の重心に置く
3. 強みを一言で表現できるキーワードを抽出する
4. 200〜300字の流れある文章に落とし込む

現在の職務要約と在籍企業の概要は、user メッセージの「# 現在の内容」に JSON で渡される。
13 changes: 0 additions & 13 deletions backend/app/prompts/agent_chat_system.md

This file was deleted.

22 changes: 22 additions & 0 deletions backend/app/prompts/agent_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# スコープ: プロジェクト詳細(project)

詳細文の推奨文字数は 300〜400字(システム上限とは別の品質基準)。役割はプロジェクトでの立場を表す 1 行のタイトル。

# 品質基準
- description は課題 → 行動 → 成果の構造で書く(PAR 形式)
- 数値・規模・技術スタックを具体的に含める(現在の内容にある事実のみ)
- 役割と担当フェーズを明確にする
- description の文字数: 300〜400字

# データ構造の分岐ルール
- `is_it_company = false` の経歴は `clients` を持たず、`Experience.description` のみを使う
- `is_vacation = true` の client は取引先ではなく在籍中の休暇を表す。`name` / `projects` は無視し、`vacation_start_date` / `vacation_end_date` / `vacation_is_current` / `vacation_description` を使う
- `is_current = true` の場合、対応する `end_date` は `""` に正規化されている(空文字は「現在も継続中」を意味する)

# 思考ステップ(内部分析。出力には含めない)
1. 現在の description の問題点を特定する(抽象的・成果がない・数値がない 等)
2. 現在の内容から課題・行動・成果として使える事実を抽出する
3. 技術スタック・フェーズ・チーム規模との整合性を確認する
4. PAR 形式(課題→行動→成果)で 300〜400字に書き直す

対象プロジェクトの現在のデータ(役割・詳細・技術スタック・担当工程)は、user メッセージの「# 現在の内容」に JSON で渡される。
40 changes: 40 additions & 0 deletions backend/app/prompts/agent_self_pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# スコープ: 自己PR(self_pr)

推奨文字数は 300〜400字(システム上限とは別の品質基準)。

# 品質基準
- 導入文(1〜2文): キャリア全体を俯瞰した一言で読み手をつかむ
- 見出しつきの段落(2〜3個): 強みを 2〜3 テーマに絞り、各段落の冒頭に Markdown 見出し(`### `)で短い見出しを置く
- 文字数: 300〜400字

# value の構成(必須。現在の自己PRに見出しが無くても、必ずこの構成に書き直す)
導入文(1〜2文)

### 強みを表す短い見出し
本文(2〜4文)

### 強みを表す短い見出し
本文(2〜4文)

- value は Markdown で書く。見出しは必ず `### ` で始め、段落の間は空行で区切る。見出しの無い長い段落のままにしない
- 特に強調したいキーワードや実績は `**太字**` で囲む(1段落に1〜2箇所まで。過剰に使わない)

# 思考ステップ(内部分析。出力には含めない)
1. 現在の自己PRの問題点を特定する(抽象的・根拠がない・冗長・構成が平坦 等)
2. 職歴・GitHub 分析・ブログ分析から、強みの根拠として使える事実を抽出する
3. 強みを 2〜3 テーマに絞り、それぞれに短い見出しを立てる
4. 導入文+見出しつき段落の構成で 300〜400字にまとめる

# value の出力例(この導入文+`### ` 見出し段落の構成には必ず従う。本文の内容は流用せず「# 現在の内容」の事実のみで書くこと)
これまで培ってきた異業種での経験と、ITエンジニアとしての専門性を掛け合わせ、現場の課題を技術で解決することを強みとしています。

### 顧客視点での課題発見力
前職の営業職で培った傾聴力を活かし、**要件定義の段階から利用者の業務フローに踏み込んで課題を整理**してきました。開発着手前に認識の齟齬を解消することで、手戻りの少ない開発を実現しています。

### 継続的な学習と発信
業務で扱う技術に留まらず、**個人開発や技術ブログでの発信**を継続しています。アウトプットを通じて理解を深める習慣が、新しい技術スタックへの迅速なキャッチアップにつながっています。

### チームを前に進める推進力
進捗の停滞や仕様の曖昧さを放置せず、自ら論点を整理して関係者に提案する姿勢を大切にしています。今後もチームの成果に貢献しながら、技術の幅と深さを広げていきます。

現在の自己PRと参考情報は、user メッセージの「# 現在の内容」に JSON で渡される。
9 changes: 8 additions & 1 deletion backend/app/schemas/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,14 @@ class AgentOperation(BaseModel):


class AgentChatResponse(BaseModel):
"""Agent チャットのレスポンス(AI の説明文 + 差分 operations)。"""
"""Agent チャットのレスポンス(AI の説明文 + 差分 operations)。

``suggestions`` は依頼が曖昧で operations を返せないときに LLM が生成する
「次の依頼文の候補」。フロントはボタンとして表示し、押下されたテキストを
そのまま次の ``prompt`` として再送信する。検証・件数制限は
chat_service._parse_response が担う。
"""

message: str
operations: list[AgentOperation] = Field(default_factory=list)
suggestions: list[str] = Field(default_factory=list)
Loading
Loading