
1. 为什么 Java 工程师现在要关注 LangChain4j1.1 从一次真实的需求说起去年底我接了个内部知识库的项目需求很朴素把公司散落在 Confluence、语雀和一堆 Word 文档里的技术资料整合起来做一个能问答的助手。团队清一色 Java 技术栈Spring Boot 加 MyBatis-Plus 那套东西闭着眼都能写但一提到大模型应用开发第一反应就是这不是 Python 的活儿吗。当时我也这么想直到把 LangChain4j 拉下来跑了个 Demo。说实话第一次看到用 Java 写出来的链式调用能跑通 RAG 流程心里是有点震撼的。它把大模型调用、提示词模板、向量检索、对话记忆这些原本需要自己拼装的东西封装成了符合 Java 工程师直觉的 API。你不需要去学 Python 的异步生态不需要理解 asyncio 那套事件循环用你熟悉的 Builder 模式、接口声明、依赖注入就能把 AI 应用搭起来。这篇内容就是把我从零上手 LangChain4j 到跑通一个完整 RAG 问答系统的过程整理出来。适合两类人看一类是 Java 后端工程师想在自己的项目里加 AI 能力但不想换语言另一类是对 RAG、AI Agent 这些概念有耳闻但没动过手想找个能落地的切入点。我会把版本选型、依赖配置、Embedding 模型接入、向量库选型、检索链路调优这些环节都拆开讲包括我踩过的坑和最后怎么绕过去的。1.2 LangChain4j 到底解决了什么问题在没有 LangChain4j 之前Java 要接大模型你得自己写 HTTP 客户端调 API自己处理流式返回自己维护对话上下文自己实现向量相似度计算。每个环节单独看都不难但拼在一起就是几百行胶水代码而且换个模型厂商就得重写一遍。LangChain4j 的核心价值在于抽象层。它定义了一套统一的接口比如ChatLanguageModel、EmbeddingModel、EmbeddingStore你面向接口编程底层换 OpenAI 还是通义千问还是本地部署的模型上层业务代码基本不用动。这跟 JDBC 的思路是一样的——你写Connection和Statement具体连 MySQL 还是 PostgreSQL 由驱动决定。另一个让我觉得舒服的点是它对 Spring Boot 的适配。通过langchain4j-spring-boot-starter你可以像注入JdbcTemplate一样注入ChatLanguageModel配置写在application.yml里跟现有项目的集成成本极低。对于已经在用 Spring 生态的团队来说这意味着不需要引入新的构建工具或运行时Maven 加个依赖就能开工。1.3 版本选择与依赖矩阵我写这篇内容时用的是 LangChain4j 0.35.0 版本JDK 17Spring Boot 3.2.x。这里有个坑要提前说LangChain4j 在 0.30 之前 API 变动比较频繁网上很多老教程的包名和类名跟新版对不上。建议直接上 0.35 或更高稳定性和文档完整度都好很多。核心依赖分三块基础核心包、模型接入包、向量存储包。基础核心包是必须的模型接入包看你用哪家向量存储包看你选什么数据库。下面这张表是我整理的最小依赖组合可以直接抄功能模块Maven 坐标说明核心库dev.langchain4j:langchain4j基础接口和链式调用OpenAI 接入dev.langchain4j:langchain4j-open-ai兼容 OpenAI 协议的模型通义千问接入dev.langchain4j:langchain4j-dashscope阿里云百炼平台本地向量库dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2内置轻量 EmbeddingRedis 向量存储dev.langchain4j:langchain4j-redis用 Redis Stack 做向量检索Spring Boot 集成dev.langchain4j:langchain4j-spring-boot-starter自动配置和依赖注入注意如果你用的是 Spring Boot 3.x确保 LangChain4j 版本在 0.31 以上否则会有 Jakarta EE 命名空间的兼容问题。我一开始用 0.28 版本启动直接报javax.servlet找不到换成 0.35 就好了。2. 核心概念拆解Embedding、RAG 与 AI Agent 的关系2.1 Embedding 是整条链路的基石很多人一上来就急着搭 RAG结果检索效果一塌糊涂根本原因是没搞懂 Embedding 在干什么。Embedding 就是把一段文本映射成一个固定维度的浮点数向量比如 768 维或 1536 维。这个向量在语义空间里的位置代表了这段文本的含义。语义相近的文本向量距离就近。举个生活化的例子把每段文本想象成地图上的一个点如何配置数据库连接和数据库连接池怎么设置这两个点会挨得很近而今天天气怎么样这个点就离得很远。RAG 的检索过程就是把你问的问题也变成一个点然后在地图上找离它最近的几个点把对应的原文拿出来喂给大模型。LangChain4j 里 Embedding 的接口是EmbeddingModel核心方法就两个embed(String text)返回单个向量embedAll(ListString texts)批量返回。批量接口很重要因为建索引的时候文档动辄几千条一条条调 API 既慢又费钱。选 Embedding 模型有几个维度要考虑。维度越高表达能力越强但存储和计算成本越大中文场景要选中文语料训练过的模型如果数据敏感不能出内网就得用本地模型。我实测下来中文技术文档场景通义千问的text-embedding-v2效果明显好于 OpenAI 的text-embedding-ada-002尤其在专业术语的语义匹配上。2.2 RAG 不是万能药先搞清楚它的瓶颈RAG 全称检索增强生成流程是检索相关文档 → 拼进提示词 → 让大模型基于文档回答。它解决的是大模型知识过时和幻觉的问题但用不好反而会放大问题。我总结的 RAG 瓶颈主要有四个。第一是切分粒度文档切得太碎单块信息不完整检索出来答非所问切得太大一块里混了多个主题向量表达被稀释检索精度下降。第二是检索召回向量检索本质是语义相似度对精确匹配的关键词比如错误码、函数名反而不敏感。第三是上下文窗口检索回来十块文档可能超出模型上下文限制得做重排序和截断。第四是答案忠实度模型可能无视检索到的文档按自己的记忆瞎编。针对这些瓶颈LangChain4j 提供了对应的工具DocumentSplitter控制切分策略EmbeddingStoreContentRetriever配置检索参数ReRankingContentAggregator做重排序。后面实操部分我会逐个展开。2.3 AI Agent 与 RAG 的边界在哪这两个概念经常被混在一起。简单区分RAG 是查资料再回答Agent 是自己决定做什么再执行。RAG 的流程是固定的检索、拼接、生成一条路走到黑。Agent 则是有自主性的它会根据任务判断该调用哪个工具比如查数据库、调 API、发邮件然后根据工具返回结果决定下一步。LangChain4j 从 0.31 版本开始正式支持 AI Agent核心是AiServices配合Tool注解。你定义一个 Java 接口方法上标注ToolLangChain4j 会自动把这些方法暴露给大模型模型在对话中判断需要调用哪个工具框架负责执行并把结果回传。一个典型的 Agent 场景用户问帮我查一下订单 A123 的物流状态并给客户发个通知。Agent 会先调用查询订单的工具拿到物流信息再调用发送通知的工具。整个过程不需要你写 if-else 判断模型自己规划步骤。当然实际生产环境里这种自主性要加约束不然模型可能乱调工具这个后面细说。3. 从零搭建第一个 RAG 问答系统3.1 项目骨架与配置先建一个标准的 Spring Boot 项目Maven 结构JDK 17。pom.xml里加上前面表格里的依赖。我建议一开始只加核心库和 OpenAI 接入包跑通最小闭环再加向量库避免依赖冲突排查困难。配置文件application.yml里模型相关的配置长这样langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-4o-mini temperature: 0.7 timeout: PT60S embedding-model: api-key: ${OPENAI_API_KEY} model-name: text-embedding-3-small这里temperature控制输出的随机性做知识问答建议设 0.2 到 0.3让回答更稳定。timeout用 ISO-8601 格式PT60S就是 60 秒。API Key 千万别硬编码在文件里用环境变量注入这是基本的安全习惯。如果你用通义千问配置项换成langchain4j.dashscopeapi-key换成百炼平台的 Key模型名用qwen-plus或qwen-turbo。通义千问在国内访问速度快价格也便宜做中文场景我优先推荐它。3.2 文档加载与切分策略RAG 的第一步是把原始文档变成可检索的文本块。LangChain4j 提供了DocumentLoader接口内置了从文件、URL、类路径加载的实现。我一般用FileSystemDocumentLoader.loadDocuments()批量加载一个目录下的所有文件。加载进来的是Document对象里面是整篇文本。接下来要切分这是最影响效果的一步。LangChain4j 的DocumentSplitters.recursive()是最常用的切分器它按优先级尝试用段落、句子、空格来切尽量保持语义完整。参数怎么定我踩过的坑是这样maxSegmentSizeInChars设 500 到 800 字符比较合适太小了信息不完整太大了检索不准。maxOverlapSizeInChars设 50 到 100让相邻块有重叠避免关键信息正好被切断。这个重叠就像拼图时的搭边保证上下文连贯。DocumentSplitter splitter DocumentSplitters.recursive(600, 80); ListDocument segments splitter.splitAll(documents);实操心得技术文档里有大量代码块和表格按字符数硬切会把代码切得七零八落。我的做法是先用正则把代码块提取出来单独处理正文部分再走递归切分。虽然麻烦点但检索质量提升明显。3.3 向量化与存储切分完的文本块要转成向量存起来。LangChain4j 的EmbeddingStoreIngestor把嵌入和存储两步封装在一起用起来很省事EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(splitter) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); ingestor.ingest(documents);InMemoryEmbeddingStore适合开发和测试重启就没了。生产环境要换成持久化的Redis Stack 是个不错的选择它支持向量索引而且大部分团队已经有 Redis 运维经验。用RedisEmbeddingStore替换即可配置里指定 Redis 地址和向量维度。维度必须和 Embedding 模型对齐。text-embedding-3-small是 1536 维通义千问text-embedding-v2也是 1536 维但有些本地模型是 384 维或 768 维。维度对不上会直接报错这个坑我踩过一次排查了半天才发现是维度配置写错了。3.4 检索与生成链路组装检索器负责根据用户问题找出最相关的文本块。EmbeddingStoreContentRetriever是标准实现关键参数是maxResults和minScore。maxResults控制返回几块一般设 3 到 5太多了会稀释重点还占上下文。minScore是相似度阈值低于这个分数的直接丢弃避免把不相关的内容喂给模型。ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(4) .minScore(0.7) .build();最后把检索器和模型组装成RetrievalAugmentor再配合AiServices生成一个声明式的问答接口interface Assistant { String chat(String userMessage); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .retrievalAugmentor(retrievalAugmentor) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build();MessageWindowChatMemory保留最近 10 条消息作为对话记忆这样多轮对话时模型能记住上下文。窗口别设太大否则 token 消耗会飙升。4. 检索效果调优与常见问题排查4.1 检索不准的三种典型表现与对策第一种是答非所问问 A 却检索到 B 的内容。这通常是切分粒度问题块太大导致向量被多个主题平均。对策是减小maxSegmentSizeInChars或者改用按标题层级切分让每块只讲一个点。第二种是漏检明明文档里有答案却检索不到。这多半是 Embedding 模型对领域术语不敏感。对策有两个换更强的 Embedding 模型或者在检索前用大模型把用户问题改写成更规范的表述。LangChain4j 支持QueryTransformer可以配置CompressingQueryTransformer做查询改写。第三种是检索到了但答案不对模型没按文档回答。这是提示词的问题需要在系统提示里明确要求只基于提供的参考资料回答资料中没有的信息就说不知道。LangChain4j 的AiServices支持SystemMessage注解把约束写进去。4.2 常见问题速查表问题现象可能原因排查方向解决方案启动报类找不到依赖版本不匹配检查 LangChain4j 与 Spring Boot 版本统一升级到 0.35向量维度报错Embedding 模型与存储维度不一致核对模型输出维度修改存储配置的维度参数检索结果为空minScore 设太高打印实际相似度分数降低阈值到 0.5 试回答超时上下文太长或网络慢看 token 消耗和响应时间减少 maxResults加超时配置中文乱码文件编码不是 UTF-8检查源文件编码加载时指定 UTF-8内存溢出文档太多全量加载看堆内存占用分批加载用流式处理4.3 我踩过的几个坑第一个坑是 API Key 泄露。我一开始图省事把 Key 写在application.yml里提交到了 Git结果被安全扫描工具告警。后来改成环境变量注入本地开发用.env文件并加进.gitignore。这个习惯一定要养成Key 泄露的后果比想象中严重。第二个坑是 Embedding 调用太频繁。建索引时每改一次文档就全量重新嵌入几千条文档跑一次要十几分钟还费钱。后来我做了增量更新给每个文档块算个哈希值只有内容变了才重新嵌入。LangChain4j 本身不直接支持这个得自己在业务层实现但省下的时间和费用很值。第三个坑是对话记忆无限增长。MessageWindowChatMemory如果不设上限长对话会把上下文撑爆。我设了 10 条消息的窗口但发现有些关键信息在早期对话里被挤掉了。后来改成用TokenWindowChatMemory按 token 数控制而不是消息条数更合理。5. 进阶用 AiServices 构建带工具的 AI Agent5.1 声明式接口的威力LangChain4j 最让我惊艳的设计是AiServices。你只需要定义一个 Java 接口框架会自动生成实现类把方法调用转成大模型交互。这跟 MyBatis 的 Mapper 接口思路一模一样——你声明方法框架负责实现。interface CustomerServiceAgent { SystemMessage(你是一个电商客服助手帮用户查询订单和处理售后) String handleQuery(UserMessage String query); }SystemMessage定义角色设定UserMessage标记用户输入。框架启动时会用动态代理生成实现你直接注入使用就行。这种声明式风格对 Java 工程师来说几乎没有学习成本。5.2 用 Tool 注解暴露工具能力Agent 的核心是能调用外部工具。LangChain4j 用Tool注解标记方法方法参数和返回值会自动转成模型能理解的格式。class OrderTools { Tool(根据订单号查询订单状态) String queryOrderStatus(P(订单号) String orderId) { // 实际查数据库 return orderService.getStatus(orderId); } Tool(给客户发送通知消息) boolean sendNotification(P(客户ID) String customerId, P(消息内容) String message) { return notifyService.send(customerId, message); } }P注解描述参数含义帮助模型正确填充。把这些工具注册到AiServices里模型在对话中就会自动判断该调哪个工具。我实测下来对于查订单 A123 状态这种明确指令模型调用工具的准确率很高。5.3 Agent 的约束与安全边界Agent 的自主性是把双刃剑。我遇到过模型连续调用同一个工具十几次的情况因为它没理解返回结果。也遇到过模型试图调用不存在的工具导致报错。约束手段有几个。第一是限制工具数量一次别暴露太多5 个以内模型判断更准。第二是在系统提示里明确工具的使用条件和边界比如只有当用户明确提供订单号时才调用查询工具。第三是加调用次数上限LangChain4j 的AiServices可以配置maxSequentialToolsInvocations超过就中断。重要提示涉及写操作的工具发消息、改数据、扣款一定要加人工确认或权限校验别让模型直接执行。我见过有人让 Agent 自动回复客户消息结果模型理解偏差发了一堆错误信息这种事故在生产环境是灾难性的。6. 从 Demo 到生产的工程化考量6.1 性能与成本控制大模型调用是 IO 密集型操作响应时间从几百毫秒到几秒不等。在 Web 应用里如果每个请求都同步等待模型返回并发一高线程池就爆了。我的做法是用CompletableFuture做异步调用配合 Spring 的Async把模型调用放到独立线程池。成本控制方面token 就是钱。几个省 token 的技巧系统提示尽量精简别写一大段废话检索返回的文档块做去重和截断简单问题用小模型如qwen-turbo复杂问题才用大模型。我做过统计优化提示词和检索策略后token 消耗降了将近一半。6.2 可观测性与日志生产环境必须能追踪每次调用的输入输出、耗时、token 消耗。LangChain4j 支持ChatModelListener接口可以在调用前后插入日志逻辑。我把这些数据打到 ELK 里方便排查问题和分析成本。ChatModelListener listener new ChatModelListener() { Override public void onResponse(ChatModelResponse response) { log.info(token消耗: {}, response.tokenUsage()); log.info(响应耗时: {}, response.finishReason()); } };6.3 知识库的持续更新RAG 系统上线不是终点知识库要持续维护。我设计了一个简单的增量更新流程文档变更时触发事件重新切分和嵌入受影响的块更新向量库。LangChain4j 的EmbeddingStore支持按 ID 删除和更新配合文档的元数据如文件路径、修改时间就能实现精准更新。元数据在检索时也很有用。比如给每个文档块打上部门文档类型更新时间的标签检索时可以按标签过滤只搜某个部门的文档。LangChain4j 的MetadataFilter支持这种过滤写法类似 SQL 的 where 条件。7. 一些个人体会LangChain4j 让我最满意的地方是它没有试图把 Java 工程师变成 Python 工程师。它尊重 Java 生态的习惯用接口、注解、Builder 这些我们熟悉的东西来封装 AI 能力。你不需要理解 Transformer 的注意力机制不需要会写 PyTorch只要会写 Service 和 Mapper就能把 AI 集成进现有系统。但它也不是银弹。RAG 的效果高度依赖数据质量和切分策略Agent 的可靠性需要大量测试和约束。我见过太多团队兴冲冲搭了个 Demo 就以为能上线结果真实场景一跑全是问题。AI 应用的工程化难度不比传统后端低甚至因为引入了不确定性调试和测试更麻烦。如果你正准备上手我的建议是先跑通最小闭环再逐步加复杂度。别一上来就搞多路召回、重排序、Agent 编排先把文档切分 向量检索 提示词约束这三件事做扎实。这三件事做好了80% 的问答场景就够用了。剩下的 20%等你真正遇到瓶颈了再去优化那时候你也更有判断力知道该往哪个方向使劲。最后分享一个我常用的调试技巧把检索到的文档块和最终提示词完整打印出来人工看一眼。很多时候问题一眼就能看出来——要么检索的内容根本不相关要么提示词里模型被误导了。这个笨办法比任何自动化工具都管用。