第1675篇:Agent任务规划的质量控制——Plan Validation与执行前检查
第1675篇:Agent任务规划的质量控制——Plan Validation与执行前检查
做了这么多Agent项目,我发现一个规律:大多数Agent任务失败,不是在执行阶段出的问题,而是计划阶段就错了。LLM生成了一个有缺陷的计划,然后Agent忠实地执行了这个缺陷,最终任务失败,或者更糟——静默地完成了错误的任务。
计划质量控制是一个被严重低估的话题。很多工程师把精力都放在了执行层面的重试、错误处理,却忽略了在执行之前做一次严格的计划验证。这篇文章就专门讲这件事。
计划质量问题的典型表现
先举几个我遇到过的真实案例,让你感受一下问题的严重性。
案例1:资源顺序错误
用户让Agent"删除用户A的所有订单,然后删除用户A"。LLM生成的计划是:先删用户A,再删订单。执行时因为有外键约束,订单删除失败,用户A却已经没了。
案例2:参数幻觉
Agent在计划中生成了一个工具调用:send_email(to="manager@company.com", subject="..."),但这个邮箱地址是LLM编造的,根本不存在于系统里。
案例3:遗漏必要步骤
计划里有"修改价格"这个步骤,但没有"通知受影响的客户"步骤,导致价格修改后客户毫无感知。
案例4:计划与实际能力不匹配
LLM生成了一个调用export_to_excel工具的计划步骤,但这个工具在当前环境里根本不可用。
这些问题的共同点是:在执行前做一次仔细的检查,完全可以提前发现并修复。
Plan Validation的整体框架
计划验证分三层:
三层验证的职责:
- 语法验证:计划的格式是否正确,工具调用参数是否符合Schema
- 语义验证:步骤顺序是否合理,依赖关系是否满足,工具是否可用
- 业务规则验证:是否符合业务约束,是否存在风险操作,是否需要审批
语法验证:JSONSchema检查
最基础的验证,确保LLM生成的计划格式正确、参数合法:
@Service
public class SyntacticPlanValidator {
private final ToolRegistryService toolRegistry;
private final JsonSchemaValidator schemaValidator;
public ValidationResult validate(AgentPlan plan) {
List<ValidationError> errors = new ArrayList<>();
for (int i = 0; i < plan.getSteps().size(); i++) {
PlanStep step = plan.getSteps().get(i);
validateStep(step, i, errors);
}
return new ValidationResult(errors.isEmpty(), errors);
}
private void validateStep(PlanStep step, int index,
List<ValidationError> errors) {
// 1. 检查工具是否存在
Optional<ToolRegistration> tool = toolRegistry.findByName(step.getTool());
if (tool.isEmpty()) {
errors.add(ValidationError.error(
String.format("步骤%d: 工具'%s'不存在或不可用", index + 1, step.getTool())
));
return; // 工具不存在,后续参数校验没意义
}
// 2. 检查必填参数是否都提供了
ToolRegistration toolDef = tool.get();
for (ToolParameter param : toolDef.getParameters()) {
if (param.isRequired() && !step.getParams().containsKey(param.getName())) {
// 检查是否用了占位符(表示依赖前一步的输出)
boolean isPlaceholder = step.getDependencies() != null
&& step.getDependencies().containsKey(param.getName());
if (!isPlaceholder) {
errors.add(ValidationError.error(
String.format("步骤%d: 工具'%s'缺少必填参数'%s'",
index + 1, step.getTool(), param.getName())
));
}
}
}
// 3. 检查参数类型
for (Map.Entry<String, Object> entry : step.getParams().entrySet()) {
String paramName = entry.getKey();
Object paramValue = entry.getValue();
ToolParameter paramDef = toolDef.getParameters().stream()
.filter(p -> p.getName().equals(paramName))
.findFirst()
.orElse(null);
if (paramDef == null) {
errors.add(ValidationError.warning(
String.format("步骤%d: 工具'%s'不支持参数'%s',将被忽略",
index + 1, step.getTool(), paramName)
));
continue;
}
// 类型检查
if (!isTypeCompatible(paramValue, paramDef.getType())) {
errors.add(ValidationError.error(
String.format("步骤%d: 参数'%s'类型错误,期望%s,实际%s",
index + 1, paramName,
paramDef.getType(), getJavaType(paramValue))
));
}
// 枚举值检查
if (paramDef.getEnumValues() != null && !paramDef.getEnumValues().isEmpty()) {
if (!paramDef.getEnumValues().contains(String.valueOf(paramValue))) {
errors.add(ValidationError.error(
String.format("步骤%d: 参数'%s'的值'%s'不在允许范围内: %s",
index + 1, paramName, paramValue,
paramDef.getEnumValues())
));
}
}
}
}
}语义验证:依赖关系与步骤顺序
语义验证更复杂,要检查步骤之间的逻辑关系:
@Service
public class SemanticPlanValidator {
/**
* 验证步骤依赖关系:A依赖B,B必须在A之前执行
*/
public ValidationResult validateDependencies(AgentPlan plan) {
List<ValidationError> errors = new ArrayList<>();
List<PlanStep> steps = plan.getSteps();
// 构建依赖图
Map<String, Integer> stepIndexMap = new HashMap<>();
for (int i = 0; i < steps.size(); i++) {
stepIndexMap.put(steps.get(i).getStepId(), i);
}
for (int i = 0; i < steps.size(); i++) {
PlanStep step = steps.get(i);
if (step.getDependencies() == null) continue;
for (String depStepId : step.getDependencies().values()) {
Integer depIndex = stepIndexMap.get(depStepId);
if (depIndex == null) {
errors.add(ValidationError.error(
String.format("步骤%d依赖了不存在的步骤: %s",
i + 1, depStepId)
));
} else if (depIndex >= i) {
// 依赖的步骤在当前步骤之后,顺序错误
errors.add(ValidationError.error(
String.format("步骤%d依赖步骤%d,但步骤%d在步骤%d之后执行(依赖顺序错误)",
i + 1, depIndex + 1, depIndex + 1, i + 1)
));
}
}
}
// 检测循环依赖
if (hasCyclicDependency(steps)) {
errors.add(ValidationError.error("计划中存在循环依赖"));
}
return new ValidationResult(errors.isEmpty(), errors);
}
/**
* 检测常见的逻辑错误(基于规则库)
*/
public List<ValidationWarning> detectLogicIssues(AgentPlan plan) {
List<ValidationWarning> warnings = new ArrayList<>();
List<PlanStep> steps = plan.getSteps();
// 规则1:读取-修改-提交模式检查
// 如果计划里有"查询X"后"修改X",但没有"提交X",警告
Set<String> queriedResources = new HashSet<>();
Set<String> modifiedResources = new HashSet<>();
Set<String> committedResources = new HashSet<>();
for (PlanStep step : steps) {
String tool = step.getTool();
String resource = extractResourceFromTool(tool);
if (isReadOperation(tool)) queriedResources.add(resource);
if (isWriteOperation(tool)) modifiedResources.add(resource);
if (isCommitOperation(tool)) committedResources.add(resource);
}
Set<String> modifiedButNotCommitted = new HashSet<>(modifiedResources);
modifiedButNotCommitted.removeAll(committedResources);
for (String resource : modifiedButNotCommitted) {
warnings.add(new ValidationWarning(
String.format("资源'%s'被修改但没有提交/保存步骤", resource)
));
}
// 规则2:删除前检查依赖
for (int i = 0; i < steps.size(); i++) {
if (isDeleteOperation(steps.get(i).getTool())) {
// 检查是否有后续步骤依赖这个被删除的资源
String deletedResource = extractResourceFromParams(steps.get(i).getParams());
for (int j = i + 1; j < steps.size(); j++) {
if (referencesResource(steps.get(j), deletedResource)) {
warnings.add(new ValidationWarning(
String.format("步骤%d删除了资源'%s',但步骤%d还在使用它",
i + 1, deletedResource, j + 1)
));
}
}
}
}
return warnings;
}
/**
* 检测循环依赖(DFS)
*/
private boolean hasCyclicDependency(List<PlanStep> steps) {
Map<String, List<String>> graph = buildDependencyGraph(steps);
Set<String> visited = new HashSet<>();
Set<String> inStack = new HashSet<>();
for (String stepId : graph.keySet()) {
if (dfsCycleDetect(stepId, graph, visited, inStack)) {
return true;
}
}
return false;
}
}业务规则验证:风险操作识别
这一层根据具体业务定制,重点是识别高风险操作并触发相应的控制流程:
@Service
public class BusinessRulePlanValidator {
// 风险操作配置,可以从配置中心动态加载
private final List<RiskRule> riskRules;
@PostConstruct
public void loadRules() {
riskRules = List.of(
// 规则1:批量删除超过阈值需要人工审批
RiskRule.of("BULK_DELETE",
step -> isDeleteOperation(step.getTool())
&& getBatchSize(step) > 100,
RiskLevel.HIGH,
"批量删除超过100条记录,需要人工审批"),
// 规则2:涉及财务操作需要二次确认
RiskRule.of("FINANCIAL_OPERATION",
step -> FINANCIAL_TOOLS.contains(step.getTool()),
RiskLevel.MEDIUM,
"财务相关操作,请确认参数无误"),
// 规则3:外部API调用需要检查数据合规性
RiskRule.of("EXTERNAL_DATA_TRANSFER",
step -> EXTERNAL_API_TOOLS.contains(step.getTool()),
RiskLevel.MEDIUM,
"向外部传输数据,请确认符合数据合规要求"),
// 规则4:不可逆操作
RiskRule.of("IRREVERSIBLE_OPERATION",
step -> IRREVERSIBLE_TOOLS.contains(step.getTool()),
RiskLevel.HIGH,
"此操作不可逆,一旦执行无法撤销")
);
}
public BusinessRuleValidationResult validate(AgentPlan plan,
String operatorRole) {
List<RiskViolation> violations = new ArrayList<>();
for (PlanStep step : plan.getSteps()) {
for (RiskRule rule : riskRules) {
if (rule.matches(step)) {
violations.add(new RiskViolation(
step.getStepId(),
rule.getRuleId(),
rule.getLevel(),
rule.getDescription()
));
}
}
}
// 根据风险等级决定处理方式
List<RiskViolation> highRiskViolations = violations.stream()
.filter(v -> v.getLevel() == RiskLevel.HIGH)
.collect(Collectors.toList());
List<RiskViolation> mediumRiskViolations = violations.stream()
.filter(v -> v.getLevel() == RiskLevel.MEDIUM)
.collect(Collectors.toList());
if (!highRiskViolations.isEmpty()) {
// 高风险:拦截并要求人工审批
return BusinessRuleValidationResult.requiresApproval(highRiskViolations);
}
if (!mediumRiskViolations.isEmpty()) {
// 中等风险:允许执行但发出警告
return BusinessRuleValidationResult.withWarnings(mediumRiskViolations);
}
return BusinessRuleValidationResult.passed();
}
}计划自动修复:不是拒绝,而是修正
验证失败不一定要直接拒绝,很多情况下可以自动修复:
@Service
public class PlanAutoFixer {
private final LLMClient llmClient;
/**
* 尝试自动修复计划中的问题
*/
public Optional<AgentPlan> tryAutoFix(AgentPlan originalPlan,
List<ValidationError> errors) {
// 只自动修复低风险、可确定的问题
List<ValidationError> autoFixableErrors = errors.stream()
.filter(this::isAutoFixable)
.collect(Collectors.toList());
if (autoFixableErrors.isEmpty()) {
return Optional.empty();
}
// 构建修复指令
String fixInstructions = buildFixInstructions(originalPlan, autoFixableErrors);
// 让LLM重新生成修复后的计划
String fixPrompt = """
以下计划存在一些问题,请根据指出的错误修正计划,只修复列出的问题,不要改动其他部分:
原始计划:
%s
需要修复的问题:
%s
请输出修复后的完整计划JSON:
""".formatted(JSON.toJSONString(originalPlan), fixInstructions);
try {
String fixedPlanJson = llmClient.complete(fixPrompt, "gpt-4o");
AgentPlan fixedPlan = JSON.parseObject(fixedPlanJson, AgentPlan.class);
log.info("计划自动修复成功,修复了{}个问题", autoFixableErrors.size());
return Optional.of(fixedPlan);
} catch (Exception e) {
log.warn("计划自动修复失败", e);
return Optional.empty();
}
}
private boolean isAutoFixable(ValidationError error) {
// 缺少可选参数(可以用默认值填充)
// 参数类型不匹配但可以强转
// 步骤顺序错误(可以重新排序)
return error.getType() == ErrorType.MISSING_OPTIONAL_PARAM
|| error.getType() == ErrorType.TYPE_MISMATCH_COERCIBLE
|| error.getType() == ErrorType.WRONG_ORDER;
}
}预执行检查(Pre-flight Check)
在计划真正开始执行前,还有最后一道检查:确认当前环境满足执行条件。
@Service
public class PreflightChecker {
public PreflightResult check(AgentPlan plan) {
List<PreflightCheck> checks = List.of(
this::checkToolAvailability,
this::checkResourcePermissions,
this::checkRateLimits,
this::checkExternalDependencies
);
List<PreflightFailure> failures = new ArrayList<>();
for (PreflightCheck check : checks) {
try {
List<PreflightFailure> checkResult = check.apply(plan);
failures.addAll(checkResult);
} catch (Exception e) {
failures.add(new PreflightFailure("检查执行异常: " + e.getMessage()));
}
}
return new PreflightResult(failures.isEmpty(), failures);
}
/**
* 检查所有用到的工具当前是否可用(心跳正常、未下线)
*/
private List<PreflightFailure> checkToolAvailability(AgentPlan plan) {
List<PreflightFailure> failures = new ArrayList<>();
Set<String> requiredTools = plan.getSteps().stream()
.map(PlanStep::getTool)
.collect(Collectors.toSet());
for (String toolName : requiredTools) {
Optional<ToolRegistration> tool = toolRegistry.findActive(toolName);
if (tool.isEmpty()) {
failures.add(new PreflightFailure(
PreflightFailureType.TOOL_UNAVAILABLE,
"工具不可用: " + toolName
));
} else {
// 检查最后心跳时间
Duration sinceLastHeartbeat = Duration.between(
tool.get().getLastHeartbeatAt(), LocalDateTime.now()
);
if (sinceLastHeartbeat.toSeconds() > 90) {
failures.add(new PreflightFailure(
PreflightFailureType.TOOL_UNHEALTHY,
"工具心跳超时,可能不健康: " + toolName
));
}
}
}
return failures;
}
/**
* 检查当前用户/Agent对相关资源是否有权限
*/
private List<PreflightFailure> checkResourcePermissions(AgentPlan plan) {
List<PreflightFailure> failures = new ArrayList<>();
String currentUserId = SecurityContext.getCurrentUserId();
for (PlanStep step : plan.getSteps()) {
// 提取步骤操作的资源
String resource = extractResource(step);
String operation = extractOperation(step.getTool());
if (!permissionChecker.hasPermission(currentUserId, resource, operation)) {
failures.add(new PreflightFailure(
PreflightFailureType.PERMISSION_DENIED,
String.format("没有权限执行操作: %s on %s", operation, resource)
));
}
}
return failures;
}
/**
* 检查限流:估算计划执行的API调用量,是否超过限额
*/
private List<PreflightFailure> checkRateLimits(AgentPlan plan) {
List<PreflightFailure> failures = new ArrayList<>();
// 按工具统计调用次数
Map<String, Long> toolCallCounts = plan.getSteps().stream()
.collect(Collectors.groupingBy(PlanStep::getTool, Collectors.counting()));
for (Map.Entry<String, Long> entry : toolCallCounts.entrySet()) {
String toolName = entry.getKey();
long callCount = entry.getValue();
Optional<ToolRegistration> tool = toolRegistry.findActive(toolName);
if (tool.isEmpty()) continue;
int rateLimit = tool.get().getPolicy().getRateLimit();
int currentUsage = rateLimiter.getCurrentUsage(toolName);
if (currentUsage + callCount > rateLimit) {
failures.add(new PreflightFailure(
PreflightFailureType.RATE_LIMIT_EXCEEDED,
String.format("工具'%s'调用次数(%d)超过限额(%d)",
toolName, currentUsage + callCount, rateLimit)
));
}
}
return failures;
}
}把所有验证组合起来
@Service
public class PlanQualityController {
private final SyntacticPlanValidator syntacticValidator;
private final SemanticPlanValidator semanticValidator;
private final BusinessRulePlanValidator businessRuleValidator;
private final PlanAutoFixer autoFixer;
private final PreflightChecker preflightChecker;
public PlanQualityResult assess(AgentPlan plan, ExecutionContext context) {
log.info("开始计划质量检查: planId={}, steps={}",
plan.getPlanId(), plan.getSteps().size());
// 第一层:语法验证
ValidationResult syntactic = syntacticValidator.validate(plan);
if (!syntactic.isPassed()) {
// 尝试自动修复
Optional<AgentPlan> fixedPlan = autoFixer.tryAutoFix(
plan, syntactic.getErrors()
);
if (fixedPlan.isPresent()) {
// 修复成功,用修复后的计划继续验证
plan = fixedPlan.get();
log.info("语法错误自动修复成功");
} else {
return PlanQualityResult.failed("语法验证失败", syntactic.getErrors());
}
}
// 第二层:语义验证
ValidationResult semantic = semanticValidator.validateDependencies(plan);
List<ValidationWarning> semanticWarnings = semanticValidator.detectLogicIssues(plan);
if (!semantic.isPassed()) {
return PlanQualityResult.failed("语义验证失败", semantic.getErrors());
}
// 第三层:业务规则验证
BusinessRuleValidationResult businessResult = businessRuleValidator.validate(
plan, context.getOperatorRole()
);
if (businessResult.requiresApproval()) {
// 提交人工审批
String approvalId = approvalService.submitForApproval(
plan, businessResult.getViolations(), context
);
return PlanQualityResult.pendingApproval(approvalId);
}
// 预执行检查
PreflightResult preflight = preflightChecker.check(plan);
if (!preflight.isPassed()) {
return PlanQualityResult.preflightFailed(preflight.getFailures());
}
// 全部通过
log.info("计划质量检查通过: planId={}", plan.getPlanId());
return PlanQualityResult.passed(plan, semanticWarnings,
businessResult.getWarnings());
}
}计划验证指标与改进
建立计划质量的量化指标,用数据驱动改进:
@Service
public class PlanQualityMetricsCollector {
private final MeterRegistry meterRegistry;
public void recordValidationResult(AgentPlan plan, PlanQualityResult result) {
// 记录验证通过率
meterRegistry.counter("plan.validation",
"result", result.getStatus().name(),
"failure_type", result.getFailureType() != null
? result.getFailureType().name() : "NONE"
).increment();
// 记录自动修复率
if (result.wasAutoFixed()) {
meterRegistry.counter("plan.auto_fix.success").increment();
}
// 记录计划步骤数分布
meterRegistry.summary("plan.step_count")
.record(plan.getSteps().size());
// 记录验证耗时
meterRegistry.timer("plan.validation.duration")
.record(result.getValidationDuration());
}
/**
* 定期分析失败模式,发现高频问题
*/
@Scheduled(cron = "0 0 9 * * MON") // 每周一上午9点
public void analyzeFailurePatterns() {
List<ValidationFailureRecord> recentFailures =
failureRepository.findLastWeek();
// 按错误类型统计
Map<String, Long> failureByType = recentFailures.stream()
.collect(Collectors.groupingBy(r -> r.getErrorType(), Collectors.counting()));
// 发送周报
reportService.sendWeeklyReport("计划验证失败分析", failureByType);
// 如果某类错误占比超过30%,触发告警
long total = recentFailures.size();
for (Map.Entry<String, Long> entry : failureByType.entrySet()) {
if (entry.getValue() * 100.0 / total > 30) {
alertService.sendAlert(
"高频计划验证失败: " + entry.getKey()
+ " 占比 " + entry.getValue() * 100 / total + "%"
);
}
}
}
}我踩过的坑
坑1:验证太严格导致Agent完全动不了。
最开始我们的验证规则写得很严格,几乎所有LLM生成的计划都被拦截了。后来意识到要区分"硬性错误"(必须修复才能执行)和"警告"(可以继续执行但需要注意),两类问题的处理方式完全不同。
坑2:自动修复引入了新问题。
让LLM修复计划时,有时候它"修复"了一个问题但引入了另一个问题。后来改成修复后必须重新跑一遍验证,最多修复3次,超过次数直接拒绝并要求重新规划。
坑3:预执行检查的时效性。
预执行检查在t0时刻通过了,但等真正开始执行时(t1时刻),工具状态可能已经变了(心跳超时、限流重置等)。这个问题没有完美解法,我们的处理是把预检结果的有效期设置得比较短(30秒),超时后重新检查。
坑4:业务规则维护成本高。
规则写死在代码里,每次业务变化都要改代码部署。后来把规则提取到配置中心,支持动态加载,运营人员可以自己配置规则,不需要开发介入。
计划质量控制是一个"前期投入,后期受益"的事情。建立起这套框架后,我们Agent任务的成功率从70%出头提升到了90%以上,而且大量问题在执行前就被发现,避免了副作用。
