Spaces:
Paused
Paused
File size: 4,865 Bytes
3161850 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | """
embedding_client.py — 语义向量召回客户端
支持两种模式:
1. API 模式(DeepSeek / OpenAI / Qwen 兼容嵌入接口)
2. 本地 sentence-transformers 模式(可选,需安装)
优先级:API → sentence-transformers → token cosine fallback
"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from typing import Optional
try:
from dotenv import load_dotenv
except ModuleNotFoundError: # python-dotenv is optional; env vars still work.
def load_dotenv(*args, **kwargs):
return False
load_dotenv()
class EmbeddingClient:
"""统一嵌入接口。未配置时自动回退。"""
def __init__(self) -> None:
self.api_key = os.getenv("EMBEDDING_API_KEY") or os.getenv("LLM_API_KEY")
self.base_url = os.getenv("EMBEDDING_BASE_URL") or os.getenv("LLM_BASE_URL")
self.model = os.getenv("EMBEDDING_MODEL", "text-embedding-v1")
self.provider = os.getenv("EMBEDDING_PROVIDER", "").lower()
self._client = None
self._local_model = None
self.mode: Optional[str] = None # "api" | "local" | None
# 尝试加载 API 模式
if self.api_key and self.base_url:
try:
from openai import OpenAI
self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
self.mode = "api"
print(f"[OK] EmbeddingClient: API 模式 ({self.base_url})")
except Exception as exc:
print(f"[WARN] EmbeddingClient API 初始化失败:{exc}")
# API 失败则尝试本地模型
if self.mode is None:
self._try_load_local()
def _try_load_local(self) -> None:
try:
from sentence_transformers import SentenceTransformer
model_name = os.getenv("LOCAL_EMBEDDING_MODEL", "BAAI/bge-small-zh-v1.5")
self._local_model = SentenceTransformer(model_name)
self.mode = "local"
print(f"[OK] EmbeddingClient: 本地模式 ({model_name})")
except Exception:
self.mode = None
def is_available(self) -> bool:
return self.mode is not None
def embed(self, texts: list[str]) -> Optional[list[list[float]]]:
"""批量获取嵌入向量。返回 List[List[float]] 或 None。"""
if self.mode == "api":
return self._embed_api(texts)
if self.mode == "local":
return self._embed_local(texts)
return None
def _embed_api(self, texts: list[str]) -> Optional[list[list[float]]]:
try:
resp = self._client.embeddings.create(model=self.model, input=texts)
return [d.embedding for d in resp.data]
except Exception as exc:
print(f"[WARN] Embedding API 调用失败:{exc}")
return None
def _embed_local(self, texts: list[str]) -> Optional[list[list[float]]]:
try:
embeddings = self._local_model.encode(texts, normalize_embeddings=True)
return embeddings.tolist()
except Exception as exc:
print(f"[WARN] 本地嵌入失败:{exc}")
return None
def cosine_similarity(self, vec1: list[float], vec2: list[float]) -> float:
"""余弦相似度。"""
if not vec1 or not vec2:
return 0.0
dot = sum(a * b for a, b in zip(vec1, vec2))
norm1 = sum(a * a for a in vec1) ** 0.5
norm2 = sum(b * b for b in vec2) ** 0.5
if norm1 == 0 or norm2 == 0:
return 0.0
return dot / (norm1 * norm2)
def semantic_similarity(self, resume_text: str, job_text: str) -> Optional[float]:
"""计算简历与岗位的语义相似度(0~1)。"""
embeddings = self.embed([resume_text, job_text])
if embeddings is None:
return None
return self.cosine_similarity(embeddings[0], embeddings[1])
def rank_by_semantic(
self, resume_text: str, jobs: list[dict], top_k: int = 8
) -> list[tuple[int, float]]:
"""用语义向量对岗位重新排序。返回 [(index, score), ...] 按分数降序。"""
if not self.is_available():
return []
texts = [f"{j.get('title', '')} {j.get('jd', '')}" for j in jobs]
all_texts = [resume_text] + texts
embeddings = self.embed(all_texts)
if embeddings is None:
return []
resume_emb = embeddings[0]
scores: list[tuple[int, float]] = []
for i, job_emb in enumerate(embeddings[1:], start=0):
sim = self.cosine_similarity(resume_emb, job_emb)
scores.append((i, sim))
scores.sort(key=lambda x: x[1], reverse=True)
return scores[:top_k]
def get_embedding_client() -> EmbeddingClient:
"""工厂函数。"""
return EmbeddingClient()
|