計測台に置いたノートPC、計測機器、デスクトップPCで表したPoC実験。

Edge0連載 5: M2 Air 16GBでの実測と社内PoCへの拡張条件

16GBのM2 MacBook Airで、Microsoft Learn検索、Edge0ストリーミング、文単位の韓国語翻訳を接続したエージェントを実行しました。2026年9月13日に確認したサンプルGistには、コード全体とともにTTFTが53.47秒だった実行結果を公開しています。 最終回では、計測に使った質問、検索と最初の応答までの時間、生成回答の根拠、出力速度の解釈、今後の最適化、社内PoCへ拡張する条件を扱います。第4回で説明したクライアント側の計測を基準に結果を読みます。 実行ログと計測値から説明します。各段階の時間を比較し、現在の結果から判断できる範囲と、追加実験が必要な範囲を分けて扱います。 数値を比較する際は、次の基準を適用します。 文書は2026年9月13日に確認しました。以下の数値は、特定の質問と検索結果を使用した一度の実行記録です。当時のすべてのバージョン、キャッシュ状態、反復計測の結果は、この記録だけでは確認できません。tok/sと表示した値は、コンテンツを含むSSEイベント数から計算しています。 Native AOTの質問を使った単一実行 実験にはM2 MacBook Air 16GBとEdge0-35B-A3B-previewを使用しました。PythonホストがMicrosoft Learnを検索し、Apple Translationで入出力を翻訳する構成は、Gistの実行手順と同じです。 質問は次のように入力しました。 NativeAOT에서 Reflection.Emit을 사용할 수 있는지도 설명해주세요. モデルの一般知識を評価するよりも、検索した公式文書を読み、実行時コード生成の対応有無を判定できるかを確認しました。この一度の結果を、機器の平均性能や別の質問の回答品質へ一般化してはいません。 検索完了から最初の応答までの待ち時間 サンプルコードの状態メッセージは、入力処理の開始を基準に経過時間を表示します。TTFTは、Edge0リクエストの開始時に記録する別の時刻を基準に計算します。 質問変換と検索にかかった約2.6秒 韓国語の案内を使った実験ログでは、次の順序で検索を完了しました。現在のGistは同じ種類の案内を英語で表示します。 [ 0.0s] 한국어 질문 정규화 [ 1.3s] 영어 검색 질의 생성 완료 [ 1.3s] Microsoft Learn 검색 [ 2.6s] 관련 문서 3개 검색 완료 근거 자료 [1] Native AOT deployment [2] Introduction to AOT warnings [3] ASP.NET Core support for Native AOT 約2.6秒には、韓国語の質問の正規化、英語クエリの生成、Microsoft Learn検索が含まれます。この実行では、その区間は後続の最初の応答までの待ち時間より短くなりました。 ...

2026年9月17日 · 2 分 ·  rkttu
時間を測る装置とノートPCの間のレール上で、最初の空白区間に続いて並ぶ青いガラスのパネル。

Edge0連載 4: ストリーミング応答のTTFTと出力速度の計測

2026年9月13日時点で、連載のPythonサンプルはEdge0のSSE応答を受信しながら、最初の応答までの待ち時間と出力速度を記録します。今回は第3回で作ったグラウンディング用プロンプトを実際の生成リクエストへ接続します。 この記事では、計測時点、待機状態の表示、ワーカースレッドでの受信、文単位の韓国語翻訳、計測値の解釈を扱います。16GBのM2 MacBook Airでの実験では最初の応答まで数十秒かかったため、その前後の時間を分けて記録しました。 計測値の定義から説明します。サーバー応答の受信と画面出力を分離し、生成文を翻訳して最後に指標を表示する順に進めます。プログラム全体にはGistのmicrosoft_expert_agent.pyを使用します。 以下の計測は、次の範囲で解釈します。 公開資料は2026年9月13日に確認しました。Edge0-35B-A3B-previewと公開ストリーミングインターフェースを前提とします。サンプルはクライアントが観測した時間とコンテンツイベント数を記録し、サーバー内部のプリフィル、デコード、待機時間を個別には計測しません。 最初のコンテンツと受信終了を基準にした計測値 GistのStreamStatsは、リクエストを処理するワーカースレッドを起動する直前に基準時刻を保存します。その後、空でない最初のコンテンツを受信した時刻と、ストリームの受信を終えた時刻を記録します。 TTFT:リクエストの基準時刻から最初のコンテンツを受信するまでの時間を測ります。クライアントが観測した最初の出力までの待ち時間を表します。 総生成時間:リクエストの基準時刻からストリームの受信が終わるまでの時間を測ります。検索時間は含まず、韓国語翻訳がすべて完了する時点までは測りません。 生成量:コンテンツを含むSSEイベント数を数えます。現在の転送動作を前提に、この値をトークン数の推定値として使います。 最初の応答後の時間:最初のコンテンツを受信してからストリームの受信が終わるまでの時間を測ります。ネットワーク受信や終了処理にかかる時間も含みます。 出力速度:生成量を最初の応答後の時間で割ります。コンテンツイベント単位で処理率を計算します。 時刻と生成量を保存するオブジェクト 既存サンプルのフィールド名と計算式を保持した計測オブジェクトを示します。 from dataclasses import dataclass @dataclass class StreamStats: request_started: float = 0.0 first_token_at: float | None = None done_at: float | None = None completion_tokens: int = 0 @property def ttft(self) -> float | None: if self.first_token_at is None: return None return self.first_token_at - self.request_started @property def total_generation_time(self) -> float | None: if self.done_at is None: return None return self.done_at - self.request_started @property def decode_time(self) -> float | None: if self.first_token_at is None or self.done_at is None: return None return self.done_at - self.first_token_at @property def output_tokens_per_second(self) -> float | None: duration = self.decode_time if not duration or duration <= 0: return None return self.completion_tokens / duration TTFTには、スレッド起動、HTTP処理、サーバーのキュー待ち、トークン化、プリフィル、最初の生成ステップの時間が含まれる場合があります。decode_timeという名前のフィールドも、サーバー内部の純粋なデコード時間だけを意味しません。 ...

2026年9月16日 · 3 分 ·  rkttu
文書の強調箇所を拡大する虫眼鏡と、選択した3枚の参照カードを入れたガラスの箱。

Edge0連載 3: Microsoft Learn検索を組み込むローカルRAGエージェント

2026年9月13日時点で、サンプルGistはEdge0サーバー、Microsoft Learn CLI、Apple TranslationをPythonで接続するコードを提供しています。第2回で準備した環境を基に、今回は検索した資料をモデルの入力に変える部分を説明します。 この記事では、ホストとモデルの役割分担、質問の正規化と翻訳、検索結果の選択、グラウンディング用プロンプト、会話コンテキストの管理を扱います。ホストが検索の順序を決め、モデルは渡された資料を読んで回答を構成します。 データ構造と入力処理から説明します。検索した箇所に出典番号を付け、現在の質問と一緒に渡した後、続く質問に必要な文脈を構成します。ストリーミング要求と性能計測は第4回で扱います。 本文のコードと公開バージョンは、次の範囲で扱います。 公開資料は2026年9月13日に確認しました。本文ではRAGの流れを説明するためにコードを抜粋し、実行ファイル全体に含まれる一部の補助処理を省略しています。Edge0-35B-A3B-previewの動作や外部ツールの応答形式は、今後変わる可能性があります。 検索とコンテキスト構成を担うPythonホスト Pythonコード全体では、ホストが入力を正規化して翻訳し、Microsoft Learnを検索します。検索結果の重複URLを除き、資料の長さを制限し、直近の会話をリクエストに含めます。 モデルの役割は、質問と文書の関係を把握し、回答に使う根拠を選び、英語で回答を生成することに絞ります。資料から事実を確認できない場合は、根拠が不足していると回答するようプロンプトに指定します。 検索結果はタイトル、URL、本文を持つSourceオブジェクトで管理します。入力の量を制限する定数も一緒に定義します。 from dataclasses import dataclass @dataclass class Source: title: str url: str content: str MAX_SOURCES = 3 MAX_SOURCE_CHARS = 1800 MAX_SOURCE_CHARSは各検索箇所の文字数を制限します。出典タイトル、URL、システムプロンプト、会話履歴はこの上限の外で追加するため、リクエスト全体のサイズは別途計算できます。 間接疑問文を直接疑問文に変えるルール 韓国語の質問の正規化は、Gistのnormalize_korean_question()が処理します。生成の有無を尋ねた質問が翻訳時に生成方法を尋ねる依頼へ変わる例を減らすため、決められた文型を変換します。 次の関数は四つのパターンに一致する文を、直接疑問文と後続の依頼に分けます。 import re def make_followup(action: str) -> str: if action == "알려주세요": return "그 여부도 알려주세요." if action == "확인해주세요": return "그 여부도 확인해주세요." return "그 여부도 설명해주세요." def normalize_korean_question(text: str) -> str: suffix = ( r"\s*(?:함께\s*)?" r"(설명해주세요|알려주세요|확인해주세요)" r"\.?$" ) patterns = [ ( rf"^(.*)할 수 있는지도{suffix}", lambda m: f"{m.group(1)}할 수 있습니까? " f"{make_followup(m.group(2))}", ), ( rf"^(.*)되는지도{suffix}", lambda m: f"{m.group(1)}됩니까? " f"{make_followup(m.group(2))}", ), ( rf"^(.*)하는지도{suffix}", lambda m: f"{m.group(1)}합니까? " f"{make_followup(m.group(2))}", ), ( rf"^(.*)필요한지도{suffix}", lambda m: f"{m.group(1)}필요합니까? " f"{make_followup(m.group(2))}", ), ] for pattern, replacement in patterns: match = re.match(pattern, text) if match: return replacement(match) return text パターンに一致しなければ、入力をそのまま返します。文型を追加する際には、原文、変換結果、英語訳を比較すると、質問の意味を維持できているか確認できます。 ...

2026年9月15日 · 3 分 ·  rkttu
ノートPCの前で接続した演算装置、文書カード、半透明の吹き出し。

Edge0連載 2: Microsoft LearnとApple Translationの実行環境

2026年9月13日時点で、この連載のサンプルはEdge0-35B、Microsoft Learn CLI、macOSのTranslationフレームワークを使用します。第1回で決めた役割分担に沿って、推論サーバーとPythonエージェントのホストを別々のターミナルで実行します。 この記事ではEdge0とモデルの導入、ストリーミングサーバーの起動、公式文書の検索、韓国語と英語の翻訳、質問の正規化を扱います。ファイル全体はサンプルGistのREADME.md、apple_translate.swift、microsoft_expert_agent.pyにあります。 実行条件とインストール手順から説明します。最初のターミナルでサーバーを起動した後、検索ツールと翻訳CLIを準備し、二つ目のターミナルでエージェントを実行できる状態にします。 以下の手順は、次の公開バージョンとAPI条件を前提にしています。 文書は2026年9月13日に確認しました。Edge0-35B-A3B-previewの構成は今後変わる可能性があります。Swiftの例で使用するTranslationSession(installedSource:target:preferredStrategy:)は、macOS 26.4以降と、このAPIを含むSDKを前提とします。 Edge0と35Bモデルの導入条件 実験にはApple Silicon Mac、Python 3.12、Node.js 22以降、Xcode Command Line Toolsを使用します。約23GBのモデルファイルを保存する空き容量と、韓国語および英語の翻訳リソースも用意します。対応環境とモデルファイルの構成は公式READMEに記載されています。 リポジトリとPython仮想環境 作業ディレクトリでリポジトリを取得し、仮想環境を有効にしてサンプルの依存関係をインストールします。 git clone https://github.com/Edge0-AI/Edge0.git cd Edge0 python3.12 -m venv .venv source .venv/bin/activate pip install -e '.[dev,fetch]' pip install flask requests 以後のコマンドはEdge0リポジトリのルートを作業ディレクトリにします。flaskはストリーミング転送、requestsはPythonホストのHTTPリクエストに使用します。 モデルのダウンロードとパスの指定 公式スクリプトで35Bモデルを取得し、現在のシェルにモデルパスを設定します。 python scripts/fetch_models.py \ --tier edge0-35b \ --target-dir models export EDGE0_35B_MODEL="$PWD/models/edge0-35b" models/edge0-35b/には、設定、モデルの重み、トークナイザー、LoRAとプリルーターのアダプターが含まれます。アダプターを含むディレクトリ全体をモデルパスとして指定します。 最初のターミナルで動かすストリーミングサーバー Edge0の35Bモデル文書は、Flask転送層によるSSEストリーミングと、状態確認用の/healthzを説明しています。このサンプルでは接続先を127.0.0.1:8083に設定します。 仮想環境の再有効化とサーバー起動 新しいターミナルでは、作業パス、仮想環境、モデルの環境変数を再設定します。/path/to/Edge0は、リポジトリを取得した実際のパスに置き換えます。 cd /path/to/Edge0 source .venv/bin/activate export EDGE0_35B_MODEL="$PWD/models/edge0-35b" edge0 serve edge0-35b \ --host 127.0.0.1 \ --port 8083 \ --flask サーバーの実行中は最初のターミナルを開いたままにします。別のターミナルで有効にした仮想環境は、新しいシェルに自動では適用されません。 ...

2026年9月14日 · 2 分 ·  rkttu
銀色のノートPCの上に浮かぶガラスのブロックと積み重ねたストレージで表したローカルモデルの推論。

Edge0連載 1: 16GB Macで動かす35BローカルLLMと根拠に基づくエージェント

2026年9月13日時点で、Edge0はApple Silicon向けのMLXバックエンドとEdge0-35B-A3B-previewモデルを公開しています。私は16GBのM2 MacBook Airでこのモデルを実行し、Microsoft Learnの資料を根拠に回答する技術専門エージェントを構成しました。 この連載では、35Bクラスのモデルを動かす条件、検索資料による回答の変化、韓国語の入出力、ストリーミング性能の計測、社内PoCへ拡張する条件を扱います。実行コード全体はEdge0 Microsoft Technology Expert AgentのGistにまとめています。 第1回ではモデルを実行した経験から説明します。外部の根拠を与えた実験と入力資料を短くした結果を比較し、後続の記事で実装するエージェントの構造を紹介します。 公開状況と計測結果には、次の範囲を適用します。 公開文書は2026年9月13日に確認しました。モデルはプレビュー段階にあり、構成や性能は今後変わる可能性があります。実行時間と回答例は私が行った個別の実験記録であり、すべての機器や質問に共通する性能を示すものではありません。 必要な重みをSSDから読み込む35Bモデル Edge0のメモリ利用方式から確認します。プロジェクトの説明によると、Edge0は疎なMoEモデルのエキスパート重みをSSDに置き、推論に必要な部分を読み込みます。このため、起動時にモデル全体をRAMへ読み込む構成とはメモリの利用状況が異なります。 公開モデルの仕様と公式の計測値、私の実験環境を項目ごとに分けて示します。 公開モデル:Edge0-35B-A3B-previewは35BクラスのMoE構造に4ビット量子化を適用しています。 モデルファイルのサイズ:SSDに保存するチェックポイントは約23GBを占めます。 公式のメモリ計測値:短いコンテキストで、MLXアロケーターの最大アクティブメモリが約2.9GiBを記録しました。 個人の実験機器:この連載ではM2 MacBook Air 16GBを使用しました。 最初の生成実験:特定の入力と実行条件で、128トークンの生成に約43.8秒かかりました。 公式のメモリ値はシステム全体の使用量を表していません。OS、トークナイザー、キャッシュ、コンテキスト長による追加の使用量も考慮できます。重みの読み込み経路はアーキテクチャ文書が説明しています。 私の機器でもモデルは文章を生成しました。16GBのノートPCで実行できることを確認したうえで、応答速度と回答の正確さは別々に評価しました。 外部の根拠を中心にした回答生成 質問だけを与えるzero-shotの実験では、C# Native AOTとRustのコンパイルモデルを誤って説明する回答がありました。英語の質問でも同様の誤りが繰り返されました。十分な数の質問を用いた品質評価ではないため、この経験だけでモデル全体の性能は判断していません。 そこで、事実知識をモデル自身に任せる範囲を狭めました。自分で回答を評価できる.NET Native AOTとJITの質問を使い、Microsoft LearnのNative AOT文書を一緒に渡しました。 その後のエージェントでは、役割を次のように分けました。図内の表記は韓国語原稿のまま保持しています。 Edge0 질문과 문서의 내용 파악 관련 근거 선택 근거에 따른 답변 생성 외부 시스템과 Python 호스트 공식 문서 검색 입력 자료 구성 대화 상태 관리 한국어 입출력 처리 実験では、モデルが渡された文書を読み、質問に合う回答を構成できるかに焦点を当てました。 ...

2026年9月13日 · 1 分 ·  rkttu
明るい茶色の五線譜上の C# と分岐するノードで表現したプラットフォーム別の自動配布

netcoredbg 統合 C# 拡張の 8 プラットフォーム自動リリース構築記

2026年9月6日、私が運営する vscode-csharp-autobuild リポジトリで C# (with netcoredbg) 2.148.23001 の初回リリースを完了しました。Windows、Linux、Alpine Linux、macOS の x64 と ARM64 向けパッケージをすべて Open VSX に公開し、公開ファイルのハッシュが検証に使用した VSIX と一致することを確認しました。初回リリースには、C# 拡張と netcoredbg のソースバージョン、プラットフォーム別パッケージ、検証記録をまとめています。 本稿では、既存コミュニティの貢献と AI を活用したプロジェクトの出発点、8 プラットフォームの互換性検証、公開処理が中断した場合の復旧を扱います。定期ワークフローが新しいバージョンを検出すると、ソースビルドから公開ファイルの確認まで処理し、問題があればその候補の公開を停止する構成にしました。今回のリリースを踏まえて考えている後続の開発ツールについても紹介します。 まず別の拡張を作った理由から始め、プラットフォームの互換性問題とテスト範囲を説明します。その後、バージョンの対応付け、初回公開で判明した運用上の問題、実験用ワークフローの整理を取り上げます。最後に VS Code OSS ベースの .NET 開発ツール構想を紹介します。本稿のサポート範囲と実行結果は、2026年9月6日の初回リリースを基準としています。 既存の C# 配布を維持する別の拡張 ID 従来のリポジトリでは、Microsoft の C# 拡張ソースを自動ビルドして Open VSX に公開してきました。対象は C# 拡張であり、C# Dev Kit は含みません。公開用メタデータとビルド環境を調整しつつ、上流拡張の動作とデバッガー実装を維持する配布です。 C# 拡張のソース公開と、同梱デバッガーの利用範囲は別々に考えられます。Microsoft は vsdbg に関する文書で、このデバッガーをプロプライエタリな構成要素と説明し、Microsoft の IDE での利用に制限しています。そのため、拡張を Open VSX に移すだけでは VS Code 派生エディターにおけるデバッガーの制約までは解消できません。 既存の配布には、上流の動作をできる限り維持する価値があります。既存ユーザー全員にデバッガーの置き換えを適用するのではなく、別の拡張 ID を作りました。二つの配布は次の基準で選択できます。 配布 拡張 ID 維持または変更する範囲 従来の C# 配布 dotnetdev-kr-custom.csharp 上流拡張の動作とデバッガー実装を維持 C# (with netcoredbg) dotnetdev-kr-custom.csharp-with-netcoredbg coreclr デバッグアダプターを本リポジトリでビルドした netcoredbg に置き換え コミュニティでは muhammadsammy/free-vscode-csharp がすでに netcoredbg を統合し、VS Code 派生エディターで使える C# 開発環境を提供してきました。私は、このプロジェクトが築いた選択肢と、その過程で積み重ねた貢献に大きな歴史的価値を感じています。代替手段を実際に使える拡張として公開し、維持してきた muhammadsammy と貢献者の皆さんに敬意と感謝を表します。 ...

2026年9月6日 · 3 分 ·  rkttu
構成ファイルが状態管理ツールを経由してユーザー単位の SDK とランタイムコンポーネントへ接続されるイラスト

dotnetup プレビュー: global.json からの SDK とランタイム管理

Microsoft は Microsoft Build 2026 の事前収録セッション OD804 「Simplifying .NET installs with .NET Up」で、ユーザー単位の .NET インストールを管理する新しいツール dotnetup を紹介しました。2026年8月22日現在、dotnet/sdk リポジトリの release/dnup ブランチでは、Windows、macOS、Linux 向けのプレビュー版インストール手順とコマンドリファレンスを公開しています。この記事では、セッションが示した課題と設計意図をもとに、現在のプレビューで確認できる動作を整理します。 dotnetup は管理者権限を使わず、ユーザープロファイル配下に .NET SDK とランタイムをインストールします。リポジトリの global.json を読み取って必要な SDK チャネルを追跡し、複数の要件が共有するインストールファイルを更新または整理します。シェルやアプリケーションが管理対象の dotnet を見つける方法も選択できます。この記事では、インストール方法、global.json の解釈、SDK とランタイムの分離、インストール状態の管理、自動化での利用を扱います。 最初に、既存のインストール方法に残っていた管理上の空白を確認します。次に、プレビュー版のインストールとリポジトリ単位の SDK 準備をたどります。後半ではランタイムと更新モデルを説明し、現在の実装と OD804 のロードマップの差を検討します。 基準日: 2026年8月22日。dotnetup はプレビュー段階であり、コマンド名や動作が変わる可能性があります。この記事は release/dnup ブランチの公式ドキュメントと、同日にダウンロードした 0.2.0-preview.1.26410.1 を基準にしています。 インストールスクリプトより rustup に近い状態管理ツール .NET のインストール経路を一つの管理ツールへまとめる理由から確認します。OD804 セッションは、Windows で Visual Studio がツールチェーンを管理する場合と、それ以外の場合を対比しています。後者では、オペレーティングシステムのパッケージマネージャー、Web インストーラー、dotnet-install スクリプト、DNVM や mise のようなバージョン管理ツールが混在します。インストール主体と更新周期が異なると、必要な SDK を準備して古いインストールを削除する手順もリポジトリごとに変わります。セッションの課題定義は、この差をツールが解決しようとする出発点として示しています。 既存の dotnet-install スクリプトも、管理者権限を使わないインストールをサポートします。ただし、Microsoft はこのスクリプトを、実行のたびに SDK が消えても問題のない CI 環境で主に使うものと説明しています。開発環境にはインストーラーを案内してきました。dotnet-install の公式ドキュメントが、この利用範囲を明記しています。 ...

2026年8月23日 · 3 分 ·  rkttu
dotnet bot が半透明の数字11へ飛んでいく明るい紫色のイラスト

リリースを控えた .NET 11 の変更点と導入判断

Microsoft は .NET 11 の正式版を 2026年11月10日にリリースする予定です。2026年8月23日現在、8月11日に公開された .NET 11 Preview 7 が最新ビルドです。Microsoft はランタイムと SDK、C# 15、ASP.NET Core、.NET MAUI、Entity Framework Core を含む製品群を引き続き調整しています。正式リリースまでに機能や動作が変わる可能性も残っています。Microsoft による .NET 11 Preview 7 の発表 この記事では .NET 11 の変更を、サポート期間、ハードウェア基準、ランタイムと開発ツール、C# 15、アプリケーションフレームワークという五つの軸で整理します。機能の数より、既存サービスの移行に影響する点を中心に扱います。 最初にサポート期間と実行環境を扱い、次に Runtime Async と SDK を確認します。その後で C# 15 の型モデルと ASP.NET Core、EF Core の主な機能を検討します。最後に正式リリース前に適用できる確認手順を示します。 この記事は、次の基準日に公開されていたプレビュー資料をもとに作成しました。 基準日: 2026年8月23日 対象バージョン: .NET 11 Preview 7、SDK 11.0.100-preview.7 状態: プレビュー機能と互換性変更の一覧は正式リリースまでに変わる可能性があります。プレビュービルドは一般に本番環境での利用をサポートしません。 サポート期間を延ばさない STS リリース .NET 11 のサポート期間から整理します。Microsoft は .NET 11 を STS に分類し、2026年11月10日から2028年11月9日までの2年間サポートする予定です。LTS と STS の品質水準は同じで、サポート期間だけが異なります。LTS は3年間、STS は2年間にわたってパッチと技術サポートを提供します。この予定は .NET 11 のリリース計画と .NET のサポートポリシーで確認できます。 ...

2026年8月23日 · 3 分 ·  rkttu
C# 50行でOpenAI Codex CLIを任意のLLMに繋ぐコンセプト画像

OpenAI Codex CLIをClaude・Gemini・Llamaの上で動かす — C# 50行で

OpenAIのCodex CLIは優れたエディタエージェントUXを提供します(shellツール、apply_patch、plan tracking がすべて揃っています)。問題は、2026年2月時点でOpenAI Responses APIだけをサポートしていることです。Chat Completionサポートは削除されており(codex-rs/model-provider-info/src/lib.rsのWireApi enumにはResponsesのみが残っています)、Chat Completionのみをサポートするエンドポイント(Ollama、LM Studio、お気に入りのLlama runner)はそのまま閉ざされてしまいます。本記事は.NET 10のfile-basedプログラムとMicrosoft.Extensions.AIのIChatClient抽象化を活用し、50行のC#一ファイルでResponses互換サーバーを立て、OpenRouterを介してCodex CLIを任意のモデル上で動作させた過程をまとめます。 はじめに Codex CLIはResponsesを話すどんなサーバーとも気持ちよく対話します。model_provider configブロックがまさにこのために存在します。つまり、好きなモデルでバックされたResponses互換HTTPエンドポイントを立てさえすれば、Codexは汎用フロントエンドになり、頭脳はユーザーが選べます。 最近私が気に入っているトリックは次のとおりです。Microsoft.Extensions.AIのベンダー中立なIChatClient抽象化の上で、OpenAI Chat CompletionサーバーとResponses APIサーバーを同時に動かす50行のC#スクリプトを起動します。バックエンドはOpenRouterに向けます(APIキー1つで Claude、Gemini、Llama、GPTなど数百のモデルを使えます)。そしてCodexに対して、OpenAIではなくこのローカルスクリプトと対話するよう設定します。 最終的な結果は、OpenAI Codex CLIがAnthropicのClaude 3.5 Sonnetの上で動作する状態です(あるいはその日に使いたい別のモデルの上でも)。 構成要素 私が自分で公開しているCadenza.AgentというMSBuild SDKを使います。単一の.csファイルを実行可能なエージェントサーバーに変換するSDKで、.NET 10のfile-basedプログラム向け単一ファイルスクリプティングSDKファミリーの一部です(dotnet run script.csと同じ発想ですが、より豊富なTier-1 API(Tool、UseOllama、UseOpenAi、Runなど)を提供します)。Agentバリアントは次を公開します。 POST /v1/chat/completions — Aider / Continue / Cursor / Copilot BYOK / sgpt 向け POST /v1/responses — Codex CLI 向け どちらもユーザーが構成した同じIChatClientでバックされます。バックエンドを変えても wireフォーマットはそのままです。 LLM側はOpenRouterを使います。OpenAIのChat Completion wireフォーマットを別のbase URLでそのまま提供するので、Microsoft.Extensions.AI.OpenAIのChatClientをそのまま挿して使えます。環境変数1つ、任意のモデル。 Codexの設定はCODEX_HOME環境変数のトリックを活用します。~/.codex/config.tomlを編集する代わりに、Codexが指すディレクトリをサンプル専用に別途作っておけば、そこから新しいconfig.tomlを読みます。これでユーザーのグローバル設定に一切触れない、自己完結型のサンプルが作れます。 スクリプト バックエンドのすべて、ファイル1つです。 #!/usr/bin/env dotnet run #:sdk Cadenza.Agent@1.0.14 using System.ClientModel; using OpenAI; var apiKey = Env.Get("OPENROUTER_API_KEY") ?? throw new InvalidOperationException("OPENROUTER_API_KEY env var missing"); var model = Env.Get("OPENROUTER_MODEL") ?? "anthropic/claude-3.5-sonnet"; ServedModelName = "cadenza-codex-openrouter"; // サンプル専用のCodex homeディレクトリを作成。 var codexHome = Path.Combine(Env.Cwd, ".cadenza-codex-openrouter"); MakeDir(codexHome); var catalogPath = Path.Combine(codexHome, "cadenza-catalog.json").Replace('\\', '/'); var configToml = $""" model = "cadenza-codex-openrouter" model_provider = "cadenza" model_catalog_json = "{catalogPath}" [model_providers.cadenza] name = "Cadenza.Agent (OpenRouter-backed)" base_url = "http://localhost:8080/v1" wire_api = "responses" env_key = "CADENZA_API_KEY" stream_idle_timeout_ms = 300000 """; WriteText(Path.Combine(codexHome, "config.toml"), configToml); // Catalog JSON: Codex に提供するモデル id を宣言して "Defaulting to // fallback metadata" 警告を防ぎます。フィールドは codex-rs/protocol/src/ // openai_models.rs の ModelInfo スキーマ基準 — すべてのキーが必須です。 var catalogJson = """ { "models": [{ "slug": "cadenza-codex-openrouter", "display_name": "Cadenza (OpenRouter)", "description": "OpenRouter-backed agent served by Cadenza.Agent", "supported_reasoning_levels": [], "shell_type": "default", "visibility": "list", "supported_in_api": true, "priority": 50, "availability_nux": null, "upgrade": null, "base_instructions": "", "supports_reasoning_summaries": false, "support_verbosity": false, "default_verbosity": null, "apply_patch_tool_type": "freeform", "truncation_policy": { "mode": "tokens", "limit": 8192 }, "supports_parallel_tool_calls": true, "context_window": 200000, "max_context_window": 200000, "auto_compact_token_limit": 180000, "effective_context_window_percent": 95, "experimental_supported_tools": [] }] } """; WriteText(Path.Combine(codexHome, "cadenza-catalog.json"), catalogJson); WriteLine($"Codex config generated at: {codexHome}"); WriteLine("In another terminal, run:"); WriteLine($" $env:CODEX_HOME = \"{codexHome}\""); WriteLine($" $env:CADENZA_API_KEY = \"any-non-empty-string\""); WriteLine($" codex"); // OpenRouter を LLM バックエンドとして接続。 var openAiOptions = new OpenAIClientOptions { Endpoint = new Uri("https://openrouter.ai/api/v1") }; var chatClient = new OpenAI.Chat.ChatClient(model, new ApiKeyCredential(apiKey), openAiOptions) .AsIChatClient(); UseChatClient(chatClient); await Run(); これがすべてです。プロジェクトファイルも、.csprojも、Program.csもありません。一番上の#:sdkディレクティブが.NET 10のfile-basedプログラムシステムに「このスクリプトはCadenza.Agent SDKを使う」と知らせ、SDKがHTTPサーバー、Responses wireフォーマット、すべてのパッケージ参照を引き込みつつ、Tool、UseOllama、UseChatClient、Runを名前空間なしですぐ呼べる名前として公開します。 ...

2026年5月27日 · 3 分 ·  rkttu
C#とPythonが出会う機械学習インターロップの概念図

C#からHugging Faceモデルを呼ぶ:DotNetPy 0.6.0でWhisper・sentence-transformers・Stable Diffusionを動かす

週末に小さなC#ライブラリ DotNetPy の0.6.0をリリースしました。CPythonのC APIを直接呼び出して、.NETアプリの中でPythonを動かすインターロップライブラリです。本記事は0.6.0に含まれる3つの機械学習サンプル(sentence-transformersによる意味検索、Whisperによる音声認識、Stable Diffusion Turboによる画像生成)をどうまとめたか、そしてその過程でPEP 703 free-threaded CPythonをどのように検証したかを記録したものです。 出発点:手元にあるのはC#だけ、モデルはHugging Faceにある 数か月ごとに同じパターンが繰り返されます。字幕用にWhisperが必要、検索用にsentence-transformerが必要、ときにはStable Diffusionのようなモデルを使いたい。しかし手元にあるのはC#一つだけ。こんなとき、よく取られる回避策にはそれぞれ決定的な欠点があります。 ONNXに変換する。 画像系やエンコーダ系のモデルにはよく合いますが、新しいアーキテクチャやdiffusionパイプラインでは、変換そのものが別プロジェクトになります。 Pythonマイクロサービスとして立ち上げる。 プロセスが2つ、デプロイのシナリオが2つ、そしてホットパスにネットワークホップが1つ追加されます。 外部APIを呼び出す。 コストがかかり、インターネット接続が必要で、データが箱の外に出ます。 pythonnet や CSnakes を使う。 堅実な選択肢ですが、pythonnetはまだNative AOTをサポートしておらず、CSnakesはSource Generatorベースのワークフローを強制します。さらに、両ライブラリともfree-threaded CPythonビルドに対する公開された検証結果をまだ出していません。 私はもう少し薄い選択肢が欲しかったのです。C#コードの中にPythonスニペットを文字列としてインラインで書き、配列をそのまま渡し、JSON形式で結果を受け取り、全体がAOTコンパイルされて単一バイナリになる 形です。それがDotNetPyの目標であり、以下の3つのサンプルはすべてGPUのない普通のWindows 11ノートPCで最初から最後まで動作します。 サンプル1 — sentence-transformers による意味検索 最初のサンプルは小さなコーパスを埋め込み、クエリをエンコードして、最も類似する上位K件の文を返します。戻り値はDotNetPyValueで、内部的にはJSONドキュメントをラップしたもので、GetString()・GetInt32()・GetDouble()、そしてパスベースのプロパティアクセスを通じて.NETの世界に戻ってきます。 using DotNetPy; using DotNetPy.Uv; using var project = PythonProject.CreateBuilder() .WithProjectName("dotnetpy-ml-embeddings") .WithPythonVersion("==3.12.*") .AddDependencies( "sentence-transformers==2.7.0", "transformers==4.40.2", "torch>=2.2,<2.5") .Build(); await project.InitializeAsync(); var executor = project.GetExecutor(); executor.Execute(@" import numpy as np from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2') "); var corpus = new[] { "Python is a popular programming language for data science.", "C# and .NET are great for building enterprise applications.", "Rust offers memory safety without garbage collection.", "Pizza is delicious with various toppings.", // … }; var query = "Tell me about programming languages"; using var hits = executor.ExecuteAndCapture(@" corpus_emb = model.encode(corpus, normalize_embeddings=True) query_emb = model.encode([query], normalize_embeddings=True)[0] sims = corpus_emb @ query_emb top_idx = np.argsort(-sims)[:3] result = [ {'rank': int(rank + 1), 'score': float(sims[i]), 'text': corpus[int(i)]} for rank, i in enumerate(top_idx) ] ", new Dictionary<string, object?> { { "corpus", corpus }, { "query", query } }); foreach (var hit in hits!.RootElement.EnumerateArray()) { Console.WriteLine($" {hit.GetProperty("rank").GetInt32()}. " + $"[{hit.GetProperty("score").GetDouble():F3}] " + $"{hit.GetProperty("text").GetString()}"); } 実際の出力はこうなります。 ...

2026年5月11日 · 4 分 ·  rkttu