前言

上一篇 把 PDF 文字與案例放進待核准的估點流程。實作到第二階段時,系統遇到另一個問題:開發機可用 Codex CLI 跑 non-interactive analysis,server 環境則比較適合直接呼叫 OpenAI Responses API。若兩條路各自定義 prompt、JSON 與資料庫寫入,結果很快就會分岔。

這個案例最後沒有做兩套估點功能,而是固定一個 provider contract,讓執行介面可以替換,領域流程、schema validation 與 audit record 保持一致。

原本的做法與問題症狀

最直接的做法是在 server action 裡判斷:

本機 → spawn CLI
正式環境 → fetch API

但如果分支一路延伸到 prompt、output parsing、錯誤格式與 database write,會出現四種差異:

  • 同一欄位在 CLI 與 API 使用不同名稱。
  • 一邊保證 JSON,另一邊只從自然語言猜 JSON。
  • Timeout、temporary files 與 HTTP errors 沒有共同失敗語意。
  • 切換 provider 後,無法比較是哪個 input、schema 與 model 產生結果。

另一個看似彈性的方向是讓每個 provider 回傳任意 object,再由 UI 判斷。這等於把不可信 output 擴散到整個 application,任何漏欄位都要等畫面 render 或資料庫寫入才爆炸。

問題是怎麼推敲出來的?

來源把 boundary 放在 AiAnalysisProvider:輸入包含 request、附件文字、flags 與 reference cases;輸出包含 provider、model、input snapshot 與 structured analysis。

Factory 只決定建立哪個 adapter:

test             → deterministic provider
local automation → Codex CLI provider
server API       → OpenAI provider
未指定           → deterministic fallback

Codex adapter 與 OpenAI adapter 最後都呼叫同一個 structured parser。Estimate runner 再把結果寫成相同的 AiEstimateAnalysisRun。因此 provider 是 infrastructure choice,不是第二套 business workflow。

最後採用的架構

RequestEstimateRunner
  → AiAnalysisProviderInput
  → Provider Factory
       ├─ Deterministic
       ├─ Codex CLI Adapter
       └─ OpenAI Responses Adapter
  → Structured Analysis Parser
  → AiEstimate
  → AnalysisRun(provider/model/input hash/status)

Codex CLI adapter 負責 process boundary:stdin prompt、read-only sandbox、schema-constrained final message、timeout、temporary directory 與 cleanup。

OpenAI adapter 負責 HTTP boundary:server-side credential、Responses request、strict JSON Schema、AbortController timeout、non-2xx error 與 response extraction。

兩者都不擁有「是否核准」的決策。

關鍵實作

共用介面只暴露領域需要的資料:

interface AnalysisProvider {
  analyze(input: AnalysisInput): Promise<{
    provider: string;
    model?: string;
    inputJson: unknown;
    output: StructuredAnalysis;
  }>;
}

CLI 路徑使用 non-interactive mode,並限制最終輸出:

codex exec
  --sandbox read-only
  --ask-for-approval never
  --ephemeral
  --output-schema <schema>
  --output-last-message <temporary-output>
  -

這裡的 never 不表示放棄安全確認,而是因為 analysis 本身被設為 read-only,不需要執行會改動系統的命令。Temporary output 在 finally 清除。

API 路徑則要求 strict structured output:

const response = await fetch(responsesEndpoint, {
  method: 'POST',
  signal: controller.signal,
  body: JSON.stringify({
    model,
    input,
    text: {
      format: {
        type: 'json_schema',
        name: 'ai_estimate',
        schema: analysisSchema,
        strict: true,
      },
    },
  }),
});

Provider 回傳後還會再次由 application schema parser 驗證;API schema adherence 與本地 validation 是兩道不同防線。

實際驗證

既有 tests 證明:

  • Codex adapter 的 command 包含 read-only、no approval prompt、ephemeral、output schema、working directory 與 stdin。
  • CLI 回傳的 final message 會被解析並通過 structured schema。
  • OpenAI adapter 呼叫 Responses endpoint,且 request 使用 strict JSON Schema。
  • Server-side credential 缺失時會立即失敗,不會送出匿名請求。
  • Non-2xx response 會轉成可讀錯誤;timeout 有獨立訊息。
  • Estimate runner 會保存 provider、model、input hash、input/output snapshot 與 success/failure status。
  • Test environment 使用 deterministic provider,不依賴外部服務。

本次只做 source 與 test contract 查核,沒有執行 live Codex CLI 或 OpenAI request,也沒有公開 executable path、credential、production model 或正式 prompt。

仍然存在的限制

第一,兩個 provider 使用同一 schema,不代表品質、延遲、費用與資料治理相同。正式切換前仍要用固定案例集比較 accuracy、approval delta、timeout 與 failure rate。

第二,來源有 timeout 與 readable error,但沒有 retry/backoff、rate-limit queue、circuit breaker 或 provider fallback。若直接自動重試,還要避免重複計費與重複寫入。

第三,CLI adapter 依賴 local executable、working directory 與使用者 authentication;API adapter 依賴 server credential、network 與 endpoint policy。Deployment checklist 必須分開,不可只測 interface。

第四,AnalysisRun 保存 input/output snapshot 有助稽核,也會增加敏感資料面。需求附件若含個資或商業資訊,要定義 redaction、retention、access control 與 deletion。

第五,Structured Outputs 保證 schema adherence,不保證估點內容正確。合法的 JSON 仍可能漏看需求或引用錯案例。

可以延伸到哪些情境?

Provider adapter 適合內容分類、文件抽取、客服草稿與 code analysis。穩定邊界通常是:

Provider 負責「如何執行」
Schema 負責「資料長什麼樣」
Runner 負責「如何記錄成功與失敗」
Domain workflow 負責「結果能不能生效」

下一篇把最後一條責任拉開來看:AI 可以提出建議,但不能直接核准:Approval Ledger 與權限分離

參考資料

以上平台文件查核日期:2026-07-31。