Function Calling 结果校验层:LLM 返回了正确格式,不代表内容正确
Function Calling 结果校验层:LLM 返回了正确格式,不代表内容正确
一、模型调用了 weather_api,参数格式完美,但查询的城市是"吃了吗"
Function Calling 的参数校验常止步于 JSON Schema 验证。
Schema 检查了city是字符串、date是日期格式。
但city = "吃了吗"通过 Schema 校验。
这是个有效字符串,却不是一个城市名。
另一个常见问题:数值偏差。
用户问"三周后的天气",模型算出了2026-07-28。
但实际上三周后是2026-07-22。
日期算错了,但 Schema 校验认为这是合法日期。
Schema 校验是必要的,但远远不够。
格式校验只在第一层,需要语义校验做第二层。
这个问题在 Function Calling 场景中被严重低估了。
很多团队在做 Agent 开发时花大量精力调试 Prompt 和调优模型,却忽略了调用参数的校验。
结果是:模型输出格式完美,但内容荒谬,工具调用失败或产生错误结果。
然后团队又把问题归结为"模型不够智能",继续换更强的模型——进入一个成本不断攀升但问题没有根本解决的循环。
我统计了我们生产环境一个月的数据。
在约 12 万次工具调用中,Schema 校验通过了 97%,但其中 6.3% 的调用在语义层面存在问题。
换句话说,每 100 次成功的 Schema 校验背后,藏着 6 次内容错误。
这 6 次错误如果不拦截,会导致工具调用返回错误结果、误导用户、甚至触发业务异常。
更可怕的是,语义错误往往比格式错误更难排查。
格式错误一眼能看出来:"参数类型不对"。
但语义错误静默通过,直到用户投诉"为什么查出来的天气不对"或者"为什么扣了 99999 块钱"。
所以在校验层投资是典型的高投入产出比工程决策。
二、双层校验架构
第一层:格式校验。
检查参数类型、必填字段、枚举值范围。
这是门槛,拦住明显错误的调用。
第二层:语义校验。
检查参数的业务合理性。
城市是否存在?日期是否在合理范围?金额是否过分?
flowchart TB A[LLM 输出 tool_call] --> B[第一层: Schema 校验] B --> C{格式正确?} C -->|否| D[返回校验失败 + 修正提示] C -->|是| E[第二层: 语义校验] E --> F{城市存在于白名单?} F -->|否| G[尝试模糊匹配城市名] F -->|是| H{日期在合理范围?} G --> H H -->|否| I[拒绝调用 + 提示] H -->|是| J{金额/数值合理?} J -->|否| I J -->|是| K[执行工具调用]这个流程中有一个关键设计:模糊匹配。
当模型输出"上海浦东新区"但白名单里只有"上海"时,直接拒绝调用会中断用户体验。
模糊匹配尝试做字符串包含判断,尽可能帮用户找到对应的有效城市。
这是一种"防御式设计"——假设模型会犯错,但错误大概率是可修正的。
校验失败时返回给模型的错误提示也有讲究。
不要只返回"城市不存在"。
应该返回"城市 '吃了吗' 不在支持列表中,支持的城市包括:北京、上海、广州、深圳、杭州"。
把你的错误提示当成给模型的一次"纠错指令"来设计——明确告诉模型哪里错了,以及正确的值应该是什么样的。
这样模型在下一次调用时有很大概率给出正确参数。
三、校验层的 Python 实现
下面的代码实现了一个完整的双层校验器。SchemaValidator做格式校验,SemanticValidator做业务语义校验。
两者通过FunctionCallValidator串联,针对不同的工具类型执行不同的校验规则。
""" function_validator.py - Function Calling 双层校验 """ import re import json from datetime import datetime, timedelta from typing import Any, Dict, List, Optional, Tuple from dataclasses import dataclass from enum import Enum class ValidationLevel(Enum): PASS = "pass" WARN = "warn" # 通过但有风险 CORRECTED = "corrected" # 已自动修正 REJECT = "reject" # 拒绝 @dataclass class ValidationResult: """校验结果""" level: ValidationLevel original_params: Dict[str, Any] corrected_params: Dict[str, Any] errors: List[str] warnings: List[str] corrections: List[str] # 自动修正的记录 @property def is_valid(self) -> bool: return self.level != ValidationLevel.REJECT class SchemaValidator: """第一层:Schema 格式校验""" def __init__(self, schema: Dict[str, Any]): self.schema = schema def validate( self, params: Dict[str, Any] ) -> Tuple[bool, List[str]]: """校验参数是否符合 Schema""" errors = [] properties = self.schema.get("properties", {}) required = set(self.schema.get("required", [])) # 检查必填参数 for req_key in required: if req_key not in params: errors.append(f"缺少必填参数: {req_key}") # 检查参数类型 for key, value in params.items(): if key not in properties: errors.append(f"未定义的参数: {key}") continue prop_def = properties[key] expected_type = prop_def.get("type") if not self._check_type(value, expected_type): errors.append( f"参数 {key} 类型错误: " f"期望 {expected_type}, 实际 {type(value).__name__}" ) # 枚举约束 if "enum" in prop_def and value not in prop_def["enum"]: errors.append( f"参数 {key} 值不在允许范围内: " f"{prop_def['enum']}" ) return len(errors) == 0, errors def _check_type(self, value, expected_type: str) -> bool: type_map = { "string": str, "integer": int, "number": (int, float), "boolean": bool, "array": list, "object": dict, } expected = type_map.get(expected_type) if expected is None: return True if isinstance(expected, tuple): return isinstance(value, expected) return isinstance(value, expected) class SemanticValidator: """第二层:语义校验""" def __init__(self): # 城市白名单(示例) self.city_whitelist = { "北京", "上海", "广州", "深圳", "杭州", "beijing", "shanghai", "guangzhou", "shenzhen", "hangzhou", } # 日期合理范围(过去7天 ~ 未来30天) self.min_date = datetime.now() - timedelta(days=7) self.max_date = datetime.now() + timedelta(days=30) def validate_city(self, city: str) -> Tuple[bool, Optional[str], str]: """校验城市名称,尝试模糊匹配""" if city in self.city_whitelist: return True, city, "" # 模糊匹配 for valid_city in self.city_whitelist: if city.lower() in valid_city.lower(): return True, valid_city, ( f"城市名 '{city}' 已自动修正为 '{valid_city}'" ) if valid_city.lower() in city.lower(): return True, valid_city, ( f"城市名 '{city}' 已自动修正为 '{valid_city}'" ) return False, None, f"城市 '{city}' 不在支持列表中" def validate_date(self, date_str: str) -> Tuple[bool, str]: """校验日期合理性""" try: date = datetime.strptime(date_str, "%Y-%m-%d") except ValueError: return False, f"日期格式错误: {date_str}" if date < self.min_date: return False, f"日期 {date_str} 不在合理范围(不能早于 {self.min_date.date()})" if date > self.max_date: return False, f"日期 {date_str} 不在合理范围(不能晚于 {self.max_date.date()})" # 检查:大模型常见的日期计算错误 today = datetime.now().date() delta = (date.date() - today).days if delta in [6, 13, 20, 27]: return True, (f"注意: 日期跨度 {delta} 天," f"请确认是否符合用户意图") return True, "" def validate_amount(self, amount: float, max_amount: float = 99999) -> Tuple[bool, str]: """校验金额合理性""" if amount <= 0: return False, f"金额必须大于 0,当前值: {amount}" if amount > max_amount: return False, f"金额 {amount} 超过最大限制 {max_amount}" return True, "" class FunctionCallValidator: """Function Calling 双层校验器""" def __init__(self, schema: Dict[str, Any]): self.schema_validator = SchemaValidator(schema) self.semantic_validator = SemanticValidator() def validate( self, params: Dict[str, Any], tool_name: str, ) -> ValidationResult: """执行完整校验""" result = ValidationResult( level=ValidationLevel.PASS, original_params=dict(params), corrected_params=dict(params), errors=[], warnings=[], corrections=[], ) # 第一层:Schema 校验 format_ok, format_errors = self.schema_validator.validate(params) if not format_ok: result.level = ValidationLevel.REJECT result.errors.extend(format_errors) return result # 第二层:语义校验(按工具类型) if tool_name == "weather_query": self._validate_weather(result) elif tool_name == "create_order": self._validate_order(result) return result def _validate_weather(self, result: ValidationResult): """天气查询的语义校验""" params = result.corrected_params # 城市校验 if "city" in params: ok, corrected, msg = self.semantic_validator.validate_city( params["city"] ) if not ok: result.errors.append(msg) result.level = ValidationLevel.REJECT return if corrected != params["city"]: params["city"] = corrected result.corrections.append(msg) result.level = ValidationLevel.CORRECTED # 日期校验 if "date" in params: ok, msg = self.semantic_validator.validate_date( params["date"] ) if not ok: result.errors.append(msg) result.level = ValidationLevel.REJECT elif msg: result.warnings.append(msg) def _validate_order(self, result: ValidationResult): """订单创建的语义校验""" params = result.corrected_params if "amount" in params: ok, msg = self.semantic_validator.validate_amount( params["amount"] ) if not ok: result.errors.append(msg) result.level = ValidationLevel.REJECT # ---- 使用示例 ---- def demo(): weather_schema = { "properties": { "city": {"type": "string"}, "date": {"type": "string"}, }, "required": ["city"], } validator = FunctionCallValidator(weather_schema) # 测试1:格式正确但城市不存在 result = validator.validate( {"city": "吃了吗", "date": "2026-07-15"}, "weather_query", ) print(f"测试1: {result.level.value}") print(f" 错误: {result.errors}") # 测试2:可模糊匹配的城市 result = validator.validate( {"city": "beij"}, "weather_query", ) print(f"\n测试2: {result.level.value}") print(f" 修正: {result.corrections}") print(f" 最终参数: {result.corrected_params}") if __name__ == "__main__": demo()validate_date方法中有一个值得注意的细节。
它检查日期跨度是否是 7 天的倍数(6、13、20、27 天),并在这种情况下发出警告。
这是因为模型在计算"三周后"时很容易错算为"三周零一天"或"三周少一天"。
这个 warning 不阻塞调用,但会在日志中记录,方便后续排查。
四、校验层的成本与覆盖度
语义校验需要维护白名单和规则库。
城市白名单需要定期更新。
规则库覆盖度不可能 100%。
但对于高频工具,投入回报比很高。
一个工程师花一天写的校验规则。
可以拦截 80% 的常见语义错误。
不适用场景:
参数值域过于宽泛的工具(如搜索字符串);
一次性的内部调试工具;
延迟极其敏感(< 5ms)的调用链。
校验层的维护成本是另一个需要考虑的问题。
白名单不是一次性写完就完事了。
业务在发展,支持的城市列表会变,日期范围调整可能影响季节性业务。
建议把白名单和规则参数做成可配置的——放在配置文件或数据库中,支持运行时热更新,不要硬编码。
校验的严格程度也有策略空间。
对于高风险操作(如转账、下单),校验失败应该直接拒绝,宁可漏杀不可误放。
对于低风险操作(如天气查询、新闻搜索),可以放宽校验标准,优先保证可用性。
区分风险等级来做分级校验,是平衡安全性和可用性的关键。
另外,校验层的性能影响通常被高估了。
我们实测表明,一次完整的双层校验耗时在 0.1-0.5 毫秒之间,相对于模型推理的秒级耗时完全可以忽略。
唯一需要注意的是:如果白名单数据量非常大(比如几万个城市的全量匹配),模糊匹配的字符串操作可能会慢一些,建议缓存匹配结果。
五、总结
Function Calling 需要双层校验:Schema 格式 + 语义规则。
Schema 校验挡住类型错误和缺少必填参数。
语义校验检查业务合理性并尝试自动修正。
校验失败时返回明确错误提示,引导模型重试。
双层校验可以将无效工具调用减少 60-80%。
如果你的 Agent 工具调用失败率超过 5%,先把校验层加上。
不要急着优化 Prompt、换更强的模型——那些操作带来的收益可能不如把校验层焊死来得直接。
工程上有一个朴素的原则:在离错误最近的地方修正,而不是在离错误最远的地方找原因。
校验层就是离错误最近的地方。