From 5876154dbb7fc3c63a151b5d8d487095c2846833 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 13 May 2026 01:24:51 +0000 Subject: [PATCH] refactor: optimize AI payload by reducing token usage and streamline findings structure --- README.md | 1 + TODO.md | 5 +++++ app/findings.js | 31 +++++++++++++++---------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fa3047c..476859d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼 8. 階段五完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1 +9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量 # 使用說明 diff --git a/TODO.md b/TODO.md index 01b8e73..339124b 100644 --- a/TODO.md +++ b/TODO.md @@ -49,3 +49,8 @@ - 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 - 未驗收 + +## 階段十一:壓縮 AI 傳入內容減少 token 用量 +- 目標:傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion);system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion;AI 回傳後補回原始完整欄位(含 is_new)。 +- 驗收:AI 呼叫的 payload 不含 is_new 等內部欄位,去重與誤報過濾後的 findings 仍保有完整欄位供後續流程使用。 +- 未驗收 diff --git a/app/findings.js b/app/findings.js index 777cbe6..d045941 100644 --- a/app/findings.js +++ b/app/findings.js @@ -76,22 +76,26 @@ function fallback(label, findings, e) { return findings; } +/** 只保留 AI 需要的欄位,減少 token 用量 */ +function toAIPayload(findings) { + return findings.map(({ level, role, location, suggestion }) => ({ level, role, location, suggestion })); +} + /** * 呼叫 LLM 進行語意去重,失敗時降級回傳原始 findings */ export async function deduplicateWithAI(findings) { if (findings.length === 0) return findings; - const systemPrompt = `你是一位程式碼審查問題去重專家。 -給你一份問題清單(JSON 陣列),請移除語意重複的問題(即使描述文字不同,但指的是同一個問題)。 -保留等級較高的版本,優先保留 critical > warning > info。 -只回傳去重後的 JSON 陣列,不要有其他文字。`; + const systemPrompt = `移除語意重複的程式碼審查問題(JSON 陣列)。保留等級較高者(critical > warning > info)。只回傳去重後的 JSON 陣列。`; try { - const result = await chatJSON(systemPrompt, `以下是問題清單,請去除語意重複的項目:\n\n${JSON.stringify(findings, null, 2)}`); + const result = await chatJSON(systemPrompt, JSON.stringify(toAIPayload(findings))); if (Array.isArray(result) && result.length > 0) { console.log(` AI 去重: ${findings.length} -> ${result.length} 筆`); - return result; + // 以 location+suggestion 為 key,將原始 findings 的完整欄位(含 is_new)補回 + const origMap = new Map(findings.map(f => [`${f.location}|${String(f.suggestion).slice(0, 50)}`, f])); + return result.map(r => origMap.get(`${r.location}|${String(r.suggestion).slice(0, 50)}`) ?? r); } throw new Error('AI 回傳空陣列'); } catch (e) { @@ -131,22 +135,17 @@ export async function filterFalsePositivesWithAI(findings, exclusions = []) { if (findings.length === 0) return findings; const exclusionHint = exclusions.length > 0 - ? `\n\n以下是已知的誤報或不需處理的問題清單(供參考,相同檔案路徑且語意相近的問題應一併排除):\n${JSON.stringify(exclusions, null, 2)}` + ? `\n已知誤報(相同路徑且語意相近者一併排除):\n${JSON.stringify(exclusions.map(({ location, suggestion }) => ({ location, suggestion })))}` : ''; - const systemPrompt = `你是一位資深程式碼審查專家,負責判斷審查問題是否為誤報或不需處理。 -給你一份問題清單(JSON 陣列),每筆包含 level、role、location、suggestion。 -請移除以下類型的問題: -1. 誤報:問題描述與實際程式碼不符(例如:程式碼已正確使用環境變數或 secrets,卻被標記為硬編碼敏感資料) -2. 不適用:問題在此專案情境下不需處理(例如:CI/CD action 本來就需要透過環境變數傳遞 token) -3. 與已知誤報清單語意相近的問題(檔案路徑相同且建議內容相似) -只回傳需要保留的問題 JSON 陣列,不要有其他文字。${exclusionHint}`; + const systemPrompt = `判斷以下程式碼審查問題是否為誤報或不適用(如已正確使用 secrets、CI/CD 必要權限等),移除後只回傳需保留的 JSON 陣列。${exclusionHint}`; try { - const result = await chatJSON(systemPrompt, `請判斷以下問題清單,移除誤報或不需處理的問題:\n\n${JSON.stringify(findings, null, 2)}`); + const result = await chatJSON(systemPrompt, JSON.stringify(toAIPayload(findings))); if (Array.isArray(result) && result.length > 0) { console.log(` AI 誤報過濾: ${findings.length} -> ${result.length} 筆`); - return result; + const origMap = new Map(findings.map(f => [`${f.location}|${String(f.suggestion).slice(0, 50)}`, f])); + return result.map(r => origMap.get(`${r.location}|${String(r.suggestion).slice(0, 50)}`) ?? r); } throw new Error('AI 回傳空陣列或非陣列'); } catch (e) {