Engineering note
工具调用协议:让模型能可靠地使用你的能力
这是「Agent 工程实战」的第 5 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。
本篇要解决的问题: 理解工具定义、参数结构、调用标识和结果回传,避免“看起来会调用”的假闭环。
本章定位:本章把工具调用当作一份 API 契约来研究。你会学习工具 schema、调用 ID、结构化结果和描述质量如何共同影响 Agent 的行为。
建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。本章适合在你准备增加第二、第三个工具前阅读;工具一多,命名和契约问题会立即暴露。
本章导读
本章把工具调用当作一份 API 契约来研究。你会学习工具 schema、调用 ID、结构化结果和描述质量如何共同影响 Agent 的行为。
本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。
5.1 工具描述的形状
当前 Tool 类会序列化成类似这样的结构:
{
"type": "function",
"function": {
"name": "ReadFile",
"description": "读取指定文件中的内容",
"parameters": {
"type": "object",
"properties": {
"filePath": {
"type": "string",
"description": "读取的文件路径"
}
},
"required": ["filePath"]
}
}
}
工具描述是给模型看的“可调用 API 文档”。描述越准确,模型越容易选择正确工具和生成正确参数。
5.2 工具调用不是函数调用
模型不会直接运行 C# 方法。它只能输出结构化意图:工具名、调用 ID 和 JSON 参数。真正执行工具的是你的程序。
这意味着程序必须承担四项责任:
- 工具是否存在;
- 参数是否符合 schema;
- 当前用户是否有权限;
- 执行结果如何安全地反馈给模型。
模型说“请执行某命令”不等于程序必须执行。模型是决策建议者,程序是最终的策略执行者。
5.3 工具结果应该稳定
当前 AgentToolResult 只有 Message 和 Result。建议逐步演进为:
public sealed record ToolResult(
bool Success,
string Message,
object? Data = null,
string? ErrorCode = null);
稳定的字段可以让模型区分“文件为空”“文件不存在”“没有权限”和“程序内部错误”。不要只把异常文本原样交给模型。
5.4 工具描述的写作原则
- 说明工具能做什么,不要只写方法名;
- 明确路径是文件还是目录、是否允许相对路径;
- 说明危险操作的限制;
- 说明返回值格式和失败情况;
- 参数名要表达业务含义,不要使用
arg1、data这类模糊名称。
5.5 本章小结
工具 schema 是 Agent 的 API 契约。它既影响模型决策,也决定程序能否校验、审计和限制调用。
5.6 工具名称是公共 API
工具名一旦被模型使用,就不再是普通方法名。改名会影响提示词、评测集、日志查询和旧会话恢复。因此建议使用稳定的命名规则:
资源.动作
filesystem.read_file
filesystem.write_file
process.run_allowed
knowledge.search
内部 C# 方法可以叫 ReadFileAsync,外部工具名仍然保持 filesystem.read_file。这样既符合 C# 命名习惯,也能让模型更容易理解工具类别。
5.7 schema 的描述决定调用质量
下面两个描述看起来都能工作,但质量不同:
{"description":"读文件"}
{"description":"读取 workspace 内的 UTF-8 文本文件。参数必须是相对路径,不允许使用绝对路径或 .. 穿越。文件不存在时返回结构化错误。"}
第二种描述把边界告诉了模型,也把程序的校验规则提前暴露给调用方。描述应该与实际实现保持一致,否则模型会根据错误契约生成请求。
5.8 工具结果不是给人看的日志
模型需要结构化结果,而不是一段难以区分的控制台输出。推荐:
{
"success": false,
"errorCode": "FILE_NOT_FOUND",
"message": "目标文件不存在",
"data": {"path":"notes/today.md"}
}
其中 message 应适合模型理解,errorCode 适合程序判断,data 适合后续逻辑使用。完整异常和堆栈放在日志里,不放进工具结果。
5.9 练习:给工具做 API 评审
选择当前的 GetFiles,回答:它是否说明了搜索深度?返回的是绝对路径还是相对路径?目录不存在和目录为空能否区分?一次最多允许返回多少文件?
如果这些问题没有答案,就先修改工具描述和结果模型,再让模型调用。工具契约比工具实现更值得提前设计。
5.10 本章产出
为现有工具建立一份工具目录表,至少记录名称、用途、参数、返回值、风险等级和失败码。后续的权限、评测和帮助命令都可以从这份定义生成。
单篇实战作业
实践:挑一个现有工具,写出它的名称、参数、成功结果、失败结果和风险等级,作为工具目录的第一条记录。
建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。本章适合在你准备增加第二、第三个工具前阅读;工具一多,命名和契约问题会立即暴露。
章节复盘
复盘问题:模型看到的描述是否与程序实际允许的行为一致?不一致时,应该优先改 schema 还是改实现?
本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。
下一篇:第 6 篇《用反射把 C# 方法注册成工具:便利与边界》
如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。