from app.config import Settings from langchain_openai import ChatOpenAI import json from app.schemas import ( Evidence, PlanStep, QualityGrade, RetrievalPlan, RouteDecision, RouteName, ) SERVICE_NAME = "aliyun_ssl_vpn" FAULT_TYPE = "auth_timeout" CLIENT_OS="windows" def extract_service(query:str)->str: """只识别明确提到的阿里云 SSL-VPN。""" normalized = query.lower().replace(" ", "").replace("-", "") keywords = ( "阿里云sslvpn", "aliyunsslvpn", "ssl-vpn", ) if any(keyword.replace("-", "") in normalized for keyword in keywords): return SERVICE_NAME return "" def extract_fault_type(query: str) -> str: """只识别认证超时或认证阶段导致的频繁断线。""" normalized = query.lower().replace(" ", "") auth_timeout_keywords = ( "认证超时", "登录超时", "身份验证超时", "认证失败超时", "auth_timeout", "authenticationtimeout", ) if any(keyword in normalized for keyword in auth_timeout_keywords): return FAULT_TYPE return "" def extract_client_os(query: str) -> str: """返回标准化的 windows 或 macos;无法明确识别时返回空字符串。""" normalized = query.lower() windows_keywords = ( "windows", "win", ) if any(keyword in normalized for keyword in windows_keywords): return CLIENT_OS return "" class DeepSeekDecisionEngine: def __init__(self,settings:Settings): #获取apikey api_key = settings.deepseek_api_key.get_secret_value() if not api_key: raise RuntimeError("LLM_PROVIDER=deepseek 时必须配置 DEEPSEEK_API_KEY") #非机构化llm common_kwargs = { "model": settings.deepseek_model_name, "api_key": api_key, "base_url": settings.deepseek_base_url, "max_retries": 2, } answer_thinking = "enabled" if settings.deepseek_answer_thinking else "disabled" self.llm = ChatOpenAI( **common_kwargs, extra_body={"thinking": {"type": answer_thinking}}, ) #结构化llm self.structured_llm = ChatOpenAI( **common_kwargs, temperature=0, extra_body={"thinking": {"type": "disabled"}}, ) self.router = self.structured_llm.with_structured_output( RouteDecision, method="function_calling", ) self.grader = self.structured_llm.with_structured_output( QualityGrade, method="function_calling", ) def route(self,query:str,max_rounds:int)->RouteDecision: service = extract_service(query) fault_type = extract_fault_type(query) if not service or not fault_type: return RouteDecision( needs_retrieval=False, intent="missing_or_unsupported_vpn_fault", routes=[RouteName.CLARIFY], confidence="high", reason_code="SERVICE_OR_AUTH_TIMEOUT_NOT_CONFIRMED", max_rounds=max_rounds, ) prompt = f""" 你是企业 IT 服务台的故障诊断路由器。只输出符合 Schema 的结果。 可选路径:direct_answer、milvus_search、sql_query、web_search、clarify、refuse。 本系统仅支持阿里云 SSL-VPN(service=aliyun_ssl_vpn)的认证超时/ 频繁断线故障(fault_type=auth_timeout)。 数据源职责: - 内部 VPN 排障手册、适用操作系统、客户端版本和标准排查步骤: 使用 milvus_search。 - 最近 30 天同类工单数量、受影响终端、操作系统/客户端版本分布、 历史解决方式:使用 sql_query。 - 阿里云官网的最新运维事件、故障公告、版本通知: 使用 web_search。 milvus_search、sql_query、web_search;返回多条路径,并设置 requires_decomposition=true。 路由限制: - 用户只要求解释、总结、改写已有文本,且不需要查询外部资料时, 使用 direct_answer。 - 用户问题没有明确阿里云 SSL-VPN,或没有明确认证超时、频繁断线、 登录超时等现象时,使用 clarify。 - 用户要求修改 VPN 配置、关闭 MFA、重置账号、执行网络变更, 或请求密码、验证码、密钥等敏感信息时,使用 refuse。 - 不要为不支持的 IT 服务选择检索路径。 filters 中只填写用户问题中明确出现且可确定的字段: service、fault_type、client_os、client_version、region。 不要猜测、补全或编造这些字段。 若问题明确属于支持范围,service 固定为 aliyun_ssl_vpn, fault_type 固定为 auth_timeout。 最大检索轮数:{max_rounds} 用户问题:{query} """.strip() result=self.router.invoke(prompt) if result is None: raise RuntimeError("路由器未返回结果,请检查 LLM 配置和网络连接。") deterministic_filters = { key: value for key, value in { "service": service, "fault_type": fault_type, "client_os": extract_client_os(query), }.items() if value } result.filters = {**result.filters, **deterministic_filters} result.max_rounds = max_rounds return result def plan(self,query:str,decision:RouteDecision)->RetrievalPlan: filters=decision.filters steps:list[PlanStep] = [] service = str(filters.get("service") or "") fault_type = str(filters.get("fault_type") or "") client_os = str(filters.get("client_os") or "") if RouteName.MILVUS_SEARCH in decision.routes: steps.append( PlanStep( id="milvus_search", tool=RouteName.MILVUS_SEARCH, query=query, arguments={ "service": service, "fault_type":fault_type, "client_os":client_os, "top_k": 4, }, ) ) if RouteName.SQL_QUERY in decision.routes: steps.append( PlanStep( id="sql_query", tool=RouteName.SQL_QUERY, query=query, arguments={ "service": service, "fault_type": fault_type, "client_os": client_os, "days": 30, }, ) ) if RouteName.WEB_SEARCH in decision.routes: steps.append( PlanStep( id="web_search", tool=RouteName.WEB_SEARCH, query=query, arguments={ "max_results": 3, }, ) ) return RetrievalPlan( goal=query, steps=steps ) def grade(self,query:str,decision:RouteDecision, evidence:list[Evidence],current_round:int, max_rounds:int, min_score:float)->QualityGrade: prompt = f""" 判断证据是否足以回答问题。recommended_action只能是accept、rewrite_query 或 stop。 当前轮次:{current_round}/{max_rounds} 问题:{query} 路由:{decision.model_dump_json()} 证据:{json.dumps([item.model_dump() for item in evidence],ensure_ascii=False)} """.strip() result=self.grader.invoke(prompt) return result def rewrite(self,query:str,grade:QualityGrade)->str: missing=grade.missing_aspects return f"{query};补充条件:{missing}" def answer(self,query:str,evidence:list[Evidence],partial:bool)->str: prompt=f""" 你是企业 IT 服务台的故障诊断专家。严格依据证据回答,不得补充证据之外的事实。每个关键结论使用 [序号] 引用 问题:{query} 是否为部分证据: {partial} 证据:{json.dumps([item.model_dump() for item in evidence],ensure_ascii=False)} """.strip() return str(self.llm.invoke(prompt).content) def create_decision_engine(settings:Settings)->DeepSeekDecisionEngine: return DeepSeekDecisionEngine(settings)