DeepSeek-R1(deepseek-reasoner)についてまとめ、API から試してみる
tl;dr
DeepSeek R1 は MIT ライセンスで商用利用可能なオープンモデル
DeepSeek API は他の SOTA モデルと比較して一桁程度小さい料金で利用可能
API レスポンスは CoT 部分を返す reasoning_content と最終的な回答部分を返す content に分けられる
max_tokens を 1 に設定すると、思考過程のみを抽出できるが、CoT 部分も課金対象となる
aider のベンチマークでは、DeepSeek R1 と Claude 3.5 Sonnet の組み合わせが o1 を超える性能を発揮したが、R1 の CoT 部分を渡しているわけではない
みなさん、DeepSeek R1 を使われていらっしゃいますか?さすがに 671B とローカル環境で動かすには荷が重いパラメータ数ではありますが、DeepSeek Chat(ウェブ版)からであれば無料で使うことができ、API からも他のモデルと比べて格安で利用することのできるモデルです。
私ごとですが、今がちょうど Claude / ChatGPT のサブスク期間が過ぎたタイミングなので、普段使いでどこまで DeepSeek が使えるかを測る意図も含め、メインのチャット UI を DeepSeek Chat にしています。o1 のような reasoning のできる DeepThink はもちろん、Search も使えます。なんと ChatGPT 上ではできない、それらの同時利用もできるので、しばらくはこれ一本で事足りるのではないかと期待しています。Search も Google Advanced で使える Deep Research 並みの数十件の引用をしてくれるのでかなり満足度が高いです。利用規約によると学習される可能性があるとのことで、その点だけご注意ください。
R1 / R1-Zero のモデルの重み自体も MIT ライセンスで公開されており、商用利用に制限がなく、これからのローカル LLM の性能のひとつの基準点になるのかなと個人的には考えています。
本記事は、今更ながら公式ドキュメントを参照しながら API を叩いてみたよという単なるログではあるのですが、最後に max token を 1 にすることで思考過程のみを取り出すなんてこともしたりします。あまり難易度は高くありませんので、よろしければご自身の環境でも試されてみてください。
DeepSeek の公式ドキュメントは Docusaurus のアイコンがそのままになっていたり、アラは目立つものの必要な機能は十分に書いてあるので、一度通読しておくと良いかと思います。今回は deepseek-reasoner 関連の部分にだけ言及します。
簡単に上記のコード部分以外を手動でまとめると下記のようになるかなと思います。
DeepSeek-R1 は OpenAI の o1 モデルと同等性能かつオープンソースなモデル
(コメントまでですが、オープンモデルではあるものの、オープンソースとまでは言えないのかなと考えています)
GitHub や arXiv にて technical report が公開されている
個人的には arXiv の方が HTML (experimental) のページで日本語翻訳しながら読めるのでオススメ
ライセンスが緩く、MIT で公開、商用利用も可
DeepSeek R1 は DeepSeek Chat(ウェブ版)からも API からも利用できる
モデルの指定は deepseek-reasoner、今回の記事で取り扱います
deepseek-reasoner は、最終的な回答をいきなり出力するのではなく、その前に Chain-of-Thought を用いることで最終的な回答の精度をあげている
補足をすると CoT Prompting で用いられるような思考過程を生成し、その思考過程を元にクエリに対する回答を出力すると私は解釈しています
DeepSeek は openai ライブラリ互換で使えるが、deepseek-reasoner で用いるパラメータをサポートするために SDK のアップデートが必要
max tokens はデフォルトで 4k で、8k まで増やせる(CoT 部分は 32k まで)
API レスポンスは、CoT 部分を返す reasoning_content と最終的な回答部分を返す content に分けられる
context length は 64k で、reasoning_content はここにカウントされない
Function calling / JSON output / Fill in the middle Completion はサポートされない
その他、サポートされていないパラメータがいくつかある
マルチターンのチャットの場合、前のやり取りの CoT 部分は次のコンテクストに含まれない
少しできる or できないが不明瞭なので、細かい制御をしたいときは適宜対応可否を確認する必要がありますね。
価格は?
https://openai.com/api/pricing/
DeepSeek API はよく使われるモデルに対してどのくらい安いのでしょうか。
「*」のついているものはキャッシュヒット時の料金。他の SOTA のモデルと比べて一桁程度小さいことがわかります。
さっそく動かしてみる
ディレクトリを作成しましょう。
mkdir deepseek-reasoner
cd deepseek-reasoner
DeepSeek API は openai 互換ですので、openai ライブラリを使います。また、API Key を .env で指定するため、python-dotenv を使います。
uv init --python 3.12
uv add openai
uv add python-dotenv
https://platform.deepseek.com/api_keys
API Key を取得しましょう。上記の URL からコンソールページを開いて新しく API Key を発行しましょう。もし過去に発行した不要になった API Key があればこの機会に削除しておきましょう。
コンソールページより Create new API Key を押して、deepseek-reasoner とでもしておきましょうか。お好きな名前を指定してください。
API Key が表示されますので、Copy を押しましょう。
.env に下記のように API Key を入れましょう。
DEEPSEEK_API_KEY=sk-xxx
ストリーミングなし / あり ver のそれぞれを試してみます。
touch nostreaming.py streaming.py
公式ドキュメントに記載の内容を日本語にして、reasoning_content / content のそれぞれを出力します。
nostreaming.py として下記を保存します。
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com"
)
# Round 1
messages = [{"role": "user", "content": "9.11と9.8はどちらが大きいですか?"}]
response = client.chat.completions.create(model="deepseek-reasoner", messages=messages)
reasoning_content = response.choices[0].message.reasoning_content
content = response.choices[0].message.content
print("Round 1 Reasoning" + "-" * 20)
print(reasoning_content)
print("\nRound 1 Response" + "-" * 20)
print(content)
# Round 2
messages.append({"role": "assistant", "content": content})
messages.append(
{"role": "user", "content": "「strawberry」という単語には「r」が何個ありますか?"}
)
response = client.chat.completions.create(model="deepseek-reasoner", messages=messages)
reasoning_content = response.choices[0].message.reasoning_content
content = response.choices[0].message.content
print("\nRound 2 Reasoning" + "-" * 20)
print(reasoning_content)
print("\nRound 2 Response" + "-" * 20)
print(content)
実行しましょう。
uv run nostreaming.py
手元の実行結果は下記のようになりました。
LaTeX 記法を交えつつ、正答まで辿り着くことができています。次にストリーミングも試してみましょう。
streaming.py として下記を保存します。差分は client.chat.completions.create に stream=True と指定している点と、reasoning_content / content を空文字で初期化してから response.choices[0] から delta.reasoning_content / delta.content をチャンクごとに取り出し加えている点です。
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com"
)
# Round 1
messages = [{"role": "user", "content": "9.11と9.8はどちらが大きいですか?"}]
response = client.chat.completions.create(
model="deepseek-reasoner", messages=messages, stream=True
)
reasoning_content = ""
content = ""
print("Round 1 Reasoning" + "-" * 20)
for chunk in response:
if chunk.choices[0].delta.reasoning_content:
reasoning_content += chunk.choices[0].delta.reasoning_content
print(chunk.choices[0].delta.reasoning_content, end="", flush=True)
elif chunk.choices[0].delta.content:
content += chunk.choices[0].delta.content
print("\n\nRound 1 Response" + "-" * 20)
print(content)
# Round 2
messages.append({"role": "assistant", "content": content})
messages.append(
{"role": "user", "content": "「strawberry」という単語には「r」が何個ありますか?"}
)
response = client.chat.completions.create(
model="deepseek-reasoner", messages=messages, stream=True
)
reasoning_content = ""
content = ""
print("\nRound 2 Reasoning" + "-" * 20)
for chunk in response:
if chunk.choices[0].delta.reasoning_content:
reasoning_content += chunk.choices[0].delta.reasoning_content
print(chunk.choices[0].delta.reasoning_content, end="", flush=True)
elif chunk.choices[0].delta.content:
content += chunk.choices[0].delta.content
print("\n\nRound 2 Response" + "-" * 20)
print(content)
実行してみましょう。
uv run streaming.py
実行結果は下記のようになりました。
今回は strawberry の方で正解はしているものの少し混乱していますね!あやしい!o1 / o1-mini の思考過程が見えないので、単に正解したとしてもそれがロバストに正答できているか否かをきちんと検証するようにしたいとこの挙動を見て思いました。
おまけ
数日前に max tokens を 1 にしてみたという下記の X のポストを見かけました。プライシングページを見ると、たとえ max_tokens が 1 だったとしても CoT 部分も課金されるので少し誤解を招く表現にはなっていると思うものの、単に CoT 部分だけを抜き出してみるのもおもしろいと思うので試してみます。
先ほどの nostreaming.py の例のリクエスト部分に max tokens を指定してみます。ほぼ同じですが、下記にコードを記載します。
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com"
)
# Round 1
messages = [{"role": "user", "content": "9.11と9.8はどちらが大きいですか?"}]
response = client.chat.completions.create(model="deepseek-reasoner", messages=messages, max_tokens=1)
reasoning_content = response.choices[0].message.reasoning_content
content = response.choices[0].message.content
print("Round 1 Reasoning" + "-" * 20)
print(reasoning_content)
print("\nRound 1 Response" + "-" * 20)
print(content)
# Round 2
messages.append({"role": "assistant", "content": content})
messages.append(
{"role": "user", "content": "「strawberry」という単語には「r」が何個ありますか?"}
)
response = client.chat.completions.create(model="deepseek-reasoner", messages=messages, max_tokens=1)
reasoning_content = response.choices[0].message.reasoning_content
content = response.choices[0].message.content
print("\nRound 2 Reasoning" + "-" * 20)
print(reasoning_content)
print("\nRound 2 Response" + "-" * 20)
print(content)
実行します。
uv run nostreaming.py
レスポンスがなかなか返ってこないと思ったら、4 回も strawberry の分解をしていました。簡単なクエリに対しては無駄なトークンを消費しがちなのかもしれません。今回は中国語で CoT 部分を生成してくれました。日本語でプロンプトを書いても CoT 部分は日英中とブレる印象です。制御をしようかと思ったのですが、なかなか通常の Jailbreak では難しいようで、力不足を感じました。
また、この CoT 部分を他のモデルに適用するとどうなの?という議論を直近数日見かけることがありましたが、タスクによるでしょうと思っていたところで、ちょうど aider がブログ記事にまとめていました。
あくまで aider の polyglot benchmark & architect / editor model の役割分離に限っての話ですが、DeepSeek R1 + Claude 3.5 Sonnet の組み合わせが o1 を超える性能を発揮したとのこと。ただし、R1 の CoT 部分を渡しているわけではなく、最終回答のみを渡している点に注意。また、CoT 部分だけを渡した場合は性能の向上が見られなかったよう。
architect / editor については aider のプロンプト指定を見てみるとわかりやすいかと思います。
aider の記事は、単にコーディングの性能が DeepSeek R1 + Claude 3.5 Sonnet を組み合わせると性能があがるという話ではないことにだけご注意ください。あくまで aider 独自のベンチマーク評価における最高性能であって、汎用的なコーディングタスクにおいて R1 + Sonnet が上回るとまでは言っていないこと、CoT 部分の誤解がちらほら見かけたのでそれとは違うよという点に要注意です。
それでも DeepSeek R1 が安いことには変わりないので、もし aider 使いの方や自前の Agentic ツールなどで多段リクエストをする際はこの構成で試してみると面白いかもしれません。
以上となります。