Windows操作の画像からクリック対象を説明するため、ローカルのVision Language Model(VLM)を比較していました。
その途中で、PowerShellから実行した結果と、C#から実行した結果が一致しないことがありました。コード上では同じプロンプトを組み立てています。それでも、出力が違います。
以前のモデル比較記事では、引用符の欠落が見つかったことを短く紹介しました。今回は、その原因を切り分けるために「何を、どこで確認したか」を掘り下げます。
以下は当時の検証記録を整理したもので、今回新しく実行した結果ではありません。
1. まず、同じ経路の中でも結果が変わるかを確認した
最初から「モデルが不安定だから」と決めず、実行経路を分けて繰り返しました。
対象はQwen3.5とHoloです。調査用のH10ケースを両モデルで、H04ケースをHoloで確認したところ、それぞれの実行経路では3回とも同じ出力になりました。
この段階で確認できたのは、限られたケースでは同じ経路内の出力がそろったことです。あらゆる入力で再現性が保証されたわけではありません。
一方、PowerShell経由とC#経由の差は残りました。そこで、モデルの中ではなく、モデルへ入力を渡す手前を調べました。
2. 組み立てた文字列は一致していた
まず、実行前に組み立てたプロンプトをUTF-8のファイルへ書き出して比較しました。
H10の調査で確認した文字列は、865文字・1,639バイトで一致していました。
ここまでなら「同じプロンプトを渡している」と考えてしまいそうです。ただし、この比較で分かるのは、引数として渡す前の文字列が同じことまでです。
組み立てた文字列
↓
外部プログラムの呼び出し
↓
子プロセスが受け取った引数
調べるべき場所は、もう一段先にありました。
3. 推論プログラムを引数確認用のプログラムに置き換えた
子プロセスが受け取った内容を確認するため、推論プログラムの代わりに、受信した引数を記録する ArgvEcho.exe を使いました。
H10の調査結果は次のとおりです。
| 確認した位置・経路 | プロンプトの文字数 |
|---|---|
| 実行前に組み立てた文字列 | 865 |
Windows PowerShell 5.1の & 経由で受信した値 |
849 |
C#の ProcessStartInfo.ArgumentList 経由で受信した値 |
865 |
PowerShell側では、受信した文字列が16文字短くなっていました。
差分は、プロンプト内に含めていたJSON schemaの8個のキーを囲む二重引用符でした。キーの前後にある引用符が、合計16文字欠落していました。
これは、今回の長い複数行プロンプトを -p 引数で渡す経路で確認した結果です。PowerShellから渡す文字列が必ず16文字短くなる、という話ではありません。また、865文字という長さはこの単体調査の値です。
Windows PowerShell 5.1では、ネイティブコマンドへの引数はPowerShell側で解析された後、コマンドライン文字列として渡されます。文字列に引用符を含めたい場合は、受け渡し時の扱いにも注意が必要です。Microsoft Learnの引数解析の説明
4. 欠けた文字列を別の経路から渡して、旧出力を再現した
文字数の違いが見つかっただけでは、それが出力の差を説明するかはまだ分かりません。
そこで、PowerShell側で実際に受信した、引用符が欠けた文字列をC#側から渡しました。H10では、両モデルとも以前のPowerShell経由の出力を再現できました。
さらに、元のループとプロンプト構築を維持し、呼び出す実行ファイルだけを ArgvEcho.exe に置き換えて、Hard 20件を確認しました。両モデルで20件すべてに、同じ種類の16文字の引用符欠落が見つかりました。
その欠落した入力をC#側から再入力すると、次の結果になりました。
| モデル | 欠落した入力で、以前の結果と一致した件数(target_label比較) |
|---|---|
| Qwen3.5 | 20 / 20 |
| Holo | 20 / 20 |
この20 / 20は、認識精度100%という意味ではありません。
正しい対象を認識できた件数でも、修正後の正解数でもありません。引用符が欠けた入力を与えたときに、以前の結果と target_label が一致した件数です。出力JSON全体のバイト一致を示す数字ではありません。
今回調べた経路間の差については、入力の違いが結果に影響していたことを裏づける確認になりました。
5. 引数を渡す経路をそろえて、比較をやり直した
その後の比較では、C#側の ProcessStartInfo.ArgumentList を使い、引用符を保持する経路にそろえて再測定しました。
ここでいう ArgumentList は、.NETの ProcessStartInfo のプロパティです。PowerShellの Start-Process -ArgumentList パラメーターと同じものとして扱わないでください。
.NETの ArgumentList は、引数を個別の文字列として追加するための仕組みです。Arguments と同時には使わず、どちらか一方を使用します。Microsoft LearnのAPI説明
このプロパティは.NET Frameworkにはありません。Windows PowerShell 5.1のスクリプトへ、そのまま同じ書き方を移すための手順ではなく、今回使ったC#側の呼び出し経路の説明です。
今回の記事では、修正後のモデル順位や精度の比較表は載せません。共通の入力欠落があったからといって、全モデルへの影響が同じとも、以前の順位が保たれるとも言えないためです。入力条件をそろえたうえで、改めて評価する必要があります。
また、PowerShell 7について、この検証で動作確認したわけではありません。バージョン変更で今回と同じ結果になる、という保証もしません。
次に同じような差が出たら、どこを見るか
今回の切り分けから、次の順序で確認すると整理しやすいと感じました。
- 同じ経路で繰り返したときにも、出力が変わるかを確認する。
- 組み立てた入力文字列を保存して比較する。
- 子プロセスが実際に受け取った引数を確認する。
- 差分のある入力を別経路から再入力し、出力の変化を確かめる。
- 入力条件をそろえてから、モデルの比較に戻る。
実行ファイル、モデル、画像、生成設定、シェルのバージョンも合わせて記録しておくと、後から比較しやすくなります。引数の記録に機密情報や認証情報が含まれないよう、保存先や共有範囲には注意が必要です。
コード内の文字列が同じでも、子プロセスが同じ文字列を受け取ったとは限りません。
モデルの出力が変わったときに、生成設定だけを見るのではなく、実際に届いた入力まで確認する。今回の検証では、それが原因を切り分ける手がかりになりました。