资讯详情

资讯详情

SpringBoot+LangChain4j+Ollama:本地大模型智能客服实战

做客服类项目的同学应该都有同感业务方一上来就说“给我接个大模型”方案评审的时候数据合规的同事先拍桌子了——用户手机号、订单详情、历史会话记录哪一个都不能发到外部 API。于是方案拐了个弯把模型放进内网自己跑。这也正是我今天想聊的这套组合的典型落地场景SpringBoot LangChain4j Ollama本地大模型接入 Java 应用快速搭出智能客服。这个组合适合谁适合已经用 Java 技术栈做业务的团队想在不引入 Python 服务、不改造现有数据链路的前提下低成本验证智能客服的效果也适合数据敏感、模型并发要求不高、只求“能用且安全”的内部系统。读完你至少能 Get 到三件事为什么是这三样东西组合在一起、从零到接口跑通需要过哪些关键环节、真正上线时最容易在哪些地方翻车。1. 先想清楚本地大模型客服到底解决了什么问题1.1 为什么放着云端 API 不用偏要折腾本地部署先讲一个我实际碰到的取舍过程。接到需求时第一版方案写的是接云端大模型 API理由很直接效果最好、开发最快、不用买卡。结果一圈评审下来被否掉的原因大家也猜得到——客服会话里会带上用户 ID、收货地址甚至支付信息数据一旦发到外部责任边界就说不清了。后面改成私有化部署模型跑在公司内网机器上所有对话原文不出机房合规这边才点头。这不是说云端 API 一无是处。如果你做的是公开网站上的智能助手不涉及敏感数据直接用云端 API 是最优解效果强又省运维。但客服场景有一个天然特点它和业务系统绑得太深。要让模型回答“我的快递到哪了”必然要把订单数据喂给它数据一旦穿过边界后面的合规审计就非常被动。本地部署的第二个好处是可控性。模型参数、版本、升级节奏全在你手里回答口径可以通过提示词和检索统一管控。第三个才是成本——客服请求量大、单次问答 token 消耗又多长期跑云端按量付费并不便宜自建一台带 GPU 的工作站摊薄下来可能比按量更划算。当然也得诚实说代价要有人懂模型运维效果上限比商用大模型低硬件投入要先出。1.2 SpringBoot、LangChain4j、Ollama 在链路里各干什么这套组合选型不是拍脑袋定的而是各自补位的SpringBoot承载对外接口、业务逻辑、会话管理和已有后台系统打通这是 Java 团队的舒适区。Ollama把开源大模型装进内网并跑起来的工具。它把模型下载、量化、推理封装成一套命令行和本地 API不用自己研究模型加载细节。LangChain4jJava 侧的 LLM 编排框架负责“怎么和大模型对话”这件事——会话记忆、提示词管理、工具调用、知识库检索都靠它。用一个餐厅比喻Ollama 是把厨师请进门的那道管道负责让大厨模型在后厨把菜做出来LangChain4j 是领班知道先上什么、后上什么、客人之前说过什么还知道什么时候让服务员去库房拿东西SpringBoot 则是前厅的接待系统客人来了登记、下单、结账都是它的活。三者职责清晰、边界分明所以后面出问题时好排查——接口慢看 Ollama答非所问看记忆和提示词工具参数错了看注解。2. 模型挑选与 Ollama 环境准备这一步决定后面顺不顺2.1 客服场景模型怎么选先看显存再看中文能力模型选错后面全白搭。客服项目不需要多强的数学和复杂推理要的是中文理解好、回答稳定、加载快。当前开源模型里我建议第一台机器从qwen2.5:7b入手。这个模型中文语料训练充分对订单、物流、售后这类常见业务话术理解准确7B 参数量化后对显存压力不大单卡就能跑。为了让你选型时有参照我按常见的显存配置给一个粗略表模型量化后体积约建议显存中文效果适用场景qwen2.5:7b4.7GB8GB 起步好客服问答、知识库、工具调用首选qwen2.5:14b9GB16GB更好回答质量要求高愿意接受更慢llama3.1:8b4.7GB8GB一般英文为主的场景可考虑qwen2.5:32b20GB24GB接近商用效果预算充足、硬件充足再上这里要提醒一句只看模型文件体积还不够实际显存开销还包括 KV Cache。上下文越长、并发越高KV Cache 占的显存越多。所以宁可选小一号的模型给并发和长上下文留出余地。我之前就干过“非得用大模型结果一并发就爆显存”的事后来降到 7B反而稳定了。2.2 Ollama 安装与模型拉取几个值得注意的小细节Ollama 的安装本身不复杂官网下载对应平台安装包或者 Linux 上用官方脚本装装完先确认服务起来了ollama pull qwen2.5:7b ollama list ollama psollama pull拉取模型ollama list查看本地已有模型ollama ps查看当前正在内存/显存中加载的模型。首次 pull 会下载几个 GB需要耐心等。有几个细节值得现在就说模型默认存储在系统盘Windows 会在用户目录下建.ollama文件夹Linux 也一样。如果系统盘紧张提前设置环境变量OLLAMA_MODELS指到数据盘再启动服务不然十几 GB 的模型很容易把 C 盘塞满。默认服务只监听127.0.0.1:11434Java 应用在同一台机器上没问题。如果 SpringBoot 跑在另一台服务器上要给 Ollama 设置OLLAMA_HOST0.0.0.0同时注意防火墙和接口鉴权别裸奔到内网里。模型首次调用时会加载到显存大概要十几秒甚至更久。后面我会讲怎么用OLLAMA_KEEP_ALIVE让它驻留否则高峰期频繁冷启动会让人崩溃。2.3 先用 curl 把模型接口调通再写 Java 代码很多人装了 Ollama 直接写 Java 代码一跑就报连接拒绝然后开始怀疑依赖配错了。其实最快的方式是先用 curl 给 Ollama 发消息确认模型本身没问题再排查 Java 侧。Ollama 原生生成接口curl http://localhost:11434/api/generate -d { model: qwen2.5:7b, prompt: 你是一个客服助手请用一句话介绍退货流程。, stream: false }Ollama 同时提供一个 OpenAI 兼容的接口路径是/v1/chat/completions。LangChain4j 的 Ollama 模块走原生接口但你知道有这层兼容后面接其他中间件时更有数。调通之后顺手测试几个客服高频问题催发货、改地址、发票抬头变更看看模型中文回答的口径。这一步是给“模型效果预期”定基线——如果基线连基本话术都不太行后面别急着写代码先换模型或调提示词。3. SpringBoot 工程搭建最小可跑的问答接口3.1 依赖与版本JDK 17 起步Spring Boot 3 以上工程建议 JDK 17 起步、Spring Boot 3.2 以上。LangChain4j 在 Java 生态里更新很勤目前主流用法是引入它的 Spring Boot StarterMaven 坐标大概是dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama-spring-boot-starter/artifactId version1.0.0-beta2/version /dependency版本号你可能需要根据自己项目时间点去 Maven 仓库看最新版。注意一个坑LangChain4j 的 0.x 和 1.x API 有一些差异网上老博客里的写法可能不适用于新版本建议以官方文档的 API 为准。首次下载依赖时如果特别慢配好 Maven 镜像源后面会舒服很多。3.2 配置文件里每个参数的含义引入依赖后Spring Boot 的自动配置会读取application.yml里的langchain4j前缀配置。一个可用的最小配置langchain4j: ollama: chat-model: base-url: http://localhost:11434 model-name: qwen2.5:7b temperature: 0.3 timeout: 60s log-requests: true log-responses: true逐个说一下base-urlOllama 的地址Java 应用和 Ollama 同机就用默认地址跨机改成内网 IP。model-name必须和本地已拉取的模型名称完全一致否则 Ollama 会尝试在线拉取拉不下来就报错。temperature客服场景建议 0.2 到 0.5。温度越高回答越发散越低越稳定。客服话术需要稳定别调高。timeout本地模型推理比云端慢得多尤其首次加载时超时设太短会出现“明明模型在跑前端却报超时”的假故障。log-requests/log-responses调试阶段打开上线记得关掉。否则每个请求的完整 prompt 和响应都打日志量很大而且可能把敏感信息打出来。3.3 第一个对话接口不整花活先跑通再说配置好了直接在任意 Controller 里注入ChatLanguageModel写一个最简接口RestController RequestMapping(/api/chat) public class ChatController { private final ChatLanguageModel chatModel; public ChatController(ChatLanguageModel chatModel) { this.chatModel chatModel; } PostMapping public MapString, String chat(RequestBody MapString, String body) { String message body.get(message); String answer chatModel.generate(message); return Map.of(reply, answer); } }把服务启动起来用 curl 测一下curl -X POST http://localhost:8080/api/chat \ -H Content-Type: application/json \ -d {message: 你好请问退货怎么操作}能拿到一个像样的文本响应就说明 SpringBoot、LangChain4j、Ollama 三层已经打通。这个最小闭环是一切的基础后面的记忆、工具、知识库、流式输出都是在这个闭环上叠加能力。如果这一步没跑通大概率在三个层面Ollama 服务没起来连接拒绝、模型名不对404 或自动拉取、ChatLanguageModelbean 没注入成功依赖或配置问题。先看 Ollama 日志再看 Spring Boot 启动日志逐层排查。4. 从“问一句答一句”到真正的客服记忆、Prompt 和流式输出4.1 多轮记忆窗口机制怎么设作用域怎么隔离跑通最小接口后如果你直接拿给业务用第一个被吐槽的肯定是“没有记忆”。用户说“我昨天退的货到哪了”模型不认识“昨天”因为每次请求都是独立的。LangChain4j 里加记忆很简单用MessageWindowChatMemory包一层窗口并通过AiServices生成带记忆的客服 AgentBean CustomerSupportAgent customerSupportAgent(ChatLanguageModel chatModel, OrderTools orderTools) { return AiServices.builder(CustomerSupportAgent.class) .chatLanguageModel(chatModel) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .tools(orderTools) .build(); }withMaxMessages(20)表示保留最近 20 条消息作为上下文。为什么用窗口而不是把所有历史都塞给模型因为大模型的上下文窗口是有限的把几个月前的聊天都带上不仅浪费 token还会稀释当前意图的注意力甚至因为信息过载让模型答偏。20 条左右基本能覆盖一次客服会话的完整脉络——从报问题、追问细节、到确认结果这个量级够用。比“加不加记忆”更隐蔽的问题是记忆的作用域。如果我把这个 Agent 注册成 Spring 的单例 bean所有用户共享同一个记忆窗口。A 用户上一句问的是“退货”B 用户接着发一句“那什么时候退款”模型会以为 B 在接着 A 的话题聊回答错乱。客服场景必须按会话维度隔离记忆要么在每次请求时按 sessionId 从工厂创建独立 Agent要么按会话 ID 分配独立的ChatMemory。我在项目里用的是后者思路——按会话 ID 维护一个 Map 缓存会话结束或超过一定时间后清理避免内存泄漏。4.2 系统提示词怎么写模型才不乱说话记忆解决的是“记得住”提示词解决的是“不乱说”。客服机器人第一版效果好不好的关键往往就是系统提示词。我比较推荐直接在接口上用SystemMessage声明public interface CustomerSupportAgent { SystemMessage( 你是内部系统的智能客服助手。你的任务是根据用户问题结合工具查询结果与已有知识给出准确、友好、简洁的回答。 必须遵守以下规则 1. 语气自然不要用书面套话不要编造不存在的功能 2. 凡涉及订单查询、物流状态必须先调用查询工具获取真实数据禁止凭想象回答 3. 如果资料不足或不确定请直接告知用户“我帮你转人工处理”并结束对话 4. 回答控制在150字以内。 ) String chat(String userMessage); }几个容易被忽略的细节规则要写“必须”而不是“可以”。模型对确定性指令的执行力比语气温和的建议强得多。明确告诉模型哪些事可以做、哪些事不能做边界越具体越不容易翻车。不要在这里写“你是某某公司的客服”写“你是内部系统的客服”即可避免模型被角色描述误导生成夸大宣传内容。4.3 流式输出SSE 让客户不用干等本地 7B 模型在中等配置机器上生成一段 100 字的回答可能需要三五秒甚至更久。如果用普通 HTTP 接口用户点一下发送界面转圈几秒才出结果体验很糟糕。更合理的是流式输出让用户看到文字一个一个蹦出来延迟感知从“几秒黑屏”变成“第 0.5 秒就开始出现内容”。Spring Boot 里用SseEmitter实现 SSE 很直接GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(RequestParam String message) { SseEmitter emitter new SseEmitter(60_000L); streamingChatModel.generate(message, new StreamingResponseHandlerAiMessage() { Override public void onNext(String token) { try { emitter.send(SseEmitter.event().data(token)); } catch (Exception e) { emitter.completeWithError(e); } } Override public void onComplete(ResponseAiMessage response) { emitter.complete(); } Override public void onError(Throwable error) { emitter.completeWithError(error); } }); return emitter; }注意这里注入的是StreamingChatLanguageModel而不是普通的ChatLanguageModel。两者是独立的接口自动配置分别创建 bean别混。前端用EventSource或 fetch 的流式读取接口接上即可。流式接口的第一个坑是超时时间生成耗时长SseEmitter的超时要比普通请求放宽建议 60 秒以上第二个坑是代理层。某些中间代理服务器对长连接有闲置回收策略SSE 半天不发数据会被掐断需要定时发个心跳帧保持链路不断。5. 客服还得会查自己的业务库工具调用与知识库问答5.1 Tool 工具调用让模型连上真实业务数据对话式客服如果只能闲聊价值有限。业务方真正要的是“我的订单到哪了”“为什么发票还没开”这类需要查真实数据的回答。这里就要用到 LangChain4j 的工具调用能力。思路很简单给模型暴露一个或多个 Java 方法当模型判断用户问题需要查询真实数据时它会自己决定调用这个方法并把返回结果组织成回答。这个机制本质上是把模型当成一个“决策器”数据仍由你的业务代码掌控模型接触不到数据库连接安全边界很清晰。一个订单查询工具的示例Component public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService orderService; } Tool(根据订单号查询订单当前状态与物流信息) public String queryOrderStatus(P(用户提供的完整订单号) String orderNo) { Order order orderService.findByOrderNo(orderNo); if (order null) { return 未找到该订单请让用户核对订单号; } return 订单状态 order.status() 物流信息 order.expressInfo(); } }然后通过AiServices.builder(...).tools(new OrderTools())注册给客服 Agent前面 4.1 的代码就是这么接的。这个过程的几个关键细节Tool的描述要写清楚“这个方法能干什么、什么时候调用”模型就是靠这段描述决定是否调用工具的。描述含糊它就会在不需要查库的时候瞎调。比如“查询订单”这种描述就不够要写成“当用户询问订单状态、物流进度、配送时间时调用”。方法参数上的P同样重要它告诉模型“这个参数应该填什么”。我之前见过一个案例参数描述只写了“订单号”模型就把用户说的任何数字都往里面塞改成“用户提供的完整订单号通常以字母开头”之后准确率立刻上来了。工具返回的文本是给模型读的不是给用户看的。所以返回内容要结构化、信息完整模型会自己把它翻译成口语化答复。5.2 知识库 RAG把帮助文档变成模型可检索的记忆业务里还有一大块需求是“回答知识问题”比如退换货规则、发票政策、会员权益。这类内容会不断变化也不可能把整本手册塞进提示词。正确做法是 RAG检索增强生成先把文档切分成片段、向量化存入向量库用户提问时先检索最相关的几个片段再塞给模型作为参考。LangChain4j 里可以自己搭一个最简流程。首先用 Ollama 拉一个 embedding 模型ollama pull bge-m3然后在 Java 侧配置 embedding 模型和向量库EmbeddingModel embeddingModel OllamaEmbeddingModel.builder() .baseUrl(http://localhost:11434) .modelName(bge-m3) .build(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); // 建库把FAQ片段向量化后存入 ListTextSegment faqSegments loadFaqSegments(); store.addAll( faqSegments.stream() .map(segment - embeddingModel.embed(segment).content()) .toList(), faqSegments );检索阶段用EmbeddingStoreContentRetriever把向量库接到 AiServices 上模型就能在回答时自动带上相关文档片段。如果你的 FAQ 只有几十条、几百条内存向量库完全够用文档量大、需要持久化时再引入专用向量数据库接口设计是兼容的以后切换成本不大。这里要特别提醒embedding 模型的上下文长度有限文档切分太大会导致检索精度下降。客服 FAQ 一句一问一答每段保持在一两百字左右最合适。还有个实际经验切分时不要硬按固定字数截断尽量按“问题 回答”的完整语义块来切检索出来的片段才是用户和客服都看得懂的整体。5.3 边界兜底与转人工别让模型硬着头皮瞎编不管模型能力多强客服系统的底线能力是“知道什么时候认怂”。硬编一个答案比说“我不知道”更害人。我在系统提示词的规则里专门写了不确定就转人工。同时可以在工具层加一个转人工工具Tool(将用户转接给人工客服并附上对话小结) public String transferToHuman(P(转人工原因便于人工介入) String reason) { return 已为用户创建转人工工单原因 reason; }这样模型在遇到查不到资料、规则冲突、用户情绪激动等情况时会主动调用这个工具生成工单而不是硬着头皮编。这个设计上线后很快就体现出价值了——大部分投诉类会话最终都走了转人工通道模型只负责第一轮分流和基本信息收集既保住了体验又把责任边界划清楚了。6. 上线前必须处理的事并发、超时、资源监控与避坑总结6.1 本地模型的并发能力与排队策略本地模型不是无限并发的。Ollama 收到请求后如果模型没有加载会先完成加载然后按一定规则排队处理。并发请求多的时候不是“大家都有响应”而是“所有人都等着”。所以上线前一定要想清楚多少个请求同时进来系统会不会雪崩。Ollama 侧有几个环境变量可以调OLLAMA_KEEP_ALIVE控制模型加载后驻留内存的时间默认 5 分钟。客服流量有明显高峰低谷如果频繁冷启动会把时间浪费在加载上可以调到 30 分钟或更久。OLLAMA_NUM_PARALLEL控制同一模型的并行请求数。并行升高会显著增加显存占用毕竟每个并发请求都有自己的 KV Cache。业务上如果是零散的客服咨询一般不需要高并行调成 2 或 4 就够。OLLAMA_MAX_LOADED_MODELS最多同时驻留几个模型。如果你既跑 chat 模型又跑 embedding 模型默认配置可能把其中一个挤下去导致频繁换载。应用侧我建议再加一个自己的队列或限流。比如用信号量控制最大并发请求数排队等待的请求给前端一个友好的“当前咨询较多请稍等”提示比让所有请求一起挤压 Ollama 更稳。这里还要说一个硬件相关的经验7B 模型在纯 CPU 上生成速度可能只有每秒几个 token一段百字回答要等半分钟这种体验拿来给内部员工用都会被嫌弃。有条件一定要上 NVIDIA 显卡哪怕是 8GB 显存的入门卡体验都会有质的提升。GPU 机器就专心跑模型服务别让它同时承担桌面渲染或其他任务。6.2 超时与首次加载的坑这一节列的坑基本是我实际踩过一遍的首次请求超时。模型冷启动要从磁盘读权重、申请显存、构建 KV Cache一次可能花十几秒甚至半分钟。如果你把应用超时设成 3 秒第一次请求必挂。解决方式有两种要么把超时放宽到 60 秒以上要么系统启动后主动发一条预热请求把模型“叫醒”常驻。连接超时与读取超时要分开设置。Ollama 接口建立连接是很快的慢的是模型生成。如果你只配一个总超时会把“连接挺快、生成很慢”这种情况误判成故障。前端网关超时。就算应用侧设了 60 秒如果前端还挂着一层代理而这个代理默认超时只有 30 秒照样会在中间被切断。SSE 流式输出模式下要额外确认代理关闭了缓冲、放宽了读写超时。6.3 质量评估与灰度小技巧用数据决定上线节奏模型回答质量怎么评估是很多团队容易忽略的一环。我的做法是上线第一周不追求自动化开日志记录每一轮用户输入和模型输出每天抽 50 条人工过一遍按“完全正确 / 可接受 / 错误 / 错误且无兜底”四档打标。跑两周之后错误集中在哪一类问题、哪个场景需要补知识库、哪个工具调用需要优化数据自然说话。日志记录要处理好脱敏用户 ID、手机号、地址这些字段在入日志前做掩码处理不然本地部署也白搭。另一个很实用的灰度思路不要一上来就把所有流量交给模型。先让机器人只处理“退换货流程”“开发票时间”这类规则明确的高频问题其余问题一律转人工跑稳定之后再逐步扩大范围。这样即使模型在某些边界场景出错影响范围也可控。最后分享一点我个人体会这套链路真正跑起来之后最大的感受不是“大模型真神奇”而是“工程化细节决定了能用不能用”。工具调用描述写清楚一点、超时放宽一点、日志脱敏做好一点这三点做到位哪怕模型不是顶配客服机器人也能在日常高频问题上顶住反过来任何一个环节偷懒上线后接二连三的投诉都会找上门。如果你也是第一次在 Java 项目里接本地大模型我建议按这个顺序走先 curl 验证 Ollama再跑通最小接口然后加记忆和提示词最后才上工具调用和知识库。每一步都确认稳了再往下一层走。硬要说还有什么建议那就是先选 qwen2.5:7b 做第一个模型别一上来就挑战大参数等业务确实需要更好效果时再评估往上加显存和换模型。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →