#はじめに、
この記事は、chatgpt workを使って基本自動生成しています。
その上で不足記事を追加・修正した上で、動作確認を行っています。
この企画はもともと公式のMLFlowのチュートリアルがちょっとイケてなかったので、chatgpt workを使ってもっと簡単なチュートリアルを作成出来るか検証してみようと思い立ったところから開始しました。
そしてせっかくやるなら面白いテーマでやろうと思い、「生成AIで面白いなろう系小説第一話を書けるのか?」
という実験も兼ねています。
前回までの振り返り
第一回目は、まずは荒いプロンプトで実行、トレース出来るところまで確認しました。
第二回目は、まずは荒い評価基準を導入し、出力した第一話を評価出来るところまで確認しました。
第二の一回目は、急遽評価基準の精度を改善することにしました。
ここがかなり試行錯誤した上で、ようやく及第点(今のところ)の評価基準精度になりました。
ここから自動生成部分です。
MLflow GUIで始めるプロンプト改善 #3:失敗例からPrompt Version 2を作る
STEP2では、Prompt Version 1を3種類の小説企画へ適用し、生成結果を評価しました。STEP2-1では、評価が甘すぎる問題と、厳格化によってプロローグを不当に低く評価する問題を確認し、Judgeを次の3層へ分離しました。
- 入力仕様への適合:Yes/No
- 作品としての品質:8項目の1〜5点
- 作風・構成上の特徴:合否に使わない診断値
校正後のJudgeで代表的なVersion 1生成文を再評価すると、Quality 8項目の平均は2.88でした。多くは3点で、information_controlは2点です。これは「壊れていて読めない文章」ではなく、「必要要素は並んでいるが、案内役と内なる声が都合よく情報を供給し、人物・場面・展開がまだ一体化していない文章」と解釈できます。
STEP3では、この失敗を根拠にPrompt Version 2を作ります。Dataset、モデル、Judgeは変更しませんが、生成仕様は「1,200文字以内」から「5,000文字を目安とし、場面に必要なら最大8,000文字」へ変更します。Version 2が本当に改善したかは、次のSTEP4で総合比較と文字数を揃えた対照実験の両方から確認します。
この記事では「良くなった」という結論を先に置きません。STEP3の成果は改善仮説をPrompt Versionとして記録し、評価可能な出力を作るところまでです。採用判断はSTEP4で行います。
今回のゴール
- Version 1の失敗をPromptで変更可能な原因へ分解する
- Judgeの文章をそのまま生成Promptへコピーしない
- 第一話の役割を
episode_roleとepisode_intentで指定する - 一つの中心場面へ絞ったPrompt Version 2をGUIで登録する
- Version 2を同じ3企画へ実行し、MLflowのTraceで確認する
- STEP4で比較できる状態を作る
1. STEP2-1の教訓を改善方法へ変換する
最初の二値Judgeは、要素が少し存在するだけでほとんどをPassにしました。その反動で共通尺度を厳しくすると、今度は「説明的」「受け身」という一つの弱点が全項目へ波及しました。
この経験から、評価点を上げる条件をそのままPromptへ列挙する方法は避けます。
悪い改善例:
- opening_engagementを4点以上にする
- characterizationを4点以上にする
- emotional_credibilityを4点以上にする
- reader_momentumを4点以上にする
LLMにはJudgeの採点結果が見えません。また、このような指示は「フックを置く」「感情を書く」「次の目標を書く」というチェックリスト消化を促します。Version 1で起きた問題を再生産する可能性があります。
代わりに、低評価を生んだ本文上の原因と、Promptで変える操作を対応付けます。
| Version 1で確認した問題 | Prompt Version 2で変える操作 |
|---|---|
| 転生前、転生、案内役、能力、次の任務を順番に消化する | 第一話の中心場面を一つに限定する |
| 案内役が世界名、制度、転生者の存在をまとめて話す | 主人公が必要とした情報だけを観察・会話から得る |
| 内なる声が能力名と効果を説明する | 変化を観察し、試し、暫定的に推測させる |
| 「驚いた」「興奮した」で感情変化を済ませる | 身体反応、迷い、判断、行動の変化で示す |
| 主人公の経歴が冒頭説明だけで終わる | 過去の経験を現在の選択へ影響させる |
| 「村を助ける」「冒険が始まる」と予定を宣言して終わる | 選択の結果、危機、疑問のいずれかを残す |
future_plotの要素を第一話で先取りする |
将来要素は一つの兆候だけに制限する |
ここで重要なのは、説明、受け身、回想をすべて禁止しないことです。STEP2-1では、回想中心のプロローグや受け身の主人公でも作品として成立すると確認しました。禁止するのは形式ではなく、「物語上の必要がない情報を、入力項目の消化だけを目的に並べること」です。
2. 変更するものと固定するものを決める
比較実験では、一度に複数の条件を変えないようにします。
| 条件 | STEP3での扱い |
|---|---|
| Prompt | Version 1から2へ変更する |
| 入力する3企画 | 変更しない |
| 生成モデル |
gpt-4o-miniで固定する |
| 文字数 | 5,000文字を目安、必要なら最大8,000文字へ変更する |
max_output_tokens |
12,000へ変更し、8,000文字を途中で切らないようにする |
| Requirement Judge | STEP2-1から変更しない |
| Quality Judge | STEP2-1から変更しない |
| Diagnostic Judge | STEP2-1から変更しない |
例示した作品では、短いプロローグもある一方、本編導入はおおむね数千文字で構成されています。今回のタスクは短いプロローグではなくmain_story_openingです。世界の状況、主人公固有の反応、能力の観察と試行、場面の結果までを1,200文字へ押し込むと、説明の省略かチェックリスト的な高速展開を招きます。
そこでVersion 2は5,000文字を標準的な目安とし、その場面に必要な場合だけ8,000文字まで許容します。8,000文字を書くことを義務にはしません。内容が5,000文字程度で完結するなら、不要な回想や設定説明で水増ししないよう指示します。
この変更により、Version 1と2の差はPrompt文だけの効果ではなく、構成指示と文字数条件を含む「生成仕様全体」の効果になります。因果を切り分けるため、STEP4ではVersion 2を短文条件でも実行する対照を追加します。
LLMの生成には揺らぎがあります。STEP3で一つの出力が良く見えても、Prompt改善の証明にはなりません。ここでは同じ3企画を実行してTraceを残し、STEP4で同じDatasetとJudgeによる回帰評価を行います。
3. 第一話の役割を入力へ追加する
STEP2-1では、「第一話」という話数だけから異世界知識、能力発現、明示的な目標を一律に要求すると、プロローグを誤評価すると分かりました。
そこで生成入力にも次の2項目を追加します。
{
"episode_role": "main_story_opening",
"episode_intent": "異世界到着後の最初の出来事を描き、主人公の当面の問題を立ち上げる",
}
今回の3企画は、本編前のプロローグではありません。episode_goalに指定された出来事を扱う本編導入なので、すべてmain_story_openingとします。
この2項目は作品を同じ型へ固定するためではなく、PromptとJudgeへ「この話がシリーズ内で何を担当するか」を共有するために使います。将来プロローグを生成する場合は、episode_role="prologue"と、そのプロローグで準備する問いや転換をepisode_intentへ指定します。
Version 1のPromptにはこの変数がありませんが、余分な入力値を渡しても既存のプレースホルダー展開には影響しません。STEP4では同じ入力レコードをVersion 1と2の両方へ渡せます。
4. Prompt Version 2を設計する
Version 2では、モデルに完成形の筋書きを細かく指定しません。執筆前に内部で構成を整理させ、出力は小説本文だけにします。
改善の中心は次の5点です。
-
episode_roleとepisode_intentを明示する - 第一話を一つの中心場面へ絞る
- 主人公の経歴を現在の選択へ接続する
- 情報と能力を観察・試行・結果から示す
- 将来の予定ではなく、現在の選択が生んだ変化で終える
「一つの中心場面」は、場所を絶対に移動してはいけないという意味ではありません。読者が追う主要な問題を一つにする指示です。短い導入や回想は、その問題を強める場合だけ使用できます。
5. GUIでPrompt Version 2を登録する
MLflow GUIでExperiment STEP1:MVPプロンプトの作成と実行を開き、左メニューから「Prompts」を選択します。
step01-narou-episode-writerを開き、新しいVersionを作成します。Prompt名を変えるのではなく同じPromptへVersion 2を追加することで、Version 1との履歴を一つの画面で追えます。
Commit messageには次を入力します。
STEP3: 一場面への集中と情報提示の改善
Prompt本文は次のように変更します。
あなたはWeb小説の作家です。次の企画から、シリーズ内で指定された役割を担う第一話を書いてください。
ジャンル: {{genre}}
主人公: {{protagonist}}
主人公の能力: {{ability}}
世界観: {{world}}
第一話の到達点: {{episode_goal}}
今後の展開: {{future_plot}}
話の役割: {{episode_role}}
この話の狙い: {{episode_intent}}
執筆前に、中心となる問題と、その問題によって主人公の認識または状況がどう変わるかを内部で整理してください。整理内容は出力しません。
執筆条件:
- 日本語の小説本文だけを出力する
- 5,000文字を目安とし、場面を成立させるために必要な場合だけ8,000文字まで許容する
- 5,000文字で場面が成立する場合は、8,000文字へ近づけるための回想・設定説明・反復を追加しない
- 第一話の中心となる問題を一つに絞り、入力項目を順番に紹介する構成にしない
- 冒頭では、主人公が現在直面している具体的な違和感、危機、欲求のいずれかを示す
- 主人公の経歴は説明だけで置かず、現在の知覚、判断、迷い、選択のいずれかへ影響させる
- 世界の名称や制度は、主人公がその場で必要とする範囲だけを、観察、失敗、目的のある会話から示す
- 案内役に世界設定をまとめて説明させない
- 能力を扱う場合は、きっかけ、観察可能な変化、主人公の試行、暫定的な推測の順で描く
- 内なる声やシステムメッセージに、能力名と完全な効果を直接説明させない
- 感情は感情語だけで済ませず、身体反応、注意の向き、迷い、行動の変化のいずれかで示す
- 今後の展開は完了させず、この話に必要な兆候を一つだけ使う
- 終わりでは「冒険が始まる」と説明せず、主人公の選択が生んだ結果、危機、疑問のいずれかを残す
すべての条件を別々の出来事で消化しないでください。人物の反応、場面の変化、情報提示が同じ出来事の中で働くように構成してください。
Version 1より指示は増えていますが、評価項目の名前や点数目標は含めていません。Judgeを攻略させるのではなく、失敗した生成過程を具体的に変更するためです。
Promptを長くすれば必ず良くなるわけではありません。指示が競合すると、文章が硬くなったり、条件を守ることが目的になったりします。Version 2も仮説であり、STEP4の評価結果によっては削る必要があります。
今回はガラッと変わったので、あまり比較にはならなかったようだ。
6. NotebookからVersion 2を読み込む
新しいNotebook step3_improve_prompt_v2.ipynbを開きます。接続設定はSTEP2までと同じです。
from dotenv import load_dotenv
import mlflow
from openai import OpenAI
load_dotenv()
TRACKING_URI = "http://127.0.0.1:5000"
EXPERIMENT_NAME = "STEP1:MVPプロンプトの作成と実行"
PROMPT_URI_V2 = "prompts:/step01-narou-episode-writer/2"
MODEL = "gpt-4o-mini"
mlflow.set_tracking_uri(TRACKING_URI)
mlflow.set_experiment(EXPERIMENT_NAME)
mlflow.openai.autolog()
client = OpenAI()
Version番号を2へ固定して読み込みます。STEP5でAliasを導入するまでは、意図しないPrompt変更を避けるため明示的なVersion番号を使用します。
生成関数では、どのPrompt URIを使ったかがTraceから分かるよう引数へ含めます。
@mlflow.trace(name="generate_first_episode_v2")
def generate_first_episode_v2(story: dict[str, str]) -> str:
prompt = mlflow.genai.load_prompt(PROMPT_URI_V2)
response = client.responses.create(
model=MODEL,
input=prompt.format(**story),
max_output_tokens=12000,
)
return response.output_text
7. 同じ3企画でVersion 2を実行する
STEP2と同じ3企画へ、episode_roleとepisode_intentだけを追加します。
episode_context = {
"episode_role": "main_story_opening",
"episode_intent": "異世界到着後の最初の出来事を描き、主人公の当面の問題を立ち上げる",
}
# NotebookではSTEP2と同じ3件をリストとして定義し、
# 各dictの末尾で **episode_context を展開する
story_inputs_v2 = [
{
"genre": "異世界転生ファンタジー",
"protagonist": "ブラック企業で働いていた28歳の会社員、佐藤蓮",
"ability": "触れた道具の性能を最大化する能力",
"world": "魔法と冒険者ギルドが存在する世界",
"episode_goal": "異世界へ転生し、自分の能力に気づく",
"future_plot": "壊れた道具を修理し、仲間を増やしながら辺境の町を発展させる",
**episode_context,
},
# 残り2件もSTEP2と同じ企画を指定する
]
version2_outputs = []
for story in story_inputs_v2:
output = generate_first_episode_v2(story)
version2_outputs.append(output)
print(story["genre"], len(output))
ここでは5,000文字との差と、8,000文字以内かを記録します。5,000文字を超えたこと自体は失敗ではありません。場面を成立させるために必要な描写か、単なる反復や説明の水増しかをTraceとQuality Judgeで確認します。8,000文字を超えた場合も、都合のよい出力が出るまで繰り返して差し替えません。
for result in version2_outputs:
length = result["characters"]
print({
"characters": length,
"distance_from_5000": length - 5000,
"within_8000": length <= 8000,
})
8. GUIでTraceを確認する
MLflow GUIの「Traces」を開き、generate_first_episode_v2を選びます。STEP3ではまだQualityスコアだけを見て採用判断しません。まず、Prompt Version 2が意図した生成経路へ変わったかを確認します。
| 確認箇所 | 見る内容 |
|---|---|
| Inputs |
episode_roleとepisode_intentが渡っているか |
| Linked prompts |
step01-narou-episode-writer Version 2か |
| Outputs | 一つの中心場面として読めるか |
| Outputs | 案内役や内なる声が設定を一括説明していないか |
| Outputs | 能力を観察・試行・推測しているか |
| Outputs | 主人公の経歴が現在の判断へ影響しているか |
| Outputs | 将来の予定の宣言ではなく、現在の変化で終わるか |
| Token count / Cost / Latency | Version 1より大幅に増えていないか |
一つの項目が改善しても、別の問題が増える可能性があります。たとえば説明を禁止しすぎると世界観が理解できず、8,000文字まで使えることで不要な回想や設定説明が増える可能性もあります。そのトレードオフもTraceへ残します。
「generate_first_episode_v2」でフィルターしています。

9. STEP3では採用を決めない
目視で良く見えた出力だけを選ぶと、生成の揺らぎとPromptの効果を混同します。STEP3ではVersion 2を登録・実行した事実と、観察した懸念を記録するところまでにします。
STEP4では、Version 1と2に対して次の条件を揃えます。
- 同じ3件のEvaluation Dataset
- 同じ
episode_roleとepisode_intent - 同じ生成モデル
- Version 1は元の1,200文字条件、Version 2は5,000文字目安・最大8,000文字条件
- 同じRequirement Judge
- 同じQuality Judge
- 同じDiagnostic Judge
比較時には、Quality平均だけでなく項目別の変化を確認します。特にVersion 2で狙った次の項目を見ます。
information_controlcharacterizationemotional_credibilitynarrative_coherencereader_momentumepisode_role_fulfillment
ただし、この6項目だけが上がれば採用するわけではありません。setting_consistency、no_plot_spoilers、文字数、コスト、レイテンシに回帰がないことも必要です。
総合比較だけでは、改善がPromptの構成指示によるものか、利用できる文字数が増えたためかを分離できません。そのため、STEP4では補助的に次の3条件を比較します。
| 条件 | Prompt | 文字数 |
|---|---|---|
| Baseline | Version 1 | 最大1,200文字 |
| Version 2 short control | Version 2 | 最大1,200文字 |
| Version 2 full | Version 2 | 5,000文字目安、最大8,000文字 |
Version 2 short controlとVersion 2 fullの差が文字数条件の効果、BaselineとVersion 2 short controlの差が主に構成指示の効果です。ただしLLM出力には揺らぎがあるため、複数回実行またはDataset拡張なしに厳密な因果効果とは断定しません。
まとめ
STEP3では、Version 1の低得点を「もっと詳しく書け」という一つの指示へまとめず、本文で起きた失敗とPromptで変更できる操作を対応付けました。
Version 2の中心は、第一話を一つの場面へ絞り、主人公の経歴、情報提示、能力発現、感情変化、終わりの引きを別々のチェック項目として並べないことです。同じ出来事の中で複数の要素が働くよう指示します。
Dataset、モデル、Judgeは変更していません。一方、文字数は本編導入を成立させるため5,000文字目安・最大8,000文字へ変更しました。また、公開作品の構成を表面的に模倣したり、Judgeの点数を直接要求したりはしていません。
Version 2は改善結果ではなく、検証可能な改善仮説です。次のSTEP4で、総合的なVersion比較と文字数を揃えた対照実験を行い、品質向上と回帰の両方を数値とRationaleから判断します。
参考資料
出力された第一話
最初のプロンプトよりはかなりマシにはなった。
マシになったというレベル感でしかないですが。。。
まだ気持ちの切り替えが早すぎる点、場面と事象の説明が合っていない点など気になるところは多々あります。
このマシになったのがどのような評価点になるのか気になるところです。
佐藤蓮は、自身の心臓がバクバクと高鳴るのを感じていた。目の前には、険しい山々と広がる森が広がり、どこか不気味ささえ漂っている。彼は、ふと気がつくと異世界に立っていたのだ。黒いスーツに身を包んでいた肌寒いオフィスの環境とは違い、ここは太陽の光が燦々と降り注ぐ自然豊かな場所だ。
「一体、どうなってるんだ?」彼の心の中で、混乱と恐怖が交錯する。職場でのストレスや過労が嘘のように消え失せたが、そんな悠長なことを考えている場合ではなかった。浮かぶ疑問は、その次元に思いを巡らせるほどに首が重く感じる。彼は生きるためにまず何をしなければならないのかを、必死で考えた。
「周囲に誰かいるのか?」蓮はあたりを見回すが、ついさっきまで働いていたオフィスの仲間たちの姿はない。ただ、視界の遠くに無数の植物が生い茂る。その中には、時折、通り過ぎる小動物の影も見えるが、人間の姿はどこにも見当たらなかった。
不安が彼の胸を締めつける。「少なくとも、誰かと話さなければ。」そう決意した蓮は、近くの森へ一歩踏み出した。地面に足をつけた瞬間、どこかしら懐かしい感覚が身体を包む。会社での繁忙から逃れた瞬間の解放感だ。しかしすぐに、それは消え失せ、前に進むことに恐怖を感じ始めていた。
森の中に進むにつれ、彼の頭の中に灯りがともる。「ここには魔法があるのか?それとも、何か特別な能力があるのかもしれない。」彼は、まるで冒険者のように心が躍る。しかし、上司に叱られた日々がフラッシュバックし、内心に冷静さを取り戻そうとしても、つまらない自己疑念が持ち上がった。
「俺が冒険者になるって…無理だろ。」
その瞬間、彼の目に飛び込んできたのは、小さな石造りの小屋だった。小屋の入り口には木製の扉があり、その下には鍵のかかった異様に古びた道具が散乱している。興味本位で近づくと、片隅にぼろぼろの木槌が見えた。
「これ、壊れているのかな?」彼は手を伸ばし、木槌を拾おうとした。その瞬間、何かが彼の体を走り抜けていく。「これを触れることで、何か変わるのか?」蓮はおそるおそるその木槌を握りしめた。その瞬間、柔らかい光が彼の指先から放たれたのだ。
彼は驚いた。「なんだ、これ?」手の中の木槌が、次の瞬間、強固な木材になり、まるで新たに鍛え直されたかのように感じられる。「これは…もしかして、俺の能力なのか?」蓮は震える手で木槌を振りかざし、力強く地面に叩きつけてみた。
地面に衝撃が走り、その瞬間、木槌の性能が増したことを実感する。「これが、俺の力なのか?」普通の木槌が、まるで魔道具のように感じられた。彼は心の中で高鳴る期待と同時に、背筋が寒くなる感覚を覚えた。「いったい、これを使って何をすればいいのか…?」
その時、ふと視界の端に人影が現れた。それは、先ほどまで見えていなかった一人の青年だった。彼は蓮の存在に気づき、警戒した目を向けている。「お前…何者だ?」青年は疑いの色を浮かべながら、蓮を見つめた。
蓮は言葉を詰まらせる。「俺は…ただの会社員だった。」異世界の冒険者にしてはありふれた自己紹介だ。「でも…この場所に迷い込んだ。君は、何をしているんだ?」
青年は彼の言葉を信じるかのように、一瞬ぐらつく。しかしすぐに、警戒の色を強めてきた。「俺に、近づくな。君のような者には、何か裏があるに違いない。」
呆然とした表情のまま、蓮は思わず後ずさった。「何も、悪意はないんだ。襲おうなんて、思っていない!」彼の心臓は再び激しく鼓動する。青年が持つ小刀の刃が、気の抜けた普通の都市での会話と比べて、どれほどの緊張を表しているかを理解できないまま、身の危険を感じる。
すると、蓮の手に握った木槌が、再び光を放った。「これが、俺の力なのか?」持て余す感情と共に、木槌を青年に向けた。驚く青年の目に、放たれた光が当たる。「触れることが、力になるのかもしれない…」
それでも、何を成し遂げるべきなのか、自分がこの異世界で何をするべきなのか、蓮は未だに迷っていた。意を決して、再び前を見据える。「君と話せば、何かわかるかもしれない。その力を使って、人を助けられるかもしれない。」蓮は心の奥で決意を持ち、その場で待つことにした。
静かな緊張の中、彼の心の中に新たな希望が芽生える。「これが、俺の冒険の始まりなのかもしれない。」だが同時に、周囲に潜む危険も感じながら、その冒険を受け入れようとした。
彼の目の前には、どんな未来が待ち受けているのだろうか。危機感を背負いながら、佐藤蓮の異世界での物語が、静かに幕を開けようとしていた。


