7.3 输出质量门控与过滤
最后更新于
from dataclasses import dataclass
from enum import Enum
from typing import Optional, List
class ValidationResult(Enum):
PASS = "pass"
FAIL = "fail"
WARN = "warn"
@dataclass
class ValidationReport:
result: ValidationResult
errors: List[str]
warnings: List[str]
suggestion: Optional[str] = None
class FormatValidator:
"""基础格式验证"""
@staticmethod
def validate_tool_use_block(tool_use) -> ValidationReport:
"""验证工具调用块的结构完整性"""
errors = []
warnings = []
# 检查必填字段
if not tool_use.id:
errors.append("tool_use.id 不能为空")
if not tool_use.name:
errors.append("tool_use.name 不能为空")
if tool_use.input is None:
errors.append("tool_use.input 不能为空")
# 检查 ID 格式(Claude 的 tool_use.id 以 "toolu_" 开头)
if tool_use.id and not tool_use.id.startswith("toolu_"):
warnings.append(
f"非标准 tool_use.id: {tool_use.id}"
)
# 检查名称格式(应为 snake_case)
if tool_use.name and not FormatValidator._is_valid_name(tool_use.name):
warnings.append(
f"工具名格式非标准: {tool_use.name}"
)
if errors:
return ValidationReport(
result=ValidationResult.FAIL,
errors=errors,
warnings=warnings,
suggestion="检查 API 响应是否正确解析"
)
if warnings:
return ValidationReport(
result=ValidationResult.WARN,
errors=[],
warnings=warnings
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
@staticmethod
def _is_valid_name(name: str) -> bool:
"""检查工具名是否符合 snake_case"""
return name.islower() and name.replace("_", "").isalnum()class ToolRegistry:
"""工具注册表与存在性检查"""
def __init__(self):
self.tools = {}
def register(self, name: str, handler, schema):
"""注册工具"""
self.tools[name] = {
"handler": handler,
"schema": schema,
"enabled": True
}
def is_tool_available(self, name: str) -> bool:
"""检查工具是否可用"""
if name not in self.tools:
return False
return self.tools[name]["enabled"]
def get_tool_schema(self, name: str):
"""获取工具的参数 schema"""
if name not in self.tools:
return None
return self.tools[name]["schema"]
class ExistenceValidator:
"""工具存在性验证"""
def __init__(self, registry: ToolRegistry):
self.registry = registry
def validate(self, tool_name: str) -> ValidationReport:
"""检查工具是否存在且可用"""
errors = []
suggestions = []
if tool_name not in self.registry.tools:
errors.append(f"工具 '{tool_name}' 未注册")
# 生成相似工具建议
similar = self._find_similar_tools(tool_name)
if similar:
suggestions.append(
f"您是想用 '{similar[0]}' 吗?"
)
elif not self.registry.is_tool_available(tool_name):
errors.append(f"工具 '{tool_name}' 已禁用")
if errors:
return ValidationReport(
result=ValidationResult.FAIL,
errors=errors,
warnings=[],
suggestion=suggestions[0] if suggestions else None
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def _find_similar_tools(self, name: str) -> List[str]:
"""使用编辑距离找相似工具名"""
from difflib import get_close_matches
available = [n for n in self.registry.tools.keys()
if self.registry.is_tool_available(n)]
return get_close_matches(name, available, n=1, cutoff=0.6)from pydantic import BaseModel, ValidationError, field_validator
from typing import Optional, Any
class FileReadInput(BaseModel):
"""文件读取工具的输入"""
file_path: str
encoding: str = "utf-8"
max_lines: Optional[int] = None
@field_validator("file_path")
@classmethod
def validate_file_path(cls, v):
if not isinstance(v, str) or len(v) == 0:
raise ValueError("file_path 必须是非空字符串")
return v
@field_validator("max_lines")
@classmethod
def validate_max_lines(cls, v):
if v is not None and (v < 1 or v > 100000):
raise ValueError("max_lines 必须在 1-100000 之间")
return v
class ParameterValidator:
"""参数验证器"""
def __init__(self, registry: ToolRegistry):
self.registry = registry
def validate(self, tool_name: str, parameters: dict) -> ValidationReport:
"""验证工具参数"""
schema = self.registry.get_tool_schema(tool_name)
if schema is None:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"无法找到工具 {tool_name} 的 schema"],
warnings=[]
)
errors = []
warnings = []
try:
# 使用 Pydantic 验证参数
validated = schema(**parameters)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
except ValidationError as e:
for error in e.errors():
field = ".".join(str(x) for x in error["loc"])
msg = error["msg"]
errors.append(f"参数 {field}: {msg}")
if errors:
return ValidationReport(
result=ValidationResult.FAIL,
errors=errors,
warnings=warnings,
suggestion="请检查工具的参数定义"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=warnings
)class BusinessLogicValidator:
"""业务逻辑验证"""
def __init__(self):
self.config = {
"max_api_calls_per_minute": 100,
"max_file_size_mb": 10,
"allowed_domains": ["example.com", "api.service.com"],
}
def validate_api_call_frequency(self, tool_name: str,
call_history: List[float]) -> ValidationReport:
"""检查 API 调用频率是否超过限制"""
import time
now = time.time()
# 计算最近 1 分钟的调用数
recent_calls = [t for t in call_history if now - t < 60]
limit = self.config["max_api_calls_per_minute"]
if len(recent_calls) >= limit:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"工具 {tool_name} 在 1 分钟内调用已达 {limit} 次限制"],
warnings=[],
suggestion="请稍后再试"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def validate_file_access(self, file_path: str) -> ValidationReport:
"""检查文件访问权限和大小"""
import os
errors = []
# 检查文件存在性
if not os.path.exists(file_path):
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"文件不存在: {file_path}"],
warnings=[]
)
# 检查文件大小
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
max_size = self.config["max_file_size_mb"]
if file_size_mb > max_size:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"文件大小 {file_size_mb:.2f}MB 超过限制 {max_size}MB"],
warnings=[]
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def validate_http_request(self, url: str) -> ValidationReport:
"""检查 HTTP 请求的目标合法性"""
from urllib.parse import urlparse
parsed = urlparse(url)
domain = parsed.netloc
if domain not in self.config["allowed_domains"]:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"不允许访问域名: {domain}"],
warnings=[],
suggestion=f"允许的域名: {self.config['allowed_domains']}"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)from enum import Enum
class Permission(Enum):
READ = "read"
WRITE = "write"
DELETE = "delete"
EXECUTE = "execute"
class SecurityValidator:
"""安全与权限检查"""
def __init__(self, user_permissions: set):
self.user_permissions = user_permissions
def validate_tool_permission(self, tool_name: str) -> ValidationReport:
"""检查用户是否有权调用该工具"""
# 定义工具所需权限
tool_permissions = {
"read_file": {Permission.READ},
"write_file": {Permission.WRITE},
"delete_file": {Permission.DELETE},
"execute_command": {Permission.EXECUTE},
}
if tool_name not in tool_permissions:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"未知工具: {tool_name}"],
warnings=[]
)
required = tool_permissions[tool_name]
if not required.issubset(self.user_permissions):
missing = required - self.user_permissions
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"缺少权限: {', '.join(p.value for p in missing)}"],
warnings=[]
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)
def validate_parameter_injection(self, parameters: dict) -> ValidationReport:
"""检查参数是否包含注入攻击"""
dangerous_patterns = [
"rm -rf",
"DROP TABLE",
"<script>",
"${jndi:",
]
for key, value in parameters.items():
if isinstance(value, str):
for pattern in dangerous_patterns:
if pattern in value:
return ValidationReport(
result=ValidationResult.FAIL,
errors=[f"参数 {key} 包含危险模式: {pattern}"],
warnings=[],
suggestion="检查参数是否被恶意注入"
)
return ValidationReport(
result=ValidationResult.PASS,
errors=[],
warnings=[]
)class QualityGate:
"""完整的质量门控引擎"""
def __init__(self, registry: ToolRegistry, user_permissions: set):
self.format_validator = FormatValidator()
self.existence_validator = ExistenceValidator(registry)
self.parameter_validator = ParameterValidator(registry)
self.business_validator = BusinessLogicValidator()
self.security_validator = SecurityValidator(user_permissions)
def validate_tool_call(self, tool_call, call_history: List[float] = None):
"""执行完整的质量门控"""
results = []
# 门控 1: 格式检查
r1 = self.format_validator.validate_tool_use_block(tool_call)
results.append(("格式检查", r1))
if r1.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 2: 工具存在性
r2 = self.existence_validator.validate(tool_call.name)
results.append(("工具存在性", r2))
if r2.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 3: 参数验证
r3 = self.parameter_validator.validate(
tool_call.name, tool_call.input
)
results.append(("参数验证", r3))
if r3.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 4: 业务逻辑(对文件类参数执行业务规则)
file_path = (tool_call.input or {}).get("file_path")
if file_path:
r4 = self.business_validator.validate_file_access(file_path)
results.append(("业务逻辑", r4))
if r4.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 5: 权限检查
r5 = self.security_validator.validate_tool_permission(tool_call.name)
results.append(("权限检查", r5))
if r5.result == ValidationResult.FAIL:
return self._report_failure(results)
# 门控 6: 参数注入检测
r6 = self.security_validator.validate_parameter_injection(
tool_call.input or {}
)
results.append(("注入检测", r6))
if r6.result == ValidationResult.FAIL:
return self._report_failure(results)
return {
"passed": True,
"details": results
}
def _report_failure(self, results):
return {
"passed": False,
"details": results
}class PGEAgent:
"""规划-生成-评估三角色分离"""
def __init__(self, planner_model, generator_model, evaluator_model):
self.planner = planner_model # 高推理预算
self.generator = generator_model # 优化吞吐量
self.evaluator = evaluator_model # 独立验证
async def execute(self, user_request: str) -> ExecutionResult:
"""执行PGE流程"""
# 第一步:规划
print("[Phase 1] Planning...")
plan = await self.planner.analyze_and_plan(user_request)
print(f"Plan: {plan.steps}")
# 第二步:生成
print("[Phase 2] Generating...")
output = await self.generator.execute_plan(plan)
# 第三步:评估 - 关键:使用独立的评估者
print("[Phase 3] Evaluating...")
evaluation = await self.evaluate_output(
output=output,
plan=plan,
evaluator=self.evaluator # 独立的模型
)
if evaluation.passed:
return output
# 失败时的重试逻辑
if evaluation.retry_generator:
# 让生成者修正(基于评估反馈)
output_v2 = await self.generator.refine(
original_output=output,
feedback=evaluation.feedback,
plan=plan
)
evaluation_v2 = await self.evaluate_output(
output=output_v2,
plan=plan,
evaluator=self.evaluator
)
if evaluation_v2.passed:
return output_v2
if evaluation.retry_planner:
# 让规划者重新规划(当多次生成都失败时)
plan_v2 = await self.planner.replan(
user_request=user_request,
failure_reason=evaluation.failure_analysis
)
# 重新执行生成
return await self.execute_with_plan(plan_v2)
raise ExecutionError(f"Failed after retries: {evaluation.feedback}")
async def execute_with_plan(self, plan) -> ExecutionResult:
"""跳过规划阶段,按给定计划执行生成与评估(重新规划后的入口)"""
output = await self.generator.execute_plan(plan)
evaluation = await self.evaluate_output(
output=output, plan=plan, evaluator=self.evaluator
)
if evaluation.passed:
return output
raise ExecutionError(f"Replanned execution failed: {evaluation.feedback}")
async def evaluate_output(
self,
output: str,
plan: Plan,
evaluator: Model
) -> EvaluationResult:
"""独立评估输出"""
# 构造评估提示 - 这是关键:评估者不应该看到生成过程
evaluation_prompt = f"""
Please evaluate the following output against the original requirements.
Original Requirements:
{plan.requirements}
Verification Criteria:
{plan.verification_criteria}
Output to Evaluate:
{output}
Answer:
1. Does the output meet ALL requirements? (yes/no)
2. Are there any errors or issues?
3. If failed, what specific improvements are needed?
4. Confidence level: (high/medium/low)
"""
evaluation_text = await evaluator.infer(evaluation_prompt)
return self._parse_evaluation(evaluation_text, plan)
def _parse_evaluation(self, eval_text: str, plan: Plan) -> EvaluationResult:
"""解析评估结果"""
# 简化实现:检查评估是否包含肯定
passed = "yes" in eval_text.lower()
# 判断是重试生成还是重新规划
retry_generator = "small" in eval_text.lower() or "adjust" in eval_text.lower()
retry_planner = "fundamental" in eval_text.lower() or "rethink" in eval_text.lower()
return EvaluationResult(
passed=passed,
retry_generator=retry_generator,
retry_planner=retry_planner,
feedback=eval_text
)# 配置示例
# 注意:Opus 4.7 起已移除 temperature/top_p/top_k,传入会返回 400;
# 推理深度改用 adaptive thinking + effort 控制。Sonnet 4.6 仍接受采样参数。
PLANNER_CONFIG = {
"model": "claude-opus-4-7", # 最强模型
"adaptive_thinking": True, # 高推理预算
"effort": "high", # 取代 temperature 的确定性/深度旋钮
}
GENERATOR_CONFIG = {
"model": "claude-sonnet-4-6", # 平衡成本/性能
"adaptive_thinking": False, # 轻量级,专注执行
"temperature": 0.1, # 4.6 仍支持采样参数
}
EVALUATOR_CONFIG = {
"model": "claude-opus-4-7", # 同样强大的模型
# 但使用完全不同的上下文和提示
"adaptive_thinking": True,
"effort": "high", # 同上:4.7 不接受 temperature
}class ObjectiveEvaluator:
"""客观评估器"""
def __init__(self, evaluator_model):
self.evaluator = evaluator_model
async def evaluate(self, output: str, criteria: str) -> bool:
"""评估输出是否符合标准"""
# 关键:评估提示必须是通用的,不提及"生成器"
# 而是直接针对需求和输出
prompt = f"""
Review this output against the criteria:
Criteria:
{criteria}
Output:
{output}
Does it meet the criteria? List any gaps or issues.
"""
# 注意:不包含任何关于"生成过程"的信息
evaluation = await self.evaluator.infer(prompt)
return self._judge_pass_fail(evaluation)
def _judge_pass_fail(self, eval_text: str) -> bool:
"""判断是否通过 - 使用保守的标准"""
# 如果有任何怀疑,就认为失败
has_issues = len([l for l in eval_text.split('\n') if l.strip()]) > 1
return not has_issues
# 警告: LLM-as-Judge 偏差风险
# 评估器可能存在以下隐性偏差:
# 1. 位置偏差: 更倾向评估靠前或靠后的内容
# 2. 长度偏好: 倾向偏好较长或较短的输出
# 3. Self-Preference: 倾向支持与其训练数据相似的风格
# 建议使用多个独立评估者或人工抽样验证来规避这些风险1. 规划者:分析代码,生成审查清单
- 安全检查点
- 性能考量
- 可维护性标准
2. 生成者:执行审查
- 逐行检查
- 查找问题
- 生成初稿反馈
3. 评估者:独立验证
- 反馈是否涵盖了所有清单项
- 是否有遗漏的严重问题
- 反馈是否具体可行