model_config.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. # -*- coding: utf-8 -*-
  2. """
  3. 运行时模型配置管理器 —— 支持 API 动态切换模型(仅影响 chat_routes 下的智能体)
  4. click_routes 下的智能体和 fast 模式不受影响,继续从 .env 读取固定模型。
  5. 设计:
  6. - 模块级变量存储当前模型名,从 .env 初始化
  7. - 切换时仅更新内存中的配置,不写回 .env(重启恢复默认)
  8. - 线程安全:使用 threading.Lock 保护读写
  9. """
  10. import threading
  11. from pathlib import Path as _Path
  12. # 可用模型列表
  13. AVAILABLE_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"]
  14. # ── 思考级别配置 ──
  15. THINKING_LEVELS = ["off", "high", "highest"]
  16. # 各思考级别对应的 extra_body(透传至 DeepSeek API)
  17. # 参考:https://api-docs.deepseek.com/guides/thinking_mode
  18. # - thinking: {"type": "disabled"} → 关闭思考链
  19. # - thinking: {"type": "enabled"} → 开启思考链(deepseek-chat V3 系列)
  20. # - reasoning_effort: "low"/"high"/"max" → 思考深度(deepseek-reasoner R1 系列)
  21. # 注:V3 系列同时支持 thinking.type 和 reasoning_effort(新版 API)
  22. THINKING_KWARGS_MAP = {
  23. "off": {"thinking": {"type": "disabled"}},
  24. "high": {"thinking": {"type": "enabled"}},
  25. "highest": {"thinking": {"type": "enabled"}, "reasoning_effort": "max"},
  26. }
  27. # 从 .env 读取初始模型名
  28. _env_file = _Path(__file__).parent.parent / ".env"
  29. try:
  30. from dotenv import dotenv_values
  31. _cfg = dotenv_values(str(_env_file))
  32. _initial_model = _cfg.get("DEEPAGENT_MODEL", "deepseek-v4-pro").strip().strip('"').strip("'")
  33. if _initial_model not in AVAILABLE_MODELS:
  34. _initial_model = "deepseek-v4-pro"
  35. except Exception:
  36. _initial_model = "deepseek-v4-pro"
  37. _current_model: str = _initial_model
  38. _thinking_level: str = "off"
  39. _lock = threading.Lock()
  40. # ── 模型实例缓存(切换后自动失效)──
  41. _cached_model_instance = None
  42. _cached_model_name: str | None = None
  43. _cached_thinking_level: str | None = None
  44. def get_current_model() -> str:
  45. """获取当前运行时模型名(线程安全)。"""
  46. with _lock:
  47. return _current_model
  48. def set_current_model(model_name: str) -> str:
  49. """设置当前运行时模型名(线程安全)。
  50. 校验模型名在可用列表中,更新配置并清除模型实例缓存。
  51. Args:
  52. model_name: 模型名称,如 "deepseek-v4-pro" 或 "deepseek-v4-flash"
  53. Returns:
  54. 设置后的模型名
  55. Raises:
  56. ValueError: 模型名不在可用列表中
  57. """
  58. global _current_model, _cached_model_instance, _cached_model_name
  59. model_name = model_name.strip().strip('"').strip("'")
  60. if model_name not in AVAILABLE_MODELS:
  61. raise ValueError(
  62. f"不支持的模型: {model_name},可用模型: {', '.join(AVAILABLE_MODELS)}"
  63. )
  64. with _lock:
  65. _current_model = model_name
  66. # 清除模型实例缓存,下次调用 get_model_instance() 时重新创建
  67. _cached_model_instance = None
  68. _cached_model_name = None
  69. return model_name
  70. def get_available_models() -> list[str]:
  71. """返回可用模型列表。"""
  72. return list(AVAILABLE_MODELS)
  73. def get_thinking_level() -> str:
  74. """获取当前思考级别(线程安全)。"""
  75. with _lock:
  76. return _thinking_level
  77. def set_thinking_level(level: str) -> str:
  78. """设置思考级别(线程安全)。
  79. Args:
  80. level: "off" / "high" / "highest"
  81. Returns:
  82. 设置后的级别名
  83. Raises:
  84. ValueError: 级别不在可用列表中
  85. """
  86. global _thinking_level
  87. level = level.strip().lower()
  88. if level not in THINKING_LEVELS:
  89. raise ValueError(
  90. f"不支持的思考级别: {level},可用级别: {', '.join(THINKING_LEVELS)}"
  91. )
  92. # 清除模型实例缓存(因为 model_kwargs 变了)
  93. invalidate_model_cache()
  94. with _lock:
  95. _thinking_level = level
  96. return level
  97. def get_available_thinking_levels() -> list[str]:
  98. """返回可用思考级别列表。"""
  99. return list(THINKING_LEVELS)
  100. def invalidate_model_cache():
  101. """清除模型实例缓存(由各 Agent 模块切换时调用)。"""
  102. global _cached_model_instance, _cached_model_name, _cached_thinking_level
  103. with _lock:
  104. _cached_model_instance = None
  105. _cached_model_name = None
  106. _cached_thinking_level = None
  107. def get_model_instance():
  108. """获取当前运行时模型的 LangChain ChatModel 实例。
  109. 复用 _get_model() 的 Base URL / API Key / 超时逻辑,
  110. 但模型名来自运行时配置而非 .env。
  111. 同时根据当前思考级别传入对应的 model_kwargs。
  112. 返回的实例会被缓存,直到模型或思考级别切换时自动失效。
  113. """
  114. global _cached_model_instance, _cached_model_name, _cached_thinking_level
  115. current = get_current_model()
  116. think_level = get_thinking_level()
  117. # 缓存命中:模型未变 + 思考级别未变 + 实例已创建
  118. if (_cached_model_instance is not None
  119. and _cached_model_name == current
  120. and _cached_thinking_level == think_level):
  121. print(f"[模型] 缓存命中: {current} | 思考级别: {think_level}")
  122. return _cached_model_instance
  123. # 读取 .env 中的 API 配置(Base URL / Key / Timeout 固定不变)
  124. from dotenv import dotenv_values
  125. from langchain.chat_models import init_chat_model
  126. _cfg = dotenv_values(str(_env_file))
  127. base_url = _cfg.get("OPENAI_BASE_URL", "")
  128. api_key = _cfg.get("OPENAI_API_KEY", "")
  129. timeout_str = _cfg.get("OPENAI_TIMEOUT", "180")
  130. # 清理可能带入的引号
  131. api_key = api_key.strip().strip('"').strip("'") if api_key else ""
  132. base_url = base_url.strip().strip('"').strip("'") if base_url else ""
  133. # 解析超时时间
  134. try:
  135. timeout = float(timeout_str.strip())
  136. except (ValueError, TypeError):
  137. timeout = 180.0
  138. # 确保模型字符串带有 provider 前缀
  139. model_str = current
  140. if ":" not in model_str:
  141. model_str = f"openai:{model_str}"
  142. # 构建思考级别对应的参数
  143. # ChatOpenAI 有独立的 reasoning_effort 字段(直接透传至 API 顶层)
  144. # thinking 等 DeepSeek 特有参数必须通过 extra_body 传输
  145. thinking_cfg = dict(THINKING_KWARGS_MAP.get(think_level, {}))
  146. # 分离:reasoning_effort → 直接传参,其余 → extra_body
  147. direct_kwargs = {}
  148. extra_body_params = {}
  149. if "reasoning_effort" in thinking_cfg:
  150. direct_kwargs["reasoning_effort"] = thinking_cfg.pop("reasoning_effort")
  151. extra_body_params = thinking_cfg # 剩余的(如 thinking)
  152. if direct_kwargs or extra_body_params:
  153. print(f"[模型] 创建实例: {model_str} | 思考级别: {think_level} | direct: {direct_kwargs} | extra_body: {extra_body_params}")
  154. if base_url and api_key:
  155. instance = init_chat_model(
  156. model_str,
  157. openai_api_key=api_key,
  158. openai_api_base=base_url,
  159. temperature=0,
  160. timeout=timeout,
  161. **direct_kwargs,
  162. model_kwargs={"extra_body": extra_body_params} if extra_body_params else {},
  163. )
  164. else:
  165. instance = init_chat_model(
  166. model_str,
  167. temperature=0,
  168. timeout=timeout,
  169. **direct_kwargs,
  170. model_kwargs={"extra_body": extra_body_params} if extra_body_params else {},
  171. )
  172. # 缓存实例
  173. with _lock:
  174. _cached_model_instance = instance
  175. _cached_model_name = current
  176. _cached_thinking_level = think_level
  177. return instance