perf: ollama 彻底惰性导入(模块级+实例级),AICLI 启动不再卡 ollama

This commit is contained in:
shuiyu67 committed 2026-08-07 00:19:49 +08:00
1 parent b7e87e40f3
commit 2e4093cc03
2 files changed
+118 -23

No files matched your search

+48 -23
View File
@@ -7,15 +7,27 @@ import threading
import time import time
from colorama import Fore, Style from colorama import Fore, Style
# 延迟导入 ollama,避免程序启动时卡住 # 真正的惰性导入:只有真正用到 ollama 引擎时才 import ollama,
# 如果导入失败,ollama_client 将为 None # 避免 xiaoli-cli 启动 / 加载所有引擎时因 ollama SDK 初始化
try: # (其内部 platform.machine() -> WMI 查询)在某些环境下卡住。
import ollama _ollama_client = None
ollama_client = ollama _ollama_import_tried = False
except Exception as e:
print(f"{Fore.YELLOW}警告: 无法导入 ollama 库: {e}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}请确保已安装 ollama 包 (pip install ollama){Style.RESET_ALL}") def _ollama():
ollama_client = None """惰性获取 ollama SDK 客户端;导入失败返回 None(仅尝试一次)。"""
global _ollama_client, _ollama_import_tried
if _ollama_import_tried:
return _ollama_client
_ollama_import_tried = True
try:
import ollama
_ollama_client = ollama
except Exception as e:
_ollama_client = None
print(f"{Fore.YELLOW}警告: 无法导入 ollama 库: {e}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}请确保已安装 ollama 包 (pip install ollama){Style.RESET_ALL}")
return _ollama_client
# 添加项目根目录到sys.path # 添加项目根目录到sys.path
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -121,13 +133,10 @@ Ollama AI引擎插件帮助信息
self.thinking_start_marker = "<thinking>" self.thinking_start_marker = "<thinking>"
self.thinking_end_marker = "</thinking>" self.thinking_end_marker = "</thinking>"
# 检测Ollama服务是否运行 # 惰性服务检测:不在构造时急切点服务(避免无 ollama / 沙箱环境下
self.is_service_running = self.check_ollama_service() # import ollama 卡顿)。首次真正使用时(generate_response 等)才检测并缓存。
self._service_checked = False
if self.is_service_running: self._service_running = False
_vprint(f"{Fore.GREEN}Ollama服务检测成功,引擎已启用{Style.RESET_ALL}")
else:
print(f"{Fore.RED}Ollama服务未运行,引擎将不可用{Style.RESET_ALL}")
@staticmethod @staticmethod
def _parse_num_ctx(value): def _parse_num_ctx(value):
@@ -149,11 +158,11 @@ Ollama AI引擎插件帮助信息
def check_ollama_service(self): def check_ollama_service(self):
"""检测Ollama服务是否运行""" """检测Ollama服务是否运行"""
# 检查 ollama 库是否可用 # 检查 ollama 库是否可用
if ollama_client is None: if _ollama() is None:
return False return False
try: try:
# 尝试调用Ollama API获取版本信息 # 尝试调用Ollama API获取版本信息
response = ollama_client.list() response = _ollama().list()
# 如果没有异常,说明服务运行正常 # 如果没有异常,说明服务运行正常
return True return True
except Exception as e: except Exception as e:
@@ -166,6 +175,22 @@ Ollama AI引擎插件帮助信息
本地 Ollama 由配置 max_token_k 换算的 num_ctx 决定。""" 本地 Ollama 由配置 max_token_k 换算的 num_ctx 决定。"""
return self.num_ctx return self.num_ctx
@property
def is_service_running(self):
"""惰性检测 Ollama 服务是否运行:首次访问时才 import ollama 并点服务。
这样 AICLI() 构造 / 加载所有引擎时不会因 import ollama 卡住
(沙箱或 ollama 未安装的环境也能正常启动 CLI,仅真正选用本引擎时才校验)。
"""
if not self._service_checked:
self._service_running = self.check_ollama_service()
self._service_checked = True
if self._service_running:
_vprint(f"{Fore.GREEN}Ollama服务检测成功,引擎已启用{Style.RESET_ALL}")
else:
print(f"{Fore.RED}Ollama服务未运行,引擎将不可用{Style.RESET_ALL}")
return self._service_running
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None): def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
""" """
生成AI响应 - 使用Ollama API 生成AI响应 - 使用Ollama API
@@ -210,12 +235,12 @@ Ollama AI引擎插件帮助信息
# 使用Ollama Python库调用API(FC 不支持时降级为无工具重试一次) # 使用Ollama Python库调用API(FC 不支持时降级为无工具重试一次)
try: try:
response = ollama_client.chat(**kwargs) response = _ollama().chat(**kwargs)
except Exception as _fc_err: except Exception as _fc_err:
if fc_enabled and "tool" in str(_fc_err).lower(): if fc_enabled and "tool" in str(_fc_err).lower():
self._mark_fc_unsupported() self._mark_fc_unsupported()
kwargs.pop("tools", None) kwargs.pop("tools", None)
response = ollama_client.chat(**kwargs) response = _ollama().chat(**kwargs)
else: else:
raise raise
@@ -377,7 +402,7 @@ Ollama AI引擎插件帮助信息
}) })
# 使用Ollama Python库调用API # 使用Ollama Python库调用API
response = ollama_client.chat( response = _ollama().chat(
model=self.model, model=self.model,
messages=messages, messages=messages,
stream=False stream=False
@@ -400,7 +425,7 @@ Ollama AI引擎插件帮助信息
return ["错误:Ollama服务未运行"] return ["错误:Ollama服务未运行"]
try: try:
response = ollama_client.list() response = _ollama().list()
models = [model["name"] for model in response["models"]] models = [model["name"] for model in response["models"]]
return models return models
except Exception as e: except Exception as e:
@@ -480,7 +505,7 @@ Ollama AI引擎插件帮助信息
print(f"{Fore.YELLOW}正在拉取模型 {model_name}...{Style.RESET_ALL}") print(f"{Fore.YELLOW}正在拉取模型 {model_name}...{Style.RESET_ALL}")
# 使用Ollama Python库拉取模型 # 使用Ollama Python库拉取模型
response = ollama_client.pull(model_name, stream=True) response = _ollama().pull(model_name, stream=True)
# 逐块处理响应 # 逐块处理响应
for part in response: for part in response:
+70
View File
@@ -0,0 +1,70 @@
"""
回归测试:ollama 必须惰性导入。
原因:ollama SDK 在 import 时会触发平台探测(platform.machine() → WMI),
在部分沙箱/未安装 ollama 的环境会卡死或拖慢启动。
修复后:
- ai_engines/ollama_engine 模块顶层不得 import ollama;
- OllamaAI.__init__ 不得急切点服务(is_service_running 改为惰性 property),
只有真正选用 ollama 引擎时才 import ollama。
本测试只 import 模块、不构造 AICLI(避免触发 LSP 连接等环境依赖),
因此可在任意沙箱稳定运行,专门守住「ollama 惰性导入」这一改动。
"""
import sys
import types
import builtins
PROJECT_ROOT = sys.path[0] if sys.path else ""
# 兜底:确保能找到仓库根
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
def test_ollama_engine_module_import_is_lazy():
"""import ai_engines.ollama_engine 不应触发 import ollama。"""
imported = []
real_import = builtins.__import__
def traced(name, *a, **k):
if name == "ollama" or name.startswith("ollama."):
imported.append(name)
return real_import(name, *a, **k)
builtins.__import__ = traced
try:
import ai_engines.ollama_engine as oe
# 旧顶层 ollama_client 应已移除
assert not hasattr(oe, "ollama_client"), \
"旧的顶层 ollama_client 应已移除,改为惰性 _ollama() getter"
# 应有惰性 getter
assert hasattr(oe, "_ollama"), "应有惰性 _ollama() getter"
# 模块被 import 后不应已有 ollama 真身
assert oe._ollama_import_tried is False, \
"模块加载完成时尚不应尝试 import ollama(惰性)"
finally:
builtins.__import__ = real_import
assert imported == [], \
f"import ai_engines.ollama_engine 不应触发 import ollama,实际 {imported}"
def test_ollama_sdk_only_imported_on_use():
"""调用 _ollama() 才真正 import ollama;未调用前仍是惰性。"""
# 用一个假 ollama 顶替,避免真实 SDK / WMI;只验证「调用时才触发」
fake = types.ModuleType("ollama")
fake.list = lambda *a, **k: []
fake.chat = lambda *a, **k: {}
fake.pull = lambda *a, **k: []
sys.modules["ollama"] = fake
import ai_engines.ollama_engine as oe
assert oe._ollama_import_tried is False
client = oe._ollama() # 首次调用
assert oe._ollama_import_tried is True
assert client is fake # 取回的是我们注入的 ollama
# 第二次调用应命中缓存,不重复 import
again = oe._ollama()
assert again is fake