Featured image of post コードディスパッチャー【Prompt Registry】「次の当直が、困らないように」〜指示書は、誰かの持ち物です〜

コードディスパッチャー【Prompt Registry】「次の当直が、困らないように」〜指示書は、誰かの持ち物です〜

引き継ぎ書を作ろうとして、指示書がどこにも一元管理されていないと気づいた。散らばったプロンプトに版・所有者・変更理由を持たせ、資産として渡せる形にするPrompt Registryを解説します。

第1幕: 持ち込み ── 困りごとではなく、作業の途中で気づいたこと

その夜の指令所には、いつもと少し違う匂いがあった。サカキの机の上に、今月から様式が変わった申し送り用の書類が、真新しいまま積まれている。誰の手にもまだ馴染んでいない紙は、擦れ合う音まで硬い。私(トウヤ)は自分の席で記録帳を開き、今夜の頁の頭に日付を書き入れた。ペン先が紙を吸って、いつもと同じ重さの一行になった。

ドアが開く。ノザワさんが一人で入ってきた。会釈も、椅子に腰かける動きも、丁寧で角がなかった。「来訪」と記録の頭に書きながら、私はその平静さの正体をどう記せばいいか迷った——深刻な相談を持ち込む人の肩に、決まって入っている力が、ノザワさんにはどこにも見当たらなかった。

サカキが椅子を勧めながら訊いた。「今夜は、何を持ってこられましたか」。

ノザワさんが姿勢を正して答えた。「来月付で、異動になりまして。後任への引き継ぎ書を作ろうとしているんですが……」。

そう前置きすると、鞄から一冊のバインダーを取り出した。開いたページには、記入途中の様式が挟まっている。私が意外だったのは、その差し出し方だった。困りごとの説明が先に来るのではなく、書類そのものが先に出てきた。

私はその順序に引っかかった(独白:これまでの相談は、みな症状か疑問を言葉で説明してから始まった。今夜は、先に書類が出てきた)。

「この様式に、『現行の指示書はこちら』という欄があるんです」。ノザワさんが、様式の一箇所を指で示す。「……それで、実際どれが現行か、自分でもすぐには言えないことに気づきました」。

声のトーンに、情けなさに近い響きが混じった。

サカキが訊いた。「今日まで、それで困ったことは」。

「無かったです。全部、自分の頭の中で分かっていたので」。

その返事には、言い訳がましさがなかった。ただ事実を述べているだけだった。私はその率直な一言を、飾らずに記録帳へ写した。

「今、部署はいくつ持っていますか」。サカキが訊く。

「総務・営業・経理の3つです」。

「分かりました。今夜は、まだ良し悪しを言いません」。

サカキがそう言うと、ノザワさんの肩からわずかに力が抜けた。すぐに良し悪しを言われずに済んだことが、思いがけなかったらしい。

私の独白:これまでの相談は、動いているものが止まった、あるいは動いているはずのものが違う顔をしていた、という形で持ち込まれてきた。今夜は違う。動いているものは、今夜もちゃんと動いている。困りごとが起きたのではなく、事務手続きの途中で、自分のやり方の粗さに初めて向き合った——そういう相談は、これが初めてだった。

第2幕: 照合 ── 集めてみて、初めて分かること

「まず、今動いているものを、一つずつ集めましょう」。

サカキが言った。ノザワさんが持参した資料を広げていく。スクリプトの一部を印刷した紙、共有ドライブに置いてある設定ファイルの印刷、同僚とのやり取りが残るチャットのスクリーンショット。部署ごとに、それぞれ違う場所から来ていた。

「総務部は、スクリプトの中に直接書いています」。ノザワさんが一枚目を置く。「営業部は、去年の忙しい時期に急いで作ったので、私のノートパソコンのローカルにしか残っていないはずです。経理部は……たしか、共有ドライブの設定ファイルに書いたと思います」。

三部署、三通りの置き場所。私はその並びを記録帳に書き写しながら、指令所のこれまでの相談とは違う種類の散らかり方だと感じた(独白:これまでの相談は、一つの仕組みのどこかに欠陥があった。今夜は、欠陥ではなく、そもそも一つの仕組みとして数えていいのか、というところから怪しい)。

私は三部署ぶんの資料を、記録帳の上で部署名ごとに並べ直す手伝いをした。総務部の欄を並べているとき、ノザワさんの手が止まった。

「あ……」

スクリプトの中の文面と、共有ドライブの設定ファイルの文面が、違っていた。

「これ、半年前に直したはずのやつだ」

サカキが訊く。「直した記憶は、確かにありますか」。

「はい。総務部から『表形式にしてほしい』と言われて、設定ファイルの方は直しました」。ノザワさんが、印刷された設定ファイルの一行を指でなぞる。「日付も、私が入れたメモも、残っています」。

私はその二つの文面を見比べながら、まだ何が問題なのか掴めずにいた(独白:直した記録がある。それなら、直っているはずだ。何が引っかかるのだろう)。

サカキが言った。「これから、設定ファイルの中身を書き換えて、実際に生成結果が変わるかを確かめてみます。変わらなければ、それは異常だと考えてよいですか」。

ノザワさんが、鞄の持ち手を握り直してから答えた。「はい、それでお願いします」。

サカキが端末を操作する。今、動いているコードそのものを呼び出した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# report_generator.py の中に直書きされている指示書(総務部のみ実装済み)。
SOMU_PROMPT = "総務部向けの月次レポートを、簡潔な箇条書き形式で作成してください。"


def generate_monthly_report(department: str) -> str:
    """部署別の月次レポートを生成する。

    config/prompts.json は一切読み込んでいない。以前は読んでいたが、
    リファクタリングの際に読み込み処理だけが削除され、ファイル自体は
    名残として残っている。
    """
    if department == "総務部":
        return call_llm(SOMU_PROMPT)
    raise KeyError(department)

サカキが、config/prompts.json 相当の設定ファイルを読み込む関数も示した。

1
2
3
4
5
6
7
8
9
def load_config_file(path: str) -> dict[str, str]:
    """config/prompts.json 相当の設定ファイルを読み込む。

    generate_monthly_report からは一度も呼ばれていない ── これが本話の問題。
    ノザワさんが「直した」のはこのファイルの中身であり、動いているコードに
    その変更が届く経路が存在しない。
    """
    with open(path, encoding="utf-8") as f:
        return json.load(f)

サカキが、この場で config/prompts.json の中身を新しい文面に書き換え、そのあとで generate_monthly_report("総務部") を呼んだ。二回、出力を確かめた。

「今、設定ファイルを書き換えました。出力は、変わりましたか」。

ノザワさんが画面を確認する。表情が固まった。「……変わって、ないです」。

「直したつもりのものが、動いているコードには、届いていません」。

サカキがそう言った。私はその一文を書き取りながら、さっきの引っかかりの正体に気づいた。直したという記憶と、直っているという事実は、別のものだった。ノザワさんは、半年前に確かに手を動かした。ただ、その手を動かした先が、実際に動いているコードへ繋がっていなかっただけだった。

私は記録帳に、今の配線を描いてみた。三部署、三通りの置き場所。線を引いてみると、実際に生きている線は、たった一本しかなかった。

3部署(総務部・営業部・経理部)の指示書の置き場所から中央のgenerate_monthly_report(department)へ向かう配線図。総務部のreport_generator.py内SOMU_PROMPT(直書き)だけが実線で「実際に読まれている」と接続され、config/prompts.json・ノザワさんのノートPC・共有ドライブの設定ファイルの3つはいずれも点線で「読み込む経路が無い」「誰も呼び出し方を知らない」「本当にここか、未確認」と示され、月次レポートへは実線1本しか繋がっていないことを表す運行図表

「私の管理の仕方が、悪かったんですよね」。ノザワさんが、自分を責めるように言った。

「一人で運用している間は、それで困らなかったはずです」。サカキが答える。「困るのは、渡す相手が増えたときです」。

断罪でも、免責でもなかった。私はその一言だけを、言い換えずに書き留めた。

「今、この場に、総務部の指示書が3種類ありました」。サカキが続けた。「次の人が読むべきなのは、そのうちどれですか」。

ノザワさんが、即答できずに黙り込んだ。

「まず、指示書自体を、資産として持てる形にしましょう」。

第3幕: 指令の一手 ── 指示書を、資産として持つ

「今夜、手を入れるのは、指示書の中身ではありません」。サカキが言った。「指示書を、版・所有者・変更理由をひとまとめにした資産として持たせます」。

端末の画面へ、次のコードを表示させた。

1
2
3
4
5
6
7
@dataclass(frozen=True)
class PromptRecord:
    department: str
    text: str
    version: str
    owner: str
    change_reason: str

「これまでも、版を値として持つ話はしました」。サカキが前置きする。「今夜は、そこに『誰の持ち物か』を新しく加えます」。

ownerchange_reason の二つのフィールドを指で示す。「文面だけでなく、誰が、なぜ変えたかまで、この値の中に一緒に入ります」。

ノザワさんが訊く。「所有者って、私の名前が入るということですか」。

「今は、そうです。ただ、これは今夜のノザワさんに固定した意味ではありません。異動すれば、次の担当者の名前に変わります。この欄が『今、誰の持ち物か』を示し続ける、という意味です」。

「この指示書を、どこから引いてくるかは、あとから丸ごと入れ替えられる形にします」。

サカキが続けてコードを示す。

1
2
class PromptSource(Protocol):
    def get(self, department: str) -> PromptRecord: ...

「継承の宣言は要りません。同じ形のメソッドさえ持っていれば、差し替え先として扱われます」。

ノザワさんが「これまでの、設定ファイルが読まれていなかった問題は、これで直るんですか」と訊いた。

「読み込む場所が一つに決まれば、今のような食い違いは起きにくくなるはずです」。サカキが答える。「本当にそうなるかは、あとで確かめましょう」。

サカキが、テスト用の実装を見せる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class DictPromptSource:
    """テスト用の PromptSource 実装。渡された dict をそのまま保持するだけ。"""

    def __init__(self, records: dict[str, PromptRecord]) -> None:
        self._records = records

    def get(self, department: str) -> PromptRecord:
        try:
            return self._records[department]
        except KeyError:
            raise PromptNotFoundError(department) from None

「未登録の部署を指定すると、PromptNotFoundError になります」。サカキが一言添える。「原因の分からない例外ではなく、意味の分かる例外に翻訳しておく、という考え方は、以前にもお話ししました」。

続けて、本番相当の実装も示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class FilePromptSource:
    """本番相当の PromptSource 実装。ディレクトリ内の {department}.json から組み立てる。"""

    def __init__(self, directory: Path) -> None:
        self._directory = directory

    def get(self, department: str) -> PromptRecord:
        path = self._directory / f"{department}.json"
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except FileNotFoundError:
            raise PromptNotFoundError(department) from None
        return PromptRecord(
            department=department,
            text=data["text"],
            version=data["version"],
            owner=data["owner"],
            change_reason=data["change_reason"],
        )

「今夜はテスト用に辞書を渡していますが、大事なのは差し替えられる仕組みそのものです。渡す先が本物のファイルになるかどうかは、運用側で決めることです」。

最後に、配線そのものを示した。

1
2
3
4
5
6
7
def generate_report_from_source(department: str, source: PromptSource) -> str:
    record = source.get(department)
    return call_llm(record.text)


def build_report_generator(source: PromptSource):
    return lambda department: generate_report_from_source(department, source)

ノザワさんが、コードを見比べながら訊いた。「もとの generate_monthly_report は、書き換えるんですか」。

「いいえ」。サカキが答える。「もとの関数は、そのまま残します。名前も、部署名だけを受け取る形も変えません。今つくった build_report_generator に読み込み元を渡すと、呼び出し方が今までとまったく同じ、新しい生成係が出来上がります」。

「今夜からは、この生成係を使ってください、ということですね」。

「はい。中身は違っても、呼び方は変わりません。だから、他の場所を直して回る必要はありません」。

サカキが続ける。「report_generator.py の文字列直書きも、config/prompts.json も、この生成係からはもう呼ばれません」。

私は記録帳に、今夜組んだ配線を描き足した。差し込み口は二つあっても、出てくる形はひとつ。呼び方は、これまでと変わらないままだった。

PromptSource(差し替え可能な読み込み口)の中にDictPromptSource(テスト用)とFilePromptSource(本番相当)が並び、両方ともgetでPromptRecord(department/text/version、owner/change_reason)へ収束し、build_report_generator(source)を経て新しい生成係(呼び方はdepartmentだけ=旧関数と同一)へ一本道でつながる運行図表

ノザワさんが「読み込み先を入れ替えられる、それだけの話なら、そんなに特別なことには聞こえないですね」と素朴な感想を漏らす。

「はい」。サカキが認める。「読み込み元がどこにあるかを、使う側のコードに直接書かせない——これには、Repositoryパターンという名前が、以前から付いています」。

「ただ、今、お話ししたRepositoryという考え方自体は、読み込み元を差し替え可能にするだけです」。サカキが続ける。「今夜の新しいところは、その先——運ばれてくる値自体に、誰が・なぜ変えたかを型として持たせたことです」。

「読み込み方じゃなくて、中身の形なんですね」。ノザワさんが言い直した。

「今夜組んだのは、指示書を資産として持つ形です」。サカキが続ける。「これで、次の人は『どれが現行か』『誰の持ち物か』が分かります。ただし——」

一拍置いて、続けた。

「メタデータの欄を用意しても、書く人が書かなければ空のままです」。「あるプロンプト管理の仕組みでは、所有者を示す欄が用意されていますが、公式のドキュメント自体が、それを『任意の手動ラベル』だと説明しています。記入を強制する仕組みはありません」。

「別の仕組みでは、劣化を実際に防いでいるのは、メタデータそのものではなく、変更を自動でチェックし、基準を満たさなければ本番へ進めない、という評価の仕組みだと説明されています。今夜の仕組みは、書いてあることを読める形にしただけです。書いた内容が良いかどうかを機械的に判定する仕組みは、また別に要ります」。

ノザワさんが、少し考えてから訊いた。「じゃあ、所有者や変更理由の欄を作っても、意味が無いんですか」。

「意味は、無くはありません」。サカキが答える。「今夜より前は、その欄そのものが存在しませんでした。書く場所が無ければ、誰も書けません。書く場所があれば、書く人は出てきます。ただ、それは『必ず正しく書かれる』ことまでは保証しません」。

「今夜組んだのは、渡せる形です。渡した中身が正しいかどうかまでは、この仕組みは面倒を見ません」。

「もう一つ、気になっている話があります」。サカキが続けた。「プロンプトを扱う仕組みを作っている会社の人が、こう言っています。プロンプトの変更は、普段のコードレビューやセキュリティチェックの外側で行われがちだ、と。データベースの一行や、担当者のダッシュボードから、誰かが直接書き換えられてしまう。プルリクエストも、セキュリティの確認も、通らないことがある」。

ノザワさんが、少し表情を曇らせた。「うちも、まさにそれです。私が一人で、思いついたときに直していました」。

「今夜の仕組みは、そこに『誰が』『なぜ』という記録を必ず付けます。ただ、変更そのものを止められるようになったわけではありません。付いたのは記録であって、鍵ではない、という点は、区別しておいてください」。

私はその区別を、丸で囲んで書き添えた。渡せる形にすることと、正しく渡り続けることは、地続きではあっても、同じ一手では埋まらなかった。

ノザワさんが「今の話は、指示書の版を比べる、という話とは違うんですか」と重ねて尋ねた。サカキの説明の中に「版」という語がすでに出ていたことへの、素朴な疑問だった。

「近い部分はありますが、別物です」。サカキが答える。「版を比べる仕組みは、直した前後で何が変わったかを見るためのものでした。今夜のものは、そもそも今どれが現行なのか、誰の持ち物なのかを決めるためのものです」。

引き継ぎ書の話に戻る。「様式の『現行の指示書はこちら』の欄には、PromptSource の場所を書けばいいことになります。誰が見ても、そこを見れば分かります」。

ノザワさんが「後任が読むためのものだから、正確じゃないと困りますね」と呟いた。

サカキが応じる。「次の当直が困らないように、という考え方は、うちも同じです」。

私はその一言を書き取りながら、サカキの視線が一瞬、表示盤ではなく、どこか別の場所——壁の向こうか、もっと遠くか、私には分からない場所——に向いていることに気づいた。何を見ていたのかは、最後まで訊けなかった。訊けなかった、という事実だけを、頁の隅に小さく残した。

第4幕: シミュレータと引き渡し ── まだ決まっていないことを、自分で書き足す

「シミュレータを7本、流します。1本目だけは、今夜の仕組みを入れる前——さっきの食い違いが、記録としてどう残るかの確認です。残る6本が、PromptSource を通したあとの結果です」。

サカキがキーボードに手を置き、手順をやり直す。私が結果を読み上げる役だった。

「設定ファイルを書き換えても直したはずの内容がBeforeの出力に反映されない、通過」 「同じ部署の指示書は読み込み元でただ一つに決まる、通過」 「未登録の部署を指定するとPromptNotFoundErrorになる、通過」 「版と所有者と変更理由が読み込んだ指示書から取得できる、通過」 「読み込み元を更新すると生成結果に反映される、通過」 「読み込み元をDictからFileに差し替えても同じ内容なら生成結果は変わらない、通過」 「指示書の中身を変えても所有者と変更理由の記録は独立して残る、通過」

1本目・5本目・6本目については、サカキが少し時間をかけて説明を足した。

「1本目は、さっき見た問題の再現です。今夜の仕組みを入れる前は、設定ファイルを書き換えても出力が変わりませんでした。この振る舞いは、記録として残しておきます」。

「5本目が、今夜の核心です。読み込み元を更新すると、今度こそ生成結果に反映されます。さっきと同じ操作を、今夜の仕組みの上でやり直すと、今度は出力が変わります」。

「6本目は、辞書と本物のファイル、両方の読み込み元を試したものです。中身が同じなら、どちらから読んでも結果は変わりません」。

ノザワさんが、二つの結果を見比べた。「同じ操作をしたのに、片方は変わらなくて、片方は変わる……」。

「読み込む場所が一つに決まっているかどうかの違いです」。サカキが言う。

私はその対比を、記録の欄にそのまま書き取った。

ノザワさんが、ふと気づいたように言った。「営業部と経理部は、まだこれからですよね」。

「はい」。サカキが頷く。「今夜動かしたのは、総務部だけです。残り二つも、同じ PromptRecord として登録すれば、同じ仕組みに乗ります」。

「じゃあ、あとは同じことを、二回繰り返すだけですね」。

「はい。今夜のうちに、道具は揃いました」。

サカキが、今夜の一手がどこまでを守るかを言葉にする。

「保証すること。指示書を更新すれば、必ず生成結果に反映されます。版・所有者・変更理由が、指示書と一緒に必ず残ります」。

「保証しないこと。書かれている所有者・変更理由の中身が正しいこと。更新のたびに評価が行われること。メタデータの欄があることと、その欄が正しく使われ続けることは、別の話です」。

ノザワさんが、完成した引き継ぎ書を見返した。「現行の指示書はこちら」の欄に、PromptSource の場所が明記されている。所有者・変更理由の欄も、これから運用が続く限り埋まっていく形になった。

しばらくのあいだ、様式を黙って眺めていた。ページをめくる指の動きが、来たときよりゆっくりだった。それから、様式の余白に、自分の手でペンを走らせた。

「……ここは、後任と一緒に決めてください、と書いておきます」

サカキが答える。「それで、いいと思います」。

私は、ノザワさんが自分の手で書き足した一文を、記録の最後に書き留めた。決まっていないことを、決まっていないままにして渡す——それも、引き継ぎの形の一つだった。

私の独白で、今夜を閉じる。ノザワさんが持ち込んだのは、壊れた仕組みではなかった。むしろ、これまで一度も壊れていなかった仕組みだった。壊れていなかったからこそ、誰も気づかずにいられた——渡す相手が現れるまでは。


🚦 本日の指令記録(Dispatch Log)

  • 指令の定石(パターン名): Prompt Registry(指示書の資産管理)── 指示書を版・所有者・変更理由をひとまとめにした資産として持ち、読み込み元を差し替え可能にする
  • 申告された症状: 「引き継ぎ書を作ろうとしたら、どれが現行の指示書か自分でもすぐに言えないことに気づいた」
  • 今の運行計画の問題: 指示書がコード・設定ファイル・個人のノートパソコンに散らばり、どれが実際に使われているかがコードを読まないと分からない。直したつもりの変更が、動いているコードに届いていないことがある
  • 打った一手: 版・所有者・変更理由を持つ PromptRecord と、読み込み元を差し替え可能にする PromptSource を導入した。指示書の中身そのものは変えない
  • 配線した場所: 各部署の指示書の中身は事業者が決める。読み込み元をどこに置けば現行として扱われるかという配線は指令所側が用意した
  • 保証しないこと: 書かれている所有者・変更理由の中身が正しいこと。メタデータの欄があることと、それが正しく使われ続けることは別の話
  • シミュレータ結果: 7本。読み込み元を更新すると生成結果に反映されること、読み込み元を差し替えても同じ内容なら結果が変わらないことを確かめた
  • 次の当直への申し送り: 今夜作ったのは、指示書を資産として持つ形だけ。書いた内容が良いかどうかを評価と紐づける仕組みは、まだこれから

今、動いている仕組みを止めないまま、指示書の置き場所と、誰がなぜ変えたかを一つにまとめる——そういう相談を承っています。

ご相談はこちら

comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource
Hugo で構築されています。
テーマ StackJimmy によって設計されています。