Spring AI Tool Calling 学习:让 Agent 调工具前要想清楚边界
这篇记录我对 Spring AI Tool Calling 的理解,重点不是怎么把方法暴露出去,而是工具粒度、权限、幂等、超时和日志这些后端细节。
刚开始学 Tool Calling 的时候,我觉得它特别像“让大模型调用后端接口”。模型判断用户想做什么,然后生成一段结构化参数,后端根据参数执行某个方法。这个概念很好理解,也很容易做出效果。
但越往后想,越发现 Tool Calling 比普通聊天危险很多。普通聊天最多是回答错,Tool Calling 是可能真的去查系统、改数据、发通知、创建工单,甚至执行运维动作。所以这篇我想写得实际一点:如果我要把 Spring AI Tool Calling 放进自动化运维 Agent 里,哪些地方一定要考虑清楚。
不要把所有接口都直接暴露给模型
我最开始有一个错误想法:既然模型可以调工具,那是不是把已有 Controller 或 Service 都封装成工具就行?后来觉得这样很危险。
业务接口一般是给人或者程序调用的,不一定适合给模型调用。模型需要的是语义清楚、边界明确、参数少但表达力够的工具。
比如不要给模型一个万能工具:
executeCommand(command)
这个工具太危险了,模型如果理解错,可能执行不可控命令。更合理的是拆成几个读操作工具:
queryServiceLogs(serviceName, level, timeRange, keyword)
queryMetric(serviceName, metricName, timeRange)
getRecentDeployments(serviceName, timeRange)
searchKnowledgeBase(query, serviceName)
createIncidentDraft(serviceName, summary, evidenceIds)
这些工具的动作很明确,大部分还是只读的。模型即使选错,也比较容易兜底。
工具粒度要适中
工具太粗,风险大;工具太细,模型又容易选错。比如查日志工具,如果拆成 queryErrorLogs、queryWarnLogs、queryInfoLogs,其实没必要,直接用 level 参数就可以。
我觉得比较好的工具应该满足几个条件:
- 一个工具只做一类事情。
- 工具名能直接表达用途。
- 参数不要太多。
- 参数里尽量使用枚举和范围限制。
- 返回值要结构化,方便模型继续分析。
比如日志查询工具可以限制时间范围最大 30 分钟,避免模型一上来查最近 7 天,把日志平台打爆。
{
"serviceName": "order-service",
"level": "ERROR",
"timeRangeMinutes": 10,
"keyword": "timeout"
}
如果模型传了 1440 分钟,后端应该直接拒绝,或者自动改成最大允许范围,而不是照单全收。
权限不能写在 prompt 里
这点我觉得很重要。不能只在系统提示词里写“你不能访问没有权限的数据”。模型不是权限系统,后端才是。
每次工具调用都应该带上用户上下文:
- 用户 ID。
- 用户角色。
- 所属项目或租户。
- 会话 ID。
- traceId。
- 当前请求来源。
工具内部根据这些信息做校验。比如用户只能查自己项目下的服务日志,就算模型传了别的服务名,也要被后端拒绝。
这一点和传统后端开发其实一样。不能因为调用方变成了大模型,就放松鉴权。
写操作必须更谨慎
Tool Calling 里最需要小心的是写操作。比如:
- 创建工单。
- 发送消息。
- 重启服务。
- 修改配置。
- 切换流量。
这些操作不是不能做,而是要分风险等级。
我会把工具分成三类:
READ_ONLY:查日志、查指标、查知识库
LOW_RISK_WRITE:创建草稿、生成报告、发送内部提醒
HIGH_RISK_WRITE:重启、切流、修改配置、删除数据
对于高风险工具,模型不能直接执行,只能生成建议,然后进入人工确认流程。用户确认后,后端再执行。这一点如果能写进项目设计里,会比单纯说“支持工具调用”更有说服力。
幂等问题很容易被忽略
模型可能重复调用同一个工具。比如网络抖动、流式输出中断、模型认为刚才没执行成功,都可能导致重复操作。
如果工具是只读的,问题不大。但如果是创建工单、发送通知,就必须做幂等。
可以设计一个幂等键:
conversationId + planStepId + toolName
同一个会话、同一个计划步骤、同一个工具,如果已经成功执行过,就不要重复执行。或者重复请求时返回之前的执行结果。
这其实和消息推送系统里的幂等很像。只要涉及外部副作用,就不能假设调用只发生一次。
超时和降级也要设计
Agent 调工具时,工具可能很慢。比如日志平台慢、指标平台慢、知识库检索慢。如果不处理,用户就会一直等。
我觉得每个工具都应该有超时时间和失败返回格式。比如日志查询超过 3 秒,可以返回:
{
"status": "TIMEOUT",
"summary": "日志查询超时,建议缩小时间范围或稍后重试",
"retryable": true
}
这样模型至少知道发生了什么,而不是拿到一个异常堆栈后继续乱猜。
如果某个工具失败,Agent 也可以换一条路。比如日志查不到,可以先查指标;知识库查不到,可以说明内部文档没有覆盖,而不是编一个答案。
返回值不要是一大段文本
我以前觉得工具直接返回文本最简单,但后来发现结构化返回更适合 Agent。
比如查指标工具返回:
{
"status": "SUCCESS",
"summary": "order-service 最近 10 分钟 P95 延迟从 120ms 上升到 1800ms",
"evidence": [
{
"metric": "http.server.requests.p95",
"before": "120ms",
"current": "1800ms"
}
],
"nextHints": ["query_logs", "query_deployments"]
}
这里的 summary 给模型快速理解,evidence 方便最后生成报告,nextHints 可以辅助下一步规划。
可观测性是排查 Agent 的基础
Agent 出错时比普通接口更难查,因为错误可能来自很多地方:
- 模型选错工具。
- 工具参数生成错。
- 后端权限拒绝。
- 外部平台超时。
- RAG 召回了错误文档。
- prompt 约束不够。
所以日志必须覆盖完整链路。我会记录:
- 用户问题。
- 会话 ID。
- 模型选择的工具。
- 工具参数摘要。
- 工具返回摘要。
- 调用耗时。
- 是否重试。
- 最终回答。
当然敏感信息要脱敏,比如手机号、token、密钥都不能完整记录。
小结
Spring AI Tool Calling 的 Demo 可以很快做出来,但真正难的是把边界做好。工具粒度、权限、幂等、超时、结构化返回、日志审计,这些都是后端系统本来就要考虑的问题,只是在 Agent 场景里更明显。
我觉得一个比较靠谱的说法不是“我让大模型可以调用接口”,而是“我把系统能力封装成受控工具,让 Agent 在权限和风险边界内调用”。这句话背后才是真正的工程设计。