yangxiaohui 1 month ago
parent
commit
f0d589bf32
12 changed files with 2150 additions and 18 deletions
  1. 1 0
      .gitignore
  2. 94 0
      input/kg_v1.md
  3. 93 0
      input/kg_v1_single.md
  4. 88 0
      input/kg_v1_single_context.md
  5. 1 0
      input/kg_v1_single_q.md
  6. 78 0
      lib/my_trace.py
  7. 624 0
      lib/utils.py
  8. 44 18
      script/search_recommendations/xiaohongshu_search_recommendations.py
  9. 193 0
      sug_v1.py
  10. 316 0
      sug_v1_1.py
  11. 277 0
      sug_v2.py
  12. 341 0
      test.py

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
 data
 __pycache__
 *.swp
+tmp

+ 94 - 0
input/kg_v1.md

@@ -0,0 +1,94 @@
+<!-- 
+测试用例: case1
+生成时间: 2025-10-22 15:10:43
+工作流: KnowledgeQueryWorkflow
+-->
+
+#整体项目目标:
+针对小红书图文帖子进行逆向解构,还原完整创作流程(how)、基础信息(what)和创作原因(why),帮助学习创作逻辑,让内容创作流程可复制、可优化、可规模化。
+
+#本次任务目标:
+还原【幽默职场吐槽猫咪梗图】的完整创作方法论,为后续解构提供系统化知识基础。
+
+#上下文
+Phase 1 - 全面方法论还原阶段。覆盖创作阶段和制作阶段的完整方法论,为后续解构提供知识基础。
+
+#待解构帖子信息:
+**帖子链接**: https://www.xiaohongshu.com/explore/684c29e0000000002203561b
+**帖子标题**: 《我来上班只办七件事》
+**帖子正文**: 每周总有那么七天不想上班💔💔我来上班我只办七件事:摸鱼、喝水、吃饭、偷电、上厕所、蹭空调、到点就下班…人生区区三万天,心态好一天是一天🫶🫶上班而已,不要太在意职场上有的没的,有这时间不如上美团薅🐏毛,是谁还不知道0.1r就能喝到古茗咖啡啦☕打开美团外卖 🔍「自取古茗」进入活动页面后,咖啡口味多多,任你选[喝奶茶R][喝奶茶R]美式咖啡(限量版)0.1r,美式咖啡1.9r[棒R]GOOD轻椰拿铁2.9r,生椰拿铁5.9r[棒R]鲜奶拿铁5.9r,冰咖小黄柠5.9r[棒R]周一到周五的咖啡都给你安排好了,午休摸鱼出去门店自取,比点外卖便宜!!#外卖自取[话题]# #外卖自取更快更省[话题]# #美团[话题]# #美团外卖[话题]##自取古茗[话题]# #小红书养宠倡议[话题]# #打工人精神状态[话题]# #表情包[话题]# #摸鱼一刻[话题]# #上班摸鱼[话题]#
+**图片数量**: 7
+**图片列表**:
+- 图1: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图2: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图3: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图4: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图5: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图6: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图7: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+**内容类型**: 幽默职场吐槽猫咪梗图
+**核心内容**: 以猫咪拟人化形象幽默吐槽职场打工人的日常“摸鱼”行为和心态。
+
+#需求
+
+##内容知识需求
+
+###创作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在创作阶段需要了解:
+
+- 如何从猫咪视角捕捉职场打工人的共鸣点和幽默瞬间(如“摸鱼”、“偷电”、“卡点下班”)?
+- 如何获取与职场生活相关的创意灵感,并将其转化为猫咪拟人化的场景?
+- 如何设计“上班七件事”这类清单式、有节奏感的吐槽主题?
+- 如何撰写口语化、带有网络流行语和表情符号的幽默文案,以增强猫咪拟人化效果和共鸣感?
+- 如何构建帖子标题和正文,使其既能表达猫咪的情绪,又能引导读者产生共鸣?
+
+###制作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在制作阶段需要了解:
+
+- 制作此类多图猫咪梗图的完整工序是什么?(从素材准备到最终合成)
+- 如何获取或创作具有特定表情和姿态的白色橘斑猫咪图片素材?
+- 如何寻找或制作与职场场景相符的拟人化道具(如领带、帽子、手机、咖啡杯、办公用品)?
+- 如何将猫咪主体、拟人化道具和办公室背景进行无缝合成,并保持视觉风格统一?
+- 如何设计和叠加“大字报式”的文字,使其清晰、醒目,并与猫咪表情和场景内容相匹配?
+- 如何对图片进行后期处理,确保色彩明亮、猫咪突出,并营造幽默搞怪的整体氛围?
+
+##工具知识需求
+
+###需求约束:
+工具范围限定为: API、MCP、在线平台、脚本/插件
+(不包括桌面软件和移动应用)
+
+###工具要求:
+对于以下每个功能需求,必须包含:
+- **工具名称**:具体的工具/服务名称
+- **适用场景**:该工具适用的具体场景
+- **使用方法**:如何使用该工具实现功能
+
+###创作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在创作阶段需要能够实现以下功能的工具:
+
+- 捕捉和分析职场热点话题、流行梗和打工人情绪
+- 辅助生成或优化幽默、口语化的文案和标题
+- 收集和管理猫咪拟人化创意灵感和场景构思
+
+###制作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在制作阶段需要能够实现以下功能的工具:
+
+- 快速进行图片背景移除和替换
+- 将多个图片元素(猫咪、道具、背景)进行合成和图层管理
+- 添加自定义字体、颜色和描边的文字叠加效果
+- 搜索、获取或生成特定品种(如白色橘斑)和姿态的猫咪图片素材
+- 查找或创建各种拟人化服饰和办公用品小道具素材
+- 批量处理图片,调整色彩、光影,保持多图视觉风格一致性

+ 93 - 0
input/kg_v1_single.md

@@ -0,0 +1,93 @@
+<需求上下文>
+#整体项目目标:
+针对小红书图文帖子进行逆向解构,还原完整创作流程(how)、基础信息(what)和创作原因(why),帮助学习创作逻辑,让内容创作流程可复制、可优化、可规模化。
+
+#本次任务目标:
+还原【幽默职场吐槽猫咪梗图】的完整创作方法论,为后续解构提供系统化知识基础。
+
+#上下文
+Phase 1 - 全面方法论还原阶段。覆盖创作阶段和制作阶段的完整方法论,为后续解构提供知识基础。
+
+#待解构帖子信息:
+**帖子链接**: https://www.xiaohongshu.com/explore/684c29e0000000002203561b
+**帖子标题**: 《我来上班只办七件事》
+**帖子正文**: 每周总有那么七天不想上班💔💔我来上班我只办七件事:摸鱼、喝水、吃饭、偷电、上厕所、蹭空调、到点就下班…人生区区三万天,心态好一天是一天🫶🫶上班而已,不要太在意职场上有的没的,有这时间不如上美团薅🐏毛,是谁还不知道0.1r就能喝到古茗咖啡啦☕打开美团外卖 🔍「自取古茗」进入活动页面后,咖啡口味多多,任你选[喝奶茶R][喝奶茶R]美式咖啡(限量版)0.1r,美式咖啡1.9r[棒R]GOOD轻椰拿铁2.9r,生椰拿铁5.9r[棒R]鲜奶拿铁5.9r,冰咖小黄柠5.9r[棒R]周一到周五的咖啡都给你安排好了,午休摸鱼出去门店自取,比点外卖便宜!!#外卖自取[话题]# #外卖自取更快更省[话题]# #美团[话题]# #美团外卖[话题]##自取古茗[话题]# #小红书养宠倡议[话题]# #打工人精神状态[话题]# #表情包[话题]# #摸鱼一刻[话题]# #上班摸鱼[话题]#
+**图片数量**: 7
+**图片列表**:
+- 图1: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图2: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图3: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图4: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图5: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图6: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图7: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+**内容类型**: 幽默职场吐槽猫咪梗图
+**核心内容**: 以猫咪拟人化形象幽默吐槽职场打工人的日常“摸鱼”行为和心态。
+
+#需求
+
+##内容知识需求
+
+###创作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在创作阶段需要了解:
+
+- 如何从猫咪视角捕捉职场打工人的共鸣点和幽默瞬间(如“摸鱼”、“偷电”、“卡点下班”)?
+- 如何获取与职场生活相关的创意灵感,并将其转化为猫咪拟人化的场景?
+- 如何设计“上班七件事”这类清单式、有节奏感的吐槽主题?
+- 如何撰写口语化、带有网络流行语和表情符号的幽默文案,以增强猫咪拟人化效果和共鸣感?
+- 如何构建帖子标题和正文,使其既能表达猫咪的情绪,又能引导读者产生共鸣?
+
+###制作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在制作阶段需要了解:
+
+- 制作此类多图猫咪梗图的完整工序是什么?(从素材准备到最终合成)
+- 如何获取或创作具有特定表情和姿态的白色橘斑猫咪图片素材?
+- 如何寻找或制作与职场场景相符的拟人化道具(如领带、帽子、手机、咖啡杯、办公用品)?
+- 如何将猫咪主体、拟人化道具和办公室背景进行无缝合成,并保持视觉风格统一?
+- 如何设计和叠加“大字报式”的文字,使其清晰、醒目,并与猫咪表情和场景内容相匹配?
+- 如何对图片进行后期处理,确保色彩明亮、猫咪突出,并营造幽默搞怪的整体氛围?
+
+##工具知识需求
+
+###需求约束:
+工具范围限定为: API、MCP、在线平台、脚本/插件
+(不包括桌面软件和移动应用)
+
+###工具要求:
+对于以下每个功能需求,必须包含:
+- **工具名称**:具体的工具/服务名称
+- **适用场景**:该工具适用的具体场景
+- **使用方法**:如何使用该工具实现功能
+
+###创作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在创作阶段需要能够实现以下功能的工具:
+
+- 捕捉和分析职场热点话题、流行梗和打工人情绪
+- 辅助生成或优化幽默、口语化的文案和标题
+- 收集和管理猫咪拟人化创意灵感和场景构思
+
+###制作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在制作阶段需要能够实现以下功能的工具:
+
+- 快速进行图片背景移除和替换
+- 将多个图片元素(猫咪、道具、背景)进行合成和图层管理
+- 添加自定义字体、颜色和描边的文字叠加效果
+- 搜索、获取或生成特定品种(如白色橘斑)和姿态的猫咪图片素材
+- 查找或创建各种拟人化服饰和办公用品小道具素材
+- 批量处理图片,调整色彩、光影,保持多图视觉风格一致性
+</需求上下文>
+<当前问题>
+快速进行图片背景移除和替换
+</当前问题>

+ 88 - 0
input/kg_v1_single_context.md

@@ -0,0 +1,88 @@
+#整体项目目标:
+针对小红书图文帖子进行逆向解构,还原完整创作流程(how)、基础信息(what)和创作原因(why),帮助学习创作逻辑,让内容创作流程可复制、可优化、可规模化。
+
+#本次任务目标:
+还原【幽默职场吐槽猫咪梗图】的完整创作方法论,为后续解构提供系统化知识基础。
+
+#上下文
+Phase 1 - 全面方法论还原阶段。覆盖创作阶段和制作阶段的完整方法论,为后续解构提供知识基础。
+
+#待解构帖子信息:
+**帖子链接**: https://www.xiaohongshu.com/explore/684c29e0000000002203561b
+**帖子标题**: 《我来上班只办七件事》
+**帖子正文**: 每周总有那么七天不想上班💔💔我来上班我只办七件事:摸鱼、喝水、吃饭、偷电、上厕所、蹭空调、到点就下班…人生区区三万天,心态好一天是一天🫶🫶上班而已,不要太在意职场上有的没的,有这时间不如上美团薅🐏毛,是谁还不知道0.1r就能喝到古茗咖啡啦☕打开美团外卖 🔍「自取古茗」进入活动页面后,咖啡口味多多,任你选[喝奶茶R][喝奶茶R]美式咖啡(限量版)0.1r,美式咖啡1.9r[棒R]GOOD轻椰拿铁2.9r,生椰拿铁5.9r[棒R]鲜奶拿铁5.9r,冰咖小黄柠5.9r[棒R]周一到周五的咖啡都给你安排好了,午休摸鱼出去门店自取,比点外卖便宜!!#外卖自取[话题]# #外卖自取更快更省[话题]# #美团[话题]# #美团外卖[话题]##自取古茗[话题]# #小红书养宠倡议[话题]# #打工人精神状态[话题]# #表情包[话题]# #摸鱼一刻[话题]# #上班摸鱼[话题]#
+**图片数量**: 7
+**图片列表**:
+- 图1: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图2: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图3: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图4: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图5: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图6: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+- 图7: https://sns-webpic-qc.xhscdn.com/20240507/03/52/a9223700000032320200002203561b_origin_1024x1024.jpeg
+**内容类型**: 幽默职场吐槽猫咪梗图
+**核心内容**: 以猫咪拟人化形象幽默吐槽职场打工人的日常“摸鱼”行为和心态。
+
+#需求
+
+##内容知识需求
+
+###创作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在创作阶段需要了解:
+
+- 如何从猫咪视角捕捉职场打工人的共鸣点和幽默瞬间(如“摸鱼”、“偷电”、“卡点下班”)?
+- 如何获取与职场生活相关的创意灵感,并将其转化为猫咪拟人化的场景?
+- 如何设计“上班七件事”这类清单式、有节奏感的吐槽主题?
+- 如何撰写口语化、带有网络流行语和表情符号的幽默文案,以增强猫咪拟人化效果和共鸣感?
+- 如何构建帖子标题和正文,使其既能表达猫咪的情绪,又能引导读者产生共鸣?
+
+###制作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在制作阶段需要了解:
+
+- 制作此类多图猫咪梗图的完整工序是什么?(从素材准备到最终合成)
+- 如何获取或创作具有特定表情和姿态的白色橘斑猫咪图片素材?
+- 如何寻找或制作与职场场景相符的拟人化道具(如领带、帽子、手机、咖啡杯、办公用品)?
+- 如何将猫咪主体、拟人化道具和办公室背景进行无缝合成,并保持视觉风格统一?
+- 如何设计和叠加“大字报式”的文字,使其清晰、醒目,并与猫咪表情和场景内容相匹配?
+- 如何对图片进行后期处理,确保色彩明亮、猫咪突出,并营造幽默搞怪的整体氛围?
+
+##工具知识需求
+
+###需求约束:
+工具范围限定为: API、MCP、在线平台、脚本/插件
+(不包括桌面软件和移动应用)
+
+###工具要求:
+对于以下每个功能需求,必须包含:
+- **工具名称**:具体的工具/服务名称
+- **适用场景**:该工具适用的具体场景
+- **使用方法**:如何使用该工具实现功能
+
+###创作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在创作阶段需要能够实现以下功能的工具:
+
+- 捕捉和分析职场热点话题、流行梗和打工人情绪
+- 辅助生成或优化幽默、口语化的文案和标题
+- 收集和管理猫咪拟人化创意灵感和场景构思
+
+###制作阶段:
+
+####需求描述:
+
+当前需要制作幽默职场吐槽猫咪梗图,在制作阶段需要能够实现以下功能的工具:
+
+- 快速进行图片背景移除和替换
+- 将多个图片元素(猫咪、道具、背景)进行合成和图层管理
+- 添加自定义字体、颜色和描边的文字叠加效果
+- 搜索、获取或生成特定品种(如白色橘斑)和姿态的猫咪图片素材
+- 查找或创建各种拟人化服饰和办公用品小道具素材
+- 批量处理图片,调整色彩、光影,保持多图视觉风格一致性

+ 1 - 0
input/kg_v1_single_q.md

@@ -0,0 +1 @@
+如何获取或创作具有特定表情和姿态的白色橘斑猫咪图片素材?

+ 78 - 0
lib/my_trace.py

@@ -0,0 +1,78 @@
+from datetime import datetime
+import logging
+
+def get_current_time():
+    import uuid
+    random_uuid = str(uuid.uuid4())
+    return datetime.now().strftime("%Y%m%d/%H%M%S") + "_" + random_uuid[:2]
+
+def set_trace_logfire():
+    from agents.tracing.setup import GLOBAL_TRACE_PROVIDER
+    GLOBAL_TRACE_PROVIDER.shutdown()
+    import logfire
+    current_time = get_current_time()
+    logfire.configure(service_name=f'{current_time}')
+    logfire.instrument_openai_agents()
+    import urllib.parse
+    current_time_encoded = urllib.parse.quote(current_time)
+    import logging
+    LOG_LEVEL = "WARNING"
+    # 设置日志
+    logging.basicConfig(
+        level=getattr(logging, LOG_LEVEL),
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+    )
+    logger = logging.getLogger(__name__)
+    log_url = f'https://logfire-us.pydantic.dev/semsevens/test?q=service_name+%3D+%27{current_time_encoded}%27&last=30d'
+    logger.warning(f"任务日志链接: {log_url}")
+    return current_time, log_url
+
+def set_trace():
+    # 设置全局logging级别,覆盖所有子模块
+    logging.basicConfig(level=logging.WARNING, force=True)
+    # 确保根logger级别生效
+    logging.getLogger().setLevel(logging.WARNING)
+    return set_trace_logfire()
+
+
+def set_trace_smith():
+    from agents.tracing import GLOBAL_TRACE_PROVIDER
+    GLOBAL_TRACE_PROVIDER.shutdown()
+    from agents import set_trace_processors
+    from langsmith.wrappers import OpenAIAgentsTracingProcessor
+    import logging
+    current_time = get_current_time()
+    set_trace_processors([OpenAIAgentsTracingProcessor(name=f'{current_time}')])
+    import urllib.parse
+    LOG_LEVEL = "WARNING"
+    # 设置日志
+    logging.basicConfig(
+        level=getattr(logging, LOG_LEVEL),
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+    )
+    logger = logging.getLogger(__name__)
+    current_time_encoded = urllib.parse.quote(current_time)
+    log_url = f'https://smith.langchain.com/o/3ebe0715-9709-4594-a0aa-40a77a4e10bd/projects/p/611fa0d6-5510-4f60-b693-87e2ccc2ea5f?timeModel=%7B%22duration%22%3A%227d%22%7D&searchModel=%7B%22filter%22%3A%22and%28eq%28is_root%2C+true%29%2C+eq%28name%2C+%5C%22{current_time_encoded}%5C%22%29%29%22%2C%22searchFilter%22%3A%22eq%28is_root%2C+true%29%22%7D'
+    LOG_LEVEL = "WARNING"
+    logger.warning(f"任务日志链接: {log_url}")
+        
+def set_debug():
+    import logging
+    # 设置全局日志级别为DEBUG,确保所有模块生效
+    logging.basicConfig(
+        level=logging.DEBUG,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+    )
+    # 确保根日志记录器也设置为DEBUG级别
+    logging.getLogger().setLevel(logging.DEBUG)
+
+def set_info():
+    import logging
+    # 设置全局日志级别为INFO,确保所有模块生效
+    logging.basicConfig(
+        level=logging.INFO,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+    )
+    # 确保根日志记录器也设置为INFO级别
+    logging.getLogger().setLevel(logging.INFO)
+    

+ 624 - 0
lib/utils.py

@@ -0,0 +1,624 @@
+from typing import List, Dict, Any
+import json
+from .my_trace import get_current_time
+import re
+import uuid
+import datetime
+
+def parse_json_from_text(text: str) -> dict:
+    """
+    从文本中解析JSON,支持多种格式的JSON代码块
+    
+    Args:
+        text (str): 包含JSON的文本
+    
+    Returns:
+        dict: 解析后的JSON数据,解析失败返回空字典
+    """
+    if not text or not isinstance(text, str):
+        return {}
+    
+    # 去除首尾空白字符
+    text = text.strip()
+    
+    # 定义可能的JSON代码块标记
+    json_markers = [
+        ("'''json", "'''"),
+        ('"""json', '"""'),
+        ("```json", "```"),
+        ("```", "```")
+    ]
+    
+    # 尝试提取JSON代码块
+    json_content = text
+    for start_marker, end_marker in json_markers:
+        if text.startswith(start_marker):
+            # 找到开始标记,查找结束标记
+            start_pos = len(start_marker)
+            end_pos = text.find(end_marker, start_pos)
+            if end_pos != -1:
+                json_content = text[start_pos:end_pos].strip()
+                break
+    
+    # 如果没有找到代码块标记,检查是否以结束标记结尾并移除
+    if json_content == text:
+        for _, end_marker in json_markers:
+            if text.endswith(end_marker):
+                json_content = text[:-len(end_marker)].strip()
+                break
+    
+    # 尝试解析JSON
+    try:
+        return json.loads(json_content)
+    except json.JSONDecodeError as e:
+        print(f"JSON解析失败: {e}")
+        # 如果直接解析失败,尝试查找第一个{到最后一个}的内容
+        try:
+            first_brace = json_content.find('{')
+            last_brace = json_content.rfind('}')
+            if first_brace != -1 and last_brace != -1 and first_brace < last_brace:
+                json_part = json_content[first_brace:last_brace + 1]
+                return json.loads(json_part)
+        except json.JSONDecodeError:
+            pass
+        
+        return {}
+
+
+def get_safe_filename(filename: str) -> str:
+    """
+    生成安全的文件名,移除不安全字符
+    
+    Args:
+        filename: 原始文件名
+        
+    Returns:
+        str: 安全的文件名
+    """
+    # 移除不安全的字符,只保留字母、数字、下划线、连字符和点
+    return re.sub(r'[^\w\-\./]', '_', filename)
+
+
+def generate_image_filename(mime_type: str, prefix: str = "gemini_img") -> str:
+    """
+    生成合理的图片文件名
+
+    Args:
+        mime_type: 文件MIME类型
+        prefix: 文件名前缀
+
+    Returns:
+        str: 生成的文件名
+    """
+    # 获取当前时间戳
+    timestamp = datetime.datetime.now().strftime("%Y%m%d/%H%M%S")
+
+    # 获取文件扩展名
+    extension = mime_type.split('/')[-1]
+    if extension == "jpeg":
+        extension = "jpg"
+
+    # 生成唯一ID (短UUID)
+    unique_id = str(uuid.uuid4())[:4]
+
+    # 组合文件名
+    filename = f"{prefix}/{timestamp}_{unique_id}.{extension}"
+
+    # 确保文件名安全
+    return get_safe_filename(filename)
+
+def parse_multimodal_content(content: str) -> List[Dict[str, Any]]:
+    """解析多模态内容,保持上下文顺序,适用于AI参数传递 """
+    
+    result = []
+    lines = content.split('\n')
+    role = ''
+    
+    for line in lines:
+        line = line.strip()
+        if not line:
+            continue
+            
+        # 分割前缀和内容
+        if ':' in line:
+            prefix, content = line.split(':', 1)
+            prefix = prefix.strip().lower()
+            content = content.strip()
+            row = {}
+            if prefix == 'image':
+                row = {
+                    "type": "image_url",
+                    "image_url": {
+                        "url": content
+                    }
+                }
+            elif prefix == 'text':
+                row = {
+                    "type": "text",
+                    "text": content
+                }
+            elif prefix == 'role':
+                role = content
+            if row:
+                if role:
+                    row['role'] = role
+                    role = ''
+                result.append(row)
+    
+    return result
+
+
+def read_json(file_path):
+    """
+    读取JSON文件并返回解析后的数据
+    
+    Args:
+        file_path: JSON文件路径
+        
+    Returns:
+        解析后的JSON数据
+    """
+    try:
+        with open(file_path, 'r', encoding='utf-8') as f:
+            return json.load(f)
+    except Exception as e:
+        print(f"读取JSON文件时出错: {e}")
+        return None
+
+def save_json(data, file_path):
+    """
+    保存数据到JSON文件
+    
+    Args:
+        data: 要保存的数据
+        file_path: 保存路径
+    """
+    with open(file_path, 'w', encoding='utf-8') as f:
+        json.dump(data, f, ensure_ascii=False, indent=2)
+        
+
+def get_script_data(file_path):
+    """
+    读取JSON文件并返回解析后的数据
+    
+    Args:
+        file_path: JSON文件路径
+    """
+    return read_json(file_path)['脚本']
+
+import os
+import xml.etree.ElementTree as ET
+from typing import Dict, List, Any
+import re
+import unicodedata
+
+
+def get_model(model_name):
+    # return 'gemini/gemini-2.5-flash'
+    # return 'litellm/gemini/gemini-2.5-flash'
+    if model_name.startswith('litellm'):
+        return model_name
+    else:
+        from openai import AsyncOpenAI
+        from agents import OpenAIChatCompletionsModel
+        BASE_URL = os.getenv("EXAMPLE_BASE_URL") or "https://openrouter.ai/api/v1"
+        API_KEY = os.getenv("OPENROUTER_API_KEY") or ""
+        client = AsyncOpenAI(
+            base_url=BASE_URL,
+            api_key=API_KEY,
+        )
+        return OpenAIChatCompletionsModel(
+            # model='google/gemini-2.5-pro-preview',
+            # model='google/gemini-2.5-flash-preview-05-20',
+            # model='google/gemini-2.5-flash-preview-05-20',
+            # model='google/gemini-2.5-flash',
+            # model='google/gemini-2.5-flash',
+            # model='google/gemini-2.5-flash-preview-05-20:thinking',
+            # model='google/gemini-2.0-flash-001',
+            model=model_name,
+            openai_client=client,
+        )
+
+def read_file_as_string(file_path):
+    """读取文件内容并返回字符串"""
+    try:
+        with open(file_path, 'r', encoding='utf-8') as file:
+            content = file.read().strip()
+        return content
+    except Exception as e:
+        print(f"读取文件时出错: {e}")
+        return None
+def save_file_as_string(file_path, content):
+    """将字符串内容写入文件"""
+    with open(file_path, 'w', encoding='utf-8') as f:
+        f.write(content)
+
+def extract_html_from_markdown(text):
+    """
+    从可能包含markdown或其他代码块的文本中提取HTML内容
+    
+    参数:
+        text: 可能包含各种格式的文本
+        
+    返回:
+        提取出的纯HTML内容
+    """
+    # 处理```html```格式(反引号)
+    backtick_pattern = r"```(?:html)?\s*([\s\S]*?)```"
+    backtick_matches = re.findall(backtick_pattern, text)
+    
+    # 处理'''html'''格式(单引号)
+    single_quote_pattern = r"'''(?:html)?\s*([\s\S]*?)'''"
+    single_quote_matches = re.findall(single_quote_pattern, text)
+    
+    # 处理"""html"""格式(双引号)
+    double_quote_pattern = r'"""(?:html)?\s*([\s\S]*?)"""'
+    double_quote_matches = re.findall(double_quote_pattern, text)
+    
+    if backtick_matches:
+        # 优先使用反引号格式
+        return backtick_matches[0].strip()
+    elif single_quote_matches:
+        # 其次使用单引号格式
+        return single_quote_matches[0].strip()
+    elif double_quote_matches:
+        # 再次使用双引号格式
+        return double_quote_matches[0].strip()
+    else:
+        # 如果没有代码块格式,直接返回原get_current_time始文本
+        return text
+    
+def create_workspace_dir(current_time=None, make_dir=True):
+    if not current_time:
+        current_time = get_current_time()
+    task_dir = f"result/{current_time}"
+    if make_dir:
+        os.makedirs(task_dir, exist_ok=True)
+    task_dir_absolute = os.path.abspath(task_dir)
+    # print(f"任务目录的绝对路径: {task_dir_absolute}")
+    return task_dir_absolute, str(current_time)
+
+
+def extract_tag_content(text, tag_name):
+    """
+    从文本中提取指定标签内的内容
+    
+    参数:
+        text (str): 要处理的文本
+        tag_name (str): 要提取的标签名称
+    
+    返回:
+        str: 标签内的内容,如果未找到则返回空字符串
+    """
+    import re
+    pattern = f"<{tag_name}>(.*?)</{tag_name}>"
+    match = re.search(pattern, text, re.DOTALL)
+    if match:
+        return match.group(1).strip()
+    return ""
+
+from typing import Dict, List, Optional
+def parse_tasks(tasks_xml: str) -> List[Dict]:
+    """Parse XML tasks into a list of task dictionaries."""
+    tasks = []
+    current_task = {}
+    
+    for line in tasks_xml.split('\n'):
+        line = line.strip()
+        if not line:
+            continue
+            
+        if line.startswith("<task>"):
+            current_task = {}
+        elif line.startswith("<name>"):
+            current_task["name"] = line[6:-7].strip()
+        elif line.startswith("<output>"):
+            current_task["output"] = line[12:-13].strip()
+        elif line.startswith("</task>"):
+            if "description" in current_task:
+                if "type" not in current_task:
+                    current_task["type"] = "default"
+                tasks.append(current_task)
+    
+    return tasks
+    
+    
+def parse_xml_content(xml_string: str) -> Dict[str, Any]:
+    """
+    将XML字符串解析成字典,提取main_task、thoughts、tasks和resources
+    
+    参数:
+        xml_string: 包含任务信息的XML字符串
+        
+    返回:
+        包含所有解析信息的字典
+    """
+    # 创建结果字典
+    result = {
+        "main_task": {},
+        "thoughts": "",
+        "tasks": [],
+        "resources": []
+    }
+    
+    try:
+        # 提取thoughts内容
+        thoughts_match = re.search(r'<thoughts>(.*?)</thoughts>', xml_string, re.DOTALL)
+        if thoughts_match:
+            result["thoughts"] = thoughts_match.group(1).strip()
+        
+        # 提取main_task内容
+        main_task_match = re.search(r'<main_task>(.*?)</main_task>', xml_string, re.DOTALL)
+        if main_task_match:
+            main_task_content = main_task_match.group(1)
+            main_task = {}
+            
+            # 获取主任务名称
+            name_match = re.search(r'<name>(.*?)</name>', main_task_content, re.DOTALL)
+            if name_match:
+                main_task['name'] = name_match.group(1).strip()
+            
+            # 获取主任务输出
+            output_match = re.search(r'<output>(.*?)</output>', main_task_content, re.DOTALL)
+            if output_match:
+                main_task['output'] = output_match.group(1).strip()
+            
+            # 获取主任务描述
+            description_match = re.search(r'<description>(.*?)</description>', main_task_content, re.DOTALL)
+            if description_match:
+                main_task['description'] = description_match.group(1).strip()
+            
+            result["main_task"] = main_task
+        
+        # 提取<tasks>...</tasks>部分
+        tasks_pattern = re.compile(r'<tasks>(.*?)</tasks>', re.DOTALL)
+        tasks_match = tasks_pattern.search(xml_string)
+        
+        if tasks_match:
+            tasks_content = tasks_match.group(1)
+            
+            # 提取每个task块
+            task_pattern = re.compile(r'<task>(.*?)</task>', re.DOTALL)
+            task_matches = task_pattern.finditer(tasks_content)
+            
+            for task_match in task_matches:
+                task_content = task_match.group(1)
+                task_dict = {}
+                
+                # 获取任务名称
+                name_match = re.search(r'<name>(.*?)</name>', task_content, re.DOTALL)
+                if not name_match:
+                    continue  # 跳过没有名称的任务
+                
+                name = name_match.group(1).strip()
+                task_dict['name'] = name
+                # 获取输出信息
+                output_match = re.search(r'<output>(.*?)</output>', task_content, re.DOTALL)
+                task_dict['output'] = output_match.group(1).strip() if output_match else ""
+                
+                # 获取描述信息
+                description_match = re.search(r'<description>(.*?)</description>', task_content, re.DOTALL)
+                task_dict['description'] = description_match.group(1).strip() if description_match else ""
+                
+                # 获取依赖任务列表
+                depend_tasks = []
+                depend_tasks_section = re.search(r'<depend_tasks>(.*?)</depend_tasks>', task_content, re.DOTALL)
+                if depend_tasks_section:
+                    depend_task_matches = re.finditer(r'<depend_task>(.*?)</depend_task>', 
+                                                   depend_tasks_section.group(1), re.DOTALL)
+                    for dt_match in depend_task_matches:
+                        if dt_match.group(1).strip():
+                            depend_tasks.append(dt_match.group(1).strip())
+                
+                task_dict['depend_tasks'] = depend_tasks
+                
+                # 获取依赖资源列表
+                depend_resources = []
+                resources_match = re.search(r'<depend_resources>(.*?)</depend_resources>', task_content, re.DOTALL)
+                if resources_match and resources_match.group(1).strip():
+                    resources_text = resources_match.group(1).strip()
+                    depend_resources = [res.strip() for res in resources_text.split(',') if res.strip()]
+                
+                task_dict['depend_resources'] = depend_resources
+                
+                # 将任务添加到结果字典
+                result["tasks"].append(task_dict)
+        
+        # 提取resources内容
+        resources_pattern = re.compile(r'<resources>(.*?)</resources>', re.DOTALL)
+        resources_match = resources_pattern.search(xml_string)
+        
+        if resources_match:
+            resources_content = resources_match.group(1).strip()
+            result["resources"] = resources_content
+        return result
+    
+    except Exception as e:
+        raise ValueError(f"处理XML数据时发生错误: {e}")
+
+
+def parse_planner_result(result):
+    """
+    解析规划结果,并为每个任务添加任务目录名
+    
+    参数:
+        result: 包含thoughts、main_task、tasks和resources的规划结果字符串
+        
+    返回:
+        解析后的完整规划信息字典
+    """
+    # 使用parse_xml_content解析完整内容
+    parsed_result = parse_xml_content(result)
+    task_name_to_index = {}
+    task_dict = {
+        'tasks': {},
+        'max_index': 1,
+    }
+    
+    # 为每个任务添加task_dir字段
+    for i, task_info in enumerate(parsed_result["tasks"]):
+        # 使用sanitize_filename生成目录名
+        task_name = task_info.get("name", "task")
+        depend_tasks_dir = []
+        task_info['task_dir'] = get_task_dir(task_name, task_dict)
+        for depend_task in task_info.get("depend_tasks", []):
+            depend_tasks_dir.append(get_task_dir(depend_task, task_dict))
+        task_info['depend_tasks_dir'] = depend_tasks_dir
+        task_info['status'] = 'todo' # 任务状态,todo: 未开始,doing: 进行中,success: 已完成,fail: 失败
+        task_name_to_index[task_name] = i
+    
+    # 为主任务也添加task_dir字段
+    if parsed_result["main_task"]:
+        main_task_name = parsed_result["main_task"].get("name", "main_task")
+        parsed_result["main_task"]["task_dir"] = sanitize_filename(main_task_name)
+    
+    return parsed_result, task_name_to_index
+def get_task_dir(task_name, task_dict, append_index=True):
+    max_index = task_dict.get('max_index', 1)
+    if task_name in task_dict['tasks']:
+        return task_dict['tasks'][task_name]
+    max_index_str = f"{max_index:02d}"
+    task_dir_raw = sanitize_filename(task_name)
+    if append_index:
+        task_dir = f"{max_index_str}_{task_dir_raw}"
+    else:
+        task_dir = task_dir_raw
+    task_dict['tasks'][task_name] = task_dir
+    task_dict['max_index'] = max_index + 1
+    return task_dir
+    
+def sanitize_filename(task_name: str, max_length: int = 20) -> str:
+    """
+    将任务名称转换为适合作为文件夹名称的字符串
+    
+    参数:
+        task_name: 需要转换的任务名称
+        max_length: 文件名最大长度限制,默认80个字符
+        
+    返回:
+        处理后适合作为文件名/文件夹名的字符串
+    """
+    # 替换Windows和Unix系统中不允许的文件名字符
+    # 替换 / \ : * ? " < > | 等字符为下划线
+    sanitized = re.sub(r'[\\/*?:"<>|]', '_', task_name)
+    
+    # 替换连续的空白字符为单个下划线
+    sanitized = re.sub(r'\s+', '_', sanitized)
+    
+    # 移除开头和结尾的点和空格
+    sanitized = sanitized.strip('. ')
+    
+    # 如果名称过长,截断它
+    if len(sanitized) > max_length:
+        # 保留前面的部分和后面的部分,中间用...连接
+        half_length = (max_length - 3) // 2
+        sanitized = sanitized[:half_length] + '...' + sanitized[-half_length:]
+    
+    # 确保名称不为空
+    if not sanitized:
+        sanitized = "unnamed_task"
+    
+    return sanitized
+
+def write_json(data, file_path: str) -> None:
+    """
+    将数据写入JSON文件
+    
+    参数:
+        data: 要写入的数据对象
+        file_path: 目标文件路径
+        
+    返回:
+        无
+    """
+    import json
+    with open(file_path, 'w', encoding='utf-8') as f:
+        json.dump(data, f, ensure_ascii=False, indent=2)
+def write_string_to_file(content: str, file_path: str) -> None:
+    """
+    将字符串内容写入文件
+    
+    参数:
+        content: 要写入的字符串内容
+        file_path: 目标文件路径
+        
+    返回:
+        无
+    """
+    with open(file_path, 'w', encoding='utf-8') as f:
+        f.write(content)
+
+def pretty_process(result):
+    def format_output(in_str):
+        return in_str.replace('\n\n', '\n').replace('\\"', '"')
+    process_list = []
+    i = 0
+    call_dict = {}
+    
+    # 首先收集所有工具调用输出
+    for row in result:
+        if isinstance(row, list):
+            # 处理列表:递归处理列表中的每个项目
+            for item in row:
+                if isinstance(item, dict) and item.get('type', '') == 'function_call_output':
+                    call_id = item['call_id']
+                    call_dict[call_id] = item['output']
+        elif isinstance(row, dict) and row.get('type', '') == 'function_call_output':
+            call_id = row['call_id']
+            call_dict[call_id] = row['output']
+    
+    # 然后处理每一行
+    for row in result:
+        if isinstance(row, list):
+            # 递归处理列表中的每个项目
+            for item in row:
+                if isinstance(item, dict):
+                    process_row(item, process_list, call_dict, i)
+                    i += 1
+        else:
+            # 直接处理字典项
+            process_row(row, process_list, call_dict, i)
+            i += 1
+    
+    process_str = '\n'.join(process_list)
+    return process_str
+
+def process_row(row, process_list, call_dict, i):
+    """处理单个行项目,添加到处理列表中"""
+    def format_output(in_str):
+        return in_str.replace('\n\n', '\n').replace('\\"', '"')
+    
+    if not isinstance(row, dict):
+        return
+        
+    action = ''
+    out = ''
+    call_id = ''
+    role_ = row.get('role', '')
+    type_ = row.get('type', '')
+    
+    if type_ == 'function_call':
+        action = f'工具调用-{row.get("name")}'
+        out = row['arguments']
+        call_id = row['call_id']
+    elif type_ == 'function_call_output':
+        return  # 跳过函数调用输出,它们已经被收集到call_dict中
+    elif role_ in ('user', 'assistant'):
+        action = role_
+        if isinstance(row['content'], str):
+            out = row['content']
+        else:
+            content_text = ""
+            for this_c in row['content']:
+                if isinstance(this_c, dict) and 'text' in this_c:
+                    content_text += this_c['text']
+            out = content_text
+    
+    process_list.append('\n\n' + f'{i+1}. ' + '## ' + action + ' ' * 4 + '-' * 32 + '\n')
+    process_list.append(format_output(str(out)))
+    
+    # 如果存在对应的工具输出,添加它
+    if call_id and call_id in call_dict:
+        process_list.append('\n\n' + f'{i+2}. ' + '## ' + '工具输出' + ' ' * 4 + '-' * 32 + '\n')
+        process_list.append(format_output(call_dict[call_id]))
+

+ 44 - 18
script/search_recommendations/xiaohongshu_search_recommendations.py

@@ -8,6 +8,8 @@ import requests
 import json
 import os
 import argparse
+import time
+import ast
 from datetime import datetime
 from typing import Dict, Any
 
@@ -37,13 +39,15 @@ class XiaohongshuSearchRecommendations:
             project_root = os.path.dirname(os.path.dirname(script_dir))
             self.results_base_dir = os.path.join(project_root, "data", "search_recommendations")
 
-    def get_recommendations(self, keyword: str, timeout: int = 30) -> Dict[str, Any]:
+    def get_recommendations(self, keyword: str, timeout: int = 300, max_retries: int = 10, retry_delay: int = 2) -> Dict[str, Any]:
         """
         获取小红书搜索推荐词
 
         Args:
             keyword: 搜索关键词,例如:'长沙'、'美妆'等
-            timeout: 请求超时时间(秒),默认30秒
+            timeout: 请求超时时间(秒),默认300秒
+            max_retries: 最大重试次数,默认10次
+            retry_delay: 重试间隔时间(秒),默认2秒
 
         Returns:
             API响应的JSON数据
@@ -51,22 +55,44 @@ class XiaohongshuSearchRecommendations:
         Raises:
             requests.exceptions.RequestException: 请求失败时抛出异常
         """
-        payload = {
-            "keyword": keyword
-        }
-
-        try:
-            response = requests.post(
-                self.api_url,
-                json=payload,
-                timeout=timeout,
-                headers={"Content-Type": "application/json"}
-            )
-            response.raise_for_status()
-            return response.json()
-        except requests.exceptions.RequestException as e:
-            print(f"请求失败: {e}")
-            raise
+        payload = {"keyword": keyword}
+        last_error = None
+
+        for attempt in range(max_retries + 1):
+            try:
+                response = requests.post(
+                    self.api_url,
+                    json=payload,
+                    timeout=timeout,
+                    headers={"Content-Type": "application/json"}
+                )
+                response.raise_for_status()
+                res = response.json()
+                # 使用 ast.literal_eval 解析 Python 字典字符串(不是标准 JSON)
+                result = ast.literal_eval(res['result'])
+
+                # 成功:code == 0
+                if result.get('code') == 0:
+                    return result['data']['data']
+
+                # 失败:code != 0
+                last_error = f"code={result.get('code')}"
+
+            except Exception as e:
+                from traceback import print_exc
+                print(res['result'])
+                print(f"发生异常: {e}")
+                print_exc()
+                last_error = str(e)
+
+            # 统一处理重试逻辑
+            if attempt < max_retries:
+                print(f"请求失败 ({last_error}), 第{attempt + 1}次重试,等待{retry_delay}秒...")
+                time.sleep(retry_delay)
+            else:
+                print(f"达到最大重试次数({max_retries}),最后错误: {last_error}")
+
+        return []
 
     def save_result(self, keyword: str, result: Dict[str, Any]) -> str:
         """

+ 193 - 0
sug_v1.py

@@ -0,0 +1,193 @@
+import asyncio
+
+from agents import Agent, Runner, function_tool
+from lib.my_trace import set_trace
+from lib.utils import read_file_as_string
+from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
+
+@function_tool
+def get_query_suggestions(query: str):
+    """Fetch search recommendations from Xiaohongshu."""
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+    query_suggestions = xiaohongshu_api.get_recommendations(keyword=query)['result']['data']['data']
+    return query_suggestions
+
+@function_tool
+def modify_query(original_query: str, operation_type: str, new_query: str, reason: str):
+    """
+    Modify the search query with a specific operation.
+
+    Args:
+        original_query: The original query before modification
+        operation_type: Type of modification - must be one of: "简化", "扩展", "替换", "组合"
+        new_query: The modified query after applying the operation
+        reason: Detailed explanation of why this modification was made and what insight from previous suggestions led to this change
+
+    Returns:
+        A dict containing the modification record and the new query to use for next search
+    """
+    operation_types = ["简化", "扩展", "替换", "组合"]
+    if operation_type not in operation_types:
+        return {
+            "status": "error",
+            "message": f"Invalid operation_type. Must be one of: {', '.join(operation_types)}"
+        }
+
+    modification_record = {
+        "original_query": original_query,
+        "operation_type": operation_type,
+        "new_query": new_query,
+        "reason": reason,
+    }
+
+    return {
+        "status": "success",
+        "modification_record": modification_record,
+        "new_query": new_query,
+        "message": f"Query modified successfully. Use '{new_query}' for the next search."
+    }
+
+insrtuctions = """
+你是一个专业的搜索query优化专家,擅长通过动态探索找到最符合用户搜索习惯的query。
+
+## 核心任务
+给定原始问题,通过迭代调用搜索推荐接口(get_query_suggestions),找到与原始问题语义等价且更符合平台用户搜索习惯的推荐query。
+
+## 工作流程
+
+### 1. 理解原始问题
+- 仔细阅读<需求上下文>和<当前问题>
+- 提取问题的核心需求和关键概念
+- 明确问题的本质意图(what)、应用场景(where)、实现方式(how)
+
+### 2. 动态探索策略
+采用类似人类搜索的迭代探索方式:
+
+**第一轮尝试:**
+- 使用原始问题直接调用 get_query_suggestions(query="原始问题")
+- 仔细分析返回的推荐词列表
+- 判断是否有与原始问题等价的推荐词
+
+**后续迭代:**
+如果推荐词不满足要求,必须先调用 modify_query 函数记录修改,然后再次搜索:
+
+**工具使用流程:**
+1. 调用 modify_query(original_query, operation_type, new_query, reason)
+2. 使用返回的 new_query 调用 get_query_suggestions
+3. 分析新的推荐词列表
+4. 如果仍不满足,重复步骤1-3
+
+**四种操作类型(operation_type):**
+- **简化**:删除冗余词汇,提取核心关键词
+  - 示例:modify_query("快速进行图片背景移除和替换", "简化", "图片背景移除", "原始query过于冗长,'快速进行'和'和替换'是修饰词,核心需求是'图片背景移除'")
+
+- **扩展**:添加场景、平台、工具类型等限定词
+  - 示例:modify_query("图片背景移除", "扩展", "在线图片背景移除工具", "从推荐词看用户更关注具体工具,添加'在线'和'工具'限定词可能更符合搜索习惯")
+
+- **替换**:使用同义词、行业术语或口语化表达
+  - 示例:modify_query("背景移除", "替换", "抠图", "推荐词中出现多个口语化表达,'抠图'是用户更常用的说法")
+
+- **组合**:调整关键词顺序或组合方式
+  - 示例:modify_query("图片背景移除", "组合", "抠图换背景", "调整表达方式,结合推荐词中高频出现的'换背景'概念")
+
+**每次修改的reason必须包含:**
+- 上一轮推荐词给你的启发(如"推荐词中多次出现'抠图'一词")
+- 为什么这样修改更符合平台用户习惯
+- 与原始问题的关系(确保核心意图不变)
+
+### 3. 等价性判断标准
+推荐词满足以下条件即可视为"与原始问题等价":
+
+**语义等价:**
+- 能够回答或解决原始问题的核心需求
+- 涵盖原始问题的关键功能或场景
+
+**搜索有效性:**
+- 是平台真实推荐的query(来自推荐接口)
+- 大概率能找到相关结果(基于平台用户行为)
+
+**可接受的差异:**
+- 表达方式不同但含义相同(如"背景移除" vs "抠图")
+- 范围略有调整但核心不变(如"图片背景移除" vs "图片抠图工具")
+
+### 4. 迭代终止条件
+- **成功终止**:找到至少一个与原始问题等价的推荐query
+- **尝试上限**:最多迭代5轮,避免无限循环
+- **无推荐词**:推荐接口返回空列表或错误
+
+### 5. 输出要求
+成功找到等价query时,输出格式:
+```
+原始问题:[原问题]
+
+优化后的query:[最终找到的等价推荐query]
+
+探索路径:
+1. 第1轮:原始query "[query1]"
+   - 推荐词:[列出关键推荐词]
+   - 判断:不满足,[简要说明原因]
+
+2. 第2轮:modify_query("[query1]", "简化", "[query2]", "[reason]")
+   - 推荐词:[列出关键推荐词]
+   - 判断:不满足,[简要说明原因]
+
+3. 第3轮:modify_query("[query2]", "替换", "[query3]", "[reason]")
+   - 推荐词:[列出关键推荐词]
+   - 判断:满足!找到等价query "[最终query]"
+
+推荐理由:
+- 该query来自平台官方推荐,大概率有结果
+- 与原始问题语义等价:[具体说明]
+- 更符合用户搜索习惯:[具体说明]
+```
+
+未找到等价query时,输出:
+```
+原始问题:[原问题]
+
+探索结果:未找到完全等价的推荐query(已尝试[N]轮)
+
+尝试过的query及修改记录:
+1. "[query1]" (原始)
+2. "[query2]" (简化:[reason])
+3. "[query3]" (替换:[reason])
+...
+
+各轮推荐词分析:
+- 第1轮推荐词偏向:[分析]
+- 第2轮推荐词偏向:[分析]
+...
+
+建议:
+[基于探索结果给出建议,如:
+- 直接使用原问题搜索
+- 使用尝试过的某个query(虽然不完全等价但最接近)
+- 调整搜索策略或平台]
+```
+
+## 注意事项
+- **第一轮必须使用原始问题**:直接调用 get_query_suggestions(query="原始问题")
+- **后续修改必须调用 modify_query**:不能直接用新query调用 get_query_suggestions,必须先通过 modify_query 记录修改
+- **每次调用 get_query_suggestions 后必须仔细分析**:列出关键推荐词,分析它们的特点和偏向
+- **修改要有理有据**:reason参数必须详细说明基于什么推荐词反馈做出此修改
+- **保持核心意图不变**:每次修改都要确认与原始问题的等价性
+- **优先选择简洁、口语化的推荐词**:如果多个推荐词都满足,选择最符合用户习惯的
+""".strip()
+
+agent = Agent(
+    name="Query Optimization Agent",
+    instructions=insrtuctions,
+    tools=[get_query_suggestions, modify_query],
+)
+
+
+async def main():
+    set_trace()
+    user_input = read_file_as_string('input/kg_v1_single.md')
+    result = await Runner.run(agent, input=user_input)
+    print(result.final_output)
+    # The weather in Tokyo is sunny.
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 316 - 0
sug_v1_1.py

@@ -0,0 +1,316 @@
+import asyncio
+
+from agents import Agent, Runner, function_tool
+from lib.my_trace import set_trace
+from lib.utils import read_file_as_string
+from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
+
+@function_tool
+def get_query_suggestions(query: str):
+    """Fetch search recommendations from Xiaohongshu."""
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+    query_suggestions = xiaohongshu_api.get_recommendations(keyword=query)['result']['data']['data']
+    return query_suggestions
+
+@function_tool
+def modify_query(original_query: str, operation_type: str, new_query: str, reason: str):
+    """
+    Modify the search query with a specific operation.
+
+    Args:
+        original_query: The original query before modification
+        operation_type: Type of modification - must be one of: "简化", "扩展", "替换", "组合"
+        new_query: The modified query after applying the operation
+        reason: Detailed explanation of why this modification was made and what insight from previous suggestions led to this change
+
+    Returns:
+        A dict containing the modification record and the new query to use for next search
+    """
+    operation_types = ["简化", "扩展", "替换", "组合"]
+    if operation_type not in operation_types:
+        return {
+            "status": "error",
+            "message": f"Invalid operation_type. Must be one of: {', '.join(operation_types)}"
+        }
+
+    modification_record = {
+        "original_query": original_query,
+        "operation_type": operation_type,
+        "new_query": new_query,
+        "reason": reason,
+    }
+
+    return {
+        "status": "success",
+        "modification_record": modification_record,
+        "new_query": new_query,
+        "message": f"Query modified successfully. Use '{new_query}' for the next search."
+    }
+
+insrtuctions = """
+你是一个专业的搜索query优化专家,擅长通过动态探索找到最符合用户搜索习惯的query。
+
+## 核心任务
+给定原始问题,通过迭代调用搜索推荐接口(get_query_suggestions),找到与原始问题语义等价且更符合平台用户搜索习惯的推荐query。
+
+## 工作流程
+
+### 1. 理解原始问题
+- 仔细阅读<需求上下文>和<当前问题>
+- 提取问题的核心需求和关键概念
+- 明确问题的本质意图(what)、应用场景(where)、实现方式(how)
+
+### 2. 动态探索策略
+采用类似人类搜索的迭代探索方式:
+
+**第一轮尝试(优先不拆分):**
+- 使用完整的原始问题直接调用 get_query_suggestions(query="原始问题")
+- 仔细分析返回的推荐词列表
+- 判断是否有与原始问题**完整等价**的推荐词
+- **关键判断**:推荐词是否能覆盖原始问题的所有核心需求
+  - 如果推荐词能覆盖所有需求 → 继续单query探索
+  - 如果推荐词始终只覆盖部分需求 → 考虑拆分策略(见下文)
+
+**后续迭代:**
+如果推荐词不满足要求,必须先调用 modify_query 函数记录修改,然后再次搜索:
+
+**工具使用流程:**
+1. 调用 modify_query(original_query, operation_type, new_query, reason)
+2. 使用返回的 new_query 调用 get_query_suggestions
+3. 分析新的推荐词列表
+4. 如果仍不满足,重复步骤1-3
+
+**四种操作类型(operation_type):**
+
+- **简化**:删除冗余词汇,提取核心关键词
+  - 适用场景:原始query过于冗长,包含修饰词、限定词或多余描述
+  - 策略:保留表达核心需求的名词和动词,删除修饰性的形容词、副词、疑问词、方法词等辅助成分
+  - 理由示例:"原始query包含多个辅助词汇,推荐词显示用户倾向于使用简短的核心表达"
+
+- **扩展**:添加场景、平台、工具类型等限定词
+  - 适用场景:推荐词显示用户关注特定的使用场景、平台环境或资源类型
+  - 策略:根据推荐词的高频特征,添加相应的场景限定词、平台限定词、资源类型词等
+  - 理由示例:"推荐词中高频出现某类限定词,添加该限定词更符合用户搜索意图"
+
+- **替换**:使用同义词、行业术语或口语化表达
+  - 适用场景:推荐词中出现原始query的同义词或更常用的表达方式
+  - 策略:观察推荐词中的高频词汇,用口语化或行业通用表达替换书面语
+  - 理由示例:"推荐词中多次出现某个同义词,说明该词是平台用户的常用表达"
+
+- **组合**:调整关键词顺序或组合方式
+  - 适用场景:推荐词显示用户使用不同的关键词组合方式
+  - 策略:调整词序、拆分或合并关键词
+  - 理由示例:"推荐词中出现不同的词序组合,调整后可能更符合搜索习惯"
+
+**每次修改的reason必须包含:**
+- 上一轮推荐词的具体特征(如"推荐词中多次出现某个特定词汇"、"推荐词偏向某个方向")
+- 为什么这样修改更符合平台用户习惯(基于推荐词反馈)
+- 与原始问题的关系(确保核心意图不变)
+
+**何时考虑拆分策略:**
+只有当满足以下条件时,才考虑将原始问题拆分成多个query:
+1. 已经尝试了多轮探索(至少3-4轮)
+2. 推荐词的反馈始终显示:只覆盖原始问题的部分需求,无法找到完整覆盖的等价query
+3. 原始问题确实包含多个相对独立的功能需求(如"A和B"、"移除和替换")
+
+**拆分后的探索方式:**
+1. 识别原始问题包含的各个子需求
+2. 为每个子需求独立进行完整的探索流程(从第一轮开始)
+3. 最终输出多个query,每个覆盖一个子需求
+4. 验证所有子需求的组合能否完整覆盖原始问题
+
+### 3. 等价性判断标准
+
+**前提条件:**
+所有推荐词都来自 get_query_suggestions 返回,已经是平台基于用户行为的真实推荐,具有搜索有效性。
+
+**等价性判断(核心标准):**
+在推荐词列表中,判断某个推荐词是否与原始问题等价,标准如下:
+
+**必须满足 - 语义等价:**
+- 能够回答或解决原始问题的核心需求
+- 涵盖原始问题的关键功能、目标或场景
+- 核心概念一致,虽然表达方式可能不同
+
+**可接受的差异:**
+- 表达方式:书面语 vs 口语化表达
+- 词汇选择:专业术语 vs 通俗说法、同义词替换
+- 范围调整:核心功能一致,但加了具体场景、工具类型等限定词
+- 详略程度:简化版或扩展版,但核心不变
+
+**不可接受的差异:**
+- 核心需求改变(如原问题要"移除",推荐词是"添加")
+- 功能偏离(如原问题要"工具",推荐词是"教程")
+- 领域错位(如原问题是"图片处理",推荐词是"视频处理")
+
+### 4. 迭代终止条件
+- **成功终止**:找到至少一个与原始问题等价的推荐query
+- **尝试上限**:最多迭代5轮,避免无限循环
+- **无推荐词**:推荐接口返回空列表或错误
+
+### 5. 输出要求
+
+**成功找到等价query时:**
+
+**情况1 - 单一query即可覆盖:**
+```
+✓ 搜索成功
+
+原始问题:[完整的原始问题]
+问题分析:该问题可以用单一query充分表达
+
+优化后的query:[找到的等价推荐query]
+找到轮次:第[N]轮
+
+探索过程:
+第1轮 - 原始query
+  Query: [原始问题]
+  推荐词: [列出5-10个代表性推荐词]
+  分析: [推荐词的特点和偏向]
+  判断: 未找到等价query
+
+第2轮 - [操作类型]
+  修改: modify_query(..., operation_type="[类型]", ...)
+  Reason: [详细的修改理由]
+  Query: [新query]
+  推荐词: [列出代表性推荐词]
+  分析: [推荐词的特点变化]
+  判断: 未找到等价query
+
+第N轮 - [操作类型]
+  修改: modify_query(..., operation_type="[类型]", ...)
+  Reason: [详细的修改理由]
+  Query: [最终query]
+  推荐词: [包含等价query的推荐词列表]
+  分析: [为什么其中某个推荐词等价]
+  判断: ✓ 找到等价query "[等价的推荐词]"
+
+等价性说明:
+- 语义等价:[说明为什么与原始问题等价]
+- 用户习惯:[说明为什么更符合搜索习惯]
+- 搜索有效性:来自平台推荐,基于真实用户行为
+```
+
+**情况2 - 需要多个query覆盖(少数情况):**
+仅当尝试多轮探索后,推荐词反馈明确显示无法用单query覆盖所有需求时使用
+```
+✓ 搜索成功(多query)
+
+原始问题:[完整的原始问题]
+问题分析:经过[N]轮探索,推荐词始终只覆盖部分需求,因此拆分成多个query
+
+需求拆分:
+子需求1:[提取的第一个子需求]
+子需求2:[提取的第二个子需求]
+子需求3:[提取的第三个子需求](如有)
+...
+
+优化后的query列表:
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Query 1: [等价query1]
+  覆盖需求:子需求1
+  找到轮次:第[N]轮
+  等价性说明:[为什么与子需求1等价]
+
+Query 2: [等价query2]
+  覆盖需求:子需求2
+  找到轮次:第[M]轮
+  等价性说明:[为什么与子需求2等价]
+
+Query 3: [等价query3]
+  覆盖需求:子需求3
+  找到轮次:第[K]轮
+  等价性说明:[为什么与子需求3等价]
+...
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+拆分决策依据:
+- 为什么选择拆分:[说明尝试单query时推荐词的反馈情况]
+- 哪些轮次的推荐词显示只覆盖部分需求:[具体分析]
+
+完整性验证:
+- 原始问题包含的所有核心需求是否都被覆盖:✓
+- 各query之间是否有重叠或遗漏:[分析]
+
+每个子需求的探索过程:
+[展示每个子需求的完整探索路径]
+```
+
+**未找到等价query时:**
+```
+✗ 搜索未找到等价query
+
+原始问题:[完整的原始问题]
+探索轮次:已尝试[N]轮(达到上限或其他终止原因)
+
+探索记录:
+第1轮: [query1] → 推荐词偏向[方向]
+第2轮: [query2] ([操作类型]) → 推荐词偏向[方向]
+第3轮: [query3] ([操作类型]) → 推荐词偏向[方向]
+...
+
+整体分析:
+- 推荐词的共同特点:[综合分析]
+- 与原始问题的gap:[说明差异]
+- 可能的原因:[推测为什么找不到]
+
+建议:
+1. [最可行的替代方案]
+2. [其他建议]
+```
+
+## 注意事项
+
+**工作原则:**
+- **数据驱动**:所有决策必须基于推荐词的实际反馈,而非主观猜测
+- **优先整体**:始终优先尝试用单一query覆盖完整的原始问题
+- **灵活调整**:不同问题需要不同策略,观察推荐词特点后灵活选择操作类型
+- **保持等价性**:探索过程中始终确保与原始问题的核心意图一致
+- **按需拆分**:只有当推荐词反馈明确显示单query无法覆盖时,才基于实际情况拆分成多个query
+
+**操作规范:**
+- **第一轮必须使用原始问题**:直接调用 get_query_suggestions(query="原始问题")
+- **后续修改必须调用 modify_query**:不能直接用新query调用 get_query_suggestions,必须先记录修改
+- **仔细分析推荐词**:每次获取推荐词后,列出代表性词汇,分析特点、偏向、高频词
+- **详细说明理由**:reason参数必须说明基于推荐词的哪些具体观察做出修改
+- **选择最优结果**:如果多个推荐词都等价,优先选择最简洁、最口语化的
+
+**策略提示:**
+- **优先单query探索**:
+  - 始终先尝试用单一query覆盖完整的原始问题
+  - 即使原始问题包含"和"、"及"等连接词,也优先尝试作为整体探索
+  - 只有在推荐词反馈明确显示无法用单query覆盖时,才考虑拆分
+- **探索策略灵活调整**:
+  - 不同领域的问题需要不同的探索路径
+  - 专业术语类问题可能需要"替换"为口语化表达
+  - 描述性问题可能需要"简化"提取核心词
+  - 抽象概念可能需要"扩展"添加具体场景
+  - 观察推荐词的变化趋势,及时调整策略方向
+- **何时考虑拆分**:
+  - 已尝试3-4轮仍无法找到完整覆盖的等价query
+  - 推荐词始终只偏向原始问题的某一部分需求
+  - 此时才启动拆分策略,为各子需求分别探索
+""".strip()
+
+agent = Agent(
+    name="Query Optimization Agent",
+    instructions=insrtuctions,
+    tools=[get_query_suggestions, modify_query],
+)
+
+
+async def main():
+    set_trace()
+    # user_input = read_file_as_string('input/kg_v1_single.md')
+    user_input = """
+<当前问题>
+如何构建帖子标题和正文,能通过拟人的手法以猫咪的情绪引发读者产生共鸣?
+</当前问题>
+""".strip()
+    result = await Runner.run(agent, input=user_input)
+    print(result.final_output)
+    # The weather in Tokyo is sunny.
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 277 - 0
sug_v2.py

@@ -0,0 +1,277 @@
+import asyncio
+import json
+import os
+from datetime import datetime
+
+from agents import Agent, Runner, function_tool
+from lib.my_trace import set_trace
+from typing import Literal
+from dataclasses import dataclass
+from pydantic import BaseModel, Field
+
+
+from lib.utils import read_file_as_string
+from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
+
+from agents import Agent, RunContextWrapper, Runner, function_tool
+
+from pydantic import BaseModel, Field
+
+
+class RunContext(BaseModel):
+    q_with_context: str
+    q_context: str
+    q: str
+    log_url: str
+    log_dir: str
+    # 中间数据记录 - 按时间顺序记录所有操作
+    operations_history: list[dict] = Field(default_factory=list, description="记录所有操作的历史,包括 get_query_suggestions 和 modify_query")
+
+
+eval_insrtuctions = """
+你是一个专业的评估专家,负责评估给定的搜索query是否满足原始问题和需求,给出出评分和简明扼要的理由。
+"""
+
+@dataclass
+class EvaluationFeedback:
+    reason: str=Field(..., description="简明扼要的理由")
+    score: float=Field(..., description="评估结果,1表示等价,0表示不等价,中间值表示部分等价")
+
+evaluator = Agent[None](
+    name="评估专家",
+    instructions=eval_insrtuctions,
+    output_type=EvaluationFeedback,
+)
+
+@function_tool
+async def get_query_suggestions(wrapper: RunContextWrapper[RunContext], query: str):
+    """Fetch search recommendations from Xiaohongshu."""
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+    query_suggestions = xiaohongshu_api.get_recommendations(keyword=query)
+    print(query_suggestions)
+    async def evaluate_single_query(q_sug: str, q_with_context: str):
+        """Evaluate a single query suggestion."""
+        eval_input = f"""
+{q_with_context}
+<待评估的推荐query>
+{q_sug}
+</待评估的推荐query>
+        """
+        evaluator_result = await Runner.run(evaluator, eval_input)
+        result: EvaluationFeedback = evaluator_result.final_output
+        return {
+            "query": q_sug,
+            "score": result.score,
+            "reason": result.reason,
+        }
+
+    # 并发执行所有评估任务
+    q_with_context = wrapper.context.q_with_context
+    res = []
+    if query_suggestions:
+        res = await asyncio.gather(*[evaluate_single_query(q_sug, q_with_context) for q_sug in query_suggestions])
+    else:
+        res = '未返回任何推荐词'
+
+    # 记录到 RunContext
+    wrapper.context.operations_history.append({
+        "operation_type": "get_query_suggestions",
+        "timestamp": datetime.now().isoformat(),
+        "query": query,
+        "suggestions": query_suggestions,
+        "evaluations": res,
+    })
+
+    return res
+
+@function_tool
+def modify_query(wrapper: RunContextWrapper[RunContext], original_query: str, operation_type: str, new_query: str, reason: str):
+    """
+    Modify the search query with a specific operation.
+
+    Args:
+        original_query: The original query before modification
+        operation_type: Type of modification - must be one of: "简化", "扩展", "替换", "组合"
+        new_query: The modified query after applying the operation
+        reason: Detailed explanation of why this modification was made and what insight from previous suggestions led to this change
+
+    Returns:
+        A dict containing the modification record and the new query to use for next search
+    """
+    operation_types = ["简化", "扩展", "替换", "组合"]
+    if operation_type not in operation_types:
+        return {
+            "status": "error",
+            "message": f"Invalid operation_type. Must be one of: {', '.join(operation_types)}"
+        }
+
+    modification_record = {
+        "original_query": original_query,
+        "operation_type": operation_type,
+        "new_query": new_query,
+        "reason": reason,
+    }
+
+    # 记录到 RunContext
+    wrapper.context.operations_history.append({
+        "operation_type": "modify_query",
+        "timestamp": datetime.now().isoformat(),
+        "modification_type": operation_type,
+        "original_query": original_query,
+        "new_query": new_query,
+        "reason": reason,
+    })
+
+    return {
+        "status": "success",
+        "modification_record": modification_record,
+        "new_query": new_query,
+        "message": f"Query modified successfully. Use '{new_query}' for the next search."
+    }
+
+insrtuctions = """
+你是一个专业的搜索query优化专家,擅长通过动态探索找到最符合用户搜索习惯的query。
+
+## 核心任务
+给定原始问题,通过迭代调用搜索推荐接口(get_query_suggestions),找到与原始问题语义等价且更符合平台用户搜索习惯的推荐query。
+
+## 重要说明
+- **你不需要自己评估query的等价性**
+- get_query_suggestions 函数内部已集成评估子agent,会自动对每个推荐词进行评估
+- 返回结果包含:query(推荐词)、score(评分,1表示等价,0表示不等价)、reason(评估理由)
+- **你的职责是分析评估结果,做出决策和策略调整**
+
+## 防止幻觉 - 关键原则
+- **严禁编造数据**:只能基于 get_query_suggestions 实际返回的结果进行分析
+- **空结果处理**:如果返回的列表为空([]),必须明确说明"未返回任何推荐词"
+- **不要猜测**:在 modify_query 的 reason 中,不能引用不存在的推荐词或评分
+- **如实记录**:每次分析都要如实反映实际返回的数据
+
+## 工作流程
+
+### 1. 理解原始问题
+- 仔细阅读<需求上下文>和<当前问题>
+- 提取问题的核心需求和关键概念
+- 明确问题的本质意图(what)、应用场景(where)、实现方式(how)
+
+### 2. 动态探索策略
+
+**第一轮尝试:**
+- 使用原始问题直接调用 get_query_suggestions(query="原始问题")
+- **检查返回结果**:
+  - 如果返回空列表 []:说明"该query未返回任何推荐词",需要简化或替换query
+  - 如果有推荐词:查看每个推荐词的 score 和 reason
+- **做出判断**:是否有 score >= 0.8 的高分推荐词?
+
+**后续迭代:**
+如果没有高分推荐词(或返回空列表),必须先调用 modify_query 记录修改,然后再次搜索:
+
+**工具使用流程:**
+1. **分析评估反馈**(必须基于实际返回的数据):
+   - **情况A - 返回空列表**:
+     * 在 reason 中说明:"第X轮未返回任何推荐词,可能是query过于复杂或生僻"
+     * 不能编造任何推荐词或评分
+   - **情况B - 有推荐词但无高分**:
+     * 哪些推荐词得分较高?具体是多少分?评估理由是什么?
+     * 哪些推荐词偏离了原问题?如何偏离的?
+     * 推荐词整体趋势是什么?(过于泛化/具体化/领域偏移等)
+
+2. **决策修改策略**:基于实际评估反馈,调用 modify_query(original_query, operation_type, new_query, reason)
+   - reason 必须引用具体的数据,不能编造
+
+3. 使用返回的 new_query 调用 get_query_suggestions
+
+4. 分析新的评估结果,如果仍不满足,重复步骤1-3
+
+**四种操作类型(operation_type):**
+- **简化**:删除冗余词汇,提取核心关键词(当推荐词过于发散时)
+- **扩展**:添加限定词或场景描述(当推荐词过于泛化时)
+- **替换**:使用同义词、行业术语或口语化表达(当推荐词偏离核心时)
+- **组合**:调整关键词顺序或组合方式(当推荐词结构不合理时)
+
+**每次修改的reason必须包含:**
+- 上一轮评估结果的关键发现(引用具体的score和reason)
+- 基于评估反馈,为什么这样修改
+- 预期这次修改会带来什么改进
+
+### 3. 决策标准
+- **score >= 0.8**:认为该推荐词与原问题等价,可以作为最终结果
+- **0.5 <= score < 0.8**:部分等价,分析reason看是否可接受
+- **score < 0.5**:不等价,需要继续优化
+
+### 4. 迭代终止条件
+- **成功终止**:找到至少一个 score >= 0.8 的推荐query
+- **尝试上限**:最多迭代5轮,避免无限循环
+- **无推荐词**:推荐接口返回空列表或错误
+
+### 5. 输出要求
+
+**成功找到等价query时,输出格式:**
+```
+原始问题:[原问题]
+优化后的query:[最终找到的等价推荐query]
+评分:[score]
+```
+
+**未找到等价query时,输出格式:**
+```
+原始问题:[原问题]
+结果:未找到完全等价的推荐query
+建议:[简要建议,如:直接使用原问题搜索 或 使用最接近的推荐词]
+```
+
+## 注意事项
+- **第一轮必须使用原始问题**:直接调用 get_query_suggestions(query="原始问题")
+- **后续修改必须调用 modify_query**:不能直接用新query调用 get_query_suggestions
+- **重点关注评估结果**:每次都要仔细分析返回的 score 和 reason
+- **基于数据决策**:修改策略必须基于评估反馈,不能凭空猜测
+- **引用具体评分**:在分析和决策时,引用具体的score数值和reason内容
+- **优先选择高分推荐词**:score >= 0.8 即可认为等价
+- **严禁编造数据**:
+  * 如果返回空列表,必须在 reason 中明确说明"未返回任何推荐词"
+  * 不能引用不存在的推荐词、评分或评估理由
+  * 每次 modify_query 的 reason 必须基于上一轮实际返回的结果
+""".strip()
+
+
+
+
+async def main():
+    current_time, log_url = set_trace()
+    q_context = read_file_as_string('input/kg_v1_single_context.md')
+    q = read_file_as_string('input/kg_v1_single_q.md')
+    q_with_context = f"""
+<需求上下文>
+{q_context}
+</需求上下文>
+<当前问题>
+{q}
+</当前问题>
+""".strip()
+    log_dir = os.path.join("logs", current_time)
+    run_context = RunContext(
+        q_with_context=q_with_context,
+        q_context=q_context,
+        q=q,
+        log_dir=log_dir,
+        log_url=log_url,
+    )
+    agent = Agent[RunContext](
+        name="Query Optimization Agent",
+        instructions=insrtuctions,
+        tools=[get_query_suggestions, modify_query],
+       
+    )
+    result = await Runner.run(agent, input=q_with_context,  context = run_context,)
+    print(result.final_output)
+
+    # 保存 RunContext 到 log_dir
+    os.makedirs(run_context.log_dir, exist_ok=True)
+    context_file_path = os.path.join(run_context.log_dir, "run_context.json")
+    with open(context_file_path, "w", encoding="utf-8") as f:
+        json.dump(run_context.model_dump(), f, ensure_ascii=False, indent=2)
+    print(f"\nRunContext saved to: {context_file_path}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 341 - 0
test.py

@@ -0,0 +1,341 @@
+import asyncio
+from typing import List
+
+from agents import Agent, Runner, function_tool
+from lib.my_trace import set_trace
+from lib.utils import read_file_as_string
+from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
+
+@function_tool
+def get_query_suggestions(query: str):
+    """Fetch search recommendations from Xiaohongshu."""
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+    query_suggestions = xiaohongshu_api.get_recommendations(keyword=query)['result']['data']['data']
+    return query_suggestions
+
+@function_tool
+def evaluate_suggestions(original_problem: str, current_query: str, round_number: int, found_equivalent: bool, equivalent_query: str, evaluation_reason: str):
+    """
+    Record the evaluation result after analyzing suggestions from get_query_suggestions.
+
+    Args:
+        original_problem: The original problem from user input
+        current_query: The current query used to get these suggestions
+        round_number: Current round number (starting from 1)
+        found_equivalent: Whether an equivalent query was found in the suggestions (True/False)
+        equivalent_query: The equivalent query found (if found_equivalent=True), otherwise empty string ""
+        evaluation_reason: Detailed explanation of the evaluation result, including:
+            - If found: why the equivalent_query is semantically equivalent to original_problem
+            - If not found: what patterns were observed in suggestions and why none match
+
+    Returns:
+        A dict containing evaluation results
+    """
+    return {
+        "status": "evaluated",
+        "round": round_number,
+        "current_query": current_query,
+        "found_equivalent": found_equivalent,
+        "equivalent_query": equivalent_query if found_equivalent else None,
+        "evaluation_reason": evaluation_reason,
+        "message": f"Round {round_number} evaluation recorded. Found equivalent: {found_equivalent}." +
+                   (f" Proceed to complete_search with '{equivalent_query}'." if found_equivalent else " Continue to next round or modify query.")
+    }
+
+@function_tool
+def modify_query(original_query: str, operation_type: str, new_query: str, reason: str):
+    """
+    Modify the search query with a specific operation.
+
+    Args:
+        original_query: The original query before modification
+        operation_type: Type of modification - must be one of: "简化", "扩展", "替换", "组合"
+        new_query: The modified query after applying the operation
+        reason: Detailed explanation of why this modification was made and what insight from previous suggestions led to this change
+
+    Returns:
+        A dict containing the modification record and the new query to use for next search
+    """
+    operation_types = ["简化", "扩展", "替换", "组合"]
+    if operation_type not in operation_types:
+        return {
+            "status": "error",
+            "message": f"Invalid operation_type. Must be one of: {', '.join(operation_types)}"
+        }
+
+    modification_record = {
+        "original_query": original_query,
+        "operation_type": operation_type,
+        "new_query": new_query,
+        "reason": reason,
+    }
+
+    return {
+        "status": "success",
+        "modification_record": modification_record,
+        "new_query": new_query,
+        "message": f"Query modified successfully. Use '{new_query}' for the next search."
+    }
+
+@function_tool
+def complete_search(original_problem: str, found_query: str, source_round: int, equivalence_reason: str, total_rounds: int):
+    """
+    Mark the search as complete when an equivalent query is found.
+
+    Args:
+        original_problem: The original problem from user input
+        found_query: The equivalent query found in recommendations
+        source_round: Which round this query was found in
+        equivalence_reason: Detailed explanation of why this query is equivalent to the original problem
+        total_rounds: Total number of rounds taken
+
+    Returns:
+        A dict containing the final result
+    """
+    return {
+        "status": "completed",
+        "original_problem": original_problem,
+        "optimized_query": found_query,
+        "found_in_round": source_round,
+        "total_rounds": total_rounds,
+        "equivalence_reason": equivalence_reason,
+        "message": f"Search completed successfully! Found equivalent query '{found_query}' in round {source_round}."
+    }
+
+insrtuctions = """
+你是一个专业的搜索query优化专家,擅长通过动态探索找到最符合用户搜索习惯的query。
+
+## 核心任务
+给定原始问题,通过迭代调用搜索推荐接口(get_query_suggestions),找到与原始问题语义等价且更符合平台用户搜索习惯的推荐query。
+
+## 工作流程
+
+### 1. 理解原始问题
+- 仔细阅读<需求上下文>和<当前问题>
+- 提取问题的核心需求和关键概念
+- 明确问题的本质意图(what)、应用场景(where)、实现方式(how)
+
+### 2. 动态探索策略
+采用类似人类搜索的迭代探索方式,**每一步都必须通过函数调用记录**:
+
+**完整工具调用流程:**
+
+```
+每一轮的标准流程:
+1. get_query_suggestions(query) → 获取推荐词列表
+2. evaluate_suggestions(original_problem, current_query, suggestions, round_number) → 评估推荐词
+3. 判断分支:
+   a) 如果找到等价query → 调用 complete_search() 标记完成
+   b) 如果未找到 → 调用 modify_query() 修改query,进入下一轮
+```
+
+**第一轮(round=1):**
+```
+Step 1: get_query_suggestions(query="原始问题")
+Step 2: evaluate_suggestions(
+    original_problem="原始问题",
+    current_query="原始问题",
+    suggestions=[返回的推荐词列表],
+    round_number=1
+)
+Step 3: 判断评估结果
+  - 如果有等价query → 调用 complete_search()
+  - 如果没有 → 进入第二轮
+```
+
+**第二轮及后续(round=2,3,4,5):**
+```
+Step 1: modify_query(
+    original_query="上一轮的query",
+    operation_type="简化/扩展/替换/组合",
+    new_query="新query",
+    reason="基于上一轮推荐词的详细分析..."
+)
+Step 2: get_query_suggestions(query="新query")
+Step 3: evaluate_suggestions(
+    original_problem="原始问题",
+    current_query="新query",
+    suggestions=[返回的推荐词列表],
+    round_number=当前轮次
+)
+Step 4: 判断评估结果
+  - 如果有等价query → 调用 complete_search()
+  - 如果没有且未达5轮 → 继续下一轮
+  - 如果已达5轮 → 输出未找到的结论
+```
+
+**四种操作类型(operation_type):**
+- **简化**:删除冗余词汇,提取核心关键词
+  - 示例:modify_query("快速进行图片背景移除和替换", "简化", "图片背景移除", "原始query过于冗长,'快速进行'和'和替换'是修饰词,核心需求是'图片背景移除'")
+
+- **扩展**:添加场景、平台、工具类型等限定词
+  - 示例:modify_query("图片背景移除", "扩展", "在线图片背景移除工具", "从推荐词看用户更关注具体工具,添加'在线'和'工具'限定词可能更符合搜索习惯")
+
+- **替换**:使用同义词、行业术语或口语化表达
+  - 示例:modify_query("背景移除", "替换", "抠图", "推荐词中出现多个口语化表达,'抠图'是用户更常用的说法")
+
+- **组合**:调整关键词顺序或组合方式
+  - 示例:modify_query("图片背景移除", "组合", "抠图换背景", "调整表达方式,结合推荐词中高频出现的'换背景'概念")
+
+**每次修改的reason必须包含:**
+- 上一轮推荐词给你的启发(如"推荐词中多次出现'抠图'一词")
+- 为什么这样修改更符合平台用户习惯
+- 与原始问题的关系(确保核心意图不变)
+
+### 3. 等价性判断标准
+在 evaluate_suggestions 调用后,需要逐个分析推荐词,判断是否与原始问题等价:
+
+**语义等价:**
+- 能够回答或解决原始问题的核心需求
+- 涵盖原始问题的关键功能或场景
+- 核心概念一致(虽然表达方式可能不同)
+
+**搜索有效性:**
+- 必须是平台真实推荐的query(来自 get_query_suggestions 返回)
+- 大概率能找到相关结果(基于平台用户行为数据)
+
+**可接受的差异:**
+- 表达方式不同但含义相同(如"背景移除" vs "抠图")
+- 范围略有调整但核心不变(如"图片背景移除" vs "图片抠图工具")
+- 使用同义词或口语化表达(如"快速" vs "一键")
+
+**判断后的行动:**
+- 如果找到等价query → 立即调用 complete_search() 记录结果并结束
+- 如果未找到等价query → 分析推荐词特点,准备修改query进入下一轮
+
+### 4. 迭代终止条件
+
+**成功终止 - 调用 complete_search():**
+当在任何一轮的推荐词中找到等价query时,必须调用:
+```python
+complete_search(
+    original_problem="原始问题",
+    found_query="找到的等价query",
+    source_round=当前轮次,
+    equivalence_reason="详细说明为什么这个query与原始问题等价",
+    total_rounds=总轮次
+)
+```
+
+**失败终止 - 达到上限:**
+- 最多迭代5轮
+- 如果第5轮仍未找到,不调用 complete_search,直接输出未找到的结论
+
+**异常终止:**
+- 推荐接口返回空列表或错误
+- 函数调用失败
+
+### 5. 输出要求
+
+**当成功找到(已调用 complete_search)时:**
+```
+✓ 搜索成功完成!
+
+原始问题:[原问题]
+优化后的query:[最终找到的等价推荐query]
+找到轮次:第[N]轮
+总探索轮次:[N]轮
+
+探索路径详情:
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+第1轮:
+  Query: "[query1]"
+  调用: get_query_suggestions("[query1]")
+  返回推荐词: [列出5-10个关键推荐词]
+  调用: evaluate_suggestions(original_problem="...", current_query="...", suggestions=[...], round_number=1)
+  判断: ✗ 未找到等价query
+  原因: [简要说明]
+
+第2轮:
+  调用: modify_query("[query1]", "简化", "[query2]", "[详细reason]")
+  Query: "[query2]"
+  调用: get_query_suggestions("[query2]")
+  返回推荐词: [列出5-10个关键推荐词]
+  调用: evaluate_suggestions(original_problem="...", current_query="...", suggestions=[...], round_number=2)
+  判断: ✗ 未找到等价query
+  原因: [简要说明]
+
+第3轮:
+  调用: modify_query("[query2]", "替换", "[query3]", "[详细reason]")
+  Query: "[query3]"
+  调用: get_query_suggestions("[query3]")
+  返回推荐词: [列出5-10个关键推荐词,其中包含等价query]
+  调用: evaluate_suggestions(original_problem="...", current_query="...", suggestions=[...], round_number=3)
+  判断: ✓ 找到等价query!
+
+  调用: complete_search(
+    original_problem="...",
+    found_query="[最终query]",
+    source_round=3,
+    equivalence_reason="[详细等价性说明]",
+    total_rounds=3
+  )
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+推荐理由:
+• 该query来自平台官方推荐,基于真实用户搜索行为
+• 语义等价分析:[具体说明为什么与原始问题等价]
+• 用户习惯匹配:[说明为什么更符合搜索习惯]
+```
+
+**未找到等价query时(未调用 complete_search):**
+```
+✗ 搜索未找到完全等价的query
+
+原始问题:[原问题]
+探索轮次:已尝试5轮(达到上限)
+
+探索路径详情:
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+第1轮:Query "[query1]" → 推荐词特点:[分析]
+第2轮:Query "[query2]" (简化) → 推荐词特点:[分析]
+第3轮:Query "[query3]" (替换) → 推荐词特点:[分析]
+第4轮:Query "[query4]" (扩展) → 推荐词特点:[分析]
+第5轮:Query "[query5]" (组合) → 推荐词特点:[分析]
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+探索洞察:
+• 推荐词整体偏向:[综合分析所有推荐词的共同特点]
+• 与原始问题的gap:[说明为什么一直找不到等价query]
+
+后续建议:
+1. [最可行的方案,如使用某个接近的query]
+2. [备选方案]
+3. [其他建议]
+```
+
+## 注意事项
+
+**工具调用顺序(严格遵守):**
+1. 每轮必须先调用 get_query_suggestions 获取推荐词
+2. 然后必须调用 evaluate_suggestions 评估推荐词
+3. 如果找到等价query,立即调用 complete_search 结束
+4. 如果未找到,调用 modify_query 修改query,进入下一轮
+
+**具体要求:**
+- **第一轮使用原始问题**:get_query_suggestions(query="原始问题"),不做任何修改
+- **evaluate_suggestions必须调用**:每次获取推荐词后,都必须调用此函数记录评估过程
+- **找到即complete**:一旦判断某个推荐词等价,必须立即调用 complete_search(),不要继续探索
+- **modify_query的reason必须详细**:必须说明基于上一轮哪些推荐词反馈做出此修改
+- **保持original_problem不变**:在所有evaluate_suggestions和complete_search调用中,original_problem参数必须始终是最初的原始问题
+- **round_number从1开始连续递增**:第一轮是1,第二轮是2,以此类推
+- **优先简洁口语化**:如果多个推荐词都等价,选择最简洁、最口语化的
+""".strip()
+
+agent = Agent(
+    name="Query Optimization Agent",
+    instructions=insrtuctions,
+    tools=[get_query_suggestions, evaluate_suggestions, modify_query, complete_search],
+)
+
+
+async def main():
+    set_trace()
+    user_input = read_file_as_string('input/kg_v1_single.md')
+    result = await Runner.run(agent, input=user_input)
+    print(result.final_output)
+    # The weather in Tokyo is sunny.
+
+
+if __name__ == "__main__":
+    asyncio.run(main())