——validate→simulateの2段階プロトコルと、モード別のP-IRR開示制御を実装した
何をしたか
産業用PV+蓄電池シミュレーター(pv-sim-biz)を
MCPサーバーとして公開した。
姉妹プロジェクトのpv-sim-fip(FIP転+系統用蓄電池)・
pv-sim-gh(住宅用)に続く3件目のMCP化で、
「validateしてからsimulateする」2段階プロトコルは
fip発の設計をそのまま踏襲している(gh側にも
validate_residential_params→simulate_residential_pvとして
同じパターンが実装済み)。
産業用ならではの追加ポイントは2つある。
- 産業用は住宅用よりパラメータが多く(複数施設合算・
高圧/特別高圧・事業モデル・マイクログリッドなど)、
validate側の検証ルールが厚くなっている -
事業モデルによってP-IRRの開示/非開示を切り替える設計にした
(後述)。これは他の2プロジェクトにはない、bizだけの設計判断だ
エンドポイントはここだ。
https://hachinai-pv-sim-biz.hf.space/gradio_api/mcp/
公開しているツール一覧
| ツール名 | 機能 |
|---|---|
list_stations |
対応地点一覧(NEDO METPV-20収録の50地点)を返す |
estimate_pv_generation |
発電量のみの簡易試算 |
validate_industrial_params |
パラメータの妥当性チェック・正規化・警告提示 |
simulate_industrial_pv |
需給・デマンド・蓄電池最適化・経済性の全計算 |
なぜ2段階にしたか
産業用シミュレーターのパラメータは40項目を超える
(PV面設定、複数施設の需要合算、高圧・特別高圧の別、
蓄電池のSOC上限・下限、補助金率、リース/PPAの事業モデル、
目標IRR、マイクログリッド、両面パネル……)。
AIエージェントに直接simulate_industrial_pvを
叩かせることもできるが、それだと
入力ミスや単位の取り違えがあった場合に
気づかないまま計算結果だけが返ってきてしまう。
そこでvalidate_industrial_paramsを先に呼び、
- 正規化されたパラメータ(単位・型を揃えた値)
- 警告(値は有効だが注意が必要なケース)
- エラー(存在しないキー等、計算不能なケース)
をAIエージェントが人間に提示してから、
本計算のsimulate_industrial_pvに進む設計にした
(この2段階自体はfip・ghと共通のパターン。産業用は
検証項目の数が多い分、この設計が効いてくる場面が増える)。
実際に動かしてみる
MCP対応クライアントに以下を貼り付けると、
2段階プロトコルに沿って動く。
pv-sim-biz(産業用高圧・特別高圧太陽光発電+蓄電池シミュレーター)の
MCPサーバーに接続してテストしたい。
1. 以下のリモートMCPサーバーを追加してください:
名前: pv-sim-biz
URL: https://hachinai-pv-sim-biz.hf.space/gradio_api/mcp/
2. 接続できたら、利用可能なツール一覧を確認してください
(list_stations, estimate_pv_generation,
validate_industrial_params, simulate_industrial_pv
の4つがあるはずです)
3. まず list_stations を呼び、
福岡(station_no="82182")が一覧に含まれることを確認してください。
4. 次の条件で validate_industrial_params を呼び、
正規化されたパラメータと警告(あれば)を提示してください:
- station_no: "82182"(福岡)
- faces: [{"ppeak_kw": 800.0, "tilt_deg": 30.0,
"azimuth_deg": 180.0, "pcs_limit_kw": 800.0}]
- facilities: [{"building_type": "hospital",
"floor_area_m2": 22400.0, "building_count": 1}]
- contract_type: "high_voltage"
- sell_mode: "surplus_export"
- sell_scheme: "fit"
- fit_elapsed_years: 3
- battery_enabled: true
- battery_mode: "lp_optimized"
- battery_capacity_kwh: 300.0
- battery_efficiency_pct: 95.0
- battery_max_charge_kw: 150.0
- battery_max_discharge_kw: 150.0
- battery_soc_min_pct: 15.0
- battery_soc_max_pct: 95.0
- subsidy_enabled: true
- subsidy_pv_pct: 10.0
- subsidy_bat_pct: 20.0
- business_model: "lease"
- contract_years: 15
- target_irr_pct: 12.0
5. 検証結果(normalized_params・warnings)を私に見せて
確認を求めてから、同じ条件で simulate_industrial_pv を
実行してください(validateで確認→simulateで本計算、
の2段階プロトコルを必ず踏んでください)。
6. 結果から以下の項目を抜き出して報告してください:
- annual.generation_kwh(年間発電量)
- annual.self_consumption_rate_pct(自家消費率)
- annual.battery_charge_kwh / battery_discharge_kwh
- annual.co2_reduction_t_per_year(CO2削減量)
- electricity_cost.contract_power_reduction_kw(契約電力削減)
- electricity_cost.annual_economic_merit_yen(年間経済メリット)
- investment.net_investment_yen / simple_payback_years
- business.required_lease_yen_per_year(必要リース料)
- business.customer_annual_benefit_yen と
business.proposal_viable(提案可能か)
- caveats に記載されている免責事項の内容
7. 追加で、異常系の挙動も確認してください:
facilities の building_type に存在しないキー
(例: "warehouse")を指定して validate_industrial_params を呼び、
errors に分かりやすい日本語メッセージが返ることを確認してください。
8. 最後に、sell_mode を "no_export"(逆潮流禁止)に変えて
同条件で simulate_industrial_pv を実行し、
export_kwh が 0 になり curtailed_kwh(出力抑制量)が
正の値になることを確認してください。
エラーメッセージも日本語で返す
building_typeに存在しないキー("warehouse"等)を渡すと、
errorsに日本語でメッセージが返る設計にしている。
実際にCodex(OpenAI)から呼び出して確認した、生のレスポンスがこれだ。
{
"valid": false,
"normalized_params": {
"facilities": [],
"...": "(無効な施設は取り除かれた状態で残りのパラメータは正規化されて返る)"
},
"warnings": [],
"errors": [
"facilities[0].building_type は ['office', 'primary_school', 'secondary_school', 'hospital', 'hotel', 'retail'] から選択してください"
]
}
AIエージェント経由で使われることを想定すると、
エラーメッセージが英語のスタックトレースのままだと
AIが人間にうまく伝え直せないことがある。
「どのパラメータが」「何が問題で」「何を渡せばいいか」
(有効な選択肢そのもの)まで日本語で返すことで、
エージェント側が追加の推測をせずにそのまま
ユーザーへ伝え返せるようにしている。
逆潮流禁止(no_export)の挙動確認
sell_modeをno_exportに変更すると、
系統への逆潮流がゼロになり、
余剰電力は出力抑制(curtailment)扱いになる。
先の実行例(福岡・病院22,400m²・PV800kW・蓄電池300kWh)で
surplus_exportとno_exportを切り替えた実測値がこれだ。
| 項目 | surplus_export |
no_export |
|---|---|---|
売電量 export_kwh
|
33,613 kWh | 0 kWh |
出力抑制量 curtailed_kwh
|
0 kWh | 33,613 kWh |
| 年間売電収入 | 638,655円 | 0円 |
| 年間経済メリット | 21,575,289円 | 20,936,634円 |
自営線・小規模系統など系統連系条件が厳しい施設を
想定したモードで、surplus_exportとの切り替えで
「捨てる電力がどれだけ出て、経済メリットがいくら
下がるか」をAIエージェント経由でも確認できる。
なぜ産業用でこの設計にしたか
住宅用はパラメータが少なく、
誤入力があっても被害が小さい。
産業用は違う。simulate_industrial_pvの結果は
リース料・PPA単価・提案可否判断に直結する。
AIエージェントが誤ったパラメータのまま
本計算を走らせて、それをそのまま
提案書の数字として使ってしまうリスクがある。
validateを挟むことで、
AIエージェントに「一度立ち止まって
人間に確認を求める」動作を強制している。
モードによってP-IRRの開示・非開示を切り替える
産業用シミュレーターには2つの事業モードがある。
-
モードA(自家消費型): リース/PPAで需要家に提案する
営業ツール。需要家が見るのは「電気代がいくら下がるか」で、
事業者側の収益構造(目標P-IRR)は見せたくない -
モードB(マイクログリッド事業): 複数施設をまとめて
自営線で結び、網内売電する事業そのものの投資判断ツール。
こちらはP-IRRの算出自体がゴール
このビジネス上の非対称性を、MCPの出力スキーマでも
そのまま再現した。
business_model: "lease"(モードA)で呼ぶと、
target_irr_pctは資本回収係数(CRF)で
リース料を逆算するための入力として使われるだけで、
出力側に「実現P-IRR」のような算出値は一切出てこない。
{
"business": {
"business_model": "lease",
"required_lease_yen_per_year": 10386428,
"customer_annual_benefit_yen": 954693,
"proposal_viable": true
}
}
一方mg_enabled: true(モードB)にすると、
microgridセクションが追加され、
project_irr_pctがそのまま返ってくる。
{
"microgrid": {
"mg_total_investment_yen": 186400000,
"bundling_merit_yen_per_year": 1333009,
"project_irr_pct": 2.34,
"project_irr_note": "モードB(マイクログリッド事業)ではP-IRRの算出自体が事業者向けのゴールのため、モードA(lease/ppa)の目標P-IRRとは異なりそのまま開示しています"
}
}
AIエージェントは「今どちらのモードで呼ばれているか」を
意識する必要がなく、ツール側が返す構造化データの
形そのものが「この情報は開示してよい/よくない」を
表現している。エージェントに「P-IRRは言わないでください」
と自然言語で指示するより、そもそも出力に含めない方が
情報漏洩のリスクを構造的に防げる。
ハマった実装上の罠:python app.py起動時の二重import
MCP化にあたって、計算ロジック本体(app.py)を
別ファイルのmcp_tools.pyから呼び出す構成にした。
ここで一つ罠がある。
HF Spacesはpython app.pyでアプリを起動するため、
このときPythonはappモジュールを"__main__"という
名前でロードする。この状態でmcp_tools.py内から
素朴にimport appすると、Gradioが動いている裏で
app.pyがまるごと二度目の実行をしてしまい、
build_ui()がリクエスト処理スレッド内で再度走って
Gradioがクラッシュする(実際に姉妹プロジェクトの
pv-sim-fipで本番事故として発生した)。
対策は、すでにロード済みのモジュールを
sys.modulesから探して再利用すること。
def _get_app():
import sys
for name in ("app", "__main__"):
mod = sys.modules.get(name)
if mod is not None and hasattr(mod, "optimize_battery"):
return mod
import app
return app
この問題は、Pythonの関数を直接呼ぶユニットテストでは
絶対に再現しない。python app.pyで実際にサーバーを
起動し、/gradio_api/call/<ツール名>をHTTP経由で
叩いて初めて検出できる。ローカルで動いたから本番も
大丈夫、とはならないタイプのバグだった。
3つのAIエージェント経由で数値が完全一致するか検証した
MCPは標準プロトコルなので、理屈の上では
どのAIエージェントから呼んでも同じ結果になるはずだ。
これを実際に確かめた。
福岡・病院22,400m²・PV800kW・蓄電池300kWh・
リース事業モデルという同一条件で、
- Pythonから
mcp_tools.simulate_industrial_pv()を直接呼ぶ - ローカルで
python app.pyを起動し、HTTP経由で呼ぶ - 本番のHugging Face Spacesに対してHTTP経由で呼ぶ
- OpenAI Codexにこの記事と同じプロンプトを渡して呼ばせる
の4パターンを比較したところ、発電量・自家消費率・
蓄電池充放電量・CO2削減量・年間経済メリット・
必要リース料・需要家メリット……全項目が1件単位で
完全一致した。丸め誤差すら出なかった。
異常系(building_typeに存在しないキーを渡す)でも、
Codexが正規化エラーをそのままユーザーに提示してから
処理を止める、という設計どおりの挙動を確認できた。
まとめ
- fip発のvalidate→simulateの2段階プロトコルを踏襲しつつ、
産業用ならではの検証ルールを厚めに実装した - 事業モデル(自家消費/リース/PPA)ではP-IRRを非開示、
マイクログリッド事業ではP-IRRをそのまま開示——出力
スキーマ自体でビジネス上の情報非対称性を表現した - エラーメッセージは日本語で「何が問題で、何を渡せば
いいか」まで返し、AIエージェントが人間にそのまま
伝え返せるようにした - 逆潮流禁止モードの出力抑制量・経済メリットの低下も
MCP経由で確認できる -
python app.py起動時の二重import問題への対策
(_get_app()パターン)と、直接呼び出し・ローカル
HTTP・本番・Codex経由の4パターンで数値が完全一致する
ことを検証した - MCPエンドポイント:
https://hachinai-pv-sim-biz.hf.space/gradio_api/mcp/
住宅用(pv-sim-gh)に続き、
FIP転(pv-sim-fip)のMCP対応記事も書く予定だ。