LangChain4j实战:Java开发者的大模型应用开发与RAG集成
1. 为什么是 LangChain4jJava 生态的大模型开发缺口先聊一个我自己的感受。2024 年之后身边做 Java 后端的朋友几乎都在讨论大模型接入的事。但如果你去搜教程满屏都是 Python 的 LangChain、LlamaIndexJava 开发者想找个正经的参考材料得翻半天。要么是官方文档的机器翻译味太冲要么是那种hello world 级的 demo跑通了也不知道下一步该干嘛。LangChain4j 就是来补这个缺口的。它是专门为 Java 和 Android 生态设计的 LLM 集成框架思路和 Python 版 LangChain 高度对齐把模型调用、消息管理、工具调用、RAG 这些高频场景抽象成统一的 API。你不需要自己封装 HTTP 请求、手动拼接 Prompt、管理多轮对话的上下文窗口——这些脏活累活框架都替你干了。我自己的使用场景很明确公司内部有个基于 Spring Boot 的知识库问答系统之前接的是第三方 HTTP API每次都要手动维护会话状态、处理流式输出、写 JSON 解析代码里全是样板逻辑。后来切到 LangChain4j一套ChatLanguageModel接口通吃所有厂商代码量直接砍掉一半还多。这篇教程我不打算讲那些抽象的大概念就按我自己从零摸到能上线的路径来写先搭环境、再跑通基础对话、然后接 RAG、再做流式输出和工具调用最后聊几个真实踩过的坑。适合谁看后端 Java 工程师、想做 AI 应用但不想换语言栈的团队以及那些被 Python 生态劝退的 Spring Boot 老手。2. 环境准备与依赖引入版本选型是第一道坑2.1 基础环境要求先说硬性条件。我开发机上的环境是这样的JDK 17官方要求是 8 以上但我建议至少 17后面某些高级特性会省很多事Maven 3.8 或 Gradle 7.5Spring Boot 3.x如果你想集成 Web 场景一个可用的模型 API Key或者本地部署的 Ollama如果你还在用 JDK 8 且暂时不打算升级LangChain4j 也能跑但在异步流式、虚拟线程等特性上会受限。能升就升这跟当年从 Java 8 迁 JDK 11 一样早升早享受。2.2 Maven 依赖配置LangChain4j 的模块划分很有意思它是按厂商和功能双重维度拆分的。你既需要引入具体的模型厂商包比如 OpenAI、Ollama、Azure OpenAI也需要引入核心包和可选的功能包。以我目前最常用的组合为例OpenAI 兼容接口 基础对话properties langchain4j.version0.33.0/langchain4j.version /properties dependencies !-- 核心包必须 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- OpenAI 兼容接口 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency !-- 如果需要 RAG引入文档解析器 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-easy-rag/artifactId version${langchain4j.version}/version /dependency /properties如果你用的是阿里云百炼、DeepSeek、智谱这些国内厂商它们通常提供 OpenAI 兼容的 HTTP 接口所以langchain4j-open-ai这个包也能直接复用只要改 baseUrl 和模型名就行。这一点非常关键——不需要为每个厂商各学一套 SDK。2.3 版本选型的一个血泪教训这里必须单独说一句版本的事。LangChain4j 的版本迭代速度非常快从 0.30 到 0.33 几乎是两周一个版本而且经常有 breaking change。我第一次接入用的 0.28照着 0.31 的文档写代码结果ChatLanguageModel的构建器方法名都变了愣是编译不过去。我的建议是不要追最新版选相对稳定且文档示例多的版本比如 0.33.0 或 0.34.0以官方 GitHub 仓库的 README 示例代码为准不要信第三方博客的代码包括我这篇代码示例尽量对照官方核实升级大版本时逐个版本看 CHANGELOG重点看Breaking Changes条目2.4 构建 ChatLanguageModel 是最核心的基础对象依赖引入后第一步是构建一个ChatLanguageModel实例。这个对象就是整个框架的心脏所有类型的对话、生成、工具调用都从它出发。ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(DASHSCOPE_API_KEY)) .modelName(qwen-plus) .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .temperature(0.7) .timeout(Duration.ofSeconds(60)) .build();注意几点apiKey优先从环境变量读取不要硬编码在代码里尤其是公司项目baseUrl决定了你用哪家厂商。用 OpenAI 官方就写官方地址用国内厂商就写对应的兼容地址temperature控制回复的随机性。做事实问答调到 0.2 以下做创意写作可以拉到 0.8 以上构建好这个对象后你可以先做个最简单的验证String answer model.generate(用一句话介绍 LangChain4j); System.out.println(answer);跑通这一步说明环境没问题接下来的所有玩法都可以在这个基础上展开。3. 对话基础与消息管理从单轮到多轮的架构演进3.1 单轮对话的简洁之美model.generate(一句话介绍你自己)这种单轮调用简单是简单但真实项目里几乎不会这么用。因为用户的问题往往需要结合上下文——比如你问他刚才那句话是什么意思如果模型不记得刚才说了什么就会答非所问。LangChain4j 对多轮对话的处理思路很清楚用ChatMemory管理消息历史用AiServices把模型和记忆黏合在一起。这是它和裸调 API 最大的区别。裸调 API 时你每次要自己把聊天记录塞进 messages 数组还要计算 token 数防止爆上下文。LangChain4j 内部帮你做了这些事你只需要告诉它记住最近 N 轮对话。3.2 引入 ChatMemory 实现多轮会话看这段带记忆的代码Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); String first assistant.chat(我的名字叫张三我是一名 Java 工程师); String second assistant.chat(我叫什么名字做什么工作); System.out.println(second);MessageWindowChatMemory.withMaxMessages(20)的含义是最多保留最近 20 条消息包括用户消息和模型回复。超过这个窗口最古老的记录会被自动丢弃。这是一个非常实用的机制——你要知道大模型 API 是按 token 计费的上下文越长费用越高响应也越慢。有个窗口上限等于给成本上了个保险。那 20 条这个数字怎么定我自己的经验是普通客服场景 10~20 条足够但要保证能容下用户在第 5 轮提到的信息在第 15 轮又引用它这样的跨度。如果业务中用户会大量上传文档内容这个窗口可能还要调大但也要警惕 token 数暴涨。3.3 AiServices 接口设计的优雅之处这里我忍不住要夸一下AiServices的设计。它让你用定义一个 Java 接口的方式来声明 AI 交互能力而不是赤裸裸地传字符串。比如你想做一个带系统提示词和记忆的客服助手interface CustomerServiceAssistant { SystemMessage(你是一个专业的电商客服回答要简洁、友好如果不知道答案就说不知道不要编造。) String chat(String userMessage); }然后构建CustomerServiceAssistant assistant AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(15)) .build();这样就干净多了。业务代码里只需要调用assistant.chat(userInput)和调一个普通 Spring Service 没有任何区别。你完全可以把CustomerServiceAssistant做成 Spring 的单例 Bean 注入到 Controller 里后面我会讲具体的整合方式。我实际项目中就是这么写的Controller 只负责接收 HTTP 请求把参数透传给 ServiceService 内部持有Assistant实例会话 ID 到 ChatMemory 的映射关系放在一个 ConcurrentHashMap 里维护。这比每次请求都 new 一个记忆窗口要合理得多——状态管理永远是状态管理不管是不是 AI。3.4 系统提示词管住模型嘴的橡皮筋SystemMessage注解其实就是系统提示词的声明式写法。系统提示词的威力我举一个真实例子公司内部有个审批助手一开始没写系统提示词模型面对这个申请批不批这种模糊问题时经常给出模棱两可的回答。后来在系统提示词里加了一条硬性规则——你是审批系统助手只做信息归纳和风险提示不要代替人工做批准或拒绝的决定——输出质量立刻上一个台阶。写系统提示词有几个原则要具体不要你要做一个有用的助手这种空话给边界明确告诉它能做什么、不能做什么给格式约束比如只输出 JSON不要 Markdown 代码块下面这个例子同时约束了角色和格式interface DataExtractor { SystemMessage(你是信息抽取助手。只从用户输入中抽取姓名、电话、地址格式为 JSON不要输出其他内容。) UserMessage({{text}}) String extract(V(text) String rawText); }这里用到了UserMessage和V注解它们的作用是把方法参数绑定到 Prompt 模板里的占位符。这个机制在做结构化数据抽取时非常好用相当于你写了一个带参数的 Prompt 方法模型只负责填空。4. RAG 实战让模型学会读文档再回答问题4.1 为什么需要 RAG以及它的完整链路大模型训练数据是有截止日期的你问它 2024 年 6 月之后的事情它只能编。更要命的是企业内部的知识库产品手册、技术文档、规章制度根本不在模型的训练数据里。RAG检索增强生成解决的就是这个问题让模型在回答前先去你的知识库里检索相关片段然后把片段作为参考输入给模型。RAG 的完整链路是加载文档 → 切分成片段 → 向量化 → 存入向量数据库 → 用户提问时把问题向量化 → 在向量库中相似度检索 → 把命中的片段塞进 Prompt → 模型生成回答。LangChain4j 把这条链路封装得比较完善关键是看你选哪些组件。不夸张地说这是 LangChain4j 里最值得学的部分也是新手最容易一头雾水的地方。4.2 技术选型哪些组件可以换、哪些必须固定RAG 里有三个核心选择1. Embedding 模型负责把文本变成向量。你可以用 Hugging Face 上的开源模型也可以通过 OpenAI 兼容接口调用。国内厂商基本都提供了 embedding 接口。我常用的是 BGE-M3 系列中文效果不错通过 Ollama 跑本地部署内网环境非常合适。2. 向量数据库负责存储和检索向量。可选的有 Chroma嵌入式适合小型项目、Milvus分布式适合大规模、Redis 向量检索、PGVector 等。我的建议是小项目直接上嵌入式向量库省去部署运维负担数据量上了百万级再考虑独立数据库。3. 分段策略把大文档切成多大的块、重叠多少字符。这个往往被新手忽略但它极大影响检索质量。LangChain4j 默认的DocumentSplitter是按固定字符数切分的比如 500 字符一个 chunk、重叠 50 字符。更高级的做法是按文档语义结构切分比如 Markdown 的标题层级。4.3 一个可运行的 RAG 示例从 PDF 到问答我以下面这个场景为例给一个 PDF 格式的产品说明文档实现基于它的问答系统。第一步加载文档做切分和向量化// 从文件系统加载 PDF Document pdfDocument Document.fromFile(Paths.get(product-manual.pdf)); // 切分成 chunk ListDocument chunks DocumentSplitter.recursive(500, 50) .split(pdfDocument); // 构建 embedding model这里用 Ollama 本地部署的 bge-m3 EmbeddingModel embeddingModel OllamaEmbeddingModel.builder() .baseUrl(http://localhost:11434) .modelName(bge-m3) .build(); // 向量化所有 chunk ListEmbedding embeddings embeddingModel.embedAll(chunks) .stream() .map(Embedding::content) .toList();第二步存入嵌入式向量库并构建检索器InMemoryEmbeddingStore embeddingStore new InMemoryEmbeddingStore(); embeddingStore.addAll(chunks, embeddings); EmbeddingStoreContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.6) .build();第三步把检索器注入 Assistant实现先检索再回答interface ProductAssistant { SystemMessage(你是产品知识助手只依据提供的资料回答不要编造资料中没有的内容。) String chat(String userMessage); } ProductAssistant assistant AiServices.builder(ProductAssistant.class) .chatLanguageModel(model) .contentRetriever(retriever) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build();就这么简单。contentRetriever会在每次用户提问时自动从向量库捡出最相关的 3 段文本maxResults3拼进 Prompt 里作为参考。整个过程对业务代码透明你调用assistant.chat(...)和之前没有任何区别。4.4 检索质量调优minScore 和 maxResults 怎么定这两个参数我单独拿出来说因为它们是 RAG 效果的胜负手。maxResults一次检索召回多少段。太少可能漏信息太多会把无关内容塞给模型造成注意力稀释。默认 3 一般是安全的。minScore相似度阈值。低于这个分数的直接过滤掉。这个参数防止问题跟某段文本八竿子打不着但因为没别的可选硬塞进来的情况。0.6 是我用的默认值但实际要看 embedding 模型的距离分布建议做一个线下测试看正确检索的分数大概在什么区间再定阈值。还有一个细节如果检索结果为空contentRetriever会怎么表现默认情况下就是不给模型额外的参考模型会直接用自己的知识回答。这其实是比较危险的行为——用户问的是产品手册里的内容如果没检索到模型就会开始编。我的做法是配置一个高minScore然后做一层兜底如果retriever没有返回任何内容就明确回复资料库中没有找到相关信息。这个逻辑如果你用默认的AiServices不好写就得自己封装一层 Service这块我会在避坑章节详细展开。5. 流式输出与工具调用从转圈圈到打字机体验5.1 流式响应的实现与前端对接用过 ChatGPT 的人都知道流式输出打字机逐字蹦是 AI 应用的体验标配。如果后端非要等模型生成完整回答再一次性返回用户面对的是大几十秒的加载转圈体验极差。LangChain4j 对流式响应的支持核心是一个StreamingChatLanguageModel接口回调里按 token 推送StreamingChatLanguageModel streamingModel OpenAiStreamingChatModel.builder() .apiKey(apiKey) .modelName(qwen-plus) .build(); Assistant assistant AiServices.builder(Assistant.class) .streamingChatLanguageModel(streamingModel) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build(); assistant.chat(讲一个关于程序员的笑话) .onPartialResponse(System.out::print) // 每来一段就打印 .onCompleteResponse(r - System.out.println(\n[DONE])) .onError(e - System.err.println(Error: e.getMessage()));这里onPartialResponse是流式回调每一次都是一个小块。在 Spring Boot 里做对接通常是把这个回调接到SseEmitter上用 SSEServer-Sent Events把内容推给前端这样前端就能在 div 里逐字渲染了。我的经验是流式输出对接前端时响应的格式建议统一为 SSE 标准格式事件名可以自己定这样前端 EventSource 就能平滑接入不需要 WebSocket 和额外的握手协议路径最短。如果是 Android 端OkHttp 对 SSE 的 EventSource 也自带支持。5.2 工具调用让模型拥有手和脚流式解决了体验问题但大模型本质上还是个嘴强王者——它能说会道但没法查数据库、调 API、执行代码。工具调用Function Calling就是给它装上手和脚。LangChain4j 的工具调用实现很优雅你写一个带Tool注解的 Java 方法框架自动把方法名、参数描述通过 Jackson 序列化参数告诉模型模型在需要时返回一个结构化的调用请求框架帮你执行并回传结果。5.3 一个真实的工具调用案例查天气 算订单折扣想象一个场景用户问北京明天天气怎么样适合穿什么如果模型没有工具它只能瞎猜。有了天气查询工具模型会先发起一次工具调用public class WeatherTool { Tool(查询指定城市未来一天的天气情况) public String getWeather(ToolParam(城市名) String city, ToolParam(日期格式 yyyy-MM-dd) String date) { // 这里去调用天气 API或者查数据库 return 北京市2025-01-06晴气温 -5~4℃风力 3 级; } }然后构建时把工具加入Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .tools(new WeatherTool()) .build();注意ToolParam里的描述信息不是写着玩的——模型会根据这些描述来决定传什么参数、什么时候调用这个工具。描述写得越精确调用的准确率越高。比如日期格式 yyyy-MM-dd就直接告诉模型该传什么格式避免它传个明天这种模型内部自己理解的相对时间。我再举一个更接近业务场景的订单折扣计算。模型本身算不清楚复杂的阶梯折扣但它可以调用一个 Java 方法来计算public class OrderTool { Tool(根据订单总金额和会员等级计算实际应付金额) public double calculateDiscount(ToolParam(订单原始金额) double amount, ToolParam(会员等级NORMAL/SILVER/GOLD) String level) { double rate switch (level) { case SILVER - 0.95; case GOLD - 0.88; default - 1.0; }; return amount * rate; } }这样用户说我买了 500 块的东西我是黄金会员打完折多少钱模型先调用calculateDiscount(500, GOLD)拿到结果后再组织语言回复。整个流程对用户是透明的。5.4 工具调用中的两个注意点第一工具返回的数据要做截断。模型单次对话的上下文有限如果工具返回一坨 5000 字的 JSON对话令牌数会直接爆掉。我的习惯是让工具返回精简后的字符串比如只返回标题和摘要不要返回整条数据库记录。第二需要关注工具调用的错误处理。如果工具执行失败比如天气 API 超时默认情况下模型会收到异常信息然后尝试回答。这往往会产生不准确的信息。我的做法是在工具方法内捕住异常返回明确的错误提示文本并引导模型回复天气服务暂时不可用请稍后再试。这远比让模型自由发挥要稳妥。6. Spring Boot 整合实践把 AI 能力写进业务接口6.1 注入与配置把模型对象变成 Spring Bean前面的示例都是把ChatLanguageModel和Assistant直接new出来。真正放到 Spring Boot 项目里应该把这些对象收编为 Bean。做法很简单写一个配置类Configuration public class LangChain4jConfig { Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(System.getenv(DASHSCOPE_API_KEY)) .modelName(qwen-plus) .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .temperature(0.3) .build(); } Bean public CustomerServiceAssistant customerServiceAssistant(ChatLanguageModel model) { return AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); } }然后 Secure 的 Controller 和 Service 做正常的依赖注入即可RestController RequestMapping(/api/chat) public class ChatController { private final CustomerServiceAssistant assistant; public ChatController(CustomerServiceAssistant assistant) { this.assistant assistant; } PostMapping(/message) public ChatResponse sendMessage(RequestBody ChatRequest request) { String reply assistant.chat(request.message()); return new ChatResponse(reply); } }这样 AI 链路和普通业务代码就完全融为一体了。你不再需要关心 API 调用细节assistant.chat()就是一个业务方法。6.2 会话管理一个用户一个 ChatMemory把 AI 集成进 Web 项目后最头疼的问题是并发会话。假设 100 个用户同时提问如果共用同一个ChatMemory所有用户的上下文就会串味——A 用户说我叫张三B 用户问我叫什么模型可能回答张三。我的解决方案是做一个简单的会话内存管理器Component public class ChatSessionManager { private final MapString, ChatMemory memoryMap new ConcurrentHashMap(); public ChatMemory getMemory(String sessionId) { return memoryMap.computeIfAbsent(sessionId, k - MessageWindowChatMemory.withMaxMessages(20)); } }然后构建 Assistant 时把chatMemory的动态获取做成自定义AiServices的上下文实现或者更简单一点——在 Service 层按用户 ID 对应不同的Assistant实例。如果用户量不大比如几千人用ConcurrentHashMapString, Assistant缓存即可。这里有一个性能细节每个ChatMemory实例会持有消息列表对象几万个 Session 就会产生几万个对象。建议给内存加个上限比如超过 10 万个 Session 后就按最近活跃时间淘汰旧的。至少你上线前需要有个清楚的内存规划否则就是个隐形的 OOM 风险。6.3 Spring Boot 3 与框架版本的兼容性LangChain4j 官方对 Spring Boot 3及 Spring Boot 2都有专门的 starter 支持。引入方式dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version${langchain4j.version}/version /dependency引入后配置文件可以直接在application.yml里写参数langchain4j: open-ai: api-key: ${DASHSCOPE_API_KEY} model-name: qwen-plus base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 temperature: 0.3但注意starter 不是必须的。它省去了手写Bean配置的麻烦但如果你有复杂的定制需求比如多个模型实例切换、动态 API Key手动Bean反而更灵活。我自己是手动配置派因为公司项目经常要在多个模型之间做灰度切换。7. 踩坑实录那些文档没写清楚的细节7.1 工具调用的底层格式变化LangChain4j 从某个版本开始把工具调用从Tool注解改成Tool注解且校验更严格。如果你升级版本后发现原来好好的工具不触发了第一个要查的就是注解包名是否变了。另外有个特别隐蔽的坑Tool方法所在的类必须是 public 的、不能是 final 的否则代理生成会失败。我有一次把工具类写成 final class编译器没报错但运行时怎么调都不触发工具排查了大半天。7.2 OpenAI 兼容地址的路径细节用国内厂商的 OpenAI 兼容接口时baseUrl的写法有讲究。有些厂商要求末尾带/v1有些不带。比如我用过的一个厂商正确写法必须是以/compatible-mode/v1结尾而另一个厂商则要求以自己的 endpoint 为根地址由 SDK 自己拼路径。一个保险的做法是每个厂商先写一个两三行的 main 方法构建模型后直接generate(你好)能通就继续不通就调 baseUrl 路径。这个调试成本极低能避免带着配置错误做完整项目。7.3 流式回调线程与 Spring MVC 的坑默认情况下的onPartialResponse回调是在 SDK 的异步线程里执行的。如果你在回调里直接操作 Spring 管理的非线程安全对象比如某些WebSocketSession、某些简易的响应对象会出现偶发性的并发问题。我的解决方案是在回调里使用线程安全的队列或直接通过SseEmitter的send它本身是线程安全的推送不要在回调里做复杂的业务操作。所有后续处理放到发送消息之后。7.4 系统提示词里的变量插值系统提示词不是纯静态的你往往想把一些动态信息插进去比如今天是{current_date}你是{bot_name}。我发现很多新手会直接拼字符串但 LangChain4j 提供了更优雅的方式interface DynamicPromptAssistant { SystemMessage(你是{{botName}}今天是{{currentDate}}。请用专业的语气回答。) String chat(V(botName) String botName, V(currentDate) String currentDate, V(userMessage) String userMessage); }全部用{{}}占位通过V绑定参数这样既清晰又避免了字符串拼接导致的 SQL 注入之类的安全风险虽然这里是模型 Prompt不是 SQL但思路是一致的。7.5 RAG 检索为空时的兜底策略我最开始做 RAG 时问一个资料库里根本没有的问题模型居然开始一本正经地编答案而且编得很像真的。后来在系统提示词里加了一句如果资料中没有提及任何相关信息请明确告知未找到相关资料禁止编造情况才好转。但纯靠提示词约束还不够可靠更稳的办法是拿到检索结果后人工判断相关度。LangChain4j 的EmbeddingStoreContentRetriever提供了minScore参数如果所有命中片段都低于阈值它返回空列表。你可以在外层封装if (retrieverResult.isEmpty())然后直接返回预设话术彻底阻断编造路径。8. 轻量级部署与成本优化个人项目也能玩得起很多人觉得接入大模型必须烧钱其实不然。如果你只是个人学习、内部小工具完全可以用本地模型跑起来LangChain4j 对 Ollama 的支持非常完善。8.1 Ollama 接入配置安装 Ollama 后拉一个量化过的模型比如 qwen2.5:7b 或 llama3.1:8bollama pull qwen2.5:7b然后 LangChain4j 里配置ChatLanguageModel localModel OllamaChatModel.builder() .baseUrl(http://localhost:11434) .modelName(qwen2.5:7b) .temperature(0.7) .build();注意本地模型和云端模型的temperature表现不完全一样同一个参数值在不同模型上产生的随机性不同。本地小模型默认参数下更容易跑偏建议把temperature调低 0.1~0.2。8.2 成本控制三板斧如果你的业务确实需要走云端大模型成本控制是必须面对的。我总结了三板斧控制 Prompt 长度指令尽量精简系统提示词不要写一千字的废话。每多 500 token成本就多一份。控制记忆窗口MessageWindowChatMemory.withMaxMessages(8)这种 8 条的小窗口足够大多数场景。太多历史消息除了费钱还会干扰模型对当前问题的判断。用本地模型做粗筛先让本地便宜的模型做意图分类只有需要高质量回答的问题才转给云端大模型。这是很经典的降本架构。8.3 降本架构的一个参考设计我之前帮某公司的内部工单系统做过一个这样的降本方案所有工单先用本地模型Ollama qwen2.5:7b做分类识别是常见问题还是疑难问题。常见问题直接从预设答案库里返回甚至是模糊匹配 模型润色只有疑难问题才走云端强模型qwen-max。整体成本降了接近 70%而用户满意度基本没变。这套架构用 LangChain4j 实现很简单无非是两个ChatLanguageModel实例加上 if-else 路由逻辑。关键是你的设计思路而不是代码量。9. 进阶从能跑到能上线的工程化思考9.1 可观测性日志与指标大模型应用的排错是出了名的难——你很难知道模型为什么答错是 Prompt 问题、检索问题还是模型本身抽风。所以上线前一定要做好可观测性。基本的三层请求日志记录每次请求的输入、输出、消耗 token 数、延迟。LangChain4j 提供了JsonStreamingResponseHandler等内置回调可以通过System.out或第三方日志框架输出。指标监控跟踪平均延迟、token 消耗速率、工具调用成功率。可以用 Prometheus Grafana 那套也可以基于日志自己统计。召回质量评估定期抽取一批真实用户问题人工检查 RAG 检索回来的内容是否匹配。不匹配就调minScore、maxResults或换 embedding 模型。9.2 流控与重试把不稳定交给策略模型 API 有一个特色比你想象的更不稳定限流、超时、返回格式异常是常态。所以消费者端的重试和熔断必须做。Spring Boot 项目里引入 Resilience4jdependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-spring-boot3/artifactId /dependency然后针对 AI 调用方法做配置单次调用超时 60 秒失败后最多重试 2 次重试间隔指数退避。如果重试也失败返回兜底话术。这套逻辑直接叠在AiServices外层即可。9.3 Prompt 版本管理很多人忽略 Prompt 也是要版本管理的但搞 AI 应用一段时间后你就会发现Prompt 的改动频率比代码还要高。我把所有系统提示词放在一个专门的类或配置文件里配合注解常量使用改动后要过 code review。有条件的话可以搞个简单的语义对比看看两版 Prompt 的输出差异再决定上线哪一版。我自己现在的习惯是把每个系统提示词写成一个独立常量并加注释说明它的适用场景和已知问题。比如public interface PromptTemplates { String CUSTOMER_SERVICE 你是一个电商客服助手。 规则 1. 只能依据提供的参考资料回答 2. 如果资料中没有回复抱歉这个问题我暂时无法回答 3. 回答不超过 200 字 ; }这样至少半年后回来看代码还能知道当时为什么这么写。10. 写在最后LangChain4j 的选型价值与未来预期聊回选型。当初我在 LangChain4j、Spring AI、以及直接手写 HTTP 调用之间做过比较。手写太原始Spring AI 当时还不够成熟LangChain4j 的优势在于发展时间更长、社区活跃度高、模块化清晰、对现有 Java 工程侵入小。尤其它的AiServices抽象让 AI 能力和业务代码的界限非常分明便于测试和替换底层模型。我的判断是如果你所在的团队是 Java 技术栈LangChain4j 会是你把大模型能力搬进生产环境的最短路径之一。它可能不会让你成为AI 专家但能帮你省下大量封装胶水代码的时间让你把精力放到真正有业务价值的部分——Prompt 工程设计、RAG 的检索质量、工具调用的业务逻辑。最后分享一个我个人的实操习惯每次新接一个模型厂商我都会用一个统一的 smoke test 类把简单对话、多轮记忆、工具调用、流式输出、RAG 检索五个场景各写一个测试方法跑一遍看通过率。这个测试类几乎不需要改换一个模型就换 baseUrl 和 key20 分钟内能评估一个新模型是否适合当前场景。这个习惯帮我避免过两次大坑一次是某厂商宣传的兼容 OpenAI结果工具调用格式有差异测试直接暴露另一次是本地模型虽然对话效果不错但检索结果的分值分布很低导致minScore一直误伤真实命中。如果你以后要和多个模型打交道我建议你也留一个这样的测试集。

相关新闻

OpenClaw 超级 AI 实战专栏【入门与环境】(七)必备工具安装:Python、Git、CUDA 快速配置(小白零踩坑版)

OpenClaw 超级 AI 实战专栏【入门与环境】(七)必备工具安装:Python、Git、CUDA 快速配置(小白零踩坑版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 17:42:09 阅读更多 →
dash.js 播放器设置(Settings)完全指南:从 updateSettings 到源码级参数详解

dash.js 播放器设置(Settings)完全指南:从 updateSettings 到源码级参数详解

音视频前端 【免费下载链接】dash.js A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers. 项目地址: https://gitcode.com/gh_mirrors/da/dash.js 点击查看 免费下载 dash.js 通过一套集中维护在 Settings.…

2026/10/12 17:41:08 阅读更多 →
混合模型时间序列预测:SARIMA基线、GMM状态识别与LSTM残差修正

混合模型时间序列预测:SARIMA基线、GMM状态识别与LSTM残差修正

简介:一套面向时间序列预测学习者的实战教程,聚焦 LSTM 与 Transformer 混合模型在复杂非线性序列数据上的应用。资源共 13 个文件,包括 Python 脚本、CSV 数据集、PNG 可视化图、需求说明文档及工程配置等,整体仅 1.74MB&#xf…

2026/10/12 17:41:08 阅读更多 →

最新新闻

CRF序列标注原理与实战:从全局最优解到工业级部署

CRF序列标注原理与实战:从全局最优解到工业级部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:01:48 阅读更多 →
Kibana汉化配置全解析:i18n.locale设置与版本兼容性指南

Kibana汉化配置全解析:i18n.locale设置与版本兼容性指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:01:48 阅读更多 →
FusionAccess虚拟桌面发放流程详解:从模板到用户登录的全链路拆解

FusionAccess虚拟桌面发放流程详解:从模板到用户登录的全链路拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:01:48 阅读更多 →
Hibernate No identifier异常:实体缺少@Id的原因排查与修复全指南

Hibernate No identifier异常:实体缺少@Id的原因排查与修复全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:01:48 阅读更多 →
进口变压器替换厂家怎么选?参数对照与CE认证全流程指南

进口变压器替换厂家怎么选?参数对照与CE认证全流程指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:01:48 阅读更多 →
claude-mem:为Claude Code打造自动跨会话记忆层

claude-mem:为Claude Code打造自动跨会话记忆层

最近一段时间一直在折腾一件事:让Claude Code在隔了一个晚上之后还能记得我昨天说过“这个模块用Python重写”以及“测试文件统一丢到tests目录”。中间试过各种“外置记忆”方案,最后真正解决问题的,是一个叫claude-mem的开源项目。claude-m…

2026/10/13 1:00:47 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →