2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DeepSeek のオープンソース agent harness「dsh」を、ローカルモデルで実際に回してみた

2
Posted at

初めに

最近、ローカルモデルで agent を回したいと思っていた。
API 利用は楽だが、コスト、遅延、そして「そもそもデータを持ち出したくない案件」がある以上、ローカルに立てられる構成をちゃんと試しておきたかった。

そのタイミングで、DeepSeek が open-source の agent harness「DeepSeek Harness」(コマンド名は dsh)を公開した。
"everything is a plugin" のアーキテクチャで、Cordis を基盤にしているという、少し変わった設計だ。

今回は、インストール → Web UI 起動 → ローカルモデル(OpenAI 互換エンドポイント)の設定まで、実際に進めてみた。
やったことと設定の落とし穴を、短くまとめる。

対象読者は、ローカルモデルや OpenAI 互換ゲートウェイを agent のバックボーンにしたいエンジニアです。
単なる紹介ではなく、設定ファイルまで書いていくので、そのまま自分の環境に落とし込めるはずです。

なお、dsh は現在 developer preview 段階で、公式 README にも COMPATIBILITY-BREAKING CHANGES がある と明記されています。
その点は踏まえて読んでください。

結論

先に結論を書くと、次の5つです。

  • dsh は DeepSeek AI が開発したオープンソースの agent harness で、プラグインの積層(Cordis)が本体の設計。Web UI、headless 実行、Python SDK は、すべて同じ core を起動する入口にすぎない
  • npx @deepseek-ai/dsh web 一条で Web UI が起動する(既定は http://127.0.0.1:3080)
  • DeepSeek API を使うだけなら、Settings → Models でキーを入力して保存するだけ。キーは write-only で、設定ファイルには秘密そのものではなく参照だけが残る
  • ローカルモデル / OpenAI 互換サーバは「custom provider」として普通に扱える。UI の Add a custom provider か settings.yaml の llm-pi-ai セクションに route を書くだけ
  • 設定変更はサーバ再起動なしで次回リクエストから有効(hot reload)になる

「ローカルモデル対応」を別物として売らず、provider 設定として普通に扱える設計は、かなり好感が持てた。

1. DeepSeek Harness とは

公式 README の定義をそのまま引用すると、dsh は

DeepSeek AI が開発した open-source agent harness。「everything is a plugin」のアーキテクチャを使用し、Cordis によって動かされる。

というものです。Cordis の設計思想は 論文 "A Programming Paradigm for Spatiotemporal Composability" に書かれています。

「everything is a plugin」が具体的な形になっているのが、リポジトリの packages です。
npm scope は @deepseek-ai/dsh-* で、役割ごとにグループ化されています。

  • core/ — セッション、プロンプト、ツール、agent サービス、実行ループの本体
  • llm/ — LLM 抽象サービスと provider アダプタ(後述の鍵)
  • shell/ terminal/ code-runtime/ — コマンド実行・PTY・コード実行
  • fs/ lsp/ subagent/ jobs/ workflow/ — ファイル操作、LSP、委譲、バックグラウンドジョブ、workflow
  • sandbox/ — プロセス隔離(bwrap / Landlock / Seatbelt)
  • session/ settings/ credentials/ — 永続化、設定、認証情報の管理
  • sdk/ acp/ — 外部から呼ぶための SDK / プロトコル

つまり「agent の機能」が全部個別プラグインで、積層の仕方(profile)で構成が変わる、という設計です。

ユーザーが触る入口は、公式に次の3つあります。

  • Web UI — dsh web。ブラウザから会話・承認・設定を行う
  • headless — dsh --profile headless "タスク"。一回きりのセッションを走らせて最終回答を出力して終了
  • Python SDK — deepseek-harness-sdk を pip install して自前のプログラムから呼ぶ

動作として、agent は workspace のファイルを読み書きし、コマンドを実行し、仕事を委譲し、計画(plan)を維持できます。
承認が必要な操作には、Web UI がアクティブな権限ポリシーに従って確認を入れます。

ライセンスは MIT。フィードバックは GitHub Discussions や Discord が公式窓口です。

2. インストールと初回起動

npm から(一番簡単)

Node.js をインストールしてから、公式 README が勧めるのはこれです。

npx @deepseek-ai/dsh web

これだけで Web UI が起動し、URL が表示されます。
既定の提供先は http://127.0.0.1:3080 です(ローカルホストのみ。ここは 6 節でまた出ます)。

ソースから

リポジトリを clone して build する流れも公式に載っています。

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web

dsh コマンドは profile 起動子

CLI の README にあるとおり、dsh は profile(プラグインバンドルの積層)を起動するローンチャー です。

コマンド 用途
dsh --profile <name> $DSH_HOME/profiles/<name> 以下から name 指定の profile を起動
dsh --profile headless "job" 一回きりの永続セッションを走らせ、最終回答を出力して終了
dsh web --profile web のエイリアス
dsh plugin --profile <name> <pnpm 引数> profile ディレクトリでプラグイン(pnpm)を管理

web と headless profile は初回利用時に同梱テンプレートから自動初期化されます。
これ以外の profile は dsh plugin で作る必要があります。

また、起動したディレクトリが既定の workspace ルートになります。
積まれた設定の確認には、起動せずにツリーを dump できます。

dsh --dump-config          # 現在の積層ツリー
dsh --dump-default-config  # ユーザー層なしの既定ツリー

3. Web UI で DeepSeek API を使う

サーバが起動している状態で、まずは公式 DeepSeek API を繋いでみます。

  1. Settings → Models を開く。DeepSeek のカードに API キー1つを入力して保存

  2. Choose workspace で、dsh を起動したプロジェクトディレクトリを追加して選択

  3. セッションを開始して、例えば

    Summarize this repository and identify its main packages.

    を投げる

これで、ファイルの読み書き・コマンド実行・委譲・plan 維持まで一通り触れます。
承認が必要な操作には Web UI が確認を挟みます。

キーの扱い(write-only)

ここが丁寧だなと感じたポイントです。

API キーは write-only として扱われます。保存後、画面が受け取るのはマスク済みの descriptor だけで、文字列そのものは返ってきません。
キー本体は $DSH_HOME/.credentials.yaml に保管され、settings 側にはその credential 参照だけが残ります。

$DSH_HOME は環境変数 DSH_HOME、未設定なら ~/.dsh です。

モデルピッカー側では、設定済みの provider が一覧に出ます。
モデルを選ぶと、それが新規セッションの既定モデルにもなります(すでにリクエストを送ったセッションは、自分のログに記録されたモデルを維持します)。

4. ローカルモデルを config する

ここからが本題です。
手元では vLLM をローカルで立てて http://127.0.0.1:8000/v1 に OpenAI 互換 API を公開しています(モデル ID はエンドポイントの GET /models が返す名前を使います。私の環境では claude-opus-5 というエイリアスになっていました)。

dsh では、こういう自前エンドポイントは custom provider として扱います。
公式 providers ガイドの表現を借りると、対象は「会社のゲートウェイ、self-hosted サーバ、インストール済み catalog に載っていない provider」です。

4.1 UI での追加(Add a custom provider)

Models ページの Add a custom provider から、次の項目を入力します。

  • Provider ID — 小文字。永続です(リクエスト、保存済みセッション、既定モデル、credential 参照のすべてがこれを使う)。リネームしたくなったら「新規追加して旧方を削除」が公式の答え
  • Display name
  • Base URL — 例: http://127.0.0.1:8000/v1
  • API protocol — 使う wire プロトコル(openai-completions / openai-responses など)
  • credential — API キー
  • Model — 最低1つ以上

Fetch available models を押すと、フォームの Base URL と credential で GET /models を叩いて候補をくれます(OpenAI 互換の GET /models 形状を使う gateway / self-hosted サーバ向け)。
候補を選んで draft を更新し、保存するまで provider はディスクに書き込まれません。

この UI 操作が、裏では何を生成しているのか——その正体が次節の settings.yaml です。

4.2 settings.yaml での追加(llm-pi-ai route)

dsh の LLM 周りには2つのアダプタプラグインが積まれています。

  • dsh-llm-deepseek — DeepSeek 公式エンドポイント専用の直接アダプタ
  • dsh-llm-pi-ai — 複数 provider 対応の汎用アダプタ(pi-ai ベース)

ローカルモデルを含む custom provider は、後者の llm-pi-ai セクションに route を1本追加する という形になります。
公式ドキュメントの例(ゲートウェイ向け)はこういう形です。

llm-pi-ai:
  providers:
    my-gateway:
      apiKeyEnv: GATEWAY_API_KEY
      api: openai-completions
      baseURL: https://gateway.example/v1
      models:
        - id: legacy-chat
        - id: vision-preview
          input: [text, image]

私の環境(vLLM)に落とすと、こうなりました。

# ~/.dsh/settings.yaml
llm-pi-ai:
  providers:
    vllm-local:
      displayName: vLLM Local
      apiKeyEnv: VLLM_LOCAL_API_KEY
      api: openai-responses
      baseURL: http://127.0.0.1:8000/v1
      models:
        - id: claude-opus-5
          name: claude-opus-5 (local)
          contextWindow: 114688
          maxTokens: 16384
          reasoningEfforts:
            xhigh: xhigh

主要フィールドの意味を、公式 README(dsh-llm-pi-ai)に基づいて整理します。

  • apiKeyEnv — 環境変数名への credential 参照。秘密は settings に書かない(4.3 参照)
  • api — wire プロトコル。catalog に載っていない route は、このフィールドで「どのプロトコルを話すか」を宣言する。supportedProtocols() が許すのは openai-completions / openai-responses / anthropic-messages の3つ(キー・エンドポイント・ヘッダで完全に表現できるプロトコルのみ、という設計)
  • baseURL — その route の全モデルのエンドポイント。/v1 付きの prefix として扱われる
  • models — route の catalog を置換するリスト(追加・拡張ではない)。id のみで書けば残りは route / catalog の既定値
  • contextWindow / maxTokens — モデルの能力。設定された maxTokens はリクエストの既定上限にもなる(未設定なら route 既定の defaultContextWindow: 262144 / defaultMaxTokens: 32768)
  • input — 入力モダリティ。手書きしたモデルは既定でテキスト専用になります。画像を受け取りたいなら input: [text, image] を明示する
  • reasoningEfforts — レベル名 → wire 上の表記のマップ。4.4 で詳述

models を書き忘れた route は dormant(起動しない) 状態です。
「catalog provider に手動モデルを追加したい」場合は modelOverrides を使うと、catalog の残りはそのまま維持したまま上書きできます(catalog の Anthropic/OpenAI 向けに有用、という公式の位置づけ)。

4.3 credential の仕組み

apiKeyEnv に書くのは環境変数名であり、秘密そのものではないです。

  • UI でキーを保存すると、credential は $DSH_HOME/.credentials.yaml に入り、settings には参照だけが残る(3 節の write-only と同じ経路)
  • settings.yaml だけで route を足した場合も、同じルールが機能する。apiKeyEnv 名の変数を credential store に置けばよい

私の環境では、この2ファイルの組み合わせになっていました。

# ~/.dsh/settings.yaml(抜粋:秘密は含まない)
llm-pi-ai:
  providers:
    vllm-local:
      apiKeyEnv: VLLM_LOCAL_API_KEY
      # ...
# ~/.dsh/.credentials.yaml
VLLM_LOCAL_API_KEY: sk-your-key-here

credential 参照が解決できないと、リクエストは MISSING_CREDENTIAL で失敗します。
つまり「キーがどこに書かれているのか」を settings から追わなくていい、というのが設計思想です。

4.4 reasoningEfforts(思考レベル)

reasoning 系モデルを使うなら、ここも押さえておきたい部分です。

pi-ai 系の provider では、reasoningEfforts のキー(レベル名)が dsh が提示する選択肢になり、値は API に実際に送られる wire 上の表記になります。

models:
  - id: some-reasoning-model
    reasoningEfforts:
      off: "disabled"
      high: "high"
      xhigh: "xhigh"
  • レベルは off / minimal / low / medium / high / xhigh / max の内から選べる
  • 例: max: "ultra" と書くと、UI の max は wire 上で ultra として送られる
  • 手書きしたモデルはこのマップを明示的に書かなければ reasoning を送らない(既定で思考が効く、とはしない)

route 単位では reasoning でデフォルトレベルを置けます(off 含む)。ここは「その route の全リクエストのデフォルト」です。

4.5 実測:headless でワンショット実行

Web UI 以外で確認したい場合、headless プロファイルが便利です。

dsh --profile headless "请只回复两个字:收到"

私の環境(vLLM + claude-opus-5)では、この通り

收到

が返ってきました。
セッションログ($DSH_HOME/sessions/ 配下)を確認すると、実際に送られたリクエストは provider: vllm-local / model: claude-opus-5 / reasoningEffort: xhigh / maxTokens: 16384 になっていて、settings.yaml に書いた値がそのまま wire に落ちていました。

設定変更後の挙動確認は、この「headless ワンショット + セッションログ」が非常に早いです。
再起動しなくても次回リクエストから反映される、という hot reload の説明もこれで実証できます。

5. その他の実行経路

Python SDK

deepseek-harness-sdk は pip で入ります(Python 3.10 以上。Linux x64/arm64、macOS 14 以上の arm64 向けで、runtime が同梱されているためシステム Node.js は不要、と公式が明記)。

python -m pip install deepseek-harness-sdk

環境変数でエンドポイントとキーを指します。

export DEEPSEEK_API_KEY=sk-your-key-here
# 公式ドキュメントの例は、OpenAI 互換プロキシ向けにこう指している
export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1

ローカルエンドポイントで回すための DEEPSEEK_BASE_URL が公式 doc の例に出ている点は、ローカル利用が想定された第一級シナリオであることを示しています。

最小構成の呼び出しはこんな感じです。

from deepseek_harness_sdk import DeepSeekHarness

harness = DeepSeekHarness(code="Hello from the Python SDK.")
session = harness.start_task("hello", content=harness.content_text("Hello from the Python SDK."))
print(session.session_id)
harness.destroy()

ツール呼び出しを許可しないなら、権限を danger-full-access にしない(既定の deny-all)ままにしておく、という注意書きも公式に載っています。

6. 注意点と失敗例

実際に進めてみて、つまずきやすいポイントをまとめます。

  • Web UI は既定で 127.0.0.1:3080 のみ。 LAN の別マシンから見るには、ポート転送か reverse proxy が必要です
  • Provider ID は永続。 後からリネームしたければ「新規 provider を追加して旧方を削除」。セッション・credential・既定モデルの参照が ID に紐づいているため
  • 手書き route は api + baseURL + models が必須。 書き忘れた route は dormant(起動しない)のままです
  • models は置換。 「catalog に1つ足したい」だけなら modelOverrides。リストを書き換えると他は消えます
  • Fetch available models が 401 になる — キーが解決できない、またはエンドポイントが GET /models を持たない、のどちらか。キーは Models ページから保存すると descriptor が present になるので確認しやすい。GET /models がないサーバは手動入力
  • UNKNOWN_MODEL — その provider に設定済みのモデルを選べていない。未登録のモデルは models(または modelOverrides)に追加する
  • 画像を添付して拒否される — 手書きしたモデルは既定でテキスト専用。input: [text, image] を model / route(defaultInput)/ catalog provider(modelOverrides)のいずれかに書く
  • developer preview。 公式 README の「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」は本物。安定性を求める用途ではバージョンを固定し、アップグレード前に --dump-config で積層を確認しておくのが無難

まとめ

DeepSeek Harness は、LLM を「交換可能なプラグイン」として扱って設計された agent harness でした。
最後に、私の感想を3つに絞って残します。

  • 「everything is a plugin」が実装レベルで本物だった点。 agent の機能は @deepseek-ai/dsh-* の積層で構成され、dsh はただの profile 起動子。 Web UI と headless が同じ core を共有する設計は、CLI → Web の二重実装のなやみを最初から消しています
  • credential が write-only なのは地味に高評価。 キーは $DSH_HOME/.credentials.yaml にしかおらず、settings には参照だけ。 設定ファイルをコピペしても秘密が漏れない、という方向性は、agent をチームで使う段階で効いてくる設計です
  • ローカルモデルが「特効機能」でなく provider 設定である点。 GET /models を叩いてモデルを取り込む UI、llm-pi-ai の route、reasoningEfforts の wire 名マッピング、hot reload—— 自前サーバを繋ぐための部品が全部揃っていて、かつ公式ドキュメントが DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 を例に出している

今回の記事で紹介した範囲(npm 起動、DeepSeek API、ローカルモデル設定)は、公式ドキュメントに忠実に進めれば数分で通ります。
ただし developer preview であることは忘れないでください。
プラグイン積層という設計がどこまで安定するのか、Discord や Discussions で動きを見ていきたいと思います。

あなたのローカルモデル環境でどう動くか、気になった人は npx @deepseek-ai/dsh web から試してみてください。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?