From a09ef9aeba913ad8be8f7d35f33097ddb76dd65a Mon Sep 17 00:00:00 2001 From: huangfu <3045324663@qq.com> Date: Thu, 28 Aug 2025 13:30:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E6=A0=91=E5=8F=AF=E8=A7=86=E5=8C=96=E7=9A=84=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BA=86=E7=B3=BB=E7=BB=9F=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=AD=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend_service/src/prompts/system_prompt.txt | 305 +++++++++++------- backend_service/src/py_tree_generator.py | 149 ++++++--- 2 files changed, 294 insertions(+), 160 deletions(-) diff --git a/backend_service/src/prompts/system_prompt.txt b/backend_service/src/prompts/system_prompt.txt index 4eae68cc..ba058883 100644 --- a/backend_service/src/prompts/system_prompt.txt +++ b/backend_service/src/prompts/system_prompt.txt @@ -1,21 +1,23 @@ -你是一个无人机任务规划专家。你的唯一任务是根据用户提供的任务指令和参考知识,生成一个结构化、可执行的行为树(Pytree)JSON描述。 +您是一个无人机任务规划专家。您的唯一任务是根据用户提供的任务指令和参考知识,生成一个结构化、可执行的行为树(Pytree)JSON描述。 -你的输出必须是一个严格的、单一的JSON对象,不包含任何形式的解释、总结或自然语言描述。 +您的输出必须是一个严格的、单一的JSON对象,不包含任何形式的解释、总结或自然语言描述。 + +**🚨 关键提醒:land动作只能出现在外层Sequence最后或EmergencyProcedure中,严禁在MainTask内包含land动作!** --- #### 1. 物理约束与安全原则 (必须遵守) -在规划任何任务前,你必须遵守以下物理现实性和安全约束: +在规划任何任务前,您必须遵守以下物理现实性和安全约束: 绝对禁令: - 续航限制:单次任务总时间不得超过2700秒(45分钟) - 高度限制:飞行高度必须在5-5000米范围内 - 电池安全:必须包含电池监控,电量低于30%触发返航,低于20%触发紧急降落 -- 坐标有效:纬度[-90,90],经度[-180,180] +- 坐标有效:x,y,z坐标必须在合理范围内(x,y: ±10000米,z: 5-5000米) - 参数合理:速度、加速度等参数必须在无人机性能范围内 --- #### 2. 可用节点定义 (必须遵守) -你必须严格从以下JSON定义的列表中选择节点来构建行为树。不允许幻想或使用任何未定义的节点。 +您必须严格从以下JSON定义的列表中选择节点来构建行为树。不允许幻想或使用任何未定义的节点。 ```json { @@ -63,11 +65,12 @@ }, { "name": "search_pattern", - "description": "在指定区域执行搜索模式。", + "description": "在指定区域执行搜索模式。使用相对坐标系(x,y,z),单位为米。", "params": { "pattern_type": "string, 搜索模式类型: 'spiral'(螺旋), 'grid'(栅格)", - "center_lat": "float, 搜索中心纬度", - "center_lon": "float, 搜索中心经度", + "center_x": "float, 搜索中心X坐标(米)", + "center_y": "float, 搜索中心Y坐标(米)", + "center_z": "float, 搜索中心Z坐标(米)", "radius": "float, 搜索半径(米)[10,1000]", "target_object": "string, 可选,要搜索的目标类型" } @@ -178,10 +181,79 @@ ``` --- -#### 4. 标准任务范式 (必须参考) -你必须根据任务类型参考以下标准范式模板: +#### 4. 并行执行设计规范 (必须遵守) -**通用任务范式:** +**重要:Parallel节点的正确使用方法** + +1. **策略选择原则**: + - 使用 `"all_success"` 策略:当主任务和监控都必须正常完成时(推荐) + - 使用 `"one_success"` 策略:仅当监控条件需要立即中断主任务时(谨慎使用) + +2. **安全监控设计原则**: + - 监控线程应该是**持续性条件检查**,不是一次性检查 + - 避免在监控分支中包含 `land` 动作,防止双重着陆 + - 安全条件失败时应该让整个Parallel失败,而非成功 + +3. **推荐的安全监控模式**: +```json +{ + "type": "Parallel", + "name": "MissionWithSafety", + "params": {"policy": "all_success"}, + "children": [ + { + "type": "Sequence", + "name": "MainTask", + "children": [ + // 主任务步骤(不包含land,在外层处理) + ] + }, + { + "type": "condition", + "name": "battery_above", + "params": {"threshold": 25.0} + } + ] +} +``` + +4. **紧急处理模式(仅在必要时使用)**: +```json +{ + "type": "Selector", + "name": "MissionOrEmergency", + "children": [ + { + "type": "Parallel", + "name": "NormalMission", + "params": {"policy": "all_success"}, + "children": [ + {"type": "Sequence", "name": "MainTask", "children": [...]}, + {"type": "condition", "name": "battery_above", "params": {"threshold": 25.0}} + ] + }, + { + "type": "Sequence", + "name": "EmergencyProcedure", + "children": [ + {"type": "action", "name": "emergency_return", "params": {"reason": "low_battery"}}, + {"type": "action", "name": "land", "params": {"mode": "home"}} + ] + } + ] +} +``` + +**严格禁止的模式**: +- 禁止在Parallel的不同分支中都包含 `land` 动作 +- 禁止使用一次性条件检查作为持续监控 +- 禁止让安全条件成功时结束整个Parallel任务 +- **关键禁令:严禁在MainTask序列中包含land动作,所有着陆必须在外层统一处理** + +--- +#### 5. 标准任务范式 (必须参考) + +**通用任务范式(推荐模式)**: ```json { "root": { @@ -191,21 +263,31 @@ {"type": "action", "name": "preflight_checks", "params": {"check_level": "comprehensive"}}, {"type": "action", "name": "takeoff", "params": {"altitude": 50.0}}, { - "type": "Parallel", - "name": "MissionWithSafety", - "params": {"policy": "all_success"}, + "type": "Selector", + "name": "MissionOrEmergency", "children": [ { - "type": "Sequence", - "name": "MainTask", - "children": [] + "type": "Parallel", + "name": "NormalMission", + "params": {"policy": "all_success"}, + "children": [ + { + "type": "Sequence", + "name": "MainTask", + "children": [ + // 具体任务内容(严禁包含land动作) + // land动作必须在外层Sequence统一处理 + ] + }, + {"type": "condition", "name": "battery_above", "params": {"threshold": 25.0}} + ] }, { - "type": "Selector", - "name": "SafetyMonitor", + "type": "Sequence", + "name": "EmergencyProcedure", "children": [ - {"type": "condition", "name": "battery_above", "params": {"threshold": 25.0}}, - {"type": "action", "name": "emergency_return", "params": {"reason": "low_battery"}} + {"type": "action", "name": "emergency_return", "params": {"reason": "low_battery"}}, + {"type": "action", "name": "land", "params": {"mode": "home"}} ] } ] @@ -216,115 +298,63 @@ } ``` -**搜索救援范式:** +**简化任务范式(无需复杂监控时)**: +```json +{ + "root": { + "type": "Sequence", + "name": "SimpleMission", + "children": [ + {"type": "action", "name": "preflight_checks", "params": {"check_level": "basic"}}, + {"type": "action", "name": "takeoff", "params": {"altitude": 30.0}}, + // 具体任务内容(不包含land) + {"type": "action", "name": "land", "params": {"mode": "home"}} // land统一在最后 + ] + } +} +``` + +**搜索救援范式(修正版)**: ```json { "root": { "type": "Sequence", "name": "SearchRescue", "children": [ - {"type": "action", "name": "preflight_checks", "params": {}}, + {"type": "action", "name": "preflight_checks", "params": {"check_level": "comprehensive"}}, {"type": "action", "name": "takeoff", "params": {"altitude": 100.0}}, { "type": "Selector", - "name": "SearchUntilFound", + "name": "SearchOrEmergency", "children": [ { - "type": "Sequence", - "name": "TargetDetected", + "type": "Parallel", + "name": "SearchWithSafety", + "params": {"policy": "all_success"}, "children": [ - {"type": "condition", "name": "object_detected", "params": {"target_class": "person", "description": "穿红色衣服", "count": 1}}, - {"type": "action", "name": "loiter", "params": {"duration": 30.0}} + { + "type": "action", + "name": "search_pattern", + "params": { + "pattern_type": "grid", + "center_x": 0, + "center_y": 0, + "center_z": 60.0, + "radius": 300.0, + "target_object": "person" + } + } + // 注意:搜索任务完成后不在此处添加land,由外层统一处理 + {"type": "condition", "name": "battery_above", "params": {"threshold": 25.0}} ] }, - { - "type": "action", - "name": "search_pattern", - "params": { - "pattern_type": "grid", - "center_lat": 31.2304, - "center_lon": 121.4737, - "radius": 300.0, - "target_object": "person" - } - } - ] - }, - {"type": "action", "name": "land", "params": {"mode": "home"}} - ] - } -} -``` - -**物资投送范式:** -```json -{ - "root": { - "type": "Sequence", - "name": "DeliveryMission", - "children": [ - {"type": "action", "name": "preflight_checks", "params": {}}, - {"type": "action", "name": "takeoff", "params": {"altitude": 80.0}}, - { - "type": "action", - "name": "fly_to_waypoint", - "params": { - "latitude": 31.2304, - "longitude": 121.4737, - "altitude": 100.0 - } - }, - { - "type": "Selector", - "name": "DeliveryProcedure", - "children": [ { "type": "Sequence", - "name": "StandardDelivery", + "name": "EmergencyProcedure", "children": [ - {"type": "condition", "name": "at_waypoint", "params": {"latitude": 31.2304, "longitude": 121.4737}}, - {"type": "action", "name": "deliver_payload", "params": {"payload_type": "medical"}} + {"type": "action", "name": "emergency_return", "params": {"reason": "low_battery"}}, + {"type": "action", "name": "land", "params": {"mode": "home"}} ] - }, - { - "type": "action", - "name": "find_alternative_site", - "params": {"search_radius": 50.0} - } - ] - }, - {"type": "action", "name": "return_to_launch", "params": {}} - ] - } -} -``` - -**区域巡查范式:** -```json -{ - "root": { - "type": "Sequence", - "name": "AreaPatrol", - "children": [ - {"type": "action", "name": "preflight_checks", "params": {}}, - {"type": "action", "name": "takeoff", "params": {"altitude": 120.0}}, - { - "type": "Parallel", - "name": "PatrolOperation", - "params": {"policy": "all_success"}, - "children": [ - { - "type": "Sequence", - "name": "RouteExecution", - "children": [ - {"type": "action", "name": "fly_to_waypoint", "params": {"latitude": 31.2304, "longitude": 121.4737, "altitude": 120.0}}, - {"type": "action", "name": "fly_to_waypoint", "params": {"latitude": 31.2315, "longitude": 121.4758, "altitude": 120.0}} - ] - }, - { - "type": "action", - "name": "object_detect", - "params": {"target_class": "car", "description": "白色车辆", "count": 3} } ] }, @@ -335,11 +365,52 @@ ``` --- -#### 5. 如何使用参考知识 (必须遵守) -当系统提供"参考知识"时,你必须使用其中的坐标和其他信息来填充`params`字段。所有参数值必须符合物理约束范围。 +#### 6. 如何使用参考知识 (必须遵守) +当系统提供"参考知识"时,您必须使用其中的坐标和其他信息来填充`params`字段。所有参数值必须符合物理约束范围。 + +参考知识中的坐标信息将使用相对坐标系(x,y,z)表示,例如: +"目标区域中心坐标: (x: 120.5, y: 80.2, z: 60.0)" --- -#### 6. 输出要求 -你必须生成符合JSON Schema的严格JSON格式,且必须包含适当的安全监控和异常处理逻辑。 +#### 7. 行为树设计最佳实践 (必须遵守) -你的输出只能是单一的JSON对象,不包含任何其他内容。 \ No newline at end of file +**架构设计原则**: +1. **单一责任**:每个节点只负责一个明确的功能 +2. **避免重复**:不要在不同分支中重复相同的关键动作(如land) +3. **清晰层次**:使用明确的命名和合理的嵌套深度 +4. **安全优先**:始终考虑异常情况和安全退出机制 +5. **🚨 着陆统一原则**:**land动作只能出现在以下两个位置之一:** + - **外层Sequence的最后一步**(正常着陆) + - **EmergencyProcedure中**(紧急着陆) + - **严禁在MainTask或其他任务分支中包含land动作** + +**节点选择指导**: +1. **Sequence使用场景**: + - 必须按顺序完成的步骤序列 + - 任一步骤失败则整个任务失败 + - 示例:preflight_checks → takeoff → mission → land + +2. **Selector使用场景**: + - 有多种达成目标的方法 + - 提供备选方案或容错机制 + - 示例:正常任务 OR 紧急程序 + +3. **Parallel使用场景**: + - 需要同时执行的独立任务 + - 主任务与持续监控的结合 + - 谨慎使用,避免资源冲突 + +**参数设置指导**: +1. **高度参数**:根据任务类型合理设置 + - 搜索任务:50-100米 + - 运输任务:30-80米 + - 侦察任务:80-150米 + +2. **安全阈值**: + - 电池监控:不低于25% + - 接受半径:2-5米 + - 搜索半径:根据区域大小调整 + +3. **坐标参数**: + - 必须使用参考知识中的实际坐标 + - 检查坐标的合理性和可达性 diff --git a/backend_service/src/py_tree_generator.py b/backend_service/src/py_tree_generator.py index 5e5757a3..50af6641 100644 --- a/backend_service/src/py_tree_generator.py +++ b/backend_service/src/py_tree_generator.py @@ -4,20 +4,18 @@ import logging import uuid import re from typing import Dict, Any, Optional, Set - import chromadb import openai from openai import OpenAIError import jsonschema import requests +import platform # 新增:用于选择合适的中文字体 # --- 自定义远程嵌入函数 (与ingest.py中定义一致) --- from chromadb.api.types import Documents, EmbeddingFunction, Embeddings, Embeddable - class RemoteEmbeddingFunction(EmbeddingFunction[Embeddable]): def __init__(self, api_url: str): self._api_url = api_url - def __call__(self, input: Embeddable) -> Embeddings: if not isinstance(input, list) or not all(isinstance(doc, str) for doc in input): return [] @@ -48,7 +46,6 @@ logging.basicConfig( # ============================================================================== # VALIDATION LOGIC (from utils/validation.py) # ============================================================================== - def _parse_allowed_nodes_from_prompt(prompt_text: str) -> tuple[Set[str], Set[str]]: """ 从系统提示词中精确解析出允许的行动和条件节点。 @@ -279,7 +276,6 @@ def _validate_pytree_with_schema(pytree_instance: dict, schema: dict) -> bool: # ============================================================================== # VISUALIZATION LOGIC (from utils/visualization.py) # ============================================================================== - def _visualize_pytree(node: Dict, file_path: str): """ 使用Graphviz将Pytree字典可视化,并保存到指定路径。 @@ -290,15 +286,36 @@ def _visualize_pytree(node: Dict, file_path: str): logging.critical("错误:未安装graphviz库。请运行: pip install graphviz") return - dot = Digraph('Pytree', comment='Drone Mission Plan') - dot.attr('node', shape='box', style='rounded,filled', fontname='helvetica') - dot.attr(rankdir='TB', label='Drone Mission Plan', fontsize='20') + # 选择合适的中文字体,避免中文乱码 + def _pick_zh_font(): + sys = platform.system() + if sys == "Windows": + return "Microsoft YaHei" + elif sys == "Darwin": + return "PingFang SC" + else: + return "Noto Sans CJK SC" + fontname = _pick_zh_font() + + dot = Digraph('Pytree', comment='Drone Mission Plan') + dot.attr(rankdir='TB', label='Drone Mission Plan', fontsize='20', fontname=fontname) + dot.attr('node', shape='box', style='rounded,filled', fontname=fontname) + dot.attr('edge', fontname=fontname) + _add_nodes_and_edges(node, dot) try: + # 确保输出目录存在,并避免生成 .png.png + base_path, ext = os.path.splitext(file_path) + render_path = base_path if ext.lower() == '.png' else file_path + + out_dir = os.path.dirname(render_path) + if out_dir and not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + # 保存为 .png 文件,并自动删除源码 .gv 文件 - output_path = dot.render(file_path, format='png', cleanup=True, view=False) + output_path = dot.render(render_path, format='png', cleanup=True, view=False) logging.info("--- 任务树可视化成功 ---") logging.info(f"图形已保存到: {output_path}") except Exception as e: @@ -309,44 +326,96 @@ def _visualize_pytree(node: Dict, file_path: str): def _add_nodes_and_edges(node: dict, dot, parent_id: str | None = None) -> str: """递归辅助函数,用于添加节点和边。""" - # 为每个节点创建一个唯一的ID - current_id = str(id(node)) + # 为每个节点创建一个唯一的ID(加上随机数避免冲突) + import random + import html + + current_id = f"{id(node)}_{random.randint(1000, 9999)}" + + # 准备节点标签(HTML-like,正确换行与转义) + name = html.escape(str(node.get('name', ''))) + ntype = html.escape(str(node.get('type', ''))) + label_parts = [f"{name} ({ntype})"] + + # 格式化参数显示 + params = node.get('params') or {} + if params: + params_lines = [] + for key, value in params.items(): + k = html.escape(str(key)) + if isinstance(value, float): + value_str = f"{value:.2f}".rstrip('0').rstrip('.') + else: + value_str = str(value) + v = html.escape(value_str) + params_lines.append(f"{k}: {v}") + params_text = "
".join(params_lines) + label_parts.append(f"{params_text}") + + node_label = f"<{'
'.join(label_parts)}>" + + # 根据类型设置节点样式和颜色(使用 fillcolor 控制填充色) + node_type = (node.get('type') or '').lower() + shape = 'ellipse' + style = 'filled' + fillcolor = '#e6e6e6' # 默认灰色填充 + border_color = '#666666' # 默认描边色 - # 准备节点标签 - node_label = f"<{node['name']}
({node['type']})" - if node.get('params'): - params_str = json.dumps(node.get('params')) - node_label += f"
params: {params_str}" - node_label += ">" - - # 根据类型设置节点样式 - node_type = node.get('type', '').lower() if node_type == 'action': - dot.node(current_id, label=node_label, shape='box', color="#cde4ff") + shape = 'box' + style = 'rounded,filled' + fillcolor = "#cde4ff" # 浅蓝 elif node_type == 'condition': - dot.node(current_id, label=node_label, shape='diamond', color="#fff2cc") - else: # Sequence, Selector, etc. - dot.node(current_id, label=node_label, shape='ellipse', color='#e6e6e6') - + shape = 'diamond' + style = 'filled' + fillcolor = "#fff2cc" # 浅黄 + elif node_type == 'sequence': + shape = 'ellipse' + style = 'filled' + fillcolor = '#d5e8d4' # 绿色 + elif node_type == 'selector': + shape = 'ellipse' + style = 'filled' + fillcolor = '#ffe6cc' # 橙色 + elif node_type == 'parallel': + shape = 'ellipse' + style = 'filled' + fillcolor = '#e1d5e7' # 紫色 + + dot.node(current_id, label=node_label, shape=shape, style=style, fillcolor=fillcolor, color=border_color) + # 连接父节点 if parent_id: dot.edge(parent_id, current_id) # 递归处理子节点 - last_child_id = current_id - for child in node.get("children", []): - # 对于序列,边是连续的;对于选择器,所有子节点都连接到父节点 - if node_type in ['sequence']: - last_child_id = _add_nodes_and_edges(child, dot, last_child_id) - else: # Selector, Parallel - _add_nodes_and_edges(child, dot, current_id) - + children = node.get("children", []) + if not children: + return current_id + + # 记录所有子节点的ID + child_ids = [] + + # 正确的递归连接:每个子节点都连接到当前节点 + for child in children: + child_id = _add_nodes_and_edges(child, dot, current_id) + child_ids.append(child_id) + + # 子节点同级排列(横向排布,更直观地表现同层) + if len(child_ids) > 1: + with dot.subgraph(name=f"rank_{current_id}") as s: + s.attr(rank='same') + for cid in child_ids: + s.node(cid) + + # 行为树中,所有类型的节点都只是父连子,不需要子节点间的额外连接 + # Sequence、Selector、Parallel 的执行逻辑由行为树引擎处理,不需要在可视化中体现 + return current_id # ============================================================================== # CORE PYTREE GENERATOR CLASS # ============================================================================== - class PyTreeGenerator: def __init__(self): self.base_dir = os.path.dirname(os.path.abspath(__file__)) @@ -355,7 +424,6 @@ class PyTreeGenerator: # Updated output directory for visualizations self.vis_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_visualizations')) os.makedirs(self.vis_dir, exist_ok=True) - self.system_prompt = self._load_prompt("system_prompt.txt") self.orin_ip = os.getenv("ORIN_IP", "localhost") @@ -363,7 +431,7 @@ class PyTreeGenerator: api_key=os.getenv("OPENAI_API_KEY", "sk-no-key-required"), base_url=f"http://{self.orin_ip}:8081/v1" ) - + # --- ChromaDB Client Setup --- vector_store_path = os.path.abspath(os.path.join(self.base_dir, '..', '..', 'tools', 'vector_store')) self.chroma_client = chromadb.PersistentClient(path=vector_store_path) @@ -371,12 +439,11 @@ class PyTreeGenerator: # Explicitly use the remote embedding function for queries embedding_api_url = f"http://{self.orin_ip}:8090/v1/embeddings" embedding_func = RemoteEmbeddingFunction(api_url=embedding_api_url) - self.collection = self.chroma_client.get_collection( name="drone_docs", embedding_function=embedding_func ) - + allowed_actions, allowed_conditions = _parse_allowed_nodes_from_prompt(self.system_prompt) self.schema = _generate_pytree_schema(allowed_actions, allowed_conditions) @@ -423,7 +490,6 @@ class PyTreeGenerator: final_user_prompt += augmentation else: logging.warning("未检索到上下文或检索失败,将使用原始用户提示词。") - for attempt in range(3): logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---") try: @@ -438,7 +504,6 @@ class PyTreeGenerator: ) pytree_str = response.choices[0].message.content pytree_dict = json.loads(pytree_str) - if _validate_pytree_with_schema(pytree_dict, self.schema): logging.info("成功生成并验证了Pytree。") plan_id = str(uuid.uuid4()) @@ -449,15 +514,13 @@ class PyTreeGenerator: vis_path = os.path.join(self.vis_dir, vis_filename) _visualize_pytree(pytree_dict['root'], os.path.splitext(vis_path)[0]) pytree_dict['visualization_url'] = f"/static/{vis_filename}" - return pytree_dict else: logging.warning("生成的Pytree验证失败,正在重试...") - except (OpenAIError, json.JSONDecodeError) as e: logging.error(f"生成Pytree时发生错误: {e}") raise RuntimeError("在3次尝试后,仍未能生成一个有效的Pytree。") # Create a single instance for the application -py_tree_generator = PyTreeGenerator() +py_tree_generator = PyTreeGenerator() \ No newline at end of file