feat:优化节点错误处理

This commit is contained in:
2026-09-29 08:17:58 -07:00
parent 352d144823
commit 6a38a5f15e
8 changed files with 113 additions and 50 deletions
+62 -34
View File
@@ -10,6 +10,12 @@ from app.processor.state import QueryState
from app.prompt.planner import PLANNER_SYSTEM_PROMPT
from app.schemas.screen_plan import ScreenPlan
# 规划输出未通过校验时,带着错误让模型重新规划的最大次数
# TODO 上游节点目前只有这里做了节点内重试,scope_node 和 requirement_node 尚未跟进
# TODO 将来可改为图级回路:规划校验失败 -> 规划版 reflection -> planner,
# 与动作阶段的重试统一由 route_failure 控制,届时可去掉这里的节点内重试
PLAN_MAX_REPAIRS = 1
class PlannerNode(BaseNode):
name = "planner_node"
@@ -26,28 +32,64 @@ class PlannerNode(BaseNode):
# 图保证组件目录已就绪后才进入规划
resources = state["component_resources"]
messages = [
# 系统消息提供稳定的规划规则和输出边界
SystemMessage(content=PLANNER_SYSTEM_PROMPT),
# 用户消息承载本次上下文、需求、范围及组件能力摘要
HumanMessage(
content=json.dumps(
{
"input_context": context.model_dump(mode="json"),
"requirement": requirement.model_dump(mode="json"),
"change_scope": scope.model_dump(mode="json"),
# 仅提供规划需要的组件摘要,省略完整默认配置
"component_resources": resources.to_planner_context(),
},
ensure_ascii=False,
)
),
]
llm_client = get_llm(ScreenPlan)
result = await llm_client.ainvoke(
[
# 系统消息提供稳定的规划规则和输出边界
SystemMessage(content=PLANNER_SYSTEM_PROMPT),
# 用户消息承载本次上下文、需求、范围及组件能力摘要
HumanMessage(
content=json.dumps(
{
"input_context": context.model_dump(mode="json"),
"requirement": requirement.model_dump(mode="json"),
"change_scope": scope.model_dump(mode="json"),
# 仅提供规划需要的组件摘要,省略完整默认配置
"component_resources": resources.to_planner_context(),
},
ensure_ascii=False,
)
),
]
)
plan = result
for attempt in range(PLAN_MAX_REPAIRS + 1):
plan = None
try:
plan = await llm_client.ainvoke(messages)
self._check(plan, state)
break
except ValueError as error:
# 解析失败、引用不存在的目标等可由模型自行纠正,超过次数才失败收尾
if attempt == PLAN_MAX_REPAIRS:
return self.fail_on_invalid_output(
state, error, "未能生成可用的大屏规划,请调整需求后重试"
)
self.logger.warning(f"规划未通过校验,重新规划: {error}")
feedback = f"上一次输出未通过校验:{error}\n请修正后重新输出完整规划。"
if plan is not None:
feedback = f"上一次输出:\n{plan.model_dump_json()}\n\n{feedback}"
messages = [*messages, HumanMessage(content=feedback)]
# 合并需求约束和规划约束,按首次出现顺序去重,保留需求约束在前
plan.constraints = list(
dict.fromkeys([*requirement.constraints, *plan.constraints])
)
# 同样合并原始假设与规划补充的假设,避免重复记录或丢失前置条件
plan.assumptions = list(
dict.fromkeys([*requirement.assumptions, *plan.assumptions])
)
# 保存通过校验的规划,供后续动作生成使用
updated_state["screen_plan"] = plan
# 返回更新后的状态,由图继续处理规划对应的动作
return updated_state
@staticmethod
def _check(plan: ScreenPlan, state: QueryState) -> None:
"""校验规划的组件类型、目标引用和修改范围,不通过时抛出 ValueError"""
requirement = state["requirement"]
scope = state["change_scope"]
context = state["input_context"]
resources = state["component_resources"]
# 校验规划中的物料类型是否真实受支持
supported_types = {component.type for component in resources.components}
@@ -97,20 +139,6 @@ class PlannerNode(BaseNode):
if outside := changed_ids - set(scope.target_ids):
raise ValueError(f"局部规划修改了范围外的组件: {sorted(outside)}")
# 合并需求约束和规划约束,按首次出现顺序去重,保留需求约束在前
plan.constraints = list(
dict.fromkeys([*requirement.constraints, *plan.constraints])
)
# 同样合并原始假设与规划补充的假设,避免重复记录或丢失前置条件
plan.assumptions = list(
dict.fromkeys([*requirement.assumptions, *plan.assumptions])
)
# 保存通过校验的规划,供后续动作生成使用
updated_state["screen_plan"] = plan
# 返回更新后的状态,由图继续处理规划对应的动作
return updated_state
if __name__ == "__main__":
import asyncio