将 AI 注入 Java 应用程序
作者 | Daniel Dominguez 译者 | 刘雅梦 策划 | 丁晓昀 AI技术现在越来越普及了。咱们做Java企业开发的,可能都在琢磨:AI到底能给业务应用带来啥价值?用Java搞AI有哪些趁手的工具?得学点啥新技能?别急,这篇文章就帮你打基础,让你轻松上手用AI打造智能又灵敏的Java应用。 这里说的AI,主要指让Java应用和大语言模型(LLM)对话——发送请求、接收响应。我们做了一个星际旅行聊天机器人当例子,用户能咨询星球推荐,还能订飞船。用了LangChain4j和Quarkus这些Java框架,和LLM交互特顺滑,做出来的应用体验也不错。 Hello AI World: 让LLM听懂你的指令 咱们先做个飞船租赁应用的初版:一个能用自然语言和用户聊天的机器人,专门解答太阳系旅行问题。完整代码放在GitHub的“spaceship rental step-01”目录里,随时可看。 用户的问题通过聊天界面发到应用,应用再和LLM交互,处理自然语言并回复。 和AI相关的部分,其实就两个文件: 一个AI服务CustomerSupportAgent.java,负责组织提示词——先告诉LLM太阳系行星的背景,再让它回答用户问题。 一个WebSocket端点ChatWebSocket.java,接收聊天消息。 AI服务是个Java接口,用了LangChain4j后,它就是个抽象层,让和LLM打交道更简单。在实际项目里,你得注意连接LLM时的安全性、可观测性和容错。除了LLM的连接细节(单独放在application.properties里),AI服务还负责组装提示词,并管理发送给LLM的聊天记忆。 提示词由两条信息组成:系统消息和用户消息。系统消息是开发者写的,给LLM提供上下文和指令,常带点例子教它怎么回。用户消息就是应用用户的实际请求。 CustomerSupportAgent接口在应用里注册为AI服务。它定义了怎么建提示词并发给LLM: @SessionScoped@RegisterAiServicepublic interface CustomerSupportAgent { @SystemMessage("""""" You are a friendly, but terse customer service agent for Rocket's Cosmic Cruisers, a spaceship rental shop. You answer questions from potential guests about the different planets they can visit. If asked about the planets, only use info from the fact sheet below. """""" + PlanetInfo.PLANET_FACT_SHEET) String chat(String userMessage); }看看这段代码在干啥:@SessionScoped注解在Web服务连接期间保持会话,存着聊天记忆。@RegisterAIService注解把接口注册为AI服务,LangChain4j自动实现它。@SystemMessage注解告诉LLM回消息时该咋表现。用户在聊天框里输入后,WebSocket端点把消息传给AI服务的chat方法。因为接口里没标@UserMessage,所以AI服务实现会自动把chat方法的参数(这儿是userMessage)当作用户消息。AI服务把用户消息拼到系统消息后,组成提示词发给LLM,回复就显示在聊天界面了。注意,为了方便阅读,行星信息单独放在PlanetInfo类里。当然,你也可以直接塞系统消息里。ChatWebSocket类给聊天机器人UI定义了个WebSocket端点:@WebSocket(path = "/chat/batch")public class ChatWebSocket { private final CustomerSupportAgent customerSupportAgent; public ChatWebSocket(CustomerSupportAgent customerSupportAgent) { this.customerSupportAgent = customerSupportAgent; } @OnOpen public String onOpen { return "Welcome to Rocket's Cosmic Cruisers! How can I help you today?"; } @OnTextMessage public String onTextMessage(String message) { return customerSupportAgent.chat(message); }}CustomerSupportAgent接口通过构造函数注入自动拿到AI服务的引用。用户一发消息,onTextMessage方法就把消息转给AI服务的chat方法。比如,用户问:“我想看火山,哪个星球合适?”应用就会推荐并解释为啥火山迷该去那儿:飞船租赁应用的聊天机器人界面营造记忆的错觉 随着聊天继续,机器人好像记得之前聊过啥——这就是对话上下文。和人聊天时,你觉得对方记得之前的话很正常,但LLM请求是无状态的,每次回复只基于当前提示词里的信息。为了保持上下文,AI服务用LangChain4j的聊天记忆存之前的用户消息和回复。默认情况下,Quarkus LangChain4j扩展把聊天记在内存里,AI服务按需管理记忆(比如删掉或汇总老消息),避免内存爆掉。光用LangChain4j的话,你得先配记忆提供者,但用Quarkus扩展就省了这步。这给用户一种有记忆的错觉,体验更好,不用老重复说过的话。另外,流式传输LLM的响应也能提升聊天体验。流式回复让用户体验更流畅 你可能发现了,聊天框的回复要等一会儿才一下子全出来。为了让机器人感觉更灵敏,我们可以改改代码,让它生成回复时逐个返回token。这叫流式传输,用户不用等全写完就能开始看。完整代码见GitHub的“spaceship rental step-02”目录。改起来挺简单。先更新CustomerSupportAgent接口,加个返回SmallRye Mutiny Multi实例的方法:@SessionScoped@RegisterAiService@SystemMessage("""""" You are a friendly, but terse customer service agent for Rocket's Cosmic Cruisers, a spaceship rental shop. You answer questions from potential guests about the different planets they can visit. If asked about the planets, only use info from the fact sheet below. """""" + PlanetInfo.PLANET_FACT_SHEET) public interface CustomerSupportAgent { String chat(String userMessage); Multi}把@SystemMessage注解挪到接口上,这样接口里每个方法就不用单独加了。streamChat方法每次返回一个token作为LLM的回复,而不是等全写完再显示。还得从WebSocket端点调新的streamChat方法。为了同时支持批处理和流处理,我们做了个新类ChatWebSocketStream,暴露/chat/stream这个WebSocket端点:@WebSocket(path = "/chat/stream")public class ChatWebSocketStream { private final CustomerSupportAgent customerSupportAgent; public ChatWebSocketStream(CustomerSupportAgent customerSupportAgent) { this.customerSupportAgent = customerSupportAgent; } @OnOpen public String onOpen { return "Welcome to Rocket's Cosmic Cruisers! How can I help you today?"; } @OnTextMessage public MultiString> onStreamingTextMessage(String message) { return customerSupportAgent.streamChat(message); }}customerSupportAgent.streamChat调用AI服务,把用户消息发给LLM。稍微调了下UI,现在聊天机器人能开关流式传输了:开了流式传输功能的应用界面流式传输一开,LLM生成的每个token(单词或词根)立马就飞到聊天框里。从非结构化数据里榨出结构化输出 目前LLM的输出都是给最终用户看的。但如果想让LLM的输出直接给程序用呢?和LLM交互的AI服务能返回结构化输出,比如POJO、POJO列表或基本类型,比String那种更规整。返回结构化输出能让LLM的输出和Java代码对接更轻松,因为应用从AI服务拿到的东西能映射到预定义的Java对象模式。咱们通过帮用户挑合适飞船的例子,展示下结构化输出的妙用。完整代码在GitHub的“spaceship rental step-03”目录里。先做个简单的Spaceship记录,存舰队里每艘飞船的信息:record Spaceship(String name, int maxPassengers, boolean hasCargoBay, ListString> allowedDestinations) { } 再做个SpaceshipQuery记录,表示用户根据聊天信息对飞船的查询:@Description("A request for a compatible spaceship")public record SpaceshipQuery(int passengers, boolean hasCargo, List}Fleet类放了一堆Spaceship对象,还提供了过滤不匹配飞船的方法。接着更新CustomerSupportAgent接口,让它能收用户消息(非结构化文本),产出SpaceshipQuery记录格式的结构化输出。很简单,只要把AI服务里新方法extractSpaceshipAttributes的返回类型设成SpaceshipQuery就行:SpaceshipQuery extractSpaceshipAttributes(String userMessage);底层呢,LangChain4j会自动生成一个带JSON模式期望响应的请求给LLM。它把LLM返回的JSON反序列化,然后按需返回SpaceshipQuery记录。咱们还得知道用户输入是不是在问飞船。用个更简单的结构化输出请求来过滤,返回布尔值:@SystemMessage(""""""You are a friendly, but terse customer service agent for Rocket's Cosmic Cruisers, a spaceship rental shop. Respond with 'true' if the user message is regarding spaceships in our rental fleet, and 'false' otherwise."""""")boolean isSpaceshipQuery(String userMessage);最后给CustomerSupportAgent接口加个功能,让代理能根据舰队和用户请求推荐飞船(支持流式传输):@UserMessage("""""" Given the user's query regarding available spaceships for a trip {message}, provide a well-formed, clear and concise response listing our applicable spaceships. Only use the spaceship fleet data from {compatibleSpaceships} for your response. """""") String suggestSpaceships(String message, List@UserMessage("""""" Given the user's query regarding available spaceships for a trip {message}, provide a well-formed, clear and concise response listing our applicable spaceships. Only use the spaceship fleet data from {compatibleSpaceships} for your response. """""")Multi}最后一步,更新ChatWebSocket和ChatWebSocketStream类,先检查用户是不是在问飞船。如果是,客户支持代理就从用户消息里提取信息,创建SpaceshipQuery记录,然后回复匹配的飞船建议。两个类的更新代码差不多,这里就展示ChatWebSocket类的:@OnTextMessagepublic String onTextMessage(String message) { boolean isSpaceshipQuery = customerSupportAgent.isSpaceshipQuery(message); if (isSpaceshipQuery) { SpaceshipQuery userQuery = customerSupportAgent.extractSpaceshipAttributes(message); ListSpaceship> spaceships = Fleet.findCompatibleSpaceships(userQuery); return customerSupportAgent.suggestSpaceships(message, spaceships); } else return customerSupportAgent.chat(message);}更新完,客户支持代理就能用结构化输出给用户推荐飞船了:应用根据结构化输出推荐飞船的界面搞定!我们做了个融合AI的Java聊天机器人,能推荐星际旅行目的地还能租飞船。想继续学?结合Quarkus和LangChain4j文档,跑跑我们示例应用的完整代码试试。这些AI概念再多聊点 大语言模型(LLM) 这儿说的AI,主要指从大语言模型(LLM)拿回复。LLM是机器学习模型,训练后能根据输入序列(通常是文本,但多模态LLM也能处理图、音视频)生成输出序列。LLM能干各种活儿,比如总结文档、翻译、提取事实、写代码等。这种根据输入创内容的能力叫生成式AI(GenAI)。你可以按需把这能力塞进你的应用里。向LLM发请求:提示词、聊天记忆和词元你怎么向LLM要信息,不仅影响回复内容,还影响用户体验和应用成本。提示词 给LLM发请求,无论是从代码还是用户在聊天框里输入,都得写提示词。提示词是LLM生成回复所需的信息(通常是文本,但不限于)。想象一下和人沟通:你怎么措辞请求,对方(或这儿的LLM)才能懂你想要啥。比如,问具体信息前先给点上下文,别堆一堆无关信息把水搅浑。聊天记忆 和人聊天不同,LLM没状态,不记得之前的请求,所以你想让它考虑的所有东西都得塞进请求里:提示词、之前的请求回复(聊天记忆),还有你提供的任何辅助工具。但提示词里给太多信息会让请求变复杂,还可能贵得离谱。词元 LLM把你提示词的单词转成一串词元(token)。大多数托管LLM按请求和回复里的词元数收费。一个词元可能代表整个词或词的一部分。比如“unbelievable”常被拆成“un”、“bel”、“ievable”几个词元。请求里词元越多——尤其当包含整个聊天记忆时——应用运行成本可能越高。请求里带上所有聊天记忆,既贵又容易让请求不清晰。LLM请求有长度限制,所以管理聊天记忆和请求信息量很重要。你用的Java框架(比如这儿的Quarkus配LangChain4j)能帮大忙。LangChain4j和Quarkus框架 LangChain4j是个开源Java框架,专门管Java应用和LLM的交互。比如,它通过AI服务的概念存并帮你管理聊天记忆,让LLM请求更高效、专注、省钱。Quarkus是个现代、云原生的开源Java框架,为开发效率优化,能在容器化环境跑,启动快、内存省。LangChain4j的Quarkus扩展简化了AI驱动Java应用里连LLM的配置。LangChain4j也能和其他Java应用框架搭配,比如Open Liberty、Spring Boot、Micronaut。MicroProfile和Jakarta EE也和LangChain4j合作,为开发AI应用提供了开放标准的编程模型。示例应用 文章里演示的完整示例应用在GitHub上。应用用Java写,靠Quarkus LangChain4j扩展在Quarkus上运行。结 论 把AI融入Java应用,既能增强功能又能提升用户体验。借助Quarkus、LangChain4j这些Java框架简化与LLM的交互,Java开发者可以轻松给业务应用注入AI能力。用Java写AI驱动的应用,意味着你在Java强大且适合企业的生态里工作,不仅和AI模型交互容易,还能让应用轻松享受企业级优势,比如性能、安全、可观测性和测试。AI领域发展飞快。掌握本文的概念和技术,你能保持领先,开始探索AI怎么帮你构建智能又吸引人的Java应用。结合Quarkus with LangChain4j文档,跑跑示例应用的完整代码试试手。感谢红帽的Clement Escoffier、Markus Eisele和Georgios Andrianakis的审阅和建议。原文链接:https://www.infoq.com/articles/infusing-ai-java/声明:本文为 InfoQ 翻译,未经许可禁止转载。今日好文推荐相关问答






