

<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>max-stackoverflow &#8211; Max的程式語言筆記</title>
	<atom:link href="https://stackoverflow.max-everyday.com/author/max-stackoverflow/feed/" rel="self" type="application/rss+xml" />
	<link>https://stackoverflow.max-everyday.com</link>
	<description>我要當一個豬頭，快樂過每一天</description>
	<lastBuildDate>Mon, 14 Sep 2026 07:31:59 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>

<image>
	<url>https://stackoverflow.max-everyday.com/wp-content/uploads/2017/02/max-stackoverflow-256.png</url>
	<title>max-stackoverflow &#8211; Max的程式語言筆記</title>
	<link>https://stackoverflow.max-everyday.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>在 Azure Container Apps 建立隔離的 Demo MSSQL 資料庫</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-demo-mssql/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-demo-mssql/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 07:31:58 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8767</guid>

					<description><![CDATA[Demo 資料說明 本文是一篇教學用範例。Exa...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-1024x572.jpg?v=1789371090" alt="" class="wp-image-8768" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-1024x572.jpg?v=1789371090 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-600x335.jpg?v=1789371090 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean-767x428.jpg?v=1789371090 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_11674646855586028103_clean.jpg?v=1789371090 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Demo 資料說明</strong></p>



<p class="wp-block-paragraph">本文是一篇教學用範例。Example Company、<code>demoproject</code>、Azure 資源名稱、帳號、網域與 tenant 都是虛構資料，不能直接拿來連線或部署。實際使用時，請替換成自己的環境設定。</p>
</blockquote>



<h2 class="wp-block-heading">先說結論</h2>



<p class="wp-block-paragraph">Example Company 的開發團隊想在 Azure Container Apps 裡快速展示 Backend 功能，但暫時不需要使用既有的 Azure SQL Database，也不需要讀取既有的 <code>demoproject-dev</code> 資料。</p>



<p class="wp-block-paragraph">這種情況可以在同一個 Container Apps Environment 裡，另外部署一個暫時的 Microsoft SQL Server container，提供一個全新的 Demo database。</p>



<p class="wp-block-paragraph">這個方案的重點是：</p>



<ul class="wp-block-list">
<li>Demo SQL Server 是獨立的新資料庫。</li>



<li>Backend 不連既有的 <code>demoproject-dev</code>。</li>



<li>不匯入正式或 staging 資料。</li>



<li>不使用既有 Azure SQL 的 Managed Identity、contained user 或 database roles。</li>



<li>Backend、MSSQL 和 Frontend 仍然要正確設定網路、secret、storage、health check 和 CI/CD。</li>
</ul>



<p class="wp-block-paragraph">換句話說，這不是把原本的 Azure SQL 搬進 container，而是建立一套<strong>隔離、短期、只供 Demo 使用的資料庫環境</strong>。</p>



<h2 class="wp-block-heading">為什麼需要這個方案？</h2>



<p class="wp-block-paragraph">假設目前的開發環境是：</p>



<pre class="wp-block-code"><code>cae-example-dev-001</code></pre>



<p class="wp-block-paragraph">這個 Environment 主要用來執行開發中的 Container App，但沒有連到既有 Azure SQL Private Endpoint 所需的完整 VNet 路徑。</p>



<p class="wp-block-paragraph">如果 Backend 直接使用 Azure SQL，可能會遇到：</p>



<ul class="wp-block-list">
<li>SQL network connection timeout。</li>



<li>Private Endpoint 無法連線。</li>



<li>Azure SQL firewall 拒絕來源 IP。</li>



<li>Managed Identity 可以取得 token，但 SQL database 沒有對應的 user 或 roles。</li>
</ul>



<p class="wp-block-paragraph">可是，這次 Demo 的需求並不是要存取既有資料，而是希望先讓團隊能夠：</p>



<ol class="wp-block-list">
<li>啟動 Backend API。</li>



<li>建立一些測試資料。</li>



<li>展示查詢、新增、修改等基本功能。</li>



<li>讓 Frontend 能呼叫 Backend。</li>
</ol>



<p class="wp-block-paragraph">因此，可以把資料庫改成和 Backend 位於同一個 Container Apps Environment 的內部 MSSQL App，讓這次 Demo 不依賴既有 Azure SQL 的 Private Endpoint。</p>



<h2 class="wp-block-heading">目標架構</h2>



<p class="wp-block-paragraph">Demo 環境可以設計成下面這樣：</p>



<pre class="wp-block-code"><code>Frontend 或測試工具
          │
          ▼
ca-example-backend-dev-001
          │ internal connection
          ▼
ca-example-mssql-demo-dev-001
          │ TCP 1433，僅限 internal ingress
          ▼
demoproject-demo</code></pre>



<p class="wp-block-paragraph">示範資源如下：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>元件</th><th>Demo 名稱</th><th>用途</th></tr></thead><tbody><tr><td>Container Apps Environment</td><td><code>cae-example-dev-001</code></td><td>執行所有 Demo App</td></tr><tr><td>Backend App</td><td><code>ca-example-backend-dev-001</code></td><td>提供 API</td></tr><tr><td>MSSQL App</td><td><code>ca-example-mssql-demo-dev-001</code></td><td>提供 Demo database</td></tr><tr><td>Database</td><td><code>demoproject-demo</code></td><td>全新、隔離的測試資料</td></tr><tr><td>Resource Group</td><td><code>rg-demo-dev-001</code></td><td>存放 Demo Container Apps</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">MSSQL 應該是獨立的 Container App，不要和 Backend 放在同一個 container。這樣可以分別更新 Backend 和資料庫，也比較容易在 Demo 結束時清理。</p>



<h2 class="wp-block-heading">需要怎麼做？</h2>



<h3 class="wp-block-heading">第一步：建立獨立的 MSSQL Container App</h3>



<p class="wp-block-paragraph">使用官方 Microsoft SQL Server Linux container image，並固定版本或 digest，不要長期使用會隨時間變動的 <code>latest</code> tag。</p>



<p class="wp-block-paragraph">MSSQL App 的基本設定應包含：</p>



<pre class="wp-block-code"><code>App name:    ca-example-mssql-demo-dev-001
Environment: cae-example-dev-001
Port:        TCP 1433
Ingress:     internal only
Database:    demoproject-demo</code></pre>



<p class="wp-block-paragraph">Container 需要設定：</p>



<ul class="wp-block-list">
<li><code>ACCEPT_EULA=Y</code></li>



<li><code>MSSQL_PID=Developer</code></li>



<li><code>MSSQL_SA_PASSWORD</code></li>



<li>足夠的 CPU 和 memory</li>



<li>TCP <code>1433</code> target port</li>



<li>readiness/startup health check</li>
</ul>



<p class="wp-block-paragraph">SQL Server container 通常至少需要 2 GiB memory；Demo 建議依實際啟動時間與資料量配置更多資源。</p>



<p class="wp-block-paragraph"><code>MSSQL_PID=Developer</code> 只適合非正式 Demo，不能當成 production license 或正式 staging database。SA password 不可以寫在 Dockerfile、image、repository 或 workflow 明文中。</p>



<h3 class="wp-block-heading">第二步：只開放內部網路</h3>



<p class="wp-block-paragraph">MSSQL App 只能使用 internal ingress：</p>



<pre class="wp-block-code"><code>Backend App ── internal DNS ──&gt; MSSQL App:1433</code></pre>



<p class="wp-block-paragraph">不要把 SQL Server 的 TCP <code>1433</code> 開成 public ingress，也不要讓測試人員直接從 Internet 連入。</p>



<p class="wp-block-paragraph">Backend 和 MSSQL App 最簡單的做法是放在同一個 <code>cae-example-dev-001</code>。Backend 使用 MSSQL App 的 internal FQDN 連線，不要自行猜測 FQDN 格式，應從 Azure Container Apps 的 App 設定或 DNS 設定取得實際值。</p>



<h3 class="wp-block-heading">第三步：建立專用 application login</h3>



<p class="wp-block-paragraph">Backend 不應使用 <code>sa</code> 登入。MSSQL container 第一次啟動後，應執行一次初始化流程：</p>



<ol class="wp-block-list">
<li>建立 <code>demoproject-demo</code> database。</li>



<li>建立專用的 Backend application login。</li>



<li>只授予 application login 必要的 read/write 權限。</li>



<li>只有需要執行 schema migration 時，才授予有限的 DDL 權限。</li>



<li>確認 Backend 不會取得 SA password。</li>
</ol>



<p class="wp-block-paragraph">這個 Demo SQL Server 是獨立的 SQL container，Backend 通常會使用 SQL login/password，而不是既有 Azure SQL 的 Microsoft Entra Managed Identity flow。</p>



<p class="wp-block-paragraph">因此，application login 的 username 和 password 必須放在 Container App secret 或 Key Vault secret reference 中，不能放在 source code 或公開設定檔。</p>



<h3 class="wp-block-heading">第四步：修改 Backend 的 connection string</h3>



<p class="wp-block-paragraph">Backend 原本如果使用 Azure SQL Managed Identity，可能會包含這類設定：</p>



<pre class="wp-block-code"><code>fedauth=ActiveDirectoryManagedIdentity
useMsi=true</code></pre>



<p class="wp-block-paragraph">切換到 Demo MSSQL 後，Backend 必須改成：</p>



<ul class="wp-block-list">
<li>SQL host：<code>ca-example-mssql-demo-dev-001</code> 的 internal FQDN。</li>



<li>Port：<code>1433</code>。</li>



<li>Database：<code>demoproject-demo</code>。</li>



<li>Username：專用 application login。</li>



<li>Password：由 secret reference 注入。</li>



<li>Authentication：使用 SQL login/password。</li>
</ul>



<p class="wp-block-paragraph">概念上的設定如下：</p>



<pre class="wp-block-code"><code>sqlserver://&lt;mssql-internal-host&gt;:1433
  ?database=demoproject-demo
  &amp;user=&lt;application-user&gt;
  &amp;password=&lt;secret-reference&gt;</code></pre>



<p class="wp-block-paragraph">實際格式要依 Backend 使用的 SQL driver 調整。不要把完整 connection string 寫進 GitHub Actions log、Dockerfile 或公開的 frontend configuration。</p>



<p class="wp-block-paragraph">如果 Demo SQL Server 使用 self-signed certificate，請明確決定測試環境的 TLS 設定。即使是 Demo，也不要把關閉加密或無條件信任憑證當成正式環境的解法。</p>



<h3 class="wp-block-heading">第五步：建立 schema 和測試資料</h3>



<p class="wp-block-paragraph">全新的 <code>demoproject-demo</code> 不會自動有任何 table，因此需要另外準備：</p>



<ul class="wp-block-list">
<li>schema migration。</li>



<li>初始化 seed data。</li>



<li>Demo 使用者和測試帳號。</li>



<li>API 所需的 reference data。</li>



<li>migration 失敗時的處理方式。</li>
</ul>



<p class="wp-block-paragraph">建議使用一次性的 bootstrap 或 job 初始化 database，不要讓每一個 Backend revision 在啟動時同時執行完整 migration，否則可能發生 migration race condition。</p>



<p class="wp-block-paragraph">Demo data 應該是新建或去識別化資料，不要將正式資料直接複製到這個 container。</p>



<h3 class="wp-block-heading">第六步：處理資料持久化</h3>



<p class="wp-block-paragraph">Container App 的 local filesystem 不適合當成可靠的 database storage。如果沒有持久化 volume，以下情況都可能讓資料消失：</p>



<ul class="wp-block-list">
<li>Container restart。</li>



<li>Revision replacement。</li>



<li>App 被重新建立。</li>



<li>Node 或 host 發生故障。</li>
</ul>



<p class="wp-block-paragraph">如果 Demo 只做一次性展示，可以接受資料在環境重建後消失；如果需要保留測試結果，至少要：</p>



<ul class="wp-block-list">
<li>將 SQL Server data/log 目錄掛到持久化 storage，例如 Azure Files。</li>



<li>驗證 SQL Server 在該 storage 上的 lock、I/O 和 recovery 行為。</li>



<li>設定 backup，並將 backup 放到獨立的儲存位置。</li>



<li>訂定資料保存期限。</li>



<li>確認刪除 App 或 volume 時，不會誤刪唯一一份資料。</li>
</ul>



<p class="wp-block-paragraph">即使使用 Azure Files，這仍然不等同於 Azure SQL Database 的 HA、backup、patching、SLA 和資料庫服務保證，所以必須把它標示為 <code>temporary demo database</code>。</p>



<h3 class="wp-block-heading">第七步：把資料庫 bootstrap 和一般 CI/CD 分開</h3>



<p class="wp-block-paragraph">MSSQL App 不應該在每次 Backend push 時被刪除或重建。建議把流程分成兩部分。</p>



<p class="wp-block-paragraph"><strong>一次性的 Demo bootstrap：</strong></p>



<ul class="wp-block-list">
<li>建立或更新 <code>ca-example-mssql-demo-dev-001</code>。</li>



<li>設定 image、secret、storage、internal ingress 和 health check。</li>



<li>等待 SQL Server ready。</li>



<li>建立 <code>demoproject-demo</code>、application login、schema 和 seed data。</li>
</ul>



<p class="wp-block-paragraph"><strong>一般 Backend CI/CD：</strong></p>



<ul class="wp-block-list">
<li>編譯 Backend。</li>



<li>Push Backend image 到 ACR。</li>



<li>更新 <code>ca-example-backend-dev-001</code>。</li>



<li>保留 MSSQL App 和 volume，不要重新初始化 database。</li>
</ul>



<p class="wp-block-paragraph">Backend runtime 仍然需要自己的 ACR <code>AcrPull</code>。GitHub OIDC Service Principal 只負責 CI/CD 所需的 <code>AcrPush</code> 和 App deployment 權限，不需要 SQL database role。</p>



<p class="wp-block-paragraph">如果 MSSQL image 直接從 MCR pull，必須確認 Environment 可以連到 MCR。如果團隊將 MSSQL image mirror 到 ACR，則 MSSQL App 的 runtime identity 也要另外授予 ACR <code>AcrPull</code>。</p>



<h3 class="wp-block-heading">第八步：處理啟動順序和健康檢查</h3>



<p class="wp-block-paragraph">SQL Server container 通常比 API container 更久才會 ready，因此 Backend 不能假設 SQL 在啟動第一秒就能連線。</p>



<p class="wp-block-paragraph">需要加入：</p>



<ul class="wp-block-list">
<li>MSSQL readiness/health check。</li>



<li>Backend 的 connection retry 和 backoff。</li>



<li>將「SQL 尚未 ready」與「帳號密碼錯誤」分開記錄。</li>



<li>SQL container restart 後，Backend 自動恢復連線。</li>



<li>避免在 logs 印出 password、完整 connection string 或 token。</li>
</ul>



<p class="wp-block-paragraph"><code>az containerapp update</code> 成功只代表 Azure 接受了 App 設定，不代表 Backend 一定已經能連到 MSSQL。必須等兩個 App 都 healthy 後，再測試 API。</p>



<h2 class="wp-block-heading">Frontend 要調整什麼？</h2>



<p class="wp-block-paragraph">Frontend 不需要 SQL 權限，也不應取得 Backend 的 SQL credentials。</p>



<p class="wp-block-paragraph">Frontend 需要確認：</p>



<ul class="wp-block-list">
<li>使用正確的 Frontend image。</li>



<li>使用新的 Backend API URL。</li>



<li>Nginx 或其他 web server 沒有寫死舊的 Backend hostname。</li>



<li>只有必要的 frontend secrets 被注入。</li>
</ul>



<p class="wp-block-paragraph">Frontend 只呼叫 Backend API；SQL connection 由 Backend 負責。</p>



<h2 class="wp-block-heading">安全設定</h2>



<p class="wp-block-paragraph">這個 Demo 方案至少要符合以下原則：</p>



<ul class="wp-block-list">
<li>MSSQL App 只有 internal ingress。</li>



<li>不對 Internet 公開 TCP <code>1433</code>。</li>



<li>SA password 和 application password 都放在 secret。</li>



<li>Backend 使用專用 application login，不使用 SA。</li>



<li>不把 SQL credentials 寫入 Git repository。</li>



<li>不讓 Frontend 取得 SQL credentials。</li>



<li>監控 container restart、CPU、memory、storage 和 backup。</li>



<li>為 Demo 設定 owner、用途和到期日。</li>



<li>Demo 結束後移除 App、secret、volume 和 backup。</li>
</ul>



<h2 class="wp-block-heading">驗證清單</h2>



<p class="wp-block-paragraph">部署完成後，依照以下順序驗證：</p>



<ol class="wp-block-list">
<li>MSSQL App revision 是 <code>Healthy / Running</code>。</li>



<li>Backend 可以解析 MSSQL App 的 internal FQDN。</li>



<li>Backend 可以建立 TCP <code>1433</code> connection。</li>



<li><code>demoproject-demo</code> database 存在。</li>



<li>Backend application login 可以登入。</li>



<li>schema migration 和 seed data 成功。</li>



<li>Backend API 可以正常讀寫 Demo database。</li>



<li>Backend 重啟後仍然可以重新連線。</li>



<li>MSSQL container 重啟後，volume 和 database recovery 正常。</li>



<li>Frontend 使用正確的 Backend URL。</li>



<li>沒有任何元件對 Internet 公開 SQL <code>1433</code>。</li>
</ol>



<h2 class="wp-block-heading">這個方案解決什麼、不解決什麼？</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>問題</th><th>隔離 Demo MSSQL 方案</th></tr></thead><tbody><tr><td>需要一個全新的短期測試 database</td><td>可以解決</td></tr><tr><td>不想依賴既有 Azure SQL Private Endpoint</td><td>可以避開這個依賴</td></tr><tr><td>必須使用既有 <code>demoproject-dev</code> 資料</td><td>不適用</td></tr><tr><td>需要 Azure SQL Managed Identity authentication</td><td>不會自動保留，通常要改成 SQL login</td></tr><tr><td>需要正式資料庫的 HA、backup、SLA</td><td>不適用</td></tr><tr><td>Frontend 是否需要 SQL 權限</td><td>仍然不需要</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Demo 結束後</h2>



<p class="wp-block-paragraph">這個方案應該被明確記錄為：</p>



<pre class="wp-block-code"><code>temporary isolated demo database</code></pre>



<p class="wp-block-paragraph">Demo 結束時，依序完成：</p>



<ol class="wp-block-list">
<li>匯出真正需要保留的 Demo 結果。</li>



<li>停止不再使用的 Backend 和 Frontend revision。</li>



<li>移除 SQL secrets 和 application credentials。</li>



<li>備份或刪除 Demo database volume。</li>



<li>刪除 MSSQL Container App。</li>



<li>清理 ACR 中不再使用的 Demo image。</li>



<li>確認後續部署不會誤連到 <code>demoproject-demo</code>。</li>
</ol>



<h2 class="wp-block-heading">總結</h2>



<p class="wp-block-paragraph">在 <code>cae-example-dev-001</code> 自架 MSSQL container，可以讓 Example Company 快速得到一個不依賴既有 Azure SQL 的隔離 Demo database。這個方法適合短期展示和功能驗證，但不是把正式資料庫搬進 Container App，也不是正式 production database 的替代品。</p>



<p class="wp-block-paragraph">真正需要完成的工作不只是部署一個 MSSQL image，還包括：</p>



<ul class="wp-block-list">
<li>建立獨立的 MSSQL App。</li>



<li>只開放 internal TCP <code>1433</code>。</li>



<li>建立專用 application login。</li>



<li>將 Backend connection string 改成新的 Demo database。</li>



<li>初始化 schema 和 seed data。</li>



<li>決定是否需要持久化 storage 和 backup。</li>



<li>將 database bootstrap 與一般 Backend CI/CD 分開。</li>



<li>加入 health check、connection retry 和 logs 保護。</li>



<li>在 Demo 結束後完整清理資源。</li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-demo-mssql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Container Apps CI/CD 部署與權限配置</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-ci-cd/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-ci-cd/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 06:51:07 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8762</guid>

					<description><![CDATA[Demo 資料說明 這是一份可以單獨閱讀的教學用...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="626" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-1024x626.jpg?v=1789368643" alt="" class="wp-image-8763" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-1024x626.jpg?v=1789368643 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-600x367.jpg?v=1789368643 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean-767x469.jpg?v=1789368643 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_15779193270832496688_clean.jpg?v=1789368643 1308w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Demo 資料說明</strong></p>



<p class="wp-block-paragraph">這是一份可以單獨閱讀的教學用範例。所有專案名稱、公司名稱、GitHub repository、Azure 資源名稱、網域、帳號與 tenant 名稱都是虛構資料，不能直接拿來連線或部署。請依照實際環境替換成自己的設定。</p>
</blockquote>



<h2 class="wp-block-heading">這份文件要解決什麼問題</h2>



<p class="wp-block-paragraph">Example Company 的開發團隊希望使用 GitHub Actions 做 Azure CI/CD，流程如下：</p>



<ol class="wp-block-list">
<li>GitHub Actions 將程式編譯成 container image。</li>



<li>將 image push 到 Azure Container Registry（ACR）。</li>



<li>更新 Azure Container App，讓 App 使用新的 image。</li>



<li>Backend 啟動後，透過 Microsoft Entra Managed Identity 連線到 Azure SQL Database。</li>
</ol>



<p class="wp-block-paragraph">目前遇到的問題通常會長這樣：</p>



<ul class="wp-block-list">
<li>GitHub Actions 可以登入 Azure，但 Container App 啟動時無法從 ACR 拉 image。</li>



<li>Container App 的部署指令顯示成功，但新的 revision 變成 <code>ActivationFailed</code>。</li>



<li>Backend image 可以啟動，卻因為沒有 SQL contained user 或 database role 而無法登入資料庫。</li>



<li>App 位於沒有 VNet 路徑的 Container Apps Environment，因此連不到 SQL Private Endpoint。</li>



<li>Frontend 還在使用舊的 Harbor image 或舊的 Backend hostname。</li>



<li>舊的部署系統與 Azure GitHub Actions 同時執行，造成兩邊互相覆蓋部署結果。</li>
</ul>



<p class="wp-block-paragraph">這些現象看起來都像「權限問題」，但實際上分屬四個不同層次：</p>



<ol class="wp-block-list">
<li>GitHub Actions 的 CI 身分能不能登入 Azure、push image、更新 App。</li>



<li>Container App 執行時的 Managed Identity 能不能從 ACR pull image。</li>



<li>Backend 的 Managed Identity 能不能登入 SQL Database。</li>



<li>Container Apps Environment 是否有通往 SQL Private Endpoint 的網路路徑。</li>
</ol>



<h2 class="wp-block-heading">Demo 情境與資源</h2>



<p class="wp-block-paragraph">以下名稱全部是示意資料：</p>



<ul class="wp-block-list">
<li>公司：<strong>Example Company</strong></li>



<li>GitHub organization/repository：<code>example-company/demo-backend</code></li>



<li>Subscription：<code>Demo-Subscription</code></li>



<li>App/ACR Resource Group：<code>rg-demo-stg-001</code></li>



<li>共用 Container Apps Environment：<code>cae-demo-stg-001</code></li>



<li>共用 CAE 所在 Resource Group：<code>rg-cae-demo-stg-001</code></li>



<li>ACR：<code>acrdemostg001.azurecr.io</code></li>



<li>SQL Server：<code>sql-demo-stg-001.database.windows.net</code></li>



<li>SQL Database：<code>demoproject-dev</code></li>



<li>SQL Private Endpoint：<code>pe-sql-demo-stg-001</code></li>



<li>VNet：<code>vnet-demo-stg-001</code></li>
</ul>



<p class="wp-block-paragraph">預期使用的兩個 Container App 是：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>App</th><th>用途</th><th>Environment</th></tr></thead><tbody><tr><td><code>ca-demo-backend-dev-001</code></td><td>Backend API</td><td><code>cae-demo-stg-001</code></td></tr><tr><td><code>ca-demo-frontend-dev-001</code></td><td>Frontend</td><td><code>cae-demo-stg-001</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">另外，情境中有一組不建議繼續使用的舊 App：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>App</th><th>舊 Environment</th><th>舊 image registry</th><th>問題</th></tr></thead><tbody><tr><td><code>ca-example-backend-dev-001</code></td><td><code>cae-example-dev-001</code></td><td><code>harbor.example.invalid</code></td><td>不在共用 CAE，也沒有正確接上 Azure ACR</td></tr><tr><td>舊 Frontend App</td><td><code>cae-example-dev-001</code></td><td>舊 Harbor</td><td>image 或 Nginx 設定可能仍指向舊 Backend</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">正確的目標架構</h2>



<p class="wp-block-paragraph">完整的部署與連線路徑應該是：</p>



<pre class="wp-block-code"><code>GitHub Actions
    │ OIDC，取得短期 token
    ▼
GitHub CI Service Principal
    │ AcrPush + Container App update
    ▼
Azure Container Registry
    │ image push
    ▼
Container App（共用 cae-demo-stg-001）
    │ App 自己的 system-assigned Managed Identity
    ├─ ACR AcrPull
    ├─ Key Vault secret get/list（如果 App 使用 Key Vault）
    └─ Azure SQL Entra token
          │
          ▼
    VNet + Private DNS + SQL Private Endpoint
          │
          ▼
    demoproject-dev</code></pre>



<p class="wp-block-paragraph">最重要的觀念是：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">GitHub Actions 的 Service Principal 負責部署，不是執行中的 App。<br>真正需要 pull image 和連 SQL 的，是各個 Container App 自己的 Managed Identity。</p>
</blockquote>



<h2 class="wp-block-heading">CI/CD 的正確流程</h2>



<h3 class="wp-block-heading">1. GitHub Actions 使用 OIDC 登入 Azure</h3>



<p class="wp-block-paragraph">GitHub Actions 不應把長期的 Azure client secret 放在 repository。建議使用 OIDC：</p>



<pre class="wp-block-code"><code>- name: Login to Azure
  uses: azure/login@v2
  with:
    client-id: ${{ vars.AZURE_CLIENT_ID }}
    tenant-id: ${{ vars.AZURE_TENANT_ID }}
    subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}</code></pre>



<p class="wp-block-paragraph">示範用的 Federated Credential subject 是：</p>



<pre class="wp-block-code"><code>repo:example-company/demo-backend:ref:refs/heads/main</code></pre>



<p class="wp-block-paragraph">如果 workflow 使用 GitHub Environment，例如 <code>staging</code>，subject 和 variables 的 scope 都要跟 GitHub Environment 的設定一致，不能只改其中一邊。</p>



<h3 class="wp-block-heading">2. CI 身分將 image push 到 ACR</h3>



<p class="wp-block-paragraph">示範用的 image repository 是：</p>



<pre class="wp-block-code"><code>acrdemostg001.azurecr.io/demo-backend:&lt;github.sha&gt;</code></pre>



<p class="wp-block-paragraph">GitHub CI Service Principal 需要 <code>AcrPush</code>，但不需要 runtime <code>AcrPull</code>。</p>



<pre class="wp-block-code"><code>AcrPush = GitHub Actions 將 image 推進 ACR
AcrPull = Container App 啟動時從 ACR 拉出 image</code></pre>



<p class="wp-block-paragraph"><code>demo-backend</code> 在第一次成功 <code>docker push</code> 前可能還不會出現在 ACR repository list 中。ACR 通常會在第一次 push 時建立 repository，這不是權限錯誤。</p>



<h3 class="wp-block-heading">3. CI 身分更新正確的 Container App</h3>



<p class="wp-block-paragraph">部署 Backend 時，target 應該是位於共用 CAE 的 App：</p>



<pre class="wp-block-code"><code>az containerapp update \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --image acrdemostg001.azurecr.io/demo-backend:${GITHUB_SHA}</code></pre>



<p class="wp-block-paragraph">這個指令成功只代表 Azure 接受了 App 設定更新，不代表新的 revision 一定能啟動。接下來還要檢查 runtime identity 是否能 pull image。</p>



<h3 class="wp-block-heading">4. Container App 使用自己的 Managed Identity pull image</h3>



<p class="wp-block-paragraph">Backend 和 Frontend 都要各自完成以下設定：</p>



<ul class="wp-block-list">
<li>啟用 system-assigned Managed Identity。</li>



<li>將該 App 的 identity 在 ACR scope 授予 <code>AcrPull</code>。</li>



<li>將 Container App registry authentication 設為 <code>identity: system</code>。</li>
</ul>



<p class="wp-block-paragraph">可用以下指令設定 registry identity：</p>



<pre class="wp-block-code"><code>az containerapp registry set \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --server acrdemostg001.azurecr.io \
  --identity system</code></pre>



<p class="wp-block-paragraph">Frontend App 也要使用相同方式設定，只是 <code>--name</code> 改為 <code>ca-demo-frontend-dev-001</code>。</p>



<p class="wp-block-paragraph">檢查 App identity、Environment、registry 與 image：</p>



<pre class="wp-block-code"><code>az containerapp show \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --query "{identity:identity,environment:properties.managedEnvironmentId,registries:properties.configuration.registries&#91;].{server:server,identity:identity},images:properties.template.containers&#91;].image}" \
  -o json</code></pre>



<p class="wp-block-paragraph">檢查 runtime identity 是否有 ACR <code>AcrPull</code>：</p>



<pre class="wp-block-code"><code>az role assignment list \
  --assignee-object-id &lt;BACKEND_APP_PRINCIPAL_ID&gt; \
  --scope "$(az acr show \
    --resource-group rg-demo-stg-001 \
    --name acrdemostg001 \
    --query id -o tsv)" \
  --query "&#91;].{role:roleDefinitionName,scope:scope}" \
  -o table</code></pre>



<p class="wp-block-paragraph">Frontend 使用自己的 <code>&lt;FRONTEND_APP_PRINCIPAL_ID&gt;</code> 查詢，不要共用 Backend 的 identity。</p>



<p class="wp-block-paragraph">如果 App 沒有 <code>AcrPull</code>，常見結果是：</p>



<ul class="wp-block-list">
<li><code>az containerapp update</code> 指令本身成功。</li>



<li>revision 建立了，但狀態是 <code>ActivationFailed</code>。</li>



<li>logs 或 revision events 出現 image pull、registry authentication 或 unauthorized 錯誤。</li>
</ul>



<p class="wp-block-paragraph">這時應該補在 <strong>Container App 的 Managed Identity</strong>，不能把 <code>AcrPull</code> 加到 GitHub OIDC Service Principal 代替。</p>



<h2 class="wp-block-heading">Backend 存取 SQL Database</h2>



<h3 class="wp-block-heading">SQL 身分的分工</h3>



<p class="wp-block-paragraph">Backend 不應把 SQL username/password 寫進 GitHub Secrets 或 container environment。建議使用：</p>



<ol class="wp-block-list">
<li>Backend App 的 system-assigned Managed Identity 取得 Microsoft Entra token。</li>



<li>SQL Entra administrator 在 <code>demoproject-dev</code> 建立對應的 contained user。</li>



<li>只授予 Backend 實際需要的 database roles。</li>
</ol>



<p class="wp-block-paragraph">示範用的 database URL 概念如下：</p>



<pre class="wp-block-code"><code>sqlserver://sql-demo-stg-001.database.windows.net:1433
  ?database=demoproject-dev
  &amp;encrypt=true
  &amp;trustservercertificate=false
  &amp;fedauth=ActiveDirectoryManagedIdentity
  &amp;useMsi=true</code></pre>



<h3 class="wp-block-heading">建立 contained user 與 database roles</h3>



<p class="wp-block-paragraph">以下 SQL 必須由 SQL Microsoft Entra administrator 或有足夠 database 權限的管理者，在 <code>demoproject-dev</code> database 執行：</p>



<pre class="wp-block-code"><code>CREATE USER &#91;ca-demo-backend-dev-001] FROM EXTERNAL PROVIDER;</code></pre>



<p class="wp-block-paragraph">再依照應用程式需要授予角色：</p>



<pre class="wp-block-code"><code>ALTER ROLE &#91;db_datareader]
ADD MEMBER &#91;ca-demo-backend-dev-001];

ALTER ROLE &#91;db_datawriter]
ADD MEMBER &#91;ca-demo-backend-dev-001];

-- 只有需要由 App 執行 schema migration 時才授予
ALTER ROLE &#91;db_ddladmin]
ADD MEMBER &#91;ca-demo-backend-dev-001];</code></pre>



<p class="wp-block-paragraph">這些是 SQL Database 裡的 data-plane permissions，不是 Azure Resource Group RBAC。因此 <code>az role assignment list</code> 看不到 <code>db_datareader</code>、<code>db_datawriter</code> 或 <code>db_ddladmin</code> membership。</p>



<p class="wp-block-paragraph">可以在 SQL 中確認：</p>



<pre class="wp-block-code"><code>SELECT
    dp.name AS database_principal,
    rp.name AS database_role
FROM sys.database_role_members AS drm
JOIN sys.database_principals AS rp
    ON rp.principal_id = drm.role_principal_id
JOIN sys.database_principals AS dp
    ON dp.principal_id = drm.member_principal_id
WHERE dp.name = N'ca-demo-backend-dev-001';</code></pre>



<p class="wp-block-paragraph">Frontend 不需要 SQL 權限，也不應為 Frontend 建立 SQL contained user。</p>



<h2 class="wp-block-heading">SQL 網路路徑</h2>



<p class="wp-block-paragraph">即使 SQL user/roles 都正確，網路不通時仍然無法連線。</p>



<p class="wp-block-paragraph">Demo 情境中的網路設計是：</p>



<ul class="wp-block-list">
<li><code>pe-sql-demo-stg-001</code> 的狀態為 Approved。</li>



<li>Private DNS zone <code>privatelink.database.windows.net</code> 連結到 <code>vnet-demo-stg-001</code>。</li>



<li>共用 <code>cae-demo-stg-001</code> 有正確的 VNet/infrastructure subnet 路徑。</li>



<li>App 放在共用 CAE，才能透過 Private Endpoint 解析並連線到 SQL。</li>
</ul>



<p class="wp-block-paragraph">舊的 <code>cae-example-dev-001</code> 沒有這條 infrastructure subnet 路徑，因此不適合作為需要存取 Private SQL 的 target。</p>



<p class="wp-block-paragraph">看到下面的錯誤時，優先檢查 CAE、VNet、Private DNS 和 Private Endpoint：</p>



<pre class="wp-block-code"><code>Client with IP address ... is not allowed to access the server</code></pre>



<p class="wp-block-paragraph">不要為了繞過問題，直接把 SQL firewall 開成 <code>0.0.0.0</code>。這會把網路問題變成不必要的公開暴露。</p>



<p class="wp-block-paragraph">看到下面的錯誤時，優先檢查 Managed Identity、contained user 和 database roles：</p>



<pre class="wp-block-code"><code>Login failed</code></pre>



<h2 class="wp-block-heading">開發者帳號能做什麼</h2>



<p class="wp-block-paragraph">示範開發者帳號是 <code>demo.user@example.com</code>，在 demo tenant 中的 UPN 示意如下：</p>



<pre class="wp-block-code"><code>demo_user_example.com#EXT#@exampletenant.onmicrosoft.com</code></pre>



<p class="wp-block-paragraph">示範直接授予的 Azure roles：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Scope</th><th>Role</th><th>可以做什麼</th></tr></thead><tbody><tr><td><code>rg-demo-stg-001</code></td><td><code>Contributor</code></td><td>在 App RG 建立、更新、刪除 Container App</td></tr><tr><td><code>rg-demo-stg-001</code></td><td><code>Reader</code></td><td>讀取 RG 資源</td></tr><tr><td><code>acrdemostg001</code></td><td><code>AcrPush</code></td><td>Push image 到 ACR</td></tr><tr><td><code>cae-demo-stg-001</code></td><td><code>Reader</code></td><td>讀取共用 CAE 設定</td></tr><tr><td><code>cae-demo-stg-001</code></td><td><code>Container Apps Environment Joiner</code></td><td>建立 App 時加入共用 CAE</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">因此，這個帳號可以：</p>



<ul class="wp-block-list">
<li>建立或更新 App。</li>



<li>將 App 放進共用 <code>cae-demo-stg-001</code>。</li>



<li>Push image 到 ACR。</li>
</ul>



<p class="wp-block-paragraph">但這個帳號不能：</p>



<ul class="wp-block-list">
<li>建立 Azure RBAC role assignment。</li>



<li>替新的 App 授予 ACR <code>AcrPull</code>。</li>



<li>修改 SQL Private Endpoint、VNet 或 SQL firewall。</li>



<li>在 <code>demoproject-dev</code> 建立 Entra contained user。</li>



<li>授予或修改 SQL database roles。</li>
</ul>



<p class="wp-block-paragraph">這是刻意的權限邊界：開發者可以部署應用程式，但不能自行擴大 App 的 Azure 權限或資料庫權限。需要新增 <code>AcrPull</code> 或 SQL user/roles 時，應請平台管理者或 SQL Entra administrator 處理。</p>



<h2 class="wp-block-heading">舊 App 如何切換到共用 CAE</h2>



<p class="wp-block-paragraph">Azure Container Apps 沒有提供用 <code>az containerapp update</code> 直接切換 Environment 的參數。既有 App 不能像更新 image 一樣，直接從：</p>



<pre class="wp-block-code"><code>cae-example-dev-001</code></pre>



<p class="wp-block-paragraph">搬到：</p>



<pre class="wp-block-code"><code>cae-demo-stg-001</code></pre>



<p class="wp-block-paragraph">較安全的切換方式是：</p>



<ol class="wp-block-list">
<li>在共用 CAE 建立新的 Container App，或使用已準備好的 <code>ca-demo-backend-dev-001</code> / <code>ca-demo-frontend-dev-001</code>。</li>



<li>逐項複製必要的 environment variables、secrets、ingress、domain、scale 和 traffic 設定。</li>



<li>啟用新 App 的 system-assigned Managed Identity。</li>



<li>由平台管理者授予新 identity ACR <code>AcrPull</code>。</li>



<li>由 SQL Entra administrator 替新的 Backend identity 建立 SQL user/roles。</li>



<li>將 CI/CD 的 target App name 改成新的 App。</li>



<li>驗證 revision、logs、API、Frontend URL、ACR pull 與 SQL 連線。</li>



<li>新 App 穩定後，才考慮移除舊 App。</li>
</ol>



<p class="wp-block-paragraph">不要先刪除舊 App 再重建，因為可能遺失 secrets、domain、ingress、traffic 和原本的部署設定。</p>



<h2 class="wp-block-heading">Frontend 的特別注意事項</h2>



<p class="wp-block-paragraph">Frontend 只需要：</p>



<ul class="wp-block-list">
<li>從 ACR pull image。</li>



<li>連到正確的 Backend URL。</li>



<li>必要時讀取自己的 Key Vault secrets。</li>
</ul>



<p class="wp-block-paragraph">Frontend 不需要 <code>demoproject-dev</code> 的 SQL roles。</p>



<p class="wp-block-paragraph">如果 Frontend revision 是 <code>ActivationFailed</code>，但 <code>AcrPull</code> 已經正確，請檢查：</p>



<ul class="wp-block-list">
<li>image 內的 Nginx upstream 設定。</li>



<li><code>BACKEND_URL</code> 或其他 Backend endpoint environment variable。</li>



<li>image 是否仍然包含舊的 <code>backend.legacy.example.invalid</code> hostname。</li>



<li>新 image 是否真的已 push 到 ACR，且 App 使用了正確 tag。</li>
</ul>



<p class="wp-block-paragraph">這類問題通常是 image/configuration 問題，不是 SQL 權限問題。</p>



<h2 class="wp-block-heading">建議的執行順序</h2>



<p class="wp-block-paragraph">平台管理者與開發團隊可以依照以下順序處理：</p>



<ol class="wp-block-list">
<li>確認 GitHub OIDC Federated Credential 的 repository、branch 和 Environment subject 正確。</li>



<li>確認 CI Service Principal 只有部署所需的 <code>Contributor</code>、<code>AcrPush</code> 等角色。</li>



<li>確認 Backend 和 Frontend 都位於 <code>cae-demo-stg-001</code>。</li>



<li>啟用兩個 App 的 system-assigned identity。</li>



<li>對兩個 App identity 各自授予 ACR <code>AcrPull</code>。</li>



<li>執行 <code>az containerapp registry set --identity system</code>。</li>



<li>由 SQL Entra administrator 建立 Backend contained user 和必要 roles。</li>



<li>確認 CAE、VNet、Private DNS、Private Endpoint 的網路路徑。</li>



<li>只啟用一條正式部署路徑，避免舊 pipeline 和 Azure CI/CD 同時更新同一個 App。</li>



<li>做一次受控部署，逐項檢查 image、revision、logs、API、Frontend 和 SQL。</li>
</ol>



<h2 class="wp-block-heading">部署後驗證指令</h2>



<h3 class="wp-block-heading">確認 ACR image tag</h3>



<pre class="wp-block-code"><code>az acr repository show-tags \
  --name acrdemostg001 \
  --repository demo-backend \
  --orderby time_desc \
  --top 5 \
  -o table</code></pre>



<h3 class="wp-block-heading">確認 Backend revision</h3>



<pre class="wp-block-code"><code>az containerapp revision list \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --query "&#91;].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}" \
  -o table</code></pre>



<p class="wp-block-paragraph">正常情況應看到 revision 為 <code>Healthy</code>，且 running state 正常。若 revision 建立成功但沒有 running，請查 revision events 和 container logs。</p>



<h3 class="wp-block-heading">查看 Backend logs</h3>



<pre class="wp-block-code"><code>az containerapp logs show \
  --resource-group rg-demo-stg-001 \
  --name ca-demo-backend-dev-001 \
  --type console \
  --tail 100</code></pre>



<h3 class="wp-block-heading">最後確認的成功條件</h3>



<ul class="wp-block-list">
<li>GitHub Actions OIDC login 成功。</li>



<li>image 成功 push 到 <code>acrdemostg001.azurecr.io</code>。</li>



<li>Backend 和 Frontend revision 能從 ACR pull image。</li>



<li>Backend revision 為 <code>Healthy / Running</code>。</li>



<li>Backend 能以 Managed Identity 取得 SQL token。</li>



<li>SQL contained user 與必要 roles 存在。</li>



<li>Backend 能連線到 <code>demoproject-dev</code>。</li>



<li>Frontend 使用新的 Backend URL。</li>



<li>舊的部署系統不會再覆蓋 Azure CI/CD 的結果。</li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-ci-cd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Container Apps 與 GitHub OIDC 無密碼部署實戰：新團隊安全上手指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-github-oidc/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-github-oidc/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:49:54 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8757</guid>

					<description><![CDATA[企業或專案引進「新開發團隊」接手部署時，常見的權...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-1024x572.jpg?v=1789271607" alt="" class="wp-image-8760" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-1024x572.jpg?v=1789271607 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-600x335.jpg?v=1789271607 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean-767x428.jpg?v=1789271607 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_5923070288072847847_clean.jpg?v=1789271607 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">企業或專案引進「新開發團隊」接手部署時，常見的權限混亂、安全風險與網路連線問題。主要包含以下四大核心問題：</p>



<ul class="wp-block-list">
<li><strong>權限與安全問題</strong>：避免為了部署而過度授予高風險權限（例如 Azure Owner 或 SQL 權限），並改用 GitHub Actions OIDC 取代長期保存的明文金鑰（ secrets ）。</li>



<li><strong>責任分工不明確</strong>：清楚劃分「平台管理者」與「開發團隊」的職責邊界，防止開發團隊誤動基礎架構（如 SQL Firewall、VNet ）。</li>



<li><strong>網路連線與架構問題</strong>：解決 Container App 因未走正確的 VNet 與 Private Endpoint 導致無法連線至 Azure SQL Database 的問題。</li>



<li><strong>新手上手缺乏規範</strong>：提供明確的跨團隊開發指南、 CI/CD 自動化流程範本與開通檢查表，縮短團隊上手時間。</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code>demoproject</code>&nbsp;與&nbsp;<code>ExampleOrg</code>&nbsp;只是示範名稱。正式環境使用前，必須由 平台管理者替換所有&nbsp;<code>&lt;PLACEHOLDER&gt;</code>&nbsp;與範例資源名稱，並確認權限範圍。</p>
</blockquote>



<h2 class="wp-block-heading">1. 先看結論</h2>



<p class="wp-block-paragraph">新加入的開發團隊平常只需要：</p>



<ol class="wp-block-list">
<li>取得 GitHub repository 與必要的 Azure RBAC 權限。</li>



<li>使用平台管理者準備好的共用 Container Apps Environment。</li>



<li>透過 GitHub Actions OIDC 登入 Azure，不使用長期 client secret。</li>



<li>Build Docker image、push 到 ACR、更新既有 Container App。</li>



<li>透過 revision 與 logs 查看部署結果。</li>
</ol>



<p class="wp-block-paragraph">新加入的開發團隊<strong>不需要</strong>：</p>



<ul class="wp-block-list">
<li>Azure <code>Owner</code>。</li>



<li>SQL Resource Group 的 <code>Contributor</code>。</li>



<li>直接修改 SQL firewall、Private Endpoint、VNet 或 Private DNS。</li>



<li>在 GitHub repository 中存放 SQL 密碼或應用程式 secrets。</li>



<li>每次部署都執行完整的基礎資源建立流程。</li>
</ul>



<p class="wp-block-paragraph">應用程式連接 SQL 的權限不是由 GitHub Actions Service Principal 提供， 而是由 Backend Container App 自己的 Managed Identity 在資料庫中取得。</p>



<h2 class="wp-block-heading">2. Azure 服務白話說明</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Azure 服務或名詞</th><th class="has-text-align-left" data-align="left">白話說明</th><th class="has-text-align-left" data-align="left">DemoProject 用途</th></tr></thead><tbody><tr><td>Subscription</td><td>Azure 帳單與資源的管理範圍</td><td><code>&lt;SUBSCRIPTION_NAME&gt;</code></td></tr><tr><td>Resource Group（RG）</td><td>把相關 Azure 資源放在一起的資料夾</td><td>App、ACR、CAE、SQL 分開管理</td></tr><tr><td>Azure Container Registry（ACR）</td><td>存放 Docker image 的私有倉庫</td><td>CI push image</td></tr><tr><td>Container Apps Environment（CAE）</td><td>Container Apps 共用的執行環境與網路邊界</td><td>Backend、Frontend 共用</td></tr><tr><td>Container App（CA）</td><td>實際執行容器的 Azure 資源</td><td>執行 DemoProject Backend/Frontend</td></tr><tr><td>Virtual Network（VNet）</td><td>Azure 內部的私有網路</td><td>連接 CAE 與 SQL Private Endpoint</td></tr><tr><td>Private Endpoint（PE）</td><td>讓 Azure 服務在 VNet 內取得私有 IP</td><td>將 SQL 接到 VNet</td></tr><tr><td>Private DNS</td><td>讓 hostname 在 VNet 內解析到私有 IP</td><td>解析 SQL hostname</td></tr><tr><td>Azure SQL Server / Database</td><td>SQL Server 與其中的資料庫</td><td>存放 DemoProject 資料</td></tr><tr><td>Managed Identity</td><td>Azure 資源自己的身分，不需要存密碼</td><td>App 登入 SQL、ACR、Key Vault</td></tr><tr><td>Key Vault</td><td>集中存放 secret、憑證與金鑰</td><td>Backend runtime 讀取 secrets</td></tr><tr><td>Azure RBAC</td><td>管理 Azure 資源的控制權</td><td>決定誰能部署或修改 App</td></tr><tr><td>SQL database role</td><td>資料庫內的資料讀寫權限</td><td><code>db_datareader</code>、<code>db_datawriter</code>、<code>db_ddladmin</code></td></tr><tr><td>GitHub OIDC</td><td>GitHub workflow 使用短期 token 登入 Azure</td><td>CI 不存放 client secret</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Azure RBAC 與 SQL 權限不是同一件事</h3>



<ul class="wp-block-list">
<li>Azure <code>Contributor</code> 可以管理 Azure Resource Manager 資源，不等於可以 查詢 SQL table。</li>



<li>SQL <code>db_datareader</code> 可以讀資料，不等於可以修改 Azure SQL Server。</li>



<li>SQL <code>db_datawriter</code> 可以寫資料。</li>



<li>SQL <code>db_ddladmin</code> 可以執行部分資料庫結構變更，應只授予需要執行 migration 的 Backend identity。</li>



<li>SQL firewall 與 Private Endpoint 決定「網路能不能到 SQL」；資料庫 roles 只有在網路已連通後才會生效。</li>
</ul>



<h2 class="wp-block-heading">3. DemoProject 資源地圖</h2>



<p class="wp-block-paragraph">以下名稱全部是示範值：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">用途</th><th class="has-text-align-left" data-align="left">Resource Group</th><th class="has-text-align-left" data-align="left">資源名稱</th></tr></thead><tbody><tr><td>Azure Container Registry</td><td><code>rg-demoproject-stg-app</code></td><td><code>acr-demoproject-stg.azurecr.io</code></td></tr><tr><td>Container Apps</td><td><code>rg-demoproject-stg-app</code></td><td><code>ca-demoproject-backend-stg</code>、<code>ca-demoproject-frontend-stg</code></td></tr><tr><td>共用 Container Apps Environment</td><td><code>rg-demoproject-stg-cae</code></td><td><code>cae-demoproject-stg</code></td></tr><tr><td>SQL Server / Database</td><td><code>rg-demoproject-stg-data</code></td><td><code>sql-demoproject-stg.database.windows.net</code>&nbsp;/&nbsp;<code>demoproject-dev</code></td></tr><tr><td>SQL Private Endpoint</td><td><code>rg-demoproject-stg-network</code></td><td><code>pe-demoproject-sql-stg</code></td></tr><tr><td>Key Vault</td><td><code>rg-demoproject-stg-app</code></td><td><code>kv-demoproject-stg</code></td></tr><tr><td>VNet</td><td><code>rg-demoproject-stg-network</code></td><td><code>vnet-demoproject-stg</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">3.1 建議的網路路徑</h3>



<pre class="wp-block-code"><code>Backend Container App
    |
    |  共用 CAE 的 VNet 網路
    v
vnet-demoproject-stg
    |
    |  Private DNS 將 SQL hostname 解析到私有 IP
    v
SQL Private Endpoint pe-demoproject-sql-stg
    |
    v
sql-demoproject-stg / demoproject-dev
</code></pre>



<p class="wp-block-paragraph">如果 Container App 位於沒有 VNet/private DNS 的獨立 CAE，它可能會透過 公網出口 IP 連線 SQL，並收到：</p>



<pre class="wp-block-code"><code>Client with IP address '...' is not allowed to access the server
</code></pre>



<p class="wp-block-paragraph">遇到這個錯誤時，先確認 App 所在 CAE 與 DNS 路徑，不要立刻把大量公網 IP 加進 SQL firewall。</p>



<h2 class="wp-block-heading">4. 權限模型</h2>



<h3 class="wp-block-heading">4.1 開發者與 CI 的建議權限</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">範圍</th><th class="has-text-align-left" data-align="left">建議角色/權限</th><th class="has-text-align-left" data-align="left">用途</th><th class="has-text-align-left" data-align="left">SQL data access</th></tr></thead><tbody><tr><td>既有 Backend Container App</td><td><code>Contributor</code>&nbsp;或專用 custom role</td><td>更新 image/revision</td><td>否</td></tr><tr><td>既有 Frontend Container App</td><td><code>Contributor</code>&nbsp;或專用 custom role</td><td>更新 image/revision</td><td>否</td></tr><tr><td>ACR</td><td><code>AcrPush</code></td><td>Docker image push</td><td>否</td></tr><tr><td>共用 CAE</td><td><code>Reader</code></td><td>讀取 CAE 設定</td><td>否</td></tr><tr><td>共用 CAE</td><td><code>Container Apps Environment Joiner</code></td><td>使用指定 CAE</td><td>否</td></tr><tr><td>SQL Resource Group</td><td>不授權</td><td>SQL 集中由管理者控管</td><td>否</td></tr><tr><td>Backend Managed Identity</td><td>SQL contained user + SQL roles</td><td>應用程式 runtime 連線 SQL</td><td>是</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code>Container Apps Environment Joiner</code>&nbsp;應只包含：</p>



<pre class="wp-block-code"><code>Microsoft.App/managedEnvironments/join/action
</code></pre>



<p class="wp-block-paragraph">不要為了讓團隊部署而授予共用 CAE Resource Group 的&nbsp;<code>Contributor</code>。</p>



<h3 class="wp-block-heading">4.2 平台管理者與開發團隊分工</h3>



<p class="wp-block-paragraph">平台管理者一次性完成：</p>



<ol class="wp-block-list">
<li>建立 App Registration 與 Service Principal。</li>



<li>建立指定 repository、branch 或 GitHub Environment 的 Federated Credential。</li>



<li>在 ACR scope 授予 <code>AcrPush</code>。</li>



<li>在既有 Container App scope 授予更新權限。</li>



<li>在共用 CAE scope 授予 <code>Reader</code> 與 <code>Container Apps Environment Joiner</code>。</li>



<li>確認 App runtime Managed Identity 具備 ACR、Key Vault 與 SQL 權限。</li>



<li>確認 App 位於正確的 VNet/private DNS CAE。</li>
</ol>



<p class="wp-block-paragraph">開發團隊負責：</p>



<ol class="wp-block-list">
<li>維護 GitHub Actions workflow。</li>



<li>Build Docker image。</li>



<li>Push image 到 ACR。</li>



<li>更新既有 Container App。</li>



<li>查看 revision 與 logs。</li>
</ol>



<p class="wp-block-paragraph">開發團隊不應該：</p>



<ul class="wp-block-list">
<li>建立或刪除 Service Principal。</li>



<li>修改 SQL firewall、Private Endpoint、VNet 或 Private DNS。</li>



<li>將 App secrets 寫入 repository 或 workflow。</li>



<li>執行 SQL <code>CREATE USER</code>。</li>



<li>修改共用 CAE 的網路設定。</li>
</ul>



<h2 class="wp-block-heading">5. GitHub Actions OIDC</h2>



<h3 class="wp-block-heading">5.1 OIDC 如何運作</h3>



<pre class="wp-block-code"><code>GitHub push to main
        |
        | GitHub 發出短期 OIDC token
        v
GitHub Actions
        |
        | azure/login@v2
        v
Microsoft Entra App / Service Principal
        |
        | Federated Credential 比對 repo + branch/environment
        | Azure RBAC 比對允許的 scope
        v
ACR push -&gt; Container App update
</code></pre>



<p class="wp-block-paragraph">OIDC 不等於自動擁有 Azure 權限：</p>



<ul class="wp-block-list">
<li>Federated Credential 決定「哪個 GitHub workflow 可以登入」。</li>



<li>Azure RBAC 決定「登入後可以做什麼」。</li>
</ul>



<h3 class="wp-block-heading">5.2 新團隊專用 Service Principal</h3>



<p class="wp-block-paragraph">建議每個團隊、每個環境使用不同 Service Principal。不要讓所有團隊共用 一個可寫入 Staging 的 identity。</p>



<p class="wp-block-paragraph">以下是示範值：</p>



<pre class="wp-block-code"><code>SP display name: demoproject-team-a-staging-ci
Client ID:       &lt;APP_CLIENT_ID&gt;
Object ID:       &lt;SERVICE_PRINCIPAL_OBJECT_ID&gt;
Tenant ID:       &lt;TENANT_ID&gt;
Subscription ID: &lt;SUBSCRIPTION_ID&gt;
</code></pre>



<p class="wp-block-paragraph">不需要建立 client secret。</p>



<h3 class="wp-block-heading">5.3 角色配置範例</h3>



<p class="wp-block-paragraph">以下指令只展示格式，執行前請替換所有 placeholder：</p>



<pre class="wp-block-code"><code>set -euo pipefail

SUB_ID="&lt;SUBSCRIPTION_ID&gt;"
APP_RG="rg-demoproject-stg-app"
CAE_RG="rg-demoproject-stg-cae"
CAE_NAME="cae-demoproject-stg"
ACR_NAME="acr-demoproject-stg"
SP_NAME="demoproject-team-a-staging-ci"

CAE_SCOPE="$(az containerapp env show \
  --subscription "$SUB_ID" \
  --resource-group "$CAE_RG" \
  --name "$CAE_NAME" \
  --query id -o tsv)"

ACR_SCOPE="$(az acr show \
  --subscription "$SUB_ID" \
  --resource-group "$APP_RG" \
  --name "$ACR_NAME" \
  --query id -o tsv)"

APP_ID="$(az ad app create \
  --display-name "$SP_NAME" \
  --query appId -o tsv)"

SP_OBJECT_ID="$(az ad sp create \
  --id "$APP_ID" \
  --query id -o tsv)"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "AcrPush" \
  --scope "$ACR_SCOPE"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Reader" \
  --scope "$CAE_SCOPE"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Container Apps Environment Joiner" \
  --scope "$CAE_SCOPE"
</code></pre>



<p class="wp-block-paragraph">若 CI 只更新既有 App，建議把寫入權限限制在 App resource scope：</p>



<pre class="wp-block-code"><code>BACKEND_SCOPE="$(az containerapp show \
  --subscription "$SUB_ID" \
  --resource-group "$APP_RG" \
  --name "ca-demoproject-backend-stg" \
  --query id -o tsv)"

az role assignment create \
  --subscription "$SUB_ID" \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Contributor" \
  --scope "$BACKEND_SCOPE"
</code></pre>



<p class="wp-block-paragraph">不要把 CI identity 授予：</p>



<pre class="wp-block-code"><code>rg-demoproject-stg-data
rg-demoproject-stg-cae 的 Contributor
demoproject-dev 的 SQL roles
</code></pre>



<h3 class="wp-block-heading">5.4 Federated Credential</h3>



<h4 class="wp-block-heading">方案 A：只允許 main branch</h4>



<p class="wp-block-paragraph">適合單一 staging deploy workflow：</p>



<pre class="wp-block-code"><code>REPO="ExampleOrg/demoproject-backend"
SAFE_NAME="${REPO//\//-}-main"
SUBJECT="repo:${REPO}:ref:refs/heads/main"

PAYLOAD="$(jq -n \
  --arg name "$SAFE_NAME" \
  --arg subject "$SUBJECT" \
  '{
    name: $name,
    issuer: "https://token.actions.githubusercontent.com",
    subject: $subject,
    audiences: &#91;"api://AzureADTokenExchange"]
  }')"

az ad app federated-credential create \
  --id "$APP_ID" \
  --parameters "$PAYLOAD"
</code></pre>



<h4 class="wp-block-heading">方案 B：使用受保護的 GitHub Environment</h4>



<p class="wp-block-paragraph">新團隊建議建立 GitHub Environment&nbsp;<code>staging</code>，設定：</p>



<ul class="wp-block-list">
<li>Required reviewers。</li>



<li>只允許 <code>main</code> branch 或指定 tag。</li>



<li>將 Azure identifiers 放在 Environment variables。</li>
</ul>



<p class="wp-block-paragraph">Federated Credential 的 subject 改為：</p>



<pre class="wp-block-code"><code>repo:ExampleOrg/demoproject-backend:environment:staging
</code></pre>



<p class="wp-block-paragraph">Workflow 的 deploy job 必須包含：</p>



<pre class="wp-block-code"><code>environment: staging
</code></pre>



<p class="wp-block-paragraph">Branch subject 與 Environment subject 不要混用。Azure 會比對完整的&nbsp;<code>sub</code>&nbsp;claim，字串不完全相同就會登入失敗。</p>



<h4 class="wp-block-heading">PR 不可取得 write token</h4>



<p class="wp-block-paragraph">以下 subject 不可綁定到具有&nbsp;<code>Contributor</code>、<code>AcrPush</code>&nbsp;或 App write 的 identity：</p>



<pre class="wp-block-code"><code>repo:ExampleOrg/demoproject-backend:pull_request
</code></pre>



<p class="wp-block-paragraph">PR 程式碼尚未審核，可能修改 workflow 並呼叫 Azure CLI。PR workflow 應只 執行 lint、test、build；若真的需要查詢 Azure，另建只有&nbsp;<code>Reader</code>&nbsp;的 identity。</p>



<h2 class="wp-block-heading">6. GitHub Actions Workflow 範本</h2>



<p class="wp-block-paragraph">以下範例使用受保護的&nbsp;<code>staging</code>&nbsp;Environment。若使用 main branch subject， 請移除&nbsp;<code>environment: staging</code>，或先建立相符的 Environment Federated Credential。</p>



<pre class="wp-block-code"><code>name: Deploy DemoProject Backend

on:
  push:
    branches:
      - main

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to Azure with OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Login to ACR
        run: az acr login --name acr-demoproject-stg

      - name: Build and push image
        env:
          IMAGE: acr-demoproject-stg.azurecr.io/demoproject-backend:${{ github.sha }}
        run: |
          docker build -t "$IMAGE" .
          docker push "$IMAGE"

      - name: Update existing Container App
        env:
          IMAGE: acr-demoproject-stg.azurecr.io/demoproject-backend:${{ github.sha }}
        run: |
          az containerapp update \
            --resource-group rg-demoproject-stg-app \
            --name ca-demoproject-backend-stg \
            --image "$IMAGE"
</code></pre>



<p class="wp-block-paragraph">Frontend 只需要替換 image repository 與 Container App name。</p>



<h3 class="wp-block-heading">6.1 PR Workflow</h3>



<p class="wp-block-paragraph">PR workflow 不應取得 Azure write token：</p>



<pre class="wp-block-code"><code>on:
  pull_request:
    branches:
      - main

permissions:
  contents: read
</code></pre>



<p class="wp-block-paragraph">PR 可執行 lint、unit test、build 或不涉及 Staging 的 container build， 但不要：</p>



<ul class="wp-block-list">
<li><code>id-token: write</code>。</li>



<li><code>azure/login</code>。</li>



<li>Push Staging ACR。</li>



<li>Update 或 delete Container App。</li>
</ul>



<h3 class="wp-block-heading">6.2 不要在 CI 執行完整部署腳本</h3>



<p class="wp-block-paragraph">完整部署腳本通常會包含 secrets、Key Vault policy、Managed Identity、 SQL 授權與首次建立資源的流程，不適合直接放進 GitHub Actions。</p>



<p class="wp-block-paragraph">CI 建議只做：</p>



<pre class="wp-block-code"><code>OIDC login
  -&gt; docker build
  -&gt; ACR push
  -&gt; containerapp update --image
</code></pre>



<p class="wp-block-paragraph">既有的環境變數與 secrets 由平台管理者預先設定；App runtime 使用自己的 Managed Identity 連接 SQL 與 Key Vault。</p>



<h2 class="wp-block-heading">7. SQL Database 與 Managed Identity</h2>



<h3 class="wp-block-heading">7.1 App runtime 才需要 SQL role</h3>



<p class="wp-block-paragraph">查詢 Backend App 的 Managed Identity：</p>



<pre class="wp-block-code"><code>az containerapp show \
  --subscription "&lt;SUBSCRIPTION_ID&gt;" \
  --resource-group "rg-demoproject-stg-app" \
  --name "ca-demoproject-backend-stg" \
  --query "{type:identity.type,principalId:identity.principalId}" \
  -o json
</code></pre>



<p class="wp-block-paragraph">如果 Container App 被刪除後重新建立，system-assigned identity 可能改變。 管理者需要重新確認 ACR、Key Vault 與 SQL 權限。</p>



<h3 class="wp-block-heading">7.2 建立 SQL contained user</h3>



<p class="wp-block-paragraph">以下 SQL 應由 SQL Entra administrator 或等效資料庫管理員，在&nbsp;<code>demoproject-dev</code>&nbsp;執行。將 App name 替換成實際值：</p>



<pre class="wp-block-code"><code>IF DATABASE_PRINCIPAL_ID(N'ca-demoproject-backend-stg') IS NULL
BEGIN
    CREATE USER &#91;ca-demoproject-backend-stg] FROM EXTERNAL PROVIDER;
END;

ALTER ROLE &#91;db_datareader]
ADD MEMBER &#91;ca-demoproject-backend-stg];

ALTER ROLE &#91;db_datawriter]
ADD MEMBER &#91;ca-demoproject-backend-stg];

<em>-- 只有 Backend migration 確實需要時才授予</em>
ALTER ROLE &#91;db_ddladmin]
ADD MEMBER &#91;ca-demoproject-backend-stg];
</code></pre>



<p class="wp-block-paragraph">CI Service Principal 不需要執行這段 SQL，也不應被加入這三個 database roles。</p>



<h3 class="wp-block-heading">7.3 人員需要直接查詢資料庫</h3>



<p class="wp-block-paragraph">不要把 Azure&nbsp;<code>Contributor</code>&nbsp;當成 SQL data access。若確實需要讓人員直接 查詢：</p>



<ol class="wp-block-list">
<li>管理者建立 Microsoft Entra security group。</li>



<li>將需要查詢的人員加入該群組。</li>



<li>在 <code>demoproject-dev</code> 建立該群組的 contained user。</li>



<li>優先只授予 <code>db_datareader</code>。</li>



<li>確認使用者有 VNet、VPN 或其他 Private Endpoint 網路路徑。</li>
</ol>



<h2 class="wp-block-heading">8. Private Endpoint 與 SQL Firewall</h2>



<h3 class="wp-block-heading">8.1 為什麼不能只新增一個 IP？</h3>



<p class="wp-block-paragraph">獨立的 Container Apps Environment 可能使用公網出口 IP，而且出口 IP 可能有很多個或因環境變更而改變。錯誤訊息中的 IP 只代表當次連線， 不一定是永久 IP。</p>



<p class="wp-block-paragraph">使用共用 CAE 時，應該走：</p>



<pre class="wp-block-code"><code>Container App
  -&gt; VNet
  -&gt; Private DNS
  -&gt; SQL Private Endpoint
  -&gt; SQL Database
</code></pre>



<p class="wp-block-paragraph">不要用大量公網 IP firewall 規則取代正確的網路拓撲，也不要未經核准就 建立：</p>



<pre class="wp-block-code"><code>start-ip-address = 0.0.0.0
end-ip-address   = 0.0.0.0
</code></pre>



<p class="wp-block-paragraph">這代表允許大範圍 Azure services 存取，不是只允許本團隊。</p>



<h3 class="wp-block-heading">8.2 確認 App 使用正確 CAE</h3>



<pre class="wp-block-code"><code>$expected = "/subscriptions/&lt;SUBSCRIPTION_ID&gt;/resourceGroups/rg-demoproject-stg-cae/providers/Microsoft.App/managedEnvironments/cae-demoproject-stg"

az containerapp show `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --query properties.managedEnvironmentId `
  -o tsv

$expected
</code></pre>



<p class="wp-block-paragraph">兩個輸出必須相同。如果 App 位於沒有 VNet/private DNS 的 CAE，先請平台 管理者確認網路路徑，不要先修改 SQL firewall。</p>



<h3 class="wp-block-heading">8.3 常見錯誤</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">錯誤</th><th class="has-text-align-left" data-align="left">常見原因</th><th class="has-text-align-left" data-align="left">正確處理</th></tr></thead><tbody><tr><td><code>Client with IP address ... is not allowed</code></td><td>App 走公網，SQL firewall 沒放行</td><td>確認 App 是否位於共用 VNet CAE</td></tr><tr><td><code>Login failed</code></td><td>App identity 未建立或沒有 SQL role</td><td>查 App principal ID，由 DB 管理者處理</td></tr><tr><td><code>Cannot resolve host</code></td><td>Private DNS 或 VNet 路徑錯誤</td><td>檢查 PE、DNS zone link、CAE subnet</td></tr><tr><td><code>AuthorizationFailed</code></td><td>CI identity 缺少對應 scope 的 role</td><td>由平台管理者補上 App、ACR 或 CAE role</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">9. 部署後驗證</h2>



<h3 class="wp-block-heading">9.1 查看 App 所在 CAE 與 revision</h3>



<pre class="wp-block-code"><code>az containerapp show `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --query "{environment:properties.managedEnvironmentId,latest:properties.latestRevisionName,ready:properties.latestReadyRevisionName}" `
  -o table
</code></pre>



<pre class="wp-block-code"><code>az containerapp revision list `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --query "&#91;].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}" `
  -o table
</code></pre>



<h3 class="wp-block-heading">9.2 查看 logs</h3>



<pre class="wp-block-code"><code>az containerapp logs show `
  --name ca-demoproject-backend-stg `
  --resource-group rg-demoproject-stg-app `
  --type console `
  --tail 100
</code></pre>



<h3 class="wp-block-heading">9.3 驗證 CI 身分</h3>



<pre class="wp-block-code"><code>az ad app federated-credential list \
  --id "$APP_ID" \
  --query "&#91;].{name:name,subject:subject,issuer:issuer,audiences:audiences}" \
  -o table
</code></pre>



<pre class="wp-block-code"><code>az role assignment list \
  --assignee-object-id "$SP_OBJECT_ID" \
  --all \
  --query "&#91;].{role:roleDefinitionName,scope:scope}" \
  -o table
</code></pre>



<p class="wp-block-paragraph">應確認：</p>



<ul class="wp-block-list">
<li>issuer 是 <code>https://token.actions.githubusercontent.com</code>。</li>



<li>audience 是 <code>api://AzureADTokenExchange</code>。</li>



<li>subject 只有指定 repo 與指定 main/environment。</li>



<li>沒有 write identity 的 <code>pull_request</code> subject。</li>



<li>沒有 SQL Resource Group 或共用 CAE Resource Group 的 <code>Contributor</code>。</li>
</ul>



<h2 class="wp-block-heading">10. 首次 bootstrap 與日常部署</h2>



<h3 class="wp-block-heading">10.1 首次 bootstrap</h3>



<p class="wp-block-paragraph">以下工作由平台管理者完成一次：</p>



<ul class="wp-block-list">
<li>建立 ACR、CAE、Container App 或確認資源已存在。</li>



<li>建立 App Registration、Service Principal 與 Federated Credentials。</li>



<li>授予 CI 的 ACR/App/CAE 角色。</li>



<li>啟用 App system-assigned Managed Identity。</li>



<li>授予 App identity <code>AcrPull</code>、Key Vault <code>get/list</code> 與 SQL roles。</li>



<li>設定 Private Endpoint、Private DNS、custom domain 與憑證。</li>
</ul>



<p class="wp-block-paragraph">如果共用 CAE 暫時不存在，不要讓腳本建立沒有 VNet/private endpoint 的替代 環境；應停止流程並通知平台管理者。</p>



<h3 class="wp-block-heading">10.2 日常部署</h3>



<p class="wp-block-paragraph">日常 CI 只需：</p>



<pre class="wp-block-code"><code>main push
  -&gt; GitHub OIDC login
  -&gt; build image
  -&gt; push ACR
  -&gt; update existing Container App
  -&gt; check revision and logs
</code></pre>



<p class="wp-block-paragraph">如果 App 被刪除並重新建立，請重新取得 identity，重新處理 ACR、Key Vault 與 SQL 授權。</p>



<h2 class="wp-block-heading">11. 撤權與團隊交接</h2>



<h3 class="wp-block-heading">11.1 移除某個 repository 的信任</h3>



<pre class="wp-block-code"><code>az ad app federated-credential list \
  --id "$APP_ID" \
  --query "&#91;].{id:id,name:name,subject:subject}" \
  -o table
</code></pre>



<pre class="wp-block-code"><code>az ad app federated-credential delete \
  --id "$APP_ID" \
  --federated-credential-id "&lt;FEDERATED_CREDENTIAL_ID&gt;"
</code></pre>



<h3 class="wp-block-heading">11.2 移除 Azure role</h3>



<pre class="wp-block-code"><code>az role assignment delete \
  --assignee-object-id "$SP_OBJECT_ID" \
  --role "AcrPush" \
  --scope "$ACR_SCOPE"
</code></pre>



<p class="wp-block-paragraph">團隊撤換時同時確認：</p>



<ol class="wp-block-list">
<li>GitHub Environment/repository variables 已移除。</li>



<li>該 repository 的 Federated Credentials 已刪除。</li>



<li>App、ACR、CAE scope 的 role assignments 已移除。</li>



<li>Container App runtime Managed Identity 沒有被誤刪。</li>



<li>Audit log 與變更紀錄已保存。</li>
</ol>



<h2 class="wp-block-heading">12. 新團隊開通檢查表</h2>



<h3 class="wp-block-heading">平台管理者</h3>



<ul class="wp-block-list">
<li>[ ] 所有資源名稱、Subscription、tenant 與 repo 已確認。</li>



<li>[ ] 每個團隊建立獨立 Service Principal。</li>



<li>[ ] 只建立 main 或受保護 Environment 的 Federated Credential。</li>



<li>[ ] 沒有建立 write identity 的 <code>pull_request</code> credential。</li>



<li>[ ] ACR scope 有 <code>AcrPush</code>。</li>



<li>[ ] 既有 App scope 有更新權限。</li>



<li>[ ] 共用 CAE scope 有 <code>Reader</code> + <code>Container Apps Environment Joiner</code>。</li>



<li>[ ] 沒有授予 SQL Resource Group 或共用 CAE Resource Group Contributor。</li>



<li>[ ] App runtime Managed Identity 有 SQL、Key Vault、ACR 權限。</li>



<li>[ ] GitHub Environment 的 reviewers 與 branch restriction 已設定。</li>
</ul>



<h3 class="wp-block-heading">開發團隊</h3>



<ul class="wp-block-list">
<li>[ ] Workflow 有 <code>permissions: id-token: write</code>。</li>



<li>[ ] <code>environment</code> 或 branch subject 與 Azure FIC 完全一致。</li>



<li>[ ] 使用 <code>azure/login@v2</code>，沒有 client secret。</li>



<li>[ ] Docker image 使用不可變的 commit SHA tag。</li>



<li>[ ] Push 到正確 ACR repository。</li>



<li>[ ] 只更新指定 Container App。</li>



<li>[ ] PR workflow 沒有 Azure write token。</li>



<li>[ ] 部署後確認 revision、logs 與功能。</li>
</ul>



<h2 class="wp-block-heading">13. 示範值替換清單</h2>



<p class="wp-block-paragraph">正式使用前，至少替換以下項目：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">示範值</th><th class="has-text-align-left" data-align="left">正式值</th></tr></thead><tbody><tr><td><code>DemoProject</code>&nbsp;/&nbsp;<code>demoproject</code></td><td>正式專案名稱</td></tr><tr><td><code>ExampleOrg</code></td><td>GitHub organization</td></tr><tr><td><code>&lt;SUBSCRIPTION_ID&gt;</code></td><td>正式 Subscription ID</td></tr><tr><td><code>&lt;TENANT_ID&gt;</code></td><td>正式 Entra tenant ID</td></tr><tr><td><code>rg-demoproject-*</code></td><td>正式 Resource Groups</td></tr><tr><td><code>acr-demoproject-stg</code></td><td>正式 ACR</td></tr><tr><td><code>cae-demoproject-stg</code></td><td>正式 CAE</td></tr><tr><td><code>ca-demoproject-*-stg</code></td><td>正式 Container Apps</td></tr><tr><td><code>sql-demoproject-stg.database.windows.net</code></td><td>正式 SQL hostname</td></tr><tr><td><code>demoproject-dev</code></td><td>正式 Database</td></tr><tr><td><code>C:\work\demoproject-staging</code></td><td>團隊實際工作目錄</td></tr><tr><td><code>ExampleOrg/demoproject-*</code></td><td>正式 repositories</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">替換後請再次搜尋以下字串，確認沒有殘留示範 placeholder 或錯誤環境：</p>



<pre class="wp-block-code"><code>&lt;SUBSCRIPTION_ID&gt;
&lt;TENANT_ID&gt;
ExampleOrg
demoproject
</code></pre>



<h2 class="wp-block-heading">14. 安全規則</h2>



<ol class="wp-block-list">
<li>不要把 client secret、SQL password、JWT secret 或 storage connection string commit 到 Git。</li>



<li>不要把 secrets 貼到 PR、Issue、聊天工具或 deployment log。</li>



<li>不要為了快速測試把 SQL firewall 設成 <code>0.0.0.0 - 0.0.0.0</code>。</li>



<li>不要把 SQL Resource Group 的 Contributor 當成 SQL data access。</li>



<li>不要把 write-capable Azure identity 授予 <code>pull_request</code>。</li>



<li>不要自行刪除共用 CAE、Private Endpoint、Private DNS 或 SQL Server。</li>



<li>遇到錯誤時，先記錄 repository、workflow run、revision 與錯誤時間， 再請平台管理者檢查權限與網路。</li>
</ol>



<h2 class="wp-block-heading">15. 官方參考文件</h2>



<ul class="wp-block-list">
<li>GitHub OIDC on Azure： <a href="https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-azure">https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-azure</a></li>



<li>GitHub OIDC subject reference： <a href="https://docs.github.com/en/actions/reference/security/oidc">https://docs.github.com/en/actions/reference/security/oidc</a></li>



<li>Azure Login with OIDC： <a href="https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect">https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect</a></li>



<li>Azure Container Apps with GitHub Actions： <a href="https://learn.microsoft.com/en-us/azure/container-apps/github-actions">https://learn.microsoft.com/en-us/azure/container-apps/github-actions</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-container-apps-github-oidc/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>GitHub Actions OIDC CI/CD 交接指南</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/azure-github-actions-oidc-ci-cd/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/azure-github-actions-oidc-ci-cd/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:36:48 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8754</guid>

					<description><![CDATA[本文件說明未來新的開發團隊如何在沒有 Azure...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-1024x572.jpg?v=1789270594" alt="" class="wp-image-8755" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-1024x572.jpg?v=1789270594 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-600x335.jpg?v=1789270594 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean-767x428.jpg?v=1789270594 767w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_14242048425730948964_clean.jpg?v=1789270594 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">本文件說明未來新的開發團隊如何在沒有 Azure Owner role 的情況下，使用 GitHub Actions 將 Backend 或 Frontend 部署到專案 Staging。</p>



<p class="wp-block-paragraph">文件目標是讓接手環境管理工作的同仁能快速回答以下問題：</p>



<ul class="wp-block-list">
<li>為什麼不需要把 Azure client secret 放進 GitHub？</li>



<li>哪些工作必須由平台管理者做一次？</li>



<li>開發團隊需要哪些最小 Azure 權限？</li>



<li>GitHub OIDC、Service Principal、Federated Credential、ACR 與 Container Apps 之間如何串接？</li>



<li>為什麼 PR 不可以直接取得可寫入 Staging 的 Azure token？</li>



<li>SQL 權限應該給誰？</li>
</ul>



<p class="wp-block-paragraph">適用環境</p>



<p class="wp-block-paragraph">本文件適用目前的專案 Staging：</p>



<ul class="wp-block-list">
<li>Subscription： DEMO-PoC</li>



<li>App/ACR Resource Group： rg-demo-stg-jpe-001</li>



<li>共用 Container Apps Environment： cae-stg-jpe-001</li>



<li>ACR： acrdemostgjpe001.azurecr.io</li>



<li>SQL Server： sql-stg-jpe-001.database.windows.net</li>



<li>SQL Database： demoproject-dev</li>
</ul>



<p class="wp-block-paragraph">重要</p>



<p class="wp-block-paragraph">目前的 CI 身分由環境管理者建立，開發團隊只負責維護 GitHub Actions。不要要求開發團隊取得 Owner，也不要把 SQL Resource Group 的 Contributor 給開發團隊。</p>



<ol start="1" class="wp-block-list">
<li>先看整體流程</li>
</ol>



<pre class="wp-block-code"><code>GitHub push to main
        |
        | GitHub 發出短期 OIDC token
        v
GitHub Actions
        |
        | azure/login@v2
        v
Microsoft Entra App / Service Principal
        |
        | Federated Credential 比對 repo + branch/environment
        | Azure RBAC 比對允許的 scope
        v
Azure Container Registry (AcrPush)
        |
        | push Docker image
        v
Azure Container App
        |
        | 使用 App 自己的 Managed Identity
        v
SQL Private Endpoint -> demoproject-dev</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">這裡有兩個不同的身分，聲明如下：</p>



<ul class="wp-block-list">
<li>GitHub Actions Service Principal：用途為 Build、push Image、更新 Container App。是否應有 SQL data access：否。</li>



<li>Backend Container App Managed Identity：用途為執行中的 Backend 連接 SQL、ACR、Key Vault。是否應有 SQL data access：是，僅限該 App。</li>
</ul>



<p class="wp-block-paragraph">GitHub Actions 的 OIDC Service Principal 只負責部署。它不應該被加入 demoproject-dev 的 SQL roles，也不應該讀取應用程式 secrets。</p>



<ol start="2" class="wp-block-list">
<li>為什麼使用 OIDC</li>
</ol>



<p class="wp-block-paragraph">傳統做法是把 AZURE_CLIENT_SECRET 存在 GitHub Actions Secret。這會產生長期憑證管理問題：</p>



<ul class="wp-block-list">
<li>secret 可能被誤印到 log。</li>



<li>secret 可能忘記輪替。</li>



<li>離職或團隊更換時，需要追查 secret 被放在哪裡。</li>
</ul>



<p class="wp-block-paragraph">OIDC 的做法是：</p>



<ol start="1" class="wp-block-list">
<li>GitHub Actions 向 GitHub OIDC provider 取得短期 token。</li>



<li>Azure 驗證 token 的 issuer、subject 與 audience。</li>



<li>只有符合 Federated Credential 條件的 workflow 才能換取 Azure access token。</li>



<li>Azure RBAC 再決定這個 Service Principal 可以操作哪些資源。</li>
</ol>



<p class="wp-block-paragraph">因此 OIDC 不等於「自動擁有 Azure 權限」：</p>



<ul class="wp-block-list">
<li>Federated Credential：決定「哪個 GitHub workflow 可以登入」。</li>



<li>Azure RBAC：決定「登入後可以做什麼」。</li>
</ul>



<ol start="3" class="wp-block-list">
<li>本次已建立的 CI 身分</li>
</ol>



<p class="wp-block-paragraph">本次已建立以下 CI Service Principal：</p>



<p class="wp-block-paragraph">Display name: demoproject-staging-ci</p>



<p class="wp-block-paragraph">Client ID: 188a7d0a-30d5-4a0d-9b04-3e118d3d46e7</p>



<p class="wp-block-paragraph">Object ID: 33ec7c42-e5b5-443c-aae5-e97e1e056d10</p>



<p class="wp-block-paragraph">Tenant ID: 7ef65350-5b77-4958-aca5-0ccadb6bd0b7</p>



<p class="wp-block-paragraph">沒有建立 client secret。</p>



<p class="wp-block-paragraph">3.1 目前已授予的角色</p>



<ul class="wp-block-list">
<li>Scope: rg-demo-stg-jpe-001 | Role: Contributor | 目的: 目前 CI 更新 App/ACR 的過渡權限</li>



<li>Scope: acrdemostgjpe001 | Role: AcrPush | 目的: 允許 Docker push 到 ACR</li>



<li>Scope: cae-stg-jpe-001 | Role: Reader | 目的: 讀取共用 CAE 設定</li>



<li>Scope: cae-stg-jpe-001 | Role: Container Apps Environment Joiner | 目的: 允許使用共用 CAE；只有 Microsoft.App/managedEnvironments/join/action</li>
</ul>



<p class="wp-block-paragraph">目前沒有授予：</p>



<ul class="wp-block-list">
<li>rg-cae-stg-jpe-001 的 Contributor。</li>



<li>rg-data-stg-jpe-001 的任何角色。</li>



<li>demoproject-dev 的 SQL role。</li>



<li>Key Vault data-plane secrets 讀取權限。</li>
</ul>



<p class="wp-block-paragraph">rg-demo-stg-jpe-001 的 Contributor 是為了讓現有流程先能運作的過渡方案。未來新增團隊時，建議改成各 App resource scope 的 Contributor 或專用 custom role，不要複製成每個團隊都能管理整個 RG。</p>



<p class="wp-block-paragraph">3.2 目前已信任的 GitHub repositories</p>



<p class="wp-block-paragraph">目前只建立以下兩個 main branch Federated Credentials：</p>



<p class="wp-block-paragraph">repo:NYCUITSC/demoproject-backend:ref:refs/heads/main</p>



<p class="wp-block-paragraph">repo:NYCUITSC/demoproject-frontend:ref:refs/heads/main</p>



<p class="wp-block-paragraph">刻意沒有建立：</p>



<ul class="wp-block-list">
<li>pull_request credential。</li>



<li>NYCUITSC/demoproject-api credential。</li>
</ul>



<p class="wp-block-paragraph">demoproject-api 目前是 Frontend 用來產生 SDK 的 dependency，不是 Azure deployment repository。除非它未來真的擁有部署 job，否則不要給它 Azure write identity。</p>



<ol start="4" class="wp-block-list">
<li>新團隊的責任分工</li>
</ol>



<p class="wp-block-paragraph">4.1 平台/環境管理者做一次</p>



<p class="wp-block-paragraph">平台管理者需要：</p>



<ol start="1" class="wp-block-list">
<li>建立該團隊專用的 App Registration 與 Service Principal。</li>



<li>建立只允許指定 repo、branch 或 GitHub Environment 的 Federated Credentials。</li>



<li>在正確的 ACR scope 授予 AcrPush。</li>



<li>在指定 Container App scope 授予更新權限。</li>



<li>在共用 CAE 授予 Reader 與 Container Apps Environment Joiner。</li>



<li>確認 Container App 的 Managed Identity 已具備 ACR、Key Vault 與 SQL 權限。</li>



<li>確認 App 位於正確的共用 CAE，而不是沒有 VNet/private DNS 的獨立 CAE。</li>



<li>將 Client ID、Tenant ID、Subscription ID 提供給團隊設定 GitHub Variables。</li>
</ol>



<p class="wp-block-paragraph">4.2 開發團隊負責</p>



<p class="wp-block-paragraph">開發團隊只需要：</p>



<ol start="1" class="wp-block-list">
<li>維護 GitHub Actions workflow。</li>



<li>在 main 或受保護的 staging Environment 執行 deploy。</li>



<li>Build Docker image。</li>



<li>Push image 到 ACR。</li>



<li>更新既有 Container App 的 image。</li>



<li>查看 revision 與 logs。</li>
</ol>



<p class="wp-block-paragraph">開發團隊不應該：</p>



<ul class="wp-block-list">
<li>建立或刪除 Service Principal。</li>



<li>修改 SQL firewall、Private Endpoint 或 Private DNS。</li>



<li>將 App secret 寫進 GitHub workflow。</li>



<li>執行 SQL CREATE USER。</li>



<li>修改共用 CAE 的 VNet 設定。</li>
</ul>



<ol start="5" class="wp-block-list">
<li>未來新增團隊的標準開通流程</li>
</ol>



<p class="wp-block-paragraph">以下流程由平台管理者執行。每個團隊建議有自己的 Service Principal，不要讓所有團隊共用一個可寫入 Staging 的 identity。</p>



<p class="wp-block-paragraph">5.1 設定資源變數</p>



<p class="wp-block-paragraph">以下是目前資源的範例。新增團隊時，App name 與 repo 名稱應換成新團隊實際值。</p>



<p class="wp-block-paragraph">set -euo pipefail</p>



<p class="wp-block-paragraph">SUB_ID=&#8221;56b72537-d985-4530-88f3-b6ed07e71c67&#8243;</p>



<p class="wp-block-paragraph">APP_RG=&#8221;rg-demo-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">CAE_RG=&#8221;rg-cae-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">CAE_NAME=&#8221;cae-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">ACR_RG=&#8221;rg-demo-stg-jpe-001&#8243;</p>



<p class="wp-block-paragraph">ACR_NAME=&#8221;acrdemostgjpe001&#8243;</p>



<h1 class="wp-block-heading">每個團隊使用不同名稱，例如：</h1>



<h1 class="wp-block-heading">demoproject-sdc-team-2027-ci</h1>



<p class="wp-block-paragraph">SP_NAME=&#8221;demoproject-new-team-ci&#8221;</p>



<p class="wp-block-paragraph">BACKEND_CA_NAME=&#8221;ca-demo-backend-dev-jpe-001&#8243;</p>



<p class="wp-block-paragraph">FRONTEND_CA_NAME=&#8221;ca-demo-frontend-dev-jpe-001&#8243;</p>



<p class="wp-block-paragraph">不要把 SQL Resource Group 放進 CI role scope：</p>



<p class="wp-block-paragraph">rg-data-stg-jpe-001</p>



<p class="wp-block-paragraph">5.2 建立 App Registration 與 Service Principal</p>



<p class="wp-block-paragraph">執行者必須具備 Entra App 建立權限。建立 Azure role assignment 另外需要 Owner 或 User Access Administrator；新加入的開發團隊不需要具備這些管理權限。</p>



<p class="wp-block-paragraph">APP_ID=&#8221;$(az ad app create</p>



<p class="wp-block-paragraph">&#8211;display-name &#8220;$SP_NAME&#8221; \ &#8211;query appId -o tsv)&#8221; SP_OBJECT_ID=&#8221;$(az ad sp create</p>



<p class="wp-block-paragraph">&#8211;id &#8220;$APP_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;query id -o tsv)&#8221;</p>



<p class="wp-block-paragraph">echo &#8220;clientId: $APP_ID&#8221;</p>



<p class="wp-block-paragraph">echo &#8220;servicePrincipalObjectId: $SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">不要用以下方式反查 App ID：</p>



<p class="wp-block-paragraph">az ad app list &#8211;display-name &#8220;$SP_NAME&#8221;</p>



<p class="wp-block-paragraph">因為 display name 可能重複，會拿到錯誤的 App。</p>



<p class="wp-block-paragraph">5.3 授予 CI 最小必要角色</p>



<p class="wp-block-paragraph">先取得 resource IDs：</p>



<p class="wp-block-paragraph">APP_SCOPE=&#8221;/subscriptions/${SUB_ID}/resourceGroups/${APP_RG}&#8221;</p>



<p class="wp-block-paragraph">CAE_SCOPE=&#8221;$(az containerapp env show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$CAE_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$CAE_NAME&#8221; \ &#8211;query id -o tsv)&#8221; ACR_SCOPE=&#8221;$(az acr show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$ACR_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$ACR_NAME&#8221; \ &#8211;query id -o tsv)&#8221; BACKEND_SCOPE=&#8221;$(az containerapp show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$APP_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$BACKEND_CA_NAME&#8221; \ &#8211;query id -o tsv)&#8221; FRONTEND_SCOPE=&#8221;$(az containerapp show</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;resource-group &#8220;$APP_RG&#8221;</p>



<p class="wp-block-paragraph">&#8211;name &#8220;$FRONTEND_CA_NAME&#8221;</p>



<p class="wp-block-paragraph">&#8211;query id -o tsv)&#8221;</p>



<p class="wp-block-paragraph">長期建議只給既有 App scope 的 Contributor：</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Contributor&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$BACKEND_SCOPE&#8221;</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Contributor&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$FRONTEND_SCOPE&#8221;</p>



<p class="wp-block-paragraph">CI push ACR 需要另外的 data-plane role：</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;AcrPush&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$ACR_SCOPE&#8221;</p>



<p class="wp-block-paragraph">使用共用 CAE 需要：</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Reader&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$CAE_SCOPE&#8221;</p>



<p class="wp-block-paragraph">az role assignment create</p>



<p class="wp-block-paragraph">&#8211;subscription &#8220;$SUB_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-object-id &#8220;$SP_OBJECT_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;assignee-principal-type ServicePrincipal</p>



<p class="wp-block-paragraph">&#8211;role &#8220;Container Apps Environment Joiner&#8221;</p>



<p class="wp-block-paragraph">&#8211;scope &#8220;$CAE_SCOPE&#8221;</p>



<p class="wp-block-paragraph">若團隊需要由 CI 建立全新的 Container App，而不是更新既有 App，先由平台管理者評估是否真的需要 RG Contributor。可以先由管理者建立 App，再把後續 CI 限制為既有 App scope。</p>



<p class="wp-block-paragraph">5.4 建立 Federated Credential</p>



<p class="wp-block-paragraph">方案 A：只允許 main branch</p>



<p class="wp-block-paragraph">適合簡單的單一 staging deploy workflow：</p>



<p class="wp-block-paragraph">REPO=&#8221;NEW_ORG/NEW_REPO&#8221;</p>



<p class="wp-block-paragraph">SAFE_NAME=&#8221;${REPO//\//-}-main&#8221; SUBJECT=&#8221;repo:${REPO}:ref:refs/heads/main&#8221;</p>



<p class="wp-block-paragraph">PAYLOAD=&#8221;$(jq -n</p>



<p class="wp-block-paragraph">&#8211;arg name &#8220;$SAFE_NAME&#8221;</p>



<p class="wp-block-paragraph">&#8211;arg subject &#8220;$SUBJECT&#8221;</p>



<p class="wp-block-paragraph">&#8216;{name: $name, issuer: &#8220;<a target="_blank" rel="noopener" href="https://www.google.com/search?q=https://token.actions.githubusercontent.com">https://token.actions.githubusercontent.com</a></p>



<p class="wp-block-paragraph">&#8220;, subject: $subject, audiences: [&#8220;api://AzureADTokenExchange&#8221;] }&#8217;)&#8221;</p>



<p class="wp-block-paragraph">az ad app federated-credential create</p>



<p class="wp-block-paragraph">&#8211;id &#8220;$APP_ID&#8221;</p>



<p class="wp-block-paragraph">&#8211;parameters &#8220;$PAYLOAD&#8221;</p>



<p class="wp-block-paragraph">方案 B：使用受保護的 GitHub Environment（推薦新團隊）</p>



<p class="wp-block-paragraph">在 GitHub repository 建立 staging Environment，設定：</p>



<ul class="wp-block-list">
<li>Required reviewers。</li>



<li>只允許 main branch 或指定 tag。</li>



<li>將 Azure identifiers 放在 Environment variables。</li>
</ul>



<p class="wp-block-paragraph">Federated Credential 的 subject 改為：</p>



<p class="wp-block-paragraph">repo:NEW_ORG/NEW_REPO:environment:staging</p>



<p class="wp-block-paragraph">Workflow 的 deploy job 必須設定：</p>



<p class="wp-block-paragraph">environment: staging</p>



<p class="wp-block-paragraph">Branch subject 與 Environment subject 不要混用。Azure 會比對完整的 sub claim，字串不完全相同就會登入失敗。</p>



<p class="wp-block-paragraph">絕對不要把 write credential 給 pull_request</p>



<p class="wp-block-paragraph">以下 subject 不可綁定到具有 Contributor、AcrPush 或 App write 的 identity：</p>



<p class="wp-block-paragraph">repo:NEW_ORG/NEW_REPO:pull_request</p>



<p class="wp-block-paragraph">PR 的程式碼尚未審核，任何可以修改 workflow 的 PR 都可能嘗試呼叫 Azure CLI。PR workflow 應只執行 lint/test/build；如果真的需要查詢 Azure，另建只有 Reader 的 CI identity。</p>



<p class="wp-block-paragraph">5.5 提供 GitHub Variables</p>



<p class="wp-block-paragraph">提供給團隊的只有識別資訊：</p>



<p class="wp-block-paragraph">AZURE_CLIENT_ID =</p>



<p class="wp-block-paragraph">AZURE_TENANT_ID = 7ef65350-5b77-4958-aca5-0ccadb6bd0b7</p>



<p class="wp-block-paragraph">AZURE_SUBSCRIPTION_ID = 56b72537-d985-4530-88f3-b6ed07e71c67</p>



<p class="wp-block-paragraph">不需要也不應該提供：</p>



<p class="wp-block-paragraph">AZURE_CLIENT_SECRET</p>



<p class="wp-block-paragraph">Client ID、Tenant ID、Subscription ID 本身不是登入密碼；真正的信任條件由 Azure Federated Credential 與 GitHub workflow permission 控制。</p>



<ol start="6" class="wp-block-list">
<li>GitHub Actions Workflow 範本</li>
</ol>



<p class="wp-block-paragraph">以下範例使用「受保護 staging Environment」方案。若使用目前既有的 main branch credential，請移除 environment: staging，或先建立相符的 Environment Federated Credential。</p>



<p class="wp-block-paragraph">name: Deploy Backend to Azure Staging</p>



<p class="wp-block-paragraph">on:</p>



<p class="wp-block-paragraph">push:</p>



<p class="wp-block-paragraph">branches:</p>



<p class="wp-block-paragraph">&#8211; main</p>



<p class="wp-block-paragraph">permissions:</p>



<p class="wp-block-paragraph">contents: read</p>



<p class="wp-block-paragraph">id-token: write</p>



<p class="wp-block-paragraph">jobs:</p>



<p class="wp-block-paragraph">deploy:</p>



<p class="wp-block-paragraph">runs-on: ubuntu-latest</p>



<p class="wp-block-paragraph">environment: staging</p>



<p class="wp-block-paragraph">steps:</p>



<p class="wp-block-paragraph">&#8211; name: Checkout</p>



<p class="wp-block-paragraph">uses: actions/checkout@v4</p>



<p class="wp-block-paragraph">&#8211; name: Login to Azure with OIDC</p>



<p class="wp-block-paragraph">uses: azure/login@v2</p>



<p class="wp-block-paragraph">with:</p>



<p class="wp-block-paragraph">client-id: ${{ vars.AZURE_CLIENT_ID }}</p>



<p class="wp-block-paragraph">tenant-id: ${{ vars.AZURE_TENANT_ID }}</p>



<p class="wp-block-paragraph">subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} &#8211; name: Login to ACR run: az acr login &#8211;name acrdemostgjpe001 &#8211; name: Build and push image env: IMAGE: acrdemostgjpe001.azurecr.io/demo-dev-backend:${{ github.sha }}</p>



<p class="wp-block-paragraph">run: |</p>



<p class="wp-block-paragraph">docker build -t &#8220;$IMAGE&#8221; .</p>



<p class="wp-block-paragraph">docker push &#8220;$IMAGE&#8221; &#8211; name: Update existing Container App env: IMAGE: acrdemostgjpe001.azurecr.io/demo-dev-backend:${{ github.sha }}</p>



<p class="wp-block-paragraph">run: |</p>



<p class="wp-block-paragraph">az containerapp update</p>



<p class="wp-block-paragraph">&#8211;resource-group rg-demo-stg-jpe-001</p>



<p class="wp-block-paragraph">&#8211;name ca-demo-backend-dev-jpe-001</p>



<p class="wp-block-paragraph">&#8211;image &#8220;$IMAGE&#8221;</p>



<p class="wp-block-paragraph">Frontend 只需要替換 Image repository 與 Container App name。</p>



<p class="wp-block-paragraph">6.1 不要在 CI 直接執行完整 deploy.dev.ps1</p>



<p class="wp-block-paragraph">deploy.dev.ps1 適合管理者或本機部署流程，不適合直接放入 GitHub Actions，原因包括：</p>



<ul class="wp-block-list">
<li>會要求 deploy.secrets.ps1。</li>



<li>會將應用程式 secrets 套用到 Container App。</li>



<li>會包含 Managed Identity、Key Vault policy、SQL 授權等 bootstrap 工作。</li>



<li>可能嘗試建立或修改基礎資源。</li>
</ul>



<p class="wp-block-paragraph">CI 的正常流程應是：</p>



<p class="wp-block-paragraph">OIDC login -&gt; docker build -&gt; ACR push -&gt; containerapp update &#8211;image</p>



<p class="wp-block-paragraph">既有的環境變數與 secrets 由平台管理者預先設定；App runtime 使用自己的 Managed Identity 連接 SQL 與 Key Vault。</p>



<p class="wp-block-paragraph">6.2 PR Workflow</p>



<p class="wp-block-paragraph">PR workflow 不應取得 Azure write token：</p>



<p class="wp-block-paragraph">on:</p>



<p class="wp-block-paragraph">pull_request:</p>



<p class="wp-block-paragraph">branches:</p>



<p class="wp-block-paragraph">&#8211; main</p>



<p class="wp-block-paragraph">permissions:</p>



<p class="wp-block-paragraph">contents: read</p>



<p class="wp-block-paragraph">PR 可執行 lint、unit test、build 或不涉及 Staging 的 container build，但不要：</p>



<ul class="wp-block-list">
<li>id-token: write。</li>



<li>azure/login。</li>



<li>Push Staging ACR。</li>



<li>Update 或 delete Container App。</li>
</ul>



<ol start="7" class="wp-block-list">
<li>目前既有 Workflow 的注意事項</li>
</ol>



<p class="wp-block-paragraph">目前 demoproject-backend/.github/workflows/dev.yaml 與 demoproject-frontend/.github/workflows/dev.yaml 的 Dev 流程原本是：</p>



<ol start="1" class="wp-block-list">
<li>Build/test。</li>



<li>Push 到 harbor.sdc.nycu.club。</li>



<li>呼叫 n8n deployment webhook。</li>
</ol>



<p class="wp-block-paragraph">建立 Azure OIDC identity 不會自動改變這些 workflow。若要切換到 Azure Container Apps：</p>



<ol start="1" class="wp-block-list">
<li>由團隊決定是否停止 Harbor+n8n deployment。</li>



<li>將 Azure login、ACR push、Container App update 加入新的 deploy job。</li>



<li>確認新 workflow 的 subject 與 Federated Credential 完全一致。</li>



<li>先在 staging Environment 做一次受審核測試。</li>



<li>確認 revision、logs、API version 與 Frontend URL 後，再移除舊流程。</li>
</ol>



<p class="wp-block-paragraph">不要讓 Harbor+n8n 與 Azure CI 同時部署同一個功能環境，否則最後一個完成的 pipeline 可能覆蓋前一個結果。</p>



<ol start="8" class="wp-block-list">
<li>SQL 與網路權限不要放到 CI</li>
</ol>



<p class="wp-block-paragraph">8.1 CI identity 不需要 SQL role</p>



<p class="wp-block-paragraph">CI Service Principal 只更新 Container App image。SQL runtime access 由 Backend Container App 的 system-assigned Managed Identity 處理：</p>



<p class="wp-block-paragraph">GitHub CI identity -&gt; ACR push -&gt; Container App update</p>



<p class="wp-block-paragraph">Backend App Managed Identity -&gt; Private Endpoint -&gt; demoproject-dev</p>



<p class="wp-block-paragraph">如果 App 被刪除後重新建立，system-assigned identity 可能改變。平台/DB 管理者需要重新確認：</p>



<ul class="wp-block-list">
<li>AcrPull。</li>



<li>Key Vault secret get/list。</li>



<li>demoproject-dev contained user。</li>



<li>db_datareader、db_datawriter、必要時 db_ddladmin。</li>
</ul>



<p class="wp-block-paragraph">8.2 CI identity 不需要修改 SQL firewall</p>



<p class="wp-block-paragraph">目前 SQL 使用 Private Endpoint 與 Private DNS。應用程式應部署到有正確 VNet 路徑的共用 cae-stg-jpe-001。</p>



<p class="wp-block-paragraph">遇到以下錯誤時，不要直接把 GitHub Actions Service Principal 加到 SQL 或把 firewall 設成 0.0.0.0：</p>



<p class="wp-block-paragraph">Client with IP address &#8216;&#8230;&#8217; is not allowed to access the server</p>



<p class="wp-block-paragraph">這通常代表 Container App 位於沒有 VNet/private DNS 的 CAE，或 hostname 解析走到公網。請先檢查 App 的 managedEnvironmentId。</p>



<ol start="9" class="wp-block-list">
<li>驗證清單</li>
</ol>



<p class="wp-block-paragraph">9.1 管理者驗證 App 與 Federated Credentials</p>



<pre class="wp-block-code"><code>az ad app federated-credential list
--id "$APP_ID"
--query "&#91;].{name:name,subject:subject,issuer:issuer,audiences:audiences}"
-o table</code></pre>



<p class="wp-block-paragraph">應確認：</p>



<ul class="wp-block-list">
<li>issuer 是 <a href="https://www.google.com/search?q=https://token.actions.githubusercontent.com" target="_blank" rel="noopener">https://token.actions.githubusercontent.com</a>。</li>



<li>audience 是 api://AzureADTokenExchange。</li>



<li>subject 只有指定 repo 與指定 main/environment。</li>



<li>沒有 write identity 的 pull_request subject。</li>
</ul>



<p class="wp-block-paragraph">9.2 驗證 Azure RBAC</p>



<pre class="wp-block-code"><code>az role assignment list
--assignee-object-id "$SP_OBJECT_ID"
--all
--query "&#91;].{role:roleDefinitionName,scope:scope}"
-o table</code></pre>



<p class="wp-block-paragraph">不應出現：</p>



<ul class="wp-block-list">
<li>Contributor at rg-cae-stg-jpe-001。</li>



<li>任何 role at rg-data-stg-jpe-001。</li>



<li>不必要的 SQL 或 Key Vault data-plane role。</li>
</ul>



<p class="wp-block-paragraph">9.3 團隊驗證 workflow</p>



<p class="wp-block-paragraph">部署成功後確認：</p>



<pre class="wp-block-code"><code>az acr repository show-tags
--name acrdemostgjpe001
--repository demo-dev-backend
--orderby time_desc
--top 5
-o table

az containerapp revision list
--resource-group rg-demo-stg-jpe-001
--name ca-demo-backend-dev-jpe-001
--query "&#91;].{name:name,health:properties.healthState,running:properties.runningState,traffic:properties.trafficWeight}"
-o table

az containerapp logs show
--resource-group rg-demo-stg-jpe-001
--name ca-demo-backend-dev-jpe-001
--type console
--tail 100</code></pre>



<ol start="10" class="wp-block-list">
<li>常見錯誤</li>
</ol>



<ul class="wp-block-list">
<li>AADSTS70021: No matching federated identity record found | 原因：subject、branch/environment、issuer 或 audience 不一致 | 處理方式：對照 GitHub workflow 的 trigger 與 environment，列出 FIC 逐字比較</li>



<li>AuthorizationFailed | 原因：CI identity 沒有對應 scope 的 role | 處理方式：由平台管理者補上 App scope、ACR 或 CAE join role</li>



<li>denied: requested access to the resource is denied | 原因：ACR 沒有 AcrPush | 處理方式：在 ACR scope 加 AcrPush，不是只加 Contributor</li>



<li>Container App cannot join environment | 原因：缺 Microsoft.App/managedEnvironments/join/action | 處理方式：在共用 CAE scope 加 Container Apps Environment Joiner</li>



<li>SQL Client with IP &#8230; not allowed | 原因：App 走公網，沒有走 Private Endpoint | 處理方式：檢查 App 是否位於共用 CAE，不要先加大量 firewall IP</li>



<li>SQL Login failed | 原因：App Managed Identity 未建立 contained user 或 SQL role | 處理方式：查 App identity，由 DB 管理者處理，不是修改 CI role</li>



<li>Image push 後沒有新 revision | 原因：workflow push 到錯誤 ACR/tag，或沒有執行 az containerapp update | 處理方式：檢查 image URI、ACR repository、revision list 與 workflow log</li>



<li>Workflow 使用 tag 觸發但 Azure login 失敗 | 原因：只有 main branch subject，沒有符合 tag/environment 的 FIC | 處理方式：改成只由 main deploy，或由管理者建立精準的 tag/environment FIC</li>
</ul>



<ol start="11" class="wp-block-list">
<li>團隊撤換與緊急撤權</li>
</ol>



<p class="wp-block-paragraph">OIDC 不使用長期 client secret，因此撤權主要是刪除 Federated Credential 或 Azure role assignment。</p>



<p class="wp-block-paragraph">移除某個 repo 的信任</p>



<p class="wp-block-paragraph">先列出 credential ID：</p>



<pre class="wp-block-code"><code>az ad app federated-credential list
--id "$APP_ID"
--query "&#91;].{id:id,name:name,subject:subject}"
-o table</code></pre>



<p class="wp-block-paragraph">再刪除指定 credential：</p>



<pre class="wp-block-code"><code>az ad app federated-credential delete
--id "$APP_ID"
--federated-credential-id ""</code></pre>



<p class="wp-block-paragraph">移除 Azure role</p>



<pre class="wp-block-code"><code>az role assignment delete
--assignee-object-id "$SP_OBJECT_ID"
--role "AcrPush"
--scope "$ACR_SCOPE"</code></pre>



<p class="wp-block-paragraph">撤換整個團隊時，應同時：</p>



<ol start="1" class="wp-block-list">
<li>移除 GitHub Environment/repository variables。</li>



<li>刪除該 repo 的 Federated Credentials。</li>



<li>移除 App、ACR、CAE scope 的 role assignments。</li>



<li>確認 Container App runtime Managed Identity 不被誤刪。</li>



<li>保留 audit log 與變更紀錄。</li>



<li>新團隊開通檢查表</li>
</ol>



<p class="wp-block-paragraph">平台管理者</p>



<ul class="wp-block-list">
<li>確認目標 repo 與實際部署的 Backend/Frontend。</li>



<li>每個團隊建立獨立 Service Principal。</li>



<li>只建立 main 或受保護 Environment 的 FIC。</li>



<li>不建立 write identity 的 pull_request FIC。</li>



<li>ACR scope 有 AcrPush。</li>



<li>既有 App scope 有更新權限。</li>



<li>共用 CAE scope 有 Reader + Container Apps Environment Joiner。</li>



<li>沒有授予 SQL Resource Group 或共用 CAE Resource Group Contributor。</li>



<li>確認 App runtime Managed Identity 的 SQL/Key Vault/ACR 權限。</li>



<li>將三個 Azure identifiers 提供給 GitHub Environment variables。</li>
</ul>



<p class="wp-block-paragraph">開發團隊</p>



<ul class="wp-block-list">
<li>Workflow 有 permissions: id-token: write。</li>



<li>Deploy job 的 environment 或 branch subject 與 Azure FIC 完全一致。</li>



<li>使用 azure/login@v2，沒有 client secret。</li>



<li>Docker image 使用不可變的 commit SHA tag。</li>



<li>Push 到正確的 ACR repository。</li>



<li>只更新指定 Container App，不修改 CAE/VNet/SQL。</li>



<li>PR workflow 沒有 Azure write token。</li>



<li>部署後確認 revision、logs 與功能。</li>
</ul>



<ol start="13" class="wp-block-list">
<li>相關文件</li>
</ol>



<ul class="wp-block-list">
<li>Staging 新手指南： STAGING-ONBOARDING-ZH-TW.md</li>



<li>Dev 部署腳本： deploy.dev.ps1</li>



<li>Backend 權限說明： demoproject-backend\docs\dev-deployment-permissions.md</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/azure-github-actions-oidc-ci-cd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>😱 救命！硬碟又被吃光了！用 Python 打造「自動刪除備份檔」</title>
		<link>https://stackoverflow.max-everyday.com/2026/09/clean_bak/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/09/clean_bak/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 08 Sep 2026 02:12:45 +0000</pubDate>
				<category><![CDATA[Python筆記]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8736</guid>

					<description><![CDATA[你是否有過這種經歷？ 某個平靜的清晨，你正端著咖...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_16295216377751810940.jpg?v=1788832742" alt="" class="wp-image-8737" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_16295216377751810940.jpg?v=1788832742 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_16295216377751810940-600x335.jpg?v=1788832742 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/09/watermarked_img_16295216377751810940-768x429.jpg?v=1788832742 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">你是否有過這種經歷？</p>



<p class="wp-block-paragraph">某個平靜的清晨，你正端著咖啡準備開啟美好的一天，突然監報系統傳來刺眼的紅字警告：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>[CRITICAL] Server Disk Space Usage: 99.8% !!!</strong></p>
</blockquote>



<p class="wp-block-paragraph">你心裡一驚，打開伺服器一查，罪魁禍首不出所料——又是那些默默囤積、永無止境的 DB 備份檔（<code>.bak</code>）！</p>



<p class="wp-block-paragraph">為了防止「伺服器暴斃」與「半夜被叫醒維護」的悲劇發生，今天我們就來聊聊怎麼用 Python 寫出一個聰明、安全又省心的 <strong>「.bak 檔案自動清理神器」</strong>！</p>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a3.png" alt="💣" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 備份檔的「膨脹魔咒」：為什麼硬碟總是不夠用？</h2>



<p class="wp-block-paragraph">在 IT 與開發者的日常裡，資料庫備份是我們的生命線。「每天備份一次」是基本常識，甚至有些高頻系統每小時都在備份。</p>



<p class="wp-block-paragraph">一個每日 1GB 的 DB 備份檔，聽起來好像還好對吧？</p>



<ul class="wp-block-list">
<li>1 天 = 1 GB</li>



<li>1 個月 = 30 GB</li>



<li>半年 = <strong>180 GB！</strong></li>



<li>1 年 = <strong>365 GB！</strong></li>
</ul>



<p class="wp-block-paragraph">如果你有好幾個資料庫，再加上歷史累積，硬碟空間很快就會像連假期間的高速公路一樣——<strong>卡到爆！</strong></p>



<p class="wp-block-paragraph">最可怕的是，許多系統管理者往往都是在<strong>硬碟被塞爆、服務掛掉的那一刻</strong>，才驚覺原來半年前的備份檔還躺在那裡吸血。</p>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9b8-200d-2642-fe0f.png" alt="🦸‍♂️" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 拯救硬碟！「備份檔終結者」腳本上陣</h2>



<p class="wp-block-paragraph">為了避免「手動刪檔刪到手軟」或是「誤刪最新備份直接準備提頭來見」，我們需要一個具備以下特性的自動化腳本：</p>



<ol class="wp-block-list">
<li><strong>預設安全機制（Dry-Run）</strong>：先預覽「會刪哪些檔」，確認無誤再真的動手，安全感滿分！</li>



<li><strong>彈性門檻設定</strong>：預設保留 180 天，但也支援隨時調整成 30 天或 90 天。</li>



<li><strong>明確的視覺回饋</strong>：清楚標示每個檔案過期了多久（例如：<code>185.2 天前</code>），不再瞎猜。</li>
</ol>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 靈魂核心程式碼</h3>



<p class="wp-block-paragraph">來看看這個輕巧又強大的 Python 腳本（<code>clean_bak.py</code>）：</p>



<pre class="wp-block-code"><code>import argparse
import sys
import time
from pathlib import Path


def cleanup_bak_files(
    target_dir: str, days: int = 180, execute_delete: bool = False
) -&gt; None:
    target_path = Path(target_dir).resolve()

    if not target_path.exists() or not target_path.is_dir():
        print(f"&#x274c; 錯誤：目標目錄 '{target_path}' 不存在。")
        sys.exit(1)

    now = time.time()
    files_found = 0
    files_matched = 0

    mode_label = (
        "&#x1f525;【實際刪除模式】" if execute_delete else "&#x1f6e1;【模擬預覽模式 (Dry-Run)】"
    )
    print(f"&#x1f3af; 目標目錄: {target_path}")
    print(f"&#x23f3; 保留天數門檻: {days} 天")
    print(f"&#x2699; 執行模式: {mode_label}\n" + "-" * 60)

    # 搜尋目錄下的所有 .bak 檔案
    for file_path in target_path.glob("*.bak"):
        files_found += 1
        mtime = file_path.stat().st_mtime
        file_age_days = (now - mtime) / 86400

        if file_age_days &gt; days:
            files_matched += 1
            age_formatted = f"{file_age_days:.1f} 天前"

            if execute_delete:
                try:
                    file_path.unlink()
                    print(
                        f"&#x1f5d1; &#91;已刪除]     {file_path.name} (修改時間: {age_formatted})"
                    )
                except Exception as e:
                    print(f"&#x26a0; &#91;刪除失敗]   無法刪除 {file_path.name}: {e}")
            else:
                print(
                    f"&#x1f440; &#91;將會刪除]   {file_path.name} (修改時間: {age_formatted})"
                )

    print("-" * 60)
    if not execute_delete:
        print(
            f"&#x1f4a1; 預覽結束：在 {files_found} 個 `.bak` 檔案中，發現 {files_matched} 個超過 {days} 天老舊檔案。"
        )
        if files_matched &gt; 0:
            print("&#x1f449; 若要真正刪除這些檔案，請加上 `--delete` 參數重新執行！")
    else:
        print(
            f"&#x1f389; 清理完成：已成功釋放空間！刪除了 {files_matched} 個過期檔案。"
        )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="預覽或刪除超過指定天數的 .bak 備份檔案。"
    )
    parser.add_argument(
        "directory",
        nargs="?",
        default=".",
        help="備份檔案所在的目錄路徑（預設為目前目錄）",
    )
    parser.add_argument(
        "--days",
        type=int,
        default=180,
        help="檔案保留天數上限（預設：180 天）",
    )
    parser.add_argument(
        "--delete",
        action="store_true",
        help="加入此參數以進行「實際刪除」操作",
    )

    args = parser.parse_args()
    cleanup_bak_files(args.directory, days=args.days, execute_delete=args.delete)
</code></pre>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 實戰演練：三步驟教你輕鬆上手</h2>



<h3 class="wp-block-heading">1&#x20e3; 第一步：演習比實戰重要！先來個「預覽演練」</h3>



<p class="wp-block-paragraph">預設狀態下，腳本只會「看」不會「刪」。直接執行腳本，看看有哪些老古董檔案準備該下架：</p>



<pre class="wp-block-code"><code>python clean_bak.py /var/backups/portal
</code></pre>



<p class="wp-block-paragraph"><strong>輸出結果超清晰：</strong></p>



<pre class="wp-block-code"><code>&#x1f3af; 目標目錄: /var/backups/portal
&#x23f3; 保留天數門檻: 180 天
&#x2699; 執行模式: &#x1f6e1;【模擬預覽模式 (Dry-Run)】
------------------------------------------------------------
&#x1f440; &#91;將會刪除]   portal_backup_2026_01_01_000000.bak (修改時間: 220.4 天前)
------------------------------------------------------------
&#x1f4a1; 預覽結束：在 25 個 `.bak` 檔案中，發現 1 個超過 180 天老舊檔案。
&#x1f449; 若要真正刪除這些檔案，請加上 `--delete` 參數重新執行！
</code></pre>



<h3 class="wp-block-heading">2&#x20e3; 第二步：確定無誤，開火刪除！</h3>



<p class="wp-block-paragraph">確認過清單後，加上 <code>--delete</code> 參數，給過期檔案來個大掃除：</p>



<pre class="wp-block-code"><code>python clean_bak.py /var/backups/portal --delete
</code></pre>



<p class="wp-block-paragraph">瞬間，硬碟空間就回歸綠色健康狀態，空氣都變得新鮮了！</p>



<h3 class="wp-block-heading">3&#x20e3; 第三步：交給排程工具，一勞永逸！</h3>



<p class="wp-block-paragraph">工程師的終極目標就是「能自動化就絕不手動」。設定好 <strong>Linux Crontab</strong> 或 <strong>Windows 工作排程器</strong>，讓伺服器每天半夜自動執行清理：</p>



<ul class="wp-block-list">
<li><strong>Linux (Crontab)：</strong><br><code>0 2 * * * /usr/bin/python3 /path/to/clean_bak.py /var/backups/portal --delete >> /var/log/clean_bak.log 2>&amp;1<br></code><em>每天凌晨 02:00 自動掃描並清理，還會把紀錄寫進 log 檔，安心度 100%！</em></li>
</ul>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 經驗談：備份維護的黃金法則</h2>



<ol class="wp-block-list">
<li><strong>離線/異地備份才是真愛</strong>：本地端只保留近 30~180 天的快取備份，更久遠的歷史資料請定期同步至 AWS S3、Google Cloud Storage 或冷儲存（Glacier）。</li>



<li><strong>權限控管要做好</strong>：確保執行清理腳本的系統帳號只有該目錄的權限，避免權限過大衍生安全風險。</li>



<li><strong>定期測試還原</strong>：<strong>能還原的備份才叫備份，不能還原的只是佔空間的垃圾！</strong> 記得定期做備份還原演練喔！</li>
</ol>



<h2 class="wp-block-heading">結語</h2>



<p class="wp-block-paragraph">別再讓爆掉的硬碟毀了你的週末！快把這套 Python 腳本部署上線，讓伺服器自動保持清爽健康吧！</p>



<p class="wp-block-paragraph">你平時是用什麼方式管理資料庫備份的呢？歡迎在下方留言交流你的「硬碟省空間妙招」！ <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">直接編輯或用 Gemini 編輯</p>



<p class="wp-block-paragraph">點選任一處即可直接輸入及編輯，或選取文字，提示 Gemini 進行修改。</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/09/clean_bak/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>將 Pi Coding Agent 無縫對接本地 llama-server 自訂模型</title>
		<link>https://stackoverflow.max-everyday.com/2026/08/pi-coding-agent-llama-server/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/08/pi-coding-agent-llama-server/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 05:16:06 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8683</guid>

					<description><![CDATA[在體驗 LLM 驅動的命令列開發工具時，許多人喜...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_11904330739739367754.jpg?v=1788066680" alt="" class="wp-image-8685" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_11904330739739367754.jpg?v=1788066680 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_11904330739739367754-600x335.jpg?v=1788066680 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_11904330739739367754-768x429.jpg?v=1788066680 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">在體驗 LLM 驅動的命令列開發工具時，許多人喜歡在本地使用 <code>llama-server.exe</code> 載入 GGUF 模型（如 Qwen 系列），既能節省 API 費用又能保護隱私。</p>



<p class="wp-block-paragraph">不過，當嘗試將 <strong>Pi Coding Agent</strong> 連接到本地伺服器時，常會遇到 <code>Server is not running in llama.cpp router mode</code> 或 OpenAI API <code>401 Incorrect API key</code> 等驗證錯誤。</p>



<p class="wp-block-paragraph">這篇文章記錄了完整的除錯過程與最終解決方案，幫助你快速完成本地環境設定。</p>



<h2 class="wp-block-heading">問題解析</h2>



<p class="wp-block-paragraph">為什麼預設情況下會連線失敗？</p>



<ul class="wp-block-list">
<li><strong>端點路由不符</strong>：Pi Agent 預設對接 <code>llama.cpp</code> 時，期待對方開啟多模型路由（Router Mode）。如果本地只是單純啟動單一 GGUF 模型，會無法通過驗證。</li>



<li><strong>API Key 驗證攔截</strong>：如果 <code>llama-server</code> 開啟了 <code>--api-key</code>，Pi Agent 若未帶入對應金鑰，或是被預設路徑引導至 OpenAI 官方伺服器，就會觸發 401 Unauthorized 錯誤。</li>
</ul>



<h2 class="wp-block-heading">解決步驟</h2>



<h3 class="wp-block-heading">步驟一：修改 llama-server 啟動批次檔</h3>



<p class="wp-block-paragraph">首先，確保你的 <code>llama-server.exe</code> 啟動時明確設定了 API Key。</p>



<p class="wp-block-paragraph">在你的批次檔（例如 <code>start-server.bat</code>）中加入 <code>--api-key</code> 參數：</p>



<pre class="wp-block-code"><code>set MODEL=models\Qwen3.8-27B-UD-IQ1_S.gguf
set EXE=llama-server.exe

REM Large context for long code.
set CTX=16384
set BATCH=512
set NP=1

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -cmoe ^
  -b %BATCH% -ub %BATCH% ^
  -ngl 999 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  -rea off ^
  --reasoning-format none ^
  --temp 0.3 ^
  --top-p 0.8 ^
  --top-k 30 ^
  --repeat-penalty 1.08 ^
  --context-shift</code></pre>



<p class="wp-block-paragraph">上面參數微調, 參考看看: 「思維鏈坍塌」超低位元量化模型遇到無休止內部思考、自我糾正<br><a href="https://stackoverflow.max-everyday.com/2026/08/chain-of-thought-collapse/">https://stackoverflow.max-everyday.com/2026/08/chain-of-thought-collapse/</a></p>



<p class="wp-block-paragraph">啟動後，可以使用 CMD 的 <code>curl</code> 測試 OpenAI 相容端點是否正常運作：</p>



<pre class="wp-block-code"><code>curl http://127.0.0.1:8080/v1/models -H "Authorization: Bearer 12345678"
</code></pre>



<p class="wp-block-paragraph">若有正確返回 JSON 格式的模型清單，代表伺服器端設定完成。</p>



<h3 class="wp-block-heading">步驟二：配置 Pi Agent 的自訂 Provider</h3>



<p class="wp-block-paragraph">Pi Agent 允許透過設定檔擴充自訂的 Provider。</p>



<p class="wp-block-paragraph">在 CMD 中建立並開啟設定檔：</p>



<pre class="wp-block-code"><code>if not exist "%USERPROFILE%\.pi\agent" mkdir "%USERPROFILE%\.pi\agent"
notepad "%USERPROFILE%\.pi\agent\models.json"
</code></pre>



<p class="wp-block-paragraph">貼上以下 JSON 設定。重點在於要明確指定 <code>"api": "openai-completions"</code>，否則 Pi 會因為缺少通訊協定設定而報錯：</p>



<pre class="wp-block-code"><code>{
  "providers": {
    "local-llama": {
      "baseUrl": "http://127.0.0.1:8080/v1",
      "apiKey": "12345678",
      "api": "openai-completions",
      "models": &#91;
        {
          "id": "models\\Qwen3.8-27B-UD-IQ1_S.gguf",
          "name": "Qwen3.8-Local",
          "contextWindow": 16384,
          "maxTokens": 4096
        },
        {
          "id": "models\\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf",
          "name": "Gemma-4-12B-Local",
          "contextWindow": 16384,
          "maxTokens": 4096
        }
      ]
    }
  }
}</code></pre>



<p class="wp-block-paragraph">實際測試，模型名稱寫錯，還是可以正常執行，滿神奇的。</p>



<h3 class="wp-block-heading">步驟三：驗證並啟動 Pi Agent</h3>



<p class="wp-block-paragraph">設定完成後，在 CMD 執行模型清單檢視指令：</p>



<pre class="wp-block-code"><code>pi --list-models
</code></pre>



<p class="wp-block-paragraph">確認列表中出現了 <code>local-llama</code> 相關模型。</p>



<p class="wp-block-paragraph">接著建立一個專用的啟動批次檔 <code>run-pi-qwen3.8.bat</code>：</p>



<pre class="wp-block-code"><code>@echo off
pi --model "local-llama/models\Qwen3.8-27B-UD-IQ1_S.gguf"
</code></pre>



<p class="wp-block-paragraph">執行 <code>run-pi.bat</code> 即可順利在 Pi Agent 中與本地模型進行對話！</p>



<h2 class="wp-block-heading">總結</h2>



<p class="wp-block-paragraph">解決此問題的核心在於：</p>



<ol start="1" class="wp-block-list">
<li>本地伺服器需顯式指定 API Key 並提供 OpenAI 相容端點。</li>



<li>Pi Agent 的 <code>models.json</code> 必須完整填寫 <code>api: "openai-completions"</code> 規範。</li>
</ol>



<p class="wp-block-paragraph">透過自訂 Provider 機制，不僅能繞過官方 API 限制，還能靈活切換各種在地端運行的 GGUF 大語言模型！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">選 llama.cpp 比較好的感覺, 可以設定的參數比較多.</p>



<p class="wp-block-paragraph"><strong>ollama</strong> 與 <strong>llama.cpp</strong> 底層皆基於 C/C++ 的推論引擎，但兩者的定位、使用對象與專案目標有著本質上的不同：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>特性</strong></td><td><strong>llama.cpp</strong></td><td><strong>Ollama</strong></td></tr></thead><tbody><tr><td><strong>主要定位</strong></td><td>底層推論核心 / 開發者工具</td><td>上層封裝與管理工具 / 終端使用者應用</td></tr><tr><td><strong>使用門檻</strong></td><td>較高（需熟悉指令列參數、自行下載模型與設定）</td><td>極低（一鍵安裝，具備類似 Docker 的指令與體驗）</td></tr><tr><td><strong>模型格式</strong></td><td><strong>GGUF</strong></td><td><strong>Modelfile</strong>（內部打包並調用 GGUF）</td></tr><tr><td><strong>服務架構</strong></td><td>原生編譯後為單一可執行檔或 C++ 函式庫</td><td>後台背景服務 (Daemon) + CLI 前端</td></tr><tr><td><strong>生態系統</strong></td><td>提供各種綁定 (Python, Rust 等) 與低階 API</td><td>提供相容 OpenAI 的 REST API，整合開源 UI 極佳</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">關鍵差異解析</h3>



<p class="wp-block-paragraph"><strong>1. llama.cpp：極致效能與底層控制</strong></p>



<ul class="wp-block-list">
<li><strong>核心價值</strong>：由 Georgi Gerganov 開發，旨在讓大型語言模型能在無 GPU 或硬體受限的普通消費級設備（如 Mac Apple Silicon、普通 PC）上高效運行。</li>



<li><strong>優勢</strong>：
<ul class="wp-block-list">
<li><strong>細粒度控制</strong>：可直接調整 KV 快取、GPU 層數分流 (<code>-ngl</code>)、Context 長度、Sampler 參數等。</li>



<li><strong>高擴充性</strong>：身為基礎架構，被無數上層工具（如 Python 庫 <code>llama-cpp-python</code>、text-generation-webui 等）整合。</li>
</ul>
</li>



<li><strong>劣勢</strong>：設定繁瑣，下載的模型需要手動管理檔案路徑與參數設定。</li>
</ul>



<p class="wp-block-paragraph"><strong>2. Ollama：極簡體驗與模型生態</strong></p>



<ul class="wp-block-list">
<li><strong>核心價值</strong>：將 llama.cpp 包裝成極簡化的桌面/伺服器工具，核心體驗借鑑了 Docker。</li>



<li><strong>優勢</strong>：
<ul class="wp-block-list">
<li><strong>開箱即用</strong>：只需執行 <code>ollama run llama3</code>，就會自動下載模型並直接啟動對話。</li>



<li><strong>模型庫管理</strong>：擁有官方模型庫 (library)，下載與更新非常方便。</li>



<li><strong>標準化 API</strong>：預設提供開箱即用的 REST API，能 seamlessly 介接 Open WebUI、AnythingLLM 或各類本地插件。</li>
</ul>
</li>



<li><strong>劣勢</strong>：預設封裝隱藏了許多底層參數，若要高度客製化推論細節，需要透過編輯 <code>Modelfile</code> 完成。</li>
</ul>



<h3 class="wp-block-heading">該如何選擇？</h3>



<ul class="wp-block-list">
<li>選 <strong>Ollama</strong>：如果你想要快速在本地端跑起 LLM、連結現成的 Web UI，或是為自己的應用程式快速接上本地 API。</li>



<li>選 <strong>llama.cpp</strong>：如果你是 C/C++ 開發者、需要整合嵌入式系統、或者需要對硬體推論細節進行極致優化與自訂。</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/08/pi-coding-agent-llama-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>「思維鏈坍塌」超低位元量化模型遇到無休止內部思考、自我糾正</title>
		<link>https://stackoverflow.max-everyday.com/2026/08/chain-of-thought-collapse/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/08/chain-of-thought-collapse/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 03:47:19 +0000</pubDate>
				<category><![CDATA[AI開發筆記]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[Qwen]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8679</guid>

					<description><![CDATA[這現象叫思維鏈坍塌，簡單說就是模型卡在思考迴圈裡...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_2106985570617172306.jpg?v=1788061504" alt="" class="wp-image-8680" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_2106985570617172306.jpg?v=1788061504 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_2106985570617172306-600x335.jpg?v=1788061504 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/watermarked_img_2106985570617172306-768x429.jpg?v=1788061504 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">這現象叫思維鏈坍塌，簡單說就是模型卡在思考迴圈裡鬼打牆！</p>



<p class="wp-block-paragraph">當你用超低位元量化模型（像 Qwen 3.8 27B 搭配 IQ1_S）時，模型的大腦精準度被嚴重壓縮。這導致它在算下一個詞的時候猶豫不決，完全搞不懂自己什麼時候該打住並輸出答案，最後就演變成無休止的自我糾正小劇場。</p>



<p class="wp-block-paragraph">想治好模型的鬼打牆，你可以從這三大招入手：</p>



<h2 class="wp-block-heading">改提示詞（最快見效）</h2>



<ul class="wp-block-list">
<li>強制封口：在提示詞裡直接下達死命令，要求模型隱藏或停用思考過程。例如寫下：「請直接回答問題，嚴禁在回答前進行任何自我剖析、內部思考或草稿推演。」</li>



<li>給定無知預設範本：遇到抓不到的資訊，直接給它一套標準答案。例如加上：「你無法取得即時的時間與日期。若使用者詢問日期，請直接回應『我無法獲取目前的系統時間』，別再浪費時間思考。」</li>
</ul>



<h2 class="wp-block-heading">調推論參數</h2>



<ul class="wp-block-list">
<li>降低溫度值（Temperature）：調到 0.1 到 0.3 之間，讓模型的選擇更果決，減少徘徊不決的機率。</li>



<li>調低 Top-P / Top-K：縮小模型的選詞範圍，強迫它選機率最高的字，避免掉進選擇困難的陷阱。</li>



<li>設定重複懲罰（Repeat Penalty）：把係數稍微調高（比如 1.1 到 1.15），強制模型打破重複喃喃自語的模式。</li>



<li>設定終止字詞（Stop Sequences）：加入常見的思考標籤（例如 或特定結尾符號）當作硬性中斷點。</li>
</ul>



<p class="wp-block-paragraph">llama-server.exe 參數</p>



<pre class="wp-block-code"><code>加入的參數說明如下：

--temp 0.2：將溫度設定為 0.2，落在你要求的 0.1 到 0.3 之間，大幅提升選擇的確定性。

--top-p 0.8 與 --top-k 20：縮小選詞範圍，只保留高機率的詞彙組合。

--repeat-penalty 1.12：設定重複懲罰係數為 1.12，防止模型陷入無意義的文字循環。</code></pre>



<p class="wp-block-paragraph">llama-server（基於 llama.cpp）中，這些參數的預設值如下：</p>



<ul class="wp-block-list">
<li>&#8211;temp（溫度值）：預設值為 0.8。</li>



<li>&#8211;top-p（Top-P 採樣）：預設值為 0.95。</li>



<li>&#8211;top-k（Top-K 採樣）：預設值為 40。</li>



<li>&#8211;repeat-penalty（重複懲罰）：預設值為 1.0（代表完全不施加重複懲罰）。</li>



<li>&#8211;reverse-prompt（終止字詞）：預設值為空（即未設定任何反向提示詞或終止標籤）。</li>



<li>原本的 llama-server 設定偏向一般對話與創意生成，因此預設的採樣範圍較廣、隨機性較高。修改後的參數則顯著降低了生成過程中的不確定因素，能讓程式碼輸出與長文本生成更加穩定。</li>
</ul>



<p class="wp-block-paragraph">改用 np 1 會是更好的選擇。</p>



<p class="wp-block-paragraph">主要原因說明：</p>



<ol start="1" class="wp-block-list">
<li>記憶體（VRAM）會被重複分配 設定 np 2 代表伺服器會把上下文長度（Context Window）預先切成 2 份獨立的空間。如果你的 context 設為 16384，系統實際上會為每份插槽分配記憶體。在單人使用的個人電腦上，這會無謂消耗大量的顯存與記憶體。</li>



<li>個人開發與 Agent 工具屬於單一請求 在使用 Pi Agent 或個人 CLI 工具時，基本上一次只會發送一個 Prompt 並等待回覆，完全不需要伺服器同時平行處理多個使用者的請求。</li>



<li>效能與推論速度無關 np 設定的是平行處理的請求數量，而不是 CPU 或 GPU 的計算核心數。設成 2 並不會讓單一回應速度變快，甚至可能因為資源被瓜分而影響效能。</li>
</ol>



<p class="wp-block-paragraph">調整建議：</p>



<p class="wp-block-paragraph">將批次檔中的設定改為： set NP=1</p>



<p class="wp-block-paragraph">使用超少 VRAM 執行 Qwen3.8-27B</p>



<pre class="wp-block-code"><code>set MODEL=models\Qwen3.8-27B-UD-IQ1_S.gguf
set EXE=llama-server.exe

REM Large context for long code.
set CTX=16384
set BATCH=512
set NP=1

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -cmoe ^
  -b %BATCH% -ub %BATCH% ^
  -ngl 999 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  -rea off ^
  --reasoning-format none ^
  --temp 0.3 ^
  --top-p 0.8 ^
  --top-k 30 ^
  --repeat-penalty 1.08 ^
  --context-shift</code></pre>



<p class="wp-block-paragraph">如果顯示:</p>



<pre class="wp-block-preformatted">KV cache shifting is not supported for this context, disabling KV cache shiftin</pre>



<p class="wp-block-paragraph">代表該模型架構不支援 &#8211;context-shift，可以直接將這個參數移除，避免系統輸出無用警告。</p>



<p class="wp-block-paragraph">如果顯示:</p>



<pre class="wp-block-preformatted">OUT_OF_DEVICE_MEMORY</pre>



<p class="wp-block-paragraph"><strong>VRAM 完全爆掉（<code>UR_RESULT_ERROR_OUT_OF_DEVICE_MEMORY</code>）</strong> Qwen3.8-27B 模型的層數（Layers）通常只有 64 層左右，但批次檔傳入了 <code>-ngl 256</code>。系統會嘗試把全部 64 層加上 KV Cache 與預載矩陣通通塞進顯示卡的記憶體（VRAM），導致後端在做矩陣相乘時直接拋出記憶體不足的例外。</p>



<p class="wp-block-paragraph"><strong>調降 <code>-ngl</code>（GPU 卸載層數）</strong> </p>



<p class="wp-block-paragraph">將 <code>-ngl</code> 從 256 調降為 <strong>0</strong> 或 <strong>4~8</strong>。</p>



<ul class="wp-block-list">
<li>如果想完全用 CPU 穩定跑：設 <code>-ngl 0</code>。</li>



<li>如果想嘗試讓內顯分擔少量運算：設 <code>-ngl 6</code>。</li>
</ul>



<p class="wp-block-paragraph"><strong>加入 <code>--load-mode non</code> 避免載入異常</strong> </p>



<p class="wp-block-paragraph">混合 CPU 與 GPU 載入大模型時，<code>--load-mode none</code>（若要開啟 mmap 則是 <code>--load-mode mmap</code>）</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">單純用 CPU 跑的比 GPU 還快</h2>



<p class="wp-block-paragraph">開啟 GPU（Intel UHD 770 混合運算，<code>-ngl 10</code>）後的處理速度變慢（從純 CPU 的 <strong>~23 t/s</strong> 降至 <strong>~9-10 t/s</strong>），主要有以下四個原因：</p>



<ul class="wp-block-list">
<li><strong>跨匯流排傳輸開銷（PCIe / System Bus Overhead）</strong> 當你設定 <code>-ngl 10</code> 時，模型被切成兩部分：10 層在 GPU 運算，剩下的 30+ 層在 CPU 運算。模型在計算每一層神經網路時，張量資料（Tensors）必須不斷在 CPU 記憶體與 GPU 共享記憶體之間透過系統匯流排來回搬移。這個「跨邊界同步」的等待時間，遠遠超過了內顯幫忙計算所節省的時間。</li>



<li><strong>Intel UHD 770 算力與記憶體頻寬有限</strong> UHD 770 是 CPU 內建的顯示晶片，它沒有獨立的高速 VRAM（如 GDDR6 或 HBM），而是與 CPU 共享相同的 DDR 系統記憶體。因此 GPU 運算時無法享受獨立顯卡的大頻寬優勢，反而會跟 CPU 搶奪記憶體頻寬。</li>



<li><strong>SYCL / Level Zero 驅動與框架轉換成本</strong> <code>llama.cpp</code> 將矩陣運算派發給 Intel SYCL/Level Zero API 時，需要額外的 Context 切換與 Kernel 呼叫成本。對於規模較小或算力較弱的內顯，API 呼叫與同步的開銷比純 CPU 執行 AVX-512 / AMX / AVX2 原生指令集還要高昂。</li>



<li><strong>Batch Size 調小帶來的代價</strong> 將 <code>-b</code> 從 512 降到 128 雖然解決了 VRAM 崩潰問題，但也降低了 Prompt Processing（平行矩陣運算）的吞吐量，導致整體的速度指標下降。</li>
</ul>



<p class="wp-block-paragraph"><strong>建議結論</strong></p>



<p class="wp-block-paragraph">對於 Intel UHD 770 這類內建顯示卡，執行 12B 以上的大模型時，<strong>完全使用純 CPU 運算（<code>-ngl 0</code>）會是整體效率最高且最穩定的選擇</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">升級模型與設定</h2>



<ul class="wp-block-list">
<li>別用 IQ1 系列超低量化：IQ1_S 壓縮得太誇張，模型邏輯會直接崩潰。建議至少提升到 Q3_K_M 或 Q4_K_M。</li>



<li>關閉內建思考機制：如果版本支援切換，直接在推論端把思考模式（Reasoning/Thinking）關掉，讓它回歸傳統的單向輸出。</li>
</ul>



<h2 class="wp-block-heading">實際遇到的回覆</h2>



<pre class="wp-block-code"><code>Hi! I don't have a way to verify the current date or time, so I'll need to rely on the user's message to determine today's date. Based on the context, it looks like the user is asking "今天幾月幾日?" (What date is today?) — the user is asking for the current date.

I should respond with today's date. But wait — I'm an AI, I don't know the exact date unless I can get it from the system. Actually, I do not have access to the current date. I should respond naturally — perhaps ask or say I don't know the exact date, or...

Wait, the user is asking in Chinese "今天幾月幾日?" = "What day is today?" I should respond in Chinese, since they asked in Chinese. I can respond in Chinese.

But do I know today's date? I can't be 100% certain of the date. I should say I'm not sure what date it is... no, as an AI I do not know today's date. I should say I don't know today's date, or... hmm.

Actually, I should respond naturally. The user said "hi". I should respond in Chinese (matching the user's language) and ask about the date, or just say hi.

Let me respond naturally in Chinese: "嗨！今天几号？" — the user is asking for the date. I should respond: I don't know today's date...

Actually I should just naturally respond: "嗨！今天幾月幾日?" — the user is asking for the date. I don't know today's date. I should respond: I'm not sure what today's date is. I don't have access to the current date.

I could ask: "What date do you think it is today?" No, that's weird. Let me just respond naturally.

I think a natural response would be: "嗨！我不太知道今天的日期……" or "嗨！今天几号？我不知道今天的日期".

Actually, maybe the user just wants to chat. Let me respond naturally in Chinese: "嗨！今天几月幾日？我不太知道今天的日期……" — or something casual.

Let me think: the user said "今天幾月幾日?" — a casual "hi". A natural response: "嗨！今天幾月幾日？" — no, that's weird. Let me just say: "嗨！今天幾月幾日？" No, that's weird.

Simple response: "嗨！今天幾月幾日？" — I don't know today's date, so I'll say: I don't know today's date. Or: I don't know today's date, but I do know it's a nice day!

I think I'll just respond: "嗨！今天幾月幾日？" — no.

Let me just respond: "Hi! 今天幾月幾日？" — hmm.

I think I'll respond naturally: "嗨！今天幾月幾日？" — the user is asking for the date. I'll respond: I don't know today's date.

Actually, since I don't know the current date, I can just say I don't know today's date. Let me respond naturally: "嗨！今天幾月幾日？" — or just "Hi! 今天幾月幾日?" —

I'll just respond: "Hi! 今天幾月幾日?" — no, that's weird.

Final response: "Hi! 今天幾月幾日？" — I'm not sure what the date is...

I think I'll just respond naturally: "Hi! 今天幾月幾日?" — no, just: "Hi! 今天幾月幾日?"

Let me just respond naturally: "Hi! 今天幾月幾日?" — no.

I'll just respond: "Hi! 今天幾月幾日?" — or "Hi! 今天幾月幾日?" —

I'll just respond naturally: "Hi! 今天</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Pi Agent（AI Client） 在呼叫 llama-server（LLM Server） 時，因為 超時（Timeout） 與 上下文視窗（Context Window）飽和 所產生中斷問題</h2>



<h3 class="wp-block-heading">一、為什麼會中斷？</h3>



<p class="wp-block-paragraph">主要原因有兩個：<strong>推論耗時過長觸發 API 超時</strong>，以及 <strong>Context 長度已逼近上限</strong>。</p>



<ol start="1" class="wp-block-list">
<li><strong>Prompt Processing 耗時近 5 分鐘，觸發前端 Client 超時</strong>
<ul class="wp-block-list">
<li>從 Task 2924 的日誌可以看到：<br><code>prompt eval time = 285341.69 ms / 2528 tokens (8.86 tokens per second) eval time = 0.00 ms / 1 tokens</code></li>



<li>光是處理送進去的 2528 個 Token（Prompt Processing），純 CPU 就花了 <strong>285 秒（約 4.75 分鐘）</strong>。</li>



<li><strong>關鍵點</strong>：當 Prompt Eval 剛算完、才剛要開始產生第 1 個文字 Token（<code>eval time = 0.00 ms</code>）時，請求就被中斷了。這是因為 Pi Agent（或中間的 HTTP Client）等不到回應，達到 Timeout 上限而強制斷線。</li>
</ul>
</li>



<li><strong>Context 容量接近上限（10K 邊界）</strong>
<ul class="wp-block-list">
<li>Task 2473 釋放時 <code>n_tokens = 10785</code>，Task 2924 釋放時 <code>n_tokens = 12867</code>。</li>



<li>你的批次檔設定 <code>-c 10240</code>，當對話紀錄疊加超過 10K 時，伺服器必須進行 Context 截斷或滑動視窗計算，這大幅拉長了 Prompt Eval 的計算時間。</li>
</ul>
</li>
</ol>



<h3 class="wp-block-heading">二、需要修改 Server 端設定嗎？</h3>



<p class="wp-block-paragraph"><strong>是的，需要同時調整 Server 端參數與 Client 端設定。</strong></p>



<h4 class="wp-block-heading">1. 調整 Client 端（Pi Agent）的 Timeout 時間</h4>



<p class="wp-block-paragraph">這是解決中斷最直接的方法。Pi Agent 預設的 API 請求超時時間可能設在 120 秒或 180 秒，而你的純 CPU 處理 2.5K Tokens 需耗時 285 秒。</p>



<ul class="wp-block-list">
<li><strong>做法</strong>：請在 Pi Agent 的設定檔（<code>config.json</code> 或環境變數）中，將 HTTP / API 的 <code>timeout</code> 時間調大（例如改為 <code>600</code> 秒或 <code>0</code> 無限制）。</li>
</ul>



<p class="wp-block-paragraph">在 Windows 系統中，預設的設定檔與相關配置資料夾位於：</p>



<h3 class="wp-block-heading">1. 修改 <code>settings.json</code> 檔</h3>



<p class="wp-block-paragraph">在 <code>~/.pi/agent/settings.json</code> 檔案中新增或設定 <code>timeout</code>（以毫秒為單位）：</p>



<p class="wp-block-paragraph">JSON</p>



<pre class="wp-block-code"><code>{
  "timeout": 600000
}
</code></pre>



<ul class="wp-block-list">
<li><strong>說明</strong>：預設通常為 5 分鐘（<code>300000</code> 毫秒）。此設定控制等待 LLM API 回傳與 HTTP 請求回應的最長時間。如需關閉或解除限制，可嘗試設為 <code>0</code> 或高數值（如 <code>600000</code> 代表 10 分鐘）。</li>
</ul>



<h3 class="wp-block-heading">2. 透過互動式 TUI / 內建命令調整</h3>



<p class="wp-block-paragraph">在 Pi 的互動模式中，可以直接開啟設定選單調校：</p>



<ol start="1" class="wp-block-list">
<li>執行 <code>pi</code> 進入互動介面。</li>



<li>輸入 <code>/settings</code> 並按下 Enter 鍵。</li>



<li>在選單中滾動尋找 <strong>HTTP Timeout</strong>（或 Timeout）選項並修改其數值。</li>
</ol>



<p class="wp-block-paragraph">建議使用 /settings 指令, 讓 http timeout = disable 即可.</p>



<p class="wp-block-paragraph"></p>



<h4 class="wp-block-heading">2. 優化 Server 端（<code>llama-server</code> 批次檔）設定</h4>



<p class="wp-block-paragraph">為了提升純 CPU 的處理效率，建議對批次檔做以下幾項調整：</p>



<ul class="wp-block-list">
<li><strong>調大 Batch Size (<code>-b</code> / <code>-ub</code>) 以提升 Prompt Processing 速度</strong>
<ul class="wp-block-list">
<li>純 CPU 在算 Prompt processing 時，把 <code>-b</code> 與 <code>-ub</code> 從 256 提高到 <strong>512</strong> 或 <strong>1024</strong>，能更好發揮 CPU 的多線程與指令集平行運算能力（AVX-512 / AVX2），大幅縮短這 285 秒的等待時間。</li>
</ul>
</li>



<li><strong>將快取數據類型改為 8-bit（<code>-ctk q8_0 -ctv q8_0</code>）</strong>
<ul class="wp-block-list">
<li>隨著對話拉長到 10K，KV Cache 的讀取會吃掉大量 RAM 頻寬。開啟 KV Cache 量化可以節省一半的 KV 記憶體與讀取時間，對 CPU 生成速度有顯著幫助。</li>
</ul>
</li>



<li><strong>釋放被佔用的線程資源</strong>
<ul class="wp-block-list">
<li>在上一輪設定中，<code>-t 8 -tb 12</code> 可能會讓 CPU 在跨 P-Core / E-Core 時產生高昂的切換成本。建議統一設為 CPU 的<strong>實體大核數（P-Cores）</strong>，例如全部設為 <code>-t 11 -tb 11</code>。</li>
</ul>
</li>
</ul>



<h3 class="wp-block-heading">三、修改後的 Server 啟動批次檔</h3>



<p class="wp-block-paragraph">修改後的腳本範例：</p>



<pre class="wp-block-code"><code>set MODEL=models\Qwen3.8-27B-UD-Q5_K_M.gguf
set EXE=llama-server.exe

set CTX=32768
set BATCH=1024
set NP=1
set NGL=0

set ONEAPI_DEVICE_SELECTOR=*:cpu

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -b %BATCH% -ub %BATCH% ^
  -ngl %NGL% ^
  -t 11 ^
  -tb 11 ^
  -ctk q8_0 -ctv q8_0 ^
  --load-mode mlock ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  --reasoning-format none ^
  --temp 0.3 ^
  --top-p 0.8 ^
  --top-k 30 ^
  --repeat-penalty 1.08
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">模型的 Context（上下文長度）已經徹底爆表，觸發了限制</h2>



<p class="wp-block-paragraph">這次中斷的原因與上一次不同。這次不是因為運算太慢超時，而是<strong>模型的 Context（上下文長度）已經徹底爆表，觸發了限制</strong>。</p>



<p class="wp-block-paragraph">看這兩行關鍵日誌：</p>



<p class="wp-block-paragraph">Plaintext</p>



<pre class="wp-block-code"><code>64.02.764.301 I slot release: id 0 | task 24 | stop processing: n_tokens = 12972, truncated = 0
</code></pre>



<p class="wp-block-paragraph">Plaintext</p>



<pre class="wp-block-code"><code>eval time = 0.00 ms / 1 tokens
</code></pre>



<p class="wp-block-paragraph">你的批次檔設定 <code>-c 10240</code>（10K），但目前對話累積的 Token 數已經達到 <strong>12,972 個</strong>（包含了先前閱讀大量 Caddyfile、index.php、JWT.php 的檔案內容）。</p>



<p class="wp-block-paragraph">當 <code>n_tokens</code>（12,972）超過 <code>-c</code> 設定的容量上限（10,240）時，<code>llama-server</code> 會直接終止該次生成並釋放 slot，導致連 <strong>1 個 Token 都沒印出來</strong> 就不動了。</p>



<h3 class="wp-block-heading">解決方案</h3>



<p class="wp-block-paragraph">要徹底解決這個問題，有以下兩種處理方式：</p>



<h4 class="wp-block-heading">方案一：在 Pi Agent 中清理對話歷史（最快速）</h4>



<p class="wp-block-paragraph">Agent 剛才讀取了大量的後端程式碼，導致 Context 被塞滿。</p>



<ul class="wp-block-list">
<li>在 Pi Agent 中輸入修剪/重置對話的命令（例如 <code>/clear</code> 或開啟一個新的 session）。</li>



<li>重新要求它修復漏洞，這樣 Context 就不會溢出。</li>
</ul>



<h4 class="wp-block-heading">方案二：調大 Server 的 Context 並開啟記憶體優化（一勞永逸）</h4>



<p class="wp-block-paragraph">由於 Agent 做寫程式與資安診斷任務時，閱讀與分析程式碼需要極大的 context，10K 實在太小。建議將 Context 擴充至 <strong>24,576 (24K)</strong> 或 <strong>32,768 (32K)</strong>。</p>



<p class="wp-block-paragraph">為了防止 Context 變大導致 RAM 爆掉，請配合 <strong>KV Cache 8-bit 量化（<code>-ctk q8_0 -ctv q8_0</code>）</strong>，這能省下近一半的記憶體用量。</p>



<p class="wp-block-paragraph">修改後的 startup 批次檔範例：</p>



<pre class="wp-block-code"><code>set MODEL=models\Qwen3.8-27B-UD-Q5_K_M.gguf
set EXE=llama-server.exe

:: 將 Context 調大到 24K (24576) 或 32K (32768)
set CTX=24576
set BATCH=512
set NP=1
set NGL=0

set ONEAPI_DEVICE_SELECTOR=*:cpu

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -b %BATCH% -ub %BATCH% ^
  -ngl %NGL% ^
  -t 8 ^
  -tb 8 ^
  -ctk q8_0 -ctv q8_0 ^
  --load-mode mlock ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  --reasoning-format none ^
  --temp 0.3 ^
  --top-p 0.8 ^
  --top-k 30 ^
  --repeat-penalty 1.08
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">max_tokens 達到上限導致被截斷</h2>



<p class="wp-block-paragraph">如果不想設為無限（<code>-n -1</code>）以防止程式陷入死迴圈，設置一個合理的上限確實是更安全的作法。</p>



<p class="wp-block-paragraph">但在 llama.cpp 中，上限大小受限於兩個邊界：</p>



<ol start="1" class="wp-block-list">
<li><strong>不能超過上下文總長度</strong>：<code>n-predict</code>（<code>-n</code>）是「單次回覆產生的最大 token 數」，它不能大於你設定的總上下文長度 <code>CTX</code>（目前為 <code>32768</code>）。</li>



<li><strong>記憶體與邊界限制</strong>：設成 1M（1,048,576）或 10M 在 <code>CTX=32768</code> 的設定下是沒有意義的，因為對話輸入（Prompt）加上輸出（Output）的總和極限就是 32k。</li>
</ol>



<h3 class="wp-block-heading">最佳修改建議</h3>



<p class="wp-block-paragraph">將 <code>-n</code> 設為 <strong><code>16384</code></strong> 或直接設為與 <code>CTX</code> 相等的 <strong><code>32768</code></strong>。</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>@echo off
chcp 65001 &gt;nul
cd /d "%~dp0"
echo ================================================
echo  gemma-4-12B-it-qat-UD-Q4_K_XL - CODE/LONG-OUTPUT mode
echo  RTX 4050 Optimized Configuration
echo ================================================
echo.

set MODEL=models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

REM Context &amp; Batch settings optimized for 6GB VRAM
set CTX=32768
set BATCH=512
set UBATCH=256
set NP=1
set NGL=999

"%EXE%" ^
  -m "%MODEL%" ^
  -c %CTX% ^
  -n 16384 ^
  -np %NP% ^
  -cmoe ^
  -b %BATCH% ^
  -ub %UBATCH% ^
  -ngl %NGL% ^
  -ctk q8_0 ^
  -ctv q8_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa 1 ^
  --context-shift

echo.
echo ================================================
echo  Exited. If VRAM/RAM overflows, lower CTX to 8192.
echo ================================================
pause
</code></pre>



<h3 class="wp-block-heading">說明</h3>



<ul class="wp-block-list">
<li><strong><code>-n 16384</code></strong>：允許模型單次輸出最多 16k 個 token。這樣既能確保長代碼（如你的 <code>.go</code> 檔）順利寫完不被切斷，又能防止異常輸出消耗過多資源。</li>



<li><strong>Client 端同步</strong>：如果在批次檔加了 <code>-n 16384</code> 後依然在 14k token 左右被截斷，請記得到 API 呼叫端（如 Continue 或 Client 工具設定）將 <code>max_tokens</code> 一併改成 <code>16384</code>。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">為何設 16K 不設 32K?</h2>



<p class="wp-block-paragraph">在 <code>CTX=32768</code>（32k）的前提下，把 <code>-n</code> 設為 <strong>16384（16k）</strong> 而非 <strong>32768（32k）</strong>，主要有三個實務考量：</p>



<ul class="wp-block-list">
<li><strong>留空間給輸入（Prompt Space）</strong></li>



<li><code>CTX</code> 是「輸入 + 輸出」的總容量上限。</li>



<li>如果把 <code>-n</code> 設為 32k，理論上只有在「輸入為 0 個 token」時模型才有可能吐滿 32k。</li>



<li>當輸入了 14k token 的程式碼時，系統能留給輸出的上限本來就只剩下 <code>32k - 14k = 18k</code> token。設 16k 可以確保輸出空間與輸入空間取得合理平衡。</li>



<li><strong>防止程式寫出死迴圈（Loop Detection）</strong></li>



<li>生成式 AI 有時會因為 Sampler 設定或上下文過長，陷入文字重複的死迴圈（例如一直重複輸出某段註解或無效代碼）。</li>



<li>設為 16k 可以在模型異常時及時止損，避免 CPU/GPU 滿載狂飆、浪費時間與筆電電力。</li>



<li><strong>客戶端 API 的相容性</strong></li>



<li>許多前端工具（如 VS Code Continue 擴充套件、Dify 等）對單次輸出的預設上限就是 4k 或 16k。</li>



<li>將 Server 端限制設在 16k，能更好地配合多數開發工具的預期設定。</li>
</ul>



<p class="wp-block-paragraph"><strong>結論</strong>：<br>如果確定任務需要「極長輸入 + 超長輸出」，直接設為 <code>-n 32768</code> 完全沒有問題，llama.cpp 會自動在 <code>Prompt + Output</code> 達到 32k 時停止。設為 16k 則是一種兼顧防護力與長文本輸出的折衷設定。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">在 RTX 4050 Laptop（ 6 GB 顯存）的環境下，不建議將 <code>CTX</code> 設為 <strong>64K（65536）</strong>，主要有以下三個硬體與效能上的致命原因：</p>



<h3 class="wp-block-heading">1. KV Cache 顯存/記憶體會直接炸開（OOM）</h3>



<p class="wp-block-paragraph">上下文記憶體（KV Cache）的體積是隨著 context 長度呈<strong>線性倍數成長</strong>的。</p>



<p class="wp-block-paragraph">即便批次檔中已經設定了 <code>-ctk q8_0 -ctv q8_0</code>（將 KV 快取進行 8-bit 量化壓縮）：</p>



<ul class="wp-block-list">
<li><strong>CTX = 16k</strong>：KV Cache 約佔用 <strong>1.0 GB ~ 1.5 GB</strong> 記憶體。</li>



<li><strong>CTX = 32k</strong>：KV Cache 約佔用 <strong>2.5 GB ~ 3.0 GB</strong> 記憶體。</li>



<li><strong>CTX = 64k</strong>：KV Cache 會直接暴增到 <strong>5.5 GB ~ 6.5 GB</strong> 以上。</li>
</ul>



<p class="wp-block-paragraph">4050 的總顯存只有 6 GB。如果單單 KV Cache 就吃掉 6 GB，顯示卡連模型的第一層權重都塞不下，所有的計算會被強制全部退回 CPU 與系統 RAM 處理，極易觸發 <code>Out of Memory</code> 崩潰。</p>



<h3 class="wp-block-heading">2. Prompt Processing（提示詞預處理）速度會慢到無法使用</h3>



<p class="wp-block-paragraph">在你的日誌中可以看到，處理 14,917 個 token 的輸入已經花了 <strong>308 秒（約 5 分鐘）</strong>：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code>prompt eval time = 312885.17 ms / 14917 tokens (47.68 tokens per second)</code></p>
</blockquote>



<p class="wp-block-paragraph">Attention（注意力機制）的計算複雜度是 $O(N^2)$。當上下文長度從 32k 翻倍到 64k 時：</p>



<ul class="wp-block-list">
<li>提示詞預處理時間不會只翻倍，而是會呈 <strong>3 到 4 倍增長</strong>。</li>



<li>丟一段大型程式碼給它，光是等待模型「看完題目」準備開始打字，可能就要等上 <strong>15 至 20 分鐘</strong>。</li>
</ul>



<h3 class="wp-block-heading">3. RoPE 位置編碼衰減（模型會變笨）</h3>



<p class="wp-block-paragraph">Gemma 4 12B 原生的訓練與最佳上下文視窗通常在 8k 到 32k 之間。</p>



<p class="wp-block-paragraph">當透過 llama.cpp 強行把 context 開到 64k 時，如果沒有特別配置複雜的 RoPE Scaling 頻率調整參數：</p>



<ul class="wp-block-list">
<li>模型在處理超過 32k 以外的文本時，注意力會嚴重分散（Needle In A Haystack 能力下降）。</li>



<li>容易出現邏輯錯亂、忘記前文設定，或是輸出與程式碼無關的廢話。</li>
</ul>



<h3 class="wp-block-heading">總結建議</h3>



<p class="wp-block-paragraph">對於 <strong>RTX 4050 Laptop + 32 GB RAM</strong> 的配置：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>CTX 設定</strong></td><td><strong>評估結果</strong></td><td><strong>適用場景</strong></td></tr></thead><tbody><tr><td><strong>16384 (16k)</strong></td><td><strong>黃金平衡點</strong>（速度快、顯存負擔輕、絕不崩潰）</td><td>80% 的日常 Code 開發、重構與對話</td></tr><tr><td><strong>32768 (32k)</strong></td><td><strong>極限穩定邊界</strong>（處理超長檔案，需耐心等待 Prompt 處理）</td><td>必須一次讀取整個大型專案檔</td></tr><tr><td><strong>65536 (64k)</strong></td><td><strong>不推薦</strong>（記憶體溢出風險高、等待時間極長、精度下降）</td><td>極端實驗場景</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Response was truncated before completion.</h2>



<p class="wp-block-paragraph">這不是上下文（CTX）長度的問題，而是 <strong>顯存 / 記憶體崩潰導致 <code>llama-server.exe</code> 當掉重啟</strong>。</p>



<p class="wp-block-paragraph">日誌中的 503 錯誤代表：伺服器在中途崩潰（Crash）了，當 Client 端繼續發送 Request 時，<code>llama-server</code> 正在嘗試重新載入模型，所以回應 503 Service Unavailable。</p>



<p class="wp-block-paragraph">崩潰的核心原因與解決步驟如下：</p>



<h3 class="wp-block-heading">1. 崩潰主因分析</h3>



<ul class="wp-block-list">
<li><strong>Prompt 處理到極限溢出 (OOM)</strong>日誌顯示 Prompt token 高達 26,465 個，在 4050（6 GB 顯存）上處理將近 2.6 萬個 token 時，計算過程產生的 <strong>臨時工作緩衝區（Scratch Buffer）</strong> 加上 <strong>KV Cache</strong>，瞬間吃滿了剩餘的顯存與 RAM，直接引發 C++ 記憶體存取違規（Access Violation）而崩潰。</li>



<li><strong>Prompt Eval 時間過長觸發 Timeout</strong>Prompt 處理花了 439 秒（約 7.3 分鐘）。許多 API Client（如 Continue、Dify、Open WebUI）預設的 HTTP Read Timeout 通常是 300 秒（5 分鐘）。Client 等不及主動斷線，伺服器在釋放 Context 資源時觸發了 Exception。</li>
</ul>



<h3 class="wp-block-heading">2. 解決方案（批次檔關鍵微調）</h3>



<p class="wp-block-paragraph">在 32k CTX 處理 2.6 萬字大檔時，請在批次檔中加入 <strong><code>--no-mmap</code></strong> 限制記憶體分配，並將 <code>-ub</code>（Micro-batch）降到 <strong>128</strong> 來壓低 Peak VRAM：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>@echo off
chcp 65001 &gt;nul
cd /d "%~dp0"
echo ================================================
echo  gemma-4-12B-it-qat-UD-Q4_K_XL - STABLE LONG-OUTPUT
echo  RTX 4050 Optimized Configuration
echo ================================================
echo.

set MODEL=models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

set CTX=32768
set BATCH=512
set UBATCH=128
set NP=1
set NGL=999

"%EXE%" ^
  -m "%MODEL%" ^
  -c %CTX% ^
  -n 16384 ^
  -np %NP% ^
  -cmoe ^
  -b %BATCH% ^
  -ub %UBATCH% ^
  -ngl %NGL% ^
  -ctk q8_0 ^
  -ctv q8_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa 1 ^
  --no-mmap ^
  --context-shift

echo.
echo ================================================
echo  Exited. If VRAM/RAM overflows, lower CTX to 8192.
echo ================================================
pause
</code></pre>



<h3 class="wp-block-heading">3. 客戶端（Client / IDE 擴充套件）防護</h3>



<ol start="1" class="wp-block-list">
<li><strong>拉長 Request Timeout</strong>：若使用 VS Code Continue 或其他 LLM 套件，請到設定檔中將 <code>requestOptions.timeout</code> 改為 <code>600000</code>（10 分鐘），避免 Client 端提早中斷連線。</li>



<li><strong>控制一次輸入的檔案量</strong>：2.6 萬 token 已經快接近 32k 的極限，建議將大型專案拆分 2 到 3 次餵給模型，可大幅降低 7 分鐘 Prompt Processing 的等待時間與崩潰風險。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">計算所需的記憶體</h2>



<p class="wp-block-paragraph">example:</p>



<pre class="wp-block-code"><code>@echo off
chcp 65001 &gt;nul
cd /d "%~dp0"
echo ================================================
echo  gemma-4-12B-it-qat-UD-Q4_K_XL - STABLE LONG-OUTPUT
echo  RTX 4050 Optimized Configuration
echo ================================================
echo.

set MODEL=models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

set CTX=32768
set BATCH=512
set UBATCH=128
set NP=1
set NGL=999

"%EXE%" ^
  -m "%MODEL%" ^
  -c %CTX% ^
  -n 16384 ^
  -np %NP% ^
  -cmoe ^
  -b %BATCH% ^
  -ub %UBATCH% ^
  -ngl %NGL% ^
  -ctk q8_0 ^
  -ctv q8_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa 1 ^
  --no-mmap ^
  --context-shift

echo.
echo ================================================
echo  Exited. If VRAM/RAM overflows, lower CTX to 8192.
echo ================================================
pause</code></pre>



<p class="wp-block-paragraph">這份設定主要會消耗三種記憶體： AI 大腦本體、對話記憶庫、計算時的暫存區。</p>



<p class="wp-block-paragraph">記憶體消耗的三大來源</p>



<ul class="wp-block-list">
<li>AI 大腦本體（模型權重 gguf 檔案）這個 Gemma 4 12B 模型（Q4_K_XL 量化版）就像一個約 7.5 GB 到 8 GB 的壓縮檔。因為設定了 -ngl 999 且關閉了 -mmap（&#8211;no-mmap），要求把整份模型一次全部塞進顯示卡，所以這裡會直接吃掉 7.5 GB 到 8 GB 的顯示記憶體（VRAM）。</li>



<li>對話記憶庫（上下文快取 KV Cache）這裡決定了 AI 能記住多少字。設定檔開了 3 萬 2 千個字的記憶空間（-c 32768），並使用了壓縮技術（-ctk q8_0 與 -ctv q8_0）來省空間。壓縮後大約吃掉 2 GB 到 2.5 GB 的 VRAM。如果沒有開啟 Q8_0 壓縮，佔用量會直接翻倍到 4 GB 到 5 GB。</li>



<li>計算暫存區（批次緩衝區 Batch Buffer）這是 AI 在思考和算字時臨時開闢的工作區，由邏輯批次（-b 512）與實體批次（-ub 128）控制。把實體批次 -ub 設為 128 非常省資源，這部分的動態算力緩衝區大約僅佔用 200 MB 到 500 MB（0.2 GB 到 0.5 GB）的 VRAM。</li>



<li>並行請求與其他優化設定並行數設定為 -np 1，代表一次只服務一位使用者，不會額外複製多份記憶空間。開啟 Flash Attention（-fa 1）與 cmoe（-cmoe）針對長文本與混合專家模型進行記憶體優化，避免資源被重複浪費。</li>
</ul>



<p class="wp-block-paragraph">總結與硬體建議</p>



<p class="wp-block-paragraph">把上述項目加起來，整體 VRAM 總需求約為 10 GB 到 11 GB。</p>



<ul class="wp-block-list">
<li>模型權重（-ngl 999 載入）：約 7.5 GB &#8211; 8.0 GB VRAM</li>



<li>上下文快取（-c 32768 + Q8_0）：約 2.0 GB &#8211; 2.5 GB VRAM</li>



<li>計算緩衝與系統開銷（-ub 128 等）：約 0.3 GB &#8211; 0.5 GB VRAM</li>
</ul>



<p class="wp-block-paragraph">由於 RTX 4050 移動版通常配備 6 GB VRAM，這個配置在執行時無法將模型全額放入顯示記憶體。系統會自動將無法放入的層數或記憶體轉移至系統主記憶體（RAM）中，執行速度可能會因此變慢。</p>



<p class="wp-block-paragraph">以上設定值有兩個主要瓶頸：一是 <code>-ngl 999</code> 試圖把 7.5 GB 的模型全塞進 6 GB 顯存，二是 <code>-c 32768</code> 加上 <code>--no-mmap</code> 會直接擠爆記憶體。</p>



<pre class="wp-block-code"><code>@echo off
chcp 65001 &gt;nul
cd /d "%~dp0"
echo ================================================
echo  gemma-4-12B-it-qat-UD-Q4_K_XL - RTX 4050 OPTIMIZED
echo ================================================
echo.

set MODEL=models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

set CTX=16384
set BATCH=512
set UBATCH=128
set NP=1
set NGL=24
set THREADS=8

"%EXE%" ^
  -m "%MODEL%" ^
  -c %CTX% ^
  -n 8192 ^
  -np %NP% ^
  -b %BATCH% ^
  -ub %UBATCH% ^
  -ngl %NGL% ^
  -t %THREADS% ^
  -ctk q8_0 ^
  -ctv q8_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa 1 ^
  --context-shift

echo.
echo ================================================
echo  Exited. If VRAM overflows, lower NGL to 20 or CTX to 8192.
echo ================================================
pause</code></pre>



<p class="wp-block-paragraph">修改重點與調整邏輯</p>



<ul class="wp-block-list">
<li><strong>-ngl 24（精準卸載）：</strong> 將模型約 60% 的層數放入 VRAM，留約 1.5 GB VRAM 給 KV Cache 和 Flash Attention，其餘走 RAM，避免 CUDA OOM 崩潰。</li>



<li><strong>-c 16384（平衡長度）：</strong> 從 32K 降至 16K。16K 搭配 <code>-ctk q8_0 -ctv q8_0</code> 約佔用 1 GB 左右 VRAM，記憶體壓力大減，且依然維持極實用的長文本能力。</li>



<li><strong>移除 &#8211;no-mmap：</strong> 讓系統恢復記憶體映射機制，CPU 與 GPU 切分載入更順暢。</li>



<li><strong>新增 -t 8（CPU 線程）：</strong> 剩餘留在 RAM 的層數需要 CPU 運算，設定為你的 CPU 實體核心數（可依實際規格調整，例如 6 或 8）能顯著提升吐字速度。</li>



<li><strong>移除 -cmoe：</strong> Gemma 12B 是 Dense（密集）模型而非 MoE（混合專家）模型，不需要 <code>-cmoe</code> 參數。</li>
</ul>



<p class="wp-block-paragraph">如果執行後發現記憶體還有空餘，可以把 <code>-ngl</code> 往上調到 26；如果跳出 CUDA 記憶體不足，再把 <code>-ngl</code> 降到 20 即可！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">request (34458 tokens) exceeds the available context size (32768 tokens), try increasing it</h2>



<p class="wp-block-paragraph">這個錯誤代表傳送給 <code>llama-server</code> 的要求（Prompt + 歷史紀錄）總長度達到了 <strong>34,458 個 token</strong>，已經超過了你在腳本中設定的上下文上限 <strong>32,768 個 token（-c 32768）</strong>。</p>



<p class="wp-block-paragraph"><code>llama-server</code> 開啟了 <code>--context-shift</code>，但在要求本身就已<strong>單次超過</strong>限制長度時，系統無法自動裁切並拒絕請求。</p>



<p class="wp-block-paragraph">可選擇以下三種解決方案：</p>



<p class="wp-block-paragraph">修改批次檔提高上限</p>



<p class="wp-block-paragraph">如果記憶體空間足夠，可以將 <code>-c</code> 參數調高至 40960 或 49152。</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>set CTX=49152
</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>注意：</strong> 提高 <code>-c</code> 會增加 KV Cache 的顯存/記憶體佔用。如果出現 Out of Memory（OOM），請將 <code>-ngl</code> 稍微調低 2 至 4 層。</p>
</blockquote>



<p class="wp-block-paragraph">在客戶端清空歷史對話</p>



<p class="wp-block-paragraph">如果你是透過 Open WebUI、SillyTavern 或其它 API 前端連線，代表目前的<strong>對話歷史紀錄太長</strong>。請在前端介面點擊「開啟新對話（New Chat）」或清除先前的上下文紀錄，即可恢復正常。</p>



<p class="wp-block-paragraph">調整前端的最大 context/out_tokens 設定</p>



<p class="wp-block-paragraph">請檢查前端應用程式（例如 Open WebUI / Jan / Chatbox）裡面的系統設定：</p>



<ul class="wp-block-list">
<li>將 <strong>Max Context Length</strong> 限制在 28,000 以內（預留空間給模型的回答）。</li>



<li>將 <strong>Max Response Tokens / Num Predict</strong> 調低（例如設為 4096），避免前端傳送過長的生成要求。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">要解決這個問題，核心重點是<strong>讓 Pi Agent 在發送請求前的 Context 上限，低於本地 llama-server 設定的 <code>-c 32768</code>（建議鎖定在 28000 ~ 30000 以下）</strong>。這樣 Pi 才能在長度快爆掉時，及時觸發 Context Compaction（壓縮/摘要機制），而不是直接硬把 34,458 token 塞給 llama-server 導致 400 錯誤。</p>



<p class="wp-block-paragraph">依據 Pi Agent 的設定方式，有以下幾種解決途徑：</p>



<p class="wp-block-paragraph">方法一：修改 Pi 的自訂模型設定 <code>models.json</code>（最根本做法）</p>



<p class="wp-block-paragraph">在 Pi Agent 的模型設定檔中（通常位於 <code>~/.pi/agent/models.json</code> 或 <code>.pi/models.json</code>），找到你為 <code>llama-server</code> 建立的自訂模型，顯式將 <code>contextWindow</code> 調低。</p>



<p class="wp-block-paragraph">例如改成：</p>



<p class="wp-block-paragraph">JSON</p>



<pre class="wp-block-code"><code>{
  "providers": {
    "custom-llama": {
      "baseUrl": "http://127.0.0.1:8080/v1",
      "apiKey": "12345678",
      "models": &#91;
        {
          "id": "gemma-4-12b",
          "contextWindow": 28000,
          "maxTokens": 4096
        }
      ]
    }
  }
}
</code></pre>



<p class="wp-block-paragraph">把 <code>contextWindow</code> 設為 <strong>28000</strong>（留 4,000 以上的安全餘裕給系統提示詞和模型回覆）。這樣當歷史對話接近 28,000 時，Pi Agent 就會自動進行記憶壓縮。</p>



<p class="wp-block-paragraph">方法二：安裝 Pi 的套件或擴充套件進行限制</p>



<p class="wp-block-paragraph">Pi Agent 支援透過 Extension 動態限制上下文。如果你想直接在 Pi 裡面下指令控管，可以在終端機安裝官方/社群擴充套件：</p>



<ul class="wp-block-list">
<li><strong>安裝 <code>max-context</code> 套件：</strong><br>Bash<code>pi install npm:max-context</code> 安裝後可以在對話中使用指令：<br>Plaintext<code>/max-context 28000</code> 這會強制 Pi 當上下文接近 28,000 時進行自動 Compact，就不會爆開。</li>



<li><strong>或安裝 <code>pi-context-cap</code>：</strong><br>Bash<code>pi install npm:pi-context-cap</code> 它會在啟動時自動將 Model Registry 中的 <code>contextWindow</code> 上限壓低。</li>
</ul>



<p class="wp-block-paragraph">方法三：修改 Pi 的 <code>settings.json</code></p>



<p class="wp-block-paragraph">在 <code>~/.pi/agent/settings.json</code> 中，確保自動壓縮機制有開啓，並可以調大預留空間 <code>reserveTokens</code>：</p>



<p class="wp-block-paragraph">JSON</p>



<pre class="wp-block-code"><code>{
  "theme": "dark",
  "httpIdleTimeoutMs": 0,
  "compaction": {
    "enabled": true,
    "reserveTokens": 4096
  }
}
</code></pre>



<p class="wp-block-paragraph">設定完成後，請<strong>重啟 Pi Agent 重新開一個 Session（對話）</strong>，Pi 就會在對話滿到 28k token 左右時主動壓縮歷史，不再傳送超過 32,768 的請求給 llama-server。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">為什麼&#8221;maxTokens&#8221;: 4096?</h2>



<p class="wp-block-paragraph"><code>maxTokens</code> 代表<strong>模型單次回答最多能輸出的文字量（Token 數量）</strong>。</p>



<p class="wp-block-paragraph">在 API 的設定邏輯中，<code>contextWindow</code>（上下文總空間）是由兩個部分相加而成的：</p>



<p class="wp-block-paragraph">就是模型能讀取的最大字數（你的 Prompt + 歷史對話）。如果設定 <code>contextWindow</code> 為 28000、<code>maxTokens</code> 為 4096，當歷史紀錄加上這次提問達到約 23904 個 Token 時，系統就會預留 4096 個 Token 給模型寫回答，並在發送請求給 <code>llama-server</code> 前主動觸發歷史紀錄壓縮。</p>



<p class="wp-block-paragraph">設定為 <strong>4096</strong> 主要有以下幾個好處：</p>



<ul class="wp-block-list">
<li><strong>留出安全緩衝</strong>：如果在 <code>models.json</code> 裡面把 <code>maxTokens</code> 設得太大（例如 16384），Pi Agent 在計算安全線時，就會被迫提早在對話才 16K Token 時就觸發壓縮。設定 4096 能讓你的歷史對話空間保持最大化。</li>



<li><strong>防止無限生成與顯存暴增</strong>：單次輸出 4096 個 Token 約等於 3000 到 4000 個繁體中文字（或約 15 頁 A4 報告），對絕大多數程式碼生成、文章撰寫與對話任務來說已經非常充裕。若不限制，萬一模型進入重複死循環，會瞬間耗盡資源。</li>



<li><strong>配合啟動腳本設定</strong>：你在 <code>llama-server</code> 啟動腳本中的設定為 <code>-n 8192</code>（或 <code>-n 16384</code>），這代表服務端「能力上」最多可以輸出到這個長度。而在 Client 端（Pi Agent）設定 <code>maxTokens: 4096</code> 是為了給予合理的輸出限制，避免單次回應耗時過久或占用過多 KV Cache。</li>
</ul>



<p class="wp-block-paragraph">如果你的應用場景經常需要一次性輸出超長程式碼或極長文件，也可以在 <code>models.json</code> 中將 <code>maxTokens</code> 調高至 <strong>8192</strong>，只要確保 <code>contextWindow + maxTokens</code> 不會超過 <code>llama-server</code> 設定的 <code>-c 32768</code> 即可。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">為了讓你的配置能夠順暢處理大型程式碼檔案，同時避免在 RTX 4050（6 GB VRAM）上發生 <strong>VRAM 爆滿（Out of Memory）</strong> 或 <strong>Context 超載錯誤</strong>，建議同時針對 <strong>llama-server 批次檔</strong> 與 <strong>Pi Agent 設定檔</strong> 進行以下調整。</p>



<h3 class="wp-block-heading">一、 llama.cpp 批次檔參數修改建議</h3>



<p class="wp-block-paragraph">你目前的批次檔設定中，<code>-c 49152</code> 與 <code>-ngl 32</code> 對於 6 GB VRAM 的 RTX 4050 來說<strong>風險非常高</strong>。49K 的上下文加上 32 層全卸載到 GPU，VRAM 幾乎確定會溢出並強制走系統 RAM，導致速度極慢或崩潰。</p>



<p class="wp-block-paragraph">建議將批次檔修改為以下配置：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>@echo off
chcp 65001 &gt;nul
cd /d "%~dp0"
echo ================================================
echo  gemma-4-12B-it-qat-UD-Q4_K_XL - LARGE CODE OPTIMIZED
echo  RTX 4050 Optimized Configuration
echo ================================================
echo.

set MODEL=models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

set CTX=32768
set BATCH=512
set UBATCH=128
set NP=1
set NGL=24
set THREADS=8

"%EXE%" ^
  -m "%MODEL%" ^
  -c %CTX% ^
  -n 8192 ^
  -np %NP% ^
  -b %BATCH% ^
  -ub %UBATCH% ^
  -ngl %NGL% ^
  -t %THREADS% ^
  -ctk q8_0 ^
  -ctv q8_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa 1 ^
  --context-shift

echo.
echo ================================================
echo  Exited. If VRAM overflows, lower NGL to 20 or CTX to 16384.
echo ================================================
pause
</code></pre>



<h4 class="wp-block-heading">關鍵修改說明：</h4>



<ol start="1" class="wp-block-list">
<li><strong><code>-c 32768</code>（從 49152 降回 32768）：</strong>32K Token 約等於 2.4 萬字繁體中文或 1,000 行以上的程式碼，對大型檔案已經非常足夠。將 CTX 控制在 32K 能顯著減輕 KV Cache 的 VRAM 負擔。</li>



<li><strong><code>-ngl 24</code>（從 32 降至 24）：</strong>Gemma 12B 約有 40 到 48 層。將 24 層放在 GPU，其餘留給 CPU/RAM 運算，可以把 VRAM 佔用控制在 4.5 GB ~ 5.0 GB 左右，留下 1 GB 以上的安全空間給 KV Cache 與 Flash Attention。</li>



<li><strong><code>-n 8192</code>（從 26384 降至 8192）：</strong>單次最大輸出設定為 8192 Token 即可（相當於一次產生近萬行程式碼或超長解答），設成 26K 會導致伺服器預估輸出空間過大而引發記憶體配額問題。</li>
</ol>



<h3 class="wp-block-heading">二、 Pi Agent 設定檔修改建議</h3>



<p class="wp-block-paragraph">為了搭配伺服器端的 <code>-c 32768</code>，必須讓 Pi Agent 知道上下文的安全上限，避免 Pi 傳送超過伺服器負荷的 Request。</p>



<p class="wp-block-paragraph">請開啟 Pi Agent 的模型設定檔（通常位於 <code>~/.pi/agent/models.json</code> 或 <code>.pi/models.json</code>），將你的自訂模型參數修改如下：</p>



<p class="wp-block-paragraph">JSON</p>



<pre class="wp-block-code"><code>{
  "providers": {
    "custom-llama": {
      "baseUrl": "http://127.0.0.1:8080/v1",
      "apiKey": "12345678",
      "api": "openai-completions",
      "models": &#91;
        {
          "id": "gemma-4-12b",
          "contextWindow": 28000,
          "maxTokens": 4096
        }
      ]
    }
  }
}
</code></pre>



<h4 class="wp-block-heading">關鍵修改說明：</h4>



<ol start="1" class="wp-block-list">
<li><strong><code>contextWindow: 28000</code>：</strong>將 Pi 的 Context 視窗設定為 <strong>28000</strong>（小於 llama-server 的 32768）。這樣留下了約 4,700 Token 的安全緩衝區。當你貼入大檔案或對話過長時，Pi Agent 會在達到 28,000 Token 時<strong>主動觸發歷史紀錄壓縮（Compaction）</strong>，絕不會觸發先前遇到的 400 錯誤。</li>



<li><strong><code>maxTokens: 4096</code>：</strong>代表限制模型單次回覆的最大長度為 4096 Token，這能防止模型生成重複程式碼時卡死，並確保寫入與輸出的穩定度。</li>
</ol>



<h3 class="wp-block-heading">三、 處理超大型程式碼檔案的技巧</h3>



<p class="wp-block-paragraph">如果程式碼檔案<strong>極大（超過 20,000 個 Token / 數千行）</strong>，光靠調整上下文可能仍會吞不下，建議配合以下使用習慣：</p>



<ol start="1" class="wp-block-list">
<li><strong>善用分段輸入：</strong>不要一次性將整個專案或數個大檔案直接貼給 Pi Agent。可以先貼出關鍵的 Interface、類別定義或特定 Function，再請它進行重構或分析。</li>



<li><strong>手動觸發壓縮：</strong>如果在處理大檔案的過程中感覺對話已經很長，可以在 Pi Agent 中手動執行 <code>/compact</code> 指令，先讓 Pi 整理摘要出之前的對話重點，清出空間後再貼入新的程式碼片段。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">RTX 4050 laptop, GPU 6GB, 共用GPU 有 5.8GB, 是否該把 ngl 調大?</h2>



<p class="wp-block-paragraph">絕對<strong>不建議</strong>為了使用「共用 GPU 記憶體」而把 <code>-ngl</code> 調大。</p>



<p class="wp-block-paragraph">任務管理員顯示的「共用 GPU 記憶體（Shared GPU Memory）5.8 GB」，本質上就是你的<strong>系統主記憶體（System RAM）</strong>。這是 NVIDIA 驅動程式在 6 GB 專用 VRAM 爆滿時的「退路與保護機制」，<strong>並不是真正的顯示記憶體</strong>。</p>



<h3 class="wp-block-heading">1. 為什麼不能硬塞給共用 GPU 記憶體？</h3>



<ul class="wp-block-list">
<li><strong>速度急劇暴跌：</strong>RTX 4050 專用 VRAM 頻寬高達 <strong>192 GB/s</strong>；而透過 PCIe 通道存取的共用 GPU 記憶體（DDR4/DDR5），頻寬通常只有 <strong>40~60 GB/s</strong>。一旦模型的層數跨越到共用記憶體，每生成一個 token 都要跨越 PCIe 傳送龐大數據，吐字速度會出現極為嚴重的卡頓甚至暴跌。</li>



<li><strong>嚴重影響系統流暢度：</strong>共用記憶體會大量佔用 Windows 系統的 RAM 與 PCIe 頻寬，容易導致 Windows 介面、瀏覽器或其它應用程式同時發生卡頓。</li>
</ul>



<h3 class="wp-block-heading">2. RTX 4050（6 GB VRAM）的最佳負載邊界</h3>



<p class="wp-block-paragraph">當使用 <code>Gemma-4-12B Q4_K_XL</code> 且將上下文開到 <code>CTX=32768</code> 時，你的 <strong>6 GB 專用 VRAM 已經幾乎被吃滿</strong>。</p>



<p class="wp-block-paragraph">記憶體實際分配大致如下：</p>



<ul class="wp-block-list">
<li><strong>模型權重（24 層於 GPU）：</strong> 約佔用 3.8 ~ 4.2 GB VRAM</li>



<li><strong>32K 上下文 KV Cache (Q8_0)：</strong> 約佔用 1.2 ~ 1.5 GB VRAM</li>



<li><strong>Flash Attention 與動態計算緩衝區：</strong> 約佔用 0.3 ~ 0.5 GB VRAM</li>
</ul>



<p class="wp-block-paragraph"><strong>總計已經來到 5.5 GB ~ 5.8 GB</strong>，非常貼近 6 GB 專用 VRAM 的安全極限。<sup></sup></p>



<h3 class="wp-block-heading">3. 正確的參數調整建議</h3>



<ul class="wp-block-list">
<li><strong><code>-ngl</code> 保持在 24（最高不超過 26）：</strong><code>-ngl 24</code> 能確保所有載入 GPU 的數據都嚴格限制在 <strong>6 GB 實體 VRAM</strong> 內，其餘層數由 CPU 與 RAM 負責處理。這是兼顧生成速度與穩定的最優配比。</li>



<li><strong>千萬不要設 <code>-ngl 32</code> 或 <code>-ngl 999</code>：</strong>這會強制 llama.cpp 試圖將超出的權重塞進「共用 GPU 記憶體」，導致顯示卡頻率被拖慢，推論速度反而比使用 CPU+RAM 還要慢。</li>
</ul>



<p class="wp-block-paragraph"><strong>結論：</strong> 請繼續維持目前建議的 <code>-ngl 24</code>，讓專用 VRAM 專心處理核心層數，千萬不要為了動用「共用 GPU 記憶體」而加大 <code>-ngl</code>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">0.00.153.019 I srv load_model: loading model &#8216;models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf&#8217;0.01.031.241 W common_fit_params: failed to fit params to free device memory: n_gpu_layers already set by user to 32, abort</p>



<h3 class="wp-block-heading">關鍵警告：VRAM 空間不足，自動放棄調整</h3>



<pre class="wp-block-code"><code>W common_fit_params: failed to fit params to free device memory: n_gpu_layers already set by user to 32, abort
</code></pre>



<ul class="wp-block-list">
<li><strong>解析：</strong> 因為你在腳本中寫死了 <code>-ngl 32</code>，系統在評估 6 GB VRAM 時，發現「<strong>根本塞不下 32 層</strong>」。但因為你手動指定了層數，llama.cpp 無法自動幫你降低層數，只能硬著頭皮載入。</li>



<li><strong>結果：</strong> 這會導致過多的層數溢出到「共用 GPU 記憶體（System RAM）」，造成推論速度嚴重卡頓。</li>



<li><strong>解決辦法：</strong> <strong>請務必把 <code>-ngl</code> 降為 <code>24</code>（甚至 <code>20</code>）</strong>，明確告訴系統只塞 24 層進 6 GB 專用 VRAM。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">上面的答案真的正確嗎? 以同一個提示詞在無誤設定值</p>



<p class="wp-block-paragraph">nlg=24</p>



<pre class="wp-block-preformatted">0.11.665.291 I srv load_model: initializing, n_slots = 1, n_ctx_slot = 32768, kv_unified = 'false'<br>0.11.679.524 I srv llama_server: model loaded<br>0.11.679.537 I srv llama_server: listening on <a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a><br>0.11.679.539 W srv llama_server: NOTICE: server default port will be changed to :9931 in a future release<br>0.11.679.541 W srv llama_server: ref: <a href="https://github.com/ggml-org/llama.cpp/pull/26508">https://github.com/ggml-org/llama.cpp/pull/26508</a><br>2.28.358.641 I slot get_availabl: id 0 | task -1 | selected slot by LRU, t_last = -1<br>2.28.360.889 I slot launch_slot_: id 0 | task 0 | processing task, is_child = 0<br>2.32.739.271 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 1024, progress = 0.08, t = 4.38 s / 233.88 tokens per second<br>2.34.297.891 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 1313, progress = 0.10, t = 5.94 s / 221.16 tokens per second<br>2.36.425.766 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 1825, progress = 0.14, t = 8.06 s / 226.29 tokens per second<br>2.38.552.364 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 2337, progress = 0.19, t = 10.19 s / 229.31 tokens per second<br>2.40.707.834 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 2849, progress = 0.23, t = 12.35 s / 230.75 tokens per second<br>2.42.855.519 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 3361, progress = 0.27, t = 14.49 s / 231.88 tokens per second<br>2.44.978.721 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 3873, progress = 0.31, t = 16.62 s / 233.06 tokens per second<br>2.47.303.651 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 4385, progress = 0.35, t = 18.94 s / 231.49 tokens per second<br>2.49.415.947 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 4897, progress = 0.39, t = 21.06 s / 232.58 tokens per second<br>2.51.536.236 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 5409, progress = 0.43, t = 23.18 s / 233.40 tokens per second<br>2.53.633.585 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 5921, progress = 0.47, t = 25.27 s / 234.28 tokens per second<br>2.55.734.939 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 6433, progress = 0.51, t = 27.37 s / 235.00 tokens per second<br>2.57.888.103 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 6945, progress = 0.55, t = 29.53 s / 235.21 tokens per second<br>3.00.013.323 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 7457, progress = 0.59, t = 31.65 s / 235.59 tokens per second<br>3.02.145.908 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 7969, progress = 0.63, t = 33.78 s / 235.87 tokens per second<br>3.04.287.440 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 8481, progress = 0.67, t = 35.93 s / 236.07 tokens per second<br>3.06.436.657 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 8993, progress = 0.71, t = 38.08 s / 236.19 tokens per second<br>3.08.640.398 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 9505, progress = 0.75, t = 40.28 s / 235.98 tokens per second<br>3.10.805.916 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10017, progress = 0.80, t = 42.44 s / 236.00 tokens per second<br>3.12.985.251 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10529, progress = 0.84, t = 44.62 s / 235.95 tokens per second<br>3.14.315.724 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10552, progress = 0.84, t = 45.95 s / 229.62 tokens per second<br>3.14.867.315 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10635, progress = 0.84, t = 46.51 s / 228.68 tokens per second<br>3.17.055.962 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 11147, progress = 0.89, t = 48.70 s / 228.91 tokens per second<br>3.19.283.231 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 11659, progress = 0.93, t = 50.92 s / 228.96 tokens per second<br>3.21.505.917 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12171, progress = 0.97, t = 53.14 s / 229.02 tokens per second<br>3.23.128.171 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12463, progress = 0.99, t = 54.77 s / 227.56 tokens per second<br>3.23.679.332 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12579, progress = 1.00, t = 55.32 s / 227.39 tokens per second<br>3.24.492.842 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12591, progress = 1.00, t = 56.13 s / 224.31 tokens per second<br>3.34.848.536 I slot print_timing: id 0 | task 0 | prompt eval time = 56584.11 ms / 12595 tokens ( 4.49 ms per token, 222.59 tokens per second)<br>3.34.848.548 I slot print_timing: id 0 | task 0 | eval time = 9898.23 ms / 49 tokens ( 206.21 ms per token, 4.85 tokens per second)<br>3.34.848.550 I slot print_timing: id 0 | task 0 | total time = 66482.34 ms / 12644 tokens<br>3.34.848.553 I slot print_timing: id 0 | task 0 | graphs reused = 95<br>3.34.861.451 I slot release: id 0 | task 0 | stop processing: n_tokens = 12643, truncated = 0<br>3.36.344.746 I slot get_availabl: id 0 | task -1 | selected slot by LCP similarity, f_sim_best = 0.997 (&gt; 0.100 thold), f_keep = 0.999<br>3.36.345.354 I slot launch_slot_: id 0 | task 79 | processing task, is_child = 0<br>3.48.294.821 I slot print_timing: id 0 | task 79 | prompt eval time = 2555.49 ms / 35 tokens ( 73.01 ms per token, 13.70 tokens per second)<br>3.48.294.828 I slot print_timing: id 0 | task 79 | eval time = 9393.93 ms / 49 tokens ( 195.71 ms per token, 5.11 tokens per second)<br>3.48.294.830 I slot print_timing: id 0 | task 79 | total time = 11949.42 ms / 84 tokens<br>3.48.294.831 I slot print_timing: id 0 | task 79 | graphs reused = 142<br>3.48.295.889 I slot release: id 0 | task 79 | stop processing: n_tokens = 12708, truncated = 0<br>3.48.883.888 I slot get_availabl: id 0 | task -1 | selected slot by LCP similarity, f_sim_best = 0.991 (&gt; 0.100 thold), f_keep = 0.999<br>3.48.884.406 I slot launch_slot_: id 0 | task 130 | processing task, is_child = 0<br>4.02.911.486 I slot print_timing: id 0 | task 130 | prompt eval time = 1060.78 ms / 119 tokens ( 8.91 ms per token, 112.18 tokens per second)<br>4.02.911.492 I slot print_timing: id 0 | task 130 | eval time = 12966.25 ms / 64 tokens ( 205.81 ms per token, 4.86 tokens per second)<br>4.02.911.493 I slot print_timing: id 0 | task 130 | total time = 14027.03 ms / 183 tokens<br>4.02.911.494 I slot print_timing: id 0 | task 130 | graphs reused = 204<br>4.02.912.197 I slot release: id 0 | task 130 | stop processing: n_tokens = 12875, truncated = 0</pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">在nbl=32 設定值, 反而 token 輸出更多.</p>



<p class="wp-block-paragraph">0.11.247.014 I cmn init: llama threadpool init, n_threads = 8<br>0.11.708.585 I srv load_model: initializing, n_slots = 1, n_ctx_slot = 32768, kv_unified = &#8216;false&#8217;<br>0.11.719.733 I srv llama_server: model loaded<br>0.11.719.744 I srv llama_server: listening on <a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a><br>0.11.719.747 W srv llama_server: NOTICE: server default port will be changed to :9931 in a future release<br>0.11.719.749 W srv llama_server: ref: <a href="https://github.com/ggml-org/llama.cpp/pull/26508">https://github.com/ggml-org/llama.cpp/pull/26508</a><br>0.19.794.187 I slot get_availabl: id 0 | task -1 | selected slot by LRU, t_last = -1<br>0.19.794.444 I slot launch_slot_: id 0 | task 0 | processing task, is_child = 0<br>0.23.261.643 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 1024, progress = 0.08, t = 3.47 s / 295.34 tokens per second<br>0.24.512.009 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 1313, progress = 0.10, t = 4.72 s / 278.32 tokens per second<br>0.26.187.768 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 1825, progress = 0.14, t = 6.39 s / 285.46 tokens per second<br>0.27.881.669 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 2337, progress = 0.18, t = 8.09 s / 288.98 tokens per second<br>0.29.591.197 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 2849, progress = 0.22, t = 9.80 s / 290.81 tokens per second<br>0.31.294.120 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 3361, progress = 0.26, t = 11.50 s / 292.27 tokens per second<br>0.32.971.017 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 3873, progress = 0.30, t = 13.18 s / 293.93 tokens per second<br>0.34.653.474 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 4385, progress = 0.34, t = 14.86 s / 295.11 tokens per second<br>0.36.360.070 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 4897, progress = 0.38, t = 16.57 s / 295.61 tokens per second<br>0.38.162.684 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 5409, progress = 0.42, t = 18.37 s / 294.48 tokens per second<br>0.40.018.180 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 5921, progress = 0.46, t = 20.22 s / 292.78 tokens per second<br>0.41.816.396 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 6433, progress = 0.50, t = 22.02 s / 292.12 tokens per second<br>0.43.483.995 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 6945, progress = 0.54, t = 23.69 s / 293.17 tokens per second<br>0.45.187.034 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 7457, progress = 0.58, t = 25.39 s / 293.67 tokens per second<br>0.47.036.932 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 7969, progress = 0.62, t = 27.24 s / 292.52 tokens per second<br>0.48.737.307 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 8481, progress = 0.66, t = 28.94 s / 293.03 tokens per second<br>0.50.440.491 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 8993, progress = 0.70, t = 30.65 s / 293.45 tokens per second<br>0.52.155.480 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 9505, progress = 0.74, t = 32.36 s / 293.72 tokens per second<br>0.53.866.976 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10017, progress = 0.78, t = 34.07 s / 293.99 tokens per second<br>0.55.606.238 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10529, progress = 0.82, t = 35.81 s / 294.01 tokens per second<br>0.56.688.545 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10552, progress = 0.83, t = 36.89 s / 286.01 tokens per second<br>0.57.110.883 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 10635, progress = 0.83, t = 37.32 s / 285.00 tokens per second<br>0.58.859.668 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 11147, progress = 0.87, t = 39.07 s / 285.34 tokens per second<br>1.00.605.512 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 11659, progress = 0.91, t = 40.81 s / 285.68 tokens per second<br>1.02.421.587 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12171, progress = 0.95, t = 42.63 s / 285.52 tokens per second<br>1.04.162.420 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12637, progress = 0.99, t = 44.37 s / 284.82 tokens per second<br>1.04.615.403 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12753, progress = 1.00, t = 44.82 s / 284.53 tokens per second<br>1.05.255.757 I slot print_timing: id 0 | task 0 | prompt processing, n_tokens = 12765, progress = 1.00, t = 45.46 s / 280.79 tokens per second<br>1.22.659.375 I slot print_timing: id 0 | task 0 | n_gen = 100, tg = 5.81 t/s, tg_3s = 5.87 t/s<br>1.25.707.350 I slot print_timing: id 0 | task 0 | n_gen = 118, tg = 5.82 t/s, tg_3s = 5.91 t/s<br>1.28.771.953 I slot print_timing: id 0 | task 0 | n_gen = 137, tg = 5.87 t/s, tg_3s = 6.20 t/s<br>1.31.844.867 I slot print_timing: id 0 | task 0 | n_gen = 156, tg = 5.91 t/s, tg_3s = 6.18 t/s<br>1.34.973.303 I slot print_timing: id 0 | task 0 | n_gen = 175, tg = 5.93 t/s, tg_3s = 6.07 t/s<br>1.38.131.635 I slot print_timing: id 0 | task 0 | n_gen = 194, tg = 5.94 t/s, tg_3s = 6.02 t/s<br>1.41.180.046 I slot print_timing: id 0 | task 0 | n_gen = 213, tg = 5.96 t/s, tg_3s = 6.23 t/s<br>1.44.038.718 I slot print_timing: id 0 | task 0 | prompt eval time = 45822.72 ms / 12769 tokens ( 3.59 ms per token, 278.66 tokens per second)<br>1.44.038.724 I slot print_timing: id 0 | task 0 | eval time = 38421.51 ms / 231 tokens ( 167.05 ms per token, 5.99 tokens per second)<br>1.44.038.725 I slot print_timing: id 0 | task 0 | total time = 84244.23 ms / 13000 tokens<br>1.44.038.726 I slot print_timing: id 0 | task 0 | graphs reused = 276<br>1.44.056.373 I slot release: id 0 | task 0 | stop processing: n_tokens = 12999, truncated = 0</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">之前的分析確實是不夠準確的。直接看數據比較，事實證明在你的硬體環境下，設定 <code>ngl=32</code> 的表現全面超越了 <code>ngl=24</code>。</p>



<p class="wp-block-paragraph">數據對比</p>



<p class="wp-block-paragraph">Prompt 處理速度（Prompt Processing）</p>



<ul class="wp-block-list">
<li><code>ngl=24</code>：約 <strong>222.59 tokens/second</strong>（耗時 56.58 秒）</li>



<li><code>ngl=32</code>：約 <strong>278.66 tokens/second</strong>（耗時 45.82 秒，<strong>提升了約 25%</strong>）</li>
</ul>



<p class="wp-block-paragraph">文字生成速度（Text Generation / Eval time）</p>



<ul class="wp-block-list">
<li><code>ngl=24</code>：約 <strong>4.85 tokens/second</strong>（每 token 206.21 ms）</li>



<li><code>ngl=32</code>：約 <strong>5.99 tokens/second</strong>（每 token 167.05 ms，<strong>提升了約 23.5%</strong>）</li>
</ul>



<p class="wp-block-paragraph">為什麼會出現這個結果？</p>



<p class="wp-block-paragraph">先前過於低估了共用顯示記憶體與 PCIe 傳輸的實際效率。</p>



<ol start="1" class="wp-block-list">
<li><strong>GPU 計算單元優勢顯著</strong>：將更多的模型層數（32 層 vs 24 層）交給 GPU 算力單元去運算，所獲得的矩陣計算加速，遠遠超過了數據透過 PCIe 或記憶體傳輸帶來的延遲開銷。</li>



<li><strong>記憶體管理機制運作良好</strong>：當前的系統與驅動程式在處理溢出至 Shared GPU Memory 的權重時，併發存取的效能優於純 CPU+RAM 計算，因此並沒有出現嚴重的卡頓。</li>
</ol>



<p class="wp-block-paragraph">調整建議</p>



<p class="wp-block-paragraph">既然數據證明 <code>ngl=32</code> 的執行效率顯著優於 <code>ngl=24</code>，請直接<strong>保持 <code>-ngl 32</code> 的設定</strong>。</p>



<p class="wp-block-paragraph">建議的批次檔參數組合：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>@echo off
chcp 65001 &gt;nul
cd /d "%~dp0"
echo ================================================
echo  gemma-4-12B-it-qat-UD-Q4_K_XL - HIGH PERFORMANCE
echo ================================================
echo.

set MODEL=models\gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

set CTX=32768
set BATCH=512
set UBATCH=128
set NP=1
set NGL=32
set THREADS=8

"%EXE%" ^
  -m "%MODEL%" ^
  -c %CTX% ^
  -n 8192 ^
  -np %NP% ^
  -b %BATCH% ^
  -ub %UBATCH% ^
  -ngl %NGL% ^
  -t %THREADS% ^
  -ctk q8_0 ^
  -ctv q8_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa 1

pause
</code></pre>



<p class="wp-block-paragraph">以數據為準的調整思路非常正確，後續只需維持 Pi Agent 端 <code>contextWindow: 28000</code> 的搭配即可。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">設到 <code>ngl=64</code> 會爆慢，是因為你跨過了<strong>系統記憶體分頁（Paging）與 PCIe 傳輸崩潰</strong>的臨界點。</p>



<p class="wp-block-paragraph">這份 Gemma 12B 模型總共只有 <strong>48 層（Layers）</strong>。你設定 <code>-ngl 64</code> 代表要求把所有層數（全額）以及高達 32K 的 KV Cache 強制塞進 GPU。</p>



<p class="wp-block-paragraph">這引發了連鎖災難反應：</p>



<ol start="1" class="wp-block-list">
<li><strong>超出專用 VRAM 太多（6GB vs 8GB+）</strong>原本 <code>ngl=32</code> 時，溢出到共用記憶體（Shared GPU Memory）的資料量還在 Windows 驅動程式的高速快取緩衝範圍內。當你改成 <code>64</code>（全部 48 層硬塞），顯示卡 6GB VRAM 會被 100% 擠爆。</li>



<li><strong>PCIe 匯流排徹底塞車</strong>模型所有的權重與 32K KV Cache（共約 8~10 GB）被迫全部擠在共用記憶體（RAM）裡面。每一層計算，GPU 都必須透過 PCIe 介面往返讀寫系統 RAM。頻寬直接從 192 GB/s 降到 40 GB/s 左右，形成了嚴重的傳輸瓶頸。</li>



<li><strong>觸發 Windows 虛擬記憶體 Swap</strong>當共用 GPU 記憶體被吃滿，Windows 驅動程式會開始把記憶體資料寫入硬碟的分頁檔（Pagefile）。從 Log 看到 Prompt 處理速度一路從 <code>146 t/s</code> 掉到 <code>82 t/s</code>（<code>ngl=32</code> 時有 <code>280 t/s</code>），這是典型的記憶體分頁交換（Thrashing）現象。</li>
</ol>



<p class="wp-block-paragraph"><strong>最佳化總結</strong></p>



<ul class="wp-block-list">
<li><strong><code>ngl=24</code></strong>：留太少給 GPU，算力沒發揮。</li>



<li><strong><code>ngl=32</code></strong>（最佳甜蜜點）：GPU 負擔大部分層數，溢出量剛好在 PCIe 高速緩衝內，達成速度最大化。</li>



<li><strong><code>ngl=64</code></strong>（邊界崩潰）：完全擠爆 VRAM 並壓垮系統記憶體與 PCIe 頻寬，導致效能暴跌。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">CPU 大小核心數的差異</h2>



<p class="wp-block-paragraph">硬體環境:</p>



<pre class="wp-block-code"><code>裝置名稱 DESKTOP-4RTC35D
處理器 12th Gen Intel(R) Core(TM) i7-12700 (2.10 GHz)
已安裝記憶體(RAM) 32.0 GB (31.7 GB 可用)
系統類型 64 位元作業系統，x64 型處理器</code></pre>



<p class="wp-block-paragraph">根據你提供的規格，你的 CPU 是 <strong>Intel Core i7-12700</strong>（採用 Alder Lake 架構，混合大核心 P-Core 與小核心 E-Core）。</p>



<p class="wp-block-paragraph">這顆處理器的詳細核心結構如下：</p>



<ul class="wp-block-list">
<li><strong>8 個 P-Core (效能大核心)</strong>：支援超執行緒，共 16 執行緒。</li>



<li><strong>4 個 E-Core (能效小核心)</strong>：不支援超執行緒，共 4 執行緒。</li>



<li><strong>總計</strong>：12 核心 / 20 執行緒。</li>
</ul>



<p class="wp-block-paragraph">在 <code>llama.cpp</code> 純 CPU 推理中，<strong>混用 E-Core 或開啟超執行緒會嚴重拉慢整體速度</strong>（因為大核心必須等待小核心完成計算，且快取命中率會下降）。針對你這顆 CPU，最佳的執行緒設定是<strong>僅使用 8 個 P-Core 的實體核心</strong>。</p>



<h3 class="wp-block-heading">i7-12700 最佳化 Batch 腳本</h3>



<p class="wp-block-paragraph">請將 <code>-t</code> 與 <code>-tb</code> 調整為 <strong>8</strong>，並套用針對 Intel 12 代 CPU 的優化參數：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>set MODEL=models\gemma-4-12b-it-Q5_K_S.gguf
set EXE=llama-server.exe

:: Context 與 Batch 調整
set CTX=24576
set BATCH=1024
set UBATCH=512
set NP=1
set NGL=0

:: 針對 i7-12700 的 8 個 P-Core 設定
set THREADS=8

:: 停用 SYCL / GPU 偵測，避免開頭 Warning 延遲
set GGML_SYCL_DISABLE=1
set ONEAPI_DEVICE_SELECTOR=*:cpu
set SYCL_DEVICE_FILTER=cpu

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -n 16384 ^
  -b %BATCH% -ub %UBATCH% ^
  -ngl %NGL% ^
  -t %THREADS% ^
  -tb %THREADS% ^
  -ctk q4_0 -ctv q4_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  --reasoning-format none ^
  --temp 0.65 ^
  --top-p 0.85 ^
  --top-k 35 ^
  --repeat-penalty 1.02
</code></pre>



<h3 class="wp-block-heading">調整後預期改進點</h3>



<ol start="1" class="wp-block-list">
<li><strong>從 <code>-t 11</code> 降至 <code>-t 8</code></strong>：排除小核心 (E-Core) 與超執行緒搶資源問題，生成的 Token 速度（t/s）預計可提升 <strong>10% ~ 20%</strong>。</li>



<li><strong><code>-ctk q4_0 -ctv q4_0</code></strong>：將 KV Cache 降至 4-bit，節省記憶體頻寬耗損，對於 long context 時的性能更有幫助。</li>



<li><strong><code>-b 1024 -ub 512</code></strong>：充分發揮 i7-12700 的 AVX2 指令集，加快 Prompt 吞吐效率。</li>
</ol>



<p class="wp-block-paragraph"><em>(註：若未來想要突破 CPU 推理的物理極限，建議將模型檔換成 <strong>Q4_K_M</strong> 規格，生成速度會再顯著提升。)</em></p>



<p class="wp-block-paragraph">在 <strong>i7-12700 (8P+4E, 32GB RAM)</strong> 的純 CPU 環境下，<strong>強烈建議選擇 12B (Q4_K_M)</strong>。</p>



<p class="wp-block-paragraph">不建議選擇 26B 的核心原因為<strong>記憶體頻寬瓶頸（生成速度）與記憶體容量極限</strong>：</p>



<h3 class="wp-block-heading">12B vs 26B 規格與效能對比 (Q4_K_M)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>評估項目</strong></td><td><strong>12B (Q4_K_M) (推薦)</strong></td><td><strong>26B (Q4_K_M) (不建議)</strong></td></tr></thead><tbody><tr><td><strong>模型檔案大小</strong></td><td>約 <strong>7.3 GB</strong></td><td>約 <strong>15.8 GB</strong></td></tr><tr><td><strong>預估 RAM 總消耗 (含 24K CTX)</strong></td><td>約 <strong>10 &#8211; 12 GB</strong></td><td>約 <strong>19 &#8211; 22 GB</strong></td></tr><tr><td><strong>預估生成速度 (Eval)</strong></td><td><strong>~ 8.5 &#8211; 10.0 t/s</strong> (順暢)</td><td><strong>~ 3.5 &#8211; 4.5 t/s</strong> (明顯卡頓)</td></tr><tr><td><strong>Prompt 處理速度 (PP)</strong></td><td><strong>~ 20 &#8211; 30 t/s</strong></td><td><strong>~ 8 &#8211; 12 t/s</strong></td></tr><tr><td><strong>系統負擔與剩餘記憶體</strong></td><td>剩餘 &gt; 20 GB，可輕鬆開瀏覽器/其他工具</td><td>剩餘 &lt; 10 GB，高機率觸發 Windows 分頁交換 (Paging)</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">為什麼選擇 12B 才是最佳解？</h3>



<ol start="1" class="wp-block-list">
<li><strong>CPU 推理的本質是「記憶體頻寬」挑戰</strong>CPU 生成 Token 時，每產生一個字，都必須將<strong>整個模型</strong>從 DDR4/DDR5 RAM 完整讀取一次：
<ul class="wp-block-list">
<li><strong>12B (7.3 GB)</strong>：i7-12700 的雙通道 DDR4/DDR5 頻寬約 50–60 GB/s，讀取一次僅需 0.12 秒，故速度能達到 <strong>8~10 t/s</strong>（接近人類閱讀速度）。</li>



<li><strong>26B (15.8 GB)</strong>：模型大了一倍，讀取一次需要近 0.3 秒，生成速度會暴跌至 <strong>3~4 t/s</strong>，實際使用時會有明顯的逐字卡頓感。</li>
</ul>
</li>



<li><strong>24K 長 Context 的 RAM 壓力</strong>雖然你擁有 32GB RAM，但當 Context 設定為 <code>24576</code> 時，KV Cache 與系統運作也會佔用大量記憶體：
<ul class="wp-block-list">
<li><strong>26B 模型</strong>光是載入模型加初始化 Context，就會吃掉近 20GB RAM。若背景同時開著 Chrome 或其他程式，實體記憶體可能吃滿，導致 Windows 開始使用硬碟 SWAP，速度會再降至 1 t/s 以下。</li>
</ul>
</li>



<li><strong>12B (Q4_K_M) 是目前的 CP 值黃金交叉點</strong>Gemma 2 / Gemma 4 12B 的能力已經超越舊款的 13B~20B 模型。換成 <strong>12B Q4_K_M</strong> 後，相較於你之前的 12B Q5_K_S，檔案減小約 1.2GB，<strong>速度能有感提升 20%~30%</strong>，且推理能力幾乎零損耗。</li>
</ol>



<h3 class="wp-block-heading">結論與建議腳本</h3>



<p class="wp-block-paragraph">請直接下載 <strong><code>Gemma-12B-Q4_K_M.gguf</code></strong>（或 Qwen 12B 等同級模型），並使用以下優化腳本執行：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>set MODEL=models\gemma-4-12b-it-Q4_K_M.gguf
set EXE=llama-server.exe

set CTX=24576
set BATCH=1024
set UBATCH=512
set NP=1
set NGL=0

:: i7-12700 8 個 P-Core 最佳設定
set THREADS=8

set GGML_SYCL_DISABLE=1
set ONEAPI_DEVICE_SELECTOR=*:cpu
set SYCL_DEVICE_FILTER=cpu

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -n 16384 ^
  -b %BATCH% -ub %UBATCH% ^
  -ngl %NGL% ^
  -t %THREADS% ^
  -tb %THREADS% ^
  -ctk q4_0 -ctv q4_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  --reasoning-format none ^
  --temp 0.65 ^
  --top-p 0.85 ^
  --top-k 35 ^
  --repeat-penalty 1.02
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">對於你的 <strong>i7-12700 + 32GB RAM 純 CPU 環境</strong>，建議選擇 <strong>UD-Q4_K_XL</strong>。</p>



<p class="wp-block-paragraph">以下是這兩種量化格式的評比與原因：</p>



<h3 class="wp-block-heading">量化格式比較表</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>評估項目</strong></td><td><strong>Q4_K_M (標準 K-quant)</strong></td><td><strong>UD-Q4_K_XL (Uncensored / Unsloth Uncensored / Custom UD)</strong></td></tr></thead><tbody><tr><td><strong>檔案大小</strong></td><td>7.12 GB</td><td>7.37 GB (+250 MB)</td></tr><tr><td><strong>對 CPU 速度影響</strong></td><td><strong>極微小</strong>（差距小於 3%）</td><td><strong>極微小</strong>（生成速度約維持在 8–10 t/s）</td></tr><tr><td><strong>邏輯與長文本精度</strong></td><td>良好</td><td><strong>優異</strong>（針對重要權重層進行保護/微調）</td></tr><tr><td><strong>記憶體佔用</strong></td><td>約 10.5 GB (含 24K CTX)</td><td>約 10.8 GB (含 24K CTX，32GB RAM 完全無壓力)</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">為什麼推薦 UD-Q4_K_XL？</h3>



<ol start="1" class="wp-block-list">
<li><strong>體積僅多 250MB，CPU 速度幾乎零感差異</strong>CPU 推理的極限在於記憶體讀取頻寬。7.12 GB 與 7.37 GB 在 i7-12700 的雙通道 RAM 讀取下，<strong>每秒生成字數（t/s）的差異不到 0.3 t/s</strong>，體感完全一致。</li>



<li><strong>UD (Unsloth / Dynamic) 量化精度更高</strong><code>UD</code>（通常為 Unsloth Dynamic Quantization 或更高等級的混合權重保留）會對 LLM 中最關鍵的 Attention 層與 MLP 層保留更高的位元精度，降低了常規 4-bit 量化容易出現的「邏輯混亂」與「長文章丟失細節」問題。</li>



<li><strong>RAM 空間綽綽有餘</strong>你的 32GB 記憶體容量相當充足，增加這 250 MB 檔案大小完全不會擠壓到系統或 24K Context 的運作空間。</li>
</ol>



<h3 class="wp-block-heading">結論與建議腳本</h3>



<p class="wp-block-paragraph">直接使用 <strong><code>UD-Q4_K_XL</code></strong> 版本的檔案，運行命令與先前一致：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>set MODEL=models\gemma-4-12b-it-UD-Q4_K_XL.gguf
set EXE=llama-server.exe

set CTX=32767
set BATCH=1024
set UBATCH=512
set NP=1
set NGL=0

:: 配合 i7-12700 實體大核心 P-Core
set THREADS=8

set GGML_SYCL_DISABLE=1
set ONEAPI_DEVICE_SELECTOR=*:cpu
set SYCL_DEVICE_FILTER=cpu

"%EXE%" ^
  -m %MODEL% ^
  -c %CTX% ^
  -np %NP% ^
  -n 16384 ^
  -b %BATCH% -ub %UBATCH% ^
  -ngl %NGL% ^
  -t %THREADS% ^
  -tb %THREADS% ^
  -ctk q4_0 -ctv q4_0 ^
  --port 8080 ^
  --host 127.0.0.1 ^
  --api-key 12345678 ^
  -fa on ^
  --reasoning-format none ^
  --temp 0.65 ^
  --top-p 0.85 ^
  --top-k 35 ^
  --repeat-penalty 1.02
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">如果只有整合顯卡 Intel UHD Graphics 770，建議直接選擇純 CPU 執行的版本（cpu-x64）。</p>



<p class="wp-block-paragraph">透過 SYCL 讓 UHD 770 執行 12B 模型，速度反而會比 i7-12700 的 CPU 核心還要慢，而且還有系統不穩定的風險。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">為什麼整合顯卡 UHD 770 不適合跑 GPU 推論</p>



<ol start="1" class="wp-block-list">
<li>沒有獨立顯示記憶體（共享系統記憶體）不像獨立顯卡擁有高速的專用顯示記憶體（GDDR6），UHD 770 需要共享主系統記憶體。將模型卸載到內顯運算，資料依然會經過相同的記憶體通道，完全無法發揮 GPU 卸載帶來的效能優勢。</li>



<li>算力顯著不足UHD 770 僅配備 32 個執行單元（EUs），主要針對基本顯示與影片解碼設計。相較之下，i7-12700 的 8 個效能核心（P-Cores）支援高效能 AVX2 指令集，處理張量矩陣運算的速度遠比內顯快得多。</li>



<li>系統凍結與驅動程式崩潰（TDR）風險Windows 針對整合顯卡設有兩秒的超時回應限制（TDR）。進行龐大的矩陣相乘時，容易導致內顯驅動程式卡死，進而引發螢幕閃爍、應用程式崩潰或系統畫面凍結。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">建議設定方式</p>



<p class="wp-block-paragraph">要在現有機器上獲得最佳且最穩定的體驗：</p>



<ol start="1" class="wp-block-list">
<li>下載版本：請使用 <code>llama-bXXXX-bin-win-cpu-x64.zip</code> 編譯版本。</li>



<li>參數設定：保持 <code>-ngl 0</code> 與 <code>-t 8</code>（充分利用 8 個效能核心）。</li>



<li>模型選擇：維持使用 <code>gemma-4-12b-it-UD-Q4_K_XL.gguf</code> 或 <code>Q4_K_M</code>。</li>
</ol>



<p class="wp-block-paragraph">目前每秒 22 個 Token 以上的提示詞處理速度，已經非常接近 DDR4 或 DDR5 記憶體架構的物理極限。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">從日誌記錄來看，模型的伺服器端（ llama.cpp ）其實已經成功處理了高達 50,082 個 Token 的 Prompt 並完成運算，但最終在前端（ 如 Open WebUI 或介面客戶端 ）卻跳出了 <strong>Response was truncated before completion</strong> 的截斷警告。</p>



<p class="wp-block-paragraph">造成這個情況的原因有兩個：</p>



<p class="wp-block-paragraph"><strong>1. 前端（ API Client ）的單次輸出上限（ max_tokens ）太小</strong></p>



<p class="wp-block-paragraph">日誌顯示模型在生成 267 個 Token 後就終止了（ <code>eval time = ... / 267 tokens</code> ），代表您的前端發送請求時，預設的 <code>max_tokens</code>（ 或 <code>max_completion_tokens</code> ）被限制得太短，導致模型還沒講完就被強行在中途中斷。</p>



<p class="wp-block-paragraph"><strong>2. 前端的 Context Window 設定與伺服器不一致</strong></p>



<p class="wp-block-paragraph">雖然 llama.cpp 伺服器端已經設定為大上下文，但前端介面（ 例如之前設定檔中的 <code>model.json</code> ）如果仍限制在 <code>contextWindow: 28000</code> 或 <code>maxTokens: 4096</code>，當總長度超過時，前端的邏輯機制會直接攔截並截斷回應。</p>



<h3 class="wp-block-heading">解決方法</h3>



<p class="wp-block-paragraph">請依照以下設定進行調整：</p>



<p class="wp-block-paragraph"><strong>步驟一：修改前端模型設定檔（ model.json ）</strong></p>



<p class="wp-block-paragraph">請將前端設定檔中的上限同步開大，並提高單次輸出的上限：</p>



<p class="wp-block-paragraph">JSON</p>



<pre class="wp-block-code"><code>{
  "id": "models\\Qwen3.8-27B-UD-IQ3_XXS.gguf",
  "name": "Qwen3.8-27B-Local",
  "contextWindow": 65536,
  "maxTokens": 8192
}
</code></pre>



<p class="wp-block-paragraph"><strong>步驟二：調整 llama.cpp 啟動參數</strong></p>



<p class="wp-block-paragraph">若要允許模型單次輸出長文本，建議在啟動 <code>llama-server</code> 時加上 <code>-n</code>（ 或 <code>--n-predict</code> ）參數，調整單次最大生成的 Token 數量：</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code>llama-server.exe ^
  -m "models\Qwen3.8-27B-UD-IQ3_XXS.gguf" ^
  -ngl 22 ^
  -c 65536 ^
  -ctk q4_0 ^
  -ctv q4_0 ^
  --context-shift ^
  -n 8192 ^
  -t 8 ^
  --port 8080
</code></pre>



<p class="wp-block-paragraph"><code>-n 8192</code>：允許模型單次最多可以回應 8192 個 Token，避免生成到一半被伺服器端切斷。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">在 Pi Agent（ 或基於 Pi 視覺/終端開發的 Agent 介面 ）中，要在<strong>不重啟 Agent</strong> 的情況下重載 <code>model.json</code>，主要有以下三種做法：</p>



<p class="wp-block-paragraph"><strong>1. 使用熱重載斜線指令（ Hot Reload Commands ）</strong></p>



<p class="wp-block-paragraph">部分 Agent 介面內建了記憶體與設定檔重載的快捷指令，可以直接在聊天輸入框輸入並發送：</p>



<p class="wp-block-paragraph"><code>/reload</code> 或 <code>/reload-config</code></p>



<p class="wp-block-paragraph">如果介面支援，這會強制 Agent 重新讀取專案目錄下的 <code>model.json</code> 並更新內部的 <code>contextWindow</code> 與 <code>maxTokens</code> 參數。</p>



<p class="wp-block-paragraph"><strong>2. 透過內建 CLI 或指令選單重設（ Menu / Options ）</strong></p>



<p class="wp-block-paragraph">若您使用的是 Pi 相關的 CLI 互動選單，可以透過快速鍵或選單觸發重載：</p>



<ul class="wp-block-list">
<li><strong>切換模型（ Model Switch ）</strong>：輸入 <code>/model</code> 或按下切換模型的快捷鍵，先切換到隨便另一個模型（ 或重複選擇同一個模型 ），這個動作會觸發 Agent 重新讀取 JSON 配置。</li>



<li><strong>清除 Session / Context</strong>：輸入 <code>/clear</code> 或 <code>/reset</code>。許多 Agent 只會在啟動新對話對話（ New Session ）時讀取最新的模型配置檔，清除舊對話紀錄能讓 Agent 套用新的 <code>contextWindow</code> 邊界。</li>
</ul>



<p class="wp-block-paragraph"><strong>3. 在不離開 Agent 情況下發送控制 API ( 適用於後端服務 )</strong></p>



<p class="wp-block-paragraph">如果您的 Pi Agent 是透過控制台運作，您可以使用內建的 Bash / CLI 執行能力，發送指令給後端或觸發內建的 reload 腳本，例如：</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code># 若 Agent 具備本地命令列執行權限，可發送信號給 Agent 進程
kill -HUP $(pgrep pi-agent)
</code></pre>



<p class="wp-block-paragraph"><strong>小建議</strong>：修改完 <code>model.json</code> 後，最快驗證是否生效的方法是直接輸入 <code>/clear</code> 開起一個全新對話，然後輸入 <code>/info</code> 或 <code>/status</code> 查看當前 Agent 載入的上下文上限值。</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/08/chain-of-thought-collapse/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Fortify 掃描耗盡了記憶體</title>
		<link>https://stackoverflow.max-everyday.com/2026/08/fortify-gc-overhead-limit-exceeded/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/08/fortify-gc-overhead-limit-exceeded/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 12 Aug 2026 04:59:44 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8663</guid>

					<description><![CDATA[Fortify 掃描發生 java.lang.O...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Fortify 掃描發生 <code>java.lang.OutOfMemoryError: GC overhead limit exceeded</code>，代表 JVM 記憶體 Heap 空間不足，且垃圾回收（GC）耗費超過 98% 的時間卻只能回收不到 2% 的記憶體。堆疊訊息顯示 Fortify 在解析 PHP 的 <code>hereDoc</code> (Heredoc語法) 時耗盡了記憶體。</p>



<p class="wp-block-paragraph">請依序嘗試以下解決方案：</p>



<h2 class="wp-block-heading">1. 增加 Fortify 掃描的記憶體上限</h2>



<p class="wp-block-paragraph">預設的記憶體設定可能不足以處理大型專案或複雜的 PHP 檔案。可以在執行 <code>sourceanalyzer</code> 命令行中加入 <code>-Xmx</code> 參數來提高記憶體配額：</p>



<p class="wp-block-paragraph"><strong>命令列執行 (CLI)：</strong></p>



<pre class="wp-block-code"><code><code>sourceanalyzer -b &lt;build_id> -Xmx16G -scan -f result.fpr </code></code></pre>



<p class="wp-block-paragraph"><em>(可依你的伺服器硬體規格調整，例如 <code>-Xmx12G</code> 或 <code>-Xmx24G</code>)</em></p>



<p class="wp-block-paragraph"><strong>若使用 GUI (Audit Workbench / ScanWizard)：</strong></p>



<p class="wp-block-paragraph">開啟 Fortify Audit Workbench。</p>



<p class="wp-block-paragraph">進入 <strong>Options</strong> > <strong>Global Settings</strong> > <strong>Memory Options</strong>。</p>



<p class="wp-block-paragraph">將 <strong>Maximum Allocation (-Xmx)</strong> 調大（如 <code>16384</code> MB）。</p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><strong>修改全域設定檔 (fortify-sca.properties)：</strong></p>



<p class="wp-block-paragraph">找到 Fortify 安裝目錄或使用者目錄下的設定檔（例如 <code>&lt;Fortify_Home>/Core/config/fortify-sca.properties</code>），修改或加入以下參數：</p>



<pre class="wp-block-code"><code><code>com.fortify.sca.ProjectScanMemoryMB=16384</code></code></pre>



<h2 class="wp-block-heading">2. 排除不必要的檔案或大型自動生成檔</h2>



<p class="wp-block-paragraph">從錯誤堆疊可看出問題出在解析 PHP 的 <code>hereDoc</code> 語法（可能是巨大的模板檔、自動生成的 SQL/Data 檔、或是包含大字串陣列的檔案）。</p>



<p class="wp-block-paragraph">如果增加記憶體後仍失敗，建議排除非必要的第三方套件與靜態檔：</p>



<p class="wp-block-paragraph"><strong>在翻譯（Translation）階段使用 <code>-exclude</code> 排除特定目錄或檔案：</strong></p>



<pre class="wp-block-code"><code>sourceanalyzer -b &lt;build_id> -exclude "/vendor/" -exclude "/node_modules/" -exclude "/*.min.js" src/</code></pre>



<p class="wp-block-paragraph"><strong>過濾大檔案：</strong></p>



<p class="wp-block-paragraph">可以在 <code>fortify-sca.properties</code> 中限制 Fortify 解析單一檔案的大小上限：</p>



<pre class="wp-block-code"><code><code>com.fortify.sca.limit.file.size=10240 </code></code></pre>



<p class="wp-block-paragraph"><em>(單位為 KB，例如設定 <code>10240</code> 會跳過大於 10MB 的單一原始碼檔案)</em></p>



<h2 class="wp-block-heading">3. 採用「雙階段掃描」分離記憶體開銷</h2>



<p class="wp-block-paragraph">若原本是單一指令執行，請改為將翻譯 (Translation)<strong>與</strong>掃描 (Scan)拆為兩個獨立步驟，並只在掃描階段調大記憶體：</p>



<pre class="wp-block-code"><code># 第一步：翻譯階段
sourceanalyzer -b myproject -clean
sourceanalyzer -b myproject ./src

# 第二步：分析掃描階段 (在此處給予最大記憶體)
sourceanalyzer -b myproject -Xmx16G -scan -f result.fpr
</code></pre>



<h3 class="wp-block-heading">建議處理順序</h3>



<ol start="1" class="wp-block-list">
<li><strong>直接將記憶體調大至 16G (<code>-Xmx16G</code>)</strong> 重試一次。</li>



<li>若依然爆記憶體，檢查專案中是否有包含大量文字資料的 <code>.php</code> 檔案（如大陣列檔、DB dump 檔、巨型 Heredoc 模板），並使用 <code>-exclude</code> 將其排除。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><strong>detail error message:</strong></p>



<pre class="wp-block-preformatted">[error]: There is not enough memory available to complete analysis.  For details on making more memory available, please consult the user manual.<br>java.lang.OutOfMemoryError: GC overhead limit exceeded<br>	at com.fortify.frontend.php.parser.PHPLexerBase.nextToken(PHPLexerBase.java:95) ~[?:?]<br>	at org.antlr.v4.runtime.BufferedTokenStream.fetch(BufferedTokenStream.java:169) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at org.antlr.v4.runtime.BufferedTokenStream.sync(BufferedTokenStream.java:152) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at org.antlr.v4.runtime.BufferedTokenStream.nextTokenOnChannel(BufferedTokenStream.java:325) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at org.antlr.v4.runtime.CommonTokenStream.adjustSeekIndex(CommonTokenStream.java:70) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at org.antlr.v4.runtime.BufferedTokenStream.consume(BufferedTokenStream.java:137) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at org.antlr.v4.runtime.Parser.consume(Parser.java:571) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at org.antlr.v4.runtime.Parser.match(Parser.java:205) ~[antlr4-runtime-4.13.1.jar:4.13.1]<br>	at com.fortify.frontend.php.parser.PHPParser.hereDocPart(PHPParser.java:9032) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.hereDocExpr(PHPParser.java:8947) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.primaryExpr(PHPParser.java:9230) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.exponentExpr(PHPParser.java:8627) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.unaryExpr(PHPParser.java:8571) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.instanceofExpr(PHPParser.java:8470) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.notExpr(PHPParser.java:8412) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.productExpr(PHPParser.java:8321) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.sumExpr(PHPParser.java:8222) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.shiftExpr(PHPParser.java:8123) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.comparisonExpr(PHPParser.java:8037) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.equalityExpr(PHPParser.java:7960) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.bitwiseAndExpr(PHPParser.java:7883) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.bitwiseXorExpr(PHPParser.java:7805) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.bitwiseOrExpr(PHPParser.java:7727) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.booleanAndExpr(PHPParser.java:7649) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.booleanOrExpr(PHPParser.java:7571) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.nullCoalescingExpr(PHPParser.java:7501) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.ternaryExpr(PHPParser.java:7426) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.assignExpr(PHPParser.java:9557) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.primaryExpr(PHPParser.java:9276) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.exponentExpr(PHPParser.java:8627) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.unaryExpr(PHPParser.java:8571) ~[?:?]<br>	at com.fortify.frontend.php.parser.PHPParser.instanceofExpr(PHPParser.java:8470) ~[?:?]<br></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">這兩個問題解答如下：</p>



<h3 class="wp-block-heading">第一題：Bash 命令列上的 <code>myproject</code> 存在哪裡？</h3>



<p class="wp-block-paragraph">在 Fortify 命令列中：</p>



<pre class="wp-block-code"><code>sourceanalyzer -b myproject -clean
sourceanalyzer -b myproject ./src
sourceanalyzer -b myproject -scan -f result.fpr
</code></pre>



<p class="wp-block-paragraph"><code>-b myproject</code> 中的 <strong><code>myproject</code></strong> 稱為 <strong>Build ID（建置識別碼）</strong>。</p>



<p class="wp-block-paragraph">它<strong>不是</strong>一個真實的專案資料夾名稱，而是 Fortify 在系統內部用來記錄「翻譯（Translation）階段抽取的原始碼 AST 語法樹與中間檔」的<strong>暫存標籤</strong>。</p>



<h4 class="wp-block-heading">1. 實際檔案存在哪裡？</h4>



<p class="wp-block-paragraph">Fortify 會將 <code>myproject</code> 的中間檔案儲存在<strong>使用者家目錄下的 Fortify SCA 暫存區</strong>：</p>



<ul class="wp-block-list">
<li><strong>Linux / macOS:</strong><br><code>~/.fortify/sca&lt;版本號>/build/myproject/</code><br></li>



<li><strong>Windows (PowerShell / CMD):</strong><br><code>C:\Users\&lt;帳號>\.fortify\sca&lt;版本號>\build\myproject\</code></li>
</ul>



<h4 class="wp-block-heading">2. 注意事項</h4>



<ul class="wp-block-list">
<li>執行 <code>-clean</code> 時（如 <code>sourceanalyzer -b myproject -clean</code>），Fortify 就會<strong>刪除</strong>這個路徑下名為 <code>myproject</code> 的暫存資料夾。</li>



<li>在同一次掃描任務中，Translation（翻譯）和 Scan（分析）必須使用<strong>完全相同的 Build ID</strong>。</li>
</ul>



<h3 class="wp-block-heading">第二題：如何匯出 DeveloperWorkbook PDF 報告？</h3>



<p class="wp-block-paragraph">Fortify 提供了一個專門的命令行報表工具 <strong><code>ReportGenerator</code></strong>，可以直接讀取掃描結果檔（<code>.fpr</code>）並套用 <code>DeveloperWorkbook</code> 範本產生 PDF 報告。<sup></sup></p>



<h4 class="wp-block-heading">1. 匯出命令（Command-line）</h4>



<p class="wp-block-paragraph">請在 Terminal / Command Prompt 執行：</p>



<pre class="wp-block-code"><code>ReportGenerator -format pdf -f DeveloperWorkbook.pdf -source result.fpr -template DeveloperWorkbook.xml
</code></pre>



<h4 class="wp-block-heading">2. 參數說明</h4>



<ul class="wp-block-list">
<li><strong><code>-format pdf</code></strong>：指定輸出格式為 PDF（亦支援 <code>rtf</code>）。</li>



<li><strong><code>-f DeveloperWorkbook.pdf</code></strong>：輸出的 PDF 檔案名稱。</li>



<li><strong><code>-source result.fpr</code></strong>：你前面掃描完成所產生的 <code>.fpr</code> 結果檔。</li>



<li><strong><code>-template DeveloperWorkbook.xml</code></strong>：指定使用研發團隊常用的 <strong>Developer Workbook</strong> 報表範本。</li>
</ul>



<h4 class="wp-block-heading">3. 補充技巧 (進階)</h4>



<p class="wp-block-paragraph"><strong>如果是系統找不到 <code>DeveloperWorkbook.xml</code>：</strong>請加上完整的範本路徑，預設位在 Fortify 安裝目錄下：</p>



<pre class="wp-block-code"><code># Linux / macOS
ReportGenerator -format pdf -f DeveloperWorkbook.pdf -source result.fpr -template /opt/Fortify/Core/config/reports/DeveloperWorkbook.xml

# Windows
ReportGenerator -format pdf -f DeveloperWorkbook.pdf -source result.fpr -template "C:\Program Files\Fortify\Core\config\reports\DeveloperWorkbook.xml"</code></pre>



<p class="wp-block-paragraph"><strong>如果報告顯示的弱點數量被限制（例如預設只印前 5 個）：</strong>可以將 <code>DeveloperWorkbook.xml</code> 複製一份出來，用文字編輯器開啟，搜尋 <code>limit="5"</code> 並將其修改為 <code>limit="-1"</code>（代表不限數量，印出全部），再將修改後的 XML 作為 <code>-template</code> 帶入即可。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">翻譯（Translation）階段需要限制 memory usage?</h2>



<p class="wp-block-paragraph">答案是需要。</p>



<p class="wp-block-paragraph">雖然 Fortify 官方文件常強調在「分析（Scan）」階段給予大記憶體，但<strong>翻譯（Translation）階段如果記憶體不足，同樣會直接發生 <code>OutOfMemoryError</code> 或導致解析中斷（Crash）。</strong></p>



<p class="wp-block-paragraph">特別是你的案例中有大型 PHP 檔案（帶有巨型 Heredoc 語法），Fortify 在 Translation 階段進行語法解析（Lexing/Parsing）並建構 AST（抽象語法樹）時，就會在記憶體中吃掉大量空間。</p>



<h2 class="wp-block-heading">1. 為什麼 Translation 階段也需要限制/調大記憶體？</h2>



<ol start="1" class="wp-block-list">
<li><strong>語法分析（AST 建構）：</strong> 翻譯階段並非只是「複製檔案」，而是將原始碼編譯成 Fortify 的內部中間語言（NST/AST）。遇到複雜的 PHP 樣板、巨型 JSON/SQL 字串或第三方套件時，記憶體開銷會瞬間衝高。</li>



<li><strong>預設記憶體通常太小：</strong> 若未手動指定，<code>-b</code> 翻譯階段會使用 Fortify 的預設配額（通常僅 1GB~2GB），極易觸發 <code>java.lang.OutOfMemoryError: GC overhead limit exceeded</code>。</li>
</ol>



<h2 class="wp-block-heading">2. 如何在 Translation 階段設定記憶體？</h2>



<p class="wp-block-paragraph">語法與 Scan 階段完全相同，直接在 <code>sourceanalyzer</code> 命令列加上 <strong><code>-Xmx</code></strong> 參數：</p>



<pre class="wp-block-code"><code># 1. 先清除舊的 Build ID 暫存
sourceanalyzer -b myproject -clean

# 2. 【Translation 階段】給予足夠的記憶體（例如 16G）
sourceanalyzer -b myproject -Xmx16G ./src

# 3. 【Scan 階段】同樣給予足夠的記憶體
sourceanalyzer -b myproject -Xmx16G -scan -f result.fpr
</code></pre>



<h2 class="wp-block-heading">3. Translation 階段記憶體優化的最佳做法</h2>



<p class="wp-block-paragraph">如果給了 <code>-Xmx16G</code> 在 Translation 階段依然記憶體溢位，請搭配以下優化策略：</p>



<p class="wp-block-paragraph"><strong>主動過濾不需翻譯的目錄（最有效）：</strong>不要將非業務邏輯的套件帶入 Translation 階段：</p>



<pre class="wp-block-code"><code>sourceanalyzer -b myproject -Xmx16G \ -exclude "/vendor/" \ -exclude "/node_modules/" \ -exclude "/*.min.js" \ ./src</code></pre>



<p class="wp-block-paragraph"><strong>設定單一檔案大小上限（全域設定）：</strong>在 <code>fortify-sca.properties</code> 設定檔中加上 limit，防止 Translation 階段去解析幾十 MB 的巨型檔案：</p>



<pre class="wp-block-code"><code>com.fortify.sca.limit.file.size=10240</code></pre>



<p class="wp-block-paragraph"><strong>全域預設記憶體配置：</strong>若不想每次命令列都打 <code>-Xmx</code>，可在 <code>fortify-sca.properties</code> 中調整全域預設值，這會<strong>同時作用於 Translation 與 Scan 階段</strong>：</p>



<pre class="wp-block-code"><code>com.fortify.sca.ProjectTranslationMemoryMB=16384
com.fortify.sca.ProjectScanMemoryMB=16384</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">要匯出符合 <strong>OWASP Top 10</strong> 安全標準的 PDF 報告，選取的範本（Template）檔案說明如下：</p>



<h3 class="wp-block-heading">1. 建議選擇的 Template 檔案</h3>



<p class="wp-block-paragraph">Fortify 官方內建了不同年份版本的 OWASP Top 10 報本 XML 檔，位於 Fortify 報表目錄中：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>範本檔名 (Template File)</strong></td><td><strong>適用 OWASP 版本</strong></td><td><strong>說明 / 適用場景</strong></td></tr></thead><tbody><tr><td><strong><code>OWASP Top 10 2021.xml</code></strong></td><td><strong>OWASP Top 10 (2021)</strong></td><td><strong>【最推薦】</strong> 目前最主流的官方標準版本，建議優先選用。</td></tr><tr><td><strong><code>OWASP Top 10 2017.xml</code></strong></td><td>OWASP Top 10 (2017)</td><td>適用於客戶或公司稽核規範明確指定 2017 年版的專案。</td></tr><tr><td><strong><code>OWASP Top 10.xml</code></strong></td><td>通用/預設連結版</td><td>部分 Fortify 舊版本的預設檔名（會指向該版本的預設 OWASP）。</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">2. 命令行 (ReportGenerator) 執行指令</h3>



<p class="wp-block-paragraph">預設範本檔案存放在 Fortify 安裝目錄下的 <code>Core/config/reports/</code> 資料夾中。</p>



<h4 class="wp-block-heading"><strong>Linux / macOS:</strong></h4>



<pre class="wp-block-code"><code>ReportGenerator -format pdf \
  -f OWASP_Top10_Report.pdf \
  -source result.fpr \
  -template "/opt/Fortify/Core/config/reports/OWASP Top 10 2021.xml"
</code></pre>



<h4 class="wp-block-heading"><strong>Windows:</strong></h4>



<pre class="wp-block-code"><code>ReportGenerator -format pdf ^
  -f OWASP_Top10_Report.pdf ^
  -source result.fpr ^
  -template "C:\Program Files\Fortify\Core\config\reports\OWASP Top 10 2021.xml"
</code></pre>



<p class="wp-block-paragraph"><em>(注意：路徑中包含空白字元，必須使用雙引號 <code>"..."</code> 包裹)</em></p>



<h3 class="wp-block-heading">3. 在 Audit Workbench (GUI) 中選擇</h3>



<p class="wp-block-paragraph">若使用圖形化介面匯出：</p>



<ol start="1" class="wp-block-list">
<li>開啟 <code>.fpr</code> 結果檔。</li>



<li>點擊選單 <strong>Tools</strong> > <strong>Generate Report</strong>。</li>



<li>在 <strong>Report Template</strong> 下拉選單中選擇 <strong><code>OWASP Top 10 2021</code></strong> (或 <code>OWASP Top 10</code>)。</li>



<li>將 <strong>Format</strong> 設為 <strong>PDF</strong>，點擊 <strong>Save Report</strong> 即可。</li>
</ol>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 關鍵技巧：印出「所有」弱點細節 (解除預設 5 筆限制)</h3>



<p class="wp-block-paragraph">Fortify 官方預設的 OWASP 範本為了控制 PDF 頁數，<strong>預設每個漏洞分類只會列出前 5 筆弱點細節 (<code>limit="5"</code>)</strong>。</p>



<p class="wp-block-paragraph">若你的報告需要列出全部弱點：</p>



<ol start="1" class="wp-block-list">
<li>將 <code>OWASP Top 10 2021.xml</code> 複製到你的工作目錄，改名為 <code>OWASP_Custom.xml</code>。</li>



<li>用文字編輯器打開它，將裡面的 <strong><code>limit="5"</code></strong> 全部覆蓋替換為 <strong><code>limit="-1"</code></strong>（<code>-1</code> 代表不限制數量）。</li>



<li>執行指令時帶入 <code>-template OWASP_Custom.xml</code> 即可印出完整清單。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">command line manual:</h2>



<pre class="wp-block-code"><code>sourceanalyzer --help
OpenText SAST (Fortify) CE 25.2.0.0116
Copyright (c) 2003-2025 Open Text

Usage:

  Clean:
     sourceanalyzer.exe -b &lt;build-id> -clean
  Build:
     sourceanalyzer.exe -b &lt;build-id> &lt;sca-build-opts>
  Scan:
     sourceanalyzer.exe -b &lt;build-id> -scan &lt;sca-scan-opts>

Detailed invocation:

  Build:
     sourceanalyzer.exe -b &lt;build-id>
          &#91; &lt;sca-build-options> ]
          &lt;file-specifier>
     sourceanalyzer.exe -b &lt;build-id>
          &#91; &lt;sca-build-options> ]
          &lt;compiler> &lt;compiler-options>
     sourceanalyzer.exe -b &lt;build-id>
          &#91; &lt;sca-build-options> ]
          touchless &lt;build-tool> &#91; &lt;build-tool-options> ]
     sourceanalyzer.exe -b &lt;build-id>
          &#91; &lt;sca-build-options> ]
          devenv &lt;solution-file> /REBUILD
     sourceanalyzer.exe -b &lt;build-id>
          &#91; &lt;sca-build-options> ]
          msbuild /t:rebuild &lt;solution-or-project-file>
     sourceanalyzer.exe -b &lt;build-id>
          &#91; &lt;sca-build-options> ]
          xcodebuild -project &lt;xcodeproj-file>
     sourceanalyzer.exe -b mybuild
          -source-base-dir &lt;webapp-root> &lt;cfm-file-specifier>
  Scan:
     sourceanalyzer.exe -b &lt;build-id> -scan
          &#91; -f &lt;output-file> ]
          &#91; -scan-precision &lt;level> ]
          &#91; -rules &lt;rules.xml> &#91; -no-default-rules ] ]
          &#91; -filter &lt;filter-file> ]
  Clean:
     sourceanalyzer.exe -b &lt;build-id> -clean
  Query:
     sourceanalyzer.exe -b &lt;build-id> { -show-build-warnings | -show-files }
     sourceanalyzer.exe { -version | -show-build-ids }
     sourceanalyzer.exe { -h | -? | -help }



Options


General Options
These options are applicable to all sourceanalyzer.exe invocations.

  @&lt;file>                     Reads command line options from the specified
                              file.  Note that there is no space before the
                              file argument.

  -debug                      Causes the build step to write additional
                              troubleshooting information to the log file.
                              Use if instructed by Fortify Customer Support.
                              Also see "-logfile".

  -logfile &lt;file>             Specifies a destination for the log file.

  -verbose                    Outputs verbose messages to the console.

  -Xmx&lt;num>M                  Specifies the maximum Java heap size.
                              Default is -Xmx1800M.

  -autoheap                   Instructs SCA to set the maximum Java heap size
                              based on available physical memory. Use instead
                              of -Xmx.  Enabled by default.

  -fcontainer                 When run in a Docker container, instructs SCA
                              to detect and use only the memory allocated to
                              the container.

  -version                    Shows the sourceanalyzer.exe version.


Command Options
Note: Only one "command" option is allowed per invocation.


  -h                          Displays this help text.
  -help
  -?

  -clean                      Deletes all intermediate files and build records.
                              When a build ID is also specified with -b, only
                              files and build records related to that build ID
                              are deleted.

  -show-binaries              See the user guide.
  -show-build-tree            See the user guide.

  -show-build-ids             Lists all the Fortify build IDs (analysis models).

  -show-build-warnings        Displays all the actionable warnings that
                              occurred during the translation phase of the build
                              ID specified by "-b".

  -show-files                 Displays all the source files built into the model
                              specified by "-b".

  -show-loc                   Displays lines of code processed for files built
                              into the model specified by "-b".

  -scan                       Causes sourceanalyzer.exe to run an analysis.

  (none)                      If no command option is present, a build step
                              is assumed.


Build Options
"Build" options translate source code into a Fortify analysis model.


  -b &lt;build-id>               Specifies a unique name that identifies the
                              Fortify analysis model to be built. Also see
                              "-scan".

  -build-label &lt;label>        Specifies an optional, arbitrary string value to
                              the Fortify analysis model. Will be included in
                              the output file.

  -build-project &lt;project>    Specifies an optional, arbitrary string value to
                              the Fortify analysis model. Will be included in
                              the output file.

  -build-version &lt;version>    Specifies an optional, arbitrary string value to
                              the Fortify analysis model. Will be included in
                              the output file.

  -encoding &lt;encoding-name>   Specifies the source file encoding.
                              Default value is the platform default.

Compiler Integration Build Options
These options are used when integrating OpenText SAST (Fortify) with a compiler.

  &lt;compiler> &lt;compiler-opts>  Specifies the compiler command line. The file
                              being compiled will be added to the analysis
                              model, and the compiler will be invoked.

  touchless &lt;build-tool>      Specifies a build tool command. The build tool
    &#91; &lt;build-tool-options> ]  will be invoked, and any file being compiled
                              will be added to the analysis model.

  -nc                         When specified, the compiler is not invoked.


File Specification Build Options
These options are used to pass source files directly to OpenText SAST (Fortify).

  &lt;file-specifier>            Expression denoting a file or a group of files,
                              optionally matching a pattern:
                              file1.java - a file
                              file*.java - files matching expression
                              "path/**/*.java" - recursive expression matches.
                              Note: Always escape ** expressions in quotes.

  -exclude &lt;file-specifier>   Excludes any files matched by &lt;file-specifier>
                              from the set of files to translate


Java-specific Build Options
These options should be used in conjunction with file specification options.

  -classpath &lt;classpath>      Uses the specified classpath value for Java
  -cp &lt;classpath>             builds.

  -extdirs                    Accepts a colon or semicolon separated list
                              of directories.  Any jar files found in
                              these directories are included on the
                              classpath. Equivalent to the -extdirs option
                              to javac.

  -sourcepath                 Specifies the location of source files which will
                              not be included in the scan but will be used for
                              name resolution. Equivalent to the -sourcepath
                              option to javac.
                              The sourcepath is like classpath, except it uses
                              source files rather than class files for
                              resolution.

  -source &lt;value>             Indicates which version of the Java language the Java
  -jdk &lt;value>                code adheres to.  Valid values are 1.8, 8, 11, 17, 21.
                              Default is "11".

  -java-build-dir &lt;dir>       Used to specify one or more directories to which
                              Java sources are being compiled. May also be
                              specified at scan time.

Other Language-Specific Build Options

  -source-base-dir &lt;root>     The base directory for a ColdFusion application.

  -python-path                Add an import directory for a Python application.

  -apex                       Set ".cls" file extension to Apex language.
                              (detected based on file content by default). Equivalent to
                              -Dcom.fortify.sca.fileextensions.cls=APEX

  -apex-sobject-path          Add file to load SObject types in Apex application.

Scan Options

  -b &lt;build-id>               Specifies the build ID.  The build ID is used
                              to track which files are compiled and linked
                              as part of a build, to later scan those files.
                              This option may be specified more than once to
                              include multiple build IDs in the same scan.

  -bin &lt;binary>               All source files compiled and linked into the
                              specified binary are scanned.  Multiple binaries
                              may be specified.

  -disable-default-rule-type  See the user guide.

  -f &lt;file>                   The file to which analysis results are written.
                              Default is stdout.

  -filter &lt;file>              Specifies a filter file.  For more information,
                              see the user guide.

  -scan-policy &lt;policy>       Specifies a scan policy for vulnerability prioritization.
                              Valid values are classic, security, devops. Default is security.
                              For more information, see the user guide.

  -java-build-dir &lt;dir>       Used to specify one or more directories to which
                              Java sources have been compiled.  May also be
                              specified at build time.

  -no-default-issue-rules     See the user guide.
  -no-default-sink-rules      See the user guide.
  -no-default-source-rules    See the user guide.

  -no-default-rules           Indicates that OpenText SAST (Fortify) should not use its
                              default rules.  Must be used in conjunction with
                              "-rules"

  -rules &lt;specifier>          Specifies custom rules file or directory.  If a
                              directory is specified, all files ending in ".bin"
                              or ".xml" are included.
                              This option may be used multiple times.

  -quick                      Runs a quick scan. Quick scans complete faster at
                              the cost of reduced accuracy.

  -scan-precision &lt;level>     Configures the depth, precision and speed of the scan
  -p &lt;level>                  with configuration properties specific for the level.
                              The valid values are 1, 2, 3, and 4.

  -quiet                      Disables the command line progress bar.

  -scan                       Causes OpenText SAST (Fortify) to perform analysis against a
                              model.  The model must be specified with "-b".


Build Sessions

  -export-build-session &lt;file.mbs>

                              Store the translated model specified by -b to the
                              specified file.

  -import-build-session &lt;file.mbs>

                              Load the specified file into a build model.  If
                              the build ID of the model already exists in the
                              model registry, the import fails with the message
                              that a build already exists with that ID.


License Directives

  -store-license-pool-credentials "&lt;lim_url>|&lt;lim_pool_name>|&lt;lim_pool_pwd>|&lt;proxy_url>|&lt;proxy_user>|&lt;proxy_pwd>"

                              Stores your LIM license pool credentials to
                              allow OpenText SAST (Fortify) to use the
                              LIM for licensing. Proxy information is optional.

  -clear-license-pool-credentials

                              Removes the LIM license pool credentials from
                              the fortify-sca.properties file.

  -request-detached-lease &lt;duration>

                              Requests a detached lease from the LIM license
                              pool for exclusive use on this system for the
                              specified duration (in minutes).

  -release-detached-lease

                              Releases a detached lease back to the license
                              pool.


EXAMPLES


Build examples:
  Generic (Java, configuration, PHP, JavaScript, ASP/VBScript, VB6):
     sourceanalyzer.exe -b mybuild .
     sourceanalyzer.exe -b mybuild file1.java file2.java
     sourceanalyzer.exe -b mybuild *.bas *.cls *.frm
     sourceanalyzer.exe -b mybuild "site/**/*.php"

  ColdFusion translation:
     sourceanalyzer.exe -b mybuild -source-base-dir /www/app "/www/app/**/*.cfm"

  SQL translation:
     sourceanalyzer.exe -b mybuild -Dcom.fortify.sca.fileextensions.sql=PLSQL *.sql
     sourceanalyzer.exe -b mybuild -Dcom.fortify.sca.fileextensions.sql=TSQL *.sql

  C/C++ builds:
     sourceanalyzer.exe -b mybuild gcc -c test.c
     sourceanalyzer.exe -b mybuild CL.EXE /o HelloWorld HelloWorld.c
     sourceanalyzer.exe -b mybuild make
     sourceanalyzer.exe -b mybuild devenv myproject.msproj /REBUILD

  Objective-C/C++ builds:
     sourceanalyzer.exe -b mybuild clang -ObjC HelloWorld.m
     sourceanalyzer.exe -b mybuild xcodebuild -project myproject.xcodeproj

  .NET builds:
     sourceanalyzer.exe -b mybuild devenv myproj.sln /REBUILD
     sourceanalyzer.exe -b mybuild msbuild /t:rebuild myproj.csproj

  Java specific builds:
     sourceanalyzer.exe -b mybuild -cp lib/dependency.jar "src/**/*.java"
     sourceanalyzer.exe -b mybuild -cp mytaglibs.jar webapp/*.jsp
     sourceanalyzer.exe -b mybuild touchless ant

  J2EE specific builds:
     sourceanalyzer.exe -b mybuild -cp "app/WEB-INF/lib/*.jar" app/*.jsp

Scan step:
     sourceanalyzer.exe -b mybuild -scan -f results.fpr


See OpenText(TM) Static Application Security Testing (Fortify) User Guide for a complete explanation of each option.


You can open a support case for Fortify products online using
our customer support system. This streamlined procedure is designed to
provide easier access and improved customer satisfaction.

Access your account at https://softwaresupport.softwaregrp.com/.</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">這代表即使給了 32G 記憶體，ANTLR 在解析 PHP 語法時仍陷入了死迴圈或極度消耗記憶體的狀態（問題依然卡在 <code>PHPParser.hereDocPart</code> / <code>PHPLexerBase</code>）。</p>



<p class="wp-block-paragraph">通常這有兩個主因：</p>



<ol start="1" class="wp-block-list">
<li><strong>Windows 通配符問題</strong>：<code>-exclude "/vendor/"</code> 在 CMD/PowerShell 根本沒有生效，Fortify 依然把 <code>vendor</code> 進去了。</li>



<li><strong>存在「超大檔」或「極長 Heredoc 檔」</strong>：例如大型 SQL dump、Base64 圖片檔、自動生成的測試資料或超大樣板檔，導致 ANTLR Parser 記憶體暴增。</li>
</ol>



<p class="wp-block-paragraph">請依序透過以下方法徹底解決：</p>



<h3 class="wp-block-heading">1. 修正 Windows 的 <code>-exclude</code> 語法（最常踩坑）</h3>



<p class="wp-block-paragraph">在 Windows 命令列，<code>-exclude</code><strong>必須使用雙引號，且建議使用 <code>**</code> (Ant 語法) 與 Windows 反斜線</strong>，否則過濾會無效：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_project -clean

sourceanalyzer -b portal_project -Xmx32G -exclude "**/vendor/**" -exclude "**/node_modules/**" -exclude "**/*.min.js" backend/
</code></pre>



<h3 class="wp-block-heading">2. 開啟弱點檔大小限制 (Limit File Size)</h3>



<p class="wp-block-paragraph">這是解決 <code>hereDoc</code> 爆記憶體最有效的武器。直接限制 Fortify 跳過單檔超過指定大小的檔案（例如 1MB 或 2MB）：</p>



<h4 class="wp-block-heading">做法 A：命令列加上 <code>-D</code> 參數</h4>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_project -Xmx32G -Dcom.fortify.sca.limit.file.size=2048 -exclude "**/vendor/**" backend/
</code></pre>



<p class="wp-block-paragraph"><em>( <code>2048</code> 代表超過 2MB 的原始碼檔案就跳過不安裝/解析，可有效避免吃到包含巨大字串或 Heredoc 的 PHP 檔案)</em></p>



<h4 class="wp-block-heading">做法 B：修改 <code>fortify-sca.properties</code></h4>



<p class="wp-block-paragraph">在 <code>C:\Users\&lt;你的帳號&gt;\.fortify\fortify-sca.properties</code> 或 Fortify 安裝目錄下的 <code>Core\config\fortify-sca.properties</code> 加入：</p>



<p class="wp-block-paragraph">Properties</p>



<pre class="wp-block-code"><code>com.fortify.sca.limit.file.size=2048
</code></pre>



<h3 class="wp-block-heading">3. 找出到底是哪個「元凶檔案」卡住，單獨排除它</h3>



<p class="wp-block-paragraph">如果是業務邏輯資料夾內有一個幾十 MB 的 PHP 檔案（例如包含超大字串陣列或字典）：</p>



<p class="wp-block-paragraph">請加上 <strong><code>-debug</code></strong> 或 <strong><code>-verbose</code></strong> 參數重新執行一次，並觀察 log 印到哪一個 <code>.php</code> 檔案時卡死：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_project -Xmx32G -verbose backend/ &gt; scan_log.txt
</code></pre>



<p class="wp-block-paragraph">打開 <code>scan_log.txt</code> 翻到最底端，查看崩潰前最後進行解析（Parsing/Lexing）的是哪一個 <code>.php</code> 檔案，接著直接將該檔案加入排除：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_project -Xmx32G -exclude "**/ProblematicFile.php" backend/
</code></pre>



<h3 class="wp-block-heading">4. （終極方案）調整 ANTLR / PHP 語法分析設定</h3>



<p class="wp-block-paragraph">如果特定 PHP 檔案無法排除，但包含極長的 Heredoc，可以在 <code>fortify-sca.properties</code> 加入以下參數，限制語法樹深度或停用部分 ANTLR 耗效能的檢查：</p>



<pre class="wp-block-code"><code># 限制 Fortify 對單一檔案最大解析時間（單位：秒，預設無限制，可設為 300 秒）
com.fortify.sca.PhaseTimeout=300

# 關閉部分過度深度的 AST 解析
com.fortify.sca.DisablePHP2MethodCallDataflow=true
</code></pre>



<h3 class="wp-block-heading">建議嘗試步驟：</h3>



<ol start="1" class="wp-block-list">
<li>先執行 <code>sourceanalyzer -b portal_project -clean</code> 清除快取。</li>



<li>加上限制檔案大小參數 <code>-Dcom.fortify.sca.limit.file.size=2048</code> 並修正過濾語法 <code>-exclude "**/vendor/**"</code> 重新執行。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">從你提供的完整 log 可以精準定位出問題的根源所在：</p>



<p class="wp-block-paragraph">Log 在處理到 <code>SendForgotPasswordTotpApi.php</code>（或緊隨其後的下一個 PHP 檔）時，Fortify 的 <strong>PHP 語法解析器 (PHPFrontEnd)</strong> 因為遇到極長或結構極度複雜的 Heredoc / String，導致 ANTLR 語法分析器陷入計算迴圈並爆記憶體崩潰，進而拋出 <code>NullPointerException</code>。</p>



<p class="wp-block-paragraph">請採用以下 <strong>4 步驟解決方案</strong> 來繞過這個解析器的死區：</p>



<h3 class="wp-block-heading">步驟 1：強制限制單檔大小 (File Size Limit)</h3>



<p class="wp-block-paragraph">這是最快且最有效的方法。加載此參數能直接讓 Fortify <strong>跳過過大或包含巨大字串 (Heredoc/Blob) 的單一原始碼檔案</strong>，避免 ANTLR 解析器卡死。</p>



<p class="wp-block-paragraph">在 command 加入 <code>-Dcom.fortify.sca.limit.file.size=2048</code>（單位為 KB，即 2MB）：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_backend -clean

sourceanalyzer -b portal_backend -Xmx32G -Dcom.fortify.sca.limit.file.size=2048 backend/
</code></pre>



<h3 class="wp-block-heading">步驟 2：明確排除 <code>TestTools</code> 或測試/資料庫備份檔案</h3>



<p class="wp-block-paragraph">從 Log 中可以看到你的 <code>backend/</code> 下包含了 <code>TestTools</code> 與 <code>db/migrations</code>。通常測試工具與 Migration 檔中會含有大量假資料、巨型 SQL 字串或 Heredoc 模板。</p>



<p class="wp-block-paragraph">請使用 <strong>Windows 專用 Ant 通配符</strong> 將它們排除：</p>



<p class="wp-block-paragraph">DOS</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_backend -clean

sourceanalyzer -b portal_backend -Xmx32G -Dcom.fortify.sca.limit.file.size=2048 -exclude "**/TestTools/**" -exclude "**/db/**" -exclude "**/vendor/**" backend/
</code></pre>



<h3 class="wp-block-heading">步驟 3：定位「致命檔案」並直接單獨排除</h3>



<p class="wp-block-paragraph">如果前兩步執行後依然在 <code>SendForgotPasswordTotpApi.php</code> 附近崩潰，代表問題出在業務邏輯程式碼本身的某個特定檔案。</p>



<ol start="1" class="wp-block-list">
<li>打開 <code>SendForgotPasswordTotpApi.php</code> 以及與它在同一目錄下的前後檔案。</li>



<li>檢查程式碼中是否有使用 <code>&lt;&lt;&lt;EOD</code> &#8220;<strong>/SendForgotPasswordTotpApi.php&#8221; &#8220;</strong>/TestTools/<strong>&#8221; &#8220;</strong>/db/<strong>&#8221; &#8220;</strong>/vendor/&#8221; # ### (2048 (5分鐘)，防止 <strong>Heredoc &#8212; -Dcom.fortify.sca.limit.file.size=&#8221;2048&#8243; -Xmx32G -b -clean -exclude -f -scan / 2MB 3. 300 4：寫入 ANTLR Fortify HTML KB) Nowdoc PHP Phase Timeout，讓 <code>-exclude</code> <code>&lt;&lt;&lt;HTML</code> <code>C:\Users\max\.fortify\fortify-sca.properties</code> cmd &#8220;`properties <code>fortify-sca.properties</code> backend/ com.fortify.sca.PhaseTimeout=&#8221;300&#8243; com.fortify.sca.limit.file.size=&#8221;2048&#8243; portal_backend portal_backend_result.fpr sourceanalyzer 之類的 在檔案末端寫入以下設定： 完成上述設定後，執行以下完整的標準清洗與翻譯流程： 將它單獨排除： 建議的標準執行指令 或 找到該檔案後，直接使用 檔案也發生 步驟 無限迴圈 秒 若要避免以後其他 解析單一檔案逾時自動跳過，而不是直接崩潰： 設定全域超時（終極防護） 設定單一階段解析超時為 語法</strong>，且裡面放了極長的 語法卡死，直接開啟 請開啟以下檔案（若沒有請自行建立）： 資料或密碼學金鑰/雜湊值。 郵件模板、JSON 限制單一檔案解析上限></li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">這個檔案 (<code>SendForgotPasswordTotpApi.php</code>) <strong>正是導致 Fortify ANTLR 語法分析器崩潰爆記憶體的元凶</strong>。</p>



<h3 class="wp-block-heading">為什麼是這個檔案？</h3>



<ol start="1" class="wp-block-list">
<li><strong>使用了巨大的 <code>Heredoc</code> (<code>$htmlBody = &lt;&lt;&lt;EOD ### #### $htmlBody="str_replace('{$totpCode}'," $htmlTemplate="file_get_contents($templatePath);" $htmlTemplate); $templatePath="__DIR__" $totpCode, '/../../templates/emails/forgot_password_totp.html'; (Inline) (圖案樣式)、HTML **ANTLR **修改後的 --- . ... / // 2. 25.2 &lt;&lt;&lt;EOD A：重構此 Base64 CSS、內嵌 Data EOD; EOD;</code>)</strong>： Fortify GC HTML Heredoc PHP SVG URI <code>$htmlBody</code><code>$textBody</code><code>.blade.php</code><code>.html</code><code>Heredoc</code><code>PHPLexerBase</code><code>PHPParser.hereDocPart</code><code>\</code>、雙引號、SVG php <code>java.lang.OutOfMemoryError: exceeded</code>。 limit overhead 使用的 使用簡單的字串替換填入動態變數 在處理包含複雜字元（如反斜線 字串。 將 將原本巨大的 抽離至獨立的 方案 時，內部的詞法狀態轉換會陷入計算遞迴，進而導致 替換為讀取外部模板檔案： 標籤的巨型 模板抽離，這同時也能讓你的程式碼結構更乾淨。 模板檔案中，改用檔案讀取方式載入： 檔案（最推薦，一勞永逸） 的 的內聯 程式碼： 與 解析器缺陷： 解決方案：重構此檔案（推薦）或針對性排除 語法）的長 這個檔案裡包含了一整段包含 這有兩種解決方式，<strong>最根本的方法是重構程式碼</strong>，將> <strong>優勢</strong>：抽離後 Fortify 解析這個 <code>.php</code> 時就不會觸發 Heredoc 的 ANTLR 解析 Bug，掃描能順利通過，且不會遺漏該 API 的安全弱點分析！</li>
</ol>



<h4 class="wp-block-heading">方案 B：單獨排除此檔案（最快）</h4>



<p class="wp-block-paragraph">如果你目前不想改動原始碼，可以在執行命令時，直接將該檔案單獨排除掉：</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_backend -clean

sourceanalyzer -b portal_backend -Xmx16G -Dcom.fortify.sca.limit.file.size=2048 -exclude "**/SendForgotPasswordTotpApi.php" -exclude "**/TestTools/**" -exclude "**/db/**" -exclude "**/vendor/**" backend/
</code></pre>



<h4 class="wp-block-heading">方案 C：將檔案大小限制調低</h4>



<p class="wp-block-paragraph">因為這個 <code>.php</code> 檔案因為包含巨型 HTML，檔案大小可能較大。你可以嘗試將檔案限制從 <code>2048</code> (2MB) 調小到 <code>100</code> (100KB)：</p>



<pre class="wp-block-code"><code>sourceanalyzer -b portal_backend -clean

sourceanalyzer -b portal_backend -Xmx16G -Dcom.fortify.sca.limit.file.size=100 -exclude "**/vendor/**" backend/
</code></pre>



<p class="wp-block-paragraph"><em>(這樣 Fortify 會自動跳過此類包含巨型模板的過大 PHP 檔)</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/08/fortify-gc-overhead-limit-exceeded/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>搞定 Ubuntu 管理員大小事：清單、新增與刪除帳號</title>
		<link>https://stackoverflow.max-everyday.com/2026/08/ubuntu-admin-mangage/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/08/ubuntu-admin-mangage/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 11 Aug 2026 01:59:43 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8656</guid>

					<description><![CDATA[想在 Ubuntu 系統裡當個有實權的大老，或是...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/ubuntu-admin-mangage3_clean-1024x572.jpg" alt="" class="wp-image-8661" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/ubuntu-admin-mangage3_clean-1024x572.jpg?v=1786413620 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/ubuntu-admin-mangage3_clean-600x335.jpg?v=1786413620 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/ubuntu-admin-mangage3_clean-768x429.jpg?v=1786413620 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/ubuntu-admin-mangage3_clean.jpg?v=1786413620 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">想在 Ubuntu 系統裡當個有實權的大老，或是想把不安份的帳號給踢出去嗎？這篇教學帶你輕鬆掌握管理者權限的召喚與封印術！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">一、 尋找幕後黑手：查看誰有最高權限</h3>



<p class="wp-block-paragraph">想知道系統裡面到底有誰偷偷開了外掛、擁有 sudo 權限嗎？直接輸入下面這行指令，就能讓所有管理員現形：</p>



<pre class="wp-block-code"><code>getent group sudo | cut -d: -f4</code></pre>



<p class="wp-block-paragraph">覺得還不夠清楚嗎？這裡還有兩招進階招術：</p>



<p class="wp-block-paragraph">把系統的最高神明 root 一併召喚出來：</p>



<pre class="wp-block-code"><code>echo "root"; getent group sudo | cut -d: -f4 | tr ',' '\n'</code></pre>



<p class="wp-block-paragraph">連他們的登入 Shell 一起抓出來看個仔細：</p>



<pre class="wp-block-code"><code>grep -E "$(getent group sudo | cut -d: -f4 | tr ',' '|')" /etc/passwd</code></pre>



<p class="wp-block-paragraph">小知識補充： Ubuntu 預設是用 sudo 群組來發放管理者權限，而不是單獨叫 admin 群組！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">二、 召喚新夥伴：新增帳號並給予管理權限</h3>



<p class="wp-block-paragraph">想給好朋友開個權限嗎？標準流程是先建帳號，再把他拉進 sudo 俱樂部：</p>



<pre class="wp-block-code"><code>sudo adduser username
sudo usermod -aG sudo username</code></pre>



<p class="wp-block-paragraph">別忘了把 username 換成你朋友的帳號名稱！</p>



<p class="wp-block-paragraph">如果你趕時間，想要一鍵搞定，可以使用這套組合拳：</p>



<pre class="wp-block-code"><code>sudo useradd -m -s /bin/bash -G sudo username
sudo passwd username</code></pre>



<p class="wp-block-paragraph">驗證身分：</p>



<p class="wp-block-paragraph">想確認他有沒有成功拿到權限，可以執行這行命令檢查：</p>



<pre class="wp-block-code"><code>groups username</code></pre>



<p class="wp-block-paragraph">只要看到輸出裡面出現 sudo 這幾個字，就代表他已經順利晉升為管理員啦！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">三、 提防豬隊友：刪除使用者帳號</h3>



<p class="wp-block-paragraph">當某些帳號不再需要，或是對方惹你生氣時，你可以根據想留下的東西選一種方式處理：</p>



<ol start="1" class="wp-block-list">
<li>純粹把人踢掉（保留他的家目錄與檔案）：</li>
</ol>



<pre class="wp-block-code"><code>sudo deluser username</code></pre>



<p class="wp-block-paragraph">或者使用 sudo userdel username 。</p>



<ol start="2" class="wp-block-list">
<li>連同他的家目錄與郵件資料一併蒸發（乾淨俐落）：</li>
</ol>



<pre class="wp-block-code"><code>sudo deluser --remove-home username</code></pre>



<p class="wp-block-paragraph">或者使用 sudo userdel -r username 。</p>



<ol start="3" class="wp-block-list">
<li>地毯式搜捕（刪除他在整個系統留下的所有檔案）：</li>
</ol>



<pre class="wp-block-code"><code>sudo deluser --remove-all-files username</code></pre>



<p class="wp-block-paragraph">動手前請注意：</p>



<p class="wp-block-paragraph">刪除前請先確定 username 已經替換成目標帳號。</p>



<p class="wp-block-paragraph">如果對方還在線上頑強抵抗，請先強制幫他登出：</p>



<pre class="wp-block-code"><code>sudo killall -u username</code></pre>



<p class="wp-block-paragraph">如果只是想拔掉他的權限，讓他變回普通百姓，不需要直接刪帳號，輸入這行命令即可：</p>



<pre class="wp-block-code"><code>sudo deluser username sudo</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">新增的隊友，需要 ip 進白名單</h2>



<p class="wp-block-paragraph">新增的隊友，需要 IP 進白名單。</p>



<p class="wp-block-paragraph">要將 IP 位址 192.168.1.100 加入 iptables 的白名單，指令會根據您希望開放的服務種類而有所不同。</p>



<p class="wp-block-paragraph">從您提供的清單來看，您之前針對特定 IP 開放了 SSH 替代連接埠（port 8822）以及 SNMP（port 161）。以下提供兩種最常見的新增方式：</p>



<p class="wp-block-paragraph">如果您的目標是允許該 IP 存取 SSH（port 8822），請執行：</p>



<pre class="wp-block-code"><code>sudo iptables -A INPUT -i ens160 -p tcp -s 192.168.1.100 --dport 8822 -j ACCEPT</code></pre>



<p class="wp-block-paragraph">如果希望該 IP 可以存取此主機的所有服務與連接埠，請執行：</p>



<pre class="wp-block-code"><code>sudo iptables -A INPUT -i ens160 -s 192.168.1.100 -j ACCEPT</code></pre>



<p class="wp-block-paragraph">設定完成後，可以驗證新規則是否順利加入：</p>



<pre class="wp-block-code"><code>sudo iptables -L -n -v</code></pre>



<p class="wp-block-paragraph">請注意，直接使用 iptables 指令新增的規則在系統重啟後會失效。建議執行下列指令將目前的防火牆規則永久保存：</p>



<pre class="wp-block-code"><code>sudo netfilter-persistent save</code></pre>



<p class="wp-block-paragraph">或是：</p>



<pre class="wp-block-code"><code>sudo iptables-save | sudo tee /etc/iptables/rules.v4</code></pre>



<p class="wp-block-paragraph">驗證與存檔完成後，新採納的 IP 就能順利通過防火牆。請問這次新增的 IP 是要開放特定的通訊埠，還是要開啟全部的存取權限呢？</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/08/ubuntu-admin-mangage/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Windows Server 密碼最長使用期限</title>
		<link>https://stackoverflow.max-everyday.com/2026/08/windows-server-maxpwage/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/08/windows-server-maxpwage/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 05:34:05 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<category><![CDATA[Windows]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8652</guid>

					<description><![CDATA[資安規範與稽核標準建議將 「密碼最長使用期限」 ...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="638" height="240" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/image-1.png?v=1786340016" alt="" class="wp-image-8654" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/image-1.png?v=1786340016 638w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/08/image-1-600x226.png?v=1786340016 600w" sizes="auto, (max-width: 638px) 100vw, 638px" /></figure>



<p class="wp-block-paragraph">資安規範與稽核標準建議將 <strong>「密碼最長使用期限」</strong> 設定為 <strong><code>90</code> 天</strong>（約 3 個月換一次密碼）。</p>



<p class="wp-block-paragraph">在《行政院資通安全防護基準》及 ISO 27001 實務中，一般機關系統的密碼更換週期標準為 <strong>90 天～180 天</strong>（也就是 3 到 6 個月）。目前你的系統設定是 <code>120</code> 天（約 4 個月），雖然也符合規範，但修改為 <strong><code>90</code> 天</strong> 是各大稽核單位最喜愛的標準黃金值。</p>



<h3 class="wp-block-heading">如何在 PowerShell (管理員) 修改？</h3>



<p class="wp-block-paragraph">請開啟 <strong>PowerShell (以系統管理員身分執行)</strong>，執行以下指令：</p>



<h4 class="wp-block-heading">1. 設定為 90 天（推薦）：</h4>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code>net accounts /MAXPWAGE:90
</code></pre>



<h4 class="wp-block-heading">2. 驗證修改結果：</h4>



<p class="wp-block-paragraph">執行以下指令確認是否生效：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code>net accounts
</code></pre>



<p class="wp-block-paragraph">觀察輸出中的這行：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>密碼最長使用期限 (天): 90</strong></p>
</blockquote>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 補充說明（完整密碼原則組合建議）</h3>



<p class="wp-block-paragraph">修改完成後，你的 Windows Server 2019 密碼原則組合將會非常完美且完全符合資安稽核：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>項目</strong></td><td><strong>你的設定值</strong></td><td><strong>稽核標準與備註</strong></td></tr></thead><tbody><tr><td><strong>密碼長度下限</strong></td><td><strong>13</strong> 字元</td><td>符合 $\ge 12$ 或 $13$ 字元標準</td></tr><tr><td><strong>密碼最長使用期限</strong></td><td><strong>90</strong> 天</td><td>符合 90~180 天標準，每 3 個月更換</td></tr><tr><td><strong>歷程記錄長度</strong></td><td><strong>3</strong> 次</td><td>符合「不可與前 3 次相同」</td></tr><tr><td><strong>帳戶鎖定閾值</strong></td><td><strong>5</strong> 次</td><td>失敗 5 次即鎖定</td></tr><tr><td><strong>鎖定持續期間</strong></td><td><strong>15</strong> 分鐘</td><td>鎖定 15 分鐘（防暴力破解）</td></tr></tbody></table></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/08/windows-server-maxpwage/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
