decision_engine.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. from app.config import Settings
  2. from langchain_openai import ChatOpenAI
  3. import json
  4. from app.schemas import (
  5. Evidence,
  6. PlanStep,
  7. QualityGrade,
  8. RetrievalPlan,
  9. RouteDecision,
  10. RouteName,
  11. )
  12. SERVICE_NAME = "aliyun_ssl_vpn"
  13. FAULT_TYPE = "auth_timeout"
  14. CLIENT_OS="windows"
  15. def extract_service(query:str)->str:
  16. """只识别明确提到的阿里云 SSL-VPN。"""
  17. normalized = query.lower().replace(" ", "").replace("-", "")
  18. keywords = (
  19. "阿里云sslvpn",
  20. "aliyunsslvpn",
  21. "ssl-vpn",
  22. )
  23. if any(keyword.replace("-", "") in normalized for keyword in keywords):
  24. return SERVICE_NAME
  25. return ""
  26. def extract_fault_type(query: str) -> str:
  27. """只识别认证超时或认证阶段导致的频繁断线。"""
  28. normalized = query.lower().replace(" ", "")
  29. auth_timeout_keywords = (
  30. "认证超时",
  31. "登录超时",
  32. "身份验证超时",
  33. "认证失败超时",
  34. "auth_timeout",
  35. "authenticationtimeout",
  36. )
  37. if any(keyword in normalized for keyword in auth_timeout_keywords):
  38. return FAULT_TYPE
  39. return ""
  40. def extract_client_os(query: str) -> str:
  41. """返回标准化的 windows 或 macos;无法明确识别时返回空字符串。"""
  42. normalized = query.lower()
  43. windows_keywords = (
  44. "windows",
  45. "win",
  46. )
  47. if any(keyword in normalized for keyword in windows_keywords):
  48. return CLIENT_OS
  49. return ""
  50. class DeepSeekDecisionEngine:
  51. def __init__(self,settings:Settings):
  52. #获取apikey
  53. api_key = settings.deepseek_api_key.get_secret_value()
  54. if not api_key:
  55. raise RuntimeError("LLM_PROVIDER=deepseek 时必须配置 DEEPSEEK_API_KEY")
  56. #非机构化llm
  57. common_kwargs = {
  58. "model": settings.deepseek_model_name,
  59. "api_key": api_key,
  60. "base_url": settings.deepseek_base_url,
  61. "max_retries": 2,
  62. }
  63. answer_thinking = "enabled" if settings.deepseek_answer_thinking else "disabled"
  64. self.llm = ChatOpenAI(
  65. **common_kwargs,
  66. extra_body={"thinking": {"type": answer_thinking}},
  67. )
  68. #结构化llm
  69. self.structured_llm = ChatOpenAI(
  70. **common_kwargs,
  71. temperature=0,
  72. extra_body={"thinking": {"type": "disabled"}},
  73. )
  74. self.router = self.structured_llm.with_structured_output(
  75. RouteDecision,
  76. method="function_calling",
  77. )
  78. self.grader = self.structured_llm.with_structured_output(
  79. QualityGrade,
  80. method="function_calling",
  81. )
  82. def route(self,query:str,max_rounds:int)->RouteDecision:
  83. service = extract_service(query)
  84. fault_type = extract_fault_type(query)
  85. if not service or not fault_type:
  86. return RouteDecision(
  87. needs_retrieval=False,
  88. intent="missing_or_unsupported_vpn_fault",
  89. routes=[RouteName.CLARIFY],
  90. confidence="high",
  91. reason_code="SERVICE_OR_AUTH_TIMEOUT_NOT_CONFIRMED",
  92. max_rounds=max_rounds,
  93. )
  94. prompt = f"""
  95. 你是企业 IT 服务台的故障诊断路由器。只输出符合 Schema 的结果。
  96. 可选路径:direct_answer、milvus_search、sql_query、web_search、clarify、refuse。
  97. 本系统仅支持阿里云 SSL-VPN(service=aliyun_ssl_vpn)的认证超时/
  98. 频繁断线故障(fault_type=auth_timeout)。
  99. 数据源职责:
  100. - 内部 VPN 排障手册、适用操作系统、客户端版本和标准排查步骤:
  101. 使用 milvus_search。
  102. - 最近 30 天同类工单数量、受影响终端、操作系统/客户端版本分布、
  103. 历史解决方式:使用 sql_query。
  104. - 阿里云官网的最新运维事件、故障公告、版本通知:
  105. 使用 web_search。
  106. milvus_search、sql_query、web_search;返回多条路径,并设置
  107. requires_decomposition=true。
  108. 路由限制:
  109. - 用户只要求解释、总结、改写已有文本,且不需要查询外部资料时,
  110. 使用 direct_answer。
  111. - 用户问题没有明确阿里云 SSL-VPN,或没有明确认证超时、频繁断线、
  112. 登录超时等现象时,使用 clarify。
  113. - 用户要求修改 VPN 配置、关闭 MFA、重置账号、执行网络变更,
  114. 或请求密码、验证码、密钥等敏感信息时,使用 refuse。
  115. - 不要为不支持的 IT 服务选择检索路径。
  116. filters 中只填写用户问题中明确出现且可确定的字段:
  117. service、fault_type、client_os、client_version、region。
  118. 不要猜测、补全或编造这些字段。
  119. 若问题明确属于支持范围,service 固定为 aliyun_ssl_vpn,
  120. fault_type 固定为 auth_timeout。
  121. 最大检索轮数:{max_rounds}
  122. 用户问题:{query}
  123. """.strip()
  124. result=self.router.invoke(prompt)
  125. if result is None:
  126. raise RuntimeError("路由器未返回结果,请检查 LLM 配置和网络连接。")
  127. deterministic_filters = {
  128. key: value
  129. for key, value in {
  130. "service": service,
  131. "fault_type": fault_type,
  132. "client_os": extract_client_os(query),
  133. }.items()
  134. if value
  135. }
  136. result.filters = {**result.filters, **deterministic_filters}
  137. result.max_rounds = max_rounds
  138. return result
  139. def plan(self,query:str,decision:RouteDecision)->RetrievalPlan:
  140. filters=decision.filters
  141. steps:list[PlanStep] = []
  142. service = str(filters.get("service") or "")
  143. fault_type = str(filters.get("fault_type") or "")
  144. client_os = str(filters.get("client_os") or "")
  145. if RouteName.MILVUS_SEARCH in decision.routes:
  146. steps.append(
  147. PlanStep(
  148. id="milvus_search",
  149. tool=RouteName.MILVUS_SEARCH,
  150. query=query,
  151. arguments={
  152. "service": service,
  153. "fault_type":fault_type,
  154. "client_os":client_os,
  155. "top_k": 4,
  156. },
  157. )
  158. )
  159. if RouteName.SQL_QUERY in decision.routes:
  160. steps.append(
  161. PlanStep(
  162. id="sql_query",
  163. tool=RouteName.SQL_QUERY,
  164. query=query,
  165. arguments={
  166. "service": service,
  167. "fault_type": fault_type,
  168. "client_os": client_os,
  169. "days": 30,
  170. },
  171. )
  172. )
  173. if RouteName.WEB_SEARCH in decision.routes:
  174. steps.append(
  175. PlanStep(
  176. id="web_search",
  177. tool=RouteName.WEB_SEARCH,
  178. query=query,
  179. arguments={
  180. "max_results": 3,
  181. },
  182. )
  183. )
  184. return RetrievalPlan(
  185. goal=query,
  186. steps=steps
  187. )
  188. def grade(self,query:str,decision:RouteDecision,
  189. evidence:list[Evidence],current_round:int,
  190. max_rounds:int,
  191. min_score:float)->QualityGrade:
  192. prompt = f"""
  193. 判断证据是否足以回答问题。recommended_action只能是accept、rewrite_query 或 stop。
  194. 当前轮次:{current_round}/{max_rounds}
  195. 问题:{query}
  196. 路由:{decision.model_dump_json()}
  197. 证据:{json.dumps([item.model_dump() for item in evidence],ensure_ascii=False)}
  198. """.strip()
  199. result=self.grader.invoke(prompt)
  200. return result
  201. def rewrite(self,query:str,grade:QualityGrade)->str:
  202. missing=grade.missing_aspects
  203. return f"{query};补充条件:{missing}"
  204. def answer(self,query:str,evidence:list[Evidence],partial:bool)->str:
  205. prompt=f"""
  206. 你是企业 IT 服务台的故障诊断专家。严格依据证据回答,不得补充证据之外的事实。每个关键结论使用 [序号] 引用
  207. 问题:{query}
  208. 是否为部分证据: {partial}
  209. 证据:{json.dumps([item.model_dump() for item in evidence],ensure_ascii=False)}
  210. """.strip()
  211. return str(self.llm.invoke(prompt).content)
  212. def create_decision_engine(settings:Settings)->DeepSeekDecisionEngine:
  213. return DeepSeekDecisionEngine(settings)