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 / Milvus | RAG 场景的向量检索 |
| 编排能力 | 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”两个字吓住,把它当成一个有点特殊的外部依赖来对待,用你熟悉的工程方法去治理它,路就走对了。