feat: add online evolution loop and 5% gray risk-policy rollout
This commit is contained in:
@@ -11,6 +11,8 @@ import asyncio
|
||||
import random
|
||||
import hashlib
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Any, Tuple
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -162,6 +164,7 @@ class CustomerServiceAgent:
|
||||
C_TOOL = "\033[93m" # yellow
|
||||
C_REPLY = "\033[92m" # green
|
||||
C_MUTED = "\033[90m" # gray
|
||||
_DEFAULT_EVOLUTION_CANDIDATE = Path("config") / "evolution_candidate.json"
|
||||
|
||||
def __init__(self, skills_dir: str = "skills"):
|
||||
self.api_key = os.getenv("OPENAI_API_KEY")
|
||||
@@ -175,6 +178,7 @@ class CustomerServiceAgent:
|
||||
self.conversations: Dict[str, ConversationState] = {}
|
||||
# 多轮对话历史(PydanticAI ModelMessage 列表,按客户ID存储)
|
||||
self.message_histories: Dict[str, list] = {}
|
||||
self.evolution_candidate = self._load_evolution_candidate()
|
||||
|
||||
# 加载 skills 内容
|
||||
self.skills_content = load_skill_md(skills_dir)
|
||||
@@ -230,6 +234,64 @@ class CustomerServiceAgent:
|
||||
# 注册工具
|
||||
self._register_tools()
|
||||
|
||||
def _load_evolution_candidate(self) -> Dict[str, Any]:
|
||||
"""读取自我进化候选配置(灰度策略),读取失败时返回空。"""
|
||||
try:
|
||||
path = Path(os.getenv("EVOLUTION_CANDIDATE_PATH", str(self._DEFAULT_EVOLUTION_CANDIDATE)))
|
||||
if not path.exists():
|
||||
return {}
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return data
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _evolution_gray_percent(self) -> int:
|
||||
"""灰度比例,默认 5%。"""
|
||||
try:
|
||||
env_pct = os.getenv("EVOLUTION_GRAY_PERCENT", "").strip()
|
||||
if env_pct:
|
||||
pct = int(float(env_pct))
|
||||
else:
|
||||
pct = int(((self.evolution_candidate or {}).get("gray_percent", 5)))
|
||||
return max(0, min(100, pct))
|
||||
except Exception:
|
||||
return 5
|
||||
|
||||
def _evolution_enabled_for_customer(self, customer_id: str) -> bool:
|
||||
"""按客户哈希稳定灰度命中,命中后启用候选策略。"""
|
||||
cand = self.evolution_candidate or {}
|
||||
if str(cand.get("status", "")).strip() != "ready_for_gray_5_percent":
|
||||
return False
|
||||
if not customer_id:
|
||||
return False
|
||||
pct = self._evolution_gray_percent()
|
||||
if pct <= 0:
|
||||
return False
|
||||
digest = hashlib.md5(customer_id.encode("utf-8")).hexdigest()
|
||||
bucket = int(digest[:8], 16) % 100
|
||||
hit = bucket < pct
|
||||
if hit:
|
||||
metrics_emit("evolution_gray_hit", customer_id=customer_id, percent=pct, version=str(cand.get("version", "")))
|
||||
return hit
|
||||
|
||||
def _evolution_has_proposal(self, proposal_id: str) -> bool:
|
||||
cand = self.evolution_candidate or {}
|
||||
for p in cand.get("proposals", []) or []:
|
||||
if str((p or {}).get("id", "")).strip() == proposal_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_service_risk_inquiry(text: str) -> bool:
|
||||
"""识别退款/投诉等服务风险场景。"""
|
||||
s = (text or "").strip().lower()
|
||||
if not s:
|
||||
return False
|
||||
kw = ("退款", "退货", "投诉", "差评", "举报", "欺骗", "骗人", "起诉", "法院", "生气", "不满意")
|
||||
return any(k in s for k in kw)
|
||||
|
||||
@staticmethod
|
||||
def _log_block(title: str, content: str):
|
||||
"""统一的控制台分层日志输出。"""
|
||||
@@ -1637,6 +1699,17 @@ class CustomerServiceAgent:
|
||||
transfer_msg = TRANSFER_MESSAGE
|
||||
metrics_emit("transfer_to_human", customer_id=message.from_id, acc_id=message.acc_id)
|
||||
|
||||
# 自我进化候选策略灰度(默认 5%):风险投诉场景强制转人工,并补安抚话术
|
||||
evo_hit = self._evolution_enabled_for_customer(message.from_id)
|
||||
if evo_hit and self._is_service_risk_inquiry(message.msg):
|
||||
if self._evolution_has_proposal("policy-risk-transfer"):
|
||||
need_transfer = True
|
||||
transfer_msg = TRANSFER_MESSAGE
|
||||
metrics_emit("evolution_force_transfer", customer_id=message.from_id, acc_id=message.acc_id)
|
||||
if self._evolution_has_proposal("tone-empathy-pack"):
|
||||
reply_text = "抱歉让您不舒服了,这边先为您转接人工专员马上处理。"
|
||||
metrics_emit("evolution_empathy_reply", customer_id=message.from_id, acc_id=message.acc_id)
|
||||
|
||||
# 未成交记录:客户表达放弃且已报价过(转人工不记录)
|
||||
customer_text, _ = self._split_customer_text(message.msg)
|
||||
no_convert_keywords = ["算了", "不要了", "不做了", "下次再说", "先不弄了"]
|
||||
@@ -1649,6 +1722,8 @@ class CustomerServiceAgent:
|
||||
|
||||
# 需要转接时不把原始回复发给客户
|
||||
should_reply = bool(reply_text and reply_text.strip()) and not need_transfer
|
||||
if evo_hit and need_transfer and self._evolution_has_proposal("tone-empathy-pack"):
|
||||
should_reply = True
|
||||
|
||||
# 记录本次回复时间,供冷却期判断
|
||||
if should_reply:
|
||||
|
||||
Reference in New Issue
Block a user