@@ -43,7 +43,7 @@ class MockLLMServer:
4343 使用方式:
4444 # 基本用法
4545 server = MockLLMServer()
46- server.install(monkeypatch)
46+ server.install(monkeypatch, respx_mock) # 需要传入 respx_mock
4747
4848 # 添加自定义场景
4949 server.add_scenario(Scenarios.simple_chat("你好", "你好!"))
@@ -67,15 +67,22 @@ class MockLLMServer:
6767 validate_tools : bool = True
6868 """是否验证工具格式(默认 True)"""
6969
70- def install (self , monkeypatch : Any ) -> "MockLLMServer" :
70+ _respx_router : Any = field (default = None , init = False , repr = False )
71+ """内部使用的 respx router 实例"""
72+
73+ def install (
74+ self , monkeypatch : Any , respx_mock : Any = None
75+ ) -> "MockLLMServer" :
7176 """安装所有 mock
7277
7378 Args:
7479 monkeypatch: pytest monkeypatch fixture
80+ respx_mock: pytest respx_mock fixture(必须传入以确保 mock 生效)
7581
7682 Returns:
7783 self: 返回自身以便链式调用
7884 """
85+ self ._respx_router = respx_mock
7986 self ._patch_model_info (monkeypatch )
8087 self ._patch_litellm (monkeypatch )
8188 self ._setup_respx ()
@@ -240,7 +247,20 @@ async def fake_acompletion(*args: Any, **kwargs: Any) -> ModelResponse:
240247 pass # google.adk not installed
241248
242249 def _setup_respx (self ):
243- """设置 respx HTTP mock"""
250+ """设置 respx HTTP mock
251+
252+ 关键修复:使用 pytest-respx fixture 提供的 router 而不是全局 respx
253+
254+ 问题背景:
255+ - 之前直接使用全局 respx.route() 在 CI 环境中不生效
256+ - 全局 respx router 在某些环境中可能没有正确初始化
257+ - 导致 HTTP 请求没有被拦截,Google ADK 发送真实请求
258+
259+ 解决方案:
260+ - 使用 pytest-respx 提供的 respx_mock fixture
261+ - 通过 install() 方法传入 respx_mock
262+ - 确保 mock 在所有环境中一致生效
263+ """
244264
245265 def extract_payload (request : Any ) -> Dict [str , Any ]:
246266 try :
@@ -274,7 +294,10 @@ def build_response(request: Any, route: Any) -> respx.MockResponse:
274294 )
275295 return respx .MockResponse (status_code = 200 , json = response_json )
276296
277- respx .route (url__startswith = self .base_url ).mock (
297+ # 关键修复:使用传入的 respx_router 而不是全局 respx
298+ # 如果没有传入 respx_router,回退到全局 respx(向后兼容)
299+ router = self ._respx_router if self ._respx_router is not None else respx
300+ router .route (url__startswith = self .base_url ).mock (
278301 side_effect = build_response
279302 )
280303
@@ -304,6 +327,27 @@ def _build_response(
304327 tools_payload is not None ,
305328 )
306329
330+ # 添加详细的消息日志,帮助调试框架的消息格式
331+ for i , msg in enumerate (messages ):
332+ role = msg .get ("role" , "unknown" )
333+ content_preview = str (msg .get ("content" , "" ))[:100 ]
334+ logger .debug (
335+ "Message[%d] role=%s, content_preview=%s" ,
336+ i ,
337+ role ,
338+ content_preview ,
339+ )
340+ if "tool_calls" in msg :
341+ logger .debug (
342+ "Message[%d] has tool_calls: %s" , i , msg .get ("tool_calls" )
343+ )
344+ if "tool_call_id" in msg :
345+ logger .debug (
346+ "Message[%d] has tool_call_id: %s" ,
347+ i ,
348+ msg .get ("tool_call_id" ),
349+ )
350+
307351 # 验证工具格式
308352 if self .validate_tools and self .expect_tools and tools_payload :
309353 self ._assert_tools (tools_payload )
@@ -319,16 +363,19 @@ def _build_response(
319363 turn = scenario .get_response (messages )
320364 return turn .to_response ()
321365
322- # 默认逻辑:根据最后一条消息决定响应
366+ # 默认逻辑:未匹配场景时使用
323367 return self ._build_default_response (messages , tools_payload )
324368
325369 def _build_default_response (
326370 self , messages : List [Dict ], tools_payload : Optional [List ]
327371 ) -> Dict [str , Any ]:
328372 """构建默认响应(无场景匹配时使用)"""
329- last_role = messages [- 1 ].get ("role" )
373+ # 检查消息历史中是否已经有 tool 结果
374+ # 这是关键修复:不只检查最后一条消息,而是检查整个历史
375+ has_tool_results = any (msg .get ("role" ) == "tool" for msg in messages )
330376
331- if last_role == "tool" :
377+ if has_tool_results :
378+ # 已经有 tool 结果,应该返回最终答案而不是再次调用工具
332379 return {
333380 "id" : "chatcmpl-mock-final" ,
334381 "object" : "chat.completion" ,
@@ -349,7 +396,7 @@ def _build_default_response(
349396 },
350397 }
351398
352- # 如果有工具 ,返回工具调用
399+ # 如果有工具且未调用过 ,返回工具调用
353400 if tools_payload :
354401 return {
355402 "id" : "chatcmpl-mock-tools" ,
0 commit comments