AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

参数校验与结构化错误:告诉 Agent 哪里失败了

我的学习 →

正在读取学习记录…

笔记助手收到一份查询,程序需要检查它,再决定是否读取。参数校验就是按规则检查实际收到的数据:字段是否齐全、类型是否正确、值是否有意义。检查通过,程序继续;检查失败,程序给出具体原因。

先从一次正常请求开始:

JSON
{"name": "read_note", "arguments": {"note_id": "n1"}}

工具名是 read_note,参数是笔记编号 n1。应用确认当前用户是 u1,在他的笔记范围里找到 n1,返回正文“退款窗口为 7 天。”。同一条路径会依次处理文本、字段、资源和结果,像收快递时先辨认单据,再核对地址,最后检查送来的东西。

把一条请求分成可理解的步骤

阶段做什么正常输入与结果
解析 parse把 JSON 文本读成程序里的值文本变成字典
路由 route根据名字找到工具选择 read_note
参数形状 shape检查字段、类型和值得到字符串编号 n1
资源 resource在当前用户范围里找到笔记u1 拥有 n1
输出 output核对工具返回的数据text 是正文字符串

“路由”在这里指根据工具名选择处理入口。“形状”指数据由哪些字段组成、每个字段是什么类型。先认识这几个名字,后面看到错误阶段就能知道程序走到了哪里。

解析与校验是相邻的两步。json.loads 能把 {"note_id":3} 解析成字典;之后类型检查会发现笔记编号是数字。文本可以读懂,仍要继续检查它是否符合工具约定。

请求按顺序经过 JSON 解析、参数形状检查、资源检查与结果检查
从左向右逐步处理。遇到失败就返回该阶段的错误,正常输入一路到达笔记结果。

让程序能读懂错误

如果所有失败都返回“出错了”,上层只能猜原因。结构化错误把原因拆成有固定含义的字段:

JSON
{
  "ok": false,
  "call_id": "call-1",
  "error": {
    "code": "invalid_arguments",
    "stage": "shape",
    "retryable": false
  }
}
字段含义这条错误里的值
ok这次处理是否成功false,处理失败
call_id本次工具请求编号用于找到对应请求
error.code错误类别invalid_arguments,参数不符合要求
error.stage发生错误的步骤shape,检查字段和类型时失败
error.retryable原样重试是否合适false,先修正参数

比如 invalid_arguments 是“参数不符合规则”,shape 是参数检查阶段。应用据此请求修正参数。错误文案可以换成更友好的中文,程序仍按稳定的 code 处理,避免每次改措辞都影响分支逻辑。

retryable: false 表示原样重试通常无法解决,需要按错误阶段处理:参数错误先改参数,资源拒绝先核对身份与归属,输出错误则修正工具实现。以后遇到短暂限流或连接失败,才会涉及等待后重试。

保存程序并运行

创建一个学习文件夹,在其中保存 validation_lab.py 和 test_validation_lab.py。程序使用固定笔记表,两个用户各有一篇笔记。

保存为 validation_lab.py:

先在 PowerShell 创建本篇的独立目录并进入;路径可换成你自己的非系统盘位置:

Bash
mkdir -Force "F:\学习\agentdepth-l2-validation"
cd "F:\学习\agentdepth-l2-validation"

将本篇的 validation_lab.py、test_validation_lab.py 保存到这个目录。保存全部文件后运行 dir *.py,核对上述文件名,再执行下文命令;新开窗口时先回到该目录,并按 Python 环境篇核对 python 的解释器路径。

Python
"""按阶段处理笔记查询,并返回结构化结果。"""
from __future__ import annotations

import json


# 固定笔记按用户身份分组。
NOTES = {"u1": {"n1": "退款窗口为 7 天。"}, "u2": {"n2": "另一份私人笔记。"}}


def handle(raw: str, identity: str, call_id: str = "call-1", result_override: dict | None = None) -> dict:
    # 用同一种格式返回错误,调用方可以按 code 和 stage 分支处理。
    def fail(code: str, stage: str, retryable: bool = False) -> dict:
        return {"ok": False, "call_id": call_id, "error": {"code": code, "stage": stage, "retryable": retryable}}

    # 第一步:把 JSON 文本解析为 Python 值。
    try:
        request = json.loads(raw)
    except json.JSONDecodeError:
        return fail("invalid_json", "parse")
    # 第二步:选择工具并检查参数字段和类型。
    if not isinstance(request, dict) or request.get("name") != "read_note":
        return fail("unknown_tool", "route")
    arguments = request.get("arguments")
    if not isinstance(arguments, dict) or set(arguments) != {"note_id"}:
        return fail("invalid_arguments", "shape")
    note_id = arguments["note_id"]
    if not isinstance(note_id, str) or not note_id.strip():
        return fail("invalid_arguments", "shape")
    # 第三步:在当前用户的笔记范围内查找。
    if identity not in NOTES or note_id not in NOTES[identity]:
        return fail("forbidden_or_missing", "resource")
    # 第四步:检查结果对象、字段、编号对应关系和正文类型。
    result = result_override if result_override is not None else {"note_id": note_id, "text": NOTES[identity][note_id]}
    if (not isinstance(result, dict) or set(result) != {"note_id", "text"}
            or not isinstance(result["note_id"], str) or result["note_id"] != note_id
            or not isinstance(result["text"], str)):
        return fail("invalid_tool_result", "output")
    return {"ok": True, "call_id": call_id, "result": result}


if __name__ == "__main__":
    # 每条固定输入观察一种处理路径。
    cases = [
        ("valid", '{"name":"read_note","arguments":{"note_id":"n1"}}', "u1"),
        ("invalid_json", '{"name":', "u1"),
        ("extra_field", '{"name":"read_note","arguments":{"note_id":"n1","admin":true}}', "u1"),
        ("wrong_owner", '{"name":"read_note","arguments":{"note_id":"n1"}}', "u2"),
    ]
    for label, raw, identity in cases:
        print(json.dumps({"case": label, **handle(raw, identity)}, ensure_ascii=False))

从文件夹的资源管理器地址栏输入 powershell,在打开的终端运行:

macOS:先在 Python 篇的学习文件夹中打开终端,执行 source .venv/bin/activate。再输入 cd ,把当前例子的文件夹拖入终端并回车,运行下面的命令。

Bash
python -X utf8 validation_lab.py

终端会逐行显示四个结果:

JSON
{"case": "valid", "ok": true, "call_id": "call-1", "result": {"note_id": "n1", "text": "退款窗口为 7 天。"}}
{"case": "invalid_json", "ok": false, "call_id": "call-1", "error": {"code": "invalid_json", "stage": "parse", "retryable": false}}
{"case": "extra_field", "ok": false, "call_id": "call-1", "error": {"code": "invalid_arguments", "stage": "shape", "retryable": false}}
{"case": "wrong_owner", "ok": false, "call_id": "call-1", "error": {"code": "forbidden_or_missing", "stage": "resource", "retryable": false}}

先读第一行:ok: true,result 包含实际读取的笔记内容。随后三行分别停在解析、参数和资源阶段,case 帮你对应演示输入。四个固定案例共用 call-1;正式应用为不同调用分配不同编号。

沿着 handle 看控制流

handle 的第一个输入 raw 是 JSON 文本,第二个输入 identity 是应用确认的用户身份。

json.loads(raw) 负责读文本;坏 JSON 会触发 JSONDecodeError,程序立刻返回解析错误。之后的工具名和参数检查都有前置 return:某一步返回了错误,函数就结束,后面的读取分支不会运行。Python json 文档介绍了解析返回值和解析异常。

set(arguments) 取得所有参数名,再与 {"note_id"} 比较。集合要求恰好相同,所以漏字段和多字段都会在这里被发现。类型与内容检查继续判断编号是否为含实际字符的字符串。

资源阶段用 NOTES[identity] 查当前用户的笔记。例子将“找不到”和“当前用户无权读取”合并为 forbidden_or_missing,对外提供相同错误类别;内部日志可以分别记录原因,方便维护人员定位。

最后的输出检查确认结果是字典、字段恰好包含 note_id 和 text、两者都是字符串,且返回编号对应请求的笔记。工具返回值也会进入后续程序,检查它与检查输入一样重要。

每次改一个值,观察检查阶段

打开程序底部 cases,修改 valid 那条输入,运行后核对结果:

note_id 改成预期阶段为什么
""shape编号为空
" "shapestrip 去掉空格后为空
3shape类型为数字
"n9"resource当前用户没有这个编号
"n1"正常结果u1 的笔记存在

当 JSON 字符串里面使用数字时,写 "note_id":3;字符串编号写 "note_id":"n1"。每次改一个值,比较输出的 error.stage,最后恢复原输入。

保存下面的测试为 test_validation_lab.py:

下面的测试检查正常结果,以及解析、参数、资源和输出各阶段的错误类别。

test_validation_lab.py:检查每个处理阶段
Python
import unittest
from validation_lab import handle


# 每项断言对应一个处理阶段。
class ValidationLabTests(unittest.TestCase):
    def test_valid(self):
        self.assertTrue(handle('{"name":"read_note","arguments":{"note_id":"n1"}}', "u1")["ok"])

    def test_parse_failure(self):
        self.assertEqual(handle("{bad", "u1")["error"]["stage"], "parse")

    def test_extra_field(self):
        self.assertEqual(handle('{"name":"read_note","arguments":{"note_id":"n1","admin":true}}', "u1")["error"]["stage"], "shape")

    def test_resource_scope(self):
        self.assertEqual(handle('{"name":"read_note","arguments":{"note_id":"n1"}}', "u2")["error"]["stage"], "resource")

    def test_call_id_preserved(self):
        self.assertEqual(handle("{bad", "u1", "abc")["call_id"], "abc")

    def test_output_failure(self):
        # 注入数字正文,确认错误发生在返回结果阶段。
        result = handle('{"name":"read_note","arguments":{"note_id":"n1"}}', "u1", result_override={"note_id": "n1", "text": 3})
        self.assertEqual(result["error"]["stage"], "output")

    def test_output_must_be_object(self):
        # 工具返回列表时,应用仍应给出结构化输出错误。
        result = handle('{"name":"read_note","arguments":{"note_id":"n1"}}', "u1", result_override=[])
        self.assertEqual(result["error"]["stage"], "output")

    def test_output_requires_matching_note_id(self):
        # 缺失编号、数字编号、返回另一篇笔记,都要在输出检查中发现。
        for value in ({"text": "正文"}, {"note_id": 3, "text": "正文"}, {"note_id": "n2", "text": "正文"}):
            with self.subTest(value=value):
                result = handle('{"name":"read_note","arguments":{"note_id":"n1"}}', "u1", result_override=value)
                self.assertEqual(result["error"]["stage"], "output")


if __name__ == "__main__":
    unittest.main()

运行:

Bash
python -X utf8 -m unittest -v test_validation_lab.py

预期是 8 项测试和 OK。测试里的 assertEqual 比较实际与预期,assertTrue 检查条件为真。test_output_failure 给工具结果注入数字正文,确认应用返回 output 阶段错误。新增输出测试还检查列表结果、缺失编号、数字编号及返回另一篇笔记。test_call_id_preserved 检查错误仍携带原调用编号,方便找到出问题的请求。

按原因选择下一步

错误类别像路口标识,上层可以依它选择修正、等待或停止:

类别常见情况合适动作
输入格式错误JSON 未闭合、编号类型错误给出字段提示,有限次数请求修正
工具选择错误工具名不在清单重新提供清单或结束这条动作
资源访问错误当前用户没有目标笔记核对身份与编号,停止读取
工具输出错误正文被返回为数字修正工具实现并重新测试
临时服务错误限流或短暂连接故障按服务约定等待后有限重试
写入结果未知订单已创建,但响应丢失查询业务状态,再决定下一步

最后一种情况要结合幂等与副作用控制:重发创建请求可能产生第二个订单。先确定之前那次动作的状态,才能安全继续。

MCP 还区分协议请求错误和工具执行结果中的错误。协议层的未知工具、消息格式问题,与工具内部的查询失败属于不同位置,集成时按 MCP 工具规范转换结果。

参数校验保证请求符合规则,结构化错误把“失败在哪、为何失败”交给上层。先跑通正常请求,再用一个改动观察一个阶段,才能把错误码读成具体处理过程。下一篇是工具权限,继续检查谁能操作哪份资源。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“参数校验与结构化错误”,哪种做法更稳妥?

对应知识点:L2-validation

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L2-validation

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L2-validation

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L2-validation

图片查看器
100%

文章配图 · 按 Esc 关闭