猪头少年 - 云南AI专家 - 云南独立开发者

Engineering note

类型安全的工具注册中心:从反射便利走向工程约束

这是「Agent 工程实战」的第 10 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。

本篇要解决的问题: 通过注册中心、参数绑定与结构化错误,让工具系统可验证、可演进。

返回专题路线


本章定位:本章让工具系统从“能调用”变成“能验证”。你会处理参数名、类型、默认值、schema、返回值和反射异常,建立一个真正的工具注册中心。

建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。工具参数是 Agent 最常见的故障入口。建议用坏参数反复测试,而不是只验证一次正常调用。

本章导读

本章让工具系统从“能调用”变成“能验证”。你会处理参数名、类型、默认值、schema、返回值和反射异常,建立一个真正的工具注册中心。

本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。

10.1 从“方法名映射”升级为“工具定义”

工具名称应当是全局唯一、稳定、可审计的 ID,例如 filesystem.read_file,而不是单独的 ReadFile

public sealed record ToolDefinition(
    string Name,
    string Description,
    JsonElement ParametersSchema,
    string Category,
    bool RequiresApproval);

10.2 参数绑定必须按名称

当前代码使用:

kv.Values.Select(x => GetValue(x)).ToArray()

这依赖字典枚举顺序,不是可靠的参数绑定。正确方向是根据 ParameterInfo.Name 逐个取值:

var args = method.GetParameters()
    .Select(parameter =>
    {
        if (!arguments.TryGetProperty(parameter.Name!, out var value))
            throw new InvalidOperationException($"缺少参数:{parameter.Name}");
        return ConvertJson(value, parameter.ParameterType);
    })
    .ToArray();

10.3 推荐的转换范围

第一阶段支持:

  • string、int、long、double、bool;
  • nullable 基本类型;
  • enum;
  • string 数组;
  • 简单 record DTO。

不支持或需要显式注册的类型:文件流、进程句柄、数据库连接、任意对象和委托。

10.4 Schema 不能只靠类型名

模型需要知道约束,例如最大长度、枚举值和是否允许为空。可以为参数特性增加元数据:

public sealed class ParameterAttribute : Attribute
{
    public ParameterAttribute(string description) => Description = description;
    public string Description { get; }
    public bool Required { get; init; } = true;
    public string[]? Enum { get; init; }
}

然后在注册阶段把这些信息转换成 JSON Schema。

10.5 本章交付物

  • 工具名冲突检测;
  • 参数名绑定;
  • 参数缺失、类型不匹配和 schema 校验;
  • 工具调用异常包装;
  • 工具注册报告,例如启动时列出工具名、风险等级和审批要求。

10.6 从 JsonElement 到 C# 参数

参数转换不能只按 JSON 的 ValueKind 粗略转换。比如所有数字都转成 long,就无法正确调用接收 intdouble 或 nullable 数字的方法。

可以把转换器设计成显式函数:

private static object? ConvertJson(JsonElement value, Type targetType)
{
    if (targetType == typeof(string)) return value.GetString();
    if (targetType == typeof(int)) return value.GetInt32();
    if (targetType == typeof(long)) return value.GetInt64();
    if (targetType == typeof(bool)) return value.GetBoolean();
    if (targetType == typeof(double)) return value.GetDouble();
    if (targetType.IsEnum)
        return Enum.Parse(targetType, value.GetString()!, true);

    return JsonSerializer.Deserialize(value.GetRawText(), targetType);
}

实际项目应补充 nullable、数组、默认值和失败消息。转换失败要指出参数名和期待类型,不要只返回“转换失败”。

10.7 schema 校验和方法校验是两回事

schema 校验面向模型调用,方法校验面向程序安全。即使 JSON 符合 schema,也可能违反业务规则,例如路径格式正确但目标在 workspace 外。

因此执行流程应是:

解析 JSON -> schema 校验 -> 类型转换 -> 业务校验 -> 权限校验 -> 执行

任何一步失败都不应触发实际副作用。

10.8 返回值适配

当前代码只处理 Task<AgentToolResult>。未来工具可能返回同步结果、Task<T>ValueTask<T> 或直接抛异常。建议统一由注册阶段适配为:

Func<JsonElement, CancellationToken, Task<ToolResult>>

这样 Agent Loop 不需要知道工具方法原本是同步还是异步。

10.9 练习:故意写坏参数

ReadFile 准备以下调用:缺少 filePath、传入数字、传入 null、传入 ../secret.txt。要求每种情况都得到不同且可理解的错误码,并保证目标文件没有被访问。

10.10 本章产出

工具注册中心要让“模型可以看到什么”和“程序可以执行什么”都有明确、可测试的中间表示。反射只是生成这些表示的手段,不是系统的公共接口。

单篇实战作业

实践:为一个工具准备缺参、错类型、null、枚举非法值和业务越权五种输入,确保都不会产生副作用。

建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。工具参数是 Agent 最常见的故障入口。建议用坏参数反复测试,而不是只验证一次正常调用。

章节复盘

复盘问题:schema 通过后,业务和安全校验是否仍然会拒绝请求?如果不会,说明边界还不够清晰。

本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。


下一篇:第 11 篇《安全的文件与命令执行:先划边界,再赋能力》

如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。

Next step

不要停在单篇文章。

沿主专题继续阅读,查看同一问题从概念到实战的完整路径。

返回「Agent 工程实战」路线