iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability系列 第 55 篇

Day 31 特別篇(下)|LangSmith 全服務解析:從 LLM Trace 到 Production AI Reliability

  • 分享至 

  • xImage
  •  

GitHub:darkstar1227/learning-sre-for-ai-era

結論先說:evaluation 通過不代表沒有問題,只代表 candidate 沒有重蹈已知的覆轍;release gate 要輸出的是條件與證據,不是一個綠色勾勾。這一篇(下)把 DIY 走完,接回本文最初的結論。

承接上文:上一篇(中)建好了 project、trace 邊界、WorkflowContext 與 dataset 骨架(Step 1 到 Step 8)。這一篇接著跑 deterministic evaluator 的比較、LLM-as-judge 的校準防線、feedback triage、investigation runbook,直到把整條 trace → dataset → evaluation → release gate 的學習迴圈收斂成一份可交接的 evidence 包。

Step 9:從 deterministic evaluator 開始比較 baseline 與 candidate

平均分數很容易掩蓋高風險錯誤。先將可審計的規則拆開,例如 citation、預期 task status、必要事實與禁止的 tool action。

def source_is_present(outputs: dict, reference_outputs: dict) -> dict:
    actual = set(outputs.get("citations", []))
    required = set(reference_outputs.get("required_sources", []))
    passed = required.issubset(actual)
    return {
        "key": "required_source_present",
        "score": int(passed),
        "comment": f"required={sorted(required)}, actual={sorted(actual)}",
    }


def task_status_matches(outputs: dict, reference_outputs: dict) -> dict:
    expected = reference_outputs["expected_task_status"]
    actual = outputs.get("task_status")
    return {
        "key": "task_status_matches",
        "score": int(actual == expected),
        "comment": f"expected={expected}, actual={actual}",
    }


def required_fact_present(outputs: dict, reference_outputs: dict) -> dict:
    expected = reference_outputs.get("required_fact", "")
    answer = outputs.get("answer", "")
    passed = not expected or expected in answer
    return {
        "key": "required_fact_present",
        "score": int(passed),
        "comment": "literal Lab check; production needs a reviewed semantic rule",
    }

把服務包成 evaluation target 時,輸出要和 dataset reference contract 對齊。不要讓 evaluator 猜 response 裡哪個欄位叫 answer。

def make_context(
    *,
    request_id: str,
    prompt_version: str,
    model_route: str,
) -> WorkflowContext:
    return WorkflowContext(
        request_id=request_id,
        task_id="task-evaluation",
        workflow_name="policy-qa",
        workflow_version="2026-09-22",
        service_version="answer-api-0.9.0",
        deployment_id="lab-local",
        prompt_version=prompt_version,
        policy_version="policy-guard-v1",
        retrieval_index_version="policy-v3",
        model_route=model_route,
    )


def baseline_target(inputs: dict) -> dict:
    result = answer_workflow(
        inputs["question"],
        make_context(
            request_id="eval-baseline",
            prompt_version="policy-v1",
            model_route="baseline",
        ),
    )
    return result


def candidate_target(inputs: dict) -> dict:
    result = answer_workflow(
        inputs["question"],
        make_context(
            request_id="eval-candidate",
            prompt_version="policy-v2",
            model_route="candidate",
        ),
    )
    return result

make_context() 應在你的服務中集中處理版本與 deployment 資訊;不要讓每個 evaluator 都各自拼出一份不同 metadata。這類不一致會讓 incident 時的比較變成猜謎。

官方 SDK 的 Client.evaluate() 可針對 dataset 執行 target 與 evaluators。先以低 concurrency 在 Lab 觀察成本與 dependency 行為;提高 concurrency 前,要確認 provider rate limit、queue、cost budget 與 evaluation timeout。

evaluators = [
    source_is_present,
    task_status_matches,
    required_fact_present,
]

baseline_results = client.evaluate(
    baseline_target,
    data="policy-task-dataset-v1",
    evaluators=evaluators,
    experiment_prefix="policy-baseline-v1",
    description="Baseline prompt and routing for sanitized Lab cases.",
    max_concurrency=1,
)

candidate_results = client.evaluate(
    candidate_target,
    data="policy-task-dataset-v1",
    evaluators=evaluators,
    experiment_prefix="policy-candidate-v2",
    description="Candidate prompt and routing for sanitized Lab cases.",
    max_concurrency=1,
)

這兩個 result 物件的 UI 與 SDK 查詢方式會隨 LangSmith 版本演進;release gate 不應依賴手動閱讀一張表。讓 CI 或 review script 明確輸出每個 required case 的 pass/fail,以及要採取的 decision。

case: policy_approval_required
  required_source_present: pass
  task_status_matches: pass
  required_fact_present: fail
  decision: blocked

case: unknown_supplier_discount
  required_source_present: pass
  task_status_matches: pass
  required_fact_present: pass
  decision: allowed

第一個案例即使整體平均 0.95,也要 blocked。因為它漏的是經確認的必要條件;平均分數不能替風險分類做決定。

這一步在驗證什麼:這是整篇文章④⑤兩段最核心的主張,落到程式碼的樣子——「用平均分數決定要不要放行」是一個看起來合理、實際上會漏接高風險錯誤的做法。三個 evaluator 函式刻意寫成各自獨立、各自回傳 0 或 1,而不是合併成一個加權平均分數,正是為了讓 policy_approval_required 這個案例即使整體分數 0.95 依然被攔下來。如果讀者把這三個函式改寫成「三項加總除以三」,這個案例反而會因為兩項滿分被平均分數蓋過去,這正是文章反覆強調「平均分數不能替風險分類做決定」想要避免的結果。

跑的時候預期看到什麼:client.evaluate() 執行完,baseline 與 candidate 各自會產生一個 experiment,每個 dataset example 對應到三個 evaluator 的分數。讀者應該會看到 baseline 和 candidate 在某些案例上表現一致、在少數案例上出現差異——如果兩者完全一樣,通常代表 baseline_target 和 candidate_target 沒有真的接到不同的 prompt 版本或 model route,值得回頭檢查 make_context() 的參數是不是被複製貼上時忘了改。

容易踩的坑:max_concurrency 這個參數在 Lab 環境刻意設為 1,是因為真實 provider 呼叫有 rate limit,跑十幾筆 dataset case 同時發送並不會比較快,反而容易觸發 429。另一個常見的坑是 dataset 裡的 reference_outputs schema 和 evaluator 函式期待的欄位名稱對不上——例如 dataset 存的是 required_source(單數),evaluator 卻讀 required_sources(複數),這種拼字誤差不會讓程式報錯,只會讓每一筆 case 都安靜地評分為 0,錯誤地以為 candidate 表現很差。

Step 10:LLM-as-a-judge 的位置與防線

有些品質問題不能用字串包含判斷,例如回答有沒有扭曲政策、摘要是否遺漏限制、引用是否真的支持結論。這時才考慮 LLM-as-a-judge。

它是 evaluator,不是真相產生器。為 judge 設定自己的 contract:

judge input
  - sanitized question
  - candidate answer
  - approved reference facts
  - allowed source excerpt or source ID

judge output
  - pass / fail / uncertain
  - failure category
  - short rationale
  - evaluator prompt version
  - evaluator model route

在把 judge score 當 release evidence 前,至少做三件事:

  1. 以人工審過的小型 Gold set 校準 evaluator,計算與人工判定的 disagreement。
  2. 對 fail、uncertain、高風險類別與分數臨界值附近的案例做人工複核。
  3. 固定 evaluator prompt、模型 route、temperature 與資料版本,否則兩次分數差異無法解釋。

不要把 judge 的 explanation 直接回傳給使用者,也不要讓 judge 決定 tool 是否有寫入權限。安全 permission 應由可審計的 policy engine 或人工批准處理;judge 最多提供一條需要調查的訊號。

第一步驟提到的「校準」不是空話,它應該產出一份具體的報告,讓團隊看見 judge 到底可不可信:

judge calibration report — policy-judge-v1

Gold set 規模:40 筆人工審過的案例
judge 與人工判定一致:34 筆(85%)
disagreement 分布:
  judge=pass,人工=fail   →  4 筆(judge 過於寬鬆)
  judge=fail,人工=pass   →  1 筆(judge 過於嚴格)
  judge=uncertain          →  1 筆(正確標記為需人工複核)

結論:judge 在「引用是否支持結論」這類語意判斷上偏寬鬆,
      高風險類別(policy_exception、safety-adjacent)
      應強制人工複核,不論 judge 分數為何。

這一步在驗證什麼:這份校準報告把前面提到的 Claude「察覺自己在被評估」現象,轉換成一個可操作的防線——不是要求團隊去偵測模型有沒有在自我調整,而是持續拿一小批人工審過的 Gold set 去測 judge 本身準不準。如果 disagreement 的分布長期偏向某一方(例如 judge 系統性地偏寬鬆),這就是明確訊號:不能再把這個 judge 的分數直接當成 release evidence,得先修正 judge 的 prompt 或提高人工複核比例。

容易踩的坑:Gold set 的規模如果太小(例如只有 5、6 筆),disagreement 的統計就沒有意義——4 筆裡錯 1 筆和 40 筆裡錯 10 筆,錯誤率看起來一樣,但後者的結論可信得多。另一個常見錯誤,是把「人工複核」設計成只複核 judge 判定為 fail 的案例,卻忽略了 judge 判定 pass 但其實應該 fail 的偽陰性——這類案例才是真正危險的地方,因為它們會被 release gate 直接放行,而不會被任何人注意到。

Step 11:讓 feedback 指回 trace,並保留「尚待查證」狀態

使用者按下倒讚,不等於系統已經被證明答錯;它是 investigation ticket。feedback 至少應有 key、score、comment、trace ID 和分類結果。

LangSmith Python SDK 可以以 create_feedback() 將 feedback 關聯 root trace 或 child run。傳入 trace_id 時,SDK 能以背景方式處理 feedback ingestion,避免把 UI 回報路徑硬塞進低延遲 request 的同步等待。

from langsmith import Client


client = Client()

def record_user_feedback(trace_id: str, message: str) -> None:
    client.create_feedback(
        key="user_feedback",
        score=0,
        trace_id=trace_id,
        comment=message,
    )

若已查明錯誤落在 retrieval,而不是整條 workflow,可將 feedback 掛在對應 child run。同一個 trace 若有多筆回報,保留時間、來源與處理狀態,別用最後一個 comment 覆蓋前面的證據。

new             使用者剛回報,尚未重現
triaged         已確認 trace、版本與 request 範圍
confirmed       已確認為產品或資料問題
not_reproduced  目前證據不足,保留原因與查詢時間窗
converted       已遮罩並轉成 dataset case

一個合理的 triage 記錄看起來像這樣:

{
  "trace_id": "trace-...",
  "request_id": "req-20260922-41",
  "feedback_state": "confirmed",
  "failure_class": "missing_critical_condition",
  "prompt_version": "policy-v2",
  "retrieval_index_version": "policy-v3",
  "next_action": "create_sanitized_regression_case",
  "owner": "ai-service-oncall"
}

資料中的 owner 應是 on-call alias 或 team role,而非私人聯絡資訊。若 comment 含敏感資料,先在服務端做 redaction;不要期望後續人工看見後才把資料「忘掉」。

Step 12:把 trace review 寫成可執行的 investigation runbook

當 quality alert、feedback 或 Day 30 chaos experiment 出現異常,依序查,而不是先猜 prompt 壞了。

次序 問題 優先證據 可能處置
1 是否為同一 deployment 後開始? release manifest、deployment_id、metrics window 停止 rollout、比較 canary
2 transport 是否成功? HTTP status、error rate、dependency span 依 Day 18/29 處理 technical failure
3 retrieval 是否有資料且版本正確? result_count、index_version、retriever run 重建或回退 index,或降級
4 模型 route 是否改變? model_route、fallback flag、latency 檢查 provider 或 routing policy
5 tool 是否被允許與正確執行? policy decision、tool run、audit event 停止 action、人工審核
6 validator 是否放過不應放行的回覆? quality/safety/task status 修正規則並建立 regression case
7 已知 failure 是否被 dataset 擋住? dataset tag、experiment、evaluator detail blocked、補 evaluator 或案例

用一個具體案例走一次。使用者問「公司每週能遠端三天,對嗎?」系統回「可以」,HTTP 200,卻漏了政策中的主管核准與天數限制。

request_id → 找到 root trace
root trace → prompt_version=policy-v2,index_version=policy-v3
retrieve run → 找到 Remote Work Policy,非 empty retrieval
model run → provider 正常、沒有 fallback
validator run → quality_status=pass,但沒有檢查必要條件
判定 → validator coverage gap,不是 provider outage
後續 → 建立 missing_critical_condition dataset case,candidate 必須 blocked

這個結論靠 evidence 得出,不是因為「模型看起來答得很自信」。同樣地,若 retriever 根本沒拿到文件,修 validator 只能讓系統更常拒絕,不能修好資料供應。

換一個根因不同的案例,會走到表格裡不同的分支。使用者問「上季的供應商合約條款是什麼?」系統一樣回 HTTP 200,回答卻是一段語氣自信、內容卻完全編造的合約條款。

request_id → 找到 root trace
root trace → prompt_version=policy-v2,index_version=policy-v3
retrieve run → result_count=0,empty retrieval(知識庫裡根本沒有這份合約)
model run → provider 正常、沒有 fallback
validator run → quality_status=insufficient_context 本應觸發,
                 但實際輸出卻是 quality_status=pass
判定 → validator 沒有正確處理 empty retrieval 的情況,
       讓模型在沒有依據時仍自信生成答案
後續 → 這不是 dataset 覆蓋不足的問題,是 validate_answer() 的
       if not documents 分支本身有 bug(見 Step 6 的範例實作),
       必須先修程式碼,再補一筆 dataset case 防止回歸

把這兩個案例放在一起看,價值在於它們走到表格裡完全不同的分支——第一個案例的根因在「規則沒寫全」(validator 沒有檢查某個必要條件),第二個案例的根因在「規則本身有 bug」(empty retrieval 該觸發的分支沒有被觸發)。這兩種根因的修法不一樣:前者是補規則、寫 dataset case;後者是先修程式碼本身的邏輯錯誤,dataset case 只是用來防止同樣的 bug 以後又悄悄回來。investigation runbook 表格的價值,正是逼著使用者在下結論前,先把這兩種可能性分開檢查,而不是看到「答案錯了」就直接跳去改 prompt。

Step 13:把 evaluation 接到 release decision,但不把它變成自動放行按鈕

release gate 應輸出條件,而非只輸出一個綠色勾勾。以下是可供 Lab 改寫的 decision spec。

release_candidate:
  candidate_id: answer-api-0.9.0-rc2
  prompt_version: policy-v2
  dataset_tag: release-candidate-2026-09-22
  required_checks:
    - required_source_present
    - task_status_matches
    - required_fact_present
    - tool_permission_denied_when_required
  block_when:
    - any_required_case_failed
    - safety_case_failed
    - dataset_version_not_pinned
    - evaluator_version_missing
  human_review_when:
    - llm_judge_uncertain
    - new_failure_class
    - evaluation_coverage_changed
  evidence:
    - experiment_link
    - release_manifest
    - evaluator_versions
    - approval_record

這份檔案不是 LangSmith 的內建設定格式,而是團隊自己的 release contract 範例。它故意把「資料版本未固定」視為阻擋條件:不能確定比較的是哪份題目,就不能宣稱 candidate 通過。

decision 本身也應有可追溯紀錄。

{
  "candidate_id": "answer-api-0.9.0-rc2",
  "dataset_tag": "release-candidate-2026-09-22",
  "experiment": "policy-candidate-v2",
  "decision": "human_review_required",
  "reasons": ["llm_judge_uncertain on policy_exception_case"],
  "approver_role": "ai-service-owner",
  "recorded_at": "2026-09-22T10:30:00Z"
}

若 decision 是 blocked,不要把候選版本偷偷調低 traffic 當成「有推出」。應明確回到 owner、修正、再評估;若是 human_review_required,指派能做決定的角色,避免 approval 變成無限期待辦。

這一步在驗證什麼:這一步把⑦段落的核心主張——「release gate 輸出的是條件,不是一個綠色勾勾」——寫成具體的 YAML 結構。它驗證的重點在於 block_when 裡刻意加入了 dataset_version_not_pinned 和 evaluator_version_missing 這兩條,這兩條不是在測 candidate 的品質,而是在測「這次比較本身有沒有資格被信任」。讀者實際跑一次就會發現,如果沒有事先在 Step 8 固定 dataset tag,這裡的 release gate 邏輯應該要主動擋下來,而不是安靜地拿最新資料跑完評估、給出一個看似正常的分數。

預期看到什麼、容易踩的坑:這份 YAML 和上面的 JSON decision 記錄不是 LangSmith 提供的內建功能,讀者需要自己寫一小段程式把 client.evaluate() 的結果轉換成這個結構。常見的偷懶做法是把 block_when 的條件寫死在程式碼裡、卻沒有寫測試驗證它,結果過了幾個月團隊新增了一個 safety_case_failed 的判斷邏輯,卻忘了同步更新這份 YAML 的說明文件,兩邊逐漸不同步。比較穩妥的做法,是讓 release decision 的產生程式碼本身就讀取這份 YAML 當作設定來源,而不是讓 YAML 淪為一份寫給人看、程式碼卻沒有真的照著做的文件。

Step 14:把 LangSmith 與既有 SRE 訊號接起來

LangSmith trace 可以讓你從一筆失敗回到 workflow;它不能單獨告訴你 error budget 是否正在燃燒。把兩條證據鏈在 service 邊界用相同 ID 關聯。

HTTP handler
  ├─ structured log: request_id, trace_id, task_status, deployment_id
  ├─ OTel span: request_id, workflow_version, dependency outcome
  ├─ LangSmith trace: prompt/retrieval/model/tool/validator evidence
  └─ Prometheus metric: route, technical_status, task_status, degraded

metric 設計要保守。例如:

ai_requests_total{route="ask",technical_status="success",task_status="completed"}
ai_requests_total{route="ask",technical_status="success",task_status="degraded"}
ai_requests_total{route="ask",technical_status="error",task_status="failed"}

它能支援 Day 14 的 SLI 分母與分子討論;但 request_id、trace_id、prompt 全文、dataset example ID 都不該進去。

alert 觸發後的調查節奏可以是:

Grafana:quality-related outcome 或 dependency error 是否異常?
  ↓
Loki:用時間窗、route、deployment_id 找到相關 request_id
  ↓
OpenTelemetry:確認跨服務與 dependency path
  ↓
LangSmith:檢查該 request 的 workflow child run、版本與 feedback
  ↓
Dataset/evaluation:是否已有 regression case?candidate 是否重現?

這是查詢順序,不是要求每次都開四個工具。若 Prometheus 顯示 provider timeout,先從服務與 dependency telemetry 開始;若只有某類 feedback 上升,才從 dataset 與 evaluation coverage 往回找。工具各有責任,少繞路才是成熟的 on-call。

這一步在驗證什麼:這是全篇唯一一個把 Day 2、Day 14 建立的 Prometheus/Loki/OTel 觀測堆疊和本篇的 LangSmith 明確接起來的 Step,驗證的是①段落「三層各司其職、不互相取代」的主張要能落地,前提是四層系統必須共享同一組 ID(request_id/trace_id)。預期看到什麼:在 Loki 用 request_id 查到一筆 log 後,同一個值應該能在 LangSmith 的 trace 搜尋欄位直接貼上去找到對應的 root run,中間不需要任何轉換或對照表。容易踩的坑:最常見的失誤是 metric label 裡不小心放進高基數欄位(例如把 request_id 當成 label),這會讓 Prometheus 的時間序列數量爆炸;另一個常見狀況,是四層系統分別由不同人建置,request_id 在某一層被重新產生而非全程傳遞,導致查詢鏈在某個環節突然斷掉,Step 2 提到的「沒有正確傳遞 ID」在這裡會以跨系統關聯失敗的方式重新出現。

Step 15:常見失敗模式與不該採取的修法

現象 容易犯的錯 較好的處理
trace 內出現完整客戶文件 以為之後手動刪掉就好 停止擴散、依資料流程處理,並在 ingest 前遮罩
evaluator 平均分提高 直接放行 candidate 先檢查 required case、safety case 和 coverage 是否變更
feedback 都掛 root trace 認為已能定位 triage 後將確認的問題指到 retrieval、tool 或 validator child run
只有 completed 被視為成功 把誠實拒答當 outage 依產品 SLI 明確定義 completed、degraded、failed 的計算
empty retrieval 後仍生成自信答案 只調高模型 temperature 先修 retrieval-to-validator contract 與降級 policy
每次 dataset 都取最新版本 CI 偶爾通過、偶爾失敗 pin dataset tag,記錄 freeze time 與修改理由
將 LLM judge 當 final approver 讓不穩定 evaluator 決定高風險 action 保留 rule-based checks、人工複核與 policy boundary
同時從多個 SDK 雙寫同一 trace UI 出現重複 run,成本難算 指定 primary tracing path,先在 staging 驗證 context propagation
dataset case 只寫「應該做什麼」 evaluator 無法判斷「不該做什麼」 同時寫 forbidden behavior,例如禁止未授權 tool 呼叫
release decision 只留在聊天訊息裡 下次同類問題重新討論一次 落地成 Step 13 的 JSON 紀錄,附 dataset tag 與 approver
把 LangSmith trace 當成唯一備份 帳號權限異動或 retention 到期後證據消失 evidence 包(Step 16)另存一份低敏感度快照

這些問題都不是「LangSmith 壞掉」。它們多半是資料治理、contract 或 release process 沒有被寫清楚。工具會忠實記錄你送給它的東西,包括不該送的東西。

這張表也可以反過來讀:每一列「較好的處理」欄位,幾乎都對應到前面①到⑦某個段落已經講過的原則。這不是巧合——常見的失敗模式,多半就是團隊在壓力下略過了某個已知的最佳實踐,而不是遇到了什麼全新的未知問題。把這張表釘在團隊內部的 wiki 或 onboarding 文件裡,比事後每次都重新討論一次「這次到底該怎麼修」更有效率。

Step 16:Lab 完成後,交出可供 review 的 evidence 包

讀者完成自己的實作後,可整理下列資料給 reviewer。不要附 API key、完整內部政策或未遮罩 trace。

1. trace 結構截圖或低敏感度匯出:正常與 degraded 各一筆
2. trace data contract:欄位、用途、retention owner
3. dataset case:來源、遮罩方式、expected behavior、risk level
4. evaluator 定義:版本、規則與已知盲點
5. baseline/candidate experiment:dataset tag、時間、結果、差異
6. release decision:allowed / human_review_required / blocked 與理由
7. 關聯證據:request_id、deployment_id、metrics/logs/OTel 查詢位置

這份 evidence 包要能回答一個很實際的問題:

你說 candidate 比較好。是哪一筆資料、哪一套規則、哪一個版本,以及誰決定它足以推出?

答不出來時,不代表產品一定有問題;代表 release decision 還沒有足夠證據。把證據補齊,比把 dashboard 截圖貼得更漂亮有用。

這一步在驗證什麼:Step 16 不是額外的收尾儀式,而是驗證整個 learning loop 有沒有真的「可交接」。前面十五個 Step 各自證明了某個機制能動,這一步證明的是這些機制產生的證據能不能脫離作者本人、被另一個團隊成員在不追問的情況下看懂。預期看到什麼:把這七項證據交給一個完全沒參與這次 Lab 的同事,對方應該能在不用再問你任何問題的情況下,自己判斷 release decision 是否合理。容易踩的坑:最常出現的缺口是第 4 項「evaluator 定義」,團隊往往只附上 evaluator 的程式碼,卻沒寫「已知盲點」——也就是這個 evaluator 檢查不到什麼。少了這一項,reviewer 很容易誤以為「evaluator 通過」等於「沒有任何問題」,這正好落回⑤段落一開始就在提醒的:evaluation 只能告訴你它有檢查的部分沒有變差,檢查不到的地方,本來就不在它的覆蓋範圍內。

Step 一覽:十六個 Step 各自驗證了哪個論點

跑完整套 Lab 後,回頭用這張表核對一次,比較容易發現有沒有漏掉某個環節——尤其是「有 trace,但 dataset 和 evaluation 沒接上」這種只做一半的狀況,光看 LangSmith UI 上有畫面,並不代表整條 learning loop 真的閉環了。

Step 對應段落 驗證的論點 若失敗,通常代表
1 ① project 能正確分隔環境 環境變數未生效或 API key 有誤
2 ①② workflow 邊界對準工程責任 function boundary 切得太粗,child run 缺失
3-4 ④⑤ 已知失敗能被寫成規則 evaluator 邏輯還沒想清楚就急著寫程式
5 ② trace data contract 先想清楚欄位邊界 收集了不該收集的敏感欄位
6-7 ②③ root/child run 對應真實 workflow,task_status 可區分 validator 邏輯藏在其他函式裡
8 ④ dataset 是可版本化、可審查的資產 dataset 案例沒有來源與遮罩紀錄
9 ⑤ deterministic evaluator 不被平均分數蓋過 evaluator 被寫成加權平均,漏接高風險案例
10 ⑤ LLM-as-judge 有校準與人工複核防線 judge 分數被直接當成唯一證據
11 ⑥ feedback 先是調查入口,不是自動事實 倒讚被直接當成 regression 案例
12 ⑦ investigation 有固定查詢順序 每次故障都憑經驗猜、沒有 runbook 可依循
13 ⑦ release gate 輸出條件而非綠燈 dataset 版本未固定卻仍宣稱通過
14 ①⑦ AI trace 與既有 SRE 訊號用共同 ID 關聯 metrics 異常後找不到對應的 LangSmith run
15-16 全篇 常見錯誤有具體修法、結果可被覆核 團隊各自為政,release 決策無法重現

這張表本身也可以直接拿來做 Step 16 evidence 包的附件——它讓 reviewer 一眼看出「這次的 candidate 評估,究竟走完了整條迴圈的哪幾段」,而不必逐字重讀十六個 Step 的說明。

擴充後的驗收

  • trace data contract 有清楚的低敏感度欄位與禁止欄位。
  • root trace 下能看見 retrieve、prompt、model、validator 等與責任對應的 child run。
  • 正常案例與資料不足案例的 task_status、quality_status 可區分。
  • dataset example 有 expected behavior、風險層級與來源說明,且不直接複製敏感 trace。
  • evaluation 比較 baseline 與 candidate 時,使用固定 dataset tag 與具名 evaluator。
  • required case、safety case 或 evaluator version 缺失時,release contract 會阻擋或要求人工複核。
  • feedback 先作為調查入口,再由 evidence 決定是否建立 regression case。
  • request、trace、deployment 與服務層 metrics/logs/OTel 有可說明的關聯方式。
  • 本文提供的是讀者可自行重跑的 Lab 步驟;本文沒有建立 DIY、安裝套件、啟動服務、呼叫 provider 或驗證讀者環境。

驗收

  • /ask 的 root run 可連回 request_id、task_id 與 deployment version。
  • trace 能區分 retrieval、model、tool、parser、validator 與 response。
  • metadata 與 tags 沒有 credential、token、個資或未遮罩的敏感內容。
  • 至少有一筆 versioned dataset example 和一個 rule-based evaluator。
  • baseline/candidate 比較能指出 required source、quality 或 safety 的差異。
  • 一筆 failure 能經過遮罩、分類、dataset、evaluation,形成可解釋的 release decision。
  • 本文未替讀者建立環境、執行 SDK、呼叫 provider 或驗證上述結果。

⑨ 本文結論

LangSmith 最有價值的地方不是多一個 trace 畫面。它讓 AI failure 可以回到版本、資料集與評估,再由 evidence 決定變更能否推出去。SRE 的主線沒有變:先定義承諾,量測使用者結果,留下能重現的證據,才談自動化。

把整篇文章的七個段落攤開來看,其實是在回答同一個問題的七個切面:當 AI workflow 出錯時,系統能不能證明「這次不會再錯」,而不是只能證明「這次修好了」。 這兩者的差別,決定了團隊是不斷救火,還是真的在累積可靠性。

只能證明「這次修好了」
  ↓ 修一個 prompt,人工測幾個問題,看起來正常,上線
  ↓ 下一次遇到類似情境,重新從頭排查
  ↓ 團隊的可靠性知識停留在個人記憶裡,離職就丟失

能證明「這次不會再錯」
  ↓ 修一個 prompt,先寫成 regression case
  ↓ baseline 與 candidate 都跑過同一批已知失敗
  ↓ release decision 有據可查,下次同類問題直接查 dataset
  ↓ 團隊的可靠性知識累積在 dataset、evaluator 與 evidence 包裡

Day 1 到 Day 30 建立的 metrics、logs、trace、SLO、error budget、chaos experiment,回答的是「系統現在健不健康」;Day 31 在這之上多接了一層,回答「這次變更,有沒有讓系統比昨天更可靠」。兩層合在一起,才是完整的 AI Reliability 迴圈——沒有底層的 observability,evaluation 無從得知 production 真正發生了什麼;沒有頂層的 evaluation,observability 只能不斷發現同一類問題,卻沒有機制阻止它重複發生。

延伸閱讀


這個系列從一條 LLM Trace 開始,最後也回到 Trace。差別是,Day 1 的 Trace 只是讓我們看見 AI workflow;Day 31 的 Trace 已經成為一條能連接 SLI/SLO、Error Budget、Metrics、Logs、OpenTelemetry、Dataset、Evaluation、Safety、Task Success 與 Release Decision 的可靠性證據鏈。

Production AI 的核心挑戰,是系統能不能在變更、故障、流量、成本與安全限制下,持續為使用者提供可信、可觀測、可評估、可恢復且可演進的結果。

LangSmith 可以放進這條 AI Reliability Learning Loop;系統能否維持可靠,仍取決於觀測、評估、故障學習與工程決策是否連得起來。

這篇是 Learning SRE for the AI Era 系列的一部分:從 SRE Lab 到 Production AI Reliability。

Build → Trace → Break → Measure → Evaluate → Recover → Improve.


上一篇
Day 31 特別篇(中)|LangSmith 全服務解析:從 LLM Trace 到 Production AI Reliability
下一篇
Day 32 特別篇(上)|Langfuse vs. LangSmith:AI Observability 要怎麼選、怎麼整合?
系列文
Learning SRE for the AI Era:從 SRE Lab 到 Production AI Reliability 共 58 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言