feat:优化节点错误处理
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -15,6 +15,15 @@ class RequirementNode(BaseNode):
|
||||
name = "requirement_node"
|
||||
|
||||
async def process(self, state: QueryState) -> QueryState:
|
||||
try:
|
||||
return await self._generate(state)
|
||||
except ValueError as error:
|
||||
# 结构化输出解析失败或引用校验不通过(含 ValidationError、OutputParserException)
|
||||
return self.fail_on_invalid_output(
|
||||
state, error, "未能可靠理解你的需求,请换个说法再试"
|
||||
)
|
||||
|
||||
async def _generate(self, state: QueryState) -> QueryState:
|
||||
# 浅拷贝状态
|
||||
updated_state: QueryState = state.copy()
|
||||
# 获取输入节点已校验的上下文,包含用户需求、当前大屏和澄清记录
|
||||
|
||||
@@ -16,6 +16,15 @@ class ScopeNode(BaseNode):
|
||||
name = "scope_node"
|
||||
|
||||
async def process(self, state: QueryState) -> QueryState:
|
||||
try:
|
||||
return await self._generate(state)
|
||||
except ValueError as error:
|
||||
# 结构化输出解析失败或引用校验不通过(含 ValidationError、OutputParserException)
|
||||
return self.fail_on_invalid_output(
|
||||
state, error, "未能确定需要修改的范围,请描述得更具体一些"
|
||||
)
|
||||
|
||||
async def _generate(self, state: QueryState) -> QueryState:
|
||||
# 浅拷贝状态
|
||||
updated_state: QueryState = state.copy()
|
||||
# 获取需求节点提取的结构化需求,作为范围判断的依据
|
||||
|
||||
Reference in New Issue
Block a user