技能驗證的進化:從暴力 LLM 到智慧分層策略
技能驗證的進化:從暴力 LLM 到智慧分層策略
驗證履歷素材是否覆蓋 JD 技能需求,聽起來簡單——把技能丟給 LLM 判斷就好。但當技能多達 20 項,且每項需要搜尋、判斷、可能重試時,成本和延遲就成了問題。本文分享我們如何用分層策略優化
verify命令。
問題:驗證一次 JD 要多久?
假設一個 JD 有 15 項技能需求:
暴力做法:
15 技能 × (embedding + LLM 判定) = 15 次 LLM 調用
每次 1-2 秒 → 總計 15-30 秒
如果再加上 Self-RAG 重試(WEAK/MISSING 要再搜一次再判一次):
最壞情況:
15 技能 × 3 次嘗試 = 45 次 LLM 調用
這太慢了,也太貴了。
解決方案:分層驗證
輸入: 15 項技能
↓
┌────────────────────────────────────┐
│ Layer 1: Skill Graph 推論 │ ← 0 ms, 0 cost
│ 用已有技能推斷 IMPLIED │
│ 例:有 LangChain → IMPLIED Python │
│ 結果:3 項直接標 IMPLIED │
└────────────────────────────────────┘
↓ 剩 12 項
┌────────────────────────────────────┐
│ Layer 2: Hybrid Search + NLI │ ← LLM 調用
│ Vector + BM25 搜索證據 │
│ 壓縮後丟 LLM 判定 │
└────────────────────────────────────┘
↓
┌────────────────────────────────────┐
│ Layer 3: Self-RAG 重試 │ ← 只對 WEAK/MISSING
│ 改寫查詢、放寬條件 │
│ 最多 2 次 │
└────────────────────────────────────┘
↓
輸出: verifications + summary + improvement_priorities
Layer 1: Skill Graph 推論
原理
如果使用者的素材標籤包含 LangChain,而 Skill Graph 記錄了:
{
"implies": {
"LangChain": ["Python", "LLM", "RAG"]
}
}
那麼當 JD 要求 Python 時,我們可以直接推斷:
「你有 LangChain 素材,LangChain implies Python → Python 是 IMPLIED」
實作
def infer_skill_status(required_skill: str, all_materials: list[dict]) -> str | None:
"""快速推論技能狀態,不需 LLM"""
possessed = get_possessed_skills_from_materials(all_materials)
# 直接有這技能的素材 → 需要 LLM 確認 COVERED vs WEAK
if required_skill.lower() in possessed:
return None
# 檢查是否可從其他技能推斷
if check_skill_implied(required_skill, possessed):
return "IMPLIED"
return None # 需要 LLM
效益
- 省錢:IMPLIED 的技能不需要 LLM 調用
- 省時:本地圖遍歷 vs 網路 API
- 可控:你決定哪些推論關係存在
Layer 2: Hybrid Search + Context Compression
Hybrid Search
單純向量搜尋可能漏掉關鍵字匹配,所以我們用:
最終分數 = 0.7 × vector_similarity + 0.3 × bm25_score
對中文還用 jieba 分詞:
def tokenize(text: str) -> list[str]:
if is_chinese(text):
return jieba.cut(text)
return text.split()
Context Compression
5 段證據原文可能有 2000 tokens,壓縮後只需 300:
def compress_evidence(skill: str, evidence_texts: list[str]) -> str:
prompt = f"""Summarize these excerpts focusing ONLY on "{skill}":
{evidence}
- Keep specific numbers and metrics
- Maximum 500 characters"""
return llm_call(prompt)
為什麼要壓縮?
- 減少 token 成本:最終判定的 LLM 調用更便宜
- 減少幻覺:無關資訊越少,判定越準確
- 加速推理:更短的 context 推理更快
Layer 3: Self-RAG 重試
當 LLM 判定為 WEAK 或 MISSING 時,可能是搜尋沒找對:
# 第一次搜尋
query = "Kubernetes"
results = search(query, limit=5) # 找到 3 筆弱相關
# 判定結果
status = "WEAK"
reasoning = "Found mentions but no concrete examples"
# Self-RAG: 改寫查詢
refined_query = refine_query("Kubernetes", reasoning)
# → "container orchestration deployment pod management"
# 第二次搜尋(放寬限制)
results = search(refined_query, limit=10) # 找到 2 筆強相關
# 重新判定
status = "COVERED" # ✓ 改善了
Query Refinement Prompt
prompt = f"""The search for "{skill}" didn't find good evidence.
Previous results: {reasoning}
Generate an alternative query that might find better evidence.
Think about:
- Related skills or synonyms
- Specific technologies or frameworks
- Common project types
Return ONLY the refined query."""
重試限制
MAX_RETRY_ATTEMPTS = 2
原因:
- 如果 2 次都找不到,大概率真的沒有
- 避免無限重試拖垮延遲
- 保守估計:每次重試 1-2 秒
輸出結構
{
"verifications": [
{
"skill": "Python",
"status": "COVERED",
"severity": "-",
"confidence": 0.92,
"reasoning": "Multiple projects using Python...",
"evidence_snippets": ["Built RAG using Python..."],
"retry_history": [],
"inferred": false
},
{
"skill": "Kubernetes",
"status": "IMPLIED",
"reasoning": "Inferred from skill graph",
"inferred": true // ← 標記這是推論來的
}
],
"summary": {
"total": 15,
"covered": 8,
"implied": 3,
"weak": 2,
"missing": 2
},
"improvement_priorities": ["GraphQL", "Java", "Docker", "AWS"]
}
improvement_priorities
按嚴重程度排序:
- MISSING 排前面(完全沒有)
- WEAK 排後面(有但不夠強)
這讓使用者知道該優先補強什麼。
CLI 使用
# 完整驗證(預設)
uv run career-kb verify --jd-id "abc123" --with-strategy
# 快速模式(跳過 Skill Graph)
uv run career-kb verify --skills "Python,React" --fast
# 省錢模式(不壓縮、不重試)
uv run career-kb verify --skills "Python,React" --no-compression --no-retry
輸出範例
Skill Verification Results
┏━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ 技能 ┃ 狀態 ┃ 嚴重性 ┃ 證據 ┃ 來源 ┃
┡━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ Python │ COVERED │ ✅ │ Built RAG... │ LLM │
│ React │ IMPLIED │ 🔵 │ - │ 推論 │
│ Kubernetes │ WEAK │ 🟡 │ Mentioned... │ LLM+1 │
│ GraphQL │ MISSING │ 🔴 │ - │ LLM+2 │
└────────────┴─────────┴────────┴────────────────┴───────┘
覆蓋率: 1/4 技能已覆蓋
🔴 核心缺失: 1 項
🟡 需補強: 1 項
改進優先順序: GraphQL, Kubernetes
來源 欄位說明:
推論:Skill Graph 直接判定LLM:一次 LLM 調用LLM+1:LLM + 1 次重試LLM+2:LLM + 2 次重試
效能比較
| 場景 | 暴力做法 | 分層策略 |
|---|---|---|
| 15 技能全 COVERED | 15 LLM | 12 LLM + 3 推論 |
| 5 技能需重試 | 20 LLM | 17 LLM |
| 最壞情況 | 45 LLM | 36 LLM |
| 預估省下 | - | 20-30% |
擴展思路
1. 批次 LLM 調用
目前是逐技能調用,可以改成:
prompt = """Verify these skills against the evidence:
Skills: [Python, React, Kubernetes]
Evidence: {...}
Return JSON with status for each."""
一次調用驗多個,進一步減少延遲。
2. 緩存驗證結果
cache_key = hash(skill + evidence_hash)
if cache_key in cache:
return cached_result
同樣的技能 + 同樣的素材 = 同樣的結果。
3. 背景預驗證
當有新素材入庫時,背景預先驗證所有歷史 JD 的覆蓋率。
總結
| 層級 | 策略 | 節省 |
|---|---|---|
| Layer 1 | Skill Graph 推論 | LLM 調用 |
| Layer 2 | Context Compression | Token 數 |
| Layer 3 | Self-RAG 重試 | 漏報率 |
分層策略讓 verify 命令從「每技能都問 LLM」進化為「能推論就推論,必要時才問,問到答案為止」。
Career Knowledge Base 是一個本地優先的履歷知識庫系統,使用 Python + LanceDB + Voyage AI 建構。
- ← Previous
履歷素材入庫:精確去重與相似變體檢測 - Next →
中文 BM25:用 jieba 解決履歷搜尋的分詞問題