技術約7分で読めます

DeepSeek HarnessをQwenとDeepSeekにつないでコード修正を試した

いけさん目次

AIモデルの指示でファイルを編集したりコマンドを実行したりするDeepSeek Harness(DSH)を、M4 Mac miniで動かしてみた。
DeepSeek製のツールだが、公式のモデル設定ガイドには他社APIへの接続方法も載っている。

今回はモデルをAPIで呼び出せるModelScopeを使い、QwenとDeepSeekに同じプログラムの修正を頼んでみた。

DSHとモデルの役割

モデルは次に何をするかを判断し、DSHはその指示を受けてファイルを編集したり、コマンドを実行したりする。
実行結果をモデルへ返し、必要なら次の操作を繰り返す。DSHのように、モデルとツールのやり取りを管理するソフトウェアをハーネスと呼ぶ。

DSHのアーキテクチャ資料によれば、モデル接続、ツール、会話履歴、処理を繰り返すループまでプラグインとして実装されている。

公式サイトではMacのApple Silicon向けとWindowsの64ビット向けデスクトップ版も配布している。
今回は同じプロンプトで繰り返すため、画面を使わずに一件の作業を実行して終了するheadlessプロファイルを使った。プロファイルは、使う機能と設定をまとめたものだ。

M4 Mac miniとModelScopeで実行

測定日は2026年10月13日。モデルの推論はModelScope側で行い、MacではDSHと修正対象のNode.jsプログラムを動かした。

項目条件
ローカル環境Apple M4、メモリ16GB
Node.js22.22.0
DSH@deepseek-ai/dsh@0.2.0-rc.2
接続先https://api-inference.modelscope.ai/v1
QwenQwen-Ambassador/Qwen3.8-Flash-Next
DeepSeekdeepseek-ai/DeepSeek-V4.1-Flash
実行方法headless、通常のツール呼び出しを使うnative設定
ファイルの書き込み権限作業フォルダーへの書き込みを許可するworkspace-write
一回の出力上限両モデルとも8,192トークンに設定

トークンは、モデルが文章を処理するときの単位。文字数とは数え方が異なる。

DSHは版を固定してインストールした。

npm install --prefix .dsh-work/runtime --save-exact @deepseek-ai/dsh@0.2.0-rc.2

この版のpackage.jsonが指定するNode.jsの範囲は^22.19.0 || >=24.0.0。
DSHの設定と会話履歴は専用ディレクトリへ分けた。実験に無関係な指示を読み込ませないよう、指示ファイルやスキルを読み込むagent-instructionsとskill-filesystemも無効にした。

ModelScopeへの接続設定

DSHにはOpenAIと同じ形式でリクエストや応答を扱うAPIへ接続するアダプターがあり、今回は接続先をmodelscope-experimentという名前で登録した。設定からQwenの接続部分を抜粋すると、こんな感じ。

- id: llm-pi-ai
  config:
    providers:
      modelscope-experiment:
        apiKeyEnv: DSH_EXPERIMENT_API_KEY
        api: openai-completions
        baseURL: https://api-inference.modelscope.ai/v1
        compat:
          supportsDeveloperRole: false
          maxTokensField: max_tokens
        models:
          - id: Qwen-Ambassador/Qwen3.8-Flash-Next

apiKeyEnvには、APIキーを設定する環境変数の名前を指定した。今回は実行スクリプトが、DSHを起動するときにこの名前でキーを設定している。
compatはリクエスト形式の調整で、指示文にdeveloperという役割を使わないようにし、出力上限の項目名にはmax_tokensを指定している。この部分は両モデルで共通にした。

実行モデルの選択や権限などを含む設定全体は、再現用コードの実行スクリプトに入れた。
今回はQwenアンバサダー用のAPIキーを使っており、利用できるモデルIDはキーによって異なる。

記事一覧を作るプログラムに3件の不具合を入れた

修正対象には、記事の公開日時やファイル名から一覧を作る15行のJavaScriptプログラムを用意し、DSHの動作確認用に不具合を入れた。

処理入れておいた不具合期待する動作
公開日時の判定現在時刻と同じ日時の記事を除外する同時刻も公開対象に含める
新しい順への並べ替え日時を文字列のまま比較するタイムゾーンを含めた実際の時刻で比較する
ファイル名の抽出パス区切りが/の場合しか処理しないWindowsの\も処理する

README.mdに仕様を書き、9件のテストを用意した。修正前は6件成功・3件失敗した。
あらかじめ用意した正しい修正例では9件すべて成功することも確認した。

最初にQwenへこのREADME.mdと実装の要約を頼むと、ファイルを読み取り、日本語で説明が返ってきた。
このときはテストも実行していて、起動から終了まで55.9秒。

修正の試行では、各回でプログラムを初期状態からコピーし、新しい会話へ同じプロンプトを送った。

このプロジェクトのREADME.mdを読んで仕様を確認し、npm testを実行して不具合を調べてください。
src/catalog.jsを修正して、既存のテストをすべて成功させてください。
既存のテストファイルやpackage.jsonは変更しないでください。

実際のプロンプトには、修正後の再テストと日本語での報告、外部パッケージの追加不要、作業対象の指定も含めた。全文は再現用コードのtask.txtに保存した。

DSHが終了したあと、モデルの報告とは別に、こちらでも9件のテストを再実行した。
作業フォルダーに入れなかった追加3件でも、別の日時やファイル名を正しく処理できるか確認し、README.md、package.json、既存テストは元のファイルと比較した。

QwenとDeepSeekを3回ずつ実行した結果

初回はQwen、DeepSeekの順で動かし、その後も交互に1回ずつ実行した。
出力のばらつきに関わる温度・シードと、モデルの思考量を調整する推論強度は指定しないで、接続先の既定値を使った。

表の「モデル呼び出し」はモデルへ判断を求めた回数、「ツール呼び出し」はファイル操作やコマンド実行などを呼び出した回数だ。一つのプロンプトに対して、モデルの判断とツール操作が何回か繰り返されている。

モデル回起動から終了までモデル呼び出しツール呼び出し
Qwen3.8 Flash-Next176.9秒8回10回
Qwen3.8 Flash-Next291.9秒10回12回
Qwen3.8 Flash-Next367.3秒8回10回
DeepSeek V4.1 Flash148.5秒8回11回
DeepSeek V4.1 Flash247.9秒7回10回
DeepSeek V4.1 Flash337.3秒6回10回

全6回で既存9件と追加3件のテストがすべて成功した。変更はsrc/catalog.jsだけで、追加ファイルはなく、README.md・package.json・既存テストも元のままだった。

この課題を3回ずつ実行した時間の平均と、ばらつきの大きさを表す標本標準偏差を計算した。Qwenは78.7秒±12.4秒、DeepSeekは44.5秒±6.3秒。
この時間にはDSHの起動、APIの応答待ち、ツール実行、最終回答を含んでいる。

Qwenの1回目は、ファイルを読み込んで修正前のテストを実行し、writeツールで実装全体を書き換えていた。
DeepSeekの1回目は、editツールで該当箇所を変更した。どちらも、編集後にnpm testを実行してから回答していた。

QwenとDeepSeekの修正コード

Qwenの1回目の差分はこんな感じ。-が修正前、+が修正後の行を表す。

+  const nowMs = Date.parse(now);

-    .filter((record) => Date.parse(record.publishedAt) < Date.parse(now))
-    .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
+    .filter((record) => {
+      const publishedMs = Date.parse(record.publishedAt);
+      return Number.isFinite(publishedMs) && publishedMs <= nowMs;
+    })
+    .sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt))

-      const slug = record.source.split('/').at(-1).replace(/\.md$/, '');
+      const filename = record.source.split(/[/\\]/).at(-1);
+      const slug = filename.replace(/\.md$/, '');

Qwenは現在時刻を変換した結果を変数に代入し、不正な日時をNumber.isFiniteで除外していた。

DeepSeekの1回目は、比較式とパスの区切りを変更していた。

- .filter((record) => Date.parse(record.publishedAt) < Date.parse(now))
- .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
+ .filter((record) => Date.parse(record.publishedAt) <= Date.parse(now))
+ .sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt))

- const slug = record.source.split('/').at(-1).replace(/\.md$/, '');
+ const slug = record.source.split(/[\\/]/).at(-1).replace(/\.md$/, '');

どちらもDate.parseでタイムゾーンを含む日時の文字列を数値へ変換し、新しい順への並べ替えには数値の差を使っている。パスの分割では/と\の両方を区切りにしている。

再現用コードと各回の記録には、修正前のプログラム、共通プロンプト、評価用テスト、実装の差分、DSHのツール呼び出し記録を置いた。

コマンドラインの履歴をブラウザでも確認

DSHにはブラウザで操作する画面もある。測定後に同じ会話保存先で起動すると、コマンドラインで実行したQwenの会話も表示できた。

DSHのWeb UIに表示したQwenの修正報告。テスト結果とQwen3.8 Flash-Nextのモデル名が表示されている