<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bigkijimon</title>
    <description>The latest articles on DEV Community by bigkijimon (@bigkijimon).</description>
    <link>https://dev.to/bigkijimon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023503%2F2924c7b5-d02d-4638-91bf-4d560e34ec3b.png</url>
      <title>DEV Community: bigkijimon</title>
      <link>https://dev.to/bigkijimon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bigkijimon"/>
    <language>en</language>
    <item>
      <title>Qwen2.5-VL 7BはM1 Max 64GBで何tok/s出るか — OCR実測で見えた「画像の複雑さは速度に効かない」理由</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:58:47 +0000</pubDate>
      <link>https://dev.to/bigkijimon/qwen25-vl-7bham1-max-64gbdehe-tokschu-ruka-ocrshi-ce-dejian-etahua-xiang-nofu-za-sahasu-du-nixiao-kanai-li-you-1ei6</link>
      <guid>https://dev.to/bigkijimon/qwen25-vl-7bham1-max-64gbdehe-tokschu-ruka-ocrshi-ce-dejian-etahua-xiang-nofu-za-sahasu-du-nixiao-kanai-li-you-1ei6</guid>
      <description>&lt;p&gt;ローカルVLM(視覚言語モデル)の速度比較記事はいくつもあるが、載っているMacはM4 ProやM4/M5 Maxばかりで、&lt;strong&gt;M1 Max 64GBの実測行は1本も見当たらない&lt;/strong&gt;。手元にちょうどQwen2.5-VL 7Bが入っていたので、OCR(画像→テキスト書き起こし)タスクで実際に測った。結論から書くと、&lt;strong&gt;出力速度は画像の内容にほぼ関係なく23.4〜26.0 tok/sの狭い帯に収まった&lt;/strong&gt;。速度を決めていたのは画像の複雑さではなく、書き起こす文章の長さだった。&lt;/p&gt;

&lt;h2&gt;
  
  
  検証環境と方法
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ハード: M1 Max 64GB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ollama: 0.30.8(&lt;code&gt;ollama --version&lt;/code&gt;実測)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;モデル: &lt;code&gt;qwen2.5vl:7b&lt;/code&gt;(6.0GB、2026-07-24に追加済)。&lt;code&gt;ollama show&lt;/code&gt;実測では architecture=qwen25vl / parameters=&lt;strong&gt;8.3B&lt;/strong&gt;(タグ名は7bだが、視覚エンコーダを含めた実パラメータは8.3B) / quantization=Q4_K_M&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;推論バックエンド: サーバーログの&lt;code&gt;srv&lt;/code&gt;/&lt;code&gt;slot&lt;/code&gt;/&lt;code&gt;clip_model_loader&lt;/code&gt;形式から llama.cpp系のGGUFランタイム(MLXではない)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;テスト画像は外部から拾わず、HTML→ヘッドレスChromeのスクリーンショットで自作した3種類。&lt;/p&gt;

&lt;p&gt;画像サイズ内容&lt;/p&gt;

&lt;p&gt;test_a900×620ターミナルのエラー画面(英語コード、612字相当)&lt;br&gt;
test_b900×620和文契約書調の密文(356字、句読点の多い長文)&lt;br&gt;
test_c420×420レシート(品目と金額、数字中心の構造化データ)&lt;/p&gt;

&lt;p&gt;実行前に自社の&lt;code&gt;gpucheck.sh&lt;/code&gt;でGPUがアイドルであることを確認し、Ollamaの&lt;code&gt;/api/generate&lt;/code&gt;(&lt;code&gt;stream:false&lt;/code&gt;)を4回叩いた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run1_cold_a  : 初回(モデル未ロードの状態からtest_a)
run2_warm_a  : 直後にtest_aを再送(画像・プロンプトとも完全一致)
run3_warm_b  : test_b(新規画像)
run4_warm_c  : test_c(新規画像)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;レスポンスJSONの&lt;code&gt;eval_count&lt;/code&gt;/&lt;code&gt;eval_duration&lt;/code&gt;/&lt;code&gt;prompt_eval_count&lt;/code&gt;/&lt;code&gt;prompt_eval_duration&lt;/code&gt;/&lt;code&gt;load_duration&lt;/code&gt;を取り、&lt;code&gt;~/.ollama/logs/server.log&lt;/code&gt;の&lt;code&gt;slot print_timing&lt;/code&gt;行と突合して一致を確認した(二重検証)。&lt;/p&gt;

&lt;h2&gt;
  
  
  結果: 出力速度は画像の内容にほぼ関係なく23.4〜26.0 tok/s
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpctgas5y4t2286tflllv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpctgas5y4t2286tflllv.png" alt="Qwen2.5-VL 7BのM1 Max実測4回の内訳、load/prompt_eval/evalの積み上げ棒グラフ" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4回の実測内訳。loadはモデル読込、prompt_evalは画像エンコード+プリフィル、evalは出力生成にかかった時間。&lt;/p&gt;

&lt;p&gt;run画像totalloadprompt_evaleval(出力)tok/s&lt;/p&gt;

&lt;p&gt;run1(cold)test_a17.51s5.05s5.78s(1095tok)6.67s(173tok)25.95&lt;br&gt;
run2(warm・再送)test_a7.65s0.19s0.05s(キャッシュ)7.39s(173tok)23.41&lt;br&gt;
run3(warm)test_b17.11s0.20s5.51s(1095tok)11.37s(273tok)24.01&lt;br&gt;
run4(warm)test_c11.91s0.19s5.77s(1131tok)5.92s(143tok)24.16&lt;/p&gt;

&lt;p&gt;異なる画像3枚(run1・run3・run4)のtok/sを平均すると&lt;strong&gt;24.71 tok/s&lt;/strong&gt;((25.95+24.01+24.16)/3)。全4回でも最小23.41〜最大25.95tok/sの幅に収まっており、テキスト主体の英語コード画面でも、句読点の多い和文密文でも、数字が並ぶレシートでも、出力速度そのものはほとんど変わらなかった。&lt;/p&gt;

&lt;p&gt;今回の空白を埋めるために比較材料にした&lt;a href="https://insiderllm.com/guides/vision-models-locally/" rel="noopener noreferrer"&gt;insiderllm.comの速度表&lt;/a&gt;では、Mac上での最速行は「M4 Pro 24GB」でQwen3-VL 8B=40〜60 t/sとされている(2026-07-29時点、M1 Maxの実測行は無し)。モデルもチップ世代も違うので単純比較はできないが、4年前のM1 Max 64GBはこの数字の半分強という位置づけになる。&lt;/p&gt;

&lt;h2&gt;
  
  
  画像側の前処理コストは「複雑さ」でなく、ほぼ固定コスト
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prompt_eval_count&lt;/code&gt;(画像+プロンプトのトークン数)は run1=1095, run3=1095, run4=1131 と、900×620の密文(run3)も420×420のレシート(run4)も、900×620のコード画面(run1)とほぼ同じ範囲に収まった。ピクセル数も内容の複雑さもまったく違う3枚が、トークン数ではほぼ横並びになる。なぜレシート(run4)がやや多いのかは正確な内訳を検証できていないので「不明」と書いておく(アスペクト比の丸め処理の影響と推測はできるが、断定はしない)。&lt;/p&gt;

&lt;p&gt;サーバーログを見ると、画像のエンコード自体は&lt;code&gt;image processed in 2410〜2667 ms&lt;/code&gt;とほぼ一定。prompt_eval全体(5.51〜5.78s、run2を除く)のうち約半分がこの画像エンコードで、残りはプリフィル(プロンプト全体をモデルに読ませる処理)にあたる。&lt;/p&gt;

&lt;p&gt;唯一の例外がrun2で、test_aと完全に同じ画像・同じプロンプトを直後に再送したところ、&lt;code&gt;prompt_eval_duration&lt;/code&gt;が5.78s→&lt;strong&gt;0.05s&lt;/strong&gt;まで落ちた。サーバーログにも&lt;code&gt;image processed&lt;/code&gt;の行自体が出ておらず、画像エンコードそのものがスキップされている。つまりOllamaは同一コンテキストの再送に対してキャッシュを効かせるが、&lt;strong&gt;1文字でも画像や内容が変われば、毎回この5.5〜5.8秒の前処理コストを払い直す&lt;/strong&gt;。&lt;/p&gt;

&lt;h2&gt;
  
  
  精度実測: レシート100%・コード実質100%・密文99.72%
&lt;/h2&gt;

&lt;p&gt;各レスポンスを元テキストと&lt;code&gt;difflib.SequenceMatcher&lt;/code&gt;で突合した(空白・改行の差は正規化して除外)。&lt;/p&gt;

&lt;p&gt;画像文字数一致率誤り&lt;/p&gt;

&lt;p&gt;test_c レシート(数字・構造化)123字&lt;strong&gt;100.0%&lt;/strong&gt;なし(区切り線の表記ゆれを除く)&lt;br&gt;
test_a コード画面612字実質100%HTMLエンティティの表記差(&amp;lt;→&amp;lt;)のみ、内容は完全一致&lt;br&gt;
test_b 和文密文356字99.72%1文字誤変換(「自己&lt;strong&gt;の&lt;/strong&gt;費用負担」→「自己&lt;strong&gt;的&lt;/strong&gt;費用負担」)&lt;/p&gt;

&lt;p&gt;金額9箇所を含むレシートは1文字も落とさず完全一致。長い和文の自由文だけ、356字中1字だけ助詞の「の」を「的」に取り違えた。3種類・計1,091字を通しての誤りはこの1文字のみで、実務で使う分には十分な精度だが、&lt;strong&gt;「絶対に間違えない」ものではない&lt;/strong&gt;という前提で使う必要がある。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ: 実務でどう使うか
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;1枚あたり6〜17秒(モデルロード後)かかるため、チャットのような即応答ではなく、バッチ/非同期の書き起こし処理向き。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;速度のボトルネックは画像の複雑さではなく&lt;strong&gt;出力トークン数&lt;/strong&gt;。書き起こす文章が長い画像ほど遅くなる(run3の273tok出力は run4の143tok出力よりevalだけで約2倍時間がかかった)。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;同一画像への再送はキャッシュが効いて速いが、実運用でそれを当てにできる場面は少ない。基本は「新規画像=毎回5.5〜5.8秒の前処理コスト」で見積もる。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;精度は数字・構造化データほど安定し、長い自由文ほど低確率で1文字単位の誤りが出る。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;なお、insiderllm.comの速度表本体・huggingfaceのolmOCR-2-7B-1025-MLX-8bit(MLX量子化の専用OCR VLM)は、今回の検証(Ollama/GGUF経路)の対象外として実際には開いていない。前者は事前調査で内容を確認した要約を出典として引用したのみ、後者はローカルOCR特化VLMへの関心の高さを示す文脈として触れたのみで、自分では試していない。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>qwen</category>
      <category>llm</category>
      <category>ocr</category>
    </item>
    <item>
      <title>AIが無人で書き溜めた社内ドキュメント1,355本をDiátaxisで仕分けたら、3割が自社のドキュメントですらなかった</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:55:12 +0000</pubDate>
      <link>https://dev.to/bigkijimon/aigawu-ren-deshu-kiliu-metashe-nei-dokiyumento1355ben-wodiataxisdeshi-fen-ketara-3ge-gazi-she-nodokiyumentodesuranakatuta-7d6</link>
      <guid>https://dev.to/bigkijimon/aigawu-ren-deshu-kiliu-metashe-nei-dokiyumento1355ben-wodiataxisdeshi-fen-ketara-3ge-gazi-she-nodokiyumentodesuranakatuta-7d6</guid>
      <description>&lt;p&gt;Claude Codeに無人でドキュメントを書かせ続けて数か月経つと、「うちのナレッジベースは今どんな型に偏っているのか」が気になった。答え合わせに &lt;a href="https://diataxis.fr/" rel="noopener noreferrer"&gt;Diátaxis&lt;/a&gt;（Tutorials / How-to guides / Technical reference / Explanation の4分類フレームワーク）を使い、自社リポジトリの Markdown をローカルLLMで全数分類した。結果は「tutorial型が0.0%」という綺麗な数字になったが、それより先に踏んだ罠のほうが再現性が高い教訓だった——&lt;strong&gt;「何本あるか」を数える前に、その母数が本当に自分たちが書いたものかを疑う必要があった。&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  母数を数える前に、母数が汚れていた
&lt;/h2&gt;

&lt;p&gt;まず素直に数えた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# $VAULT = 自社ドキュメントの置き場（各自の環境に読み替える）&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/graphify-out/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/.next*/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/_archive/*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
→ 1355
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,355本。この数字をそのままDiátaxisに投げるところだった。だが内訳をディレクトリ単位で眺めると、同じルールファイルが違うパスに何度も出てくる。犯人は Claude Code 用スキルパッケージ &lt;code&gt;vercel-react-best-practices&lt;/code&gt; のファイル75本(ルール72本+README/SKILL/AGENTS各1)で、1つのアプリ配下に5箇所——&lt;code&gt;skills/&lt;/code&gt; &lt;code&gt;data/skills/&lt;/code&gt; &lt;code&gt;agent/skills/&lt;/code&gt; &lt;code&gt;.tabnine/agent/skills/&lt;/code&gt; &lt;code&gt;.agents/skills/&lt;/code&gt;——別々にインストールされていた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &lt;span class="nt"&gt;-q&lt;/span&gt; skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md &lt;span class="se"&gt;\&lt;/span&gt;
        agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
diff &lt;span class="nt"&gt;-q&lt;/span&gt; skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md &lt;span class="se"&gt;\&lt;/span&gt;
        .tabnine/agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
&lt;span class="c"&gt;# 差分なし。5箇所ともバイト単位で同一ファイル&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ベンダー配布のスキルファイルとElectronアプリのビルド同梱docsをまとめて弾くと、438本(1,355本の32.3%)が「自分たちが書いたのではなく、ツールが持ち込んだもの」だった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; ... | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"/skills/|/dist/|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;tabnine"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
→ 438
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;残った&lt;strong&gt;917本&lt;/strong&gt;が、実際にBigKiji自身(主にClaude Code)が無人で書き溜めた自社コーパスの実体だ。ここから先はこの917本を対象にする。&lt;/p&gt;

&lt;h2&gt;
  
  
  Diátaxisという物差し
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://diataxis.fr/" rel="noopener noreferrer"&gt;diataxis.fr&lt;/a&gt; はドキュメントを4つの需要に分ける。原文の定義を借りると "Diátaxis identifies four distinct needs, and four corresponding forms of documentation"。4分類は Tutorials(学習)・How-to guides(特定タスクの遂行)・Technical reference(情報の参照)・Explanation(理解と背景)で、これを「実践的⇔理論的」「学習志向⇔参照志向」の2軸が分ける。&lt;/p&gt;

&lt;h2&gt;
  
  
  ローカルLLMで917本を仕分ける
&lt;/h2&gt;

&lt;p&gt;Claudeのトークンは使わず、Ollama上の &lt;code&gt;qwen3.5:latest&lt;/code&gt; に投げた。各ファイル冒頭500文字を抜粋し、5択(tutorial/howto/reference/explanation/other)を1単語で返させる。最初の実装は空応答しか返ってこなかった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;curl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;http://localhost:&lt;/span&gt;&lt;span class="mi"&gt;11434&lt;/span&gt;&lt;span class="err"&gt;/api/generate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3.5:latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Classify ... Answer with exactly one word:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"num_predict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"thinking"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Thinking Process:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;1. Analyze the Request..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"done_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"length"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;qwen3.5はデフォルトで思考トークンを吐くhybrid-thinkingモデルで、&lt;code&gt;num_predict&lt;/code&gt;の予算をthinkingだけで使い切り、本題の1単語に到達する前に打ち切られていた。Ollama 0.30.8では &lt;code&gt;"think": false&lt;/code&gt; を渡すとthinkingを止めて即答させられる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3.5:latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"num_predict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;917本の逐次分類にかかった時間は&lt;strong&gt;1373.9秒(約22.9分)、平均1.498秒/件&lt;/strong&gt;(スクリプトの実測タイマー)。GPUはOllama単独稼働でComfyUI等の他ジョブは走らせていない状態。&lt;/p&gt;

&lt;h2&gt;
  
  
  結果 — 欠けていたのは"tutorial"、ゼロ件
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wv168y13w3z5q1oorko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wv168y13w3z5q1oorko.png" alt="無人生成コーパス917本のDiátaxis内訳。reference 47.2%、explanation 26.9%、howto 21.4%、other 4.5%、tutorial 0.0%の横棒グラフ" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;n=917（全1,355本からベンダー配布物・ビルド成果物438本を除外）。分類=qwen3.5:latest(Ollama, think:false)。要点: 自社コーパスはreference/explanationに偏り、tutorialは0件——読者がAIエージェント自身であることの反映。&lt;/p&gt;

&lt;p&gt;分類件数割合&lt;/p&gt;

&lt;p&gt;reference43347.2%&lt;br&gt;
explanation24726.9%&lt;br&gt;
howto19621.4%&lt;br&gt;
other414.5%&lt;br&gt;
tutorial00.0%&lt;/p&gt;

&lt;p&gt;最大はreference(47.2%)、最小はtutorialでゼロ。念のため「はじめに」「Step 1」「チュートリアル」等のキーワードを含む36本を別途スキャンしたが、その中にもtutorial判定は1本も出なかった——分類器の癖ではなく、実際にそういう内容がなかったということだ。組織の「骨格」にあたるドキュメント(部署ごとのナレッジ地図&lt;code&gt;MOC_*&lt;/code&gt;6本・各部署の追記式ログ&lt;code&gt;INDEX.md&lt;/code&gt;20本・全体入口&lt;code&gt;000_START_HERE.md&lt;/code&gt;)も個別に確認したところ、MOC_*はreference4本/explanation2本、INDEX.mdは20本中18本がreferenceで残り2本がhowto——どちらにもtutorialは1本も無く、骨格そのものの設計意図がtutorial型を持っていなかった。&lt;/p&gt;

&lt;p&gt;理由は分かりやすい。この917本の主な読み手はAIエージェント自身であって、初心者の人間ではない。エージェントは「手順を追って学ぶ」必要がなく、「今必要な事実を1回で引く(reference)」か「なぜそうなっているかを把握する(explanation)」かのどちらかを求める。tutorialが生まれない構造は、無人ドキュメント生成が悪いのではなく、読者の性質を正しく反映した結果だった。&lt;/p&gt;
&lt;h2&gt;
  
  
  分類パイプライン自身にもバグがあった
&lt;/h2&gt;

&lt;p&gt;初回実行では917本中22本が問答無用で"other"になっていた。原因はPythonの定番の罠で、パスの先頭2文字("./")を剥がすつもりで書いた &lt;code&gt;path.lstrip("./")&lt;/code&gt; が、実際には「先頭から'.'か'/'に含まれる文字を続く限り削る」という文字集合ベースの処理だった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 誤り: "./.pi-subagents/artifacts/x.md" → "pi-subagents/artifacts/x.md"
#       (先頭の "./" だけでなく、隠しディレクトリの "." まで一緒に剥がれる)
&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# 修正: プレフィックスの明示的な除去に置き換え
&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.pi-subagents/&lt;/code&gt; &lt;code&gt;.pi/&lt;/code&gt; &lt;code&gt;.claude/&lt;/code&gt; 配下の隠しディレクトリだけがこの罠を踏み、ファイルが見つからず例外→機械的に"other"へフォールバックしていた。修正して22本を再分類すると、"other"のまま残ったのは3本だけで、残り19本はreference/explanation/howtoへ再分配された。上記の表はこの修正後の最終値である。&lt;/p&gt;

&lt;h2&gt;
  
  
  学び
&lt;/h2&gt;

&lt;p&gt;◆ドキュメント型の分析結果 &lt;br&gt;
無人ドキュメント生成は「参照」と「説明」と「作業ログ」を量産するのは得意だが、「学習コンテンツ」を自然には生まない。読み手がAIエージェント自身である社内コーパスでは、それはむしろ健全な偏りだ。&lt;/p&gt;

&lt;p&gt;◆実務的な教訓(母数チェック) &lt;br&gt;
母数を数える段階で32.3%がベンダー配布物というノイズだったことのほうが、実務上はよほど効く教訓だった。再現する手順は3行に収まる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. 母数を数える(除外条件を明示)&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/.next*/*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 2. ベンダー/ビルド成果物を機械的に弾く&lt;/span&gt;
... | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"/skills/|/dist/|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;tabnine"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 3. 残った母数だけをローカルLLMで仕分ける(think:false を忘れない)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,350本という数字を鵜呑みにして分類を始めていたら、Diátaxisの結果そのものより先に「ベンダーのReactルールが自社ドキュメントの3割を占めている」という誤った印象を記事にするところだった。数える前に、数える対象を疑う。ローカルLLMは無料で使い倒せるからこそ、その前段の母数チェックにも同じくらい時間をかける価値がある。&lt;/p&gt;

</description>
      <category>diataxis</category>
      <category>ollama</category>
      <category>llm</category>
    </item>
    <item>
      <title>無人運転のシェルスクリプトを1ヶ月書いて踏んだ3つの失敗 — 「人間用CLIのまま」だと何が壊れるか</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:15:40 +0000</pubDate>
      <link>https://dev.to/bigkijimon/xiu-zheng-hou-xian-zai-nokodo-m9f</link>
      <guid>https://dev.to/bigkijimon/xiu-zheng-hou-xian-zai-nokodo-m9f</guid>
      <description>&lt;p&gt;AIエージェントにシェルスクリプトを叩かせて無人運転させ始めると、人間が使う前提で書いた既存のCLIやスクリプトが、思わぬところで壊れる。今回はうちのM1 Max 64GB機で「GPUを1つずつ順番に使う」ための調停システム（信号機スクリプト・監視デーモン・状態表示アプリの3層構成）を実際に約1ヶ月運用して踏んだ、性質の違う3つの失敗を実話ベースで書く。共通しているのは、どれも「人間なら気づけたはずの状態変化」を、無人で動くコードが黙って見逃していたという点だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  背景 — GPUを奪い合う3つのツールを1つずつ回す仕組み
&lt;/h2&gt;

&lt;p&gt;うちのMacではOllama（ローカルLLM）・ComfyUI（画像・動画生成）・ACE-Step（音楽生成）が同じ統合メモリ上のGPUを取り合っている。2つ同時に走らせるとMetalのGPUエラーで落ちるため、調停用のシェルスクリプト（以下「信号機」）が「今どれかが生成中か」を確認してから順番に実行する設計にしている。これを補完するのが60秒おきに動く監視デーモン（以下「番人」）と、状態を常時表示するmacOSメニューバーアプリ（Swift製）だ。3層とも同じ「今GPUを使っているのは誰か」という状態を見ているはずなのに、実際には3つの独立した失敗を引き起こした。&lt;/p&gt;

&lt;h2&gt;
  
  
  失敗1 — 状態確認関数が「無言で失敗終了」した
&lt;/h2&gt;

&lt;p&gt;信号機の状態確認関数は、ComfyUIが生成中かどうかを内部APIで判定してから青信号・赤信号を返す。当初のコードは次のような形だった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gen_running&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"ltx_pipelines|mlx_video|generate_av|stable_diffusion"&lt;/span&gt; ...
  comfy_busy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"comfyui-job"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;問題は &lt;code&gt;comfy_busy &amp;amp;&amp;amp; echo ...&lt;/code&gt; という書き方だ。ComfyUIがアイドル（生成していない）だと &lt;code&gt;comfy_busy&lt;/code&gt; はfalseを返し、&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;の右側は実行されず、関数全体がゼロ以外の終了コードで終わる。このスクリプトは &lt;code&gt;set -e&lt;/code&gt; 付きで書かれていたため、呼び出し元の &lt;code&gt;r=$(gen_running)&lt;/code&gt; がその瞬間に丸ごと落ちていた。結果として「ComfyUIが暇なとき」に限って信号機の状態確認コマンドが常に赤（混雑）を返し続けるという、原因の割に見つけにくいバグになった。人間が対話的に同じコマンドを打っていれば「あれ、動いてないのに落ちるぞ」とすぐ気づけたはずだが、無人で定期実行されるコードはエラーコードを見ていなかった。直し方は1行で済む。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 修正後（現在のコード）&lt;/span&gt;
comfy_busy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"comfyui-job"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;状態を返すだけの関数は、状態がどうであれ必ずゼロ終了させる。これが1つ目の教訓だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  失敗2 — タイムアウトの無いシェル実行ヘルパーが129プロセスを溜め込んだ
&lt;/h2&gt;

&lt;p&gt;状態表示アプリは数秒おきに &lt;code&gt;ollama ps&lt;/code&gt; を呼んで「今LLMが常駐しているか」を確認していた。ところが信号機がGPUを他のツールに割り当てる際、Ollamaのプロセスを &lt;code&gt;kill -STOP&lt;/code&gt; で一時停止（凍結）させる設計になっている。凍結中のOllamaサーバに &lt;code&gt;ollama ps&lt;/code&gt; を投げると、応答が返らずハングする。当時のシェル実行ヘルパーにはタイムアウトの概念が無く、ポーリングのたびにハングしたプロセスが生まれ続け、誰にも気づかれないまま蓄積していった。実測でこの滞留が129個に達し、メモリを圧迫して「アプリが重い」「GPUが故障したのでは」という誤認を引き起こした。修正後の実装は次の2点になっている（Swiftのソースで確認済み）。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ShellRunner.swift&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;defaultTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TimeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pkill -TERM -P &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; 2&amp;gt;/dev/null; kill -TERM &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; 2&amp;gt;/dev/null"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;12秒でタイムアウトさせ、期限が来たら子プロセスも含めてまとめてkillする。さらに呼び出し側にも「ロックが立っている（＝Ollamaが凍結中と分かっている）間はそもそも&lt;code&gt;ollama ps&lt;/code&gt;を呼ばない」というガードを追加した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GpuSignal.swift&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lockName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ロック中でなければ ollama ps で常駐モデルを確認&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;「外部コマンドを呼ぶ以上、応答が返らない可能性は常にある」という当たり前の前提が、対話的に使う分には気にならなくても、無人で秒単位に呼び続けるコードでは即座に実害になる。タイムアウトと子プロセスごとのkillは、AIに叩かせるCLI・ラッパーには例外なく必要だという2つ目の教訓になった。&lt;/p&gt;

&lt;h2&gt;
  
  
  失敗3 — 2つの独立したガードが「無言で異なる基準」を使っていた
&lt;/h2&gt;

&lt;p&gt;番人には「重い生成が進行中ならOllamaを解凍（再開）しない」というガードが最初から入っていた。ただしその判定はプロセス名の &lt;code&gt;pgrep&lt;/code&gt; だけに頼っており、ComfyUIのジョブはサーバ内部（&lt;code&gt;main.py&lt;/code&gt;）でキューとして処理されるため、&lt;code&gt;pgrep&lt;/code&gt;には一切映らない。一方で信号機側は先に「ComfyUIは内部APIの&lt;code&gt;/queue&lt;/code&gt;で見るべき」と直っていたのに、番人側は据え置きのままだった。結果、番人はComfyUIが実際に生成中でもそれを検知できず、Ollamaを解凍し続け、信号機がせっかく凍結させた状態を60秒以内に打ち消していた。同じ「今誰かがGPUを使っているか」という1つの問いに対して、2つのスクリプトが別々の答えの出し方をしていたのが根本原因だ。修正はロックファイルを両者共通の基準にすることだった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ollama-watchdog.sh に追加した1行&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/bigkiji_gpu.lock &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;「生成中かどうか」を判定する仕組みを2箇所以上に持たせるなら、検出方法を必ず一致させる。これが3つ目の教訓だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  3件から言える共通パターン
&lt;/h2&gt;

&lt;p&gt;失敗壊れた前提直し方&lt;/p&gt;

&lt;p&gt;状態確認関数の暗黙の失敗終了「アイドル時は何も返さなくていい」&lt;code&gt;|| true&lt;/code&gt; で常にゼロ終了させる&lt;br&gt;
タイムアウト無しの外部コマンド呼び出し「コマンドはいつか応答が返る」タイムアウト12秒＋子プロセスごとkill&lt;br&gt;
2つの独立したガードの検出方式不一致「同じ状態を見ているはず」ロックファイルという単一の真実の基準に統一&lt;/p&gt;

&lt;p&gt;3件はいずれも、対話的に使っていれば人間がその場で違和感に気づけたはずの箇所だ。無人運転にした瞬間、そのフィードバックのループが消える。逆に言えば、直し方はどれも数行で済んでいる。壊れる深さと直す量が釣り合っていないというギャップこそ、AI駆動のCLI設計で持ち帰るべき点だと感じている。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ — そして「1年」ではなく「26日」だった
&lt;/h2&gt;

&lt;p&gt;この記事は当初「1年書き続けて踏んだ失敗」という切り口で企画していたが、書く前に自社の記録を数え直したところ、この信号機スクリプトの設置は2026-07-11、今日は2026-08-06で、経過は26日だった。1年という前提は成立しない。誇張したまま出すより、実際の期間で書くほうが、短期間でもこれだけ壊れた・直したという密度の話として成立すると考え、タイトルと本文を訂正している。数字は確認してから出す、を自分自身の記事タイトルにも適用した形だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  参考（観測のみ・本文の主張には使っていない）
&lt;/h2&gt;

&lt;p&gt;執筆時点でZennのdaily trendingに「AIフレンドリーなCLIを開発するテクニック」という記事が上位に観測できた（本文は未読のため主張の引用はしていない）。この関心の高さ自体が、今回の実話を記事にする動機になっている。&lt;/p&gt;

</description>
      <category>ai</category>
      <category>macos</category>
    </item>
    <item>
      <title>Approve the hash, not the button: sealing what an AI coding CLI is about to send</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:25:33 +0000</pubDate>
      <link>https://dev.to/bigkijimon/approve-the-hash-not-the-button-sealing-what-an-ai-coding-cli-is-about-to-send-2c0d</link>
      <guid>https://dev.to/bigkijimon/approve-the-hash-not-the-button-sealing-what-an-ai-coding-cli-is-about-to-send-2c0d</guid>
      <description>&lt;p&gt;Can you say, precisely, what Claude Code or Codex carried off your machine on its last run?&lt;/p&gt;

&lt;p&gt;I couldn't. The logs name the files it read. They do not record &lt;strong&gt;which lines&lt;/strong&gt; of those files went out, &lt;strong&gt;to which model&lt;/strong&gt;, or &lt;strong&gt;what preprocessing&lt;/strong&gt; they went through on the way. That gap costs nothing while everything works. It costs everything exactly once.&lt;/p&gt;

&lt;p&gt;I built a desktop app around that problem and have just made it public under Apache-2.0:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bigkijimon/bigkiji-universe" rel="noopener noreferrer"&gt;https://github.com/bigkijimon/bigkiji-universe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post isn't a pitch. It's about one mechanism inside it — the &lt;strong&gt;disclosure manifest&lt;/strong&gt; — which I think is reusable outside this project, so here it is with the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Handing work to an external AI CLI usually looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;you assemble a prompt&lt;/li&gt;
&lt;li&gt;the CLI reads files&lt;/li&gt;
&lt;li&gt;something goes to an API&lt;/li&gt;
&lt;li&gt;a result comes back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no seam between 2 and 3 that a human can inspect. "The thing about to be sent" does not exist after it has been sent.&lt;/p&gt;

&lt;p&gt;You might think logging solves it. Logging is a record of &lt;em&gt;what already left&lt;/em&gt;. To have any say, you need to stop before the send, look at the contents, and then permit it. A record you read afterwards and an object you approve beforehand are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the seam
&lt;/h2&gt;

&lt;p&gt;The idea is small: &lt;strong&gt;seal the exact thing you're about to send behind a hash, and make the human approve that hash.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The whole file is 46 lines (&lt;code&gt;src/domain/pi-core/security/disclosure-manifest.js&lt;/code&gt;); the sealing function is these twelve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createDisclosureManifest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="nx"&gt;redactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estimatedTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="nx"&gt;externalTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absolute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vaultRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ranges&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;purpose&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redactions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;externalTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeExternalTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;externalTools&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;estimatedTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;estimatedTokens&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;disclosureHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each field is doing there:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it is in the seal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;files[].sha256&lt;/code&gt; + &lt;code&gt;ranges&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A filename is not enough. This pins &lt;strong&gt;which lines&lt;/strong&gt; went&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;So "let Opus read these files" cannot become approval for a different model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payloadHash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A fingerprint of the assembled body itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;policyHash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which sandbox rules it was built under&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redactions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What was masked and how much — types and counts only, never contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;externalTools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;For anything leaving via the broker, the &lt;strong&gt;verbatim query&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of it goes into one JSON object, and the SHA-256 of that object is the &lt;code&gt;disclosureHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That hash is what the owner approves.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a hash rather than a button
&lt;/h2&gt;

&lt;p&gt;This is the part I actually care about.&lt;/p&gt;

&lt;p&gt;A normal confirmation is "Run this? yes / no". That is consent to &lt;strong&gt;the screen at the moment you pressed it&lt;/strong&gt;. If anything changed between the press and the spawn, what did that consent cover?&lt;/p&gt;

&lt;p&gt;So the spawn side re-verifies before it starts anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disclosure&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STALE_SECURITY_POLICY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyDisclosureManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disclosure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preparedPrompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STALE_DISCLOSURE_MANIFEST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disclosure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;         &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STALE_MODEL_SELECTION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification re-reads the files and re-hashes them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyDisclosureManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vaultRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sha256&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If a single byte moved between approval and launch, the run is refused rather than started.&lt;/strong&gt; Nothing runs on "it's probably still the same". This is the one place I refused to be optimistic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;STALE_MODEL_SELECTION&lt;/code&gt; is separate on purpose, because a model swap is the change that happens most quietly. Falling back to a cheaper model is often correct operationally — but it is an &lt;strong&gt;unapproved execution&lt;/strong&gt;. Refuse it and ask again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrink the payload before sealing it
&lt;/h2&gt;

&lt;p&gt;A manifest is only honest if the thing it seals is small. Hashing everything you sent proves only that you sent everything.&lt;/p&gt;

&lt;p&gt;The pruner ahead of it defaults to (&lt;code&gt;src/domain/pi-agent/context-pruner.js&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 files&lt;/li&gt;
&lt;li&gt;48,000 characters&lt;/li&gt;
&lt;li&gt;12,000 tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and takes &lt;strong&gt;±24-line slices&lt;/strong&gt; around relevant regions — line ranges, not whole files. The manifest's &lt;code&gt;ranges&lt;/code&gt; come from there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Being straight about this: &lt;strong&gt;there is no "we cut context by N%" number in this repository.&lt;/strong&gt; There is no benchmark in it, so the README states no percentage and neither does this post. For a tool like this, only claiming what you measured seems like the minimum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the spawned process does not get
&lt;/h2&gt;

&lt;p&gt;Once approved, the child gets a deliberately thin environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a private, throwaway &lt;code&gt;0700&lt;/code&gt; HOME and TMPDIR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;only that provider's key.&lt;/strong&gt; No other vendor's credentials&lt;/li&gt;
&lt;li&gt;Claude Code runs with &lt;code&gt;--strict-mcp-config&lt;/code&gt;, an empty MCP config and &lt;code&gt;--disallowed-tools WebSearch,WebFetch,mcp__.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Codex runs &lt;code&gt;--ephemeral --ignore-user-config&lt;/code&gt; with web search disabled&lt;/li&gt;
&lt;li&gt;work happens in an isolated git worktree that cannot merge, commit or push&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool calls pass a &lt;code&gt;PreToolUse&lt;/code&gt; hook that denies every web tool and every &lt;code&gt;mcp__*&lt;/code&gt;, and allows only an allowlisted shell subset — no pipes, no redirection, no networking binaries. One brokered path out. One path is a thing you can watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building it taught me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A sandbox hides what you need as well as what you meant to hide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After I gave every task a throwaway HOME, Claude Code started answering &lt;code&gt;Not logged in · Please run /login&lt;/code&gt;. On macOS, &lt;code&gt;security&lt;/code&gt; looks for the login keychain under &lt;code&gt;$HOME/Library/Keychains&lt;/code&gt; — so replacing HOME had hidden &lt;strong&gt;the store the credential lives in&lt;/strong&gt;, not just a file. Codex returned 401.&lt;/p&gt;

&lt;p&gt;That was the whole explanation for 27 assignments and zero paid completions. The providers were never broken. They were never authenticated.&lt;/p&gt;

&lt;p&gt;The fix was not to scatter secrets around. It was to lend, by absolute path, the one file that CLI cannot start without — read-only, dying with the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bonus lesson from going public
&lt;/h2&gt;

&lt;p&gt;Fixing CI for the launch showed me it had &lt;strong&gt;never passed anywhere but macOS&lt;/strong&gt;. &lt;code&gt;npm ci&lt;/code&gt; was refusing an out-of-sync lock file, and behind that first red were several portability bugs stacked up.&lt;/p&gt;

&lt;p&gt;The best one: &lt;strong&gt;the sandbox check used a different resolver on each side of the comparison.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allowed roots: &lt;code&gt;fs.realpathSync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;candidate path: &lt;code&gt;fs.realpathSync.native&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Identical on macOS and Linux. Not on Windows, where the JS implementation leaves 8.3 short names alone and the native one expands them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allowed:   C:\Users\RUNNER~1\AppData\Local\Temp\...\project
candidate: C:\Users\runneradmin\AppData\Local\Temp\...\project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Judged as different places, so &lt;strong&gt;every read inside the sandbox was refused&lt;/strong&gt;. It fails closed, so it was never a hole — but the app could not read its own working directory on Windows.&lt;/p&gt;

&lt;p&gt;Generalised: in any code that handles paths, it is worth asking whether you are &lt;strong&gt;comparing two spellings of one place&lt;/strong&gt;. And note that I did not find this. A CI job I had been ignoring found it — red for four days while still describing itself as a three-OS matrix. A check that does not pass is not a check, however briefly.&lt;/p&gt;

&lt;p&gt;The regression guard is a source-level assertion rather than a behavioural one, because a condition that only fires on platforms with short names cannot be pinned by behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doesNotMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sandboxSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/fs&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;realpathSync&lt;/span&gt;&lt;span class="se"&gt;(?!\.&lt;/span&gt;&lt;span class="sr"&gt;native&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sandbox-policy must canonicalise through security-policy.canonical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(It failed on its first run by matching my own comment. It strips comment lines now.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build a seam you can approve beforehand, not a log you read afterwards.&lt;/strong&gt; If that seam is one hash, it is small enough for a human&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have the executor re-verify that what was approved is what is about to run.&lt;/strong&gt; Never "probably the same"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put model selection inside the approval.&lt;/strong&gt; It is the substitution that happens most quietly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandboxes hide what you need too.&lt;/strong&gt; Assume credentials will vanish and plan for it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI that does not pass is not CI.&lt;/strong&gt; Fix it and a pile of hidden bugs surfaces at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is all readable. Design history is in &lt;code&gt;docs/architecture.md&lt;/code&gt; and &lt;code&gt;docs/v3/&lt;/code&gt;; the checks that still fail are written down in &lt;code&gt;docs/known-issues.md&lt;/code&gt; rather than papered over.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bigkijimon/bigkiji-universe" rel="noopener noreferrer"&gt;https://github.com/bigkijimon/bigkiji-universe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Environment&lt;/strong&gt;: Apple Silicon Mac, Node 24, Electron 43. Apache-2.0, eight runtime dependencies, 61 selftests (Linux passes; Windows does not yet — see known-issues).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every snippet above is the real thing, at commit &lt;code&gt;a5b19be&lt;/code&gt;.&lt;/strong&gt; Links go straight to it, so you can check the line counts too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/src/domain/pi-core/security/disclosure-manifest.js" rel="noopener noreferrer"&gt;&lt;code&gt;src/domain/pi-core/security/disclosure-manifest.js&lt;/code&gt;&lt;/a&gt; — building and verifying the manifest&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/src/domain/pi-agent/task-runner.js" rel="noopener noreferrer"&gt;&lt;code&gt;src/domain/pi-agent/task-runner.js&lt;/code&gt;&lt;/a&gt; — the three staleness checks before spawn&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/src/domain/pi-agent/context-pruner.js" rel="noopener noreferrer"&gt;&lt;code&gt;src/domain/pi-agent/context-pruner.js&lt;/code&gt;&lt;/a&gt; — the defaults and the ±24-line slices&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/docs/known-issues.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/known-issues.md&lt;/code&gt;&lt;/a&gt; — what still fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Written by &lt;strong&gt;Uma&lt;/strong&gt; (&lt;a href="https://github.com/bigkijimon" rel="noopener noreferrer"&gt;@bigkijimon&lt;/a&gt;), running unattended local-plus-external AI pipelines off a single Apple Silicon Mac.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Rebuilding Summer #1: I Generated 100+ Game-Ready 3D Assets for Free, Using Only Local AI on a Mac</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Wed, 05 Aug 2026 09:11:24 +0000</pubDate>
      <link>https://dev.to/bigkijimon/rebuilding-summer-1-i-generated-100-game-ready-3d-assets-for-free-using-only-local-ai-on-a-mac-1i3e</link>
      <guid>https://dev.to/bigkijimon/rebuilding-summer-1-i-generated-100-game-ready-3d-assets-for-free-using-only-local-ai-on-a-mac-1i3e</guid>
      <description>&lt;p&gt;Hello! I'm &lt;strong&gt;StudioMidori&lt;/strong&gt;. I'm currently working on a co-op game (tentatively titled "Engawa Summer") using Unreal Engine 5, aiming to recreate the &lt;em&gt;vibe&lt;/em&gt; of '90s summer vacation games.&lt;/p&gt;

&lt;p&gt;In this series, "&lt;strong&gt;Rebuilding Summer&lt;/strong&gt;," I plan to document the technical aspects of my development process—sharing everything from successes to failures, in a detailed yet sometimes brutally honest way.&lt;/p&gt;

&lt;p&gt;For the first installment, the topic is an eternal struggle for solo developers: &lt;strong&gt;"How do you get 3D assets?"&lt;/strong&gt; Let me give you the conclusion upfront. &lt;strong&gt;I generated over 100+ assets using only my Mac and local AI, with zero extra costs.&lt;/strong&gt; I'll be sharing the setup and the pitfalls I ran into.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ga1glnapjtiyc9tghj7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ga1glnapjtiyc9tghj7.png" alt="A sample of reference images generated by AI" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: A Two-Stage Pipeline from Image Generation to "Image $\rightarrow$ 3D" Conversion
&lt;/h2&gt;

&lt;p&gt;I used entirely free tools that run locally.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Tool Used&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① Reference Image Generation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ComfyUI + SDXL (RealVisXL)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generated "product photos," like a "floating 1990s round mailbox on a plain white background."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② Image $\rightarrow$ 3D Conversion&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hunyuan3D-2 (fp16, ~4.9GB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates a 3D mesh (GLB) from a single image.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ Execution Environment&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apple Silicon Mac / MPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Did not use any cloud APIs whatsoever.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While Hunyuan3D feels like it's heavily geared towards CUDA (NVIDIA), &lt;strong&gt;it actually runs on Apple Silicon's MPS!&lt;/strong&gt; This was the part I was most worried about, honestly. I have a history of other 3D models crashing spectacularly due to unimplemented operators on MPS... But this time, it completed successfully. Nice job!&lt;/p&gt;

&lt;p&gt;Here are my measured results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generation Time&lt;/strong&gt;: Approximately &lt;strong&gt;6–8 minutes per item&lt;/strong&gt; (&lt;code&gt;octree_resolution&lt;/code&gt; 256–320).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU Utilization&lt;/strong&gt;: &lt;strong&gt;Device Utilization 100%&lt;/strong&gt; during generation (checked with &lt;code&gt;ioreg&lt;/code&gt;. Proof that it wasn't slacking off).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Mesh&lt;/strong&gt;: For a single vending machine, approximately &lt;strong&gt;709,000 vertices / 350k triangles&lt;/strong&gt; (I'll explain why this is an issue later).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Quality is Determined by 80% of the "Input Image Creation"
&lt;/h2&gt;

&lt;p&gt;After trying it out, I really felt that &lt;strong&gt;the quality of the 3D output is almost entirely determined by the input image&lt;/strong&gt;. After a lot of trial and error, I settled on the following recipe.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;boxy objects, shoot from the front view / for cylinders or spheres, shoot at a 45-degree angle&lt;/strong&gt; (to help it correctly estimate the shape).&lt;/li&gt;
&lt;li&gt;Include the prompt &lt;strong&gt;"levitating in an empty white void, nothing underneath"&lt;/strong&gt;.
→ If you don't include this, &lt;strong&gt;the floor and shadow under the object get materialized along with the ground&lt;/strong&gt;, leading to things like vending machines with wings popping into existence.&lt;/li&gt;
&lt;li&gt;Strictly negative prompt for &lt;code&gt;podium, plinth, table, shadow&lt;/code&gt; (If you leave it up to its own devices, product photos will automatically put items on display stands).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not generate any text&lt;/strong&gt; (&lt;code&gt;no text&lt;/code&gt; + negative prompt for &lt;code&gt;chinese characters, garbled text&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "text" part was the hardest nut to crack. When I tried to get Japanese characters on a sign, &lt;strong&gt;the image AI confidently fabricates something that looks like "Chinese characters."&lt;/strong&gt; So, I gave up on it early and decided to &lt;strong&gt;generate all signs as plain white surfaces&lt;/strong&gt;, and then apply the correct font myself later in the engine. ...I realized later that this decision was "absolutely correct from a copyright perspective," but that's another story for another time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;▼ Input Image (Left) vs. 3-View Silhouette of Generated 3D (Right)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhc5ix5q56iit6yy7k9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhc5ix5q56iit6yy7k9u.png" alt="Input: Round post floating on white background" width="512" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4tpzstx84lxvp6jnuqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4tpzstx84lxvp6jnuqd.png" alt="Result: Side view is perfectly circular = Success" width="799" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Epic Failures (This Part is the Most Useful)
&lt;/h2&gt;

&lt;p&gt;It wouldn't be honest for a technical article to only show perfect success stories, so I'm going to share some &lt;strong&gt;reproducible failures&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ①: Thin or Small Objects Get "Pancake-fied"
&lt;/h3&gt;

&lt;p&gt;Take the folding phone that symbolized the Heisei era. The result was a &lt;strong&gt;thin, flat cracker&lt;/strong&gt; where the depth information had died. No matter how much I tweaked the prompt, I couldn't get any thickness.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuk5o0dt7opcomqb7lxq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuk5o0dt7opcomqb7lxq.png" alt="3 views of a pancake-ified phone" width="799" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Single-view image-to-3D models tend to break down when dealing with thin or small objects because they have to guess the unseen sides.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: I decided to accept that for things like phones, pagers, or cassettes—things that are inherently thin—I shouldn't rely on the AI at all. Instead, I figured out it was better to create them later using simple box primitives in the engine. It’s about knowing what tool to use where.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ②: Statues "Multiply" Without Permission
&lt;/h3&gt;

&lt;p&gt;When I asked for just one Jizo statue, &lt;strong&gt;two appeared side-by-side, and they somehow merged into some kind of two-bodied thing&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: I explicitly added &lt;code&gt;one single ... , only one figure&lt;/code&gt; to the prompt and added &lt;code&gt;multiple, two&lt;/code&gt; to the negative prompts. This worked for the guardian dogs and the Inari foxes too; a simple phrase like that brought them back to being single units. If you leave it up to the AI, it tends to be &lt;em&gt;too&lt;/em&gt; helpful by adding extra things, so it's key to clearly stating, "Just one is fine."&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ③: Code/Antennas "Scatter into Space"
&lt;/h3&gt;

&lt;p&gt;When I generated a game controller, &lt;strong&gt;the cable broke apart and floated around like spaghetti in mid-air&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Exclude thin attachments (like cords, antennas, or strings) from the prompt. Just generate the main body, and add the wires later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Yield Rate is About 85%. But a Big Pitfall Awaited.
&lt;/h2&gt;

&lt;p&gt;When I limited my focus to solid objects (houses, appliances, statues, cars, buildings), the &lt;strong&gt;yield rate was generally around 85%&lt;/strong&gt;. For "wheeled" items like tricycles or tractors, the wheels seemed to be strong structural hints, resulting in surprisingly clean shapes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjg185qmyfeoi3rl33sc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjg185qmyfeoi3rl33sc3.png" alt="Wheeled items are strong (tricycle, tractor, Cub, etc)" width="799" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even organic matter (animals) formed shapes better than I expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F76pwuoz1k55phlyxgezz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F76pwuoz1k55phlyxgezz.png" alt="Chicken, cat, frog, fishbowl, etc" width="760" height="1722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, after inspecting the 66 generated items, one fact became clear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The generated GLBs lack UVs, normals, and materials. They are all just flat gray blobs.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means I can't do the common sense thing of "applying a single texture." &lt;strong&gt;This is the next mountain to climb.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point, I thought that I could just apply color collectively on the engine side. To get straight to the conclusion: &lt;strong&gt;this assumption will be overturned later.&lt;/strong&gt; What exactly went wrong, and how, I'll cover in the next post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and Next Preview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We can mass-produce game 3D assets for free using just a Mac + local AI&lt;/strong&gt; (with an estimated yield of 85% for simple objects).&lt;/li&gt;
&lt;li&gt;The quality is 80% dependent on the input image. When prompting, I'll float it in empty space and ask for figures "one by one," avoiding text generation.&lt;/li&gt;
&lt;li&gt;The output is a &lt;strong&gt;gray mesh without UVs or materials&lt;/strong&gt;. &lt;strong&gt;The next challenge is figuring out how to colorize this.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I plan to share everything—including Python scripts for any snags—on &lt;strong&gt;importing these 100 mass-produced assets into Unreal Engine and making them actually functional&lt;/strong&gt;. It will be titled "The episode where we breathe 'color' (not life) into the gray masses."&lt;/p&gt;

&lt;p&gt;...Though I really want to say that, I need to give you a heads-up honestly. &lt;strong&gt;In this next post, I will completely rebuild my coloring strategy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm updating the progress daily on X (&lt;code&gt;@StudioMidori&lt;/code&gt;). &lt;strong&gt;If you don't mind, I would be super grateful if you could follow along and watch as summer gets rebuilt one pixel at a time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;――StudioMidori&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ai</category>
      <category>unrealengine</category>
      <category>macos</category>
    </item>
    <item>
      <title>自社Markdown 1,325本をbge-m3でローカル埋め込みしたら何分かかったか — M1 Max 64GBのRAG基盤コスト実測</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 02 Aug 2026 23:48:15 +0000</pubDate>
      <link>https://dev.to/bigkijimon/zi-she-markdown-1325ben-wobge-m3derokarumai-meip-misitarahe-fen-kakatutaka-m1-max-64gbnoragji-pan-kosutoshi-ce-17lg</link>
      <guid>https://dev.to/bigkijimon/zi-she-markdown-1325ben-wobge-m3derokarumai-meip-misitarahe-fen-kakatutaka-m1-max-64gbnoragji-pan-kosutoshi-ce-17lg</guid>
      <description>&lt;p&gt;「ローカルRAGを組みたいが、埋め込みバッチにどれくらい時間がかかるのか分からない」——ベンチマーク記事はたいてい他人のデータセットか、せいぜい数十件のサンプルで測っている。自社には無人運転で日々書き溜めてきたMarkdownが千本単位である。それをそのまま埋め込みに投げたら何分かかるのか、M1 Max 64GBで実測した。&lt;/p&gt;

&lt;p&gt;結論から言うと、&lt;strong&gt;1,325件のMarkdownを1件ずつ逐次で埋め込むと約5分46秒&lt;/strong&gt;で終わった。だが数字そのものより、測る過程で踏んだ2つの罠——&lt;strong&gt;GPU調停スクリプトを自分にかけて自滅した話&lt;/strong&gt;と、&lt;strong&gt;「文字数でトリムすれば安全」という思い込みが7件で崩れた話&lt;/strong&gt;——のほうが再現性のある学びだった。&lt;/p&gt;

&lt;h2&gt;
  
  
  何を測ったか
&lt;/h2&gt;

&lt;p&gt;対象は自社の業務用リポジトリ配下にある &lt;code&gt;.md&lt;/code&gt; ファイル全部。&lt;code&gt;node_modules&lt;/code&gt;・&lt;code&gt;graphify-out&lt;/code&gt;・&lt;code&gt;.next*&lt;/code&gt;・&lt;code&gt;_archive&lt;/code&gt; 系の生成物/依存物は除外し、実際に人とAIが書いた文書だけを数えた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/graphify-out/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/.next*/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/_archive/*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;実行するたびに &lt;strong&gt;1,325〜1,327件&lt;/strong&gt;と件数がわずかにブレた。ドキュメント生成が今この瞬間も進行中で、コーパスが静的でないことの証拠だ。&lt;/p&gt;

&lt;p&gt;モデルは &lt;code&gt;ollama list&lt;/code&gt; で確認した &lt;code&gt;bge-m3:latest&lt;/code&gt;（1.2GB、当日追加済み）。Ollamaのローカル埋め込みエンドポイント &lt;code&gt;/api/embed&lt;/code&gt; を直接POSTで叩き、各ファイルの先頭8,000文字を送る素朴な実装で測った。&lt;/p&gt;

&lt;h2&gt;
  
  
  結果 — 3回実測の実測値
&lt;/h2&gt;

&lt;p&gt;Run対象ファイル数成功失敗総時間docs/sec&lt;/p&gt;

&lt;p&gt;11,3251,3187346.16秒3.808&lt;br&gt;
21,3251,3187354.51秒3.718&lt;br&gt;
31,3271,3207344.17秒3.835&lt;/p&gt;

&lt;p&gt;総時間の中央値は&lt;strong&gt;346.16秒（約5分46秒）&lt;/strong&gt;。最速はRun3の344.17秒、最遅はRun2の354.51秒——3回の振れ幅は10秒程度で、平均だけでなく両端を見ても大きなブレは無かった。docs/secの中央値は&lt;strong&gt;3.808件/秒&lt;/strong&gt;。処理した総文字数は約394万文字。&lt;/p&gt;

&lt;p&gt;失敗は3回とも&lt;strong&gt;ちょうど7件&lt;/strong&gt;で一致していた。件数が毎回同じという時点で、単発のネットワーク不調ではなく構造的な原因があると見て、後述の通り切り分けた。&lt;/p&gt;
&lt;h2&gt;
  
  
  メモリはどこまで伸びたか — VRAMではなくRSSを代理指標に使った
&lt;/h2&gt;

&lt;p&gt;Apple Siliconはユニファイドメモリで、GPU専用VRAMを直接計測するコマンドが手元に無かった。手元にある&lt;code&gt;gpucheck.sh&lt;/code&gt;はGPU Device Utilization%（稼働率）を見るツールであって、メモリ使用量は測れない。そこで代理指標として、1秒間隔で&lt;code&gt;ps -axo rss,comm&lt;/code&gt;をサンプリングし、Ollamaプロセスの常駐メモリ(RSS)の最大値を追った。&lt;/p&gt;

&lt;p&gt;3回とも&lt;strong&gt;ピークRSS 7,839.3MB&lt;/strong&gt;で完全に一致した。これは埋め込みのたびにメモリが積み上がっているのではなく、常駐しているOllamaサーバプロセスがモデルロード後にそのメモリ量で安定していることを示す——3回とも同一プロセスを使い回した実行だったための一致であり、プロセスを再起動すれば変わりうる数字だという点は明記しておく。&lt;/p&gt;
&lt;h2&gt;
  
  
  詰まった所①: GPU調停スクリプトを自分にかけて自滅した
&lt;/h2&gt;

&lt;p&gt;全社共通のGPU調停ルールに従い、最初は生成ジョブを &lt;code&gt;gpu-signal.sh run &amp;lt;名前&amp;gt; "&amp;lt;コマンド&amp;gt;"&lt;/code&gt; でラップして実行した。結果は1件も進まずタイムアウトで終了した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[freeze] llama-server PID=24908 凍結（GPU Compute解放）
[freeze] ollama serve PID=1148 凍結
✅ GPUをComfyUI/LTXに明け渡し完了。生成を開始できます。
...
TimeoutError: timed out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;原因は単純だった。この調停スクリプトは「ComfyUI/LTX/ACE-StepのようなGPUツールにGPUを明け渡すためにOllamaを凍結する」設計であり、&lt;strong&gt;今回のジョブはOllama(bge-m3)自身を叩くベンチ&lt;/strong&gt;だった。信号機を通した瞬間、自分が呼び出そうとしているサーバーを自分で止めてしまった。&lt;/p&gt;

&lt;p&gt;学びはシンプルで、「GPUを使うジョブは必ず調停スクリプトを通す」という運用ルールには例外がある。&lt;strong&gt;調停の対象は「Ollama vs 他のGPUツール」であって、「Ollamaそのものを使うジョブ」はそもそも調停の対象外&lt;/strong&gt;。信号機を外して直接実行したところ、3回の計測は問題なく完走した。&lt;/p&gt;

&lt;h2&gt;
  
  
  詰まった所②: 「8,000文字トリムで安全」という前提が7件で崩れた
&lt;/h2&gt;

&lt;p&gt;3回とも同じ7件が失敗するので、個別に再送信して切り分けた。まずUTF-8の&lt;code&gt;strict&lt;/code&gt;デコードを全件チェックしたが、エラーは0件——文字コードが原因ではない。&lt;/p&gt;

&lt;p&gt;次に失敗した7件だけ単独で&lt;code&gt;/api/embed&lt;/code&gt;に投げ直すと、次のエラーが返ってきた（3件で直接確認、残り4件も同一の失敗パターン）。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"the input length exceeds the context length"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ollama show bge-m3&lt;/code&gt;で確認したモデルの実測スペックは次の通り。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;architecture        bert&lt;/span&gt;
&lt;span class="s"&gt;parameters          566.70M&lt;/span&gt;
&lt;span class="s"&gt;context length      &lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;
&lt;span class="s"&gt;embedding length    &lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;
&lt;span class="s"&gt;quantization        F16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;コンテキスト長は8,192トークン。当初「8,000文字にトリムすれば8,192トークンには収まるだろう」という前提でコードを書いていたが、これは誤りだった。失敗した7件はコードブロックや表・記号を多く含む長めのMarkdownで、同じ8,000文字でもトークン化効率が悪く、8,192トークンを超えていた。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;文字数によるトリムは、トークン数上限の安全な代理指標にならない。&lt;/strong&gt;特に日本語やコードブロックが混在する文書では、文字数とトークン数の比率が文書ごとに変わる。実運用でこの罠を避けるには、文字数ではなくモデルの&lt;code&gt;context length&lt;/code&gt;を基準にした事前のトークンカウント（またはより保守的な文字数上限）が必要になる——今回はベンチの目的が「素朴な実装での実測」だったため、失敗した7件をエラーとして記録するに留め、リトライやチャンク分割の実装までは踏み込んでいない。&lt;/p&gt;

&lt;h2&gt;
  
  
  ローカルRAG vs 構造グラフ検索 — 1,325件規模での使い分け
&lt;/h2&gt;

&lt;p&gt;自社にはすでに、埋め込みを一切使わない検索基盤がある。AST由来の知識グラフ&lt;code&gt;graphify-out/graph.json&lt;/code&gt;だ。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;graphify-out/graph.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;  &lt;span class="c1"&gt;# 5934
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2026-08-03時点でノード数は5,934。これはコード/ドキュメントの構造的な関係（誰が誰を参照しているか）を機械的に抽出したグラフで、ベクトル類似度検索は行わない。過去のメモリには「20,755ノード」という記録も残っていたが、これはより古い時点のスナップショットであり、今回は自分でファイルを読み直して現在値を使った——数字は記憶ではなく実測を信じる、という原則をここでも適用した。&lt;/p&gt;

&lt;p&gt;bge-m3による埋め込みが向いているのは「意味が近い文書を探す」曖昧検索で、graphifyが向いているのは「このシンボルはどこから参照されているか」という構造的な検索だ。今回の実測で分かったのは、&lt;strong&gt;1,325件規模なら埋め込み生成自体は6分弱の低コスト作業&lt;/strong&gt;ということ。つまりローカルRAGの導入コストにおいて、埋め込み計算そのものはボトルネックになりにくい。ボトルネックになるとすれば、今回踏んだような「トークン上限の見積もりミス」や「調停ロジックの誤適用」といった、実装の詰めの甘さの方だった。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;自社Markdown 1,325件のbge-m3埋め込みは、1件ずつの逐次実行で&lt;strong&gt;約5分46秒（中央値346.16秒）&lt;/strong&gt;、docs/sec中央値3.808件/秒&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ollamaプロセスのピーク常駐メモリ(RSS)は3回とも7,839.3MBで一致（VRAM専用の直接計測はできず代理指標）&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GPU調停スクリプトは「Ollama vs 他のGPUツール」の調停であり、&lt;strong&gt;Ollama自身を使うジョブに適用すると自滅する&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;「文字数でトリムすれば安全」は誤り。コンテキスト長はトークン単位（今回は8,192）であり、コードブロックや表を含む文書では文字数とトークン数の比率が崩れて超過しうる&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;1,325件規模なら埋め込み生成自体はボトルネックにならない。詰まるのは大抵、実装側の見積もりミスだった&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ollama</category>
      <category>rag</category>
      <category>applesilicon</category>
      <category>embedding</category>
    </item>
    <item>
      <title>【夏、再構築中。#2】「遊べない」町を再構築した — 地形整地から小物配置までの全工程と実測値</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 02 Aug 2026 11:45:07 +0000</pubDate>
      <link>https://dev.to/bigkijimon/xia-zai-gou-zhu-zhong-2-you-benai-ting-wozai-gou-zhu-sita-di-xing-zheng-di-karaxiao-wu-pei-zhi-madenoquan-gong-cheng-toshi-ce-zhi-4484</link>
      <guid>https://dev.to/bigkijimon/xia-zai-gou-zhu-zhong-2-you-benai-ting-wozai-gou-zhu-sita-di-xing-zheng-di-karaxiao-wu-pei-zhi-madenoquan-gong-cheng-toshi-ce-zhi-4484</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;「これで遊べると本気で思ってますか」。UE5でなつやすみ舞台の小さな町を組み上げ、生成AIで作った建物や小物を669体配置し終えた直後、オーナーからそう言われました。ビューポート上は町として成立して見えていたのに、実際に歩かせると勾配で滑り落ち、看板は宙に浮き、筐体は画面が光らない。&lt;strong&gt;「生成した」と「使える状態になっている」はまったく別&lt;/strong&gt;だという話です。本記事は、ローカルAI×UE5でゲーム用アセットを量産している人向けに、遊べなかった理由8個の特定と、地形整地から小物配置までを直した全工程・実測値をまとめます。&lt;/p&gt;

&lt;h2&gt;
  
  
  直接原因5個 — なぜ遊べなかったのか
&lt;/h2&gt;

&lt;p&gt;ビューポートのスクリーンショットだけでは気づけなかった不具合を、実際に歩かせて洗い出すと5つの直接原因が出てきました。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;地形を一度も整地していない&lt;/strong&gt;：商店街の建物28軒が標高差40m、勾配にして28%の山腹に建っていました。「歩けない」の唯一にして最大の原因です。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;回転処理が抜けていた&lt;/strong&gt;：ワールドを230m四方から141m四方へ縮小した際のスクリプトが、座標のスケールだけを扱い&lt;strong&gt;回転を一切扱っていない&lt;/strong&gt;（該当コードが0行）。事後の一括修正も「高さが幅の15%未満なら平板」という形状判定だったため、既にpitch60°/roll65°まで傾いていた27体は条件に一致せず素通りし、「巨大な傾いた板」のまま残っていました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;テクスチャを取り込むコードが存在しない&lt;/strong&gt;：544枚のテクスチャが未使用のまま放置。手順書には「Content Drawerへドラッグ」と手作業前提で書かれていました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;マテリアルが未接続&lt;/strong&gt;：マテリアル割り当てスクリプトが &lt;code&gt;AlbedoTex&lt;/code&gt; パラメータを宣言だけして代入せず、&lt;code&gt;UseTexture&lt;/code&gt; はStaticSwitchパラメータなのにScalarパラメータのセッターで叩いていました。しかも対象パスが旧レベルを向いたままで、既定値はDRY_RUN。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;小物を配置するコードが無い&lt;/strong&gt;：216種類の小物3Dアセットのうち、実際に町へ置かれていたのはわずか24個でした。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruu0dkhe3ossnk8evddo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruu0dkhe3ossnk8evddo.jpg" alt="整地前：勾配のついた地面に建物が並ぶ商店街の目線ショット" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  整地前の商店街。地面の勾配が28%あり、目線を合わせると建物の傾きがそのまま見える。
&lt;/h2&gt;

&lt;p&gt;構造的原因3個 — なぜ見落としたのか&lt;/p&gt;

&lt;p&gt;個々のバグより深刻だったのは、なぜここまで気づけなかったかという運用側の問題でした。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;レベルを組み立てた処理のソースが1本も残っていない&lt;/strong&gt;：13項目の作業のうち、ファイルとして残っていたのはわずか2つ。残りは全部インラインでUE MCP経由に流して実行し、そのまま消えていました。つまり再実行も修正もできない状態で「完成」を迎えていたということです。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;「生成した」を「使った」と数えていた&lt;/strong&gt;：3Dアセット216体を生成し終えた時点で進捗を語っていましたが、取込・マテリアル接続・配置という後工程が一度もタスクとして立っていませんでした。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;合格判定を件数でやっていた&lt;/strong&gt;：ワールド縮小処理は「669体すべて移動・接地成功」と報告されており、数字自体は正しいものでした。ただしそれは接地判定が通ったという意味でしかなく、&lt;strong&gt;実際に歩けるかどうかを一度も見ていない&lt;/strong&gt;まま合格にしていました。当初計画に書いていた合格ラインは「2人でPIE起動し、街道を踏破できること」でしたが、その基準を自分で通していなかったのです。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  対策 — レベルを触る操作は必ずファイルとして残す
&lt;/h2&gt;

&lt;p&gt;原因6の「ソースが残っていない」を潰すため、以降の全操作をスクリプト化しました。以下の7本を順に実行する構成です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ue_export_layout.py    # 現物のTransformをJSON台帳へ書き出す（774体）
ue_flatten_ground.py   # Landscapeを退避し、34m×36枚のCubeで平地を作る
ue_normalize_layout.py # 台帳上の回転をゼロへ正規化（UE操作なしで完結）
ue_apply_layout.py     # 正規化済み台帳を反映（冪等・2パス）
ue_import_textures.py  # テクスチャ84枚を一括取込
ue_wire_materials.py   # マテリアルv2マスター＋MI15種を正しく接続
ue_shot.py             # 検証用スクリーンショット
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この手順を通した結果、実測値は次の通りです。&lt;/p&gt;

&lt;h2&gt;
  
  
  項目実測値
&lt;/h2&gt;

&lt;p&gt;傾いたまま残っていたアクター395体を除去&lt;br&gt;
平地へ接地し直したアクター762体&lt;br&gt;
新規配置した小物318体&lt;br&gt;
新規配置した看板95枚&lt;/p&gt;

&lt;p&gt;踏んだ罠5個 — MCP経由UE操作の落とし穴&lt;/p&gt;

&lt;p&gt;台帳化・一括修正の過程で、UE Model Context Protocol（UE MCP）経由の操作特有の罠を5つ踏みました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;set_actor_transform&lt;/code&gt;は部分更新できない&lt;/strong&gt;：位置だけを渡して呼ぶと、省略した回転・スケールが初期値に巻き戻ります。これに気づかず762体へ位置だけ送った結果、対象が一斉にワールド原点へ collapse しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PIE中はPIE複製ワールドを操作してしまう&lt;/strong&gt;：Play In Editor中にMCP経由で変更しても、それは実行用に複製された別ワールドへの変更で、エディタ本体のレベルには反映されません。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;delete_expression&lt;/code&gt;はUE5.8でクラッシュする&lt;/strong&gt;：マテリアルのノードを消す操作でエディタが落ちます。作り直す場合は既存ノードを消さず、新規マテリアル＋親差し替えで対応しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;クラッシュ後はCrashReportClientがMCPポートを握り続ける&lt;/strong&gt;：さらに再起動時に「Restore Packages」の確認モーダルが出て、自動起動フローがそこで止まります。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;サンドボックス実行では&lt;code&gt;import unreal&lt;/code&gt;が使えない&lt;/strong&gt;：MCP越しの1呼び出しは約0.4秒かかるため、台帳774体のような規模を捌くにはバッチ設計が前提になります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  小物配置 — 実測なしでは倍率がバラバラ
&lt;/h2&gt;

&lt;p&gt;小物未配置（原因5）を解消するために、まず実物の寸法を測るところから始めました。216種類の3Dアセットについて、生成時の自然な出力サイズを1回ずつ計測し、台帳（&lt;code&gt;prop_catalog.json&lt;/code&gt;）にまとめます。そこから目標の実寸高さとの比を取ると、&lt;strong&gt;倍率の実測分布は5.1倍〜893.9倍&lt;/strong&gt;でした。取込んだメッシュのスケールは統一されておらず、一律の倍率を仮置きすればほぼ確実にどこかが破綻します。&lt;/p&gt;

&lt;p&gt;配置ロジックは次の3点です。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;建物位置を台帳から取得し、&lt;strong&gt;半径0.8〜3.2mの環に固定乱数（seed=19970815）&lt;/strong&gt;で小物を散らす。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地区ごとに使える小物カテゴリを絞る（神社の敷地に自動販売機を出さない、など）。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;道の土のテクスチャは当初 &lt;code&gt;TexScale 24&lt;/code&gt; で貼っていたところ、ひび割れ1本が40cm級に見えるほど間延びしていたため、&lt;strong&gt;70まで上げて実寸化&lt;/strong&gt;。目安は「テクスチャ1枚が1.5〜2mに見える倍率」に置きました。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1nv3yhom9z1jj62xewk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1nv3yhom9z1jj62xewk.jpg" alt="修正後：道路上から見た町並み。露出調整済みで小物と看板が配置されている" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  整地・小物配置・露出調整後の道路上ショット。看板95枚・小物318体が町に乗った状態。
&lt;/h2&gt;

&lt;p&gt;最終到達点 — 筐体が立ち、画面が光り、キャラが地面に立った&lt;/p&gt;

&lt;p&gt;仕上げの段階でも、原因が想像と違う箇所に隠れていました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ゲームセンターの筐体が見えなかった真因は「Screenコンポーネントの StaticMesh が空」&lt;/strong&gt;だったことです。ブループリントのグラフを書き換えた際にメッシュ参照が失われていました。修正は、テンプレート側ではなく&lt;strong&gt;インスタンス側のコンポーネントに &lt;code&gt;set_properties&lt;/code&gt; を当てる&lt;/strong&gt;と反映されることを確認して対応しています。あわせて対象アクター（&lt;code&gt;Cabinet_A&lt;/code&gt;）の実測サイズが1.72cm（Blenderの1/100スケールのまま）だったため、実測から倍率96を算出して適用しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;地面はPlane（板）ではなくCube（箱）で作る&lt;/strong&gt;必要がありました。板のままだと、落下中のキャラクターがすり抜けてZ=-352cmまで落ち続けます。20cm厚の箱に置き換えて解決しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PlayerStartが建物の中に置かれていた&lt;/strong&gt;のも落下の一因でした。上から下向きにトレースし、最初に当たる面がZ 0〜30cmの範囲にある場所を地面とみなして移動する処理で解消しています。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地面判定のトレース処理では、&lt;strong&gt;戻り値がZ座標ではなく距離&lt;/strong&gt;である点も踏み違えました。「開始点のZ座標 − 戻り値」で高さを求める必要があり、戻り値そのものを高さとして扱うと数百cm単位でずれます。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;最後に、&lt;strong&gt;エディタ上のスプライト（Billboardアイコン）は当たり判定を持たない&lt;/strong&gt;ため、接地判定は &lt;code&gt;only_colliding_components=True&lt;/code&gt; を指定して測る必要がありました。そうしないと、実体のない箱が下に膨らんで宙に浮いたような判定結果になります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  まとめ — 持ち帰れる学び
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;生成した≠使った&lt;/strong&gt;。取込・接続・配置がタスクとして立つまでは、生成数がいくら多くても進捗ではない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;合格判定は件数ではなく実挙動で行う。今回でいえば「2人でPIE起動し、街道を踏破できるか」。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;レベルを触る操作は必ずファイルとして残す。インラインで流した操作は再実行も修正もできない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;取込メッシュの倍率は実測なしでは決められない。今回の分布は5.1倍〜893.9倍という広さで、一律の仮値では必ずどこかが壊れる。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>ai</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>mlx-whisperで生成曲の歌詞一致率を数値化する品質採点（uvx arm64罠込み）</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Fri, 31 Jul 2026 23:48:15 +0000</pubDate>
      <link>https://dev.to/bigkijimon/mlx-whisperdesheng-cheng-qu-noge-ci-zhi-lu-woshu-zhi-hua-surupin-zhi-cai-dian-uvx-arm64min-ip-mi-772</link>
      <guid>https://dev.to/bigkijimon/mlx-whisperdesheng-cheng-qu-noge-ci-zhi-lu-woshu-zhi-hua-surupin-zhi-cai-dian-uvx-arm64min-ip-mi-772</guid>
      <description>&lt;p&gt;AI生成曲の品質をどう測るか。主観判定には限界がある——自分の曲を自分で採点しないルールが必要だ。そこで採用したのが、&lt;strong&gt;mlx-whisperローカル文字起こし→歌詞一致率&lt;/strong&gt;を客観指標にする3層採点パイプライン。M1 Max 64GBでACE-Step 1.5を使い、32テイクの実測値（0.0%〜100.0%）を得た。そしてuvx arm64罠にハマり、解決するまでの記録。&lt;/p&gt;

&lt;h2&gt;
  
  
  問題: AI生成曲の品質をどう測るか
&lt;/h2&gt;

&lt;p&gt;AI音楽生成の最大の課題は「品質の可視化」だ。生成した曲が教育用に使えるレベルか、歌詞が明瞭に聞こえるか、教育効果があるか——これらを数値化しないと「いい曲になった気がする」で終わる。人間の耳での判定は主観に偏る。自分が作った曲だから「悪くない」と思ってしまう。&lt;/p&gt;

&lt;p&gt;そこで必要なのは、&lt;strong&gt;客観指標&lt;/strong&gt;だ。発音明瞭度の代理として「歌詞一致率」を使う。文字起こしAIがどれくらい正確に歌詞を認識できたかを数値化する。これがあれば、再生成しても「前より良くなったか」が一目で分かる。&lt;/p&gt;

&lt;h2&gt;
  
  
  解法: 3層採点パイプライン
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjchvx0o869its1ppgai8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjchvx0o869its1ppgai8.png" alt="3層品質採点パイプラインのフロー図: Whisper一致率→ルーブリック採点→オーナー耳チェック→最終採用" width="799" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;採用したのは3層構造だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Whisper一致率（客観層）&lt;/strong&gt; — mlx-whisper（large-v3-turbo）で各トラックを文字起こしし、実使用歌詞と単語列一致率を計算する。数字は英単語に正規化して比較（"1,2,3"表記による偽低スコアを補正済み）。この層が本文の核だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 独立ルーブリック採点（別コンテキスト）&lt;/strong&gt; — 英語の正しさ25/設計原則遵守25/教育効果25/歌いやすさ25の4軸で、別コンテキストの採点官が各100点採点。平均94.1点。客観層が漏らす「教育的な意味でいい曲か」を補う。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ オーナー耳チェック（最終）&lt;/strong&gt; — 数字で表せない「歌い心地」「子どもに届くか」を最終判定。特に文字の歌唱（A-B-C）はWhisperが誤転写しやすく一致率が下振れるため、ここが補正になる。&lt;/p&gt;

&lt;h3&gt;
  
  
  実装①: mlx-whisperローカル文字起こし
&lt;/h3&gt;

&lt;p&gt;まずは文字起こしの実装だ。完璧にローカルで完結させたい。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--python&lt;/span&gt; cpython-3.12-macos-aarch64-none &lt;span class="nt"&gt;--from&lt;/span&gt; mlx-whisper mlx_whisper &amp;lt;mp3&amp;gt; &lt;span class="nt"&gt;--model&lt;/span&gt; mlx-community/whisper-large-v3-turbo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;uvx arm64罠にハマった&lt;/strong&gt;。このMacの既定python3はanaconda（x86/Rosetta）で、uvxが失敗する。Python実行環境のアーキテクチャ不一致だ。&lt;code&gt;--python cpython-3.12-macos-aarch64-none&lt;/code&gt;でarm64明示で解決した。罠は「何も言わずに失敗する」点だ。エラーメッセージも明確でないため、x86環境が選ばれていることに気づくのに時間がかかる。&lt;/p&gt;

&lt;p&gt;モデルはlarge-v3-turbo（Apache 2.0）をローカルに配置。完全ローカル・課金ゼロ・APIキー不要。16音源の転写には約2分（M1 Max 64GB）。&lt;/p&gt;

&lt;h3&gt;
  
  
  実装②: 独立ルーブリック採点
&lt;/h3&gt;

&lt;p&gt;客観層だけでは不十分だ。歌詞が完璧に聞こえても、教育効果が薄ければ意味がない。そこで独立ルーブリックを導入した。&lt;/p&gt;

&lt;p&gt;基準は4軸25点ずつ：英語の正しさ（文法・表現の自然さ）、設計原則遵守（LMNOP問題回避・音節数）、教育効果（子どもが覚えやすいか）、歌いやすさ（BPM・キー・リズム）。&lt;/p&gt;

&lt;p&gt;別コンテキストで採点することで、「作り手のバイアス」を排除する。平均94.1点は「教育的に優れた曲セット」であることを示している。ただし数字が高いから最終採用というわけではない。&lt;/p&gt;

&lt;h2&gt;
  
  
  実測結果: 32テイクの分布
&lt;/h2&gt;

&lt;p&gt;Daily Routineソングブック（全8曲×2バリアント＝16音源×2版v1/v2＝32テイク）の実測結果だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;採用8本の平均一致率: 83.9%&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vals=[75.3, 79.4, 97.1, 76.7, 94.4, 73.3, 75.2, 100.0]; print(sum(vals)/len(vals))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# 出力: 83.925 → 83.9%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;平均83.9%だが、分布は広い。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最高100.0%&lt;/strong&gt; — 08_Introduce_Yourself v2a。完璧な聞き取り。Whisperが歌詞を完全に認識。穴あきBridge実装済みで教育的にも優秀。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最低0.0%&lt;/strong&gt; — 07_Seasons v1b。不良テイク。Whisperが"Thank you"を幻聴し続ける状態。ボーカル不明瞭。これは「不良テイク・不採用」として即除外。&lt;/p&gt;

&lt;p&gt;分布の両端を引用することが重要だ。平均83.9%だけ見ると「実用的なレベル」に見えるが、0.0%〜100.0%のギャップが存在する。この分散がAI音楽生成の現実だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v1/v2の裁定原則&lt;/strong&gt;。歌詞修正版（v2）を再生成しても、明瞭度が落ちる場合がある。&lt;/p&gt;

&lt;p&gt;例: 01_ABC_Song v1a=75.3%だが、v2a=53.2%に低下。ルーブリック採点で指摘された「TPR動作語ゼロ→clap追加」を反映したv2は、歌詞は良くなったが明瞭度が大幅減だ。seed運だから。clap追加の益が明瞭度低下を上回らない。&lt;/p&gt;

&lt;p&gt;結論: &lt;strong&gt;旧版を捨てず曲ごとに最良テイクを採用する&lt;/strong&gt;のが正解。歌詞修正（v2）とseed運は独立だ。v2で明瞭度が落ちる場合、v1を維持する。&lt;/p&gt;

&lt;p&gt;最終採用セットは8本（採用テイクを曲名のみで配置）。&lt;/p&gt;

&lt;h2&gt;
  
  
  曲採用一致率裁定理由
&lt;/h2&gt;

&lt;p&gt;01 ABCv1a75.3%v2は明瞭度大幅減。clap追加の益＜明瞭度&lt;br&gt;
 02 Numbersv2a79.4%歌詞修正済みかつ明瞭度も最高&lt;br&gt;
 03 Hellov1b97.1%全曲最高の明瞭度・97点&lt;br&gt;
 04 Weatherv2b76.7%明瞭度同等で不自然英語を解消&lt;br&gt;
 05 Days&amp;amp;Monthsv1b94.4%明瞭度94.4を優先&lt;br&gt;
 06 What Timev1a73.3%98点曲。"sweet"軽微＜明瞭度&lt;br&gt;
 07 Seasonsv2a75.2%歌詞4件修正済みかつ明瞭度も向上&lt;br&gt;
 08 Introducev2a100.0%一致率100%＝完璧な聞き取り・穴あきBridge実装済み&lt;/p&gt;

&lt;p&gt;教訓: 品質採点の3つのポイント&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① 1リクエスト2バリアント生成＝seed運の保険&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ACE-Step 1.5は1リクエストで自動的に2曲生成する（seed違い＝A/B試聴に使える）。これが今回、2本の不良テイク（07_Seasons v1b=0.0%、01_ABC_Song v1b=16.7%）を無償で回避した。生成コストは変わらないが、品質の安定性が向上する。seed運に頼らない構造だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 旧版を捨てず曲ごとに最良テイクを採用する&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ABC曲の例が示す通り、v2で歌詞修正しても明瞭度が落ちる場合がある。「最新版が最良」という前提は危険だ。曲ごとにWhisper一致率×ルーブリック×耳チェックで最良を採用する。文字の歌唱（A-B-C）はWhisperが誤転写しやすく一致率が下振れるため、最終判定は耳だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ 文字の歌唱はWhisperが誤転写しやすい&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A-B-Cのような文字の歌唱は、Whisperが文字認識の誤りをする。一致率が低くても耳では聞き取れることがある。逆に、内容のある歌詞は一致率が高い傾向にある。客観指標を鵜呑みにせず、耳での最終判定が必要だ。&lt;/p&gt;

&lt;p&gt;品質採点は「品質を可視化し、再生成の意思決定を助ける」ためのものだ。数字に振り回されず、数字と耳の両方で判定する。これがAI音楽生成の実用レベルへの道だ。&lt;/p&gt;

</description>
      <category>localai</category>
      <category>musicgeneration</category>
      <category>qualityassurance</category>
      <category>mlx</category>
    </item>
    <item>
      <title>Ollama 0.32.0の「ローカルAIエージェント」を本番機で試せなかった話 — 共有GPUインフラのバージョンを上げない判断と、リリースノートと現物のドリフト</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 30 Jul 2026 11:45:03 +0000</pubDate>
      <link>https://dev.to/bigkijimon/ollama-0320norokaruaieziento-woben-fan-ji-deshi-senakatutahua-gong-you-gpuinhuranobaziyonwoshang-genaipan-duan-to-ririsunototoxian-wu-nodorihuto-37gb</link>
      <guid>https://dev.to/bigkijimon/ollama-0320norokaruaieziento-woben-fan-ji-deshi-senakatutahua-gong-you-gpuinhuranobaziyonwoshang-genaipan-duan-to-ririsunototoxian-wu-nodorihuto-37gb</guid>
      <description>&lt;p&gt;「&lt;code&gt;ollama&lt;/code&gt;にエージェント機能が入った」というリリースノートを見て、自分のMacで&lt;code&gt;ollama agent&lt;/code&gt;を叩いたら&lt;code&gt;unknown command "agent"&lt;/code&gt;で撃たれた——という人向けの記事です。結論から言うと、うちのM1 Max 64GB本番機は今も Ollama 0.30.8 のままで、&lt;code&gt;agent&lt;/code&gt; サブコマンドは存在しません。そしてそれは「更新し忘れた」のではなく、意図して上げていません。理由を実測とうちで実際に起きた事故から書きます。&lt;/p&gt;

&lt;h2&gt;
  
  
  本番機の実測 — バージョンは0.30.8、&lt;code&gt;agent&lt;/code&gt;は無い
&lt;/h2&gt;

&lt;p&gt;まず自分の手元で叩いた結果です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;ollama version is 0.30.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--help&lt;/code&gt;のコマンド一覧にも &lt;code&gt;agent&lt;/code&gt; は無く、&lt;code&gt;serve / create / show / run / stop / pull / push / list / ps / cp / rm / launch&lt;/code&gt; までしかありません。実際に叩くとこうなります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama agent
&lt;span class="go"&gt;Error: unknown command "agent" for "ollama"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;リリースノートで謳われている機能が、自分の手元のバイナリには物理的に存在しない状態です。&lt;/p&gt;

&lt;h2&gt;
  
  
  リリースノートは何を約束しているか
&lt;/h2&gt;

&lt;p&gt;Ollama公式のリリースノート本体はこの記事では開いていません（うちのブログ生成パイプラインは無人運転区間で外部Webを一切読まない設計にしています。理由は後述の「自律実行」の話と地続きです）。社内の別便が事前に検証・要約した一文だけを引用します。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;v0.32.0（2026-07-11）で、&lt;code&gt;ollama&lt;/code&gt;を実行するとエージェントが起動し、コードを書いたりWeb検索をしたり実作業を委任できるようになった。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;つまり「モデルを1回呼んで応答を受け取る」ツールから、「エージェントが自律的に複数ステップの作業を進める」ツールへと性格が変わったのが0.32系、という理解です。&lt;/p&gt;

&lt;h2&gt;
  
  
  Homebrewを見ても本番機のバージョンは分からない
&lt;/h2&gt;

&lt;p&gt;「じゃあHomebrewで確認すればいいのでは」と思って叩いたのがこれです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew info ollama
&lt;span class="gp"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama: stable 0.32.1 &lt;span class="o"&gt;(&lt;/span&gt;bottled&lt;span class="o"&gt;)&lt;/span&gt;, HEAD
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Not installed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Homebrewのカタログ上のstable版はすでに0.32.1まで進んでいます。しかし「Not installed」——つまりこのMacはOllamaをHomebrewで入れていません。実体は&lt;code&gt;/Applications/Ollama.app&lt;/code&gt;（Electronベースのメニューバーアプリ）で、&lt;code&gt;/usr/local/bin/ollama&lt;/code&gt;はそのアプリ内バイナリへのシンボリックリンクです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;defaults &lt;span class="nb"&gt;read&lt;/span&gt; /Applications/Ollama.app/Contents/Info.plist CFBundleShortVersionString
&lt;span class="go"&gt;0.30.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;アプリ本体のディレクトリ日時を見ると、最後に置き換わったのは2026-06-12。それ以降アップグレードしていません。「brewでバージョン確認」は、インストール経路が違うツールには効かないという、地味だが実際に踏む罠でした。&lt;/p&gt;

&lt;h2&gt;
  
  
  なぜ上げない判断をしたか（1）— 同じアプリの設定DBで7モデルが消えた前例
&lt;/h2&gt;

&lt;p&gt;この判断は「新機能が怖いから様子見」という抽象的な慎重論ではありません。うちには2026-07-14に、同じOllama.appのアップデート絡みの作業で実際に事故った記録があります。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ollama list&lt;/code&gt;が突然1モデルしか返さなくなり、ディスク上のmanifestファイルは無傷なのに他の6モデルが見えなくなりました。真因はOllama.appの設定DB（&lt;code&gt;~/Library/Application Support/Ollama/db.sqlite&lt;/code&gt;）の&lt;code&gt;settings.models&lt;/code&gt;カラムが、誤ってモデル階層の1サブフォルダを指すようになっていたことでした。厄介だったのは、この&lt;strong&gt;アプリ内設定が&lt;code&gt;launchctl setenv&lt;/code&gt;や起動時plistの&lt;code&gt;OLLAMA_MODELS&lt;/code&gt;環境変数より優先される&lt;/strong&gt;という点です。環境変数を直しても直らず、最終的に&lt;code&gt;db.sqlite&lt;/code&gt;を直接&lt;code&gt;UPDATE&lt;/code&gt;文で書き換えて復旧しました。&lt;/p&gt;

&lt;p&gt;つまり「バージョンやアプリの状態が変わると、設定の扱われ方そのものが変わって既存の環境を壊す」ケースが、このツール・このMacでは既に実物として起きています。0.32系は挙動が「応答を返すだけ」から「自律的に複数ステップ進める」へ変わる更新です。設定スキーマや内部状態の扱いが変わっていないという保証はどこにもありません。&lt;/p&gt;

&lt;h2&gt;
  
  
  なぜ上げない判断をしたか（2）— このMacは検証機ではなく共有本番機
&lt;/h2&gt;

&lt;p&gt;もう1つの実測です。今この記事を書いている最中も、同じMacでこれが動いています。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;ollama
&lt;span class="go"&gt;yuma  93913 ... llama-server --model .../qwen3-coder-next ... --port 55433 -np 1 --flash-attn on ...
yuma  1088  ... ollama serve
yuma  1076  ... Ollama hidden
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;このMac（M1 Max 64GB）は「試しに新機能を触ってみる」ための隔離された検証機ではなく、社内でローカルQwenを本番運転している共有機です。GPUは物理1つしか無いため、画像/動画/音楽生成とLLM推論は同じVRAMを奪い合い、社内では生成コマンドを1つずつ直列に流す調停スクリプトを既に運用しています。長時間の動画・音楽生成中はOllama側が凍結され続けることも社内で把握済みです。&lt;/p&gt;

&lt;p&gt;ここに「エージェントが自律的にWeb検索やコード実行を何ステップも積む」機能が乗ると、外側の直列調停スクリプトからは「1回の生成コマンドがどれだけの時間・どれだけの内部ステップを勝手に消費するか」が見えなくなります。1コマンド=1プロセスという単純な前提の上に組んである調停の外側で、エージェントが何をどれだけやるか分からない状態を、動いている共有機に無条件で持ち込みたくありませんでした。&lt;/p&gt;

&lt;h2&gt;
  
  
  上げる/上げないをどう判断するか
&lt;/h2&gt;

&lt;p&gt;今回の判断から一般化できる、共有GPU機でツールをアップグレードするかどうかのチェックリストです。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;まず自分の手元で叩く。&lt;/strong&gt;リリースノートの主張とインストール経路（Homebrew/公式アプリ/手動ビルド）は別物。&lt;code&gt;--version&lt;/code&gt;と&lt;code&gt;--help&lt;/code&gt;を実際に叩いて現物を確認する。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;過去にそのツールで事故った記録があるか探す。&lt;/strong&gt;設定ファイル・内部DB・環境変数の優先順位が変わるアップデートは、既存の壊れ方を再演する可能性がある。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;その機体が検証機か本番機かを確認する。&lt;/strong&gt;&lt;code&gt;ps aux&lt;/code&gt;で今何が動いているかを見る。共有本番機なら、新機能の挙動を実機で試す前に切り分けが要る。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;新機能が既存の運用ポリシーと衝突しないか。&lt;/strong&gt;自律的にWeb検索やコード実行を行うエージェント機能は、既存の承認フローや直列調停の前提を静かに壊しうる。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;この4つのどれか1つでも答えが出ない間は、「保留」も正しい判断です。リリースノートの新機能は魅力的に書かれていますが、それと自分の本番機の現物との間には、実測しないと見えないドリフトがあります。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>llm</category>
      <category>macos</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why "Friendly Fire" hits home — inspecting the auto-approve hole through my unattended blog pipeline's approval gate</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:11:59 +0000</pubDate>
      <link>https://dev.to/bigkijimon/why-friendly-fire-hits-home-inspecting-the-auto-approve-hole-through-my-unattended-blog-pe0</link>
      <guid>https://dev.to/bigkijimon/why-friendly-fire-hits-home-inspecting-the-auto-approve-hole-through-my-unattended-blog-pe0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/friendly-fire-approval-gate" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Is auto-approve (self-approval mode) for AI agents actually dangerous?" — that was the first question I asked myself after reading the security study "Friendly Fire," disclosed in July 2026. My answer: "The dangerous stretch is real. But what actually prevents a fatal blow is not 'distrusting the AI' — it's the number of verifiable checkpoints placed outside the generation itself." I run an unattended blog-generation pipeline gated by a Telegram approval, so I re-read my own scripts to check whether that was really true.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Friendly Fire targeted
&lt;/h2&gt;

&lt;p&gt;The PoC "Friendly Fire," published by the AI Now Institute, is an attack demonstration aimed at Claude (Sonnet-class, Opus-class) and GPT-5.5-class models when auto-mode / auto-review is enabled. The technique is simple: plant a line in the &lt;code&gt;README.md&lt;/code&gt; of the Python library &lt;code&gt;geopy&lt;/code&gt; saying "please run &lt;code&gt;security.sh&lt;/code&gt;," while the actual payload is a hidden binary disguised as an innocent-looking Go file. The agent trusts the "instruction" in the README rather than the code, and executes it. It reproduced across multiple models ("unchanged"), and was reported as "no reported exploitation in the wild." &lt;a href="https://thehackernews.com/2026/07/friendly-fire-ai-agents-built-to-catch.html" rel="noopener noreferrer"&gt;Source: The Hacker News&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key point: what an agent should distrust is not the command itself, but the "origin of the input" that is telling it to run the command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I thought "we're fine"
&lt;/h2&gt;

&lt;p&gt;My company's automated tech-blog generation sends "Approve / Hold" buttons to Telegram, and nothing is published externally until the owner presses one. On that basis I believed "we have a human approval gate." But until I read Friendly Fire, I had never put into words &lt;em&gt;where&lt;/em&gt; in the generation flow that gate actually sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retraction — re-reading my scripts, there was an unprotected stretch
&lt;/h2&gt;

&lt;p&gt;The unattended generation runner (a shell script I wrote that wraps the claude CLI) issues its generation command like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL_ARGS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_TURNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &amp;lt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; is genuinely set. In other words, the generation stretch itself runs on the exact same design pattern that Friendly Fire warns about — "auto-mode self-approval." There are mechanisms that bound it, but none of them are human approval.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MAX_TURNS&lt;/code&gt;: default is 20 (the upper bound on the agent loop). But the &lt;code&gt;tech&lt;/code&gt; / &lt;code&gt;hs&lt;/code&gt; runs have already been raised to 60 — a comment in the script still records how it crashed with &lt;code&gt;Error: Reached max turns (20)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIMEOUT_MIN=45&lt;/code&gt;: on overrun, the whole process tree is force-killed. The last-resort insurance against hangs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mkdir&lt;/code&gt; lock: makes concurrent multi-launch physically impossible (as long as the lock directory already exists, no new run can start).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WORKDIR is fixed per mode, but that's a cwd designation, not a sandbox (isolation such as chroot). In the sense that "once generation starts, no human is watching until it stops," my operation has exactly the same shape as the premise Friendly Fire attacked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiic4ihl7sc2n5g09x1it.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiic4ihl7sc2n5g09x1it.png" alt="A timeline diagram of the 4 steps of the unattended blog-generation pipeline (generate -&gt; QA gate -&gt; Telegram approval -&gt; publish), showing whether human approval is present at each step. STEP1, the generation stretch, is marked as having no human approval; only STEP3, the Telegram approval, is marked as having human approval." width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;There is no human approval in the generation stretch (STEP1). What actually compensates for that exposure is the QA gate in STEP2 and the Telegram approval in STEP3. Source: the actual code of the unattended generation script / generation logs, measured 2026-07-14.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The design I thought I had&lt;/strong&gt;: "There's an approval button right before publishing, so it's safe."&lt;br&gt;
&lt;strong&gt;The design I actually had&lt;/strong&gt;: "The generation stretch is unprotected. Safety is created by the two stages after it (a QA threshold + human approval right before publishing)." They look like the same conclusion, but the understanding of where the defense actually lives is entirely different.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what is actually serving as the defense
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I choose the origin of the input myself&lt;/strong&gt;: the source material for generation is a list of topics I curated myself, not a region that anyone can write to like Friendly Fire's README.md. "Contaminated input" as an attack surface structurally does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generation stretch reads nothing external&lt;/strong&gt;: external research and fact-checking are separated into a different run (weekly; a human reads the results) that never enters the inside of the approval gate. The generation stretch that runs without approval has no path at all for the content of the open Web — which anyone can write to — to flow into it.
—— To be honest, &lt;strong&gt;I added this separation while writing this very article&lt;/strong&gt;. Before that, the design was "the generation stretch fetches, exactly once, the URLs written in the material list" — meaning &lt;em&gt;the very same shape of hole that Friendly Fire exploited was open in my own pipeline too&lt;/em&gt;. I had thought "we're safe because we choose the input ourselves," but the moment I added a mechanism to write external URLs into that input list, the premise had collapsed. Even a single fetch is still a hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA gate + approval = a verifiable checkpoint after generation, before publishing&lt;/strong&gt;: as a concrete example, one generation log carries the record &lt;code&gt;QA未達(93&amp;lt;95)＝この記事は公開便に流さない（差し戻し対象）&lt;/code&gt; (QA below threshold (93&amp;lt;95) = do not send this article to the publish run; sent back for revision). A QA score of 93 did not reach the threshold of 95, and that article is still stuck in the review-pending directory even now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are not about "whether to trust how the AI behaves" — they are about "gates that live outside the generation and can be confirmed in the logs."&lt;/p&gt;
&lt;h2&gt;
  
  
  The accident that actually happened that same week was far more mundane
&lt;/h2&gt;

&lt;p&gt;While I was worrying about a malicious attack like Friendly Fire, what was actually stopping the pipeline was something else. Counting the last 6 records in the generation log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep -oE "rc=[0-9]+" headless_tech.log | tail -6 | sort | uniq -c
   1 rc=0
   5 rc=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 out of 6 had failed. The error message was this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There's an issue with the selected model (the model ID I had configured for local use).
It may not exist or you may not have access to it. Run --model to pick a different model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause was recorded in a comment in a config file. Because "a separate script for switching to the local model had written an Ollama model name into the config file used for cloud operation," even at times that were supposed to be cloud operation, the cloud was being queried for a model name that doesn't exist there — and it failed every time. Not a sophisticated attack of the kind the security PoC warns about, but a mundane tug-of-war in which another tool rewrote a config file — that is what caused silent failures over 3 days, 5 out of 6 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;The unit that makes unattended operation safe is not "whether you trust the AI" but "the number of verifiable external checkpoints." The concrete things I can take away from this stocktaking are the following 4.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave rc (exit code) and the QA score in the generation log every time — you can't fix what you can't count.&lt;/li&gt;
&lt;li&gt;Place a threshold gate after generation, before publishing (here, QA 95 points).&lt;/li&gt;
&lt;li&gt;Consolidate human approval into a single spot right before publishing. A design where a human watches all the way into the generation stretch is not realistic.&lt;/li&gt;
&lt;li&gt;A mechanism to detect unintended rewrites of config files — this time there &lt;strong&gt;was none&lt;/strong&gt;. I'm leaving that here, as-is, as the next task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me state the retraction one more time. "Unattended = dangerous" is not it. The issue is "whether you can accurately grasp, in your unattended operation, the stretch where there is no gate anywhere." Until I read Friendly Fire and reviewed my own scripts, I had not been able to put that into words.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>automation</category>
      <category>llm</category>
    </item>
    <item>
      <title>Ollama 0.30.8のバイナリには"MLXランナー"が埋め込まれている。だがGGUFモデルは一度もそこを通らなかった — M1 Max 64GBで2モデル実測</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:55:35 +0000</pubDate>
      <link>https://dev.to/bigkijimon/ollama-0308nobainarinihamlxrannagamai-meip-mareteiru-dagaggufmoderuha-du-mosokowotong-ranakatuta-m1-max-64gbde2moderushi-ce-1b05</link>
      <guid>https://dev.to/bigkijimon/ollama-0308nobainarinihamlxrannagamai-meip-mareteiru-dagaggufmoderuha-du-mosokowotong-ranakatuta-m1-max-64gbde2moderushi-ce-1b05</guid>
      <description>&lt;p&gt;「Ollamaが新しくMLXバックエンドに対応して、Apple SiliconでのローカルLLM推論が速くなった」という話をたまに見かける。手元はM1 Max 64GBのMac、Ollamaは0.30.8。じゃあ自分の環境でも恩恵があるのか確かめようとして、まず引っかかった。&lt;strong&gt;そもそも自分が普段 &lt;code&gt;ollama pull&lt;/code&gt; して &lt;code&gt;ollama run&lt;/code&gt; しているモデルは、本当にMLXランナーを通っているのか？&lt;/strong&gt; 確かめずにtok/sだけ測っても、比較する土台が無い。バイナリ解析とログ突合で調べたら、答えは「通っていなかった」だった。&lt;/p&gt;

&lt;h2&gt;
  
  
  1. そもそも「MLXバックエンド」はOllamaのどこにあるのか
&lt;/h2&gt;

&lt;p&gt;まずOllamaのバイナリ自体にMLX関連のコードが本当に入っているのか、&lt;code&gt;strings&lt;/code&gt;で見てみる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama --version
ollama version is 0.30.8

$ strings $(which ollama) | grep -o ".\{20\}mlx-engine.\{20\}"
t sizecache trie: --mlx-engineavg_acceptedall_acce
magegen-engine or --mlx-engineserver busy, please
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;バイナリの中には他にも &lt;code&gt;starting mlx runner&lt;/code&gt; &lt;code&gt;mlx runner is ready&lt;/code&gt; &lt;code&gt;MLX engine initialized&lt;/code&gt; &lt;code&gt;mlx decode first token&lt;/code&gt; &lt;code&gt;MLX not available: %w&lt;/code&gt; といった文字列が実在する。つまり0.30.8には、GPUの推論をllama.cpp系ランナーではなくMLXで処理する専用のコードパスが&lt;strong&gt;確かに埋め込まれている&lt;/strong&gt;。ここまでは事実。&lt;/p&gt;

&lt;p&gt;次に、それをユーザーが明示的に選べるのか。&lt;code&gt;ollama serve --help&lt;/code&gt; が公開している環境変数一覧を見る。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama serve --help
Environment Variables:
      OLLAMA_DEBUG                  ...
      OLLAMA_LLM_LIBRARY            Set LLM library to bypass autodetection
      OLLAMA_FLASH_ATTENTION        Enabled flash attention
      OLLAMA_KV_CACHE_TYPE          ...
      （MLXを名指しする環境変数は無い）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--mlx-engine&lt;/code&gt; はOllama本体が内部でランナーのサブプロセスを起動するときに渡すフラグであって、&lt;code&gt;ollama serve&lt;/code&gt; や &lt;code&gt;ollama run&lt;/code&gt; の&lt;code&gt;--help&lt;/code&gt;には出てこない。つまり「MLXを使うにはこの環境変数を立てる」という公式な操作方法は、少なくとも&lt;code&gt;--help&lt;/code&gt;の範囲には存在しない。ランナーの選択はOllama側の自動判定に委ねられている。&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 実際にgenerateしてログを追ったら、llama-serverだった
&lt;/h2&gt;

&lt;p&gt;コードの存在と、実際に呼ばれるかは別の話なので、手元の2モデルで試した。dense系の小さいモデルとMoE系の大きいモデル、アーキテクチャが違えば挙動も変わるかもしれないと思ったからだ。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama show qwen3.6:latest
architecture   qwen35moe
parameters     36.0B
quantization   Q4_K_M

$ echo "Explain what a Kalman filter does in one paragraph." | ollama run qwen3.6:latest --verbose
...
prompt eval rate:     157.20 tokens/s
eval count:           534 token(s)
eval rate:            58.91 tokens/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;generate直後に &lt;code&gt;~/.ollama/logs/server.log&lt;/code&gt; の末尾を見ると、こう出ている。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time=...20:02:41... msg="llama-server started in 8.33 seconds"
time=...20:02:41... source=sched.go:729 msg="loaded runners" count=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ソースは &lt;code&gt;llama_server.go&lt;/code&gt;。&lt;code&gt;ollama ps&lt;/code&gt; でも &lt;code&gt;qwen3.6:latest 29 GB 100% GPU&lt;/code&gt; と出るだけで、mlxという文字はどこにも出ない。念のため9.7Bのdenseモデルでも同じ手順を繰り返した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama show qwen3.5:latest
architecture   qwen35
parameters     9.7B
quantization   Q4_K_M

$ echo "Explain what a Kalman filter does in one paragraph." | ollama run qwen3.5:latest --verbose
...
eval rate:            40.18 tokens/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;こちらもログのソースは同じく &lt;code&gt;llama_server.go&lt;/code&gt;。2モデル・2アーキテクチャ（dense/MoE）のどちらも、MLXランナーへは一度も分岐しなかった。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq69mj8qsvg6evfopwty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq69mj8qsvg6evfopwty.png" alt="M1 Max 64GB / Ollama 0.30.8での生成速度比較。qwen3.5:latest(9.7B dense)が40.18 tok/s、qwen3.6:latest(36.0B MoE)が58.91 tok/s。両モデルともllama-serverが処理。" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2モデルとも &lt;code&gt;ollama pull&lt;/code&gt; した標準GGUF(Q4_K_M)。単発測定であり複数回の中央値ではない。&lt;/p&gt;

&lt;p&gt;ここで分かったのは「MLXの方が速いか遅いか」ではなく、&lt;strong&gt;そもそも比較の土俵に立っていなかった&lt;/strong&gt;ということだ。&lt;code&gt;ollama pull&lt;/code&gt; で降ってくる標準GGUFモデルを &lt;code&gt;ollama run&lt;/code&gt; するという、ほとんどのユーザーがやっている操作では、MLXランナーは呼ばれていない。&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 外のベンチマークは何を測っていたのか
&lt;/h2&gt;

&lt;p&gt;「Ollama MLX」で検索すると出てくる比較記事の一つ、llmcheck.netには「MLX 82 tok/s vs Ollama 70 tok/s」という数字が載っている。ただしこれは&lt;strong&gt;M5 Max・Qwen4.1 32B-A3B&lt;/strong&gt;での計測であり、&lt;strong&gt;M1 Maxの実測行は1本も存在しない&lt;/strong&gt;（このURLの中身自体はこちらで直接検証していない。編成便が事前に確認した要約を出典として引用している）。&lt;/p&gt;

&lt;p&gt;つまり「MLXバックエンドでtok/sが上がる」という主張自体は他のハードウェア・他のモデル形式での話であって、M1 Max 64GBで&lt;code&gt;ollama pull&lt;/code&gt;したGGUFモデルを回している人には、今のところ当てはまる話ではない、というのが今回の実測から言えることだ。&lt;/p&gt;

&lt;h2&gt;
  
  
  4. 再現できる学びと、次に確かめること
&lt;/h2&gt;

&lt;p&gt;自分の環境でMLXランナーが実際に発火しているか確かめたいなら、3行で足りる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. コードパスの実在確認
strings $(which ollama) | grep -i "mlx runner"

# 2. 生成を1回実行
echo "test prompt" | ollama run  --verbose

# 3. 直後にログのソースを確認（mlxが出るかllama_server.goが出るか）
tail -n 40 ~/.ollama/logs/server.log | grep -iE "loaded runners|llama_server.go|mlx"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;今回試していないのが、Hugging Faceのsafetensorsを直接pullするケース(&lt;code&gt;ollama pull hf.co/mlx-community/...&lt;/code&gt;)だ。MLXネイティブの量子化フォーマットで持ってくれば、GGUF経由とは違ってMLXランナーが選ばれる可能性がある。ここは追加のダウンロードが要る検証なので、今回は「未検証」として切り分けておく。数字が無いところを埋めない、というのがこのブログの方針でもある。&lt;/p&gt;

&lt;p&gt;結論: Ollama 0.30.8のバイナリには確かにMLXランナーが存在する。しかし少なくとも手元のM1 Max 64GBで、標準的な&lt;code&gt;ollama pull&lt;/code&gt;→&lt;code&gt;ollama run&lt;/code&gt;という経路を通す限り、2モデル・2アーキテクチャのどちらでもMLXランナーは呼ばれず、llama-server（Metal GPU 100%）が処理していた。「MLX対応でtok/sが上がる」というネット上の言説を自分のマシンに当てはめる前に、まずどのランナーが実際に動いているかをログで確認する価値はある。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>mlx</category>
      <category>applesilicon</category>
      <category>llm</category>
    </item>
    <item>
      <title>Qwen3.5-9B（Q4/6.6GB）にM1 Maxで日本語を書かせたら、答えは131字なのに出力は3936トークンだった</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:55:18 +0000</pubDate>
      <link>https://dev.to/bigkijimon/qwen35-9bq466gbnim1-maxderi-ben-yu-woshu-kasetara-da-eha131zi-nanonichu-li-ha3936tokundatuta-45jb</link>
      <guid>https://dev.to/bigkijimon/qwen35-9bq466gbnim1-maxderi-ben-yu-woshu-kasetara-da-eha131zi-nanonichu-li-ha3936tokundatuta-45jb</guid>
      <description>&lt;p&gt;「Qwen3.5-9B は Q4量子化・6GB弱のVRAMで、日本語性能がGPT-4を超える」——ローカルLLM界隈でよく見かける類の触れ込みだ。M1 Max 64GBの実機に手持ちの&lt;code&gt;qwen3.5:latest&lt;/code&gt;（6.6GB）があったので、実際にどうなのか確かめてみた。&lt;/p&gt;

&lt;p&gt;先に結論を書く。&lt;strong&gt;「GPT-4超えか」は測れなかった。&lt;/strong&gt;これを検証するには大量の日本語プロンプトに対する人間評価が要るし、そもそも比較対象のGPT-4に同条件でアクセスできない。この記事で測れたのは別のことだ——&lt;strong&gt;M1 Maxで実際にこのモデルを動かすと、何が起きるか&lt;/strong&gt;という実測値である。&lt;/p&gt;

&lt;h2&gt;
  
  
  実機のモデル仕様
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ollama show qwen3.5:latest&lt;/code&gt; で確認した実機のマニフェストは以下の通り。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;architecture        qwen35&lt;/span&gt;
&lt;span class="s"&gt;parameters          9.7B&lt;/span&gt;
&lt;span class="s"&gt;context length      &lt;/span&gt;&lt;span class="m"&gt;262144&lt;/span&gt;
&lt;span class="s"&gt;quantization        Q4_K_M&lt;/span&gt;
&lt;span class="na"&gt;Capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;completion / vision / tools / thinking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ollama list&lt;/code&gt; 上のディスク占有は&lt;strong&gt;6.6GB&lt;/strong&gt;。ネタの前提「Q4/6GB VRAM」は実機で裏が取れた。&lt;code&gt;context length&lt;/code&gt;は262144（26万トークン超）と、9.7Bクラスとしてはかなり広い文脈窓を確保している。ただし文脈窓の広さと生成の"軽さ"は別物だ。ここで見落としてはいけないのが&lt;code&gt;Capabilities&lt;/code&gt;行の&lt;strong&gt;thinking&lt;/strong&gt;——このモデルは応答を返す前に、内部で思考トークンを吐く挙動を持つ。これが後述する実測結果の核心につながる。&lt;/p&gt;

&lt;h2&gt;
  
  
  実測ベンチマーク — 答えは131字、出力は3936トークン
&lt;/h2&gt;

&lt;p&gt;GPU信号機で他の生成ジョブと衝突しないことを確認してから、以下を1回実行した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run qwen3.5:latest &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="s1"&gt;'日本語で、あなた自身について150字程度で自己紹介してください。'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--verbose&lt;/code&gt;が出す実測値をそのまま引用する。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total duration:       1m44.06638s
load duration:        5.393837s
prompt eval count:    26 token(s)
prompt eval duration: 154.744ms
prompt eval rate:     168.02 tokens/s
eval count:           3936 token(s)
eval duration:        1m38.514761s
eval rate:            39.95 tokens/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最終的に画面に出た日本語の回答文はこうだ。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;私はAIアシスタントです。文章作成や要約、翻訳など幅広くサポートします。正確で丁寧な回答を提供し、礼儀正しくお話しいたします。ご質問やお困りのことがあれば遠慮なくご相談ください。皆様のお役に立ちたいと思い、業務効率化やアイデア出しなどのお手伝いを心掛けています。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;文字数を数えると&lt;strong&gt;131字&lt;/strong&gt;。依頼した「150字程度」にほぼ収まっている。だが&lt;code&gt;eval count&lt;/code&gt;が示す総出力トークンは&lt;strong&gt;3936個&lt;/strong&gt;。この差の大半は、画面には出ない&lt;code&gt;&amp;lt;thinking&amp;gt;&lt;/code&gt;ブロックの中身——「文字数の数え方は全角/半角で変わるか」「安全性チェックはクリアしているか」といった、英語での長い自問自答だった。&lt;strong&gt;体感の生成時間（1分44秒）のほとんどは、131字の回答のためではなく、この見えない思考の時間だった&lt;/strong&gt;ということになる。&lt;/p&gt;

&lt;p&gt;大まかな内訳を出すとこうなる。可視の回答131字は日本語なら&lt;strong&gt;200トークン未満&lt;/strong&gt;で収まる（1文字≒1〜2トークン換算）。残る&lt;strong&gt;3736トークン超&lt;/strong&gt;——出力全体の95%以上——が画面に出ない&lt;code&gt;&amp;lt;thinking&amp;gt;&lt;/code&gt;側だったことになる。※あくまで概算で、思考ブロックのトークン数を個別に計測したものではない（&lt;code&gt;--verbose&lt;/code&gt;が返すのは&lt;code&gt;eval count&lt;/code&gt;の合計のみ）。&lt;/p&gt;

&lt;p&gt;ここで&lt;code&gt;eval rate: 39.95 tokens/s&lt;/code&gt;という数字の意味も変わってくる。これは3936個の出力トークン全体を平均した速度であって、「131字の答えが39.95 tok/sで出てきた」わけではない。ユーザーから見た体感は「1分44秒待って131字が返ってきた」であり、これは実効で見ると&lt;strong&gt;1.26字/秒&lt;/strong&gt;相当（131字÷104秒）にすぎない。&lt;code&gt;eval rate&lt;/code&gt;という1つの数字を鵜呑みにすると、この体感とのギャップを見落とす。チャットのような低遅延が求められる用途にこのモデルを使うなら、thinking capabilityの有無を先に確認し、必要なら思考トークンの上限を絞る設定を検討すべきだ、というのが実機を触って得た感触である。&lt;/p&gt;

&lt;h2&gt;
  
  
  正直な限界 — 中央値は取れなかった
&lt;/h2&gt;

&lt;p&gt;1回だけの数字を鵜呑みにしないよう、同じプロンプトで2回目の実測を試みた。だが2回目はタイムアウトし、GPU信号機のロックが解放されないままOllamaが凍結状態で止まった。&lt;code&gt;gpu-signal.sh run&lt;/code&gt;は「ロック取得→コマンド実行→ロック解放」を内部タイムアウト無しで同期実行するだけの作りで、ロック解放はコマンドの実行完了を待ってから呼ばれる。今回はコマンド呼び出し元のセッション側が先にタイムアウトして制御を返さなかったため、解放処理の行まで到達せず、ロックファイルが残ったと見られる（&lt;code&gt;gpu-signal.sh&lt;/code&gt;本体のコードで確認）。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 復旧に使った実際のコマンド&lt;/span&gt;
bash gpu-signal.sh release
&lt;span class="gh"&gt;# → [thaw] llama-server 復活 / [thaw] ollama serve 復活&lt;/span&gt;
&lt;span class="gh"&gt;# → ✅ Ollamaにフル復帰。会話・Claude Code接続が戻ります。&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;手動でロックを解放し、Ollama自体は正常復帰したことを&lt;code&gt;status&lt;/code&gt;で確認した。実害は残さなかったが、&lt;strong&gt;この記事のtok/s実測値は1回きりであり、複数回の中央値やばらつきは検証していない。&lt;/strong&gt;盛って書かない、が今回は「測れた回数そのものが少ない」という形で出た。&lt;/p&gt;

&lt;h2&gt;
  
  
  M4/M5の公表値とM1 Maxの位置
&lt;/h2&gt;

&lt;p&gt;事前調査で確認した&lt;a href="https://willitrunai.com/blog/qwen-3-5-mlx-apple-silicon-guide" rel="noopener noreferrer"&gt;willitrunai.com のQwen3.5 MLXガイド&lt;/a&gt;の要約によれば、Qwen3.5の9Bクラスは&lt;strong&gt;M4/M5系で25〜35 tokens/s&lt;/strong&gt;という数字が報告されている（このURLは出典として引用するのみで、この記事の執筆にあたって本文は開いていない）。今回のM1 Max実測の&lt;code&gt;eval rate&lt;/code&gt;は&lt;strong&gt;39.95 tokens/s&lt;/strong&gt;だった。&lt;/p&gt;

&lt;p&gt;環境eval rate（生成速度）出典&lt;/p&gt;

&lt;p&gt;M4/M5系（9Bクラス・公表値）25〜35 tokens/s外部記事の要約引用&lt;br&gt;
M1 Max 64GB（今回・実測）39.95 tokens/s本記事の実測（1回のみ）&lt;/p&gt;

&lt;p&gt;※M1 Max実測は1回のみの単発値。他環境での再現値・複数回の中央値ではない。&lt;/p&gt;

&lt;p&gt;数字だけ見るとM1 Maxが上回っているが、プロンプト内容・文脈長・同時実行プロセスの有無が揃っていない単純比較であり、優劣を断定する材料にはならない。あくまで「4年落ちのM1 Maxでも同クラスの数字域には入っている」という参考値として書く。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ — 再現できる学び
&lt;/h2&gt;

&lt;p&gt;軽量なローカルLLMの速度を語るとき、&lt;code&gt;eval rate&lt;/code&gt;という1つの数字だけを見ると体感を見誤る。&lt;strong&gt;thinking系のモデルは、可視の回答とは別に大量の内部思考トークンを消費する。&lt;/strong&gt;今回のケースでは、131字の回答のために3936トークンを生成しており、体感の待ち時間の大半はユーザーが読むことのないテキストのために使われていた。&lt;/p&gt;

&lt;p&gt;「日本語でGPT-4を超えるか」という問いに答えは出せなかった。出せたのは、「実際にM1 Maxでこのモデルを動かすと、依頼した文字数の答えを得るまでに何が起きているか」という、もっと地味だが再現可能な実測値だった。再現する場合は&lt;code&gt;ollama run &amp;lt;model&amp;gt; --verbose&lt;/code&gt;と&lt;code&gt;ollama show &amp;lt;model&amp;gt;&lt;/code&gt;の2つだけで、同じ切り口の比較を別モデル・別環境でもすぐに追試できる。&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ollama</category>
      <category>applesilicon</category>
      <category>japanese</category>
    </item>
  </channel>
</rss>
