Skip to content

Commit fc2aef2

Browse files
committed
fix: roll back failed chat model switches
1 parent c4147d5 commit fc2aef2

4 files changed

Lines changed: 162 additions & 6 deletions

File tree

src/chat/core.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,27 @@ def _initialize_vector_store(self):
6565
def reload_llm(self) -> bool:
6666
try:
6767
llm_key = self.session_config.active_llm_configuration
68-
self.llm_model = ModelProviderFactory.get_llm_provider(llm_key)
68+
llm_model = ModelProviderFactory.get_llm_provider(llm_key)
6969
if self.retrieval_service is None:
7070
raise RuntimeError("检索服务尚未初始化。")
71-
self.chat_service = ChatService(self.llm_model, self.retrieval_service)
71+
chat_service = ChatService(llm_model, self.retrieval_service)
72+
self.llm_model = llm_model
73+
self.chat_service = chat_service
7274
return True
7375
except Exception as exc:
7476
self.console.print(f"[bold red]重载 LLM 出错: {exc}[/bold red]")
7577
return False
7678

79+
def apply_config_update(self, updated_config: SessionConfig | Dict[str, Any], llm_needs_reload: bool) -> None:
80+
previous_llm = self.session_config.active_llm_configuration
81+
self.chat_config = updated_config
82+
if not llm_needs_reload:
83+
return
84+
if self.reload_llm():
85+
return
86+
self.session_config.active_llm_configuration = previous_llm
87+
self.console.print("[bold yellow]LLM 切换失败,已保留当前模型配置。[/bold yellow]")
88+
7789
async def _identify_intent_async(self, user_query: str) -> str:
7890
if getattr(self, "chat_service", None) is None:
7991
return user_query
@@ -159,13 +171,16 @@ async def start_chat_session_async():
159171
break
160172

161173
if user_query.lower() == "/config":
174+
editable_config = (
175+
bot.chat_config.to_dict()
176+
if isinstance(bot.chat_config, SessionConfig)
177+
else dict(bot.chat_config)
178+
)
162179
llm_needs_reload, updated_config = await asyncio.to_thread(
163180
launch_config_editor,
164-
bot.chat_config,
181+
editable_config,
165182
)
166-
bot.chat_config = updated_config
167-
if llm_needs_reload:
168-
bot.reload_llm()
183+
bot.apply_config_update(updated_config, llm_needs_reload)
169184
display_chat_config(console, bot.chat_config)
170185
continue
171186

src/retrieval_test/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ async def run_retrieval_test_async():
7878
except (KeyboardInterrupt, EOFError):
7979
break
8080
except Exception as exc:
81+
console.print(f"[red]召回测试出错: {exc}[/red]")
8182
logger.error("召回测试出错: %s", exc)
8283

8384

tests/chat/test_core.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from rich.console import Console
55

6+
from src.runtime.contracts import SessionConfig
7+
from src.utils.config import ModelDetail, RetrievalMethod
68
from src.chat.core import Chatbot, start_chat_session_async
79

810

@@ -40,6 +42,9 @@ def __init__(self, _console: Console):
4042
def reload_llm(self):
4143
return True
4244

45+
def apply_config_update(self, updated_config, _llm_needs_reload):
46+
self.chat_config = updated_config
47+
4348
async def chat_async(self, _user_input):
4449
if False:
4550
yield ""
@@ -143,3 +148,113 @@ async def consume():
143148

144149
assert retrieval_queries == ["清除浏览器缓存的操作步骤"]
145150
assert chunks == ["回答"]
151+
152+
153+
def test_reload_llm_keeps_existing_model_on_failure(monkeypatch):
154+
class OldModel:
155+
async def ainvoke(self, *_args, **_kwargs):
156+
if False:
157+
yield ""
158+
159+
bot = object.__new__(Chatbot)
160+
bot.console = Console()
161+
bot.session_config = SimpleNamespace(active_llm_configuration="new-model")
162+
bot.retrieval_service = object()
163+
bot.llm_model = old_model = OldModel()
164+
bot.chat_service = old_chat_service = object()
165+
166+
monkeypatch.setattr(
167+
"src.chat.core.ModelProviderFactory.get_llm_provider",
168+
lambda _llm_key: (_ for _ in ()).throw(RuntimeError("reload failed")),
169+
)
170+
171+
assert bot.reload_llm() is False
172+
assert bot.llm_model is old_model
173+
assert bot.chat_service is old_chat_service
174+
175+
176+
def test_apply_config_update_reverts_llm_key_when_reload_fails(monkeypatch):
177+
class DummySessionConfig:
178+
def __init__(self, active_llm_configuration: str):
179+
self.active_llm_configuration = active_llm_configuration
180+
181+
def __setitem__(self, key, value):
182+
setattr(self, key, value)
183+
184+
messages = []
185+
bot = object.__new__(Chatbot)
186+
bot.console = SimpleNamespace(print=lambda message: messages.append(str(message)))
187+
bot.session_config = DummySessionConfig(active_llm_configuration="old-model")
188+
bot.llm_model = object()
189+
bot.chat_service = object()
190+
monkeypatch.setattr(bot, "reload_llm", lambda: False)
191+
192+
bot.apply_config_update({"active_llm_configuration": "new-model"}, llm_needs_reload=True)
193+
194+
assert bot.session_config.active_llm_configuration == "old-model"
195+
assert any("LLM 切换失败" in message for message in messages)
196+
197+
198+
def test_start_chat_session_async_reverts_llm_after_editor_mutation(monkeypatch):
199+
class RealishChatbot(FakeChatbot):
200+
last_instance = None
201+
202+
def __init__(self, _console: Console):
203+
RealishChatbot.last_instance = self
204+
self.console = _console
205+
self.llm_model = object()
206+
self.session_config = SessionConfig(
207+
retrieval_method=RetrievalMethod.HYBRID_SEARCH,
208+
vector_weight=0.3,
209+
keyword_weight=0.7,
210+
hybrid_fusion_strategy="rrf",
211+
retrieval_candidate_multiplier=3,
212+
rerank_enabled=False,
213+
top_k=5,
214+
score_threshold=0.4,
215+
active_llm_configuration="old-model",
216+
active_rerank_configuration="siliconflow",
217+
llm_configurations={"old-model": ModelDetail(provider="openai", model_name="qwen3-max")},
218+
rerank_configurations={"siliconflow": ModelDetail(provider="siliconflow", model_name="rerank")},
219+
chat_temperature=0.7,
220+
)
221+
self.chat_service = object()
222+
223+
@property
224+
def chat_config(self):
225+
return self.session_config
226+
227+
@chat_config.setter
228+
def chat_config(self, value):
229+
if isinstance(value, SessionConfig):
230+
self.session_config = value
231+
return
232+
for key, item in value.items():
233+
self.session_config[key] = item
234+
235+
def reload_llm(self):
236+
return False
237+
238+
def apply_config_update(self, updated_config, llm_needs_reload):
239+
return Chatbot.apply_config_update(self, updated_config, llm_needs_reload)
240+
241+
monkeypatch.setattr("src.chat.core.Chatbot", RealishChatbot)
242+
monkeypatch.setattr(
243+
"src.chat.core.PromptSession",
244+
lambda: FakePromptSession(["/config", "/quit"]),
245+
)
246+
monkeypatch.setattr("src.chat.core.display_chat_config", lambda *_args, **_kwargs: None)
247+
248+
edited_configs = []
249+
250+
def fake_launch(chat_config):
251+
edited_configs.append(chat_config)
252+
chat_config["active_llm_configuration"] = "new-model"
253+
return True, chat_config
254+
255+
monkeypatch.setattr("src.chat.core.launch_config_editor", fake_launch)
256+
257+
asyncio.run(start_chat_session_async())
258+
259+
assert edited_configs[0]["active_llm_configuration"] == "new-model"
260+
assert RealishChatbot.last_instance.session_config.active_llm_configuration == "old-model"

tests/retrieval_test/test_retrieval_cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,28 @@ def test_run_retrieval_test_async_can_exit(monkeypatch):
2323
monkeypatch.setattr("src.retrieval_test.core.ExcelLogger", lambda: None)
2424

2525
asyncio.run(run_retrieval_test_async())
26+
27+
28+
def test_run_retrieval_test_async_prints_error_when_retrieval_fails(monkeypatch):
29+
printed_messages = []
30+
31+
class FailingRetrievalService:
32+
def __init__(self, *_args, **_kwargs):
33+
return None
34+
35+
async def retrieve(self, *_args, **_kwargs):
36+
raise RuntimeError("boom")
37+
38+
monkeypatch.setattr(
39+
"src.retrieval_test.core.PromptSession",
40+
lambda: FakePromptSession(["query", "/quit"]),
41+
)
42+
monkeypatch.setattr("src.retrieval_test.core.ExcelLogger", lambda: None)
43+
monkeypatch.setattr("src.retrieval_test.core.RetrievalService", FailingRetrievalService)
44+
monkeypatch.setattr("src.retrieval_test.core.EmbeddingService", lambda *_args, **_kwargs: object())
45+
monkeypatch.setattr("src.retrieval_test.core.VectorStoreFactory.get_default_vector_store", lambda: object())
46+
monkeypatch.setattr("src.retrieval_test.core.console.print", lambda message, *args, **kwargs: printed_messages.append(str(message)))
47+
48+
asyncio.run(run_retrieval_test_async())
49+
50+
assert any("召回测试出错" in message for message in printed_messages)

0 commit comments

Comments
 (0)