news 2026/11/2 8:54:30

Java开发者AI应用工程实战:Spring AI从入门到RAG与Agent

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java开发者AI应用工程实战:Spring AI从入门到RAG与Agent

1. Java 开发者切入 AI 的真实路径与选型逻辑

1.1 为什么 Java 开发者不需要从零学 Python

我身边不少写了五六年 Java 的朋友,一提到 AI 第一反应就是“我是不是得先把 Python 学一遍”。这个念头本身就把门槛抬高了。实际情况是,AI 应用开发早就分成了两层:一层是模型训练和底层算法研究,那确实是 Python 和 PyTorch 的主场;另一层是把大模型能力接进业务系统,也就是常说的 AI 应用工程,这一层拼的是工程能力、系统设计、并发处理、可观测性,恰恰是 Java 开发者最擅长的地盘。

你想想,一个电商系统要接入智能客服,核心难点是模型本身吗?不是。难点在于怎么把模型调用封装成稳定的服务、怎么做限流降级、怎么管理多轮会话上下文、怎么把结果落库、怎么和现有的订单系统打通。这些全是 Java 后端每天在干的事。模型那一层,调用 API 就行,跟调一个第三方支付接口没有本质区别。

所以我的判断很明确:Java 开发者入门 AI,主线应该是AI 应用工程,而不是去卷模型训练。路线图大致是三步——先跑通一次模型调用建立体感,再掌握 Spring AI 这类框架把调用工程化,最后深入到 RAG、Agent、多模型编排这些真实业务场景。这条路线不需要你放弃 Java 技术栈,反而能把你已有的 Spring 生态经验直接复用上。

1.2 工具链全景:从裸调 API 到 Spring AI

刚上手的时候,很多人会直接用 HTTP 客户端去调模型接口,比如用RestTemplate或者OkHttp发一个 POST 请求。这么做能跑通,但很快就会遇到问题:不同厂商的接口格式不一样,参数命名五花八门,切换模型要改一堆代码,流式输出要自己处理 SSE,函数调用要自己拼 JSON Schema。写到最后你会发现,一半时间在写业务,一半时间在写适配层。

Spring AI 出现的意义就在这。它做的事情,本质上和当年 Spring 对 JDBC 做的事情一样——把各家模型厂商的差异抽象成统一的接口。你面向ChatClient编程,底层换 OpenAI、换通义千问、换本地部署的模型,业务代码基本不用动。这就是工具链的价值:不是让你少写几行代码,而是让你的架构不被某一家厂商绑死。

完整的工具链我建议这样搭:

层次推荐工具作用
构建工具Maven / Gradle依赖管理,Spring AI 有 starter
核心框架Spring Boot 3.x + Spring AI统一模型调用抽象
模型接入OpenAI / 通义千问 / Ollama云端或本地模型
向量存储Redis / PGVector / MilvusRAG 场景的向量检索
编排能力Spring AI Alibaba / LangGraph4j复杂 Agent 流程
可观测Micrometer + Actuator调用耗时、Token 消耗监控

这张表不是让你一次全上,而是让你知道每个位置该放什么。新手阶段只需要前两行就能跑起来,后面按需加。

1.3 一个必须先想清楚的问题:Spring AI 还是 LangGraph4j

这是最近被问得最多的问题,我直接给结论。如果你的场景是单轮问答、简单对话、知识库检索,用 Spring AI 就够了,它的ChatClient加Advisor机制能覆盖八成需求。如果你的场景是多步骤推理、条件分支、循环调用工具、多个 Agent 协作,那 LangGraph4j 这种基于状态图的编排框架更合适,因为它把流程显式建模成节点和边,复杂逻辑不会写成一团乱麻。

我的经验是,先用 Spring AI 把简单场景跑通,等你真的遇到“这个流程用 if-else 写不下去了”的时候,再引入 LangGraph4j。不要一上来就上重武器,那会让你在还没理解模型调用本质的时候,先被框架概念淹没。

2. 环境搭建与第一个可运行 Demo

2.1 版本选择与依赖引入的坑

Spring AI 的版本迭代很快,这一点要有心理准备。截至我写这篇内容的时候,稳定可用的是 1.0.x 系列,网上很多教程还停留在 0.8 或 0.9 的里程碑版本,API 差异不小,照抄会报错。所以第一步不是急着写代码,而是去确认你用的版本对应的文档。

依赖引入以 Maven 为例,核心就一个 starter:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>

如果你用的是里程碑版本,还需要额外配置 Spring 的里程碑仓库,否则依赖拉不下来。这个坑我踩过,当时对着报错找了半天,最后发现是仓库地址没配。

<repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> </repositories>

注意:Spring Boot 版本必须和 Spring AI 匹配。Spring AI 1.0.x 要求 Spring Boot 3.3 以上,用 2.x 的 Boot 会直接启动失败。这个在升级项目时特别容易忽略。

2.2 配置模型连接:把密钥管好

配置文件里最基础的三项是密钥、模型名、接口地址:

spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.openai.com chat: options: model: gpt-4o-mini temperature: 0.7

这里我要强调一个实操习惯:密钥永远不要硬编码在配置文件里提交到代码仓库。用环境变量注入,或者用配置中心。我见过太多项目因为把密钥写死在application.yml里,最后泄露出去被人刷了一堆调用。${AI_API_KEY}这种写法,本地开发时在 IDE 的运行配置里设环境变量,线上用容器编排的 Secret 注入,这是基本操作。

temperature这个参数值得说一下。它控制输出的随机性,范围一般 0 到 2。做知识问答、代码生成这类需要确定性的场景,调到 0.2 到 0.3;做创意文案、头脑风暴,可以到 0.8 以上。默认值 0.7 是个中庸选择,但真实项目里几乎都要根据场景调。

2.3 第一个 ChatClient 调用

配置好之后,写一个最简单的 Controller 就能验证链路:

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }

启动项目,访问/chat?message=你好,如果返回了模型的回复,说明整条链路通了。这一步看着简单,但它验证了四件事:依赖引入正确、配置读取正确、网络能通、密钥有效。任何一环出问题,这里都会暴露。

我建议新手在这个 Demo 基础上多试几个参数。比如把.call()换成.stream(),返回类型改成Flux<String>,你就能看到流式输出的效果。流式输出在真实产品里几乎是标配,因为用户等一个完整回复要好几秒,体验很差,而逐字返回能让用户立刻看到反馈。

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(@RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }

提示:流式接口的produces必须设成text/event-stream,否则前端拿到的不是标准的 SSE 格式,处理起来会麻烦。

3. 从 Demo 到可用:核心能力逐个击破

3.1 提示词模板:别把提示词写死在代码里

Demo 阶段我们直接把用户输入丢给模型,但真实业务里,提示词往往是一段结构化的模板。比如客服场景,你需要先给模型设定角色、背景、约束,再拼接用户问题。如果把这些字符串硬编码在 Java 代码里,改一次提示词就要重新编译部署,效率极低。

Spring AI 提供了PromptTemplate,把提示词外置成模板:

String template = """ 你是一名{role},请用{style}的风格回答用户问题。 用户问题:{question} """; PromptTemplate promptTemplate = new PromptTemplate(template); Prompt prompt = promptTemplate.create(Map.of( "role", "电商客服", "style", "简洁专业", "question", userInput ));

这样提示词可以放到配置文件甚至数据库里,运营人员都能改。我实际项目里的做法是,把提示词模板存到数据库,配一个简单的管理后台,改完即时生效,不用发版。这个设计在需要频繁调优提示词的场景下,能省下大量时间。

3.2 多轮对话与上下文管理

单轮问答很简单,但用户真正需要的是多轮对话。模型本身是无状态的,它不记得上一句说了什么,所谓“记忆”其实是每次请求都把历史消息一起发过去。

Spring AI 用ChatMemory来管理这个:

ChatMemory memory = MessageWindowChatMemory.builder() .maxMessages(20) .build(); ChatClient chatClient = builder .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build();

maxMessages控制保留多少条历史消息。这里有个关键权衡:保留越多,模型越能记住上下文,但 Token 消耗越大,成本越高,而且超过模型上下文窗口后还会报错。20 条是个常见起点,具体要看你单条消息的长度和模型的窗口大小。

注意:MessageWindowChatMemory是简单按条数截断的,更聪明的做法是按 Token 数截断,或者对早期对话做摘要压缩。生产环境里,长会话一定要做摘要,否则成本会失控。

3.3 结构化输出:让模型返回能直接用的对象

模型返回的是自然语言文本,但业务系统需要的是结构化数据。比如你让模型从一段用户描述里提取订单信息,你希望拿到的是一个Order对象,而不是一段“好的,我提取到的订单号是……”的文字。

Spring AI 支持直接把输出映射成 Java 对象:

public record OrderInfo(String orderNo, String product, Integer quantity) {} OrderInfo info = chatClient.prompt() .user("从这段话提取订单信息:" + text) .call() .entity(OrderInfo.class);

底层它会自动在提示词里加上格式说明,并把返回的 JSON 反序列化成对象。这个能力在对接业务系统时极其有用,省掉了大量正则解析的脏活。

但这里有个坑:模型不一定每次都返回合法 JSON。所以生产代码里一定要做异常兜底,解析失败时要么重试,要么降级到人工处理。我一般会包一层 try-catch,失败后把原始返回记下来,方便排查。

3.4 函数调用:让模型能操作你的系统

函数调用是 AI 应用从“聊天”走向“干活”的关键。它的原理是:你把一些可用的函数描述给模型,模型判断需要调用哪个,返回调用意图和参数,你的代码执行完再把结果喂回模型,模型据此生成最终回复。

在 Spring AI 里,定义一个工具函数:

public class WeatherService { @Tool(description = "查询指定城市的天气") public String getWeather(@ToolParam(description = "城市名") String city) { return weatherApi.query(city); } }

注册到 ChatClient:

ChatClient chatClient = builder .defaultTools(new WeatherService()) .build();

用户问“北京今天天气怎么样”,模型会自动决定调用getWeather("北京"),拿到结果后组织成自然语言回复。整个过程对用户是透明的。

这个能力打开了巨大的想象空间:查订单、下单、改地址、查库存,全都可以通过函数调用让模型来驱动。但安全上必须谨慎,涉及写操作、资金、权限的函数,一定要加二次确认和权限校验,不能让模型直接执行。我见过有人把删除接口暴露成工具函数,结果模型理解偏差就删了数据,这种事故完全可以避免。

4. RAG 与 Agent:真实业务场景的落地

4.1 RAG 解决什么问题

模型的知识有截止日期,也不知道你公司内部的文档。你问它“我们产品的退款政策是什么”,它只能瞎编。RAG(检索增强生成)就是解决这个问题的:先把你的文档切块、向量化、存进向量库,用户提问时先检索出最相关的几段,拼进提示词让模型基于这些内容回答。

整个流程分两个阶段。离线阶段:文档加载、切分、向量化、入库。在线阶段:问题向量化、相似度检索、拼接上下文、调用模型。

切分这一步最容易被低估。切得太碎,语义不完整;切得太大,检索精度下降还浪费 Token。我的经验是,中文文档按 500 到 800 字切,块之间保留 10% 到 20% 的重叠,避免关键信息正好被切断。

4.2 向量库选型:别一上来就上 Milvus

向量库的选择上,新手容易犯的错是直接上 Milvus 这种专业向量数据库。其实如果你的数据量在百万级以下,用 Redis 或者 PostgreSQL 的 PGVector 扩展完全够用,而且能和现有系统共用一套运维体系,省心得多。

方案适用规模优势代价
Redis十万级已有 Redis 直接复用检索能力相对基础
PGVector百万级和业务数据同库,事务一致需要 PG 运维经验
Milvus千万级以上专业检索,性能强独立运维成本高

我的建议是,从 PGVector 起步。它最大的好处是向量和业务数据在同一个数据库里,做权限过滤、关联查询时特别方便。比如你只想检索某个用户有权限的文档,直接在 SQL 里加 where 条件就行,不用在应用层做复杂的过滤。

4.3 Agent 编排:什么时候需要状态图

简单 RAG 是“检索一次,回答一次”,但真实任务往往需要多步。比如用户说“帮我分析上季度销售数据并生成报告”,这需要:查数据库、做统计、生成图表、写文字总结,可能还要根据中间结果决定下一步做什么。

这种场景用 if-else 硬写会非常痛苦,因为流程是动态的。LangGraph4j 把流程建模成状态图,每个节点是一个处理步骤,边是转移条件,状态在节点间流转。这样复杂逻辑变得可视、可调试、可复用。

但我要泼盆冷水:不是所有场景都需要 Agent。很多团队被概念带偏,明明一个固定流程能解决的问题,非要上 Agent,结果调试困难、成本高、还不稳定。判断标准很简单:如果你的流程步骤是固定的,用工作流引擎或者普通代码编排就行;只有当“下一步做什么取决于上一步的结果”时,才需要 Agent。

4.4 成本与性能的实战控制

AI 应用上线后,最现实的两个问题是成本和延迟。我总结几个实测有效的做法。

成本控制:一是缓存,相同或相似的问题直接返回缓存结果,尤其是 FAQ 类场景,命中率能到 40% 以上;二是模型分级,简单问题用小模型,复杂问题才用大模型;三是控制上下文长度,历史消息做摘要,检索结果只取 Top 3 而不是 Top 10。

延迟优化:一是流式输出,让用户感知到的首字延迟降到几百毫秒;二是并行调用,多个独立的模型请求同时发;三是预热连接池,避免每次请求都重新建连。

提示:一定要在项目里加 Token 消耗的监控。用 Micrometer 把每次调用的输入输出 Token 数打点,接上 Prometheus 和 Grafana,你才能知道钱花在哪了。没有监控的 AI 应用,成本就是一笔糊涂账。

5. 常见问题与排查实录

5.1 启动就报错的几类原因

新手最常卡在启动阶段。我把遇到过的整理成一张速查表:

现象大概率原因解决方向
依赖拉不下来缺里程碑仓库加 spring-milestones 仓库
启动报 Bean 创建失败密钥未配置检查环境变量是否注入
版本冲突Boot 与 AI 版本不匹配对齐版本矩阵
连接超时网络或 base-url 错误检查地址和网络连通性

其中版本冲突最隐蔽,因为报错信息往往指向别的地方。我的习惯是,新建项目时先只引入 Spring AI 一个依赖,跑通再加别的,这样出问题容易定位。

5.2 调用返回异常的处理

运行起来之后,会遇到各种调用异常。常见的有:返回内容被截断(达到 max tokens 上限)、返回空内容(提示词触发了内容策略)、超时(模型响应慢)。

处理这些异常的核心思路是分类重试。超时和限流可以重试,内容策略问题重试没用,得改提示词。我一般会封装一个统一的调用包装器,把异常分类,可重试的走退避重试,不可重试的直接降级。

public String safeCall(String prompt) { try { return chatClient.prompt().user(prompt).call().content(); } catch (RateLimitException e) { // 限流,退避重试 return retryWithBackoff(prompt); } catch (ContentFilterException e) { // 内容策略,降级 return "抱歉,我无法回答这个问题。"; } }

5.3 输出不稳定的应对

模型输出不稳定是常态,同样的输入两次结果可能不一样。应对方法有几个层次。第一,把 temperature 调低,减少随机性。第二,用结构化输出,强制格式。第三,在提示词里给示例,few-shot 能显著提升稳定性。第四,加输出校验,格式不对就重试。

我踩过最深的一个坑是,早期做信息抽取时没做校验,模型偶尔返回带 markdown 代码块的 JSON,解析直接失败。后来在提示词里明确要求“只返回 JSON,不要任何额外文字”,并在解析前先剥离代码块标记,问题才解决。

5.4 几个容易被忽略的细节

最后分享几个实操中总结的小点。第一,日志里不要打印完整的提示词和返回,可能包含敏感信息,只打长度和摘要。第二,给模型调用设置独立的线程池,别和业务线程混用,否则模型慢会拖垮整个服务。第三,本地开发可以用 Ollama 跑小模型,省钱又能离线调试,但要注意本地模型能力和云端差距很大,别用它来评估最终效果。第四,提示词要版本化管理,每次调整都记录改了什么、效果如何,否则调着调着就忘了哪版最好。

我个人在实际操作中的体会是,Java 开发者做 AI 应用,最大的优势不是算法,而是工程化能力。模型调用本身不难,难的是把它做成稳定、可观测、成本可控的生产系统,而这恰恰是 Java 生态最擅长的。所以别被“AI”两个字吓住,把它当成一个有点特殊的外部依赖来对待,用你熟悉的工程方法去治理它,路就走对了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 8:54:00

蓝绿与灰度环境实战:从线上事故到发布体系重构

1. 从一次线上事故说起&#xff1a;为什么我们需要多套环境去年冬天的一个周五下午&#xff0c;我正端着咖啡准备收工&#xff0c;运维群里突然炸了锅——新版本上线后&#xff0c;核心接口的报错率在十分钟内从0.1%飙到了17%。更难受的是&#xff0c;当时采用的是"停机替…

作者头像 李华
网站建设 2026/11/2 8:53:33

B端系统架构演进:从三层架构到微服务的实战经验

1. 项目概述去年这个时候&#xff0c;我们团队负责的XX服务正式上线了。作为核心开发成员之一&#xff0c;我完整经历了从架构设计到上线的全过程&#xff0c;也见证了这一年来的各种起起落落。今天想用这篇长文记录下这段难忘的经历&#xff0c;既是对过去工作的复盘&#xff…

作者头像 李华
网站建设 2026/11/2 8:53:09

声网财报解读:实时音视频云服务市场趋势分析

1. 声网母公司最新财报深度解读今天看到声网母公司最新发布的财报数据&#xff0c;作为实时音视频云服务行业的长期观察者&#xff0c;我仔细研读了这份报告。3816万美元的季度营收和490万美元的净利润&#xff0c;这两个数字背后反映了不少行业趋势和企业发展状况&#xff0c;…

作者头像 李华
网站建设 2026/11/2 8:51:52

具身智能机器人在工业自动化中的应用与核心技术

1. 具身智能机器人的工业革命车间里&#xff0c;一台六轴机械臂正以惊人的精度抓取传送带上的金属零件。与传统自动化设备不同&#xff0c;它能在0.5秒内自主识别零件类型、检测表面缺陷&#xff0c;并根据不同材质自动调整抓取力度——这就是具身智能&#xff08;Embodied Int…

作者头像 李华
网站建设 2026/11/2 8:51:01

M0-markconv:轻量级Markdown文档转换工具解析

1. M0-markconv项目背景解析M0-markconv是一个专注于Markdown文档转换的开源工具库&#xff0c;由开发者基于实际文档处理需求独立构建。我在技术文档协作场景中经常遇到这样的痛点&#xff1a;不同平台对Markdown语法支持存在差异&#xff0c;团队内部文档格式不统一&#xff…

作者头像 李华
网站建设 2026/11/2 8:49:05

vLLM 0.27重构:跨GPU可移植推理架构深度解析

1. 这不是“重构”&#xff0c;是GPU生态裂变下的生存性重写vLLM最近把整个底层抽象层推倒重来&#xff0c;这事在推理框架圈子里炸开了锅。很多人第一反应是&#xff1a;“又来&#xff1f;刚用熟的Engine API、AsyncLLMEngine、ModelRunner还没捂热&#xff0c;怎么又要换&am…

作者头像 李华