SpringBoot + LangChain4j + Ollama 这套组合,最近在 Java 圈讨论度挺高。简单说,它解决的是“Java 应用怎么快速、低成本地接入本地大模型”这件事。过去想给业务系统加一个 AI 对话能力,要么调用云端 API,要么用 Python 写服务再通过 HTTP 对接,链路长、成本高、还得处理跨语言调用的各种问题。现在用 LangChain4j 这个 Java 生态的 AI 框架,配合 Ollama 直接拉起本地模型,SpringBoot 项目里加几行配置就能跑通一个智能客服 Demo,而且数据不出内网,隐私和成本都好控制。
这篇文章我会从零开始,把整套方案的选型理由、环境搭建、核心代码、实战避坑全部过一遍。适合正在评估本地大模型落地、又不想把技术栈搞得太复杂的 Java 工程师。内容偏实战,我尽量把每一步为什么这么做讲清楚,而不是光贴代码。
1. 方案选型:为什么是 SpringBoot + LangChain4j + Ollama
1.1 这套组合解决了什么问题
先说应用场景。很多企业内部有着大量“重复性问答”需求,比如 IT 支持、HR 咨询、业务规则查询。传统做法是维护一个问答库或者机器人脚本,问题是规则写死了,用户换种说法就匹配不上。接入云端大模型 API 能解决理解能力的问题,但数据要发到外部服务,很多公司对这一点非常敏感。而本地部署模型,数据全程在内网,又面临调用门槛高、工程化难的困境。
LangChain4j 这个框架很有意思,它在 Java 世界里做的事情,相当于 Python 生态里 LangChain 的角色,把大模型调用、Prompt 管理、上下文记忆、知识库检索这些能力包装成 Java 程序员熟悉的 API。配合 Ollama,你可以在本地把模型跑起来,暴露一个兼容 OpenAI 格式的接口。SpringBoot 负责业务编排和 Web 层,LangChain4j 负责 AI 交互,Ollama 负责模型推理。三层职责清晰,整个链路完全可控。
这套组合的核心优势在于工程成本低到不像本地 AI 方案。不需要 GPU 服务器,一台 16G 内存的普通开发机就能跑 7B 级别的量化模型;不需要学习 Python 和额外的服务框架;不需要复杂的外部依赖,Ollama 自动处理模型下载和运行时环境。对于想快速验证“大模型到底能在业务里干什么”的团队,这是性价比极高的起步姿势。
1.2 模型选型与资源规划
Ollama 支持的模型很多,但选错模型是新手最常见的坑。如果是面向中文场景的智能客服,我建议优先考虑 qwen2.5 系列,它在中文理解和指令跟随上表现稳定,而且 Ollama 官方库里有直接可用的量化版本。追求英文效果或者代码能力,llama3.1 也是备选。要注意的是,模型不是越大越好,7B 和 8B 级别的模型,在 16G 内存的机器上跑量化版,响应速度还能接受;13B 或 70B 模型对资源的要求是成倍增长的,体验反而会下降。
内存规划有个简单的估算方法:模型本身要占一份内存,加载后的上下文窗口(context window)要占一份,应用本身还要留一份。比如 qwen2.5:7b 的量化版大约 4.7G,加上 8K 上下文约 2G,再算上 SpringBoot 应用和操作系统开销,整机 16G 内存是及格线。如果你想同时跑知识库的 embedding 模型,那我建议直接 32G 起步,资源预留充足能省掉大量排查问题的时间。
显存方面,Ollama 会自动检测机器上有没有 NVIDIA 显卡,有就用 CUDA 加速,没有就纯 CPU 推理。实测下来,纯 CPU 跑 7B 量化模型生成一段回复,速度大约每秒 10-20 个 token,能做完 Demo 演示,但生产环境还是建议至少一张 8G 显存的显卡。这个速度差异,直接决定了你是“能用”还是“好用”。
1.3 环境准备与安装
先把基础环境搭起来。Ollama 的安装非常简单,Windows 和 macOS 直接下载安装包,Linux 用一键脚本。装完之后在终端验证一下版本,再拉取目标模型。这里我强烈建议先单独把模型跑通,再用 Java 去接,否则很容易把“模型没下好”和“代码有 bug”混在一起排查。
拉取模型和启动服务的命令如下:
# 安装 Ollama(Linux / macOS) curl -fsSL https://ollama.com/install.sh | sh # 验证安装 ollama --version # 拉取中文表现较好的 7B 模型 ollama pull qwen2.5:7b # 启动服务(默认监听 11434 端口) ollama serve服务起来之后,先用命令行验证模型是否正常。这一步成功,说明模型侧没问题,接下来所有精力都可以放在 Java 接入上。
ollama run qwen2.5:7b "你好,请介绍一下你自己"SpringBoot 项目这边,要求版本别太旧,JDK 17 以上、SpringBoot 3.x 是 LangChain4j 官方主推的组合。如果你的项目还停留在 JDK 8 或 SpringBoot 2.x,也能用,但某些高级特性受限,建议尽量升级。
2. 接入大模型:写一个最简单的对话接口
2.1 引入依赖与基础配置
LangChain4j 的 SpringBoot Starter 把大部分自动配置都做好了,引入之后基本就是写业务代码。在 Maven 的pom.xml里加入以下依赖:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>1.0.0</version> </dependency>版本号这里要多说一句,LangChain4j 的版本迭代非常快,API 调整也比较频繁。我写这篇文章时用的 1.0.0,你实际使用时建议去 Maven 中央仓库看最新的稳定版,同时留意 CHANGELOG 里的破坏性变更。版本不匹配最常见的报错就是方法签名不对或者找不到实现类,这类问题排查起来很头疼,所以一开始就锁一个稳定的版本组合。
然后是在application.yml里做基础配置。核心配置只有两项:Ollama 服务的地址和模型名称。
langchain4j: ollama: chat-model: base-url: http://localhost:11434 model-name: qwen2.5:7b temperature: 0.7 timeout: 60s这里base-url是 Ollama 服务的地址。默认端口是 11434,如果你把 Ollama 部署到了其他机器,改成对应 IP 和端口就行。temperature控制回复的随机性,智能客服场景建议调低到 0.3 左右,让回答更稳定、更不容易胡说八道。timeout是请求超时时间,本地模型推理速度不稳定,设个 60 秒比较保险,避免业务侧先超时了。
2.2 第一行对话代码
配置完成之后,你不需要手动创建任何客户端对象,直接通过依赖注入拿到ChatModel接口就能开始对话。这是 LangChain4j 对 SpringBoot 生态最大的贡献,它把模型交互抽象成和调用本地 Service 一样简单。
下面是最简单的对话接口实现:
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatModel chatModel; public ChatController(ChatModel chatModel) { this.chatModel = chatModel; } @PostMapping public String chat(@RequestBody String message) { return chatModel.generate(message); } }这段代码的核心价值在于它的简洁。ChatModel接口封装了与大模型的所有交互细节,包括请求构建、协议转换、响应解析。你传入一个字符串,它返回一个字符串,其他事情一概不用关心。启动应用,用 POST 请求调用这个接口,就能体验第一条本地大模型生成的回复。
curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: text/plain" \ -d "你好,请问你们公司的年假制度是怎样的?"2.3 流式响应与超时调优
上面的例子是普通的阻塞式响应,也就是模型生成完所有 token 才返回。在本地模型上,这种体验不太好,生成一段 200 字的回复可能要等 10 到 20 秒,用户看到的是一直转圈。更好的方案是用流式响应,像 ChatGPT 网页版那样一个字一个字蹦出来。
LangChain4j 对流式响应的支持是开箱即用的,用StreamingChatModel接口就能实现。在 SpringBoot 里,结合 SSE(Server-Sent Events)可以把流式输出直接推到前端。
@RestController @RequestMapping("/api/chat-stream") public class ChatStreamController { private final StreamingChatModel streamingChatModel; public ChatStreamController(StreamingChatModel streamingChatModel) { this.streamingChatModel = streamingChatModel; } @PostMapping(produces = "text/event-stream") public SseEmitter chatStream(@RequestBody String message) { SseEmitter emitter = new SseEmitter(0L); streamingChatModel.generate(message, new StreamingResponseHandler<ChatResponse>() { @Override public void onNext(String token) { try { emitter.send(SseEmitter.event().data(token)); } catch (IOException e) { emitter.completeWithError(e); } } @Override public void onError(Throwable error) { emitter.completeWithError(error); } @Override public void onComplete() { emitter.complete(); } }); return emitter; } }这段代码要注意一点:SseEmitter的构造函数参数是超时时间,传0L表示不超时,让连接一直保持到模型回复完成为止。否则默认超时时间是 30 秒,而本地模型生成大段文本可能超过这个时间,前端会看到连接意外断开。
流式响应对用户体验的提升是质的改变。哪怕底层还是同一台机器上慢吞吞的本地模型,用户感知到的却是“流畅输出”,这个心理差异在智能客服场景里非常重要。我在实际项目里明显感觉到,同样的模型,流式输出时用户几乎不会抱怨响应慢,但阻塞式输出时经常被催问“是不是卡死了”。
2.4 自定义模型参数
ChatModel接口生成回复时,支持传入ChatRequest对象,这样就能对单次请求做更精细的控制。比如临时提高温度让模型回答更有创造性,或者传入图片数据做多模态识别。
ChatResponse response = chatModel.generate(ChatRequest.builder() .messages(AiMessage.from("请用专业的客服语气回答")) .temperature(0.3) .build());这里我想提醒一个容易被忽略的细节:温度参数的取值范围在不同模型上会略有差异,Ollama 的模型一般支持 0 到 1 之间的浮点数。调参的时候不要走极端,在本地模型上温度过高,回复的胡言乱语概率会明显上升。智能客服场景建议在 0.2 到 0.4 之间反复试几轮,找到既能稳定输出、又不至于太死板的平衡点。
3. 智能客服进阶:Prompt、RAG 与结构化返回
3.1 系统提示词的设计与坑
单纯能对话不算智能客服,智能客服的核心是“在限定范围内回答准确”。把一个大模型接入应用而不做任何约束,它什么都敢说。你问它公司年假制度,它可能会一本正经地编一个劳动法条款。解决这个问题的关键,是用系统提示词(System Prompt)限定模型的角色和行为边界。
LangChain4j 里可以通过SystemMessage来设置系统提示词,更优雅的方式是使用AiServices注解。下面是一个带系统提示词的示例:
ChatResponse response = chatModel.generate(ChatRequest.builder() .messages( SystemMessage.from(""" 你是一名企业智能客服助手,名叫小助手。 你的职责是回答员工关于公司制度、流程和IT支持的问题。 回答要求: 1. 简洁、专业、友好。 2. 如果问题超出你的知识范围,请明确告知员工"需要转接人工客服",不要编造答案。 3. 涉及敏感信息时,提醒员工遵守公司保密规定。 """), UserMessage.from("你能帮我写一首诗吗?") ) .build());我在设计系统提示词时踩过不少坑,这里分享几个最关键的教训。第一,不要只告诉模型“不能做什么”,还要告诉它“应该怎么做”。比如“遇到不会的问题要转人工”,这个指令足够明确,模型能理解;但如果你只说“别乱回答”,模型可能就沉默或给出模棱两可的回应。第二,提示词里要给出格式范例,让模型知道输出应该长什么样。第三,适当加入一些“防御性”表述,减少幻觉。但注意别写太多,系统提示词过长会挤占上下文窗口,导致对话质量下降。
3.2 引入本地知识库
真正合格的智能客服离不开知识库,也就是 RAG(Retrieval-Augmented Generation,检索增强生成)。原理很好理解:用户提问后,先从知识库里检索出相关片段,把这些片段作为参考材料塞给大模型,让模型基于这些材料来回答。这样回答有依据,幻觉大幅减少,而且还能实现“不知道就不说”。
LangChain4j 对 RAG 的支持比较完善,它把“文档切分 -> 向量化 -> 存储 -> 检索”这套流程都封装好了。我们只需要提供文档,然后通过EmbeddingStore来管理向量索引。在本地环境下,embedding 模型也用 Ollama 跑一个小的(比如 nomic-embed-text),这样整个链路完全本地化。
一个简单的实现思路是:先用DocumentSplitter把 PDF 或文本切分成小块,然后用EmbeddingModel把它转成向量,存入InMemoryEmbeddingStore。每次对话前,先检索 topK 个相关片段,拼接到 Prompt 里。
// 启动时加载文档并构建向量索引 @Configuration public class KnowledgeBaseConfig { @Bean EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) { InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); // 从指定路径读取文档 List<Document> documents = loadDocuments("src/main/resources/knowledge-base"); // 切分文本,每段最大500字符,重叠50字符 DocumentSplitter splitter = DocumentSplitters.recursive(500, 50); List<TextSegment> segments = splitter.splitAll(documents); // 生成向量并存储 List<Embedding> embeddings = embeddingModel.embedAll(segments).get(); store.addAll(embeddings, segments); return store; } }这里的核心参数是文档切分的大小。切得太大,检索出来的片段包含太多无关信息,模型反而找不到重点;切得太小,上下文割裂导致语义丢失。经过多轮测试,我觉得 400 到 600 字符是知识库问答场景的黄金区间。重叠部分设置 50 字符左右,避免把一个完整语义强行切到两段里,检索时漏掉关键信息。
需要注意的是,InMemoryEmbeddingStore只适合项目启动后一次性加载小规模知识库(几十份文档)。如果知识库量级达到几百份文档,内存占用和加载时间会明显上升。这时候需要换成支持持久化的向量数据库,比如本地的 Chroma 或 LanceDB,LangChain4j 对它们都有适配。
3.3 用 Function Calling 打通业务系统
智能客服做到这一步,能回答知识库内的问题了,但还有一类需求绕不开:用户问“我的订单到哪了?”“我的工单处理进度如何?”。这些信息不在文档里,而在业务系统数据库里。要回答这类问题,需要用 Function Calling 能力,让大模型根据用户意图主动调用 Java 方法。
LangChain4j 的@Tool注解让这个功能落地非常轻松。定义一个 Bean,里面写一个查询订单状态的方法,加上@Tool注解,LangChain4j 会自动把方法描述和参数结构告诉模型。当模型判断用户的问题需要查询订单时,它会生成一个调用请求,LangChain4j 帮你拦截下来,执行方法,然后把结果返回给模型,模型再组织成自然语言回复。
@Component public class OrderTools { @Tool("查询订单物流状态,参数为订单号") public String getOrderStatus(String orderId) { // 从订单服务查询状态 OrderStatus status = orderService.queryStatus(orderId); return String.format("订单 %s 当前状态:%s,预计送达:%s", orderId, status.getCurrentStatus(), status.getEstimatedDelivery()); } }使用的时候,用AiServices为客服构建一个带工具能力的服务对象。这个AiServices是 LangChain4j 里很核心的类,它把ChatModel、工具、记忆这几个组件粘合在一起。
CustomerServiceAgent agent = AiServices.builder(CustomerServiceAgent.class) .chatModel(chatModel) .tools(new OrderTools()) .build(); String response = agent.chat("帮我查一下订单 DD20240101 到哪了");这段逻辑的关键在于,模型自己会判断调不调用工具。你说“你好”,它就正常寒暄。你说“查订单”,它就触发getOrderStatus方法。它还可能识别出信息缺失,比如你没提供订单号,它会主动追问你。这种“模型主动发起动作”的能力,是智能客服自动化程度的分水岭。
实测里 Function Calling 有两个容易踩的坑。一个是@Tool注解的方法签名要尽量用简单类型,如果参数太复杂,模型生成的 JSON 容易解析失败。另一个是工具方法必须返回字符串,LangChain4j 目前对复杂对象返回的支持还不友好,最好在方法内部就把结果转成一句话。
4. 生产落地:性能优化与部署实践
4.1 实测常见问题与排查
这部分我把实际跑这套方案时遇到的典型问题整理成一个速查表,都是经常反复出现的,值得收藏。
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 首次请求极慢 | 模型还在加载 | 预热策略,应用启动后先发一个空请求让模型常驻内存 |
| 请求超时 | 模型推理速度慢于超时时间 | 调大 LangChain4j 和 SpringBoot 的超时时间 |
| 中文回答夹杂英文 | 提示词未指定中文 | 系统提示词里明确写“请始终使用中文回答” |
| 回答内容重复或死循环 | 温度过高或上下文过长 | 调低温度,启用最大 token 限制 |
| 内存不足导致 OOM | 模型+应用占用过高 | 减小上下文长度,更换更小的量化模型 |
| 模型离线但服务正常 | Ollama 服务被系统杀掉 | 设置 systemd 服务或 Docker 容器时加自动重启策略 |
这里展开讲两个最常见的问题。第一个是开发时ChatModel偶尔会报connection refused,但 Ollama 明明在跑。十次里有八次是 Ollama 服务只监听了本地回环地址,而 Java 应用尝试用局域网 IP 访问。除非有特殊需求,保持默认的localhost访问就行,别主动跨主机访问。
第二个是本地模型幻觉问题。7B 模型的幻觉率比在线大模型要高不少,尤其在涉及数字、日期、法律条款时。一个实用的缓解策略是,在系统提示词里加一句“如果用户询问的信息未在参考资料中明确出现,请回答无法确定”。同时,在回复下方固定展示一句免责声明“以上内容由 AI 生成,仅供参考,请以官方信息为准”。这既是技术上的兜底,也是产品上的合规考量。两条防线一起上,用户投诉率会明显下降。
4.2 性能与并发调优
单用户玩得转,不代表多用户并发也扛得住。Ollama 的特点是按需加载模型,默认情况下一个模型只加载一份,所有请求排队通过同一份模型做推理。如果同时来 5 个客服对话请求,后面的请求会在 Ollama 的请求队列里排队,响应时间大幅拉长。
针对这个瓶颈,有几个实战调优思路。第一,给 Ollama 设置更大的并发批处理能力,在启动时通过环境变量OLLAMA_NUM_PARALLEL指定并行请求数,但要注意这会成倍增加显存或内存占用。我建议从 2 开始测,逐级往上加,观察响应延迟。第二,对话历史是内存消耗大户,智能客服的在线人数越多,上下文累积的内存占用越夸张,做好 Session 级的历史裁剪,只要最后几轮就够了。
# 设置 Ollama 可同时处理的最大请求数 OLLAMA_NUM_PARALLEL=4还有一个很重要但容易被忽视的点:定时清理闲置模型。Ollama 支持通过环境变量设置模型闲置时间,超时自动释放。这样不会有模型一直霸占内存。在部署环境里,这个参数非常管用。
4.3 部署与运维注意事项
本地大模型应用部署和普通 SpringBoot 应用不太一样,资源规划是核心课题。如果目标机器没有 GPU,建议直接上 Docker Compose 把 Ollama 和应用编排到一起。Ollama 官方提供了容器镜像,好处是环境隔离,模型文件和运行时都打包好,换机器直接拉起来就能跑。注意要给容器挂载一个数据卷,否则每次重建容器都要重新拉模型,非常浪费时间。
Ollama 在 Docker 里的启动部分配置示例:
version: '3' services: ollama: image: ollama/ollama:latest ports: - "11434:11434" volumes: - ollama_data:/root/.ollama environment: - OLLAMA_KEEP_ALIVE=1h restart: unless-stopped volumes: ollama_data:这个OLLAMA_KEEP_ALIVE=1h环境变量是一个很实用的经验:它让模型在空闲 1 小时后才从内存释放。设得太短,频繁的加载模型会让请求体验很差;设得太长,模型一直占用内存,影响其他服务。1 小时是我测下来比较均衡的值。
生产环境里一定要加内存监控,我强烈建议在任何监控面板上加上 Ollama 进程内存的告警。本地模型最怕的内存溢出和 OOM 问题,往往发生在并发高峰时,如果提前设置了内存告警阈值,就能在故障发生前介入。语言模型服务跟普通 Web 应用不同,它没有优雅降级,内存一爆就是整体崩溃。
4.4 从 Demo 到真正可用的智能客服
很多人觉得用 LangChain4j 写个 Demo 很快,但 Demo 到生产之间有一条巨大的鸿沟。这条鸿沟不是技术上的,而是体验和信任上的。做智能客服,我的建议是先圈定一个极窄的领域,比如“IT 报障助手”或“入职咨询助手”,只回答这一个领域的问题,知识库文档控制在 20 份以内。这个范围内模型准确率能做得很高,用户感受到的是“这机器人真懂”,而不是“这 AI 又瞎说”。
等到这个窄领域跑顺了,再逐步扩展知识库范围,每一步都做回归测试。千万不要一上来就试图覆盖公司所有制度,那样知识库一多,检索相关性就会明显下降,模型幻觉率也会回升。我见过好几个项目就是死在“想一步到位”上,结果上线没几天被用户反馈淹没了。
实测下来,20 份以内文档、回答准确率能做到 85% 以上;扩展到 100 份文档,准确率可能掉到 60% 左右。这不是模型变笨了,而是检索召回难度随知识量非线性上升。所以如果知识库注定很大,就得在检索策略上下功夫,比如做分类目录、分段继承、关键词加权,这些是 RAG 进阶的话题了。但从一个窄领域起步,是成本最低、见效最快的路径。
在实际项目里,我还有一个体会:接大模型这件事,真正花时间的不是写代码,而是打磨提示词、调整知识库结构、反复测试边界情况。所以如果你想用这套技术栈,先在本地把 Ollama 和 LangChain4j 跑起来,然后用一天时间把你手头最熟悉的业务文档整理成知识库,做成一个能回答你同事真实问题的机器人。改 Prompt、调切分参数的过程,就是你对这套方案建立直觉的过程。项目成败的关键,恰恰在于这个迭代过程里你积累的对模型脾气的理解。