LangChain4j构建医疗智能客服:RAG、多轮对话与生产实践

发布时间:2026/10/21 12:11:21
LangChain4j构建医疗智能客服:RAG、多轮对话与生产实践
硅谷小智这个项目说起来其实是被客服排班逼出来的。当时我们接手某医疗机构的智能客服改造人工客服每天要回答几千条高度重复的咨询挂号挂哪个科、周末能不能做检查、化验报告多久出、某个常用药的用法用量。这些问题答案相对固定重复度极高但又不能随便答错。我们想用大模型把这部分负载接下来后端最终选了 LangChain4j 来做整套 RAG 与多轮对话管道。这篇文章是这份实战笔记的第一篇重点讲三件事为什么选型选到 LangChain4j、一条医疗咨询消息在链路里经历了什么以及我们把项目跑进生产环境之后发现的关键边界和调整。适合正在做客服类项目、尤其医疗信息化的 Java 团队参考也适合想从 Python 系框架迁移到 Java 系 AI 框架的开发者。1. 为什么是 LangChain4j而不是继续用 Python 系框架1.1 原型不是没跑通而是交付遇到了现实问题我们最开始确实是用 Python 快速搭的 Demo。在模型能力允许的范围内做一个“喂知识库文档然后问答”的原型一周多一点就出来了当时组里都觉得这事稳了。真正的问题出在交付环节医疗机构的核心业务系统基本都是 Java 技术栈运维规范也偏保守服务器大多在内网环境变更要走流程。Python 那一套虚拟环境、依赖版本、进程守护在研发机上是好东西到了生产环境就成了运维同事反复确认的对象。加上 Demo 里使用了一堆第三方库版本一多锁依赖就变得烦躁。后来我们做了一次内部复盘结论很直接不是 Python 不行而是这个项目的土壤在 Java 这边。团队里大部分后端同事对 Java 是骨子里的熟悉与其为了一个客服项目单独建立一套 Python 运维体系不如找一个 Java 原生的 AI 开发框架。1.2 选型对照LangChain4j 处在最舒服的位置我把当时重点比较的几条路线整理了一下这个表格后来也直接放在了我们项目的设计文档里。路线优势劣势结论Python 系 LangChain 生态案例多、资料多、组件全与现有 Java 系统集成成本高、运维割裂放弃直接 HTTP 调用模型 API最简单、没有框架依赖多轮对话、知识库召回、记忆管理全要手写工作量不可控LangChain4jJava 原生、与 Spring Boot 无缝、RAG 组件开箱即用生态迭代快、版本间 API 有调整选定表格里最值得说的一条是纯 HTTP 调用。很多团队觉得“我又不搞复杂场景直接请求模型接口总行了吧”。短期的确行但客服场景不是单轮问答它有上下文、有知识库、有意图判断、有异常兜底这些逻辑如果全用裸 HTTP 拼接最终会变成一大坨没人敢改的字符串拼接代码。LangChain4j 把“会话记忆”“向量检索”“内容注入”“服务化接口”这些通用能力做成了可配置组件这正是我们需要的。1.3 真正让我拍板的三个点第一是 AiServices 抽象。LangChain4j 允许你用一个普通 Java 接口定义智能客服的行为比如String answer(String userMessage)框架会在背后帮你完成模型调用、上下文注入、工具调用等环节。这对习惯了接口驱动开发的 Java 团队来说学习曲线非常低。第二是会话记忆内置且便于替换。客服应用避不开多轮对话而多轮对话管理如果自己实现往往会陷入“又要存又要截断又要控制 token 成本”的泥潭。LangChain4j 提供了多种 ChatMemory 实现还能配持久化存储完全贴合我们的需求。第三是 Spring Boot 集成太顺。配置类、注解、bean 装配都是 Java 社区熟悉的套路代码审查、单测、打包部署都能走既有流程这在医疗项目里极其重要。我们用 LangChain4j 不是因为它是“最火”的框架而是在我们的约束条件下它是能把项目顺利交付的一个选择。这也是整个项目第一个值得记录的决策。2. 硅谷小智的整体结构一条咨询消息的完整旅程2.1 接入层网页端和小程序共用同一个后端服务硅谷小智的前端形态有两种医院官网上的悬浮问答框和就诊小程序里的客服入口。两个入口复用同一个后端接口。用户在输入框里打出一句话比如“我这两天一直咳嗽应该挂什么科”请求到达后端进入一条固定管道先做基础校验再判断是否有紧急关键词然后交给大模型会话服务。这一层要特别提一下响应方式。医疗场景里用户通常是带着烦躁或焦虑来的如果回答要等三四秒才完整返回体验就很差。我们用流式输出服务端通过 SSE 把大模型生成的文本一段一段推给前端用户几乎能看到“打字机效果”。第一个 token 出来很快用户感知到的等待时间大幅缩短。2.2 会话记忆窗口机制和持久化客服场景的核心是上下文。用户第一句问“我挂消化内科需要注意什么”第二句很可能就是“那他让我做胃镜胃镜需要空腹吗”。如果模型不记得上一轮的“消化内科”和“胃镜”这一句就答不准。我们在 LangChain4j 里用的是MessageWindowChatMemory它本质上是在一个滑动窗口里保留最近若干条用户消息和助手消息。窗口太大token 成本高、响应慢窗口太小多轮对话容易失忆。实测下来 10 到 12 条左右比较均衡既能覆盖大多数咨询对话的来回又不会撑爆上下文。会话的持久化也很重要。用户中途退出再进来或者从网页端切到小程序端他的历史上下文最好还在。我们把 ChatMemoryStore 接了一层数据库实现用会话 ID 做维度存储历史消息这样即使用户隔了几小时再回来客服也能延续之前的诊断思路。2.3 知识库层从院内文档到可检索片段医疗客服准确性的根基不是模型而是知识库。我们接入了大量院内文档导诊手册、科室介绍、检查注意事项、常见药物说明、规章制度问答等等。这些文档原始的格式很杂有 Markdown、有 Word、有 PDF还有一部分是既往客服团队整理的 Excel 问答对。流程上分为几步文档清洗、分段、向量化、入向量库。LangChain4j 提供了 Document 和 DocumentSplitter 机制可以把长文档切成一节一节便于检索的片段。医疗文档有个特点很多关键信息藏在长段落里比如“胃镜检查前需要空腹 8 小时”可能藏在某一大段的中间。如果切分策略不好语义就会被拆散检索召回率会明显下降。我们最后按语义边界而不是固定字符数切分同时保留段落标题作为元数据检索时优先返回带明确标题的片段。2.4 意图路由先判断要不要进入 RAG不是所有输入都应该触发模型回答。我们做了一个前置分流判断用户当前这句话的类型是医疗咨询、是闲聊、是投诉还是紧急情况。医疗咨询进入 RAG 管道紧急情况直接走应急话术闲聊用轻量回复带过去投诉则转人工。意图判断我们开始想用独立模型后来发现成本没必要。绝大部分分流的判断靠关键词规则加上当前模型的一行指令就能完成准确率已经足够。真正复杂的不是判断意图而是判断用户是否处于紧急状态。3. 核心代码落地RAG 多轮客服在 Java 里怎么写3.1 基础依赖与模型配置我们使用的版本是当时比较稳定的一个中间版本LangChain4j 的 API 在不同版本里有过调整但整体思路一致。项目的 pom 里加了两个核心依赖dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.31.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.31.0/version /dependency模型层我们走的是一条统一的模型网关代码里只需要指定兼容 OpenAI 协议的 endpoint、密钥和模型名。把模型配置放到 Spring 的配置类里后续切换模型只需要改配置。Configuration public class LlmConfig { Bean public ChatLanguageModel chatLanguageModel( Value(${llm.base-url}) String baseUrl, Value(${llm.api-key}) String apiKey, Value(${llm.model-name}) String modelName) { return OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .temperature(0.1) .maxTokens(1024) .timeout(Duration.ofSeconds(60)) .build(); } }医疗场景 temperature 必须压低我们设在 0.1。这个参数控制输出的随机性数值越低答案越保守。做客服可以 “话少一点、稳一点”但不能为了“看起来自然”而牺牲准确性。真实项目里我曾见过有人把 temperature 调到 0.8结果同一个问题两次答案完全不同这在医疗场景是不能接受的。3.2 知识库导入加载、切分、向量化、入库知识库导入不是一次性工程后面会反复执行。每次院内文档更新后我们会重新跑一遍导入任务。核心代码如下Document document FileDocumentLoader.load( new File(导诊手册.md), DocumentLoader.DocumentType.MD); ListTextSegment segments DocumentSplitter.recursive(300, 50) .split(document); EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(bge-m3) .build(); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); store.addAll(embeddings, segments);这里最需要琢磨的是切分参数。300表示目标长度50表示重叠部分。为什么要重叠因为文档里的一句话很可能被从中间切断如果下一片段不携带上一片段尾巴上的 50 个字那“空腹 8 小时”的语义就可能丢失。重叠是给检索召回买的保险。向量化模型我们没有选太大参数量级的版本。知识库片段通常就几百个字bge-m3 这类开源嵌入模型足够用而且部署成本低。有一个经验嵌入模型和后台主模型可以不同源只要语义空间能对齐不影响检索效果。3.3 用接口定义会话服务LangChain4j 的 AiServices 最让我喜欢的一点是客服能力可以抽象成接口interface MedicalAssistant { SystemMessage( 你是某医疗机构的智能客服助手名字叫硅谷小智。 你只能依据知识库中提供的资料回答问题不得编造。 回答要简短、准确、温和。 如果知识库中没有相关信息明确告知用户并提供人工客服联系方式。 严禁给出诊断结论或具体用药剂量。 ) UserMessage( 用户的问题是{{userMessage}} 参考资料如下 {{information}} ) String chat(V(userMessage) String userMessage); }注意{{userMessage}}和{{information}}这两个占位符。前者代表用户输入后者由 LangChain4j 的 ContentRetriever 根据问题从向量库检索出来的上下文自动注入。开发人员不需要在业务代码里手动拼接资料框架把这个过程做掉了。接口定义里还有一处关键SystemMessage 中必须把医疗安全规则写透。包括不诊断、不指导用药、不知晓时说不知道。医生面对不确定情况都有“请进一步检查明确”AI 客服更不能不假思索地回答。3.4 组装服务并对外提供接下来用 AiServices 把接口、模型、会话记忆、检索器组装起来MedicalAssistant assistant AiServices.builder(MedicalAssistant.class) .chatLanguageModel(chatLanguageModel) .chatMemory(MessageWindowChatMemory.builder() .maxMessages(12) .chatMemoryStore(chatMemoryStore) .build()) .contentRetriever(EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.55) .build()) .build();两个参数值得单独解释。maxResults是每次检索最多带几个知识片段给模型不是越多越好片段太多会把关键信息淹没minScore是相似度门槛低于 0.55 的片段会被丢弃。这个门槛调低了会让不相关内容混入答案调高了会导致很多问题召回不到资料。我们通过标注一批真实问题反复测试最终卡在 0.55。对外暴露的接口就是一个普通的 Spring Controller入参里有 userId 和 message后端用 userId 作为会话记忆维度调用assistant.chat(message)再把结果用 SseEmitter 返回前端。流程清爽业务代码里几乎没有大模型相关的胶水代码。4. 医疗场景里的边界处理这块才是项目真正的核心4.1 不诊断、不给药量是一票否决的红线医疗智能客服和普通电商客服最大的区别是答错一次的成本可能极高。纠正性反馈系统里有一句话模型宁可说不知道也不能基于猜测给一个“好像没问题”的建议。我们在 SystemMessage 里写死了几条规则还在后置校验层做了正则和关键词拦截。比如用户问“我头疼应该吃布洛芬还是对乙酰氨基酚”如果知识库里有该药物的说明书信息模型可以复述说明书上的标准描述比如“成人单次服用剂量为 X24 小时内不超过 Y”但不能输出“你吃 A 会更好”。说明书信息是经过监管审核的可以引用个性化给药建议是做诊断绝对不能碰。这个边界我们在测试集里反复压实凡是触碰到“我建议你”“你这种情况应该”这类表述的回答都视为不合格答案。4.2 紧急情况识别有专门的旁路拦截知识库问答可以慢但不能漏掉紧急状态。在我们项目里用户在输入“胸痛”“呼吸困难”“抽搐”“大出血”之类关键词时流程会直接旁路到应急处理分支不再进入常规 RAG 管道。旁路分支的回复是固定话术告诉用户这可能是需要紧急处理的症状建议立刻前往急诊或拨打急救电话并附上急诊位置和电话。这个分支不经过模型生成是纯逻辑代码保证任何时候都能触发。为什么不用模型判断因为模型的延迟不可控而且万一它那天“犯傻”把胸痛当成普通胃病回复后果就严重了。规则虽然朴素但在安全场景里朴素即可靠。4.3 答案要能溯源给用户也给质检留证据医疗客服的每次回答都应该有出处。我们让接口返回的数据结构里多带一个来源字段包含命中的知识库文档名称和相关片段。前端展示时在回答末尾用很小的一行字标注“参考来源XX说明文档”既增加了用户信任也能在事后审计时快速定位问题。内部还做了一层追问如果该次回答没有命中任何知识库片段也就是information为空但模型仍然给出了答案这条记录会被标记为“低置信回答”自动进入质检池。质检人员每周看一眼这些低置信回答很容易发现知识库缺口。这个机制帮我们发现了不少问题比如某个新药上线后说明书文档没有及时同步用户连续问了一个星期全被标记为低置信才倒逼我们把知识库更新频率提上去。5. 上线前后的实测与排障从 3 秒到 800 毫秒5.1 首版慢在上下文太重第一版上线后我们抓到的第一个问题是响应延迟偏高晚间高峰时段平均要 3 秒多才返回完整回答。一开始怀疑是模型服务本身慢后来发现根因是我们的上下文太重ChatMemory 窗口里虽然只有 12 条消息但每条消息本身包含了大段历史知识片段而且maxResults当时设成了 8每次检索带进去的碎片过多模型处理这些无用上下文白白消耗了时间。调整思路有几步。首先把maxResults从 8 降到 5接着把minScore从 0.4 提到 0.55把相关性较低的片段挡在门外。然后给 ChatMemory 的消息做了精简窗口内只保留用户消息和助手的最终回答不保留内部注入的参考资料。最后对知识库切分长度做了压缩300 字改成 240 字。经过这三轮调整平均响应时间降到了 800 毫秒左右体感提升非常明显。这个案例说明RAG 项目的性能瓶颈往往不在模型推理本身而在你喂给模型的东西太多太杂。5.2 并发挤压下的线程与限流设计医疗客服有明显的忙闲周期。周一到周三上午是咨询高峰尤其是挂号相关的问题集中爆发瞬间并发请求经常是平时的五六倍。LangChain4j 本身不限制并发但它背后的模型服务有吞吐上限尤其在内网私有化部署环境下GPU 资源是有限的。我们给后端加了一个信号量限流允许多少个请求同时进入大模型调用超过阈值的请求进入排队。排队体验相比报错来说要好得多只不过用户感知到的首字时间会稍微变长。另外给模型调用设置了明确的超时时间超时直接返回兜底话术“服务繁忙请稍后再试”防止一个慢请求把应用线程全部拖死。这个取舍在医疗场景里很关键用户可以等但应用不能崩。5.3 用回归测试集盯住回答质量模型升级和参数调整最怕什么怕“感觉变好了但不知道哪里变坏了”。我们手工建了一个小规模的回归测试集大概 200 条真实脱敏后的咨询问题每条问题标注了期望的核心答案点。每次调整知识库、提示词或模型配置都跑一遍这套回归集。评测当然不能完全自动化但可以做半自动程序把模型返回的文本和期望答案点做关键词命中分析命中率低于阈值的自动标红再人工确认。这个方法很土但对客服场景非常有效。我们曾经在升级一次模型版本后发现回归集里“挂号要带什么证件”这类问题全部答偏了语气变得异常热情内容却丢了三要素。正是这套回归集赶在用户大量反馈之前拦住了问题。6. 给这个项目留几条约定硅谷小智上线后稳定运行了一段时间知识库覆盖越来越大人工转接量明显下降。如果你也要做类似的项目下面几条是我现在回头看最想提前告诉自己的约定。第一知识库更新必须走定时任务不能靠人工想起才导一次。我们把院内科室排班、检查注意事项的更新绑到了周一固定执行模型再聪明也补不上缺失的数据。第二LangChain4j 版本升级要克制。框架迭代很快新版本往往带来新能力但也会带来一些细微的行为变化。没有完整跑过回归集之前生产环境不要动版本。第三所有回答都要可追溯。不只是为了用户也是为了研发团队自己。低置信回答的拦截和质检池是医疗客服持续改进最重要的信息来源。这个项目做到后面我的一个很深体会是医疗智能客服的难度不在“智能”而在“克制”。模型能说出一段漂亮的回答并不难难的是让它在该沉默的时候沉默该转人工的时候不犹豫该引用来源的时候不省略。LangChain4j 给了我们一套稳当的工程骨架但真正让硅谷小智可用的是那些反复打磨的边界规则和流程设计。后续如果再开项目我大概率还会沿用这套骨架只是会继续把知识库的质量和边界校验经营得更细。