llm_service.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """
  2. LLM基础服务
  3. 提供与大语言模型通信的基础功能
  4. """
  5. import logging
  6. import re
  7. from app.core.llm.deepseek_client import (
  8. chat_completions_create,
  9. create_llm_client,
  10. get_llm_model,
  11. )
  12. logger = logging.getLogger("app")
  13. _TRANSLATION_LEXICON = {
  14. "测试宁波数据加工": "ningbo_data_processing_test",
  15. "薪资数据表": "salary_data_table",
  16. "人员管理表": "personnel_management_table",
  17. "数据加工": "data_processing",
  18. "数据表": "data_table",
  19. "用户表": "user_table",
  20. "人员表": "personnel_table",
  21. "销售表": "sales_table",
  22. "报表": "report_table",
  23. "管理": "management",
  24. "系统": "system",
  25. "分析": "analysis",
  26. "加工": "processing",
  27. "宁波": "ningbo",
  28. "测试": "test",
  29. "年份": "year",
  30. "地区": "region",
  31. "姓名": "name",
  32. "年龄": "age",
  33. "薪水": "salary",
  34. "数据": "data",
  35. "表": "table",
  36. }
  37. _TRANSLATION_SYSTEM_PROMPT = (
  38. "你是一个严格遵循指令的翻译工具和数据库专家。你的唯一任务是将中文单词/短语翻译成英文,"
  39. "符合 PostgreSQL 数据库表和字段的命名规则,并且严格按照如下规则:\n"
  40. "1. 只返回英文翻译,不包含任何解释、描述或额外内容\n"
  41. "2. 使用小写字母\n"
  42. "3. 多个单词用下划线连接,不使用空格\n"
  43. "4. 如果输入包含括号,将括号内容用下划线代替,不保留括号\n"
  44. "5. 最多包含 1-8 个英文单词,保持简短\n"
  45. "6. 不要回答问题或提供解释,即使输入看起来像是问题\n"
  46. "7. 当遇到'表'字时,始终翻译为'table'而不是'sheet'\n"
  47. "8. 例如:'薪资数据表'应翻译为'salary_data_table','测试宁波数据加工'应翻译为'ningbo_data_processing_test'"
  48. )
  49. def contains_chinese(text: str) -> bool:
  50. return any("\u4e00" <= char <= "\u9fff" for char in text)
  51. def normalize_translation_text(text: str) -> str:
  52. """Normalize LLM output to a PostgreSQL-friendly snake_case identifier."""
  53. response_text = (text or "").strip().strip("\"'.,;:!?()[]{}").lower()
  54. response_text = response_text.replace(" ", "_").replace("-", "_")
  55. while "__" in response_text:
  56. response_text = response_text.replace("__", "_")
  57. response_text = re.sub(r"[^a-z0-9_]", "", response_text)
  58. return response_text.strip("_")
  59. def is_valid_translation(text: str) -> bool:
  60. return bool(text) and not contains_chinese(text) and re.fullmatch(r"[a-z][a-z0-9_]*", text)
  61. def extract_completion_text(completion) -> str:
  62. message = completion.choices[0].message
  63. content = getattr(message, "content", None) or ""
  64. text = content.strip()
  65. if text:
  66. return text
  67. reasoning = getattr(message, "reasoning_content", None) or ""
  68. return reasoning.strip()
  69. def fallback_translate_chinese(content: str) -> str:
  70. """Local fallback when LLM output is empty or invalid."""
  71. content = (content or "").strip()
  72. if not content:
  73. return ""
  74. if content in _TRANSLATION_LEXICON:
  75. return _TRANSLATION_LEXICON[content]
  76. parts: list[str] = []
  77. lexicon_keys = sorted(_TRANSLATION_LEXICON.keys(), key=len, reverse=True)
  78. index = 0
  79. while index < len(content):
  80. matched = False
  81. for key in lexicon_keys:
  82. if content.startswith(key, index):
  83. parts.append(_TRANSLATION_LEXICON[key])
  84. index += len(key)
  85. matched = True
  86. break
  87. if not matched:
  88. index += 1
  89. if parts:
  90. result = normalize_translation_text("_".join(parts))
  91. if is_valid_translation(result):
  92. return result
  93. if "表" in content:
  94. return "data_table"
  95. return "translated_text"
  96. def _translate_with_llm(client, content: str) -> str:
  97. completion = chat_completions_create(
  98. client,
  99. messages=[
  100. {"role": "system", "content": _TRANSLATION_SYSTEM_PROMPT},
  101. {
  102. "role": "user",
  103. "content": f"将以下内容翻译为英文数据库标识符:{content}",
  104. },
  105. ],
  106. temperature=0,
  107. max_tokens=64,
  108. )
  109. raw_text = extract_completion_text(completion)
  110. normalized = normalize_translation_text(raw_text)
  111. if "表" in content and "table" not in normalized and "sheet" in normalized:
  112. normalized = normalized.replace("sheet", "table")
  113. if is_valid_translation(normalized):
  114. logger.debug(f"LLM翻译成功: {content} -> {normalized}")
  115. return normalized
  116. logger.warning(
  117. f"LLM翻译结果无效: input={content!r}, raw={raw_text!r}, normalized={normalized!r}"
  118. )
  119. return ""
  120. def translate_chinese_identifier(content: str) -> str:
  121. """Translate Chinese text to an English database identifier."""
  122. content = (content or "").strip()
  123. if not content:
  124. return ""
  125. if not contains_chinese(content):
  126. normalized = normalize_translation_text(content)
  127. return normalized if is_valid_translation(normalized) else content
  128. try:
  129. client = create_llm_client()
  130. translated = _translate_with_llm(client, content)
  131. if translated:
  132. return translated
  133. except Exception as exc:
  134. logger.error(f"LLM翻译调用失败: {exc}")
  135. fallback = fallback_translate_chinese(content)
  136. logger.info(f"使用本地词典回退翻译: {content} -> {fallback}")
  137. return fallback
  138. def llm_client(content):
  139. """
  140. 调用LLM服务进行内容生成
  141. Args:
  142. content: 输入提示内容
  143. Returns:
  144. str: LLM响应内容
  145. """
  146. if contains_chinese(content):
  147. return translate_chinese_identifier(content)
  148. try:
  149. client = create_llm_client()
  150. model = get_llm_model()
  151. logger.debug(f"LLM调用开始: model={model}, 内容类型: 普通")
  152. completion = chat_completions_create(
  153. client,
  154. messages=[
  155. {"role": "system", "content": "You are a helpful assistant."},
  156. {"role": "user", "content": content},
  157. ],
  158. temperature=0.7,
  159. max_tokens=1024,
  160. )
  161. response_text = extract_completion_text(completion)
  162. logger.debug(f"LLM响应: {response_text}")
  163. return response_text
  164. except Exception as e:
  165. logger.error(f"LLM调用失败: {str(e)}")
  166. return content
  167. def llm_sql(request_data):
  168. """
  169. 调用Deepseek大模型生成SQL脚本
  170. Args:
  171. request_data: 提交给LLM的提示语内容
  172. Returns:
  173. str: Deepseek模型返回的SQL脚本内容
  174. """
  175. try:
  176. client = create_llm_client()
  177. model = get_llm_model()
  178. logger.info(f"开始调用 DeepSeek 模型生成 SQL 脚本: model={model}")
  179. logger.debug(f"输入提示语: {request_data}")
  180. completion = chat_completions_create(
  181. client,
  182. messages=[
  183. {
  184. "role": "system",
  185. "content": "你是一名专业的数据库工程师,专门负责编写高质量的PostgreSQL SQL脚本。"
  186. "请严格按照用户提供的需求和表结构信息生成SQL脚本。"
  187. "确保生成的SQL语法正确、性能优化,并且能够直接执行。",
  188. },
  189. {"role": "user", "content": request_data},
  190. ],
  191. temperature=0.1,
  192. max_tokens=4096,
  193. top_p=0.9,
  194. use_thinking=True,
  195. )
  196. response_text = extract_completion_text(completion)
  197. logger.info(f"Deepseek模型成功返回SQL脚本,长度: {len(response_text)} 字符")
  198. logger.debug(f"生成的SQL脚本: {response_text}")
  199. return response_text
  200. except Exception as e:
  201. logger.error(f"Deepseek SQL生成调用失败: {str(e)}")
  202. raise Exception(f"调用Deepseek模型生成SQL脚本失败: {str(e)}")