ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java开发者AI应用集成实战:Spring AI与LangChain4j路线图

Java开发者AI应用集成实战:Spring AI与LangChain4j路线图

1. Java 开发者切入 AI 的真实路径与核心思路

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

我做了十多年 Java 后端,这两年身边问得最多的问题就是“搞 AI 是不是必须转 Python”。说实话,这个判断只对了一半。如果你要训练大模型、发论文、做底层算子优化,Python 确实是主战场。但如果你是想把 AI 能力集成到现有的 Java 业务系统里——比如给订单系统加一个智能客服、给 CRM 加一个语义搜索、给报表系统加一个自然语言查询——那你完全没必要推翻自己已有的技术栈。

Java 在企业级应用里的地位没有被动摇。银行、保险、电商、政务、制造,这些行业的核心系统绝大多数还是跑在 JVM 上。AI 要产生业务价值,最终得落到这些系统里。而落地的过程,就是 Java 开发者的机会。你不需要成为算法专家,但你需要知道怎么把大模型的能力“接进来”,怎么管理提示词,怎么处理上下文,怎么做检索增强,怎么控制成本和安全边界。这些恰恰是 Java 工程师的强项:工程化、稳定性、可观测性、事务管理。

所以我的核心观点是:Java 开发者入门 AI,路线应该是“应用集成优先,原理理解辅助,工具链补齐短板”。不要一上来就去啃 Transformer 的数学推导,也不要急着装 CUDA 环境。先从 Spring AI 或者 LangChain4j 这样的框架入手,把一个大模型调用跑通,再逐步深入。

1.2 路线图的三层结构:接入层、编排层、数据层

我把 Java 开发者进入 AI 的路线拆成三层,这个结构在我自己的项目里反复验证过,比较务实。

接入层解决的是“怎么让 Java 程序和大模型说上话”。核心是 HTTP 客户端、API 密钥管理、超时重试、流式响应处理。Spring AI 在这一层做了很好的封装,它把 OpenAI、Azure OpenAI、Anthropic、Ollama 等不同提供商的接口统一成一套ChatClientAPI。你不需要为每个厂商写一套适配代码。

编排层解决的是“怎么让大模型按业务逻辑干活”。这包括提示词模板、多轮对话记忆、函数调用(Function Calling)、输出解析、链式调用。Spring AI 的PromptTemplate、ChatMemory、@Tool注解都是干这个的。LangChain4j 在这一层更灵活,适合复杂流程。

数据层解决的是“怎么让大模型知道你的私有知识”。这就是 RAG(检索增强生成)的范畴。你需要把文档切块、向量化、存进向量数据库,然后在用户提问时先检索再生成。Spring AI 支持 PGVector、Redis、Milvus、Chroma 等常见向量库,Java 开发者熟悉的 PostgreSQL 加 pgvector 扩展是最容易上手的组合。

这三层不需要一次性全上。我的建议是先跑通接入层,做一个最简单的问答接口;然后加编排层,让对话有记忆、能调用工具;最后再上数据层,接入企业知识库。

1.3 工具链选型:Spring AI 还是 LangChain4j

这是被问得最多的问题。我的答案取决于你的项目形态。

如果你的项目已经是 Spring Boot 体系,团队熟悉 Spring 的编程模型,那 Spring AI 是首选。它的优势是“Spring 原生”——自动配置、依赖注入、Actuator 监控、配置属性绑定,这些你都熟。Spring AI 1.0 之后 API 稳定了很多,ChatClient的流式 API 和Advisor机制设计得比较优雅。Spring AI Alibaba 还额外提供了对通义千问等国内模型的支持,以及 NL2SQL 之类的场景化组件。

如果你需要更复杂的链式编排、更细粒度的控制,或者项目不是 Spring 体系,LangChain4j 更合适。它的AiServices可以把接口自动实现成 AI 代理,RetrievalAugmentor的检索策略也更丰富。缺点是文档相对分散,版本迭代快,需要花点时间跟。

还有一个选择是直接裸调 HTTP API。我不推荐在生产项目里这么做,但学习阶段可以试试,能帮你理解框架到底封装了什么。用java.net.http.HttpClient发一个 POST 请求到模型接口,手动拼 JSON,手动解析 SSE 流,跑一遍你就明白 Spring AI 帮你省了多少事。

注意:无论选哪个框架,都要把 API 密钥放在环境变量或配置中心,绝对不要硬编码在代码里,更不要提交到代码仓库。这是最基本的安全底线。

2. 核心细节解析与实操要点

2.1 环境准备:JDK、构建工具与依赖管理

Spring AI 目前要求 JDK 17 及以上,我建议直接用 JDK 21,虚拟线程对 IO 密集型的 AI 调用场景有实际收益。构建工具用 Maven 或 Gradle 都行,Spring AI 的 BOM 管理做得不错,版本冲突比早期少了很多。

Maven 项目里,先在dependencyManagement引入 Spring AI BOM:

<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>

然后按需引入 starter。用 OpenAI 就加spring-ai-openai-spring-boot-starter,用 Ollama 本地模型就加spring-ai-ollama-spring-boot-starter。如果你在国内,Spring AI Alibaba 的spring-ai-alibaba-starter可以直接对接通义千问,省去网络层面的折腾。

配置文件里至少要设置三样东西:API 地址、API 密钥、模型名称。以 OpenAI 兼容接口为例:

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

temperature控制输出的随机性。做客服问答建议 0.2 到 0.5,做创意文案可以到 0.8 以上。这个参数不是越高越好,高温度意味着更发散但也更容易胡说。

2.2 第一个可运行的 AI 接口:从 Controller 到 ChatClient

环境搭好后,写一个最简单的对话接口。Spring AI 的自动配置会给你一个ChatClient.Builder,注入后构建ChatClient即可。

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个专业的客服助手,回答要简洁准确。") .build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }

这段代码跑起来,你就已经完成了 Java 接入 AI 的第一步。defaultSystem设置的是系统提示词,它决定了模型的角色和行为边界。很多人忽略系统提示词的重要性,实际上它比你在用户消息里写一堆“请认真回答”有效得多。

流式输出稍微复杂一点,返回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(); }

前端用 EventSource 接收,就能实现打字机效果。这里有个坑:如果你在流式响应过程中抛异常,Spring 默认的错误处理可能不会正确关闭 SSE 连接,需要在全局异常处理器里特殊处理。

2.3 提示词模板与结构化输出

实际项目里,用户输入不能直接拼进提示词,否则会有注入风险,也不利于维护。Spring AI 的PromptTemplate用{placeholder}占位:

PromptTemplate template = new PromptTemplate(""" 请根据以下产品信息回答用户问题。 产品名称:{product} 产品特点:{features} 用户问题:{question} 如果产品信息中没有相关内容,请回答"暂无相关信息"。 """); Prompt prompt = template.create(Map.of( "product", "智能音箱X1", "features", "支持语音控制、续航12小时、防水等级IPX5", "question", userQuestion ));

结构化输出是另一个高频需求。你希望模型返回 JSON,而不是一段自然语言。Spring AI 提供BeanOutputConverter,可以把模型输出直接映射成 Java 对象:

record ProductReview(String sentiment, int score, List<String> tags) {} BeanOutputConverter<ProductReview> converter = new BeanOutputConverter<>(ProductReview.class); String result = chatClient.prompt() .user(u -> u.text("分析这条评论的情感:{review}") .param("review", reviewText)) .call() .content(); ProductReview parsed = converter.convert(result);

实测下来,BeanOutputConverter会在提示词里自动附加格式说明,但模型偶尔还是会返回带 markdown 代码块的 JSON。稳妥的做法是在系统提示词里明确“只返回 JSON,不要任何其他文字”,并且在解析前做一次清洗。

提示:结构化输出的可靠性跟模型能力直接相关。小模型在复杂结构上容易出错,生产环境建议加一层校验和重试。如果解析失败,把错误信息拼回提示词让模型重新生成,通常第二次就能对。

3. 实操过程与核心环节实现

3.1 RAG 落地:从文档到可检索的知识库

RAG 是 Java 开发者做 AI 应用最有价值的一块。它的逻辑不复杂:把企业文档切块、转向量、存库;用户提问时,先把问题转向量,在库里找最相似的几块,拼进提示词让模型基于这些内容回答。

第一步是文档读取。Spring AI 提供了DocumentReader接口,常见格式有TextReader、JsonReader、PdfReader(需要额外依赖)。如果是 Word 文档,可以用 Apache POI 先转成文本,再交给 Spring AI 处理。这里有个经验:PDF 里的表格和双栏排版经常解析乱序,重要文档建议人工校对一遍。

第二步是切块。TokenTextSplitter是默认实现,按 token 数切,默认块大小 800 token,重叠 350 token。重叠是为了避免一个完整语义被切断。如果你的文档是技术手册,按章节切可能比按 token 切更合理,可以自定义TextSplitter。

TokenTextSplitter splitter = new TokenTextSplitter(800, 350, 5, 10000, true); List<Document> chunks = splitter.apply(documents);

第三步是向量化。EmbeddingModel负责把文本转成浮点数组。OpenAI 的text-embedding-3-small性价比不错,国内可以用通义千问的 embedding 模型。注意 embedding 模型和对话模型是两回事,要分别配置。

第四步是存储和检索。用 PostgreSQL + pgvector 的话,先建表:

CREATE TABLE vector_store ( id UUID PRIMARY KEY, content TEXT, metadata JSONB, embedding vector(1536) ); CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);

Spring AI 的PgVectorStore会自动处理建表和读写。检索时设置similarityThreshold和topK,前者过滤掉相似度过低的结果,后者控制返回条数。topK 不是越大越好,太多无关内容会稀释提示词的有效信息,一般 3 到 5 条够用。

3.2 对话记忆与多轮上下文管理

单轮问答不需要记忆,但客服、助手类应用必须记住上下文。Spring AI 的ChatMemory接口有两个实现:InMemoryChatMemory和基于 JDBC 的持久化实现。生产环境肯定用后者,否则重启就丢。

ChatMemory memory = new JdbcChatMemory(dataSource, JdbcChatMemoryConfig.builder() .maxMessages(20) .build()); ChatClient client = builder .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build();

maxMessages控制保留多少条历史消息。设太大有两个问题:一是 token 成本线性增长,二是模型在长上下文里容易“迷失”,忘记最早的系统指令。我的经验是保留最近 10 到 20 轮,更早的内容用摘要代替。

摘要的做法是:当消息数超过阈值时,调一次模型把前面的对话压缩成一段摘要,存进系统消息。这样既保留了关键信息,又控制了 token 消耗。这个逻辑需要自己写,Spring AI 没有内置,但实现起来不复杂。

3.3 函数调用:让 AI 操作你的业务系统

函数调用(Function Calling)是 AI 从“聊天”走向“干活”的关键。你告诉模型有哪些工具可用,模型根据用户意图决定调哪个,你的 Java 代码执行后把结果返回给模型,模型再组织成自然语言。

Spring AI 用@Tool注解标记方法:

@Component public class OrderTools { @Tool(description = "根据订单号查询订单状态") public String queryOrderStatus(@ToolParam(description = "订单号") String orderId) { // 实际查库逻辑 return orderService.getStatus(orderId); } }

然后在 ChatClient 里注册:

ChatClient client = builder .defaultTools(new OrderTools()) .build();

用户问“我的订单 12345 到哪了”,模型会自动识别出要调queryOrderStatus,参数是12345。你不需要写任何意图识别的代码。

这里有几个实操要点。第一,工具方法的描述要写清楚,模型靠描述来判断什么时候调用。第二,参数类型尽量用 String、int 这类简单类型,复杂对象容易解析失败。第三,工具方法里要做好异常处理,如果抛异常,要把错误信息友好地返回给模型,让它告诉用户“查询失败,请稍后重试”,而不是直接 500。

注意:函数调用意味着模型可以触发你的业务逻辑,权限控制必须在工具方法内部做,不能依赖模型判断。比如查询订单时,要校验当前登录用户是否有权查看该订单,这个校验写在queryOrderStatus里,而不是指望模型“自觉”。

4. 常见问题与排查技巧实录

4.1 连接超时与流式中断

AI 接口的响应时间波动很大,尤其是长文本生成,几十秒很正常。默认的 HTTP 超时往往不够用。Spring AI 的 OpenAI 客户端可以配置超时:

spring: ai: openai: chat: options: timeout: 60s

流式场景下,如果网络抖动导致 SSE 连接断开,前端要能自动重连。后端的Flux要正确处理onError,不要直接把异常抛给客户端。我的做法是在流式接口外面包一层重试逻辑,但要注意:重试意味着重新生成,已经输出的内容会重复,所以重试只适合在还没有任何 token 输出时触发。

4.2 模型返回格式不符合预期

结构化输出解析失败是最常见的问题。排查步骤:第一,打印原始返回内容,看模型到底返回了什么;第二,检查提示词里的格式说明是否足够明确;第三,确认模型是否支持结构化输出(有些小模型不支持 JSON mode);第四,在解析前做清洗,去掉 markdown 代码块标记和前后空白。

如果反复失败,可以降级处理:先让模型返回自然语言,再用正则提取关键字段。虽然不够优雅,但比直接报错强。

4.3 向量检索召回不准

RAG 效果不好,八成出在检索环节。排查方向:切块大小是否合适(太大导致语义稀释,太小导致上下文断裂);embedding 模型是否匹配(中英文混排要用多语言模型);相似度阈值是否合理(太高召回少,太低引入噪声);topK 是否够用。

我踩过的一个坑是:文档里有很多表格,切块后表格被切碎,检索出来的内容残缺不全。后来改成先把表格转成 markdown 格式再切块,效果好很多。另一个坑是:用户提问的口语化表达和文档的书面表达差异大,直接向量检索匹配不上。解决办法是加一层“查询改写”,让模型先把用户问题改写成更正式的检索语句,再去向量库查。

4.4 成本失控与限流

AI 调用的成本很容易失控,尤其是 RAG 场景,每次问答都要调 embedding 和 chat 两个接口。控制手段有几个:设置单次请求的 max token;对相同问题做缓存;对 embedding 结果做缓存,避免重复文档反复向量化;监控每日调用量和费用,设阈值告警。

限流方面,模型提供商通常有 RPM(每分钟请求数)和 TPM(每分钟 token 数)限制。Spring AI 本身没有内置限流,需要自己用 Resilience4j 或 Sentinel 做。我的做法是在 ChatClient 外面包一层 RateLimiter,超过阈值直接返回友好提示,而不是让请求堆积导致超时。

问题现象可能原因排查动作解决方向
连接超时默认超时太短查看日志中的 timeout 异常调大 timeout 配置
流式中断网络抖动或服务端断开检查 SSE 连接状态前端自动重连,后端处理 onError
JSON 解析失败模型输出带额外文字打印原始返回清洗输出,加强格式提示
检索不准切块或 embedding 问题检查召回内容调整切块策略,换 embedding 模型
成本过高无缓存、token 无限制统计调用量和费用加缓存,设 max token
触发限流超过 RPM/TPM查看 429 响应加限流器,错峰调用

4.5 本地模型与离线场景

有些场景不能用云端模型,比如数据敏感的内网系统。这时候可以用 Ollama 在本地跑开源模型。Spring AI 对 Ollama 的支持很完整,配置里把 base-url 指向本地 Ollama 服务即可。缺点是本地模型能力有限,7B 参数的模型做简单问答还行,复杂推理和结构化输出容易翻车。如果硬件允许,13B 或 32B 的模型体验会好很多,但显存要求也上去了。

我的建议是:开发和测试阶段用本地模型省钱,生产环境根据数据敏感度决定用云端还是本地。混合方案也可以——敏感数据走本地,通用问答走云端。

5. 从能跑到好用:工程化收尾的几个关键点

5.1 可观测性:日志、指标与追踪

AI 应用的调试比传统应用难,因为输出是不确定的。必须把每次请求的输入、输出、耗时、token 消耗都记下来。Spring AI 的ChatClient支持注册Advisor,可以在调用前后插入日志逻辑。Micrometer 指标可以记录调用次数、延迟分布、错误率。如果用了多个模型提供商,还要按提供商打标签,方便对比。

追踪方面,OpenTelemetry 可以串起一次请求里的多次模型调用和向量检索。虽然 AI 调用的链路不像微服务那么复杂,但 RAG 场景下“检索-生成”两步的耗时占比很关键,追踪能帮你定位瓶颈在检索还是在生成。

5.2 提示词版本管理

提示词是 AI 应用的“代码”,但很多人把它硬编码在 Java 字符串里,改一次就要重新部署。更好的做法是把提示词抽到配置文件或数据库里,支持热更新和版本回滚。A/B 测试不同版本的提示词,看哪个效果更好。这个投入在项目初期可能觉得多余,但一旦提示词数量超过十个,没有版本管理会非常痛苦。

5.3 安全边界与内容过滤

模型可能被诱导输出不当内容,也可能泄露系统提示词。防护措施包括:在系统提示词里明确禁止事项;对用户输入做敏感词过滤;对模型输出做二次审核(可以用另一个模型或规则引擎);限制函数调用的权限范围。这些不是可选项,是生产环境的必选项。

我在实际项目里的体会是,Java 开发者做 AI 应用,最大的优势不是算法能力,而是工程能力。模型会选错、会胡说、会超时、会限流,这些都是工程问题,而工程问题正是我们擅长的。把重试、降级、缓存、监控、限流这些手段用上,AI 应用的稳定性可以做到和传统服务一样可靠。最后分享一个小技巧:在项目初期就建一个“提示词库”文档,把每个场景的提示词、预期输出、实际效果都记下来,迭代几轮之后你会发现,调提示词比调代码的收益高得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进