CareerWise 審查代理升級:讓 AI 自己檢查自己的回答

AI 回答看起來頭頭是道,但仔細看可能會發現怪怪的——這就是常見的 AI 幻覺問題。讓同一個模型同時負責「生成」和「驗證」本來就很矛盾,就像寫考卷的人同時兼任閱卷老師,很難發現自己的盲點。解法是把這兩個角色拆開。

CareerWise 是一個職涯諮詢 AI 服務,使用者在上面詢問面試準備、履歷健檢、轉職規劃等問題,由 Summer 代理扮演職涯顧問來回答。這篇文章來探討 CareerWise 如何實作評論家與審查者模式,以及升級過程學到的事。

什麼是評論家與審查者模式?

評論家與審查者(Critic & Reviewer) 的核心是把「生成」和「驗證」兩個功能拆開,而不是讓同一個模型同時生產又同時檢查。

   [生成代理] ──→ 產出草稿 ──→ [審查代理]
        ↑                          │
        │                   帶著檢查清單:
        │                   • 事實正確性
        │                   • 資料來源
        │                   • 語氣一致性
        │                   • 安全性
        │                          │
        └──── 打回票 + 修改建議 ←──┘

生成代理負責大膽創作,審查代理帶著嚴格的評分標準進行審查。如果發現問題,審查代理不會幫忙改,而是附上具體建議打回重做,透過來回迭代確保最終輸出的品質。

reflect.tssrc/lib/chains/reflect.ts)是 CareerWise 對評論家與審查者模式的實作——但原本是簡化版。CareerWise 裡的「Summer 代理」和「審查代理」分別是兩次不同的 LLM 呼叫,各自有獨立的 system prompt、獨立的 temperature、獨立的 max_tokens:

// Summer 代理:一次 LLM 呼叫,被設定為 Summer 角色
const summerResponse = await groq.chat.completions.create({
  messages: [{ role: "system", content: "你是 Summer — 職涯顧問..." }],
  temperature: 0.7,
})

// 審查代理:另一次 LLM 呼叫,被設定為審查員角色
const reviewResult = await groq.chat.completions.create({
  messages: [{ role: "system", content: "你是一個回答審查員..." }],
  temperature: 0.1,
})
  Summer 代理 審查代理
本質 一次 LLM 呼叫 另一次 LLM 呼叫
角色 被設定為 Summer(職涯顧問) 被設定為審查員(品質檢查)
system prompt 「你是 Summer — 一位擁有豐富經驗的…」 「你是一個回答審查員…」
temperature 0.7(較有創造力) 0.1(較確定性)
職責 產出回答 挑錯、打回票

「Summer」只是該次 LLM 呼叫被賦予的角色名稱(persona),並非一個獨立運行的代理實體。多代理協作指的是這兩次不同的 LLM 呼叫(各自扮演不同角色)之間的迭代協作。

原本的 reflect.ts 在 Summer 代理產出回答後,用審查代理檢查回答品質,發現問題就直接修正:

Summer 產出回答(第一次 LLM 呼叫)
  │
  ▼
reflect.ts:檢查回答(第二次 LLM 呼叫)
  檢查清單:
  ① 事實錯誤或幻覺
  ② 建議是否具體可行
  ③ 語氣是否恰當
  ④ 是否針對使用者具體情況回答
  │
  ├─ OK → 保留原回答
  └─ 有問題 → 輸出修正版本取代原回答

這樣做的問題是生成與驗證混在一起,審查代理幫 Summer 代理改答案,而不是讓 Summer 代理自己根據 feedback 改善。

reflect.ts 原本在三個路徑被呼叫:

路徑 位置 觸發條件
career chain(medium tier) career-chain.ts:161 每次完成 3-step chain 後
resume chain resume-chain.ts:160 每次完成 3-step chain 後
career simple(getReply chat.ts:89,94 非串流 career 路徑(LINE Bot)

串流路徑(getReplyStream)和 planned / complex 路徑不經過 reflect.ts

升級後變成這樣

這次升級的目標很明確:審查代理只負責挑錯不打修改,把問題打回票讓 Summer 代理自己重寫。

型別定義

先定義審查結果的資料結構:

type ReviewCategory =
  | "factual_error"   // 事實錯誤或幻覺
  | "vague_advice"    // 建議空泛不具體
  | "tone"            // 語氣不恰當
  | "not_specific"    // 未針對使用者具體情況
  | "ungrounded"      // 回答無資料來源依據

type ReviewIssue = {
  category: ReviewCategory
  quote: string         // 問題所在的原文片段
  description: string   // 問題描述
  suggestion: string    // 建議修改方向
}

type ReviewResult = {
  passed: boolean
  issues: ReviewIssue[]
}

流程

                   [Summer 代理]                   [Review 代理]
                        │                              │
   產出回答 ─────────────────────────────────────────→  │
                        │                              │
                        │               檢查 5 項清單:
                        │               ① factual_error
                        │               ② vague_advice
                        │               ③ tone
                        │               ④ not_specific
                        │               ⑤ ungrounded
                        │                              │
                        │◀─── { passed: false,         │
                        │       issues: [...] } ────── │
                        │                              │
   根據 issues 重寫 ──→  │                              │
                        │                              │
   產出修正版 ──────────→                              │
                        │                              │
                        │◀─── { passed: true } ──────  │
                        │                              │
   輸出最終回答

迭代最多 3 次,若 3 次後仍未通過,以最後一次結果輸出(最佳努力)。

審查代理 Prompt

const REVIEW_PROMPT = `你是一個回答審查員。檢查以下職涯顧問的回答。

檢查項目:
1. 事實錯誤或幻覺:是否有與事實或常識矛盾的地方
2. 建議具體性:建議是否具體可行,而非空泛鼓勵
3. 語氣恰當性:語氣是否直接但不失溫暖
4. 針對性:是否根據使用者的具體情況回答
5. 資料來源:回答是否有所依據,而非憑空生成

回傳嚴格符合 JSON schema 的結果,不要有任何額外文字:
{
  "passed": true/false,
  "issues": [
    {
      "category": "factual_error" | "vague_advice" | "tone" | "not_specific" | "ungrounded",
      "quote": "原文片段",
      "description": "問題描述",
      "suggestion": "修改建議"
    }
  ]
}

如果全部通過,passed 為 true,issues 為空陣列。`

重寫代理 Prompt

const REWRITE_PROMPT = `你是 Summer — 一位擁有豐富經驗的前端工程師與職涯顧問。

你的回答被審查員指出以下問題,請根據建議修改:

{issues}

原始回答:
{originalResponse}

請輸出修正後的完整回答,不要加任何說明。`

迭代邏輯

const MAX_ITERATIONS = 3

export async function reflectResponse(response: string, userMessage: string): Promise<string> {
  let current = response

  for (let i = 0; i < MAX_ITERATIONS; i++) {
    const review = await reviewAgent(current, userMessage)
    if (review.passed) return current

    console.log(`[Review] Iteration ${i + 1}: ${review.issues.length} issues found`)
    current = await rewriteAgent(current, userMessage, review.issues)
  }

  return current
}

測試結果

Phase 1:審查代理

測試輸入:怎麼談薪水

原始回答:談薪水要注意幾個點:第一,要了解市場行情。第二,要準備好你的成就。第三,要保持積極的態度。

審查代理成功找出 2 個問題:

{
  "passed": false,
  "issues": [
    {
      "category": "vague_advice",
      "quote": "要準備好你的成就",
      "description": "這個建議太過模糊,沒有提供具體的方法或技巧",
      "suggestion": "例如,你可以準備好你的工作成績、業績表或其他相關證據"
    },
    {
      "category": "vague_advice",
      "quote": "要保持積極的態度",
      "description": "這個建議太過空泛,沒有提供具體的方法或技巧",
      "suggestion": "例如,你可以練習如何有效地表達你的需求、或如何處理壓力"
    }
  ]
}

Phase 2:重寫代理

Summer 代理根據審查建議重寫,原本空泛的描述變為具體可執行的方法:

重寫後:
談薪水要注意幾個點:第一,要了解市場行情。了解市場行情可以幫助你
知道自己在薪水方面的實力和競爭力。第二,準備好你的成就。例如,
你可以準備好你的工作成績、業績表或其他相關證據,來證明你的價值
和貢獻。第三,練習如何有效地表達你的需求和期望。

Before: 「要準備好你的成就」→ 空泛
After:  「準備好你的工作成績、業績表或其他相關證據」→ 具體 ✅

Before: 「要保持積極的態度」→ 空泛
After:  「練習如何有效地表達你的需求和期望」→ 具體 ✅
項目 結果
審查代理正確找出問題
結構化 JSON 格式正確
建議具體、可執行
重寫代理根據 feedback 產出修正版
修正後回答比原始回答更具體

迭代邏輯的調整

CareerWise 審查代理升級:讓 AI 自己檢查自己的回答

第一次上線測試「前端工程師面試大概會問什麼」,跑了 3 次 iteration(6 次 LLM 呼叫),回應時間 41 秒:

Server log:
[Review] Iteration 1: 4 issues
[Review] Iteration 2: 3 issues
[Review] Iteration 3: 3 issues
[Review] Max iterations (3) reached, returning last version
POST /api/chat 200 in 41s
迭代 Issues 結果
1 4 重寫
2 3 改善 1 個,重寫
3 3 未再改善,以最後版本輸出

觀察:

根據測試結果做了以下調整:

項目 改善前 改善後
最大迭代次數 3 2
通過標準 passed === true passed === true 或 issue ≤ 2
無改善處理 繼續到上限 立即退出
LLM 呼叫次數(最壞) 6 次 2-3 次
回應時間(最壞) ~41s ~14s

改善後的迭代邏輯:

Iteration 1:
  review → 4 issues (≥ 3, 未通過)
  → 有改善(prev=∞ > 4)→ 重寫

Iteration 2:
  review → 3 issues (≥ 3, 未通過)
  → 有改善(prev=4 > 3)→ 重寫

Iteration 3(不存在,MAX=2):
  如果第 2 次無改善 → 立即退出返回
  如果第 2 次通過 → 返回

改善後重新測試:

Server log:
[Review] Iteration 1: improved to 4 issues (was Infinity)
[Review] Iteration 2: 4 issues (no improvement), exiting
POST /api/chat 200 in 18s
迭代 Issues 結果
1 4 有改善(∞→4)→ 重寫
2 4 無改善(4→4)→ 退出
指標 改善前(MAX=3) 改善後(MAX=2)
迭代次數 3 2
LLM 呼叫次數 6 3
回應時間 41s 18s
未通過時行為 繼續迭代到上限 無改善立即退出

回應時間從 41s 降至 18s,減少 56%。品質上沒有因為減少迭代而犧牲——因為第 3 次本來就沒有改善。

注意事項

JSON 輸出方式

審查代理的回傳格式是 JSON,但 CareerWise 沒有用 response_format 參數來強制。原因是 Groq 的 response_format 在某些模型上不支援,而且就算支援,回傳的 JSON 也不保證 100% 符合預期的 schema。在這裡解法是用 prompt 明確要求回傳 JSON,然後在程式端用 try/catch 做保護:

try {
  const parsed = JSON.parse(reviewContent)
  // 確認必要的欄位都存在
  if (typeof parsed.passed !== 'boolean' || !Array.isArray(parsed.issues)) {
    throw new Error('Invalid schema')
  }
  return parsed as ReviewResult
} catch {
  // JSON parse 失敗或 schema 不符 → 視為 passed,不阻擋回答
  return { passed: true, issues: [] }
}

如果 parse 失敗就回傳 { passed: true, issues: [] },讓回答正常通過。這個設計的考量是:審查代理的目的是提升品質,但不該因為審查環節出錯而讓使用者得不到回答。比起審查失敗擋住回答,放行一個可能不是最好的回答反而是比較好的選擇。

Token 成本

每次迭代有兩次 LLM 呼叫(review + rewrite),各約 1K tokens:

環節 tokens 說明
review 輸入 ~0.5K 使用者問題 + Summer 的回答
review 輸出 ~0.3K JSON 審查結果
rewrite 輸入 ~0.5K 原始回答 + issues
rewrite 輸出 ~0.5K 修正後的回答
單次迭代 ~1.8K 來回一次的總和

最多 2 次迭代,最壞情況約 3.6K tokens。以 Groq 的定價來說成本幾乎可以忽略。

介面相容

reflectResponse 的函數簽名 (function signature) 維持不變:

export async function reflectResponse(
  response: string,
  userMessage: string
): Promise<string>

三個呼叫端(career-chain.tsresume-chain.tschat.ts)都不需要修改。把改動限制在 reflect.ts 內部,降低上線風險。

總結

這次改完的最大收穫是:回應時間從 41 秒降到 18 秒,減少 56%,而且輸出品質完全沒打折——因為第 3 次迭代本來就沒改善,砍掉剛剛好。

Review → rewrite 迭代迴圈讓 Summer 代理自己根據 feedback 重寫,比審查代理直接代勞來得有品質多了。多代理協作的精神不是把工作集中給一個代理,而是讓每個代理做好自己擅長的事——Summer 代理負責創作,審查代理負責挑錯。


審查代理 Agentic Design Pattern Agentic AI CareerWise AI LLM Architecture Design Pattern