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,就无法正确调用接收 int、double 或 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 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。