Spring AI 实战:从零搭建 AI Agent 最小案例与工程化落地指南
2026/9/8 2:12:02 网站建设 项目流程

在实际项目中,AI 应用开发真正难的不是调用一次模型接口,而是把模型调用、提示词管理、工具编排、结果评估和部署监控串成一条可维护的工程链路。很多人把 AI 工程实践等同于把 Prompt 写得更长、把模型换成更大,结果项目一到联调或生产阶段就暴露出超时、乱答、上下文失控、成本不可控等一系列问题。下面以 Spring AI 为基础,从零搭建一个可运行的 AI Agent 最小案例,然后按“参数理解 -> 结果验证 -> 问题排查 -> 生产加固”的顺序,把 AI 应用落地时应该关注的工程环节完整讲一遍。

1. 先理解 AI 工程实践与 AI Agent 的关系

1.1 AI 工程实践解决的是什么问题

先从一个通俗的说法开始:AI 工程实践,就是把模型从一个“能回复文本的黑盒”变成“能够稳定完成业务功能的模块”。单次调用模型很简单,把问题发给接口、拿到回答就算结束;但一个真实项目要处理的是:请求怎么进、上下文怎么存、工具怎么调、答错了怎么办、模型升级后输出变了怎么发现、用户在高峰期同时请求会不会把模型服务打满。

这些内容加起来,才是 AI 工程实践。它不等于算法知识,也不等于 Prompt 调优,而是一个覆盖集成、编排、评估、部署、监控和迭代的完整工程流程。对后端工程师来说,最直接的入手点是先熟悉一套成熟的 AI 开发框架,把“调用模型”这件事变成普通业务代码一样可测试、可维护的模块。

1.2 Agent 不是必选项,先分清楚三种调用模式

在写代码之前,要先区分三种越来越常见、但经常被混为一谈的调用模式。

第一种是单轮问答。用户发一条消息,模型回一条结果,前后请求没有关联。适合翻译、摘要、分类等无状态场景。

第二种是多轮对话。系统把之前的用户消息和模型回复一起传给模型,模型能理解“它”指代的是谁、上下文里发生了什么。适合客服、对话助手等需要记忆历史的场景。

第三种是工具调用,也就是 Agent 最核心的行为。模型收到用户请求后,不是直接输出答案,而是先决定“我需要调用哪个工具”,比如查询订单、计算价格、查询天气,然后把工具返回的结果组织成最终回复。工具调用让模型从“只会说话”变成“能执行动作”。

这三种模式不是递进关系,而是按需求选择。如果只需要做分类,多轮对话反而引入额外成本和混乱。Agent 最大的价值在于需要模型自主决定“下一步做什么”的场景。下面这个最小案例选择 Agent 模式,因为它最能体现工程化需要考虑的复杂点。

2. 环境准备与 Spring AI 依赖配置

2.1 本地模型方案优先,调试阶段不要直接烧模型服务费用

AI 应用开发最常见的坑,是一开始就把项目绑在云厂商模型 API 上,每次调试都产生一次计费,试 Prompt、调工具、查异常的成本很高。推荐在学习和联调阶段先使用本地模型运行时 Ollama,把模型跑在本机,等到功能稳定后再把配置切换到线上模型服务。

Ollama 的使用方式很直接:安装完成后,在终端拉取一个支持工具调用的模型。下面以 qwen2.5:7b 为例,如果你的机器显存或内存有限,可以换成 qwen2.5:3b 或 llama3.2:3b。

ollama pull qwen2.5:7b ollama serve

拉取完成后,可以用一条命令验证模型是否可调用,不需要先写好代码:

curl http://localhost:11434/api/generate -d '{"model": "qwen2.5:7b", "prompt": "1+1=?"}'

这一步有两个目的:一是确认本地模型服务已经启动,二是排除“模型没下载”这个最常见的低级问题。如果请求长时间无响应,大概率是模型过大、机器内存不足或仍在后台加载。

2.2 Maven 依赖与配置文件先对齐,才能减少后面的大量报错

Spring AI 是 Spring 生态中把模型 API 抽象成统一接口的框架。它屏蔽了不同模型服务之间的差异,让业务代码不直接依赖某一家模型厂商的 SDK。下面示例基于 Spring AI 1.0 系列 API 编写,具体版本要结合你使用的 Spring Boot 版本确认兼容性后决定。

pom.xml 中需要引入 Spring AI BOM 和对应的 Ollama starter:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.1</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> </dependencies>

配置文件 application.yml 中,最关键的是模型服务地址和模型名称:

server: port: 8080 spring: application: name: ai-agent-demo ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b temperature: 0.7

先解释 base-url。它告诉 Spring AI 去哪里找模型服务,本地默认是 11434 端口。后面切换到线上模型服务时,只需要把 starter、base-url、模型名和密钥一起换掉,业务代码可以基本不动,这是使用框架的第一个收益。

temperature 是控制模型输出随机性的参数,0.7 是比较通用的初始值。它具体怎么影响输出,会在第 4 节统一说明。

2.3 项目结构按最小可运行案例来组织

下面这个目录结构足够支撑演示,也符合 Spring Boot 常规约定:

ai-agent-demo/ ├── pom.xml ├── src/main/java/com/example/aiagent/ │ ├── AiAgentApplication.java │ ├── ChatController.java │ └── tool/ │ └── OrderQueryTool.java └── src/main/resources/ └── application.yml

主启动类就是普通的 Spring Boot 入口:

package com.example.aiagent; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class AiAgentApplication { public static void main(String[] args) { SpringApplication.run(AiAgentApplication.class, args); } }

到这里,环境准备部分就结束了。检查标准是:项目能启动,且配置文件中的模型存在、服务可访问。不要急着写复杂逻辑,先把最小链路跑通。

3. 用 Spring AI 实现一个可运行的 AI Agent 最小案例

3.1 先通过 ChatClient 打通模型调用链路

Spring AI 在 1.0 系列中把 ChatClient 作为推荐的统一客户端。它可以理解成封装了提示词构建、模型调用、响应解析的对象。先写一个最简接口,用来验证整体链路是否工作。

package com.example.aiagent; import java.util.Map; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.*; @RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } @PostMapping("/chat") public String chat(@RequestBody Map<String, String> payload) { String message = payload.get("message"); return chatClient.prompt(message) .call() .content(); } }

启动项目后,用下面的请求验证:

curl -X POST http://localhost:8080/chat \ -H "Content-Type: application/json" \ -d '{"message": "用一句话介绍 AI Agent"}'

如果返回了一段有意义的文本,说明 Spring Boot、Spring AI、Ollama、模型四层链路已经打通。这时再进入 Agent 的工具调用阶段。

这一步有几个容易忽视的检查点:

  • 项目是否能正常启动。如果启动失败,先看是否缺少 Spring AI BOM 导致依赖版本无法解析。
  • 请求是否超时。本地模型首次调用会加载模型到内存,耗时较长,第二次开始通常会快很多。
  • 返回内容是否异常。如果模型输出乱码或英文,可能是模型本身不支持中文或请求参数有问题。

3.2 编写订单查询工具,让模型学会“先查再说”

Agent 与普通问答的区别在于工具调用。这里设计一个非常小的业务场景:用户在聊天窗口中询问订单状态,模型必须调用订单查询工具,而不是凭空编造结果。

先定义一个工具类:

package com.example.aiagent.tool; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; @Component public class OrderQueryTool { @Tool(name = "query_order_status", description = "根据订单号查询订单当前状态") public String queryOrderStatus(String orderId) { if (orderId == null || orderId.isBlank()) { return "订单号不能为空,请告诉用户提供订单号"; } // 实际项目这里替换为数据库查询或远程接口调用 return "订单 " + orderId + " 当前状态:已发货,预计 3 天内送达"; } }

然后修改 ChatController,把工具名称注册到对话请求上:

@PostMapping("/agent/chat") public String agentChat(@RequestBody Map<String, String> payload) { String message = payload.get("message"); return chatClient.prompt(message) .tools("query_order_status") .call() .content(); }

Spring AI 的机制是这样的:框架扫描带有 @Tool 注解的方法,把方法名、描述、参数结构生成一份工具描述,随用户消息一起发给模型。模型判断需要查订单时,在回复中声明调用该工具,框架负责真正执行这个方法,再把执行结果回传给模型,最终由模型组织成自然语言回复。

启动项目,请求示例:

curl -X POST http://localhost:8080/agent/chat \ -H "Content-Type: application/json" \ -d '{"message": "我的订单 O2025011001 现在到哪了?"}'

正常输出类似:

您的订单 O2025011001 已发货,预计 3 天内送达。

这里的关键不是工具代码有多复杂,而是理解一个链路:用户请求 -> 模型决定调用工具 -> 框架执行工具 -> 结果返回模型 -> 模型生成最终回答。任何一步出问题,都可能表现为“模型编造订单状态”或“模型完全不调用工具”。

3.3 验证 Agent 行为的方式不只一种

写完代码后,不要只看一次调用成功就结束。建议从三个角度验证:

第一,验证正确路径。请求一个存在的订单号,确认模型调用工具并给出正确状态。

第二,验证参数缺失路径。请求消息不带订单号,观察模型是否要求用户补充,而不是硬造一个订单号。可以在工具方法里加入参数校验来完成兜底。

第三,验证工具不存在的场景。问一个与订单无关的问题,确认模型不会强行调用工具,而是直接回答。

这三种情况的日志和响应各自不同,但都能帮你在早期发现模型行为不符合预期的问题。

4. 关键参数与配置项的工程含义

4.1 模型参数直接影响输出的质量和稳定性

Spring AI 中频繁出现的几个参数,工程含义很容易被低估。整理成下面的速查表:

参数含义常见值调大的影响调小的影响推荐场景
temperature采样随机度0.0 - 1.0输出更随机、更有创造力,但可能跑题输出更确定、更保守事实型任务 0.2 左右,创意任务 0.8 左右
max-tokens单次回复最大 token 数取决于模型上下文回答更长,但费用和等待时间增加回答被截断,信息不完整短分类任务 200 内,长文任务按需调大
top-p概率累积采样0.0 - 1.0候选词范围更大候选词范围更集中一般保持默认,与 temperature 二选一重点调
context 长度模型可用的输入上下文2K - 1M 不等能容纳更多历史上下文较早被截断多轮对话调大,单轮问答不需要

需要留意的是,temperature 和 top-p 不是两个都要调到完美,多数情况下固定一个、调整另一个即可。事实查询类场景,比如订单状态、文档问答,temperature 建议压低到 0.2 左右,避免模型在确定性问题上自由发挥。

max-tokens 是另一个常见坑。设置过小,长回答会被截断,接口返回的内容不完整;设置过大,无效等待和费用都会上升。合理做法是先从应用场景的典型答复长度出发,设置一个偏小值,再根据真实数据逐步放大。

4.2 提示词不是写进代码里就行,要区分角色和位置

Spring AI 的 prompt 结构里,通常把提示词分成 System 和 User 两种角色。System 负责描述系统身份、规则、工具使用边界,User 负责承载用户的具体问题。

推荐这样组织提示词:

String systemPrompt = """ 你是订单助手。回答订单问题时,必须使用 query_order_status 工具查询真实状态。 不要根据用户的描述推测订单状态。 订单号格式为字母 O 后跟数字。 """; String answer = chatClient.prompt() .system(systemPrompt) .user(message) .tools("query_order_status") .call() .content();

把规则放在 System 里有两个好处:一是规则与用户输入隔离,二是多轮对话时,System 部分可以每次固定注入,而 User 部分只追加用户消息,避免把对话历史搅乱。

实际项目中最常见的错误,是用户输入直接拼进提示词而没有做任何边界处理。恶意用户可能通过输入“忽略之前指令”等语句诱导模型执行预设之外的命令,这就是提示词注入问题。生产环境至少要做的防护是:对系统提示词与用户输入做隔离,不在系统提示词中放置不应对用户暴露的内部规则,同时在代码层面对工具调用权限做校验,而不是完全信任模型的判断。

4.3 超时、重试与并发控制要放在联调阶段就确定

模型调用是远程 IO,最怕的情况是接口长期挂起不返回。调整基础连接参数时,可以从下面的思路出发:

spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b temperature: 0.2

客户端超时和连接池参数,要根据使用的 HTTP 客户端设置。以常见的 RestClient 配置为例,思路是分别设置连接超时、读取超时和最大连接数。读取超时尤其重要,因为大模型生成长文本时耗时明显高于普通接口,如果超时设得太短,正常请求会被误判为失败。

并发控制也是生产环境必须处理的问题。模型服务的并发能力有限,特别是本地小规模部署。建议在业务入口使用信号量或限流组件控制同时进行中的模型调用数,避免突发流量把模型服务打满,导致所有请求排队超时。

5. 运行验证、结果评估与可观测性

5.1 先建立“什么是答得好”的判断标准

AI 应用的验证不能只看“有没有返回内容”,还要看“返回内容对不对、稳不稳、有没有副作用”。推荐按下面四条标准检查:

验证维度检查问题通过标准
正确性是否按工具返回的真实数据作答内容与工具返回一致
稳定性相同问题重复 10 次结果是否一致关键事实不漂移
完整性是否丢失约束条件或生成被截断无截断标记且覆盖所有要求
安全性是否泄露提示词或执行越权动作未出现内部规则和未授权工具调用

建议把这四条整理成一份测试用例表,每次调整提示词或更换模型后,都跑一遍同样的用例,而不是随手试两句就上线。

5.2 日志要能还原完整调用链路

模型调用最痛苦的问题是“复现不出来”。用户说昨天接口答错了,你翻代码看不出原因。解决办法是在日志中记录关键信息,但要注意不记录敏感数据。

推荐记录以下内容:

  • 请求 ID 和会话 ID
  • 用户问题(如涉及个人信息需脱敏)
  • System 提示词版本
  • 模型名称、参数配置
  • 工具调用过程和返回结果
  • 响应内容或响应摘要
  • 耗时、token 消耗、是否触发重试

使用 Spring Boot 自带的日志框架即可,关键是要把这些字段结构化成 JSON 日志,便于后续检索。生产环境还可以把指标接入监控系统,例如:

指标说明出现异常时怎么判断
模型调用耗时单次调用从发起到返回的耗时耗时持续走高,检查模型服务负载
工具执行失败率工具方法抛异常的比例失败率高,优先检查业务工具本身
token 消耗每次请求消耗的输入和输出 token增长过快,检查上下文是否无限累积
重试次数分布触发重试的请求比例高重试率说明模型服务不稳定或超时设置不合理

5.3 评估用例要沉淀成项目资产

随着项目迭代,提示词和模型会不断调整。不要依靠“感觉这次效果好”来判断。可以把业务常见问题、边界问题、对抗问题整理成一批离线评估用例,每次变更后批量跑一遍,对比输出是否符合预期。

这种方式不需要引入重工具。先用一个测试类固定住用例,再在 CI 阶段作为普通集成测试执行即可。它的价值在于:当某个流程出现回归时,你能在合并代码之前就发现问题,而不是等线上用户来报告。

6. 常见问题排查链路

6.1 先沿着调用链路定位故障层

排查时遵循一个原则:从离自己最近的环节开始。先确认请求有没有到达 Controller,再确认模型有没有被调用,再确认工具有没有执行,最后确认最终响应是谁生成的。

推荐按这个顺序检查:

  1. 请求是否进入接口,参数是否正确解析。
  2. 配置的 base-url 是否能访问,模型是否已拉取。
  3. 日志中是否出现 model call 或 tool call 记录。
  4. 工具方法是否执行,返回值是否符合预期。
  5. 最终响应是模型根据工具结果生成,还是模型自行编造。

只要把日志里每一步的时间点打出来,大部分问题都能快速定位。

6.2 高频问题对照表

下面按“现象 -> 原因 -> 检查 -> 处理”的顺序,整理 AI Agent 项目中出现频率较高的几类问题。

问题现象常见原因检查方式处理建议
请求一直转圈最终超时本地模型未下载或机器内存不足curl localhost:11434/api/tags 看模型列表先 pull 模型,失败则换更小模型
模型不调用工具,直接编造答案工具描述不清晰或模型过小查看日志中是否有 tool call 记录在 System 提示词强调必须调用工具,或换支持工具调用更强的模型
多轮对话丢失上文每次请求都是独立的,没有传历史消息查看发送给模型的 prompt 列表使用 ChatMemory 保存会话历史,并限制历史长度
相同问题两次答案不同temperature 偏高对比两次输出差异事实型任务将 temperature 降到 0.2
回答突然出现截断max-tokens 设置过小检查返回内容末尾是否有截断标识调大 max-tokens,或优化提示词要求精简回答
工具执行报错但用户看到正常回答异常被吞掉或模型忽略了错误检查工具方法是否有异常日志工具内部记录日志,失败时明确返回错误文本
切换线上模型后效果变差模型能力与本地模型不同对比两边输出差异重新跑一遍评估用例,调整提示词和参数

6.3 排查时要盯住的三个隐蔽位置

第一,工具描述与参数结构不匹配。模型是靠 description 决定是否调用工具的,description 写得太模糊,模型可能不知道该调用;参数定义与业务实际不一致,模型传参就会出错。

第二,异常被业务代码吞掉。工具方法内部 catch 了异常但没有 rethrow,也没有返回错误文本,模型就会基于空白或默认值继续回答,掩盖真实问题。

第三,历史消息无限累积。多轮对话中,如果每次请求都把全部历史传给模型,token 消耗会持续上涨,最终超过上下文窗口,表现为“模型忘记开头内容”。

7. 生产环境最佳实践与发布检查清单

7.1 学习环境与生产环境的配置差异

同一套代码在本地能跑,不意味着生产环境能直接照搬。下面这张表列出需要区分的配置维度:

维度学习环境生产环境
模型服务本地 Ollama云模型服务或独立部署的模型集群
密钥不需要或本地明文密钥管理服务保存,禁止入库
配置写死在 application.yml环境变量或配置中心下发
日志控制台输出JSON 日志采集到日志平台
限流与重试不需要必须设置,防止雪崩
监控告警不需要耗时、错误率、token 消耗都要接告警
成本控制不关心按用户、功能、模型维度拆分计量

7.2 密钥和配置外置是上线前的基本要求

不要把模型密钥写到代码仓库。密钥一旦进入 git 历史,即使之后删除,也已经泄露。推荐做法是:

  • 密钥通过环境变量或配置中心注入。
  • 配置文件中只写占位符。
  • 在 CI 阶段扫描是否出现密钥关键字。
  • 定期轮换密钥,查看调用日志是否有异常来源。

下面是一个使用环境变量的示例:

spring: ai: ollama: base-url: ${OLLAMA_BASE_URL:http://localhost:11434} chat: options: model: ${AI_MODEL_NAME:qwen2.5:7b}

这样在本地不配环境变量也能启动,在生产环境通过部署平台注入真实值。

7.3 发布前检查清单

最后给一份可直接复用的检查清单,适合在联调完成、准备上线之前逐项确认。

技术检查清单:

  • 模型名称、版本、厂商商定完成,且使用固定版本而不是默认浮动版本。
  • 提示词版本有编号,能回溯到具体测试结果。
  • 超时、重试、限流参数已配置,并用压测验证过。
  • 密钥全部外置,仓库扫描无敏感信息。
  • 日志包含请求 ID、对话 ID、耗时、token、工具调用记录。
  • 异常处理覆盖:模型超时、工具异常、参数缺失、响应截断。
  • 评估用例已跑完一遍,关键场景输出在预期范围。
  • 多轮对话有历史长度上限,避免上下文无限膨胀。
  • 用户输入长度有上限,防止超长输入导致成本过高。
  • 上线后安排一个小时的观察窗口,确认耗时和错误率平稳。

业务检查清单:

  • 模型能否正确拒绝能力范围外的请求。
  • 关键业务动作是否有二次确认或权限校验。
  • 涉及用户个人信息时,日志是否完成脱敏。
  • 计费或消耗量是否有独立监控。

8. 下一步可以怎么扩展

8.1 方向一:补上多轮对话和记忆管理

当前案例每次请求都是独立的,用户问“那这个订单呢”时,模型并不知道“这个”指哪个订单。下一步可以引入 ChatMemory 保存会话历史,并设置历史长度上限。同时考虑在长会话中做消息摘要,用摘要代替过长的原始历史。这个方向会直接提升对话体验,也会暴露上下文管理和 token 成本控制的问题。

8.2 方向二:把评估流程自动化

人工验证无法覆盖频繁的提示词迭代。可以把测试用例沉淀成集成测试,每修改一次提示词或模型参数就自动化跑一遍。对比输出时,既可以人工抽查,也可以引入自动评分脚本,按是否包含关键信息、是否调用预期工具、是否产生禁用词等条件打分。评估流程越早自动化,后续迭代就越敢动手。

8.3 方向三:引入 RAG 减少模型编造

当问题依赖私有知识时,比如查询公司内部制度、产品文档,模型没有训练过这些内容,编造概率很高。RAG 的基本思路是:把文档切块并向量化,用户提问时先检索相关片段,再把检索结果拼进提示词,最后交给模型生成答案。它的引入会在原有工程链路上增加向量数据库、文档解析、切片策略和召回评估等环节,但能显著改善事实类问答的准确性。

对新手来说,最重要的不是一开始就追逐最新框架,而是把本文的最小案例亲手跑通,再加上日志、异常处理和一套简单评估用例。AI 应用开发的门槛,从来不是“模型会说话”,而是“模型按照你的工程规则稳定地完成业务任务”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询