search.py 24.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
"""
专为 AI Agent 设计的多模态搜索工具集 (Bocha)

版本: 1.1
最后更新: 2025-08-22

此脚本将复杂的 Bocha AI Search 功能分解为一系列目标明确、参数极少的独立工具,
专为 AI Agent 调用而设计。Agent 只需根据任务意图(如常规搜索、查找结构化数据或时效性新闻)
选择合适的工具,无需理解复杂的参数组合。

核心特性:
- 强大多模态能力: 能同时返回网页、图片、AI总结、追问建议,以及丰富的“模态卡”结构化数据。
- 模态卡支持: 针对天气、股票、汇率、百科、医疗等特定查询,可直接返回结构化数据卡片,便于Agent直接解析和使用。

主要工具:
- comprehensive_search: 执行全面搜索,返回网页、图片、AI总结及可能的模态卡。
- search_for_structured_data: 专门用于查询天气、股票、汇率等可触发“模态卡”的结构化信息。
- web_search_only: 执行纯网页搜索,不请求AI总结,速度更快。
- search_last_24_hours: 获取过去24小时内的最新信息。
- search_last_week: 获取过去一周内的主要报道。
"""

import os
import json
import sys
import datetime
from typing import List, Dict, Any, Optional, Literal

from loguru import logger
from config import settings

# 运行前请确保已安装 requests 库: pip install requests
try:
    import requests
except ImportError:
    raise ImportError("requests 库未安装,请运行 `pip install requests` 进行安装。")

# 添加utils目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(current_dir))
utils_dir = os.path.join(root_dir, 'utils')
if utils_dir not in sys.path:
    sys.path.append(utils_dir)

from retry_helper import with_graceful_retry, SEARCH_API_RETRY_CONFIG

# --- 1. 数据结构定义 ---
from dataclasses import dataclass, field

@dataclass
class WebpageResult:
    """网页搜索结果"""
    name: str
    url: str
    snippet: str
    display_url: Optional[str] = None
    date_last_crawled: Optional[str] = None

@dataclass
class ImageResult:
    """图片搜索结果"""
    name: str
    content_url: str
    host_page_url: Optional[str] = None
    thumbnail_url: Optional[str] = None
    width: Optional[int] = None
    height: Optional[int] = None

@dataclass
class ModalCardResult:
    """
    模态卡结构化数据结果
    这是 Bocha 搜索的核心特色,用于返回特定类型的结构化信息。
    """
    card_type: str  # 例如: weather_china, stock, baike_pro, medical_common
    content: Dict[str, Any]  # 解析后的JSON内容

@dataclass
class BochaResponse:
    """封装 Bocha API 的完整返回结果,以便在工具间传递"""
    query: str
    conversation_id: Optional[str] = None
    answer: Optional[str] = None  # AI生成的总结答案
    follow_ups: List[str] = field(default_factory=list) # AI生成的追问
    webpages: List[WebpageResult] = field(default_factory=list)
    images: List[ImageResult] = field(default_factory=list)
    modal_cards: List[ModalCardResult] = field(default_factory=list)

@dataclass
class AnspireResponse:
    """封装 Anspire API 的完整返回结果,以便在工具间传递"""
    query: str
    conversation_id: Optional[str] = None
    score: Optional[float] = None
    webpages: List[WebpageResult] = field(default_factory=list)


# --- 2. 核心客户端与专用工具集 ---

class BochaMultimodalSearch:
    """
    一个包含多种专用多模态搜索工具的客户端。
    每个公共方法都设计为供 AI Agent 独立调用的工具。
    """

    BOCHA_BASE_URL = settings.BOCHA_BASE_URL or "https://api.bocha.cn/v1/ai-search"

    def __init__(self, api_key: Optional[str] = None):
        """
        初始化客户端。
        Args:
            api_key: Bocha API密钥,若不提供则从环境变量 BOCHA_API_KEY 读取。
        """
        if api_key is None:
            api_key = settings.BOCHA_WEB_SEARCH_API_KEY
            if not api_key:
                raise ValueError("Bocha API Key未找到!请设置 BOCHA_API_KEY 环境变量或在初始化时提供")

        self._headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
            'Accept': '*/*'
        }

    def _parse_search_response(self, response_dict: Dict[str, Any], query: str) -> BochaResponse:
        """从API的原始字典响应中解析出结构化的BochaResponse对象"""

        final_response = BochaResponse(query=query)
        final_response.conversation_id = response_dict.get('conversation_id')

        messages = response_dict.get('messages', [])
        for msg in messages:
            role = msg.get('role')
            if role != 'assistant':
                continue

            msg_type = msg.get('type')
            content_type = msg.get('content_type')
            content_str = msg.get('content', '{}')

            try:
                content_data = json.loads(content_str)
            except json.JSONDecodeError:
                # 如果内容不是合法的JSON字符串(例如纯文本的answer),则直接使用
                content_data = content_str

            if msg_type == 'answer' and content_type == 'text':
                final_response.answer = content_data

            elif msg_type == 'follow_up' and content_type == 'text':
                final_response.follow_ups.append(content_data)

            elif msg_type == 'source':
                if content_type == 'webpage':
                    web_results = content_data.get('value', [])
                    for item in web_results:
                        final_response.webpages.append(WebpageResult(
                            name=item.get('name'),
                            url=item.get('url'),
                            snippet=item.get('snippet'),
                            display_url=item.get('displayUrl'),
                            date_last_crawled=item.get('dateLastCrawled')
                        ))
                elif content_type == 'image':
                    final_response.images.append(ImageResult(
                        name=content_data.get('name'),
                        content_url=content_data.get('contentUrl'),
                        host_page_url=content_data.get('hostPageUrl'),
                        thumbnail_url=content_data.get('thumbnailUrl'),
                        width=content_data.get('width'),
                        height=content_data.get('height')
                    ))
                # 所有其他 content_type 都视为模态卡
                else:
                    final_response.modal_cards.append(ModalCardResult(
                        card_type=content_type,
                        content=content_data
                    ))

        return final_response


    @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return=BochaResponse(query="搜索失败"))
    def _search_internal(self, **kwargs) -> BochaResponse:
        """内部通用的搜索执行器,所有工具最终都调用此方法"""
        query = kwargs.get("query", "Unknown Query")
        payload = {
            "stream": False,  # Agent工具通常使用非流式以获取完整结果
        }
        payload.update(kwargs)

        try:

            response = requests.post(self.BOCHA_BASE_URL, headers=self._headers, json=payload, timeout=30)
            response.raise_for_status()  # 如果HTTP状态码是4xx或5xx,则抛出异常

            response_dict = response.json()
            if response_dict.get("code") != 200:
                logger.error(f"API返回错误: {response_dict.get('msg', '未知错误')}")
                return BochaResponse(query=query)

            return self._parse_search_response(response_dict, query)

        except requests.exceptions.RequestException as e:
            logger.exception(f"搜索时发生网络错误: {str(e)}")
            raise e  # 让重试机制捕获并处理
        except Exception as e:
            logger.exception(f"处理响应时发生未知错误: {str(e)}")
            raise e  # 让重试机制捕获并处理

    # --- Agent 可用的工具方法 ---

    def comprehensive_search(self, query: str, max_results: int = 10) -> BochaResponse:
        """
        【工具】全面综合搜索: 执行一次标准的、包含所有信息类型的综合搜索。
        返回网页、图片、AI总结、追问建议和可能的模态卡。这是最常用的通用搜索工具。
        Agent可提供搜索查询(query)和可选的最大结果数(max_results)。
        """
        logger.info(f"--- TOOL: 全面综合搜索 (query: {query}) ---")
        return self._search_internal(
            query=query,
            count=max_results,
            answer=True  # 开启AI总结
        )

    def web_search_only(self, query: str, max_results: int = 15) -> BochaResponse:
        """
        【工具】纯网页搜索: 只获取网页链接和摘要,不请求AI生成答案。
        适用于需要快速获取原始网页信息,而不需要AI额外分析的场景。速度更快,成本更低。
        """
        logger.info(f"--- TOOL: 纯网页搜索 (query: {query}) ---")
        return self._search_internal(
            query=query,
            count=max_results,
            answer=False # 关闭AI总结
        )

    def search_for_structured_data(self, query: str) -> BochaResponse:
        """
        【工具】结构化数据查询: 专门用于可能触发“模态卡”的查询。
        当Agent意图是查询天气、股票、汇率、百科定义、火车票、汽车参数等结构化信息时,应优先使用此工具。
        它会返回所有信息,但Agent应重点关注结果中的 `modal_cards` 部分。
        """
        logger.info(f"--- TOOL: 结构化数据查询 (query: {query}) ---")
        # 实现上与 comprehensive_search 相同,但通过命名和文档引导Agent的意图
        return self._search_internal(
            query=query,
            count=5, # 结构化查询通常不需要太多网页结果
            answer=True
        )

    def search_last_24_hours(self, query: str) -> BochaResponse:
        """
        【工具】搜索24小时内信息: 获取关于某个主题的最新动态。
        此工具专门查找过去24小时内发布的内容。适用于追踪突发事件或最新进展。
        """
        logger.info(f"--- TOOL: 搜索24小时内信息 (query: {query}) ---")
        return self._search_internal(query=query, freshness='oneDay', answer=True)

    def search_last_week(self, query: str) -> BochaResponse:
        """
        【工具】搜索本周信息: 获取关于某个主题过去一周内的主要报道。
        适用于进行周度舆情总结或回顾。
        """
        logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---")
        return self._search_internal(query=query, freshness='oneWeek', answer=True)

class AnspireAISearch:
    """
    Anspire AI Search 客户端
    """
    ANSPIRE_BASE_URL = settings.ANSPIRE_BASE_URL or "https://plugin.anspire.cn/api/ntsearch/search"

    def __init__(self, api_key: Optional[str] = None):
        """
        初始化客户端。
        Args:
            api_key: Anspire API密钥,若不提供则从环境变量 ANSPIRE_API_KEY 读取。
        """
        if api_key is None:
            api_key = settings.ANSPIRE_API_KEY
            if not api_key:
                raise ValueError("Anspire API Key未找到!请设置 ANSPIRE_API_KEY 环境变量或在初始化时提供")

        self._headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
            'Connection': 'keep-alive',
            'Accept': '*/*'
        }

    def _parse_search_response(self, response_dict: Dict[str, Any], query: str) -> AnspireResponse:
        final_response = AnspireResponse(query=query)
        final_response.conversation_id = response_dict.get('Uuid')

        messages = response_dict.get("results", [])
        for msg in messages:
            final_response.score = msg.get("score")
            final_response.webpages.append(WebpageResult(
                name = msg.get("title", ""),
                url = msg.get("url", ""),
                snippet = msg.get("content", ""),
                date_last_crawled = msg.get("date", None)
            ))

        return final_response
    
    @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return=AnspireResponse(query="搜索失败"))
    def _search_internal(self, **kwargs) -> AnspireResponse:
        """内部通用的搜索执行器,所有工具最终都调用此方法"""
        query = kwargs.get("query", "Unknown Query")
        payload = {
            "query": query,
            "top_k": kwargs.get("top_k", 10),
            "Insite": kwargs.get("Insite", ""),
            "FromTime": kwargs.get("FromTime", ""),
            "ToTime": kwargs.get("ToTime", "")
        }
        
        try:
            response = requests.get(self.ANSPIRE_BASE_URL, headers=self._headers, params=payload, timeout=30)
            response.raise_for_status()  # 如果HTTP状态码是4xx或5xx,则抛出异常

            response_dict = response.json()
            return self._parse_search_response(response_dict, query)
        except requests.exceptions.RequestException as e:
            logger.exception(f"搜索时发生网络错误: {str(e)}")
            raise e  # 让重试机制捕获并处理
        except Exception as e:
            logger.exception(f"处理响应时发生未知错误: {str(e)}")
            raise e  # 让重试机制捕获并处理
    
    def comprehensive_search(self, query: str, max_results: int = 10) -> AnspireResponse:
        """
        【工具】综合搜索: 获取关于某个主题的全面信息,包括网页。
        适用于需要多种信息来源的场景。
        """
        logger.info(f"--- TOOL: 综合搜索 (query: {query}) ---")
        return self._search_internal(
            query=query,
            top_k=max_results
        )

    def search_last_24_hours(self, query: str, max_results: int = 10) -> AnspireResponse:
        """
        【工具】搜索24小时内信息: 获取关于某个主题的最新动态。
        此工具专门查找过去24小时内发布的内容。适用于追踪突发事件或最新进展。
        """
        logger.info(f"--- TOOL: 搜索24小时内信息 (query: {query}) ---")
        to_time = datetime.datetime.now()
        from_time = to_time - datetime.timedelta(days=1)
        return self._search_internal(query=query,
                                     top_k=max_results,
                                     FromTime=from_time.strftime("%Y-%m-%d %H:%M:%S"), 
                                     ToTime=to_time.strftime("%Y-%m-%d %H:%M:%S"))

    def search_last_week(self, query: str, max_results: int = 10) -> AnspireResponse:
        """
        【工具】搜索本周信息: 获取关于某个主题过去一周内的主要报道。
        适用于进行周度舆情总结或回顾。
        """
        logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---")
        to_time = datetime.datetime.now()
        from_time = to_time - datetime.timedelta(weeks=1)
        return self._search_internal(query=query,
                                     top_k=max_results,
                                     FromTime=from_time.strftime("%Y-%m-%d %H:%M:%S"),
                                     ToTime=to_time.strftime("%Y-%m-%d %H:%M:%S"))


# --- 3. 测试与使用示例 ---
def load_agent_from_config():
    """根据配置文件选择并加载搜索Agent"""
    if settings.BOCHA_WEB_SEARCH_API_KEY:
        logger.info("加载 BochaMultimodalSearch Agent")
        return BochaMultimodalSearch()
    elif settings.ANSPIRE_API_KEY:
        logger.info("加载 AnspireAISearch Agent")
        return AnspireAISearch()
    else:
        raise ValueError("未配置有效的搜索Agent")

def print_response_summary(response):
    """简化的打印函数,用于展示测试结果"""
    if not response or not response.query:
        logger.error("未能获取有效响应。")
        return

    logger.info(f"\n查询: '{response.query}' | 会话ID: {response.conversation_id}")
    if hasattr(response, 'answer') and response.answer:
        logger.info(f"AI摘要: {response.answer[:150]}...")

    logger.info(f"找到 {len(response.webpages)} 个网页")
    if hasattr(response, 'images'):
        logger.info(f"找到 {len(response.images)} 张图片")
    if hasattr(response, 'modal_cards'):
        logger.info(f"找到 {len(response.modal_cards)} 个模态卡")

    if hasattr(response, 'modal_cards') and response.modal_cards:
        first_card = response.modal_cards[0]
        logger.info(f"第一个模态卡类型: {first_card.card_type}")

    if response.webpages:
        first_result = response.webpages[0]
        logger.info(f"第一条网页结果: {first_result.name}")

    if hasattr(response, 'follow_ups') and response.follow_ups:
        logger.info(f"建议追问: {response.follow_ups}")

    logger.info("-" * 60)


if __name__ == "__main__":
    # 在运行前,请确保您已设置 BOCHA_API_KEY 环境变量

    try:
        # 初始化多模态搜索客户端,它内部包含了所有工具
        search_client = load_agent_from_config()

        # 场景1: Agent进行一次常规的、需要AI总结的综合搜索
        response1 = search_client.comprehensive_search(query="人工智能对未来教育的影响")
        print_response_summary(response1)

        # 场景2: Agent需要查询特定结构化信息 - 天气
        if isinstance(search_client, BochaMultimodalSearch):
            response2 = search_client.search_for_structured_data(query="上海明天天气怎么样")
            print_response_summary(response2)
            # 深度解析第一个模态卡
            if response2.modal_cards and response2.modal_cards[0].card_type == 'weather_china':
                logger.info("天气模态卡详情:", json.dumps(response2.modal_cards[0].content, indent=2, ensure_ascii=False))


        # 场景3: Agent需要查询特定结构化信息 - 股票
        if isinstance(search_client, BochaMultimodalSearch):
            response3 = search_client.search_for_structured_data(query="东方财富股票")
            print_response_summary(response3)

        # 场景4: Agent需要追踪某个事件的最新进展
        response4 = search_client.search_last_24_hours(query="C929大飞机最新消息")
        print_response_summary(response4)

        # 场景5: Agent只需要快速获取网页信息,不需要AI总结
        if isinstance(search_client, BochaMultimodalSearch):
            response5 = search_client.web_search_only(query="Python dataclasses用法")
            print_response_summary(response5)

        # 场景6: Agent需要回顾一周内关于某项技术的新闻
        response6 = search_client.search_last_week(query="量子计算商业化")
        print_response_summary(response6)

        '''下面是测试程序的输出:
        --- TOOL: 全面综合搜索 (query: 人工智能对未来教育的影响) ---

查询: '人工智能对未来教育的影响' | 会话ID: bf43bfe4c7bb4f7b8a3945515d8ab69e
AI摘要: 人工智能对未来教育有着多方面的影响。

从积极影响来看:
- 在教学资源方面,人工智能有助于教育资源的均衡分配[引用:4]。例如通过人工智能云平台,可以实现优质资源的共享,这对于偏远地区来说意义重大,能让那里的学生也接触到优质的教育内 容,一定程度上缓解师资短缺的问题,因为AI驱动的智能教学助手或虚拟...
找到 10 个网页, 1 张图片, 1 个模态卡。
第一个模态卡类型: video
第一条网页结果: 人工智能如何影响教育变革
建议追问: [['人工智能将如何改变未来的教育模式?', '在未来教育中,人工智能会给教师带来哪些挑战?', '未来教育中,学生如何利用人工智能提升学习效果?']]
------------------------------------------------------------
--- TOOL: 结构化数据查询 (query: 上海明天天气怎么样) ---

查询: '上海明天天气怎么样' | 会话ID: e412aa1548cd43a295430e47a62adda2
AI摘要: 根据所给信息,无法确定上海明天的天气情况。

首先,所提供的信息都是关于2025年8月22日的天气状况,包括当天的气温、降水、风力、湿度以及高温预警等信息[引用:1][引用:2][引用:3][引用:5]。然而,这些信息没有涉及到明天(8月23 日)天气的预测内容。虽然提到了副热带高压一直到8月底高温都...
找到 5 个网页, 1 张图片, 2 个模态卡。
第一个模态卡类型: video
第一条网页结果: 今日冲击38!上海八月高温天数和夏季持续高温天数有望双双破纪录_天气_低压_气象站
建议追问: [['能告诉我上海明天的气温范围吗?', '上海明天会有降雨吗?', '上海明天的天气是晴天还是阴天呢?']]
------------------------------------------------------------
--- TOOL: 结构化数据查询 (query: 东方财富股票) ---

查询: '东方财富股票' | 会话ID: 584d62ed97834473b967127852e1eaa0
AI摘要: 仅根据提供的上下文,无法确切获取东方财富股票的相关信息。

从给出的这些数据来看,并没有直接表明与东方财富股票相关的特定数据。例如,没有东方财富股票的涨跌幅情况、成交量、市值等具体数据[引用:1][引用:3]。也没有涉及东方财富股票在研报 、评级方面的信息[引用:2]。同时,上下文里关于股票价格、成交...
找到 5 个网页, 1 张图片, 2 个模态卡。
第一个模态卡类型: video
第一条网页结果: 股票价格_分时成交_行情_走势图—东方财富网
建议追问: [['东方财富股票近期的走势如何?', '东方财富股票有哪些主要的投资亮点?', '东方财富股票的历史最高和最低股价是多少?']]
------------------------------------------------------------
--- TOOL: 搜索24小时内信息 (query: C929大飞机最新消息) ---

查询: 'C929大飞机最新消息' | 会话ID: 5904021dc29d497e938e04db18d7f2e2
AI摘要: 根据提供的上下文,没有关于C929大飞机的直接消息,无法确切给出C929大飞机的最新消息。

目前提供的上下文涵盖了众多航空领域相关事件,但多是围绕波音787、空客A380相关专家的人事变动、国产飞机“C909云端之旅”、科德数控的营收情况、俄制航空发动机供应相关以及其他非C929大飞机相关的内容。...
找到 10 个网页, 1 张图片, 1 个模态卡。
第一个模态卡类型: video
第一条网页结果: 放弃美国千万年薪,波音787顶尖专家回国,或可协助破解C929
建议追问: [['C929大飞机目前的研发进度如何?', '有没有关于C929大飞机预计首飞时间的消息?', 'C929大飞机在技术创新方面有哪些新进展?']]
------------------------------------------------------------
--- TOOL: 纯网页搜索 (query: Python dataclasses用法) ---

查询: 'Python dataclasses用法' | 会话ID: 74c742759d2e4b17b52d8b735ce24537
找到 15 个网页, 1 张图片, 1 个模态卡。
第一个模态卡类型: video
第一条网页结果: 不可不知的dataclasses  python小知识_python dataclasses-CSDN博客
------------------------------------------------------------
--- TOOL: 搜索本周信息 (query: 量子计算商业化) ---

AI摘要: 量子计算商业化正在逐步推进。

量子计算商业化有着多方面的体现和推动因素。从国际上看,美国能源部橡树岭国家实验室选择IQM Radiance作为其首台本地部署的量子计算机,计划于2025年第三季度交付并集成至高性能计算系统中[引用:4];英国量子计算公司Oxford Ionics的全栈离子阱量子计算...
找到 10 个网页, 1 张图片, 1 个模态卡。
第一个模态卡类型: video
第一条网页结果: 量子计算商业潜力释放正酣,微美全息(WIMI.US)创新科技卡位“生态高地”
建议追问: [['量子计算商业化目前有哪些成功的案例?', '哪些公司在推动量子计算商业化进程?', '量子计算商业化面临的主要挑战是什么?']]
------------------------------------------------------------'''

    except ValueError as e:
        logger.exception(f"初始化失败: {e}")
        logger.error("请确保 BOCHA_API_KEY 环境变量已正确设置。")
    except Exception as e:
        logger.exception(f"测试过程中发生未知错误: {e}")