1. Java 开发者切入 AI 的真实路径与全局思路
1.1 为什么 Java 开发者不需要从零学 Python
我做了十多年 Java 后端,这两年身边问得最多的问题就是“要不要转 Python 才能搞 AI”。说实话,这个判断本身就是个误区。AI 工程落地分两层:一层是模型训练和微调,那确实是 Python 生态的天下;另一层是推理服务的集成、编排、治理和业务落地,这一层恰恰是 Java 的主场。企业里绝大多数业务系统是 Java 写的,订单、风控、CRM、ERP 全是 Spring 那一套,你不可能为了接一个大模型把整个技术栈推倒重来。
所以 Java 开发者入门 AI 的正确姿势,不是去啃 PyTorch 和 Transformer 论文,而是把 AI 能力当成一种新的外部依赖来集成。就像你当年接 Redis、接 MQ、接支付网关一样,现在多了一个叫“大模型”的下游服务。这个认知一旦转过来,路线图就清晰了:先跑通调用,再做提示词工程,然后上 RAG,最后做 Agent 编排。每一步都在 Java 体系内完成,用你熟悉的 Spring 生态。
我实测下来,一个熟练的 Java 后端,两周内就能把 Spring AI 的基础调用、流式输出、结构化解析跑通,一个月能上线一个带知识库的问答服务。这个速度比重新学 Python 数据科学栈快得多,而且产出直接能进生产环境。
1.2 路线图的四个阶段与对应工具链
我把这条路线拆成四个阶段,每个阶段都有明确的产出物和工具链,你可以对照自己的进度看卡在哪一层。
| 阶段 | 核心目标 | 关键工具 | 产出物 |
|---|---|---|---|
| 第一阶段:调用打通 | 能稳定调通大模型 API | Spring AI、OkHttp、WebClient | 一个能对话的 REST 接口 |
| 第二阶段:提示词工程 | 让输出可控、可解析 | PromptTemplate、BeanOutputConverter | 结构化 JSON 输出 |
| 第三阶段:RAG 检索增强 | 让模型回答私有知识 | VectorStore、EmbeddingModel、ETL Pipeline | 带知识库的问答服务 |
| 第四阶段:Agent 编排 | 让模型自主调用工具 | Function Calling、ToolCallback、工作流引擎 | 能查库、能下单的智能体 |
这个分层的逻辑是依赖递进:没有稳定的调用层,提示词工程无从谈起;没有结构化的输出,RAG 的召回结果没法喂给模型;没有 RAG 的知识注入,Agent 就是空中楼阁。我见过太多人一上来就想做 Agent,结果连流式响应和超时重试都没处理好,线上直接雪崩。
提示:不要跳过第一阶段直接上 RAG。调用层的超时、重试、限流、降级没做扎实,后面每一层都会把问题放大。
1.3 工具链选型的取舍逻辑
工具链这块,Spring AI 是当前 Java 生态里最顺手的入口,尤其是 1.0 之后的版本,把 ChatClient、EmbeddingModel、VectorStore 这些抽象做得比较干净。但要注意,Spring AI 迭代很快,不同小版本 API 有 breaking change,生产环境一定要锁版本。
向量库的选择上,本地开发我建议先用 SimpleVectorStore 或者内存版,别一上来就搭 Milvus 集群。等数据量到十万级向量以上,再考虑 PGVector 或者 Milvus。Embedding 模型优先选支持中文的,维度不用追求最高,1536 维和 1024 维在实际召回效果上差距没有想象中大,但存储和检索成本差不少。
至于要不要引入 LangChain4j,我的看法是:如果你团队全是 Java 背景,Spring AI 足够;如果你需要更灵活的链式编排和更丰富的社区集成,LangChain4j 可以作为补充。两者不是互斥的,我有个项目就是 Spring AI 做调用层,LangChain4j 做复杂的 Agent 流程。
2. 核心细节解析与实操要点
2.1 Spring AI 的依赖引入与版本锁定
先说过滤掉的一个坑:Spring AI 的 artifact 命名和 Spring Boot 版本强绑定。你 Spring Boot 用 3.2.x,Spring AI 就得选对应的 1.0.x 分支,乱配直接启动报 NoSuchMethodError。
Maven 里核心依赖就两个,BOM 加 starter:
<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.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> </dependencies>配置文件里把 key 和 base-url 配好,注意 base-url 要看你实际用的服务商,别照抄网上的。超时时间一定要显式配,默认值在生产环境偏短:
spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://your-endpoint/v1 chat: options: model: gpt-4o-mini temperature: 0.7 retry: max-attempts: 3 backoff: initial-interval: 1000 multiplier: 2注意:api-key 绝对不要硬编码进代码或提交到仓库,用环境变量或者配置中心。我见过有人把 key 写进 application.yml 推到公开仓库,第二天就被刷爆了额度。
2.2 ChatClient 的构建与流式输出处理
ChatClient 是 Spring AI 里最核心的入口,我习惯把它封装成一个单例 Bean,而不是每次 new。构建方式有两种,推荐用 Builder 模式,因为可以预设 system prompt 和默认参数:
@Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem("你是一个严谨的技术助手,回答要准确、简洁。") .defaultOptions(ChatOptions.builder() .temperature(0.3) .maxTokens(2000) .build()) .build(); }流式输出这块,返回 Flux 直接对接 WebFlux 的 SSE,前端体验会好很多。但有个细节:流式场景下异常处理不能用传统的 try-catch,要用 onErrorResume 兜底,否则一个网络抖动整个连接就断了:
public Flux<String> streamChat(String message) { return chatClient.prompt() .user(message) .stream() .content() .onErrorResume(e -> { log.error("stream error", e); return Flux.just("服务暂时不可用,请稍后重试"); }); }实测下来,流式输出的首字延迟比一次性返回体感好太多,用户等待焦虑明显降低。但要注意,流式场景下 token 统计和计费会复杂一些,如果你的服务商按 token 计费,记得在流结束时汇总用量。
2.3 结构化输出的转换器使用
大模型返回的是自然语言,但你的业务代码需要的是对象。Spring AI 提供了 BeanOutputConverter,能把模型输出直接映射成 Java 对象,这是提示词工程里最实用的一个能力。
用法分两步,先定义目标类,再在 prompt 里挂 converter:
public record ProductInfo(String name, BigDecimal price, List<String> tags) {} BeanOutputConverter<ProductInfo> converter = new BeanOutputConverter<>(ProductInfo.class); String response = chatClient.prompt() .user(u -> u.text("从这段描述里提取商品信息:{desc}") .param("desc", rawText)) .options(ChatOptions.builder() .responseFormat(converter.getFormat()) .build()) .call() .content(); ProductInfo info = converter.convert(response);这里的关键是converter.getFormat()会往 prompt 里注入一段 JSON Schema 说明,模型就会按这个格式输出。但别指望 100% 稳定,我实测大概有 3% 到 5% 的概率模型会多输出解释性文字导致解析失败。所以生产环境一定要加一层容错:解析失败就重试一次,或者用正则把 JSON 块抠出来再解析。
提示:temperature 设低一点(0.1 到 0.3)能显著提升结构化输出的稳定性,创意类任务才需要调高。
2.4 RAG 数据管道的构建细节
RAG 的核心是把私有文档切块、向量化、存库,检索时按相似度召回。Spring AI 的 ETL Pipeline 把这几步串起来了,但每一步都有讲究。
文档读取用 DocumentReader,切块用 TokenTextSplitter。切块大小是个经验活,我一般用 500 到 800 token 一块,重叠 100 token。块太大召回不准,块太小上下文断裂。中文场景下按 token 切比按字符切更合理,因为中英文 token 比例差异很大。
TokenTextSplitter splitter = new TokenTextSplitter(800, 100, 5, 10000, true); List<Document> chunks = splitter.apply(documents); VectorStore vectorStore = ...; vectorStore.add(chunks);向量库这块,开发阶段用 SimpleVectorStore 就够了,它把向量存在内存里,重启就没了,但调试方便。生产环境我推荐 PGVector,因为你大概率已经有 PostgreSQL 了,不用额外维护一套中间件,运维成本最低。
检索时的相似度阈值要调,默认值往往召回太多无关内容。我一般从 0.7 开始试,根据实际召回质量上下调。还有个技巧是加 metadata 过滤,比如按文档类型、时间范围先筛一遍再向量检索,能大幅提升准确率。
3. 实操过程与核心环节实现
3.1 从零搭建一个带知识库的问答服务
我拿一个真实场景走一遍:给内部技术文档做一个问答机器人。整个流程分五步,我按实际操作顺序写。
第一步,准备文档。把 Markdown、PDF、Word 都收集到一个目录,Spring AI 的 DocumentReader 支持多种格式,但 PDF 解析质量参差不齐,扫描件基本没戏,需要先做 OCR。这一步没有捷径,文档质量直接决定最终效果。
第二步,构建 ETL 管道。读取、切块、向量化、入库,写成一段可重复执行的代码:
@Component public class KnowledgeIngestor { private final VectorStore vectorStore; private final EmbeddingModel embeddingModel; public void ingest(Resource resource) { DocumentReader reader = new TextReader(resource); List<Document> docs = reader.get(); TokenTextSplitter splitter = new TokenTextSplitter(800, 100, 5, 10000, true); List<Document> chunks = splitter.apply(docs); vectorStore.add(chunks); log.info("ingested {} chunks", chunks.size()); } }第三步,写检索增强的问答接口。核心是把用户问题先向量化,召回 top-k 相关块,拼进 prompt 的上下文里:
public String ask(String question) { List<Document> relevant = vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.7) .build() ); String context = relevant.stream() .map(Document::getText) .collect(Collectors.joining("\n\n")); return chatClient.prompt() .system(""" 你是一个技术文档助手。只根据下面提供的上下文回答问题, 如果上下文里没有答案,直接说"文档中没有相关内容",不要编造。 上下文: {context} """) .user(question) .call() .content(); }第四步,加引用溯源。把召回的文档块带上来源信息返回给前端,用户能点开看原文。这个功能对信任度提升很大,实现上就是把 Document 的 metadata 一起返回。
第五步,做评估。准备一批标准问题和期望答案,跑一遍看准确率。我一般会记录每次问答的召回块和最终回答,人工抽查几十条,找出召回不准或者模型幻觉的 case,针对性调切块大小和阈值。
3.2 Function Calling 让模型调用你的 Java 方法
Agent 的基础是 Function Calling,让模型在需要的时候调用你定义好的 Java 方法。Spring AI 里用 @Tool 注解就能把一个方法暴露给模型:
@Component public class OrderTools { @Tool(description = "根据订单号查询订单状态") public OrderStatus queryOrder(String orderId) { return orderService.getStatus(orderId); } @Tool(description = "根据用户ID查询最近订单列表") public List<Order> recentOrders(String userId, int limit) { return orderService.recent(userId, limit); } }然后在 ChatClient 里注册这些工具:
ChatClient client = builder .defaultTools(new OrderTools()) .build();模型会根据用户问题自主决定调不调、调哪个、传什么参数。这里有个关键点:方法的 description 写得越清楚,模型调用越准。我踩过的坑是 description 写得太简略,模型经常传错参数类型,比如把订单号当用户ID传。后来我把每个参数的说明也写进 description,准确率明显上来了。
还有个安全考量:工具方法一定要做权限校验和参数校验,不能因为调用方是模型就放松。模型可能被诱导调用不该调的方法,所以敏感操作要在方法内部再校验一次用户身份。
3.3 多轮对话的上下文管理
多轮对话不是简单地把历史消息全塞回去,那样 token 消耗爆炸,而且模型容易被早期无关内容干扰。我的做法是滑动窗口加摘要:保留最近 N 轮完整对话,更早的用模型压缩成一段摘要。
public class ConversationManager { private final ChatClient chatClient; private final int maxRounds = 10; public String chat(String sessionId, String message) { List<Message> history = store.get(sessionId); if (history.size() > maxRounds * 2) { String summary = summarize(history.subList(0, history.size() - maxRounds * 2)); history = rebuildWithSummary(summary, history.subList(history.size() - maxRounds * 2, history.size())); } // 拼接历史和新消息,调用模型 // ... } }摘要这一步本身也要调模型,会增加延迟和成本,所以不是每轮都做,而是超过阈值才触发。实测下来,10 轮窗口加摘要的方案,在客服场景下能覆盖 90% 以上的对话需求,token 消耗比全量历史降低 60% 以上。
注意:会话状态不要存在 JVM 内存里,多实例部署会丢。用 Redis 存,key 带 sessionId,设置合理的过期时间。
4. 常见问题与排查技巧实录
4.1 调用超时与限流的处理
大模型调用最常遇到的问题就是超时和 429 限流。超时方面,流式接口的首字超时和整体超时要分开设,首字超时设短一点(比如 10 秒),整体超时设长一点(比如 120 秒)。限流方面,Spring AI 的 retry 配置能处理一部分,但更稳妥的是在应用层加令牌桶限流,控制并发请求数。
我遇到过一个典型问题:批量任务并发调模型,直接把配额打满,导致线上正常请求全部 429。后来加了信号量控制并发数,批量任务走独立队列,和线上请求隔离,问题就解决了。
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 首字延迟高 | 网络或服务端排队 | 看首字耗时指标 | 换区域节点、降并发 |
| 429 频繁 | 并发超配额 | 统计 QPS | 令牌桶限流、请求排队 |
| 响应截断 | maxTokens 太小 | 检查 finish_reason | 调大 maxTokens |
| 解析失败 | 模型输出格式漂移 | 打印原始响应 | 降 temperature、加重试 |
| 召回不准 | 切块或阈值问题 | 看召回块内容 | 调切块大小、相似度阈值 |
4.2 模型幻觉的抑制手段
幻觉是 RAG 场景下最头疼的问题,模型会一本正经地编造文档里没有的内容。抑制手段有几个层次:最基础的是在 system prompt 里明确要求“只根据上下文回答,没有就说不知道”;进阶一点的是在检索层加相似度阈值,召回质量差的时候直接返回“未找到相关内容”,不调模型;再进一步是做答案校验,把模型回答和召回块做一次相似度比对,偏离太大就标记为可疑。
我实测下来,prompt 约束加阈值过滤能解决 80% 的幻觉问题,剩下的靠人工审核和持续调优。别指望一次做到零幻觉,那不现实。
4.3 成本控制的几个实操技巧
大模型调用是持续成本,控制不好一个月能烧掉不少预算。几个我常用的技巧:第一,简单任务用小模型,复杂任务才用大模型,路由逻辑可以基于问题长度和关键词;第二,缓存高频问题的答案,相同或相似问题直接返回缓存;第三,压缩 prompt,把 system prompt 里冗余的说明精简掉,历史对话做摘要;第四,设置单用户日调用上限,防止异常刷量。
缓存这块要注意,语义缓存比精确匹配缓存命中率高得多,但需要额外做向量检索,有成本。我的经验是精确匹配缓存加短 TTL 就够了,命中率能到 20% 到 30%,性价比最高。
4.4 版本升级的避坑经验
Spring AI 迭代快,升级时最容易踩的坑是 API 签名变化和默认行为调整。我的做法是:生产环境锁死版本,升级前先在测试环境跑全量回归;关注官方 release notes 里的 breaking change;把 Spring AI 相关的调用封装在独立的 service 层,升级时只改这一层,业务代码不动。
还有个小技巧:把模型调用相关的配置全部外置到配置中心,切换模型、调参数不用重新发版。我有个项目从 gpt-4o-mini 切到国产模型,只改了配置中心的几行配置,十分钟搞定。
5. 进阶方向与个人实践体会
5.1 从单 Agent 到工作流编排
单 Agent 能做的事有限,真正复杂的是多步骤工作流。比如一个报销审批 Agent,要先查发票、再核对预算、再走审批流、最后通知。这种场景用 Function Calling 串起来会很脆弱,更适合用工作流引擎编排,每一步的输入输出都显式定义,模型只在需要判断的节点介入。
Spring AI 本身不提供工作流引擎,但可以和现有的流程引擎结合,或者用状态机自己实现。我的做法是把工作流拆成若干节点,每个节点是一个独立的 Spring Bean,节点之间用事件驱动,模型负责节点内的决策。这样既保留了 AI 的灵活性,又有工程上的可控性。
5.2 可观测性建设不能省
AI 服务的可观测性和传统服务不一样,除了常规的 QPS、延迟、错误率,还要记录 token 用量、召回块、模型原始输出、用户反馈。这些数据是后续调优的基础。我一般用 Micrometer 打点,把关键指标接到 Prometheus,再配 Grafana 看板。
特别要记录的是用户反馈,点赞点踩的数据是最宝贵的调优信号。我有个项目就是靠分析点踩的 case,发现是切块策略有问题,调整后准确率提升了 15 个百分点。
5.3 我个人的几点体会
最后说几句掏心窝的话。Java 开发者做 AI,最大的优势是工程能力,最大的劣势是容易用工程思维硬套 AI。AI 有不确定性,你不能像对待数据库事务那样要求它 100% 准确。接受这个前提,把 AI 当成一个“能力很强但偶尔会犯错的实习生”,你的架构设计思路就对了。
另外,别追新追得太狠。Spring AI 每周都在更新,但生产环境要的是稳定。我见过团队为了用最新特性,一个月升级三次,结果线上事故不断。选一个稳定版本,把核心功能做扎实,比什么都强。
还有一点,AI 项目的成败往往不在技术,而在场景选择。选一个容错率高、用户预期合理的场景切入,比如内部知识问答、文档摘要、代码辅助,这些场景即使模型偶尔出错,影响也可控。等跑顺了再往核心业务渗透。
这个方向后续还可以往多模态扩展,图片、语音的接入 Spring AI 也在逐步支持。但那是下一步的事,先把文本这条链路走通走稳,比什么都重要。