ENGINEERING GUIDE / 2026.10.20
BUILD WITH DEEPSEEK · HANDS-ON

DeepSeek V4.1 Flash × Search API検索で根拠をつかみ、自分で調べるエージェントをつくる。

deepseek-flash の中身は、2026 年 10 月 17 日に更新された DeepSeek-V4.1-Flash-1017 です。同じ日に公開された Search / Fetch API と組み合わせ、Web の最新情報を根拠に答えるプログラムを Python で動かします。

目安 90〜120 分Python 基礎図解 10 点 + 実行サンプル
01 / ORIENTATION

考える・探す・読むを、自分のコードでつなぐ

DeepSeek API には、このガイドで使う入口が 3 つあります。文章を生成する Chat Completions、Web を検索する Search API、1 ページを読み込む Fetch API です。どれも同じ API キーで呼び出せます。

Search API と Fetch API は、モデルとは独立した HTTP の API です。このガイドの方法(deepseek-flash + Chat Completions)では、モデルが勝手に検索することはありません。検索するかどうか、何を読ませるかは、あなたのコードが決めます。この役割分担を最初に押さえておくと、後の章が読みやすくなります。DeepSeek のサーバー側で検索する経路もあり、11 章で扱います。

このガイドの到達点
① deepseek-flash が何を指し、何が強みなのかを数字で説明できる。② Search / Fetch API を直接呼び、結果の採否を判断できる。③ 検索結果を根拠にした回答と、モデルが自分で検索するエージェントを動かせる。④ エラー・課金・レート制限への備えを説明できる。
あなたのコード Python・curl など API キーを持つ api.deepseek.com · 同じ API キーで Bearer 認証 Chat Completions — 考えて答える POST /chat/completions · model: deepseek-flash Search API — Web を検索する POST /v1/tools/search · 結果を JSON で返す Fetch API — 1 ページを読む POST /v1/tools/fetch · 本文を Markdown で返す
図 1 このガイドで使う 3 つの入口

実習は 6 つです。最初の挨拶から始め、検索 → ページ取得 → 根拠つき回答 → 自分で検索するエージェント → エラー処理と進みます。API キーがなくても、同梱のモックサーバーで全実習を動かせます。

  • LAB 1 最初の呼び出しで、思考モードの出力を見る
  • LAB 2 Search API で検索し、結果の質を数値で判定する
  • LAB 3 Fetch API でページを Markdown として読む
  • LAB 4 検索結果だけを根拠に、出典つきで答えさせる
  • LAB 5 モデルに検索ツールを渡し、自分で調べさせる
  • LAB 6 再試行・空の結果・入力エラーを確かめる

一次情報:Search & Fetch / Your First API Call

02 / NAMES & VERSIONS

「書く名前」と「応答するモデル」を区別する

DeepSeek API の model に書く値は、2026 年 10 月 20 日時点で deepseek-flash と deepseek-v4-pro の 2 つです。deepseek-flash には現在 DeepSeek-V4.1-Flash-1017 が応答します。名前は同じまま、裏のモデルが更新される仕組みです。2026 年 10 月 17 日の更新でも、呼び出し方法と価格は変わっていません。

model に書く値(API 名) 応答するモデル(2026-10-20 時点) deepseek-flash これを使う deepseek-v4-flash 旧名 deepseek-v4-flash-vision-exp 旧名 deepseek-v4-pro V4 系の別モデル deepseek-chat / deepseek-reasoner 2026-04-24 に「2026-07-24 に廃止」と告知 矢印なし = 現行の Quick Start・料金表に記載なし DeepSeek-V4.1-Flash-1017 Flash 価格で課金 旧名 2 つも受け付け、同じモデルが応答 DeepSeek-V4-Pro-0813
図 2 API 名と応答するモデルの対応
版名を model に書かない。 「DeepSeek-V4.1-Flash-1017」は応答するモデルの版名です。本ガイドで確認した公式ページには、deepseek-v4.1-flash のような API 名は出てきません。書く値は deepseek-flash です。また、Web 上の古い記事には deepseek-chat / deepseek-reasoner を使う例が残っています。この 2 つは 2026-04-24 の更新で「3 か月後(2026-07-24)に廃止」と告知され、現行の Quick Start と料金表には載っていません。

旧名 deepseek-v4-flash と deepseek-v4-flash-vision-exp は今も受け付けます。ただし元のモデルは退役しており、V4.1-Flash-1017 が Flash 価格で応答します。新しく書くコードでは deepseek-flash を使いましょう。

Anthropic 形式の入口(https://api.deepseek.com/anthropic)では、Claude のモデル名を読み替えます。claude-opus で始まる名前は deepseek-v4-pro(V4 Pro 価格)、claude-haiku / claude-sonnet で始まる名前は deepseek-flash になります。claude で始まらない未知の名前は 400 エラーです。

半年の動き

2026-04-24V4 プレビュー — deepseek-v4-pro / deepseek-v4-flash を追加 2026-07-24deepseek-chat / deepseek-reasoner の廃止予定日(4 月に告知) 2026-07-31V4-Flash 更新(0731)— Responses API 形式に対応(Codex 向け) 2026-08-13V4-Pro GA — API 全体で Responses API 対応と記載・effort 3 段階ピーク / オフピーク料金を発表(8/16 16:00 UTC 発効) 2026-08-21V4-Flash-Vision-Exp — 視覚理解の実験モデル 2026-09-10V4.1-Flash — 新アーキテクチャ、deepseek-flash で提供、値下げ 2026-10-17V4.1-Flash-1017 — deepseek-flash を更新、Search / Fetch API を公開
図 3 2026 年の主な更新(Change Log より)

V4-Pro の扱いについて、2026-09-10 のニュースページは「段階的に終了し、9 月 14 日から deepseek-v4-pro を V4.1-Flash へ転送する」と書いています。一方、同日の Change Log は「要望を受け、9 月 14 日以降も V4 Pro の API 提供を続ける」と書き、現行の料金表も V4-Pro-0813 を独自の価格で載せています。本ガイドは Change Log と料金表を現行の情報として扱います。

一次情報:Models & Pricing / Change Log / DeepSeek-V4.1-Flash Release / Anthropic API

03 / BENCHMARKS

1017 版で何が伸びたか

DeepSeek は 1017 版を「前版より強いベンチマーク、速いタスク完了、短い出力」と説明し、性能とコストの両方が良くなった(パレート改善)としています。モデルカードによれば、推論・コーディング・エージェント・視覚のベンチマークで改善し、タスクの実行時間と出力の長さも短くなりました。

この章の数値はすべて DeepSeek の公表値(2026-10-20 時点)です。第三者による検証ではありません。比較には Hugging Face のモデルカードの表を使います。同じ表に前版(2026-09-10 版)が並んでいるため、条件をそろえて比べられます。

V4.1-Flash-1017 V4.1-Flash(2026-09-10 版) HLE(ツールなし) HLE(ツールなし) · V4.1-Flash-1017: 42.6 HLE(ツールなし) · V4.1-Flash(2026-09-10 版): 36.8 42.6(+5.8) Terminal-Bench 4.0 Terminal-Bench 4.0 · V4.1-Flash-1017: 42.3 Terminal-Bench 4.0 · V4.1-Flash(2026-09-10 版): 31.2 42.3(+11.1) DeepSWE v1.1 DeepSWE v1.1 · V4.1-Flash-1017: 77.0 DeepSWE v1.1 · V4.1-Flash(2026-09-10 版): 74.2 77.0(+2.8) ProgramBench ProgramBench · V4.1-Flash-1017: 29.1 ProgramBench · V4.1-Flash(2026-09-10 版): 20.3 29.1(+8.8) NL2Repo-Bench NL2Repo-Bench · V4.1-Flash-1017: 68.2 NL2Repo-Bench · V4.1-Flash(2026-09-10 版): 65.4 68.2(+2.8) Automation-Bench v1.0.6 Automation-Bench v1.0.6 · V4.1-Flash-1017: 60.5 Automation-Bench v1.0.6 · V4.1-Flash(2026-09-10 版): 54.8 60.5(+5.7) Agents’ Last Exam Agents’ Last Exam · V4.1-Flash-1017: 35.4 Agents’ Last Exam · V4.1-Flash(2026-09-10 版): 31.8 35.4(+3.6) Apex Bench v1.1 Apex Bench v1.1 · V4.1-Flash-1017: 72.1 Apex Bench v1.1 · V4.1-Flash(2026-09-10 版): 64.6 72.1(+7.5) Chartography(ツールあり) Chartography(ツールあり) · V4.1-Flash-1017: 83.0 Chartography(ツールあり) · V4.1-Flash(2026-09-10 版): 78.9 83.0(+4.1) BabyVision(ツールあり) BabyVision(ツールあり) · V4.1-Flash-1017: 92.5 BabyVision(ツールあり) · V4.1-Flash(2026-09-10 版): 89.6 92.5(+2.9) ZeroBench-main(ツールあり) ZeroBench-main(ツールあり) · V4.1-Flash-1017: 51.0 ZeroBench-main(ツールあり) · V4.1-Flash(2026-09-10 版): 49.0 51.0(+2.0) 0 25 50 75 100
図 4 前版との比較。数値は 1017 版の値と前版からの差(DeepSeek 公表値・2026-10-20 時点。HF モデルカードの表より。前版の値は下の表とバーのツールチップに記載)

11 項目すべてで 1017 版が前版を上回っています。差が大きいのは Terminal-Bench 4.0(+11.1)、ProgramBench(+8.8)、Apex Bench v1.1(+7.5)、HLE(+5.8)、Automation-Bench v1.0.6(+5.7)です。

他のモデルと並べると

同じモデルカードには、他社モデルとの比較も載っています。表の中で最も高い値を太字にしました。

DeepSeek-V4.1-Flash-1017 と他モデルのベンチマーク比較(DeepSeek 公表値)
ベンチマークDS-V4.1-Flash-1017DS-V4.1-FlashK3GLM-5.3GPT-5.6 SolOpus 5
HLE w/o tools42.6(44.0†)36.8(39.1†)43.542.0†44.556.3
Terminal-Bench 4.042.331.212.637.939.951.8
DeepSWE v1.177.074.267.566.973.074.0
ProgramBench29.120.317.519.023.037.0
NL2Repo-Bench68.265.458.058.056.875.3
Automation-Bench v1.0.660.554.846.748.845.850.3
Agents’ Last Exam35.431.827.628.526.728.6
Apex Bench v1.172.164.647.856.654.365.8
Chartography (w/tools)83.078.968.1—79.984.0
BabyVision (w/tools)92.589.685.7—88.994.1
ZeroBench-main (w/tools)51.049.041.0—53.052.0

1017 版は、DeepSWE v1.1・Automation-Bench・Agents’ Last Exam・Apex Bench の 4 項目で表内の最高値です。Opus 5 は 6 項目で最高値、GPT-5.6 Sol は ZeroBench-main で最高値でした。V4.1-Flash は新しいアーキテクチャ系列で最小のモデルと説明されています。最小のモデルでありながら、公表値の上では比較対象を上回る項目と、まだ差がある項目の両方があります。

数字を読むときの注意
  • † は HLE のテキストのみのサブセットでの値です。
  • Terminal-Bench 4.0 は Claude Code、DeepSWE v1.1 は Mini-SWE-V2 というハーネス(評価用の実行環境)で測っています。Change Log には「ZeroBench-main は Pass@5」とも書かれています。
  • Change Log の 2026-09-10 版の記載は「HLE」「Automation-Bench」で、1017 版の「HLE (w/o tools)」「Automation-Bench v1.0.6」と表記が違います。前版との比較は、同じ表に両方が並ぶモデルカードの値で行いました。
  • 自分の用途での良し悪しは、自分のデータで試して判断します。

1017 版について、DeepSeek は「自己フィードバックによる強化学習で、ゲーム制作から 3D モデリングまでのプロジェクト開発を改善した」と説明し、故宮の 3D モデルや Web のレースゲームなどの作例を公開しています。また DeepSeek Harness・Tencent CodeBuddy・WorkBuddy 向けに最適化され、DeepSeek Harness は v0.2.1-rc.1 以降が推奨されています。

一次情報:DeepSeek-V4.1-Flash-1017(Hugging Face) / Change Log 2026-10-17 / DeepSeek-V4.1-Flash-1017 Release

04 / STRENGTHS & PRICING

強みを 6 つの数字でつかむ

PRICE

安い単価と時間帯割引

入力(キャッシュミス)は 100 万トークンあたり $0.3、出力は $1.2(ピーク時)。オフピークは半額です。キャッシュヒットした入力は $0.006 で、ミスの 50 分の 1 です。

CONTEXT

1M トークンの文脈

コンテキスト長は 1M、最大出力は 384K(料金表)。長い資料や、ツール結果が積み重なるエージェントの会話を 1 回の要求に収めやすくなります。

THINKING

考える深さを選べる

思考モードは既定で有効で、深さ(effort)の既定は high です。low / high / max から選べ、Change Log(2026-08-13)は「簡単な作業は low、日常のエージェント作業は high、より複雑な場面は max」を目安に挙げています。料金は入出力のトークン数で決まり、DeepSeek は 1017 版で出力が短くなったと説明しています。ただし、思考のトークンがどう課金されるかは、本ガイドで確認したページには明記されていません。

COMPATIBILITY

既存のコードから移りやすい

OpenAI 形式・Anthropic 形式・Responses API の 3 形式に対応します。JSON Output、Tool Calls、Vision(画像入力)も使えます。Vision は deepseek-flash だけの機能です。

OPEN WEIGHTS

重みが公開されている

MIT ライセンスです(Hugging Face のモデルカードの表示)。重みは Hugging Face と ModelScope で公開されています。総パラメータ数は、2026-09-10 の発表では「552B の MoE(入力 8B・出力 16B がアクティブ)」(MoE は Mixture of Experts の略で、多数の専門家ネットワークのうち一部だけを動かして計算量を抑える構成)、Hugging Face のページ表示では 763B で、数え方の説明はありません。

THROUGHPUT

多くの要求を同時に流せる

アカウントあたりの同時実行数の上限は 2,500(deepseek-v4-pro は 500)。9 月の発表では、KV キャッシュの容量を前世代の HBM 4 分の 1・SSD 8 分の 1 に圧縮したとしています。

料金表(100 万トークンあたり・USD・2026-10-20 時点)

deepseek-flash と deepseek-v4-pro の料金(2026-10-20 時点)
項目flash オフピークflash ピークv4-pro オフピークv4-pro ピーク
入力(キャッシュヒット)$0.003$0.006$0.022$0.044
入力(キャッシュミス)$0.15$0.3$0.66$1.32
出力$0.6$1.2$1.98$3.96
Search / Fetch
(単位が異なる:成功 1,000 件あたり)
それぞれ $0.75。直接呼ぶ場合はこの料金で、モデルの料金とは別(時間帯による区別の記載なし)。Harness / Claude Code / Codex 経由のサーバー側呼び出しは北京時間 2026-11-01 00:00(日本時間 01:00)まで無料で、以後は同じ Search / Fetch 料金(11 章)

ピークは月〜金の 01:00–04:00 と 06:00–10:00(UTC)で、日本時間では月〜金の 10:00–13:00 と 15:00–19:00です。中国の祝日は終日オフピーク、週末もオフピークです。急がない一括処理はオフピークに回すと半額になります。

キャッシュ(ディスク上の文脈キャッシュ)は設定不要で、既定で有効です。要求の入力の終わりや出力の終わりなどで「キャッシュ単位(cache prefix unit)」が保存され、次の要求の先頭がその単位と完全に一致したときだけヒットします。たとえば 1 回目が A + B、2 回目が A + B + C なら、A + B の部分がヒットします。応答の usage にある prompt_cache_hit_tokens と prompt_cache_miss_tokens で、ヒットした量を確認できます。ヒット率は保証されず(ベストエフォート)、キャッシュの構築には数秒かかり、使われなくなると通常数時間〜数日で消えます。

TRY · 料金計算機

月の利用量から費用を見積もる

時間帯
内訳deepseek-flashdeepseek-v4-pro

2026-10-20 時点の公表単価で計算します。ヒット率 80% は例示値なので、実際の値は usage の prompt_cache_hit_tokens から求めて入れてください。Search / Fetch は直接呼ぶ場合の料金です。Harness / Claude Code / Codex 経由のサーバー側呼び出しは北京時間 2026-11-01 00:00(日本時間 01:00)まで無料で、以後は同じ Search / Fetch 料金です。実際の請求は時間帯ごとの利用量で決まり、料金は改定されることがあります。

一次情報:Models & Pricing / Thinking Mode / Context Caching / Rate Limit & Isolation / Change Log 2026-08-13 / V4.1-Flash Release / V4.1-Flash-1017 Release(ModelScope) / Hugging Face

05 / PREPARE

環境を準備し、最初の 1 回を呼ぶ

実習コードは Python 3.10 以上で動きます。使うライブラリは、Chat Completions 用の OpenAI SDK と、Search / Fetch 用の requests の 2 つです。DeepSeek API は OpenAI 形式と互換なので、OpenAI SDK の base_url を差し替えるだけで使えます。

requirements.txt
openai==3.35.1
requests==2.34.2
仮想環境を作る · macOS / Linux(展開したフォルダーで実行)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
仮想環境を作る · Windows(PowerShell)
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt

PowerShell の実行ポリシーで Activate.ps1 が止められた場合は、先に Set-ExecutionPolicy -Scope Process RemoteSigned を実行し、確認を求められたら Y を入力します(そのウィンドウだけに効きます)。仮想環境を有効にした後は、どの OS でも python で実行します。新しいターミナルを開いたら、最初に仮想環境を有効にしてください。有効にしないまま実行すると ModuleNotFoundError になります。

API キーを用意する

DeepSeek Platform で API キーを発行し、環境変数 DEEPSEEK_API_KEY に入れます。キーをコードに直接書かないでください。

実 API を使う場合 · macOS / Linux
source .venv/bin/activate
export DEEPSEEK_API_KEY="<DeepSeek API Key>"
unset DEEPSEEK_BASE_URL     # 既定の https://api.deepseek.com を使う
実 API を使う場合 · Windows(PowerShell)
.venv\Scripts\Activate.ps1
$env:DEEPSEEK_API_KEY="<DeepSeek API Key>"
Remove-Item Env:DEEPSEEK_BASE_URL -ErrorAction SilentlyContinue
キーがなくても試せます。 同梱の mock_server.py は、公式ドキュメントに書かれた入力検証・エラー形式・応答の形を再現したローカルサーバーです。料金はかからず、ネットワークにも出ません。応答の文面は固定の練習用です。
モックを使う場合 · ターミナル 1
source .venv/bin/activate        # Windows: .venv\Scripts\Activate.ps1
python mock_server.py            # http://127.0.0.1:8787 で待ち受け
モックを使う場合 · ターミナル 2 · macOS / Linux
source .venv/bin/activate
export DEEPSEEK_BASE_URL=http://127.0.0.1:8787
export DEEPSEEK_API_KEY=test-key   # モックは空でないキーなら受け付ける
モックを使う場合 · ターミナル 2 · Windows(PowerShell)
.venv\Scripts\Activate.ps1
$env:DEEPSEEK_BASE_URL="http://127.0.0.1:8787"
$env:DEEPSEEK_API_KEY="test-key"

まず curl で形を見る

公式の Quick Start と同じ要求です。thinking で思考モードを有効にし、reasoning_effort で深さを指定しています。エンドポイントは /chat/completions で、/v1 は付きません。

curl https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${DEEPSEEK_API_KEY}" \
  -d '{
        "model": "deepseek-flash",
        "messages": [
          {"role": "system", "content": "You are a helpful assistant."},
          {"role": "user", "content": "Hello!"}
        ],
        "thinking": {"type": "enabled"},
        "reasoning_effort": "high",
        "stream": false
      }'

共通部品を読む

全実習が使う部品を ds_client.py にまとめました。接続先は環境変数 DEEPSEEK_BASE_URL で切り替えます。モデル名は deepseek-flash だけを使います。

ds_client.py の全文を見る
ds_client.py
"""DeepSeek API 用の共通部品。

Chat は OpenAI SDK、Search / Fetch は生の JSON API(requests)で呼ぶ。
01〜05 のスクリプトはすべてここを経由する。
"""

from __future__ import annotations

import os
import random
import sys
import time
from typing import Any

import requests
from openai import APIError, APIStatusError, OpenAI

# DEEPSEEK_BASE_URL が無ければ本番の既定値(/v1 は付けない)。
BASE_URL: str = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com").rstrip("/")
MODEL = "deepseek-flash"
HIGH_QUALITY_SCORE = 1.5  # 公式ガイド(Search & Fetch): rerank_score がこれ以上で高品質
HIGH_AUTHORITY = 3  # 公式ガイド(Search & Fetch): authority がこれ以上で高権威

# DeepSeekToolError.status の特別な値。
# 0: requests の接続エラー・タイムアウトで、そもそも HTTP ステータスが無い場合。
# -1: HTTP 200 だが、応答の形(JSON か・必要なキーがあるか)が期待と違う場合。
CONNECTION_ERROR_STATUS = 0
MALFORMED_RESPONSE_STATUS = -1

RETRY_AFTER_CAP_SECONDS = 30.0  # サーバー指定の待機時間が長すぎても CLI を固まらせないための上限

# テストから差し替えられるよう、time.sleep をモジュール変数として公開する。
sleep = time.sleep


class DeepSeekToolError(Exception):
    """Search / Fetch 呼び出しの失敗を表す例外。"""

    def __init__(self, status: int, message: str, *, retryable: bool) -> None:
        super().__init__(f"[{status}] {message}")
        self.status = status
        self.message = message
        self.retryable = retryable


def get_api_key() -> str:
    """DEEPSEEK_API_KEY を返す。未設定なら日本語メッセージを出して終了コード 2。"""
    key = os.environ.get("DEEPSEEK_API_KEY")
    if not key:
        print(
            "環境変数 DEEPSEEK_API_KEY が設定されていません。"
            "DeepSeek の API キーを設定してから実行してください。",
            file=sys.stderr,
        )
        raise SystemExit(2)
    return key


def chat_client() -> OpenAI:
    """Chat Completions 用の OpenAI クライアントを作る。"""
    return OpenAI(api_key=get_api_key(), base_url=BASE_URL)


def _extract_error_message(resp: requests.Response) -> str:
    try:
        data = resp.json()
    except ValueError:
        return resp.text
    if not isinstance(data, dict):
        return resp.text
    error = data.get("error")
    if isinstance(error, dict):
        return str(error.get("message", resp.text))
    if isinstance(error, str):
        return error
    return resp.text


def format_tool_error(exc: DeepSeekToolError) -> str:
    """DeepSeekToolError を表示用の日本語 1 行メッセージにする(02/03/04 で共通に使う)。"""
    if exc.status == CONNECTION_ERROR_STATUS:
        # requests の接続エラー・タイムアウトには HTTP ステータスが無いので、
        # 「HTTP 0」ではなく通信エラーであることをそのまま伝える。
        return f"通信エラー: {exc.message}"
    if exc.status == MALFORMED_RESPONSE_STATUS:
        # HTTP 200 なので「HTTP 200」と言うと成功したように読めてしまう。
        return f"応答の形式が不正です: {exc.message}"
    return f"エラー(HTTP {exc.status}): {exc.message}"


def format_chat_error(exc: APIError) -> str:
    """openai の例外を表示用の日本語 1 行メッセージにする(01/04/05 で共通に使う)。"""
    if isinstance(exc, APIStatusError):
        body = exc.body
        message = body.get("message") if isinstance(body, dict) else str(exc)
        return f"エラー(HTTP {exc.status_code}): {message}"
    # APIConnectionError など、ステータスコードを持たない例外はまとめて通信エラーとして扱う。
    return f"通信エラー: {getattr(exc, 'message', str(exc))}"


def _backoff_delay(attempt: int, retry_after: str | None) -> float:
    """指数バックオフ(0.5s, 1s, 2s…)+ ジッター。Retry-After があれば優先(ただし 30 秒を上限にする)。"""
    if retry_after is not None:
        try:
            return min(float(retry_after), RETRY_AFTER_CAP_SECONDS)
        except ValueError:
            pass
    base = 0.5 * (2 ** (attempt - 1))
    return base + random.uniform(0, base * 0.1)


def post_tool(path: str, payload: dict[str, Any], *, max_attempts: int = 4) -> dict[str, Any]:
    """Search / Fetch の生 JSON API を叩く。再試行つき。"""
    key = get_api_key()
    url = f"{BASE_URL}{path}"
    headers = {"Authorization": f"Bearer {key}"}

    attempt = 0
    while True:
        attempt += 1
        try:
            resp = requests.post(url, headers=headers, json=payload, timeout=60)
        except (requests.ConnectionError, requests.Timeout) as exc:
            if attempt >= max_attempts:
                raise DeepSeekToolError(CONNECTION_ERROR_STATUS, str(exc), retryable=True) from exc
            sleep(_backoff_delay(attempt, None))
            continue

        if resp.status_code == 200:
            try:
                return resp.json()
            except ValueError:
                raise DeepSeekToolError(MALFORMED_RESPONSE_STATUS, "JSON として解釈できませんでした", retryable=False)

        message = _extract_error_message(resp)

        if resp.status_code == 402:
            # 残高不足は再試行しても解決しないため即座に送出する。サーバーのメッセージも付記する。
            raise DeepSeekToolError(
                402,
                f"残高不足です。入金してから再試行してください(サーバー: {message})。",
                retryable=False,
            )

        if resp.status_code == 408 or resp.status_code == 429 or resp.status_code >= 500:
            if attempt >= max_attempts:
                raise DeepSeekToolError(resp.status_code, message, retryable=True)
            sleep(_backoff_delay(attempt, resp.headers.get("Retry-After")))
            continue

        # それ以外の 4xx は要求を変えずに再試行しない。
        raise DeepSeekToolError(resp.status_code, message, retryable=False)


def search(
    query: str,
    *,
    count: int = 5,
    need_content: bool = False,
    ori_question: str | None = None,
) -> list[dict[str, Any]]:
    """Search API を呼び、results を返す。"""
    payload: dict[str, Any] = {"query": query, "count": count, "need_content": need_content}
    if ori_question is not None:
        payload["ori_question"] = ori_question
    data = post_tool("/v1/tools/search", payload)
    results = data.get("results")
    if not isinstance(results, list):
        raise DeepSeekToolError(MALFORMED_RESPONSE_STATUS, "results がありません", retryable=False)
    return results


def fetch(url: str) -> dict[str, Any]:
    """Fetch API を呼び、{"title", "content"} を返す。"""
    data = post_tool("/v1/tools/fetch", {"url": url})
    if not isinstance(data.get("title"), str) or not isinstance(data.get("content"), str):
        raise DeepSeekToolError(
            MALFORMED_RESPONSE_STATUS, "title または content が文字列ではありません", retryable=False
        )
    return data


def is_high_quality(result: dict[str, Any]) -> bool:
    """rerank_score が高品質の基準を満たすか。"""
    return result.get("rerank_score", 0) >= HIGH_QUALITY_SCORE


def is_authoritative(result: dict[str, Any]) -> bool:
    """authority が高権威の基準を満たすか。"""
    return result.get("authority", -1) >= HIGH_AUTHORITY
LAB 1 · 10 分 · Chat Completions

思考モードの出力を見る

01_hello.py
"""最初の Chat Completions 呼び出し。

思考モードの reasoning_content(思考の過程)と content(最終回答)、
usage(トークン使用量)を表示する。
"""

from __future__ import annotations

import argparse
import sys
from typing import Any

from openai import APIError

from ds_client import MODEL, chat_client, format_chat_error

DEFAULT_QUESTION = "DeepSeek の Search API と Fetch API の違いを 2 文で説明してください。"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="DeepSeek Chat Completions の最初の呼び出し")
    parser.add_argument("question", nargs="?", default=DEFAULT_QUESTION, help="質問文(省略可)")
    parser.add_argument("--no-thinking", action="store_true", help="思考モードを無効にする")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    client = chat_client()

    messages = [
        {"role": "system", "content": "あなたは日本語で簡潔に答えるアシスタントです。"},
        {"role": "user", "content": args.question},
    ]

    # 公式ガイド(Thinking Mode): OpenAI SDK では thinking を extra_body に入れる。
    # 無効にするときは reasoning_effort を付けない。
    kwargs: dict[str, Any] = {}
    if args.no_thinking:
        kwargs["extra_body"] = {"thinking": {"type": "disabled"}}
    else:
        kwargs["reasoning_effort"] = "high"
        kwargs["extra_body"] = {"thinking": {"type": "enabled"}}

    try:
        response = client.chat.completions.create(model=MODEL, messages=messages, **kwargs)
    except APIError as exc:
        print(format_chat_error(exc), file=sys.stderr)
        raise SystemExit(1)
    message = response.choices[0].message

    reasoning_content = getattr(message, "reasoning_content", None)
    print("[思考]")
    print(reasoning_content or "(思考なし)")
    print()
    print("[回答]")
    print(message.content)
    print()

    usage = response.usage
    print("[usage]")
    print(f"prompt_tokens: {usage.prompt_tokens}")
    print(f"completion_tokens: {usage.completion_tokens}")
    # ディスクキャッシュのヒット/ミス(公式ガイドの KV Cache)。応答に無い場合がある。
    hit = getattr(usage, "prompt_cache_hit_tokens", None)
    miss = getattr(usage, "prompt_cache_miss_tokens", None)
    if hit is not None:
        print(f"prompt_cache_hit_tokens: {hit}")
    if miss is not None:
        print(f"prompt_cache_miss_tokens: {miss}")


if __name__ == "__main__":
    main()
python 01_hello.py
python 01_hello.py --no-thinking
モックでの表示例(実 API では文面と数値が変わる)
[思考]
「DeepSeek の Search API と Fetch API の違いを 2」について考えます。

[回答]
「DeepSeek の Search API と Fetch API の違いを 2 文で説明してください。」についてお答えします(モック応答)。

[usage]
prompt_tokens: 72
completion_tokens: 62
prompt_cache_hit_tokens: 0
prompt_cache_miss_tokens: 72

思考モードでは、答え(content)の前にモデルの考えた過程が reasoning_content として返ります。--no-thinking を付けると思考が無効になり、reasoning_content は返りません。

OpenAI SDK で書くときの注意
  • thinking は SDK の通常の引数にないため、extra_body={"thinking": {...}} に入れます。reasoning_effort は通常の引数で渡せます。
  • 思考モードでは temperature・presence_penalty・frequency_penalty が効きません。エラーにもならないので、気づかずに調整してしまいがちです。
  • deepseek-flash の top_p は思考モードでのみ有効で、0.95 未満は 0.95 として扱われます。非思考モードでは 1.0 に固定され、指定した値は無視されます。
  • effort に medium や xhigh を指定すると high に、minimal は low に、ultra は max に読み替えられます。

一次情報:Your First API Call / Thinking Mode

07 / FETCH API

Fetch API でページを読む

Fetch API は、URL を 1 つ受け取り、そのページを Markdown に変換して返します。検索結果の要約では足りないとき、元のページを読ませるために使います。

curl https://api.deepseek.com/v1/tools/fetch \
  -H "Authorization: Bearer <DeepSeek API Key>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/article"
  }'
{
  "title": "Example article",
  "content": "# Example article\n\nArticle content."
}
  • リクエストは url の 1 項目だけです。8,192 文字以内で、ホスト名のある HTTP / HTTPS の URL を指定します。ユーザー名やパスワードを含む URL は使えません。
  • title は取得できなければ空文字です。content は Markdown のテキストです。
  • 取得できるかどうかは、対象サイトの稼働状況とアクセス方針に左右されます。
ステータス意味対応
403セキュリティポリシーで要求が拒否されたその URL は使わず、別の情報源を探す
404使える本文を取得できなかった(URL が無効、サイトの方針で遮断、対象サイトのエラー、未対応の形式、Markdown を抽出できない など)同じ URL で再試行せず、検索結果の summary で代用する
429QPS(1 秒あたりの要求数)の上限を超えた。Fetch にはアカウント単位の 25 に加え、ドメイン単位の上限もある待ってから再試行する
LAB 3 · 5 分 · Fetch API

ページを Markdown として読む

03_fetch.py
"""Fetch API の呼び出しサンプル。

使い方:
    python 03_fetch.py https://api-docs.deepseek.com/guides/search --chars 600
"""

from __future__ import annotations

import argparse
import sys

from ds_client import DeepSeekToolError, fetch, format_tool_error


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="DeepSeek Fetch API を呼び出す")
    parser.add_argument("url", help="取得する URL")
    parser.add_argument("--chars", type=int, default=600, help="本文を表示する先頭文字数(既定 600)")
    return parser.parse_args()


def main() -> None:
    args = parse_args()

    try:
        page = fetch(args.url)
    except DeepSeekToolError as exc:
        if exc.status == 403:
            print("セキュリティポリシーで拒否されました。", file=sys.stderr)
        elif exc.status == 404:
            print("使える本文を取得できなかった(URL 不可・サイト方針・未対応形式など)。", file=sys.stderr)
        else:
            print(format_tool_error(exc), file=sys.stderr)
        raise SystemExit(1)

    print(f"[タイトル] {page['title'] or '(タイトルなし)'}")
    print("[本文]")
    print(page["content"][: args.chars])


if __name__ == "__main__":
    main()
python 03_fetch.py https://api-docs.deepseek.com/guides/search --chars 600
python 03_fetch.py https://blocked.example/page    # モックで 403 を体験
python 03_fetch.py https://missing.example/page    # モックで 404 を体験

モックでは、ホスト名が blocked.example なら 403、missing.example なら 404 を返します。エラー時の表示と終了コードを確かめてください。

一次情報:Search & Fetch — Fetch API / Fetch API Reference

08 / GROUNDED ANSWER

検索結果を根拠に、出典つきで答えさせる

モデルは学習した時点までの知識で答えます。最新の情報を答えさせるには、検索した資料を要求に入れ、「資料だけを根拠に答える」よう指示します。このように検索と生成を組み合わせる方法を RAG(検索拡張生成)と呼びます。ここではその最小形を作ります。

① 検索する Search API count=8 ori_question ② 選別する rerank_score 1.5 以上を優先 上位 3 件 ③ 資料を組む [1] タイトル URL・日付 summary ④ 答えさせる deepseek-flash 思考モード OFF 出典を [n] で示す
図 7 出典つき回答を作る 4 段階
  1. 検索する。ori_question にユーザーの質問をそのまま渡すと、検索語の解釈を助けられます。query と ori_question はどちらも 128 文字までなので、長い質問は先頭 128 文字に切って渡します。
  2. 選別する。rerank_score が 1.5 以上の結果を優先し、上位 3 件に絞ります。高品質な結果が 1 件もなければ、警告を出したうえで上位 3 件を使います。結果が 0 件なら、モデルを呼ばずに終了します(空の結果は課金されません)。
  3. 資料を組む。結果ごとに番号・タイトル・URL・日付・summary を並べます。番号があると、回答の文末で出典を示せます。
  4. 答えさせる。system メッセージで「与えた資料だけを根拠に答え、資料にないことは分からないと書く」と指示します。
なぜ思考モードを切るのか。 この段階の仕事は、渡した資料を読みやすくまとめることです。深い推論は必要ないため、思考を無効にしています。料金は入出力のトークン数で決まるので、思考の出力がなければその分のトークンが減ると期待できます(実際の差は usage で確かめてください)。複数の資料を突き合わせて判断させたい場合は、思考モードを有効にして比べてください。
LAB 4 · 15 分 · Search + Chat

出典つきで答えさせる

04_grounded_answer.py
"""検索結果を文脈に入れて出典つきで回答させる、RAG(検索拡張生成)の最小形。

使い方:
    python 04_grounded_answer.py "質問"
"""

from __future__ import annotations

import argparse
import sys

from openai import APIError

from ds_client import MODEL, DeepSeekToolError, chat_client, format_chat_error, format_tool_error, is_high_quality, search


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="検索結果を根拠に出典つきで回答する")
    parser.add_argument("question", help="質問文")
    return parser.parse_args()


def build_context(selected: list[dict]) -> str:
    """選んだ検索結果を `[番号] タイトル / URL / 日付 / 要約` の形で文脈テキストにする。"""
    blocks = []
    for i, result in enumerate(selected, start=1):
        date = result.get("date") or "-"
        blocks.append(f"[{i}] {result['title']}\nURL: {result['url']}\n日付: {date}\n要約: {result['summary']}")
    return "\n\n".join(blocks)


def main() -> None:
    args = parse_args()

    if not args.question.strip():
        print("質問が空です。1 文字以上の質問を指定してください。", file=sys.stderr)
        raise SystemExit(2)

    # 公式ガイド(Search & Fetch): query / ori_question はどちらも 1〜128 文字の制限があるため、
    # 質問が長い場合は先頭 128 文字に切り詰めてから渡す(チャットに渡す質問文そのものは切らない)。
    query = args.question[:128]
    try:
        results = search(query, count=8, ori_question=query)
    except DeepSeekToolError as exc:
        print(format_tool_error(exc), file=sys.stderr)
        raise SystemExit(1)
    if not results:
        print("検索結果が 0 件でした(空の結果は課金されません)。質問を変えて再実行してください。")
        raise SystemExit(1)

    high_quality = [r for r in results if is_high_quality(r)]
    if high_quality:
        selected = high_quality[:3]
    else:
        print("警告: 高品質(rerank_score >= 1.5)の結果が無いため、上位 3 件をそのまま使います。")
        selected = results[:3]

    context = build_context(selected)
    messages = [
        {
            "role": "system",
            "content": (
                "与えた資料だけを根拠に日本語で答え、文末に [番号] で出典を示してください。"
                "資料に無いことは「資料からは分からない」と書いてください。\n\n" + context
            ),
        },
        {"role": "user", "content": args.question},
    ]

    client = chat_client()
    # 出典つきの短い要約タスクなので思考モードを無効にし、応答を速く・安くする
    # (公式ガイド(Thinking Mode)の切替の実演。04 は 01/05 と対照的に非思考モードを使う)。
    try:
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            extra_body={"thinking": {"type": "disabled"}},
        )
    except APIError as exc:
        print(format_chat_error(exc), file=sys.stderr)
        raise SystemExit(1)

    print("[回答]")
    print(response.choices[0].message.content)
    print()
    print("[出典]")
    for i, result in enumerate(selected, start=1):
        print(f"[{i}] {result['title']} ({result['url']})")


if __name__ == "__main__":
    main()
python 04_grounded_answer.py "DeepSeek の Search API の料金体系は?"

回答の文末に [1] のような番号が付き、その下に出典一覧が表示されれば成功です。モックは count が 6 を超えても 6 件までしか返しません(実 API は最大 count 件)。実 API では、検索語を変えて出典の顔ぶれがどう変わるか確かめてください。

一次情報:Search & Fetch / Thinking Mode

09 / SEARCH AGENT

モデルに、自分で調べさせる

前の章では、検索するかどうかをコードが決めていました。この章では、検索と取得をツールとしてモデルに渡し、いつ何を調べるかをモデルに決めさせます。Chat Completions の Tool Calls(関数呼び出し)を使います。

押さえておく原則は 1 つです。モデルは関数を実行しません。モデルが返すのは「この関数をこの引数で呼んでほしい」という依頼(tool_calls)です。実際に Search / Fetch API を呼び、その結果を会話に戻すのはあなたのコードです。

あなたのコード deepseek-flash Search / Fetch API ① messages(user)+ tools ② tool_calls: web_search(思考つき) ③ POST /v1/tools/search ④ results(件数分) ⑤ 履歴(reasoning_content 付き)+ 結果 ⑥ tool_calls: web_fetch ⑦ POST /v1/tools/fetch ⑧ title + Markdown 本文 ⑨ 履歴(reasoning_content 付き)+ 結果 ⑩ content = 最終回答(tool_calls なし) ⑤⑨ で reasoning_content を返さないと 400 エラー
図 8 検索エージェントの 1 ターン(ツール呼び出し 2 回の例)

最重要ルール:reasoning_content を返す

tools を付けた要求では、それまでの全ターンの reasoning_content を返す。 思考モードでツールを使うとき、モデルの思考(reasoning_content)は次の要求で会話に連結され、モデルは前の思考の続きから考えます。ツールを呼ばなかったターンも含めて、assistant メッセージの reasoning_content を残して送り返してください。返さないと API は 400 エラーを返します。tools を付けない普通の会話では返送は不要で、送っても無視されます。

公式サンプルは messages.append(response.choices[0].message) で、SDK が返したメッセージをそのまま履歴に追加しています。このメッセージには content・reasoning_content・tool_calls が含まれます。本ガイドの実習コードは、何を送っているかが見えるように、同じ内容の辞書を明示的に作っています。

TRY · ループの手順表示

messages が育っていく様子を 1 段ずつ見る

  1. 要求 1
  2. 応答 1
  3. 要求 2
  4. 応答 2
  5. 要求 3
  6. 最終応答

ツールの定義とループ

ツールは JSON Schema で定義します。web_search は検索語と件数、web_fetch は URL を受け取ります。ループは「モデルを呼ぶ → tool_calls があれば実行して結果を追加 → もう一度モデルを呼ぶ」をくり返し、tool_calls がない応答が来たら終わります。

  • 上限を決める。モデルが検索をくり返して止まらない場合に備え、ステップ数の上限(既定 8、--max-steps で変更)を設けます。
  • ツールの失敗は結果として返す。Fetch が 404 でも例外で止めず、{"error": "..."} をツール結果として返します。モデルは別の URL や検索語を選べます。引数が JSON として読めない・オブジェクトでない・必須の項目(query / url)がない場合も、同じようにエラーとして返します。この実習ではツール定義に strict を付けていないため、モデルの引数がスキーマどおりとは限らないからです。
  • 引数を範囲に収める。ツール定義で count を 1〜10 としていても、範囲外の値が来ることがあります。コード側で 1〜10 に丸めてから Search API を呼びます。
  • 結果を絞って返す。検索結果はタイトル・URL・日付・権威性・スコア・summary に絞り、取得した本文は先頭 6,000 文字で切ります。文脈とトークンの節約になります。
LAB 5 · 20 分 · Tool Calls + 思考モード

自分で調べるエージェントを動かす

05_search_agent.py
"""web_search / web_fetch を function tool としてモデルに渡すツール呼び出しエージェント。

使い方:
    python 05_search_agent.py "質問" [--max-steps 8] [--follow-up "2 問目"]
"""

from __future__ import annotations

import argparse
import json
import sys
from typing import Any

from openai import APIError, OpenAI

from ds_client import MODEL, DeepSeekToolError, chat_client, format_chat_error
from ds_client import fetch as ds_fetch
from ds_client import search as ds_search

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "Web を検索して関連ページの一覧を返す。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "検索クエリ(検索演算子を使える)"},
                    "count": {"type": "integer", "description": "取得件数(1〜10)", "minimum": 1, "maximum": 10},
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "web_fetch",
            "description": "指定した URL のページ本文を Markdown として取得する。",
            "parameters": {
                "type": "object",
                "properties": {"url": {"type": "string", "description": "取得する URL"}},
                "required": ["url"],
            },
        },
    },
]

FETCH_CONTENT_LIMIT = 6000  # 文脈節約のため、取得した本文は先頭 6000 文字に切る


def assistant_message(msg: Any) -> dict[str, Any]:
    """公式サンプルの `messages.append(response.choices[0].message)` と等価な辞書を作る。

    tools 付きの要求では、過去の assistant ターンの reasoning_content をすべて返送する
    (返送しないと 400)。ツールを呼ばなかったターンも同じなので、空文字でもキー自体は入れる。
    tool_calls が無ければキーごと省く。
    """
    message: dict[str, Any] = {
        "role": "assistant",
        "content": msg.content or "",
        "reasoning_content": getattr(msg, "reasoning_content", None) or "",
    }
    if msg.tool_calls:
        message["tool_calls"] = [tc.model_dump() for tc in msg.tool_calls]
    return message


def run_web_search(query: str, count: Any) -> tuple[str, str]:
    """web_search を実行する。戻り値は (モデルに返す JSON 文字列, 経過表示用の要約行)。"""
    # スキーマでは count を 1〜10 に制限しているが、non-strict モードではモデルが
    # 守らないことがある(999 を渡す、文字列で渡す、省略して None になる、など)ので丸める。
    try:
        count = int(count)
    except (TypeError, ValueError):
        count = 5
    count = min(max(count, 1), 10)

    try:
        results = ds_search(query, count=count)
    except DeepSeekToolError as exc:
        return json.dumps({"error": exc.message}, ensure_ascii=False), f"エラー: {exc.message}"
    trimmed = [
        {
            "title": r["title"],
            "url": r["url"],
            "date": r.get("date"),
            "authority": r["authority"],
            "rerank_score": r["rerank_score"],
            "summary": r["summary"],
        }
        for r in results
    ]
    return json.dumps(trimmed, ensure_ascii=False), f"検索結果 {len(trimmed)} 件"


def run_web_fetch(url: str) -> tuple[str, str]:
    """web_fetch を実行する。戻り値は (モデルに返す JSON 文字列, 経過表示用の要約行)。"""
    try:
        page = ds_fetch(url)
    except DeepSeekToolError as exc:
        return json.dumps({"error": exc.message}, ensure_ascii=False), f"エラー: {exc.message}"
    content = page["content"][:FETCH_CONTENT_LIMIT]
    trimmed = {"title": page["title"], "content": content}
    return json.dumps(trimmed, ensure_ascii=False), f"取得: {page['title']}({len(content)} 文字)"


def summarize_messages(messages: list[dict[str, Any]]) -> str:
    """messages 配列を 1 行に要約する(--show-messages 用)。

    assistant だけは reasoning_content の有無と tool_calls の件数を表示し、
    2 ターン目以降も前のターンの reasoning_content が残っていることを確認できるようにする。
    """
    parts = []
    for message in messages:
        role = message.get("role")
        if role == "assistant":
            has_reasoning = "あり" if message.get("reasoning_content") else "なし"
            tool_call_count = len(message.get("tool_calls") or [])
            parts.append(f"assistant(reasoning_content {has_reasoning}, tool_calls {tool_call_count})")
        else:
            parts.append(str(role))
    return "送信 messages: " + " / ".join(parts)


def call_tool(name: str, arguments: Any) -> tuple[str, str]:
    """モデルが生成した引数を実行する。引数の形が壊れていても例外を投げず、エラーを結果として返す。"""
    if not isinstance(arguments, dict):
        message = "引数が JSON オブジェクトではありません"
        return json.dumps({"error": message}, ensure_ascii=False), f"エラー: {message}"

    if name == "web_search":
        if "query" not in arguments:
            message = "引数に query がありません"
            return json.dumps({"error": message}, ensure_ascii=False), f"エラー: {message}"
        return run_web_search(arguments["query"], arguments.get("count", 5))

    if name == "web_fetch":
        if "url" not in arguments:
            message = "引数に url がありません"
            return json.dumps({"error": message}, ensure_ascii=False), f"エラー: {message}"
        return run_web_fetch(arguments["url"])

    message = f"未知のツール: {name}"
    return json.dumps({"error": message}, ensure_ascii=False), f"エラー: {message}"


def run_loop(
    messages: list[dict[str, Any]],
    client: OpenAI,
    max_steps: int,
    *,
    turn: int,
    show_messages: bool,
) -> bool:
    """ツール呼び出しループを 1 ターン分回す。最終回答に到達したら True を返す。"""
    for step in range(1, max_steps + 1):
        if show_messages:
            # 2 ターン目以降でも、前のターンの assistant(reasoning_content あり) が
            # messages に残ったまま送られていることをここで確認できる。
            print(summarize_messages(messages))

        try:
            response = client.chat.completions.create(
                model=MODEL,
                messages=messages,
                tools=TOOLS,
                reasoning_effort="high",
                extra_body={"thinking": {"type": "enabled"}},
            )
        except APIError as exc:
            print(format_chat_error(exc), file=sys.stderr)
            raise SystemExit(1)
        msg = response.choices[0].message
        messages.append(assistant_message(msg))  # reasoning_content を必ず含める

        reasoning = (getattr(msg, "reasoning_content", None) or "")[:120]
        print(f"[turn {turn} / step {step}] 思考: {reasoning}")

        if not msg.tool_calls:
            print("[回答]")
            print(msg.content)
            return True

        for call in msg.tool_calls:
            try:
                arguments = json.loads(call.function.arguments)
            except json.JSONDecodeError:
                # モデルの出力が壊れていても、例外で落とさずツール結果としてエラーを返す。
                print(f"→ {call.function.name}(引数の JSON 解析に失敗)")
                print("← エラー: 引数を JSON として読めませんでした")
                error_result = json.dumps({"error": "引数を JSON として読めませんでした"}, ensure_ascii=False)
                messages.append({"role": "tool", "tool_call_id": call.id, "content": error_result})
                continue

            print(f"→ {call.function.name}({arguments})")
            result, summary_line = call_tool(call.function.name, arguments)
            print(f"← {summary_line}")
            messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

    print("ステップ上限に達しました。")
    return False


def _positive_int(value: str) -> int:
    """argparse 用: 1 以上の整数のみを受け付ける。"""
    parsed = int(value)
    if parsed < 1:
        raise argparse.ArgumentTypeError("1 以上の整数を指定してください")
    return parsed


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="web_search / web_fetch を使うツール呼び出しエージェント")
    parser.add_argument("question", help="質問文")
    parser.add_argument("--max-steps", type=_positive_int, default=8, help="1 ターンあたりの最大ステップ数")
    parser.add_argument("--follow-up", help="2 問目の質問(指定すると続けて 2 ターン目を実行する)")
    parser.add_argument(
        "--show-messages",
        action="store_true",
        help="各ステップで送信する messages 配列を 1 行要約で表示する",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    client = chat_client()
    messages: list[dict[str, Any]] = [{"role": "user", "content": args.question}]

    if not run_loop(messages, client, args.max_steps, turn=1, show_messages=args.show_messages):
        raise SystemExit(1)

    if args.follow_up:
        # 前のターンの reasoning_content も messages に残したまま送り続ける。
        messages.append({"role": "user", "content": args.follow_up})
        if not run_loop(messages, client, args.max_steps, turn=2, show_messages=args.show_messages):
            raise SystemExit(1)


if __name__ == "__main__":
    main()
python 05_search_agent.py "DeepSeek の Fetch API の QPS 上限は?"
python 05_search_agent.py "DeepSeek の Fetch API の QPS 上限は?" --follow-up "Search API の上限は?" --show-messages
1 行目のコマンドのモックでの表示例
[turn 1 / step 1] 思考: ユーザーの質問に答えるため、まず web_search で情報を集めます。
→ web_search({'query': 'DeepSeek の Fetch API の QPS 上限は?', 'count': 3})
← 検索結果 3 件
[turn 1 / step 2] 思考: 検索結果の中から有力なページを 1 件取得します。
→ web_fetch({'url': 'https://example.com/mock/1'})
← 取得: Mock page(53 文字)
[turn 1 / step 3] 思考: 収集した資料を踏まえて最終回答をまとめます。
[回答]
収集した1 件の資料(例: Mock page)をもとに回答します(モック応答)。

各ステップで、モデルの思考の冒頭・呼んだツール・結果(検索の件数、取得したページのタイトルと文字数、またはエラー)が表示されます。--follow-up を付けると、同じ履歴に 2 問目を足してもう一度ループを回します(表示は turn 2)。--show-messages を付けると、モデルを呼ぶ直前に、送る messages の並びを 送信 messages: で始まる 1 行に要約して表示します。2 行目のコマンドを実行し、[turn 2 / step 1] の直前の行に、1 問目の assistant(reasoning_content あり, …) が残っていることを確かめてください。

2 行目のコマンドのモックでの表示例([turn 2 / step 1] の直前の行)
送信 messages: user / assistant(reasoning_content あり, tool_calls 1) / tool / assistant(reasoning_content あり, tool_calls 1) / tool / assistant(reasoning_content あり, tool_calls 0) / user

--max-steps は 1 ターンあたりのステップ数の上限で、1 以上の整数を指定します(0 以下は使い方のエラーとして終了コード 2)。

発展:strict モードで引数の形を保証する

Tool Calls には strict モード(Beta)があります。base_url を https://api.deepseek.com/beta にし、すべての function に "strict": true を付けると、モデルの出す引数が JSON Schema に厳密に従います。思考モード・非思考モードのどちらでも使えます。object では全プロパティを required に並べ、additionalProperties を false にする必要があります。

一次情報:Thinking Mode — Tool Calls / Tool Calls

10 / ERRORS, LIMITS & BILLING

エラー・上限・課金に備える

Search と Fetch は読み取り専用の API です。同じ要求を再送しても副作用はありません。そのため、ステータスコードを見て「待って再試行する」「直してから送る」を機械的に判断できます。

HTTP ステータス Search / Fetch の応答 200 結果を使う。results が空なら課金されない 408 / 429 / 5xx(500・504) 待って再試行する(読み取り専用なので安全) 402 残高不足。入金してから再試行する 400 / 401 / 403 / 404 / 413 要求を直す。そのまま再試行しない
図 9 ステータスコードごとの対応(公式の再試行ルール)

エラーの本文は OpenAI 互換の形で、param は常に null です。

{
  "error": {
    "message": "'count' must be between 1 and 50",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_request_error"
  }
}

実習コードの ds_client.post_tool() は、この図のとおりに動きます。408・429・5xx と通信エラーは最大 4 回まで試します。待ち時間は、応答に Retry-After ヘッダーがあればその秒数(ただし 30 秒が上限)、なければ間隔を倍々に広げます(指数バックオフ)。402 とそれ以外の 4xx は再試行せずにエラーを送出します。Chat の呼び出し(01・04・05)で API がエラーを返したり通信できなかったりしたときは、トレースバックではなく エラー(HTTP n): … または 通信エラー: … と表示して、終了コード 1 で終わります。

上限

対象上限補足
Search100 QPSアカウント単位。どの API キーでも合算。Fetch とは別枠
Fetch25 QPSアカウント単位に加え、取得先ドメインごとの上限もある(数値は非公開)
deepseek-flash同時実行 2,500要求を送ってから応答が終わるまでを 1 つと数える。拡張申請は無料
deepseek-v4-pro同時実行 500同上

上限を超えると、どれも HTTP 429 が返ります。Search / Fetch の QPS はモデルの同時実行数とは別に数えられます。また、推論が始まらないまま 10 分たった要求は、サーバー側で接続が閉じられます。

課金のルール

  • Search / Fetch は成功した要求 1 件ごとに課金されます(どちらも 1,000 件あたり $0.75)。トークン数や返った件数には関係なく、count を増やしても 1 件の料金は同じです。
  • 課金されるのは、HTTP 200 で結果がある場合だけです。Search は results が空でないこと、Fetch は本文が返ったことが条件です。失敗・空の結果・タイムアウトは課金されません。
  • モデルの料金は「トークン数 × 単価」です。付与された残高があれば、先にそちらから差し引かれます。

利用者ごとに分ける:user_id

1 つのアカウントで複数の利用者にサービスを提供する場合は、要求に user_id を付けると、コンテンツ安全・KV キャッシュ・スケジューリングを利用者ごとに分けられます。英数字・-・_ の 512 文字以内で、個人情報は入れません。OpenAI SDK では extra_body={"user_id": "..."} で渡します。

LAB 6 · 10 分 · モックで異常系

再試行・空の結果・入力エラーを確かめる

python 02_search.py "mock:429 DeepSeek"     # 最初の 2 回は 429、3 回目で成功
python 02_search.py "mock:empty DeepSeek"   # results が空(課金されない)
python 02_search.py "DeepSeek" --count 51   # count が範囲外 → 400、再試行しない
余裕があれば
python 02_search.py "mock:402 DeepSeek"     # 残高不足 → 402、再試行しない
python 01_hello.py "mock:chat400 テスト"     # Chat API の 400 をエラー表示で確かめる

モックは検索語に mock:429 を含むと最初の 2 回だけ 429 を返し、mock:empty を含むと空の結果を返します。429 の回数は、モックを起動してからの通算で検索語ごとに数えます。もう一度試すときはモックを再起動してください。1 つ目は待ち時間のあとに結果が表示されれば、再試行が働いています。3 つ目は再試行せずに エラー(HTTP 400): 'count' must be between 1 and 50 と表示し、終了コード 1 で止まります。エラーの message に、直すべき項目が書かれていることを確かめてください。

ほかにも、検索語に mock:408 を含むと最初の 2 回だけ 408、mock:500 を含むと常に 500(4 回試して打ち切り)、mock:402 を含むと 402 を返します。Chat では、質問に mock:chat400 を含めると 400 になります。「余裕があれば」の 2 つは、それぞれ次のように表示して終了コード 1 で終わります。

エラー(HTTP 402): 残高不足です。入金してから再試行してください(サーバー: Insufficient Balance)。
エラー(HTTP 400): forced 400 for testing (mock:chat400)

一次情報:Search & Fetch — Errors / Billing / Rate Limits / Rate Limit & Isolation / Models & Pricing

11 / AGENT TOOLS

Claude Code などのツールから使う

DeepSeek API は Anthropic 形式にも対応しているため、Claude Code のバックエンドとして使えます。公式の手順では、次の環境変数を設定してから claude を起動します。

Claude Code 用の環境変数(公式手順の値)
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
export ANTHROPIC_AUTH_TOKEN=<your DeepSeek API Key>
export ANTHROPIC_MODEL=deepseek-flash[1m]
export ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-flash[1m]
export ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-flash[1m]
export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-flash
export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-flash
export CLAUDE_CODE_EFFORT_LEVEL=max
export CLAUDE_CODE_AUTO_COMPACT_WINDOW=786432

Claude Code の Web Search にも DeepSeek API が対応しています。モデルが検索が必要と判断すると、DeepSeek の API 経由で検索します。このとき、検索結果を要約するための追加の LLM 要求が発生し、その分のトークン料金がかかります。検索そのものも Search API の課金対象です(北京時間 2026-11-01 00:00 から。それまでは無料)。

2026-11-01 から課金が始まります。 DeepSeek Harness・Claude Code・Codex 経由で DeepSeek API を使うと、DeepSeek のサーバー側ツールが Search / Fetch API を呼びます。どのモデルでサーバー側検索を使えるかは経路ごとに違うので、下の図 10 で確認してください。2026-10-20 時点ではこの呼び出しは無料ですが、北京時間 2026-11-01 00:00(UTC 2026-10-31 16:00、日本時間 2026-11-01 01:00)から要求単位で課金されます。

どの経路で検索できるか

検索の経路 deepseek-flash deepseek-v4-pro ① 自分のコードから呼ぶ/v1/tools/search・fetch(本ガイド) ✓ 使える ✓ 使える ② Responses API の web_searchサーバー側で検索して続きを生成 × 無視される ✓ 使える ③ Claude Code の Web SearchAnthropic 形式で接続 Web Search 対応を記載(モデル名の指定なし)推奨設定は全枠 deepseek-flash(モデル別の可否は記載なし) ④ Codex の組み込み Web 検索Responses API 形式で接続 × 使えない組み込み検索は v4-pro のみ ✓ 使えるweb_search = "live" が必要 ⑤ DeepSeek Harnessdeveloper preview モデル別の可否は記載なし ③〜⑤ は DeepSeek のサーバー側ツールが Search / Fetch API を呼ぶ経路です。 北京時間 2026-11-01 00:00(日本時間 01:00)から要求単位で課金。それまでは無料。
図 10 モデルごとに使える検索の経路(2026-10-20 時点の公式記載)
  • Responses API の web_search は deepseek-v4-pro 専用です。API がサーバー側で検索し、結果を文脈に加えて応答を続けます。deepseek-flash ではエラーにならず無視されるため、「検索したつもりで検索されていない」ことが起こりえます。Flash で検索を使うなら、本ガイドのように自分のコードから Search / Fetch API を呼びます。
  • Codex は Responses API でモデルと通信します。Codex 自体は deepseek-flash でも使えます(セットアップスクリプトのメニュー 1)。公式の「Integrate with Codex」によると、Codex の組み込み Web 検索は deepseek-v4-pro でのみ使えます。セットアップスクリプトはメニュー 1 で deepseek-flash、2 で deepseek-v4-pro を設定し、web_search = "disabled" を書き込みます。検索を使うには、下のように ~/.codex/config.toml を書き換えて Codex を再起動します。検索は DeepSeek のサーバー側で実行され、Search API の料金で課金されます。
  • Claude Code は Anthropic 形式で接続します。Web Search へのネイティブ対応は公式ページに記載があり、Anthropic 形式の仕様表でも server_tool_use と web_search_tool_result のブロックが「対応」です。モデルごとの制限は書かれていません。
  • Claude Code の推奨設定は全枠が deepseek-flash です。設定せずに claude-opus 系の名前が送られると deepseek-v4-pro(V4 Pro 価格)に読み替えられます。
  • DeepSeek Harness は、エージェントのハーネス開発者向けに developer preview として公開されています。1017 版には v0.2.1-rc.1 以降が推奨されています。モデルごとにサーバー側検索を使えるかは記載がありません。
Codex で Web 検索を使う設定 · ~/.codex/config.toml(該当の 2 行)
model = "deepseek-v4-pro"
web_search = "live"

一次情報:Integrate with Claude Code(Agent Integrations) / Integrate with Codex(Agent Integrations) / Change Log 2026-07-31・2026-08-13 / Responses API / Anthropic API / Search & Fetch — Billing / DeepSeek-V4.1-Flash-1017 Release

12 / CHECK YOUR UNDERSTANDING

判断できるか確かめる

各問で答えを選ぶと、理由を表示します。迷ったら対応する章へ戻りましょう。

Q1. 最新の V4.1-Flash-1017 を使いたい。model に書く値は?

Q2. tools を付けた 2 回目の要求。前の assistant メッセージの reasoning_content は?

Q3. Search API が HTTP 200 で空の results を返した。料金は?

Q4. 再試行してよいのはどれ?

Q5. deepseek-flash で Responses API の web_search ツールを指定した。

発展課題:自分のサービスに組み込む
  1. LAB 4 の選別に authority を加え、「高品質かつ高権威」を優先する。
  2. date が古い結果を除く条件を、after: 演算子とコード側の両方で試し、違いを比べる。
  3. LAB 5 のツール結果に、検索 1 回あたりの料金の累計を表示する。
  4. オフピーク(ピークは日本時間の平日 10:00–13:00 と 15:00–19:00。中国の祝日を除く。それ以外の時間)に一括処理を回す仕組みを作る。
13 / SOURCES & SCOPE

出典と、このガイドの検証範囲

仕様・数値・日付は、DeepSeek の公式ドキュメントと Hugging Face のモデルカードをもとにしています。確認日:。料金・無料期間・モデルの対応は、この日時点の情報です。

  1. Search & Fetch — Search / Fetch API のリクエスト・レスポンス・演算子・エラー・課金・レート制限
  2. Search API Reference / Fetch API Reference — 項目の型と制約
  3. Your First API Call / Models & Pricing — モデル名、料金、機能対応
  4. Change Log / DeepSeek-V4.1-Flash-1017 Release / DeepSeek-V4.1-Flash Release — 更新履歴、公表ベンチマーク
  5. DeepSeek-V4.1-Flash-1017(Hugging Face) — 比較表、ライセンス、推奨パラメータ
  6. Thinking Mode / Tool Calls / Context Caching — 思考モード、reasoning_content、関数呼び出し、キャッシュ
  7. Rate Limit & Isolation — 同時実行数、user_id
  8. Responses API / Anthropic API / Integrate with Claude Code(Agent Integrations) / Integrate with Codex(Agent Integrations) — 他形式とツール連携
  9. StellarArc design-system — カラー、タイポグラフィ、文書レイアウト、inline SVG 規定

検証済み:実習コード(Python)を同梱のモックサーバーに対して実行し、次の項目を自動テスト(30 件。モックサーバーに対する実行と、部品単位のテストを含む)で確認しました。要求の形(thinking・reasoning_effort・Search の各項目の値、--content での need_content と 本文: の表示)、再試行(429 と 408 は 3 回目で成功、500 は 4 回で打ち切り、402 と 400 は再試行しない、Retry-After の待ち時間は 30 秒が上限。これは部品単位のテストで、モックは Retry-After を返さない)、空の結果、Fetch の 403 / 404、reasoning_content の返送(2 ターン目を含む)、ターン番号と --show-messages の表示、--max-steps の検証、不正なツール引数と count の丸め込み、長い質問の切り詰め、エラー時にトレースバックを出さないこと(02・04 の Search のエラー、01 の Chat API のエラー)、API キー未設定時の終了コードです。Chat の通信エラー(接続できない場合)の表示は自動テストの対象外です。モックは公式ドキュメントに書かれた入力検証・エラー形式・「tools 付きの要求で reasoning_content がなければ 400」の規則を再現しています。

未検証:教材作成時に実際の DeepSeek API は呼び出していません(API キーを使わずに作成したため)。実 API の応答文面・検索結果の中身・処理時間はモックと異なります。DEEPSEEK_API_KEY を設定すれば、同じコードで実 API に対して実行できます。

数値の扱い:ベンチマークは DeepSeek の公表値です。総パラメータ数は公式情報の間で表記が一致しないため、断定していません。

用語

トークン
モデルが文章を数える単位。料金はトークン数で決まる。
思考モード
答える前に考える過程を出力するモード。過程は reasoning_content に入る。
Tool Calls
モデルが「この関数を呼んでほしい」と依頼する仕組み。実行するのは利用者のコード。
RAG
検索した資料を要求に入れて答えさせる方法。
QPS
1 秒あたりの要求数。
キャッシュヒット
要求の先頭が保存済みのキャッシュ単位と完全に一致し、安い単価で処理された入力。
MoE
Mixture of Experts。多数の専門家ネットワークのうち一部だけを動かして計算量を抑えるモデル構成。
RFC 3339
日時の書き方の規格。例 2026-06-01T00:00:00+00:00。

ブラウザの実習チェックは localStorage に保存します。このページは外部の API へ通信しません。Web フォントの読み込みには Google Fonts を使い、通信できない場合はローカル書体へ切り替わります。