ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java后端集成LangChain4j实战:AiService、TokenStream与RAG落地指南

Java后端集成LangChain4j实战:AiService、TokenStream与RAG落地指南 1. 为什么 Java 后端值得花时间搞明白 LangChain4j做 Java 后端的这几年我最大的感受是AI 功能已经从“要不要接”变成了“什么时候接、怎么接得不难看”。以前团队里想做个智能问答或者文档摘要第一反应是让 Python 同学搭个服务Java 这边通过 HTTP 去调。结果就是多了一个服务要部署、多了一套鉴权要维护、多了一份超时和重试逻辑要写联调的时候两边互相甩锅。LangChain4j 出现之后这件事的逻辑变了——它把大模型调用、提示词模板、对话记忆、向量检索这些能力用 Java 开发者熟悉的方式封装了起来直接塞进 Spring Boot 项目里就能跑。这篇内容我打算按一个真实项目的推进节奏来写从依赖引入、AiService 声明式接口、TokenStream 流式输出到 RAG 检索增强、和 Spring Boot 的整合细节再到实际踩过的坑。适合已经有 Java 基础、用过 Spring Boot、想在自己项目里落地 AI 能力的后端同学。如果你连 Maven 依赖都没配过建议先把 Spring Boot 跑起来再回来看不然中间有些地方会卡住。先说清楚 LangChain4j 到底解决什么问题。你可以把它理解成一层“翻译官”左边是你熟悉的 Java 对象、接口、注解右边是大模型厂商各自的 HTTP API、参数格式、返回结构。没有它的时候你得自己拼 JSON、自己解析响应、自己管理对话历史有了它你定义一个接口加个注解调用起来就像调本地 Service 一样。这个体验上的差别用过一次就回不去了。2. 核心概念拆解AiService、TokenStream 与 RAG 到底在干嘛2.1 AiService把大模型调用伪装成普通接口AiService 是 LangChain4j 里我觉得最舒服的设计。传统写法是你要拿到一个模型客户端然后手动构造消息列表再调chat()方法最后从响应里抠出文本。AiService 把这套流程反过来你先定义一个 Java 接口方法签名写清楚输入输出然后用AiServices.create()生成实现。框架在运行时用动态代理帮你把方法调用翻译成模型请求。举个最直观的例子。假设我要做一个“代码解释器”输入一段 Java 代码输出中文解释。接口可以这么写interface CodeExplainer { SystemMessage(你是一个资深 Java 工程师用简洁的中文解释代码逻辑) String explain(UserMessage String code); }然后创建实例CodeExplainer explainer AiServices.create(CodeExplainer.class, chatModel); String result explainer.explain(public int add(int a, int b) { return a b; });这里SystemMessage定义的是系统角色设定UserMessage标记的是用户输入。框架会自动把参数填进消息模板发给模型再把返回文本映射成 String。整个过程你不需要碰任何 JSON。为什么这个设计重要因为它让 AI 调用变成了“面向接口编程”的一部分。你可以像注入普通 Bean 一样注入 AiService可以在接口上做 AOP可以写单元测试时 mock 掉。对于习惯了 Spring 生态的 Java 后端来说这种心智负担几乎为零。2.2 TokenStream流式输出不是炫技是体验刚需大模型生成一段 500 字的回答如果等全部生成完再返回用户盯着转圈可能要等五六秒。TokenStream 的作用是把生成过程拆成一个个 token边生成边推给前端。用户看到文字一个个蹦出来主观等待感会大幅下降。LangChain4j 里流式调用有两种常见形态。一种是直接用StreamingChatLanguageModel通过回调处理每个 tokenStreamingChatLanguageModel model OpenAiStreamingChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); model.generate(用一句话介绍 Java 的垃圾回收, new StreamingResponseHandlerAiMessage() { Override public void onNext(String token) { System.out.print(token); } Override public void onComplete(ResponseAiMessage response) { System.out.println(\n--- 完成 ---); } Override public void onError(Throwable error) { error.printStackTrace(); } });另一种是在 AiService 接口里把返回类型声明成TokenStream框架会自动帮你处理流式逻辑interface StreamingAssistant { TokenStream chat(UserMessage String message); }调用后拿到 TokenStream可以注册onNext、onComplete、onError回调也可以配合 Spring 的 SSE 或 WebSocket 推给前端。注意流式接口和普通接口不要混用在同一个方法上。返回TokenStream的方法框架不会等结果生成完所以你不能在方法内部再做后处理。需要后处理就老老实实用同步返回。2.3 RAG让模型回答“它本来不知道”的事RAG 是 Retrieval-Augmented Generation 的缩写中文一般叫检索增强生成。核心思路很朴素模型训练数据里没有你公司的内部文档那就先把相关文档片段检索出来拼进提示词里让模型基于这些片段回答。LangChain4j 的 RAG 流程分两大阶段。索引阶段把文档切块、向量化、存进向量库。检索阶段把用户问题向量化去向量库找最相似的片段拼进上下文。索引阶段的代码大致长这样EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .build(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); DocumentSplitter splitter DocumentSplitters.recursive(500, 50); ListDocument documents FileSystemDocumentLoader.loadDocuments(/path/to/docs); ListTextSegment segments splitter.splitAll(documents); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(splitter) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); ingestor.ingest(documents);检索阶段可以做成一个 ContentRetriever挂到 AiService 上ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .contentRetriever(retriever) .build();这样每次调用assistant.chat()框架会先检索、再拼提示词、再调模型。你不需要手动写“请根据以下资料回答”这种模板框架帮你做了。3. 从零搭一个 Spring Boot LangChain4j 的最小可运行项目3.1 依赖引入与版本选择先说版本。LangChain4j 迭代很快不同版本 API 有差异。我写这篇时用的是 0.35.0 附近的版本核心依赖分几个模块properties langchain4j.version0.35.0/langchain4j.version /properties dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency /dependencies如果你用的是其他模型厂商把langchain4j-open-ai换成对应模块即可。langchain4j-spring-boot-starter提供自动配置能省掉不少手动创建 Bean 的代码。提示不要盲目追最新版本。LangChain4j 有些版本之间包名和类名会调整升级前先看 release notes否则编译报错会浪费很多时间。3.2 配置文件与模型 Bean 的创建在application.yml里放模型配置langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-4o-mini temperature: 0.7 timeout: PT60S streaming-chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-4o-mini embedding-model: api-key: ${OPENAI_API_KEY} model-name: text-embedding-3-small如果不想用 starter 的自动配置也可以手动建 BeanConfiguration public class AiConfig { Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .temperature(0.7) .timeout(Duration.ofSeconds(60)) .build(); } Bean public StreamingChatLanguageModel streamingChatLanguageModel() { return OpenAiStreamingChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); } }手动建 Bean 的好处是参数一目了然出问题好排查。自动配置的好处是代码少。我一般项目初期用手动稳定后再考虑切自动。3.3 定义第一个 AiService 并注入使用定义一个客服助手接口public interface CustomerServiceAssistant { SystemMessage(你是电商平台的客服助手回答要礼貌、简洁不确定的信息不要编造) String answer(UserMessage String question); SystemMessage(你是电商平台的客服助手) TokenStream answerStream(UserMessage String question); }在配置类里注册成 BeanBean public CustomerServiceAssistant customerServiceAssistant( ChatLanguageModel chatModel, StreamingChatLanguageModel streamingModel) { return AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(chatModel) .streamingChatLanguageModel(streamingModel) .build(); }然后在 Controller 里注入RestController RequestMapping(/api/assistant) public class AssistantController { private final CustomerServiceAssistant assistant; public AssistantController(CustomerServiceAssistant assistant) { this.assistant assistant; } PostMapping(/ask) public String ask(RequestBody String question) { return assistant.answer(question); } }到这里一个最小可运行的 AI 接口就完成了。启动项目用 Postman 发个请求能看到模型返回就说明链路通了。4. 流式输出与前端联调TokenStream 落地细节4.1 SSE 推送的完整实现流式输出最常见的落地方式是 SSE。Spring Boot 里用SseEmitter配合 TokenStream 回调GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(RequestParam String question) { SseEmitter emitter new SseEmitter(120_000L); TokenStream tokenStream assistant.answerStream(question); tokenStream.onNext(token - { try { emitter.send(SseEmitter.event().data(token)); } catch (IOException e) { emitter.completeWithError(e); } }).onComplete(response - emitter.complete()) .onError(emitter::completeWithError) .start(); return emitter; }前端用EventSource接收const source new EventSource(/api/assistant/stream?question encodeURIComponent(q)); source.onmessage (event) { document.getElementById(output).textContent event.data; }; source.onerror () source.close();这里有几个细节值得说。第一SseEmitter的超时时间要设得比模型生成时间长否则长回答会被截断。第二onNext里发数据可能抛 IOException必须捕获不然线程会挂。第三.start()不能忘忘了回调不会触发。4.2 流式场景下的异常处理流式接口的异常处理和同步接口不一样。同步接口抛异常全局异常处理器能兜住。流式接口一旦开始推送HTTP 状态码已经发出去了再抛异常前端只能通过连接断开感知。我的做法是在onError里推一个特殊标记比如[ERROR]前缀前端识别到就展示错误提示.onError(error - { try { emitter.send(SseEmitter.event().data([ERROR] 生成失败请重试)); } catch (IOException ignored) { } emitter.completeWithError(error); })另外模型调用超时、限流、余额不足这些情况最好在进入流式之前先做一次轻量校验能提前失败的不要拖到流中间。注意SSE 连接在 Nginx 后面容易被缓冲导致前端看不到逐字效果。需要在 Nginx 配置里对 SSE 路径关闭proxy_buffering否则你本地测试正常上线就变成一次性返回。5. RAG 实战把内部文档变成可检索知识库5.1 文档切块策略与参数选择切块是 RAG 里最容易被忽视但影响最大的环节。切太大检索出来的片段包含太多无关信息模型容易被干扰切太小语义不完整检索命中率下降。LangChain4j 提供DocumentSplitters.recursive(maxSegmentSize, maxOverlapSize)。我的经验值中文文档maxSegmentSize设 300 到 500 字符maxOverlapSize设 50 到 80 字符。重叠是为了防止一句话被切断后两边都读不懂。DocumentSplitter splitter DocumentSplitters.recursive(400, 60);如果你的文档是 Markdown建议按标题层级切保留结构信息。LangChain4j 有DocumentByParagraphSplitter、DocumentByLineSplitter等按文档类型选。5.2 向量库选型内存、Redis 还是专用库开发阶段用InMemoryEmbeddingStore最省事重启数据就没了但调试方便。生产环境要持久化常见选择向量库适用场景特点InMemoryEmbeddingStore开发调试、小数据量零依赖重启丢失Redis已有 Redis 基础设施部署简单性能够用PgVector已用 PostgreSQL和业务数据同库事务方便Milvus大规模向量检索专业向量库运维成本高Chroma快速原型轻量Python 生态常用我一般中小项目直接上 PgVector因为业务库本来就是 PostgreSQL少维护一个组件。LangChain4j 有对应的langchain4j-pgvector模块。5.3 检索质量调优的几个抓手RAG 效果不好八成出在检索环节。我常用的调优手段第一调整maxResults和minScore。maxResults是返回片段数太多会撑爆上下文太少可能漏掉关键信息一般 3 到 5 个。minScore是相似度阈值低于这个分数的片段直接丢弃避免无关内容干扰。第二加元数据过滤。比如文档有部门、时间、类型字段检索时可以限定范围ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .dynamicFilter(query - metadataKey(department).isEqualTo(tech)) .build();第三查询改写。用户问“报销怎么弄”直接检索可能命中率低可以先让模型把问题改写成“报销流程 报销标准 报销材料”再检索。LangChain4j 有QueryTransformer接口CompressingQueryTransformer和ExpandingQueryTransformer都能用。6. 踩坑记录与常见问题速查6.1 依赖冲突与类找不到LangChain4j 依赖了一些 HTTP 客户端和 JSON 库和 Spring Boot 自带的有时候会打架。典型症状是启动报NoSuchMethodError或ClassNotFoundException。排查方法mvn dependency:tree看冲突用exclusions排掉旧版本。另一个常见问题是模型模块和核心模块版本不一致。比如核心用 0.35.0open-ai 模块用 0.34.0编译能过但运行时报错。统一用${langchain4j.version}管理。6.2 超时与重试配置大模型调用慢是常态默认超时往往不够。同步调用建议 60 秒流式调用建议 120 秒以上。重试要谨慎因为模型调用可能已经产生费用盲目重试会翻倍扣费。我的做法是只对网络类异常重试对业务类异常如内容审核不通过不重试。OpenAiChatModel.builder() .apiKey(apiKey) .timeout(Duration.ofSeconds(60)) .maxRetries(2) .build();6.3 常见问题速查表现象可能原因排查方向启动报 Bean 找不到AiService 没注册成 Bean检查配置类是否扫描到调用返回空字符串提示词模板参数没填检查 UserMessage 参数绑定流式无输出忘了调 start()检查 TokenStream 调用链RAG 答非所问检索片段不相关调 minScore、换切块策略中文乱码编码未指定统一 UTF-8响应特别慢上下文太长减少 maxResults、精简提示词6.4 几个我踩过的具体坑第一个坑SystemMessage里写了变量占位符但没传参启动不报错调用时抛异常。解决办法是变量用{{var}}语法方法参数用V(var)标注。第二个坑AiService 接口方法返回TokenStream但配置里只给了ChatLanguageModel没给StreamingChatLanguageModel运行时报错说找不到流式模型。两个都要配。第三个坑RAG 索引时文档编码不是 UTF-8向量化出来全是乱码检索自然不准。加载文档时显式指定编码。第四个坑在SystemMessage里写了很长的角色设定结果每次调用都消耗大量 token。角色设定建议控制在 200 字以内细节放到检索片段里。7. 和 Spring Boot 生态整合的进阶玩法7.1 用 Bean 注入控制不同场景用不同模型一个项目里往往需要多个模型便宜的模型做分类贵的模型做生成。可以定义多个 Bean用Qualifier区分Bean(cheapModel) public ChatLanguageModel cheapModel() { ... } Bean(smartModel) public ChatLanguageModel smartModel() { ... } Bean public ClassifierAssistant classifierAssistant(Qualifier(cheapModel) ChatLanguageModel model) { return AiServices.create(ClassifierAssistant.class, model); }这样成本可控该省的地方省该花的地方花。7.2 对话记忆的持久化AiService 默认的对话记忆是内存的重启就丢。生产环境要持久化LangChain4j 提供ChatMemoryStore接口可以自己实现存到 Redis 或数据库public class RedisChatMemoryStore implements ChatMemoryStore { Override public ListChatMessage getMessages(Object memoryId) { ... } Override public void updateMessages(Object memoryId, ListChatMessage messages) { ... } Override public void deleteMessages(Object memoryId) { ... } }挂到 AiService 上AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .chatMemoryProvider(memoryId - MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .chatMemoryStore(redisStore) .build()) .build();maxMessages控制保留多少轮对话太多会撑爆上下文太少会“失忆”。我一般设 10 到 20 轮。7.3 可观测性日志与指标AI 调用是黑盒出问题不好查。建议在 AiService 外面包一层切面记录请求参数、响应内容、耗时、token 消耗。LangChain4j 本身有ChatModelListener接口可以挂监听器OpenAiChatModel.builder() .apiKey(apiKey) .listeners(List.of(new LoggingChatModelListener())) .build();监听器里能拿到请求消息、响应消息、token 用量。把这些打到日志或者推到时序库后面做成本分析和性能优化就有依据了。8. 一些关于落地节奏的个人建议我见过不少团队一上来就想做“全能 AI 助手”结果三个月没上线。我的建议是分三步走。第一步先做一个单轮问答接口把链路跑通验证模型效果和成本。第二步加上流式输出和对话记忆让体验接近可用。第三步再引入 RAG把内部知识接进来。每一步都能独立上线每一步都有可衡量的产出。成本这块要有预期。以 gpt-4o-mini 为例输入 token 比输出便宜不少但 RAG 场景下输入会膨胀好几倍因为每次都要带上检索片段。控制成本的关键是精简检索片段数量和长度别把整篇文档塞进去。最后分享一个我常用的调试技巧把发给模型的完整提示词打到日志里。很多时候效果不好不是模型的问题是提示词拼错了、参数没填对、检索片段不相关。看到完整提示词问题基本一目了然。LangChain4j 的监听器或者自己包一层都能做到这个习惯帮我省了大量排查时间。
返回列表