SpringAI写第一个Agent-把工单循环跑起来

Spring AI 写第一个 Agent:把工单循环跑起来

之前把工具调用讲成工单循环:模型写一张 JSON 工单,你的代码去跑腿,结果回填,模型接着说话。纸上谈兵到此为止。这篇用 Spring AI 把这个循环真正跑起来:一个查天气的工具,一次真实的调用,工单的报文全程可见。写完你手里会有一个能跑的 agent,以及看懂所有 agent 文档的底气。

先搭台子

新建一个 Spring Boot 工程(3.x,Java 17+),添加依赖。Spring AI 从 1.0 起按模型供应商拆分模块,用哪家模型就加哪家的 starter,下面以 OpenAI 兼容接口为例(版本口径:Spring AI 2.0.1,核验于 2026-10-25,动手前请对照官方快速开始页)。

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

配置就两行,模型的地址和密钥。

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${AI_API_KEY}
      base-url: https://api.example.com/v1   # 用哪家填哪家

工具就是一个带注解的方法

工单哲学在 Spring AI 里的落点出奇地朴素:工具就是一个普通 Bean 里的普通方法,加一个 @Tool 注解,方法签名就是工单的参数表。文档确认的包路径是 org.springframework.ai.tool.annotation.Tool。

java 复制代码
@Component
public class WeatherTools {

    @Tool(description = "查询指定城市的当前天气")
    public String getWeather(@ToolParam("城市名") String city) {
        // 真实场景在这里发 HTTP,这里返回模拟数据
        return city + ",晴,25 度";
    }
}

方法名和参数名会被翻译成工单里的工具名和参数名,description 会被翻译成给模型看的说明书。你写的还是 Java,模型看到的是一张接口文档。
#mermaid-svg-EK2dW4ggm9yZxrWK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EK2dW4ggm9yZxrWK .error-icon{fill:#552222;}#mermaid-svg-EK2dW4ggm9yZxrWK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EK2dW4ggm9yZxrWK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EK2dW4ggm9yZxrWK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EK2dW4ggm9yZxrWK .marker.cross{stroke:#333333;}#mermaid-svg-EK2dW4ggm9yZxrWK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EK2dW4ggm9yZxrWK p{margin:0;}#mermaid-svg-EK2dW4ggm9yZxrWK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK .cluster-label text{fill:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK .cluster-label span{color:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK .cluster-label span p{background-color:transparent;}#mermaid-svg-EK2dW4ggm9yZxrWK .label text,#mermaid-svg-EK2dW4ggm9yZxrWK span{fill:#333;color:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK .node rect,#mermaid-svg-EK2dW4ggm9yZxrWK .node circle,#mermaid-svg-EK2dW4ggm9yZxrWK .node ellipse,#mermaid-svg-EK2dW4ggm9yZxrWK .node polygon,#mermaid-svg-EK2dW4ggm9yZxrWK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EK2dW4ggm9yZxrWK .rough-node .label text,#mermaid-svg-EK2dW4ggm9yZxrWK .node .label text,#mermaid-svg-EK2dW4ggm9yZxrWK .image-shape .label,#mermaid-svg-EK2dW4ggm9yZxrWK .icon-shape .label{text-anchor:middle;}#mermaid-svg-EK2dW4ggm9yZxrWK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EK2dW4ggm9yZxrWK .rough-node .label,#mermaid-svg-EK2dW4ggm9yZxrWK .node .label,#mermaid-svg-EK2dW4ggm9yZxrWK .image-shape .label,#mermaid-svg-EK2dW4ggm9yZxrWK .icon-shape .label{text-align:center;}#mermaid-svg-EK2dW4ggm9yZxrWK .node.clickable{cursor:pointer;}#mermaid-svg-EK2dW4ggm9yZxrWK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EK2dW4ggm9yZxrWK .arrowheadPath{fill:#333333;}#mermaid-svg-EK2dW4ggm9yZxrWK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EK2dW4ggm9yZxrWK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EK2dW4ggm9yZxrWK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EK2dW4ggm9yZxrWK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EK2dW4ggm9yZxrWK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EK2dW4ggm9yZxrWK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EK2dW4ggm9yZxrWK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EK2dW4ggm9yZxrWK .cluster text{fill:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK .cluster span{color:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EK2dW4ggm9yZxrWK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EK2dW4ggm9yZxrWK rect.text{fill:none;stroke-width:0;}#mermaid-svg-EK2dW4ggm9yZxrWK .icon-shape,#mermaid-svg-EK2dW4ggm9yZxrWK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EK2dW4ggm9yZxrWK .icon-shape p,#mermaid-svg-EK2dW4ggm9yZxrWK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EK2dW4ggm9yZxrWK .icon-shape .label rect,#mermaid-svg-EK2dW4ggm9yZxrWK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EK2dW4ggm9yZxrWK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EK2dW4ggm9yZxrWK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EK2dW4ggm9yZxrWK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} tool_use 工单

name= get_weather

input= city: 北京
模型
Spring AI 解析
调用 @Tool 方法

getWeather(北京)
结果回填 tool_result

这张图和之前画的工单循环是同一张,只是每个格子都有了 Spring 的落地。

把模型接进来

ChatClient 是发请求的入口,.tools(...) 把你的 Bean 注册成工具,剩下的循环它替你转。

java 复制代码
@Component
public class AgentRunner implements ApplicationRunner {

    private final ChatClient chatClient;
    private final WeatherTools weatherTools;

    public AgentRunner(ChatClient.Builder builder, WeatherTools weatherTools) {
        this.chatClient = builder.build();
        this.weatherTools = weatherTools;
    }

    @Override
    public void run(ApplicationArguments args) {
        String answer = chatClient.prompt()
                .user("北京今天多少度?")
                .tools(weatherTools)
                .call()
                .content();
        System.out.println("最终回答:" + answer);
    }
}

第一次跑通时,最值得做的事是把日志级别调高,看清这一来一回的报文。模型返回的 tool_use、你的方法被执行、结果被回填,整个过程在日志里一目了然,工单循环从图变成了你机器上的事实。

亲手跑

上面三段加上启动类就是一个完整工程,运行 mvn spring-boot:run 就能看结果。想先不碰真模型也行,把 API Key 随便填,看框架在发请求前打出的报文,工具的 JSON Schema 一样能见到,那正是模型看到的「接口文档」。

跑通之后有两个顺手的实验。把 question 换成「北京和上海今天多少度」,看模型会不会一次写两张工单,这是并行工单的入口。把 getWeather 改成真的抛异常,看错误怎么被包进 tool_result 回给模型,这是失败路径的入口。

记住一句话:@Tool 注解标记的不是魔法,是一张给模型看的接口文档,执行永远在你手里。

适用边界

这一篇管单工具、单轮的最小闭环。多轮对话的记忆、工具的权限边界,以及流式输出的接法,是接下来的篇目。Spring AI 各版本的注解包路径和配置前缀会微调,本文按 2026-10-25 的 2.x 口径整理,动手前对一下官方文档。跑通这个 demo 之后,你对所有 agent 框架的文档都有了阅读坐标,剩下的只是换语法。

相关推荐
威哥爱编程1 小时前
Python 智能体开发实战 02:你的 AI 助教说话像换了个模型?九成的 system prompt 都写错了
python·agent
威哥爱编程1 小时前
Python 智能体开发实战 01:装了三遍环境还在报错?虚拟环境和 DeepSeek 一次配到位
python·agent
后端小肥肠2 小时前
群友靠公众号漫画工坊入池!10w+IP漫画一键复刻,新增矩阵功能
人工智能·aigc·agent
ZhengEnCi2 小时前
想让AI能力翻倍需要多少数据:874条专家轨迹的后训练杠杆效应
llm·agent·强化学习
步行cgn3 小时前
@ResponseBody 详解:Spring MVC 中把返回值直接写入响应体
java·spring·mvc
武子康4 小时前
H3-World 的动作没按时发生,先检查时间线变成了什么
人工智能·后端·agent
jason成都4 小时前
RTKLIB 的 Java 移植版来了:rtklib-java v3.3.0 功能全景
java·gnss·rtklib·rtklib_java
前进的李工4 小时前
Agent 学习笔记 02:Harness 与工作流怎么理解
笔记·学习·agent·工作流·harness
LazyHunter94 小时前
JDK 28:Java 下一步方向解析与新特性展望
java·jdk