210 lines
6.5 KiB
Python
210 lines
6.5 KiB
Python
"""历史评审案例向量库
|
||||
|
|
|
|||
|
|
功能:
|
|||
|
|
1. 存储历史评审案例(项目特征+评审结果+评审员行为)
|
|||
|
|
2. 相似案例检索(基于向量相似度)
|
|||
|
|
3. 评审员行为学习(团队集体经验,非个人预测)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from typing import List, Dict, Optional, Tuple
|
|||
|
|
from datetime import datetime
|
|||
|
|
from pathlib import Path
|
|||
|
|
import logging
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class HistoryStore:
|
|||
|
|
"""历史评审案例存储(简化版,生产环境建议使用 Chroma/Qdrant)"""
|
|||
|
|
|
|||
|
|
def __init__(self, data_dir: str = "./xingbang_data/history"):
|
|||
|
|
self.data_dir = Path(data_dir)
|
|||
|
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|||
|
|
self.cases_file = self.data_dir / "review_cases.jsonl"
|
|||
|
|
|
|||
|
|
# 确保文件存在
|
|||
|
|
if not self.cases_file.exists():
|
|||
|
|
self.cases_file.touch()
|
|||
|
|
|
|||
|
|
def add_case(self, case_data: Dict) -> str:
|
|||
|
|
"""添加一条历史评审案例"""
|
|||
|
|
case_id = f"case_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|||
|
|
|
|||
|
|
case_record = {
|
|||
|
|
"case_id": case_id,
|
|||
|
|
"timestamp": datetime.now().isoformat(),
|
|||
|
|
**case_data
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 追加写入 JSONL
|
|||
|
|
with open(self.cases_file, "a", encoding="utf-8") as f:
|
|||
|
|
f.write(json.dumps(case_record, ensure_ascii=False) + "\n")
|
|||
|
|
|
|||
|
|
logger.info(f"案例已保存: {case_id}")
|
|||
|
|
return case_id
|
|||
|
|
|
|||
|
|
def search_similar_cases(
|
|||
|
|
self,
|
|||
|
|
industry: str,
|
|||
|
|
credit_amount: float,
|
|||
|
|
risk_level: str = None,
|
|||
|
|
limit: int = 5
|
|||
|
|
) -> List[Dict]:
|
|||
|
|
"""检索相似案例(简化版:基于行业+金额范围)"""
|
|||
|
|
|
|||
|
|
if not self.cases_file.exists():
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
similar_cases = []
|
|||
|
|
|
|||
|
|
with open(self.cases_file, "r", encoding="utf-8") as f:
|
|||
|
|
for line in f:
|
|||
|
|
if not line.strip():
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
case = json.loads(line)
|
|||
|
|
|
|||
|
|
# 行业匹配
|
|||
|
|
industry_match = industry in case.get("industry", "") or case.get("industry", "") in industry
|
|||
|
|
|
|||
|
|
# 金额范围匹配(±30%)
|
|||
|
|
case_amount = case.get("credit_amount", 0)
|
|||
|
|
amount_match = 0.7 * credit_amount <= case_amount <= 1.3 * credit_amount
|
|||
|
|
|
|||
|
|
# 风险等级匹配(可选)
|
|||
|
|
risk_match = risk_level is None or case.get("risk_level") == risk_level
|
|||
|
|
|
|||
|
|
if industry_match and amount_match and risk_match:
|
|||
|
|
similar_cases.append(case)
|
|||
|
|
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 按时间倒序返回最近的案例
|
|||
|
|
similar_cases.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
|||
|
|
return similar_cases[:limit]
|
|||
|
|
|
|||
|
|
def get_case_by_id(self, case_id: str) -> Optional[Dict]:
|
|||
|
|
"""根据 ID 获取案例详情"""
|
|||
|
|
|
|||
|
|
if not self.cases_file.exists():
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
with open(self.cases_file, "r", encoding="utf-8") as f:
|
|||
|
|
for line in f:
|
|||
|
|
if not line.strip():
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
case = json.loads(line)
|
|||
|
|
if case.get("case_id") == case_id:
|
|||
|
|
return case
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def get_team_patterns(self, industry: str = None) -> Dict:
|
|||
|
|
"""提取团队集体经验模式(高频风险点、常见处理方式)"""
|
|||
|
|
|
|||
|
|
if not self.cases_file.exists():
|
|||
|
|
return {"high_frequency_risks": [], "common_solutions": []}
|
|||
|
|
|
|||
|
|
risk_counter = {}
|
|||
|
|
solution_counter = {}
|
|||
|
|
|
|||
|
|
with open(self.cases_file, "r", encoding="utf-8") as f:
|
|||
|
|
for line in f:
|
|||
|
|
if not line.strip():
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
case = json.loads(line)
|
|||
|
|
|
|||
|
|
# 行业过滤
|
|||
|
|
if industry and industry not in case.get("industry", ""):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 统计高频风险点
|
|||
|
|
for risk in case.get("identified_risks", []):
|
|||
|
|
risk_key = risk.get("type", "unknown")
|
|||
|
|
risk_counter[risk_key] = risk_counter.get(risk_key, 0) + 1
|
|||
|
|
|
|||
|
|
# 统计常见解决方案
|
|||
|
|
solution = case.get("solution", "")
|
|||
|
|
if solution:
|
|||
|
|
solution_counter[solution] = solution_counter.get(solution, 0) + 1
|
|||
|
|
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 排序
|
|||
|
|
high_frequency_risks = sorted(
|
|||
|
|
[{"risk": k, "count": v} for k, v in risk_counter.items()],
|
|||
|
|
key=lambda x: x["count"],
|
|||
|
|
reverse=True
|
|||
|
|
)[:10]
|
|||
|
|
|
|||
|
|
common_solutions = sorted(
|
|||
|
|
[{"solution": k, "count": v} for k, v in solution_counter.items()],
|
|||
|
|
key=lambda x: x["count"],
|
|||
|
|
reverse=True
|
|||
|
|
)[:10]
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"high_frequency_risks": high_frequency_risks,
|
|||
|
|
"common_solutions": common_solutions,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def get_statistics(self) -> Dict:
|
|||
|
|
"""获取统计数据"""
|
|||
|
|
|
|||
|
|
if not self.cases_file.exists():
|
|||
|
|
return {"total_cases": 0}
|
|||
|
|
|
|||
|
|
total_cases = 0
|
|||
|
|
industry_dist = {}
|
|||
|
|
approval_rate = {"approved": 0, "rejected": 0, "pending": 0}
|
|||
|
|
|
|||
|
|
with open(self.cases_file, "r", encoding="utf-8") as f:
|
|||
|
|
for line in f:
|
|||
|
|
if not line.strip():
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
case = json.loads(line)
|
|||
|
|
total_cases += 1
|
|||
|
|
|
|||
|
|
# 行业分布
|
|||
|
|
industry = case.get("industry", "未知")
|
|||
|
|
industry_dist[industry] = industry_dist.get(industry, 0) + 1
|
|||
|
|
|
|||
|
|
# 通过率
|
|||
|
|
result = case.get("review_result", "pending")
|
|||
|
|
if result in approval_rate:
|
|||
|
|
approval_rate[result] += 1
|
|||
|
|
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"total_cases": total_cases,
|
|||
|
|
"industry_distribution": industry_dist,
|
|||
|
|
"approval_rate": approval_rate,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局单例
|
|||
|
|
_history_store: Optional[HistoryStore] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_history_store() -> HistoryStore:
|
|||
|
|
"""获取历史案例存储单例"""
|
|||
|
|
global _history_store
|
|||
|
|
if _history_store is None:
|
|||
|
|
_history_store = HistoryStore()
|
|||
|
|
return _history_store
|