3. 实现工具调用参数校验

实现工具调用参数校验 学习卡片

任务目标

在AI Agent Harness中,当大模型请求调用工具时,传入的参数可能不符合工具定义(类型错误、缺少必填字段、多余字段等)。本案例将集成JSON Schema校验器,在工具执行前拦截非法参数,返回结构化错误信息,确保只有符合工具参数定义的调用才会被真正执行。

核心实现

import json
from typing import Any, Callable, Dict
from jsonschema import Draft7Validator, ValidationError

class Tool:
    """工具定义:包含名称、参数JSON Schema及执行函数"""
    def __init__(self, name: str, param_schema: Dict[str, Any], handler: Callable[..., Any]):
        self.name = name
        self.param_schema = param_schema
        self.handler = handler
        self._validator = Draft7Validator(param_schema)

    def validate_params(self, params: Dict[str, Any]) -> None:
        """校验参数,不通过时抛出带详细信息的ValidationError"""
        errors = sorted(self._validator.iter_errors(params), key=lambda e: list(e.path))
        if errors:
            details = []
            for err in errors:
                loc = "/".join(str(p) for p in err.path) or "(root)"
                details.append(f"{loc}: {err.message}")
            raise ValidationError("; ".join(details))

    def invoke(self, params: Dict[str, Any]) -> Any:
        """先校验后执行"""
        self.validate_params(params)
        return self.handler(**params)

class Harness:
    """Agent Harness:管理工具注册与调用"""
    def __init__(self):
        self._tools: Dict[str, Tool] = {}

    def register_tool(self, tool: Tool) -> None:
        self._tools[tool.name] = tool

    def call_tool(self, name: str, params: Dict[str, Any]) -> Dict[str, Any]:
        """供LLM调用的入口,返回统一格式结果"""
        tool = self._tools.get(name)
        if not tool:
            return {"success": False, "error": f"工具 '{name}' 不存在"}

        try:
            result = tool.invoke(params)
            return {"success": True, "result": result}
        except ValidationError as e:
            return {"success": False, "error": f"参数校验失败: {e.message}"}
        except Exception as e:
            return {"success": False, "error": f"执行异常: {str(e)}"}

# ---------- 最小可复现示例 ----------
def get_weather(city: str, days: int = 1):
    """模拟天气查询工具"""
    return f"{city}未来{days}天天气晴朗"

def send_email(to: str, subject: str, body: str):
    """模拟邮件发送工具"""
    return f"邮件已发送至{to},主题:{subject}"

# 定义工具参数Schema
weather_schema = {
    "type": "object",
    "properties": {
        "city": {"type": "string", "minLength": 1},
        "days": {"type": "integer", "minimum": 1, "maximum": 7}
    },
    "required": ["city"],
    "additionalProperties": False
}

email_schema = {
    "type": "object",
    "properties": {
        "to": {"type": "string", "format": "email"},
        "subject": {"type": "string", "minLength": 1},
        "body": {"type": "string", "minLength": 1}
    },
    "required": ["to", "subject", "body"],
    "additionalProperties": False
}

# 注册工具
harness = Harness()
harness.register_tool(Tool("get_weather", weather_schema, get_weather))
harness.register_tool(Tool("send_email", email_schema, send_email))

# 模拟LLM发起的工具调用
test_calls = [
    ("get_weather", {"city": "北京", "days": 3}),       # 合法
    ("get_weather", {"city": "上海", "days": 0}),       # days小于最小值
    ("get_weather", {"city": "广州", "extra": "x"}),    # 多余字段
    ("get_weather", {"days": 2}),                       # 缺少必填city
    ("send_email", {"to": "a@b.com", "subject": "hi", "body": "hello"}),  # 合法
    ("send_email", {"to": "not-an-email", "subject": "hi", "body": "hello"}),  # 邮箱格式错误
]

if __name__ == "__main__":
    for tool_name, params in test_calls:
        resp = harness.call_tool(tool_name, params)
        status = "✓" if resp["success"] else "✗"
        print(f"{status} {tool_name}({json.dumps(params, ensure_ascii=False)})")
        if resp["success"]:
            print(f"  → {resp['result']}")
        else:
            print(f"  → {resp['error']}")

运行与验证

  1. 安装依赖:pip install jsonschema
  2. 运行代码:将上述代码保存为 tool_validation.py,执行 python tool_validation.py
  3. 预期输出(与代码逻辑严格对应):
✓ get_weather({"city": "北京", "days": 3})
  → 北京未来3天天气晴朗
✗ get_weather({"city": "上海", "days": 0})
  → 参数校验失败: days: 0 is less than the minimum of 1
✗ get_weather({"city": "广州", "extra": "x"})
  → 参数校验失败: extra: Additional properties are not allowed ('extra' was unexpected)
✗ get_weather({"days": 2})
  → 参数校验失败: city: 'city' is a required property
✓ send_email({"to": "a@b.com", "subject": "hi", "body": "hello"})
  → 邮件已发送至a@b.com,主题:hi
✗ send_email({"to": "not-an-email", "subject": "hi", "body": "hello"})
  → 参数校验失败: to: 'not-an-email' is not a 'email'

通过判定标准:6条测试中,3条合法调用返回success: true并正确执行,3条非法调用返回success: false且错误信息准确指出具体字段与原因。若输出与上述一致,说明JSON Schema校验已正确集成到Harness的工具调用链路中。

关键说明

  • 校验时机:校验发生在Tool.invoke()内部,即工具执行函数被调用之前,确保非法参数不会触达业务逻辑。
  • 错误信息格式:使用iter_errors收集所有错误(而非只报第一个),并拼接字段路径与具体原因,便于LLM根据错误信息自行修正参数后重试。
  • Schema能力:本案例演示了type、required、minimum/maximum、additionalProperties: false(禁止多余字段)及format: email(格式校验)。实际项目中可按需扩展enum、pattern、嵌套对象等JSON Schema特性。
  • 集成方式:在真实Harness中,call_tool方法通常由LLM的function calling机制触发,返回的{"success": false, "error": ...}结构可直接回传给模型作为工具调用的错误反馈。