用 NLI 驗證履歷技能覆蓋:一個完整的實戰案例
用 NLI 驗證履歷技能覆蓋:一個完整的實戰案例
問題:JD 要求的技能,履歷素材真的能證明嗎?
你收到一份 JD,要求候選人具備 Python、Kubernetes、RAG 三項技能。你有一個素材庫存放過去的專案經歷。問題是:
這些素材能否證明你具備這些技能?
關鍵字匹配太弱——提到 "K8s" 不代表真的會用。Embedding 相似度只看語意相近,不看邏輯支持。我們需要的是 NLI(Natural Language Inference):判斷素材是否「蘊含」技能主張。
本文用一個完整案例帶你建構這套系統。
案例背景
輸入:
- JD 要求技能:
["Python", "Kubernetes", "RAG"] - 素材庫:LanceDB 向量資料庫,存放工作經歷
輸出:
- 每個技能的覆蓋狀態(COVERED / IMPLIED / WEAK / MISSING)
- 支持該判斷的證據
- 可追溯的推理過程
Step 1:定義輸出結構
用 Pydantic 強制 LLM 輸出格式,避免 JSON 解析錯誤:
from pydantic import BaseModel, Field
from enum import Enum
class EntailmentStatus(str, Enum):
COVERED = "COVERED" # 有明確證據(蘊含)
IMPLIED = "IMPLIED" # 相關技能推論(弱蘊含)
WEAK = "WEAK" # 關鍵字出現但缺深度(中立)
MISSING = "MISSING" # 無證據(矛盾/無證據)
class EvidenceAnalysis(BaseModel):
"""單條證據的分析"""
evidence_text: str = Field(description="被分析的證據片段")
supports_skill: bool = Field(description="是否支持該技能")
reasoning: str = Field(description="為什麼支持/不支持")
class SkillVerdict(BaseModel):
"""技能驗證結果"""
skill: str
status: EntailmentStatus
confidence: float = Field(ge=0.0, le=1.0)
evidence_analysis: list[EvidenceAnalysis]
final_reasoning: str
Step 2:從素材庫檢索相關證據
用混合搜尋(向量 + BM25)從 LanceDB 撈取候選證據:
import lancedb
from langchain_openai import OpenAIEmbeddings
def search_evidence(skill: str, db_path: str, limit: int = 10) -> list[str]:
"""混合搜尋相關證據"""
db = lancedb.connect(db_path)
table = db.open_table("materials")
# 向量搜尋
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
query_vector = embeddings.embed_query(skill)
results = (
table.search(query_vector)
.limit(limit)
.to_pandas()
)
return results["text"].tolist()
Step 3:Context Compression(減少噪音)
不是每個句子都跟技能相關。用 Embedding 相似度過濾:
import numpy as np
from langchain_openai import OpenAIEmbeddings
def compress_context(
skill: str,
evidence_texts: list[str],
threshold: float = 0.35
) -> list[str]:
"""只保留與技能語意相關的句子"""
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
skill_vector = embeddings.embed_query(skill)
filtered = []
for text in evidence_texts:
# 拆成句子
sentences = text.split("。")
for sentence in sentences:
if len(sentence.strip()) < 10:
continue
sent_vector = embeddings.embed_query(sentence)
# Cosine similarity
similarity = np.dot(skill_vector, sent_vector) / (
np.linalg.norm(skill_vector) * np.linalg.norm(sent_vector)
)
if similarity >= threshold:
filtered.append(sentence)
return filtered
效果:壓縮率通常 30-60%,同時減少 LLM 處理的噪音。
Step 4:NLI 判定(核心)
用 LangChain 的 with_structured_output 強制輸出 Pydantic schema:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
NLI_PROMPT = """你正在進行自然語言推論(NLI),判斷證據是否支持技能假設。
## 假設(Hypothesis)
候選人具備技能:"{skill}"
## 證據(Premise)
{evidence}
## 任務
1. 逐條分析每項證據是否支持假設
2. 綜合判斷最終狀態
## 狀態定義
- COVERED:有明確證據展示該技能(具體專案、量化成果)
- IMPLIED:相關技能暗示能力,但未直接提及
- WEAK:關鍵字出現但缺乏深度/範例
- MISSING:找不到任何支持證據
"""
def verify_skill(skill: str, evidence: list[str]) -> SkillVerdict:
"""用 NLI 驗證技能覆蓋"""
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
structured_llm = llm.with_structured_output(SkillVerdict)
prompt = ChatPromptTemplate.from_template(NLI_PROMPT)
chain = prompt | structured_llm
result = chain.invoke({
"skill": skill,
"evidence": "\n".join(f"- {e}" for e in evidence)
})
return result
Step 5:Self-RAG Retry(失敗時重試)
當結果是 WEAK 或 MISSING,自動生成替代查詢重試:
def refine_query(skill: str, reason: str, tried: list[str]) -> str:
"""根據失敗原因生成替代查詢"""
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
prompt = f"""技能 "{skill}" 驗證失敗。
原因:{reason}
已嘗試查詢:{tried}
生成一個替代查詢來找更好的證據。
策略:
- 同義詞擴展(如 Kubernetes → K8s, container orchestration)
- 上下文豐富(如 Python → Python backend, Python API)
- 分解技能(如 Full-stack → React frontend, Node.js backend)
只回傳一個查詢字串。"""
return llm.invoke(prompt).content.strip()
def verify_with_retry(
skill: str,
db_path: str,
max_retries: int = 2
) -> SkillVerdict:
"""帶重試的技能驗證"""
tried_queries = [skill]
current_query = skill
for attempt in range(max_retries + 1):
# 搜尋
raw_evidence = search_evidence(current_query, db_path)
# 壓縮
compressed = compress_context(skill, raw_evidence)
if not compressed:
if attempt < max_retries:
current_query = refine_query(skill, "無相關證據", tried_queries)
tried_queries.append(current_query)
continue
else:
return SkillVerdict(
skill=skill,
status=EntailmentStatus.MISSING,
confidence=0.9,
evidence_analysis=[],
final_reasoning="搜尋無結果"
)
# NLI 驗證
verdict = verify_skill(skill, compressed)
if verdict.status in [EntailmentStatus.COVERED, EntailmentStatus.IMPLIED]:
return verdict
# 失敗,準備重試
if attempt < max_retries:
current_query = refine_query(skill, verdict.final_reasoning, tried_queries)
tried_queries.append(current_query)
return verdict
Step 6:批次驗證所有技能
def verify_all_skills(skills: list[str], db_path: str) -> dict[str, SkillVerdict]:
"""驗證 JD 要求的所有技能"""
results = {}
for skill in skills:
print(f"驗證:{skill}")
verdict = verify_with_retry(skill, db_path)
results[skill] = verdict
# 顯示結果
print(f" 狀態:{verdict.status.value}")
print(f" 信心:{verdict.confidence:.0%}")
print(f" 理由:{verdict.final_reasoning}")
print()
return results
# 使用範例
if __name__ == "__main__":
skills = ["Python", "Kubernetes", "RAG"]
results = verify_all_skills(skills, "./lancedb")
# 統計
covered = sum(1 for v in results.values() if v.status == EntailmentStatus.COVERED)
print(f"覆蓋率:{covered}/{len(skills)}")
完整流程圖
flowchart TD
S[JD 技能清單] --> SEARCH[混合搜尋]
SEARCH --> COMP[Context Compression]
COMP --> NLI[NLI 判定]
NLI -->|COVERED/IMPLIED| DONE[完成]
NLI -->|WEAK/MISSING| REFINE[生成替代查詢]
REFINE --> SEARCH
實際輸出範例
驗證:Python
狀態:COVERED
信心:95%
理由:多個專案明確使用 Python 建構後端 API 和資料處理流程
驗證:Kubernetes
狀態:WEAK
信心:40%
理由:提及 Docker 容器化,但未直接展示 K8s 經驗
驗證:RAG
狀態:COVERED
信心:90%
理由:Career Knowledge Base 專案完整實作 RAG 流水線
覆蓋率:2/3
附錄
A. NLI 理論背景
NLI(Natural Language Inference)是判斷兩個句子之間邏輯關係的任務:
| 關係 | 定義 | 範例 |
|---|---|---|
| Entailment(蘊含) | 前提能推導出假設 | 「他在 Google 當 SWE 三年」→「他有軟體工程經驗」✓ |
| Contradiction(矛盾) | 前提與假設衝突 | 「沒有雲端經驗」→「精通 AWS」✗ |
| Neutral(中立) | 無法判斷關係 | 「他會 Python」→「他會 Kubernetes」? |
B. 為什麼 Evidence Selection > Chunks?
| 方面 | 傳統 Chunks | Evidence Selection |
|---|---|---|
| 選擇標準 | 向量相似度 | 邏輯支持度 |
| 處理衝突 | 全部塞進 prompt | 衝突檢測 + 過濾 |
| 幻覺率 | 高 | 低(已預先驗證) |
| 可追溯性 | 無 | 每個結論對應明確證據 |
C. 為什麼不在 Indexing 時做 Evidence Selection?
NLI 需要 Premise(證據)和 Hypothesis(假設)兩者才能判斷蘊含關係。
| 階段 | 已知資訊 |
|---|---|
| Indexing | ✅ 有 Premise(原文)、❌ 沒有 Hypothesis(查詢) |
| Query | ✅ 有 Premise、✅ 有 Hypothesis |
因此 Evidence Selection 只能在 Query 時進行。
D. 延伸閱讀
- EG-RAG:建立「證據圖」量化句子間支持度
- MedTrust-RAG:用 NLI 迭代驗證證據完整性
- Self-RAG:根據生成結果動態調整檢索策略
本文基於 Career Knowledge Base 專案,一個使用 Python + LanceDB + LangChain 建構的本地優先履歷知識庫系統。
- ← Previous
2026 年主流 RAG 架構:從 Hybrid 到 Agentic / Graph 的工程化落地 - Next →
MCP Tools vs Agent Skills:我到底需要哪一個?