背景与痛点:传统客服系统为什么“跑不动”
过去两年,我帮三家客户做过客服系统升级,总结下来最痛的点有三处:
- 响应慢:老系统把 FAQ 做成关键词匹配,用户一句话里只要多一个“的”,就匹配不到答案,平均响应 3-5 秒,高峰期直接 10 秒起步。
- 扩展难:规则库用 XML 维护,新增一条问答要重启整个服务;想做多渠道(微信、网页、App)就得复制三套代码。
- 无上下文:用户问完“怎么退货”,接着追问“邮费谁出”,系统却当成新问题,体验非常割裂。
要同时解决“语义理解 + 上下文 + 高并发”,继续堆规则已经不现实,引入 LLM 几乎是唯一选择。但 LLM 本身“不可控、不业务、不高性能”,于是 LangChain4j 成了最顺手的“胶水”——它把提示模板、记忆、工具调用、流式输出做成了 Java 工程师能看懂的 DSL,而 Spring Boot 生态又负责把这一切包装成可水平扩展的微服务。
技术选型:LangChain4j 为什么比“裸调”LLM 更香
先给出对比表,方便一眼看懂差异:
| 方案 | 提示模板 | 多轮记忆 | 工具/插件 | 流式输出 | Java 友好度 | 学习成本 |
|---|---|---|---|---|---|---|
| 裸调 OpenAI HTTP | 自己拼 | 自己存 | 自己写 | 自己解析 | 低 | 高 |
| LangChain(Python) | 有 | 有 | 丰富 | 有 | 极低(需 Python) | 中 |
| LangChain4j | 有 | 有 | 有 | 有 | 高 | 低 |
结论:团队如果主力是 Java,Spring Boot + LangChain4j 可以把“提示管理、记忆、函数调用”三件事一次性解决,而且 jar 包直接启动,无需额外 Python 服务,运维复杂度直线下降。
核心实现:30 分钟跑通第一个问答接口
下面用 Spring Boot 3.2 + LangChain4j 0.28 做演示,OpenAI 作为 LLM 后端,最终暴露一个 REST 接口/chat,支持多轮会话。
1. 依赖与配置
在pom.xml中引入 BOM,避免版本打架:
<dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-bom</artifactId> <version>0.28.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>核心依赖只选两个:
<!-- LLM 后端 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> </dependency> <!-- Spring Boot 集成 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> </dependency>application.yml里把 key 和超时配好:
langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-3.5-turbo timeout: 15s max-tokens: 6002. 领域 Prompt 模板
客服场景需要“角色 + 边界 + 输出格式”三件套,新建CustomerServicePromptTemplate.java:
public class CustomerServicePromptTemplate { public static final String SYSTEM_PROMPT = """ 你是小助手,专门回答电商售后问题。 只能回答退货、退款、邮费、开发票四类问题,其余问题直接回复“抱歉,我帮不上忙”。 回答不超过 80 字,语气亲切。 上下文历史:{{history}} 用户问题:{{question}} """; }LangChain4j 使用PromptTemplate.from(...)即可把{{}}占位符动态替换。
3. 会话记忆组件
LangChain4j 自带ChatMemory接口,我们用线程安全的MessageMemory实现,每用户一个实例,生命周期 30 分钟:
@Component public class InMemoryChatMemory implements ChatMemory { private final Cache<String, Deque<ChatMessage>> cache = Caffeine.newBuilder() .expireAfterAccess(30, TimeUnit.MINUTES) .build(); @Override public void add(String sessionId, ChatMessage message) { cache.get(sessionId, k -> new ArrayDeque<>()).add(message); } @Override public List<ChatMessage> getMessages(String sessionId) { return List.copyOf(cache.getIfPresent(sessionId)); } }4. Service 层拼装
@Service public class ChatService { @Resource private OpenAiChatModel chatModel; @Resource private InMemoryChatMemory memory; private final PromptTemplate promptTemplate = PromptTemplate.from(CustomerServicePromptTemplate.SYSTEM_PROMPT); public String chat(String sessionId, String question) { // 1. 取历史 List<ChatMessage> history = memory.getMessages(sessionId); // 2. 渲染提示 Map<String, Object> vars = Map.of("history", history, "question", question); String prompt = promptTemplate.render(vars); // 3. 调 LLM String answer = chatModel.generate(prompt); // 4. 更新记忆 memory.add(sessionId, new UserMessage(question)); memory.add(sessionId, new AiAnswer(answer)); return answer; } }5. REST 接口
@RestController @RequestMapping("/chat") public class ChatController { @Resource private ChatService chatService; @PostMapping public Map<String, String> chat(@RequestBody ChatRequest req) { String answer = chatService.chat(req.getSessionId(), req.getQuestion()); return Map.of("answer", answer); } }启动后curl一把即可看到多轮效果:
curl -X POST localhost:8080/chat \ -H "Content-Type: application/json" \ -d '{"sessionId":"u123","question":"如何退货?"}'性能优化:让 GPT 在 200 ms 内返回
LLM 本身延迟 800 ms 起步,但线上 SLA 要求 500 ms,必须把“能省的都省掉”。下面 4 步实测可把 P99 从 1.2 s 压到 220 ms:
- 流式输出 + 前端打字机效果
把generate()换成generateStream(),Spring WebFlux 返回Flux<String>,前端边读边渲染,用户感知延迟降低 50%。 - 连接池与 Keep-Alive
默认OpenAiChatModel每次新建 HTTP 连接,在application.yml里把max-connections调到 200,并开启connection-pool: true,可减少 80 ms TLS 握手。 - Prompt 缓存
对高频问题做本地缓存,Key 用“问题 32 位 MD5”,Value 放“最佳答案”,命中率 38%,直接省掉一次 LLM 调用。 - 垂直扩容 → 水平扩容
单容器 2 vCPU 时,并发 50 QPS CPU 就打满;无状态后直接上 3 副本,K8s HPA 根据 CPU 60% 阈值自动弹到 10 副本,轻松扛 500 QPS。
生产环境避坑指南:我们踩过的 5 个坑
- Token 超限
GPT-3.5 最大 4096 token,对话一长就抛InvalidRequestException。解决:记忆队列长度按 token 计数而非条数,超过 3000 自动摘要旧消息。 - 敏感词泄露
LLM 可能返回“激活码”“内部价”等违规内容。上线前必须加一层“输出过滤器”,用 DFA + 正则双保险,命中直接替换成“***”。 - 会话内存泄漏
早期用ConcurrentHashMap存记忆,30 分钟过期靠ScheduledExecutor,结果大促期间 20 万在线用户把 Map 打满 Old Gen。切到 Caffeine 带权重的expireAfterAccess后,FullGC 次数降到 0。 - API Key 轮换
OpenAI 对并发 QPS 有限流,单 Key 超过 3 次 429 就会封 1 分钟。生产环境用 Vault 存 5 组 Key,客户端侧轮询,保证 429 自动重试下一组。 - 多环境混淆
测试同学曾把压测流量打到生产,导致真用户 503。后来给 LLM 调用加一层EnvironmentHeaderInterceptor,非 prod 环境 header 带x-env=test,网关直接拒掉。
总结与展望:下一步还能怎么玩
把客服机器人跑通只是第一步,LangChain4j 的“函数调用”能力还没完全用上。接下来我们准备做三件事:
- 工具链:让 LLM 直接调用内部订单接口,用户说“帮我退掉昨天那单”,机器人自动查到 orderId 并发起退款流。
- 多模型:GPT-4 负责“复杂推理”,本地部署的 ChatGLM3-6B 负责“高频简单问答”,用路由层按问题长度 + 意图分流,成本可再降 45%。
- 插件市场:把“开发票”“价查询”做成独立插件,运营通过 YAML 上架,系统动态加载,做到“零代码”扩展新业务。
如果你也在用 Java 栈,LangChain4j 基本能让你“不写 Python 也能玩 LLM”。先把问答链路跑通,再逐步加记忆、加工具、加并发,路线很清晰。希望这篇笔记能帮你少踩几个坑,早点下班。