<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Wei Builds：AI 與自動化</title><description>MCP、Claude Code，拿 AI 做事的方法</description><link>https://www.weibuilds.com/</link><language>zh-TW</language><atom:link rel="hub" href="https://pubsubhubbub.appspot.com/"/><atom:link rel="self" type="application/rss+xml" href="https://www.weibuilds.com/topics/ai/rss.xml"/><item><title>讓 AI 代理自動批准指令有多危險：Pixel Crew 的 Bash 圍欄是怎麼補的</title><link>https://www.weibuilds.com/blog/2026-10-pixel-crew-auto-approve-fence/</link><guid isPermaLink="true">https://www.weibuilds.com/blog/2026-10-pixel-crew-auto-approve-fence/</guid><description>Pixel Crew 同時跑最多 20 個 Claude Code 與 Codex，自動批准不開沒辦法用。10 月 7 日那次安全審視抓到八種繞過，每一種附一行會繞過的指令、怎麼修，還有明講修不掉的部分。</description><pubDate>Tue, 27 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pixel Crew 是我做的本機工具，把多個 Claude Code 與 Codex session 放進一間像素辦公室，最多同時 20 個。這種用法下自動批准一定要開，不然 20 個代理每做一件事都要你按一次允許，等於沒有自動化。但自動批准的另一面是，代理一旦被餵了惡意內容，或者單純判斷錯誤，它跑的指令不會再經過你。10 月 7 日我對這塊做了一次完整審視，抓到八種繞過，當天全部修掉。這篇一個一個講：怎麼繞、為什麼會漏、怎麼補。程式碼都在公開 repo，連結放在各段。&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;像素辦公室裡一個 NPC 停在「允許執行這個指令？」的卡片前，等人按拒絕、本次皆允許或允許&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./approval.webp&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;沒開自動批准的樣子：NPC 停在原地等你按。20 個 NPC 都這樣，你一整天就在按允許。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;先講結論白名單放行黑名單只負責說理由&quot;&gt;先講結論：白名單放行，黑名單只負責說理由&lt;a href=&quot;#先講結論白名單放行黑名單只負責說理由&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;第一版的想法很直覺，列一張危險指令清單，&lt;code&gt;rm -rf&lt;/code&gt;、&lt;code&gt;sudo&lt;/code&gt;、&lt;code&gt;mkfs&lt;/code&gt;、&lt;code&gt;curl | bash&lt;/code&gt;，沒命中就放行。審視之後整個反過來：自動批准只靠一張很窄的白名單，只放行認得出來的唯讀指令；危險指令清單還在，但它的工作變成「被拒絕的時候把理由講清楚」。&lt;/p&gt;
&lt;p&gt;原因很簡單。黑名單只能擋你想得到的形狀，shell 能表達同一件事的方法太多了。下面八個洞，有六個都是「這條指令長得不像危險指令，但做的事是」。&lt;/p&gt;
&lt;p&gt;Pixel Crew 現在有四檔：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;行為&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;每件事都問&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;safe&lt;/td&gt;
&lt;td&gt;只放行白名單裡的唯讀指令，其餘都問&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;預設放行，只擋黑名單命中的毀滅性指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;invincible&lt;/td&gt;
&lt;td&gt;什麼都不擋，等於 Claude Code 的 &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;full 有一件事要講明：非 Bash 的工具呼叫，包括每一台接上來的 MCP server 的每一個工具，在 full 模式是無條件放行的。沒有逐工具的風險分級。所以 full 的意思不是「對 shell 寬鬆一點」，是「MCP server 能做的事都不再問」。這句話我寫在程式碼註解裡，也要求所有介面文案照這個意思寫。&lt;/p&gt;
&lt;p&gt;safe 模式一條 Bash 指令要連過五關才會自動放行，任何一關沒過就回到問你。後面八個洞裡有五個，都是其中某一關原本漏了：&lt;/p&gt;
&lt;figure&gt;
&lt;svg class=&quot;diagram&quot; viewBox=&quot;0 0 760 250&quot; role=&quot;img&quot; aria-label=&quot;safe 模式的五關：指令名在白名單、旗標不在寫入或執行表、引號外沒有括號與萬用字元、串接每一段都唯讀、重導向只有丟棄輸出，全過才放行，任一關沒過就交給人&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;ar&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;var(--ink-2)&quot;/&gt;&lt;/marker&gt;
    &lt;marker id=&quot;ar-ok&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;var(--topic-infra)&quot;/&gt;&lt;/marker&gt;
    &lt;marker id=&quot;ar-ask&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;var(--accent)&quot;/&gt;&lt;/marker&gt;
  &lt;/defs&gt;
  &lt;g font-size=&quot;13&quot; font-family=&quot;inherit&quot; fill=&quot;var(--ink)&quot;&gt;
    &lt;rect x=&quot;12&quot; y=&quot;40&quot; width=&quot;128&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;76&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot;&gt;指令名在&lt;/text&gt;&lt;text x=&quot;76&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot;&gt;唯讀白名單&lt;/text&gt;
    &lt;rect x=&quot;164&quot; y=&quot;40&quot; width=&quot;128&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;228&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot;&gt;旗標不在&lt;/text&gt;&lt;text x=&quot;228&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot;&gt;寫入／執行表&lt;/text&gt;
    &lt;rect x=&quot;316&quot; y=&quot;40&quot; width=&quot;128&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;380&quot; y=&quot;62&quot; text-anchor=&quot;middle&quot;&gt;引號外沒有&lt;/text&gt;&lt;text x=&quot;380&quot; y=&quot;80&quot; text-anchor=&quot;middle&quot;&gt;( ) { } 與&lt;/text&gt;&lt;text x=&quot;380&quot; y=&quot;98&quot; text-anchor=&quot;middle&quot;&gt;萬用字元&lt;/text&gt;
    &lt;rect x=&quot;468&quot; y=&quot;40&quot; width=&quot;128&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;532&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot;&gt;串接的每一段&lt;/text&gt;&lt;text x=&quot;532&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot;&gt;都唯讀&lt;/text&gt;
    &lt;rect x=&quot;620&quot; y=&quot;40&quot; width=&quot;128&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;684&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot;&gt;重導向只有&lt;/text&gt;&lt;text x=&quot;684&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot;&gt;丟棄輸出&lt;/text&gt;
    &lt;line x1=&quot;140&quot; y1=&quot;76&quot; x2=&quot;162&quot; y2=&quot;76&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ar)&quot;/&gt;
    &lt;line x1=&quot;292&quot; y1=&quot;76&quot; x2=&quot;314&quot; y2=&quot;76&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ar)&quot;/&gt;
    &lt;line x1=&quot;444&quot; y1=&quot;76&quot; x2=&quot;466&quot; y2=&quot;76&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ar)&quot;/&gt;
    &lt;line x1=&quot;596&quot; y1=&quot;76&quot; x2=&quot;618&quot; y2=&quot;76&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ar)&quot;/&gt;
    &lt;line x1=&quot;684&quot; y1=&quot;112&quot; x2=&quot;684&quot; y2=&quot;160&quot; stroke=&quot;var(--topic-infra)&quot; stroke-width=&quot;1.8&quot; marker-end=&quot;url(#ar-ok)&quot;/&gt;
    &lt;rect x=&quot;628&quot; y=&quot;164&quot; width=&quot;112&quot; height=&quot;40&quot; rx=&quot;20&quot; fill=&quot;var(--topic-infra)&quot;/&gt;
    &lt;text x=&quot;684&quot; y=&quot;189&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--paper)&quot; font-weight=&quot;600&quot;&gt;自動放行&lt;/text&gt;
    &lt;g stroke=&quot;var(--accent)&quot; stroke-width=&quot;1.6&quot; stroke-dasharray=&quot;4 3&quot;&gt;
      &lt;line x1=&quot;76&quot; y1=&quot;112&quot; x2=&quot;76&quot; y2=&quot;150&quot; marker-end=&quot;url(#ar-ask)&quot;/&gt;
      &lt;line x1=&quot;228&quot; y1=&quot;112&quot; x2=&quot;228&quot; y2=&quot;150&quot; marker-end=&quot;url(#ar-ask)&quot;/&gt;
      &lt;line x1=&quot;380&quot; y1=&quot;112&quot; x2=&quot;380&quot; y2=&quot;150&quot; marker-end=&quot;url(#ar-ask)&quot;/&gt;
      &lt;line x1=&quot;532&quot; y1=&quot;112&quot; x2=&quot;532&quot; y2=&quot;150&quot; marker-end=&quot;url(#ar-ask)&quot;/&gt;
    &lt;/g&gt;
    &lt;line x1=&quot;76&quot; y1=&quot;152&quot; x2=&quot;532&quot; y2=&quot;152&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;1.6&quot; stroke-dasharray=&quot;4 3&quot;/&gt;
    &lt;line x1=&quot;304&quot; y1=&quot;152&quot; x2=&quot;304&quot; y2=&quot;162&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;1.6&quot;/&gt;
    &lt;rect x=&quot;248&quot; y=&quot;164&quot; width=&quot;112&quot; height=&quot;40&quot; rx=&quot;20&quot; fill=&quot;none&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;1.6&quot;/&gt;
    &lt;text x=&quot;304&quot; y=&quot;189&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--accent)&quot; font-weight=&quot;600&quot;&gt;交給人按&lt;/text&gt;
    &lt;text x=&quot;380&quot; y=&quot;236&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;超過 20,000 字元的指令不進這條流程，直接交給人。&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;figcaption&gt;safe 模式的五關。虛線是任一關沒過的去向。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;洞一唯讀指令配上旗標就會寫檔或執行程式&quot;&gt;洞一：唯讀指令配上旗標就會寫檔或執行程式&lt;a href=&quot;#洞一唯讀指令配上旗標就會寫檔或執行程式&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;白名單一開始認的是指令前綴：&lt;code&gt;git diff&lt;/code&gt;、&lt;code&gt;git status&lt;/code&gt;、&lt;code&gt;rg&lt;/code&gt;、&lt;code&gt;ls&lt;/code&gt;、&lt;code&gt;cat&lt;/code&gt; 這些。問題是同一個指令配上某些旗標，就不唯讀了。實測過的兩個：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; diff&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --output=/tmp/outside.txt&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;   # 會把外部檔案截斷成空檔&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;rg&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --pre&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ./evil.sh&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; TODO&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;              # 會對每個檔案執行 evil.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修法是在前綴比對之後再比對一張「寫入或執行旗標」的表，而且比對前先把引號跟反斜線剝掉，因為在 shell 裡 &lt;code&gt;&quot;--output=x&quot;&lt;/code&gt;、&lt;code&gt;--out&quot;&quot;put=x&lt;/code&gt;、&lt;code&gt;\--output&lt;/code&gt; 都等於 &lt;code&gt;--output=x&lt;/code&gt;。tsc 的選項名稱不分大小寫，&lt;code&gt;--OUTDIR&lt;/code&gt; 一樣有效，所以那條要忽略大小寫。npm、pnpm、yarn 的 &lt;code&gt;--script-shell&lt;/code&gt;、&lt;code&gt;--node-options&lt;/code&gt;、&lt;code&gt;--prefix&lt;/code&gt; 也在表上。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sed&lt;/code&gt; 乾脆不用前綴放行。就算是 &lt;code&gt;sed -n&lt;/code&gt;，也能用 &lt;code&gt;w&lt;/code&gt; 指令寫檔、用 &lt;code&gt;e&lt;/code&gt; 指令執行外部命令，例如 &lt;code&gt;echo x | sed -n &quot;w /path&quot;&lt;/code&gt;。白名單只收 Codex 讀檔時慣用的那一種整句形式，&lt;code&gt;sed -n &apos;1,200p&apos; 檔案&lt;/code&gt;，腳本只能是行號範圍加 &lt;code&gt;p&lt;/code&gt;，檔案參數不能是選項、不能有萬用字元。&lt;/p&gt;
&lt;h2 id=&quot;洞二zsh-不需要分號就能執行另一個指令&quot;&gt;洞二：zsh 不需要分號就能執行另一個指令&lt;a href=&quot;#洞二zsh-不需要分號就能執行另一個指令&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;白名單原本用一組 shell 中繼字元判斷「這條指令有沒有夾帶別的東西」：換行、&lt;code&gt;;&lt;/code&gt;、&lt;code&gt;&amp;amp;&lt;/code&gt;、&lt;code&gt;|&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;、反引號、&lt;code&gt;$(&lt;/code&gt;、&lt;code&gt;${&lt;/code&gt;。這在 bash 大致夠用，但 macOS 的預設 shell 是 zsh，Claude 的 Bash 工具也跑在上面，zsh 多了幾種不需要任何中繼字元的寫法：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;ls&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;e:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;&apos;touch pwned&apos;&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)    &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# glob qualifier，列檔案的同時執行 touch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)                &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# process substitution&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; diff&lt;/span&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --out&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;put,}&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt;=x&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;    # brace expansion 拼出上面擋掉的旗標&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修法：引號外出現 &lt;code&gt;(&lt;/code&gt;、&lt;code&gt;)&lt;/code&gt;、&lt;code&gt;{&lt;/code&gt;、&lt;code&gt;}&lt;/code&gt; 任何一個，就不算白名單安全指令。&lt;/p&gt;
&lt;h2 id=&quot;洞三萬用字元會展開成檔名檔名可以長得像旗標&quot;&gt;洞三：萬用字元會展開成檔名，檔名可以長得像旗標&lt;a href=&quot;#洞三萬用字元會展開成檔名檔名可以長得像旗標&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rg TODO *.ts&lt;/code&gt; 看起來無害。但 &lt;code&gt;*.ts&lt;/code&gt; 會在執行前被 shell 展開成目錄裡的檔名，而檔名可以就叫 &lt;code&gt;--pre=x.ts&lt;/code&gt;。洞一那張旗標表是在展開前比對的，看不到展開後才出現的選項。&lt;/p&gt;
&lt;p&gt;修法只對有危險旗標的工具收緊：git、rg、eslint、tsc、npm、pnpm、yarn 這幾個，引號外出現 &lt;code&gt;*&lt;/code&gt;、&lt;code&gt;?&lt;/code&gt;、&lt;code&gt;[&lt;/code&gt; 就不自動放行。&lt;code&gt;ls *.ts&lt;/code&gt;、&lt;code&gt;cat *.md&lt;/code&gt; 這類沒有能寫檔或執行的旗標，照舊可以用萬用字元，不然白名單會窄到沒人想用。&lt;/p&gt;
&lt;h2 id=&quot;洞四寫入重導向有很多種寫法&quot;&gt;洞四：寫入重導向有很多種寫法&lt;a href=&quot;#洞四寫入重導向有很多種寫法&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Pixel Crew 對檔案工具有一道圍欄：寫入目標逃出 workspace 就硬擋。Bash 沒有結構化的路徑參數，所以另外做了一個函式，掃出指令裡「寫入型重導向」的字面目標，逃出 workspace 就回 true。第一版只認 &lt;code&gt;&amp;gt;&lt;/code&gt; 跟 &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;。審視時補上的：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt; 1&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; /etc/x&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;        # 帶 fd 編號&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt; 2&amp;gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.zshrc&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;     # fd 加 append，目標還要先展開 ~&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt; $HOME/x       &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# stdout 跟 stderr 一起寫，$HOME 在雙引號內會展開、單引號內不會&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt; &amp;gt;|&lt;/span&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt; /tmp/x&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;        # 強制覆寫，zsh 也認 &amp;gt;!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt; &amp;gt;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#032F62;--shiki-dark:#9ECBFF&quot;&gt; /tmp/x&lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;        # &amp;gt;&amp;amp; 後面接檔名就是寫檔，不是 fd 編號&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同時要排除不是寫檔的：&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;、&lt;code&gt;&amp;gt;&amp;amp;2&lt;/code&gt;、&lt;code&gt;&amp;gt;&amp;amp;-&lt;/code&gt; 是 fd 複製或關閉，&lt;code&gt;/dev/null&lt;/code&gt;、&lt;code&gt;/dev/stdout&lt;/code&gt; 是丟棄輸出。&lt;code&gt;~user&lt;/code&gt;、&lt;code&gt;~+&lt;/code&gt; 這類靜態判定不了的，一律當成逃逸。&lt;/p&gt;
&lt;p&gt;這個函式的註解我寫得很白：&lt;strong&gt;刻意不做完整 shell 解析，這是防禦縱深，不是圍牆&lt;/strong&gt;。它只認字面重導向目標，對變數展開（&lt;code&gt;p=/etc; echo &amp;gt; $p&lt;/code&gt;）、子殼、&lt;code&gt;python -c &quot;open(...)&quot;&lt;/code&gt;、&lt;code&gt;cp&lt;/code&gt;、&lt;code&gt;mv&lt;/code&gt;、&lt;code&gt;tee&lt;/code&gt; 的目的地，一律「認不出等於不擋」。測試檔裡有一個案例叫 &lt;code&gt;documented residual bypasses are NOT flagged&lt;/code&gt;，專門確認這些繞過真的不會被擋，免得哪天有人以為圍欄比實際上厚。&lt;/p&gt;
&lt;figure&gt;
&lt;svg class=&quot;diagram&quot; viewBox=&quot;0 0 760 236&quot; role=&quot;img&quot; aria-label=&quot;寫入重導向圍欄擋得到的形狀與擋不到的形狀&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
  &lt;g font-family=&quot;inherit&quot; fill=&quot;var(--ink)&quot;&gt;
    &lt;rect x=&quot;12&quot; y=&quot;12&quot; width=&quot;356&quot; height=&quot;212&quot; rx=&quot;8&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;rect x=&quot;12&quot; y=&quot;12&quot; width=&quot;356&quot; height=&quot;34&quot; rx=&quot;8&quot; fill=&quot;var(--topic-infra)&quot;/&gt;
    &lt;rect x=&quot;12&quot; y=&quot;30&quot; width=&quot;356&quot; height=&quot;16&quot; fill=&quot;var(--topic-infra)&quot;/&gt;
    &lt;text x=&quot;30&quot; y=&quot;35&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot; fill=&quot;var(--paper)&quot;&gt;擋得到：字面上的寫入重導向&lt;/text&gt;
    &lt;g font-size=&quot;13&quot; font-family=&quot;ui-monospace, Menlo, monospace&quot;&gt;
      &lt;text x=&quot;30&quot; y=&quot;76&quot;&gt;cmd &amp;gt; /etc/x&lt;/text&gt;
      &lt;text x=&quot;30&quot; y=&quot;100&quot;&gt;cmd 2&amp;gt;&amp;gt; ~/.zshrc&lt;/text&gt;
      &lt;text x=&quot;30&quot; y=&quot;124&quot;&gt;cmd &amp;amp;&amp;gt; $HOME/x&lt;/text&gt;
      &lt;text x=&quot;30&quot; y=&quot;148&quot;&gt;cmd &amp;gt;| /tmp/x&lt;/text&gt;
      &lt;text x=&quot;30&quot; y=&quot;172&quot;&gt;cmd &amp;gt;&amp;amp; /tmp/x&lt;/text&gt;
    &lt;/g&gt;
    &lt;text x=&quot;30&quot; y=&quot;206&quot; font-size=&quot;12&quot; fill=&quot;var(--ink-2)&quot;&gt;目標逃出 workspace 就擋；2&amp;gt;&amp;amp;1、/dev/null 不算&lt;/text&gt;
    &lt;rect x=&quot;392&quot; y=&quot;12&quot; width=&quot;356&quot; height=&quot;212&quot; rx=&quot;8&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;rect x=&quot;392&quot; y=&quot;12&quot; width=&quot;356&quot; height=&quot;34&quot; rx=&quot;8&quot; fill=&quot;var(--accent)&quot;/&gt;
    &lt;rect x=&quot;392&quot; y=&quot;30&quot; width=&quot;356&quot; height=&quot;16&quot; fill=&quot;var(--accent)&quot;/&gt;
    &lt;text x=&quot;410&quot; y=&quot;35&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot; fill=&quot;var(--paper)&quot;&gt;擋不到：認不出就不擋，寫在文件裡&lt;/text&gt;
    &lt;g font-size=&quot;13&quot; font-family=&quot;ui-monospace, Menlo, monospace&quot;&gt;
      &lt;text x=&quot;410&quot; y=&quot;76&quot;&gt;p=/etc; echo x &amp;gt; $p&lt;/text&gt;
      &lt;text x=&quot;410&quot; y=&quot;100&quot;&gt;python -c &quot;open(&apos;/etc/x&apos;,&apos;w&apos;)&quot;&lt;/text&gt;
      &lt;text x=&quot;410&quot; y=&quot;124&quot;&gt;echo x | tee /etc/x&lt;/text&gt;
      &lt;text x=&quot;410&quot; y=&quot;148&quot;&gt;cp x /etc/x&lt;/text&gt;
      &lt;text x=&quot;410&quot; y=&quot;172&quot;&gt;( echo x &amp;gt; /etc/x )&lt;/text&gt;
    &lt;/g&gt;
    &lt;text x=&quot;410&quot; y=&quot;206&quot; font-size=&quot;12&quot; fill=&quot;var(--ink-2)&quot;&gt;防禦縱深，不是圍牆；full 模式下這些會過&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;figcaption&gt;寫入重導向圍欄的兩邊。右邊是刻意不處理、但明講的部分。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;洞五檢查看的是卡片文字不是完整指令&quot;&gt;洞五：檢查看的是卡片文字，不是完整指令&lt;a href=&quot;#洞五檢查看的是卡片文字不是完整指令&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;核准卡片顯示的指令有長度上限，20,000 字元。原本的檢查是對卡片上的文字跑，所以在危險片段前面塞兩萬個字的填充，檢查就只看得到無害的開頭。&lt;/p&gt;
&lt;p&gt;修法兩條：危險指令的比對一律對完整指令跑；長度超過上限的指令不自動放行，直接交給人，理由寫「指令超過 20,000 字元，無法完整顯示與檢查」。卡片顯示不完整的東西，本來就不該有人幫你按允許。&lt;/p&gt;
&lt;h2 id=&quot;洞六windows-的防線根本沒接上&quot;&gt;洞六：Windows 的防線根本沒接上&lt;a href=&quot;#洞六windows-的防線根本沒接上&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;黑名單有 Unix 的 &lt;code&gt;rm&lt;/code&gt;、&lt;code&gt;mkfs&lt;/code&gt;、&lt;code&gt;dd&lt;/code&gt;，卻漏了 Windows 的對等指令，而 Windows 是 Pixel Crew 的主要平台之一。補上的：&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Remove-Item&lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;Recurse &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;Force C:\repo\dist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5;--shiki-dark:#79B8FF&quot;&gt;Remove-Item&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt; build &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;r &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;fo        &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# PowerShell 參數可以縮寫&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;rd &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;s &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;q C:\repo\dist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;del &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;f &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;s &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;q &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;.log&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;format D: &lt;/span&gt;&lt;span style=&quot;color:#D73A49;--shiki-dark:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#24292E;--shiki-dark:#E1E4E8&quot;&gt;q&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;幾個細節。&lt;code&gt;del&lt;/code&gt;、&lt;code&gt;erase&lt;/code&gt;、&lt;code&gt;rd&lt;/code&gt;、&lt;code&gt;rmdir&lt;/code&gt;、&lt;code&gt;ri&lt;/code&gt; 都是 &lt;code&gt;Remove-Item&lt;/code&gt; 的別名，要一起認。&lt;code&gt;format&lt;/code&gt; 後面要直接接磁碟代號才算，不然會誤中到處都是的 &lt;code&gt;--format&lt;/code&gt; 旗標。Windows 的指令與別名解析不分大小寫，&lt;code&gt;Git Push --Force&lt;/code&gt; 跟 &lt;code&gt;git push --force&lt;/code&gt; 一樣，所以整組比對都要忽略大小寫。&lt;/p&gt;
&lt;p&gt;路徑也有 Windows 專屬的洞。判斷「目標有沒有逃出 workspace」用的是 &lt;code&gt;path.relative()&lt;/code&gt;，看結果是不是 &lt;code&gt;..&lt;/code&gt; 開頭。但跨磁碟（&lt;code&gt;D:\&lt;/code&gt;）、UNC（&lt;code&gt;\\server\share&lt;/code&gt;）、&lt;code&gt;\\?\&lt;/code&gt; 路徑跟 workspace 沒有共同的根，&lt;code&gt;relative()&lt;/code&gt; 會直接回傳目標的絕對路徑，不是 &lt;code&gt;..&lt;/code&gt; 開頭，第一版就放過去了。修法多一個條件：結果是絕對路徑也算逃逸。&lt;/p&gt;
&lt;h2 id=&quot;洞七對外提供檔案的目錄不能是-symlink&quot;&gt;洞七：對外提供檔案的目錄不能是 symlink&lt;a href=&quot;#洞七對外提供檔案的目錄不能是-symlink&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;每個 NPC 有一個 outbox 目錄，裡面的檔案可以從網頁下載。路徑檢查會擋 &lt;code&gt;..&lt;/code&gt;，但如果 outbox 本身是個 symlink 指到別處，檢查過了、讀到的卻是別的地方的檔案。修法是提供檔案前先確認 outbox 目錄不經過 symlink，workspace 裡的路徑檢查本來就會逐層 &lt;code&gt;lstat&lt;/code&gt;，這裡補成一致。&lt;/p&gt;
&lt;h2 id=&quot;洞八read-跟-webfetch-不該預先放行&quot;&gt;洞八：Read 跟 WebFetch 不該預先放行&lt;a href=&quot;#洞八read-跟-webfetch-不該預先放行&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;為了少跳提示，第一版把 Read、Glob、Grep、WebSearch、WebFetch 當成唯讀工具預先放行。問題是 Read 可以讀 workspace 外的檔案，WebFetch 可以抓任意網址，前者外洩，後者是把外部內容餵給代理的入口。現在這兩個在 safe 模式一樣要經過批准，只有讀 workspace 內的檔案才直接過。&lt;/p&gt;
&lt;h2 id=&quot;還有一個不是洞的坑codex-的指令外面包了一層殼&quot;&gt;還有一個不是洞的坑：Codex 的指令外面包了一層殼&lt;a href=&quot;#還有一個不是洞的坑codex-的指令外面包了一層殼&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Codex CLI 回報的每一條指令都包成 &lt;code&gt;/bin/zsh -lc &amp;lt;cmd&amp;gt;&lt;/code&gt;，內層有單引號時改用雙引號、單字時不加引號。前綴白名單永遠對不上這個外殼，等於 Codex 在 safe 模式什麼都要問。修法是只拆「一層」，而且只拆引號內不可能有跳脫或展開的形式；拆不乾淨就回 null，照一般判定走，不會因此多放行任何東西。&lt;/p&gt;
&lt;h2 id=&quot;修完之後的規則&quot;&gt;修完之後的規則&lt;a href=&quot;#修完之後的規則&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;自動放行只看窄白名單；黑名單負責拒絕理由。&lt;/li&gt;
&lt;li&gt;白名單比對指令名、也比對旗標；比對前剝引號與反斜線。&lt;/li&gt;
&lt;li&gt;引號外出現 &lt;code&gt;(){}&lt;/code&gt; 或對旗標敏感的工具遇到萬用字元，不放行。&lt;/li&gt;
&lt;li&gt;串接指令逐段檢查，每一段都唯讀才整條放行；只容忍 &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;、&lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; 這類丟棄輸出。&lt;/li&gt;
&lt;li&gt;寫入重導向逃出 workspace 就擋，認不出的形狀不擋，但寫在文件裡。&lt;/li&gt;
&lt;li&gt;看完整指令，超過 20,000 字元交給人。&lt;/li&gt;
&lt;li&gt;Windows 的刪除與格式化指令、跨磁碟與 UNC 路徑，跟 Unix 同等待遇。&lt;/li&gt;
&lt;li&gt;只有使用者親手切換才能進 full 與 invincible，介面要說清楚 full 對 MCP 工具是全放行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;殘餘的風險我也列在文件裡：變數展開、子殼、直譯器內的檔案操作、&lt;code&gt;tee&lt;/code&gt; 與 &lt;code&gt;cp&lt;/code&gt; 的目的地，這些在 full 模式下會過。開 full 的人要知道自己換到的是什麼。&lt;/p&gt;
&lt;p&gt;改到的檔案：&lt;a href=&quot;https://github.com/juinwei7/Pixel-Crew/blob/main/server/src/dangerousCommand.ts&quot;&gt;dangerousCommand.ts&lt;/a&gt;、&lt;a href=&quot;https://github.com/juinwei7/Pixel-Crew/blob/main/server/src/bashWriteFence.ts&quot;&gt;bashWriteFence.ts&lt;/a&gt;、&lt;a href=&quot;https://github.com/juinwei7/Pixel-Crew/blob/main/server/src/safeLocalPath.ts&quot;&gt;safeLocalPath.ts&lt;/a&gt;，以及對應的測試。發現繞過請走 &lt;a href=&quot;https://github.com/juinwei7/Pixel-Crew/security/advisories/new&quot;&gt;私密弱點回報&lt;/a&gt;，七天內回覆。&lt;/p&gt;
</content:encoded><category>AI 與自動化</category><category>claude-code</category><category>codex</category><category>pixel-crew</category><category>安全</category><author>weiwei</author></item><item><title>Mac 本機轉錄中文會議誰準：Breeze ASR 25、Qwen3-ASR、macOS 內建實測，還有預設為什麼不選最準的</title><link>https://www.weibuilds.com/blog/2026-10-shiyin-local-asr-engines/</link><guid isPermaLink="true">https://www.weibuilds.com/blog/2026-10-shiyin-local-asr-engines/</guid><description>拾音在 Apple Silicon 上本機轉錄會議。六人遠距收音的中文會議，Qwen3-ASR 1.7B 字錯誤率 17.5%，Breeze ASR 25 是 25.7%，macOS 內建約 25%。4.1 版把 Qwen3-ASR 放進設定當選項，預設還是 Breeze，理由跟準確度無關。這篇把資料集、每個引擎的數字、換模型要處理的分段與簡轉繁，還有沒採用的方法一次寫清楚。</description><pubDate>Tue, 27 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;拾音是我做的 Mac 會議紀錄工具，錄 Zoom、Meet、Teams 不用機器人，轉錄在本機跑，不把錄音送出去。本機轉錄的模型從第一版起就是聯發科的 Breeze ASR 25，走 whisper.cpp。10 月 24 日那次評估我把 Qwen3-ASR、TEA-ASR 和 macOS 內建辨識一起拉進來比，結果 Qwen3-ASR 1.7B 在六人遠距收音的中文會議上字錯誤率 17.5%，Breeze 25.7%。4.1 版把 Qwen 放進設定裡當選項，預設沒換。這篇把資料集、數字、換模型要多做的事，還有為什麼預設不選最準的，一次寫清楚。評估腳本和資料集清單都在 &lt;a href=&quot;https://github.com/juinwei7/Shiyin&quot;&gt;公開 repo&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;拿什麼量&quot;&gt;拿什麼量&lt;a href=&quot;#拿什麼量&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;轉錄準不準，要看拿什麼錄音量。乾淨的朗讀句子每個模型都 2% 上下，拉不開差距，會議才會。我用的錄音：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;錄音&lt;/th&gt;
&lt;th&gt;內容&lt;/th&gt;
&lt;th&gt;量什麼&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AISHELL-4 M_R003S01C01&lt;/td&gt;
&lt;td&gt;中文會議，6 人，38 分鐘，遠距麥克風陣列收音&lt;/td&gt;
&lt;td&gt;多人搶話、遠距收音的中文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMI ES2004a&lt;/td&gt;
&lt;td&gt;英文會議，4 人&lt;/td&gt;
&lt;td&gt;英文、同時說話&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TaiMECS&lt;/td&gt;
&lt;td&gt;100 句台灣國語中英夾雜，20 句真人、80 句聲音複製&lt;/td&gt;
&lt;td&gt;中英夾雜的句子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;合成的中英夾雜會議&lt;/td&gt;
&lt;td&gt;91 秒，塞了 6 個產品名、系統名&lt;/td&gt;
&lt;td&gt;專有名詞（關鍵詞）對幾個&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;台大機器學習 2021 課程&lt;/td&gt;
&lt;td&gt;前 15 分鐘&lt;/td&gt;
&lt;td&gt;單人講課；Breeze 和 TEA 都拿它訓練過，只當參考&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日本眾議院 AI 法案專家聽證&lt;/td&gt;
&lt;td&gt;兩段，約 15 分鐘與 10 分鐘，各 6 位說話者&lt;/td&gt;
&lt;td&gt;日文&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;算分之前先正規化：去標點和空白、中文統一成繁體（AISHELL-4 的答案是簡體）、數字寫法和常見異體字統一。中文算字錯誤率（CER），英文算詞錯誤率（WER），另外算插入、刪除、替換各佔多少，才知道錯的是哪一種。下面的表都是去掉語助詞後的數字，因為 Whisper 系的模型會「整理」口語，把「嗯」「那個」丟掉，算進去會把漏字跟聽錯混在一起。含語助詞的原始數字我也留著，AISHELL-4 上 Breeze 約 29%、macOS 內建約 25%、Qwen 約 18%。&lt;/p&gt;
&lt;p&gt;機器是 M3 Pro。評估跑的是 app 真正的轉錄流程，不是另外寫的腳本，所以說話者辨識、分段、詞彙表提示全部照實際使用時的設定來。&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;拾音的逐字稿分頁，左邊是時間點和說話者，右邊是轉錄文字，底下是可以點的波形&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./transcript.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;拾音的逐字稿分頁。示範錄音就是 AMI 的 ES2004a，這篇英文的數字都從它來。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;數字&quot;&gt;數字&lt;a href=&quot;#數字&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;figure&gt;
&lt;svg class=&quot;diagram&quot; viewBox=&quot;0 0 760 352&quot; role=&quot;img&quot; aria-label=&quot;去語助詞後的錯誤率長條圖。AISHELL-4 中文會議：Breeze ASR 25 是 25.7%，Qwen3-ASR 1.7B 拾音實作 17.5%，Qwen3-ASR 0.6B 17.0%，TEA-ASR 1.1 34.8%。AMI 英文會議：Breeze 32.6%，Qwen3-ASR 1.7B 31.3%，Qwen3-ASR 0.6B 32.4%，TEA-ASR 1.1 36.3%。&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
  &lt;g font-family=&quot;inherit&quot; font-size=&quot;13&quot; fill=&quot;var(--ink)&quot;&gt;
    &lt;text x=&quot;16&quot; y=&quot;24&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot;&gt;AISHELL-4 中文會議（6 人、遠距收音）&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;56&quot;&gt;Breeze ASR 25 q8_0&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;42&quot; width=&quot;193&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot;/&gt;
    &lt;text x=&quot;437&quot; y=&quot;56&quot; fill=&quot;var(--ink-2)&quot;&gt;25.7%&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;86&quot;&gt;Qwen3-ASR 1.7B（拾音實作）&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;72&quot; width=&quot;131&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--topic-ai)&quot;/&gt;
    &lt;text x=&quot;375&quot; y=&quot;86&quot; fill=&quot;var(--ink-2)&quot;&gt;17.5%&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;116&quot;&gt;Qwen3-ASR 0.6B（30 秒一段）&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;102&quot; width=&quot;128&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--topic-ai)&quot; opacity=&quot;0.55&quot;/&gt;
    &lt;text x=&quot;372&quot; y=&quot;116&quot; fill=&quot;var(--ink-2)&quot;&gt;17.0%&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;146&quot;&gt;TEA-ASR 1.1（台灣微調版）&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;132&quot; width=&quot;261&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--ink-2)&quot; opacity=&quot;0.45&quot;/&gt;
    &lt;text x=&quot;505&quot; y=&quot;146&quot; fill=&quot;var(--ink-2)&quot;&gt;34.8%&lt;/text&gt;
    &lt;line x1=&quot;16&quot; y1=&quot;172&quot; x2=&quot;744&quot; y2=&quot;172&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;16&quot; y=&quot;200&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot;&gt;AMI 英文會議（4 人）&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;232&quot;&gt;Breeze ASR 25 q8_0&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;218&quot; width=&quot;245&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot;/&gt;
    &lt;text x=&quot;489&quot; y=&quot;232&quot; fill=&quot;var(--ink-2)&quot;&gt;32.6%&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;262&quot;&gt;Qwen3-ASR 1.7B（拾音實作）&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;248&quot; width=&quot;235&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--topic-ai)&quot;/&gt;
    &lt;text x=&quot;479&quot; y=&quot;262&quot; fill=&quot;var(--ink-2)&quot;&gt;31.3%&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;292&quot;&gt;Qwen3-ASR 0.6B（30 秒一段）&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;278&quot; width=&quot;243&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--topic-ai)&quot; opacity=&quot;0.55&quot;/&gt;
    &lt;text x=&quot;487&quot; y=&quot;292&quot; fill=&quot;var(--ink-2)&quot;&gt;32.4%&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;322&quot;&gt;TEA-ASR 1.1（台灣微調版）&lt;/text&gt;
    &lt;rect x=&quot;236&quot; y=&quot;308&quot; width=&quot;272&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--ink-2)&quot; opacity=&quot;0.45&quot;/&gt;
    &lt;text x=&quot;516&quot; y=&quot;322&quot; fill=&quot;var(--ink-2)&quot;&gt;36.3%&lt;/text&gt;
    &lt;text x=&quot;744&quot; y=&quot;346&quot; text-anchor=&quot;end&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;去語助詞後的錯誤率，越短越好&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;figcaption&gt;中文會議拉開 8 個百分點，英文會議大家一樣爛，錯的都是同時說話的地方。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;完整的表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;去語助詞錯誤率&lt;/th&gt;
&lt;th&gt;合成中英夾雜&lt;/th&gt;
&lt;th&gt;TaiMECS&lt;/th&gt;
&lt;th&gt;台大課程&lt;/th&gt;
&lt;th&gt;AISHELL-4&lt;/th&gt;
&lt;th&gt;AMI 英文&lt;/th&gt;
&lt;th&gt;日文聽證&lt;/th&gt;
&lt;th&gt;速度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breeze ASR 25 q8_0&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;3.8&lt;/td&gt;
&lt;td&gt;25.7&lt;/td&gt;
&lt;td&gt;32.6&lt;/td&gt;
&lt;td&gt;9.0（turbo）&lt;/td&gt;
&lt;td&gt;9 倍速&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-ASR 1.7B q8_0，30 秒一段&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;16.2&lt;/td&gt;
&lt;td&gt;31.6&lt;/td&gt;
&lt;td&gt;17.1&lt;/td&gt;
&lt;td&gt;17 倍速&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-ASR 0.6B q8_0，30 秒一段&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;td&gt;6.7&lt;/td&gt;
&lt;td&gt;17.0&lt;/td&gt;
&lt;td&gt;32.4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;36 倍速&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TEA-ASR 1.1 q8_0，30 秒一段&lt;/td&gt;
&lt;td&gt;2.8&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;34.8&lt;/td&gt;
&lt;td&gt;36.3&lt;/td&gt;
&lt;td&gt;22.2&lt;/td&gt;
&lt;td&gt;15 倍速&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-ASR 1.7B，拾音的實作（預設分段）&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;td&gt;6.6&lt;/td&gt;
&lt;td&gt;17.5&lt;/td&gt;
&lt;td&gt;31.3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;12 倍速&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;速度是音訊長度除以轉錄時間，9 倍速就是 38 分鐘的會議轉 4 分多鐘。macOS 內建辨識沒放進表裡，它在 AISHELL-4 上含語助詞約 25%，跟 Breeze 差不多，而且快 160 倍、不用下載模型；但合成的中英夾雜會議裡六個關鍵詞它只對一個，Breeze 加詞彙表對五個。台灣公司的會議就是中英夾雜，所以它一直是選項，不是預設。&lt;/p&gt;
&lt;p&gt;台大課程那欄 Breeze 比 Qwen 好，是因為那份錄音在 Breeze 的訓練資料裡。Qwen 在那裡的「錯」多半是向量名稱的寫法，標準答案寫 a1、a4，它寫 a one、a 四，替換的 136 個字裡約 40 個是這種，不是聽錯。會議的結論以 AISHELL、AMI、TaiMECS 為準。&lt;/p&gt;
&lt;h2 id=&quot;breeze-的錯是哪一種&quot;&gt;Breeze 的錯是哪一種&lt;a href=&quot;#breeze-的錯是哪一種&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;換模型之前先確定不是參數沒調好。用評分腳本的混淆統計看 Breeze 在 AISHELL-4 漏掉的字，35% 落在多人同時說話的地方，另一大塊是語助詞和兒化音，真正聽錯而替換的多是同音字。&lt;/p&gt;
&lt;p&gt;然後掃解碼參數：關掉 context、不做降溫重試、beam 開到 8、提示詞改成自然句。每一種跟預設差距都在 1% 以內，不做降溫重試還讓日文從 9.0% 變差到 12.6%。參數沒有空間了，瓶頸是模型。&lt;/p&gt;
&lt;h2 id=&quot;候選怎麼篩&quot;&gt;候選怎麼篩&lt;a href=&quot;#候選怎麼篩&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;三個候選各有一個致命傷。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TEA-ASR 1.1&lt;/strong&gt; 是台灣微調版，原生輸出繁體，乾淨錄音最好（TaiMECS 2.4%）。但吵雜的多人錄音會跳針，「那那那那」一路到被截斷，還會整段漏，AISHELL-4 34.8%，比 Breeze 還差。Q4_K_M 量化更糟，TaiMECS 掉到 31%。日文會把漢字改成繁體，「国」變「國」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qwen3-ASR 0.6B&lt;/strong&gt; 快到 36 倍速，AISHELL-4 也有 17.0%，但單人講課 6.7%，TaiMECS 2.5%，比 1.7B 差一截。省下的時間在會議紀錄這個場景沒意義，反正轉錄是背景跑的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qwen3-ASR 1.7B&lt;/strong&gt; 完全不跳針，數字最好，兩個問題：只輸出簡體，用 context 怎麼導都導不成繁體；而且不輸出時間點。這兩個問題拾音自己處理，下面兩段講。&lt;/p&gt;
&lt;h2 id=&quot;沒有時間點段落自己切&quot;&gt;沒有時間點，段落自己切&lt;a href=&quot;#沒有時間點段落自己切&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;whisper 的流程是整段丟進去，它吐出帶時間的段落，再拿段落去配說話者。Qwen3-ASR 整段一次轉（官方最長 20 分鐘）出來是一整坨字，沒有時間，說話者標不了。所以順序要反過來，先辨識說話者，再依說話者的片段切段轉錄。&lt;/p&gt;
&lt;p&gt;怎麼切影響很大。我試過的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;照標準答案的說話者片段切：重疊處的話被切掉，AISHELL-4 從 16.2% 退到 22.2%，AMI 45.9%。&lt;/li&gt;
&lt;li&gt;固定長度：10／15／30／60 秒一段，AISHELL-4 是 17.1／16.3／16.2／15.7%，長一點準一點。但 AMI 在 15 秒掉到 39.6%，30 秒是 31.6%。&lt;/li&gt;
&lt;li&gt;在換人處的安靜點切、每段最長 15 秒：AISHELL-4 17.7%，說話者標對 95.5%。AMI 卻是 40.0%，多出來的 228 個字是短而吵的段落自己編出來的整段話，有一段 19 秒冒出一整段天氣預報。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最後一種加上幻覺防線才能用。每一段「有聲音的秒數」能講的字有上限，英文每秒 5 個字加 3，中日文每秒 8 個字加 4，輸出超過就是在編。超過的段落換一組溫度和種子再解一次，兩次相似度低於 0.6 就丟掉，差不多就留短的那次並標低信心。加上這條，AMI 從 40.0% 回到 32.7%，多出的字從 228 個變 18 個。&lt;/p&gt;
&lt;figure&gt;
&lt;svg class=&quot;diagram&quot; viewBox=&quot;0 0 760 300&quot; role=&quot;img&quot; aria-label=&quot;拾音接 Qwen3-ASR 的轉錄流程：解碼整段音訊、先辨識說話者、在換人處的安靜點切段且每段最長 15 秒、逐段轉錄並把詞彙表當 context、跳針檢查（重試、切半、修剪）、幻覺防線（有聲音的秒數限制字數，不一致就丟）、簡轉繁後輸出。&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;qa&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;var(--ink-2)&quot;/&gt;&lt;/marker&gt;
  &lt;/defs&gt;
  &lt;g font-family=&quot;inherit&quot; font-size=&quot;13&quot; fill=&quot;var(--ink)&quot;&gt;
    &lt;rect x=&quot;16&quot; y=&quot;24&quot; width=&quot;160&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;96&quot; y=&quot;47&quot; text-anchor=&quot;middle&quot;&gt;解碼整段音訊&lt;/text&gt;&lt;text x=&quot;96&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;4 小時也先解完&lt;/text&gt;
    &lt;rect x=&quot;208&quot; y=&quot;24&quot; width=&quot;160&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;288&quot; y=&quot;47&quot; text-anchor=&quot;middle&quot;&gt;先辨識說話者&lt;/text&gt;&lt;text x=&quot;288&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;FluidAudio，超過 5 秒才做&lt;/text&gt;
    &lt;rect x=&quot;400&quot; y=&quot;24&quot; width=&quot;160&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--topic-ai)&quot; stroke-width=&quot;1.6&quot;/&gt;
    &lt;text x=&quot;480&quot; y=&quot;47&quot; text-anchor=&quot;middle&quot;&gt;換人處的安靜點切&lt;/text&gt;&lt;text x=&quot;480&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;每段最長 15 秒&lt;/text&gt;
    &lt;rect x=&quot;592&quot; y=&quot;24&quot; width=&quot;152&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;668&quot; y=&quot;47&quot; text-anchor=&quot;middle&quot;&gt;逐段轉錄&lt;/text&gt;&lt;text x=&quot;668&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;詞彙表當 context&lt;/text&gt;
    &lt;line x1=&quot;176&quot; y1=&quot;52&quot; x2=&quot;206&quot; y2=&quot;52&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;line x1=&quot;368&quot; y1=&quot;52&quot; x2=&quot;398&quot; y2=&quot;52&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;line x1=&quot;560&quot; y1=&quot;52&quot; x2=&quot;590&quot; y2=&quot;52&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;path d=&quot;M668 80 V104 H96 V126&quot; fill=&quot;none&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;rect x=&quot;16&quot; y=&quot;130&quot; width=&quot;224&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;128&quot; y=&quot;152&quot; text-anchor=&quot;middle&quot;&gt;跳針檢查&lt;/text&gt;
    &lt;text x=&quot;128&quot; y=&quot;171&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;到 max_tokens、壓縮比 &amp;gt; 2.4&lt;/text&gt;
    &lt;text x=&quot;128&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;同片段連 5 次 → 重試、切半、修剪&lt;/text&gt;
    &lt;rect x=&quot;272&quot; y=&quot;130&quot; width=&quot;224&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--topic-ai)&quot; stroke-width=&quot;1.6&quot;/&gt;
    &lt;text x=&quot;384&quot; y=&quot;152&quot; text-anchor=&quot;middle&quot;&gt;幻覺防線&lt;/text&gt;
    &lt;text x=&quot;384&quot; y=&quot;171&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;有聲音的秒數決定字數上限&lt;/text&gt;
    &lt;text x=&quot;384&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;超過就換種子重解，不像就丟&lt;/text&gt;
    &lt;rect x=&quot;528&quot; y=&quot;130&quot; width=&quot;216&quot; height=&quot;72&quot; rx=&quot;6&quot; fill=&quot;var(--paper-2)&quot; stroke=&quot;var(--rule)&quot;/&gt;
    &lt;text x=&quot;636&quot; y=&quot;152&quot; text-anchor=&quot;middle&quot;&gt;簡轉繁&lt;/text&gt;
    &lt;text x=&quot;636&quot; y=&quot;171&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;OpenCC 字表，最長匹配&lt;/text&gt;
    &lt;text x=&quot;636&quot; y=&quot;190&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;詞組 49,238 條、單字 4,012 條&lt;/text&gt;
    &lt;line x1=&quot;240&quot; y1=&quot;166&quot; x2=&quot;270&quot; y2=&quot;166&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;line x1=&quot;496&quot; y1=&quot;166&quot; x2=&quot;526&quot; y2=&quot;166&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;path d=&quot;M636 202 V232&quot; fill=&quot;none&quot; stroke=&quot;var(--ink-2)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#qa)&quot;/&gt;
    &lt;rect x=&quot;528&quot; y=&quot;236&quot; width=&quot;216&quot; height=&quot;40&quot; rx=&quot;20&quot; fill=&quot;var(--topic-ai)&quot;/&gt;
    &lt;text x=&quot;636&quot; y=&quot;261&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--paper)&quot; font-weight=&quot;600&quot;&gt;帶時間與說話者的段落&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;264&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;whisper 的流程相反：先轉錄拿到時間點，再配說話者。&lt;/text&gt;
    &lt;text x=&quot;16&quot; y=&quot;284&quot; fill=&quot;var(--ink-2)&quot; font-size=&quot;12&quot;&gt;沒有說話者辨識時，從頭每 30 秒在最安靜處切。&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;figcaption&gt;拾音接 Qwen3-ASR 的流程。框線是紫色的兩步決定了 AMI 的數字能不能看。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;還有一條：真的說話者辨識比標準答案粗得多，AISHELL-4 的 38 分鐘只抓到 88 次換人，所以「換人處的安靜點」很少真的切到。補救是一段裡第二多的說話者超過 1.5 秒而且超過這段的 25% 時，照說話者片段的起點再切一次。理想的說話者辨識下 AISHELL-4 說話者標對從 95.5% 到 98.2%，AMI 從 92.4% 到 97.5%，錯誤率各多 0.3 跟 1 個百分點。這是現在的預設。&lt;/p&gt;
&lt;p&gt;跳針的處理放在同一段流程裡。結果到達 max_tokens、壓縮比超過 2.4、或同一個一到六字的片段連續出現五次以上就算壞掉。先加重複懲罰、降溫重試；還壞而且這段超過 6 秒就在最安靜處切成兩半各自再來，最多三層；再壞就留壓縮比較低的那次，連續重複縮成三次，標低信心。&lt;/p&gt;
&lt;h2 id=&quot;簡體轉台灣用字不帶-opencc&quot;&gt;簡體轉台灣用字，不帶 OpenCC&lt;a href=&quot;#簡體轉台灣用字不帶-opencc&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Qwen 只出簡體。ICU 的 Hans-Hant 是逐字轉，「頭發」「皇後」這種一對多的字會錯。帶 OpenCC 的 C++ 程式庫要為 Mac 和 Windows 各包一次。最後是拿 OpenCC 的字表自己寫：詞組表 49,238 條、單字表 4,012 條、台灣異體字 40 條，只留每個鍵的第一個候選，約 1 MB。演算法從左到右，先在詞組表找最長匹配（最長 16 字），沒有就查單字，查不到原樣保留，最後套異體字，「臺」寫成「台」。&lt;/p&gt;
&lt;p&gt;一個要留意的點：鍵和值相同的詞組要保留。「公里」「皇后」這些條目看起來沒用，是拿來擋逐字轉換的，拿掉之後「公里」會變「公裡」。&lt;/p&gt;
&lt;p&gt;用語替換（軟件→軟體）沒做。辨識結果寫的是說話者說出口的詞，人家說「軟件」就寫「軟件」。繁簡也算錯的時候 TaiMECS 2.0%、合成會議 2.0%，還是比原生繁體的 TEA 好。&lt;/p&gt;
&lt;h2 id=&quot;預設為什麼不換&quot;&gt;預設為什麼不換&lt;a href=&quot;#預設為什麼不換&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;數字擺在那裡，Qwen3-ASR 1.7B 在中文會議上少 8 個百分點，速度 12 倍對 9 倍，記憶體 2.6 GB 對 2.8 GB，都比 Breeze 好。&lt;/p&gt;
&lt;p&gt;預設沒換，是因為拾音的使用者是台灣的公司，阿里巴巴的模型在那個環境裡會有顧慮。這是產品定位，跟準確度無關。10 月 24 日決定 Breeze 維持預設，Qwen 放在「設定 → 錄音與轉錄 → 正式轉錄」當第二個選項，說明文字寫明是阿里巴巴開源的模型、全程在這台 Mac 上跑、不連網、要下載約 2.5 GB。沒選它的人不會下載任何東西，流程跟 4.0.0 一模一樣。&lt;/p&gt;
&lt;p&gt;Breeze 還有兩個地方比較好。說話者標對率它是 97.2%／96.1%，Qwen 的實作是 96.3%／93.8%，因為 Qwen 的段落最長 15 秒，比 whisper 的粗，搶話多的會議標錯的比例高一些。另一個是中英夾雜的訓練資料就是針對台灣，單人講課 3.8% 對 6.6%。&lt;/p&gt;
&lt;h2 id=&quot;日文另外一套&quot;&gt;日文另外一套&lt;a href=&quot;#日文另外一套&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;日文會議三個模型都試過。Breeze 會把片假名外來語寫成英文。kotoba-whisper v2.0 在 whisper.cpp 上會亂輸出、整句漏掉。Qwen3-ASR 日文聽證 17.1%。最後用 Whisper large-v3-turbo q5_0（574 MB），國會聽證 9.2%／7.7%，合成會議 4.5%，而且標準答案用的國會會議錄本身刪掉了語助詞，實際錯誤率會比這個低。只有第一次轉錄日文會議時才下載。&lt;/p&gt;
&lt;p&gt;語言偵測也不能用 Breeze，它會把英文判成中文。偵測用 Whisper base q5_1，60 MB，錄音開始後收滿 10 秒有聲音的片段就判一次。&lt;/p&gt;
&lt;h2 id=&quot;詞彙表當提示詞有多大用&quot;&gt;詞彙表當提示詞有多大用&lt;a href=&quot;#詞彙表當提示詞有多大用&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;這是 Breeze 從第一版就在用的做法，詞彙表以「、」隔開塞進 initial_prompt，中文 150 字、英文 400 個字元以內。合成的中英夾雜會議裡六個關鍵詞，加詞彙表對五個，不加的 macOS 內建辨識對一個，CER 3.2% 對 5.8%。Qwen 接上後用同一份詞彙表當 context，上限一樣。&lt;/p&gt;
&lt;h2 id=&quot;試過但沒採用的&quot;&gt;試過但沒採用的&lt;a href=&quot;#試過但沒採用的&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;轉錄前的語音活動偵測（VAD）&lt;/strong&gt;：whisper.cpp 內建的那個，開了之後 AISHELL-4 從 30.5% 惡化到 38.4% 到 47.8%，只有乾淨的合成錄音變好。它想解決的「安靜時憑空產生文字」，事後過濾已經擋掉大部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;麥克風跟電腦聲音分聲道各自轉錄&lt;/strong&gt;：只在戴耳機的線上會議有幫助，錯誤率少約 3 個百分點；有回音時沒有比較好，說話者標對率還從 95% 掉到 79%～88%，速度減半。設定裡沒開放，要測得用 defaults 寫隱藏鍵。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只調 Breeze 的參數&lt;/strong&gt;：上面講過，差不到 1%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;自己重跑&quot;&gt;自己重跑&lt;a href=&quot;#自己重跑&quot; class=&quot;anchor&quot; aria-label=&quot;這一段的連結&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;拾音 DEBUG 版的 &lt;code&gt;shiyinctl transcribe-eval &amp;lt;錄音&amp;gt; &amp;lt;設定.json&amp;gt; &amp;lt;輸出.json&amp;gt;&lt;/code&gt; 走 app 真正的轉錄流程，不建會議、不寫資料庫，設定可以指定引擎、語言、詞彙表、分聲道、回音處理。評分用 &lt;a href=&quot;https://github.com/juinwei7/Shiyin/blob/main/scripts/dev/asr-score.swift&quot;&gt;asr-score.swift&lt;/a&gt;，正規化規則和混淆統計都在裡面。資料集的來源、授權和下載位置在 &lt;a href=&quot;https://github.com/juinwei7/Shiyin/blob/main/docs/design.md&quot;&gt;docs/design.md&lt;/a&gt; 第 32 節。AISHELL-4 和 AMI 都是公開語料，TaiMECS 是 CC BY 4.0。&lt;/p&gt;
&lt;p&gt;數字是 2026 年 10 月 24 日的，模型版本是 Breeze ASR 25 q8_0、Qwen3-ASR 1.7B q8_0（&lt;code&gt;ggml-org/Qwen3-ASR-1.7B-GGUF&lt;/code&gt;）、llama.cpp b8769 之後的版本。模型更新了數字會變，重跑比引用快。&lt;/p&gt;
</content:encoded><category>AI 與自動化</category><category>shiyin</category><category>whisper</category><category>qwen3-asr</category><category>breeze-asr</category><category>語音辨識</category><author>weiwei</author></item></channel></rss>