|
@@ -33,7 +33,7 @@
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
"#导入库和参数配置\n",
|
|
"#导入库和参数配置\n",
|
|
|
- "from hello_agents import SimpleAgent, HelloAgentsLLM, ReflectionAgent\n",
|
|
|
|
|
|
|
+ "from hello_agents import SimpleAgent, HelloAgentsLLM, ReflectionAgent, ToolRegistry\n",
|
|
|
"from hello_agents.tools import Tool, ToolParameter\n",
|
|
"from hello_agents.tools import Tool, ToolParameter\n",
|
|
|
"from typing import Dict, Any, List\n",
|
|
"from typing import Dict, Any, List\n",
|
|
|
"from tavily import TavilyClient\n",
|
|
"from tavily import TavilyClient\n",
|
|
@@ -42,23 +42,34 @@
|
|
|
"import re\n",
|
|
"import re\n",
|
|
|
"import numpy as np \n",
|
|
"import numpy as np \n",
|
|
|
"from dotenv import load_dotenv\n",
|
|
"from dotenv import load_dotenv\n",
|
|
|
|
|
+ "import asyncio\n",
|
|
|
|
|
+ "import nest_asyncio\n",
|
|
|
|
|
+ "from mcp.client.sse import sse_client\n",
|
|
|
|
|
+ "from mcp.client.session import ClientSession\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
"load_dotenv()\n",
|
|
"load_dotenv()\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
"#LLM参数\n",
|
|
"#LLM参数\n",
|
|
|
"LLM_MODEL_ID = os.getenv(\"LLM_MODEL_ID\")\n",
|
|
"LLM_MODEL_ID = os.getenv(\"LLM_MODEL_ID\")\n",
|
|
|
- "#\"deepseek-chat\"\n",
|
|
|
|
|
"LLM_API_KEY = os.getenv(\"LLM_API_KEY\")\n",
|
|
"LLM_API_KEY = os.getenv(\"LLM_API_KEY\")\n",
|
|
|
"LLM_BASE_URL = os.getenv(\"LLM_BASE_URL\")\n",
|
|
"LLM_BASE_URL = os.getenv(\"LLM_BASE_URL\")\n",
|
|
|
"#Tavily参数\n",
|
|
"#Tavily参数\n",
|
|
|
- "TAVILY_API_KEY = os.getenv(\"TAVILY_API_KEY\")\n",
|
|
|
|
|
|
|
+ "TAVILY_API_KEY = os.getenv(\"TAVILY_API_KEY\",\"\")\n",
|
|
|
|
|
+ "#百度MCP参数\n",
|
|
|
|
|
+ "BAIDU_TOKEN = os.getenv(\"BAIDU_MCP_TOKEN\",\"\")\n",
|
|
|
|
|
+ "#输入json路径配置\n",
|
|
|
|
|
+ "INPUT_FILENAME = \"data/test_cases.json\"\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ "# 搜索源配置\n",
|
|
|
|
|
+ "# 可选值: \"tavily\" (通用/海外) 或 \"baidu\" (电商/国内)\n",
|
|
|
|
|
+ "os.environ[\"SEARCH_PROVIDER\"] = \"baidu\" \n",
|
|
|
"\n",
|
|
"\n",
|
|
|
"print(\"✅ 环境配置完成\")\n"
|
|
"print(\"✅ 环境配置完成\")\n"
|
|
|
]
|
|
]
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
"cell_type": "markdown",
|
|
"cell_type": "markdown",
|
|
|
- "id": "27482621",
|
|
|
|
|
|
|
+ "id": "9404c299",
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"source": [
|
|
"source": [
|
|
|
"### 第2部分:定义工具"
|
|
"### 第2部分:定义工具"
|
|
@@ -67,60 +78,110 @@
|
|
|
{
|
|
{
|
|
|
"cell_type": "code",
|
|
"cell_type": "code",
|
|
|
"execution_count": null,
|
|
"execution_count": null,
|
|
|
- "id": "2087de45",
|
|
|
|
|
|
|
+ "id": "37b994bc",
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
|
|
+ "# [Cell 2 终极版] 定义统一搜索工具 (兼容 Tavily 和 Baidu)\n",
|
|
|
|
|
+ "# 允许 Jupyter 运行异步\n",
|
|
|
|
|
+ "nest_asyncio.apply()\n",
|
|
|
|
|
+ "\n",
|
|
|
"class BatchSearchTool(Tool):\n",
|
|
"class BatchSearchTool(Tool):\n",
|
|
|
" def __init__(self):\n",
|
|
" def __init__(self):\n",
|
|
|
" super().__init__(\n",
|
|
" super().__init__(\n",
|
|
|
" name=\"batch_search\",\n",
|
|
" name=\"batch_search\",\n",
|
|
|
- " description=\"高级批量搜索工具。\"\n",
|
|
|
|
|
|
|
+ " description=\"统一搜索工具,支持 Tavily 和 Baidu 切换。\"\n",
|
|
|
" )\n",
|
|
" )\n",
|
|
|
|
|
+ " self.provider = os.environ.get(\"SEARCH_PROVIDER\", \"tavily\").lower()\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ " def run(self, parameters: Any) -> str:\n",
|
|
|
|
|
+ " return \"请使用 Python 代码直接调用 search_raw 方法获取数据。\"\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
" def search_raw(self, query: str) -> List[Dict]:\n",
|
|
" def search_raw(self, query: str) -> List[Dict]:\n",
|
|
|
- " if \"TAVILY_API_KEY\" not in os.environ: \n",
|
|
|
|
|
- " print(\"❌ 错误:缺少 API Key\")\n",
|
|
|
|
|
- " return []\n",
|
|
|
|
|
|
|
+ " if self.provider == \"baidu\":\n",
|
|
|
|
|
+ " return self._search_baidu(query)\n",
|
|
|
|
|
+ " else:\n",
|
|
|
|
|
+ " return self._search_tavily(query)\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
- " print(f\" 🚀 [直连搜索] 正在抓取: {query} ...\")\n",
|
|
|
|
|
|
|
+ " # --- 引擎 A: Tavily ---\n",
|
|
|
|
|
+ " def _search_tavily(self, query: str) -> List[Dict]:\n",
|
|
|
|
|
+ " api_key = os.environ.get(\"TAVILY_API_KEY\")\n",
|
|
|
|
|
+ " if not api_key: return []\n",
|
|
|
|
|
+ " print(f\" 🚀 [Tavily] 正在搜索: {query} ...\")\n",
|
|
|
" try:\n",
|
|
" try:\n",
|
|
|
- " tavily = TavilyClient(api_key=os.environ[\"TAVILY_API_KEY\"])\n",
|
|
|
|
|
- " # 搜索包含图片\n",
|
|
|
|
|
- " response = tavily.search(query, max_results=5, include_images=True) # 增加到5条,提高命中率\n",
|
|
|
|
|
- " \n",
|
|
|
|
|
|
|
+ " tavily = TavilyClient(api_key=api_key)\n",
|
|
|
|
|
+ " response = tavily.search(query, max_results=5, include_images=True)\n",
|
|
|
" results = []\n",
|
|
" results = []\n",
|
|
|
- " # 1. 提取文本结果\n",
|
|
|
|
|
" if 'results' in response:\n",
|
|
" if 'results' in response:\n",
|
|
|
" for r in response['results']:\n",
|
|
" for r in response['results']:\n",
|
|
|
" results.append({\n",
|
|
" results.append({\n",
|
|
|
- " \"title\": r['title'],\n",
|
|
|
|
|
- " \"url\": r['url'],\n",
|
|
|
|
|
- " \"content\": r['content'], \n",
|
|
|
|
|
- " \"type\": \"text\"\n",
|
|
|
|
|
|
|
+ " \"title\": r['title'], \"url\": r['url'], \"content\": r['content'], \n",
|
|
|
|
|
+ " \"type\": \"text\", \"img\": \"\" # Tavily 文本通常不带图\n",
|
|
|
" })\n",
|
|
" })\n",
|
|
|
- " \n",
|
|
|
|
|
- " # 2. 提取图片结果\n",
|
|
|
|
|
" if 'images' in response and response['images']:\n",
|
|
" if 'images' in response and response['images']:\n",
|
|
|
- " results.append({\n",
|
|
|
|
|
- " \"images\": response['images'][:3], # 取前3张\n",
|
|
|
|
|
- " \"type\": \"image\"\n",
|
|
|
|
|
- " })\n",
|
|
|
|
|
- " \n",
|
|
|
|
|
|
|
+ " results.append({\"images\": response['images'][:3], \"type\": \"image\"})\n",
|
|
|
" return results\n",
|
|
" return results\n",
|
|
|
- " \n",
|
|
|
|
|
" except Exception as e:\n",
|
|
" except Exception as e:\n",
|
|
|
- " print(f\" ⚠️ 搜索异常: {e}\")\n",
|
|
|
|
|
|
|
+ " print(f\" ⚠️ Tavily 异常: {e}\")\n",
|
|
|
|
|
+ " return []\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ " # --- 引擎 B: Baidu MCP ---\n",
|
|
|
|
|
+ " def _search_baidu(self, query: str) -> List[Dict]:\n",
|
|
|
|
|
+ " token = os.environ.get(\"BAIDU_MCP_TOKEN\")\n",
|
|
|
|
|
+ " if not token: return []\n",
|
|
|
|
|
+ " print(f\" 🐼 [百度优选] 正在搜索: {query} ...\")\n",
|
|
|
|
|
+ " try:\n",
|
|
|
|
|
+ " raw_json_str = asyncio.run(self._async_baidu_call(query, token))\n",
|
|
|
|
|
+ " print(f\" 🔍 原始 JSON 响应: {raw_json_str}\")\n",
|
|
|
|
|
+ " return self._parse_baidu_response(raw_json_str)\n",
|
|
|
|
|
+ " except Exception as e:\n",
|
|
|
|
|
+ " print(f\" ⚠️ 百度 MCP 异常: {e}\")\n",
|
|
|
" return []\n",
|
|
" return []\n",
|
|
|
- " \n",
|
|
|
|
|
- " # 兼容 Agent 调用接口\n",
|
|
|
|
|
- " def run(self, parameters: Any) -> str:\n",
|
|
|
|
|
- " return \"请使用 Python 代码直接调用 search_raw 方法获取数据。\"\n",
|
|
|
|
|
- " \n",
|
|
|
|
|
- " def get_parameters(self) -> List[ToolParameter]:\n",
|
|
|
|
|
- " return [ToolParameter(name=\"query\", type=\"string\", description=\"关键词\", required=True)]\n",
|
|
|
|
|
"\n",
|
|
"\n",
|
|
|
- "print(\"✅ BatchSearchTool定义完成\")"
|
|
|
|
|
|
|
+ " async def _async_baidu_call(self, query: str, token: str) -> str:\n",
|
|
|
|
|
+ " sse_url = f\"https://mcp-youxuan.baidu.com/mcp/sse?key={token}\"\n",
|
|
|
|
|
+ " async with sse_client(sse_url) as (read, write):\n",
|
|
|
|
|
+ " async with ClientSession(read, write) as session:\n",
|
|
|
|
|
+ " await session.initialize()\n",
|
|
|
|
|
+ " result = await session.call_tool(\"goods_search\", arguments={\"query\": query})\n",
|
|
|
|
|
+ " return result.content[0].text if result.content else \"\"\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ " def _parse_baidu_response(self, json_str: str) -> List[Dict]:\n",
|
|
|
|
|
+ " results = []; images = []\n",
|
|
|
|
|
+ " try:\n",
|
|
|
|
|
+ " data = json.loads(json_str)\n",
|
|
|
|
|
+ " items = data if isinstance(data, list) else []\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " for item in items[:5]:\n",
|
|
|
|
|
+ " title = item.get(\"goodsName\") or item.get(\"title\") or \"未知商品\"\n",
|
|
|
|
|
+ " price = item.get(\"price\") or item.get(\"minPrice\") or \"\"\n",
|
|
|
|
|
+ " shop = item.get(\"shopName\") or item.get(\"mall\") or \"\"\n",
|
|
|
|
|
+ " url = item.get(\"detailUrl\") or item.get(\"url\") or item.get(\"ori_url\") or \"#\"\n",
|
|
|
|
|
+ " img = item.get(\"imgUrl\") or item.get(\"picUrl\") or item.get(\"img\")\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " content = f\"价格: {price}元。店铺: {shop}。商品详情: {title}\"\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " # 📝【修复点】直接在 text 类型结果里绑定 img\n",
|
|
|
|
|
+ " results.append({\n",
|
|
|
|
|
+ " \"title\": title, \"url\": url, \"content\": content, \n",
|
|
|
|
|
+ " \"type\": \"text\", \"img\": img \n",
|
|
|
|
|
+ " })\n",
|
|
|
|
|
+ " if img: images.append(img)\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " if images: results.append({\"images\": images[:3], \"type\": \"image\"})\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " except json.JSONDecodeError:\n",
|
|
|
|
|
+ " print(\" ⚠️ 百度返回非 JSON 数据\")\n",
|
|
|
|
|
+ " return results\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ " def get_parameters(self):\n",
|
|
|
|
|
+ " return [ToolParameter(name=\"query\", type=\"string\", description=\"关键词\")]\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ "tool_registry = ToolRegistry()\n",
|
|
|
|
|
+ "tool_registry.register_tool(BatchSearchTool())\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ "print(\"✅ 统一搜索工具已加载!\")\n",
|
|
|
|
|
+ "print(f\"当前模式: {'百度优选 (电商)' if os.environ.get('SEARCH_PROVIDER') == 'baidu' else 'Tavily (通用)'}\")"
|
|
|
]
|
|
]
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
@@ -139,15 +200,6 @@
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
- "from hello_agents import ToolRegistry\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
- "#创建工具\n",
|
|
|
|
|
- "tool_registry = ToolRegistry()\n",
|
|
|
|
|
- "tool_registry.register_tool(BatchSearchTool())\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
- "print(tool_registry.list_tools())\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
"# 初始化大模型\n",
|
|
"# 初始化大模型\n",
|
|
|
"llm = HelloAgentsLLM()\n",
|
|
"llm = HelloAgentsLLM()\n",
|
|
|
"\n",
|
|
"\n",
|
|
@@ -159,8 +211,7 @@
|
|
|
"【⚠️ 时效性死命令 (CRITICAL)】\n",
|
|
"【⚠️ 时效性死命令 (CRITICAL)】\n",
|
|
|
"当前时间视作 **2025年11月**。\n",
|
|
"当前时间视作 **2025年11月**。\n",
|
|
|
"1. **严禁过时**:绝对不要推荐 2024 年或更早的旧款(除非是经典恒久款如黑胶唱片)。\n",
|
|
"1. **严禁过时**:绝对不要推荐 2024 年或更早的旧款(除非是经典恒久款如黑胶唱片)。\n",
|
|
|
- "2. **锁定新品**:对于**化妆品、数码、盲盒**,必须搜索 **\"2025圣诞限定\"**、**\"2025秋冬新品\"** 或 **\"2026春季预告\"**。\n",
|
|
|
|
|
- "3. **价格尺度**:给出的价格单位是人民币元。请严格遵照范围进行联想,禁止超出预算范围。\n",
|
|
|
|
|
|
|
+ "2. **价格尺度**:给出的价格单位是人民币元。请严格遵照范围进行联想,禁止超出预算范围。\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
"为了确保推荐质量,请参考以下的【优秀思考范例】:\n",
|
|
"为了确保推荐质量,请参考以下的【优秀思考范例】:\n",
|
|
|
"\n",
|
|
"\n",
|
|
@@ -325,9 +376,6 @@
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
- "\n",
|
|
|
|
|
- "INPUT_FILENAME = \"data/test_cases.json\"\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
"def load_user_profile(filename):\n",
|
|
"def load_user_profile(filename):\n",
|
|
|
" # 1. 检查文件是否存在\n",
|
|
" # 1. 检查文件是否存在\n",
|
|
|
" if not os.path.exists(filename):\n",
|
|
" if not os.path.exists(filename):\n",
|
|
@@ -359,9 +407,7 @@
|
|
|
" return {}\n",
|
|
" return {}\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
"# 加载数据\n",
|
|
"# 加载数据\n",
|
|
|
- "user_input_data = load_user_profile(INPUT_FILENAME)\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
- "\n"
|
|
|
|
|
|
|
+ "user_input_data = load_user_profile(INPUT_FILENAME)\n"
|
|
|
]
|
|
]
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
@@ -379,7 +425,6 @@
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
- "\n",
|
|
|
|
|
"def parse_budget_range(budget_str):\n",
|
|
"def parse_budget_range(budget_str):\n",
|
|
|
" \"\"\"解析用户预算字符串,返回 (min, max)\"\"\"\n",
|
|
" \"\"\"解析用户预算字符串,返回 (min, max)\"\"\"\n",
|
|
|
" nums = [float(x) for x in re.findall(r'\\d+', str(budget_str).replace(',', ''))]\n",
|
|
" nums = [float(x) for x in re.findall(r'\\d+', str(budget_str).replace(',', ''))]\n",
|
|
@@ -411,97 +456,139 @@
|
|
|
" prices.append(val)\n",
|
|
" prices.append(val)\n",
|
|
|
" return prices\n",
|
|
" return prices\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
- "def find_best_product(hunter, profiler_agent, keyword, budget_limit):\n",
|
|
|
|
|
- " \"\"\"\n",
|
|
|
|
|
- " 智能搜索核心函数:包含 初次搜索 -> 价格校验 -> 策略修正 -> 自动降级 -> 智能兜底\n",
|
|
|
|
|
- " \"\"\"\n",
|
|
|
|
|
- " # 容器:用于收集所有搜索到的潜在结果(用于兜底)\n",
|
|
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ "def find_best_product(hunter, profiler_agent, keyword, budget_min, budget_max):\n",
|
|
|
|
|
+ " limit_upper = budget_max * 1.2\n",
|
|
|
|
|
+ " limit_lower = budget_min * 0.8\n",
|
|
|
|
|
+ " \n",
|
|
|
" all_candidates = []\n",
|
|
" all_candidates = []\n",
|
|
|
" current_kw = keyword\n",
|
|
" current_kw = keyword\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
" # --- Round 1: 首次搜索 ---\n",
|
|
" # --- Round 1: 首次搜索 ---\n",
|
|
|
" print(f\" 🕵️ 第1次搜索: {current_kw} 价格\")\n",
|
|
" print(f\" 🕵️ 第1次搜索: {current_kw} 价格\")\n",
|
|
|
- " results_1 = hunter.search_raw(f\"{current_kw} 价格 RMB\")\n",
|
|
|
|
|
|
|
+ " results_1 = hunter.search_raw(f\"{current_kw} 价格\")\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " # 提取第一轮搜索到的通用图片(以防具体条目没图)\n",
|
|
|
|
|
" fallback_img = \"\"\n",
|
|
" fallback_img = \"\"\n",
|
|
|
" for r in results_1:\n",
|
|
" for r in results_1:\n",
|
|
|
" if r.get('images'): \n",
|
|
" if r.get('images'): \n",
|
|
|
" fallback_img = r['images'][0]\n",
|
|
" fallback_img = r['images'][0]\n",
|
|
|
" break\n",
|
|
" break\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
- " # 检查 Round 1 结果\n",
|
|
|
|
|
|
|
+ " has_valid_info = False\n",
|
|
|
" for res in results_1:\n",
|
|
" for res in results_1:\n",
|
|
|
" if res.get('type') == 'text':\n",
|
|
" if res.get('type') == 'text':\n",
|
|
|
|
|
+ " has_valid_info = True\n",
|
|
|
" p_vals = extract_all_prices([res])\n",
|
|
" p_vals = extract_all_prices([res])\n",
|
|
|
" if p_vals:\n",
|
|
" if p_vals:\n",
|
|
|
" res['price_val'] = p_vals[0]\n",
|
|
" res['price_val'] = p_vals[0]\n",
|
|
|
- " all_candidates.append(res) # 存入候选池\n",
|
|
|
|
|
- " if p_vals[0] <= budget_limit:\n",
|
|
|
|
|
- " # 完美命中!\n",
|
|
|
|
|
|
|
+ " # 📝【核心修复点1】记录当前结果所属的关键词\n",
|
|
|
|
|
+ " res['source_kw'] = current_kw \n",
|
|
|
|
|
+ " all_candidates.append(res)\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " if limit_lower <= p_vals[0] <= limit_upper:\n",
|
|
|
" if not res.get('img') and fallback_img: res['img'] = fallback_img\n",
|
|
" if not res.get('img') and fallback_img: res['img'] = fallback_img\n",
|
|
|
" return res, f\"约 {p_vals[0]}元\", current_kw\n",
|
|
" return res, f\"约 {p_vals[0]}元\", current_kw\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
- " # --- Round 2: 策略修正 (Feedback Loop) ---\n",
|
|
|
|
|
- " # 只有当找到了价格但都超标时,才触发修正\n",
|
|
|
|
|
- " avg_price = np.mean([c['price_val'] for c in all_candidates]) if all_candidates else 0\n",
|
|
|
|
|
- " \n",
|
|
|
|
|
- " if avg_price > budget_limit:\n",
|
|
|
|
|
- " print(f\" 💸 价格超标 (均价 {int(avg_price)} > 预算 {int(budget_limit/1.2)}),触发修正...\")\n",
|
|
|
|
|
- " \n",
|
|
|
|
|
- " correction_prompt = f\"\"\"\n",
|
|
|
|
|
- " 原策略 \"{current_kw}\" 市场均价约 {int(avg_price)}元,超预算。\n",
|
|
|
|
|
- " 请推荐一个 **同品类但更便宜** 的具体型号(平替)。\n",
|
|
|
|
|
- " 只输出关键词,不要解释。\n",
|
|
|
|
|
- " \"\"\"\n",
|
|
|
|
|
|
|
+ " # --- 机制 3: 无数据防御 ---\n",
|
|
|
|
|
+ " if not has_valid_info:\n",
|
|
|
|
|
+ " print(f\" ⚠️ [机制3触发] 首次搜索无有效信息。\")\n",
|
|
|
|
|
+ " correction_prompt = f\"原策略 '{current_kw}' 搜索结果为空,请推荐一个同品类但更热门的具体商品型号。只输出关键词。\"\n",
|
|
|
" new_kw = profiler_agent.run(correction_prompt).strip()\n",
|
|
" new_kw = profiler_agent.run(correction_prompt).strip()\n",
|
|
|
- " print(f\" 🔄 军师修正策略为: {new_kw}\")\n",
|
|
|
|
|
- " current_kw = new_kw # 更新关键词\n",
|
|
|
|
|
|
|
+ " print(f\" 🔄 军师换词: {new_kw}\")\n",
|
|
|
|
|
+ " current_kw = new_kw\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " # 执行第二次搜索\n",
|
|
|
|
|
- " results_2 = hunter.search_raw(f\"{new_kw} 价格 RMB\")\n",
|
|
|
|
|
|
|
+ " results = hunter.search_raw(f\"{current_kw} 价格\")\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " # 提取第二轮的图片\n",
|
|
|
|
|
- " for r in results_2:\n",
|
|
|
|
|
|
|
+ " # 更新 fallback_img\n",
|
|
|
|
|
+ " fallback_img = \"\" \n",
|
|
|
|
|
+ " for r in results:\n",
|
|
|
" if r.get('images'): \n",
|
|
" if r.get('images'): \n",
|
|
|
" fallback_img = r['images'][0]\n",
|
|
" fallback_img = r['images'][0]\n",
|
|
|
" break\n",
|
|
" break\n",
|
|
|
- " \n",
|
|
|
|
|
- " # 检查 Round 2 结果\n",
|
|
|
|
|
- " for res in results_2:\n",
|
|
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " for res in results:\n",
|
|
|
" if res.get('type') == 'text':\n",
|
|
" if res.get('type') == 'text':\n",
|
|
|
" p_vals = extract_all_prices([res])\n",
|
|
" p_vals = extract_all_prices([res])\n",
|
|
|
" if p_vals:\n",
|
|
" if p_vals:\n",
|
|
|
" res['price_val'] = p_vals[0]\n",
|
|
" res['price_val'] = p_vals[0]\n",
|
|
|
- " all_candidates.append(res) \n",
|
|
|
|
|
- " if p_vals[0] <= budget_limit:\n",
|
|
|
|
|
- " if not res.get('img') and fallback_img: res['img'] = fallback_img\n",
|
|
|
|
|
- " return res, f\"约 {p_vals[0]}元 (平替)\", current_kw\n",
|
|
|
|
|
|
|
+ " # 📝【核心修复点1】记录关键词\n",
|
|
|
|
|
+ " res['source_kw'] = current_kw\n",
|
|
|
|
|
+ " all_candidates.append(res)\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
- " # --- Round 3: 智能兜底 ---\n",
|
|
|
|
|
- " # 既然都没完美匹配,就从候选池里挑一个最便宜的,或者直接硬取第一个\n",
|
|
|
|
|
|
|
+ " # --- 机制 1 & 2: 价格修正 ---\n",
|
|
|
|
|
+ " avg_price = np.mean([c['price_val'] for c in all_candidates]) if all_candidates else 0\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
|
|
+ " if avg_price > 0:\n",
|
|
|
|
|
+ " correction_prompt = \"\"\n",
|
|
|
|
|
+ " if avg_price > limit_upper:\n",
|
|
|
|
|
+ " print(f\" 💸 [机制1触发] 均价 {int(avg_price)} > 上限 {int(limit_upper)},找平替...\")\n",
|
|
|
|
|
+ " correction_prompt = f\"原策略 '{current_kw}' 均价约 {int(avg_price)}元,超预算 ({budget_max}元)。请推荐一个同品类更便宜的具体型号(平替)。只输出关键词。\"\n",
|
|
|
|
|
+ " elif avg_price < limit_lower:\n",
|
|
|
|
|
+ " print(f\" 📉 [机制2触发] 均价 {int(avg_price)} < 下限 {int(limit_lower)},找升级款...\")\n",
|
|
|
|
|
+ " correction_prompt = f\"原策略 '{current_kw}' 均价约 {int(avg_price)}元,低于预算下限 ({budget_min}元)。请推荐一个同品类更高端的型号。只输出关键词。\"\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " if correction_prompt:\n",
|
|
|
|
|
+ " new_kw = profiler_agent.run(correction_prompt).strip()\n",
|
|
|
|
|
+ " print(f\" 🔄 军师修正: {new_kw}\")\n",
|
|
|
|
|
+ " current_kw = new_kw\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " results_2 = hunter.search_raw(f\"{new_kw} 价格\")\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " # 更新 fallback_img\n",
|
|
|
|
|
+ " fallback_img = \"\" \n",
|
|
|
|
|
+ " for r in results_2:\n",
|
|
|
|
|
+ " if r.get('images'): \n",
|
|
|
|
|
+ " fallback_img = r['images'][0]\n",
|
|
|
|
|
+ " break\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " for res in results_2:\n",
|
|
|
|
|
+ " if res.get('type') == 'text':\n",
|
|
|
|
|
+ " p_vals = extract_all_prices([res])\n",
|
|
|
|
|
+ " if p_vals:\n",
|
|
|
|
|
+ " res['price_val'] = p_vals[0]\n",
|
|
|
|
|
+ " # 📝【核心修复点1】记录关键词\n",
|
|
|
|
|
+ " res['source_kw'] = current_kw\n",
|
|
|
|
|
+ " all_candidates.append(res) \n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " if limit_lower <= p_vals[0] <= limit_upper:\n",
|
|
|
|
|
+ " if not res.get('img') and fallback_img: res['img'] = fallback_img\n",
|
|
|
|
|
+ " tag = \"(平替)\" if avg_price > limit_upper else \"(升级)\"\n",
|
|
|
|
|
+ " return res, f\"约 {p_vals[0]}元 {tag}\", current_kw\n",
|
|
|
|
|
+ "\n",
|
|
|
|
|
+ " # --- 机制 4: 兜底防御 ---\n",
|
|
|
|
|
+ " print(\" ⚠️ [机制4触发] 启用强制兜底模式...\")\n",
|
|
|
" best_fallback = None\n",
|
|
" best_fallback = None\n",
|
|
|
" status_msg = \"暂无报价\"\n",
|
|
" status_msg = \"暂无报价\"\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
" if all_candidates:\n",
|
|
" if all_candidates:\n",
|
|
|
- " # 按价格排序,取最低的那个\n",
|
|
|
|
|
- " best_fallback = sorted(all_candidates, key=lambda x: x['price_val'])[0]\n",
|
|
|
|
|
- " status_msg = f\"约 {best_fallback['price_val']}元 (⚠️超预算)\"\n",
|
|
|
|
|
|
|
+ " # 选离预算最近的\n",
|
|
|
|
|
+ " target = (budget_min + budget_max) / 2\n",
|
|
|
|
|
+ " best_fallback = sorted(all_candidates, key=lambda x: abs(x['price_val'] - target))[0]\n",
|
|
|
|
|
+ " p = best_fallback['price_val']\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " if p > limit_upper: status_msg = f\"约 {p}元 (⚠️超预算)\"\n",
|
|
|
|
|
+ " elif p < limit_lower: status_msg = f\"约 {p}元 (📉低于预算)\"\n",
|
|
|
|
|
+ " else: status_msg = f\"约 {p}元\"\n",
|
|
|
|
|
+ " \n",
|
|
|
" elif results_1:\n",
|
|
" elif results_1:\n",
|
|
|
- " # 连价格都没搜到,就硬取第一条文本结果\n",
|
|
|
|
|
|
|
+ " # 实在没数据,硬取第一条\n",
|
|
|
" for res in results_1:\n",
|
|
" for res in results_1:\n",
|
|
|
- " if res.get('type') == 'text':\n",
|
|
|
|
|
|
|
+ " if res.get('type') == 'text': \n",
|
|
|
" best_fallback = res\n",
|
|
" best_fallback = res\n",
|
|
|
|
|
+ " # 兜底时如果也没价格,就用原始关键词\n",
|
|
|
|
|
+ " best_fallback['source_kw'] = keyword \n",
|
|
|
" break\n",
|
|
" break\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
" if best_fallback:\n",
|
|
" if best_fallback:\n",
|
|
|
- " # 补图逻辑\n",
|
|
|
|
|
" if not best_fallback.get('img') and fallback_img:\n",
|
|
" if not best_fallback.get('img') and fallback_img:\n",
|
|
|
" best_fallback['img'] = fallback_img\n",
|
|
" best_fallback['img'] = fallback_img\n",
|
|
|
- " return best_fallback, status_msg, current_kw\n",
|
|
|
|
|
" \n",
|
|
" \n",
|
|
|
- " return None, \"搜索失败\", current_kw\n"
|
|
|
|
|
|
|
+ " # 📝【核心修复点2】返回结果里记录的那个 source_kw,而不是当前的 current_kw\n",
|
|
|
|
|
+ " final_name_to_use = best_fallback.get('source_kw', current_kw)\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " return best_fallback, status_msg, final_name_to_use\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " return None, \"搜索失败\", keyword"
|
|
|
]
|
|
]
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
@@ -511,17 +598,12 @@
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
- "# ==========================================\n",
|
|
|
|
|
- "# 🚀 主执行流程\n",
|
|
|
|
|
- "# ==========================================\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
"if not user_input_data:\n",
|
|
"if not user_input_data:\n",
|
|
|
" print(\"❌ 未加载用户数据\")\n",
|
|
" print(\"❌ 未加载用户数据\")\n",
|
|
|
"else:\n",
|
|
"else:\n",
|
|
|
" # 0. 解析预算\n",
|
|
" # 0. 解析预算\n",
|
|
|
- " budget_min, budget_max = parse_budget_range(user_input_data.get('预算', ''))\n",
|
|
|
|
|
- " budget_limit = budget_max * 1.2 # 允许超标 20%\n",
|
|
|
|
|
- " print(f\"\\n💰 预算红线: {budget_max}元 (容忍至 {budget_limit}元)\")\n",
|
|
|
|
|
|
|
+ " b_min, b_max = parse_budget_range(user_input_data.get('预算', ''))\n",
|
|
|
|
|
+ " print(f\"\\n💰 预算范围: {b_min} - {b_max}元\")\n",
|
|
|
"\n",
|
|
"\n",
|
|
|
" # 1. 军师制定策略\n",
|
|
" # 1. 军师制定策略\n",
|
|
|
" profile_text = \"\\n\".join([f\"- {k}: {v if v else '未知/不限'}\" for k, v in user_input_data.items()])\n",
|
|
" profile_text = \"\\n\".join([f\"- {k}: {v if v else '未知/不限'}\" for k, v in user_input_data.items()])\n",
|
|
@@ -540,8 +622,8 @@
|
|
|
" for index, kw in enumerate(keywords):\n",
|
|
" for index, kw in enumerate(keywords):\n",
|
|
|
" print(f\"\\n 👉 [商品 {index+1}/{len(keywords)}] 正在处理: {kw}\")\n",
|
|
" print(f\"\\n 👉 [商品 {index+1}/{len(keywords)}] 正在处理: {kw}\")\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " # 调用智能搜索函数\n",
|
|
|
|
|
- " valid_result, price_status, final_kw = find_best_product(hunter, profiler_agent, kw, budget_limit)\n",
|
|
|
|
|
|
|
+ " # 调用智能搜索函数 (传入 min 和 max)\n",
|
|
|
|
|
+ " valid_result, price_status, final_kw = find_best_product(hunter, profiler_agent, kw, b_min, b_max)\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
" if not valid_result:\n",
|
|
" if not valid_result:\n",
|
|
|
" print(\" ❌ 彻底无数据,跳过。\")\n",
|
|
" print(\" ❌ 彻底无数据,跳过。\")\n",
|
|
@@ -550,7 +632,7 @@
|
|
|
" # === 生成文案 ===\n",
|
|
" # === 生成文案 ===\n",
|
|
|
" product_name = valid_result.get('title', final_kw)\n",
|
|
" product_name = valid_result.get('title', final_kw)\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " print(f\" ✍️ 正在撰写文案: {product_name[:15]}...\")\n",
|
|
|
|
|
|
|
+ " print(f\" ✍️ 正在撰写文案: {product_name[:30]}...\")\n",
|
|
|
" pitch_prompt = f\"\"\"\n",
|
|
" pitch_prompt = f\"\"\"\n",
|
|
|
" 商品:{product_name}\n",
|
|
" 商品:{product_name}\n",
|
|
|
" 价格:{price_status}\n",
|
|
" 价格:{price_status}\n",
|
|
@@ -560,7 +642,6 @@
|
|
|
" \"\"\"\n",
|
|
" \"\"\"\n",
|
|
|
" pitch = pitcher_agent.run(pitch_prompt)\n",
|
|
" pitch = pitcher_agent.run(pitch_prompt)\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " # 整理最终数据\n",
|
|
|
|
|
" final_items.append({\n",
|
|
" final_items.append({\n",
|
|
|
" \"name\": final_kw, \n",
|
|
" \"name\": final_kw, \n",
|
|
|
" \"title_full\": product_name,\n",
|
|
" \"title_full\": product_name,\n",
|
|
@@ -588,28 +669,46 @@
|
|
|
"metadata": {},
|
|
"metadata": {},
|
|
|
"outputs": [],
|
|
"outputs": [],
|
|
|
"source": [
|
|
"source": [
|
|
|
- "# --- 4. 渲染与保存 (保持不变) ---\n",
|
|
|
|
|
|
|
+ "# --- 4. 渲染与保存 ---\n",
|
|
|
"print(f\"\\n💾 正在生成最终报告...\")\n",
|
|
"print(f\"\\n💾 正在生成最终报告...\")\n",
|
|
|
|
|
+ "\n",
|
|
|
"if not final_items:\n",
|
|
"if not final_items:\n",
|
|
|
" final_md = \"很抱歉,网络搜索似乎出现了问题,未能获取到任何商品信息。\"\n",
|
|
" final_md = \"很抱歉,网络搜索似乎出现了问题,未能获取到任何商品信息。\"\n",
|
|
|
"else:\n",
|
|
"else:\n",
|
|
|
" table_header = \"| 🎁 礼物名称 | 💰 价格 | ✨ 种草理由 | 🖼️ 图片/链接 |\\n| :--- | :--- | :--- | :--- |\\n\"\n",
|
|
" table_header = \"| 🎁 礼物名称 | 💰 价格 | ✨ 种草理由 | 🖼️ 图片/链接 |\\n| :--- | :--- | :--- | :--- |\\n\"\n",
|
|
|
" table_rows = []\n",
|
|
" table_rows = []\n",
|
|
|
|
|
+ " \n",
|
|
|
" for item in final_items:\n",
|
|
" for item in final_items:\n",
|
|
|
- " name = item['name'].replace(\"|\", \"/\")\n",
|
|
|
|
|
- " price = item['price']\n",
|
|
|
|
|
- " desc = item['desc'].replace(\"|\", \"/\")\n",
|
|
|
|
|
- " link = item['link']\n",
|
|
|
|
|
- " img = item['img']\n",
|
|
|
|
|
|
|
+ " # 1. 清洗文本字段 (防止 | 破坏表格)\n",
|
|
|
|
|
+ " name = item.get('name', '未知').replace(\"|\", \"/\")\n",
|
|
|
|
|
+ " price = item.get('price', '暂无').replace(\"|\", \"/\")\n",
|
|
|
|
|
+ " desc = item.get('desc', '').replace(\"|\", \"/\")\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " # 2. 🚨【核心修复】清洗链接中的竖线\n",
|
|
|
|
|
+ " # 百度/京东链接常包含 '|',必须替换为 '%7C',否则 Markdown 表格会炸\n",
|
|
|
|
|
+ " raw_link = item.get('link', '#')\n",
|
|
|
|
|
+ " safe_link = raw_link.replace(\"|\", \"%7C\")\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " if img and img.startswith(\"http\"):\n",
|
|
|
|
|
- " media = f\"[]({link})\"\n",
|
|
|
|
|
|
|
+ " raw_img = item.get('img', '')\n",
|
|
|
|
|
+ " safe_img = raw_img.replace(\"|\", \"%7C\")\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " # 3. 构建媒体列\n",
|
|
|
|
|
+ " if safe_img and safe_img.startswith(\"http\"):\n",
|
|
|
|
|
+ " # 图片链接套购买链接\n",
|
|
|
|
|
+ " media = f\"[]({safe_link})\"\n",
|
|
|
" else:\n",
|
|
" else:\n",
|
|
|
- " media = f\"[点击购买]({link})\"\n",
|
|
|
|
|
|
|
+ " media = f\"[点击购买]({safe_link})\"\n",
|
|
|
|
|
+ " \n",
|
|
|
|
|
+ " # 4. 组装行 (注意名字上的链接也要用 safe_link)\n",
|
|
|
|
|
+ " # 使用 strip() 去除可能的首尾空格\n",
|
|
|
|
|
+ " row = f\"| [{name}]({safe_link}) | {price} | {desc} | {media} |\"\n",
|
|
|
|
|
+ " table_rows.append(row)\n",
|
|
|
" \n",
|
|
" \n",
|
|
|
- " table_rows.append(f\"| [{name}]({link}) | {price} | {desc} | {media} |\")\n",
|
|
|
|
|
" final_md = table_header + \"\\n\".join(table_rows)\n",
|
|
" final_md = table_header + \"\\n\".join(table_rows)\n",
|
|
|
"filename = \"outputs/gift_plan_output.md\"\n",
|
|
"filename = \"outputs/gift_plan_output.md\"\n",
|
|
|
|
|
+ "# 确保输出目录存在\n",
|
|
|
|
|
+ "os.makedirs(os.path.dirname(filename), exist_ok=True)\n",
|
|
|
|
|
+ "\n",
|
|
|
"with open(filename, \"w\", encoding=\"utf-8\") as f:\n",
|
|
"with open(filename, \"w\", encoding=\"utf-8\") as f:\n",
|
|
|
" f.write(final_md)\n",
|
|
" f.write(final_md)\n",
|
|
|
"print(f\"🎉 任务完成!文件已保存: {os.path.abspath(filename)}\")"
|
|
"print(f\"🎉 任务完成!文件已保存: {os.path.abspath(filename)}\")"
|
|
@@ -631,16 +730,20 @@
|
|
|
"#### 实现的功能\n",
|
|
"#### 实现的功能\n",
|
|
|
"- 基于用户输入的个人信息,生成符合预算的礼物建议\n",
|
|
"- 基于用户输入的个人信息,生成符合预算的礼物建议\n",
|
|
|
"- 支持用户自定义预算范围、节日、个人喜好等\n",
|
|
"- 支持用户自定义预算范围、节日、个人喜好等\n",
|
|
|
- "- 利用搜索引擎获取最新的商品信息和价格\n",
|
|
|
|
|
|
|
+ "- 支持百度MCP和Tavily API双数据源,利用搜索引擎获取最新的商品信息和价格\n",
|
|
|
"- 提供可视化的建议结果展示\n",
|
|
"- 提供可视化的建议结果展示\n",
|
|
|
- "#### 遇到的挑战\n",
|
|
|
|
|
- "- 如何解决大模型的幻觉问题\n",
|
|
|
|
|
- "- 如何解决上下文过长导致提取失败\n",
|
|
|
|
|
- "- 如何设计合理的提示词,优化大模型的输出\n",
|
|
|
|
|
- "\n",
|
|
|
|
|
|
|
+ "#### 遇到的挑战与解决方案\n",
|
|
|
|
|
+ "- 大模型的“幻觉”问题(JSON格式错误/编造数据)\n",
|
|
|
|
|
+ " - 解决方案:放弃让 LLM 直接生成最终数据。改为使用 Python 正则表达式 从搜索结果中暴力提取硬数据(价格、图片),仅让 LLM 负责生成文案。代码逻辑负责准确性,模型负责创造性。\n",
|
|
|
|
|
+ "- 上下文过长导致提取失败\n",
|
|
|
|
|
+ " - 解决方案:结合实际业务场景,分析各个阶段对上下文的要求,在搜索阶段限制返回长度。同时通过拆分 “硬数据流”(找参数)和 “软数据流”(找卖点),大幅降低单次上下文长度,提升响应速度。\n",
|
|
|
|
|
+ "- 大模型推荐的礼品价格超出预算\n",
|
|
|
|
|
+ " - 解决方案:引入检核机制。如果搜到的商品均价超预算,系统会自动呼叫“军师”重新制定“平替”策略,直到找到合适商品为止。\n",
|
|
|
|
|
+ "- Agent传入的参数格式问题\n",
|
|
|
|
|
+ " - 解决方案:在工具层兼容 Agent 传入的各种参数格式(JSON/字符串、逗号/换行符分隔),确保搜索指令不丢失。\n",
|
|
|
"#### 未来改进方向\n",
|
|
"#### 未来改进方向\n",
|
|
|
- "- 前端交互:新增前端页面,提供更好的用户交互体验\n",
|
|
|
|
|
- "- 数据源增强:接入百度或BigGo的MCP Server,获取更精准的实时价格和商品信息\n",
|
|
|
|
|
|
|
+ "- 前端交互:开发前端页面,替代目前的Notebook交互,提供更好的用户交互体验\n",
|
|
|
|
|
+ "- 数据源深度集成:完全接入百度优选MCP 的比价与历史价格接口,获取更精准的实时价格和库存信息,实现“全网比价”\n",
|
|
|
"- 丰富选项:增加更多的个人喜好选项,如喜欢的商品类型、品牌等\n"
|
|
"- 丰富选项:增加更多的个人喜好选项,如喜欢的商品类型、品牌等\n"
|
|
|
]
|
|
]
|
|
|
}
|
|
}
|