topic_derivation.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. """
  2. 选题推导工具 - 图数据库游走
  3. 提供选题推导任务的状态管理和游走操作。
  4. """
  5. import logging
  6. import uuid
  7. import json
  8. import os
  9. from pathlib import Path
  10. from typing import List, Dict, Optional, Any
  11. from agent.tools import tool, ToolResult, ToolContext
  12. # 导入数据查询工具
  13. from .search_library import (
  14. _search_class_by_point,
  15. _search_point_by_class,
  16. _search_relation_class_by_class
  17. )
  18. from .search_pattern import _search_pattern
  19. logger = logging.getLogger(__name__)
  20. # 状态存储目录
  21. STATE_DIR = Path(__file__).parent.parent / ".db" / "derivation_states"
  22. STATE_DIR.mkdir(parents=True, exist_ok=True)
  23. def _get_state_file(derivation_id: str) -> Path:
  24. """获取状态文件路径"""
  25. return STATE_DIR / f"{derivation_id}.json"
  26. def _save_state(derivation_id: str, state: Dict[str, Any], change_description: str = "") -> None:
  27. """
  28. 保存状态到文件,并记录变更历史
  29. Args:
  30. derivation_id: 推导任务 ID
  31. state: 当前状态
  32. change_description: 变更描述
  33. """
  34. from datetime import datetime
  35. import copy
  36. # 初始化 history 字段(如果不存在)
  37. if "history" not in state:
  38. state["history"] = []
  39. # 记录本次变更(保留完整快照)
  40. if change_description:
  41. # 深拷贝当前状态作为快照(排除 history 本身,避免递归)
  42. snapshot = {
  43. "person_name": state.get("person_name"),
  44. "top_k_paths": state.get("top_k_paths"),
  45. "max_rounds": state.get("max_rounds"),
  46. "loop": state.get("loop", 0),
  47. "current_paths": copy.deepcopy(state.get("current_paths", [])),
  48. "discarded_paths": copy.deepcopy(state.get("discarded_paths", [])),
  49. "edges_to_expand": copy.deepcopy(state.get("edges_to_expand", [])),
  50. "candidate_paths": copy.deepcopy(state.get("candidate_paths", [])),
  51. "tool_call_stats": copy.deepcopy(state.get("tool_call_stats", {})),
  52. "prune_stats": copy.deepcopy(state.get("prune_stats", {})),
  53. "evidence_log": copy.deepcopy(state.get("evidence_log", []))
  54. }
  55. history_entry = {
  56. "timestamp": datetime.now().isoformat(),
  57. "round": state.get("loop", 0),
  58. "action": change_description,
  59. "snapshot": snapshot
  60. }
  61. state["history"].append(history_entry)
  62. # 保存到文件
  63. state_file = _get_state_file(derivation_id)
  64. with open(state_file, 'w', encoding='utf-8') as f:
  65. json.dump(state, f, ensure_ascii=False, indent=2)
  66. def _load_state(derivation_id: str) -> Optional[Dict[str, Any]]:
  67. """
  68. 从文件加载状态
  69. Args:
  70. derivation_id: 推导任务 ID
  71. Returns:
  72. 状态字典,如果不存在则返回 None
  73. """
  74. state_file = _get_state_file(derivation_id)
  75. if not state_file.exists():
  76. return None
  77. with open(state_file, 'r', encoding='utf-8') as f:
  78. state = json.load(f)
  79. # 向后兼容:为旧状态文件添加 history 字段
  80. if "history" not in state:
  81. state["history"] = []
  82. return state
  83. @tool(hidden_params=["context"])
  84. async def init_topic_derivation(
  85. person_name: str,
  86. constants: List[Dict[str, str]],
  87. top_k_paths: int,
  88. max_rounds: int,
  89. context: Optional[ToolContext] = None,
  90. ) -> ToolResult:
  91. """
  92. 初始化选题推导任务
  93. Args:
  94. person_name: 人设名称
  95. constants: 常量点列表,每个包含 {"名称": "...", "维度": "实质/形式/意图"}
  96. top_k_paths: 每轮保留路径数
  97. max_rounds: 最大推导轮次
  98. context: 工具上下文
  99. Returns:
  100. derivation_id 和初始状态
  101. """
  102. try:
  103. # 生成唯一任务 ID
  104. derivation_id = str(uuid.uuid4())
  105. # 为每个常量点创建初始路径
  106. initial_paths = []
  107. for constant in constants:
  108. path = [{
  109. "名称": constant["名称"],
  110. "类型": constant.get("类型", ""),
  111. "维度": constant["维度"]
  112. }]
  113. initial_paths.append(path)
  114. # 提取待扩展的末端点
  115. edges_to_expand = [
  116. {
  117. "名称": constant["名称"],
  118. "维度": constant["维度"],
  119. "类型": constant["类型"]
  120. }
  121. for constant in constants
  122. ]
  123. # 初始化状态
  124. state = {
  125. "person_name": person_name,
  126. "top_k_paths": top_k_paths,
  127. "max_rounds": max_rounds,
  128. "loop": 0,
  129. "current_paths": initial_paths,
  130. "discarded_paths": [],
  131. "edges_to_expand": edges_to_expand,
  132. "tool_call_stats": {},
  133. "prune_stats": {
  134. "语义冲突淘汰": 0,
  135. "低置信度淘汰": 0
  136. },
  137. "history": []
  138. }
  139. # 存储状态
  140. _save_state(derivation_id, state, "初始化推导任务")
  141. output = f"✅ 初始化成功\n\n"
  142. output += f"任务 ID: {derivation_id}\n"
  143. output += f"人设: {person_name}\n"
  144. output += f"常量点数量: {len(constants)}\n"
  145. output += f"初始路径数: {len(initial_paths)}\n"
  146. output += f"每轮保留: TOP {top_k_paths} 路径\n"
  147. output += f"最大轮次: {max_rounds}\n\n"
  148. output += f"常量点列表:\n"
  149. for idx, constant in enumerate(constants, 1):
  150. output += f" {idx}. {constant['名称']} ({constant['维度']}) ({constant['类型']})\n"
  151. return ToolResult(
  152. title="✅ 推导任务已初始化",
  153. output=output,
  154. metadata={
  155. "derivation_id": derivation_id,
  156. "initial_path_count": len(initial_paths),
  157. "constants": constants
  158. }
  159. )
  160. except Exception as e:
  161. logger.error(f"初始化推导任务失败: {e}")
  162. return ToolResult(
  163. title="❌ 初始化失败",
  164. output=f"错误: {str(e)}",
  165. error=str(e)
  166. )
  167. @tool(hidden_params=["context"])
  168. async def get_current_state(
  169. derivation_id: str,
  170. context: Optional[ToolContext] = None,
  171. ) -> ToolResult:
  172. """
  173. 获取当前推导状态
  174. Args:
  175. derivation_id: 推导任务 ID
  176. context: 工具上下文
  177. Returns:
  178. 当前状态,包含:
  179. - loop: 当前轮次
  180. - active_paths: 活跃路径列表
  181. - edges_to_expand: 待扩展的末端点
  182. """
  183. try:
  184. # 加载状态
  185. state = _load_state(derivation_id)
  186. if state is None:
  187. return ToolResult(
  188. title="❌ 任务不存在",
  189. output=f"未找到任务 ID: {derivation_id}",
  190. error="任务不存在"
  191. )
  192. # 提取关键信息
  193. loop = state["loop"]
  194. current_paths = state["current_paths"]
  195. edges_to_expand = state["edges_to_expand"]
  196. max_rounds = state["max_rounds"]
  197. top_k_paths = state["top_k_paths"]
  198. # 构建输出
  199. output = f"📊 当前状态\n\n"
  200. output += f"任务 ID: {derivation_id}\n"
  201. output += f"人设: {state['person_name']}\n"
  202. output += f"当前轮次: {loop} / {max_rounds}\n"
  203. output += f"活跃路径数: {len(current_paths)}\n"
  204. output += f"待扩展末端点数: {len(edges_to_expand)}\n"
  205. output += f"已淘汰路径数: {len(state['discarded_paths'])}\n\n"
  206. if edges_to_expand:
  207. output += f"待扩展的末端点:\n"
  208. for idx, edge in enumerate(edges_to_expand[:10], 1): # 最多显示10个
  209. output += f" {idx}. {edge['名称']} ({edge.get('维度', '')})\n"
  210. if len(edges_to_expand) > 10:
  211. output += f" ... 还有 {len(edges_to_expand) - 10} 个\n"
  212. else:
  213. output += "⚠️ 没有待扩展的末端点,无法继续游走\n"
  214. output += f"\n活跃路径示例 (前3条):\n"
  215. for idx, path in enumerate(current_paths[:3], 1):
  216. path_str = " → ".join([node["名称"] for node in path])
  217. output += f" {idx}. {path_str} (长度: {len(path)})\n"
  218. return ToolResult(
  219. title=f"📊 轮次 {loop}/{max_rounds}",
  220. output=output,
  221. metadata={
  222. "loop": loop,
  223. "active_paths": current_paths,
  224. "edges_to_expand": edges_to_expand,
  225. "max_rounds": max_rounds,
  226. "top_k_paths": top_k_paths,
  227. "can_continue": len(edges_to_expand) > 0 and loop < max_rounds
  228. }
  229. )
  230. except Exception as e:
  231. logger.error(f"获取状态失败: {e}")
  232. return ToolResult(
  233. title="❌ 获取状态失败",
  234. output=f"错误: {str(e)}",
  235. error=str(e)
  236. )
  237. @tool(hidden_params=["context"])
  238. async def add_nodes_to_paths(
  239. derivation_id: str,
  240. path_extensions: List[Dict[str, Any]],
  241. context: Optional[ToolContext] = None,
  242. ) -> ToolResult:
  243. """
  244. 将节点添加到指定路径,生成候选路径
  245. Agent 负责智能决策:
  246. - 调用数据查询工具获取候选节点
  247. - 分析和选择要添加的节点
  248. - 决定每条路径的扩展方式
  249. 工具负责机械操作:
  250. - 将节点添加到路径中
  251. - 记录 Evidence 日志
  252. - 检查路径连续性和避免循环
  253. - 生成候选路径
  254. Args:
  255. derivation_id: 推导任务 ID
  256. path_extensions: 路径扩展列表,每个包含:
  257. - path_id: 路径索引
  258. - new_nodes: 要添加的节点列表,每个节点包含:
  259. - 名称: 节点名称
  260. - 类型: 节点类型(可选)
  261. - 维度: 节点维度(可选)
  262. - 分类: "point" 或 "class"
  263. - step_type: 游走方法(generalize/specialize/relate/pattern)
  264. - link_type: 推导关系类型
  265. - evidence: Evidence 信息
  266. - tool: 使用的工具名称
  267. - query: 查询参数
  268. - reasoning: 推理依据
  269. context: 工具上下文
  270. Returns:
  271. 扩展后的候选路径信息
  272. """
  273. try:
  274. # 加载状态
  275. state = _load_state(derivation_id)
  276. if state is None:
  277. return ToolResult(
  278. title="❌ 任务不存在",
  279. output=f"未找到任务 ID: {derivation_id}",
  280. error="任务不存在"
  281. )
  282. current_paths = state["current_paths"]
  283. loop = state["loop"]
  284. # 存储新路径(每条路径可能产生多个候选)
  285. candidate_paths = []
  286. evidence_log = state.get("evidence_log", [])
  287. tool_call_stats = state.get("tool_call_stats", {})
  288. # 处理每个路径扩展
  289. for extension in path_extensions:
  290. path_id = extension["path_id"]
  291. new_nodes = extension["new_nodes"]
  292. step_type = extension["step_type"]
  293. link_type = extension["link_type"]
  294. evidence_info = extension.get("evidence", {})
  295. reasoning = extension["reasoning"]
  296. if path_id >= len(current_paths):
  297. logger.warning(f"路径 ID {path_id} 超出范围,跳过")
  298. continue
  299. # 获取当前路径和末端节点
  300. current_path = current_paths[path_id]
  301. end_node = current_path[-1]
  302. # 为每个新节点创建新路径
  303. for new_node_info in new_nodes:
  304. # 检查是否会造成循环
  305. node_names = [node["名称"] for node in current_path]
  306. if new_node_info["名称"] in node_names:
  307. logger.info(f"跳过重复节点: {new_node_info['名称']}")
  308. continue
  309. # 创建新节点
  310. new_node = {
  311. "名称": new_node_info["名称"],
  312. "类型": new_node_info.get("类型", ""),
  313. "维度": new_node_info.get("维度", ""),
  314. "分类": new_node_info["分类"],
  315. "来源": end_node["名称"],
  316. "step_type": step_type,
  317. "link_type": link_type,
  318. "推理": reasoning
  319. }
  320. # 创建新路径
  321. new_path = current_path + [new_node]
  322. candidate_paths.append(new_path)
  323. # 记录 Evidence
  324. evidence = {
  325. "round": loop + 1,
  326. "path_id": path_id,
  327. "step_type": step_type,
  328. "evidence_type": evidence_info.get("evidence_type", "unknown"),
  329. "role": "expand",
  330. "reference_detail": {
  331. "tool": evidence_info.get("tool", "unknown"),
  332. "query": evidence_info.get("query", {}),
  333. "source_node": end_node["名称"],
  334. "result_node": new_node_info["名称"]
  335. }
  336. }
  337. evidence_log.append(evidence)
  338. # 更新工具调用统计
  339. tool_name = evidence_info.get("tool", "unknown")
  340. tool_call_stats[tool_name] = tool_call_stats.get(tool_name, 0) + 1
  341. # 更新状态(暂不保存,等待 evaluate_and_prune)
  342. state["candidate_paths"] = candidate_paths
  343. state["evidence_log"] = evidence_log
  344. state["tool_call_stats"] = tool_call_stats
  345. _save_state(derivation_id, state, f"添加节点到 {len(path_extensions)} 条路径,生成 {len(candidate_paths)} 条候选路径")
  346. # 构建输出
  347. output = f"🚶 节点已添加到路径\n\n"
  348. output += f"轮次: {loop + 1}\n"
  349. output += f"处理路径数: {len(path_extensions)}\n"
  350. output += f"生成候选路径数: {len(candidate_paths)}\n\n"
  351. output += f"候选路径示例 (前3条):\n"
  352. for idx, path in enumerate(candidate_paths[:3], 1):
  353. path_str = " → ".join([node["名称"] for node in path])
  354. output += f" {idx}. {path_str}\n"
  355. return ToolResult(
  356. title="✅ 节点添加完成",
  357. output=output,
  358. metadata={
  359. "candidate_count": len(candidate_paths),
  360. "candidate_paths": candidate_paths
  361. }
  362. )
  363. except Exception as e:
  364. logger.error(f"添加节点失败: {e}")
  365. return ToolResult(
  366. title="❌ 添加节点失败",
  367. output=f"错误: {str(e)}",
  368. error=str(e)
  369. )
  370. @tool(hidden_params=["context"])
  371. async def evaluate_and_prune(
  372. derivation_id: str,
  373. path_evaluations: List[Dict[str, Any]],
  374. context: Optional[ToolContext] = None,
  375. ) -> ToolResult:
  376. """
  377. 执行路径评估和全局 TopK 剪枝
  378. Agent 负责智能评估(语义矛盾、人设风格匹配),工具负责执行剪枝。
  379. Args:
  380. derivation_id: 推导任务 ID
  381. path_evaluations: Agent 的评估结果列表,每个包含:
  382. - path_id: 候选路径索引
  383. - score: 评分(0-10)
  384. - keep: 是否保留
  385. - reason: 评估理由
  386. context: 工具上下文
  387. Returns:
  388. 剪枝结果,包含:
  389. - retained_paths: 保留的 TOP_K 路径
  390. - discarded_paths: 被淘汰的路径及原因
  391. - can_continue: 是否可以继续游走
  392. """
  393. try:
  394. # 加载状态
  395. state = _load_state(derivation_id)
  396. if state is None:
  397. return ToolResult(
  398. title="❌ 任务不存在",
  399. output=f"未找到任务 ID: {derivation_id}",
  400. error="任务不存在"
  401. )
  402. candidate_paths = state.get("candidate_paths", [])
  403. if not candidate_paths:
  404. return ToolResult(
  405. title="❌ 没有候选路径",
  406. output="请先调用 add_nodes_to_paths 生成候选路径",
  407. error="没有候选路径"
  408. )
  409. top_k_paths = state["top_k_paths"]
  410. loop = state["loop"]
  411. # 构建评估映射
  412. evaluation_map = {eval_item["path_id"]: eval_item for eval_item in path_evaluations}
  413. # 分类路径:保留 vs 淘汰
  414. paths_to_keep = []
  415. paths_to_discard = []
  416. for idx, path in enumerate(candidate_paths):
  417. evaluation = evaluation_map.get(idx)
  418. if evaluation is None:
  419. # 如果 Agent 没有评估这条路径,默认保留
  420. paths_to_keep.append({
  421. "path": path,
  422. "score": 5.0,
  423. "reason": "未评估,默认保留"
  424. })
  425. elif evaluation.get("keep", True):
  426. paths_to_keep.append({
  427. "path": path,
  428. "score": evaluation.get("score", 5.0),
  429. "reason": evaluation.get("reason", "")
  430. })
  431. else:
  432. paths_to_discard.append({
  433. "path": path,
  434. "reason": evaluation.get("reason", "未通过评估")
  435. })
  436. # 按分数降序排序
  437. paths_to_keep.sort(key=lambda x: x["score"], reverse=True)
  438. # 全局 TopK 剪枝
  439. retained_paths = [item["path"] for item in paths_to_keep[:top_k_paths]]
  440. additional_discarded = [item["path"] for item in paths_to_keep[top_k_paths:]]
  441. # 记录淘汰原因
  442. for path in additional_discarded:
  443. paths_to_discard.append({
  444. "path": path,
  445. "reason": "全局 TopK 剪枝淘汰"
  446. })
  447. # 提取新的待扩展末端点
  448. edges_to_expand = []
  449. for path in retained_paths:
  450. end_node = path[-1]
  451. edges_to_expand.append({
  452. "名称": end_node["名称"],
  453. "维度": end_node.get("维度", ""),
  454. "分类": end_node.get("分类", "")
  455. })
  456. # 更新状态
  457. state["current_paths"] = retained_paths
  458. state["edges_to_expand"] = edges_to_expand
  459. state["loop"] = loop + 1
  460. # 记录淘汰路径
  461. discarded_paths = state.get("discarded_paths", [])
  462. for item in paths_to_discard:
  463. discarded_paths.append({
  464. "round": loop + 1,
  465. "reason": item["reason"],
  466. "path": item["path"]
  467. })
  468. state["discarded_paths"] = discarded_paths
  469. # 更新剪枝统计
  470. prune_stats = state.get("prune_stats", {})
  471. for item in paths_to_discard:
  472. reason = item["reason"]
  473. if "矛盾" in reason:
  474. prune_stats["语义冲突淘汰"] = prune_stats.get("语义冲突淘汰", 0) + 1
  475. elif "TopK" in reason:
  476. prune_stats["低置信度淘汰"] = prune_stats.get("低置信度淘汰", 0) + 1
  477. state["prune_stats"] = prune_stats
  478. # 清空候选路径
  479. state["candidate_paths"] = []
  480. # 保存状态
  481. _save_state(derivation_id, state, f"轮次 {loop + 1} 评估与剪枝:保留 {len(retained_paths)} 条,淘汰 {len(paths_to_discard)} 条")
  482. # 检查是否可以继续
  483. can_continue = len(edges_to_expand) > 0 and (loop + 1) < state["max_rounds"]
  484. # 构建输出
  485. output = f"✂️ 评估与剪枝已完成\n\n"
  486. output += f"轮次: {loop + 1}\n"
  487. output += f"候选路径数: {len(candidate_paths)}\n"
  488. output += f"保留路径数: {len(retained_paths)}\n"
  489. output += f"淘汰路径数: {len(paths_to_discard)}\n"
  490. output += f"可继续游走: {'是' if can_continue else '否'}\n\n"
  491. if paths_to_discard:
  492. output += f"淘汰原因统计:\n"
  493. reason_counts = {}
  494. for item in paths_to_discard:
  495. reason = item["reason"]
  496. reason_counts[reason] = reason_counts.get(reason, 0) + 1
  497. for reason, count in reason_counts.items():
  498. output += f" - {reason}: {count} 条\n"
  499. output += f"\n保留路径示例 (前3条):\n"
  500. for idx, path in enumerate(retained_paths[:3], 1):
  501. path_str = " → ".join([node["名称"] for node in path])
  502. score = paths_to_keep[idx-1]["score"] if idx-1 < len(paths_to_keep) else 0
  503. output += f" {idx}. [{score:.1f}分] {path_str}\n"
  504. return ToolResult(
  505. title="✅ 评估与剪枝完成",
  506. output=output,
  507. metadata={
  508. "retained_count": len(retained_paths),
  509. "discarded_count": len(paths_to_discard),
  510. "can_continue": can_continue,
  511. "current_loop": loop + 1,
  512. "max_rounds": state["max_rounds"]
  513. }
  514. )
  515. except Exception as e:
  516. logger.error(f"评估与剪枝失败: {e}")
  517. return ToolResult(
  518. title="❌ 评估与剪枝失败",
  519. output=f"错误: {str(e)}",
  520. error=str(e)
  521. )
  522. @tool(hidden_params=["context"])
  523. async def get_final_paths(
  524. derivation_id: str,
  525. expand_class_nodes: bool = True,
  526. context: Optional[ToolContext] = None,
  527. ) -> ToolResult:
  528. """
  529. 获取最终路径数据,准备生成选题
  530. 工具负责数据准备:
  531. - 获取最终保留的 TOP_K_PATHS 条路径
  532. - 自动展开分类节点为具体点(如果需要)
  533. - 提取点组合
  534. - 生成执行摘要统计
  535. Agent 负责智能生成:
  536. - 分析路径数据
  537. - 为每条路径撰写 5-8 句话的创作指导
  538. - 说明预期效果和推理过程
  539. Args:
  540. derivation_id: 推导任务 ID
  541. expand_class_nodes: 是否自动展开分类节点为具体点(默认 true)
  542. context: 工具上下文
  543. Returns:
  544. 最终路径数据和执行摘要
  545. """
  546. try:
  547. # 加载状态
  548. state = _load_state(derivation_id)
  549. if state is None:
  550. return ToolResult(
  551. title="❌ 任务不存在",
  552. output=f"未找到任务 ID: {derivation_id}",
  553. error="任务不存在"
  554. )
  555. current_paths = state["current_paths"]
  556. loop = state["loop"]
  557. max_rounds = state["max_rounds"]
  558. top_k_paths = state["top_k_paths"]
  559. tool_call_stats = state.get("tool_call_stats", {})
  560. prune_stats = state.get("prune_stats", {})
  561. discarded_paths = state.get("discarded_paths", [])
  562. # 准备最终路径列表
  563. final_paths = []
  564. for path_idx, path in enumerate(current_paths):
  565. # 如果需要展开分类节点
  566. expanded_path = []
  567. point_combination = [] # 只包含点,不包含分类
  568. for node in path:
  569. node_type = node.get("分类", "point") # 默认为 point
  570. if node_type == "class" and expand_class_nodes:
  571. # 调用 search_point_by_class 展开分类节点
  572. try:
  573. class_path = node["名称"]
  574. results = _search_point_by_class([class_path])
  575. # 获取该分类下的点(最多取3个)
  576. points = []
  577. for item in results:
  578. points.extend(item.get("points", [])[:3])
  579. if points:
  580. # 为每个点创建节点
  581. for point_name in points[:3]: # 最多3个点
  582. expanded_node = {
  583. "名称": point_name,
  584. "类型": node.get("类型", ""),
  585. "维度": node.get("维度", ""),
  586. "分类": "point",
  587. "来源": f"展开自分类: {class_path}"
  588. }
  589. expanded_path.append(expanded_node)
  590. point_combination.append({
  591. "名称": point_name,
  592. "维度": node.get("维度", ""),
  593. "来源节点": class_path
  594. })
  595. else:
  596. # 如果没有找到点,保留分类节点
  597. expanded_path.append(node)
  598. except Exception as e:
  599. logger.warning(f"展开分类节点失败: {node['名称']}, 错误: {e}")
  600. expanded_path.append(node)
  601. else:
  602. # 保留原节点
  603. expanded_path.append(node)
  604. if node_type == "point":
  605. point_combination.append({
  606. "名称": node["名称"],
  607. "维度": node.get("维度", ""),
  608. "来源节点": node.get("来源", "起始常量点")
  609. })
  610. # 构建完整路径信息
  611. final_path_info = {
  612. "路径编号": path_idx + 1,
  613. "点组合": point_combination,
  614. "完整路径": expanded_path,
  615. "路径长度": len(expanded_path),
  616. "原始路径长度": len(path)
  617. }
  618. final_paths.append(final_path_info)
  619. # 生成执行摘要
  620. execution_summary = {
  621. "总轮次": loop,
  622. "最大轮次": max_rounds,
  623. "最终路径数": len(final_paths),
  624. "目标路径数": top_k_paths,
  625. "工具调用统计": tool_call_stats,
  626. "路径统计": {
  627. "总生成路径数": len(current_paths) + len(discarded_paths),
  628. "保留路径数": len(current_paths),
  629. "淘汰路径数": len(discarded_paths)
  630. },
  631. "剪枝统计": prune_stats
  632. }
  633. # 构建输出
  634. output = f"📋 最终路径数据已准备\n\n"
  635. output += f"任务 ID: {derivation_id}\n"
  636. output += f"人设: {state['person_name']}\n"
  637. output += f"完成轮次: {loop} / {max_rounds}\n"
  638. output += f"最终路径数: {len(final_paths)}\n\n"
  639. output += f"执行摘要:\n"
  640. output += f" - 总生成路径: {execution_summary['路径统计']['总生成路径数']} 条\n"
  641. output += f" - 保留路径: {execution_summary['路径统计']['保留路径数']} 条\n"
  642. output += f" - 淘汰路径: {execution_summary['路径统计']['淘汰路径数']} 条\n\n"
  643. output += f"工具调用统计:\n"
  644. for tool_name, count in tool_call_stats.items():
  645. output += f" - {tool_name}: {count} 次\n"
  646. if prune_stats:
  647. output += f"\n剪枝统计:\n"
  648. for reason, count in prune_stats.items():
  649. output += f" - {reason}: {count} 条\n"
  650. output += f"\n路径示例 (前3条):\n"
  651. for idx, path_info in enumerate(final_paths[:3], 1):
  652. points = [p["名称"] for p in path_info["点组合"]]
  653. output += f" {idx}. {' → '.join(points)} ({len(points)} 个点)\n"
  654. output += f"\n💡 接下来请为每条路径生成选题:\n"
  655. output += f" - 选题应该是 5-8 句话的完整创作指导\n"
  656. output += f" - 说明预期效果\n"
  657. output += f" - 解释推理过程\n"
  658. return ToolResult(
  659. title="✅ 最终路径数据已准备",
  660. output=output,
  661. metadata={
  662. "final_paths": final_paths,
  663. "execution_summary": execution_summary,
  664. "person_name": state["person_name"]
  665. }
  666. )
  667. except Exception as e:
  668. logger.error(f"获取最终路径失败: {e}")
  669. return ToolResult(
  670. title="❌ 获取最终路径失败",
  671. output=f"错误: {str(e)}",
  672. error=str(e)
  673. )