欢迎光临
我们一直在努力

Agent Planner输出不是合法JSON怎么办?Schema约束、提取、校验与自动修复

一、为什么Planner比普通问答更怕格式错误

普通问答多一个标点通常没有影响。

Planner输出却要被程序直接执行:

{
"steps": [
{
"id": "S1",
"task": "查询订单",
"dependencies": []
}
]
}

如果出现以下问题,执行器就可能失败:

  • 输出Markdown代码块;
  • JSON前后有解释文字;
  • 使用单引号;
  • 字段名称变化;
  • steps不是数组;
  • 步骤ID重复;
  • 依赖不存在;
  • 循环依赖;
  • 工具名称不存在;
  • 步骤数量失控。

所以Planner必须经过:

格式解析
→ Schema校验
→ 语义校验
→ 图结构校验
→ 安全校验

二、先定义稳定的数据模型

使用Pydantic 2:

from pydantic import BaseModel, Field
from typing import Literal

class PlanStep(BaseModel):
id: str = Field(pattern=r"^S[1-9][0-9]*$")
title: str = Field(min_length=1, max_length=80)
description: str = Field(min_length=1, max_length=500)
dependencies: list[str] = Field(default_factory=list)
tool_hint: str | None = None
expected_output: str = Field(min_length=1, max_length=300)
risk_level: Literal["low", "medium", "high"] = "low"

class TaskPlan(BaseModel):
objective: str = Field(min_length=1, max_length=500)
assumptions: list[str] = Field(default_factory=list)
steps: list[PlanStep] = Field(min_length=1, max_length=20)
completion_criteria: list[str] = Field(min_length=1)

Schema本身就限制:

  • ID格式;
  • 字段长度;
  • 步骤数量;
  • 风险等级枚举;
  • 必填字段。

三、Prompt中给出严格输出协议

你是任务规划器。

只输出一个JSON对象,不要输出Markdown代码块、解释、前言或结尾。

输出必须符合以下规则:
1. steps数量为1到20。
2. 步骤ID使用S1、S2、S3格式。
3. dependencies只能引用已经存在的步骤ID。
4. 不允许循环依赖。
5. 每个步骤必须可以独立判断是否完成。
6. 高风险动作标记risk_level=high。
7. 不得生成未注册工具名称。

Prompt可以降低错误率,但不能替代程序校验。

四、提取JSON对象

模型可能返回:

以下是计划:
```json
{…}

可以先去除代码围栏:

```python
import re

def strip_markdown_fence(text: str) -> str:
text = text.strip()

fence = re.fullmatch(
r"```(?:json)?\\s*(.*?)\\s*```",
text,
flags=re.DOTALL | re.IGNORECASE
)

if fence:
return fence.group(1).strip()

return text

如果前后还有文字,可以提取第一个完整JSON对象。

推荐使用字符状态机,而不是简单正则:

def extract_first_json_object(text: str) > str:
start = text.find("{")
if start < 0:
raise ValueError("响应中没有JSON对象")

depth = 0
in_string = False
escape = False

for index in range(start, len(text)):
char = text[index]

if escape:
escape = False
continue

if char == "\\\\" and in_string:
escape = True
continue

if char == '"':
in_string = not in_string
continue

if in_string:
continue

if char == "{":
depth += 1
elif char == "}":
depth -= 1

if depth == 0:
return text[start:index + 1]

raise ValueError("JSON对象不完整")

五、执行JSON和Pydantic校验

import json
from pydantic import ValidationError

def parse_plan(raw_text: str) > TaskPlan:
clean = strip_markdown_fence(raw_text)
json_text = extract_first_json_object(clean)

try:
data = json.loads(json_text)
except json.JSONDecodeError as exc:
raise ValueError(
f"JSON解析失败:line={exc.lineno}, col={exc.colno}, msg={exc.msg}"
) from exc

try:
return TaskPlan.model_validate(data)
except ValidationError as exc:
raise ValueError(
f"计划Schema校验失败:{exc.errors()}"
) from exc

不要自动把单引号替换成双引号,因为:

  • 字符串内部可能包含单引号;
  • 错误修复可能改变原意;
  • 容易掩盖模型输出质量问题。

六、校验步骤ID与依赖

def validate_dependencies(plan: TaskPlan) > None:
step_ids = [step.id for step in plan.steps]

if len(step_ids) != len(set(step_ids)):
raise ValueError("存在重复步骤ID")

known = set(step_ids)

for step in plan.steps:
if step.id in step.dependencies:
raise ValueError(
f"步骤{step.id}不能依赖自己"
)

unknown = set(step.dependencies) known

if unknown:
raise ValueError(
f"步骤{step.id}引用未知依赖:{sorted(unknown)}"
)

还可以要求依赖只能指向前面的步骤:

def validate_dependency_order(plan: TaskPlan) > None:
positions = {
step.id: index
for index, step in enumerate(plan.steps)
}

for step in plan.steps:
for dependency in step.dependencies:
if positions[dependency] >= positions[step.id]:
raise ValueError(
f"{step.id}依赖了未提前定义的{dependency}"
)

如果执行器支持真正DAG,不强制顺序也可以,但必须做循环检测。

七、检测循环依赖

使用Kahn算法:

from collections import defaultdict, deque

def validate_acyclic(plan: TaskPlan) > None:
graph: dict[str, list[str]] = defaultdict(list)
indegree: dict[str, int] = {
step.id: 0 for step in plan.steps
}

for step in plan.steps:
for dependency in step.dependencies:
graph[dependency].append(step.id)
indegree[step.id] += 1

queue = deque(
step_id
for step_id, degree in indegree.items()
if degree == 0
)

visited = 0

while queue:
current = queue.popleft()
visited += 1

for next_step in graph[current]:
indegree[next_step] -= 1

if indegree[next_step] == 0:
queue.append(next_step)

if visited != len(plan.steps):
raise ValueError("计划中存在循环依赖")

典型错误:

S1依赖S3
S2依赖S1
S3依赖S2

格式完全合法,但永远无法开始执行。

八、校验工具白名单

REGISTERED_TOOLS = {
"search_web",
"query_order",
"query_inventory",
"generate_report"
}

def validate_tools(plan: TaskPlan) > None:
for step in plan.steps:
if (
step.tool_hint is not None
and step.tool_hint not in REGISTERED_TOOLS
):
raise ValueError(
f"步骤{step.id}使用未注册工具:{step.tool_hint}"
)

Planner不能凭空创造不存在的工具,执行器必须只接受已注册工具。

九、把错误反馈给模型修复

首次失败后,不要简单重复原Prompt。

应提供明确校验错误:

def build_repair_prompt(
original_request: str,
invalid_output: str,
error_message: str
) > str:
return f"""
你上一次生成的任务计划无法通过校验。

用户任务:
{original_request}

校验错误:
{error_message}

原始输出:
{invalid_output}

请修复计划。
只输出完整JSON对象,不要输出解释或Markdown代码块。
"""

修复最多尝试1—2次:

def generate_valid_plan(client, user_request: str) > TaskPlan:
raw = client.complete(build_plan_prompt(user_request))

for attempt in range(2):
try:
plan = parse_plan(raw)
validate_dependencies(plan)
validate_acyclic(plan)
validate_tools(plan)
return plan
except ValueError as exc:
if attempt == 1:
raise

raw = client.complete(
build_repair_prompt(
user_request,
raw,
str(exc)
)
)

raise RuntimeError("无法生成有效计划")

十、不要无限修复

如果连续失败,应降级:

1. 使用更强模型重试一次
2. 切换到简单单步计划
3. 请求用户补充信息
4. 转人工处理

建议记录:

planner_model
attempt_count
validation_error
raw_output_hash
repair_success

十一、结构合法还不够

以下计划Schema正确,但没有执行价值:

{
"steps": [
{
"id": "S1",
"title": "完成任务",
"description": "把任务完成",
"dependencies": [],
"expected_output": "任务完成",
"risk_level": "low"
}
]
}

需要语义规则:

  • 步骤必须可执行;
  • 不能简单重复用户目标;
  • expected_output必须可验证;
  • 每一步粒度适中;
  • 不得包含未提供的敏感信息;
  • 高风险步骤必须标记。

可以增加Reviewer模型,但最终安全边界仍由规则控制。

十二、生产级处理链

模型生成
→ 去除围栏
→ 提取JSON
→ JSON解析
→ Pydantic校验
→ ID与依赖校验
→ DAG循环检测
→ 工具白名单
→ 风险规则
→ 自动修复一次
→ 降级或人工

总结

Planner输出不是合法JSON,不能只靠一句“请严格输出JSON”解决。

可靠方案必须同时具备:

明确Schema
+解析器
+类型校验
+图结构校验
+工具白名单
+有限修复
+失败降级

只有经过校验的计划,才能进入执行器。

赞(0)
未经允许不得转载:171主机测评 » Agent Planner输出不是合法JSON怎么办?Schema约束、提取、校验与自动修复
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址