Kaynağa Gözat

删除无用的agent

jihuaqiang 2 gün önce
ebeveyn
işleme
df39ee7061
41 değiştirilmiş dosya ile 1462 ekleme ve 11599 silme
  1. 0 242
      CLAUDE.md
  2. 1003 0
      decode_workflow_process.md
  3. BIN
      examples/.DS_Store
  4. 58 0
      examples/run_decode_script.py
  5. BIN
      src/components/agents/__pycache__/script_form_extraction_agent.cpython-313.pyc
  6. 0 633
      src/components/agents/comment_analysis_agent.py
  7. 0 344
      src/components/agents/content_filter_agent.py
  8. 0 320
      src/components/agents/content_weight_agent.py
  9. 0 802
      src/components/agents/knowledge_requirement_agent.py
  10. 0 215
      src/components/agents/post_summary_deconstruction_agent.py
  11. 0 206
      src/components/agents/post_understanding_agent.py
  12. 0 390
      src/components/agents/readme/inspiration_points_agent.md
  13. 0 262
      src/components/agents/readme/key_points_agent.md
  14. 0 103
      src/components/agents/readme/purpose_point_agent.md
  15. 0 13
      src/components/agents/readme/result_aggregation_function.md
  16. 0 691
      src/components/agents/readme/script_element_extraction_agent.md
  17. 0 127
      src/components/agents/readme/script_section_division_agent.md
  18. 0 130
      src/components/agents/readme/topic_selection_understanding_agent.md
  19. 0 1543
      src/components/agents/script_element_extraction_agent.py
  20. 23 2
      src/components/agents/script_form_extraction_agent.py
  21. 0 328
      src/components/agents/script_orthogonal_analysis_agent.py
  22. 0 1076
      src/components/agents/script_understanding_agent.py
  23. BIN
      src/components/functions/__pycache__/video_upload_function.cpython-313.pyc
  24. 5 0
      src/components/functions/video_upload_function.py
  25. BIN
      src/states/__pycache__/what_deconstruction_state.cpython-313.pyc
  26. 1 0
      src/states/what_deconstruction_state.py
  27. 372 31
      src/workflows/decode_workflow.py
  28. 0 1358
      tech_design_document.md
  29. 0 1846
      tech_design_document_update.md
  30. BIN
      test/.DS_Store
  31. 0 0
      test/__init__.py
  32. 0 295
      test/agents/test_knowledge_requirement_agent.py
  33. 0 194
      test/agents/test_script_orthogonal_with_real_data.py
  34. 0 0
      test/functions/__init__.py
  35. 0 320
      test/functions/test_json_utils.py
  36. 0 11
      test/others/ReactAgent.py
  37. 0 0
      test/others/__init__.py
  38. 0 15
      test/others/gemini_available_test.py
  39. 0 41
      test/others/graph.py
  40. 0 0
      test/tools/__init__.py
  41. 0 61
      test/tools/test_nanobanana_simple.py

+ 0 - 242
CLAUDE.md

@@ -1,242 +0,0 @@
-# 项目开发指南
-
-## PRD拆解结构
-
-**PRD = 组件 + 工作流 + 数据结构**
-
-1. **组件** (Components) - 实现具体业务功能
-2. **工作流** (Workflow) - 编排组件执行顺序和流程逻辑  
-3. **数据结构** (Data Structures) - 定义状态格式和数据传递规范
-
-## 实现原则
-
-**优先复用,其次修改,最后完全新增**
-
-1. **优先复用** - 已有组件完全符合 → 直接复用 (import)
-2. **其次修改** - 已有组件部分符合 → Copy并修改
-3. **最后完全新增** - 没有合适组件 → 从Base组件新建文件
-
-**重要提醒**: 判断组件是否符合需求,需要查看具体代码实现,而不仅仅是组件名称
-
-## 开发约束
-
-- **面向需求**:一切以PRD需求为准,已有的组件和工作流仅供参考
-
-## 组件架构设计原则
-
-### Agent vs Tool/Function 选择标准
-
-**Agent组件特征**:
-- 任务类型: 不确定性任务
-- 核心能力: LLM智能能力 (内置多模态理解,无需额外工具支持)
-- 处理内容: 推理、多模态理解、语义分析、语义对比、内容生成、模式识别、创意分析
-- 依赖关系: 依赖LLM模型
-
-*注:Agent 自身有多模态的能力,无需额外的tool支持*
-
-**Tool/Function组件特征**:
-- 任务类型: 确定性任务
-- 核心能力: 数据处理
-- 处理内容: 数值计算、数据转换、格式化、排序、统计、API调用、文件操作等代码能简单实现的任务
-- 依赖关系: 无LLM依赖
-
-### Tool vs Function 选择标准
-
-**唯一区别:是否可以被Agent调用**
-
-**Tool组件**:
-- Agent调用: ✅ 可以被Agent调用
-- 技术实现: `@tool`装饰器或继承`BaseTool`
-
-**Function组件**:
-- Agent调用: ❌ 不能被Agent调用  
-- 技术实现: 普通函数或继承`BaseFunction`
-
-*注:除了Agent调用能力外,Tool和Function在功能上没有本质区别*
-
-## 开发建议
-
-1. **需要LLM能力** → Agent
-2. **需要Agent调用** → Tool  
-3. **其他情况** → Function
-
-**架构原则**: 非必要情况下,组件以Agent为主
-
-## 现有组件清单
-
-### Agent组件 (15个)
-
-**agents/base.py** - BaseComponent/BaseAgent/BaseLLMAgent/BaseReactAgent/BaseGraphAgent
-- 功能: Agent基础抽象类,提供四种Agent实现模式
-- 核心类:
-  - BaseComponent: 最基础的组件抽象,定义初始化和状态管理
-  - BaseAgent: Agent通用接口,支持工具绑定和模型切换
-  - BaseLLMAgent: 基于LLM的单轮对话Agent,适用于简单任务
-  - BaseReactAgent: 基于LangGraph的ReAct模式Agent,支持工具调用和推理
-  - BaseGraphAgent: 基于StateGraph的复杂工作流Agent,支持多节点编排
-
-**agents/comment_analysis_agent.py** - CommentAnalysisAgent
-- 功能: 从评论中提取帖子解构维度(两步法:评论过滤 → 维度提取)
-- 核心流程:
-  - 第1步: 过滤评论,只保留描述物理特征的相关评论
-  - 第2步: 从相关评论中提取解构维度(处理状态、色彩特征、材质特征等)
-- 输出: deconstruction_dimensions(解构维度列表)、filtered_comments(过滤后的评论)
-
-**agents/content_filter_agent.py** - ContentFilterAgent
-- 功能: 从bodytext中识别并移除与帖子主内容不相关的话题标签
-- 判断方法: 双重验证(显性匹配检查 + 主题相关性检查)
-- 输出: filtered_body(过滤后的bodytext)、removed_hashtags(移除的标签)、kept_hashtags(保留的标签)
-
-**agents/content_weight_agent.py** - ContentWeightAgent
-- 功能: 判断帖子中图片和文字的权重分配
-- 评分维度: 信息量维度(0-10分)、独立理解性(0-10分)、核心价值承载(0-10分)
-- 输出: image_weight(图片权重)、text_weight(文字权重)、primary_source(主要信息源:image/text/both)
-
-**agents/inspiration_points_agent.py** - InspirationPointsAgent
-- 功能: 从创作者视角提取触发创作的刺激源(7步流程)
-- 核心流程:
-  - Step1: 历史对比分析(三维度:共性内容、共性差异、全新内容)
-  - Step2: 识别候选灵感点(按三种来源分类)
-  - Step3: 逆向推导验证
-  - Step4: 去重检查
-  - Step5: 三维度评分(人设契合度、触发可能性、内容解释力)
-  - Step6: 排序筛选
-  - Step7: 组装最终输出
-- 输出: 分类的灵感点字典(全新内容、共性差异、共性内容)+ 元数据
-
-**agents/key_points_agent.py** - KeyPointsAgent
-- 功能: 从消费者视角提取核心吸引点(5步流程)
-- 维度体系: 形式(风格、关系)、实质(元素、分类)
-- 核心流程:
-  - Step1: 识别候选关键点
-  - Step2: 第一轮去重(与灵感点、目的点去重)
-  - Step3: 第二轮去重(关键点之间去重)
-  - Step4: 构建层级关系(树形结构)
-  - Step5: 组装最终输出
-- 输出: 具有层级关系的关键点树形结构
-
-**agents/knowledge_requirement_agent.py** - KnowledgeRequirementAgent
-- 功能: 分析帖子需要的知识补充需求
-- 输出: 知识需求列表(每个需求包含类别、描述、重要性等)
-
-**agents/post_summary_deconstruction_agent.py** - PostSummaryDeconstructionAgent
-- 功能: 帖子摘要解构
-- 输出: 帖子的摘要解构结果
-
-**agents/post_understanding_agent.py** - PostUnderstandingAgent
-- 功能: 帖子理解与分析
-- 输出: 帖子理解结果
-
-**agents/purpose_point_agent.py** - PurposePointAgent
-- 功能: 从创作者视角提取创作目的
-- 输出: 创作目的点列表(包含维度、目的点、描述等)
-
-**agents/recursive_image_deconstruction_agent.py** - RecursiveImageDeconstructionAgent
-- 功能: 递归解构图像(多层级深度分析)
-- 输出: 图像的递归解构结果
-
-**agents/recursive_text_deconstruction_agent.py** - RecursiveTextDeconstructionAgent
-- 功能: 递归解构文本(多层级深度分析)
-- 输出: 文本的递归解构结果
-
-**agents/script_section_division_agent.py** - ScriptSectionDivisionAgent
-- 功能: 脚本段落划分 - 分析已有帖子的分段结构,理解创作者是如何组织内容的
-- 核心流程: 根据图文权重策略识别创作者的分段逻辑
-- 输出: 段落列表(树状结构)、内容品类
-
-**agents/script_element_extraction_agent.py** - ScriptElementExtractionAgent
-- 功能: 脚本元素提取 - 从已有帖子内容中提取核心元素(实质性+概念性)
-- 核心流程(4步):
-  - Step1: 候选元素识别(实质性元素+概念性元素)
-  - Step2: 双维度评分(主题支撑性+多段落共性,分视角分析)
-  - Step3: 严格筛选(阈值6.0)
-  - Step4: 去重与整合(生成树状结构)
-- 输出: 元素列表(树状结构)
-
-**agents/topic_selection_understanding_agent.py** - TopicSelectionUnderstandingAgent
-- 功能: 选题理解与分析
-- 输出: 选题理解结果
-
-### Tool组件 (4个)
-
-**tools/base.py** - BaseTool
-- 功能: Tool基础抽象类
-- 特性: 可被Agent调用,使用@tool装饰器或继承BaseTool
-
-**tools/knowledge_retrieval_tools.py** - 知识检索工具集
-- 功能: 根据Query词查询知识库
-- 核心工具: knowledge_search(知识搜索)
-
-**tools/nanobanana_tools.py** - NanoBanana图像生成与编辑工具
-- 功能: 图像生成、图像编辑、图片融合、图片元素提取
-- 核心工具: generate_image、edit_image、merge_images、extract_elements
-
-**tools/segment_tools.py** - 图像分割工具
-- 功能: 图像分割与目标检测
-- 核心工具: segment_image(图像分割)
-
-### Function组件 (3个)
-
-**functions/base.py** - BaseFunction
-- 功能: Function基础抽象类
-- 特性: 不能被Agent调用,适用于纯函数式数据处理
-
-**functions/json_utils.py** - JSON处理函数集
-- 功能: JSON格式化、解析、验证
-- 核心函数: format_json、parse_json、validate_json
-
-**functions/result_aggregation_function.py** - ResultAggregationFunction
-- 功能: 聚合多个组件的处理结果,生成最终输出
-- 核心方法: aggregate(结果聚合)
-
-### 公共组件 (Utils)
-
-**utils/json_extractor.py** - JSONExtractor
-- 功能: JSON提取和解析工具类
-- 核心方法:
-  - extract_json_from_response: 从LLM响应中提取JSON内容(支持markdown代码块)
-  - safe_parse_json: 安全解析JSON,失败时返回默认值
-  - extract_and_parse: 一步完成提取和解析
-
-**utils/logger.py** - DeconstructionLogger
-- 功能: 解构系统专用日志器(单例模式)
-- 核心方法:
-  - get_logger: 获取指定名称的子日志器
-  - log_agent_start: 记录Agent开始执行
-  - log_agent_finish: 记录Agent完成执行
-  - log_tool_call: 记录工具调用
-  - log_validation_result: 记录验证结果
-  - log_error_with_context: 记录带上下文的错误
-- 特性: 文件日志+控制台日志,自动创建日志目录
-
-**utils/llm_invoker.py** - LLMInvoker
-- 功能: LLM调用封装工具类
-- 核心方法:
-  - safe_invoke: 安全的LLM调用,支持自动初始化、JSON解析、降级处理
-
-**utils/utils.py** - 通用工具函数集
-- 功能: 提供常用工具函数
-- 核心方法:
-  - remove_duplicates: 去重并保持元素顺序
-  - oss_resize_image: OSS图片URL尺寸调整
-
-## 现有工作流
-
-### WhatDeconstructionWorkflow
-- 数据结构: WhatDeconstructionState
-- 功能: What业务递归解构工作流
-- 核心流程:
-  1. 内容过滤(ContentFilterAgent)- 过滤bodytext中的无关标签
-  2. 内容权重判断(ContentWeightAgent)- 判断图文权重
-  3. 评论分析(CommentAnalysisAgent)- 提取解构维度
-  4. 灵感点提取(InspirationPointsAgent)- 创作者视角的触发源
-  5. 目的点提取(PurposePointAgent)- 创作者视角的创作目的
-  6. 关键点提取(KeyPointsAgent)- 消费者视角的吸引点
-  7. 选题理解(TopicSelectionUnderstandingAgent)- 选题分析
-  8. 知识需求分析(KnowledgeRequirementAgent)- 知识补充需求
-  9. 结果汇总(ResultAggregationFunction)- 聚合所有结果
-- 依赖组件:
-  - Agent: ContentFilterAgent、ContentWeightAgent、CommentAnalysisAgent、InspirationPointsAgent、PurposePointAgent、KeyPointsAgent、TopicSelectionUnderstandingAgent、KnowledgeRequirementAgent
-  - Function: ResultAggregationFunction
-
----

+ 1003 - 0
decode_workflow_process.md

@@ -0,0 +1,1003 @@
+# 解码工作流执行过程总结
+
+## 工作流概览
+
+解码工作流是一个完整的视频分析流程,包含10个节点:
+1. 视频上传 → 2. 灵感点提取 → 3. 目的点提取 → 4. 关键点提取 → 5. 选题理解 → 
+6. 段落划分 → 7. 实质提取 → 8. 形式提取 → 9. 分离结果 → 10. 结果汇总
+
+---
+
+## 节点1: 视频上传
+
+**功能**: 下载视频到本地并上传至Gemini,保存上传链接
+
+**执行过程**: 
+- 检查examples/videos目录下是否有现有文件
+- 如无则从URL下载视频到examples/videos目录
+- 上传视频到Gemini File API
+- 保存文件URI和名称到state
+
+**输出**: `video_uploaded_uri`, `video_file_name`
+
+---
+
+## 节点2: 灵感点提取
+
+**功能**: 从创作者视角提取触发创作的深层刺激源
+
+**执行流程**: 6步提取流程
+
+### Step 1: 识别候选灵感点(逐帧分析视频)
+
+**Prompt**:
+```
+# 任务:识别候选灵感点(逐帧分析视频)
+
+## 当前视频
+请对视频进行逐帧或关键帧分析。
+
+**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
+- 画面内容:场景、物体、人物、环境等视觉元素
+- 动作变化:人物的动作、物体的运动、场景的转换
+- 时间线索:视频的时间顺序、节奏变化、关键时间点
+- 视觉细节:颜色、光影、构图、视觉焦点等
+- 音频信息(如有):对话、音效、背景音乐等
+
+## 核心概念(统一说明,避免重复)
+
+### 字段定义
+**分类**(维度):创作者接收外界信息刺激的角度或通道
+- 格式:2-4个字,简洁直观,避免抽象表述
+
+**灵感点**:创作前遇到的、触发创作冲动的客观刺激源(作者被动接收的信息:看到的、听说的、发现的、观察到的、感知到的)
+- 格式:不超过15个字,使用自然、通俗、口语化的表达方式
+- 本质:刺激源的内容本身(讲什么、做什么、说什么),而非表达手段(如何呈现、如何表达)
+- 表达要求:
+  * 使用日常生活中的自然语言,避免学术化、抽象化的词汇堆砌
+  * 优先使用"的"字短语结构(如"夏日的热闹景象")或动宾短语(如"观察到的自然互动")
+  * 禁止使用多个抽象名词连用(如"具象化动态互动自然拟人")
+  * 表达要让普通人一看就懂,不需要"翻译"
+
+**描述**:对刺激源本身是什么的详细说明,讲清楚这个灵感点具体指的是什么
+- 内容要求:描述刺激源的具体特征、形态、场景、内容等客观信息,让读者能清晰理解"这个灵感点到底是什么东西"
+- 注意区分:刺激源内容本身 vs 呈现方式/表现形式
+
+**推理**:说明这个灵感点是如何得出来的,解释从视频内容推导出这个灵感点的过程
+- 内容要求:连贯的描述性文字,说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者接收到了这个刺激源
+
+### 严格禁止(适用于灵感点和描述)
+- 不描述创作者如何运用/展现/表达这个刺激,不使用推理性词汇
+- 不能是创作形式、表现手法、表达方式、呈现方式、风格、格式等创作应用层面的东西
+- 必须是被动接收的刺激,不能是主动创造的内容
+- 不解释创作者为什么被触发、如何使用这个刺激
+- 不进行主观推理和价值判断
+- 禁止词汇堆砌,避免多个抽象概念连用
+
+### 反思验证(识别每个灵感点后必须进行)
+- 这个灵感点描述的是内容本身,还是仅仅描述表达手段?
+- 如果仅描述表达手段(如呈现方式、创作形式、风格、格式等),应重新提炼真正的刺激源内容
+- 这个表达是否符合日常口语习惯?是否通俗易懂?
+- 如果描述的是内容本身且表达自然 → 保留
+
+### 识别要求
+- **独立性**: 不同灵感点必须是不同的刺激源,本质相同的只保留一个
+- **完整性**: 分类→灵感点→描述应形成从刺激通道到刺激内容的完整表达
+- **真实性**: 从"作者接收到了什么刺激"(被动接收)而非"作者如何创作"(主动输出)的角度出发
+- **视频特性**: 需要关注视频的动态特性,包括画面变化、动作序列、时间线索等
+
+## 输出格式(JSON)
+{
+  "候选灵感点列表": [
+      {
+        "候选编号": 1,
+        "分类": "...",
+        "灵感点": "...",
+        "描述": "...",
+        "推理": "..."
+      }
+  ]
+}
+
+**重要提醒**:
+- 推理必须是连贯的描述性文字,说明为什么这个灵感点能刺激创作
+- 候选编号全局唯一(连续编号:1, 2, 3...)
+- 必须基于视频的实际内容进行分析,不能凭空想象
+```
+
+### Step 2: 逆向推导验证
+
+**Prompt**:
+```
+# 任务:逆向推导验证
+
+## 当前视频
+请对视频进行逐帧或关键帧分析。
+
+## 候选灵感点
+{candidates_text}
+
+## 验证要求
+对每个候选灵感点进行逆向推导验证:
+
+**验证问题**:
+- 从这个灵感点出发,能否推导出整个视频的内容?
+- 这个刺激源是创作前的触发,而不是创作目的?
+- 这个灵感点是否真正捕捉到了深层动机?
+
+**验证规则**:
+- 能推导出 → 验证通过(true)
+- 不能推导出或逻辑不通 → 验证失败(false)
+
+## 输出格式(JSON - 简化版)
+{
+  "验证结果列表": [
+    {
+      "候选编号": 1,
+      "验证结果": true,
+      "推导说明": "从这个灵感点可以推导出..."
+    }
+  ]
+}
+```
+
+### Step 3: 去重检查
+
+**Prompt**:
+```
+# 任务:灵感点去重
+
+## 候选灵感点列表
+{candidates_text}
+
+## 核心判断标准
+
+**唯一标准:是否指向同一个刺激源?**
+
+- 如果两个灵感点描述的是**同一个事物/现象/刺激**,只是从不同角度、不同层面、不同粒度来表达 → 重复,去重
+
+## 判断方法
+
+1. **识别刺激源本质**:每个灵感点背后的刺激源到底是什么?
+2. **对比刺激源**:是同一个刺激源的不同表述?还是两个不同的刺激源?
+3. **忽略表述差异**:不要被具体用词、分类标签、角度差异迷惑,抓住本质
+
+## 保留规则(重复时选择)
+
+从重复组中选择**最能直接反映刺激源本身**的那一个:
+- 优先保留描述刺激源本身的(客体)
+- 其次保留最具体、最精准的
+
+## 输出(JSON)
+{
+  "去重分析": [{"重复组": [2, 3], "重复原因": "两者都在描述同一个刺激源:[具体说明]", "保留编号": 2, "保留理由": "[原因]"}],
+  "保留的候选编号列表": [1, 2]
+}
+```
+
+### Step 4: 三维度评分
+
+**Prompt**:
+```
+# 任务:评估灵感点
+
+## 待评分的灵感点
+{points_text}
+
+## 评分标准
+
+对每个灵感点从三个维度评分(每个维度1-10分):
+
+### 1. 人设契合度(默认先都给5分)
+- 因没有历史视频信息无法推算人设,先都给5分
+
+### 2. 触发可能性(1-10分)
+- 评估这个刺激点触发创作的可能性有多大
+- 考虑刺激点的吸引力强度
+- 考虑创作者遇到这个刺激时产生创作冲动的概率
+
+### 3. 内容解释力(1-10分)
+- 评估这个刺激点能否解释视频的核心内容
+- 判断从这个刺激点能否推导出视频的主要内容
+- 考虑刺激点与视频内容的关联强度
+
+**总分 = 人设契合度 + 触发可能性 + 内容解释力(范围:3-30分)**
+
+## 评分原则
+- 客观评分,不要预设分数高低
+- 如果与视频内容不符,触发可能性也会很低
+- 如果是视频核心特征,触发可能性反而很高
+
+## 输出格式(JSON)
+{
+  "评分结果列表": [
+    {
+      "候选编号": 1,
+      "人设契合度": 8,
+      "触发可能性": 7,
+      "内容解释力": 9,
+      "总分": 24,
+      "评分说明": "详细说明评分理由:为什么这个刺激点符合人设、触发创作的可能性如何、能否解释视频内容"
+    }
+  ]
+}
+```
+
+### Step 5: 排序筛选
+- 按总分降序排序,筛选总分>=15分的灵感点,最多保留2个
+
+### Step 6: 组装最终输出
+- 从原始候选点中提取完整字段,合并验证信息和评分信息
+
+**输出**: `inspiration_points` (列表格式)
+
+---
+
+## 节点3: 目的点提取
+
+**功能**: 从创作者视角分析创作目的和动机
+
+**Prompt**:
+```
+# 任务:提取创作目的(视频逐帧分析)
+
+## 视频内容
+请对视频进行逐帧或关键帧分析。
+
+**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
+- **画面内容**:场景、物体、人物、环境等视觉元素
+- **动作变化**:人物的动作、物体的运动、场景的转换
+- **时间线索**:视频的时间顺序、节奏变化、关键时间点
+- **视觉细节**:颜色、光影、构图、视觉焦点等
+- **音频信息**(如有):对话、音效、背景音乐等
+
+## 分析步骤
+1. **逐帧分析**:观察视频的每一帧或关键帧,理解视频的整体内容和结构
+2. **识别意图**:从视频的画面、动作、场景、对话等维度,推断创作者想让受众产生什么反应/影响(Why)
+3. **维度归类**:从标准分类体系中选择最匹配的维度(优先二级分类)
+4. **提炼目的点**:在维度下提炼具体目标(≤15字,聚焦最终效果,不写手段)
+
+## 重要约束
+- **每个二级分类最多1个目的点**(例如不能同时有2个"记录"维度的目的点)
+- **宁缺毋滥**:如果某个维度的目的不明确,不要强行生成
+- **总数控制**:最终输出1-3个目的点,不要为了凑数而硬造
+- **证据支撑**:每个目的点都必须有足够的视频内容支撑,不能臆测
+- **基于视频**:所有目的点必须基于视频的实际内容,不能凭空想象
+
+## 输出格式(JSON)
+{
+    "目的点列表": [
+        {
+            "维度": {
+                "一级分类": "",
+                "二级分类": ""
+            },
+            "目的点": "",
+            "描述": "",
+            "推理": "说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者的这个目的"
+        }
+    ]
+}
+```
+
+**输出**: `purpose_point` (包含perspective, purposes, total_count)
+
+---
+
+## 节点4: 关键点提取
+
+**功能**: 从消费者视角提取核心吸引点
+
+**执行流程**: 5步提取流程
+
+### Step 1: 识别候选关键点
+
+**Prompt**:
+```
+# 任务:识别候选关键点(视频逐帧分析,扁平化列表)
+
+## 视频内容
+请对视频进行逐帧或关键帧分析。
+
+**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度捕捉吸引点:
+- **画面内容**:场景、物体、人物、环境等视觉元素中的吸引点
+- **动作变化**:人物的动作、物体的运动、场景的转换等动态吸引点
+- **时间线索**:视频的时间顺序、节奏变化、关键时间点等时序吸引点
+- **视觉细节**:颜色、光影、构图、视觉焦点等视觉吸引点
+- **音频信息**(如有):对话、音效、背景音乐等听觉吸引点
+
+## 字段定义
+
+### 维度大类
+只能是"形式"或"实质"
+
+### 维度细分
+- 形式类:只能是"风格"或"关系"
+- 实质类:只能是"元素"或"分类"
+
+### 关键点
+≤15字核心发现(只写"是什么",不写"如何呈现")
+
+### 描述
+- **本质**: 讲清楚这个关键点是什么(吸引点的具体特征、形态、内容等客观信息)
+- **内容要求**:
+  - 描述吸引点本身的客观特征
+  - 说明这个吸引点的完整上下文
+  - 让读者能清晰理解"这个关键点到底是什么"
+- **严格禁止**:
+  - 不解释为什么吸引
+  - 不进行价值判断和主观评价
+  - 不分析效果和影响
+
+## 维度分类要求(二级分类体系)
+
+**维度大类:形式**
+- **风格**:关注内容的整体呈现方式、结构、格式等外在特征
+- **关系**:关注内容中各个实质要素之间的关联、逻辑关系
+
+**维度大类:实质**
+- **元素**:关注内容中的具体实体、对象、要素等
+- **分类**:关注对具体元素的抽象、总结、归纳
+
+## 输出(JSON)
+{
+  "候选关键点列表": [
+    {
+      "候选编号": 1,
+      "维度大类": "形式|实质",
+      "维度细分": "风格|关系|元素|分类",
+      "关键点": "核心发现",
+      "描述": "充分说明"
+    }
+  ]
+}
+
+要求:
+- 广泛收集:从视频的各个维度(画面、动作、场景、对话等)广泛收集吸引点
+- 客观陈述:只陈述事实,不做主观判断
+- 严格按照二级维度体系归类:必须明确标注维度大类(形式/实质)和维度细分(风格/关系/元素/分类)
+- 编号连续:候选编号从1开始连续编号
+- 基于视频:所有关键点必须基于视频的实际内容,不能凭空想象
+
+注意:此步骤输出扁平化列表,后续会构建层级关系
+```
+
+### Step 2: 第一轮去重(与灵感点、目的点去重)
+- 移除与灵感点/目的点重复的关键点
+
+### Step 3: 第二轮去重(关键点之间去重)
+- 移除本质相同的关键点,只保留一个
+
+### Step 4: 构建层级关系
+
+**Prompt**:
+```
+# 任务:构建关键点层级关系
+
+## 关键点列表(扁平化)
+{points_text}
+
+## 层级判断标准
+
+**层级关系定义**:
+- **大类 → 小类**:一个关键点是另一个关键点的具体类别
+- **抽象 → 具体**:一个关键点是另一个关键点的具体表现
+- **整体 → 局部**:一个关键点是另一个关键点的组成部分
+
+**判断方法**:
+1. 识别父子关系:子关键点是父关键点的细分、具体化或局部
+2. 构建树形结构:父节点可以有多个子节点,子节点也可以继续有子节点
+3. 确保无循环:不能出现A→B→A的情况
+
+**层级原则**:
+- 同一层级的关键点应该是并列关系(不存在包含、细分关系)
+- 不同层级的关键点应该是从属关系(父→子)
+- 一级关键点(根节点)应该是最抽象、最整体的关键点
+- 如果两个关键点没有明确的层级关系,则保持在同一层级
+
+## 输出(JSON)
+{
+  "层级分析": [
+    {
+      "父编号": 1,
+      "子编号列表": [2, 3],
+      "层级关系": "大类→小类|抽象→具体|整体→局部",
+      "关系说明": "说明为什么存在这个层级关系",
+      "子节点原因": {
+        "2": "说明编号2作为子节点的具体原因",
+        "3": "说明编号3作为子节点的具体原因"
+      }
+    }
+  ],
+  "一级关键点编号列表": [1, 4, 5]
+}
+```
+
+### Step 5: 组装最终输出
+- 构建树形结构的关键点列表
+
+**输出**: `key_points` (包含key_points树形列表, total_count, root_count)
+
+---
+
+## 节点5: 选题理解
+
+**功能**: 整合视频内容和三点分析结果,理解选题策略
+
+**Prompt**:
+```
+# 输入数据
+
+## 视频内容 
+请对视频进行逐帧或关键帧分析。
+
+**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度理解视频内容:
+- **画面内容**:场景、物体、人物、环境等视觉元素
+- **动作变化**:人物的动作、物体的运动、场景的转换
+- **时间线索**:视频的时间顺序、节奏变化、关键时间点
+- **视觉细节**:颜色、光影、构图、视觉焦点等
+- **音频信息**(如有):对话、音效、背景音乐等
+
+基于视频的实际内容,结合以下三点分析结果,理解选题策略。
+
+## 灵感点分析
+{inspiration_section}
+
+## 目的点分析
+{purpose_section}
+
+## 关键点分析
+{key_points_section}
+
+---
+
+# 执行指令
+
+基于以上数据,输出选题策略分析。
+
+## 质量要求
+- **描述**: 必须是完整流畅的1-2句话,清晰展现完整逻辑链"灵感来源→关键手法→呈现效果→创作目的"
+  - **必须包含目的点**: 明确说明创作者的目的,不能只描述内容本身
+  - **必须体现三点内在关联**: 分析三点的有机融合逻辑,而非简单罗列
+  - **基于视频内容**: 所有分析必须基于视频的实际内容,不能凭空想象
+  - 拒绝笼统概念,要具体到本质特征
+  - 删除非核心修饰词,只保留最本质的词
+  - 禁用空洞词汇: "精心"、"独特"、"全面"、"旨在"、"充分"、"深度"、"夸张的"、"强烈的"、"明显的"
+
+**重要提醒**:
+- 不需要输出 `explicit_elements` 字段,系统会自动从三点数据中提取
+- 所有分析必须基于视频的实际内容和三点分析结果
+```
+
+**输出**: `topic_selection_understanding` (包含主题、描述、覆盖情况、explicit_elements)
+
+---
+
+## 节点6: 段落划分
+
+**功能**: 分析视频的分段结构,理解创作者如何组织内容
+
+**Prompt**:
+```
+# 任务
+
+从**创作者视角**分析这个视频是如何组织内容的。
+
+## Section切分流程
+
+**第一步:识别主题显著变化位置**
+扫描整个视频,识别**主题发生显著变化**的位置:
+
+- **判断标准**:
+  * 语义跃迁: 讨论对象发生根本性改变
+  * 逻辑转换: 从"是什么"转向"为什么"或"怎么办"
+  * 功能变化: 从"问题陈述"转向"解决方案"
+  * 场景切换: 画面、场景、动作发生明显变化
+  * 对话转换: 话题、语气、情绪发生明显变化
+
+- **划分原则**:
+  * 避免过度细分(每个小变化都成为顶层段落)
+  * 避免过度粗放(将所有内容合并为1个顶层段落)
+  * 以"主题板块"而非"内容单元"为划分粒度
+
+**第二步:初步划分**
+- 基于主题显著变化位置进行划分
+- 支持主Section和子Section的层级结构
+- 需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的结构变化
+
+**第三步:顺序验证与反思**
+- 检查每个Section内的视频片段是否保持时间顺序的连续性
+- 验证同一Section内的内容是否有语义关联
+- 确认段落之间的过渡是否自然合理
+
+## 层级要求
+
+**段落必须至少保留2层结构**:
+1. **第1层(抽象层)**:从具象中聚合出的共性维度
+2. **第2层(具象层)**:具体的内容细节
+
+**层级关系说明**:
+- 抽象层是对多个具象内容的归纳和提炼
+- 具象层是抽象层的具体展开
+- 每个抽象层下必须有至少1个具象层子项
+
+### 整体情绪价值(Emotion Value)
+**内容背后的情绪钩子或社会价值**。仅在有明显的升维价值时提取(如:民族自信、致青春、趋吉避凶、鞠躬尽瘁),否则留空。
+** 输出案例: "致青春", "友谊长存", "猎奇", "颠覆认知"等 6字以内的词语,可输出多个,用逗号分隔**
+
+## Section字段
+
+- 描述: 段落描述(共性维度名称;具体内容概括)
+- 内容范围: **列表格式,包含具体视频片段**
+  - 格式:["0:00-0:30", "0:30-1:15"] 或 ["片段1: 开场介绍", "片段2: 问题阐述"]
+  - 要求:必须包含具体的时间戳范围或视频片段描述,清晰标识该Section涵盖的视频内容
+- 推理依据: 为什么这样划分
+- 子项: 子Section列表(树状结构)
+
+# 输出(JSON)- 树状结构
+
+{
+  "内容品类": "内容品类",
+  "整体情绪分析": ["整体情绪分析1(6个字以内)", "整体情绪分析2(6个字以内)"],
+  "段落衔接机制": [
+    {
+      "from": "段落1",
+      "to": "段落2",
+      "衔接方式": "如何过渡到下一个段落",
+      "衔接语/画面特征": "衔接语或画面特征",
+      "设计意图": "设计意图"
+    }
+  ],
+  "段落列表": [
+    {
+      "描述": "共性维度名称",
+      "内容范围": ["0:00-0:30", "0:30-1:15"],
+      "推理依据": "为什么这样划分这个抽象层",
+      "子项": [
+        {
+          "描述": "具体内容概括",
+          "内容范围": ["0:00-0:30"],
+          "推理依据": "这个具象内容如何支撑上层抽象",
+          "子项": []
+        }
+      ]
+    }
+  ]
+}
+```
+
+**输出**: `内容品类`, `段落列表` (树状结构)
+
+---
+
+## 节点7: 实质提取
+
+**功能**: 从视频内容中提取实质元素(具体元素、具象概念、抽象概念)
+
+**执行流程**: 8步提取流程
+
+### Step 1: 提取具体元素
+
+**Prompt**:
+```
+# 任务
+从视频中提取"具体元素"
+
+# 核心定义
+
+## 具体元素
+- **定义**:
+  -- 1.从视频画面中直接观察到的、可独立存在的**单一视觉实体对象**
+  -- 2.视频的背景音乐、音效等非口播内容的声音
+- **判断标准**:
+  -- 1.可以指着画面说"这是一个X"(单一、具体、可见的实体)
+  -- 2.有背景音乐、音效等非口播内容的声音,直接用"背景音乐/音效声"作为名称即可,不要重复提取
+- **示例**:
+  -- 1.胡萝卜、青椒、西兰花(每个都是单独的实体)
+  -- 2.背景音乐/音效声
+- **禁止**:
+  - 归类词(蔬菜、水果)
+  - 概念性名词(食物、植物、人)
+  - 文字内容(只关注视觉实体)
+
+## 提取原则(仅针对画面中的视觉实体对象)
+- 只从视频画面中提取,不关注文字
+- 每个元素必须是单一的、具体的视觉实体
+- 使用"剥离测试":去掉表达方式后,这个实体仍然存在
+
+# 命名规范
+- 原子性:单一原子名词,不可再拆分
+- 名词性:纯名词,严禁形容词、动词、副词
+- 具体性:直接指向可观察的实体
+
+# 输出json结构
+[
+    {
+      "id": "从1开始的自增序列",
+      "名称": "单一原子名词",
+      "描述": "说明这个元素是什么,外观特征",
+      "维度": {"一级": "实质", "二级": "具体元素"},
+      "来源": ["视频画面"],
+      "推理": "为什么识别这个具体元素"
+    }
+]
+```
+
+### Step 2: 提取具象概念
+
+**Prompt**:
+```
+# 任务
+从视频中提取"具象概念"
+
+# 核心定义
+## 具象概念
+- **定义**:视频画面内的文字或者口播内容中明确提到的完整名词
+
+## 排除的名称(来自第一步,仅用于排除)
+**禁止提取的名称**:{element_names_text}
+
+## 判断标准
+- **视频画面内的文字或者口播内容**中实际出现的**完整名词**
+- **不能是视频画面中出现的元素的名称等归类词**
+- 去掉表达方式后,这个概念仍然存在
+
+# 约束
+- 禁止通过语义推导、联想、理解得出的名词
+- **禁止归类词(蔬菜、水果、人等)**
+- **禁止使用第一步中已提取的具体元素名称**
+- 禁止拆分复合词
+- 禁止提取形容词、动词
+- 禁止提取谓语、定语、状语、补语
+- 禁止提取副词
+
+## 提取原则
+- **词语完整性**:必须提取完整的**名词**,不允许拆分复合词
+- **严格约束**:必须是**画面文字或者口播内容中实际出现**的完整名词
+- **严格的名词验证**(必须同时满足以下两个条件):
+   - 条件1:词性是名词(词典意义上的名词)
+   - 条件2:在当前上下文中作为名词使用(语境判断)
+
+   **验证方法**:
+   - 找到该词在视频画面内的文字或者口播内容中的具体位置
+   - 分析该词在句子中的语法成分和实际作用
+   - 判断:该词是否在这个语境中充当"事物/对象/概念"的角色?
+
+# 输出json结构
+[
+    {
+      "id": "从1开始的自增序列",
+      "名称": "字面原词(完整名词)",
+      "描述": "说明这个概念是什么",
+      "维度": {"一级": "实质", "二级": "具象概念"},
+      "来源": "HH:MM:SS",
+      "上下文验证": {
+        "原文位置": "该词在原视频画面内的文字或者口播内容中的具体句子",
+        "语法成分": "该词在句子中的语法成分(主语/宾语/定语中心语等)",
+        "语境判断": "说明该词在此语境中确实作为名词使用的理由"
+      },
+      "推理": "为什么这个名词被认为是具象概念"
+    }
+]
+```
+
+### Step 3: 总结抽象概念
+
+**Prompt**:
+```
+# 任务
+基于已提取的具体元素和具象概念,总结新的"抽象概念"
+
+# 已提取的具体元素
+{elements_text}
+
+# 已提取的具象概念
+{concepts_text}
+
+# 核心定义
+
+# 定义与分类
+**抽象概念**分两类:
+
+**类型1-上位抽象**:对具体元素/具象概念的归类
+**类型2-引申含义**:具体元素/具象概念无法直接表达的深层含义
+
+# 提取原则
+- 对具体元素/具象概念的归类
+- 具体元素和具象概念无法直接表达的深层含义
+- 基于归纳:基于已提取的具体元素/具象概念
+- 来源追溯:准确标明所有来源ID(具体元素ID、具象概念ID),必须完整可追溯
+
+# 命名规范
+- 有完整独立语义的概念
+- 单一原子名词,不可拆分
+- 纯名词,禁止形容词、动词、副词
+- 精准描述概念,不做修饰
+
+# 判断标准
+- 去掉表达方式后,概念仍存在
+
+# 输出json结构
+[
+    {
+      "id": "从1开始的自增序列",
+      "名称": "单一名词或短语",
+      "描述": "说明这个抽象概念是什么",
+      "维度": {"一级": "实质", "二级": "抽象概念"},
+      "类型": "上位抽象 | 引申含义",
+        "来源": {
+        "具体元素": [{"id":"具体元素-X", "名称":"具体元素-X的名称"}, {"id":"具体元素-Y", "名称":"具体元素-Y的名称"}],
+        "具象概念": [{"id":"具象概念-A", "名称":"具象概念-A的名称"}, {"id":"具象概念-B", "名称":"具象概念-B的名称"}]
+      },
+      "推理过程": "明确说明如何从上述来源(具体哪些元素ID和概念ID)推导出这个抽象概念",
+    }
+]
+```
+
+### Step 4: 共性分析
+- 统计每个实质元素在视频中的出现频次和段落覆盖率
+
+### Step 5: 多维度评分
+- 已废弃相似度计算逻辑,仅返回空结果
+
+### Step 5.1: 意图支撑评估
+- 评估实质元素对灵感点/目的点/关键点的支撑关系(基于视频画面和文字语境)
+
+### Step 6: 筛选
+- 筛选条件:出现频次>1 且 有意图支撑关系 且 段落覆盖率>30%(声音类型元素豁免)
+
+### Step 7: 分类
+- 按二级维度(具体元素/具象概念/抽象概念)分别进行分类
+
+### Step 8: 合并信息
+- 合并所有中间信息到最终元素
+
+**输出**: `substance_final_elements` (列表格式)
+
+---
+
+## 节点8: 形式提取
+
+**功能**: 从视频内容中提取形式元素(具象概念形式、具体元素形式、整体形式)
+
+**执行流程**: 8步提取流程
+
+### Step 1: 提取具象概念形式
+
+**Prompt**:
+```
+# 任务
+从视频中提取"具象概念形式"元素(关注画面中的文字呈现方式和口播表达方式)
+
+# 已提取的具象概念(供参考,不允许从中直接抄写内容)
+{concepts_text}
+
+# 核心定义
+
+## 具象概念形式
+- 定义:对具象概念的描述、修饰、表达方式(包括画面文字呈现方式 + 口播表达方式)
+- 特征:表达方式本身,而不是概念内容
+- 包含:
+  - 对概念的强调方式(如:反复重复、放大字号、加重语气)
+  - 画面文字呈现方式(如:字体、颜色、大小、位置、动画等)
+  - 口播语气特性(如:教学口吻、科普式表达、聊天式口吻、语气特征、语速、停顿等)
+  - 口播内容的修饰手法(如:夸张、比喻、反问、排比)
+  - 口播内容的叙述方式(如:第一人称、第二人称、对话体)
+  - 口播内容的节奏特征(如:语速快慢、停顿设计、重音、语气变化等)
+
+## 提取原则
+1. 只提取画面文字展示方式和口播表达方式,不涉及视觉呈现
+2. 必须依附于已提取的具象概念
+3. 使用"剥离测试":去掉表达手法后,概念本身仍存在
+4. **原子化命名**:形式名称必须是最原子的,不包含实质内容
+5. **建立关联关系**:通过"支撑"字段建立形式与实质的关联边
+6. **段落级别**:形式作用域至少是整个段落,不能仅针对单句
+
+## 作用域约束(重要)
+1. **段落级别作用域**(核心约束):
+   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的某一句话
+   - 必须是该段落的整体画面文字呈现方式和口播表达方式,而非局部单句的特征
+2. **同段落作用域**:形式必须与其支撑的具象概念在同一段落中
+3. **事实对应关系**:形式对概念的支撑必须在视频中有明确的对应关系
+   - 不能仅凭推测建立支撑关系,必须实际看到这种支撑
+
+# 输出json结构
+[
+  {
+    "id": "从1开始的自增序列",
+    "名称": "单一名词或短语(必须是原子化的形式名称)",
+    "描述": "说明形式的本质是什么,以及在视频中的具体定义",
+    "维度": {"一级": "形式", "二级": "具象概念形式"},
+    "支撑": [{"id":"具象概念id", "名称":"具象概念名称"}],
+    "推理": "在文字中的具体体现方式(引用视频中的说法或呈现方式)",
+    "段落来源": ["段落ID1", "段落ID2"]
+  }
+]
+```
+
+### Step 2: 提取具体元素形式
+
+**Prompt**:
+```
+# 任务
+从视频中提取"具体元素形式"元素
+
+# 已提取的具体元素
+{elements_text}
+
+# 已提取的具象概念
+{concepts_text}
+
+# 核心定义
+
+## 具体元素形式
+- **定义**:视觉画面的形式,支撑具体元素、具象概念的视觉呈现以及背景音乐、音效等非口播内容的声音呈现方式
+- **特征**:图像的视觉表现手法、外观特征或者背景音乐和音效的类别、风格、节奏等
+- **范围**:构图、色彩、光影、质感、视觉技巧等图片层面的特征或者声音方面的特征
+- **重要**:不关注画面里的文字以及口播内容说了什么,只关注视觉呈现以及声音呈现方式
+
+## 提取原则
+1. **视觉性**:必须是画面层面或者背景音乐音效层面的形式,而非文字层面或者口播内容层面
+2. **边界明确**:不关注画面里的文字以及口播内容,只关注视觉呈现手法或者声音呈现方式
+3. **支撑性**:可以支撑具体元素,也可以支撑具象概念
+4. **语义完整性**:必须提取具有完整独立语义的形式特征
+5. 使用"剥离测试":去掉表达方式后,元素本身仍存在
+6. **段落级别**:形式作用域至少是整个段落,不能仅针对单个元素
+
+## 作用域约束(重要)
+1. **段落级别作用域**(核心约束):
+   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的某一句话
+   - 必须是该段落的整体呈现方式,而非局部单个元素的特征
+2. **同段落作用域**:形式必须与其支撑的实质元素/概念在同一段落中
+3. **事实对应关系**:形式对实质的支撑必须在视频中有明确的视觉对应关系
+   - 不能仅凭推测建立支撑关系,必须在画面中实际看到这种支撑
+
+# 命名与关联分离原则(重要)
+
+## 核心规则
+- **名称**:只描述形式手法本身(纯形式词汇)
+- **支撑**:说明这个形式手法应用在哪些实质内容上(通过id关联具体元素/具象概念)
+- **推理**:说明具体如何体现(在图像中的呈现细节或者声音方面的呈现方式)
+- **段落来源**:记录形式来自哪个段落
+
+## 关键要求
+1. **名称的纯粹性**:名称中不能包含任何实质内容(具体元素名称、具象概念名称等)
+2. **通过支撑建立关联**:实质内容通过"支撑"字段的id关联,而非写在名称中
+3. **推理的具体性**:在推理中详细说明这个形式手法如何应用在支撑的实质内容上
+4. **来源的准确性**:图片来源和段落来源必须与支撑的实质元素/概念一致
+
+# 输出json结构
+[
+    {
+      "id": "从1开始的自增序列",
+      "名称": "单一名词或短语(必须是原子化的纯形式词汇)",
+      "描述": "说明形式的本质及在帖子中的具体定义",
+      "维度": {"一级": "形式", "二级": "具体元素形式"},
+      "支撑": {
+        "具体元素": [{"id":"元素id", "名称":"元素名称"}],
+        "具象概念": [{"id":"概念id", "名称":"概念名称"}]
+      },
+      "推理": "在画面中或者声音方面的具体体现(说明这个形式手法如何应用在支撑的实质内容上)",
+      "段落来源": ["段落ID1", "段落ID2"]
+    }
+]
+```
+
+### Step 3: 提取整体形式
+
+**Prompt**:
+```
+# 任务
+从视频中提取"整体形式"元素
+
+# 已提取的具体元素
+{elements_text}
+
+# 已提取的具象概念
+{concepts_text}
+
+# 已提取的抽象概念
+{abstracts_text}
+
+# 核心定义
+
+## 整体形式
+- 定义:多模态内容的组合方式、结构特征、整体呈现策略
+- 特点:不针对单个元素/概念,而是整体组织方式
+
+## 提取原则
+1. 整体性:必须是整体层面的特征,不是局部细节
+2. 形式性:使用"剥离测试" - 去掉表达方式后内容失去结构
+3. 可命名性:能用简洁的名词/短语概括
+4. 避免重复:与具体元素形式、具象概念形式区分开
+7. **宏观层面**:作用范围至少是整个段落或多段落,不能是单句或局部内容
+
+## 作用域约束(重要)
+1. **段落级别作用域**(核心约束):
+   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的局部内容
+   - 整体形式通常作用于整个帖子或多个段落,必须是宏观层面的特征
+2. **支撑关系**:整体形式支撑特定实质元素时,必须记录这些元素所在的段落
+3. **来源标注**:标注整体形式主要体现在哪些段落中
+
+## 常见维度参考(不限于此)
+- 多模态组合:画面+口播+字幕的配比、关系、时序、模态切换
+- 结构方式:内容组织逻辑(并列/递进/对比/因果/总分/时间/空间顺序等)
+- 呈现策略:整体表达风格(教程式/故事式/问答式/列表式/对话式等)
+- 节奏控制:信息密度、镜头节奏、高潮设计
+- 视角设计:叙述视角(第一人称/第二人称/第三人称)
+- 互动方式:对观众的提问、召回、引导
+
+# 输出json结构
+[
+  {
+    "id": "从1开始的自增序列",
+    "名称": "单一名词或短语(简洁准确)",
+    "描述": "说明形式的本质是什么,以及在视频中的具体定义",
+    "维度": {"一级": "形式", "二级": "整体形式"},
+    "支撑": {
+      "具体元素": [{"id":"元素id1", "名称":"元素名称1"}, {"id":"元素id2", "名称":"元素名称2"}],
+      "具象概念": [{"id":"概念id1", "名称":"概念名称1"}, {"id":"概念id2", "名称":"概念名称2"}]
+    },
+    "推理": "在视频中的具体体现(结合镜头、节奏、结构说明)",
+    "段落来源": ["段落ID1", "段落ID2"]
+  }
+]
+```
+
+### Step 4: 共性分析
+- 统计每个形式元素在视频中的出现频次
+
+### Step 5: 支撑判断
+- 评估形式元素对灵感点/目的点/关键点的支撑关系(按维度分别评估)
+
+### Step 6: 权重计算
+- 纯代码实现,不调用LLM。动态自适应权重策略(总分100分):共性维度50分(频次40% + 覆盖段落数30% + 覆盖率30%)+ 支撑维度50分(灵感点20分 + 目的点15分 + 关键点15分)
+
+### Step 7: 筛选
+- 筛选条件:权重分>=40
+
+### Step 8: 分类
+- 按二级维度(具象概念形式/具体元素形式/整体形式)分别进行分类
+
+**输出**: `form_final_elements` (列表格式)
+
+---
+
+## 节点9: 分离结果
+
+**功能**: 将实质和形式结果分离到独立字段
+
+**执行过程**: 
+- 从state中获取`substance_final_elements`和`form_final_elements`
+- 分别存储到`实质列表`和`形式列表`字段
+
+**输出**: `实质列表`, `形式列表`
+
+---
+
+## 节点10: 结果汇总
+
+**功能**: 将所有解构结果汇总为最终的JSON结构
+
+**执行过程**: 
+- 提取视频信息(URL、标题、正文)
+- 汇总三点解构结果(灵感点、目的点、关键点)
+- 汇总选题理解结果
+- 汇总脚本理解结果(内容品类、段落列表、实质列表、形式列表)
+- 验证JSON格式
+
+**输出**: `final_result` (包含视频信息、三点解构、选题理解、脚本理解)
+
+---
+
+## 总结
+
+整个解码工作流通过10个节点完成视频的全面分析:
+1. **视频上传**: 下载并上传视频到Gemini
+2. **三点解构**: 提取灵感点、目的点、关键点
+3. **选题理解**: 整合三点分析结果,理解选题策略
+4. **脚本理解**: 段落划分、实质提取、形式提取
+5. **结果汇总**: 将所有结果组装为最终JSON结构
+
+每个节点都基于视频逐帧分析,确保提取的信息准确且全面。
+

BIN
examples/.DS_Store


+ 58 - 0
examples/run_decode_script.py

@@ -131,6 +131,64 @@ def main() -> None:
             # 调用工作流
             decode_result = workflow.invoke(decode_input)
 
+            # 检查workflow返回结果中是否包含错误
+            if isinstance(decode_result, dict):
+                # 检查是否有错误字段(支持多种错误字段名)
+                error_msg = (
+                    decode_result.get("error") or 
+                    decode_result.get("错误") or
+                    decode_result.get("workflow_error")
+                )
+                workflow_status = decode_result.get("workflow_status")
+                
+                # 如果返回了错误信息,视为失败
+                if error_msg or workflow_status == "failed" or workflow_status == "incomplete":
+                    error_msg = error_msg or "工作流执行失败"
+                    logger.error(
+                        f"[{idx}/{len(video_list)}] 处理失败: channel_content_id={channel_content_id}, error={error_msg}"
+                    )
+                    record = {
+                        "video_data": video_data,
+                        "what_deconstruction_result": None,
+                        "script_result": None,
+                        "success": False,
+                        "error": error_msg,
+                    }
+                    output_data["fail_count"] = output_data.get("fail_count", 0) + 1
+                    output_data["results"].append(record)
+                    output_data["total"] = output_data.get("total", 0) + 1
+                    save_json(output_path, output_data)
+                    continue
+                
+                # 检查结果是否为空(可能表示失败)
+                # 如果所有关键字段都为空,可能表示处理失败
+                video_info = decode_result.get("视频信息", {})
+                three_points = decode_result.get("三点解构", {})
+                topic_understanding = decode_result.get("选题理解", {})
+                script_understanding = decode_result.get("脚本理解", {})
+                
+                # 如果所有关键结果都为空,且没有明确的成功标志,视为失败
+                if (not video_info and not three_points and 
+                    not topic_understanding and not script_understanding):
+                    error_msg = "工作流执行完成,但所有结果都为空"
+                    logger.warning(
+                        f"[{idx}/{len(video_list)}] 处理结果为空: channel_content_id={channel_content_id}"
+                    )
+                    # 这里可以选择记录为失败或警告,根据业务需求决定
+                    # 暂时记录为失败
+                    record = {
+                        "video_data": video_data,
+                        "what_deconstruction_result": None,
+                        "script_result": None,
+                        "success": False,
+                        "error": error_msg,
+                    }
+                    output_data["fail_count"] = output_data.get("fail_count", 0) + 1
+                    output_data["results"].append(record)
+                    output_data["total"] = output_data.get("total", 0) + 1
+                    save_json(output_path, output_data)
+                    continue
+
             # 按照 output_demo_script.json 的格式组织结果
             # what_deconstruction_result: 包含视频信息、三点解构、选题理解
             what_deconstruction_result = {

BIN
src/components/agents/__pycache__/script_form_extraction_agent.cpython-313.pyc


+ 0 - 633
src/components/agents/comment_analysis_agent.py

@@ -1,633 +0,0 @@
-"""
-Comment Analysis Agent.
-
-评论分析Agent:从评论中提取帖子解构维度。
-"""
-
-from typing import Dict, Any, List
-import json
-
-from src.components.agents.base import BaseLLMAgent
-from src.states.what_deconstruction_state import WhatDeconstructionState
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class CommentAnalysisAgent(BaseLLMAgent):
-    """评论分析Agent
-
-    核心功能:
-    1. 过滤评论:过滤掉与帖子内容无关的评论
-    2. 判断评论与内容元素的相关性
-    3. 从相关评论中提取帖子应该解构的维度
-    4. 为后续解构提供结构化的维度数据
-
-    工作流程(两步法):
-    - 第1步:评论过滤 → 保留相关评论
-    - 第2步:相关评论 → 元素相关性判断 → 提取解构维度 → 输出结构化维度数据
-    """
-
-    def __init__(
-        self,
-        name: str = "comment_analysis_agent",
-        description: str = "从评论中提取帖子解构维度",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        # 维度提取的 system prompt
-        system_prompt = """你是一个专业的内容分析专家。你的核心任务是:从用户评论中提取出**帖子元素物理拆解所需的维度**。
-
-# 背景说明
-
-你收到的评论已经过筛选,都是与帖子内容相关、描述物理特征的评论。你的任务是从这些评论中提取解构维度。
-
-# 核心原则
-
-**只提取与元素物理拆解密切相关的维度**,这些维度必须是:
-- 可观察的物理特征和属性
-- 可量化的客观指标
-- 与元素分割、重组、复现直接相关
-- 能够指导后续元素解构和分析
-
-# 工作流程
-
-## 步骤1:分析评论关注点
-
-分析评论中提到的物理特征和客观属性:
-- 评论关注了哪些**可观察的物理特征**?
-- 评论讨论了哪些**客观属性或状态**?
-- 评论询问了哪些**具体细节**?
-
-## 步骤2:识别解构维度
-
-**维度识别的4个标准**(必须同时满足):
-1. **可观察性**:该维度必须是通过视觉、触觉等感官可以直接观察到的物理特征
-2. **可量化性**:该维度可以用具体参数、数值、状态来描述(如尺寸、颜色值、处理状态)
-3. **拆解相关性**:该维度必须与元素的物理分割、重组、复现直接相关
-4. **客观验证性**:该维度可以通过客观标准验证,不依赖个人主观判断
-
-**典型的物理拆解维度**:
-- ✅ 处理状态(如"切块状态"、"烹饪程度")
-- ✅ 色彩特征(如"颜色分布"、"色调风格")
-- ✅ 材质特征(如"表面质地"、"材料类型")
-- ✅ 尺寸规格(如"大小比例"、"形状轮廓")
-- ✅ 结构组成(如"构图方式"、"元素排列")
-- ✅ 工艺细节(如"制作工艺"、"呈现手法")
-
-**不应提取的维度**:
-- ❌ 情感类维度(如"温馨感"、"幸福感")
-- ❌ 抽象概念(如"氛围"、"风格",除非可以具体量化)
-- ❌ 主观评价(如"好看程度"、"吸引力")
-
-## 步骤3:提取并描述维度
-
-对于每个识别出的维度:
-- **维度名称**:2-6个字,简洁明确
-- **维度描述**:说明为什么要从这个维度解构,这个维度包含什么具体内容
-- **提取理由**:基于评论内容,说明为什么提取这个维度
-
-## 步骤4:输出结构化数据
-
-必须严格按照以下JSON格式输出:
-
-{
-    "deconstruction_dimensions": [
-        {
-            "dimension_name": "维度名称(2-6个字)",
-            "dimension_description": "该维度的详细说明(为什么要从这个维度解构,这个维度包含什么)",
-            "extraction_reason": "为什么从评论中提取这个维度(基于评论内容的客观分析)"
-        }
-    ]
-}
-
-# 重要提醒
-
-- 如果评论中没有涉及物理特征的内容,返回空的 deconstruction_dimensions 列表
-- 每个维度都必须是**可操作的**、**可验证的**物理特征
-- 维度要能够指导后续的图片/文本解构工作
-- 严格排除主观感受类、情感类维度
-"""
-
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-        # 评论过滤的 system prompt
-        self.filter_system_prompt = """你是一个专业的评论过滤专家。你的任务是:判断每条评论是否在描述帖子中肉眼可见元素的具体特征。
-
-# 核心标准(唯一判断标准)
-
-**只保留描述帖子中肉眼可见元素具体特征的评论**
-
-评论必须**同时满足**以下条件才能保留:
-1. **可见性**:评论描述的内容必须在帖子中肉眼可见(文字、图片、视频等)
-2. **客观性**:评论是在描述"看到的具体内容",而不是"联想、想法、感受、分类"
-3. **物理性**:评论涉及元素的**具体物理特征**(颜色、大小、形状、材质、状态、构成、细节等)
-4. **描述性**:评论是在**描述特征**,而不是在**讨论分类、质疑标签、表达观点**
-
-# 判断方法
-
-**核心问题:评论是在描述"帖子具体内容的特征",还是在"讨论其他问题"?**
-
-**判断框架**:
-
-1. **确认可见性**
-   - 评论提到的内容在帖子中能看到吗?
-
-2. **识别表达对象**
-   - 评论是在描述"帖子中元素的具体特征"吗?
-
-3. **判断表达性质**
-   - 评论是在客观描述特征,还是在讨论分类/标签/观点?
-
-4. **检查物理特征**
-   - 评论是否包含可观察的物理特征描述?
-
-# 重点区分:描述特征 vs 讨论分类
-
-**❌ 过滤掉(只讨论分类/标签/观点)**:
-- "咋不是表情包" → 只在质疑分类,未描述任何特征
-- "这是搞笑类的" → 只在讨论类别,未描述内容
-- "应该打上XX标签" → 只在讨论标签,未描述特征
-- "这算XX风格吗?" → 只在讨论风格定义,未描述具体内容
-
-**✅ 保留(描述具体特征)**:
-- "表情包中的猫咪眼睛圆圆的,很可爱" → 描述了猫咪的具体特征
-- "图片是上下对比的形式" → 描述了图片的构成结构
-- "这个芒果是切成块的" → 描述了芒果的物理状态
-- "色调偏暖,橘色为主" → 描述了色彩特征
-
-# 输出格式
-
-必须严格按照以下JSON格式输出:
-
-{
-    "filtered_comments": [
-        {
-            "content": "评论原文",
-            "is_relevant": true/false,
-            "reason": "理由(5-10字精准表达)"
-        }
-    ],
-    "summary": {
-        "total": 10,
-        "relevant": 7,
-        "filtered_out": 3
-    }
-}
-
-# 重要提醒
-
-- **描述特征优先**:只保留描述元素具体特征的评论
-- **排除分类讨论**:过滤仅讨论分类、标签、类别的评论(如"咋不是表情包")
-- **排除情感表达**:过滤所有情感、共鸣、联想、个人经历等内容
-- **排除主观判断**:过滤主观评价、观点表达、建议等内容
-- **物理特征为准**:只保留包含物理特征(颜色、大小、形状、材质、状态、构成等)的评论
-- **精简理由**:理由控制在5-10字,精准表达核心判断依据
-
-**典型过滤场景**:
-- "咋不是XX分类" → 过滤(质疑分类,非描述特征)
-- "太XX了" → 过滤(情感表达,非物理描述)
-- "我也有同感" → 过滤(主观共鸣,非内容描述)
-- "应该XX" → 过滤(观点建议,非客观描述)
-"""
-
-    def process(self, state: WhatDeconstructionState) -> Dict[str, Any]:
-        return {"deconstruction_dimensions": [], "filtered_comments": []}
-        """处理流程(重写父类方法)
-
-        两步处理:
-        1. 过滤评论
-        2. 提取维度
-        """
-        try:
-            # 第1步:过滤评论
-            logger.info(f"\n{'#'*60}")
-            logger.info("第1步:开始过滤评论")
-            logger.info(f"{'#'*60}")
-            filtered_comments = self._filter_comments(state)
-
-            if not filtered_comments:
-                logger.warning("\n⚠️  没有相关评论,跳过维度提取\n")
-                return {"deconstruction_dimensions": [], "filtered_comments": []}
-
-            # 第2步:提取维度(使用过滤后的评论)
-            logger.info(f"\n{'#'*60}")
-            logger.info("第2步:开始提取解构维度")
-            logger.info(f"{'#'*60}\n")
-
-            # 确保已初始化
-            if not self.is_initialized:
-                self.initialize()
-
-            # 临时修改 state 中的评论为过滤后的评论
-            original_comments = state.get("comments", [])
-            state["comments"] = filtered_comments
-
-            # 构建消息
-            messages = self._build_extraction_messages(state)
-
-            # 调用 LLM
-            response = self.model.invoke(messages)
-
-            # 更新状态
-            result = self._update_state(state, response)
-
-            # 恢复原始评论
-            state["comments"] = original_comments
-
-            # 添加过滤后的评论到返回结果
-            result["filtered_comments"] = filtered_comments
-
-            return result
-
-        except Exception as e:
-            logger.error(f"评论分析处理失败: {e}", exc_info=True)
-            return {"deconstruction_dimensions": [], "filtered_comments": []}
-
-    def _filter_comments(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """过滤评论:只保留与帖子内容相关的评论
-
-        支持分批处理大量评论,避免LLM输出长度限制
-
-        Returns:
-            List[Dict]: 过滤后的相关评论列表
-        """
-        try:
-            comments = state.get("comments", [])
-
-            if not comments:
-                logger.info("没有评论需要过滤")
-                return []
-
-            # 调试日志:打印传入的评论数量
-            total_input = len(comments)
-            sub_input = sum(len(c.get("sub_comments", [])) for c in comments if isinstance(c, dict))
-            logger.info(f"开始过滤评论 - 主评论: {total_input}条, 子评论: {sub_input}条, 总计: {total_input + sub_input}条")
-
-            # 确保已初始化
-            if not self.is_initialized:
-                self.initialize()
-
-            # 分批处理评论(每批最多30条主评论)
-            batch_size = 30
-            all_relevant_comments = []
-            all_filtered_out_comments = []
-            total_processed = 0
-
-            for batch_idx in range(0, len(comments), batch_size):
-                batch_comments = comments[batch_idx:batch_idx + batch_size]
-                batch_num = batch_idx // batch_size + 1
-                total_batches = (len(comments) + batch_size - 1) // batch_size
-
-                logger.info(f"\n处理第 {batch_num}/{total_batches} 批评论({len(batch_comments)}条主评论)")
-
-                # 临时修改state中的评论为当前批次
-                batch_state = state.copy()
-                batch_state["comments"] = batch_comments
-
-                # 构建过滤消息
-                messages = self._build_filter_messages(batch_state)
-
-                # 调用 LLM 进行过滤
-                response = self.model.invoke(messages)
-
-                # 解析响应
-                filtered_result = self._parse_filter_response(response)
-
-                # 分类评论
-                for comment in filtered_result.get("filtered_comments", []):
-                    if comment.get("is_relevant", False):
-                        all_relevant_comments.append(comment)
-                    else:
-                        all_filtered_out_comments.append(comment)
-
-                total_processed += len(filtered_result.get("filtered_comments", []))
-
-            # 打印详细的过滤结果
-            logger.info(f"\n{'='*60}")
-            logger.info(f"评论过滤完成 - 总数: {total_input + sub_input}, 相关: {len(all_relevant_comments)}, 过滤: {len(all_filtered_out_comments)}")
-            logger.info(f"{'='*60}")
-
-            # 打印相关评论详情
-            if all_relevant_comments:
-                logger.info(f"\n✅ 保留的相关评论 ({len(all_relevant_comments)}条):")
-                for idx, comment in enumerate(all_relevant_comments, 1):
-                    logger.info(f"  [{idx}] {comment.get('content', '')}")
-                    logger.info(f"      理由: {comment.get('reason', '')}")
-
-            # 打印被过滤的评论详情
-            if all_filtered_out_comments:
-                logger.info(f"\n❌ 被过滤的无关评论 ({len(all_filtered_out_comments)}条):")
-                for idx, comment in enumerate(all_filtered_out_comments, 1):
-                    logger.info(f"  [{idx}] {comment.get('content', '')}")
-                    logger.info(f"      理由: {comment.get('reason', '')}")
-
-            logger.info(f"\n{'='*60}\n")
-
-            return all_relevant_comments
-
-        except Exception as e:
-            logger.error(f"评论过滤失败: {e}, 返回所有评论", exc_info=True)
-            # 失败时返回所有评论
-            return state.get("comments", [])
-
-    def _parse_filter_response(self, response: Any) -> Dict[str, Any]:
-        """解析评论过滤响应"""
-        try:
-            # 提取响应内容
-            if hasattr(response, 'content'):
-                content = response.content
-            else:
-                content = str(response)
-
-            # 尝试提取JSON
-            content = content.strip()
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-
-            if "filtered_comments" not in result:
-                raise ValueError("响应缺少filtered_comments字段")
-
-            return result
-
-        except Exception as e:
-            logger.error(f"解析过滤响应失败: {e}")
-            logger.error(f"原始响应内容: {response}")
-            return {"filtered_comments": [], "summary": {"total": 0, "relevant": 0, "filtered_out": 0}}
-
-    def _build_filter_messages(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """构建评论过滤的消息(第1步)
-
-        包含帖子内容(文本+图片)和所有评论
-        """
-        messages = [
-            {"role": "system", "content": self.filter_system_prompt}
-        ]
-
-        # 构建用户消息内容列表(支持多模态)
-        content_parts = []
-
-        # 添加帖子文本内容
-        user_prompt = "# 帖子内容\n\n"
-
-        text_data = state.get("text", {})
-        if text_data.get("title"):
-            user_prompt += f"**标题**:{text_data['title']}\n\n"
-
-        if text_data.get("body"):
-            body = text_data['body']
-            user_prompt += f"**正文**:{body}\n\n"
-
-        if text_data.get("hashtags"):
-            hashtags_str = " ".join(text_data["hashtags"])
-            user_prompt += f"**话题标签**:{hashtags_str}\n\n"
-
-        # 添加图片
-        images = state.get("images", [])
-        if images:
-            user_prompt += f"**图片**:帖子包含 {len(images)} 张图片,如下所示:\n\n"
-
-        # 添加文本部分
-        content_parts.append({
-            "type": "text",
-            "text": user_prompt
-        })
-
-        # 添加图片(多模态)
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                content_parts.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-                # 在每张图片后添加标注
-                content_parts.append({
-                    "type": "text",
-                    "text": f"↑ 第{idx}张图片\n\n"
-                })
-
-        # 添加评论内容
-        comment_text = self._format_comments(state)
-        content_parts.append({
-            "type": "text",
-            "text": comment_text
-        })
-
-        # 构建多模态消息
-        messages.append({
-            "role": "user",
-            "content": content_parts
-        })
-
-        return messages
-
-    def _build_extraction_messages(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """构建维度提取的消息(第2步)
-
-        包含帖子内容(文本+图片)和过滤后的评论
-        """
-        messages = [
-            {"role": "system", "content": self.system_prompt}
-        ]
-
-        # 构建用户消息内容列表(支持多模态)
-        content_parts = []
-
-        # 添加帖子文本内容
-        user_prompt = "# 帖子内容\n\n"
-
-        text_data = state.get("text", {})
-        if text_data.get("title"):
-            user_prompt += f"**标题**:{text_data['title']}\n\n"
-
-        if text_data.get("body"):
-            body = text_data['body']
-            user_prompt += f"**正文**:{body}\n\n"
-
-        if text_data.get("hashtags"):
-            hashtags_str = " ".join(text_data["hashtags"])
-            user_prompt += f"**话题标签**:{hashtags_str}\n\n"
-
-        # 添加图片
-        images = state.get("images", [])
-        if images:
-            user_prompt += f"**图片**:帖子包含 {len(images)} 张图片,如下所示:\n\n"
-
-        # 添加文本部分
-        content_parts.append({
-            "type": "text",
-            "text": user_prompt
-        })
-
-        # 添加图片(多模态)
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                content_parts.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-                # 在每张图片后添加标注
-                content_parts.append({
-                    "type": "text",
-                    "text": f"↑ 第{idx}张图片\n\n"
-                })
-
-        # 添加评论内容(已过滤的相关评论)
-        comment_text = self._format_comments(state)
-        comment_text += "\n\n# 任务说明\n\n"
-        comment_text += "以上评论已经过筛选,都是与帖子内容相关、描述物理特征的评论。\n\n"
-        comment_text += "**你的任务**:从这些评论中提取帖子元素物理拆解所需的维度\n\n"
-        comment_text += "**要求**:\n"
-        comment_text += "- 只提取与元素物理拆解相关的维度(如:处理状态、色彩参数、材质特征、尺寸规格、结构组成、工艺细节等)\n"
-        comment_text += "- 完全排除主观感受类、情感类维度\n"
-        comment_text += "- 严格按照JSON格式输出,包含所有必需字段\n"
-        comment_text += "- 如果评论中没有涉及物理特征的内容,返回空的 deconstruction_dimensions 列表"
-
-        content_parts.append({
-            "type": "text",
-            "text": comment_text
-        })
-
-        # 构建多模态消息
-        messages.append({
-            "role": "user",
-            "content": content_parts
-        })
-
-        return messages
-
-    def _format_comments(self, state: WhatDeconstructionState) -> str:
-        """格式化评论文本"""
-        comment_text = ""
-        comments = state.get("comments", [])
-
-        if not comments:
-            comment_text += "\n# 评论区\n\n(暂无评论)\n"
-            return comment_text
-
-        # 计算总评论数
-        total_count = len(comments)
-        for comment in comments:
-            if isinstance(comment, dict):
-                total_count += len(comment.get("sub_comments", []))
-
-        # 调试日志:打印实际评论数量
-        logger.info(f"格式化评论 - 主评论数: {len(comments)}, 总评论数(含子评论): {total_count}")
-
-        comment_text += f"\n# 评论区(共 {total_count} 条相关评论)\n\n"
-
-        # 遍历所有评论
-        comment_idx = 1
-        for comment_data in comments:
-            if isinstance(comment_data, dict):
-                # 处理完整的评论对象
-                user_name = comment_data.get("user_nickname") or comment_data.get("user", "匿名用户")
-                content = comment_data.get("content", "")
-
-                if content:
-                    comment_text += f"{comment_idx}. **{user_name}**:{content}\n"
-                    comment_idx += 1
-
-                # 处理子评论
-                sub_comments = comment_data.get("sub_comments", [])
-                if sub_comments:
-                    for sub_comment in sub_comments:
-                        sub_user_name = sub_comment.get("user_nickname") or sub_comment.get("user", "匿名用户")
-                        sub_content = sub_comment.get("content", "")
-
-                        if sub_content:
-                            comment_text += f"  {comment_idx}. **{sub_user_name}**(回复):{sub_content}\n"
-                            comment_idx += 1
-            else:
-                # 处理简单的评论内容
-                comment_text += f"{comment_idx}. {comment_data}\n"
-                comment_idx += 1
-
-        logger.info(f"实际格式化的评论数: {comment_idx - 1}")
-        return comment_text
-
-    def _build_messages(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """构建消息(保留以兼容父类,实际使用 _build_extraction_messages)"""
-        return self._build_extraction_messages(state)
-
-    def _update_state(self, state: WhatDeconstructionState, response: Any) -> Dict[str, Any]:
-        """更新状态
-
-        解析LLM返回的JSON,更新解构维度数据
-        """
-        try:
-            # 提取响应内容
-            if hasattr(response, 'content'):
-                content = response.content
-            else:
-                content = str(response)
-
-            # 尝试提取JSON
-            content = content.strip()
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-
-            # 验证必需字段
-            if "deconstruction_dimensions" not in result:
-                raise ValueError("响应缺少deconstruction_dimensions字段")
-
-            dimensions = result["deconstruction_dimensions"]
-
-            # 验证每个维度的必需字段
-            required_fields = [
-                "dimension_name",
-                "dimension_description",
-                "extraction_reason"
-            ]
-
-            for idx, dimension in enumerate(dimensions):
-                for field in required_fields:
-                    if field not in dimension:
-                        logger.warning(f"维度 {idx} 缺少字段: {field}")
-
-            # 打印详细的维度提取结果
-            logger.info(f"\n{'='*60}")
-            logger.info(f"维度提取完成 - 共提取 {len(dimensions)} 个解构维度")
-            logger.info(f"{'='*60}")
-
-            if dimensions:
-                for idx, dimension in enumerate(dimensions, 1):
-                    logger.info(f"\n📊 维度 [{idx}]:{dimension.get('dimension_name', '未命名')}")
-                    logger.info(f"{'─'*60}")
-                    logger.info(f"\n维度描述: {dimension.get('dimension_description', '')}")
-                    logger.info(f"\n提取理由: {dimension.get('extraction_reason', '')}")
-                    logger.info(f"{'─'*60}")
-            else:
-                logger.info("\n⚠️  未提取到任何解构维度(评论中可能没有涉及物理特征的内容)")
-
-            logger.info(f"\n{'='*60}\n")
-
-            # 返回更新后的字段
-            return {
-                "deconstruction_dimensions": dimensions
-            }
-
-        except Exception as e:
-            logger.error(f"解析评论分析响应失败: {e},原始内容: {response}")
-            # 返回默认值
-            return {
-                "deconstruction_dimensions": []
-            }

+ 0 - 344
src/components/agents/content_filter_agent.py

@@ -1,344 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-内容过滤Agent
-
-功能:过滤bodytext中与帖子主内容不相关的话题标签,返回过滤后的bodytext
-"""
-
-from typing import Any, Dict, List
-import re
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-
-class ContentFilterAgent(BaseLLMAgent):
-    """内容过滤Agent - 过滤bodytext中不相关的话题标签
-
-    职责:
-    - 分析帖子的主要内容(图片、标题、正文bodytext)
-    - 识别bodytext中的话题标签
-    - 判断每个话题标签是否与主内容相关
-    - 移除不相关的标签,返回过滤后的bodytext
-    """
-
-    def __init__(
-        self,
-        name: str = "content_filter_agent",
-        description: str = "内容过滤Agent - 过滤bodytext中不相关的话题标签",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 20480
-    ):
-        """初始化内容过滤Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商
-            temperature: 生成温度(较低以确保准确判断)
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是内容过滤专家,从bodytext中识别并移除与帖子主内容不相关的话题标签。
-
-# 核心原则
-
-1. **显性匹配**:标签必须直接描述帖子中明确展示的内容(不能基于隐喻、联想、推理)
-2. **主题相关**:标签必须与帖子核心主题有关(而非仅描述表现形式)
-3. **严格过滤**:有疑问时倾向于移除
-
-# 判断方法
-
-对每个标签进行双重验证,两项都通过才保留:
-
-**A. 显性匹配检查**
-- 标签描述的主体与帖子展示的主体字面意义一致?
-- 不需要任何推理就能直接证明标签?
-- 避免使用"体现"、"符合"、"象征"、"代表"、"反映"等推理词汇
-
-**B. 主题相关性检查**
-- 标签描述的是主题内容本身(讲什么、做什么、说什么)?
-- 还是仅描述表现形式(呈现方式、风格、格式)?
-- 主题内容 → 相关;仅表现形式 → 不相关
-
-**最终判断**:A通过 且 B通过 → 保留;否则 → 移除
-
-# 输出要求
-
-从bodytext移除不相关标签,保持其他内容和格式不变,给出简洁明确的理由。"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 过滤bodytext中的话题标签
-
-        输入state包含:
-        - text: {title, body}
-        - images: 图片URL列表
-
-        输出:
-        {
-            "filtered_body": "过滤后的bodytext",
-            "original_body": "原始bodytext",
-            "removed_hashtags": ["被移除的标签1", "被移除的标签2", ...],
-            "kept_hashtags": ["保留的标签1", "保留的标签2", ...],
-            "filter_details": [
-                {
-                    "hashtag": "标签",
-                    "is_relevant": true/false,
-                    "reason": "判断理由"
-                },
-                ...
-            ]
-        }
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("内容过滤 (ContentFilterAgent)")
-        logger.info("=" * 80)
-
-        try:
-            text_data = state.get("text", {})
-            images = state.get("images", [])
-
-            # 获取原始bodytext
-            original_body = text_data.get("body", "")
-
-            if not original_body:
-                logger.info("无bodytext内容,跳过过滤")
-                return {
-                    "filtered_body": "",
-                    "original_body": "",
-                    "removed_hashtags": [],
-                    "kept_hashtags": [],
-                    "filter_details": []
-                }
-
-            logger.info(f"原始bodytext长度: {len(original_body)}")
-
-            # 提取bodytext中的所有标签
-            hashtags = self._extract_hashtags(original_body)
-            logger.info(f"检测到 {len(hashtags)} 个标签: {hashtags}")
-
-            if not hashtags:
-                logger.info("bodytext中未检测到标签,无需过滤")
-                return {
-                    "filtered_body": original_body,
-                    "original_body": original_body,
-                    "removed_hashtags": [],
-                    "kept_hashtags": [],
-                    "filter_details": []
-                }
-
-            # 调用LLM进行标签过滤
-            filter_result = self._filter_content(
-                text_data,
-                images,
-                original_body,
-                hashtags
-            )
-
-            # 提取过滤后的bodytext
-            filtered_body = filter_result.get("filtered_body", original_body)
-            filter_details = filter_result.get("filter_details", [])
-
-            # 统计被保留和移除的标签
-            kept_hashtags = [
-                detail["hashtag"]
-                for detail in filter_details
-                if detail.get("is_relevant", False)
-            ]
-            removed_hashtags = [
-                detail["hashtag"]
-                for detail in filter_details
-                if not detail.get("is_relevant", False)
-            ]
-
-            logger.info(f"过滤后bodytext长度: {len(filtered_body)}")
-            logger.info(f"保留的标签 ({len(kept_hashtags)}): {kept_hashtags}")
-            logger.info(f"移除的标签 ({len(removed_hashtags)}): {removed_hashtags}")
-
-            # 输出详细过滤结果
-            if filter_details:
-                logger.info("\n过滤详情:")
-                for detail in filter_details:
-                    status = "✓ 保留" if detail.get("is_relevant") else "✗ 移除"
-                    logger.info(f"  {status} - {detail.get('hashtag')}: {detail.get('reason')}")
-
-            logger.info(f"过滤后的bodytext: {filtered_body}")
-            
-            result = {
-                "filtered_body": filtered_body,
-                "original_body": original_body,
-                "removed_hashtags": removed_hashtags,
-                "kept_hashtags": kept_hashtags,
-                "filter_details": filter_details
-            }
-
-            logger.info("=" * 80)
-
-            return result
-
-        except Exception as e:
-            logger.error(f"内容过滤失败: {e}", exc_info=True)
-            # 失败时返回原始内容
-            return {
-                "filtered_body": text_data.get("body", ""),
-                "original_body": text_data.get("body", ""),
-                "removed_hashtags": [],
-                "kept_hashtags": [],
-                "filter_details": [],
-                "error": str(e)
-            }
-
-    def _extract_hashtags(self, text: str) -> List[str]:
-        """从文本中提取所有标签
-
-        Args:
-            text: 文本内容
-
-        Returns:
-            标签列表(去重)
-        """
-        # 匹配 #标签 格式(支持中英文、数字、下划线)
-        pattern = r'#([A-Za-z0-9_\u4e00-\u9fa5]+)'
-        hashtags = re.findall(pattern, text)
-
-        # 去重并保持顺序
-        seen = set()
-        unique_hashtags = []
-        for tag in hashtags:
-            if tag not in seen:
-                seen.add(tag)
-                unique_hashtags.append(f"#{tag}")
-
-        return unique_hashtags
-
-    def _filter_content(
-        self,
-        text_data: Dict[str, Any],
-        images: List[str],
-        original_body: str,
-        hashtags: List[str]
-    ) -> Dict[str, Any]:
-        """执行内容过滤
-
-        Args:
-            text_data: 文本数据(包含title、body)
-            images: 图片URL列表
-            original_body: 原始bodytext
-            hashtags: 提取的标签列表
-
-        Returns:
-            过滤结果,包含filtered_body和filter_details
-        """
-        # 构建帖子内容摘要
-        content_parts = []
-
-        if text_data.get("title"):
-            content_parts.append(f"**标题**: {text_data['title']}")
-
-        # 只展示去除标签后的正文核心内容
-        body_without_tags = original_body
-        for tag in hashtags:
-            body_without_tags = body_without_tags.replace(tag, "").strip()
-
-        if body_without_tags:
-            content_parts.append(f"**正文**: {body_without_tags}")
-
-        content_summary = "\n".join(content_parts) if content_parts else "无文本内容"
-
-        # 构建标签列表
-        hashtags_text = "\n".join([f"{i+1}. {tag}" for i, tag in enumerate(hashtags)])
-
-        # 构建prompt(明确区分原始和纯净内容)
-        prompt = f"""# 帖子内容(不含标签)
-
-{content_summary}
-
-{'(包含 ' + str(len(images)) + ' 张图片)' if images else '(无图片)'}
-
-# 待判断的标签
-
-{hashtags_text}
-
-# 原始bodytext(含标签)
-
-{original_body}
-
-# 任务
-
-1. 基于"帖子内容(不含标签)"判断每个标签是否与内容相关
-2. 从"原始bodytext"中移除不相关的标签,生成过滤后的bodytext
-
-# 输出(JSON)
-
-{{
-  "filter_details": [
-    {{"hashtag": "#标签", "is_relevant": true/false, "reason": "简洁理由"}}
-  ],
-  "filtered_body": "移除不相关标签后的bodytext"
-}}"""
-
-        # 构建多模态消息
-        message_content = [{"type": "text", "text": prompt}]
-
-        # 添加图片(用于更准确的判断)
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                message_content.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": message_content}
-        ]
-
-        # 调用LLM
-        result = LLMInvoker.safe_invoke(
-            self,
-            "内容过滤",
-            messages,
-            fallback={
-                "filter_details": [
-                    {"hashtag": tag, "is_relevant": True, "reason": "过滤失败,保留原标签"}
-                    for tag in hashtags
-                ],
-                "filtered_body": original_body
-            }
-        )
-
-        return result
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - ContentFilterAgent 不使用此方法
-
-        本 Agent 在 process 方法中直接构建消息
-        """
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - ContentFilterAgent 不使用此方法
-
-        本 Agent 在 process 方法中直接返回结果
-        """
-        return state

+ 0 - 320
src/components/agents/content_weight_agent.py

@@ -1,320 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-内容权重判断Agent
-
-功能:判断帖子中图片和文字哪个权重更高,为后续灵感点提取提供指导
-"""
-
-from typing import Any, Dict, List
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-
-class ContentWeightAgent(BaseLLMAgent):
-    """内容权重判断Agent - 判断图片和文字的权重分配及图文关系
-
-    输出格式:
-    {
-        "image_weight": 0-10的整数(图片权重分数),
-        "text_weight": 0-10的整数(文字权重分数),
-        "weight_difference": "明显图片为主" | "明显文字为主" | "权重相近",
-        "primary_source": "image" | "text" | "both",
-        "content_relationship": "相关" | "不相关",
-        "analysis": "详细的权重判断说明"
-    }
-    """
-
-    def __init__(
-        self,
-        name: str = "content_weight_agent",
-        description: str = "内容权重判断Agent - 判断图片和文字的权重分配",
-        model_provider: str = "google_genai",
-        temperature: float = 0.3,  # 较低温度,要求客观判断
-        max_tokens: int = 20480
-    ):
-        """
-        初始化内容权重判断Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商
-            temperature: 生成温度(较低,保证客观性)
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是内容权重分析专家,专门判断帖子中图片和文字哪个权重更高,以及图文之间的关系。
-
-# 核心任务
-1. 从创作者视角分析:这个帖子的核心信息承载在图片还是文字?
-2. 判断图文关系:图片和文字是相关的还是不相关的?
-
-# 判断维度
-
-## 1. 信息量维度(0-10分)
-- **图片信息量**:图片承载了多少独特信息?
-  - 纯展示性图片(如单纯的产品图、自拍):2-4分
-  - 场景丰富、细节多的图片(如旅游风景、复杂场景):5-7分
-  - 信息密集型图片(如图表、对比图、教程图):8-10分
-
-- **文字信息量**:文字承载了多少独特信息?
-  - 简短标题或标签:2-4分
-  - 中等长度描述性文字:5-7分
-  - 详细的说明、故事、教程:8-10分
-
-## 2. 独立理解性(0-10分)
-- **图片独立性**:不看文字,仅看图片能理解多少内容?
-  - 完全无法理解主题:0-3分
-  - 能理解部分内容但缺少关键信息:4-6分
-  - 能完整理解核心内容:7-10分
-
-- **文字独立性**:不看图片,仅看文字能理解多少内容?
-  - 完全无法理解主题:0-3分
-  - 能理解部分内容但缺少关键信息:4-6分
-  - 能完整理解核心内容:7-10分
-
-## 3. 核心价值承载(0-10分)
-- **图片核心价值**:帖子的核心吸引力、核心观点、核心创意是否体现在图片中?
-  - 图片只是装饰:0-3分
-  - 图片提供重要支持但非核心:4-6分
-  - 图片承载核心价值:7-10分
-
-- **文字核心价值**:帖子的核心吸引力、核心观点、核心创意是否体现在文字中?
-  - 文字只是描述:0-3分
-  - 文字提供重要支持但非核心:4-6分
-  - 文字承载核心价值:7-10分
-
-# 评分计算
-- **图片权重** = (图片信息量 + 图片独立性 + 图片核心价值) / 3(四舍五入到整数)
-- **文字权重** = (文字信息量 + 文字独立性 + 文字核心价值) / 3(四舍五入到整数)
-
-# 权重差异判定
-- **明显图片为主**:图片权重 - 文字权重 >= 3
-- **明显文字为主**:文字权重 - 图片权重 >= 3
-- **权重相近**:|图片权重 - 文字权重| < 3
-
-# 主要信息源判定
-- **image**(图片为主):图片权重 > 文字权重 + 2
-- **text**(文字为主):文字权重 > 图片权重 + 2
-- **both**(都重要):|图片权重 - 文字权重| <= 2
-
-# 图文关系判定
-判断图片和文字之间是"相关"还是"不相关":
-
-## 相关的判定标准
-- 图文描述同一主题/对象
-- 图文相互补充、相互解释
-- 文字是对图片的说明,或图片是对文字的展示
-
-## 不相关的判定标准
-- 图文描述不同主题/对象
-- 图片和文字分别承载独立的信息,没有相互解释的关系
-
-# 分析原则
-- 客观评估,基于实际内容而非主观偏好
-- 考虑创作者意图:TA想通过图片还是文字传达核心信息?
-- 考虑受众视角:用户主要从图片还是文字获取价值?
-- 仔细判断图文关系:不能仅凭权重,还要看图文是否相关"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 判断图片和文字权重
-
-        Args:
-            state: 包含 text(文字数据)和 images(图片URL列表)
-
-        Returns:
-            权重判断结果
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("内容权重判断 (ContentWeightAgent)")
-        logger.info("=" * 80)
-
-        try:
-            text_data = state.get("text", {})
-            images = state.get("images", [])
-
-            # 构建提示词
-            prompt = self._build_weight_judgment_prompt(text_data, images)
-
-            # 构建多模态消息
-            message_content = [{"type": "text", "text": prompt}]
-
-            # 添加图片
-            if images:
-                for idx, image_path in enumerate(images, 1):
-                    message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                    message_content.append({
-                        "type": "image_url",
-                        "image_url": {"url": image_path}
-                    })
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": message_content}
-            ]
-
-            # 调用LLM
-            result = LLMInvoker.safe_invoke(
-                self,
-                "内容权重判断",
-                messages,
-                fallback={
-                    "image_weight": 5,
-                    "text_weight": 5,
-                    "weight_difference": "权重相近",
-                    "primary_source": "both",
-                    "content_relationship": "相关",
-                    "analysis": "权重判断失败,默认图片和文字同等重要,图文相关"
-                }
-            )
-
-            logger.info(f"权重判断结果: 图片={result.get('image_weight', 0)}, "
-                       f"文字={result.get('text_weight', 0)}, "
-                       f"主要信息源={result.get('primary_source', 'both')}")
-            logger.info(f"权重差异: {result.get('weight_difference', '未知')}")
-            logger.info(f"图文关系: {result.get('content_relationship', '未知')}")
-            logger.info(f"分析说明: {result.get('analysis', '')}")
-            logger.info("=" * 80)
-
-            return {
-                "content_weight": result
-            }
-
-        except Exception as e:
-            logger.error(f"内容权重判断失败: {e}", exc_info=True)
-            return {
-                "content_weight": {
-                    "image_weight": 5,
-                    "text_weight": 5,
-                    "weight_difference": "权重相近",
-                    "primary_source": "both",
-                    "content_relationship": "相关",
-                    "analysis": f"权重判断失败: {str(e)},默认图片和文字同等重要,图文相关",
-                    "error": str(e)
-                }
-            }
-
-    def _build_weight_judgment_prompt(
-        self,
-        text_data: Dict[str, Any],
-        images: List[str]
-    ) -> str:
-        """构建权重判断提示词"""
-
-        # 构建文字摘要
-        text_parts = []
-        if text_data.get("title"):
-            text_parts.append(f"标题: {text_data['title']}")
-        if text_data.get("body"):
-            text_parts.append(f"正文: {text_data['body']}")
-        if text_data.get("hashtags"):
-            text_parts.append(f"话题标签: {' '.join(text_data['hashtags'])}")
-
-        text_summary = "\n".join(text_parts) if text_parts else "无文字内容"
-
-        # 图片信息
-        image_info = f"图片数量: {len(images)}张" if images else "无图片"
-
-        prompt = f"""# 任务:判断图片和文字的权重
-
-## 帖子内容
-
-### 文字内容
-{text_summary}
-
-### 图片内容
-{image_info}
-{'(图片将在后续消息中提供)' if images else ''}
-
-## 分析要求
-
-请按照以下步骤进行分析:
-
-### 第一步:分维度评分(每个维度0-10分)
-
-**图片评分**:
-1. 信息量(0-10):图片承载了多少独特信息?
-2. 独立理解性(0-10):不看文字,仅看图片能理解多少内容?
-3. 核心价值承载(0-10):帖子的核心吸引力、核心观点、核心创意是否体现在图片中?
-
-**文字评分**:
-1. 信息量(0-10):文字承载了多少独特信息?
-2. 独立理解性(0-10):不看图片,仅看文字能理解多少内容?
-3. 核心价值承载(0-10):帖子的核心吸引力、核心观点、核心创意是否体现在文字中?
-
-### 第二步:计算权重
-- **图片权重** = (图片信息量 + 图片独立性 + 图片核心价值) / 3(四舍五入)
-- **文字权重** = (文字信息量 + 文字独立性 + 文字核心价值) / 3(四舍五入)
-
-### 第三步:判定权重差异
-- 图片权重 - 文字权重 >= 3 → "明显图片为主"
-- 文字权重 - 图片权重 >= 3 → "明显文字为主"
-- |图片权重 - 文字权重| < 3 → "权重相近"
-
-### 第四步:确定主要信息源
-- 图片权重 > 文字权重 + 2 → "image"
-- 文字权重 > 图片权重 + 2 → "text"
-- 其他情况 → "both"
-
-### 第五步:判断图文关系
-判断图片和文字是"相关"还是"不相关":
-- **相关**:图文描述同一主题/对象,相互补充解释
-- **不相关**:图文描述不同主题,或图片是成果展示而文字是过程说明
-
-## 输出格式(JSON)
-```json
-{{
-  "dimension_scores": {{
-    "image": {{
-      "information_volume": 8,
-      "independence": 7,
-      "core_value": 9
-    }},
-    "text": {{
-      "information_volume": 5,
-      "independence": 6,
-      "core_value": 4
-    }}
-  }},
-  "image_weight": 8,
-  "text_weight": 5,
-  "weight_difference": "明显图片为主",
-  "primary_source": "image",
-  "content_relationship": "相关",
-  "analysis": "详细说明为什么得出这个判断,包括:\\n1. 图片的信息承载情况\\n2. 文字的信息承载情况\\n3. 两者的配合关系\\n4. 核心信息主要来源\\n5. 图文关系判断依据"
-}}
-```
-
-**重要**:
-- 必须客观评分,基于实际内容
-- analysis字段必须详细说明判断理由
-- 所有评分都是0-10的整数"""
-
-        return prompt
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - ContentWeightAgent 在 process 方法中直接构建消息"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - ContentWeightAgent 在 process 方法中直接返回结果"""
-        return state

+ 0 - 802
src/components/agents/knowledge_requirement_agent.py

@@ -1,802 +0,0 @@
-"""知识需求生成Agent
-
-KnowledgeRequirementGenerationAgent - 根据输入生成知识需求文档并获取对应知识
-
-功能:
-- 分析PRD文档、小红书帖子、解构上下文等输入
-- 生成结构化的知识需求清单
-- 调用知识检索工具获取每项需求对应的知识
-- 输出markdown格式的知识需求文档
-
-实现方式: Agent组件(需要LLM推理、语义分析、动态决策)
-依据: What解构业务需要动态获取知识来指导解构过程
-"""
-
-from typing import Dict, Any, List, Optional
-from pydantic import BaseModel, Field
-from langchain_core.prompts import ChatPromptTemplate
-from langchain_core.language_models import BaseChatModel
-
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class KnowledgeRequirementInput(BaseModel):
-    """知识需求生成Agent输入Schema"""
-
-    prd_content: Optional[str] = Field(
-        default=None,
-        description="PRD文档内容,用于理解业务需求和解构目标"
-    )
-
-    post_content: Optional[Dict[str, Any]] = Field(
-        default=None,
-        description="小红书帖子内容,包含images、text、comments等"
-    )
-
-    deconstruction_context: Optional[Dict[str, Any]] = Field(
-        default=None,
-        description="解构过程中产生的上下文信息,如品类、主题、关键词、消费者亮点等"
-    )
-
-    current_element: Optional[Dict[str, Any]] = Field(
-        default=None,
-        description="当前正在解构的元素信息,包含what字段值、描述维度等"
-    )
-
-    knowledge_scope: Optional[List[str]] = Field(
-        default=None,
-        description="需要获取的知识范围,如['描述维度', '拆分判断', '工具推荐']"
-    )
-
-    task_stage: Optional[str] = Field(
-        default=None,
-        description="当前执行的任务阶段,例如:'帖子整体解构'、'图片元素解构'、'文本元素解构'等"
-    )
-
-
-class KnowledgeRequirementOutput(BaseModel):
-    """知识需求生成Agent输出Schema"""
-
-    markdown_document: str = Field(
-        description="Markdown格式的知识需求文档(包含完整的需求清单和上下文信息)"
-    )
-
-    summary: str = Field(
-        description="知识需求的简要总结"
-    )
-
-
-class KnowledgeRequirementGenerationAgent:
-    """知识需求生成Agent
-
-    根据多种输入源生成知识需求文档(不再调用知识检索工具)
-
-    使用场景:
-    1. What解构开始前:基于PRD和帖子内容生成初始知识需求
-    2. 解构过程中:基于当前元素生成动态知识需求
-    3. 解构特定节点:生成针对性的描述维度、拆分判断知识需求
-    """
-
-    def __init__(
-        self,
-        llm: BaseChatModel,
-        knowledge_tool: Optional[Any] = None,  # 保留参数以兼容旧代码
-        enable_retrieval: bool = False  # 默认不启用知识检索
-    ):
-        """初始化知识需求生成Agent
-
-        Args:
-            llm: 语言模型实例
-            knowledge_tool: 知识检索工具实例(已废弃,保留以兼容)
-            enable_retrieval: 是否启用知识检索(已废弃)
-        """
-        self.llm = llm
-        self.enable_retrieval = False  # 强制禁用知识检索
-
-        # 构建Prompt模板
-        self._build_prompts()
-
-    def _build_prompts(self):
-        """构建Prompt模板"""
-
-        # 知识需求分析Prompt(生成新的输出格式)
-        self.requirement_analysis_prompt = ChatPromptTemplate.from_messages([
-            ("system", """你是一个专业的知识需求分析专家,负责分析What解构任务并生成结构化的知识需求文档。
-
-## 核心任务
-
-根据输入的PRD文档、小红书帖子内容、解构上下文、当前任务阶段等信息,生成一份结构化的知识需求文档。
-
-## 知识需求分类(PRD 3.3.2节)
-
-知识需求分为两类:
-1. **内容知识**:如何进行what解构的方法、标准、维度等
-2. **工具知识**:解构过程需要用到什么工具
-
-## 输出格式要求
-
-严格按照以下Markdown格式输出:
-
-# 整体项目目标
-[从PRD文档中提取的核心目标,如果没有PRD则根据业务背景推断]
-
-# 本次任务目标
-[根据当前执行的阶段(task_stage)和待解构元素,明确本次任务的具体目标。例如:
-- 帖子整体解构:确定帖子的描述维度(品类、主题、脚本等)
-- 图片元素解构:确定图片拆解的维度、方法、是否继续分割的判断标准
-- 文本元素解构:确定文本拆解的维度、方法等]
-
-# 上下文
-[当前运行时信息,包括品类、主题、关键词、消费者亮点等一切有助于理解并找到有帮助的知识的信息]
-
-# 待解构帖子信息
-[帖子的完整内容,包括:
-- 标题:完整标题
-- 正文:完整正文内容(不截断)
-- 话题标签:所有话题标签
-- 图片:列出所有图片(图片1、图片2...)]
-
-# 需求
-## 内容知识需求
-### 需求约束
-[列出内容知识需求的约束条件,例如:
-- 必须基于小红书平台特点
-- 必须从消费者视角出发
-- 必须符合当前品类和主题]
-
-### 需求描述
-[具体描述需要什么内容知识,每项需求单独一行,格式为:
-- 需求1:具体描述(包含完整上下文:品类、主题、元素类型等)
-- 需求2:具体描述
-- ...]
-
-## 工具知识需求
-### 需求约束
-[列出工具知识需求的约束条件,例如:
-- 工具必须支持多模态内容处理
-- 工具必须能够进行精确分割
-- 工具必须适用于当前场景]
-
-### 需求描述
-[具体描述需要什么工具知识,参考PRD 3.3.2节的Query句式,每项需求单独一行,格式为:
-- 需求1:具体描述(例如:对一张"XXX图片描述"的图片,需要使用什么图片分割/抠图工具?)
-- 需求2:具体描述
-- ...]
-
-## 重要原则
-
-1. **项目目标提取**:从PRD中准确提取整体项目目标
-2. **任务目标明确**:根据task_stage明确本次任务的具体目标
-3. **上下文完整**:包含品类、主题、关键词、消费者亮点等所有关键信息
-4. **帖子信息完整**:在"待解构帖子信息"部分,必须输出输入中提供的完整帖子内容,包括:
-   - 完整的标题
-   - 完整的正文(不截断、不省略)
-   - 所有话题标签
-   - 所有图片列表
-5. **需求具体化**:每项知识需求必须包含完整的上下文信息(品类、主题、元素类型等)
-6. **避免抽象**:使用具体的品类、主题、元素名称,不要用"XXX类内容"这样的占位符
-7. **工具Query规范**:工具知识需求应遵循PRD 3.3.2节定义的Query句式
-
-## 内容知识 vs 工具知识示例
-
-**内容知识示例**:
-- "美食品类中关于'食物双标'主题的小红书帖子的描述维度框架(品类、主题、脚本、内容亮点、情绪共鸣点等)"
-- "幽默搞笑类图片中对比手法和表情包风格的识别方法和标准"
-- "图片元素是否继续分割的判断标准和停止条件"
-
-**工具知识示例**(参考PRD 3.3.2节):
-- "对一张描述为'年轻人对食物的双标表现对比图'的图片,从内容创作者视角进行视觉元素分割,需要使用哪些图片分割/抠图工具?"
-- "对一段描述为'食物双标主题的幽默文案'的文本段落,从内容创作者视角进行结构化分割拆解,需要使用什么文本切分工具或文本结构化大模型?"
-"""),
-            ("user", """{input_description}
-
-请严格按照上述格式生成知识需求文档。直接输出Markdown文本,不要添加代码块标记。
-""")
-        ])
-
-    def generate_requirements(
-        self,
-        input_data: KnowledgeRequirementInput
-    ) -> KnowledgeRequirementOutput:
-        """生成知识需求Markdown文档
-
-        Args:
-            input_data: 输入数据
-
-        Returns:
-            知识需求输出,包含Markdown文档和总结
-        """
-        logger.info("开始生成知识需求清单")
-
-        # Step 1: 构建输入描述
-        input_description = self._build_input_description(input_data)
-
-        # Step 2: 调用LLM生成Markdown格式的知识需求文档
-        markdown_doc = self._generate_requirement_list(input_description)
-
-        # Step 3: 生成总结
-        summary = self._generate_summary_from_markdown(markdown_doc)
-
-        logger.info(f"知识需求生成完成,文档长度: {len(markdown_doc)} 字符")
-
-        return KnowledgeRequirementOutput(
-            markdown_document=markdown_doc,
-            summary=summary
-        )
-
-    def _build_input_description(self, input_data: KnowledgeRequirementInput) -> str:
-        """构建输入描述文本
-
-        Args:
-            input_data: 输入数据
-
-        Returns:
-            格式化的输入描述
-        """
-        sections = []
-
-        # PRD内容
-        if input_data.prd_content:
-            sections.append(f"## PRD文档内容\n{input_data.prd_content}")
-
-        # 当前任务阶段
-        if input_data.task_stage:
-            sections.append(f"## 当前任务阶段\n{input_data.task_stage}")
-
-        # 解构上下文
-        if input_data.deconstruction_context:
-            ctx = input_data.deconstruction_context
-            ctx_desc = []
-
-            if ctx.get("category"):
-                ctx_desc.append(f"品类: {ctx['category']}")
-            if ctx.get("theme"):
-                ctx_desc.append(f"主题: {ctx['theme']}")
-            if ctx.get("keywords"):
-                ctx_desc.append(f"关键词: {ctx['keywords']}")
-            if ctx.get("consumer_highlights"):
-                ctx_desc.append(f"消费者亮点: {ctx['consumer_highlights']}")
-
-            if ctx_desc:
-                sections.append(f"## 解构上下文\n" + "\n".join(ctx_desc))
-
-        # 帖子内容(完整详细版本)
-        if input_data.post_content:
-            post_desc = []
-            post_desc.append("## 完整帖子内容")
-            post_desc.append("")
-
-            # 文本内容(完整)
-            if input_data.post_content.get("text"):
-                text_data = input_data.post_content["text"]
-                post_desc.append("### 文本内容")
-                if text_data.get('title'):
-                    post_desc.append(f"**标题**: {text_data['title']}")
-                if text_data.get('body'):
-                    # 不截断正文,显示完整内容
-                    post_desc.append(f"**正文**: {text_data['body']}")
-                if text_data.get('hashtags'):
-                    post_desc.append(f"**话题标签**: {text_data['hashtags']}")
-                post_desc.append("")
-
-            # 图片内容(列出所有图片URL)
-            if input_data.post_content.get("images"):
-                images = input_data.post_content["images"]
-                post_desc.append("### 图片内容")
-                post_desc.append(f"**图片数量**: {len(images)}张")
-                post_desc.append("**图片列表**:")
-                for i, img in enumerate(images, 1):
-                    if isinstance(img, str):
-                        post_desc.append(f"  - 图片{i}: {img}")
-                    elif isinstance(img, dict) and img.get('url'):
-                        post_desc.append(f"  - 图片{i}: {img['url']}")
-                post_desc.append("")
-
-            # 评论内容已移除 - 不在知识需求文档中包含评论信息
-
-            sections.append("\n".join(post_desc))
-
-        # 当前元素
-        if input_data.current_element:
-            elem = input_data.current_element
-            elem_desc = []
-
-            if elem.get("what"):
-                elem_desc.append(f"元素What值: {elem['what']}")
-            if elem.get("element_type"):
-                elem_desc.append(f"元素类型: {elem['element_type']}")
-            if elem.get("depth"):
-                elem_desc.append(f"当前深度: {elem['depth']}")
-
-            if elem_desc:
-                sections.append(f"## 当前解构元素\n" + "\n".join(elem_desc))
-
-        # 知识范围
-        if input_data.knowledge_scope:
-            sections.append(f"## 需要的知识范围\n" + "\n".join([f"- {scope}" for scope in input_data.knowledge_scope]))
-
-        return "\n\n".join(sections)
-
-    def _generate_requirement_list(self, input_description: str) -> str:
-        """调用LLM生成Markdown格式的知识需求文档
-
-        Args:
-            input_description: 输入描述
-
-        Returns:
-            Markdown格式的知识需求文档
-        """
-        try:
-            # 调用LLM
-            messages = self.requirement_analysis_prompt.format_messages(
-                input_description=input_description
-            )
-            response = self.llm.invoke(messages)
-
-            # 直接返回Markdown内容
-            markdown_content = response.content
-
-            # 清理可能的代码块标记
-            if "```markdown" in markdown_content:
-                markdown_content = markdown_content.split("```markdown")[1].split("```")[0].strip()
-            elif markdown_content.startswith("```") and markdown_content.endswith("```"):
-                # 移除开头和结尾的代码块标记
-                lines = markdown_content.split("\n")
-                if lines[0].startswith("```"):
-                    lines = lines[1:]
-                if lines and lines[-1].strip() == "```":
-                    lines = lines[:-1]
-                markdown_content = "\n".join(lines)
-
-            logger.info(f"LLM生成了Markdown格式的知识需求文档,长度: {len(markdown_content)} 字符")
-            return markdown_content
-
-        except Exception as e:
-            logger.error(f"生成知识需求文档失败: {e}", exc_info=True)
-            # 返回默认的Markdown文档
-            return self._get_default_markdown_requirements()
-
-    def _get_default_markdown_requirements(self) -> str:
-        """获取默认Markdown格式的知识需求(LLM失败时的备用方案)
-
-        Returns:
-            默认的Markdown格式知识需求文档
-        """
-        logger.warning("使用默认知识需求文档")
-
-        return """## 知识需求清单
-
-### 高优先级需求
-
-#### 需求1:指导整体解构流程和各环节的执行
-
-**知识需求描述**:
-小红书多模态内容的What解构方法论和标准流程
-
-**知识类别**:方法论
-
-**需求原因**:
-需要了解整体解构方法论作为指导框架
-
-**应用场景**:
-指导整体解构流程和各环节的执行
-
-**上下文信息**:
-- 适用范围:所有小红书多模态内容
-- 解构层级:全流程
-
----
-
-#### 需求2:判断图片元素是否继续分割拆解
-
-**知识需求描述**:
-图片元素分割的判断标准和停止条件
-
-**知识类别**:标准规范
-
-**需求原因**:
-图片解构中需要明确的决策标准
-
-**应用场景**:
-判断图片元素是否继续分割拆解
-
-**上下文信息**:
-- 适用对象:图片类元素
-- 决策节点:递归解构过程中
-
----
-
-### 中优先级需求
-
-#### 需求3:为各类元素确定描述维度和分类标准
-
-**知识需求描述**:
-多模态内容的描述维度框架和要素分类方法
-
-**知识类别**:方法论
-
-**需求原因**:
-需要统一的描述框架来组织解构结果
-
-**应用场景**:
-为各类元素确定描述维度和分类标准
-
-**上下文信息**:
-- 适用范围:所有多模态元素
-- 输出目标:结构化的描述框架
-
----
-"""
-
-    def _add_input_overview(self, markdown_doc: str, input_data: KnowledgeRequirementInput) -> str:
-        """在Markdown文档开头添加输入概览
-
-        Args:
-            markdown_doc: Markdown文档内容
-            input_data: 输入数据
-
-        Returns:
-            添加了概览的完整Markdown文档
-        """
-        lines = []
-
-        # 文档标题和时间
-        lines.append("# What解构知识需求文档")
-        lines.append("")
-        lines.append(f"**生成时间**: {self._get_current_time()}")
-        lines.append("")
-
-        # 输入概览 - 只显示关键的解构上下文信息
-        lines.append("## 输入概览")
-        lines.append("")
-
-        # 优先显示解构上下文(最核心的上下文信息)
-        has_context = False
-        if input_data.deconstruction_context:
-            ctx = input_data.deconstruction_context
-            if ctx.get("category") or ctx.get("theme"):
-                has_context = True
-                if ctx.get("category"):
-                    lines.append(f"- **品类**: {ctx['category']}")
-                if ctx.get("theme"):
-                    lines.append(f"- **主题**: {ctx['theme']}")
-
-        # 显示帖子标题(提供内容概览)
-        if input_data.post_content:
-            post = input_data.post_content
-            if post.get("text", {}).get("title"):
-                has_context = True
-                lines.append(f"- **帖子标题**: {post['text']['title']}")
-            if post.get("images"):
-                has_context = True
-                lines.append(f"- **图片数量**: {len(post['images'])}张")
-
-        # 如果没有任何上下文,显示提示信息
-        if not has_context:
-            lines.append("_无特定上下文信息_")
-
-        lines.append("")
-        lines.append("---")
-        lines.append("")
-
-        # 拼接原始markdown文档
-        overview = "\n".join(lines)
-        return overview + markdown_doc
-
-    def _generate_summary_from_markdown(self, markdown_doc: str) -> str:
-        """从Markdown文档中生成总结
-
-        Args:
-            markdown_doc: Markdown文档内容
-
-        Returns:
-            总结文本
-        """
-        # 统计内容知识需求数量(通过计算"## 内容知识需求"下"### 需求描述"中的"-"数量)
-        content_knowledge_count = 0
-        tool_knowledge_count = 0
-
-        # 简单统计:查找"内容知识需求"和"工具知识需求"部分
-        if "## 内容知识需求" in markdown_doc:
-            content_section = markdown_doc.split("## 内容知识需求")[1]
-            if "## 工具知识需求" in content_section:
-                content_section = content_section.split("## 工具知识需求")[0]
-            # 统计需求描述部分的"-"开头的行数
-            if "### 需求描述" in content_section:
-                desc_section = content_section.split("### 需求描述")[1]
-                content_knowledge_count = len([line for line in desc_section.split('\n') if line.strip().startswith('-')])
-
-        if "## 工具知识需求" in markdown_doc:
-            tool_section = markdown_doc.split("## 工具知识需求")[1]
-            # 统计需求描述部分的"-"开头的行数
-            if "### 需求描述" in tool_section:
-                desc_section = tool_section.split("### 需求描述")[1]
-                tool_knowledge_count = len([line for line in desc_section.split('\n') if line.strip().startswith('-')])
-
-        total_count = content_knowledge_count + tool_knowledge_count
-
-        summary = f"生成了{total_count}项知识需求"
-        if content_knowledge_count > 0:
-            summary += f",其中{content_knowledge_count}项内容知识需求"
-        if tool_knowledge_count > 0:
-            summary += f",{tool_knowledge_count}项工具知识需求"
-        summary += "。"
-
-        return summary
-
-    def _retrieve_all_knowledge(self, requirements: List[Dict[str, Any]]) -> Dict[str, Any]:
-        """不再调用知识检索工具(已废弃)
-
-        Args:
-            requirements: 知识需求清单
-
-        Returns:
-            空字典
-        """
-        logger.info("知识检索功能已禁用,使用大模型内置知识")
-        return {}
-
-    def _convert_requirement_to_query(self, requirement: str, knowledge_category: str) -> str:
-        """不再使用(已废弃)
-
-        Args:
-            requirement: 知识需求描述
-            knowledge_category: 知识类别
-
-        Returns:
-            原始需求文本
-        """
-        return requirement
-
-    def _map_category_to_type(self, knowledge_category: str) -> str:
-        """不再使用(已废弃)
-
-        Args:
-            knowledge_category: 知识类别
-
-        Returns:
-            默认类型
-        """
-        return "general"
-
-    def _generate_markdown_document(
-        self,
-        requirements: List[Dict[str, Any]],
-        retrieved_knowledge: Dict[str, Any],
-        input_data: KnowledgeRequirementInput
-    ) -> str:
-        """生成Markdown格式的知识需求文档
-
-        Args:
-            requirements: 知识需求清单
-            retrieved_knowledge: 检索到的知识
-            input_data: 输入数据
-
-        Returns:
-            Markdown文档字符串
-        """
-        lines = []
-
-        # 文档标题
-        lines.append("# What解构知识需求文档")
-        lines.append("")
-        lines.append(f"生成时间: {self._get_current_time()}")
-        lines.append("")
-
-        # 输入概览
-        lines.append("## 输入概览")
-        lines.append("")
-
-        if input_data.deconstruction_context:
-            ctx = input_data.deconstruction_context
-            if ctx.get("category"):
-                lines.append(f"- **品类**: {ctx['category']}")
-            if ctx.get("theme"):
-                lines.append(f"- **主题**: {ctx['theme']}")
-            if ctx.get("keywords"):
-                lines.append(f"- **关键词**: {', '.join(ctx['keywords']) if isinstance(ctx['keywords'], list) else ctx['keywords']}")
-
-        if input_data.post_content:
-            post = input_data.post_content
-            if post.get("text", {}).get("title"):
-                lines.append(f"- **帖子标题**: {post['text']['title']}")
-            if post.get("images"):
-                lines.append(f"- **图片数量**: {len(post['images'])}张")
-
-        lines.append("")
-
-        # 知识需求清单
-        lines.append("## 知识需求清单")
-        lines.append("")
-        lines.append(f"共识别 **{len(requirements)}** 项知识需求")
-        lines.append("")
-
-        # 按优先级分组
-        high_priority = [r for r in requirements if r.get("priority") == "高"]
-        medium_priority = [r for r in requirements if r.get("priority") == "中"]
-        low_priority = [r for r in requirements if r.get("priority") == "低"]
-
-        if high_priority:
-            lines.append("### 高优先级需求")
-            lines.append("")
-            for req in high_priority:
-                lines.extend(self._format_requirement(req, retrieved_knowledge))
-
-        if medium_priority:
-            lines.append("### 中优先级需求")
-            lines.append("")
-            for req in medium_priority:
-                lines.extend(self._format_requirement(req, retrieved_knowledge))
-
-        if low_priority:
-            lines.append("### 低优先级需求")
-            lines.append("")
-            for req in low_priority:
-                lines.extend(self._format_requirement(req, retrieved_knowledge))
-
-        # 知识检索统计
-        lines.append("## 知识检索统计")
-        lines.append("")
-
-        total_reqs = len(requirements)
-        successful_reqs = len([k for k, v in retrieved_knowledge.items() if v.get("results_count", 0) > 0])
-        avg_confidence = sum([v.get("confidence", 0) for v in retrieved_knowledge.values()]) / len(retrieved_knowledge) if retrieved_knowledge else 0
-
-        lines.append(f"- **总需求数**: {total_reqs}")
-        lines.append(f"- **成功检索**: {successful_reqs}/{total_reqs}")
-        lines.append(f"- **平均置信度**: {avg_confidence:.4f}")
-        lines.append("")
-
-        # 附录:完整知识内容
-        lines.append("## 附录:完整知识内容")
-        lines.append("")
-
-        for req in requirements:
-            req_id = req.get("id", "unknown")
-            if req_id in retrieved_knowledge:
-                knowledge = retrieved_knowledge[req_id]
-                lines.append(f"### [{req_id}] {req.get('application', 'N/A')}")
-                lines.append("")
-                lines.append("**知识需求**:")
-                lines.append(f"> {req.get('requirement', 'N/A')}")
-                lines.append("")
-                lines.append("**知识类别**:")
-                lines.append(f"> {req.get('knowledge_category', 'N/A')}")
-                lines.append("")
-                lines.append("**知识内容**:")
-                lines.append("")
-                lines.append("```")
-                lines.append(knowledge.get("knowledge", "暂无知识"))
-                lines.append("```")
-                lines.append("")
-
-                if knowledge.get("sources"):
-                    lines.append(f"**来源**: {', '.join(knowledge['sources'])}")
-                    lines.append("")
-
-                lines.append(f"**置信度**: {knowledge.get('confidence', 0):.4f}")
-                lines.append("")
-                lines.append("---")
-                lines.append("")
-
-        return "\n".join(lines)
-
-    def _format_requirement(
-        self,
-        req: Dict[str, Any],
-        retrieved_knowledge: Dict[str, Any]
-    ) -> List[str]:
-        """格式化单个需求为Markdown
-
-        Args:
-            req: 需求项
-            retrieved_knowledge: 检索到的知识
-
-        Returns:
-            Markdown行列表
-        """
-        lines = []
-
-        req_id = req.get("id", "unknown")
-
-        lines.append(f"#### {req_id}: {req.get('application', 'N/A')}")
-        lines.append("")
-        lines.append(f"- **知识需求**: {req.get('requirement', 'N/A')}")
-        lines.append(f"- **知识类别**: {req.get('knowledge_category', 'N/A')}")
-        lines.append(f"- **需求原因**: {req.get('reason', 'N/A')}")
-
-        # 检索结果摘要
-        if req_id in retrieved_knowledge:
-            knowledge = retrieved_knowledge[req_id]
-            lines.append(f"- **检索状态**: ✅ 成功 (置信度: {knowledge.get('confidence', 0):.4f})")
-            lines.append(f"- **结果数量**: {knowledge.get('results_count', 0)}条")
-
-            # 显示知识摘要(前200字符)
-            knowledge_text = knowledge.get("knowledge", "")
-            if knowledge_text:
-                summary = knowledge_text
-                lines.append(f"- **知识摘要**: {summary}")
-        else:
-            lines.append("- **检索状态**: ❌ 未检索")
-
-        lines.append("")
-
-        return lines
-
-    def _generate_summary(
-        self,
-        requirements: List[Dict[str, Any]],
-        retrieved_knowledge: Dict[str, Any]
-    ) -> str:
-        """生成知识需求总结
-
-        Args:
-            requirements: 知识需求清单
-            retrieved_knowledge: 检索到的知识
-
-        Returns:
-            总结文本
-        """
-        total_reqs = len(requirements)
-        high_priority_count = len([r for r in requirements if r.get("priority") == "高"])
-        successful_count = len([k for k, v in retrieved_knowledge.items() if v.get("results_count", 0) > 0])
-
-        summary = f"生成了{total_reqs}项知识需求,其中{high_priority_count}项为高优先级。"
-
-        if retrieved_knowledge:
-            summary += f"成功检索{successful_count}/{total_reqs}项。"
-
-        return summary
-
-    def _get_current_time(self) -> str:
-        """获取当前时间字符串"""
-        from datetime import datetime
-        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
-
-# 便捷函数:快速生成知识需求文档
-def generate_knowledge_requirements(
-    llm: BaseChatModel,
-    prd_content: Optional[str] = None,
-    post_content: Optional[Dict[str, Any]] = None,
-    deconstruction_context: Optional[Dict[str, Any]] = None,
-    current_element: Optional[Dict[str, Any]] = None,
-    knowledge_scope: Optional[List[str]] = None,
-    task_stage: Optional[str] = None,
-    knowledge_tool: Optional[Any] = None,  # 保留参数以兼容
-    enable_retrieval: bool = False  # 默认不启用
-) -> KnowledgeRequirementOutput:
-    """快速生成知识需求文档的便捷函数
-
-    Args:
-        llm: 语言模型实例
-        prd_content: PRD文档内容
-        post_content: 小红书帖子内容
-        deconstruction_context: 解构上下文
-        current_element: 当前元素
-        knowledge_scope: 知识范围
-        task_stage: 当前任务阶段
-        knowledge_tool: 知识检索工具(已废弃)
-        enable_retrieval: 是否启用知识检索(已废弃)
-
-    Returns:
-        知识需求输出
-    """
-    agent = KnowledgeRequirementGenerationAgent(
-        llm=llm,
-        knowledge_tool=knowledge_tool,
-        enable_retrieval=False  # 强制禁用
-    )
-
-    input_data = KnowledgeRequirementInput(
-        prd_content=prd_content,
-        post_content=post_content,
-        deconstruction_context=deconstruction_context,
-        current_element=current_element,
-        knowledge_scope=knowledge_scope,
-        task_stage=task_stage
-    )
-
-    return agent.generate_requirements(input_data)

+ 0 - 215
src/components/agents/post_summary_deconstruction_agent.py

@@ -1,215 +0,0 @@
-"""
-Post Summary Deconstruction Agent.
-
-帖子整体解构Agent:解构帖子整体的各种描述维度。
-"""
-
-from typing import Dict, Any, List
-import json
-
-from src.components.agents.base import BaseLLMAgent
-from src.states.what_deconstruction_state import WhatDeconstructionState
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class PostSummaryDeconstructionAgent(BaseLLMAgent):
-    """帖子整体解构Agent
-
-    根据 PRD 1.4 - 2.2.2.2 帖子整体节点要求:
-
-    **核心功能**:
-    1. 综合所有子元素的解构结果(图片、文本)
-    2. 从多个描述维度解构帖子整体
-    3. 帖子总体的各种描述、总结、归纳等维度或角度,每一个维度都是一个字段项
-
-    **必须满足的约束条件**:
-    1. 维度名称和数量根据每篇帖子动态变化
-    2. **一定要包含本帖子最关键/最吸引内容消费者的亮点维度**
-       - 包括但不限于:内容亮点、情绪共鸣点、创作手法等维度
-    3. **描述维度的数量不得少于3个**
-       - 要尽可能多而全的覆盖这个帖子的所有可描述角度
-    4. 包括但不限于:品类、主题、脚本等维度
-    5. 描述维度应根据帖子的品类、主题、关键词等信息来向知识库动态请求询问
-    """
-
-    def __init__(
-        self,
-        name: str = "post_summary_deconstruction_agent",
-        description: str = "解构帖子整体的各种描述维度",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 10240
-    ):
-        system_prompt = """你是一个专业的内容分析专家。你的任务是从多个维度解构帖子整体。
-
-根据 PRD 1.4 - 2.2.2.2 帖子整体节点要求:
-
-**核心任务**:
-1. 综合分析所有子元素(图片、文本)的解构结果
-2. 从多个描述维度解构帖子整体
-3. 帖子总体的各种描述、总结、归纳等维度或角度,每一个维度都是一个字段项
-
-**必须满足的约束条件**:
-1. ✅ 维度名称和数量根据每篇帖子动态变化
-2. ✅ **一定要包含本帖子最关键/最吸引内容消费者的亮点维度**
-   - 包括但不限于:内容亮点、情绪共鸣点、创作手法等维度
-3. ✅ **描述维度的数量不得少于3个**
-   - 要尽可能多而全的覆盖这个帖子的所有可描述角度
-4. ✅ 包括但不限于:品类、主题、脚本等维度
-5. ✅ 描述维度应根据帖子的品类、主题、关键词等信息来向知识库动态请求询问
-   - Query句式(参见 PRD 1.4 - 3.2):
-     对于一篇主题为"{帖子主题}",品类为"{帖子品类}",关键词包含"{帖子关键词列表}"的多模态社交媒体帖子,
-     从内容创作者视角进行What要素的初步识别和分类,需要使用哪些通用工具?
-
-**输出格式必须是JSON**:
-{
-    "品类": "值",
-    "主题": "值",
-    "脚本": "值",
-    "内容亮点": "值",           # 必须包含
-    "情绪共鸣点": "值",         # 必须包含
-    "创作手法": "值",           # 必须包含
-    "整体风格": "值",
-    "视觉呈现": "值",
-    "内容价值": "值",
-    "目标受众": "值",
-    ... # 其他动态维度(根据帖子特点)
-}
-
-**输出要求**:
-1. ✅ 客观准确,严格基于实际解构结果,禁止主观臆测、凭空假设、虚构数据
-2. ✅ 维度名称和数量根据帖子内容动态确定
-3. ✅ 突出核心特征和亮点
-4. ✅ **最少包含3个描述维度**
-5. ✅ 必须包含"内容亮点"、"情绪共鸣点"、"创作手法"等吸引消费者的亮点维度
-"""
-
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_messages(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """构建消息
-
-        包含所有子元素解构结果、消费者亮点、总结维度
-        """
-        messages = [
-            {"role": "system", "content": self.system_prompt}
-        ]
-
-        # 构建用户消息
-        user_prompt = "# 帖子解构结果\n\n"
-
-        # 添加帖子基本信息
-        category = state.get("category", "未知")
-        theme = state.get("theme", "未知")
-        keywords = state.get("keywords", [])
-
-        user_prompt += f"**品类**:{category}\n"
-        user_prompt += f"**主题**:{theme}\n"
-        user_prompt += f"**关键词**:{', '.join(keywords)}\n\n"
-
-        # 添加图片解构结果
-        image_results = state.get("image_deconstruction_results", [])
-        if image_results:
-            user_prompt += "## 图片元素解构\n\n"
-            for idx, img_result in enumerate(image_results, 1):
-                what = img_result.get("what", "")
-                desc = img_result.get("description", img_result.get("描述", {}))
-                user_prompt += f"### 图片 {idx}\n"
-                user_prompt += f"**What**: {what}\n"
-                user_prompt += f"**描述**: {json.dumps(desc, ensure_ascii=False, indent=2)}\n\n"
-
-        # 添加文本解构结果
-        text_results = state.get("text_deconstruction_results", [])
-        if text_results:
-            user_prompt += "## 文本元素解构\n\n"
-            for idx, text_result in enumerate(text_results, 1):
-                what = text_result.get("what", "")
-                desc = text_result.get("description", text_result.get("描述", {}))
-                user_prompt += f"### 文本元素 {idx}\n"
-                user_prompt += f"**What**: {what}...\n"  # 限制长度
-                user_prompt += f"**描述**: {json.dumps(desc, ensure_ascii=False, indent=2)}\n\n"
-
-        # 添加消费者亮点
-        consumer_highlights = state.get("consumer_highlights", [])
-        if consumer_highlights:
-            user_prompt += "## 消费者关注的亮点\n\n"
-            for idx, highlight in enumerate(consumer_highlights, 1):
-                element = highlight.get("element", "")
-                highlight_desc = highlight.get("highlight", "")
-                emotion = highlight.get("emotion", "")
-                user_prompt += f"{idx}. **{element}**: {highlight_desc} ({emotion})\n"
-            user_prompt += "\n"
-
-        # 添加总结维度要求
-        # 根据 PRD 1.4 - 2.2.2.2:
-        # - 描述维度有哪些、有多少个,需要根据本帖子的品类、主题、关键词等信息来向知识库动态请求询问
-        # - Query句式:参见3.2 环节中对query的定义
-        # 这里使用默认维度作为示例(实际应从知识库获取)
-        summary_dimensions = state.get("summary_dimensions", [
-            "品类",
-            "主题",
-            "脚本",
-            "内容亮点",
-            "情绪共鸣点",
-            "创作手法",
-            "整体风格",
-            "视觉呈现",
-            "内容价值",
-            "目标受众"
-        ])
-
-        user_prompt += "## 解构维度\n\n"
-        user_prompt += "请根据以下维度进行帖子整体解构(至少包含3个维度):\n"
-        for dim in summary_dimensions:
-            user_prompt += f"- {dim}\n"
-
-        user_prompt += "\n请严格按照JSON格式输出,字段名为上述维度名。至少包含3个描述维度。"
-
-        messages.append({"role": "user", "content": user_prompt})
-
-        return messages
-
-    def _update_state(self, state: WhatDeconstructionState, response: Any) -> Dict[str, Any]:
-        """更新状态
-
-        解析LLM返回的JSON,更新帖子整体
-        """
-        try:
-            # 提取响应内容
-            if hasattr(response, 'content'):
-                content = response.content
-            else:
-                content = str(response)
-
-            # 尝试提取JSON
-            content = content.strip()
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-
-            # 返回更新后的字段
-            return {
-                "post_overall": result  # 更新为 post_overall
-            }
-
-        except Exception as e:
-            logger.error(f"解析帖子整体响应失败: {e},原始内容: {response}")
-            # 返回默认值
-            return {
-                "post_overall": {
-                    "错误": f"解析失败: {str(e)}"
-                }
-            }

+ 0 - 206
src/components/agents/post_understanding_agent.py

@@ -1,206 +0,0 @@
-"""
-Post Understanding Agent.
-
-帖子初理解Agent:分析帖子多模态内容,提取品类、主题、关键词。
-"""
-
-from typing import Dict, Any, List
-import json
-import base64
-from pathlib import Path
-from urllib.parse import urlparse
-
-from src.components.agents.base import BaseLLMAgent
-from src.states.what_deconstruction_state import WhatDeconstructionState
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class PostUnderstandingAgent(BaseLLMAgent):
-    """帖子初理解Agent
-
-    功能:
-    - 综合分析图片视觉风格、文本语义、话题标签
-    - 推理帖子的核心主题
-    - 提取品类、主题、关键词列表
-    """
-
-    def __init__(
-        self,
-        name: str = "post_understanding_agent",
-        description: str = "分析帖子多模态内容,提取品类、主题、关键词",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 10240
-    ):
-        system_prompt = """你是一个专业的小红书内容分析专家。你的任务是分析帖子的多模态内容(图片+文本),提取核心信息。
-
-你需要:
-1. 综合分析图片的视觉风格、主题、元素
-2. 理解标题、正文、话题标签的语义
-3. 推理出帖子所属的品类(如:时尚美妆、美食探店、旅游攻略等)
-4. 提取帖子的核心主题(一句话概括帖子在讲什么)
-5. 提取3-5个关键词
-
-输出格式必须是JSON:
-{
-    "category": "品类",
-    "theme": "核心主题(一句话)",
-    "keywords": ["关键词1", "关键词2", "关键词3"]
-}
-
-要求:
-- 客观准确,不要主观臆测
-- 关键词要具体且有代表性
-- 品类和主题要简洁明了
-"""
-
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_messages(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """构建消息
-
-        包含系统提示、图片和文本内容
-        """
-        # 系统消息
-        messages = [
-            {"role": "system", "content": self.system_prompt}
-        ]
-
-        # 构建用户消息内容
-        message_content = []
-
-        # 添加文本内容
-        text_data = state.get("text", {})
-        text_prompt = "请分析以下帖子内容:\n\n"
-
-        if text_data.get("title"):
-            text_prompt += f"标题:{text_data['title']}\n\n"
-
-        if text_data.get("body"):
-            text_prompt += f"正文:{text_data['body']}\n\n"
-
-        if text_data.get("hashtags"):
-            hashtags_str = " ".join(text_data["hashtags"])
-            text_prompt += f"话题标签:{hashtags_str}\n\n"
-
-        message_content.append({"type": "text", "text": text_prompt})
-
-        # 添加图片内容(多模态)
-        images = state.get("images", [])
-        if images:
-            message_content.append({
-                "type": "text",
-                "text": f"帖子包含 {len(images)} 张图片,请结合图片内容进行分析:"
-            })
-
-            for image_path in images:
-                try:
-                    image_url = self._encode_image(image_path)
-                    message_content.append({
-                        "type": "image_url",
-                        "image_url": {"url": image_url}
-                    })
-                except Exception as e:
-                    logger.warning(f"图片 {image_path} 处理失败: {e}")
-
-        # 添加输出格式要求
-        message_content.append({
-            "type": "text",
-            "text": "\n\n请严格按照JSON格式输出分析结果。"
-        })
-
-        messages.append({"role": "user", "content": message_content})
-
-        return messages
-
-    def _encode_image(self, image_path: str) -> str:
-        """处理图片URL,对于远程URL直接返回,本地文件转换为base64
-
-        Args:
-            image_path: 图片路径或URL
-
-        Returns:
-            图片URL(远程URL直接返回,本地文件返回base64 data URL)
-        """
-        # 判断是否为URL
-        parsed = urlparse(image_path)
-        is_url = parsed.scheme in ['http', 'https']
-
-        if is_url:
-            # Gemini支持直接使用远程URL,无需下载和编码
-            logger.info(f"使用远程图片URL: {image_path}")
-            return image_path
-        else:
-            # 本地文件需要转换为base64
-            path = Path(image_path)
-            if not path.exists():
-                raise FileNotFoundError(f"图片文件不存在: {image_path}")
-
-            # 读取图片文件
-            with open(path, "rb") as image_file:
-                image_data = image_file.read()
-
-            # 根据文件扩展名确定MIME类型
-            mime_type = "image/jpeg"
-            if path.suffix.lower() in [".png"]:
-                mime_type = "image/png"
-            elif path.suffix.lower() in [".webp"]:
-                mime_type = "image/webp"
-            elif path.suffix.lower() in [".gif"]:
-                mime_type = "image/gif"
-
-            # 编码为base64
-            encoded_string = base64.b64encode(image_data).decode('utf-8')
-            logger.info(f"本地图片已编码为base64: {image_path}")
-            return f"data:{mime_type};base64,{encoded_string}"
-
-    def _update_state(self, state: WhatDeconstructionState, response: Any) -> Dict[str, Any]:
-        """更新状态
-
-        解析LLM返回的JSON,更新品类、主题、关键词
-        """
-        try:
-            # 提取响应内容
-            if hasattr(response, 'content'):
-                content = response.content
-            else:
-                content = str(response)
-
-            # 尝试提取JSON(可能被包裹在```json```中)
-            content = content.strip()
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-
-            # 验证必需字段
-            if "category" not in result or "theme" not in result or "keywords" not in result:
-                raise ValueError("响应缺少必需字段")
-
-            # 返回更新后的字段
-            return {
-                "category": result["category"],
-                "theme": result["theme"],
-                "keywords": result["keywords"]
-            }
-
-        except Exception as e:
-            logger.error(f"解析响应失败: {e},原始内容: {response}")
-            # 返回默认值
-            return {
-                "category": "未知",
-                "theme": "无法识别主题",
-                "keywords": []
-            }

+ 0 - 390
src/components/agents/readme/inspiration_points_agent.md

@@ -1,390 +0,0 @@
-# 灵感点提取Agent
-
-## 总结
-
-该 Agent 通过6个步骤完成灵感点提取:
-1. **识别候选灵感点**:通过视频逐帧分析识别候选灵感点(使用 LLM + 视频分析)
-2. **逆向推导验证**:验证候选灵感点能否推导出整个视频内容(使用 LLM + 视频分析)
-3. **去重检查**:移除本质相同的灵感点(使用 LLM)
-4. **三维度评分**:对灵感点进行人设契合度、触发可能性、内容解释力三维评分(使用 LLM + 视频分析)
-5. **排序筛选**:按分数筛选,最多保留2个高质量灵感点(纯代码逻辑)
-6. **组装最终输出**:将完整信息组装成最终格式(纯代码逻辑)
-
-## 系统提示词(System Prompt)
-
-```
-你是创作灵感分析专家,从创作者视角逆向推导触发创作的深层刺激源。
-
-# 灵感点定义(核心概念)
-**灵感点** = 创作前的触发源(让创作者产生创作冲动的刺激)
-- 区分:灵感点(触发源)≠ 目的点(动机)≠ 关键点(吸引点)
-- 验证:能否从这个灵感点推导出整个视频内容
-
-# 核心原则(所有分析遵循)
-- 透过表面内容挖掘深层心理动机、行为模式、价值观冲突
-- 灵感点可以来自视频的任何部分,包括画面、动作、场景、对话等
-- 需要对视频进行逐帧或关键帧分析,捕捉动态变化和细节
-```
-
-## 执行步骤
-
-### Step 1: 识别候选灵感点(逐帧分析视频)
-
-通过视频逐帧分析识别候选灵感点,按三种来源分类:全新内容、共性差异、共性内容。
-
-**Prompt**:
-
-```
-# 任务:识别候选灵感点(逐帧分析视频)
-
-## 当前视频
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
-- 画面内容:场景、物体、人物、环境等视觉元素
-- 动作变化:人物的动作、物体的运动、场景的转换
-- 时间线索:视频的时间顺序、节奏变化、关键时间点
-- 视觉细节:颜色、光影、构图、视觉焦点等
-- 音频信息(如有):对话、音效、背景音乐等
-
-## 核心概念(统一说明,避免重复)
-
-### 字段定义
-**分类**(维度):创作者接收外界信息刺激的角度或通道
-- 格式:2-4个字,简洁直观,避免抽象表述
-
-**灵感点**:创作前遇到的、触发创作冲动的客观刺激源(作者被动接收的信息:看到的、听说的、发现的、观察到的、感知到的)
-- 格式:不超过15个字,使用自然、通俗、口语化的表达方式
-- 本质:刺激源的内容本身(讲什么、做什么、说什么),而非表达手段(如何呈现、如何表达)
-- 表达要求:
-  * 使用日常生活中的自然语言,避免学术化、抽象化的词汇堆砌
-  * 优先使用"的"字短语结构(如"夏日的热闹景象")或动宾短语(如"观察到的自然互动")
-  * 禁止使用多个抽象名词连用(如"具象化动态互动自然拟人")
-  * 表达要让普通人一看就懂,不需要"翻译"
-
-**描述**:对刺激源本身是什么的详细说明,讲清楚这个灵感点具体指的是什么
-- 内容要求:描述刺激源的具体特征、形态、场景、内容等客观信息,让读者能清晰理解"这个灵感点到底是什么东西"
-- 注意区分:刺激源内容本身 vs 呈现方式/表现形式
-
-**推理**:说明这个灵感点是如何得出来的,解释从视频内容推导出这个灵感点的过程
-- 内容要求:连贯的描述性文字,说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者接收到了这个刺激源
-
-### 严格禁止(适用于灵感点和描述)
-- 不描述创作者如何运用/展现/表达这个刺激,不使用推理性词汇
-- 不能是创作形式、表现手法、表达方式、呈现方式、风格、格式等创作应用层面的东西
-- 必须是被动接收的刺激,不能是主动创造的内容
-- 不解释创作者为什么被触发、如何使用这个刺激
-- 不进行主观推理和价值判断
-- 禁止词汇堆砌,避免多个抽象概念连用
-
-### 反思验证(识别每个灵感点后必须进行)
-- 这个灵感点描述的是内容本身,还是仅仅描述表达手段?
-- 如果仅描述表达手段(如呈现方式、创作形式、风格、格式等),应重新提炼真正的刺激源内容
-- 这个表达是否符合日常口语习惯?是否通俗易懂?
-- 如果描述的是内容本身且表达自然 → 保留
-
-### 识别要求
-- **独立性**: 不同灵感点必须是不同的刺激源,本质相同的只保留一个
-- **完整性**: 分类→灵感点→描述应形成从刺激通道到刺激内容的完整表达
-- **真实性**: 从"作者接收到了什么刺激"(被动接收)而非"作者如何创作"(主动输出)的角度出发
-- **视频特性**: 需要关注视频的动态特性,包括画面变化、动作序列、时间线索等
-
-## 灵感点识别策略(基于视频内容特征分类)
-
-### 1. 全新内容灵感点
-- 特征:视频中出现的全新刺激源,与视频其他部分或常见模式完全不同
-- 搜索方向:视频中哪些画面/动作/场景/对话是全新的、独特的、首次出现的
-- 分析重点:关注视频中的创新点、独特元素、异常场景
-
-### 2. 共性差异灵感点
-- 特征:视频中基于某种常见模式但有新变化的刺激源
-- 搜索方向:在视频中,哪些内容基于常见模式但有创新/变化/新角度
-- 额外字段:基于哪个共性、具体差异
-- 分析重点:关注视频中的变化点、创新点、差异化元素
-
-### 3. 共性内容灵感点
-- 特征:视频中符合常见模式或重复出现的刺激源
-- 搜索方向:视频中哪些内容符合常见模式、重复出现、形成规律
-- 额外字段:基于哪个共性
-- 分析重点:关注视频中的规律性、重复性、模式化内容
-
-## 输出格式(JSON)
-{
-  "候选灵感点列表": {
-    "全新内容": [
-      {
-        "候选编号": 1,
-        "分类": "...",
-        "灵感点": "...",
-        "描述": "...",
-        "推理": "..."
-      }
-    ],
-    "共性差异": [
-      {
-        "候选编号": 2,
-        "分类": "...",
-        "灵感点": "...",
-        "描述": "...",
-        "基于哪个共性": "...",
-        "具体差异": "...",
-        "推理": "..."
-      }
-    ],
-    "共性内容": [
-      {
-        "候选编号": 3,
-        "分类": "...",
-        "灵感点": "...",
-        "描述": "...",
-        "基于哪个共性": "...",
-        "推理": "..."
-      }
-    ]
-  }
-}
-
-**重要提醒**:
-- 推理必须是连贯的描述性文字,说明为什么这个灵感点能刺激创作
-- 候选编号全局唯一(跨三个分类连续编号:1, 2, 3...)
-- 必须基于视频的实际内容进行分析,不能凭空想象
-
-**三个类别必须互斥**(判断优先级):
-1. 视频中全新、独特的元素 → 全新内容
-2. 基于常见模式但有明显变化 → 共性差异
-3. 完全符合常见模式或重复出现 → 共性内容
-```
-
----
-
-### Step 2: 逆向推导验证
-
-验证每个候选灵感点是否能够推导出整个视频内容,只保留验证通过的灵感点。
-
-**Prompt**:
-
-```
-# 任务:逆向推导验证
-
-## 当前视频
-请对视频进行逐帧或关键帧分析。
-[视频文本信息:标题、正文等]
-
-## 候选灵感点
-[候选灵感点列表:包含候选编号、分类、灵感点]
-
-## 验证要求
-对每个候选灵感点进行逆向推导验证:
-
-**验证问题**:
-- 从这个灵感点出发,能否推导出整个视频的内容?
-- 这个刺激源是创作前的触发,而不是创作目的?
-- 这个灵感点是否真正捕捉到了深层动机?
-
-**验证规则**:
-- 能推导出 → 验证通过(true)
-- 不能推导出或逻辑不通 → 验证失败(false)
-
-## 输出格式(JSON - 简化版)
-{
-  "验证结果列表": [
-    {
-      "候选编号": 1,
-      "验证结果": true,
-      "推导说明": "从这个灵感点可以推导出..."
-    },
-    {
-      "候选编号": 2,
-      "验证结果": false,
-      "推导说明": "无法从这个灵感点推导出..."
-    }
-  ]
-}
-
-**格式说明**:
-- 候选编号:整数,对应上面的候选灵感点编号(1, 2, 3...)
-- 验证结果:布尔值(true或false)
-- 推导说明:能否从这个灵感点推导出视频内容的详细说明
-
-**重要**:只输出编号、验证结果和推导说明,不需要复制其他字段
-```
-
----
-
-### Step 3: 去重检查
-
-识别并移除本质相同的灵感点,每组重复的灵感点只保留最能直接反映刺激源本身的那一个。
-
-**Prompt**:
-
-```
-# 任务:灵感点去重
-
-## 候选灵感点列表
-[候选灵感点列表,包含候选编号、灵感点、描述]
-
-## 核心判断标准
-
-**唯一标准:是否指向同一个刺激源?**
-
-- 如果两个灵感点描述的是**同一个事物/现象/刺激**,只是从不同角度、不同层面、不同粒度来表达 → 重复,去重
-
-## 判断方法
-
-1. **识别刺激源本质**:每个灵感点背后的刺激源到底是什么?
-2. **对比刺激源**:是同一个刺激源的不同表述?还是两个不同的刺激源?
-3. **忽略表述差异**:不要被具体用词、分类标签、角度差异迷惑,抓住本质
-
-## 保留规则(重复时选择)
-
-从重复组中选择**最能直接反映刺激源本身**的那一个:
-- 优先保留描述刺激源本身的(客体)
-- 其次保留最具体、最精准的
-
-## 输出(JSON)
-{
-  "去重分析": [
-    {"重复组": [2, 3], "重复原因": "两者都在描述同一个刺激源:[具体说明]", "保留编号": 2, "保留理由": "[原因]"}
-  ],
-  "保留的候选编号列表": [1, 2]
-}
-
-## 原则
-
-- 宁可过度去重,不遗漏重复
-- 抓住本质,忽略表面差异
-- 不确定时 → 判定重复 → 去重
-```
-
----
-
-### Step 4: 三维度评分
-
-对每个灵感点从三个维度评分(每个维度1-10分):人设契合度、触发可能性、内容解释力。总分范围:3-30分。
-
-**Prompt**:
-
-```
-# 任务:评估灵感点
-
-## 待评分的灵感点
-[待评分的灵感点列表,包含候选编号、分类、灵感点、描述]
-
-## 评分标准
-
-对每个灵感点从三个维度评分(每个维度1-10分):
-
-### 1. 人设契合度(1-10分)
-- 评估这个刺激点是否符合创作者的人设特征
-- 从视频内容推断创作者的兴趣、价值观、情感模式、行为习惯
-- 判断这个刺激点与人设的匹配程度
-
-### 2. 触发可能性(1-10分)
-- 评估这个刺激点触发创作的可能性有多大
-- 考虑刺激点的吸引力强度
-- 考虑创作者遇到这个刺激时产生创作冲动的概率
-
-### 3. 内容解释力(1-10分)
-- 评估这个刺激点能否解释视频的核心内容
-- 判断从这个刺激点能否推导出视频的主要内容
-- 考虑刺激点与视频内容的关联强度
-
-**总分 = 人设契合度 + 触发可能性 + 内容解释力(范围:3-30分)**
-
-## 评分原则
-- 客观评分,不要因分类(全新/共性差异/共性)而预设分数高低
-- 全新内容:如果与视频内容不符,触发可能性也会很低
-- 共性内容:如果是视频核心特征,触发可能性反而很高
-
-## 输出格式(JSON)
-{
-  "评分结果列表": [
-    {
-      "候选编号": 1,
-      "人设契合度": 8,
-      "触发可能性": 7,
-      "内容解释力": 9,
-      "总分": 24,
-      "评分说明": "详细说明评分理由:为什么这个刺激点符合人设、触发创作的可能性如何、能否解释视频内容"
-    }
-  ]
-}
-
-**要求**:
-- 候选编号:整数,对应待评分灵感点的编号
-- 人设契合度、触发可能性、内容解释力:1-10的整数
-- 总分:3-30的整数(三个评分之和)
-- 评分说明:详细说明评分理由
-- 只输出编号和评分信息,不需要复制其他字段
-```
-
----
-
-### Step 5: 排序筛选
-
-按总分排序并筛选,优先保留总分 >= 15分的灵感点,如果3个类目都没有达标的则保底保留全局最高分的1个,最终输出最多2个高质量灵感点。
-
----
-
-### Step 6: 组装最终输出
-
-将完整信息组装成最终格式,包括从原始候选点提取的字段、验证结果中的推导说明、评分信息等。
-
----
-
-## 输出格式
-
-最终输出结构:
-
-```json
-{
-  "inspiration_points": {
-    "全新内容": [
-      {
-        "候选编号": 1,
-        "分类": "...",
-        "灵感点": "...",
-        "描述": "...",
-        "推理": "...",
-        "推导说明": "...",
-        "scoring": {
-          "人设契合度": 8,
-          "触发可能性": 7,
-          "内容解释力": 9,
-          "总分": 24,
-          "评分说明": "..."
-        }
-      }
-    ],
-    "共性差异": [...],
-    "共性内容": [...]
-  },
-  "metadata": {
-    "total_count": 2,
-    "perspective": "创作者视角",
-    "analysis_mode": "视频逐帧分析",
-    "process_summary": {
-      "candidate_count": 10,
-      "verified_count": 8,
-      "deduplicated_count": 6,
-      "scored_count": 6,
-      "filtered_count": 2,
-      "final_count": 2,
-      "category_counts": {
-        "全新内容": 1,
-        "共性差异": 1,
-        "共性内容": 0
-      }
-    }
-  }
-}
-```
-
----
-
-## 配置常量
-
-- `MIN_QUALITY_SCORE = 15`: 最低质量分数阈值(总分3-30)
-- `HISTORICAL_POSTS_BATCH_SIZE = 5`: 每批处理的历史帖子数量(当前未使用)
-
----

+ 0 - 262
src/components/agents/readme/key_points_agent.md

@@ -1,262 +0,0 @@
-# 关键点提取Agent
-
-## 总结
-
-该 Agent 通过5个步骤完成关键点提取:
-1. **识别候选关键点**:基于视频逐帧分析识别候选关键点(使用 LLM + 视频分析)
-2. **第一轮去重**:与灵感点、目的点去重(使用 LLM)
-3. **第二轮去重**:关键点之间去重(使用 LLM)
-4. **构建层级关系**:将扁平关键点组织成树形结构(使用 LLM)
-5. **组装最终输出**:组装最终输出(纯代码逻辑)
-
-## 系统提示词(System Prompt)
-
-```
-你是内容分析专家,从消费者视角提取核心吸引点。
-
-# 关键点定义
-消费者视角下能够引起关注的核心要素
-
-# 维度定义(有限集合 - 二级分类)
-关键点必须归属于以下维度体系:
-
-## 维度大类:形式
-**细分维度:**
-1. **风格** - 关注内容的整体呈现方式、结构、格式等外在特征
-2. **关系** - 关注内容中各个实质要素之间的关联、逻辑关系
-
-## 维度大类:实质
-**细分维度:**
-1. **元素** - 关注内容中的具体实体、对象、要素等
-2. **分类** - 关注对具体元素的抽象、总结、归纳
-
-# 关键点层级关系
-关键点之间存在层级关系,形成树形结构:
-- **层级判断标准**:
-  - 大类 → 小类
-  - 抽象 → 具体
-  - 整体 → 局部
-- **层级表达**:一级关键点 → 二级关键点 → 三级关键点(支持多层嵌套)
-- **层级原则**:子关键点是父关键点的细分、具体化或局部
-
-# 核心原则
-- 消费者视角:思考"什么吸引了消费者"
-- 客观性:只陈述事实,不做主观判断
-- 视频逐帧分析:需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的吸引点
-- 二级维度归类:必须明确标注维度大类(形式/实质)和维度细分(风格/关系/元素/分类)
-- 关键点精准:不超过15个字,只写核心发现(不写如何呈现)
-- 描述充分:详细解释关键点的含义和价值
-- 层级清晰:识别关键点之间的层级关系,构建树形结构
-```
-
-## 执行步骤
-
-### Step 1: 识别候选关键点(视频逐帧分析)
-
-基于视频逐帧分析从消费者视角识别所有可能的吸引点,输出扁平化列表。
-
-**Prompt**:
-
-```
-# 任务:识别候选关键点(视频逐帧分析,扁平化列表)
-
-## 视频内容
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度捕捉吸引点:
-- **画面内容**:场景、物体、人物、环境等视觉元素中的吸引点
-- **动作变化**:人物的动作、物体的运动、场景的转换等动态吸引点
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点等时序吸引点
-- **视觉细节**:颜色、光影、构图、视觉焦点等视觉吸引点
-- **音频信息**(如有):对话、音效、背景音乐等听觉吸引点
-
-## 字段定义
-
-### 维度大类
-只能是"形式"或"实质"
-
-### 维度细分
-- 形式类:只能是"风格"或"关系"
-- 实质类:只能是"元素"或"分类"
-
-### 关键点
-≤15字核心发现(只写"是什么",不写"如何呈现")
-
-### 描述
-- **本质**: 讲清楚这个关键点是什么(吸引点的具体特征、形态、内容等客观信息)
-- **内容要求**:
-  - 描述吸引点本身的客观特征
-  - 说明这个吸引点的完整上下文
-  - 让读者能清晰理解"这个关键点到底是什么"
-- **严格禁止**:
-  - 不解释为什么吸引
-  - 不进行价值判断和主观评价
-  - 不分析效果和影响
-
-## 维度分类要求(二级分类体系)
-
-**维度大类:形式**
-- **风格**:关注内容的整体呈现方式、结构、格式等外在特征
-- **关系**:关注内容中各个实质要素之间的关联、逻辑关系
-
-**维度大类:实质**
-- **元素**:关注内容中的具体实体、对象、要素等
-- **分类**:关注对具体元素的抽象、总结、归纳
-
-## 输出(JSON)
-{
-  "候选关键点列表": [
-    {
-      "候选编号": 1,
-      "维度大类": "形式|实质",
-      "维度细分": "风格|关系|元素|分类",
-      "关键点": "核心发现",
-      "描述": "充分说明"
-    }
-  ]
-}
-
-要求:
-- 广泛收集:从视频的各个维度(画面、动作、场景、对话等)广泛收集吸引点
-- 客观陈述:只陈述事实,不做主观判断
-- 严格按照二级维度体系归类:必须明确标注维度大类(形式/实质)和维度细分(风格/关系/元素/分类)
-- 编号连续:候选编号从1开始连续编号
-- 基于视频:所有关键点必须基于视频的实际内容,不能凭空想象
-
-注意:此步骤输出扁平化列表,后续会构建层级关系
-```
-
----
-
-### Step 2: 第一轮去重(与灵感点、目的点去重)
-
-移除与灵感点、目的点重复的关键点,确保关键点≠灵感点≠目的点。
-
-**Prompt**:
-
-```
-# 任务:与灵感点、目的点去重
-
-## 候选关键点
-[候选关键点列表]
-
-## 灵感点(创作触发源)
-[灵感点列表]
-
-## 目的点(创作目标)
-[目的点列表]
-
-## 去重规则
-
-**核心原则**:关键点与灵感点/目的点重合 → 必须移除关键点
-
-**判断方法**:
-1. 角色检查:关键点应是"消费吸引点",不应是"触发源"或"创作目标"
-2. 语义去重:移除修饰词,提取核心概念对比 → 核心概念相同 → 移除关键点
-
-**重要**:只要存在重合(角色错误或核心概念相同),就移除关键点
-
-## 输出(JSON)
-{
-  "去重分析": [{"去重类型": "与灵感点去重|与目的点去重", "重复候选编号": 1, "重复原因": "说明"}],
-  "保留的候选编号列表": [1, 2, 3]
-}
-```
-
----
-
-### Step 3: 第二轮去重(关键点之间去重)
-
-移除本质相同的关键点,每组重复的只保留最能直接反映吸引点的那一个。
-
-**Prompt**:
-
-```
-# 任务:关键点之间去重
-
-## 候选关键点
-[候选关键点列表]
-
-## 去重规则
-
-**本质相同的定义**:
-- 核心发现相同(描述同一个吸引点)
-- 包含关系(一个是另一个的具体/一般化)
-- 粒度不同(抽象层级不同,但指向同一吸引点)
-
-**判断方法**:移除修饰词 → 提取核心发现 → 核心相同 = 本质相同 → 去重
-
-**保留优先级**(本质相同时):
-1. 具体 > 一般
-2. 精准 > 模糊
-3. 原始 > 衍生
-4. 本质 > 形式
-
-## 输出(JSON)
-{
-  "去重分析": [{"重复组": [1, 5], "重复原因": "说明", "保留编号": 1, "保留理由": "说明"}],
-  "保留的候选编号列表": [1, 2, 3]
-}
-```
-
----
-
-### Step 4: 构建层级关系
-
-将扁平化关键点组织成树形结构,识别关键点之间的层级关系(大类→小类、抽象→具体、整体→局部)。
-
-**Prompt**:
-
-```
-# 任务:构建关键点层级关系
-
-## 关键点列表(扁平化)
-[关键点列表]
-
-## 层级判断标准
-
-**层级关系定义**:
-- **大类 → 小类**:一个关键点是另一个关键点的具体类别
-- **抽象 → 具体**:一个关键点是另一个关键点的具体表现
-- **整体 → 局部**:一个关键点是另一个关键点的组成部分
-
-**判断方法**:
-1. 识别父子关系:子关键点是父关键点的细分、具体化或局部
-2. 构建树形结构:父节点可以有多个子节点,子节点也可以继续有子节点
-3. 确保无循环:不能出现A→B→A的情况
-
-**层级原则**:
-- 同一层级的关键点应该是并列关系(不存在包含、细分关系)
-- 不同层级的关键点应该是从属关系(父→子)
-- 一级关键点(根节点)应该是最抽象、最整体的关键点
-- 如果两个关键点没有明确的层级关系,则保持在同一层级
-
-## 输出(JSON)
-{
-  "层级分析": [
-    {
-      "父编号": 1,
-      "子编号列表": [2, 3],
-      "层级关系": "大类→小类|抽象→具体|整体→局部",
-      "关系说明": "说明为什么存在这个层级关系",
-      "子节点原因": {
-        "2": "说明编号2作为子节点的具体原因",
-        "3": "说明编号3作为子节点的具体原因"
-      }
-    }
-  ],
-  "一级关键点编号列表": [1, 4, 5]
-}
-
-注意:
-- "一级关键点编号列表":列出所有一级关键点(根节点)的候选编号
-- "层级分析":描述所有父子关系,可以有多层嵌套
-- "子节点原因":为每个子节点说明为什么它是父节点的子节点(细分、具体化、局部的具体原因)
-```
-
----
-
-### Step 5: 组装最终输出
-
-将关键点组装成最终格式,包括层级结构和所有字段。
-

+ 0 - 103
src/components/agents/readme/purpose_point_agent.md

@@ -1,103 +0,0 @@
-# 目的点提取Agent
-
-## 总结
-
-该 Agent 通过1个步骤完成目的点提取:
-1. **提取目的点**:基于视频逐帧分析提取创作目的(使用 LLM + 视频分析)
-
-## 系统提示词(System Prompt)
-
-```
-你是内容创作分析专家,从创作者视角提取创作目的。
-
-# 核心概念
-
-**目的点** = 创作者希望通过内容对受众产生的影响或效果(Why创作)
-- 区分:目的点(Why)≠ 创作手段(How)≠ 灵感点(触发源)
-- 验证:问"创作者为什么要做这个内容?"而非"如何做"
-- 数量:1-3个,每个二级分类最多1个目的点
-
-# 输出结构
-
-每个目的点包含:
-1. **维度**:结构化对象 {"一级分类": "", "二级分类": ""}
-2. **目的点**:≤15字,描述创作者希望达成的最终效果
-3. **描述**:解释创作者希望受众产生什么反应/影响
-4. **推理**:说明这个目的点是如何得出来的,解释从视频内容推导出这个创作目的的过程(基于视频的哪些画面/动作/场景/对话等线索,推导出创作者的这个目的)
-
-# 标准分类体系
-
-```
-# 一级:个人
-## 二级:记录(个人记录、日记、生活点滴)
-## 二级:分享(分享经验、知识、感受、故事)
-
-# 一级:创作者
-## 二级:面向粉丝(粉丝运营、互动、维护关系)
-## 二级:品牌种草(推广产品/品牌、种草带货)
-## 二级:商业化效果(转化、ROI、销售业绩)
-
-# 一级:其他(无法归类的特殊情况)
-```
-
-# 核心原则
-
-- **目的优先**:问"为什么"
-- **创作者视角**:站在创作者角度思考"我想让受众产生什么反应"
-- **证据支撑**:基于视频内容具体体现推断目的
-- **分类规范**:维度必须从标准分类体系中选择,优先选择二级分类
-- **互斥约束**:每个二级分类只能出现一次
-- **宁缺毋滥**:如果目的不明确,不要强行生成,最少可以只有1个目的点
-- **视频逐帧分析**:需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的信息
-```
-
-## 执行步骤
-
-### Step 1: 提取目的点(视频逐帧分析)
-
-基于视频逐帧分析提取创作目的,识别创作者希望通过内容对受众产生的影响或效果,输出1-3个目的点。
-
-**Prompt**:
-
-```
-# 任务:提取创作目的(视频逐帧分析)
-
-## 视频内容
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
-- **画面内容**:场景、物体、人物、环境等视觉元素
-- **动作变化**:人物的动作、物体的运动、场景的转换
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点
-- **视觉细节**:颜色、光影、构图、视觉焦点等
-- **音频信息**(如有):对话、音效、背景音乐等
-
-## 分析步骤
-1. **逐帧分析**:观察视频的每一帧或关键帧,理解视频的整体内容和结构
-2. **识别意图**:从视频的画面、动作、场景、对话等维度,推断创作者想让受众产生什么反应/影响(Why)
-3. **维度归类**:从标准分类体系中选择最匹配的维度(优先二级分类)
-4. **提炼目的点**:在维度下提炼具体目标(≤15字,聚焦最终效果,不写手段)
-
-## 重要约束
-- **每个二级分类最多1个目的点**(例如不能同时有2个"记录"维度的目的点)
-- **宁缺毋滥**:如果某个维度的目的不明确,不要强行生成
-- **总数控制**:最终输出1-3个目的点,不要为了凑数而硬造
-- **证据支撑**:每个目的点都必须有足够的视频内容支撑,不能臆测
-- **基于视频**:所有目的点必须基于视频的实际内容,不能凭空想象
-
-## 输出格式(JSON)
-{
-    "目的点列表": [
-        {
-            "维度": {
-                "一级分类": "",
-                "二级分类": ""
-            },
-            "目的点": "",
-            "描述": "",
-            "推理": "说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者的这个目的"
-        }
-    ]
-}
-```
-

+ 0 - 13
src/components/agents/readme/result_aggregation_function.md

@@ -1,13 +0,0 @@
-# 结果汇总函数
-
-## 总结
-
-该 Function 通过1个步骤完成结果汇总:
-1. **结果汇总**:将所有解构结果(三点解构和选题理解)汇总为最终的JSON结构(纯代码逻辑)
-
-## 执行步骤
-
-### Step 1: 结果汇总
-
-汇总所有解构结果为最终的JSON结构,包括视频信息、三点解构(灵感点、目的点、关键点)和选题理解,验证JSON格式并返回最终结果。
-

+ 0 - 691
src/components/agents/readme/script_element_extraction_agent.md

@@ -1,691 +0,0 @@
-# 脚本元素提取Agent - 流程文档
-
-## 概述
-
-从视频内容中提取核心实质点(元素 + 概念)的8步流程,每步独立调用LLM。
-
-**系统提示词**:
-```
-你是实质点提取专家,擅长从视频内容中识别和提取核心实质点(元素 + 概念)。
-
-# 核心能力
-1. 判断视频倾向:判断概念和元素谁更重要
-2. 实质点识别:区分元素(具体实体)和概念(抽象名词)
-3. 共性定量分析:定量分析实质点在段落中的出现情况(基础数据)
-4. 多维度支撑性评分:评估对灵感点、目的点、关键点、段落的支撑(基于共性数据)
-5. 分类整合:为实质点添加分类标签,确保同级正交
-
-# 工作原则
-- 名词化:所有实质点必须是名词形式
-- 定量化:所有评分必须有客观依据和定量指标
-- 同类去重:概念与概念去重,元素与元素去重,不做跨类型去重
-- 结构化:使用方括号标记分类,确保同级正交
-```
-
----
-
-## Step 1: 判断视频倾向
-
-**目标**:判断对于这个视频来说,概念更重要还是元素更重要
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 选题描述
-
-{topic_text}
-
-# 任务
-
-判断对于**这个视频**来说,**概念更重要**还是**元素更重要**。
-
-## 定义
-
-### 概念(Concept)
-- 抽象的名词,不能是形式的
-
-### 元素(Element)
-- 具体的、客观存在的实体,视觉可见
-
-## 判断标准
-
-### 概念更重要
-视频的核心价值在于**抽象的方法论、流程、原理**:
-- 强调过程和方法
-- 强调原理和知识传递
-- 强调教学、指导、分享经验
-
-### 元素更重要
-视频的核心价值在于**具体的物品、实物展示**:
-- 强调物品本身
-- 强调视觉呈现和展示
-- 强调具体实物的特征
-
-## 判断流程
-
-1. **分析视频核心价值**:这个视频的核心价值是什么?
-2. **识别主要内容类型**:是方法论/流程/原理,还是物品/实物展示?
-3. **评估抽象 vs 具体的比重**:哪个占主导地位?
-4. **做出判断**:概念更重要 or 元素更重要
-
-## 输出要求
-
-- 必须明确给出判断结果
-- 必须给出充分的理由
-- 必须引用视频内容作为证据
-
-# 输出(JSON)
-
-{
-  "判断结果": "概念更重要/元素更重要",
-  "核心价值": "这个视频的核心价值是什么",
-  "主要内容类型": "方法论/流程/原理/物品展示/实物介绍等",
-  "抽象vs具体比重": "抽象内容占X%,具体内容占Y%(估算)",
-  "判断理由": "为什么做出这个判断(引用视频内容作为证据)",
-  "支撑证据": ["证据1:具体引用", "证据2:具体引用"]
-}
-```
-
----
-
-## Step 2: 主视角提取
-
-**目标**:从更重要的视角提取(概念 or 元素)
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 选题描述(仅供参考理解视频方向)
-
-{topic_text}
-
-**重要约束**:选题描述仅用于帮助你理解视频的主题方向和定位,**不要从选题描述中提取实质点**。所有实质点必须从**视频实际内容**中提取。
-
-# 视频倾向判断
-
-{tendency_result.get("判断理由", "")}
-
-# 任务
-
-从**{primary_type}视角**提取视频中的核心**{primary_type}**。
-
-## {primary_type}定义
-
-### 概念(Concept)
-**定义**:抽象的名词,不能是形式的
-
-**特征**:
-- ✅ 必须是名词
-- ✅ 抽象的,非具象的
-- ✅ 不能是形式的(不是风格、颜色、材质等形式特征)
-- ✅ 虽然没有直接提到,但隐含在内容中
-- ✅ 对理解视频核心价值至关重要
-
-**原子名词约束**:
-- ❌ 禁止形容词
-- ❌ 禁止动词
-- ✅ 只能是纯粹的名词形式
-
-### 元素(Element)
-**定义**:具体的、客观存在的实体,视觉可见
-
-**特征**:
-- ✅ 必须是名词
-- ✅ 具体的物体,视觉可见
-- ✅ 客观存在的实体
-- ✅ 使用原子名词(最基础的名词形式,去除修饰语)
-- ❌ 不是动作、状态、情绪
-- ❌ 不是抽象概念
-
-## 提取原则
-
-1. **全面识别**:宁可多识别,后续会筛选
-2. **名词化**:所有{primary_type}必须是名词形式
-3. **原子化**:使用最基础的名词,去除修饰语
-4. **聚焦主视角**:只提取{primary_type},不提取另一种类型
-5. **仅从视频内容提取**:只从视频中提取,**禁止从选题描述中提取**
-
-## 输出要求
-
-每个{primary_type}需要标注:
-- 名称:原子名词
-- 识别依据:为什么识别这个{primary_type}(简要说明)
-
-# 输出(JSON)
-
-{
-  "实质点列表": [
-    {
-      "名称": "{primary_type}名称(原子名词)",
-      "类型": "{primary_type}",
-      "识别依据": "为什么识别这个{primary_type}"
-    }
-  ]
-}
-```
-
----
-
-## Step 3: 辅视角提取
-
-**目标**:从另一个视角提取,并与主视角去重
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 选题描述(仅供参考理解视频方向)
-
-{topic_text}
-
-**重要约束**:选题描述仅用于帮助你理解视频的主题方向和定位,**不要从选题描述中提取实质点**。所有实质点必须从**视频实际内容**中提取。
-
-# 主视角已提取的实质点
-
-{primary_text}
-
-# 任务
-
-从**{secondary_type}视角**提取视频中的核心**{secondary_type}**,并与主视角提取的实质点去重。
-
-## {secondary_type}定义
-
-(同Step 2的定义,根据secondary_type选择概念或元素)
-
-## 提取原则
-
-1. **全面识别**:宁可多识别,后续会筛选
-2. **名词化**:所有{secondary_type}必须是名词形式
-3. **原子化**:使用最基础的名词,去除修饰语
-4. **聚焦辅视角**:只提取{secondary_type},不提取另一种类型
-5. **仅从视频内容提取**:只从视频中提取,**禁止从选题描述中提取**
-
-## 去重要求
-
-**关键**:与主视角提取的实质点去重
-
-- 如果{secondary_type}与主视角实质点存在语义关联,不要重复提取
-- 只提取主视角中没有覆盖到的{secondary_type}
-
-## 输出要求
-
-每个{secondary_type}需要标注:
-- 名称:原子名词
-- 识别依据:为什么识别这个{secondary_type}(简要说明)
-
-# 输出(JSON)
-
-{
-  "实质点列表": [
-    {
-      "名称": "{secondary_type}名称(原子名词)",
-      "类型": "{secondary_type}",
-      "识别依据": "为什么识别这个{secondary_type}"
-    }
-  ]
-}
-```
-
-**代码逻辑**:合并主视角和辅视角的实质点列表。
-
----
-
-## Step 4: 共性定量分析 + 段落支撑评分
-
-**目标**:对所有实质点进行共性定量分析和段落支撑评分
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 段落划分结果
-
-{section_text}
-
-段落总数: {total_sections}
-
-# 实质点列表
-
-{substantials_text}
-
-# 任务
-
-对每个实质点进行**共性定量分析 + 段落支撑评分**。
-
-**重要**:只输出新增的分析数据和评分数据,不要重复输出名称、类型、识别依据等已有信息。
-
-## 第一部分:共性分析(元素和概念统一指标)
-
-1. **出现段落数**:该实质点在多少个段落中出现(整数)
-2. **段落覆盖率**:出现段落数 / 总段落数(百分比,保留2位小数)
-3. **出现频次**:该实质点总共出现多少次(整数)
-4. **出现段落列表**:该实质点出现的段落ID列表
-
-**关键约束 - 段落统计规则**:
-- **只统计具体段落**(叶子节点),不统计抽象段落(父节点)
-- 例如:段落1是段落1.1的父节点,如果元素出现在段落1.1中,只记录段落1.1,不记录段落1
-- 判断方法:有子项的段落是抽象段落,没有子项的段落是具体段落
-- 段落总数={total_sections}已经是具体段落的总数
-
-**说明**:
-- 元素:统计该元素在视频中出现的段落和频次
-- 概念:统计该概念在视频中被提及或隐含的段落和频次
-
-## 第二部分:段落支撑评分(每个段落0-10分)
-
-评估该实质点在每个出现的段落中的权重/重要性
-
-**关键约束 - 段落评分规则**:
-- **只评估具体段落**(叶子节点),不评估抽象段落(父节点)
-- 与第一部分的段落统计保持一致
-- 段落ID必须与"出现段落列表"中的ID一致
-
-**评分标准**:
-- 9-10分:段落核心元素,不可或缺
-- 7-8分:段落重要元素,有明显作用
-- 5-6分:段落一般元素,有一定作用
-- 3-4分:段落辅助元素,作用较小
-- 0-2分:段落边缘元素或未出现
-
-**依据要求**:
-- 可以参考共性分析中的段落出现数据
-- 必须逐个段落评分(只对出现的段落评分)
-- 必须说明在该段落中的具体表现
-- 必须提供客观的、直接的证据
-
-## 输出要求
-
-- 定量指标必须准确计算
-- 所有评分必须有客观依据
-- **只输出名称和对应的分析+评分数据,不要重复已有字段**
-
-# 输出(JSON)
-
-{
-  "分析结果": [
-    {
-      "名称": "实质点名称",
-      "出现段落数": 3,
-      "段落覆盖率": 0.75,
-      "出现频次": 5,
-      "出现段落列表": ["段落1", "段落1.1", "段落2.1"],
-      "支撑段落详情": [
-        {
-          "段落ID": "段落1",
-          "得分": 8,
-          "依据": "在该段落中的具体表现 + 客观证据"
-        },
-        {
-          "段落ID": "段落1.1",
-          "得分": 6,
-          "依据": "在该段落中的具体表现 + 客观证据"
-        }
-      ]
-    }
-  ]
-}
-
-**注意**:
-- 元素和概念使用统一的分析指标
-- **出现段落列表**: 必须使用具体段落ID列表(只包含叶子节点,不包含父节点)
-- **支撑段落详情**: 必须使用段落ID字段,包含得分和依据,只对具体段落评分(与"出现段落列表"一致)
-- 不要输出类型、识别依据等字段,这些会由代码自动合并
-```
-
-**代码逻辑**:将LLM返回的分析数据与原始实质点数据合并。
-
----
-
-## Step 5: 多维度支撑评分
-
-**目标**:对所有实质点进行3个维度的支撑性评分(并行评估)
-
-### 5.1 支撑灵感点评分
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 实质点列表(含共性分析数据和段落支撑评分)
-
-{items_text}
-
-# 灵感点
-
-{inspiration_text}
-
-# 任务
-
-对每个实质点进行**支撑灵感点评分**(0-10分)。
-
-**重要**:只输出实质点名称和灵感点评分,不要输出其他已有信息。
-
-## 评分维度:支撑灵感点(0-10分)
-
-评估该实质点如何支撑灵感点(触发创作者的灵感)
-
-**评分标准**:
-- 9-10分:核心支撑,直接触发创作灵感
-- 7-8分:重要支撑,对灵感有明显作用
-- 5-6分:一定支撑,有一定触发作用
-- 3-4分:辅助支撑,间接相关
-- 0-2分:弱支撑或无关
-
-**依据要求**:
-- 必须引用具体的灵感点
-- 必须说明实质点如何支撑该灵感点
-- 必须提供客观的、直接的证据(引用视频内容)
-- 可以利用共性分析数据和段落支撑评分辅助判断
-
-## 输出要求
-
-- 所有评分必须有客观依据
-- 所有依据必须引用视频内容
-- **只输出名称和灵感点评分数据**
-
-# 输出(JSON)
-
-{
-  "评分结果": [
-    {
-      "名称": "实质点名称",
-      "支撑灵感点得分": 8,
-      "支撑灵感点依据": "引用具体灵感点 + 如何支撑 + 客观证据"
-    }
-  ]
-}
-
-**注意**:
-- 只输出名称和支撑灵感点的评分数据
-- 不要输出类型、识别依据、共性分析、段落支撑等字段
-```
-
-### 5.2 支撑目的点评分
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 实质点列表(含共性分析数据和段落支撑评分)
-
-{items_text}
-
-# 目的点
-
-{purpose_text}
-
-# 任务
-
-对每个实质点进行**支撑目的点评分**(0-10分)。
-
-**重要**:只输出实质点名称和目的点评分,不要输出其他已有信息。
-
-## 评分维度:支撑目的点(0-10分)
-
-评估该实质点如何支撑目的点(帮助达成创作目的)
-
-**评分标准**:
-- 9-10分:核心支撑,直接承载创作目的
-- 7-8分:重要支撑,对目的达成有明显作用
-- 5-6分:一定支撑,有一定帮助
-- 3-4分:辅助支撑,间接相关
-- 0-2分:弱支撑或无关
-
-**依据要求**:
-- 必须引用具体的目的点
-- 必须说明实质点如何支撑该目的点
-- 必须提供客观的、直接的证据(引用视频内容)
-- 可以利用共性分析数据和段落支撑评分辅助判断
-
-## 输出要求
-
-- 所有评分必须有客观依据
-- 所有依据必须引用视频内容
-- **只输出名称和目的点评分数据**
-
-# 输出(JSON)
-
-{
-  "评分结果": [
-    {
-      "名称": "实质点名称",
-      "支撑目的点得分": 7,
-      "支撑目的点依据": "引用具体目的点 + 如何支撑 + 客观证据"
-    }
-  ]
-}
-
-**注意**:
-- 只输出名称和支撑目的点的评分数据
-- 不要输出类型、识别依据、共性分析、段落支撑等字段
-```
-
-### 5.3 支撑关键点评分
-
-**Prompt**:
-```
-# 视频内容
-
-{video_content}
-
-# 实质点列表(含共性分析数据和段落支撑评分)
-
-{items_text}
-
-# 关键点
-
-{key_text}
-
-# 任务
-
-对每个实质点进行**支撑关键点评分**(0-10分)。
-
-**重要**:只输出实质点名称和关键点评分,不要输出其他已有信息。
-
-## 评分维度:支撑关键点(0-10分)
-
-评估该实质点如何支撑关键点(吸引消费者)
-
-**评分标准**:
-- 9-10分:核心支撑,直接吸引消费者
-- 7-8分:重要支撑,有明显吸引力
-- 5-6分:一定支撑,有一定吸引作用
-- 3-4分:辅助支撑,间接相关
-- 0-2分:弱支撑或无关
-
-**依据要求**:
-- 必须引用具体的关键点
-- 必须说明实质点如何支撑该关键点
-- 必须提供客观的、直接的证据(引用视频内容)
-- 可以利用共性分析数据和段落支撑评分辅助判断
-
-## 输出要求
-
-- 所有评分必须有客观依据
-- 所有依据必须引用视频内容
-- **只输出名称和关键点评分数据**
-
-# 输出(JSON)
-
-{
-  "评分结果": [
-    {
-      "名称": "实质点名称",
-      "支撑关键点得分": 9,
-      "支撑关键点依据": "引用具体关键点 + 如何支撑 + 客观证据"
-    }
-  ]
-}
-
-**注意**:
-- 只输出名称和支撑关键点的评分数据
-- 不要输出类型、识别依据、共性分析、段落支撑等字段
-```
-
-**代码逻辑**:并行调用3次LLM,然后将3个维度的评分结果合并到实质点数据中。
-
----
-
-## Step 6: 筛选
-
-**目标**:基于评分筛选核心实质点
-
-**代码逻辑**:根据以下筛选标准进行代码筛选(不依赖LLM):
-
-1. **综合得分 > 6.0**:综合得分 = (支撑灵感点得分 + 支撑目的点得分 + 支撑关键点得分 + 平均段落得分) / 4
-2. **或任一单项得分 > 8.0**:最高单项得分(灵感点/目的点/关键点/段落)
-3. **或共性得分 > 7.0**:共性得分 = (段落覆盖率 × 0.7 + 频次归一化 × 0.3) × 10
-
-满足任意一个条件即通过筛选。
-
----
-
-## Step 7: 去重(同类去重)
-
-**目标**:对筛选后的实质点进行同类去重(概念与概念,元素与元素)
-
-**Prompt**:
-```
-# 筛选后的实质点列表
-
-{items_text}
-
-# 任务
-
-对筛选后的实质点进行**同类去重**。
-
-**重要**:只输出去重决策和保留的实质点名称列表,不要输出完整的实质点数据。
-
-## 去重规则
-
-**核心原则**:只进行同类去重,概念与概念去重,元素与元素去重,不做跨类型去重。
-
-### 规则1: 概念 vs 概念
-- 如果两个概念语义相似或有包含关系,保留**综合得分更高**的那个
-
-### 规则2: 元素 vs 元素
-- 如果两个元素是同一类物体的不同表述,保留**综合得分更高**的那个
-
-**重要约束**:
-- ✅ 概念只与概念比较去重
-- ✅ 元素只与元素比较去重
-- ❌ 概念与元素之间**不进行**去重比较
-
-## 去重流程
-
-1. **分组**:将实质点按类型分为概念组和元素组
-2. **概念组内去重**:找出概念组内语义相似的概念,保留得分更高的
-3. **元素组内去重**:找出元素组内同类物体,保留得分更高的
-4. **合并结果**:将去重后的概念和元素合并
-
-# 输出(JSON)
-
-{
-  "去重决策": [
-    {
-      "冲突组": ["实质点1", "实质点2"],
-      "冲突类型": "概念vs概念/元素vs元素",
-      "应用规则": "规则1/规则2",
-      "保留": "实质点1",
-      "去除": ["实质点2"],
-      "决策理由": "为什么保留实质点1,去除实质点2"
-    }
-  ],
-  "保留列表": ["实质点名称1", "实质点名称2", ...]
-}
-
-**注意**:
-- 只输出去重决策和保留的实质点名称
-- 不要输出完整的实质点数据(类型、识别依据、评分等)
-- 这些完整数据会由代码自动填充
-```
-
-**代码逻辑**:根据LLM返回的保留列表,从筛选结果中提取对应的完整实质点数据。
-
----
-
-## Step 8: 整合
-
-**目标**:为实质点添加分类标签(用字典表示层级关系)
-
-**Prompt**:
-```
-# 去重后的实质点列表
-
-{items_text}
-
-# 任务
-
-为每个实质点分配分类,分类用字典表示层级关系。
-
-**重要**:只输出实质点名称和对应的分类字典,不要输出完整的实质点数据。
-
-## 分类规则
-
-**核心原则**:分类基于实质点本身的 What 维度(本质属性)
-
-- **What 维度**:回答"这是什么类型的东西"
-- **分类来源**:从实质点本身的本质属性中抽象出分类
-- **同级正交**:同一分类下的节点应该具有相同的 What 维度
-
-**MECE分类视角**:
-- 物理维度:基于物理属性
-- 化学维度:基于化学成分
-- 生物维度:基于生物分类
-- 功能维度:基于客观功能
-
-**禁止**:
-- ❌ 从"对其他事物的影响/效果"角度分类
-- ❌ 从"目的/价值"角度分类
-
-## 分类层级
-
-- 根据实际情况灵活确定层级深度
-
-## 输出要求
-
-- 同级正交:同一分类下的实质点应该具有相同的 What 维度
-- **只输出实质点名称和对应的分类字典**
-
-# 输出(JSON)
-
-{
-  "分类结果": [
-    {
-      "名称": "实质点名称",
-      "分类": {"一级": "分类名", "二级": "子分类名"}
-    }
-  ]
-}
-
-**注意**:
-- 只输出名称和分类字典
-- 分类字典的key必须是"一级"、"二级"、"三级"等
-- 不要输出类型、识别依据、评分等字段
-- 这些完整数据会由代码自动填充
-```
-
-**代码逻辑**:将LLM返回的分类结果合并到去重后的实质点数据中,生成最终的元素列表。
-
----
-
-## 输出结果
-
-最终返回:
-- `元素列表`:包含所有历史数据(名称、类型、识别依据、共性分析、评分、分类等)的实质点列表
-- `视频倾向判断`:Step 1的判断结果
-

+ 0 - 127
src/components/agents/readme/script_section_division_agent.md

@@ -1,127 +0,0 @@
-# 脚本段落划分Agent - 流程文档
-
-## 概述
-
-分析视频的分段结构,理解创作者是如何组织内容的(创作者视角),输出树状结构的段落列表。
-
-**系统提示词**:
-```
-你是脚本结构分析专家,擅长从创作者视角理解视频内容的分段结构。
-
-# 核心能力
-1. 结构识别:分析视频内容,识别创作者的分段逻辑
-2. 树状输出:通过子项字段表示层级关系,不使用parent_id
-
-# 工作原则
-- 识别创作者如何分段组织视频内容(树状结构)
-- 需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的结构变化
-```
-
----
-
-## Step 1: 获取视频文件
-
-**代码逻辑**:从 state 中获取视频文件对象,如果无法获取则返回默认值。
-
----
-
-## Step 2: 段落划分分析
-
-**目标**:从创作者视角分析视频的分段结构
-
-**Prompt**:
-```
-# 任务
-
-从**创作者视角**分析这个视频是如何组织内容的。
-
-## Section切分流程
-
-**第一步:识别主题显著变化位置**
-扫描整个视频,识别**主题发生显著变化**的位置:
-
-- **判断标准**:
-  * 语义跃迁: 讨论对象发生根本性改变
-  * 逻辑转换: 从"是什么"转向"为什么"或"怎么办"
-  * 功能变化: 从"问题陈述"转向"解决方案"
-  * 场景切换: 画面、场景、动作发生明显变化
-  * 对话转换: 话题、语气、情绪发生明显变化
-
-- **划分原则**:
-  * 避免过度细分(每个小变化都成为顶层段落)
-  * 避免过度粗放(将所有内容合并为1个顶层段落)
-  * 以"主题板块"而非"内容单元"为划分粒度
-
-**第二步:初步划分**
-- 基于主题显著变化位置进行划分
-- 支持主Section和子Section的层级结构
-- 需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的结构变化
-
-**第三步:顺序验证与反思**
-- 检查每个Section内的视频片段是否保持时间顺序的连续性
-- 验证同一Section内的内容是否有语义关联
-- 确认段落之间的过渡是否自然合理
-
-## 层级要求
-
-**段落必须至少保留2层结构**:
-1. **第1层(抽象层)**:从具象中聚合出的共性维度
-2. **第2层(具象层)**:具体的内容细节
-
-**层级关系说明**:
-- 抽象层是对多个具象内容的归纳和提炼
-- 具象层是抽象层的具体展开
-- 每个抽象层下必须有至少1个具象层子项
-
-## Section字段
-
-- 描述: 段落描述(共性维度名称;具体内容概括)
-- 内容范围: **列表格式,包含具体视频片段**
-  - 格式:["0:00-0:30", "0:30-1:15"] 或 ["片段1: 开场介绍", "片段2: 问题阐述"]
-  - 要求:必须包含具体的时间戳范围或视频片段描述,清晰标识该Section涵盖的视频内容
-- 推理依据: 为什么这样划分
-- 子项: 子Section列表(树状结构)
-
-# 输出(JSON)- 树状结构
-
-{
-  "内容品类": "内容品类",
-  "段落列表": [
-    {
-      "描述": "共性维度名称",
-      "内容范围": ["0:00-0:30", "0:30-1:15"],
-      "推理依据": "为什么这样划分这个抽象层",
-      "子项": [
-        {
-          "描述": "具体内容概括",
-          "内容范围": ["0:00-0:30"],
-          "推理依据": "这个具象内容如何支撑上层抽象",
-          "子项": []
-        }
-      ]
-    }
-  ]
-}
-```
-
-**调用方式**:使用 `LLMInvoker.safe_invoke_video_analysis` 进行视频分析。
-
----
-
-## Step 3: 添加段落ID
-
-**代码逻辑**:递归为每个段落添加唯一ID(格式:`段落1`、`段落1.1`、`段落1.2` 等),保持层级关系。
-
----
-
-## 输出结果
-
-最终返回:
-- `内容品类`:视频内容品类
-- `段落列表`:树状结构的段落列表,每个段落包含:
-  - `id`:唯一标识符
-  - `描述`:段落描述
-  - `内容范围`:视频片段列表
-  - `推理依据`:划分理由
-  - `子项`:子段落列表(递归结构)
-

+ 0 - 130
src/components/agents/readme/topic_selection_understanding_agent.md

@@ -1,130 +0,0 @@
-# 选题理解Agent
-
-## 总结
-
-该 Agent 通过1个步骤完成选题理解:
-1. **选题理解**:基于视频内容和三点分析结果(灵感点、目的点、关键点),理解选题策略(使用 LLM + 视频分析)
-
-## 系统提示词(System Prompt)
-
-```
-你是选题策略分析专家,擅长整合多维度信息揭示视频内容的选题逻辑。
-
-# 视频分析要求
-- 需要对视频进行逐帧或关键帧分析,理解视频的整体内容和结构
-- 关注视频的画面、动作、场景、对话等各个维度
-- 基于视频的实际内容进行分析,不能凭空想象
-
-# 核心能力
-1. **三点关系理解**:
-   - 灵感点 = 创作前的触发源(什么刺激触发了创作)
-   - 目的点 = 创作时的动机(为什么要创作)
-   - 关键点 = 消费时的吸引力(什么吸引受众)
-   - 选题 = 将灵感转化为目的,通过关键点实现
-
-2. **有机整合**:分析三点之间的内在关联,而非简单罗列
-
-3. **证据驱动**:所有结论基于具体数据推导
-
-4. **覆盖验证**:确保选题理解覆盖所有灵感点、目的点、关键点,未覆盖的点需要说明原因
-
-# 输出规范
-严格按照以下JSON结构输出:
-```json
-{
-    "主题": "string - 一句话概括选题主题",
-    "描述": "string - 用具体细节描述核心逻辑(灵感来源→关键手法→呈现效果→创作目的),必须明确体现所有目的点",
-    "覆盖情况": {
-        "灵感点覆盖情况": [
-            {
-                "灵感点": "string - 灵感点名称",
-                "是否覆盖": true/false,
-                "覆盖说明": "string - 如何在选题中体现,如果未覆盖说明原因"
-            }
-        ],
-        "目的点覆盖情况": [
-            {
-                "目的点": "string - 目的点名称",
-                "是否覆盖": true/false,
-                "覆盖说明": "string - 如何在选题中实现,如果未覆盖说明原因"
-            }
-        ],
-        "关键点覆盖情况": [
-            {
-                "关键点": "string - 关键点名称",
-                "是否覆盖": true/false,
-                "覆盖说明": "string - 如何在选题中支撑,如果未覆盖说明原因"
-            }
-        ]
-    }
-}
-```
-
-**注意**:不需要输出 `explicit_elements` 字段,该字段由系统自动提取。
-
-# topic_description 核心原则
-**描述本质,精炼核心**:
-1. **拒绝笼统概念** - 要具体到本质特征,不用抽象方法论
-2. **删除非核心修饰** - 只保留最本质的词
-3. **具体但不冗余** - 描述具体现象,不堆砌修饰词
-
-# coverage_analysis 核心原则
-**确保完整覆盖**:
-1. **逐一检查** - 检查每个灵感点、目的点、关键点是否在选题中有体现
-2. **明确说明** - 如果覆盖,说明如何体现;如果未覆盖,说明为什么未体现
-3. **追求完整** - 努力让选题理解覆盖所有三点,未覆盖需要充分理由
-```
-
-## 执行步骤
-
-### Step 1: 选题理解
-
-基于视频逐帧分析和三点数据(灵感点、目的点、关键点),理解选题策略,分析灵感如何转化为目的并通过关键点实现。
-
-**Prompt**:
-
-```
-# 输入数据
-
-## 视频内容
-视频URL: [视频URL]
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度理解视频内容:
-- **画面内容**:场景、物体、人物、环境等视觉元素
-- **动作变化**:人物的动作、物体的运动、场景的转换
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点
-- **视觉细节**:颜色、光影、构图、视觉焦点等
-- **音频信息**(如有):对话、音效、背景音乐等
-
-基于视频的实际内容,结合以下三点分析结果,理解选题策略。
-
-## 灵感点分析
-[灵感点列表:包含分类、灵感点、描述等信息]
-
-## 目的点分析
-[目的点列表:包含维度、目的点、描述等信息]
-
-## 关键点分析
-[关键点列表:包含维度、关键点、描述、层级结构等信息]
-
----
-
-# 执行指令
-
-基于以上数据,输出选题策略分析。
-
-## 质量要求
-- **描述**: 必须是完整流畅的1-2句话,清晰展现完整逻辑链"灵感来源→关键手法→呈现效果→创作目的"
-  - **必须包含目的点**: 明确说明创作者的目的,不能只描述内容本身
-  - **必须体现三点内在关联**: 分析三点的有机融合逻辑,而非简单罗列
-  - **基于视频内容**: 所有分析必须基于视频的实际内容,不能凭空想象
-  - 拒绝笼统概念,要具体到本质特征
-  - 删除非核心修饰词,只保留最本质的词
-  - 禁用空洞词汇: "精心"、"独特"、"全面"、"旨在"、"充分"、"深度"、"夸张的"、"强烈的"、"明显的"
-
-**重要提醒**:
-- 不需要输出 `explicit_elements` 字段,系统会自动从三点数据中提取
-- 所有分析必须基于视频的实际内容和三点分析结果
-```
-

+ 0 - 1543
src/components/agents/script_element_extraction_agent.py

@@ -1,1543 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-脚本元素提取Agent(完全重构版 - 8步流程)
-
-功能: 从视频内容中提取核心实质点(元素 + 概念)
-核心流程(8步,每步独立调用LLM):
-  1. 判断视频倾向 - 概念更重要 or 元素更重要
-  2. 主视角提取 - 从更重要的视角提取
-  3. 辅视角提取 - 从另一个视角提取,并与主视角去重
-  4. 共性定量分析 - 元素和概念在段落中的出现分析(先统计基础数据)
-  5. 多维度支撑评分 - 支撑灵感点、目的点、关键点、段落(基于共性数据评分)
-  6. 筛选 - 基于评分筛选
-  7. 去重 - 同类去重(概念与概念,元素与元素)
-  8. 整合 - 为实质点添加分类标签(用方括号表示)
-"""
-
-import json
-from typing import List
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-
-class ScriptElementExtractionAgent(BaseLLMAgent):
-    """脚本元素提取Agent - 提取视频的核心实质点(元素 + 概念)
-    """
-
-    def __init__(
-        self,
-        name: str = "script_element_extraction_agent",
-        description: str = "脚本元素提取Agent - 提取视频的核心实质点",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        """初始化脚本元素提取Agent
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是实质点提取专家,擅长从视频内容中识别和提取核心实质点(元素 + 概念)。
-
-# 核心能力
-1. 判断视频倾向:判断概念和元素谁更重要
-2. 实质点识别:区分元素(具体实体)和概念(抽象名词)
-3. 共性定量分析:定量分析实质点在段落中的出现情况(基础数据)
-4. 多维度支撑性评分:评估对灵感点、目的点、关键点、段落的支撑(基于共性数据)
-5. 分类整合:为实质点添加分类标签,确保同级正交
-
-# 工作原则
-- 名词化:所有实质点必须是名词形式
-- 定量化:所有评分必须有客观依据和定量指标
-- 同类去重:概念与概念去重,元素与元素去重,不做跨类型去重
-- 结构化:使用方括号标记分类,确保同级正交"""
-
-    def process(self, state: dict) -> dict:
-        """处理state,执行完整的元素提取流程
-
-        从state中提取所需数据,执行8步流程,并返回结果
-
-        Args:
-            state: 工作流状态,包含:
-                - video: 视频URL
-                - topic_selection_understanding: 选题理解结果
-                - section_division: 段落划分结果
-                - inspiration_points: 灵感点
-                - purpose_points: 目的点
-                - key_points: 关键点
-
-        Returns:
-            dict: 包含元素列表结果
-        """
-        logger.info("=== 开始脚本元素提取处理(完全重构版 - 8步流程) ===")
-
-        # 从state中获取视频文件
-        video_file = get_video_file_from_state(state)
-        if not video_file:
-            logger.error("无法从state中获取视频文件")
-            return {
-                "元素列表": [],
-                "视频倾向判断": {}
-            }
-
-        # 提取选题描述
-        topic_understanding = state.get("topic_selection_understanding", {})
-        topic_description = {
-            "主题": topic_understanding.get("主题", ""),
-            "描述": topic_understanding.get("描述", "")
-        }
-
-        # 提取段落划分结果
-        section_division = state.get("section_division", {})
-
-        # 提取灵感点、目的点、关键点
-        inspiration_points = state.get("inspiration_points", {})
-        purpose_points = state.get("purpose_points", [])
-        key_points = state.get("key_points", {})
-
-        # 执行8步流程
-        logger.info("执行完整的实质点提取流程(8步)")
-        elements_result = self._execute_8_steps_pipeline(
-            video_file=video_file,
-            topic_description=topic_description,
-            section_division=section_division,
-            inspiration_points=inspiration_points,
-            purpose_points=purpose_points,
-            key_points=key_points
-        )
-
-        # 打印JSON结构
-        logger.info(f"元素提取最终结果:\n{json.dumps(elements_result, ensure_ascii=False, indent=2)}")
-
-        return {
-            "元素列表": elements_result.get("元素列表", []),
-            "视频倾向判断": elements_result.get("视频倾向判断", {})
-        }
-
-    def _execute_8_steps_pipeline(
-        self,
-        video_file,
-        topic_description: dict,
-        section_division: dict,
-        inspiration_points: dict,
-        purpose_points: list,
-        key_points: dict
-    ) -> dict:
-        """执行完整的8步流程
-
-        每一步都是独立的LLM调用
-        """
-        logger.info("\n" + "█" * 80)
-        logger.info("█" + " " * 78 + "█")
-        logger.info("█" + " " * 20 + "【实质点提取】8步流程(每步独立)" + " " * 21 + "█")
-        logger.info("█" + " " * 78 + "█")
-        logger.info("█" * 80)
-
-        # Step 1: 判断视频倾向
-        logger.info("\n▶ Step 1: 判断视频倾向(概念 or 元素更重要)")
-        tendency_result = self._step1_judge_tendency(
-            video_file, topic_description
-        )
-
-        # Step 2: 主视角提取
-        logger.info("\n▶ Step 2: 主视角提取(从更重要的视角提取)")
-        primary_result = self._step2_extract_primary(
-            tendency_result, video_file, topic_description
-        )
-
-        # Step 3: 辅视角提取
-        logger.info("\n▶ Step 3: 辅视角提取(从另一个视角提取,并与主视角去重)")
-        secondary_result = self._step3_extract_secondary(
-            tendency_result, primary_result, video_file, topic_description
-        )
-
-        # 合并主视角和辅视角的结果
-        all_substantials = primary_result.get("实质点列表", []) + secondary_result.get("实质点列表", [])
-        logger.info(f"合并后总计: {len(all_substantials)} 个实质点")
-
-        # Step 4: 共性定量分析 + 段落支撑评分(合并)
-        logger.info("\n▶ Step 4: 共性定量分析 + 段落支撑评分(合并)")
-        analyzed_result = self._step4_quantitative_analysis(
-            all_substantials,
-            video_file,
-            section_division
-        )
-
-        # Step 5: 多维度支撑评分(3个维度并行 - 基于共性数据评分)
-        logger.info("\n▶ Step 5: 多维度支撑评分(3个维度并行 - 基于共性数据)")
-        scored_result = self._step5_multi_dimensional_scoring(
-            analyzed_result,
-            video_file,
-            inspiration_points,
-            purpose_points,
-            key_points
-        )
-
-        # Step 6: 筛选
-        logger.info("\n▶ Step 6: 筛选核心实质点")
-        filtered_result = self._step6_filter(scored_result)
-
-        # Step 7: 去重(同类去重)
-        logger.info("\n▶ Step 7: 去重(概念与概念,元素与元素)")
-        deduplicated_result = self._step7_deduplicate(
-            filtered_result
-        )
-
-        # Step 8: 整合
-        logger.info("\n▶ Step 8: 整合(生成树状结构)")
-        final_result = self._step8_integrate(
-            deduplicated_result
-        )
-
-        # 在最终结果中添加 step1 的判断结果(这个是全局的)
-        final_result["视频倾向判断"] = tendency_result
-
-        return final_result
-
-    def _step1_judge_tendency(
-        self,
-        video_file,
-        topic_description: dict
-    ) -> dict:
-        """Step 1: 判断视频倾向
-
-        判断对于这个视频来说,概念更重要还是元素更重要
-
-        参数说明:
-        - video_file: Gemini视频文件对象
-        - topic_description: 选题描述,帮助理解视频主题和方向
-        """
-        logger.info("=" * 80)
-        logger.info("【Step 1】判断视频倾向 - 概念 or 元素更重要")
-        logger.info("=" * 80)
-
-        # 构建内容
-        topic_text = self._build_topic_text(topic_description)
-
-        # 构建prompt
-        prompt = f"""# 选题描述
-
-{topic_text}
-
-# 任务
-
-判断对于**这个视频**来说,**概念更重要**还是**元素更重要**。
-
-## 定义
-
-### 概念(Concept)
-- 抽象的名词,不能是形式的
-
-### 元素(Element)
-- 具体的、客观存在的实体,视觉可见
-
-## 判断标准
-
-### 概念更重要
-视频的核心价值在于**抽象的方法论、流程、原理**:
-- 强调过程和方法
-- 强调原理和知识传递
-- 强调教学、指导、分享经验
-
-### 元素更重要
-视频的核心价值在于**具体的物品、实物展示**:
-- 强调物品本身
-- 强调视觉呈现和展示
-- 强调具体实物的特征
-
-## 判断流程
-
-1. **分析视频核心价值**:这个视频的核心价值是什么?
-2. **识别主要内容类型**:是方法论/流程/原理,还是物品/实物展示?
-3. **评估抽象 vs 具体的比重**:哪个占主导地位?
-4. **做出判断**:概念更重要 or 元素更重要
-
-## 输出要求
-
-- 必须明确给出判断结果
-- 必须给出充分的理由
-- 必须引用视频内容作为证据
-
-# 输出(JSON)
-
-{{
-  "判断结果": "概念更重要/元素更重要",
-  "核心价值": "这个视频的核心价值是什么",
-  "主要内容类型": "方法论/流程/原理/物品展示/实物介绍等",
-  "抽象vs具体比重": "抽象内容占X%,具体内容占Y%(估算)",
-  "判断理由": "为什么做出这个判断(引用视频内容作为证据)",
-  "支撑证据": ["证据1:具体引用", "证据2:具体引用"]
-}}"""
-
-        # 调用LLM(使用视频分析接口)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "判断视频倾向",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"判断结果": "概念更重要"}
-        )
-
-        # 打印结果
-        logger.info(f"Step 1 结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _step2_extract_primary(
-        self,
-        tendency_result: dict,
-        video_file,
-        topic_description: dict
-    ) -> dict:
-        """Step 2: 主视角提取
-
-        从更重要的视角提取(概念 or 元素)
-
-        职责:只负责提取实质点,不做深度分析
-        - 全面识别候选实质点
-        - 后续由 Step 4 负责统计和深度理解
-
-        参数说明:
-        - video_file: Gemini视频文件对象
-        - topic_description: 选题描述,帮助理解提取范围和主题方向
-        """
-        logger.info("=" * 80)
-        logger.info("【Step 2】主视角提取 - 从更重要的视角提取")
-        logger.info("=" * 80)
-
-        # 获取判断结果
-        tendency = tendency_result.get("判断结果", "概念更重要")
-        primary_type = "概念" if tendency == "概念更重要" else "元素"
-
-        logger.info(f"主视角类型: {primary_type}")
-
-        # 构建内容
-        topic_text = self._build_topic_text(topic_description)
-
-        # 构建prompt
-        prompt = f"""# 选题描述(仅供参考理解视频方向)
-
-{topic_text}
-
-**重要约束**:选题描述仅用于帮助你理解视频的主题方向和定位,**不要从选题描述中提取实质点**。所有实质点必须从**视频实际内容**中提取。
-
-# 视频倾向判断
-
-{tendency_result.get("判断理由", "")}
-
-# 任务
-
-从**{primary_type}视角**提取视频中的核心**{primary_type}**。
-
-## {primary_type}定义
-
-"""
-
-        if primary_type == "概念":
-            prompt += """### 概念(Concept)
-**定义**:抽象的名词,不能是形式的
-
-**特征**:
-- ✅ 必须是名词
-- ✅ 抽象的,非具象的
-- ✅ 不能是形式的(不是风格、颜色、材质等形式特征)
-- ✅ 虽然没有直接提到,但隐含在内容中
-- ✅ 对理解视频核心价值至关重要
-
-**原子名词约束**:
-- ❌ 禁止形容词
-- ❌ 禁止动词
-- ✅ 只能是纯粹的名词形式
-"""
-        else:
-            prompt += """### 元素(Element)
-**定义**:具体的、客观存在的实体,视觉可见
-
-**特征**:
-- ✅ 必须是名词
-- ✅ 具体的物体,视觉可见
-- ✅ 客观存在的实体
-- ✅ 使用原子名词(最基础的名词形式,去除修饰语)
-- ❌ 不是动作、状态、情绪
-- ❌ 不是抽象概念
-"""
-
-        prompt += f"""
-## 提取原则
-
-1. **全面识别**:宁可多识别,后续会筛选
-2. **名词化**:所有{primary_type}必须是名词形式
-3. **原子化**:使用最基础的名词,去除修饰语
-4. **聚焦主视角**:只提取{primary_type},不提取另一种类型
-5. **仅从视频内容提取**:只从视频中提取,**禁止从选题描述中提取**
-
-## 输出要求
-
-每个{primary_type}需要标注:
-- 名称:原子名词
-- 识别依据:为什么识别这个{primary_type}(简要说明)
-
-# 输出(JSON)
-
-{{{{
-  "实质点列表": [
-    {{{{
-      "名称": "{primary_type}名称(原子名词)",
-      "类型": "{primary_type}",
-      "识别依据": "为什么识别这个{primary_type}"
-    }}}}
-  ]
-}}}}"""
-
-        # 调用LLM(使用视频分析接口)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            f"主视角提取({primary_type})",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"实质点列表": []}
-        )
-
-        # 打印结果
-        logger.info(f"Step 2 结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _step3_extract_secondary(
-        self,
-        tendency_result: dict,
-        primary_result: dict,
-        video_file,
-        topic_description: dict
-    ) -> dict:
-        """Step 3: 辅视角提取
-
-        从另一个视角提取,并与主视角去重
-
-        职责:只负责提取实质点,不做深度分析
-        - 从辅视角识别候选实质点
-        - 与主视角去重,避免重复提取
-        - 后续由 Step 4 负责统计和深度理解
-
-        参数说明:
-        - video_file: Gemini视频文件对象
-        - topic_description: 选题描述,帮助理解提取范围和主题方向
-        """
-        logger.info("=" * 80)
-        logger.info("【Step 3】辅视角提取 - 从另一个视角提取,并与主视角去重")
-        logger.info("=" * 80)
-
-        # 获取判断结果
-        tendency = tendency_result.get("判断结果", "概念更重要")
-        secondary_type = "元素" if tendency == "概念更重要" else "概念"
-
-        logger.info(f"辅视角类型: {secondary_type}")
-
-        # 构建内容
-        topic_text = self._build_topic_text(topic_description)
-
-        # 构建主视角实质点文本
-        primary_items = primary_result.get("实质点列表", [])
-        primary_text = ""
-        for item in primary_items:
-            primary_text += f"\n- {item.get('名称', 'N/A')}"
-
-        # 构建prompt
-        prompt = f"""# 选题描述(仅供参考理解视频方向)
-
-{topic_text}
-
-**重要约束**:选题描述仅用于帮助你理解视频的主题方向和定位,**不要从选题描述中提取实质点**。所有实质点必须从**视频实际内容**中提取。
-
-# 主视角已提取的实质点
-
-{primary_text}
-
-# 任务
-
-从**{secondary_type}视角**提取视频中的核心**{secondary_type}**,并与主视角提取的实质点去重。
-
-## {secondary_type}定义
-
-"""
-
-        if secondary_type == "概念":
-            prompt += """### 概念(Concept)
-**定义**:抽象的名词,不能是形式的
-
-**特征**:
-- ✅ 必须是名词
-- ✅ 抽象的,非具象的
-- ✅ 不能是形式的(不是风格、颜色、材质等形式特征)
-- ✅ 虽然没有直接提到,但隐含在内容中
-- ✅ 对理解视频核心价值至关重要
-
-**原子名词约束**:
-- ❌ 禁止形容词
-- ❌ 禁止动词
-- ✅ 只能是纯粹的名词形式
-"""
-        else:
-            prompt += """### 元素(Element)
-**定义**:具体的、客观存在的实体,视觉可见
-
-**特征**:
-- ✅ 必须是名词
-- ✅ 具体的物体,视觉可见
-- ✅ 客观存在的实体
-- ✅ 使用原子名词(最基础的名词形式,去除修饰语)
-- ❌ 不是动作、状态、情绪
-- ❌ 不是抽象概念
-"""
-
-        prompt += f"""
-## 提取原则
-
-1. **全面识别**:宁可多识别,后续会筛选
-2. **名词化**:所有{secondary_type}必须是名词形式
-3. **原子化**:使用最基础的名词,去除修饰语
-4. **聚焦辅视角**:只提取{secondary_type},不提取另一种类型
-5. **仅从视频内容提取**:只从视频中提取,**禁止从选题描述中提取**
-
-## 去重要求
-
-**关键**:与主视角提取的实质点去重
-
-- 如果{secondary_type}与主视角实质点存在语义关联,不要重复提取
-- 只提取主视角中没有覆盖到的{secondary_type}
-
-## 输出要求
-
-每个{secondary_type}需要标注:
-- 名称:原子名词
-- 识别依据:为什么识别这个{secondary_type}(简要说明)
-
-# 输出(JSON)
-
-{{{{
-  "实质点列表": [
-    {{{{
-      "名称": "{secondary_type}名称(原子名词)",
-      "类型": "{secondary_type}",
-      "识别依据": "为什么识别这个{secondary_type}"
-    }}}}
-  ]
-}}}}"""
-
-        # 调用LLM(使用视频分析接口)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            f"辅视角提取({secondary_type})",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"实质点列表": []}
-        )
-
-        # 打印结果
-        logger.info(f"Step 3 结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _step4_quantitative_analysis(
-        self,
-        all_substantials: list,
-        video_file,
-        section_division: dict
-    ) -> dict:
-        """Step 4: 共性定量分析 + 段落支撑评分(合并)
-
-        对所有实质点进行:
-        1. 共性定量分析:出现段落数、段落覆盖率、出现频次
-        2. 段落支撑评分:评估每个实质点在各段落中的支撑得分(0-10分)
-        """
-        logger.info("=" * 80)
-        logger.info("【Step 4】共性定量分析 + 段落支撑评分")
-        logger.info("=" * 80)
-
-        if not all_substantials:
-            logger.warning("⚠️  没有实质点可供分析")
-            return {"分析结果": []}
-
-        logger.info(f"输入: {len(all_substantials)} 个实质点")
-
-        # 构建内容
-        section_text = self._build_section_text(section_division)
-
-        # 提取段落总数
-        total_sections = self._count_sections(section_division)
-
-        # 构建实质点列表文本
-        substantials_text = ""
-        for s in all_substantials:
-            substantials_text += f"\n- {s.get('名称', 'N/A')} ({s.get('类型', 'N/A')})"
-            substantials_text += f"\n  识别依据: {s.get('识别依据', 'N/A')}"
-
-        # 构建prompt
-        prompt = f"""# 段落划分结果
-
-{section_text}
-
-段落总数: {total_sections}
-
-# 实质点列表
-
-{substantials_text}
-
-# 任务
-
-对每个实质点进行**共性定量分析 + 段落支撑评分**。
-
-**重要**:只输出新增的分析数据和评分数据,不要重复输出名称、类型、识别依据等已有信息。
-
-## 第一部分:共性分析(元素和概念统一指标)
-
-1. **出现段落数**:该实质点在多少个段落中出现(整数)
-2. **段落覆盖率**:出现段落数 / 总段落数(百分比,保留2位小数)
-3. **出现频次**:该实质点总共出现多少次(整数)
-4. **出现段落列表**:该实质点出现的段落ID列表
-
-**关键约束 - 段落统计规则**:
-- **只统计具体段落**(叶子节点),不统计抽象段落(父节点)
-- 例如:段落1是段落1.1的父节点,如果元素出现在段落1.1中,只记录段落1.1,不记录段落1
-- 判断方法:有子项的段落是抽象段落,没有子项的段落是具体段落
-- 段落总数={total_sections}已经是具体段落的总数
-
-**说明**:
-- 元素:统计该元素在视频中出现的段落和频次
-- 概念:统计该概念在视频中被提及或隐含的段落和频次
-
-## 第二部分:段落支撑评分(每个段落0-10分)
-
-评估该实质点在每个出现的段落中的权重/重要性
-
-**关键约束 - 段落评分规则**:
-- **只评估具体段落**(叶子节点),不评估抽象段落(父节点)
-- 与第一部分的段落统计保持一致
-- 段落ID必须与"出现段落列表"中的ID一致
-
-**评分标准**:
-- 9-10分:段落核心元素,不可或缺
-- 7-8分:段落重要元素,有明显作用
-- 5-6分:段落一般元素,有一定作用
-- 3-4分:段落辅助元素,作用较小
-- 0-2分:段落边缘元素或未出现
-
-**依据要求**:
-- 可以参考共性分析中的段落出现数据
-- 必须逐个段落评分(只对出现的段落评分)
-- 必须说明在该段落中的具体表现
-- 必须提供客观的、直接的证据
-
-## 输出要求
-
-- 定量指标必须准确计算
-- 所有评分必须有客观依据
-- **只输出名称和对应的分析+评分数据,不要重复已有字段**
-
-# 输出(JSON)
-
-{{
-  "分析结果": [
-    {{
-      "名称": "实质点名称",
-      "出现段落数": 3,
-      "段落覆盖率": 0.75,
-      "出现频次": 5,
-      "出现段落列表": ["段落1", "段落1.1", "段落2.1"],
-      "支撑段落详情": [
-        {{
-          "段落ID": "段落1",
-          "得分": 8,
-          "依据": "在该段落中的具体表现 + 客观证据"
-        }},
-        {{
-          "段落ID": "段落1.1",
-          "得分": 6,
-          "依据": "在该段落中的具体表现 + 客观证据"
-        }}
-      ]
-    }}
-  ]
-}}
-
-**注意**:
-- 元素和概念使用统一的分析指标
-- **出现段落列表**: 必须使用具体段落ID列表(只包含叶子节点,不包含父节点)。例如:如果元素出现在段落1.1中,只记录["段落1.1"],不记录["段落1", "段落1.1"]
-- **支撑段落详情**: 必须使用段落ID字段,包含得分和依据,只对具体段落评分(与"出现段落列表"一致)
-- 不要输出类型、识别依据等字段,这些会由代码自动合并"""
-
-        # 调用LLM(使用视频分析接口)
-        llm_result = LLMInvoker.safe_invoke_video_analysis(
-            "共性定量分析+段落支撑评分",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"分析结果": []}
-        )
-
-        # 代码合并:将原始实质点数据与分析数据合并
-        analyzed_items = []
-        analysis_results = llm_result.get("分析结果", [])
-
-        for analysis in analysis_results:
-            analysis_name = analysis.get("名称", "")
-            # 找到对应的原始实质点
-            original = None
-            for s in all_substantials:
-                if s.get("名称") == analysis_name:
-                    original = s
-                    break
-
-            if original:
-                # 合并数据:原始数据 + 分析数据
-                merged_item = {
-                    **original,  # 保留原始的名称、类型、识别依据
-                    **{k: v for k, v in analysis.items() if k != "名称"}  # 添加分析数据(排除名称)
-                }
-                analyzed_items.append(merged_item)
-            else:
-                logger.warning(f"⚠️  未找到实质点 '{analysis_name}' 的原始数据")
-                # 如果找不到原始数据,仍然保留分析结果
-                analyzed_items.append(analysis)
-
-        result = {"分析结果": analyzed_items}
-
-        # 打印结果
-        logger.info(f"Step 4 合并后结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _step5_multi_dimensional_scoring(
-        self,
-        analyzed_result: dict,
-        video_file,
-        inspiration_points: dict,
-        purpose_points: list,
-        key_points: dict
-    ) -> dict:
-        """Step 5: 多维度支撑评分(3个维度并行评估)
-
-        对所有实质点进行3个维度的支撑性评分(并行):
-        1. 支撑灵感点
-        2. 支撑目的点
-        3. 支撑关键点
-
-        注意:段落支撑评分已经在Step4中完成
-
-        参数说明:
-        - analyzed_result: 包含共性分析数据和段落支撑评分
-        - video_file: Gemini视频文件对象
-        - inspiration_points/purpose_points/key_points: 用于评估支撑性
-        """
-        logger.info("=" * 80)
-        logger.info("【Step 5】多维度支撑评分 - 3个维度并行评估")
-        logger.info("=" * 80)
-
-        analyzed_items = analyzed_result.get("分析结果", [])
-        if not analyzed_items:
-            logger.warning("⚠️  没有分析结果可供评分")
-            return {"评分结果": []}
-
-        logger.info(f"输入: {len(analyzed_items)} 个分析实质点")
-
-        # 并行评估3个维度(通过3次独立的LLM调用)
-        logger.info("\n开始并行评估3个维度...")
-
-        # 评估维度1: 支撑灵感点
-        logger.info("  [1/3] 评估支撑灵感点...")
-        inspiration_scoring = self._evaluate_inspiration_support(
-            analyzed_items, video_file, inspiration_points
-        )
-
-        # 评估维度2: 支撑目的点
-        logger.info("  [2/3] 评估支撑目的点...")
-        purpose_scoring = self._evaluate_purpose_support(
-            analyzed_items, video_file, purpose_points
-        )
-
-        # 评估维度3: 支撑关键点
-        logger.info("  [3/3] 评估支撑关键点...")
-        key_scoring = self._evaluate_key_support(
-            analyzed_items, video_file, key_points
-        )
-
-        # 合并3个维度的评分结果
-        logger.info("\n合并3个维度的评分结果...")
-        scored_items = self._merge_scoring_results(
-            analyzed_items,
-            inspiration_scoring,
-            purpose_scoring,
-            key_scoring
-        )
-
-        result = {"评分结果": scored_items}
-
-        # 打印结果
-        logger.info(f"Step 5 合并后结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _evaluate_inspiration_support(
-        self,
-        analyzed_items: list,
-        video_file,
-        inspiration_points: dict
-    ) -> dict:
-        """评估支撑灵感点"""
-        # 构建内容
-        items_text = self._build_analyzed_items_text(analyzed_items)
-        inspiration_text = self._build_points_text("灵感点", inspiration_points)
-
-        # 构建prompt
-        prompt = f"""# 实质点列表(含共性分析数据和段落支撑评分)
-
-{items_text}
-
-# 灵感点
-
-{inspiration_text}
-
-# 任务
-
-对每个实质点进行**支撑灵感点评分**(0-10分)。
-
-**重要**:只输出实质点名称和灵感点评分,不要输出其他已有信息。
-
-## 评分维度:支撑灵感点(0-10分)
-
-评估该实质点如何支撑灵感点(触发创作者的灵感)
-
-**评分标准**:
-- 9-10分:核心支撑,直接触发创作灵感
-- 7-8分:重要支撑,对灵感有明显作用
-- 5-6分:一定支撑,有一定触发作用
-- 3-4分:辅助支撑,间接相关
-- 0-2分:弱支撑或无关
-
-**依据要求**:
-- 必须引用具体的灵感点
-- 必须说明实质点如何支撑该灵感点
-- 必须提供客观的、直接的证据(引用视频内容)
-- 可以利用共性分析数据和段落支撑评分辅助判断
-
-## 输出要求
-
-- 所有评分必须有客观依据
-- 所有依据必须引用视频内容
-- **只输出名称和灵感点评分数据**
-
-# 输出(JSON)
-
-{{
-  "评分结果": [
-    {{
-      "名称": "实质点名称",
-      "支撑灵感点得分": 8,
-      "支撑灵感点依据": "引用具体灵感点 + 如何支撑 + 客观证据"
-    }}
-  ]
-}}
-
-**注意**:
-- 只输出名称和支撑灵感点的评分数据
-- 不要输出类型、识别依据、共性分析、段落支撑等字段"""
-
-        # 调用LLM(使用视频分析接口)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "评估支撑灵感点",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"评分结果": []}
-        )
-
-        return result
-
-    def _evaluate_purpose_support(
-        self,
-        analyzed_items: list,
-        video_file,
-        purpose_points: list
-    ) -> dict:
-        """评估支撑目的点"""
-        # 构建内容
-        items_text = self._build_analyzed_items_text(analyzed_items)
-        purpose_text = self._build_points_text("目的点", purpose_points)
-
-        # 构建prompt
-        prompt = f"""# 实质点列表(含共性分析数据和段落支撑评分)
-
-{items_text}
-
-# 目的点
-
-{purpose_text}
-
-# 任务
-
-对每个实质点进行**支撑目的点评分**(0-10分)。
-
-**重要**:只输出实质点名称和目的点评分,不要输出其他已有信息。
-
-## 评分维度:支撑目的点(0-10分)
-
-评估该实质点如何支撑目的点(帮助达成创作目的)
-
-**评分标准**:
-- 9-10分:核心支撑,直接承载创作目的
-- 7-8分:重要支撑,对目的达成有明显作用
-- 5-6分:一定支撑,有一定帮助
-- 3-4分:辅助支撑,间接相关
-- 0-2分:弱支撑或无关
-
-**依据要求**:
-- 必须引用具体的目的点
-- 必须说明实质点如何支撑该目的点
-- 必须提供客观的、直接的证据(引用视频内容)
-- 可以利用共性分析数据和段落支撑评分辅助判断
-
-## 输出要求
-
-- 所有评分必须有客观依据
-- 所有依据必须引用视频内容
-- **只输出名称和目的点评分数据**
-
-# 输出(JSON)
-
-{{
-  "评分结果": [
-    {{
-      "名称": "实质点名称",
-      "支撑目的点得分": 7,
-      "支撑目的点依据": "引用具体目的点 + 如何支撑 + 客观证据"
-    }}
-  ]
-}}
-
-**注意**:
-- 只输出名称和支撑目的点的评分数据
-- 不要输出类型、识别依据、共性分析、段落支撑等字段"""
-
-        # 调用LLM(使用视频分析接口)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "评估支撑目的点",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"评分结果": []}
-        )
-
-        return result
-
-    def _evaluate_key_support(
-        self,
-        analyzed_items: list,
-        video_file,
-        key_points: dict
-    ) -> dict:
-        """评估支撑关键点"""
-        # 构建内容
-        items_text = self._build_analyzed_items_text(analyzed_items)
-        key_text = self._build_points_text("关键点", key_points)
-
-        # 构建prompt
-        prompt = f"""# 实质点列表(含共性分析数据和段落支撑评分)
-
-{items_text}
-
-# 关键点
-
-{key_text}
-
-# 任务
-
-对每个实质点进行**支撑关键点评分**(0-10分)。
-
-**重要**:只输出实质点名称和关键点评分,不要输出其他已有信息。
-
-## 评分维度:支撑关键点(0-10分)
-
-评估该实质点如何支撑关键点(吸引消费者)
-
-**评分标准**:
-- 9-10分:核心支撑,直接吸引消费者
-- 7-8分:重要支撑,有明显吸引力
-- 5-6分:一定支撑,有一定吸引作用
-- 3-4分:辅助支撑,间接相关
-- 0-2分:弱支撑或无关
-
-**依据要求**:
-- 必须引用具体的关键点
-- 必须说明实质点如何支撑该关键点
-- 必须提供客观的、直接的证据(引用视频内容)
-- 可以利用共性分析数据和段落支撑评分辅助判断
-
-## 输出要求
-
-- 所有评分必须有客观依据
-- 所有依据必须引用视频内容
-- **只输出名称和关键点评分数据**
-
-# 输出(JSON)
-
-{{
-  "评分结果": [
-    {{
-      "名称": "实质点名称",
-      "支撑关键点得分": 9,
-      "支撑关键点依据": "引用具体关键点 + 如何支撑 + 客观证据"
-    }}
-  ]
-}}
-
-**注意**:
-- 只输出名称和支撑关键点的评分数据
-- 不要输出类型、识别依据、共性分析、段落支撑等字段"""
-
-        # 调用LLM(使用视频分析接口)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "评估支撑关键点",
-            video_file,
-            prompt,
-            agent=self,
-            fallback={"评分结果": []}
-        )
-
-        return result
-
-    def _merge_scoring_results(
-        self,
-        analyzed_items: list,
-        inspiration_scoring: dict,
-        purpose_scoring: dict,
-        key_scoring: dict
-    ) -> list:
-        """合并3个维度的评分结果"""
-        # 创建名称到评分的映射
-        inspiration_map = {item.get("名称"): item for item in inspiration_scoring.get("评分结果", [])}
-        purpose_map = {item.get("名称"): item for item in purpose_scoring.get("评分结果", [])}
-        key_map = {item.get("名称"): item for item in key_scoring.get("评分结果", [])}
-
-        # 合并所有数据
-        scored_items = []
-        for analyzed_item in analyzed_items:
-            name = analyzed_item.get("名称")
-
-            # 合并数据
-            merged_item = {
-                **analyzed_item,  # 保留分析数据(包含段落支撑评分)
-            }
-
-            # 添加灵感点评分
-            if name in inspiration_map:
-                inspiration_data = inspiration_map[name]
-                merged_item["支撑灵感点得分"] = inspiration_data.get("支撑灵感点得分", 0)
-                merged_item["支撑灵感点依据"] = inspiration_data.get("支撑灵感点依据", "")
-
-            # 添加目的点评分
-            if name in purpose_map:
-                purpose_data = purpose_map[name]
-                merged_item["支撑目的点得分"] = purpose_data.get("支撑目的点得分", 0)
-                merged_item["支撑目的点依据"] = purpose_data.get("支撑目的点依据", "")
-
-            # 添加关键点评分
-            if name in key_map:
-                key_data = key_map[name]
-                merged_item["支撑关键点得分"] = key_data.get("支撑关键点得分", 0)
-                merged_item["支撑关键点依据"] = key_data.get("支撑关键点依据", "")
-
-            scored_items.append(merged_item)
-
-        return scored_items
-
-    def _build_analyzed_items_text(self, analyzed_items: list) -> str:
-        """构建分析实质点列表文本(包含共性分析数据和段落支撑评分)"""
-        items_text = ""
-        for item in analyzed_items:
-            items_text += f"\n- {item.get('名称', 'N/A')} ({item.get('类型', 'N/A')})"
-            items_text += f"\n  出现段落数: {item.get('出现段落数', 0)}"
-            items_text += f"\n  段落覆盖率: {item.get('段落覆盖率', 0)}"
-            items_text += f"\n  出现频次: {item.get('出现频次', 0)}"
-
-            # 添加段落支撑评分摘要
-            section_details = item.get('支撑段落详情', [])
-            if section_details:
-                avg_section_score = sum(s.get("得分", 0) for s in section_details) / len(section_details)
-                items_text += f"\n  段落支撑平均得分: {avg_section_score:.1f}"
-
-        return items_text
-    def _step6_filter(self, scored_result: dict) -> dict:
-        """Step 6: 筛选
-
-        筛选标准:
-        1. 支撑性综合得分 > 6.0
-        2. 或者任一单项得分 > 8.0
-        3. 或者共性得分 > 7.0(基于多段落共性数据)
-        """
-        logger.info("=" * 80)
-        logger.info("【Step 6】筛选核心实质点")
-        logger.info("=" * 80)
-
-        # 获取评分结果
-        scored_items = scored_result.get("评分结果", [])
-        if not scored_items:
-            logger.warning("⚠️  没有评分结果可供筛选")
-            return {"筛选结果": []}
-
-        logger.info(f"输入: {len(scored_items)} 个评分实质点")
-        logger.info("\n筛选规则:")
-        logger.info("  1. 综合得分 > 6.0")
-        logger.info("  2. 或任一单项得分 > 8.0")
-        logger.info("  3. 或共性得分 > 7.0")
-
-        filtered_items = []
-
-        for item in scored_items:
-            name = item.get("名称", "未知")
-
-            # 提取支撑性得分
-            inspiration_score = item.get("支撑灵感点得分", 0)
-            purpose_score = item.get("支撑目的点得分", 0)
-            key_score = item.get("支撑关键点得分", 0)
-
-            # 计算平均段落得分
-            section_details = item.get("支撑段落详情", [])
-            avg_section_score = 0
-            if section_details:
-                avg_section_score = sum(s.get("得分", 0) for s in section_details) / len(section_details)
-
-            # 计算综合得分
-            comprehensive_score = (inspiration_score + purpose_score + key_score + avg_section_score) / 4
-
-            # 判断是否通过筛选
-            max_single_score = max(inspiration_score, purpose_score, key_score, avg_section_score)
-
-            # 计算共性得分(基于 Step 4 的共性数据)
-            # 统一计算方式:覆盖率(70%) + 频次归一化(30%)
-            coverage_rate = item.get("段落覆盖率", 0)  # 0-1
-            frequency = item.get("出现频次", 0)  # 整数
-
-            # 频次归一化:假设超过10次为满分
-            normalized_frequency = min(frequency / 10, 1.0)
-
-            # 共性得分 = 覆盖率权重(70%) + 频次权重(30%)
-            # 转换为 0-10 分制
-            commonality_score = (coverage_rate * 0.7 + normalized_frequency * 0.3) * 10
-
-            # 三个条件满足任意一个即通过
-            if comprehensive_score > 6.0 or max_single_score > 8.0 or commonality_score > 7.0:
-                logger.info(
-                    f"  ✓ {name}: 通过筛选 "
-                    f"(综合={comprehensive_score:.2f}, "
-                    f"最高单项={max_single_score:.2f}, "
-                    f"共性={commonality_score:.2f})"
-                )
-                filtered_items.append(item)
-            else:
-                logger.info(
-                    f"  ✗ {name}: 未通过筛选 "
-                    f"(综合={comprehensive_score:.2f}, "
-                    f"最高单项={max_single_score:.2f}, "
-                    f"共性={commonality_score:.2f})"
-                )
-
-        # 打印结果
-        logger.info(f"Step 6 结果:\n{json.dumps({'筛选结果': filtered_items}, ensure_ascii=False, indent=2)}")
-
-        return {"筛选结果": filtered_items}
-
-    def _step7_deduplicate(
-        self,
-        filtered_result: dict
-    ) -> dict:
-        """Step 7: 去重(同类去重)
-
-        只进行同类去重:概念与概念去重,元素与元素去重
-
-        参数说明:
-        - filtered_result: 筛选后的实质点,包含所有历史数据(名称、类型、识别依据、共性分析、评分等)
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("【Step 7】去重 - 同类去重(概念与概念,元素与元素)")
-        logger.info("=" * 80)
-
-        filtered_items = filtered_result.get("筛选结果", [])
-        if not filtered_items:
-            logger.warning("⚠️  没有筛选结果可供去重")
-            return {"去重结果": []}
-
-        logger.info(f"输入: {len(filtered_items)} 个筛选后的实质点")
-
-        # 构建筛选实质点文本(包含关键信息辅助去重判断)
-        items_text = ""
-        for item in filtered_items:
-            items_text += f"\n- {item.get('名称', 'N/A')} ({item.get('类型', 'N/A')})"
-            items_text += f"\n  识别依据: {item.get('识别依据', 'N/A')}"
-
-            # 计算综合得分
-            inspiration_score = item.get("支撑灵感点得分", 0)
-            purpose_score = item.get("支撑目的点得分", 0)
-            key_score = item.get("支撑关键点得分", 0)
-            section_details = item.get("支撑段落详情", [])
-            avg_section_score = 0
-            if section_details:
-                avg_section_score = sum(s.get("得分", 0) for s in section_details) / len(section_details)
-            comprehensive_score = (inspiration_score + purpose_score + key_score + avg_section_score) / 4
-
-            items_text += f"\n  综合得分: {comprehensive_score:.2f}"
-            items_text += f"\n  出现段落数: {item.get('出现段落数', 0)}"
-
-        # 构建prompt
-        prompt = f"""# 筛选后的实质点列表
-
-{items_text}
-
-# 任务
-
-对筛选后的实质点进行**同类去重**。
-
-**重要**:只输出去重决策和保留的实质点名称列表,不要输出完整的实质点数据。
-
-## 去重规则
-
-**核心原则**:只进行同类去重,概念与概念去重,元素与元素去重,不做跨类型去重。
-
-### 规则1: 概念 vs 概念
-- 如果两个概念语义相似或有包含关系,保留**综合得分更高**的那个
-
-### 规则2: 元素 vs 元素
-- 如果两个元素是同一类物体的不同表述,保留**综合得分更高**的那个
-
-**重要约束**:
-- ✅ 概念只与概念比较去重
-- ✅ 元素只与元素比较去重
-- ❌ 概念与元素之间**不进行**去重比较
-
-## 去重流程
-
-1. **分组**:将实质点按类型分为概念组和元素组
-2. **概念组内去重**:找出概念组内语义相似的概念,保留得分更高的
-3. **元素组内去重**:找出元素组内同类物体,保留得分更高的
-4. **合并结果**:将去重后的概念和元素合并
-
-# 输出(JSON)
-
-{{
-  "去重决策": [
-    {{
-      "冲突组": ["实质点1", "实质点2"],
-      "冲突类型": "概念vs概念/元素vs元素",
-      "应用规则": "规则1/规则2",
-      "保留": "实质点1",
-      "去除": ["实质点2"],
-      "决策理由": "为什么保留实质点1,去除实质点2"
-    }}
-  ],
-  "保留列表": ["实质点名称1", "实质点名称2", ...]
-}}
-
-**注意**:
-- 只输出去重决策和保留的实质点名称
-- 不要输出完整的实质点数据(类型、识别依据、评分等)
-- 这些完整数据会由代码自动填充"""
-
-        # 构建消息
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        # 调用LLM
-        llm_result = LLMInvoker.safe_invoke(
-            self,
-            "去重",
-            messages,
-            fallback={"保留列表": []}
-        )
-
-        # 代码执行去重:基于保留列表提取完整数据
-        deduplicated_items = []
-        retained_names = llm_result.get("保留列表", [])
-
-        for name in retained_names:
-            # 找到对应的完整数据
-            full_item = None
-            for item in filtered_items:
-                if item.get("名称") == name:
-                    full_item = item
-                    break
-
-            if full_item:
-                deduplicated_items.append(full_item)
-            else:
-                logger.warning(f"⚠️  未找到实质点 '{name}' 的完整数据")
-
-        result = {
-            "去重决策": llm_result.get("去重决策", []),
-            "去重结果": deduplicated_items
-        }
-
-        # 打印结果
-        logger.info(f"Step 7 合并后结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _step8_integrate(
-        self,
-        deduplicated_result: dict
-    ) -> dict:
-        """Step 8: 整合
-
-        为实质点添加分类标签(用方括号表示)
-
-        参数说明:
-        - deduplicated_result: 去重后的实质点列表,包含所有历史数据(名称、类型、识别依据、共性分析、评分等)
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("【Step 8】整合 - 为实质点添加分类标签")
-        logger.info("=" * 80)
-
-        deduplicated_items = deduplicated_result.get("去重结果", [])
-        if not deduplicated_items:
-            logger.warning("⚠️  没有去重结果可供整合")
-            return {"元素列表": []}
-
-        logger.info(f"输入: {len(deduplicated_items)} 个去重后的实质点")
-
-        # 构建去重实质点文本(包含关键信息辅助分类)
-        items_text = ""
-        for item in deduplicated_items:
-            items_text += f"\n- {item.get('名称', 'N/A')} ({item.get('类型', 'N/A')})"
-            items_text += f"\n  识别依据: {item.get('识别依据', 'N/A')}"
-
-        # 构建prompt
-        prompt = f"""# 去重后的实质点列表
-
-{items_text}
-
-# 任务
-
-为每个实质点分配分类,分类用字典表示层级关系。
-
-**重要**:只输出实质点名称和对应的分类字典,不要输出完整的实质点数据。
-
-## 分类规则
-
-**核心原则**:分类基于实质点本身的 What 维度(本质属性)
-
-- **What 维度**:回答"这是什么类型的东西"
-- **分类来源**:从实质点本身的本质属性中抽象出分类
-- **同级正交**:同一分类下的节点应该具有相同的 What 维度
-
-**MECE分类视角**:
-- 物理维度:基于物理属性
-- 化学维度:基于化学成分
-- 生物维度:基于生物分类
-- 功能维度:基于客观功能
-
-**禁止**:
-- ❌ 从"对其他事物的影响/效果"角度分类
-- ❌ 从"目的/价值"角度分类
-
-## 分类层级
-
-- 根据实际情况灵活确定层级深度
-
-## 输出要求
-
-- 同级正交:同一分类下的实质点应该具有相同的 What 维度
-- **只输出实质点名称和对应的分类字典**
-
-# 输出(JSON)
-
-{{
-  "分类结果": [
-    {{
-      "名称": "实质点名称",
-      "分类": {{"一级": "分类名", "二级": "子分类名"}}
-    }}
-  ]
-}}
-
-**注意**:
-- 只输出名称和分类字典
-- 分类字典的key必须是"一级"、"二级"、"三级"等
-- 不要输出类型、识别依据、评分等字段
-- 这些完整数据会由代码自动填充"""
-
-        # 构建消息
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        # 调用LLM
-        llm_result = LLMInvoker.safe_invoke(
-            self,
-            "整合(添加分类)",
-            messages,
-            fallback={"分类结果": []}
-        )
-
-        # 代码合并:将分类添加到实质点中
-        integrated_items = []
-        classification_results = llm_result.get("分类结果", [])
-
-        for classification in classification_results:
-            name = classification.get("名称", "")
-            category = classification.get("分类", "")
-
-            # 找到对应的完整数据
-            full_item = None
-            for item in deduplicated_items:
-                if item.get("名称") == name:
-                    full_item = item
-                    break
-
-            if full_item:
-                # 创建新的实质点对象,添加分类字段
-                integrated_item = {
-                    **full_item,  # 保留所有历史数据
-                    "分类": category  # 添加分类字段
-                }
-                integrated_items.append(integrated_item)
-            else:
-                logger.warning(f"⚠️  未找到实质点 '{name}' 的完整数据")
-
-        result = {"元素列表": integrated_items}
-
-        # 打印结果
-        logger.info(f"Step 8 整合后结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    # ========== 辅助方法 ==========
-
-    def _build_video_content(self, video_url: str) -> str:
-        """构建视频内容文本"""
-        if video_url:
-            return f"视频URL: {video_url}"
-        return "无视频信息"
-
-    def _build_topic_text(self, topic_description: dict) -> str:
-        """构建选题描述文本"""
-        parts = []
-        if topic_description.get("主题"):
-            parts.append(f"主题: {topic_description['主题']}")
-        if topic_description.get("描述"):
-            parts.append(f"描述: {topic_description['描述']}")
-        return "\n".join(parts) if parts else "无选题描述"
-
-    def _build_section_text(self, section_division: dict) -> str:
-        """构建段落划分文本(递归展示所有层级,包含ID)"""
-        if not section_division:
-            return "无段落划分信息"
-
-        sections = section_division.get("段落列表", [])
-        if not sections:
-            return "无段落信息"
-
-        def build_section_list(section_list, indent=0):
-            """递归构建段落列表文本"""
-            text = ""
-            for section in section_list:
-                prefix = "  " * indent
-                section_id = section.get('id', 'N/A')
-                section_desc = section.get('描述', 'N/A')
-                content_range = section.get('内容范围', 'N/A')
-
-                text += f"{prefix}- {section_id}: {section_desc}\n"
-                text += f"{prefix}  内容范围: {content_range}\n"
-
-                # 递归处理子项
-                if section.get('子项'):
-                    text += build_section_list(section['子项'], indent + 1)
-
-            return text
-
-        text = "段落列表:\n"
-        text += build_section_list(sections)
-
-        return text
-
-    def _build_points_text(self, point_type: str, points_data) -> str:
-        """构建点(灵感点/目的点/关键点)的文本"""
-        if not points_data:
-            return f"无{point_type}信息"
-
-        if isinstance(points_data, dict):
-            # 处理字典格式(如灵感点)
-            text = f"{point_type}:\n"
-            for key, value in points_data.items():
-                if isinstance(value, list):
-                    text += f"- {key}:\n"
-                    for item in value:
-                        if isinstance(item, dict):
-                            text += f"  - {item.get('点', item.get('关键点', 'N/A'))}\n"
-                        else:
-                            text += f"  - {item}\n"
-                else:
-                    text += f"- {key}: {value}\n"
-            return text
-        elif isinstance(points_data, list):
-            # 处理列表格式(如目的点)
-            text = f"{point_type}:\n"
-            for item in points_data:
-                if isinstance(item, dict):
-                    text += f"- {item.get('目的点', item.get('点', 'N/A'))}\n"
-                else:
-                    text += f"- {item}\n"
-            return text
-        else:
-            return f"{point_type}: {points_data}"
-
-    def _count_sections(self, section_division: dict) -> int:
-        """统计段落总数(只统计叶子节点,即具体段落)"""
-        if not section_division:
-            return 0
-
-        sections = section_division.get("段落列表", [])
-        if not sections:
-            return 0
-
-        def count_leaf_nodes(section_list):
-            """只统计叶子节点(没有子项的段落)"""
-            count = 0
-            for section in section_list:
-                children = section.get("子项", [])
-                if children:
-                    # 有子项,是父节点,递归统计子项
-                    count += count_leaf_nodes(children)
-                else:
-                    # 无子项,是叶子节点,计数+1
-                    count += 1
-            return count
-
-        return count_leaf_nodes(sections)
-
-    def _build_messages(self, state: dict) -> List[dict]:
-        """构建消息 - 本Agent不使用此方法"""
-        return []
-
-    def _update_state(self, state: dict, response) -> dict:
-        """更新状态 - 本Agent不使用此方法"""
-        return state

+ 23 - 2
src/components/agents/script_form_extraction_agent.py

@@ -112,8 +112,29 @@ class ScriptFormExtractionAgent(BaseLLMAgent):
         else:
             inspiration_points = []
         
-        purpose_points = state.get("purpose_points", [])
-        key_points = state.get("key_points", [])
+        # 兼容 purpose_point 的多种格式:
+        # 1. 字典格式:{"purpose_point": {"purposes": [...], "total_count": ...}}
+        # 2. 列表格式:[...](直接是目的点列表)
+        # 3. 兼容旧格式:{"purpose_points": [...]}
+        purpose_point_raw = state.get("purpose_point", {})
+        if isinstance(purpose_point_raw, dict):
+            purpose_points = purpose_point_raw.get("purposes", [])
+        elif isinstance(purpose_point_raw, list):
+            purpose_points = purpose_point_raw
+        else:
+            # 兼容旧格式:直接读取 purpose_points(复数)
+            purpose_points = state.get("purpose_points", [])
+        
+        # 兼容 key_points 的多种格式:
+        # 1. 字典格式:{"key_points": [...], "total_count": ...}
+        # 2. 列表格式:[...](直接是关键点列表)
+        key_points_raw = state.get("key_points", {})
+        if isinstance(key_points_raw, dict):
+            key_points = key_points_raw.get("key_points", [])
+        elif isinstance(key_points_raw, list):
+            key_points = key_points_raw
+        else:
+            key_points = []
 
         # 获取最终达标的实质元素(优先使用 substance_final_elements)
         substance_final_elements = state.get("substance_final_elements", [])

+ 0 - 328
src/components/agents/script_orthogonal_analysis_agent.py

@@ -1,328 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-脚本正交分析Agent
-
-功能: 将脚本元素和段落进行正交分析,生成二维矩阵表格
-输出: 正交分析矩阵(段落 × 元素维度)
-"""
-
-from typing import List
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-
-class ScriptOrthogonalAnalysisAgent(BaseLLMAgent):
-    """脚本正交分析Agent - 将元素和段落进行正交分析生成矩阵表格
-    """
-
-    def __init__(
-        self,
-        name: str = "script_orthogonal_analysis_agent",
-        description: str = "脚本正交分析Agent - 将元素和段落进行正交分析生成矩阵表格",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        """初始化脚本正交分析Agent
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是脚本正交分析专家,擅长将脚本元素和段落进行二维矩阵分析。
-
-# 核心能力
-1. 矩阵生成:将段落作为行,元素作为列,生成正交分析矩阵
-2. 细粒度分析:针对每个段落×元素的交叉点,分析该元素在该段落中的具体表现
-3. 结构化输出:生成清晰的二维表格结构
-
-# 工作原则
-- 全面覆盖:覆盖所有段落和所有元素维度
-- 精准分析:每个交叉点都要有明确的分析结论
-- 留空规则:如果某元素在某段落中不存在或不明显,填写空字符串"""
-
-    def process(self, state: dict) -> dict:
-        """处理state,执行正交分析
-
-        从state中提取所需数据,执行正交分析,并返回结果
-
-        Args:
-            state: 工作流状态,包含:
-                - text: 文本数据 {title, body}
-                - images: 图片URL列表
-                - script_sections: 段落列表(来自script_section_division_agent)
-                - script_elements: 元素列表(来自script_element_extraction_agent)
-
-        Returns:
-            dict: 包含正交分析矩阵
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=== 开始脚本正交分析 ===")
-
-        # 从state中提取数据
-        text_data = state.get("text", {})
-        images = state.get("images", [])
-        script_sections = state.get("script_sections", {})
-        script_elements = state.get("script_elements", {})
-
-        # 执行正交分析
-        logger.info("执行正交分析")
-        result = self._orthogonal_analysis(
-            text_data=text_data,
-            images=images,
-            script_sections=script_sections,
-            script_elements=script_elements
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"正交分析结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _extract_leaf_sections(self, sections: List[dict], parent_desc: str = "") -> List[dict]:
-        """递归提取叶子段落(最底层段落)
-
-        Args:
-            sections: 段落列表(可能包含嵌套)
-            parent_desc: 父级描述(用于构建完整路径)
-
-        Returns:
-            List[dict]: 叶子段落列表,每个包含 {"路径": "父级>子级", "内容范围": [...]}
-        """
-        leaf_sections = []
-
-        for section in sections:
-            section_desc = section.get("描述", "")
-            full_path = f"{parent_desc} > {section_desc}" if parent_desc else section_desc
-
-            children = section.get("子项", [])
-
-            if children:
-                # 有子项,递归处理
-                leaf_sections.extend(self._extract_leaf_sections(children, full_path))
-            else:
-                # 叶子节点
-                leaf_sections.append({
-                    "路径": full_path,
-                    "内容范围": section.get("内容范围", [])
-                })
-
-        return leaf_sections
-
-    def _extract_element_types(self, elements: List[dict]) -> List[dict]:
-        """提取元素类型(第1层元素作为类型)
-
-        Args:
-            elements: 元素列表(可能包含嵌套)
-
-        Returns:
-            List[dict]: 元素类型列表,每个包含 {"类型名称": "...", "描述": "...", "子元素": [...]}
-        """
-        element_types = []
-
-        for element in elements:
-            element_name = element.get("元素名称", "")
-            element_desc = element.get("描述", "")
-            children = element.get("子项", [])
-
-            if element_name:
-                element_types.append({
-                    "类型名称": element_name,
-                    "描述": element_desc,
-                    "子元素": children
-                })
-
-        return element_types
-
-    def _orthogonal_analysis(self, text_data: dict, images: List[str],
-                            script_sections: dict, script_elements: dict) -> dict:
-        """正交分析
-        将段落和元素类型进行二维矩阵分析
-        """
-        logger.info("【正交分析】生成段落×元素类型矩阵")
-
-        # 提取段落和元素
-        sections_list = script_sections.get("段落列表", [])
-        elements_list = script_elements.get("元素列表", [])
-
-        # 提取叶子段落
-        leaf_sections = self._extract_leaf_sections(sections_list)
-        logger.info(f"提取到 {len(leaf_sections)} 个叶子段落")
-
-        # 提取元素类型(第1层)
-        element_types = self._extract_element_types(elements_list)
-        logger.info(f"提取到 {len(element_types)} 个元素类型")
-
-        # 构建帖子内容
-        post_content_parts = []
-        if text_data.get("title"):
-            post_content_parts.append(f"标题: {text_data['title']}")
-        if text_data.get("body"):
-            post_content_parts.append(f"正文: {text_data['body']}")
-
-        post_content = "\n".join(post_content_parts) if post_content_parts else "无文本信息"
-
-        # 构建段落信息
-        sections_info = ""
-        for idx, section in enumerate(leaf_sections, 1):
-            sections_info += f"\n{idx}. {section['路径']}"
-            sections_info += f"\n   内容范围: {section['内容范围']}"
-
-        # 构建元素类型信息(包含子元素)
-        elements_info = ""
-        for idx, elem_type in enumerate(element_types, 1):
-            elements_info += f"\n{idx}. **{elem_type['类型名称']}**: {elem_type['描述']}"
-            if elem_type['子元素']:
-                elements_info += "\n   包含子元素:"
-                for child in elem_type['子元素']:
-                    child_name = child.get('元素名称', '')
-                    child_desc = child.get('描述', '')
-                    elements_info += f"\n   - {child_name}: {child_desc}"
-
-        # 提取元素类型名称列表
-        element_type_names = [et['类型名称'] for et in element_types]
-
-        # 构建prompt
-        prompt = f"""# 帖子内容
-
-{post_content}
-
-# 段落列表(叶子段落)
-{sections_info}
-
-# 元素类型列表(第1层分类)
-{elements_info}
-
-# 任务
-
-对每个**段落×元素类型**的交叉点进行分析,生成正交矩阵表格。
-
-## 分析原则
-
-1. **全面覆盖**:覆盖所有段落和所有元素类型
-2. **精准分析**:针对每个交叉点,分析该元素类型在该段落中的具体表现
-3. **类型层面分析**:分析时要考虑该元素类型下的所有子元素
-4. **留空规则**:
-   - 如果该元素类型在该段落中**不存在**或**不明显**,填写空字符串 ""
-   - 如果该元素类型在该段落中**存在且明显**,填写具体表现描述(可以综合该类型下的多个子元素)
-
-## 元素类型说明
-
-每个元素类型都代表一个抽象维度,包含多个具体子元素。
-分析时需要:
-- 识别该类型下哪些子元素在段落中出现
-- 综合描述该类型在段落中的整体表现
-- 如果该类型下的子元素都不存在,则留空
-
-## 输出要求
-
-- **矩阵结构**:段落×元素类型的二维表格
-- **行**:每一行代表一个段落
-- **列**:每一列代表一个元素类型(第1层分类)
-- **交叉点值**:该元素类型在该段落中的具体表现(不存在则为空字符串)
-
-# 输出(JSON)
-
-{{
-  "正交矩阵": [
-    {{
-      "段落": "段落路径",
-      "内容范围": ["图1", "正文——具体文字内容片段"],
-      "元素类型分析": {{
-        "元素类型1": "该类型在此段落的具体表现(综合该类型下所有子元素的表现)",
-        "元素类型2": "该类型在此段落的具体表现",
-        "元素类型3": "",
-        ...
-      }}
-    }}
-  ],
-  "元素类型列表": ["元素类型1", "元素类型2", "元素类型3", ...]
-}}
-
-**示例输出**:
-
-{{
-  "正交矩阵": [
-    {{
-      "段落": "开头 > 引入场景",
-      "内容范围": ["图1", "正文——早晨的阳光洒进房间"],
-      "元素类型分析": {{
-        "核心主题与现象": "展现日常生活场景,为后续主题奠定基础",
-        "内容对象与状态": "女主角小美刚醒来,处于放松的原始状态",
-        "内容呈现形式": "采用温馨的视觉风格,配以旁白文字",
-        "传播目标与效果": "引起观众的代入感,营造轻松的氛围"
-      }}
-    }},
-    {{
-      "段落": "过程 > 遇到问题",
-      "内容范围": ["图2", "图3", "正文——突然发现钥匙不见了"],
-      "元素类型分析": {{
-        "核心主题与现象": "揭示冲突点,推动故事发展",
-        "内容对象与状态": "女主角处于焦虑状态,钥匙丢失成为核心问题",
-        "内容呈现形式": "采用紧张的镜头语言和对白,强化情绪",
-        "传播目标与效果": "制造悬念,引发观众的情绪共鸣"
-      }}
-    }}
-  ],
-  "元素类型列表": ["核心主题与现象", "内容对象与状态", "内容呈现形式", "传播目标与效果"]
-}}"""
-
-        # 构建多模态消息
-        message_content = [{"type": "text", "text": prompt}]
-
-        # 添加图片
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                message_content.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": message_content}
-        ]
-
-        # 调用LLM
-        result = LLMInvoker.safe_invoke(
-            self,
-            "正交分析",
-            messages,
-            fallback={"正交矩阵": [], "元素类型列表": []}
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"正交分析详细结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _build_messages(self, state: dict) -> List[dict]:
-        """构建消息 - ScriptOrthogonalAnalysisAgent 不使用此方法
-
-        本 Agent 使用 _orthogonal_analysis 方法直接构建消息
-        """
-        return []
-
-    def _update_state(self, state: dict, response) -> dict:
-        """更新状态 - ScriptOrthogonalAnalysisAgent 不使用此方法
-
-        本 Agent 使用 _orthogonal_analysis 方法直接返回结果
-        """
-        return state

+ 0 - 1076
src/components/agents/script_understanding_agent.py

@@ -1,1076 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-脚本理解Agent
-
-功能: 脚本理解与分析(创作者视角)
-- step1: Section划分 - 分析已有帖子的分段结构,理解创作者是如何组织内容的(树状结构输出)
-- step2: 元素列表生成 - 从已有帖子内容中提取元素列表(树状结构输出)
-"""
-
-from typing import List
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-
-class ScriptUnderstandingAgent(BaseLLMAgent):
-    """脚本理解Agent - 分析已有帖子的脚本结构
-    """
-
-    def __init__(
-        self,
-        name: str = "script_understanding_agent",
-        description: str = "脚本理解Agent - 分析已有帖子的脚本结构(创作者视角)",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        """初始化脚本理解Agent
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是脚本结构分析专家,擅长从创作者视角理解内容的分段结构。
-
-# 核心能力
-1. 结构识别:分析已有帖子内容,识别创作者的分段逻辑
-2. 树状输出:通过children字段表示层级关系,不使用parent_id
-
-# 工作原则
-- step1(Section划分):识别创作者如何分段组织内容(树状结构)
-- step2(元素提取):提取具体元素列表(树状结构)"""
-
-    def step1(self, text_data: dict, images: List[str], topic_description: dict, content_weight: dict = None) -> dict:
-        """第一步:Section划分
-        分析已有帖子的内容,识别创作者是如何分段组织内容的
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("【Step1】Section划分 - 分析帖子的分段结构")
-
-        # 提取权重信息
-        weight_info = ""
-        strategy_guidance = ""
-        if content_weight:
-            image_weight = content_weight.get("图片权重", 5)
-            text_weight = content_weight.get("文字权重", 5)
-            primary_source = content_weight.get("主要信息源", "both")
-            content_relationship = content_weight.get("图文关系", "相关")
-
-            weight_info = f"""
-# 图文权重信息
-
-- 图片权重: {image_weight}/10
-- 文字权重: {text_weight}/10
-- 主要信息源: {primary_source}
-- 图文关系: {content_relationship}
-"""
-
-            # 根据权重和关系确定策略指导
-            weight_diff = image_weight - text_weight
-
-            # 确定策略类型和验证要求
-            if weight_diff >= 3:
-                strategy_type = "以图为主"
-                strategy_guidance = """
-## 图文策略: 以图为主
-
-**核心原则**: 用图划分段落,图与图之间有顺序,文可以乱序对应到段落
-
-**Section切分要点**:
-- 以图片序列为核心主线进行划分
-- **图片编号必须保持严格连续性**
-- 文字可以灵活对应到图片
-- 关注图片之间的关系、图片内容的语义分段、叙事逻辑
-"""
-                validation_guidance = """
-**验证内容**:
-1. ✅ **图片连续性验证**(核心验证)
-   - 检查每个Section内的图片编号是否严格连续
-   - 方法:逐个检查图片编号,确保无跳跃
-
-2. ✅ **图片语义关系验证**
-   - 同一Section内的图片是否有语义关联
-   - 图片之间的过渡是否自然合理
-
-3. ❌ **不验证文字顺序**
-   - 文字可以灵活对应到图片
-
-**禁止行为**:
-- ❌ 跨段落跳跃式引用图片
-- ❌ 打乱图片的原始顺序
-
-**验证流程**:
-- Step1: 提取每个Section的图片编号列表
-- Step2: 检查图片编号是否连续
-- Step3: 如发现跳跃,重新调整Section划分
-"""
-            elif weight_diff <= -3:
-                strategy_type = "以文为主"
-                strategy_guidance = """
-## 图文策略: 以文为主
-
-**核心原则**: 用文划分段落,文与文之间有顺序,图可以乱序对应到段落
-
-**Section切分要点**:
-- 以文字段落为核心主线进行划分
-- **文字必须保持原始顺序**
-- 图片可以灵活对应到文字
-- 关注文字的话题转换、逻辑推进、情绪变化
-"""
-                validation_guidance = """
-**验证内容**:
-1. ✅ **文字顺序验证**(核心验证)
-   - 检查每个Section内的文字是否保持原始顺序
-   - 段落之间的文字流转是否自然连贯
-
-2. ✅ **文字逻辑性验证**
-   - 同一Section内的文字是否有逻辑关联
-   - 话题转换是否合理
-
-3. ❌ **不验证图片顺序**
-   - 图片可以灵活对应到文字
-
-**禁止行为**:
-- ❌ 打乱文字的原始顺序
-- ❌ 跨段落错乱文字位置
-
-**验证流程**:
-- Step1: 提取每个Section的文字内容
-- Step2: 按原文顺序检查文字是否连贯
-- Step3: 如发现乱序,重新调整Section划分
-"""
-            else:
-                # 策略3/4: 图文同权
-                if content_relationship == "相关":
-                    strategy_type = "图文同权-图文相关"
-                    strategy_guidance = """
-## 图文策略: 图文同权-图文相关
-
-**核心原则**: 根据消费者吸引力,判断图/文为主,另一个为辅
-
-**Section切分要点**:
-- 综合图文信息进行Section划分
-- 根据图片排版质量、文字表达清晰度等判断主导维度
-- 保持图文的对应关系,相互解释
-- 关注图文相互补充的部分、哪个维度的表达更完整
-"""
-                    validation_guidance = """
-**前置步骤**:
-- 先判断主导维度(图为主 or 文为主)
-- 明确记录判断依据
-
-**验证内容**:
-1. ✅ **主导维度顺序验证**(核心验证)
-   - 如果图为主:验证图片序列的严格连续性
-   - 如果文为主:验证文字段落的逻辑性
-
-2. ✅ **图文对应关系验证**
-   - 检查图文之间的对应关系是否合理
-   - 图文是否相互解释、相互补充
-
-3. ✅ **辅助维度合理性验证**
-   - 辅助维度是否支撑主导维度
-
-**禁止行为**:
-- ❌ 主导维度出现顺序跳跃或错乱
-- ❌ 图文对应关系混乱
-
-**验证流程**:
-- Step1: 判断并记录主导维度
-- Step2: 针对主导维度执行严格顺序验证
-- Step3: 验证图文对应关系
-- Step4: 如发现问题,重新调整Section划分
-"""
-                else:
-                    strategy_type = "图文同权-图文不相关"
-                    strategy_guidance = """
-## 图文策略: 图文同权-图文不相关
-
-**核心原则**: 根据创作者目的,判断只关注图还是只关注文
-
-**Section切分要点**:
-- 明确选择关注图或关注文,不能同时关注
-- 分析创作者的主要表达意图
-- 选定维度后,完全基于该维度进行Section划分
-- 关注创作者想重点传达什么、解构的目标是什么
-"""
-                    validation_guidance = """
-**前置步骤**:
-- 先判断选择关注的维度(图 or 文)
-- 明确记录选择依据
-
-**验证内容**:
-1. ✅ **选定维度顺序验证**(核心验证)
-   - 如果关注图:验证图片序列的严格连续性
-   - 如果关注文:验证文字段落的逻辑性
-
-2. ✅ **单一维度一致性验证**
-   - 确认所有Section都基于选定的单一维度划分
-
-3. ❌ **不验证未选择的维度**
-   - 未选择的维度可以完全忽略
-
-**禁止行为**:
-- ❌ 选定维度出现顺序跳跃或错乱
-- ❌ 同时关注图和文进行Section划分
-
-**验证流程**:
-- Step1: 判断并记录关注的维度
-- Step2: 针对选定维度执行严格顺序验证
-- Step3: 确认未使用另一维度进行划分
-- Step4: 如发现问题,重新调整Section划分
-"""
-
-            logger.info(f"使用图文策略: {strategy_type} - 图片:{image_weight}, 文字:{text_weight}, 关系:{content_relationship}")
-        else:
-            strategy_type = "未指定"
-            validation_guidance = ""
-            logger.info("未提供图文权重信息")
-
-        # 构建帖子内容
-        post_content_parts = []
-        if text_data.get("title"):
-            post_content_parts.append(f"标题: {text_data['title']}")
-        if text_data.get("body"):
-            post_content_parts.append(f"正文: {text_data['body']}")
-
-        post_content = "\n".join(post_content_parts) if post_content_parts else "无文本信息"
-
-        # 构建选题描述文本
-        topic_parts = []
-        if topic_description.get("主题"):
-            topic_parts.append(f"主题: {topic_description['主题']}")
-        if topic_description.get("描述"):
-            topic_parts.append(f"描述: {topic_description['描述']}")
-        topic_text = "\n".join(topic_parts) if topic_parts else "无选题描述"
-
-        # 构建prompt
-        prompt = f"""{weight_info}
-{strategy_guidance}
-
-# 帖子内容
-
-{post_content}
-
-# 选题描述
-
-{topic_text}
-
-# 任务
-
-从**创作者视角**分析这个已有帖子是如何组织内容的。
-
-## Section切分流程
-
-**第一步:应用图文策略**
-根据上述图文策略指导,确定Section切分的主导维度和具体方法。
-
-**第二步:识别主题显著变化位置**
-扫描全部内容,识别**主题发生显著变化**的位置:
-
-- **判断标准**:
-  * 语义跃迁: 讨论对象发生根本性改变
-  * 逻辑转换: 从"是什么"转向"为什么"或"怎么办"
-  * 功能变化: 从"问题陈述"转向"解决方案"
-
-- **划分原则**:
-  * 避免过度细分(每个小变化都成为顶层段落)
-  * 避免过度粗放(将所有内容合并为1个顶层段落)
-  * 以"主题板块"而非"内容单元"为划分粒度
-
-**第三步:初步划分**
-- 按照策略要点确定核心驱动
-- 基于主题显著变化位置进行划分
-- 支持主Section和子Section的层级结构
-
-**第四步:顺序验证与反思**
-{validation_guidance}
-
-## 层级要求
-
-**段落必须至少保留2层结构**:
-1. **第1层(抽象层)**:从具象中聚合出的共性维度
-2. **第2层(具象层)**:具体的内容细节
-
-**层级关系说明**:
-- 抽象层是对多个具象内容的归纳和提炼
-- 具象层是抽象层的具体展开
-- 每个抽象层下必须有至少1个具象层子项
-
-## Section字段
-
-- 描述: 段落描述(共性维度名称;具体内容概括)
-- 内容范围: **列表格式,包含具体内容**
-  - 格式:["图X","正文-XXXX"]
-  - 要求:必须包含具体的图片编号或文字原文片段
-- 推理依据: 为什么这样划分
-- 子项: 子Section列表(树状结构)
-
-# 输出(JSON)- 树状结构
-
-{{
-  "内容品类": "内容品类",
-  "段落列表": [
-    {{
-      "描述": "共性维度名称",
-      "内容范围": ["图1", "图2", "正文——具体文字内容片段"],
-      "推理依据": "为什么这样划分这个抽象层",
-      "子项": [
-        {{
-          "描述": "具体内容概括",
-          "内容范围": ["图1", "正文——具体文字内容片段"],
-          "推理依据": "这个具象内容如何支撑上层抽象",
-          "子项": []
-        }}
-      ]
-    }}
-  ]
-}}"""
-
-        # 构建多模态消息
-        message_content = [{"type": "text", "text": prompt}]
-
-        # 添加图片
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                message_content.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": message_content}
-        ]
-
-        # 调用LLM
-        result = LLMInvoker.safe_invoke(
-            self,
-            "Section划分",
-            messages,
-            fallback={"内容品类": "未知品类", "段落列表": []}
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"Step1结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def process(self, state: dict) -> dict:
-        """处理state,执行完整的脚本理解流程(step1 + step2)
-
-        从state中提取所需数据,执行step1和step2,并返回结果
-
-        Args:
-            state: 工作流状态,包含:
-                - text: 文本数据 {title, body}
-                - images: 图片URL列表
-                - topic_selection_understanding: 选题理解结果
-                - content_weight: 图文权重信息
-
-        Returns:
-            dict: 包含script_understanding结果
-        """
-        logger.info("=== 开始脚本理解处理 ===")
-
-        # 从state中提取数据
-        text_data = state.get("text", {})
-        images = state.get("images", [])
-
-        # 提取选题描述(作为字典)
-        topic_understanding = state.get("topic_selection_understanding", {})
-        topic_description = {
-            "主题": topic_understanding.get("主题", ""),
-            "描述": topic_understanding.get("描述", "")
-        }
-
-        # 提取图文权重
-        content_weight = state.get("content_weight", {})
-
-        # 执行 step1 - Section划分
-        logger.info("执行 Step1 - Section划分")
-        sections_result = self.step1(
-            text_data=text_data,
-            images=images,
-            topic_description=topic_description,
-            content_weight=content_weight
-        )
-        sections = sections_result.get("段落列表", [])
-
-        # 执行 step2 - 元素列表生成
-        logger.info("执行 Step2 - 元素列表生成")
-        elements_result = self.step2(
-            text_data=text_data,
-            images=images,
-            topic_description=topic_description,
-            content_weight=content_weight
-        )
-
-        # 组装最终结果
-        script_understanding = {
-            "内容品类": sections_result.get("内容品类", "未知品类"),
-            "段落列表": sections,
-            "元素列表": elements_result.get("元素列表", []),
-            "图片列表": images  # 添加原始图片URL列表
-        }
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"脚本理解最终结果:\n{json.dumps(script_understanding, ensure_ascii=False, indent=2)}")
-
-        return {"脚本理解": script_understanding}
-
-    def step2_1_identify_candidates(self, text_data: dict, images: List[str], topic_description: dict, content_weight: dict = None) -> dict:
-        """Step2.1: 候选元素识别
-
-        基于选题主题,识别与主题相关的候选元素(包括实质性和概念性元素)
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("【Step2.1】候选元素识别 - 基于选题主题识别候选元素(实质性+概念性)")
-        logger.info("=" * 80)
-
-        # 构建权重信息
-        weight_info = self._build_weight_info(content_weight)
-
-        # 构建内容
-        post_content = self._build_post_content(text_data)
-        topic_text = self._build_topic_text(topic_description)
-
-        # 构建prompt
-        prompt = f"""{weight_info}
-
-# 帖子内容
-
-{post_content}
-
-# 选题描述
-
-{topic_text}
-
-# 任务
-
-**基于选题主题作为关注锚点**,识别与主题相关或支撑主题的候选元素。
-
-## 选题的作用
-
-选题主题是识别的**关注锚点**:
-- ✅ **直接相关**:与选题主题直接相关的核心元素
-- ✅ **支撑主题**:对主题有支撑作用的辅助元素(即使不是核心,但能帮助理解主题)
-- ✅ **间接相关**:与选题主题间接相关的背景元素
-- ❌ **完全无关**:与选题主题没有任何关联的元素
-
-## 识别原则
-
-- **全面识别**:宁可多识别,不要漏掉与主题相关或支撑主题的元素
-- **关系多样**:包括直接相关、支撑性、补充性、背景性等各种关系
-- **宽松标准**:此阶段标准宽松,后续会进一步筛选
-- **倾向保留**:不确定是否相关时,倾向于保留
-
-## 识别标准:实质性元素 + 概念性元素
-
-### 实质性元素(Substantial Elements)
-**定义**:看得见、摸得着的具体物体
-- ✅ 必须是名词形式
-- ✅ 必须是可观察的具体事物
-- ✅ 必须是**原子的名词**,关注的是类/ID
-- ✅ 包括所有出现的物体,无论大小或重要性
-- ❌ 不包括动作或状态
-- ❌ 不包括情绪或氛围
-- ❌ 不包括带修饰语的复合名词(修饰语应在后续分析中体现)
-
-### 概念性元素(Conceptual Elements)
-**定义**:虽然不是客观实体,但是对理解帖子至关重要的抽象概念
-- ✅ 必须是名词形式的概念
-- ✅ 虽然没有直接提及,但图片或文字隐含了这个概念
-- ✅ 出现频率高或重要性强的描述性概念
-- ✅ 对理解主题有显著帮助的抽象维度
-
-**概念性元素的判断标准**:
-- ✅ 这个概念是否帮助理解帖子的核心价值?
-- ✅ 这个概念是否贯穿多个段落或图片?
-- ✅ 这个概念是否隐含在视觉或文字表达中?
-- ❌ 不是单纯的形容词或副词,必须能转化为名词概念
-
-## 识别要求
-
-- **全面浏览**:不要遗漏任何出现的元素(实质性+概念性)
-- **原始记录**:此阶段不进行筛选,全部记录
-- **原子名词**:使用最基础的名词形式,去除修饰语
-- **显性+隐性**:既要识别显性出现的实体,也要识别隐性的概念
-
-## 元素类型标注
-
-每个元素需要标注类型:
-- **实质性**:具体的客观物体
-- **概念性**:抽象的概念维度
-
-# 输出(JSON)
-
-{{
-  "候选元素列表": [
-    {{
-      "元素名称": "元素名称(原子名词)",
-      "元素类型": "实质性/概念性",
-      "出现位置": "该元素出现在哪些位置",
-      "识别依据": "为什么识别这个元素(特别是概念性元素,需要说明隐含的依据)"
-    }}
-  ]
-}}"""
-
-        # 构建多模态消息
-        message_content = [{"type": "text", "text": prompt}]
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                message_content.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": message_content}
-        ]
-
-        # 调用LLM
-        result = LLMInvoker.safe_invoke(
-            self,
-            "候选元素识别",
-            messages,
-            fallback={"候选元素列表": []}
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"Step2.1结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def step2_2_score_dimensions(self, candidates_result: dict, text_data: dict, images: List[str], topic_description: dict, content_weight: dict = None) -> dict:
-        """Step2.2: 双维度评分(分视角分析)
-
-        对每个候选元素进行双维度评分(每维度0-10分)
-        - 先分视角:判断概念性维度 vs 实质性维度哪个更重
-        - 优先处理权重更大的维度
-        - 维度1: 主题支撑性 - 评估元素对选题展开和目的达成的支撑程度
-        - 维度2: 多段落共性 - 评估元素的贯穿性和共性
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("\n" + "=" * 80)
-        logger.info("【Step2.2】双维度评分(分视角分析)")
-        logger.info("=" * 80)
-
-        candidates = candidates_result.get("候选元素列表", [])
-        if not candidates:
-            logger.warning("⚠️  没有候选元素可供评分")
-            return {"评分结果": []}
-
-        # 统计概念性和实质性元素数量
-        conceptual_count = sum(1 for c in candidates if c.get("元素类型") == "概念性")
-        substantial_count = sum(1 for c in candidates if c.get("元素类型") == "实质性")
-
-        logger.info(f"输入: {len(candidates)} 个候选元素(概念性: {conceptual_count}, 实质性: {substantial_count})")
-
-        # 构建权重信息
-        weight_info = self._build_weight_info(content_weight)
-
-        # 构建内容
-        post_content = self._build_post_content(text_data)
-        topic_text = self._build_topic_text(topic_description)
-
-        # 构建候选元素列表文本
-        candidates_text = ""
-        for c in candidates:
-            candidates_text += f"\n- {c.get('元素名称', 'N/A')} ({c.get('元素类型', 'N/A')})"
-            candidates_text += f"\n  出现位置: {c.get('出现位置', 'N/A')}"
-            if c.get('识别依据'):
-                candidates_text += f"\n  识别依据: {c.get('识别依据', 'N/A')}"
-
-        # 构建prompt
-        prompt = f"""{weight_info}
-
-# 帖子内容
-
-{post_content}
-
-# 选题描述
-
-{topic_text}
-
-# 候选元素列表
-
-{candidates_text}
-
-# 任务
-
-对每个候选元素进行**双维度评分**(每维度0-10分)。
-
-## 分视角分析流程
-
-**第一步:判断维度权重**
-根据候选元素列表,判断概念性元素和实质性元素哪个更重:
-- 如果概念性元素数量多或对主题更核心 → 概念性维度权重更大
-- 如果实质性元素数量多或对主题更核心 → 实质性维度权重更大
-- 如果两者相当 → 两个维度权重相同
-
-**第二步:优先处理权重更大的维度**
-- 先对权重更大的维度进行详细评分和分析
-- 确保权重更大维度的元素得到充分评估
-- 再处理另一个维度作为补充
-
-**第三步:综合评分**
-- 对所有元素进行双维度评分
-- 权重更大的维度应有更多高分元素
-
-## 两大评分维度
-
-### 1. 主题支撑性(0-10分)
-评估元素对选题展开和目的达成的支撑程度:
-- **核心支撑(8-10分)**:元素是选题展开的核心支撑,直接承载创作目的
-- **重要支撑(5-7分)**:元素为选题提供重要支撑,对目的达成有明显作用
-- **辅助支撑(3-4分)**:元素为选题提供辅助性支撑
-- **弱支撑(0-2分)**:元素与选题关联度低,对目的达成作用微弱
-
-### 2. 多段落共性(0-10分)
-评估元素在多个段落中的贯穿性和共性:
-- **跨段落出现**:元素在多个段落/Section中出现
-- **持续性存在**:元素在内容中的连续性呈现
-- **代表性**:元素的典型性和普遍性,能否代表一类事物
-
-**贯穿段落字段说明**:
-- 格式:列表格式,包含该元素出现的具体内容
-- 要求:必须包含具体的图片编号或文字原文片段
-
-## 评分要求
-
-- 客观评分:基于实际内容,不要主观臆测
-- 严格打分:真正优秀的元素才能得高分
-- 详细说明:每个维度都要给出评分依据
-- 分视角优先:优先处理权重更大的维度
-
-# 输出(JSON)
-
-{{
-  "维度判断": {{
-    "概念性元素重要性": "高/中/低",
-    "实质性元素重要性": "高/中/低",
-    "优先维度": "概念性/实质性/均衡",
-    "判断依据": "为什么这个维度更重要"
-  }},
-  "评分结果": [
-    {{
-      "元素名称": "元素名称",
-      "元素类型": "概念性/实质性",
-      "主题支撑性得分": 8,
-      "主题支撑性依据": "评分依据说明(如何支撑选题展开和目的达成)",
-      "多段落共性得分": 7,
-      "多段落共性依据": "评分依据说明",
-      "贯穿段落": ["图1", "图2", "正文——具体文字内容片段"]
-    }}
-  ]
-}}"""
-
-        # 构建多模态消息
-        message_content = [{"type": "text", "text": prompt}]
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                message_content.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": message_content}
-        ]
-
-        # 调用LLM
-        result = LLMInvoker.safe_invoke(
-            self,
-            "双维度评分",
-            messages,
-            fallback={"评分结果": []}
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"Step2.2结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def step2_3_strict_filter(self, scored_result: dict, single_threshold: float = 6.0, weighted_threshold: float = 5.0) -> dict:
-        """Step2.3: 严格筛选
-
-        根据评分结果进行严格筛选,满足以下条件之一即可通过:
-        1. 主题支撑性 > single_threshold
-        2. 多段落共性 > single_threshold
-        3. 加权得分 > weighted_threshold(加权:主题支撑性 * 0.5 + 多段落共性 * 0.5)
-
-        Args:
-            scored_result: 评分结果
-            single_threshold: 单维度筛选阈值,默认为6.0(降低阈值,返回更多支撑主题的元素)
-            weighted_threshold: 加权得分筛选阈值,默认为5.0(整体合格即可)
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("\n" + "=" * 80)
-        logger.info("【Step2.3】严格筛选")
-        logger.info("=" * 80)
-        logger.info("筛选规则(满足任一条件即通过):")
-        logger.info(f"  1. 主题支撑性 > {single_threshold} (单维度良好)")
-        logger.info(f"  2. 多段落共性 > {single_threshold} (单维度良好)")
-        logger.info(f"  3. 加权得分 > {weighted_threshold} (整体合格,权重:主题支撑性=0.5, 多段落共性=0.5)")
-        logger.info(f"  注:阈值已从7降低到6,以返回更多支撑主题的元素")
-
-        scored_elements = scored_result.get("评分结果", [])
-        if not scored_elements:
-            logger.warning("⚠️  没有评分结果可供筛选")
-            return {"筛选结果": []}
-
-        logger.info(f"\n输入: {len(scored_elements)} 个评分元素")
-        logger.info("\n筛选过程:")
-
-        filtered_elements = []
-
-        for element in scored_elements:
-            element_name = element.get("元素名称", "未知")
-
-            # 提取分数
-            theme_score = element.get("主题支撑性得分", 0)
-            multi_para_score = element.get("多段落共性得分", 0)
-
-            # 计算加权得分(主题支撑性和多段落共性各占50%)
-            weighted_score = theme_score * 0.5 + multi_para_score * 0.5
-
-            # 判断是否满足任一条件
-            conditions_met = []
-
-            if theme_score > single_threshold:
-                conditions_met.append(f"主题支撑性={theme_score}")
-
-            if multi_para_score > single_threshold:
-                conditions_met.append(f"多段落共性={multi_para_score}")
-
-            if weighted_score > weighted_threshold:
-                conditions_met.append(f"加权得分={weighted_score:.1f}")
-
-            # 如果满足任一条件,则通过筛选
-            if conditions_met:
-                logger.info(f"  ✓ {element_name}: 通过筛选 ({', '.join(conditions_met)})")
-                filtered_elements.append(element)
-            else:
-                logger.info(f"  ✗ {element_name}: 未通过筛选 (主题支撑性={theme_score}, 多段={multi_para_score}, 加权={weighted_score:.1f})")
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"Step2.3结果:\n{json.dumps({'筛选结果': filtered_elements}, ensure_ascii=False, indent=2)}")
-
-        return {"筛选结果": filtered_elements}
-
-    def step2_4_deduplicate(self, filtered_result: dict, text_data: dict, images: List[str], topic_description: dict) -> dict:
-        """Step2.4: 去重与整合
-
-        合并同类元素,确保元素之间正交独立,生成树状结构
-        - 非叶子节点:分类(抽象维度)
-        - 叶子节点:具体元素
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("\n" + "=" * 80)
-        logger.info("【Step2.4】去重与整合")
-        logger.info("=" * 80)
-
-        filtered_elements = filtered_result.get("筛选结果", [])
-        if not filtered_elements:
-            logger.warning("⚠️  没有筛选结果可供去重")
-            return {"元素列表": []}
-
-        logger.info(f"输入: {len(filtered_elements)} 个筛选后的元素")
-
-        # 构建内容
-        post_content = self._build_post_content(text_data)
-        topic_text = self._build_topic_text(topic_description)
-
-        # 构建筛选元素文本
-        elements_text = ""
-        for elem in filtered_elements:
-            elements_text += f"\n- {elem.get('元素名称', 'N/A')}"
-            elements_text += f"\n  主题支撑性: {elem.get('主题支撑性得分', 0)}, 多段落共性: {elem.get('多段落共性得分', 0)}"
-            elements_text += f"\n  贯穿段落: {elem.get('贯穿段落', 'N/A')}"
-
-        # 构建prompt
-        prompt = f"""# 帖子内容
-
-{post_content}
-
-# 选题描述
-
-{topic_text}
-
-# 筛选后的元素列表
-{elements_text}
-
-# 任务
-
-对筛选后的元素进行**去重与整合**,生成最终的树状结构元素列表。
-
-## 核心原则
-
-**树状结构:非叶子节点为分类,叶子节点为具体元素**
-
-- 非叶子节点:分类(抽象维度),可以包含子分类或具体元素
-- 叶子节点:具体元素,是实际的元素
-- 层级深度:根据实际内容灵活确定,不限定层数
-- 分类节点通过"子项"字段包含下一层的分类或元素
-
-## 去重规则
-
-1. **合并同类**:将具有相同上位概念的元素归类到同一分类下
-2. **正交独立**:同级节点之间必须互不重叠、相互独立
-
-## 分类规则(使用元素本身 What 的维度)
-
-**核心原则:分类基于元素本身的 What 维度(本质属性)**
-
-- **What 维度**:回答"这是什么类型的东西"
-- **分类来源**:从元素本身的本质属性中抽象出分类,基于"它们是什么"来分类
-- **分类层级**:分类可以嵌套(分类下可以有子分类)
-- **同级正交**:同一分类下的节点应该具有相同的 What 维度(上位概念)
-
-**MECE分类视角框架(选择最适合的一个维度)**:
-- **物理维度**:基于物理属性(形态、结构、状态等)
-- **化学维度**:基于化学成分和组成
-- **生物维度**:基于生物分类(动物、植物、微生物等)
-- **功能维度**:基于客观功能分类(注意:是物体本身的功能属性,不是对其他事物的影响)
-
-**分类禁止项**:
-- ❌ 禁止从"对其他事物的影响/效果"角度分类
-- ❌ 禁止从"目的/价值"角度分类
-- ❌ 禁止使用抽象概念作为分类
-
-**判断标准**:
-分类名称应该能回答"XX是一种什么?",而不是"XX对YY有什么作用?"或"XX导致什么结果?"
-
-## 节点字段说明
-
-### 分类节点(非叶子节点)
-- **元素名称**:分类名称(基于 What 维度的抽象分类)
-- **描述**:对这个分类的定义说明(回答"这是什么类型的东西")
-- **子项**:该分类下的子分类或具体元素列表
-
-### 元素节点(叶子节点)
-- **元素名称**:使用原子名词(最基础的名词形式,去除修饰语)
-- **描述**:该元素在帖子中的定义,包含具体表现
-- **上游支撑**:说明该元素如何支撑选题的展开和目的达成
-- **贯穿段落**:该元素贯穿的内容范围,列表格式(如:["图1", "图2", "正文——具体文字内容片段"])
-- **主题支撑性得分**:0-10分
-- **多段落共性得分**:0-10分
-- **推理依据**:为什么这是核心元素
-- **子项**:[](叶子节点,必须为空列表)
-
-## 输出要求
-
-- 元素数量:宁少勿多,只保留真正核心的元素
-- 同级正交:同级节点之间必须相互独立
-- 描述清晰:每个节点都要有清晰的定义
-- 支撑明确:每个元素都要说明如何支撑选题
-- 层级灵活:根据实际内容确定树的深度
-
-# 输出(JSON)- 树状结构
-
-{{
-  "元素列表": [
-    {{
-      "元素名称": "一级分类名称",
-      "描述": "对这个分类的定义说明",
-      "子项": [
-        {{
-          "元素名称": "二级分类名称(可选)",
-          "描述": "对这个子分类的定义说明",
-          "子项": [
-            {{
-              "元素名称": "具体元素名称(原子名词)",
-              "描述": "该元素在帖子中的定义,包含具体表现",
-              "上游支撑": "该元素如何支撑选题的展开和目的达成",
-              "贯穿段落": ["图1", "图2", "正文——具体文字内容片段"],
-              "主题支撑性得分": 8,
-              "多段落共性得分": 9,
-              "推理依据": "为什么这是核心元素(如何满足双维度要求)",
-              "子项": []
-            }}
-          ]
-        }},
-        {{
-          "元素名称": "具体元素名称(原子名词)",
-          "描述": "该元素在帖子中的定义,包含具体表现",
-          "上游支撑": "该元素如何支撑选题的展开和目的达成",
-          "贯穿段落": ["图3", "正文——具体文字内容片段"],
-          "主题支撑性得分": 7,
-          "多段落共性得分": 8,
-          "推理依据": "为什么这是核心元素",
-          "子项": []
-        }}
-      ]
-    }}
-  ]
-}}
-
-**注意**:
-- 如果分类下直接是具体元素,则元素是该分类的直接子项
-- 如果需要更细的分类,可以在分类下再创建子分类
-- 具体元素(叶子节点)必须包含完整的元素字段(描述、上游支撑、评分等)
-- 分类节点(非叶子)只需要包含名称、描述和子项"""
-
-        # 构建多模态消息
-        message_content = [{"type": "text", "text": prompt}]
-        if images:
-            for idx, image_url in enumerate(images, 1):
-                message_content.append({"type": "text", "text": f"[图片{idx}]"})
-                message_content.append({
-                    "type": "image_url",
-                    "image_url": {"url": image_url}
-                })
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": message_content}
-        ]
-
-        # 调用LLM
-        result = LLMInvoker.safe_invoke(
-            self,
-            "去重与整合",
-            messages,
-            fallback={"元素列表": []}
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"Step2.4结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def step2(self, text_data: dict, images: List[str], topic_description: dict, content_weight: dict = None) -> dict:
-        """第二步:元素 - 提取与主题相关的核心元素
-
-        执行完整的4步筛选流程:
-        1. 候选元素识别
-        2. 双维度评分(主题支撑性 + 多段落共性)
-        3. 严格筛选(满足任一条件即可)
-        4. 去重与整合
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("\n" + "█" * 80)
-        logger.info("█" + " " * 78 + "█")
-        logger.info("█" + " " * 25 + "【Step2】元素提取 - 四步筛选流程" + " " * 23 + "█")
-        logger.info("█" + " " * 78 + "█")
-        logger.info("█" * 80)
-
-        # Step 2.1: 候选元素识别
-        logger.info("\n▶ 开始执行 Step 2.1: 候选元素识别")
-        candidates_result = self.step2_1_identify_candidates(
-            text_data, images, topic_description, content_weight
-        )
-
-        # Step 2.2: 双维度评分
-        logger.info("\n▶ 开始执行 Step 2.2: 双维度评分(主题支撑性 + 多段落共性)")
-        scored_result = self.step2_2_score_dimensions(
-            candidates_result, text_data, images, topic_description, content_weight
-        )
-
-        # Step 2.3: 严格筛选
-        logger.info("\n▶ 开始执行 Step 2.3: 严格筛选(满足任一条件即可)")
-        filtered_result = self.step2_3_strict_filter(scored_result)
-
-        # Step 2.4: 去重与整合
-        logger.info("\n▶ 开始执行 Step 2.4: 去重与整合")
-        final_result = self.step2_4_deduplicate(
-            filtered_result, text_data, images, topic_description
-        )
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"Step2最终结果:\n{json.dumps(final_result, ensure_ascii=False, indent=2)}")
-
-        return final_result
-
-    def _build_weight_info(self, content_weight: dict = None) -> str:
-        """构建权重信息文本"""
-        if not content_weight:
-            return ""
-
-        image_weight = content_weight.get("图片权重", 5)
-        text_weight = content_weight.get("文字权重", 5)
-        primary_source = content_weight.get("主要信息源", "both")
-
-        return f"""# 图文权重信息
-
-- 图片权重: {image_weight}/10
-- 文字权重: {text_weight}/10
-- 主要信息源: {primary_source}
-
-**元素提取时需要考虑权重**:
-- 如果图片权重高,元素提取应更多关注图片中的视觉元素
-- 如果文字权重高,元素提取应更多关注文字描述的概念和对象
-- 如果权重相近,需要综合图文信息提取元素
-"""
-
-    def _build_post_content(self, text_data: dict) -> str:
-        """构建帖子内容文本"""
-        post_content_parts = []
-        if text_data.get("title"):
-            post_content_parts.append(f"标题: {text_data['title']}")
-        if text_data.get("body"):
-            post_content_parts.append(f"正文: {text_data['body']}")
-
-        return "\n".join(post_content_parts) if post_content_parts else "无文本信息"
-
-    def _build_topic_text(self, topic_description: dict) -> str:
-        """构建选题描述文本"""
-        topic_parts = []
-        if topic_description.get("主题"):
-            topic_parts.append(f"主题: {topic_description['主题']}")
-        if topic_description.get("描述"):
-            topic_parts.append(f"描述: {topic_description['描述']}")
-
-        return "\n".join(topic_parts) if topic_parts else "无选题描述"
-
-    def _build_messages(self, state: dict) -> List[dict]:
-        """构建消息 - ScriptUnderstandingAgent 不使用此方法
-
-        本 Agent 使用 step1 和 step2 方法直接构建消息
-        """
-        return []
-
-    def _update_state(self, state: dict, response) -> dict:
-        """更新状态 - ScriptUnderstandingAgent 不使用此方法
-
-        本 Agent 使用 step1 和 step2 方法直接返回结果
-        """
-        return state

BIN
src/components/functions/__pycache__/video_upload_function.cpython-313.pyc


+ 5 - 0
src/components/functions/video_upload_function.py

@@ -61,6 +61,7 @@ class VideoUploadFunction(BaseFunction[Dict[str, Any], Dict[str, Any]]):
                 logger.warning("未提供视频URL,跳过上传")
                 return {
                     **input_data,
+                    "video_file": None,
                     "video_uploaded_uri": None,
                     "video_upload_error": "未提供视频URL"
                 }
@@ -75,6 +76,7 @@ class VideoUploadFunction(BaseFunction[Dict[str, Any], Dict[str, Any]]):
             if not local_video_path:
                 return {
                     **input_data,
+                    "video_file": None,
                     "video_uploaded_uri": None,
                     "video_upload_error": "视频下载失败"
                 }
@@ -97,6 +99,7 @@ class VideoUploadFunction(BaseFunction[Dict[str, Any], Dict[str, Any]]):
             if not video_file:
                 return {
                     **input_data,
+                    "video_file": None,
                     "video_uploaded_uri": None,
                     "video_file_name": None,
                     "video_upload_error": "视频上传到Gemini失败"
@@ -116,6 +119,7 @@ class VideoUploadFunction(BaseFunction[Dict[str, Any], Dict[str, Any]]):
             # 4. 更新state
             return {
                 **input_data,
+                "video_file": video_file,  # 直接保存文件对象,避免后续通过API重新获取
                 "video_uploaded_uri": file_uri,  # 兼容旧版本
                 "video_file_name": file_name,  # 新字段,用于获取文件对象
                 "video_upload_error": None
@@ -125,6 +129,7 @@ class VideoUploadFunction(BaseFunction[Dict[str, Any], Dict[str, Any]]):
             logger.error(f"视频上传失败: {e}", exc_info=True)
             return {
                 **input_data,
+                "video_file": None,
                 "video_uploaded_uri": None,
                 "video_file_name": None,
                 "video_upload_error": str(e)

BIN
src/states/__pycache__/what_deconstruction_state.cpython-313.pyc


+ 1 - 0
src/states/what_deconstruction_state.py

@@ -20,6 +20,7 @@ class WhatDeconstructionState(TypedDict, total=False):
     history_baseline: Optional[Dict[str, Any]]  # 历史基准(可选)
 
     # ========== 中间结果 ==========
+    video_file: Optional[Any]  # 视频文件对象(Gemini File对象,优先使用)
     video_uploaded_uri: Optional[str]  # 视频上传后的URI(兼容旧版本)
     video_file_name: Optional[str]  # 视频上传后的文件名称(用于获取文件对象)
     video_upload_error: Optional[str]  # 视频上传错误信息

+ 372 - 31
src/workflows/decode_workflow.py

@@ -113,16 +113,73 @@ class DecodeWorkflow(BaseGraphAgent):
 
         # 定义流程的边
         workflow.set_entry_point("video_upload")
-        # What解构流程
-        workflow.add_edge("video_upload", "inspiration_points_extraction")
-        workflow.add_edge("inspiration_points_extraction", "purpose_point_extraction")
-        workflow.add_edge("purpose_point_extraction", "key_points_extraction")
-        workflow.add_edge("key_points_extraction", "topic_selection_understanding")
+        # 视频上传后使用条件边:成功则继续,失败则终止
+        workflow.add_conditional_edges(
+            "video_upload",
+            self._check_video_upload_success,
+            {
+                "success": "inspiration_points_extraction",
+                "failure": END
+            }
+        )
+        # What解构流程 - 在关键节点后添加错误检查
+        workflow.add_conditional_edges(
+            "inspiration_points_extraction",
+            self._check_workflow_status,
+            {
+                "continue": "purpose_point_extraction",
+                "terminate": END
+            }
+        )
+        workflow.add_conditional_edges(
+            "purpose_point_extraction",
+            self._check_workflow_status,
+            {
+                "continue": "key_points_extraction",
+                "terminate": END
+            }
+        )
+        workflow.add_conditional_edges(
+            "key_points_extraction",
+            self._check_workflow_status,
+            {
+                "continue": "topic_selection_understanding",
+                "terminate": END
+            }
+        )
+        workflow.add_conditional_edges(
+            "topic_selection_understanding",
+            self._check_workflow_status,
+            {
+                "continue": "section_division",
+                "terminate": END
+            }
+        )
         # 脚本理解流程
-        workflow.add_edge("topic_selection_understanding", "section_division")
-        workflow.add_edge("section_division", "substance_extraction")
-        workflow.add_edge("substance_extraction", "form_extraction")
-        workflow.add_edge("form_extraction", "merge_all_results")
+        workflow.add_conditional_edges(
+            "section_division",
+            self._check_workflow_status,
+            {
+                "continue": "substance_extraction",
+                "terminate": END
+            }
+        )
+        workflow.add_conditional_edges(
+            "substance_extraction",
+            self._check_workflow_status,
+            {
+                "continue": "form_extraction",
+                "terminate": END
+            }
+        )
+        workflow.add_conditional_edges(
+            "form_extraction",
+            self._check_workflow_status,
+            {
+                "continue": "merge_all_results",
+                "terminate": END
+            }
+        )
         workflow.add_edge("merge_all_results", "result_aggregation")
         workflow.add_edge("result_aggregation", END)
 
@@ -130,6 +187,109 @@ class DecodeWorkflow(BaseGraphAgent):
 
         return workflow
 
+    def _check_video_upload_success(self, state: Dict[str, Any]) -> str:
+        """检查视频上传是否成功
+        
+        Returns:
+            "success" 如果上传成功,否则返回 "failure"
+        """
+        video_uri = state.get("video_uploaded_uri")
+        video_error = state.get("video_upload_error")
+        
+        # 如果URI存在且没有错误,则认为成功
+        if video_uri and not video_error:
+            logger.info("视频上传成功,继续执行后续流程")
+            return "success"
+        else:
+            error_msg = video_error or "视频上传失败:未获取到视频URI"
+            logger.error(f"视频上传失败,终止workflow: {error_msg}")
+            # 设置失败信息到状态中
+            state["workflow_failed"] = True
+            state["workflow_error"] = error_msg
+            return "failure"
+
+    def _check_critical_error(self, state: Dict[str, Any], error_source: str = "") -> bool:
+        """检查关键错误,如果存在则设置失败标志
+        
+        Args:
+            state: 状态字典
+            error_source: 错误来源(用于日志)
+            
+        Returns:
+            True 如果存在致命错误,False 否则
+        """
+        # 检查是否已经失败
+        if state.get("workflow_failed"):
+            return True
+        
+        # 检查视频文件是否可用
+        from src.utils.llm_invoker import get_video_file_from_state
+        video_file = get_video_file_from_state(state)
+        if not video_file:
+            error_msg = f"无法获取视频文件对象{('(' + error_source + ')' if error_source else '')}"
+            logger.error(f"{error_msg},终止workflow")
+            state["workflow_failed"] = True
+            state["workflow_error"] = error_msg
+            return True
+        
+        # 检查视频URI是否存在
+        video_uri = state.get("video_uploaded_uri")
+        if not video_uri:
+            video_error = state.get("video_upload_error", "未知错误")
+            error_msg = f"视频URI不存在{('(' + error_source + ')' if error_source else '')}:{video_error}"
+            logger.error(f"{error_msg},终止workflow")
+            state["workflow_failed"] = True
+            state["workflow_error"] = error_msg
+            return True
+        
+        return False
+
+    def _check_agent_result_for_errors(self, result: Dict[str, Any], agent_name: str) -> bool:
+        """检查Agent返回结果中是否包含关键错误
+        
+        Args:
+            result: Agent返回的结果字典
+            agent_name: Agent名称(用于日志)
+            
+        Returns:
+            True 如果存在关键错误,False 否则
+        """
+        if not isinstance(result, dict):
+            return False
+        
+        # 检查常见的错误字段
+        error_fields = ["error", "错误", "video_upload_error"]
+        for field in error_fields:
+            error_value = result.get(field)
+            if error_value:
+                # 检查是否是关键错误(无法获取视频文件等)
+                error_str = str(error_value)
+                if "无法获取视频文件" in error_str or "无法获取视频文件对象" in error_str:
+                    logger.error(f"{agent_name}返回关键错误: {error_str}")
+                    return True
+        
+        # 检查metadata中的错误
+        metadata = result.get("metadata", {})
+        if isinstance(metadata, dict):
+            error_value = metadata.get("error")
+            if error_value:
+                error_str = str(error_value)
+                if "无法获取视频文件" in error_str or "无法获取视频文件对象" in error_str or "未找到视频URI" in error_str:
+                    logger.error(f"{agent_name}返回关键错误: {error_str}")
+                    return True
+        
+        return False
+
+    def _check_workflow_status(self, state: Dict[str, Any]) -> str:
+        """检查workflow状态,用于条件边
+        
+        Returns:
+            "continue" 如果继续执行,否则返回 "terminate"
+        """
+        if state.get("workflow_failed"):
+            return "terminate"
+        return "continue"
+
     def _video_upload_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
         """节点:视频上传(第一步)- 下载视频并上传至Gemini"""
         logger.info("=== 执行节点:视频上传 ===")
@@ -150,7 +310,12 @@ class DecodeWorkflow(BaseGraphAgent):
                 logger.info(f"视频上传完成 - URI: {video_uri}")
             else:
                 error = result.get("video_upload_error", "未知错误")
-                logger.warning(f"视频上传失败: {error}")
+                logger.error(f"视频上传失败: {error}")
+                # 确保失败信息被设置
+                state.update({
+                    "video_uploaded_uri": None,
+                    "video_upload_error": error
+                })
 
         except Exception as e:
             logger.error(f"视频上传失败: {e}", exc_info=True)
@@ -165,6 +330,10 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:灵感点提取(What解构)"""
         logger.info("=== 执行节点:灵感点提取 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "灵感点提取"):
+            return state
+
         try:
             # 初始化Agent
             if not self.inspiration_points_agent.is_initialized:
@@ -176,6 +345,13 @@ class DecodeWorkflow(BaseGraphAgent):
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误
+            if self._check_agent_result_for_errors(result, "灵感点提取Agent"):
+                error_msg = "灵感点提取失败:无法获取视频文件"
+                state["workflow_failed"] = True
+                state["workflow_error"] = error_msg
+                return state
+
             # 安全地获取灵感点数量:total_count 在 metadata 中
             if isinstance(result, dict):
                 metadata = result.get("metadata", {})
@@ -193,6 +369,8 @@ class DecodeWorkflow(BaseGraphAgent):
 
         except Exception as e:
             logger.error(f"灵感点提取失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"灵感点提取异常: {str(e)}"
             state.update({
                 "inspiration_points": {
                     "error": str(e),
@@ -207,6 +385,10 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:目的点提取(What解构)"""
         logger.info("=== 执行节点:目的点提取 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "目的点提取"):
+            return state
+
         try:
             # 初始化Agent
             if not self.purpose_point_agent.is_initialized:
@@ -218,11 +400,20 @@ class DecodeWorkflow(BaseGraphAgent):
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误
+            if self._check_agent_result_for_errors(result, "目的点提取Agent"):
+                error_msg = "目的点提取失败:无法获取视频文件"
+                state["workflow_failed"] = True
+                state["workflow_error"] = error_msg
+                return state
+
             main_purpose = result.get("purpose_point", {}).get("main_purpose", "未知")
             logger.info(f"目的点提取完成 - 主要目的: {main_purpose}")
 
         except Exception as e:
             logger.error(f"目的点提取失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"目的点提取异常: {str(e)}"
             state.update({
                 "purpose_point": {
                     "error": str(e),
@@ -236,6 +427,10 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:关键点提取(What解构)"""
         logger.info("=== 执行节点:关键点提取 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "关键点提取"):
+            return state
+
         try:
             # 初始化Agent
             if not self.key_points_agent.is_initialized:
@@ -247,11 +442,20 @@ class DecodeWorkflow(BaseGraphAgent):
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误
+            if self._check_agent_result_for_errors(result, "关键点提取Agent"):
+                error_msg = "关键点提取失败:无法获取视频文件"
+                state["workflow_failed"] = True
+                state["workflow_error"] = error_msg
+                return state
+
             total_key_points = result.get("key_points", {}).get("total_count", 0)
             logger.info(f"关键点提取完成 - 共 {total_key_points} 个关键点")
 
         except Exception as e:
             logger.error(f"关键点提取失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"关键点提取异常: {str(e)}"
             state.update({
                 "key_points": {
                     "error": str(e),
@@ -266,6 +470,10 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:选题理解(What解构)"""
         logger.info("=== 执行节点:选题理解 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "选题理解"):
+            return state
+
         try:
             # 初始化Agent
             if not self.topic_selection_understanding_agent.is_initialized:
@@ -277,12 +485,23 @@ class DecodeWorkflow(BaseGraphAgent):
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误
+            if self._check_agent_result_for_errors(result, "选题理解Agent"):
+                error_msg = "选题理解失败:无法获取视频文件"
+                state["workflow_failed"] = True
+                state["workflow_error"] = error_msg
+                return state
+
             logger.info(f"选题理解完成 - result: {result}")
 
         except Exception as e:
             logger.error(f"选题理解失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"选题理解异常: {str(e)}"
             state.update({
-                "topic_selection_understanding": {}
+                "topic_selection_understanding": {
+                    "错误": str(e)
+                }
             })
 
         return state
@@ -291,6 +510,10 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:脚本段落划分(脚本理解)"""
         logger.info("=== 执行节点:脚本段落划分 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "脚本段落划分"):
+            return state
+
         try:
             # 初始化Agent
             if not self.section_agent.is_initialized:
@@ -302,12 +525,15 @@ class DecodeWorkflow(BaseGraphAgent):
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误(段落划分如果没有视频文件会返回空结果,不算致命错误)
             sections = result.get("段落列表", [])
             content_category = result.get("内容品类", "未知")
             logger.info(f"脚本段落划分完成 - 内容品类: {content_category}, 段落数: {len(sections)}")
 
         except Exception as e:
             logger.error(f"脚本段落划分失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"脚本段落划分异常: {str(e)}"
             state.update({
                 "内容品类": "未知品类",
                 "段落列表": []
@@ -319,6 +545,10 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:实质元素提取(脚本理解)"""
         logger.info("=== 执行节点:实质元素提取 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "实质元素提取"):
+            return state
+
         try:
             # 初始化Agent
             if not self.substance_agent.is_initialized:
@@ -328,17 +558,54 @@ class DecodeWorkflow(BaseGraphAgent):
             sections = state.get("段落列表", [])
             state["section_division"] = {"段落列表": sections}
 
+            # 验证三点解构信息是否可用
+            inspiration_points = state.get("inspiration_points", [])
+            purpose_point = state.get("purpose_point", {})
+            key_points = state.get("key_points", {})
+            
+            # 统计三点解构信息
+            inspiration_count = len(inspiration_points) if isinstance(inspiration_points, list) else 0
+            if not inspiration_count and isinstance(inspiration_points, dict):
+                inspiration_count = len(inspiration_points.get("points", []))
+            
+            purpose_count = 0
+            if isinstance(purpose_point, dict):
+                purpose_count = len(purpose_point.get("purposes", []))
+            elif isinstance(purpose_point, list):
+                purpose_count = len(purpose_point)
+            
+            key_points_count = 0
+            if isinstance(key_points, dict):
+                key_points_list = key_points.get("key_points", [])
+                key_points_count = len(key_points_list) if isinstance(key_points_list, list) else 0
+            elif isinstance(key_points, list):
+                key_points_count = len(key_points)
+            
+            logger.info(
+                f"实质提取节点 - 三点解构信息检查: "
+                f"灵感点={inspiration_count}, 目的点={purpose_count}, 关键点={key_points_count}"
+            )
+
             # 执行Agent
             result = self.substance_agent.process(state)
 
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误
+            if self._check_agent_result_for_errors(result, "实质元素提取Agent"):
+                error_msg = "实质元素提取失败:无法获取视频文件"
+                state["workflow_failed"] = True
+                state["workflow_error"] = error_msg
+                return state
+
             final_elements = result.get("substance_final_elements", [])
             logger.info(f"实质元素提取完成 - 最终元素数: {len(final_elements)}")
 
         except Exception as e:
             logger.error(f"实质元素提取失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"实质元素提取异常: {str(e)}"
             state.update({
                 "concrete_elements": [],
                 "concrete_concepts": [],
@@ -357,22 +624,63 @@ class DecodeWorkflow(BaseGraphAgent):
         """节点:形式元素提取(脚本理解)"""
         logger.info("=== 执行节点:形式元素提取 ===")
 
+        # 检查关键错误
+        if self._check_critical_error(state, "形式元素提取"):
+            return state
+
         try:
             # 初始化Agent
             if not self.form_agent.is_initialized:
                 self.form_agent.initialize()
 
+            # 验证三点解构信息是否可用
+            inspiration_points = state.get("inspiration_points", [])
+            purpose_point = state.get("purpose_point", {})
+            key_points = state.get("key_points", {})
+            
+            # 统计三点解构信息
+            inspiration_count = len(inspiration_points) if isinstance(inspiration_points, list) else 0
+            if not inspiration_count and isinstance(inspiration_points, dict):
+                inspiration_count = len(inspiration_points.get("points", []))
+            
+            purpose_count = 0
+            if isinstance(purpose_point, dict):
+                purpose_count = len(purpose_point.get("purposes", []))
+            elif isinstance(purpose_point, list):
+                purpose_count = len(purpose_point)
+            
+            key_points_count = 0
+            if isinstance(key_points, dict):
+                key_points_list = key_points.get("key_points", [])
+                key_points_count = len(key_points_list) if isinstance(key_points_list, list) else 0
+            elif isinstance(key_points, list):
+                key_points_count = len(key_points)
+            
+            logger.info(
+                f"形式提取节点 - 三点解构信息检查: "
+                f"灵感点={inspiration_count}, 目的点={purpose_count}, 关键点={key_points_count}"
+            )
+
             # 执行Agent(依赖实质元素)
             result = self.form_agent.process(state)
 
             # 更新状态
             state.update(result)
 
+            # 检查Agent返回结果中是否包含关键错误
+            if self._check_agent_result_for_errors(result, "形式元素提取Agent"):
+                error_msg = "形式元素提取失败:无法获取视频文件"
+                state["workflow_failed"] = True
+                state["workflow_error"] = error_msg
+                return state
+
             final_elements = result.get("form_final_elements", [])
             logger.info(f"形式元素提取完成 - 最终元素数: {len(final_elements)}")
 
         except Exception as e:
             logger.error(f"形式元素提取失败: {e}", exc_info=True)
+            state["workflow_failed"] = True
+            state["workflow_error"] = f"形式元素提取异常: {str(e)}"
             state.update({
                 "concrete_element_forms": [],
                 "concrete_concept_forms": [],
@@ -451,15 +759,6 @@ class DecodeWorkflow(BaseGraphAgent):
     def invoke(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
         """执行工作流(公共接口)- 视频分析版本
 
-        Args:
-            input_data: 输入数据,包含 video 字段(视频URL)
-                格式参考:examples/56898272/视频详情.json
-                {
-                    "video": "http://...",
-                    "title": "...",
-                    ...
-                }
-
         Returns:
             最终解码结果
         """
@@ -469,28 +768,70 @@ class DecodeWorkflow(BaseGraphAgent):
         if not self.is_initialized:
             self.initialize()
 
-        # 构建 text(兼容两种输入方式)
-        if "text" in input_data and isinstance(input_data.get("text"), dict):
-            text = input_data.get("text", {})
-        else:
-            text = {
-                "title": input_data.get("title", ""),
-                "body": input_data.get("body_text", ""),
+        # 验证输入参数
+        video_url = input_data.get("video", "")
+        if not video_url:
+            error_msg = "未提供视频URL,无法执行工作流"
+            logger.error(error_msg)
+            return {
+                "error": error_msg,
+                "workflow_status": "failed",
+                "input_data": input_data
             }
 
         # 初始化状态(包含视频信息,供视频上传和后续Agent使用)
         initial_state = {
-            "video": input_data.get("video", ""),
+            "video": video_url,
             "channel_content_id": input_data.get("channel_content_id", ""),
-            "text": text,
+            "title": input_data.get("title", ""),
             "current_depth": 0,
             "max_depth": self.max_depth,
+            "task_id": input_data.get("task_id", ""),
         }
 
         # 执行工作流
-        result = self.compiled_graph.invoke(initial_state)
+        try:
+            result = self.compiled_graph.invoke(initial_state)
+        except Exception as e:
+            error_msg = f"工作流执行异常: {str(e)}"
+            logger.error(error_msg, exc_info=True)
+            return {
+                "error": error_msg,
+                "workflow_status": "failed",
+                "exception_type": type(e).__name__
+            }
+
+        # 检查是否因为错误而终止
+        if result.get("workflow_failed"):
+            error_msg = result.get("workflow_error", "工作流执行失败")
+            logger.error(f"工作流因错误而终止: {error_msg}")
+            return {
+                "error": error_msg,
+                "video_upload_error": result.get("video_upload_error"),
+                "workflow_status": "failed",
+                "failed_at": result.get("failed_at", "unknown")
+            }
+
+        # 检查是否有最终结果
+        final_result = result.get("final_result")
+        if not final_result:
+            # 如果没有最终结果,检查是否有错误信息
+            if result.get("workflow_error"):
+                error_msg = result.get("workflow_error", "工作流执行失败,未生成结果")
+                logger.error(f"工作流执行失败: {error_msg}")
+                return {
+                    "error": error_msg,
+                    "workflow_status": "failed"
+                }
+            else:
+                logger.warning("工作流执行完成,但未生成最终结果")
+                return {
+                    "error": "工作流执行完成,但未生成最终结果",
+                    "workflow_status": "incomplete",
+                    "state": result
+                }
 
         logger.info("=== 解码工作流执行完成(视频分析) ===")
 
-        return result.get("final_result", {})
+        return final_result
 

+ 0 - 1358
tech_design_document.md

@@ -1,1358 +0,0 @@
-# What解构业务技术设计文档 v1.0
-
-## 1. PRD目标解析
-
-### 1.1 核心目标
-针对小红书多模态内容(图文、视频、音频),从**消费者视角**进行充分必要的What要素逆向解构,通过层级化递归深入分析,识别和提取内容中所有构成成分"what",并将这些要素结构化组织且直接关联原始多媒体素材。
-
-### 1.2 核心价值
-通过对大量内容的"What"要素进行聚合分析,提取爆款内容的内容特征与新兴趋势,为创作者提供可参考的、经过验证的成功要素组合,提升内容创作的确定性。
-
-### 1.3 关键特征
-- **递归深入**:最多10层的递归解构
-- **多模态**:支持图片、视频、音频、文本
-- **层级化**:输出树状结构的元素关系
-- **知识驱动**:通过外部知识库动态获取解构维度
-- **消费者视角**:从内容消费者角度分析亮点和关键点
-
----
-
-## 2. 功能需求拆解
-
-### 2.1 核心功能清单
-
-| 功能模块 | 功能描述 | 实现方式 |
-|---------|---------|---------|
-| **帖子初理解** | 理解帖子整体,提取品类、主题、关键词 | Agent组件 |
-| **知识检索** | 根据query动态获取解构所需的维度知识和工具知识 | Tool组件 |
-| **评论理解** | 分析评论区内容,提取消费者关注的亮点 | Agent组件 |
-| **图片元素解构** | 递归解构图片中的视觉元素 | Agent组件 |
-| **图片分割** | 将图片分割为多个独立的视觉元素 | Tool组件 |
-| **文本元素解构** | 递归解构标题、正文、话题标签 | Agent组件 |
-| **文本切分** | 将文本按知识库指导切分为子元素 | Function组件 |
-| **帖子整体解构** | 从点线面体维度解构帖子整体 | Agent组件 |
-| **解构结果汇总** | 将所有解构结果组织为树状JSON结构 | Function组件 |
-
-### 2.2 输入输出定义
-
-#### 2.2.1 系统输入
-```python
-{
-    # 必选
-    "multimedia_content": {
-        "images": ["image_path1", "image_path2", ...],  # 1-9张图片
-        "video": "video_path",  # 可选
-        "audio": "audio_path",  # 可选
-        "text": {
-            "title": "标题原文",
-            "body": "正文原文",
-            "hashtags": ["#标签1", "#标签2", ...]
-        },
-        "metadata": {
-            "resolution": "1920x1080",
-            "format": "jpg",
-            "duration": 120,
-            ...
-        }
-    },
-
-    # 必选
-    "comments": [
-        {"user": "user1", "content": "评论内容1"},
-        {"user": "user2", "content": "评论内容2"},
-        ...
-    ],
-
-    # 可选
-    "creator_info": {
-        "nickname": "创作者昵称",
-        "avatar": "avatar_url",
-        "followers": 10000,
-        "category": "时尚美妆"
-    },
-
-    # 可选
-    "history_baseline": {
-        "recent_posts": [...],  # 最近20-30个帖子
-        "style_features": {...}
-    }
-}
-```
-
-#### 2.2.2 系统输出
-```python
-{
-    "帖子总结": {
-        "总结维度1": "值1",  # 动态维度,由知识库决定
-        "总结维度2": "值2",
-        ...
-    },
-    "帖子包含元素": [
-        {
-            "id": "1",
-            "what": "元素概述",
-            "描述": {
-                "描述维度1": "值1",  # 动态维度,由知识库决定
-                "描述维度2": "值2",
-                ...
-            },
-            "子节点元素关系": ["关系描述1", "关系描述2", ...],
-            "元素重要性权重": 0.7,
-            "子节点元素": [
-                {
-                    "id": "1_1",
-                    "what": "1_1元素概述",
-                    "描述": {...},
-                    "子节点元素关系": [...],
-                    "元素重要性权重": 0.3,
-                    "子节点元素": [...]
-                },
-                ...
-            ]
-        },
-        ...
-    ]
-}
-```
-
----
-
-## 3. 架构设计
-
-### 3.1 工作流 vs 组件划分原则
-
-根据CLAUDE.md指导原则:
-- **工作流**:负责编排组件执行顺序和流程逻辑
-- **组件**:实现具体业务功能
-
-#### 3.1.1 工作流职责
-1. **WhatDeconstructionWorkflow**(主工作流)
-   - 编排整体解构流程
-   - 控制递归深度(最多10层)
-   - 管理状态传递
-   - 协调各Agent/Tool/Function的调用顺序
-
-#### 3.1.2 组件职责
-根据任务特征选择组件类型:
-- **不确定性任务** → Agent(需要LLM推理、多模态理解、语义分析)
-- **确定性任务 + 需要Agent调用** → Tool
-- **确定性任务 + 不需要Agent调用** → Function
-
----
-
-## 4. 组件设计
-
-### 4.1 Agent组件设计(核心业务逻辑)
-
-#### 4.1.1 PostUnderstandingAgent(帖子初理解Agent)
-
-**组件类型**:Agent(BaseLLMAgent)
-
-**选择依据**:
-- PRD依据:3.1节 "帖子初理解,输出品类、主题、关键词"
-- 需要LLM的语义理解能力
-- 需要多模态理解能力(图文混合)
-- 任务具有不确定性
-
-**功能定义**:
-- 输入:帖子的多模态内容(图片、文本)
-- 输出:品类、主题、关键词列表
-- 处理逻辑:综合分析图片视觉风格、文本语义、话题标签,推理帖子的核心主题
-
-**关键点**:
-1. 多模态融合理解(图文结合)
-2. 提取结构化信息(品类、主题、关键词)
-3. 为后续知识检索提供query参数
-
-**代码框架**:
-```python
-class PostUnderstandingAgent(BaseLLMAgent):
-    """帖子初理解Agent"""
-
-    async def ainvoke(self, state: WhatDeconstructionState) -> dict:
-        """
-        分析帖子多模态内容,提取品类、主题、关键词
-        """
-        # 1. 获取图片和文本内容
-        images = state.get("images")
-        text = state.get("text")
-
-        # 2. 构建多模态prompt
-        prompt = self._build_understanding_prompt(images, text)
-
-        # 3. 调用LLM进行理解
-        result = await self.llm.ainvoke(prompt)
-
-        # 4. 解析返回的品类、主题、关键词
-        parsed_result = self._parse_result(result)
-
-        return {
-            "category": parsed_result["category"],
-            "theme": parsed_result["theme"],
-            "keywords": parsed_result["keywords"]
-        }
-```
-
----
-
-#### 4.1.2 CommentAnalysisAgent(评论理解Agent)
-
-**组件类型**:Agent(BaseLLMAgent)
-
-**选择依据**:
-- PRD依据:2.1.4节 "评论信息用于提取内容消费者关注的亮点"
-- 需要LLM的语义理解和情感分析能力
-- 需要识别隐含的消费者关注点
-- 任务具有不确定性
-
-**功能定义**:
-- 输入:评论区文字内容列表
-- 输出:消费者关注的亮点列表(对应帖子的哪些元素)
-- 处理逻辑:分析评论中的高频词、情感倾向、提及的具体元素
-
-**关键点**:
-1. 情感分析(共鸣点、兴趣点)
-2. 关键点提取(评论中提到的具体内容元素)
-3. 聚合分析(多条评论的共同关注点)
-4. 与帖子内容的映射关系
-
-**代码框架**:
-```python
-class CommentAnalysisAgent(BaseLLMAgent):
-    """评论理解Agent"""
-
-    async def ainvoke(self, state: WhatDeconstructionState) -> dict:
-        """
-        分析评论区内容,提取消费者关注的亮点
-        """
-        # 1. 获取评论数据
-        comments = state.get("comments")
-        post_content = state.get("text")
-
-        # 2. 构建分析prompt
-        prompt = self._build_comment_analysis_prompt(comments, post_content)
-
-        # 3. 调用LLM分析
-        result = await self.llm.ainvoke(prompt)
-
-        # 4. 提取亮点并与帖子内容映射
-        highlights = self._extract_highlights(result)
-
-        return {
-            "consumer_highlights": highlights
-        }
-```
-
----
-
-#### 4.1.3 RecursiveImageDeconstructionAgent(图片递归解构Agent)
-
-**组件类型**:Agent(BaseReactAgent)
-
-**选择依据**:
-- PRD依据:3.3节 "对单一节点的递归解构"
-- 需要LLM的视觉理解能力
-- 需要动态调用知识库和图片分割工具(ReAct模式)
-- 需要递归判断是否继续分割
-- 任务具有高度不确定性
-
-**功能定义**:
-- 输入:当前图片节点、父节点信息、递归深度
-- 输出:图片的what描述、描述维度、子元素列表
-- 处理逻辑:
-  1. 粗理解图片,生成what字段值
-  2. 根据what值生成query,调用知识库获取描述维度
-  3. 根据描述维度细致理解图片,填充描述值
-  4. 判断是否需要继续分割(调用知识库)
-  5. 如需分割,调用图片分割工具,递归处理子元素
-
-**关键点**:
-1. **ReAct模式**:思考→行动→观察的循环(调用知识库、调用分割工具)
-2. **递归控制**:深度限制(10层)、停止条件判断
-3. **知识库交互**:动态生成query获取解构维度
-4. **图片分割判断**:优先判断是否为多子图拼接
-5. **多模态理解**:将视觉信息转化为文字描述
-
-**代码框架**:
-```python
-class RecursiveImageDeconstructionAgent(BaseReactAgent):
-    """图片递归解构Agent"""
-
-    def __init__(self, llm, tools: List[BaseTool], max_depth: int = 10):
-        super().__init__(llm, tools)
-        self.max_depth = max_depth
-        self.knowledge_retrieval_tool = None  # 知识检索工具
-        self.image_segment_tool = None  # 图片分割工具
-
-    async def ainvoke(self, state: WhatDeconstructionState) -> dict:
-        """
-        递归解构图片元素
-        """
-        # 1. 获取当前节点信息
-        current_image = state.get("current_image")
-        parent_id = state.get("parent_id", "")
-        depth = state.get("depth", 0)
-
-        # 检查递归深度
-        if depth >= self.max_depth:
-            return {"continue_decompose": False}
-
-        # 2. 粗理解图片(生成what字段)
-        what_value = await self._rough_understanding(current_image)
-
-        # 3. 生成query,获取描述维度
-        query = self._generate_description_query(what_value)
-        description_dimensions = await self.knowledge_retrieval_tool.ainvoke(query)
-
-        # 4. 根据描述维度细致理解图片
-        description_values = await self._detailed_understanding(
-            current_image, description_dimensions
-        )
-
-        # 5. 判断是否需要继续分割
-        split_query = self._generate_split_query(what_value, description_values)
-        should_split = await self.knowledge_retrieval_tool.ainvoke(split_query)
-
-        result = {
-            "what": what_value,
-            "description": description_values,
-            "continue_decompose": should_split
-        }
-
-        # 6. 如需分割,执行分割并递归
-        if should_split:
-            # 判断是否为多子图拼接
-            sub_elements = await self._identify_sub_elements(current_image)
-
-            if sub_elements:
-                # 调用图片分割工具
-                segmented_images = await self.image_segment_tool.ainvoke(
-                    image=current_image,
-                    elements_description=sub_elements
-                )
-
-                # 递归处理子元素
-                children = []
-                for idx, sub_img in enumerate(segmented_images):
-                    child_state = {
-                        "current_image": sub_img,
-                        "parent_id": f"{parent_id}_{idx+1}",
-                        "depth": depth + 1
-                    }
-                    child_result = await self.ainvoke(child_state)
-                    children.append(child_result)
-
-                result["children"] = children
-
-        return result
-```
-
----
-
-#### 4.1.4 RecursiveTextDeconstructionAgent(文本递归解构Agent)
-
-**组件类型**:Agent(BaseReactAgent)
-
-**选择依据**:
-- PRD依据:3.3节 "对单一节点的递归解构"
-- 需要LLM的文本语义理解能力
-- 需要动态调用知识库和文本切分工具
-- 需要递归判断是否继续切分
-- 任务具有不确定性
-
-**功能定义**:
-- 输入:当前文本节点(标题/正文/标签)、父节点信息、递归深度
-- 输出:文本的what描述、描述维度、子元素列表
-- 处理逻辑:与图片解构类似,但针对文本元素
-
-**关键点**:
-1. **文本原文保留**:当节点是文本时,what字段必须是原文,不可改写
-2. **语义切分**:根据知识库指导切分文本结构
-3. **递归控制**:深度限制(10层)
-4. **充分必要原则**:子节点文本必须完整覆盖父节点文本
-
-**代码框架**:
-```python
-class RecursiveTextDeconstructionAgent(BaseReactAgent):
-    """文本递归解构Agent"""
-
-    def __init__(self, llm, tools: List[BaseTool], max_depth: int = 10):
-        super().__init__(llm, tools)
-        self.max_depth = max_depth
-        self.knowledge_retrieval_tool = None
-        self.text_split_tool = None
-
-    async def ainvoke(self, state: WhatDeconstructionState) -> dict:
-        """
-        递归解构文本元素
-        """
-        # 1. 获取当前节点信息
-        current_text = state.get("current_text")
-        text_type = state.get("text_type")  # title/body/hashtags
-        parent_id = state.get("parent_id", "")
-        depth = state.get("depth", 0)
-
-        if depth >= self.max_depth:
-            return {"continue_decompose": False}
-
-        # 2. what字段直接使用原文
-        what_value = current_text
-
-        # 3. 生成query,获取描述维度
-        query = self._generate_description_query(what_value, text_type)
-        description_dimensions = await self.knowledge_retrieval_tool.ainvoke(query)
-
-        # 4. 根据描述维度分析文本
-        description_values = await self._analyze_text(
-            current_text, description_dimensions
-        )
-
-        # 5. 判断是否需要继续切分
-        split_query = self._generate_split_query(what_value, description_values)
-        should_split = await self.knowledge_retrieval_tool.ainvoke(split_query)
-
-        result = {
-            "what": what_value,
-            "description": description_values,
-            "continue_decompose": should_split
-        }
-
-        # 6. 如需切分,执行切分并递归
-        if should_split:
-            # 根据知识库指导切分文本
-            split_result = await self.text_split_tool.ainvoke(
-                text=current_text,
-                split_guidance=description_dimensions
-            )
-
-            # 递归处理子元素
-            children = []
-            for idx, sub_text in enumerate(split_result):
-                child_state = {
-                    "current_text": sub_text,
-                    "text_type": text_type,
-                    "parent_id": f"{parent_id}_{idx+1}",
-                    "depth": depth + 1
-                }
-                child_result = await self.ainvoke(child_state)
-                children.append(child_result)
-
-            result["children"] = children
-
-        return result
-```
-
----
-
-#### 4.1.5 PostSummaryDeconstructionAgent(帖子整体解构Agent)
-
-**组件类型**:Agent(BaseLLMAgent)
-
-**选择依据**:
-- PRD依据:3.2节 "帖子整体解构环节"
-- 需要LLM的综合理解能力
-- 需要从点线面体维度抽象总结
-- 任务具有不确定性
-
-**功能定义**:
-- 输入:帖子的所有元素解构结果、知识库返回的总结维度
-- 输出:帖子总结对象(动态维度)
-- 处理逻辑:综合所有子元素的解构结果,从宏观维度总结帖子特征
-
-**关键点**:
-1. 动态总结维度(由知识库决定)
-2. 综合多模态元素的解构结果
-3. 从整体视角抽象提炼
-4. 包含消费者亮点维度
-
-**代码框架**:
-```python
-class PostSummaryDeconstructionAgent(BaseLLMAgent):
-    """帖子整体解构Agent"""
-
-    async def ainvoke(self, state: WhatDeconstructionState) -> dict:
-        """
-        从点线面体维度解构帖子整体
-        """
-        # 1. 获取所有子元素解构结果
-        image_results = state.get("image_deconstruction_results")
-        text_results = state.get("text_deconstruction_results")
-        consumer_highlights = state.get("consumer_highlights")
-
-        # 2. 获取总结维度(通过知识库)
-        category = state.get("category")
-        theme = state.get("theme")
-        keywords = state.get("keywords")
-
-        query = self._generate_summary_query(category, theme, keywords)
-        summary_dimensions = await self._call_knowledge_retrieval(query)
-
-        # 3. 根据总结维度综合分析
-        prompt = self._build_summary_prompt(
-            image_results, text_results, consumer_highlights, summary_dimensions
-        )
-
-        result = await self.llm.ainvoke(prompt)
-
-        # 4. 解析总结结果
-        summary = self._parse_summary(result, summary_dimensions)
-
-        return {
-            "post_summary": summary
-        }
-
-    def _generate_summary_query(self, category, theme, keywords):
-        """生成总结维度query"""
-        return f"""对于一篇主题为"{theme}",品类为"{category}",关键词包含"{', '.join(keywords)}"的多模态社交媒体帖子,从内容创作者视角进行What要素的初步识别和分类,需要使用哪些通用工具?"""
-```
-
----
-
-### 4.2 Tool组件设计(可被Agent调用)
-
-#### 4.2.1 KnowledgeRetrievalTool(知识检索工具)
-
-**组件类型**:Tool(BaseTool)
-
-**选择依据**:
-- PRD依据:3.2节、3.3.2节"对单一节点解构如何获取知识"
-- 需要被Agent调用(各解构Agent都需要调用)
-- 任务确定性(API调用)
-
-**功能定义**:
-- 输入:query字符串
-- 输出:知识库返回的结果(描述维度、工具推荐、判断结果等)
-- 处理逻辑:调用外部知识库API,解析返回结果
-
-**关键点**:
-1. 支持多种query类型(描述维度、工具知识、分割判断)
-2. 结果格式化(转换为Agent可用的结构化数据)
-3. 错误处理和降级(知识库不可用时使用LLM默认知识)
-
-**代码框架**:
-```python
-from tools.base import BaseTool
-from typing import Dict, Any
-
-class KnowledgeRetrievalTool(BaseTool):
-    """知识检索工具"""
-
-    name: str = "knowledge_retrieval"
-    description: str = "根据query检索What解构所需的知识(描述维度、工具推荐、分割判断等)"
-
-    def __init__(self, knowledge_base_url: str):
-        super().__init__()
-        self.knowledge_base_url = knowledge_base_url
-
-    async def _arun(self, query: str, query_type: str = "description") -> Dict[str, Any]:
-        """
-        调用知识库检索
-
-        Args:
-            query: 查询语句
-            query_type: 查询类型(description/tool/split_decision)
-        """
-        # 1. 调用知识库API
-        response = await self._call_knowledge_base(query)
-
-        # 2. 根据query类型解析结果
-        if query_type == "description":
-            # 返回描述维度列表
-            return self._parse_description_dimensions(response)
-        elif query_type == "tool":
-            # 返回推荐工具列表
-            return self._parse_tool_recommendations(response)
-        elif query_type == "split_decision":
-            # 返回是否分割的判断
-            return self._parse_split_decision(response)
-
-        return response
-
-    def _parse_description_dimensions(self, response) -> Dict[str, Any]:
-        """解析描述维度"""
-        return {
-            "dimensions": response.get("dimensions", []),
-            "count": len(response.get("dimensions", []))
-        }
-```
-
-**复用决策**:
-- 现有组件:`tools/knowledge_retrieval_tools.py` 已存在知识检索工具
-- 决策:**复用并修改**
-- 理由:现有工具实现了基本的知识库查询功能,但需要增强支持本PRD中的多种query类型
-
----
-
-#### 4.2.2 ImageSegmentTool(图片分割工具)
-
-**组件类型**:Tool(BaseTool)
-
-**选择依据**:
-- PRD依据:3.3.1节 "判断图片元素是否要继续分割拆解"
-- 需要被Agent调用(RecursiveImageDeconstructionAgent调用)
-- 任务确定性(调用分割模型)
-
-**功能定义**:
-- 输入:图片路径、子元素文字描述列表
-- 输出:分割后的子图片路径列表
-- 处理逻辑:调用图片分割模型(如SAM、GroundingDINO等),根据文字描述分割图片
-
-**关键点**:
-1. 支持多种分割模式(完整子图拼接拆解、视觉元素抠图)
-2. 保存分割结果并返回路径
-3. 分割质量验证
-
-**代码框架**:
-```python
-from tools.base import BaseTool
-from typing import List
-
-class ImageSegmentTool(BaseTool):
-    """图片分割工具"""
-
-    name: str = "image_segment"
-    description: str = "根据文字描述将图片分割为多个独立的视觉元素"
-
-    def __init__(self, segment_model, output_dir: str):
-        super().__init__()
-        self.segment_model = segment_model
-        self.output_dir = output_dir
-
-    async def _arun(
-        self,
-        image_path: str,
-        elements_description: List[str],
-        segment_type: str = "object"  # object/grid
-    ) -> List[str]:
-        """
-        分割图片
-
-        Args:
-            image_path: 原始图片路径
-            elements_description: 子元素文字描述列表
-            segment_type: 分割类型(object: 视觉元素分割, grid: 子图拼接拆解)
-
-        Returns:
-            分割后的子图片路径列表
-        """
-        # 1. 加载图片
-        image = self._load_image(image_path)
-
-        # 2. 根据分割类型选择策略
-        if segment_type == "grid":
-            # 拼接图拆解(简单切割)
-            sub_images = self._grid_split(image, elements_description)
-        else:
-            # 视觉元素分割(调用分割模型)
-            sub_images = await self._segment_objects(image, elements_description)
-
-        # 3. 保存子图片
-        saved_paths = []
-        for idx, sub_img in enumerate(sub_images):
-            save_path = f"{self.output_dir}/{self._generate_filename(idx)}"
-            self._save_image(sub_img, save_path)
-            saved_paths.append(save_path)
-
-        return saved_paths
-
-    async def _segment_objects(self, image, descriptions):
-        """调用分割模型分割视觉元素"""
-        # 调用SAM/GroundingDINO等模型
-        masks = await self.segment_model.segment(image, descriptions)
-        return self._extract_objects(image, masks)
-```
-
-**复用决策**:
-- 现有组件:`tools/segment_tools.py` 已存在图片分割工具
-- 决策:**直接复用**
-- 理由:现有工具已实现"根据对象名称对图像进行分割,抽取指定对象的切片"功能,完全符合本PRD需求
-
----
-
-### 4.3 Function组件设计(不可被Agent调用)
-
-#### 4.3.1 TextSplitFunction(文本切分函数)
-
-**组件类型**:Function(BaseFunction)
-
-**选择依据**:
-- PRD依据:3.5节 "文本切分工具"
-- 不需要被Agent直接调用(在工作流中调用)
-- 任务确定性(字符串处理)
-
-**功能定义**:
-- 输入:文本字符串、切分规则(由知识库返回)
-- 输出:切分后的子文本列表
-- 处理逻辑:根据切分规则(句子、段落、语义块)切分文本
-
-**关键点**:
-1. 多种切分策略(按句、按段、按语义)
-2. 保留原文完整性(子文本合并后等于父文本)
-3. 不重复不遗漏
-
-**代码框架**:
-```python
-from functions.base import BaseFunction
-from typing import List, Dict
-
-class TextSplitFunction(BaseFunction):
-    """文本切分函数"""
-
-    def __init__(self):
-        super().__init__()
-
-    def invoke(self, text: str, split_rules: Dict) -> List[str]:
-        """
-        切分文本
-
-        Args:
-            text: 待切分文本
-            split_rules: 切分规则(由知识库返回)
-                {
-                    "strategy": "sentence/paragraph/semantic",
-                    "params": {...}
-                }
-
-        Returns:
-            切分后的子文本列表
-        """
-        strategy = split_rules.get("strategy", "sentence")
-
-        if strategy == "sentence":
-            return self._split_by_sentence(text)
-        elif strategy == "paragraph":
-            return self._split_by_paragraph(text)
-        elif strategy == "semantic":
-            return self._split_by_semantic(text, split_rules.get("params"))
-
-        return [text]
-
-    def _split_by_sentence(self, text: str) -> List[str]:
-        """按句子切分"""
-        import re
-        sentences = re.split(r'[。!?\n]', text)
-        return [s.strip() for s in sentences if s.strip()]
-
-    def _split_by_paragraph(self, text: str) -> List[str]:
-        """按段落切分"""
-        paragraphs = text.split('\n\n')
-        return [p.strip() for p in paragraphs if p.strip()]
-
-    def _split_by_semantic(self, text: str, params: Dict) -> List[str]:
-        """按语义块切分(需要语义模型)"""
-        # TODO: 实现语义切分逻辑
-        pass
-```
-
-**复用决策**:
-- 现有组件:无
-- 决策:**完全新增**
-- 理由:项目中不存在文本切分功能,需要从BaseFunction新建
-
----
-
-#### 4.3.2 ResultAggregationFunction(结果汇总函数)
-
-**组件类型**:Function(BaseFunction)
-
-**选择依据**:
-- 任务确定性(数据结构转换)
-- 不需要被Agent调用(工作流最后环节)
-
-**功能定义**:
-- 输入:各Agent的解构结果
-- 输出:最终的树状JSON结构
-- 处理逻辑:将分散的解构结果组装为符合PRD格式的JSON
-
-**关键点**:
-1. 递归组装树状结构
-2. 字段格式验证
-3. ID生成规则
-4. 元素重要性权重计算
-
-**代码框架**:
-```python
-from functions.base import BaseFunction
-from typing import Dict, List
-
-class ResultAggregationFunction(BaseFunction):
-    """结果汇总函数"""
-
-    def invoke(self, state: Dict) -> Dict:
-        """
-        汇总所有解构结果为树状JSON
-
-        Args:
-            state: 工作流状态,包含所有解构结果
-
-        Returns:
-            最终的树状JSON结构
-        """
-        # 1. 提取各部分结果
-        post_summary = state.get("post_summary")
-        image_results = state.get("image_deconstruction_results")
-        text_results = state.get("text_deconstruction_results")
-
-        # 2. 构建树状结构
-        elements = []
-
-        # 添加图片元素
-        for idx, img_result in enumerate(image_results):
-            element = self._build_element_tree(
-                id=f"{idx+1}",
-                result=img_result,
-                element_type="image"
-            )
-            elements.append(element)
-
-        # 添加文本元素
-        text_id_start = len(image_results) + 1
-        for idx, text_result in enumerate(text_results):
-            element = self._build_element_tree(
-                id=f"{text_id_start+idx}",
-                result=text_result,
-                element_type="text"
-            )
-            elements.append(element)
-
-        # 3. 组装最终JSON
-        final_result = {
-            "帖子总结": post_summary,
-            "帖子包含元素": elements
-        }
-
-        # 4. 验证JSON格式
-        self._validate_result(final_result)
-
-        return final_result
-
-    def _build_element_tree(self, id: str, result: Dict, element_type: str) -> Dict:
-        """递归构建元素树"""
-        element = {
-            "id": id,
-            "what": result["what"],
-            "描述": result["description"],
-            "子节点元素关系": result.get("relationships", []),
-            "元素重要性权重": result.get("importance_weight", 0.5),
-            "子节点元素": []
-        }
-
-        # 递归处理子节点
-        if "children" in result:
-            for idx, child in enumerate(result["children"]):
-                child_element = self._build_element_tree(
-                    id=f"{id}_{idx+1}",
-                    result=child,
-                    element_type=element_type
-                )
-                element["子节点元素"].append(child_element)
-
-        return element
-```
-
-**复用决策**:
-- 现有组件:`functions/json_utils.py` 存在JSON处理函数
-- 决策:**复用并修改**
-- 理由:可复用JSON格式化和验证功能,但需新增树状结构构建逻辑
-
----
-
-## 5. 工作流设计
-
-### 5.1 WhatDeconstructionWorkflow(主工作流)
-
-**继承基类**:BaseGraphAgent(LangGraph)
-
-**状态定义**:
-```python
-from typing import TypedDict, List, Dict, Any
-
-class WhatDeconstructionState(TypedDict):
-    # 输入数据
-    images: List[str]
-    video: str
-    audio: str
-    text: Dict[str, Any]  # {title, body, hashtags}
-    comments: List[Dict[str, str]]
-    creator_info: Dict[str, Any]
-
-    # 中间状态
-    category: str
-    theme: str
-    keywords: List[str]
-    consumer_highlights: List[Dict]
-
-    # 解构结果
-    image_deconstruction_results: List[Dict]
-    text_deconstruction_results: List[Dict]
-    post_summary: Dict[str, Any]
-
-    # 最终输出
-    final_result: Dict[str, Any]
-
-    # 递归控制
-    current_depth: int
-    max_depth: int
-```
-
-**节点定义**:
-```python
-nodes = {
-    "post_understanding": PostUnderstandingAgent,
-    "comment_analysis": CommentAnalysisAgent,
-    "image_deconstruction": RecursiveImageDeconstructionAgent,
-    "text_deconstruction": RecursiveTextDeconstructionAgent,
-    "post_summary": PostSummaryDeconstructionAgent,
-    "result_aggregation": ResultAggregationFunction
-}
-```
-
-**流程图**:
-```mermaid
-graph TD
-    START([开始]) --> A[帖子初理解<br/>PostUnderstandingAgent]
-    A --> B[评论理解<br/>CommentAnalysisAgent]
-    B --> C{并行解构}
-    C --> D[图片递归解构<br/>RecursiveImageDeconstructionAgent]
-    C --> E[文本递归解构<br/>RecursiveTextDeconstructionAgent]
-    D --> F[帖子整体解构<br/>PostSummaryDeconstructionAgent]
-    E --> F
-    F --> G[结果汇总<br/>ResultAggregationFunction]
-    G --> END([结束])
-```
-
-**核心代码**:
-```python
-from langgraph.graph import StateGraph
-from agents.base import BaseGraphAgent
-
-class WhatDeconstructionWorkflow(BaseGraphAgent):
-    """What解构主工作流"""
-
-    def __init__(self, llm, tools: List[BaseTool]):
-        super().__init__()
-        self.llm = llm
-        self.tools = tools
-
-        # 初始化所有组件
-        self.post_understanding_agent = PostUnderstandingAgent(llm)
-        self.comment_analysis_agent = CommentAnalysisAgent(llm)
-        self.image_deconstruction_agent = RecursiveImageDeconstructionAgent(
-            llm, tools, max_depth=10
-        )
-        self.text_deconstruction_agent = RecursiveTextDeconstructionAgent(
-            llm, tools, max_depth=10
-        )
-        self.post_summary_agent = PostSummaryDeconstructionAgent(llm)
-        self.result_aggregation_func = ResultAggregationFunction()
-
-        # 构建工作流图
-        self.graph = self._build_graph()
-
-    def _build_graph(self) -> StateGraph:
-        """构建LangGraph工作流"""
-        workflow = StateGraph(WhatDeconstructionState)
-
-        # 添加节点
-        workflow.add_node("post_understanding", self._post_understanding_node)
-        workflow.add_node("comment_analysis", self._comment_analysis_node)
-        workflow.add_node("image_deconstruction", self._image_deconstruction_node)
-        workflow.add_node("text_deconstruction", self._text_deconstruction_node)
-        workflow.add_node("post_summary", self._post_summary_node)
-        workflow.add_node("result_aggregation", self._result_aggregation_node)
-
-        # 定义边(流程顺序)
-        workflow.set_entry_point("post_understanding")
-        workflow.add_edge("post_understanding", "comment_analysis")
-        workflow.add_edge("comment_analysis", "image_deconstruction")
-        workflow.add_edge("comment_analysis", "text_deconstruction")
-        workflow.add_edge("image_deconstruction", "post_summary")
-        workflow.add_edge("text_deconstruction", "post_summary")
-        workflow.add_edge("post_summary", "result_aggregation")
-        workflow.set_finish_point("result_aggregation")
-
-        return workflow.compile()
-
-    async def _post_understanding_node(self, state: WhatDeconstructionState):
-        """节点:帖子初理解"""
-        result = await self.post_understanding_agent.ainvoke(state)
-        return {
-            "category": result["category"],
-            "theme": result["theme"],
-            "keywords": result["keywords"]
-        }
-
-    async def _comment_analysis_node(self, state: WhatDeconstructionState):
-        """节点:评论理解"""
-        result = await self.comment_analysis_agent.ainvoke(state)
-        return {
-            "consumer_highlights": result["consumer_highlights"]
-        }
-
-    async def _image_deconstruction_node(self, state: WhatDeconstructionState):
-        """节点:图片递归解构"""
-        images = state.get("images", [])
-        results = []
-
-        for idx, image_path in enumerate(images):
-            img_state = {
-                "current_image": image_path,
-                "parent_id": f"image_{idx+1}",
-                "depth": 0,
-                "max_depth": 10,
-                "category": state["category"],
-                "theme": state["theme"]
-            }
-            result = await self.image_deconstruction_agent.ainvoke(img_state)
-            results.append(result)
-
-        return {"image_deconstruction_results": results}
-
-    async def _text_deconstruction_node(self, state: WhatDeconstructionState):
-        """节点:文本递归解构"""
-        text_data = state.get("text", {})
-        results = []
-
-        # 解构标题
-        if text_data.get("title"):
-            title_state = {
-                "current_text": text_data["title"],
-                "text_type": "title",
-                "parent_id": "title",
-                "depth": 0,
-                "max_depth": 10
-            }
-            title_result = await self.text_deconstruction_agent.ainvoke(title_state)
-            results.append(title_result)
-
-        # 解构正文
-        if text_data.get("body"):
-            body_state = {
-                "current_text": text_data["body"],
-                "text_type": "body",
-                "parent_id": "body",
-                "depth": 0,
-                "max_depth": 10
-            }
-            body_result = await self.text_deconstruction_agent.ainvoke(body_state)
-            results.append(body_result)
-
-        # 解构话题标签
-        if text_data.get("hashtags"):
-            hashtags_text = " ".join(text_data["hashtags"])
-            hashtags_state = {
-                "current_text": hashtags_text,
-                "text_type": "hashtags",
-                "parent_id": "hashtags",
-                "depth": 0,
-                "max_depth": 10
-            }
-            hashtags_result = await self.text_deconstruction_agent.ainvoke(hashtags_state)
-            results.append(hashtags_result)
-
-        return {"text_deconstruction_results": results}
-
-    async def _post_summary_node(self, state: WhatDeconstructionState):
-        """节点:帖子整体解构"""
-        result = await self.post_summary_agent.ainvoke(state)
-        return {"post_summary": result["post_summary"]}
-
-    async def _result_aggregation_node(self, state: WhatDeconstructionState):
-        """节点:结果汇总"""
-        final_result = self.result_aggregation_func.invoke(state)
-        return {"final_result": final_result}
-
-    async def ainvoke(self, input_data: Dict) -> Dict:
-        """执行工作流"""
-        # 初始化状态
-        initial_state = WhatDeconstructionState(
-            images=input_data.get("multimedia_content", {}).get("images", []),
-            video=input_data.get("multimedia_content", {}).get("video", ""),
-            audio=input_data.get("multimedia_content", {}).get("audio", ""),
-            text=input_data.get("multimedia_content", {}).get("text", {}),
-            comments=input_data.get("comments", []),
-            creator_info=input_data.get("creator_info", {}),
-            current_depth=0,
-            max_depth=10
-        )
-
-        # 执行工作流
-        result = await self.graph.ainvoke(initial_state)
-
-        return result["final_result"]
-```
-
----
-
-## 6. 数据结构设计
-
-### 6.1 WhatDeconstructionState(工作流状态)
-
-```python
-from typing import TypedDict, List, Dict, Any, Optional
-
-class WhatDeconstructionState(TypedDict):
-    """What解构工作流状态"""
-
-    # ========== 输入数据 ==========
-    images: List[str]  # 图片路径列表(1-9张)
-    video: Optional[str]  # 视频路径
-    audio: Optional[str]  # 音频路径
-    text: Dict[str, Any]  # {title, body, hashtags}
-    comments: List[Dict[str, str]]  # [{user, content}, ...]
-    creator_info: Optional[Dict[str, Any]]  # 创作者信息(可选)
-    history_baseline: Optional[Dict[str, Any]]  # 历史基准(可选)
-
-    # ========== 中间状态 ==========
-    category: str  # 品类
-    theme: str  # 主题
-    keywords: List[str]  # 关键词列表
-    consumer_highlights: List[Dict]  # 消费者关注的亮点
-
-    # ========== 解构结果 ==========
-    image_deconstruction_results: List[Dict]  # 图片解构结果
-    text_deconstruction_results: List[Dict]  # 文本解构结果
-    post_summary: Dict[str, Any]  # 帖子总结
-
-    # ========== 最终输出 ==========
-    final_result: Dict[str, Any]  # 最终的树状JSON结果
-
-    # ========== 递归控制 ==========
-    current_depth: int  # 当前递归深度
-    max_depth: int  # 最大递归深度(默认10)
-```
-
-### 6.2 ElementNode(元素节点结构)
-
-```python
-class ElementNode(TypedDict):
-    """元素节点结构(树状结构的节点)"""
-
-    id: str  # 节点ID(如 "1", "1_1", "1_1_2")
-    what: str  # 元素概述(文本原文 or 图片描述)
-    描述: Dict[str, Any]  # 动态描述维度(由知识库决定)
-    子节点元素关系: List[str]  # 子节点之间的关系描述
-    元素重要性权重: float  # 0-1之间的权重值
-    子节点元素: List['ElementNode']  # 递归的子节点列表
-    图片链接: str  # 图片分解后的链接
-```
-
-### 6.3 FinalOutput(最终输出结构)
-
-```python
-class FinalOutput(TypedDict):
-    """最终输出的JSON结构"""
-
-    帖子总结: Dict[str, Any]  # 动态总结维度(由知识库决定)
-    帖子包含元素: List[ElementNode]  # 元素树的根节点列表
-```
-
----
-
-## 7. 技术决策依据总结
-
-### 7.1 工作流设计决策
-
-| 决策点 | 选择 | PRD依据 | 技术理由 |
-|-------|------|---------|---------|
-| **工作流框架** | LangGraph | CLAUDE.md要求 | 支持状态管理、节点编排、递归控制 |
-| **并行解构** | 图片和文本并行 | 3.1节流程图 | 提升效率,两者无依赖关系 |
-| **递归深度** | 最多10层 | 2.2.3节 | 防止无限递归,控制计算成本 |
-
-### 7.2 Agent组件决策
-
-| Agent | 选择原因 | PRD依据 | 关键能力 |
-|-------|---------|---------|---------|
-| **PostUnderstandingAgent** | 多模态理解+语义推理 | 3.1节 | 提取品类/主题/关键词 |
-| **CommentAnalysisAgent** | 情感分析+语义理解 | 2.1.4节 | 识别消费者关注点 |
-| **RecursiveImageDeconstructionAgent** | ReAct模式+递归控制 | 3.3节+3.3.1节 | 动态调用工具+递归分割 |
-| **RecursiveTextDeconstructionAgent** | ReAct模式+递归控制 | 3.3节 | 动态调用工具+递归切分 |
-| **PostSummaryDeconstructionAgent** | 综合分析+抽象提炼 | 3.2节 | 点线面体维度总结 |
-
-### 7.3 Tool组件决策
-
-| Tool | 选择原因 | PRD依据 | 关键能力 |
-|------|---------|---------|---------|
-| **KnowledgeRetrievalTool** | Agent需调用+确定性任务 | 3.2节+3.3.2节 | 知识库API调用 |
-| **ImageSegmentTool** | Agent需调用+确定性任务 | 3.3.1节+3.5节 | 图片分割模型调用 |
-
-### 7.4 Function组件决策
-
-| Function | 选择原因 | PRD依据 | 关键能力 |
-|----------|---------|---------|---------|
-| **TextSplitFunction** | 不需Agent调用+确定性任务 | 3.5节 | 文本切分逻辑 |
-| **ResultAggregationFunction** | 不需Agent调用+确定性任务 | 2.2.2节 | JSON结构组装 |
-
-### 7.5 复用决策
-
-| 组件 | 决策 | 理由 |
-|------|------|------|
-| **knowledge_retrieval_tools.py** | 复用并修改 | 已有基础查询功能,需增强query类型支持 |
-| **segment_tools.py** | 直接复用 | 功能完全匹配PRD需求 |
-| **json_utils.py** | 复用并修改 | 可复用JSON处理,需新增树状结构构建 |
-| **TextSplitFunction** | 完全新增 | 项目中无此功能 |
-| **所有Agent** | 完全新增 | PRD业务逻辑全新 |
-
----
-
-## 8. 实现优先级
-
-### Phase 1: 基础框架(核心路径)
-1. **WhatDeconstructionState** - 定义工作流状态
-2. **PostUnderstandingAgent** - 帖子初理解
-3. **KnowledgeRetrievalTool**(复用修改)- 知识检索
-4. **WhatDeconstructionWorkflow** - 主工作流骨架
-
-### Phase 2: 核心解构能力
-5. **RecursiveImageDeconstructionAgent** - 图片递归解构
-6. **ImageSegmentTool**(直接复用)- 图片分割
-7. **RecursiveTextDeconstructionAgent** - 文本递归解构
-8. **TextSplitFunction**(新增)- 文本切分
-
-### Phase 3: 增强功能
-9. **CommentAnalysisAgent** - 评论理解
-10. **PostSummaryDeconstructionAgent** - 帖子整体解构
-11. **ResultAggregationFunction**(复用修改)- 结果汇总
-
-### Phase 4: 完善优化
-12. 递归深度控制优化
-13. 错误处理和降级策略
-14. 性能优化和并行化
-15. 单元测试和集成测试
-
----
-
-## 9. 关键技术挑战
-
-### 9.1 递归控制
-**挑战**:如何准确判断是否停止递归?
-**方案**:
-- 依赖知识库返回的判断结果
-- 设置硬性深度限制(10层)
-- 最小元素判断(不可再分的最小语义/视觉单元)
-
-### 9.2 知识库Query设计
-**挑战**:如何动态生成有效的query?
-**方案**:
-- 固定句式模板 + 动态内容填充
-- 多种query类型(描述维度、工具推荐、分割判断)
-- 降级策略(知识库不可用时使用LLM默认知识)
-
-### 9.3 图片分割准确性
-**挑战**:如何准确分割视觉元素?
-**方案**:
-- 优先判断是否为多子图拼接(简单切割)
-- 使用成熟的分割模型(SAM、GroundingDINO)
-- LLM生成精准的元素描述文本
-
-### 9.4 不重复不遗漏原则
-**挑战**:如何保证子节点完整覆盖父节点?
-**方案**:
-- 文本:子节点文本合并后必须等于父节点文本
-- 图片:子元素的掩码合并后必须覆盖父图片区域
-- LLM验证:最后由LLM验证是否符合"充分必要"原则
-
----
-
-## 10. 附录:Query模板汇总
-
-### 10.1 帖子整体解构Query
-```
-对于一篇主题为"{帖子主题}",品类为"{帖子品类}",关键词包含"{帖子关键词列表}"的多模态社交媒体帖子,从内容创作者视角进行What要素的初步识别和分类,需要使用哪些通用工具?
-```
-
-### 10.2 描述维度获取Query
-```
-刻画描述"{元素的what字段值}"核心特征的角度和维度有哪些?请尽可能不重不漏列举全。
-```
-
-### 10.3 分割判断Query
-```
-该节点"{元素的what字段值}"是否需要继续拆解分割?
-```
-
-### 10.4 图片整体分割工具Query
-```
-对一张描述为"{图片整体内容的简要描述}"的图片,从内容创作者视角进行视觉元素分割,需要使用哪些图片分割/抠图工具?
-```
-
-### 10.5 图片亮点提取工具Query
-```
-从内容创作者视角,对一张描述为"{图片整体内容的简要描述}"的图片,进行亮点或关键点的提取,需要使用什么图片分析工具或大模型视觉理解工具?
-```
-
-### 10.6 图片特定元素分割工具Query
-```
-对一张"{XXX图片整体描述}"的图片中的"{YYY具体视觉元素描述}"的视觉元素,从内容创作者视角进行细致分割拆解,需要使用哪些高精度图片分割/抠图工具或细粒度图像理解大模型?
-```
-
-### 10.7 图片特定元素亮点提取Query
-```
-从内容创作者视角,对一张"{XXX图片整体描述}"的图片中的"{YYY具体视觉元素描述}"的视觉元素,进行亮点或关键点的提取,需要使用什么图像特征提取工具或专业视觉大模型?
-```
-
-### 10.8 文本段落分割工具Query
-```
-对一段描述为"{文本段落内容摘要}"的文本段落,从内容创作者视角进行结构化分割拆解,需要使用什么文本切分工具或文本结构化大模型?
-```
-
-### 10.9 文本段落亮点提取Query
-```
-从内容创作者视角,对一段描述为"{文本段落内容摘要}"的文本段落(包含标题、正文、话题标签),进行亮点或关键点的提取,需要使用什么情感分析工具、语义理解工具或文本摘要大模型?
-```
-
----
-
-## 11. 文件结构规划
-
-```
-project/
-├── workflows/
-│   └── what_deconstruction_workflow.py  # 主工作流(新增)
-├── agents/
-│   ├── post_understanding_agent.py  # 帖子初理解Agent(新增)
-│   ├── comment_analysis_agent.py  # 评论理解Agent(新增)
-│   ├── recursive_image_deconstruction_agent.py  # 图片递归解构Agent(新增)
-│   ├── recursive_text_deconstruction_agent.py  # 文本递归解构Agent(新增)
-│   └── post_summary_deconstruction_agent.py  # 帖子整体解构Agent(新增)
-├── tools/
-│   ├── knowledge_retrieval_tools.py  # 知识检索工具(复用修改)
-│   └── segment_tools.py  # 图片分割工具(直接复用)
-├── functions/
-│   ├── text_split_function.py  # 文本切分函数(新增)
-│   └── result_aggregation_function.py  # 结果汇总函数(复用修改)
-├── states/
-│   └── what_deconstruction_state.py  # 工作流状态定义(新增)
-└── tests/
-    ├── test_agents/
-    ├── test_tools/
-    ├── test_functions/
-    └── test_workflow/
-```
-
----
-
-## 12. 总结
-
-本技术设计文档基于PRD v1.2,完整设计了What解构业务的技术实现方案:
-
-### 12.1 核心设计原则
-- **组件化**:Agent/Tool/Function清晰分工
-- **递归解构**:层级化深入分析(最多10层)
-- **知识驱动**:动态从知识库获取解构维度
-- **充分必要**:子节点不重不漏覆盖父节点
-
-### 12.2 技术架构
-- **1个工作流**:WhatDeconstructionWorkflow(LangGraph)
-- **5个Agent**:帖子初理解、评论理解、图片递归解构、文本递归解构、帖子整体解构
-- **2个Tool**:知识检索、图片分割
-- **2个Function**:文本切分、结果汇总
-
-### 12.3 复用策略
-- **直接复用**:segment_tools.py
-- **复用修改**:knowledge_retrieval_tools.py、json_utils.py
-- **完全新增**:所有Agent、TextSplitFunction、主工作流
-
-### 12.4 实现路径
-按Phase 1-4完成全部路径
-

+ 0 - 1846
tech_design_document_update.md

@@ -1,1846 +0,0 @@
-# What解构业务技术设计文档 v2.0
-
-> **文档目标**: 为开发者提供清晰、可执行的技术实现指导
->
-> **设计原则**: 每个决策都有明确的PRD依据和技术理由
-
----
-
-## 目录
-1. [PRD目标解析](#1-prd目标解析)
-2. [功能需求拆解](#2-功能需求拆解)
-3. [工作流 vs 组件职责划分](#3-工作流-vs-组件职责划分)
-4. [工作流设计](#4-工作流设计)
-5. [组件设计](#5-组件设计)
-6. [数据结构设计](#6-数据结构设计)
-7. [实现路线图](#7-实现路线图)
-
----
-
-## 1. PRD目标解析
-
-### 1.1 核心目标是什么?
-
-**PRD原文** (1. 需求目标):
-> 针对给定的小红书多模态内容(图文、视频、音频),从消费者视角进行充分必要的What要素逆向解构,通过层级化递归深入分析,识别和提取内容中所有构成成分"what"。
-
-**核心目标拆解**:
-
-| 维度 | 具体含义 | PRD依据 |
-|------|---------|---------|
-| **输入对象** | 小红书多模态内容(图+文+视频+音频) | 2.1.1节 |
-| **视角** | 从消费者视角分析 | 1. 需求目标 |
-| **方法** | 层级化递归深入分析 | 1. 需求目标、2.2.3节 |
-| **产出** | What要素树(JSON结构) | 2.2.1节、2.2.2节 |
-| **价值** | 通过聚合分析提取爆款特征和趋势 | 1. 需求目标(核心价值) |
-
-**目标提炼**:
-> **构建一个递归解构系统,将多模态帖子内容自上而下、从整体到局部地拆解为树状的"What元素"结构,每个元素包含动态描述维度(由知识库提供),并关联原始素材。**
-
-### 1.2 关键约束条件
-
-**PRD依据** (2.2.3节、4节):
-
-1. **递归深度**: 最多10层
-2. **停止条件**:
-   - 已是不可再分的最小单元
-   - 达到10层深度
-3. **解构原则**:
-   - 由宏观到微观、由整体到局部
-   - 子节点不重不漏(充分必要)
-4. **知识驱动**: 描述维度、分割判断都由知识库动态提供
-5. **客观性**: 严格基于帖子内容,禁止臆测
-
----
-
-## 2. 功能需求拆解
-
-### 2.1 核心功能清单
-
-基于PRD 3.1节"总体解构流程"和3.3节"对单一节点的递归解构",提炼出以下核心功能:
-
-| 功能ID | 功能名称 | 功能描述 | PRD依据 | 实现方式 |
-|--------|---------|---------|---------|---------|
-| **F1** | 帖子初理解 | 分析帖子整体,提取品类、主题、关键词 | 3.1节步骤1 | Agent组件 |
-| **F2** | 知识需求生成 | 根据品类/主题/关键词生成解构所需的query | 3.2节 | 工作流逻辑 |
-| **F3** | 知识检索 | 调用知识库获取描述维度、工具推荐、分割判断 | 3.2节、3.3.2节 | Tool组件 |
-| **F4** | 评论理解 | 分析评论,提取消费者关注的亮点 | 3.1节步骤4 | Agent组件 |
-| **F5** | 图片递归解构 | 递归解构图片元素,直到不可再分 | 3.1节步骤6-7、3.3节 | Agent组件 |
-| **F6** | 图片分割 | 将图片分割为多个独立的视觉元素 | 3.3.1节、3.5节 | Tool组件 |
-| **F7** | 文本递归解构 | 递归解构文本元素(标题/正文/标签) | 3.1节步骤8-11、3.3节 | Agent组件 |
-| **F8** | 文本切分 | 根据知识库指导切分文本 | 3.5节 | Function组件 |
-| **F9** | 帖子整体解构 | 从点线面体维度总结帖子 | 3.1节步骤13、3.2节 | Agent组件 |
-| **F10** | 结果汇总 | 将所有解构结果组装为树状JSON | 2.2.2节 | Function组件 |
-
-### 2.2 功能依赖关系
-
-```
-F1(帖子初理解)
-  ↓
-F2(知识需求生成) → F3(知识检索)
-  ↓
-F4(评论理解)
-  ↓
-[F5(图片递归解构) + F6(图片分割)] 并行 [F7(文本递归解构) + F8(文本切分)]
-  ↓
-F9(帖子整体解构)
-  ↓
-F10(结果汇总)
-```
-
-**关键观察**:
-- F5和F7内部都会多次调用F3(知识检索)
-- F6和F8是F5、F7的支撑工具
-- F2是一个轻量级逻辑,不需要独立组件
-
----
-
-## 3. 工作流 vs 组件职责划分
-
-### 3.1 划分原则
-
-**CLAUDE.md原则**:
-> - **工作流**:负责编排组件执行顺序和流程逻辑
-> - **组件**:实现具体业务功能
-
-### 3.2 职责划分表
-
-| 功能 | 实现方式 | 决策依据 |
-|------|---------|---------|
-| **F1 帖子初理解** | **Agent组件** | 需要多模态理解+语义推理(不确定性任务) |
-| **F2 知识需求生成** | **工作流逻辑** | 简单的字符串拼接,不需要独立组件 |
-| **F3 知识检索** | **Tool组件** | 需要被Agent调用 + 确定性API调用 |
-| **F4 评论理解** | **Agent组件** | 需要情感分析+语义理解(不确定性任务) |
-| **F5 图片递归解构** | **Agent组件** | 需要LLM视觉理解+ReAct模式调用工具 |
-| **F6 图片分割** | **Tool组件** | 需要被Agent调用 + 确定性模型调用 |
-| **F7 文本递归解构** | **Agent组件** | 需要LLM语义理解+ReAct模式调用工具 |
-| **F8 文本切分** | **Function组件** | 确定性字符串处理 + 工作流直接调用 |
-| **F9 帖子整体解构** | **Agent组件** | 需要综合分析+抽象提炼(不确定性任务) |
-| **F10 结果汇总** | **Function组件** | 确定性数据结构转换 + 工作流直接调用 |
-| **递归控制** | **工作流逻辑** | 编排递归流程,管理状态和深度 |
-| **并行执行** | **工作流逻辑** | 编排图片和文本并行解构 |
-
-**总结**:
-- **1个主工作流**: WhatDeconstructionWorkflow
-- **5个Agent组件**: F1, F4, F5, F7, F9
-- **2个Tool组件**: F3, F6
-- **2个Function组件**: F8, F10
-
----
-
-## 4. 工作流设计
-
-### 4.1 工作流核心职责
-
-**WhatDeconstructionWorkflow** 的核心职责:
-
-1. **编排执行顺序**: 按PRD 3.1节流程图编排各组件
-2. **管理状态传递**: 维护 WhatDeconstructionState,在各节点间传递数据
-3. **控制递归深度**: 确保不超过10层
-4. **并行执行控制**: 图片和文本解构并行执行
-5. **知识query生成**: 根据中间结果动态生成知识库query
-
-### 4.2 工作流流程图
-
-**PRD依据**: 3.1节"总体解构流程"
-
-```mermaid
-graph TD
-    START([开始]) --> N1[节点1: 帖子初理解<br/>PostUnderstandingAgent]
-    N1 --> N2[节点2: 评论理解<br/>CommentAnalysisAgent]
-    N2 --> PARALLEL{并行分支}
-
-    PARALLEL --> N3[节点3: 图片递归解构<br/>RecursiveImageDeconstructionAgent]
-    PARALLEL --> N4[节点4: 文本递归解构<br/>RecursiveTextDeconstructionAgent]
-
-    N3 --> N5[节点5: 帖子整体解构<br/>PostSummaryDeconstructionAgent]
-    N4 --> N5
-
-    N5 --> N6[节点6: 结果汇总<br/>ResultAggregationFunction]
-    N6 --> END([结束])
-
-    style N3 fill:#e1f5ff
-    style N4 fill:#e1f5ff
-    style PARALLEL fill:#fff3cd
-```
-
-**流程说明**:
-
-| 节点 | 输入 | 输出 | PRD依据 |
-|------|------|------|---------|
-| N1 | 帖子多模态内容 | category, theme, keywords | 3.1节步骤1 |
-| N2 | 评论列表 | consumer_highlights | 3.1节步骤4 |
-| N3 | images列表 | image_deconstruction_results | 3.1节步骤6-7 |
-| N4 | text对象 | text_deconstruction_results | 3.1节步骤8-11 |
-| N5 | 所有子元素解构结果 | post_summary | 3.1节步骤13 |
-| N6 | 所有解构结果 | final_result (JSON树) | 2.2.2节 |
-
-### 4.3 工作流核心代码框架
-
-```python
-from langgraph.graph import StateGraph, END
-from typing import TypedDict, List, Dict, Any
-
-# 1. 定义状态
-class WhatDeconstructionState(TypedDict):
-    """工作流状态定义"""
-    # 输入
-    images: List[str]
-    text: Dict[str, Any]  # {title, body, hashtags}
-    comments: List[Dict[str, str]]
-
-    # 中间状态
-    category: str
-    theme: str
-    keywords: List[str]
-    consumer_highlights: List[Dict]
-
-    # 解构结果
-    image_deconstruction_results: List[Dict]
-    text_deconstruction_results: List[Dict]
-    post_summary: Dict[str, Any]
-
-    # 最终输出
-    final_result: Dict[str, Any]
-
-# 2. 工作流类
-class WhatDeconstructionWorkflow:
-    """What解构主工作流"""
-
-    def __init__(self, llm, knowledge_retrieval_tool, image_segment_tool):
-        # 初始化所有组件
-        self.post_understanding_agent = PostUnderstandingAgent(llm)
-        self.comment_analysis_agent = CommentAnalysisAgent(llm)
-        self.image_agent = RecursiveImageDeconstructionAgent(
-            llm, knowledge_retrieval_tool, image_segment_tool, max_depth=10
-        )
-        self.text_agent = RecursiveTextDeconstructionAgent(
-            llm, knowledge_retrieval_tool, max_depth=10
-        )
-        self.summary_agent = PostSummaryDeconstructionAgent(llm, knowledge_retrieval_tool)
-        self.aggregation_func = ResultAggregationFunction()
-
-        # 构建图
-        self.graph = self._build_graph()
-
-    def _build_graph(self) -> StateGraph:
-        """构建LangGraph工作流"""
-        workflow = StateGraph(WhatDeconstructionState)
-
-        # 添加节点
-        workflow.add_node("post_understanding", self._node_post_understanding)
-        workflow.add_node("comment_analysis", self._node_comment_analysis)
-        workflow.add_node("image_deconstruction", self._node_image_deconstruction)
-        workflow.add_node("text_deconstruction", self._node_text_deconstruction)
-        workflow.add_node("post_summary", self._node_post_summary)
-        workflow.add_node("result_aggregation", self._node_result_aggregation)
-
-        # 定义边(流程)
-        workflow.set_entry_point("post_understanding")
-        workflow.add_edge("post_understanding", "comment_analysis")
-
-        # 并行分支:评论理解后,图片和文本并行解构
-        workflow.add_edge("comment_analysis", "image_deconstruction")
-        workflow.add_edge("comment_analysis", "text_deconstruction")
-
-        # 汇聚:图片和文本都完成后,执行帖子整体解构
-        workflow.add_edge("image_deconstruction", "post_summary")
-        workflow.add_edge("text_deconstruction", "post_summary")
-
-        workflow.add_edge("post_summary", "result_aggregation")
-        workflow.add_edge("result_aggregation", END)
-
-        return workflow.compile()
-
-    # 3. 节点实现
-    async def _node_post_understanding(self, state: WhatDeconstructionState):
-        """节点1: 帖子初理解"""
-        result = await self.post_understanding_agent.ainvoke({
-            "images": state["images"],
-            "text": state["text"]
-        })
-        return {
-            "category": result["category"],
-            "theme": result["theme"],
-            "keywords": result["keywords"]
-        }
-
-    async def _node_comment_analysis(self, state: WhatDeconstructionState):
-        """节点2: 评论理解"""
-        result = await self.comment_analysis_agent.ainvoke({
-            "comments": state["comments"],
-            "post_content": state["text"]
-        })
-        return {"consumer_highlights": result["highlights"]}
-
-    async def _node_image_deconstruction(self, state: WhatDeconstructionState):
-        """节点3: 图片递归解构(处理所有图片)"""
-        results = []
-        for idx, image_path in enumerate(state["images"]):
-            result = await self.image_agent.ainvoke({
-                "image_path": image_path,
-                "node_id": f"img_{idx+1}",
-                "depth": 0,
-                "category": state["category"],
-                "theme": state["theme"]
-            })
-            results.append(result)
-        return {"image_deconstruction_results": results}
-
-    async def _node_text_deconstruction(self, state: WhatDeconstructionState):
-        """节点4: 文本递归解构(标题+正文+标签)"""
-        results = []
-        text_data = state["text"]
-
-        # 解构标题
-        if text_data.get("title"):
-            title_result = await self.text_agent.ainvoke({
-                "text": text_data["title"],
-                "text_type": "title",
-                "node_id": "title",
-                "depth": 0
-            })
-            results.append(title_result)
-
-        # 解构正文
-        if text_data.get("body"):
-            body_result = await self.text_agent.ainvoke({
-                "text": text_data["body"],
-                "text_type": "body",
-                "node_id": "body",
-                "depth": 0
-            })
-            results.append(body_result)
-
-        # 解构话题标签
-        if text_data.get("hashtags"):
-            hashtags_result = await self.text_agent.ainvoke({
-                "text": " ".join(text_data["hashtags"]),
-                "text_type": "hashtags",
-                "node_id": "hashtags",
-                "depth": 0
-            })
-            results.append(hashtags_result)
-
-        return {"text_deconstruction_results": results}
-
-    async def _node_post_summary(self, state: WhatDeconstructionState):
-        """节点5: 帖子整体解构"""
-        result = await self.summary_agent.ainvoke({
-            "category": state["category"],
-            "theme": state["theme"],
-            "keywords": state["keywords"],
-            "image_results": state["image_deconstruction_results"],
-            "text_results": state["text_deconstruction_results"],
-            "consumer_highlights": state["consumer_highlights"]
-        })
-        return {"post_summary": result["summary"]}
-
-    async def _node_result_aggregation(self, state: WhatDeconstructionState):
-        """节点6: 结果汇总"""
-        final_result = self.aggregation_func.invoke({
-            "post_summary": state["post_summary"],
-            "image_results": state["image_deconstruction_results"],
-            "text_results": state["text_deconstruction_results"]
-        })
-        return {"final_result": final_result}
-
-    # 4. 入口方法
-    async def ainvoke(self, input_data: Dict) -> Dict:
-        """执行工作流"""
-        initial_state = WhatDeconstructionState(
-            images=input_data["multimedia_content"]["images"],
-            text=input_data["multimedia_content"]["text"],
-            comments=input_data["comments"]
-        )
-        result = await self.graph.ainvoke(initial_state)
-        return result["final_result"]
-```
-
-**关键设计点**:
-
-1. **并行执行**: `image_deconstruction` 和 `text_deconstruction` 从 `comment_analysis` 同时出发
-2. **状态传递**: 通过 `WhatDeconstructionState` 在节点间传递数据
-3. **递归封装**: 图片/文本的递归逻辑封装在各自的Agent内部
-4. **知识库调用**: 在Agent内部调用 KnowledgeRetrievalTool
-
----
-
-## 5. 组件设计
-
-### 5.1 Agent组件总览
-
-**CLAUDE.md原则**:
-> **Agent组件特征**: 不确定性任务 + LLM智能能力 + 推理/多模态理解/语义分析
-
-| Agent | 任务类型 | LLM能力需求 | PRD依据 |
-|-------|---------|------------|---------|
-| PostUnderstandingAgent | 多模态理解 | 图文融合理解 | 3.1节步骤1 |
-| CommentAnalysisAgent | 语义理解 | 情感分析、关键点提取 | 3.1节步骤4、2.1.4节 |
-| RecursiveImageDeconstructionAgent | 视觉理解+推理 | 图片理解、分割判断 | 3.3节、3.3.1节 |
-| RecursiveTextDeconstructionAgent | 语义理解+推理 | 文本分析、切分判断 | 3.3节 |
-| PostSummaryDeconstructionAgent | 综合分析 | 抽象提炼、多维度总结 | 3.2节 |
-
----
-
-### 5.2 Agent组件详细设计
-
-#### 5.2.1 PostUnderstandingAgent(帖子初理解)
-
-**组件类型**: Agent (继承 BaseLLMAgent)
-
-**选择依据**:
-- **PRD依据**: 3.1节步骤1 "帖子初理解,输出品类、主题、关键词"
-- **CLAUDE.md依据**: 需要多模态理解(图+文),属于不确定性任务
-
-**核心职责**:
-1. 综合分析图片和文本内容
-2. 推理提取品类(如"时尚美妆"、"美食"等)
-3. 总结主题(如"夏日穿搭")
-4. 提取关键词(如"OOTD"、"小白裙")
-
-**关键点**:
-
-| 关键点 | 实现方式 | 依据 |
-|--------|---------|------|
-| **多模态输入** | 将图片和文本一起输入给LLM | LLM具备多模态理解能力 |
-| **结构化输出** | 使用JSON格式约束LLM输出 | 需要提取固定字段 |
-| **品类识别准确性** | 在prompt中提供常见品类列表 | 提升品类识别准确性 |
-
-**代码框架**:
-
-```python
-from agents.base import BaseLLMAgent
-from typing import List, Dict, Any
-
-class PostUnderstandingAgent(BaseLLMAgent):
-    """帖子初理解Agent
-
-    PRD依据: 3.1节步骤1
-    功能: 分析帖子多模态内容,提取品类、主题、关键词
-    """
-
-    async def ainvoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        执行帖子理解
-
-        Args:
-            state: {
-                "images": List[str],  # 图片路径列表
-                "text": {             # 文本内容
-                    "title": str,
-                    "body": str,
-                    "hashtags": List[str]
-                }
-            }
-
-        Returns:
-            {
-                "category": str,      # 品类
-                "theme": str,         # 主题
-                "keywords": List[str] # 关键词
-            }
-        """
-        # 1. 构建多模态prompt
-        prompt = self._build_prompt(state["images"], state["text"])
-
-        # 2. 调用LLM
-        response = await self.llm.ainvoke(prompt)
-
-        # 3. 解析结果
-        result = self._parse_response(response)
-
-        return result
-
-    def _build_prompt(self, images: List[str], text: Dict) -> str:
-        """构建prompt
-
-        关键点:
-        - 多模态输入(图片+文本)
-        - 明确要求输出JSON格式
-        - 提供品类参考列表
-        """
-        return f"""
-你是一个小红书内容分析专家。请分析以下帖子的多模态内容,提取品类、主题和关键词。
-
-【文本内容】
-标题: {text.get('title', '')}
-正文: {text.get('body', '')}
-话题标签: {', '.join(text.get('hashtags', []))}
-
-【图片】
-{self._format_images(images)}
-
-【常见品类参考】
-时尚美妆、美食、旅游、家居、母婴、运动健身、数码科技等
-
-请以JSON格式输出:
-{{
-    "category": "品类",
-    "theme": "主题",
-    "keywords": ["关键词1", "关键词2", ...]
-}}
-"""
-
-    def _parse_response(self, response: str) -> Dict:
-        """解析LLM响应,提取JSON"""
-        import json
-        # 提取JSON部分
-        json_str = self._extract_json(response)
-        return json.loads(json_str)
-```
-
----
-
-#### 5.2.2 CommentAnalysisAgent(评论理解)
-
-**组件类型**: Agent (继承 BaseLLMAgent)
-
-**选择依据**:
-- **PRD依据**: 2.1.4节 "评论信息用于提取内容消费者关注的亮点"
-- **CLAUDE.md依据**: 需要情感分析和语义理解,属于不确定性任务
-
-**核心职责**:
-1. 分析评论区文字内容
-2. 识别消费者关注的亮点(对应帖子的哪些元素)
-3. 提取高频关注点和情绪共鸣点
-
-**关键点**:
-
-| 关键点 | 实现方式 | 依据 |
-|--------|---------|------|
-| **亮点映射** | 将评论中的关注点映射到帖子具体元素 | PRD 2.1.4节要求 |
-| **情感分析** | 识别评论的情感倾向(共鸣、兴趣、疑问等) | 理解消费者反应 |
-| **高频提取** | 聚合多条评论的共同关注点 | 识别关键吸引点 |
-
-**代码框架**:
-
-```python
-class CommentAnalysisAgent(BaseLLMAgent):
-    """评论理解Agent
-
-    PRD依据: 2.1.4节
-    功能: 分析评论,提取消费者关注的亮点
-    """
-
-    async def ainvoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        执行评论分析
-
-        Args:
-            state: {
-                "comments": List[Dict],  # [{user, content}, ...]
-                "post_content": Dict     # 帖子内容(用于映射)
-            }
-
-        Returns:
-            {
-                "highlights": List[Dict]  # 消费者关注的亮点列表
-                # [
-                #   {
-                #     "element": "封面图中的小白裙",
-                #     "reason": "多条评论询问购买链接",
-                #     "emotion": "兴趣、购买意愿"
-                #   },
-                #   ...
-                # ]
-            }
-        """
-        prompt = self._build_prompt(state["comments"], state["post_content"])
-        response = await self.llm.ainvoke(prompt)
-        return {"highlights": self._parse_highlights(response)}
-
-    def _build_prompt(self, comments: List[Dict], post_content: Dict) -> str:
-        """构建prompt
-
-        关键点:
-        - 提供帖子内容作为参考(用于映射)
-        - 要求识别具体元素
-        - 要求分析情感和关注原因
-        """
-        comments_text = "\n".join([
-            f"- {c['content']}" for c in comments
-        ])
-
-        return f"""
-你是一个内容分析专家。请分析评论区内容,提取消费者关注的亮点。
-
-【帖子内容】
-标题: {post_content.get('title', '')}
-正文: {post_content.get('body', '')}
-
-【评论区】
-{comments_text}
-
-请分析:
-1. 评论中提到了帖子的哪些具体元素(封面、图片、标题、正文中的哪部分)
-2. 消费者对这些元素的关注原因(为什么吸引他们)
-3. 评论的情感倾向(共鸣、兴趣、疑问、赞美等)
-
-以JSON格式输出亮点列表:
-{{
-    "highlights": [
-        {{
-            "element": "具体元素描述",
-            "reason": "关注原因",
-            "emotion": "情感倾向"
-        }},
-        ...
-    ]
-}}
-"""
-```
-
----
-
-#### 5.2.3 RecursiveImageDeconstructionAgent(图片递归解构)
-
-**组件类型**: Agent (继承 BaseReactAgent)
-
-**选择依据**:
-- **PRD依据**: 3.3节 "对单一节点的递归解构"、3.3.1节 "判断图片元素是否要继续分割拆解"
-- **CLAUDE.md依据**: 需要LLM视觉理解 + 动态调用工具(ReAct模式)
-
-**核心职责**:
-1. 粗理解图片,生成what字段
-2. 调用知识库获取描述维度
-3. 细致理解图片,填充描述值
-4. 判断是否需要分割(调用知识库)
-5. 如需分割,调用图片分割工具
-6. 递归处理子元素(最多10层)
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **ReAct模式** | 思考→行动→观察循环 | 需要多次调用工具 |
-| **递归控制** | 深度参数 + 停止条件判断 | 2.2.3节(最多10层) |
-| **知识驱动** | 描述维度和分割判断都由知识库提供 | 3.3.2节 |
-| **分割优先判断** | 优先判断是否为多子图拼接 | 3.3.1节 |
-| **what字段生成** | 对图片的文本描述 | 2.2.2节 |
-| **判断不依赖分割** | 判断是否拆解基于完整原图+文字描述,不依赖分割结果 | PRD 3.3.1节原则 |
-
-**递归流程图**:
-
-```mermaid
-graph TD
-    START([接收图片节点]) --> DEPTH{检查深度<br/>是否≥10层?}
-    DEPTH -->|是| STOP([返回结果<br/>不再递归])
-    DEPTH -->|否| ROUGH[粗理解图片<br/>基于完整原图+节点描述<br/>生成what字段]
-
-    ROUGH --> QUERY1[生成query:<br/>获取描述维度]
-    QUERY1 --> KB1[调用知识库]
-    KB1 --> DETAIL[细致理解图片<br/>基于完整原图+节点描述+维度<br/>填充描述维度值]
-
-    DETAIL --> QUERY2[生成query:<br/>是否需要分割?]
-    QUERY2 --> KB2[调用知识库<br/>基于完整原图+what+描述]
-    KB2 --> DECISION{知识库判断:<br/>是否分割?}
-
-    DECISION -->|否| STOP
-    DECISION -->|是| IDENTIFY[识别子元素<br/>基于完整原图<br/>生成子元素描述]
-    IDENTIFY --> SEGMENT[调用图片分割工具<br/>传入完整原图<br/>获得子图片列表]
-
-    SEGMENT --> RECURSE[遍历子图片<br/>递归调用自身<br/>传递完整原图]
-    RECURSE --> STOP
-
-    style ROUGH fill:#e1f5ff
-    style DETAIL fill:#e1f5ff
-    style KB1 fill:#fff3cd
-    style KB2 fill:#fff3cd
-    style SEGMENT fill:#d4edda
-```
-
-**代码框架**:
-
-```python
-from agents.base import BaseReactAgent
-from tools.base import BaseTool
-from typing import List, Dict, Any
-
-class RecursiveImageDeconstructionAgent(BaseReactAgent):
-    """图片递归解构Agent
-
-    PRD依据: 3.3节、3.3.1节
-    功能: 递归解构图片元素,最多10层
-    """
-
-    def __init__(
-        self,
-        llm,
-        knowledge_retrieval_tool: BaseTool,
-        image_segment_tool: BaseTool,
-        max_depth: int = 10
-    ):
-        super().__init__(llm, tools=[knowledge_retrieval_tool, image_segment_tool])
-        self.knowledge_tool = knowledge_retrieval_tool
-        self.segment_tool = image_segment_tool
-        self.max_depth = max_depth
-
-    async def ainvoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        递归解构图片
-
-        Args:
-            state: {
-                "original_image_path": str,  # **完整原图路径**(PRD 3.3.1:始终传递未分割的完整图)
-                "current_image_path": str,   # 当前节点对应的图片路径(首次调用时=original_image_path)
-                "node_description": str,     # 当前节点的文字描述(首次为空,递归时为子元素描述)
-                "node_id": str,              # 节点ID(如"1", "1_1")
-                "depth": int,                # 当前递归深度
-                "category": str,             # 帖子品类(用于生成query)
-                "theme": str                 # 帖子主题(用于生成query)
-            }
-
-        Returns:
-            {
-                "id": str,
-                "what": str,
-                "description": Dict,
-                "children": List[Dict],  # 子元素(递归结构)
-                "importance_weight": float
-            }
-        """
-        # 步骤1: 检查递归深度(PRD 2.2.3节)
-        if state["depth"] >= self.max_depth:
-            return self._build_leaf_node(state)
-
-        # 步骤2: 粗理解当前节点,生成what字段(PRD 3.3节流程步骤B0)
-        # **关键**: 基于完整原图 + 节点描述进行理解(PRD 3.3.1原则)
-        what_value = await self._rough_understanding(
-            original_image=state["original_image_path"],
-            current_image=state["current_image_path"],
-            node_description=state.get("node_description", "")
-        )
-
-        # 步骤3: 调用知识库获取描述维度(PRD 3.3.2节)
-        description_query = self._generate_description_query(what_value)
-        description_dimensions = await self.knowledge_tool.ainvoke({
-            "query": description_query,
-            "query_type": "description"
-        })
-
-        # 步骤4: 根据描述维度细致理解当前节点(PRD 3.3节流程步骤C)
-        # **关键**: 基于完整原图 + 节点描述 + 描述维度进行理解(PRD 3.3.1原则)
-        description_values = await self._detailed_understanding(
-            original_image=state["original_image_path"],
-            current_image=state["current_image_path"],
-            node_description=state.get("node_description", ""),
-            description_dimensions=description_dimensions
-        )
-
-        # 步骤5: 判断是否需要分割(PRD 3.3节流程步骤D0)
-        # **关键**: 判断分割的输入是完整原图 + what + 描述,不依赖分割结果(PRD 3.3.1原则)
-        split_query = self._generate_split_query(what_value, description_values)
-        split_decision = await self.knowledge_tool.ainvoke({
-            "query": split_query,
-            "query_type": "split_decision"
-        })
-
-        # 构建当前节点结果
-        result = {
-            "id": state["node_id"],
-            "what": what_value,
-            "description": description_values,
-            "children": [],
-            "importance_weight": 0.5  # TODO: 计算权重逻辑
-        }
-
-        # 步骤6: 如需分割,执行分割并递归(PRD 3.3节流程步骤E1-E2)
-        if split_decision.get("should_split", False):
-            # 识别子元素(基于完整原图 + 当前节点描述)
-            sub_elements = await self._identify_sub_elements(
-                original_image=state["original_image_path"],
-                current_node_what=what_value,
-                current_node_description=state.get("node_description", "")
-            )
-
-            # 调用分割工具(输入:完整原图 + 子元素描述列表)
-            sub_images = await self.segment_tool.ainvoke({
-                "image_path": state["original_image_path"],  # **传入完整原图**
-                "elements_description": sub_elements
-            })
-
-            # 递归处理每个子元素
-            children = []
-            for idx, (sub_img_path, sub_element_desc) in enumerate(zip(sub_images, sub_elements)):
-                child_state = {
-                    "original_image_path": state["original_image_path"],  # **传递完整原图**
-                    "current_image_path": sub_img_path,  # 分割后的子图片
-                    "node_description": sub_element_desc,  # 子元素的文字描述
-                    "node_id": f"{state['node_id']}_{idx+1}",
-                    "depth": state["depth"] + 1,
-                    "category": state["category"],
-                    "theme": state["theme"]
-                }
-                child_result = await self.ainvoke(child_state)
-                children.append(child_result)
-
-            result["children"] = children
-
-        return result
-
-    async def _rough_understanding(
-        self,
-        original_image: str,
-        current_image: str,
-        node_description: str
-    ) -> str:
-        """粗理解当前节点,生成what字段
-
-        PRD依据: 3.3节步骤B0, 3.3.1节原则
-        关键点: 基于完整原图 + 节点描述进行理解
-
-        Args:
-            original_image: 完整原图路径(提供上下文)
-            current_image: 当前节点对应的图片路径
-            node_description: 当前节点的文字描述(首次为空,递归时为子元素描述)
-        """
-        # 构建prompt:如果有节点描述,说明是递归调用,需要结合原图理解特定部分
-        if node_description:
-            prompt = f"""
-请基于完整原图的上下文,描述指定的视觉元素。
-
-【完整原图】: {original_image}
-【当前关注的元素描述】: {node_description}
-【当前元素图片】: {current_image}
-
-请用一句话描述这个视觉元素的核心内容。
-要求: 简洁准确,聚焦于该元素本身。
-"""
-        else:
-            # 首次调用,直接理解整图
-            prompt = f"""
-请用一句话描述这张图片的核心内容和视觉元素。
-要求: 简洁准确,涵盖主要视觉要素。
-
-图片: {original_image}
-"""
-        response = await self.llm.ainvoke(prompt)
-        return response.strip()
-
-    def _generate_description_query(self, what_value: str) -> str:
-        """生成描述维度query
-
-        PRD依据: 3.3.2节、2.2.2节
-        Query句式: PRD 3.3.2节定义
-        """
-        return f'刻画描述"{what_value}"核心特征的角度和维度有哪些?请尽可能不重不漏列举全。'
-
-    def _generate_split_query(self, what_value: str, description: Dict) -> str:
-        """生成分割判断query
-
-        PRD依据: 3.3节步骤D0
-        """
-        return f'该节点"{what_value}"是否需要继续拆解分割?'
-
-    async def _detailed_understanding(
-        self,
-        original_image: str,
-        current_image: str,
-        node_description: str,
-        dimensions: Dict
-    ) -> Dict:
-        """根据描述维度细致理解当前节点
-
-        PRD依据: 3.3节步骤C, 3.3.1节原则
-        关键点: 基于完整原图 + 节点描述 + 描述维度进行理解
-
-        Args:
-            original_image: 完整原图路径(提供上下文)
-            current_image: 当前节点对应的图片路径
-            node_description: 当前节点的文字描述
-            dimensions: 知识库返回的描述维度
-        """
-        # 构建prompt:结合原图上下文和节点描述
-        if node_description:
-            prompt = f"""
-请基于完整原图的上下文,分析指定视觉元素的特征。
-
-【完整原图】: {original_image}
-【当前关注的元素描述】: {node_description}
-【当前元素图片】: {current_image}
-
-请从以下维度分析这个视觉元素:
-{self._format_dimensions(dimensions)}
-
-以JSON格式输出各维度的值。
-"""
-        else:
-            # 首次调用,分析整图
-            prompt = f"""
-请从以下维度分析图片:
-{self._format_dimensions(dimensions)}
-
-图片: {original_image}
-
-以JSON格式输出各维度的值。
-"""
-        response = await self.llm.ainvoke(prompt)
-        return self._parse_description(response)
-
-    async def _identify_sub_elements(
-        self,
-        original_image: str,
-        current_node_what: str,
-        current_node_description: str
-    ) -> List[str]:
-        """识别当前节点中的子元素
-
-        PRD依据: 3.3.1节
-        关键点: 基于完整原图 + 当前节点描述识别子元素(不依赖分割结果)
-
-        Args:
-            original_image: 完整原图路径(提供上下文)
-            current_node_what: 当前节点的what字段值
-            current_node_description: 当前节点的文字描述
-        """
-        # 构建prompt:基于原图和当前节点描述识别子元素
-        if current_node_description:
-            prompt = f"""
-请基于完整原图的上下文,识别指定视觉元素中包含的子元素。
-
-【完整原图】: {original_image}
-【当前关注的元素】: {current_node_what}
-【当前元素的详细描述】: {current_node_description}
-
-判断步骤:
-1. 该元素是否为多张子图拼接?(如4宫格、9宫格)
-2. 如果不是,该元素包含哪些独立的视觉对象?(如人物、商品、背景元素)
-
-请列出所有子元素的文字描述,每个元素一行。
-"""
-        else:
-            # 首次调用,分析整图
-            prompt = f"""
-分析图片"{current_node_what}",识别其中包含的子视觉元素。
-
-【图片】: {original_image}
-
-判断步骤:
-1. 是否为多张子图拼接?(如4宫格、9宫格)
-2. 如果不是,包含哪些独立的视觉对象?(如人物、商品、背景元素)
-
-请列出所有子元素的文字描述,每个元素一行。
-"""
-        response = await self.llm.ainvoke(prompt)
-        return [line.strip() for line in response.strip().split('\n') if line.strip()]
-```
-
----
-
-#### 5.2.4 RecursiveTextDeconstructionAgent(文本递归解构)
-
-**组件类型**: Agent (继承 BaseReactAgent)
-
-**选择依据**:
-- **PRD依据**: 3.3节 "对单一节点的递归解构"
-- **CLAUDE.md依据**: 需要LLM语义理解 + 动态调用工具
-
-**核心职责**:
-1. 提取文本原文作为what字段(不可改写)
-2. 调用知识库获取描述维度
-3. 分析文本,填充描述值
-4. 判断是否需要切分
-5. 如需切分,调用文本切分函数
-6. 递归处理子文本
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **what字段是原文** | 直接使用文本原文,不可总结改写 | 2.2.2节 |
-| **充分必要原则** | 子文本合并后必须等于父文本 | 3.4节 |
-| **递归控制** | 深度限制 + 最小语义单元判断 | 2.2.3节 |
-
-**代码框架**:
-
-```python
-class RecursiveTextDeconstructionAgent(BaseReactAgent):
-    """文本递归解构Agent
-
-    PRD依据: 3.3节
-    功能: 递归解构文本元素(标题/正文/标签)
-    """
-
-    def __init__(
-        self,
-        llm,
-        knowledge_retrieval_tool: BaseTool,
-        max_depth: int = 10
-    ):
-        super().__init__(llm, tools=[knowledge_retrieval_tool])
-        self.knowledge_tool = knowledge_retrieval_tool
-        self.max_depth = max_depth
-
-    async def ainvoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        递归解构文本
-
-        Args:
-            state: {
-                "text": str,          # 当前文本内容
-                "text_type": str,     # 文本类型(title/body/hashtags)
-                "node_id": str,       # 节点ID
-                "depth": int          # 当前递归深度
-            }
-
-        Returns:
-            {
-                "id": str,
-                "what": str,          # 文本原文
-                "description": Dict,
-                "children": List[Dict]
-            }
-        """
-        # 步骤1: 检查递归深度
-        if state["depth"] >= self.max_depth:
-            return self._build_leaf_node(state)
-
-        # 步骤2: what字段直接使用原文(PRD 2.2.2节)
-        what_value = state["text"]
-
-        # 步骤3: 调用知识库获取描述维度
-        description_query = self._generate_description_query(
-            what_value,
-            state["text_type"]
-        )
-        description_dimensions = await self.knowledge_tool.ainvoke({
-            "query": description_query,
-            "query_type": "description"
-        })
-
-        # 步骤4: 根据描述维度分析文本
-        description_values = await self._analyze_text(
-            what_value,
-            description_dimensions,
-            state["text_type"]
-        )
-
-        # 步骤5: 判断是否需要切分
-        split_query = self._generate_split_query(what_value)
-        split_decision = await self.knowledge_tool.ainvoke({
-            "query": split_query,
-            "query_type": "split_decision"
-        })
-
-        result = {
-            "id": state["node_id"],
-            "what": what_value,  # 原文,不可改写
-            "description": description_values,
-            "children": []
-        }
-
-        # 步骤6: 如需切分,执行切分并递归
-        if split_decision.get("should_split", False):
-            # 获取切分规则
-            split_rules = split_decision.get("split_rules", {})
-
-            # 执行文本切分(调用Function组件)
-            sub_texts = self._split_text(what_value, split_rules)
-
-            # 递归处理子文本
-            children = []
-            for idx, sub_text in enumerate(sub_texts):
-                child_state = {
-                    "text": sub_text,
-                    "text_type": state["text_type"],
-                    "node_id": f"{state['node_id']}_{idx+1}",
-                    "depth": state["depth"] + 1
-                }
-                child_result = await self.ainvoke(child_state)
-                children.append(child_result)
-
-            result["children"] = children
-
-        return result
-
-    def _split_text(self, text: str, split_rules: Dict) -> List[str]:
-        """切分文本
-
-        PRD依据: 3.5节
-        关键点: 保证子文本合并后等于父文本(充分必要)
-        """
-        strategy = split_rules.get("strategy", "sentence")
-
-        if strategy == "sentence":
-            # 按句子切分
-            import re
-            sentences = re.split(r'[。!?\n]', text)
-            return [s.strip() for s in sentences if s.strip()]
-        elif strategy == "paragraph":
-            # 按段落切分
-            paragraphs = text.split('\n\n')
-            return [p.strip() for p in paragraphs if p.strip()]
-        else:
-            return [text]
-```
-
----
-
-#### 5.2.5 PostSummaryDeconstructionAgent(帖子整体解构)
-
-**组件类型**: Agent (继承 BaseLLMAgent)
-
-**选择依据**:
-- **PRD依据**: 3.2节 "帖子整体解构环节"
-- **CLAUDE.md依据**: 需要综合分析和抽象提炼能力
-
-**核心职责**:
-1. 调用知识库获取总结维度
-2. 综合所有子元素解构结果
-3. 从点线面体维度总结帖子
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **动态总结维度** | 由知识库根据品类/主题/关键词提供 | 2.2.2节、3.2节 |
-| **综合分析** | 整合图片、文本、评论的解构结果 | 3.1节步骤13 |
-| **维度数量** | 不少于3个维度 | 2.2.2节 |
-
-**代码框架**:
-
-```python
-class PostSummaryDeconstructionAgent(BaseLLMAgent):
-    """帖子整体解构Agent
-
-    PRD依据: 3.2节
-    功能: 从点线面体维度总结帖子
-    """
-
-    def __init__(self, llm, knowledge_retrieval_tool: BaseTool):
-        super().__init__(llm)
-        self.knowledge_tool = knowledge_retrieval_tool
-
-    async def ainvoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        执行帖子整体解构
-
-        Args:
-            state: {
-                "category": str,
-                "theme": str,
-                "keywords": List[str],
-                "image_results": List[Dict],
-                "text_results": List[Dict],
-                "consumer_highlights": List[Dict]
-            }
-
-        Returns:
-            {
-                "summary": Dict  # 动态维度的总结
-            }
-        """
-        # 步骤1: 生成总结维度query(PRD 3.2节)
-        summary_query = self._generate_summary_query(
-            state["category"],
-            state["theme"],
-            state["keywords"]
-        )
-
-        # 步骤2: 调用知识库获取总结维度
-        summary_dimensions = await self.knowledge_tool.ainvoke({
-            "query": summary_query,
-            "query_type": "description"
-        })
-
-        # 步骤3: 根据总结维度分析帖子
-        prompt = self._build_summary_prompt(
-            summary_dimensions,
-            state["image_results"],
-            state["text_results"],
-            state["consumer_highlights"]
-        )
-
-        response = await self.llm.ainvoke(prompt)
-        summary = self._parse_summary(response)
-
-        return {"summary": summary}
-
-    def _generate_summary_query(
-        self,
-        category: str,
-        theme: str,
-        keywords: List[str]
-    ) -> str:
-        """生成总结维度query
-
-        PRD依据: 3.2节
-        Query句式: PRD 3.2节定义
-        """
-        keywords_str = ', '.join(keywords)
-        return f'对于一篇主题为"{theme}",品类为"{category}",关键词包含"{keywords_str}"的多模态社交媒体帖子,从内容创作者视角进行What要素的初步识别和分类,需要使用哪些通用工具?'
-```
-
----
-
-### 5.3 Tool组件详细设计
-
-**CLAUDE.md原则**:
-> **Tool组件特征**: 确定性任务 + 可被Agent调用
-
-#### 5.3.1 KnowledgeRetrievalTool(知识检索工具)
-
-**组件类型**: Tool (继承 BaseTool)
-
-**选择依据**:
-- **PRD依据**: 3.2节、3.3.2节 "对单一节点解构如何获取知识"
-- **CLAUDE.md依据**: 确定性API调用 + 需要被Agent调用
-
-**核心职责**:
-1. 接收query和query类型
-2. 调用外部知识库API
-3. 根据query类型解析返回结果
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **支持多种query类型** | description/tool/split_decision | 3.3.2节 |
-| **降级策略** | 知识库不可用时使用LLM默认知识 | 容错处理 |
-| **结果格式化** | 转换为Agent可用的结构化数据 | 提升可用性 |
-
-**代码框架**:
-
-```python
-from tools.base import BaseTool
-from typing import Dict, Any
-
-class KnowledgeRetrievalTool(BaseTool):
-    """知识检索工具
-
-    PRD依据: 3.2节、3.3.2节
-    功能: 根据query检索解构所需的知识
-    """
-
-    name: str = "knowledge_retrieval"
-    description: str = "根据query检索What解构所需的知识(描述维度、工具推荐、分割判断等)"
-
-    def __init__(self, knowledge_base_url: str, fallback_llm=None):
-        super().__init__()
-        self.knowledge_base_url = knowledge_base_url
-        self.fallback_llm = fallback_llm  # 降级LLM
-
-    async def _arun(
-        self,
-        query: str,
-        query_type: str = "description"
-    ) -> Dict[str, Any]:
-        """
-        调用知识库检索
-
-        Args:
-            query: 查询语句(PRD 3.3.2节定义的句式)
-            query_type: 查询类型
-                - description: 获取描述维度
-                - tool: 获取工具推荐
-                - split_decision: 获取分割判断
-
-        Returns:
-            根据query_type返回不同格式的结果
-        """
-        try:
-            # 调用知识库API
-            response = await self._call_knowledge_base(query)
-
-            # 根据query类型解析结果
-            if query_type == "description":
-                return self._parse_description_dimensions(response)
-            elif query_type == "tool":
-                return self._parse_tool_recommendations(response)
-            elif query_type == "split_decision":
-                return self._parse_split_decision(response)
-
-            return response
-
-        except Exception as e:
-            # 降级策略: 使用LLM默认知识
-            if self.fallback_llm:
-                return await self._fallback_with_llm(query, query_type)
-            raise e
-
-    def _parse_description_dimensions(self, response) -> Dict[str, Any]:
-        """解析描述维度
-
-        返回格式:
-        {
-            "dimensions": ["维度1", "维度2", "维度3", ...],
-            "count": 3
-        }
-        """
-        dimensions = response.get("dimensions", [])
-        return {
-            "dimensions": dimensions,
-            "count": len(dimensions)
-        }
-
-    def _parse_split_decision(self, response) -> Dict[str, Any]:
-        """解析分割判断
-
-        返回格式:
-        {
-            "should_split": True/False,
-            "split_rules": {  # 如果should_split=True
-                "strategy": "sentence/paragraph/grid",
-                "params": {...}
-            }
-        }
-        """
-        return {
-            "should_split": response.get("should_split", False),
-            "split_rules": response.get("split_rules", {})
-        }
-```
-
-**复用决策**:
-- **现有组件**: `tools/knowledge_retrieval_tools.py`
-- **决策**: **复用并修改**
-- **修改点**: 增强支持多种query_type,增加降级策略
-
----
-
-#### 5.3.2 ImageSegmentTool(图片分割工具)
-
-**组件类型**: Tool (继承 BaseTool)
-
-**选择依据**:
-- **PRD依据**: 3.3.1节 "判断图片元素是否要继续分割拆解"、3.5节
-- **CLAUDE.md依据**: 确定性模型调用 + 需要被Agent调用
-
-**核心职责**:
-1. 接收图片路径和子元素描述
-2. 调用分割模型(SAM/GroundingDINO)
-3. 保存分割后的子图片
-4. 返回子图片路径列表
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **支持多种分割模式** | grid(子图拼接)/ object(视觉元素) | 3.3.1节 |
-| **分割质量验证** | 验证mask是否覆盖元素 | 保证准确性 |
-| **文件管理** | 统一命名规范和存储路径 | 2.1.1节 |
-
-**代码框架**:
-
-```python
-from tools.base import BaseTool
-from typing import List
-
-class ImageSegmentTool(BaseTool):
-    """图片分割工具
-
-    PRD依据: 3.3.1节、3.5节
-    功能: 根据文字描述将图片分割为多个独立的视觉元素
-    """
-
-    name: str = "image_segment"
-    description: str = "根据文字描述将图片分割为多个独立的视觉元素"
-
-    def __init__(self, segment_model, output_dir: str):
-        super().__init__()
-        self.segment_model = segment_model
-        self.output_dir = output_dir
-
-    async def _arun(
-        self,
-        image_path: str,
-        elements_description: List[str],
-        segment_type: str = "object"
-    ) -> List[str]:
-        """
-        分割图片
-
-        Args:
-            image_path: 原始图片路径
-            elements_description: 子元素文字描述列表
-            segment_type: 分割类型
-                - object: 视觉元素分割(抠图)
-                - grid: 子图拼接拆解(简单切割)
-
-        Returns:
-            分割后的子图片路径列表
-        """
-        # 1. 加载图片
-        image = self._load_image(image_path)
-
-        # 2. 根据分割类型选择策略
-        if segment_type == "grid":
-            # 拼接图拆解(简单切割)
-            sub_images = self._grid_split(image, len(elements_description))
-        else:
-            # 视觉元素分割(调用分割模型)
-            sub_images = await self._segment_objects(image, elements_description)
-
-        # 3. 保存子图片
-        saved_paths = []
-        for idx, sub_img in enumerate(sub_images):
-            save_path = f"{self.output_dir}/{self._generate_filename(image_path, idx)}"
-            self._save_image(sub_img, save_path)
-            saved_paths.append(save_path)
-
-        return saved_paths
-
-    def _grid_split(self, image, num_elements: int):
-        """拼接图拆解
-
-        PRD依据: 3.3.1节(优先判断是否为多子图拼接)
-        支持: 2x2, 3x3, 1xN等常见拼接模式
-        """
-        # TODO: 根据num_elements推测拼接模式
-        pass
-
-    async def _segment_objects(self, image, descriptions: List[str]):
-        """视觉元素分割
-
-        调用SAM/GroundingDINO等模型
-        """
-        masks = await self.segment_model.segment(image, descriptions)
-        return self._extract_objects(image, masks)
-```
-
-**复用决策**:
-- **现有组件**: `tools/segment_tools.py`
-- **决策**: **直接复用**
-- **理由**: 已实现图片分割功能,完全符合PRD需求
-
----
-
-### 5.4 Function组件详细设计
-
-**CLAUDE.md原则**:
-> **Function组件特征**: 确定性任务 + 不需要被Agent调用
-
-#### 5.4.1 TextSplitFunction(文本切分函数)
-
-**组件类型**: Function (继承 BaseFunction)
-
-**选择依据**:
-- **PRD依据**: 3.5节 "文本切分工具"
-- **CLAUDE.md依据**: 确定性字符串处理 + 工作流直接调用
-
-**核心职责**:
-1. 接收文本和切分规则
-2. 按规则切分文本
-3. 保证不重不漏(子文本合并=父文本)
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **充分必要原则** | 验证子文本合并后等于父文本 | 3.4节 |
-| **多种切分策略** | sentence/paragraph/semantic | 3.5节 |
-| **保留原文** | 不修改文本内容 | 2.2.2节 |
-
-**代码框架**:
-
-```python
-from functions.base import BaseFunction
-from typing import List, Dict
-
-class TextSplitFunction(BaseFunction):
-    """文本切分函数
-
-    PRD依据: 3.5节
-    功能: 根据切分规则切分文本
-    """
-
-    def invoke(self, text: str, split_rules: Dict) -> List[str]:
-        """
-        切分文本
-
-        Args:
-            text: 待切分文本
-            split_rules: 切分规则
-                {
-                    "strategy": "sentence/paragraph/semantic",
-                    "params": {...}
-                }
-
-        Returns:
-            切分后的子文本列表
-        """
-        strategy = split_rules.get("strategy", "sentence")
-
-        if strategy == "sentence":
-            sub_texts = self._split_by_sentence(text)
-        elif strategy == "paragraph":
-            sub_texts = self._split_by_paragraph(text)
-        elif strategy == "semantic":
-            sub_texts = self._split_by_semantic(text, split_rules.get("params"))
-        else:
-            sub_texts = [text]
-
-        # 验证充分必要原则(PRD 3.4节)
-        self._validate_completeness(text, sub_texts)
-
-        return sub_texts
-
-    def _split_by_sentence(self, text: str) -> List[str]:
-        """按句子切分"""
-        import re
-        sentences = re.split(r'[。!?\n]', text)
-        return [s.strip() for s in sentences if s.strip()]
-
-    def _validate_completeness(self, parent_text: str, child_texts: List[str]):
-        """验证子文本合并后是否等于父文本
-
-        PRD依据: 3.4节 "充分必要原则"
-        """
-        merged = ''.join(child_texts)
-        if merged.replace(' ', '') != parent_text.replace(' ', ''):
-            raise ValueError("子文本合并后不等于父文本,违反充分必要原则")
-```
-
-**复用决策**:
-- **现有组件**: 无
-- **决策**: **完全新增**
-- **理由**: 项目中不存在文本切分功能
-
----
-
-#### 5.4.2 ResultAggregationFunction(结果汇总函数)
-
-**组件类型**: Function (继承 BaseFunction)
-
-**选择依据**:
-- **PRD依据**: 2.2.2节 "输出格式定义"
-- **CLAUDE.md依据**: 确定性数据结构转换
-
-**核心职责**:
-1. 接收所有解构结果
-2. 组装为树状JSON结构
-3. 生成节点ID
-4. 验证JSON格式
-
-**关键点**:
-
-| 关键点 | 实现方式 | PRD依据 |
-|--------|---------|---------|
-| **递归构建树** | 递归函数处理children | 2.2.2节 |
-| **ID生成规则** | "1", "1_1", "1_1_2"格式 | 2.2.2节 |
-| **格式验证** | 验证所有必需字段存在 | 2.2.2节 |
-
-**代码框架**:
-
-```python
-from functions.base import BaseFunction
-from typing import Dict, List
-
-class ResultAggregationFunction(BaseFunction):
-    """结果汇总函数
-
-    PRD依据: 2.2.2节
-    功能: 将所有解构结果组装为树状JSON
-    """
-
-    def invoke(self, state: Dict) -> Dict:
-        """
-        汇总所有解构结果
-
-        Args:
-            state: {
-                "post_summary": Dict,
-                "image_results": List[Dict],
-                "text_results": List[Dict]
-            }
-
-        Returns:
-            最终的树状JSON结构(PRD 2.2.2节格式)
-        """
-        # 1. 提取各部分结果
-        post_summary = state["post_summary"]
-        image_results = state["image_results"]
-        text_results = state["text_results"]
-
-        # 2. 构建元素列表
-        elements = []
-
-        # 添加图片元素
-        for idx, img_result in enumerate(image_results):
-            element = self._build_element_tree(
-                id=str(idx + 1),
-                result=img_result
-            )
-            elements.append(element)
-
-        # 添加文本元素
-        text_id_start = len(image_results) + 1
-        for idx, text_result in enumerate(text_results):
-            element = self._build_element_tree(
-                id=str(text_id_start + idx),
-                result=text_result
-            )
-            elements.append(element)
-
-        # 3. 组装最终JSON
-        final_result = {
-            "帖子总结": post_summary,
-            "帖子包含元素": elements
-        }
-
-        # 4. 验证格式
-        self._validate_result(final_result)
-
-        return final_result
-
-    def _build_element_tree(self, id: str, result: Dict) -> Dict:
-        """递归构建元素树
-
-        PRD依据: 2.2.2节
-        """
-        element = {
-            "id": id,
-            "what": result["what"],
-            "描述": result["description"],
-            "子节点元素关系": result.get("relationships", []),
-            "元素重要性权重": result.get("importance_weight", 0.5),
-            "子节点元素": []
-        }
-
-        # 递归处理children
-        if "children" in result and result["children"]:
-            for idx, child in enumerate(result["children"]):
-                child_element = self._build_element_tree(
-                    id=f"{id}_{idx+1}",
-                    result=child
-                )
-                element["子节点元素"].append(child_element)
-
-        return element
-
-    def _validate_result(self, result: Dict):
-        """验证JSON格式
-
-        PRD依据: 2.2.2节
-        检查: 必需字段、维度数量≥3等
-        """
-        # 验证帖子总结
-        assert "帖子总结" in result, "缺少'帖子总结'字段"
-        assert len(result["帖子总结"]) >= 3, "帖子总结维度不足3个"
-
-        # 验证帖子包含元素
-        assert "帖子包含元素" in result, "缺少'帖子包含元素'字段"
-
-        # 递归验证每个元素节点
-        for element in result["帖子包含元素"]:
-            self._validate_element(element)
-
-    def _validate_element(self, element: Dict):
-        """递归验证元素节点"""
-        required_fields = ["id", "what", "描述", "子节点元素关系", "元素重要性权重", "子节点元素"]
-        for field in required_fields:
-            assert field in element, f"元素节点缺少'{field}'字段"
-
-        # 验证描述维度数量
-        assert len(element["描述"]) >= 3, f"元素{element['id']}的描述维度不足3个"
-
-        # 递归验证子节点
-        for child in element["子节点元素"]:
-            self._validate_element(child)
-```
-
-**复用决策**:
-- **现有组件**: `functions/result_aggregation.py`
-- **决策**: **复用并修改**
-- **理由**: 可复用结果聚合逻辑,需新增树状结构构建
-
----
-
-## 6. 数据结构设计
-
-### 6.1 WhatDeconstructionState(工作流状态)
-
-**PRD依据**: 2.1节(输入)、2.2节(输出)
-
-```python
-from typing import TypedDict, List, Dict, Any, Optional
-
-class WhatDeconstructionState(TypedDict):
-    """What解构工作流状态
-
-    PRD依据: 2.1节、2.2节
-    """
-
-    # ========== 输入数据(PRD 2.1节) ==========
-    images: List[str]                      # 图片路径列表(1-9张)
-    text: Dict[str, Any]                   # {title, body, hashtags}
-    comments: List[Dict[str, str]]         # [{user, content}, ...]
-    creator_info: Optional[Dict]           # 创作者信息(可选)
-
-    # ========== 中间状态 ==========
-    category: str                          # 品类(F1输出)
-    theme: str                             # 主题(F1输出)
-    keywords: List[str]                    # 关键词(F1输出)
-    consumer_highlights: List[Dict]        # 消费者亮点(F4输出)
-
-    # ========== 解构结果 ==========
-    image_deconstruction_results: List[Dict]   # 图片解构结果(F5输出)
-    text_deconstruction_results: List[Dict]    # 文本解构结果(F7输出)
-    post_summary: Dict[str, Any]               # 帖子总结(F9输出)
-
-    # ========== 最终输出(PRD 2.2.2节) ==========
-    final_result: Dict[str, Any]           # 树状JSON结果
-```
-
-### 6.2 ElementNode(元素节点结构)
-
-**PRD依据**: 2.2.2节 "输出格式定义"
-
-```python
-class ElementNode(TypedDict):
-    """元素节点结构(递归树的节点)
-
-    PRD依据: 2.2.2节
-    """
-
-    id: str                          # 节点ID(如"1", "1_1", "1_2_3")
-    what: str                        # 元素概述(文本原文 or 图片描述)
-    描述: Dict[str, Any]             # 动态描述维度(≥3个)
-    子节点元素关系: List[str]        # 子节点之间的关系描述
-    元素重要性权重: float            # 0-1之间
-    子节点元素: List['ElementNode']  # 递归的子节点列表
-```
-
-### 6.3 FinalOutput(最终输出结构)
-
-**PRD依据**: 2.2.2节
-
-```python
-class FinalOutput(TypedDict):
-    """最终输出的JSON结构
-
-    PRD依据: 2.2.2节
-    """
-
-    帖子总结: Dict[str, Any]         # 动态总结维度(≥3个)
-    帖子包含元素: List[ElementNode]  # 元素树的根节点列表
-```
-
----
-
-## 7. 实现路线图
-
-### Phase 1: 基础框架搭建
-
-**目标**: 建立工作流骨架和基础组件
-
-| 任务 | 组件 | 优先级 | 依赖 |
-|------|------|--------|------|
-| 1.1 定义数据结构 | WhatDeconstructionState | P0 | 无 |
-| 1.2 实现PostUnderstandingAgent | Agent | P0 | 1.1 |
-| 1.3 修改KnowledgeRetrievalTool | Tool | P0 | 无 |
-| 1.4 搭建工作流骨架 | Workflow | P0 | 1.1, 1.2, 1.3 |
-| 1.5 端到端测试(简化版) | - | P0 | 1.4 |
-
-**验收标准**:
-- 工作流能够运行
-- PostUnderstandingAgent能够输出品类/主题/关键词
-- 知识库工具能够返回模拟数据
-
-### Phase 2: 核心解构能力
-
-**目标**: 实现图片和文本的递归解构
-
-| 任务 | 组件 | 优先级 | 依赖 |
-|------|------|--------|------|
-| 2.1 实现RecursiveImageDeconstructionAgent | Agent | P0 | Phase 1 |
-| 2.2 复用ImageSegmentTool | Tool | P0 | 2.1 |
-| 2.3 实现RecursiveTextDeconstructionAgent | Agent | P0 | Phase 1 |
-| 2.4 实现TextSplitFunction | Function | P0 | 2.3 |
-| 2.5 集成测试(单个图片+单个文本) | - | P0 | 2.1-2.4 |
-
-**验收标准**:
-- 能够递归解构单张图片(至少2层)
-- 能够递归解构单段文本(至少2层)
-- 递归深度控制生效(不超过10层)
-
-### Phase 3: 增强功能
-
-**目标**: 完成评论理解、帖子总结、结果汇总
-
-| 任务 | 组件 | 优先级 | 依赖 |
-|------|------|--------|------|
-| 3.1 实现CommentAnalysisAgent | Agent | P1 | Phase 2 |
-| 3.2 实现PostSummaryDeconstructionAgent | Agent | P1 | Phase 2 |
-| 3.3 修改ResultAggregationFunction | Function | P0 | Phase 2 |
-| 3.4 完整端到端测试 | - | P0 | 3.1-3.3 |
-
-**验收标准**:
-- 能够分析评论并提取亮点
-- 能够生成帖子整体总结
-- 能够输出符合PRD 2.2.2节格式的完整JSON
-
-### Phase 4: 完善优化
-
-**目标**: 优化性能、容错、测试覆盖
-
-| 任务 | 优先级 |
-|------|--------|
-| 4.1 递归深度优化(提前停止) | P2 |
-| 4.2 错误处理和降级策略 | P1 |
-| 4.3 并行执行优化 | P2 |
-| 4.4 单元测试覆盖 | P1 |
-| 4.5 性能基准测试 | P2 |
-
-**验收标准**:
-- 单元测试覆盖率 > 80%
-- 错误情况下有合理降级
-- 性能满足预期(待定义)
-
----
-
-## 8. 决策依据总结表
-
-### 8.1 工作流设计决策
-
-| 决策点 | 选择 | PRD依据 | CLAUDE.md依据 | 技术理由 |
-|--------|------|---------|---------------|---------|
-| 工作流框架 | LangGraph | - | 要求使用LangGraph | 支持状态管理、节点编排 |
-| 并行执行 | 图片和文本并行 | 3.1节流程图 | 工作流负责编排 | 两者无依赖,可并行提升效率 |
-| 递归深度 | 最多10层 | 2.2.3节 | - | 防止无限递归,控制成本 |
-| 知识query生成 | 工作流逻辑 | 3.2节 | 轻量级逻辑不需组件 | 简单字符串拼接 |
-
-### 8.2 组件类型选择决策
-
-| 组件 | 类型 | PRD依据 | CLAUDE.md依据 | 选择理由 |
-|------|------|---------|---------------|---------|
-| PostUnderstandingAgent | Agent | 3.1步骤1 | 多模态理解(不确定性) | 需要LLM理解图文 |
-| CommentAnalysisAgent | Agent | 2.1.4节 | 语义理解(不确定性) | 需要LLM情感分析 |
-| RecursiveImageDeconstructionAgent | Agent (ReAct) | 3.3节 | 视觉理解+工具调用 | 需要多次调用知识库和分割工具 |
-| RecursiveTextDeconstructionAgent | Agent (ReAct) | 3.3节 | 语义理解+工具调用 | 需要多次调用知识库 |
-| PostSummaryDeconstructionAgent | Agent | 3.2节 | 综合分析(不确定性) | 需要LLM抽象提炼 |
-| KnowledgeRetrievalTool | Tool | 3.3.2节 | Agent需调用 | 确定性API调用 |
-| ImageSegmentTool | Tool | 3.3.1节 | Agent需调用 | 确定性模型调用 |
-| TextSplitFunction | Function | 3.5节 | 确定性+工作流调用 | 简单字符串处理 |
-| ResultAggregationFunction | Function | 2.2.2节 | 确定性+工作流调用 | 数据结构转换 |
-
-### 8.3 复用决策
-
-| 组件 | 决策 | 理由 | 修改点 |
-|------|------|------|--------|
-| knowledge_retrieval_tools.py | 复用并修改 | 已有基础查询功能 | 增强query类型支持、降级策略 |
-| segment_tools.py | 直接复用 | 功能完全匹配 | 无 |
-| result_aggregation.py | 复用并修改 | 可复用聚合逻辑 | 新增树状结构构建 |
-| TextSplitFunction | 完全新增 | 项目中无此功能 | - |
-| 所有Agent | 完全新增 | PRD业务逻辑全新 | - |
-
----
-
-## 附录: Query模板速查表
-
-**PRD依据**: 3.2节、3.3.2节
-
-| Query类型 | 句式模板 | 使用场景 |
-|-----------|---------|---------|
-| 帖子整体总结维度 | 对于一篇主题为"{主题}",品类为"{品类}",关键词包含"{关键词}"的多模态社交媒体帖子,从内容创作者视角进行What要素的初步识别和分类,需要使用哪些通用工具? | PostSummaryDeconstructionAgent |
-| 元素描述维度 | 刻画描述"{元素的what字段值}"核心特征的角度和维度有哪些?请尽可能不重不漏列举全。 | 所有递归解构Agent |
-| 分割判断 | 该节点"{元素的what字段值}"是否需要继续拆解分割? | 所有递归解构Agent |
-| 图片整体分割工具 | 对一张描述为"{图片描述}"的图片,从内容创作者视角进行视觉元素分割,需要使用哪些图片分割/抠图工具? | RecursiveImageDeconstructionAgent |
-| 文本段落分割工具 | 对一段描述为"{文本摘要}"的文本段落,从内容创作者视角进行结构化分割拆解,需要使用什么文本切分工具或文本结构化大模型? | RecursiveTextDeconstructionAgent |
-
----
-
-**文档版本**: v2.0
-**最后更新**: 2025-10-21
-**适用PRD版本**: v1.3

BIN
test/.DS_Store


+ 0 - 0
test/__init__.py


+ 0 - 295
test/agents/test_knowledge_requirement_agent.py

@@ -1,295 +0,0 @@
-"""
-知识需求生成Agent的完整测试脚本
-
-测试KnowledgeRequirementGenerationAgent的新输出格式(PRD 1.4)
-
-新格式包含:
-- 整体项目目标
-- 本次任务目标
-- 上下文
-- 待解构帖子信息
-- 需求(内容知识需求 + 工具知识需求)
-"""
-
-import sys
-import json
-import os
-from pathlib import Path
-from dotenv import load_dotenv
-
-# 添加项目根目录到路径
-project_root = Path(__file__).parent.parent.parent
-sys.path.insert(0, str(project_root))
-
-# 加载环境变量
-load_dotenv(project_root / ".env")
-
-from langchain.chat_models import init_chat_model
-from src.components.agents.knowledge_requirement_agent import generate_knowledge_requirements
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-def load_test_data(file_path: str) -> dict:
-    """加载测试数据
-
-    Args:
-        file_path: JSON文件路径
-
-    Returns:
-        解析后的JSON数据
-    """
-    with open(file_path, 'r', encoding='utf-8') as f:
-        return json.load(f)
-
-
-def read_prd_content(pdf_path: str) -> str:
-    """读取PRD PDF内容
-
-    Args:
-        pdf_path: PDF文件路径
-
-    Returns:
-        PRD文本内容
-    """
-    try:
-        import pymupdf  # PyMuPDF
-
-        # 打开PDF文件
-        doc = pymupdf.open(pdf_path)
-
-        # 提取所有页面的文本
-        text_content = []
-        for page_num in range(len(doc)):
-            page = doc[page_num]
-            text_content.append(page.get_text())
-
-        doc.close()
-
-        # 合并所有页面的文本
-        full_text = "\n".join(text_content)
-
-        logger.info(f"成功读取PDF文件: {pdf_path}, 内容长度: {len(full_text)} 字符")
-        return full_text
-
-    except ImportError:
-        logger.warning("未安装 pymupdf 库,使用 PyPDF2 作为备选方案")
-        try:
-            from PyPDF2 import PdfReader
-
-            reader = PdfReader(pdf_path)
-            text_content = []
-
-            for page in reader.pages:
-                text_content.append(page.extract_text())
-
-            full_text = "\n".join(text_content)
-            logger.info(f"成功读取PDF文件 (PyPDF2): {pdf_path}, 内容长度: {len(full_text)} 字符")
-            return full_text
-
-        except ImportError:
-            logger.error("未安装 PDF 读取库 (pymupdf 或 PyPDF2),无法读取PDF文件")
-            raise ImportError("请安装 pymupdf (推荐) 或 PyPDF2: pip install pymupdf 或 pip install PyPDF2")
-
-    except Exception as e:
-        logger.error(f"读取PDF文件失败: {e}", exc_info=True)
-        raise
-
-
-def format_post_content(raw_data: dict) -> dict:
-    """格式化帖子内容为Agent所需格式
-
-    Args:
-        raw_data: 原始帖子数据
-
-    Returns:
-        格式化后的帖子内容
-    """
-    return {
-        "text": {
-            "title": raw_data.get("title", ""),
-            "body": raw_data.get("body_text", ""),
-            "hashtags": []  # 可以从body_text中提取
-        },
-        "images": raw_data.get("images", []),
-        "metadata": {
-            "link": raw_data.get("link", ""),
-            "content_id": raw_data.get("channel_content_id", ""),
-            "account_name": raw_data.get("channel_account_name", ""),
-            "content_type": raw_data.get("content_type", ""),
-            "comment_count": raw_data.get("comment_count", 0),
-            "like_count": raw_data.get("like_count", 0),
-            "collect_count": raw_data.get("collect_count", 0)
-        }
-    }
-
-
-def test_complete_knowledge_requirement_generation():
-    """完整测试:知识需求生成(新格式 - PRD 1.4)
-
-    测试场景:
-    1. 帖子整体解构
-    2. 验证输出格式包含所有必需章节
-    3. 验证知识需求正确分类(内容知识 vs 工具知识)
-
-    测试步骤:
-    - Step 1: 初始化LLM
-    - Step 2: 加载PRD内容
-    - Step 3: 加载测试帖子数据
-    - Step 4: 定义任务阶段
-    - Step 5: 创建Agent并生成知识需求
-    - Step 6: 展示结果
-    - Step 7: 格式验证
-    - Step 8: 保存Markdown文档
-    - Step 9: 测试总结
-    """
-    print("\n" + "=" * 100)
-    print("📝 完整测试:KnowledgeRequirementGenerationAgent(新格式 - PRD 1.4)")
-    print("   - 输入:PRD文档 + 小红书帖子 + 解构上下文 + 任务阶段")
-    print("   - 输出:结构化知识需求文档(包含项目目标、任务目标、上下文、需求等)")
-    print("=" * 100)
-
-    # Step 1: 初始化LLM
-    print("\n🤖 Step 1: 初始化LLM (Gemini)...")
-    # 确保 GOOGLE_API_KEY 环境变量已设置
-    google_api_key = os.getenv("GEMINI_API_KEY")
-    if google_api_key:
-        os.environ["GOOGLE_API_KEY"] = google_api_key
-    llm = init_chat_model("gemini-2.5-flash", model_provider="google_genai")
-    print("   ✓ LLM初始化成功")
-
-    # Step 2: 加载PRD内容
-    print("\n📄 Step 2: 加载PRD内容...")
-    prd_path = project_root / "prd1.4.pdf"
-    prd_content = read_prd_content(str(prd_path))
-    print(f"   ✓ PRD加载成功,内容长度: {len(prd_content)} 字符")
-    print(f"   PRD摘要(前200字): {prd_content}...")
-
-    # Step 3: 加载测试帖子数据
-    print("\n📁 Step 3: 加载测试帖子数据...")
-    test_data_path = project_root / "examples/测试数据/阿里多多酱/待解构帖子.json"
-    raw_data = load_test_data(str(test_data_path))
-    post_content = format_post_content(raw_data)
-    print(f"   ✓ 帖子数据加载成功")
-    print(f"   - 标题: {raw_data.get('title', 'N/A')}")
-    print(f"   - 图片数量: {len(raw_data.get('images', []))}张")
-    print(f"   - 点赞数: {raw_data.get('like_count', 0)}")
-
-    # Step 4: 定义任务阶段
-    print("\n🎯 Step 4: 定义任务阶段...")
-    task_stage = "帖子整体解构"
-    print(f"   ✓ 任务阶段: {task_stage}")
-    print(f"   - 说明: 确定帖子的描述维度(品类、主题、脚本、内容亮点、情绪共鸣点等)")
-
-    # Step 5: 创建Agent并生成知识需求
-    print("\n⚙️  Step 5: 创建Agent并生成知识需求...")
-    print("   选项:启用知识检索 = False(加快测试速度)")
-
-    result = generate_knowledge_requirements(
-        llm=llm,
-        prd_content=prd_content,
-        post_content=post_content,
-        task_stage=task_stage,  # 新增参数
-        enable_retrieval=False  # 设为True可启用知识检索,但会较慢
-    )
-
-    # Step 6: 展示结果
-    print("\n" + "=" * 100)
-    print("📊 Step 6: 生成结果展示")
-    print("=" * 100)
-
-    print(f"\n📝 总结:")
-    print(f"   {result.summary}")
-
-    print(f"\n📄 Markdown文档:")
-    print("-" * 100)
-    print(result.markdown_document)
-    print("-" * 100)
-
-    # Step 7: 格式验证
-    print("\n" + "=" * 100)
-    print("🔍 Step 7: 格式验证")
-    print("=" * 100)
-
-    required_sections = [
-        "# 整体项目目标",
-        "# 本次任务目标",
-        "# 上下文",
-        "# 待解构帖子信息",
-        "# 需求",
-        "## 内容知识需求",
-        "### 需求约束",
-        "### 需求描述",
-        "## 工具知识需求"
-    ]
-
-    all_passed = True
-    for section in required_sections:
-        if section in result.markdown_document:
-            print(f"   ✓ 包含章节: {section}")
-        else:
-            print(f"   ✗ 缺少章节: {section}")
-            all_passed = False
-
-    if all_passed:
-        print(f"\n   🎉 格式验证通过!所有必需章节均存在。")
-    else:
-        print(f"\n   ⚠️  格式验证未通过,缺少部分章节。")
-
-    # Step 8: 保存Markdown文档
-    print("\n" + "=" * 100)
-    print("💾 Step 8: 保存Markdown文档")
-    print("=" * 100)
-
-    output_dir = project_root / "test/outputs"
-    output_dir.mkdir(parents=True, exist_ok=True)
-
-    output_path = output_dir / "knowledge_requirement_complete.md"
-    with open(output_path, 'w', encoding='utf-8') as f:
-        f.write(result.markdown_document)
-
-    print(f"\n   ✓ Markdown文档已保存到: {output_path}")
-    print(f"   - 文件大小: {len(result.markdown_document)} 字符")
-
-    # Step 9: 测试总结
-    print("\n" + "=" * 100)
-    print("✅ 测试完成总结")
-    print("=" * 100)
-    print(f"   ✓ 成功读取PRD文档 (prd1.4.pdf)")
-    print(f"   ✓ 成功加载小红书帖子数据")
-    print(f"   ✓ 成功定义任务阶段: {task_stage}")
-    print(f"   ✓ 成功生成知识需求文档(新格式 - PRD 1.4)")
-    print(f"   ✓ 格式验证: {'通过' if all_passed else '未通过'}")
-    print(f"   ✓ 总结: {result.summary}")
-    print(f"   ✓ Markdown文档大小: {len(result.markdown_document)} 字符")
-
-    print("\n" + "🎉" * 50)
-    print("测试成功完成! 新输出格式符合 PRD 1.4 要求")
-    print("🎉" * 50 + "\n")
-
-    return result
-
-
-def main():
-    """主测试函数"""
-    print("\n" + "🚀" * 50)
-    print("KnowledgeRequirementGenerationAgent 完整测试套件(PRD 1.4 新格式)")
-    print("🚀" * 50)
-
-    try:
-        _ = test_complete_knowledge_requirement_generation()  # noqa: F841
-
-        print("\n✅ 所有测试通过!")
-        return 0
-
-    except Exception as e:
-        logger.error(f"测试失败: {e}", exc_info=True)
-        print(f"\n❌ 测试失败: {e}\n")
-        import traceback
-        traceback.print_exc()
-        return 1
-
-
-if __name__ == "__main__":
-    exit(main())

+ 0 - 194
test/agents/test_script_orthogonal_with_real_data.py

@@ -1,194 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-使用真实数据测试脚本正交分析Agent
-"""
-
-import sys
-import os
-import json
-from pathlib import Path
-from dotenv import load_dotenv
-
-# 添加项目根目录到 Python 路径
-project_root = Path(__file__).parent.parent.parent
-sys.path.insert(0, str(project_root))
-
-# 加载环境变量
-load_dotenv(project_root / ".env")
-
-from src.components.agents.script_orthogonal_analysis_agent import ScriptOrthogonalAnalysisAgent
-
-
-def load_script_result(file_path: str) -> dict:
-    """加载脚本解析结果文件
-
-    Args:
-        file_path: JSON文件路径
-
-    Returns:
-        解析后的JSON数据
-    """
-    with open(file_path, 'r', encoding='utf-8') as f:
-        return json.load(f)
-
-
-def test_with_real_data():
-    """使用真实数据测试脚本正交分析Agent"""
-
-    print("=" * 100)
-    print("使用真实数据测试脚本正交分析Agent")
-    print("=" * 100)
-
-    # 加载真实数据
-    script_result_path = project_root / "examples/阿里多多酱/output/script_result_20251118_152626.json"
-    print(f"\n📁 加载脚本解析结果: {script_result_path}")
-
-    script_result = load_script_result(str(script_result_path))
-
-    # 提取所需数据
-    topic_description = script_result.get("选题描述", {})
-    content_weight = script_result.get("图文权重", {})
-    script_understanding = script_result.get("脚本理解", {})
-
-    # 构建state
-    state = {
-        "text": {
-            "title": "当代年轻人对食物的双标日常",
-            "body": "#讨好型水果[话题]#"
-        },
-        "images": script_understanding.get("图片列表", []),
-        "topic_selection_understanding": topic_description,
-        "content_weight": content_weight,
-        "script_sections": {
-            "内容品类": script_understanding.get("内容品类", ""),
-            "段落列表": script_understanding.get("段落列表", [])
-        },
-        "script_elements": {
-            "元素列表": script_understanding.get("元素列表", [])
-        }
-    }
-
-    print(f"\n✓ 数据加载成功")
-    print(f"  - 选题主题: {topic_description.get('主题', 'N/A')}")
-    print(f"  - 内容品类: {script_understanding.get('内容品类', 'N/A')}")
-    print(f"  - 段落数量: {len(script_understanding.get('段落列表', []))}")
-    print(f"  - 元素数量: {len(script_understanding.get('元素列表', []))}")
-    print(f"  - 图片数量: {len(script_understanding.get('图片列表', []))}")
-
-    # 初始化Agent
-    print("\n🤖 初始化ScriptOrthogonalAnalysisAgent...")
-    agent = ScriptOrthogonalAnalysisAgent()
-    print("  ✓ Agent初始化成功")
-
-    # 执行正交分析
-    print("\n⚙️  开始执行正交分析...")
-    print("-" * 100)
-    result = agent.process(state)
-    print("-" * 100)
-
-    # 输出结果
-    print("\n" + "=" * 100)
-    print("📊 正交分析结果")
-    print("=" * 100)
-
-    orthogonal_matrix = result.get("正交矩阵", [])
-    element_type_list = result.get("元素类型列表", [])
-
-    print(f"\n✓ 正交矩阵生成成功")
-    print(f"  - 段落行数: {len(orthogonal_matrix)}")
-    print(f"  - 元素类型列数: {len(element_type_list)}")
-
-    print(f"\n📋 元素类型列表 (共{len(element_type_list)}个):")
-    for idx, element_type in enumerate(element_type_list, 1):
-        print(f"  {idx}. {element_type}")
-
-    print(f"\n📋 正交矩阵预览 (前3个段落):")
-    for idx, row in enumerate(orthogonal_matrix[:3], 1):
-        print(f"\n  段落 {idx}: {row['段落']}")
-        print(f"  内容范围: {row['内容范围']}")
-        print(f"  元素类型分析:")
-        for element_type, analysis in row.get('元素类型分析', {}).items():
-            if analysis:  # 只显示非空的分析
-                print(f"    - {element_type}: {analysis[:80]}{'...' if len(analysis) > 80 else ''}")
-
-    # 保存结果
-    output_dir = project_root / "examples/阿里多多酱/output"
-    output_path = output_dir / "orthogonal_analysis_result.json"
-
-    print(f"\n💾 保存结果到: {output_path}")
-    with open(output_path, 'w', encoding='utf-8') as f:
-        json.dump(result, f, ensure_ascii=False, indent=2)
-
-    print(f"  ✓ 结果保存成功")
-
-    # 生成表格形式的输出(Markdown格式)
-    markdown_output_path = output_dir / "orthogonal_analysis_table.md"
-    print(f"\n📝 生成Markdown表格: {markdown_output_path}")
-
-    with open(markdown_output_path, 'w', encoding='utf-8') as f:
-        f.write("# 脚本正交分析矩阵\n\n")
-        f.write(f"**选题主题**: {topic_description.get('主题', 'N/A')}\n\n")
-        f.write(f"**内容品类**: {script_understanding.get('内容品类', 'N/A')}\n\n")
-
-        # 生成表格
-        f.write("## 正交矩阵表格\n\n")
-
-        # 表头
-        header = "| 段落 |"
-        separator = "|------|"
-        for element_type in element_type_list:
-            header += f" {element_type} |"
-            separator += "------|"
-        f.write(header + "\n")
-        f.write(separator + "\n")
-
-        # 表格内容
-        for row in orthogonal_matrix:
-            line = f"| {row['段落']} |"
-            for element_type in element_type_list:
-                analysis = row.get('元素类型分析', {}).get(element_type, "")
-                # 处理换行和特殊字符
-                analysis = analysis.replace("\n", " ").replace("|", "\\|")
-                line += f" {analysis} |"
-            f.write(line + "\n")
-
-        # 添加详细说明
-        f.write("\n## 段落内容范围详情\n\n")
-        for idx, row in enumerate(orthogonal_matrix, 1):
-            f.write(f"### {idx}. {row['段落']}\n\n")
-            f.write(f"**内容范围**:\n")
-            for content in row['内容范围']:
-                f.write(f"- {content}\n")
-            f.write("\n")
-
-    print(f"  ✓ Markdown表格生成成功")
-
-    # 测试总结
-    print("\n" + "=" * 100)
-    print("✅ 测试完成总结")
-    print("=" * 100)
-    print(f"  ✓ 成功加载真实数据文件")
-    print(f"  ✓ 成功提取 {len(orthogonal_matrix)} 个段落")
-    print(f"  ✓ 成功提取 {len(element_type_list)} 个元素类型")
-    print(f"  ✓ 成功生成正交分析矩阵")
-    print(f"  ✓ 结果已保存为JSON和Markdown格式")
-    print(f"\n  📂 输出文件:")
-    print(f"     - JSON: {output_path}")
-    print(f"     - Markdown: {markdown_output_path}")
-
-    print("\n" + "🎉" * 50)
-    print("测试成功完成!")
-    print("🎉" * 50 + "\n")
-
-    return result
-
-
-if __name__ == "__main__":
-    try:
-        test_with_real_data()
-    except Exception as e:
-        print(f"\n❌ 测试失败: {e}\n")
-        import traceback
-        traceback.print_exc()
-        exit(1)

+ 0 - 0
test/functions/__init__.py


+ 0 - 320
test/functions/test_json_utils.py

@@ -1,320 +0,0 @@
-"""
-JSON 安全解析功能组件的单元测试
-"""
-
-import pytest
-import json
-from src.components.functions.json_utils import (
-    JsonSafeParseFunction,
-    JSONParseError,
-    safe_json_parse,
-    batch_json_parse,
-    validate_json_structure
-)
-
-
-class TestJsonSafeParseFunction:
-    """测试 JsonSafeParseFunction 类"""
-    
-    def setup_method(self):
-        """设置测试方法"""
-        self.parser = JsonSafeParseFunction()
-    
-    def test_execute_basic(self):
-        """测试基本 JSON 解析"""
-        # 测试简单对象
-        json_str = '{"name": "Alice", "age": 30}'
-        result = self.parser.execute(json_str)
-        assert result == {"name": "Alice", "age": 30}
-        
-        # 测试数组
-        json_str = '[1, 2, 3, "test"]'
-        result = self.parser.execute(json_str)
-        assert result == [1, 2, 3, "test"]
-    
-    def test_execute_wrapped_json(self):
-        """测试包装的 JSON 解析"""
-        # 测试 ```json``` 包装
-        json_str = '```json\n{"key": "value"}\n```'
-        result = self.parser.execute(json_str)
-        assert result == {"key": "value"}
-        
-        # 测试 ``` 包装
-        json_str = '```\n{"key": "value"}\n```'
-        result = self.parser.execute(json_str)
-        assert result == {"key": "value"}
-        
-        # 测试带额外空白的包装
-        json_str = '   ```json   \n  {"key": "value"}  \n   ```   '
-        result = self.parser.execute(json_str)
-        assert result == {"key": "value"}
-    
-    def test_execute_with_context(self):
-        """测试带上下文的解析"""
-        json_str = '{"name": "Alice", "age": 30}'
-        context = {"options": {"strict": False}}
-        result = self.parser.execute(json_str, context)
-        assert result == {"name": "Alice", "age": 30}
-    
-    def test_pure_function_property(self):
-        """测试纯函数特性"""
-        json_str = '{"test": "data"}'
-        
-        # 多次调用应返回相同结果
-        result1 = self.parser.execute(json_str)
-        result2 = self.parser.execute(json_str)
-        assert result1 == result2
-        
-        # 不同实例应返回相同结果
-        parser2 = JsonSafeParseFunction()
-        result3 = parser2.execute(json_str)
-        assert result1 == result3
-    
-    def test_invalid_input_type(self):
-        """测试无效输入类型"""
-        with pytest.raises(JSONParseError) as exc_info:
-            self.parser.execute(123)  # 非字符串输入
-        assert "Expected string input" in str(exc_info.value)
-    
-    def test_invalid_json(self):
-        """测试无效 JSON"""
-        with pytest.raises(JSONParseError) as exc_info:
-            self.parser.execute('{"invalid": json}')  # 无效 JSON
-        assert "JSON decode error" in str(exc_info.value)
-    
-    def test_empty_input(self):
-        """测试空输入"""
-        with pytest.raises(JSONParseError) as exc_info:
-            self.parser.execute('')
-        assert "Empty or invalid JSON content" in str(exc_info.value)
-    
-    def test_extract_json_from_wrapped_string(self):
-        """测试 JSON 提取方法"""
-        # 测试各种格式
-        test_cases = [
-            ('```json\n{"key": "value"}\n```', '{"key": "value"}'),
-            ('```\n[1, 2, 3]\n```', '[1, 2, 3]'),
-            ('{"key": "value"}', '{"key": "value"}'),
-            ('[1, 2, 3]', '[1, 2, 3]'),
-            ('   ```JSON   \n  {"test": true}  \n   ```   ', '{"test": true}'),
-        ]
-        
-        for input_str, expected in test_cases:
-            result = self.parser._extract_json_from_wrapped_string(input_str)
-            assert result == expected
-    
-    def test_complex_nested_json(self):
-        """测试复杂嵌套 JSON"""
-        complex_json = {
-            "users": [
-                {"name": "Alice", "age": 30, "active": True},
-                {"name": "Bob", "age": 25, "active": False}
-            ],
-            "metadata": {
-                "total": 2,
-                "timestamp": "2024-01-01T00:00:00Z"
-            }
-        }
-        
-        json_str = f'```json\n{json.dumps(complex_json, indent=2)}\n```'
-        result = self.parser.execute(json_str)
-        assert result == complex_json
-
-
-class TestSafeJsonParse:
-    """测试 safe_json_parse 便捷函数"""
-    
-    def test_basic_parsing(self):
-        """测试基本解析功能"""
-        result = safe_json_parse('{"name": "test"}')
-        assert result == {"name": "test"}
-    
-    def test_wrapped_parsing(self):
-        """测试包装格式解析"""
-        result = safe_json_parse('```json\n{"wrapped": true}\n```')
-        assert result == {"wrapped": True}
-    
-    def test_error_handling(self):
-        """测试错误处理"""
-        with pytest.raises(JSONParseError):
-            safe_json_parse('invalid json')
-
-
-class TestBatchJsonParse:
-    """测试批量 JSON 解析功能"""
-    
-    def test_batch_parsing_success(self):
-        """测试成功的批量解析"""
-        json_strings = [
-            '{"name": "Alice"}',
-            '```json\n{"name": "Bob"}\n```',
-            '[1, 2, 3]'
-        ]
-        
-        results = batch_json_parse(json_strings)
-        expected = [
-            {"name": "Alice"},
-            {"name": "Bob"},
-            [1, 2, 3]
-        ]
-        assert results == expected
-    
-    def test_batch_parsing_error(self):
-        """测试批量解析中的错误"""
-        json_strings = [
-            '{"name": "Alice"}',
-            'invalid json',  # 这个会导致错误
-            '[1, 2, 3]'
-        ]
-        
-        with pytest.raises(JSONParseError) as exc_info:
-            batch_json_parse(json_strings)
-        assert "Failed to parse JSON at index 1" in str(exc_info.value)
-    
-    def test_invalid_input_type(self):
-        """测试无效输入类型"""
-        with pytest.raises(JSONParseError) as exc_info:
-            batch_json_parse("not a list")
-        assert "Expected list input" in str(exc_info.value)
-
-
-class TestValidateJsonStructure:
-    """测试 JSON 结构验证功能"""
-    
-    def test_validate_string_input(self):
-        """测试字符串输入验证"""
-        result = validate_json_structure('{"name": "Alice", "age": 30}')
-        assert result['valid'] is True
-        assert result['data'] == {"name": "Alice", "age": 30}
-        assert len(result['errors']) == 0
-    
-    def test_validate_dict_input(self):
-        """测试字典输入验证"""
-        data = {"name": "Alice", "age": 30}
-        result = validate_json_structure(data)
-        assert result['valid'] is True
-        assert result['data'] == data
-        assert len(result['errors']) == 0
-    
-    def test_validate_with_required_keys(self):
-        """测试必需字段验证"""
-        context = {
-            'validation_rules': {
-                'required_keys': ['name', 'age', 'email']
-            }
-        }
-        
-        # 缺少字段的情况
-        result = validate_json_structure('{"name": "Alice", "age": 30}', context)
-        assert result['valid'] is False
-        assert any("Missing required keys" in error for error in result['errors'])
-        
-        # 包含所有字段的情况
-        result = validate_json_structure(
-            '{"name": "Alice", "age": 30, "email": "alice@test.com"}', 
-            context
-        )
-        assert result['valid'] is True
-    
-    def test_validate_expected_type(self):
-        """测试期望类型验证"""
-        # 期望对象但得到数组
-        context = {'validation_rules': {'expected_type': 'object'}}
-        result = validate_json_structure('[1, 2, 3]', context)
-        assert result['valid'] is False
-        assert any("Expected JSON object" in error for error in result['errors'])
-        
-        # 期望数组但得到对象
-        context = {'validation_rules': {'expected_type': 'array'}}
-        result = validate_json_structure('{"key": "value"}', context)
-        assert result['valid'] is False
-        assert any("Expected JSON array" in error for error in result['errors'])
-    
-    def test_validate_invalid_json(self):
-        """测试无效 JSON 验证"""
-        result = validate_json_structure('invalid json')
-        assert result['valid'] is False
-        assert result['data'] is None
-        assert len(result['errors']) > 0
-    
-    def test_validate_non_json_types(self):
-        """测试非 JSON 类型验证"""
-        # 测试无法解析为 JSON 的字符串
-        result = validate_json_structure("just a string")
-        assert result['valid'] is False
-        assert any("JSON decode error" in error for error in result['errors'])
-        
-        # 测试解析成功但不是 JSON 对象或数组的情况(如字符串、数字等)
-        result = validate_json_structure('"just a string"')  # 这是有效的 JSON 字符串
-        assert result['valid'] is False
-        assert any("not a JSON object or array" in error for error in result['errors'])
-
-
-class TestPipelineComposition:
-    """测试管道组合能力"""
-    
-    def test_json_parse_in_pipeline(self):
-        """测试 JSON 解析在管道中的使用"""
-        from src.components.functions import create_simple_function, create_pipeline
-        
-        # 创建一个简单的数据转换函数
-        def extract_name(data, context=None):
-            if isinstance(data, dict) and 'name' in data:
-                return data['name']
-            raise ValueError("No name field found")
-        
-        # 创建管道
-        parse_func = JsonSafeParseFunction()
-        extract_func = create_simple_function("extract_name", "提取姓名", extract_name)
-        pipeline = create_pipeline("parse_and_extract", "解析并提取", [parse_func, extract_func])
-        
-        # 测试管道执行
-        json_str = '```json\n{"name": "Alice", "age": 30}\n```'
-        result = pipeline(json_str)
-        assert result == "Alice"
-    
-    def test_error_propagation_in_pipeline(self):
-        """测试管道中的错误传播"""
-        from src.components.functions import create_simple_function, create_pipeline
-        
-        def dummy_transform(data, context=None):
-            return data
-        
-        parse_func = JsonSafeParseFunction()
-        transform_func = create_simple_function("dummy", "虚拟转换", dummy_transform)
-        pipeline = create_pipeline("parse_pipeline", "解析管道", [parse_func, transform_func])
-        
-        # 测试无效 JSON 在管道中的错误传播
-        with pytest.raises(JSONParseError):
-            pipeline('invalid json')
-
-
-class TestContextHandling:
-    """测试上下文处理"""
-    
-    def test_context_options(self):
-        """测试上下文选项"""
-        parser = JsonSafeParseFunction()
-        
-        # 测试严格模式
-        context_strict = {'options': {'strict': True}}
-        result = parser.execute('{"name": "Alice"}', context_strict)
-        assert result == {"name": "Alice"}
-        
-        # 测试非严格模式
-        context_non_strict = {'options': {'strict': False}}
-        result = parser.execute('{"name": "Alice"}', context_non_strict)
-        assert result == {"name": "Alice"}
-    
-    def test_context_preservation(self):
-        """测试上下文保持"""
-        # 验证函数不会修改传入的上下文
-        original_context = {'options': {'strict': True}, 'other': 'data'}
-        context_copy = original_context.copy()
-        
-        safe_json_parse('{"test": true}', context_copy)
-        assert context_copy == original_context
-
-
-if __name__ == "__main__":
-    pytest.main([__file__])

+ 0 - 11
test/others/ReactAgent.py

@@ -1,11 +0,0 @@
-from langgraph.prebuilt import create_react_agent
-from src.components.tools import search_tools, think_tools
-
-agent = create_react_agent(
-    model="openai:gpt-4o",
-    tools=[think_tools.think_and_plan, search_tools.perplexity_search_agent],
-)
-
-r = agent.invoke(
-    {"messages": [{"role": "user", "content": "查询北京的天气和计算 1+2+3 ,用可用的工具"}]})
-print(r)

+ 0 - 0
test/others/__init__.py


+ 0 - 15
test/others/gemini_available_test.py

@@ -1,15 +0,0 @@
-import getpass
-import os
-
-if not os.environ.get("GOOGLE_API_KEY"):
-  os.environ["GOOGLE_API_KEY"] = getpass.getpass("Enter API key for Google Gemini: ")
-
-from langchain.chat_models import init_chat_model
-
-model = init_chat_model("gemini-2.5-flash", model_provider="google_genai")
-
-
-r = model.invoke("Hello, world!")
-
-print(r)
-

+ 0 - 41
test/others/graph.py

@@ -1,41 +0,0 @@
-import os
-from langchain.chat_models import init_chat_model
-
-from typing import Annotated
-
-from typing_extensions import TypedDict
-
-from langgraph.graph import StateGraph, START, END
-from langgraph.graph.message import add_messages
-
-
-class State(TypedDict):
-    # Messages have the type "list". The `add_messages` function
-    # in the annotation defines how this state key should be updated
-    # (in this case, it appends messages to the list, rather than overwriting them)
-    messages: Annotated[list, add_messages]
-
-
-graph_builder = StateGraph(State)
-
-os.environ["OPENAI_API_KEY"] = "sk-..."
-
-llm = init_chat_model("openai:gpt-4.1")
-
-
-def chatbot(state: State):
-    return {"messages": [llm.invoke(state["messages"])]}
-
-
-# The first argument is the unique node name
-# The second argument is the function or object that will be called whenever
-# the node is used.
-graph_builder.add_node("chatbot", chatbot)
-
-graph_builder.add_edge(START, "chatbot")
-
-graph = graph_builder.compile()
-
-img_data = graph.get_graph().draw_mermaid_png()
-with open("graph.png", "wb") as f:
-    f.write(img_data)

+ 0 - 0
test/tools/__init__.py


+ 0 - 61
test/tools/test_nanobanana_simple.py

@@ -1,61 +0,0 @@
-"""
-简单测试脚本 - 测试Nano Banana图像生成与编辑工具
-"""
-
-import sys
-from pathlib import Path
-
-# 添加项目根目录到路径
-project_root = Path(__file__).parent.parent.parent
-sys.path.insert(0, str(project_root))
-
-from src.components.tools.nanobanana_tools import nano_banana_generate_image
-
-
-# 真实测试图片
-REAL_IMAGE_URL = "https://res.cybertogether.net/crawler/image/097f43c71d746f7bd10f62df64222459.jpeg"
-
-
-def test_extract_mango():
-    """测试提取左上芒果"""
-    print("\n" + "=" * 70)
-    print("🎨 Nano Banana图像编辑工具 - 提取左上芒果测试")
-    print("=" * 70)
-    print(f"\n图片URL: {REAL_IMAGE_URL}\n")
-
-    test_prompt = "Extract the mango in the upper left corner of the image as a separate image"
-
-    print(f"📸 测试任务: {test_prompt}")
-    print("-" * 70)
-
-    result = nano_banana_generate_image(
-        prompt=test_prompt,
-        images=REAL_IMAGE_URL
-    )
-
-    print(f"\n结果:")
-    print(f"  {result}")
-    print()
-
-    # 判断是否成功
-    if result.startswith("http"):
-        print("✅ 成功! 返回了图片URL")
-    elif "失败" in result or "超时" in result or "错误" in result:
-        print("❌ 失败! API服务出现问题")
-        print("\n💡 可能的原因:")
-        print("   - Nano Banana后端服务超时")
-        print("   - API服务器负载过高")
-        print("   - 网络连接问题")
-    else:
-        print("⚠️  未知状态")
-
-    print("\n" + "=" * 70)
-
-
-if __name__ == "__main__":
-    try:
-        test_extract_mango()
-    except Exception as e:
-        print(f"\n❌ 发生错误: {str(e)}")
-        import traceback
-        traceback.print_exc()