モジュール 00 · 第 3 回

はじめての LLM 呼び出し

DeepSeek を呼び出す十数行のプログラムを書き、リクエストとレスポンスをフィールドごとに読み解きます。メッセージのロール、終了理由、トークン使用量と料金。さらに curl で、それが本質的にはただの HTTP リクエストであることを確かめます。

  • 約 30 分
  • 難易度:入門
  • 検証:2026-09-14 deepseek-flash

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

ネットで少し検索すれば LLM を呼び出すサンプルコードが見つかり、コピーして質問を変えれば動きます。ところが多くの人はそこで止まってしまいます。プログラムは動くけれど、返ってきた大きなオブジェクトに何が入っているのかはわからない。そのため、あとで「回答が急に途中で切れたのはなぜか」「今月の請求がこんなに高いのはなぜか」といった問題にぶつかったとき、どこから調べればいいのか見当もつきません。

この課で書くのはごく短いプログラム一つだけですが、リクエストとレスポンスのフィールドを一つずつきちんと説明します。

最小の呼び出し

前の課で作った ai-course ディレクトリに first_call.py を新しく作ります。

import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["LLM_API_KEY"],
    base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com"),
)
MODEL = os.environ.get("LLM_MODEL", "deepseek-flash")

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "你是一个说话简短的助手,每次回答不超过两句话。"},
        {"role": "user", "content": "Python 里的列表和元组有什么区别?"},
    ],
)

message = response.choices[0].message
print("回答:", message.content)
print("结束原因:", response.choices[0].finish_reason)
print("实际使用的模型:", response.model)
print("输入词元:", response.usage.prompt_tokens)
print("输出词元:", response.usage.completion_tokens)

# DeepSeek 的模型默认先思考再回答,思考过程放在 reasoning_content 里。
# 别的服务商没有这个字段,所以用 getattr 取,取不到就是 None。
reasoning = getattr(message, "reasoning_content", None)
if reasoning:
    print("思考过程(前 100 字):", reasoning[:100])

実行します。

uv run python first_call.py

私が実行した結果です。

回答: 列表可变、用 `[]`,元组不可变、用 `()`。因此列表适合频繁修改的数据,元组更适合固定不变的数据。
结束原因: stop
实际使用的模型: deepseek-flash
输入词元: 52
输出词元: 145
思考过程(前 100 字): 我们需要回答中文。用户要求:你是一个说话简短的助手,每次回答不超过两句话。问题:Python 里的列表和元组有什么区别?需要不超过两句话。要准确。可以一句或两句。核心区别:列表可变,用方括号;元组不可

あなたの回答は言い回しが違い、トークン数も少しずれるはずですが、それが普通です。

プログラム自体は三つのステップだけです。クライアントを作り、chat.completions.create を呼び出し、戻り値から中身を取り出す。一つずつ分解して見ていきましょう。

クライアント

OpenAI(...) が作るのはクライアントオブジェクトです。リクエストを base_url で指定したサーバーに送り、リクエストヘッダーに api_key を付けるのが役目です。クラス名は OpenAI ですが、OpenAI 互換の API を持つサービスならどれにでも接続できます。base_url を DeepSeek に向ければ、DeepSeek に接続します。

chat.completions という名前は、OpenAI が最初に設計した「チャット補完」API に由来します。これが事実上の業界標準になり、国内外のほとんどのモデルサービスが同じ形式の API を提供しています。ですから、この一式を覚えれば、ほぼどのサービスでも使えます。

リクエスト:model と messages

リクエストの必須パラメータは二つだけです。

model はモデル名です。サーバーはこれを見て、どのモデルに回答させるかを決めます。

messages はリストで、各要素が 1 件のメッセージです。role(ロール)と content(内容)の二つのフィールドを持ちます。ロールは三種類あります。

ロール 誰の発言か 何に使うか
system 開発者 モデルにルールを与える:どんな役割を演じるか、どんな口調か、どんな制約があるか
user ユーザー ユーザーの質問や指示
assistant モデル モデルの以前の回答。マルチターン会話では、モデルが前に言ったことを戻してあげる

上の例では、system メッセージで「毎回の回答は 2 文以内」と求めたので、モデルは本当に 2 文だけで答えました。ユーザーには system メッセージは見えませんが、モデルのすべての回答に影響します。アプリを作るとき、製品の「キャラクター設定」やルールは基本的にここに書きます。

assistant ロールはこの課ではまだ使いません。モデルは前回の質問を覚えていると思うかもしれませんが、実は覚えていません。呼び出しは毎回独立しています。前の会話を「覚えさせ」たければ、それまでの質問と回答を userassistant のメッセージとして 1 件ずつ messages に入れて、もう一度送る必要があります。この話はモジュール 03 の第 1 課で詳しく扱います。

レスポンス:choices、finish_reason、usage

返ってくる response オブジェクトのうち、よく使うのは次のものです。

response.choices[0].message.content:モデルの回答です。choices がリストなのは、API が一度に複数の回答候補を求められるようになっているからですが、ほとんどの場合は一つだけなので、0 番目をそのまま取ります。

response.choices[0].finish_reason:モデルが止まった理由です。よくある値は次のとおりです。

意味
stop モデルが言い終えたと判断し、正常に終了した
length 長さの上限に達し、強制的に打ち切られた。回答は不完全な可能性が高い
tool_calls モデルがツールを呼び出したがっている(モジュール 03 第 3 課で説明)
content_filter サービス提供者の安全フィルターで止められた

プログラムではこれを必ずチェックしてください。length なら、受け取ったのは文の途中までかもしれません。そのままユーザーに見せたり JSON として解析したりすると問題が起きます。

response.model:実際に回答したモデルです。たいていはリクエストしたモデルそのものですが、例外もあります。古いモデル名がサービス側で新しいモデルに対応づけられていることがあるのです。たとえば 2026 年 9 月時点では、旧名の deepseek-chat をリクエストすると、実際には deepseek-flash の非思考モードが回答します。ですから問題を調べるときは、自分で書いたモデル名よりこのフィールドを見るほうが確実です。

response.usage:この呼び出しで使ったトークン(token)の数です。prompt_tokens が入力、completion_tokens が出力です。トークンはモデルが文字を処理する基本単位で、漢字 1 文字のこともあれば、英単語の半分や、数文字の組み合わせのこともあります。一つの文章がいくつのトークンに分かれるかはモデルごとに違い、次のモジュールの第 1 課で実際に分割してみせます。サービスはトークン単位で課金するので、usage があなたの請求書そのものです。

この呼び出しにいくらかかったか

2026 年 9 月時点で、deepseek-flash の料金は次のとおりです(100 万トークンあたり、米ドル)。

ピーク時間帯 オフピーク時間帯
入力(キャッシュミス) 0.30 0.15
入力(キャッシュヒット) 0.006 0.003
出力 1.20 0.60

ピーク時間帯は UTC で月曜から金曜の 01:00~04:00 と 06:00~10:00、つまり北京時間の平日 9:00~12:00 と 14:00~18:00 で、それ以外はすべてオフピーク料金の半額です。「キャッシュヒット」とは、今回の入力の先頭が以前のリクエストと同じで、サーバーが以前の計算を再利用できる場合を指します。これを使って節約する方法はモジュール 06 で扱います。

ピーク料金で計算すると、上の呼び出しは次のようになります。

input_cost = 52 * 0.30 / 1_000_000
output_cost = 145 * 1.20 / 1_000_000
print(f"{input_cost + output_cost:.6f} 美元")
0.000190 美元

1 ドルでこの程度の質問が 5 千回以上できます。とても安く見えますが、二つのことに注意してください。一つ目に、出力は入力の 4 倍高いので、モデルに無駄話をさせないことがそのまま節約になります。二つ目に、プログラムが毎回ドキュメント 1 冊分を丸ごと入力に詰め込み、1 日に何万回も呼ばれるようになると、この数字はすぐに膨らみます。

あの 145 個の出力トークンはどこから来たのか

回答はたった 2 文、およそ 40 文字なのに、出力は 145 トークンもあります。多い分は思考過程です。

deepseek-flash は標準で思考モードがオンになっています。まず reasoning_content で一通り考え、それから content で正式な回答を返します。上の出力にはその思考が見えています。まず「2 文以内」という要求を復唱し、それから答えを組み立てています。思考に使ったトークンは completion_tokens に含まれ、出力の料金で課金されます。このプログラムを続けて 3 回実行したところ、思考部分はそれぞれ 137、110、189 トークンで、正式な回答の何倍もの長さでした。

思考は複雑な問題でモデルの正確さを高めますが(モジュール 02 第 3 課で比較実験をします)、簡単な問題ではお金と時間の無駄です。DeepSeek ではオフにできます。

response = client.chat.completions.create(
    model=MODEL,
    messages=[...],
    extra_body={"thinking": {"type": "disabled"}},
)

extra_body は、サービス固有のパラメータを渡すために OpenAI SDK が用意している抜け道です。thinking は DeepSeek のパラメータで、他社が理解するとは限りません。サービスを替えるときは削除するか、相手が思考の制御にどんなパラメータを使っているか確認してください。

入力トークンにも細かい点があります。同じ二つのメッセージ(合わせて 43 文字)でも、非思考モードで実行すると prompt_tokens は 27、思考モードをオンにすると 52 でした。メッセージの内容は同じなので、増えた 25 トークンはサーバーが付け加えた書式用のマーカーです。どこが system でどこが user か、どこから思考を始めるかをメッセージの外側に書き添えてからモデルに渡しており、これらのマーカーも入力トークンに数えられます。43 文字がわずか二十数トークンにしかならないのは、DeepSeek のトークナイザーが漢字 2~3 文字をよく 1 トークンにまとめるからで、次のモジュールの第 1 課で詳しく見ます。

落とし穴:思考が上限を使い切る

max_tokens パラメータで出力トークン数の上限を決められます。コスト管理や、モデルがいつまでも止まらないのを防ぐためによく使います。ただし思考モードでは、思考過程もこの上限を消費します

max_tokens を 30 にして「Python のリスト内包表記を紹介して」と聞き、思考をオフにした場合とオンにした場合でそれぞれ実行してみました。

== 非思考: finish_reason=length completion_tokens=30 reasoning_tokens=None
content: '## Python 列表推导式(List Comprehension)\n\n列表推导式是 Python 中一种**简洁优雅**的创建列表的方式,可以用一行代码'
reasoning: ''
== 思考: finish_reason=length completion_tokens=30 reasoning_tokens=30
content: ''
reasoning: 'We need answer in Chinese. User asks "介绍一下 Python 的列表推导式。" Need introduce Python'

非思考モードでは回答が文の途中で切れました。これは予想どおりです。思考モードでは 30 トークンがすべて思考に使われ、正式な回答の content空文字列でした。プログラムが content しか見ていなければ、モデルが何も言わなかったと思ってしまいます。

教訓は二つです。思考をオンにしているときは max_tokens を余裕を持って設定すること。そしてプログラムでは必ず finish_reason をチェックし、length を見たら結果が不完全だと認識することです。

curl で素の姿を見る

SDK がやってくれていることは、実は HTTP リクエストを一つ送ることだけです。Python を使わなくても、コマンドラインの curl で呼び出せます。

curl https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -d '{
    "model": "deepseek-flash",
    "messages": [{"role": "user", "content": "用五个字形容秋天"}],
    "thinking": {"type": "disabled"}
  }'

Windows の PowerShell は引用符の扱いが違うので、このコマンドはうまく動かないかもしれません。Git Bash か WSL で実行するか、このステップは飛ばしてもかまいません。この先の学習には影響しません。

返ってくるのは JSON で、整形すると次のようになります。

{
    "id": "10bef209-3437-4efc-906b-dcb18fe30f7f",
    "object": "chat.completion",
    "created": 1789444049,
    "model": "deepseek-flash",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "**金风送爽凉**\n\n如果不局限于这五个字,还有其他不同角度的五字形容:\n\n- **秋高气爽天** — 天高云淡,气候宜人\n- **霜叶红于花** — 枫叶经霜比花还红\n- **硕果满枝头** — 丰收的景象\n- **一叶知秋来** — 落叶预示着秋天到来\n- **寒蝉鸣凄切** — 秋蝉叫声悲凉\n- **天凉好个秋** — 辛弃疾词句,凉爽舒适"
            },
            "logprobs": null,
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 9,
        "completion_tokens": 121,
        "total_tokens": 130,
        "prompt_tokens_details": {
            "cached_tokens": 0
        },
        "prompt_cache_hit_tokens": 0,
        "prompt_cache_miss_tokens": 9
    },
    "system_fingerprint": "aeb56401ca74e127821c4f9126dcb669"
}

Python で見たフィールドと一対一で対応しています。choices[0].message.contentfinish_reasonusage。SDK はこの JSON を Python のオブジェクトに変え、ついでにリトライやタイムアウトといった雑事を処理しているだけです。これがわかれば、どんな言語からでも LLM を呼び出せますし、問題が起きたときは curl で直接確かめて、自分のコードの問題かサーバー側の問題かを切り分けられます。

thinking は curl では JSON のトップレベルに直接書いている点に注意してください。Python で extra_body として渡した場合も、SDK が最終的にこの JSON に合流させています。

もう一つ見ておきたい細かい点があります。「秋を 5 文字で表して」と頼んだところ、モデルは 5 文字を返したあと、頼まれてもいない 6 項目を勝手に付け加えました。モデルはよく余計なことを言います。この点はモジュール 02 のプロンプトの回で詳しく扱います。

よくある問題

contentNone や空文字列になる:まず finish_reason を見てください。length なら、max_tokens が小さすぎて思考過程に使い切られています。tool_calls なら、モデルはツールを呼び出したがっていて、回答は別のフィールドに入っています。

429 エラー:リクエストが多すぎてレート制限されています。数秒待ってから再試行してください。自動リトライの方法はモジュール 03 第 4 課で説明します。

プログラムが長いあいだ固まって反応しない:思考モードでは、複雑な問題の思考が数十秒続くことがあります。まず簡単な質問に替えて、プログラム自体に問題がないことを確かめてください。回答を生成しながら表示するストリーミング出力は、モジュール 03 第 2 課で扱います。

練習問題

  1. system メッセージを「あなたは漢文調でしか答えない老学者です」に変え、同じ質問をもう一度して、回答がどう変わるか見てください。
  2. 呼び出しに extra_body={"thinking": {"type": "disabled"}} を加え、思考をオフにする前と後で completion_tokens と実行時間を比べてください。
  3. あなたのアプリが 1 日に 1 万回呼ばれ、1 回あたり入力 2000 トークン、出力 500 トークン(思考オフ)で、すべてピーク料金だとします。1 か月(30 日)でいくらかかるか、Python で計算してください。
  4. messages に手動で 2 件のメッセージを加え、すでに交わされた会話を再現してください。まず user が「私は小王(シャオワン)です、覚えておいて」と言い、次に assistant が「わかりました、小王さん」と答え、最後に user が「私の名前は何?」と聞きます。モデルが正しく答えられるか確かめ、その理由を考えてみてください。

確認テスト

1. finish_reason が length になっているのは何を意味しますか?プログラムはどう対処すべきですか?

モデルの出力が max_tokens の上限に達して強制的に打ち切られ、回答が不完全な可能性が高いことを意味します。プログラムはそれを正常な結果として扱ってはいけません。max_tokens を増やしてリクエストし直すか、少なくともユーザーに回答が不完全だと知らせます。回答を JSON として解析するつもりなら、途中で切れた JSON は必ず解析に失敗します。

2. 思考モードをオンにして max_tokens を 50 にしたら、content が空でした。なぜですか?

思考過程も max_tokens の上限を消費するからです。50 トークンがすべて思考に使われ、正式な回答を書き始める前に上限に達しました。思考モードをオンにするときは max_tokens を余裕を持って設定するか、簡単なタスクでは思考をオフにします。

3. deepseek-chat をリクエストしたのに、response.model には deepseek-flash と表示されています。これは正常ですか?

正常です。サービス提供者は古いモデル名を新しいモデルに対応づけて提供を続けることがあります。response.model は実際にどのモデルが回答したかを示すので、問題の調査や請求の確認ではこちらを基準にします。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…