deepseek.ac日本語で学ぶ、実践する

実装

DeepSeek APIのプロンプトをコードとして管理する

· 約10分

リード

プロンプトをソースコードに直接書くと、変更履歴もテストも効かなくなる。 この記事では、プロンプトをコードとして扱うための最小構成を、DeepSeek APIの呼び出しと組み合わせて示す。

前提

注: モデル名・エンドポイント・価格は変更される可能性があるため、実装時は公式ドキュメントで最新値を確認してください。

本文

なぜプロンプトを「コードとして」扱うのか

文字列リテラルの埋め込みで始めたプロンプトは、次の順で破綻します。

  1. 変更理由が分からなくなる(git blame が「1行の巨大文字列」になる)
  2. どのバージョンが本番か分からなくなる
  3. 回帰テストが書けない
  4. 再利用できない(同じ system prompt を3か所にコピー)

対策は「プロンプトを独立したモジュール・ファイルに切り出し、バージョンを持たせ、テスト可能にする」ことです。

最小構成: prompts パッケージ

ディレクトリを切って、プロンプトを名前付きで管理します。

project/
├── prompts/
│   ├── __init__.py
│   ├── base.py       # 共通の型・ローダ
│   └── summarize.py  # 個別プロンプト
└── main.py

プロンプトは「関数」として定義します。変数を渡せるようにするためです。

# prompts/summarize.py
from dataclasses import dataclass

VERSION = "v2"

SYSTEM = """\
あなたは技術記事を要約するアシスタントです。
- 必ず日本語で出力する
- 箇条書きは使わず、3文以内で要約する
- 原文にない情報を追加しない
"""

def build_user(text: str) -> str:
    return f"次の記事を要約してください:\n\n{text}"

呼び出し側はこうなります。

# main.py
from openai import OpenAI
from prompts import summarize

client = OpenAI(
    api_key=...,          # 環境変数から読む。直書きしない
    base_url="https://api.deepseek.com",
)

def summarize(text: str) -> str:
    resp = client.chat.completions.create(
        model="deepseek-chat",   # モデル名は公式ドキュメントで要確認
        messages=[
            {"role": "system", "content": summarize.SYSTEM},
            {"role": "user", "content": summarize.build_user(text)},
        ],
        temperature=0.2,
    )
    return resp.choices[0].message.content

なぜ関数にするか: SYSTEM を str.format で埋め込むと、ユーザー入力に {} が含まれたときに壊れます。build_user のような関数で組み立てれば、その事故を防げます。

バージョンを明示する

プロンプトはモデルと同じく「変えたら結果が変わる資産」です。VERSION を持たせ、ログに残します。

import logging

logging.info("prompt=%s version=%s", "summarize", summarize.VERSION)

なぜログに残すか: 出力品質が落ちたとき、「どのプロンプトの版で出たか」が追えないと原因調査ができません。A/Bテスト(シリーズ「運用」)の前提にもなります。

テストを書く

プロンプトのテストは「壊れていないこと」を保証する回帰テストとして書きます。

# tests/test_summarize_prompt.py
from prompts import summarize

def test_build_user_contains_input():
    user = summarize.build_user("テスト本文")
    assert "テスト本文" in user

def test_system_rules_present():
    # ルールが消えたら気づけるようにする
    assert "日本語" in summarize.SYSTEM
    assert "3文以内" in summarize.SYSTEM

なぜ書くか: プロンプトの「削ってはいけない一文」をテストで固定できます。レビューで消されて品質が落ちる事故を防げます。

外部ファイルにするか、Pythonにするか

形式 向くケース 注意
.py モジュール 変数展開・条件分岐がある ロジックが混ざると見通しが悪化
.txt / .md + ローダ 長文・条件分岐なし バージョンはファイル名かメタで持つ
Jinja2 テンプレート 複雑な条件・ループ 依存が増える。過剰になりやすい

目安: 分岐が1つ以下なら .py、長文の固定文なら .txt。Jinja2 は本当に必要になってから導入するのが無難です。

つまずきポイント

症状 対処
{} を含む入力でエラー str.format をやめ、f-string か関数で組み立てる
本番とテストでプロンプトが違う プロンプトを1か所に集約し、importで共有する
変更前後で品質が比較できない VERSION をログに残し、評価用データセットを用意する
APIキーをコミットしてしまう 環境変数+.gitignore。プロンプトと鍵は別管理

まとめ

チェックリスト