Java开发者上手LangChain4j:从环境搭建到RAG实战 作为一个常年写 Java 后端的人我第一次接触 LangChain4j 的时候其实挺抗拒的。当时团队里只要涉及大模型默认解法就是起一个 Python 小服务Java 这边用 HTTP 去调prompt 稍微改一版就要跨项目走一次联调链路拉得又长又脆。后来在一次内部工具改造里我狠下心把整块逻辑用 LangChain4j 搬进了 Java 工程才发现这条路其实早就是通的。这篇教程就是写给想用 Java 直接接入大模型的应用开发者内容包括环境搭建、基础会话、结构化输出、工具调用、记忆管理再到 RAG 本地知识库问答最后是一份我自己踩出来的避坑清单。整个项目以 Java 17 为基础适合从零开始跟着敲也适合想在现有 Spring Boot 工程里集成 AI 能力的人参考。1. Java项目接大模型的老路我为什么最终选了LangChain4j1.1 没有LangChain4j之前Java是这样接大模型的如果你所在团队既有 Java 又有 Python早期接大模型的典型姿势是Python 同学写一个 FastAPI 或 Flask 服务把某个 prompt 模板固定成接口参数Java 这边用 WebClient、RestTemplate 甚至 OkHttp 去调然后对返回的 JSON 字符串做切割和解析。这套模式跑通一个 Demo 一点问题没有可一旦业务复杂起来痛点会非常密集。刚换了一个新模型、接口路径变了改的是 Python 那边想要修改 system prompt还是 Python 那边需要把多轮对话的上下文在请求里拼起来依然是 Python 那边。Java 工程师想自己调一遍 prompt 看效果得先去装 Python 环境、拉依赖、起服务这套动作看着不重次数多了就会让人烦躁。更麻烦的是返回解析。模型返回的内容本质是一个文本流你需要自己设计 JSON 格式的 prompt再靠 Jackson 去反序列化。模型一旦不听话回复里混进一句解释性文字解析直接抛异常或者静默失败。多轮对话的记忆状态没人管理工具调用的循环逻辑也要自己写。我见过不少 Java 项目所谓的“AI 能力”最后变成了 Python 服务里几百行拼 prompt 的胶水代码维护成本比业务代码还高。1.2 LangChain4j 与 LangChain 的关系以及它填补的空缺LangChain4j 的命名很明显就是 LangChain 的 Java 版本。但要澄清一点它并不是把 Python 的 LangChain 逐个类翻译过来而是借鉴了 LangChain 里被验证过的核心概念比如 ChatModel、ChatMemory、Tool、Retriever再用贴近 Java 开发习惯的方式重新组织了一遍。它在工程上的优势我认为体现在三点。第一所有东西都是类型安全的模型的输入输出可以落到 Java 的 class、enum、record 上不再靠字符串正则去抠。第二AiServices这个组件让我可以用接口定义 AI 能力框架在运行时通过动态代理生成实现调用模型、解析结果、维护会话这些事情都被底层处理掉了。第三模型供应商的接入层做得很薄OpenAI、Azure OpenAI、Ollama、Google Gemini、Anthropic 这些都有对应模块业务代码只需要面向统一的ChatLanguageModel接口以后换模型厂商不需要大面积改代码。语言层面也不必担心JDK 8 的老项目也能跑但我个人建议直接用 JDK 17。一方面新版本框架在 Java 17 上的表现更稳定另一方面像 record、switch 表达式这类语法在写结构化输出时确实省事。1.3 什么场景其实不需要它不是说所有 Java 项目都必须引入 LangChain4j。如果你的需求就是拿着一个写死的 prompt 去调一次大模型拿到结果就完事那直接用 HTTP 客户端调官方接口反而更轻没有必要为一次调用引入一个框架。另外如果公司已经有一个成熟的 AI 网关统一封装了模型调用、限流和计费下游业务只需要通过网关 SDK 发请求那也可以不引入。LangChain4j 更适合的场景是需要在 Java 工程内部完成 prompt 编排、多轮对话、工具调度、文档检索这些组合逻辑时它能省掉大量自己造轮子的工作。2. 环境准备与第一个可运行程序Maven、JDK 和 API Key2.1 依赖怎么选以及各模块对应什么能力第一步先把依赖加上。我建议用 Maven虽然 Gradle 也没问题但 Maven 的中央仓库信息更全搜版本号方便。核心依赖只需要两个一个是langchain4j主模块另一个是你实际要用的模型供应商模块。我以 OpenAI 兼容接口和本地 Ollama 两个方向做示例pom.xml里的依赖大概是这样的dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency !-- 如果要用本地 Ollama把上面那个 open-ai 替换成这个 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama/artifactId version0.36.2/version /dependency真实项目的版本号建议以上 Maven 仓库里的最新稳定版为准不要盲目追最新尤其是beta和alpha的版本API 变动会比较频繁。我的习惯是每半年左右升级一次升级前先看 GitHub 上的CHANGELOG。下表是我实际用过或调研过的模块按场景整理给你参考Maven 模块适用场景常见模型示例langchain4j-open-aiOpenAI 官方接口、以及兼容 OpenAI 协议的模型gpt-4o-mini、qwen、deepseek 等langchain4j-ollama本地部署或内网环境无需 API Keyllama3.1、qwen2.5langchain4j-azure-open-aiAzure 企业环境走微软云发布GPT-4o 系列部署名langchain4j-google-ai-geminiGoogle Gemini 系列gemini-1.5-flash有一点要提醒并不是每个模型供应商都有自己的模块LangChain4j 社区有一个langchain4j-community组织很多第三方模型集成都放在那里。如果官方模块里找不到你要的厂商先去社区仓库搜而不是自己手写 HTTP 接入。2.2 API Key 管理不要在代码里硬编码接入 OpenAI 兼容接口最常见的问题是 Key 的管理。初学者通常会在代码里直接写apiKey(sk-xxxx)这在本地 Demo 无所谓但只要代码一提交到 Git 仓库这个 Key 就泄露了。我见过不止一次因为误提交 Key 导致账号被盗刷的案例那账单真的能让人血压飙升。正确做法是用环境变量或配置文件注入。比如export OPENAI_API_KEYsk-your-key-here代码里通过System.getenv(OPENAI_API_KEY)读取。如果用的是 Spring Boot可以用ConfigurationProperties把 Key 放到application.yml里并在.gitignore中排除。不管哪种方式基本原则就一条任何环境相关的凭证都不进代码库。如果你用的是本地 Ollama就没有 Key 的问题这也是我推荐新手先跑本地模型的原因之一。没有网络请求没有额度限制随便调试 prompt 不心疼。2.3 第一个 Hello World验证整条链路环境搭好后先写一个最小程序验证 Maven 依赖、Key、网络、模型服务四件事。代码如下import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; public class HelloLangChain4j { public static void main(String[] args) { ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); String answer model.generate(用一句话解释什么是大模型); System.out.println(answer); } }如果你用的是 Ollama只需要把模型构造换成import dev.langchain4j.model.ollama.OllamaChatModel; ChatLanguageModel model OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .build();第一次跑大概率会遇到几个常见错误我按排查顺序整理成一张表报错现象常见原因处理方式401 UnauthorizedAPI Key 错误或环境变量没读到检查环境变量是否注入Key 是否有多余空格404 model not foundmodelName拼写错误或该模型不在账号权限内去模型平台控制台确认可用的模型名ConnectException网络不通或 Ollama 服务没启动本地 Ollama 先执行ollama serve确认 11434 端口超时异常首次请求冷启动慢或配置的 timeout 太短把.timeout(Duration.ofSeconds(60))放宽一点跑通这第一个 Hello World后面所有内容都只是在这个基础上做叠加所以这一步值得慢一点把环境彻底弄明白。3. 从单向问答到连续对话消息类型、上下文与流式输出3.1 一次对话在LangChain4j里的消息骨架大模型的聊天接口本质上接收的是消息列表而不是单个字符串。LangChain4j 把消息抽象成四种类型SystemMessage系统指令、UserMessage用户输入、AiMessage模型回复、ToolMessage工具调用结果。一次完整的会话就是这四种消息按顺序排列的产物。SystemMessage 很关键它是你控制模型行为最重要的手段。同样的模型加上“你是一个严谨的 Java 代码评审专家”和加上“你是一个口语化的编程助手”输出风格完全不同。UserMessage 和 AiMessage 很好理解就是来回的对话记录。ToolMessage 只在模型调用工具时出现后面工具调用那一节会专门展开。明白这个消息结构你就能理解为什么 LangChain4j 的多轮会话不是靠魔法实现的它只是替你把历史消息一条条保存下来再在每次请求时重新拼进列表里发给模型。3.2 同步调用和 ChatResponse 的解析如果只想拿最终回答用上一节的model.generate(String)就足够了。但真实业务里往往需要更多信息比如模型用了多少个 token、为什么截断、以及完整的消息内容这时候就要走更完整的调用方式ChatResponse response model.chat(介绍一下 Java 17 的 record 特性); AiMessage aiMessage response.aiMessage(); System.out.println(aiMessage.text()); TokenUsage usage response.tokenUsage(); System.out.println(输入token: usage.inputTokenCount()); System.out.println(输出token: usage.outputTokenCount());这里我用的model.chat()是较新版本的 API早期版本里这套逻辑叫generate()现在generate依然保留但更推荐chat。如果 IDE 提示方法已废弃不用慌看它推荐的新方法签名照抄即可。有一点容易被忽略model.chat()接收的不只是单个字符串也可以传ChatMessage或消息列表。这意味着你可以自己控制发送窗口比如只把最近 5 轮对话发给模型而不是全量发送这在控制 token 成本时非常实用。3.3 流式输出做一个像 ChatGPT 一样的打字机效果同步调用有个问题模型生成几百字可能要等两三秒页面一直空白用户很容易以为程序卡死了。解决办法是流式输出把模型生成的 token 一个个或一小批一小批推给前端还原 ChatGPT 那种打字机效果。LangChain4j 中流式模型的接口是StreamingChatLanguageModelimport dev.langchain4j.model.chat.StreamingChatLanguageModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.output.Response; import dev.langchain4j.message.AiMessage; import dev.langchain4j.model.StreamingResponseHandler; StreamingChatLanguageModel model OpenAiStreamingChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); model.chat(讲一个程序员冷笑话, 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(); } });回调方法也不难理解onNext每来一个片段就触发一次onComplete在全部生成完成后触发onError处理异常。Web 场景下通常会把onNext里的内容通过 WebSocket 或 SSE 推给前端。我自己的经验是在 Spring Boot 里用SseEmitter和它配合非常顺手前端用EventSource接收基本零成本实现打字机效果。需要注意不同版本的 LangChain4j 对StreamingResponseHandler的方法签名有过调整有些版本onNext的参数是ChatMessage而不是String。遇到编译报错时先看 IDE 里提示的抽象方法是什么照着实现即可思路完全一样。3.4 两个最常被忽略的配置项temperature 和 timeouttemperature控制随机性数值越低越保守适合事实问答类任务数值高一点更发散适合头脑风暴。我在实际项目中默认给 0.3只有在生成营销文案这类偏创意的场景才调到 0.8。这个参数放进 builder 链式调用就行OpenAiChatModel.builder() .apiKey(key) .modelName(gpt-4o-mini) .temperature(0.3) .timeout(Duration.ofSeconds(60)) .build();timeout很容易被忽略。模型推理高峰期响应慢是很常见的事如果你不显式设置某些 HTTP 客户端的默认读超时只有几秒稍微一慢就直接报超时。我建议至少给到 30 秒长文本生成场景给 60 秒更稳妥。4. 让接口变成AI能力AiServices的结构化输出、工具调用与记忆4.1 结构化输出让模型直接返回POJO和枚举而不是JSON字符串很多 Java 开发者第一次接大模型都会被一个问题搞疯怎么让模型稳定返回 JSON常见做法是在 prompt 里写“请以 JSON 格式返回”然后在代码里解析再写一堆 try-catch 处理模型偶尔抽风的情况。LangChain4j 的AiServices提供了一种更优雅的方式。你定义一个接口用注解声明规则框架自动帮你完成提示词构造和结果解析。比如做一个情感分析import dev.langchain4j.service.AiServices; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE } interface SentimentAnalyzer { SystemMessage(你是情感分析专家只能返回POSITIVE、NEUTRAL或NEGATIVE。) Sentiment analyze(UserMessage String text); } // 使用 SentimentAnalyzer analyzer AiServices.builder(SentimentAnalyzer.class) .chatLanguageModel(model) .build(); Sentiment result analyzer.analyze(这个产品太好用了强烈推荐); System.out.println(result); // POSITIVE我当初第一次跑通这段代码时最震撼的点在于框架真的能解析枚举完全不用我手写正则。它内部其实就是帮你构造了一个带格式要求的高质量 prompt再用模型的 JSON 模式约束输出最后反序列化成目标类型。你可以把返回类型换成自定义 record比如record ProductInfo(String name, BigDecimal price, String brand) {} interface ProductExtractor { SystemMessage(从用户描述中提取商品信息只输出事实不要猜测。) ProductInfo extract(UserMessage String text); }如果模型偶尔解析失败框架会抛出OutputParsingException你可以在调用处捕获并降级重试。结构化输出实际是“提示词工程 数据解析”的组合不是模型保证万能这一点后面避坑部分我还会展开。4.2 工具调用让模型具备计算、查询和执行能力大模型本身不会做数学计算也不会查数据库但你可以给它“工具”。LangChain4j 里在方法上标一个Tool注解该方法就会暴露给模型模型在需要时发起调用框架负责把调用结果回传给模型形成一次完整循环。举个最经典的例子——给模型一个加法计算器import dev.langchain4j.agent.tool.Tool; public class CalculatorTools { Tool(计算两个整数相加) public int add(int a, int b) { return a b; } } interface MathAssistant { String chat(UserMessage String message); } MathAssistant assistant AiServices.builder(MathAssistant.class) .chatLanguageModel(model) .tools(new CalculatorTools()) .build(); String answer assistant.chat(请帮我计算 123 456 等于多少); System.out.println(answer);你不会真的看到“模型去调函数”这个动作实际过程是模型收到问题后根据Tool注解里的描述决定调用add框架执行方法得到579构造一条ToolMessage再发回模型模型看到结果后才给出最终回答。整个循环是自动的用户无感知。一个有用的经验Tool的描述写得越具体模型越知道什么时候该用。比如只写“计算两个整数相加”就比“加法”好很多。如果工具参数要表达复杂语义可以直接用 record 作为入参类型LangChain4j 会把它序列化成模型需要的 JSON Schema。实际项目里我给这个模式接通过库存查询、订单状态查询、权限校验相当于把一个标准的“函数输出”变成了模型可用的“能力”。4.3 会话记忆用ChatMemory解决多轮对话失忆问题如果你只用上一节的接口每调一次就是一次独立会话模型不会记得你上一句说了什么。要让 AI 有“记忆”就需要引入ChatMemory。LangChain4j 提供了MessageWindowChatMemory它就像一个滑动窗口只保留最近 N 条消息import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; interface ChatAssistant { String chat(UserMessage String message); } ChatMemory chatMemory MessageWindowChatMemory.builder() .maxMessages(20) .build(); ChatAssistant assistant AiServices.builder(ChatAssistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build(); System.out.println(assistant.chat(我叫小明喜欢喝美式咖啡)); System.out.println(assistant.chat(我叫什么爱喝什么));第二次调用时模型能够正确回答“你叫小明喜欢喝美式咖啡”。maxMessages(20)表示窗口内最多保留 20 条消息超出后最老的消息会被丢弃。这个机制也暗示了一个事实记忆只是“把历史消息重新发给模型”所以窗口越大越贵也越容易超长。如果你的对话很长建议按业务需求动态调整窗口大小而不是无脑设置一个很大的数。多用户场景还要注意如果ChatMemory是全局单例所有用户会共享上下文互相污染。正确做法是按用户或会话维度创建独立的 memory 实例并定期清理不活跃会话。5. 给模型装一个内部知识库RAG的最小落地与参数调优5.1 RAG是什么什么时候必须用它大模型的知识截止于训练数据公司内部的制度文档、产品手册、私有代码库它一概不知道。想要让模型基于这些资料回答问题有两个思路一是微调成本高、周期长只适合大规模固定知识二是 RAGRetrieval-Augmented Generation检索增强生成先检索出相关文档片段把片段拼到 prompt 里再让模型回答。大多数业务场景选 RAG 就对了。它不需要训练模型新增文档随时入库来源可追溯模型答错了还能排查是检索问题还是生成问题。我做的第一个 RAG 项目就是一个内部客服问答机器人把几百份常见问题文档切块后向量化用户提问时先召回最相关的几个片段再由模型组织回答准确率比纯靠 prompt 高了一大截。5.2 Embedding 模型与向量存储是 RAG 的两个轮子RAG 依赖两个核心组件EmbeddingModel负责把文本转成向量EmbeddingStore负责存向量并做相似度检索。LangChain4j 中OpenAI 的text-embedding-3-small是目前性价比很高的选择import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(text-embedding-3-small) .build();向量存储方面InMemoryEmbeddingStore适合中小规模场景数据量不大时直接用这个就够了。生产环境可以换成 Redis、PGvector、Elasticsearch 这类支持向量索引的存储LangChain4j 都有对应的模块接口保持一致。5.3 一个最小可用的文档问答流程完整流程分五步读取文档、切分文本、向量化、存入向量库、检索并回答。我写一个最精简的版本import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.UserMessage; // 1. 准备文档 Document doc Document.from(LangChain4j是一个Java生态的大模型应用框架... 它支持OpenAI、Ollama、Gemini等多个模型供应商... 通过AiServices可以快速实现结构化输出和工具调用。); // 2. 切分每500字符一块前后重叠50字符 ListTextSegment segments DocumentSplitters.recursive(500, 50) .split(doc) .stream() .map(d - TextSegment.from(d.text())) .toList(); // 3. 向量化 ListEmbedding embeddings embeddingModel.embedAll(segments).content(); // 4. 存向量库 EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); embeddingStore.addAll(embeddings, segments); // 5. 构建检索器并组装问答接口 EmbeddingStoreContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.65) .build(); interface KnowledgeAssistant { String answer(UserMessage String question); } KnowledgeAssistant assistant AiServices.builder(KnowledgeAssistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .build(); String answer assistant.answer(LangChain4j支持哪些模型供应商); System.out.println(answer);走完这五步你就拥有一个能回答私有知识问题的机器人了。实际项目中文档来源可能是 PDF、Word 或数据库记录需要按数据源写不同的 loader。LangChain4j 的langchain4j-document-parser系列模块对常见格式已经很友好能省不少事。5.4 召回参数怎么调到靠谱的水平RAG 效果差十有八九不是模型问题而是检索参数没配好。下面这几个参数是我反复调过之后总结的初始值和调优方向参数作用建议初始值调优经验切块大小单段文本长度决定向量化的粒度500 字符太短容易把一句话拆断太长检索噪音大重叠字符数前后两块的重复字符50防止关键信息恰好在切缝处被拆散maxResults召回片段数量3-5太少可能漏信息太多会撑爆上下文还引入噪音minScore相似度阈值低于该值的片段不召回0.6-0.7阈值越高越精准但召回量越少一个我特别想强调的细节RAG 的 prompt 设计也很重要。不要只把文档片段原样拼进去最好在系统提示词里明确写“只能根据提供的资料回答资料中没有的信息要如实说明不知道”。否则模型一旦自信地编造RAG 的溯源优势就全没了。6. 我踩过的坑和排查思路LangChain4j 开发中的五个真实问题6.1 Maven 依赖冲突导致 NoSuchMethodError现象是代码编译没问题一运行就抛NoSuchMethodError指向某个 LangChain4j 内部类的方法。这种错误八成是多个 LangChain4j 模块版本不一致导致的比如核心模块是 0.34OpenAI 模块却是 0.35两个版本的内部类不兼容。排查思路很简单在pom.xml里运行mvn dependency:tree把所有dev.langchain4j开头的依赖版本对齐。我是用一个properties变量统一管理版本号的properties langchain4j.version0.36.2/langchain4j.version /properties所有 LangChain4j 模块都引用${langchain4j.version}从根上避免版本漂移。6.2 上下文超长模型直接报 400 或“maximum context length”错误当聊天窗口maxMessages设得过大或者某个文档被整段塞进 prompttoken 数会超过模型上限报错信息通常很明确一眼就能定位。但更难查的是文档切片没生效把整本文档当成一个DocumentSegment提交了。我遇到过同事把 PDF 全文直接作为 prompt 传给模型10 万 token 直接打爆。预防办法有两个一是所有待输入文本必须经过切分二是对单次请求增加 token 计数保护。LangChain4j 的TokenCountEstimator接口可以在请求前估算 token 数超过阈值就直接走降级策略而不是硬着头皮发请求。6.3 结构化输出不稳定模型偶尔返回废话用AiServices定义枚举或 POJO 返回时绝大多数请求都能正确解析但偶尔模型会不按规则来比如在 JSON 前后加一句解释。框架会抛出OutputParsingException如果你没捕获整个请求就失败了。我的应对方案是三层兜底。第一层在SystemMessage里写得足够强硬明确“只输出JSON不要任何解释”。第二层调用处捕获OutputParsingException用稍低的 temperature 重试一次。第三层如果结构化输出是核心链路返回类型尽量用包含String rawOutput的 record 作为兜底把原始文本也保留下来宁可业务侧自行处理也不要让数据丢。6.4 工具对象本身要考虑线程安全和无状态在用AiServices.builder().tools(new CalculatorTools())时如果这个Assistant是全局单例那么CalculatorTools实例也会被多线程共享。如果你的工具方法里有内部可变状态比如一个累加计数器会在并发场景下出现各种诡异结果。经验之谈工具类务必保持无状态所有的入参通过方法参数传进来方法内部不要依赖成员变量。如果工具必须访问外部服务把外部服务作为依赖注入确保它本身是线程安全的。6.5 不同模型供应商之间的参数差异LangChain4j 抽象了供应商差异但没完全抹平全部差异。比如 OpenAI 的gpt-4o-mini支持 JSON 输出模式但某些开源本地模型在这方面的表现就不稳定minScore阈值对不同的 embedding 模型也不能用同一个经验值。我踩过最实际的坑是同一套 RAG 代码从 OpenAI 切到 Ollama 本地模型后召回质量明显下降。最后发现不是因为代码逻辑变了而是本地模型的 embedding 向量维度和语义能力弱于商用模型。解决方式也很朴素——针对不同模型单独调召回参数并建立一份参数对比表切换模型时直接查表而不是凭感觉改。一些收尾的私人建议如果让我给刚入门的人提一条最实在的建议就是不要一上来就追着 LangChain4j 的高级功能跑。先把一个最基础的model.chat()跑通再去折腾流式输出和结构化输出然后才是工具调用和 RAG。这个顺序不是按难度排的而是按依赖关系排的每一步踩的坑都会成为下一步的认知基础。还有一个很实际的小提醒在你的日志里把每次调用的模型名、输入 token 数、输出 token 数、耗时全打出来。有了这些日志你才能在线上环境快速判断问题到底出在模型、检索还是你自己的代码。我靠这套日志排查过不少线上问题也靠着它说服过同事不要盲目加大模型参数。LangChain4j 的模型供应商模块里大多提供.logRequests(true)和.logResponses(true)这类开关本地调试时打开开发环境部署前记得关掉避免敏感信息泄露。