3849 字
约 12 分钟
20
4.3 参数校验与失败回填
2026-10-25
无标签

4.3 参数校验与失败回填

一句话结论:模型传错参数不是偶发事故,而是必然现象(2.3、2.4 已论证)。所以关键不在于"让它别错",而在于错了之后能不能自己改回来——这取决于你回传的错误信息质量。

读这篇之前

需要先读 4.1(工具调用是结构化输出)、3.2(schema 校验)、2.5(工具层的五项职责)。这一篇是工具设计里最影响"实际可用度"的一篇。


一、为什么参数一定会错

参数错误不是"模型不够聪明",有四条结构性原因:

原因 机制 例子
幻觉 它在预测"这个位置通常填什么" 编出 date: "明天下午" 而不是 2026-10-26
采样随机 高温下选了次优 Token 参数名拼错一个字母
描述含糊 不知道该填什么 只写"日期",它自创格式
上下文缺信息 拿不到该填的值 用户没说城市,它编一个

四条里有三条可以直接改善(描述、schema、上下文);剩下"幻觉"那条,靠的是校验与回填。


二、三层校验:语法、结构、语义

工具层的校验分三层,每层拦不同的问题:

层 检查什么 不做会怎样
语法层 JSON 能否解析 解析异常,整轮崩溃
结构层 必填字段在不在、类型对不对 下游拿不到值,静默用默认值
语义层 值是否合法(枚举、范围、格式) 误操作——最危险

第三层最容易被省,也最该做

举例:一个 delete_file 工具,参数 path: string。

  • 语法:{"path": "/etc/passwd"} —— 合法 JSON ✅
  • 结构:path 存在且是字符串 ✅
  • 语义:这个路径在允许的目录内吗? —— 没有这层检查,就等于把整个文件系统交出去了

语义校验的具体形态:

类型 校验内容
枚举 值必须在集合内(status 只能是 active/disabled)
范围 数字在区间内(size 1~1000)
格式 正则匹配(日期 YYYY-MM-DD)
路径白名单 前缀必须匹配允许目录
业务规则 依赖其他数据(不能删除自己创建的最后一个项目)

一条实用原则:

凡是"值域有限"的参数,都用枚举。凡是指向系统的参数(路径、命令、URL),都做白名单。


三、失败回填:让模型自己修

这是本篇的核心。参数错了之后,有两种处理方式:

❌ 直接失败:抛异常、任务终止
✅ 失败回填:把错误信息作为工具结果返回,让模型修正后重试

第二种之所以有效,是因为(1.3 讲过):工具结果会进入上下文,成为模型下一轮判断的依据。

所以"错误信息"本质上是一种特殊的上下文。它的质量决定了模型能不能自我修复。

错误信息的三种质量

① 最差:Error: 400 Bad Request
② 中等:参数校验失败:status 字段不合法
③ 最好:参数校验失败:status 只能是 active 或 disabled,
        你传入的是 "enabled"。请改为 active 后重试。

差别在哪?

质量 模型能做什么
① 不知道错在哪,只能瞎猜或放弃
② 知道哪个字段错了,但不知道正确值是什么
③ 知道错在哪、错成什么、该怎么改

第三种能让模型一次修正;第二种往往要试两三次;第一种基本修不好。


四、好的错误信息的五个要素

要素 说明 示例
哪个字段 精确定位 "status 字段"
错在哪 说清违反了什么约束 "不合法"
约束是什么 给出合法范围 "只能是 active 或 disabled"
你传了什么 让它看到自己的输入 "你传入的是 enabled"
怎么改 给可执行的指示 "请改为 active"

第五个要素最容易被漏,但收益最高。 因为它把"推理"变成了"执行"——模型不需要想"我该改成什么",只需要照做。

一个反例

❌ 参数错误

问题是:它既不知道哪个参数错,也不知道该怎么改,只能重试一次(大概率再错一次)。


五、重试机制该怎么设计

失败回填不是"无限重试",需要三条边界:

边界 建议 理由
最大重试次数 2-3 次 超过说明是设计问题,不是运气问题
每次重试要带新信息 必须带上错误详情 不带就是重复同一个错误
失败要能优雅退出 明确告诉用户"这个我做不了" 避免无意义消耗(2.2 终止条件)

一个更重要的设计:把校验前移

失败回填是"事后纠错"。更省成本的是事前减少错误:

手段 效果
参数用枚举而非自由文本 大幅降低取值错误
参数描述给格式和示例(4.1) 降低格式错误
必需信息缺失时先向用户提问 避免模型编造
降温度(2.4) 降低拼写/格式漂移

这些手段的成本都比"重试"低。 优先做事前,其次才是事后。


六、一个完整的处理流程

把校验与回填串成流程:

① 模型返回 tool_calls
② 解析 arguments
   ├─ 解析失败 → 回填:"无法解析参数,请确保输出合法 JSON"
   └─ 成功 → ③
③ 结构校验(必填、类型)
   ├─ 不通过 → 回填:"缺少必填字段 X" / "字段 Y 应为数字,收到字符串"
   └─ 通过 → ④
④ 语义校验(枚举、范围、白名单)
   ├─ 不通过 → 回填:"Z 不在允许范围内(允许:A/B/C),你传的是 D"
   └─ 通过 → ⑤
⑤ 权限与副作用分级
   ├─ 需要审批 → 发起人工确认(第 14 章)
   └─ 不需要 → ⑥
⑥ 真的执行
   ├─ 执行失败 → 回填:可理解的错误 + 建议(2.5 第五节)
   └─ 成功 → ⑦
⑦ 结果归一化(排序固定、去噪声)
⑧ 追加为 tool 消息,进入下一轮

这个流程里有五个回填点,每个都要求错误信息是"可行动的"。


七、常见误区

误区一:参数错了就重试一次,不用管。
不带具体错误的重试,大概率重复同一个错误。

误区二:校验只在工具实现里做。
校验应该离模型越近越好——在调用工具之前做,避免半途失败或部分执行。

误区三:错误信息越简短越好。
越简短越没用。错误信息是模型纠错的唯一依据,值得写得详细。

误区四:多加约束总是更安全。
过严的约束(比如字段长度 20-25 字)会导致频繁违规、反复重试。约束应覆盖"必须为真"的部分。

误区五:模型不会传系统级参数(路径、命令)。
它会的。任何接收这类参数的工具都必须白名单。

误区六:校验失败说明模型不行。
更可能是工具描述不清、schema 设计不合理。先查设计,再怀疑模型。


八、动手:错误信息的四档质量

同一个错误,四种写法,效果差一个量级。

① 最差:{"error": true}
② 差:  {"error": "参数错误"}
③ 中:  {"error": "status 不合法"}
④ 好:  {"error": "status 只能是 active 或 disabled,
                  你传入的是 enabled。请改为 active 后重试。"}
档 模型能做什么 平均修复轮数
① 只能重试瞎猜 3 轮以上,或修不好
② 知道有错,不知在哪 2-3 轮
③ 知道哪个字段错 1-2 轮
④ 直接改对 1 轮

四档对应的五个要素

要素 ① ② ③ ④
哪个字段 ✅ ✅
错在哪 ✅ ✅
约束是什么 ✅
你传了什么 ✅
怎么改 ✅

从 ③ 到 ④ 的关键是最后两项(传了什么、怎么改),而它们也最容易被省。

一个实践模板

写错误信息时直接套:

{字段} 只能是 {合法取值范围},你传入的是 {实际值}。请改为 {建议值}。

填空即可,一致性也有保证。

三条补充规则

规则 理由
错误信息要"面向下一次尝试" 不是记录故障,是指导修复
不要把堆栈直接给模型 它读不懂,会瞎猜
业务错误与协议错误分开 前者给模型,后者给开发者(8.2)

一个反例

差:{"error": "Traceback (most recent call last):
    File "app.py", line 42, in execute
    ..."}

模型看到堆栈只会绕开这个工具,而不会修正参数。堆栈应该进日志(10.5),不进上下文。


九、一个实操:给工具层写单测

工具层必须能脱离模型单独测试(2.5 讲的架构约束)。三个必测项:

def test_tool_rejects_unknown():
    """未知工具应返回可用工具清单"""
    r = execute_tool(call(name="nope"), registry, policy)
    assert r.type == "error"
    assert "可用工具" in r.message

def test_tool_validates_required():
    """缺必填参数应指出缺哪个"""
    r = execute_tool(call(name="get_weather", args={}), registry, policy)
    assert "缺少必填参数" in r.message

def test_tool_blocks_irreversible():
    """不可逆动作未审批应被拦下"""
    r = execute_tool(call(name="delete_file", args={"path":"a"}), registry, policy)
    assert r.type == "need_approval"

三个测试都跑在"无模型"环境下——这证明工具层是独立的。

还有一个必测项:越权

def test_path_whitelist():
    r = execute_tool(call(name="read_file", args={"path":"../../etc/passwd"}),
                     registry, policy)
    assert r.type == "error" and "不允许" in r.message

这一条最该有,因为路径穿越是最常见的实现漏洞(14.4 第六节)。


十、一个补充:校验失败时该"重试"还是"放弃"

4.3 讲了失败回填,但没讲什么时候该停止重试。

情况 处理
参数格式错(可修正) 重试,最多 3 次
值不在枚举内(可修正) 重试
缺乏必要信息(不可修正) 放弃,向用户提问
权限不足(不可修正) 放弃,说明原因
工具本身故障(不可修正) 跳过,换方案

关键区分是"模型能否靠自己的信息修正":

参数拼错 → 它能改
缺少用户信息 → 它改不了,必须问人

如果不区分,会出现"反复重试同一种不可修正错误",浪费轮次并最终失败。

一个可实现的做法:在错误类型上加标记。

{"error": {...}, "retryable": false, "reason": "缺少必要信息"}

retryable: false 让循环立刻停止重试,转入"向用户提问"分支。


十一、一个收尾:把三件套变成可复用的模板库

概念讲完,最后一件事是让它可复用。

建议的模板库结构

prompts/
├── _base.md              # 通用骨架(角色/目标/输出/约束/失败策略)
├── review.md             # 审查类
├── extract.md            # 抽取类
├── classify.md           # 分类类
├── generate.md           # 生成类
└── chat.md               # 对话类

每类模板只需要改三处:角色、目标、输出格式。

为什么值得建库

收益 说明
一致性 同类任务的失败策略统一
可评测 改一处,所有使用它的任务都受益(第 13 章)
新人可上手 不用从零写

最关键的是第二条:如果"允许说不知道"这句话写在模板里,就不需要每个任务单独记得写。


十二、一个补充:错误信息的自我一致性

如果同一个错误在两次运行里给出不同的提示,模型学不到规律。

问题 后果
错误文案随机变化 模型在两次里学到不同"线索"
同一字段的错误措辞不一致 增加理解成本

做法:错误信息由代码模板生成,不由模型生成。

def invalid(field, allowed, given, hint=None):
    return (f"{field} 只能是 {allowed},你传入的是 {given}。"
            + (f"请改为 {hint}。" if hint else f"请从 {allowed} 中选择。"))

统一的错误文案有几个好处:

好处 说明
可预测 模型能形成稳定理解
可测试 可以断言错误文案
可统计 能按错误类型聚合(10.5)

这也顺便解决了"错误信息漏要素"的问题——模板里要素齐全,填空即可。


十三、一个补充:把校验做成"可复用的校验器"

三个常见校验器写得好的样子。

def check_enum(value, allowed, field):
    if value not in allowed:
        return f"{field} 只能是 {' / '.join(allowed)},你传入的是 {value}。"
    return None

def check_path_in(value, roots, field="path"):
    real = os.path.realpath(value)              # 规范化(防 ../ 穿越)
    if not any(real.startswith(os.path.realpath(r)) for r in roots):
        return f"{field} 必须位于 {roots} 内,你传入的是 {value}。"
    return None

def check_int_range(value, lo, hi, field):
    if not isinstance(value, int) or not (lo <= value <= hi):
        return f"{field} 需为 {lo}-{hi} 的整数,你传入的是 {value}。"
    return None

三个共性:

共性 作用
返回 None 表示通过 便于链式调用
错误信息里带"你传入的是" 模型能对照自己传的值
入参用白名单而非黑名单 默认拒绝(10.3)

中间那条最容易被忽略,但它直接决定模型能否一次改对(4.3 四档质量)。


十四、下一篇

参数对了,工具也执行了。但这只是"单个动作正确"。

真正难的是:如何设计一个让人(模型)用得顺手、不容易出错、出错也能理解的工具集。这个问题的答案不在编程规范里,而在人机交互设计里。

下一篇讲 ACI——把工具当成"给人用的界面"来设计。

4.3 参数校验与失败回填
https://clxhxhhr.top/posts/4884/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。