· 2 分钟阅读

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)

这些工具的动作很明确,大部分还是只读的。模型即使选错,也比较容易兜底。

工具粒度要适中

工具太粗,风险大;工具太细,模型又容易选错。比如查日志工具,如果拆成 queryErrorLogsqueryWarnLogsqueryInfoLogs,其实没必要,直接用 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 在权限和风险边界内调用”。这句话背后才是真正的工程设计。