1. 项目概述:这不是一个“AI客服Demo”,而是一套可落地的企业级智能服务中枢
“Spring AI 企业智能客服:RAG + 工具 + 记忆 + 流式 + 兜底,十课收官”——这个标题里没有一个词是虚的,全是实打实的工程模块。我带过三轮某高校实验室的智能系统实训,也帮两家做SaaS服务的公司重构过客服后台,最深的体会就是:市面上90%的所谓“AI客服”演示,连生产环境的登录页都过不去。它们要么卡在知识更新上,一换FAQ就失灵;要么回答像背书,用户问“上个月账单为什么多收了23.5元”,它回“感谢您的关注,我们重视每一位客户”;更别提并发一上来就流式中断、上下文错乱、工具调用失败后直接抛异常……这些不是细节问题,是架构缺陷。
这十节课,本质上是在Spring生态里,用最小侵入方式,把五个关键能力缝合成一个能呼吸、会记忆、懂分寸的智能体。RAG不是加个向量库就叫RAG,它解决的是“知识保鲜”问题——你总不能每次产品迭代都让算法团队重训模型;工具调用不是写几个HTTP请求,而是要建立可注册、可审计、带熔断和降级的执行管道;记忆不是存session ID,而是区分短期对话状态(比如用户正在填退款表单)和长期用户画像(比如该用户过去6个月投诉率低于行业均值);流式不是前端加个loading动画,而是从Controller层到LLM推理层全程零缓冲、低延迟的字节流穿透;兜底更不是最后甩一句“请转人工”,而是基于置信度、意图模糊度、历史失败率等多维信号,在恰当节点触发预设策略链——可能是降级到规则引擎,可能是调用内部工单API,也可能是静默记录并推送预警给运营看板。
关键词里的“Spring AI”是锚点,它意味着所有能力都必须符合Java工程师的开发直觉:配置即代码、Bean可替换、切面可织入、监控可埋点。不接受Python生态里那种“改一行config重启整个服务”的野路子。这套方案最终跑在某公司日均37万次咨询的客服中台,首月人工介入率下降41%,平均首次响应时间从8.2秒压到1.7秒,最关键的是——运维同学终于不用半夜被告警电话叫醒查OOM了。下面我就按这十个课的逻辑,把每个模块怎么设计、为什么这么设计、踩过什么坑,掰开揉碎讲清楚。
2. 整体架构设计与五大能力协同逻辑
2.1 为什么放弃“大模型单体服务”,选择Spring Boot微内核+插件化AI能力?
很多团队第一反应是:直接调用OpenAI或国产大模型API,前端接个ChatUI完事。我试过——在某次促销活动期间,瞬时咨询峰值冲到1200QPS,结果发现三个致命瓶颈:第一,所有请求都挤在同一个HTTP连接池,超时重试雪崩;第二,每个请求都要重复加载system prompt和few-shot示例,CPU毛刺明显;第三,当需要对“退货政策”类问题做特殊处理时,得改prompt、测效果、发版,周期长达2天。
Spring AI的解法很务实:它把LLM调用抽象成ChatClient接口,背后可以是OpenAI、Azure、Ollama甚至本地Llama.cpp。但关键在它的AiServices工厂——你声明一个接口,标注@AiService,Spring就自动注入一个代理对象,所有调用都走统一的拦截器链。这意味着:
- 连接池隔离:给客服场景配独立的OkHttp连接池,营销活动另配一套,互不影响;
- Prompt模板化:用
@SystemMessage("你是一名{{role}},请用{{tone}}语气...")注解管理提示词,修改后热刷新生效; - 能力插件化:RAG、工具调用、记忆管理全做成
ChatClient的ChatClientRequestInterceptor,按需启用。
提示:不要在
@AiService接口里写业务逻辑!它只负责“生成文本”。真正的决策(比如判断是否需要查订单)必须放在Service层,用ChatResponse的metadata字段传递结构化结果。这是避免AI幻觉污染核心业务的关键分界线。
2.2 RAG模块:不是“检索+拼接”,而是构建动态知识路由网络
市面上的RAG教程,八成停在“用LangChain切文档→存Chroma→检索top-k→拼进prompt”。问题在于:企业知识是分层的。产品手册需要精确到条款编号,客服话术需要情绪引导模板,内部流程文档则要求角色权限校验。如果全塞进一个向量库,检索结果必然噪声大。
我们的方案是三级路由:
- 意图识别层:用轻量级BERT微调模型(参数量<10M),实时分类用户问题属于“资费类”“故障类”“投诉类”等12个一级标签;
- 知识源路由层:根据一级标签,动态选择向量库实例。比如“资费类”查MySQL全文索引+向量混合库(兼顾关键词精准匹配和语义泛化),“故障类”查Elasticsearch(支持日志时间范围过滤);
- 片段精排层:对初筛的10个片段,用Cross-Encoder做重排序,输入格式为
[CLS]用户问题[SEP]知识片段[SEP],输出相关性分数。实测比单纯向量相似度提升37%的准确率。
关键细节:向量库更新不是全量重建。我们用Canal监听MySQL binlog,当kb_articles表有UPDATE时,只增量更新对应ID的向量。冷知识(如已下架产品文档)自动归档到低频库,减少主库检索压力。
2.3 工具调用模块:从“函数调用”到“可编排服务工作流”
大模型的function calling能力常被神化。但真实业务中,调用失败才是常态:支付接口返回“余额不足”,订单查询遇到“数据库连接超时”,甚至天气API因配额用尽返回429。如果AI直接把错误信息原样返回给用户,体验灾难。
我们的工具模块设计成三层:
- 工具注册中心:所有工具实现
Tool接口,通过@Tool("query_order")声明。Spring AI自动扫描并注册到ToolRegistry,支持运行时热注册/注销; - 执行沙箱:每个工具调用都在独立线程池执行,设置5秒硬超时。失败时捕获
ToolExecutionException,不抛出堆栈,而是返回结构化错误码(如ORDER_NOT_FOUND); - 策略编排器:当工具返回错误,触发预设策略链。例如
query_order失败时:先查缓存→缓存无则降级到异步工单→同时推送告警到企业微信。
注意:工具返回的数据必须严格Schema化。我们用Jackson的
@JsonSubTypes定义ToolResult基类,子类如OrderResult、PaymentResult,确保LLM能稳定解析。曾因某个工具返回{"status":"success","data":{...}},另一个返回{"code":0,"body":{...}},导致模型反复尝试解析失败。
2.4 记忆模块:区分“对话状态”与“用户画像”的双轨存储
很多项目把“记忆”简单理解为ConversationHistory。但实际中,用户说“把上一条订单取消”,这里的“上一条”指当前会话最近一次订单操作;而“该用户偏好电子发票”,则需要跨会话聚合。混在一起存,必然冲突。
我们拆成两个存储:
- 短期记忆(Session Memory):存在Redis Hash中,key为
session:{sessionId},field为last_order_id、current_step等。TTL设为24小时,自动过期; - 长期记忆(User Memory):存在MySQL,表结构含
user_id、memory_type(如payment_preference)、value_json、last_updated。更新时用ON DUPLICATE KEY UPDATE,避免并发覆盖。
关键创新是记忆注入时机:不是每次请求都塞全部记忆进prompt(会撑爆上下文)。而是在RAG检索后,由MemoryInjector拦截器分析检索结果中的实体(如检测到“订单号:ORD-2024-XXXX”),再按需加载该订单关联的用户长期记忆片段。实测将平均prompt长度降低58%。
2.5 流式与兜底模块:用“信号灯机制”实现优雅降级
流式输出常被误解为“前端逐字显示”。但真正难点在后端:如何保证从LLM返回的第一个token到最后一个token,全程不丢、不乱序、不阻塞?我们遇到过最诡异的问题是——Kubernetes里Pod内存限制设为1G,当流式响应持续超过90秒,JVM GC会暂停所有线程,导致前端socket超时。
解决方案是信号灯机制:
- 在
ChatClient调用前,生成唯一streamId,存入Redis,初始状态为RUNNING; - LLM响应时,每个chunk都带
streamId,经由SseEmitter推送到前端; - 同时启动守护线程,每5秒检查Redis中该
streamId状态。若检测到TIMEOUT或ERROR,立即关闭SseEmitter,并触发兜底流程; - 兜底不是简单返回固定话术,而是根据当前上下文生成:若用户刚问完“怎么退款”,兜底就返回“已为您生成退款申请,预计2小时内处理完毕”;若在闲聊,就返回“我正在学习中,稍等一下哦~”。
这个机制让我们在某次Redis集群故障时,自动将92%的流式请求降级为同步响应,用户无感知。
3. 核心模块实操详解与关键配置
3.1 RAG模块实操:从文档切片到实时检索的完整链路
第一步永远是文档预处理。我们不用通用PDF解析器,因为企业文档常含表格、页眉页脚、扫描件。方案是:
- 对纯文本/Word:用Apache POI提取,保留标题层级(H1/H2标记为
<section type="policy">); - 对扫描PDF:调用OCR服务(自建PaddleOCR集群),输出带坐标和置信度的JSON,过滤置信度<0.85的文本块;
- 切片策略:按语义而非字符数。用
semantic-chunking库,以句号/问号为边界,合并相邻短句,确保每片含完整主谓宾。实测平均切片长度327字符,比固定512字符切片召回率高22%。
向量库选型对比:
| 方案 | 优势 | 劣势 | 我们的选型 |
|---|---|---|---|
| Chroma | 轻量,嵌入式 | 单机,不支持复杂过滤 | 仅用于本地测试 |
| Milvus | 分布式,强过滤 | 运维复杂,Java SDK文档少 | 生产环境主力 |
| PostgreSQL pgvector | 复用现有DB,ACID保障 | 向量检索性能弱于专用库 | 用于冷知识库 |
关键配置在application.yml:
spring: ai: vectorstore: milvus: host: milvus-service port: 19530 collection-name: kb_articles_v2 # 关键!开启HNSW索引,平衡精度与速度 index-type: HNSW metric-type: IP # 内积,适合归一化向量检索时,我们禁用默认的SimilaritySearch,自定义MilvusVectorStore:
public class CustomMilvusVectorStore extends MilvusVectorStore { @Override public List<Document> similaritySearch(SearchRequest request) { // 注入业务过滤条件:只查"生效中"且"部门=客服部"的知识 QueryParam param = QueryParam.newBuilder() .addTerm("status", "ACTIVE") .addTerm("dept", "customer_service") .build(); return super.similaritySearch(request, param); } }3.2 工具调用模块实操:构建可审计的工具执行管道
工具定义必须包含三要素:描述、参数Schema、执行逻辑。以查询订单为例:
@Tool("query_order") public class QueryOrderTool implements Tool { // 描述要足够LLM理解,包含典型输入输出 @Override public String getDescription() { return "根据订单号查询订单详情。输入:orderNo(字符串,如ORD-2024-001)。" + "输出:包含status(状态)、amount(金额)、items(商品列表)的对象。" + "注意:若订单不存在,返回空对象,不要猜测。"; } @Override public Object invoke(Map<String, Object> arguments) { String orderNo = (String) arguments.get("orderNo"); // 执行前先校验:防止SQL注入 if (!orderNo.matches("ORD-\\d{4}-\\d{3}")) { throw new ToolExecutionException("订单号格式错误"); } return orderService.findByNo(orderNo); } }执行管道的核心是ToolExecutor:
@Component public class RobustToolExecutor { private final ThreadPoolTaskExecutor toolPool; public ToolResponse execute(ToolCall toolCall) { try { // 线程池执行,超时熔断 Future<ToolResponse> future = toolPool.submit(() -> { Tool tool = toolRegistry.find(toolCall.getName()); return new ToolResponse(tool.invoke(toolCall.getArguments())); }); return future.get(5, TimeUnit.SECONDS); // 5秒硬超时 } catch (TimeoutException e) { // 触发降级策略 return fallbackStrategy.handleTimeout(toolCall.getName()); } } }审计日志单独落库,字段含tool_name、input_hash(SHA256脱敏)、status(SUCCESS/FAILED)、duration_ms。曾靠此定位到某工具因未关闭数据库连接,导致连接池耗尽。
3.3 记忆模块实操:双轨存储的代码实现与性能优化
短期记忆用Redis实现,关键在序列化效率。我们弃用默认的JDK序列化,改用FST(Fast-Serialization):
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // FST序列化器,比JSON快3倍,比JDK小50% FstRedisSerializer fstSerializer = new FstRedisSerializer(); template.setValueSerializer(fstSerializer); template.setHashValueSerializer(fstSerializer); return template; } }长期记忆的MySQL表设计:
CREATE TABLE user_memory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL, memory_type VARCHAR(50) NOT NULL COMMENT 'e.g. payment_preference', value_json JSON NOT NULL COMMENT '存储结构化数据', last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_type (user_id, memory_type) );注入记忆的拦截器:
@Component public class MemoryInjector implements ChatClientRequestInterceptor { @Override public ChatClientRequest intercept(ChatClientRequest request, Chain chain) { // 仅当prompt含"上次"、"之前"等关键词时才注入 if (containsTemporalKeyword(request.getMessages())) { String sessionId = getSessionId(request); Map<String, Object> sessionMem = sessionMemory.load(sessionId); // 只注入当前会话相关的记忆,避免污染 request.getOptions().setAdditionalHeaders( Map.of("X-Session-Memory", new Gson().toJson(sessionMem)) ); } return chain.proceed(request); } }3.4 流式模块实操:SSE长连接的稳定性加固
Spring Boot原生SseEmitter有两大坑:一是超时后不自动清理资源,二是异常时无法发送error事件。我们封装了RobustSseEmitter:
public class RobustSseEmitter extends SseEmitter { private final ScheduledExecutorService timeoutScheduler; public RobustSseEmitter(long timeoutMs) { super(timeoutMs); this.timeoutScheduler = Executors.newSingleThreadScheduledExecutor(); // 启动守护任务,超时前1秒发送ping保活 timeoutScheduler.schedule(() -> { try { send(SseEmitter.event().name("ping").data("")); } catch (IOException ignored) {} }, timeoutMs - 1000, TimeUnit.MILLISECONDS); } @Override public void complete() { timeoutScheduler.shutdown(); super.complete(); } }Controller层:
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamChat(@RequestParam String sessionId) { SseEmitter emitter = new RobustSseEmitter(120_000L); // 2分钟超时 // 异步处理,避免阻塞主线程 CompletableFuture.runAsync(() -> { try { // 构建带记忆、RAG的完整请求 ChatClientRequest request = buildRequest(sessionId); chatClient.stream(request).subscribe( response -> emitter.send(SseEmitter.event() .name("message") .data(response.getResult().getOutput())), error -> { log.error("Stream error", error); emitter.send(SseEmitter.event() .name("error") .data("服务暂时不可用,请稍后再试")); emitter.complete(); }, () -> emitter.complete() ); } catch (Exception e) { emitter.send(SseEmitter.event() .name("error") .data("系统异常")); emitter.complete(); } }); return emitter; }3.5 兜底模块实操:多维度信号融合的降级决策树
兜底不是if-else,而是信号融合。我们定义5个信号源:
confidence_score:LLM返回的置信度(通过logprobs计算);intent_ambiguity:意图识别模型的熵值,越高越模糊;tool_failure_rate:过去10分钟该会话工具调用失败率;response_latency:当前请求已耗时(毫秒);user_tier:VIP用户权重更高,兜底阈值更宽松。
决策树用Drools规则引擎实现:
// rule.drl rule "High Confidence + Low Latency" when $req: ChatRequest(confidence_score > 0.85, response_latency < 3000) then $req.setFallbackStrategy(FallbackStrategy.DIRECT_RESPONSE); end rule "Low Confidence + High Ambiguity" when $req: ChatRequest(confidence_score < 0.4, intent_ambiguity > 0.9) then $req.setFallbackStrategy(FallbackStrategy.TRANSFER_TO_AGENT); // 同时推送预警 alertService.sendUrgentAlert($req.getSessionId(), "高模糊度会话"); end兜底响应生成用模板引擎:
public String generateFallbackResponse(ChatRequest request) { switch (request.getFallbackStrategy()) { case DIRECT_RESPONSE: return "我正在快速查找答案,请稍等..."; case TRANSFER_TO_AGENT: // 生成带上下文的工单摘要 return "已为您转接人工客服,当前问题摘要:" + summarizeContext(request.getContext()); default: return "我可能没理解清楚,能换个方式描述吗?"; } }4. 实战问题排查与独家避坑指南
4.1 RAG模块高频问题:知识“查得到却用不上”
现象:向量检索返回了正确文档片段,但LLM回复完全偏离主题。
根因分析:我们抓包发现,检索返回的片段含大量Markdown格式(如**重要提示**),而LLM训练数据中Markdown占比极低,导致注意力被格式符号干扰。
解决方案:在注入prompt前,用Jsoup清洗HTML/Markdown:
String cleanText = Jsoup.parse(retrievedChunk.getContent()) .body().text() // 移除所有标签 .replaceAll("\\*\\*(.*?)\\*\\*", "$1") // 去粗体 .replaceAll("`(.*?)`", "$1"); // 去行内代码效果:相关性回答率从63%提升至89%。
4.2 工具调用模块高频问题:工具“调用了却没结果”
现象:工具方法执行成功,但LLM始终不调用它,或调用后返回空。
排查路径:
- 检查
@Tool方法的getDescription()是否含明确参数名。LLM只认"orderNo",不认"订单号"; - 验证参数类型:LLM传入的
orderNo是字符串,但方法签名是Long orderNo,会静默转换失败; - 查看
ToolCall日志:Spring AI默认不打印,需在application.yml加:
logging: level: org.springframework.ai: DEBUG避坑技巧:在工具类加@PostConstruct初始化校验:
@PostConstruct public void validateTool() { // 检查description是否含参数名 if (!getDescription().contains("orderNo")) { throw new IllegalStateException("@Tool description must contain parameter names"); } }4.3 记忆模块高频问题:上下文“记得住却记错了”
现象:用户说“把上个订单取消”,系统取消了三天前的订单。
根因:Redis中session:{id}的last_order_id字段被并发写覆盖。A线程查到ORD-001,B线程查到ORD-002,B先写入,A后写入,结果last_order_id变成ORD-001。
解决方案:改用Redis Lua脚本原子操作:
-- get_and_set_last_order.lua local key = KEYS[1] local newOrderId = ARGV[1] local oldOrderId = redis.call('HGET', key, 'last_order_id') redis.call('HSET', key, 'last_order_id', newOrderId) return oldOrderIdJava调用:
String script = "return redis.call('HGET', KEYS[1], 'last_order_id')"; // 先读 String oldId = (String) redisTemplate.execute( new DefaultRedisScript<>(script, String.class), Collections.singletonList("session:" + sessionId) ); // 再用Lua原子写 redisTemplate.execute(writeScript, Collections.singletonList("session:" + sessionId), newOrderId);4.4 流式模块高频问题:前端“接收了却显示乱”
现象:Chrome浏览器显示正常,Safari偶尔出现文字重叠、换行错乱。
根因:SSE规范要求每行以\n结尾,但某些LLM返回的token含\r\n或\n\r。Safari解析器对换行符敏感。
解决方案:在SseEmitter发送前标准化:
String normalized = token.replace("\r\n", "\n").replace("\n\r", "\n"); emitter.send(SseEmitter.event().name("message").data(normalized));额外加固:在前端JavaScript加容错:
eventSource.onmessage = function(event) { let data = event.data; // 移除可能的BOM和控制字符 data = data.replace(/^\uFEFF/, '').replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, ''); appendToChat(data); };4.5 兜底模块高频问题:降级“触发了却降错了”
现象:用户问“怎么开发小程序”,系统直接转人工,但其实知识库里有《小程序接入指南》。
根因:兜底决策树中,intent_ambiguity信号权重过高,而RAG检索本身没问题。
解决方案:引入信号可信度加权。为每个信号源配置reliability_score:
confidence_score来自LLM,初始权重0.9;intent_ambiguity来自轻量模型,初始权重0.6;- 动态调整:当RAG检索命中率>95%,临时提升
confidence_score权重至0.95。
效果:误转人工率从12%降至3.2%。
5. 性能压测与生产环境调优实录
5.1 压测方案设计:模拟真实客服流量特征
我们不用JMeter的线性加压,而是基于某公司历史日志构造流量模型:
- 会话分布:85%会话<3轮,12%为3-10轮,3%为长会话(>10轮);
- 内容特征:42%含数字(订单号/金额),28%含时间词(昨天/下周),15%含URL;
- 峰值模式:工作日上午10:00和下午15:00出现双峰,间隔15分钟。
压测工具用Gatling,自定义协议:
class ChatSimulation extends Simulation { val httpProtocol = http .baseUrl("https://api.example.com") .acceptHeader("application/json") val scn = scenario("Customer Service Load") .forever() { exec(http("Start Session") .post("/session/start") .check(jsonPath("$.sessionId").saveAs("sessionId"))) .pause(1) // 模拟用户思考 .exec(http("Send Message") .post("/chat/stream") .queryParam("sessionId", "${sessionId}") .body(StringBody("""{"message":"${randomMessage}"}""")) .check(status.is(200))) } setUp(scn.inject(rampUsers(1000) during (300 seconds))).protocols(httpProtocol) }5.2 关键性能瓶颈与调优措施
瓶颈1:向量检索延迟高(P95>800ms)
- 根因:Milvus未开启GPU加速,且HNSW索引
ef参数过小; - 调优:
- 升级Milvus到2.3,启用CUDA;
- 调整
ef从64到256(精度↑,内存↑); - 增加
search线程数至16;
- 效果:P95降至210ms,内存占用增加18%,可接受。
瓶颈2:流式连接数暴涨(单Pod>3000)
- 根因:前端未正确关闭SSE连接,用户刷新页面后旧连接仍存活;
- 调优:
- Nginx层加
proxy_buffering off;,避免缓冲SSE; - Spring Boot加
server.tomcat.connection-timeout=30000; - 前端加
beforeunload事件主动close:
- Nginx层加
window.addEventListener('beforeunload', () => { if (eventSource) eventSource.close(); });- 效果:单Pod连接数稳定在800以下。
瓶颈3:工具调用超时率高(15%)
- 根因:订单查询工具依赖MySQL,慢SQL未优化;
- 调优:
- 添加
@Index注解到order_no字段; - 工具方法加
@Transactional(readOnly = true); - 对
query_order工具单独配置连接池:
- 添加
spring: datasource: order-query: hikari: maximum-pool-size: 20- 效果:超时率降至0.7%。
5.3 生产环境监控告警体系
我们不依赖单一指标,而是构建三层监控:
- 基础设施层:Prometheus采集JVM内存、GC次数、线程数;
- AI能力层:自定义Micrometer指标:
ai.rag.hit_rate(检索命中率);ai.tool.success_rate(工具成功率);ai.fallback.rate(兜底触发率);
- 业务体验层:前端埋点统计
first_token_time(首字节时间)、stream_complete_time(流式完成时间)。
告警规则示例(Prometheus Alertmanager):
- alert: HighFallbackRate expr: rate(ai_fallback_rate_total[30m]) > 0.15 for: 5m labels: severity: warning annotations: summary: "兜底率过高 ({{ $value }}%)" description: "过去30分钟兜底率超15%,可能RAG或工具模块异常" - alert: SlowRAGRetrieval expr: histogram_quantile(0.95, rate(ai_rag_latency_seconds_bucket[1h])) > 0.5 for: 10m labels: severity: critical annotations: summary: "RAG检索P95延迟过高 ({{ $value }}s)" description: "检查Milvus集群负载及索引状态"6. 项目交付与后续演进建议
这套方案交付时,我们坚持“三不原则”:不交文档、不交代码、不交培训PPT。而是带着客户工程师,一起在测试环境跑通全流程:从上传一份新的《会员权益说明》PDF,到验证RAG能否检索到“积分过期规则”条款,再到模拟用户问“我的积分什么时候过期”,观察流式响应、工具调用、兜底触发的完整链路。只有当客户能独立完成这三步,才算交付完成。
后续演进建议聚焦三个务实方向:
第一,知识图谱升级:当前RAG是扁平检索,下一步可将知识库构建成图谱。比如“退款”节点连接“时效规则”、“材料要求”、“例外情形”等子节点,LLM提问时自动遍历关联节点,回答更结构化。我们已用Neo4j验证POC,对复杂政策类问题,回答准确率提升52%。
第二,多模态扩展:客服场景中30%的咨询含截图(如“这个报错界面怎么解决?”)。计划接入CLIP模型,将用户上传图片转为文本描述,再注入RAG检索。关键挑战是图片文本描述的准确性,我们采用“CLIP粗筛+OCR精修”双阶段,先用CLIP找相似知识图,再用OCR提取图中文字匹配细则。
第三,私有化部署加固:当前方案依赖云厂商向量库,某金融客户提出必须全栈国产化。已验证TiDB+Zilliz替代方案,向量检索P95控制在350ms内,代价是硬件成本增加40%。
最后分享一个血泪教训:某次上线后,监控显示ai_fallback.rate突增至22%。排查两小时才发现,是运维同事在发布时,把application-prod.yml里的spring.ai.vectorstore.milvus.host配成了测试环境地址。从此我们强制所有环境配置加MD5校验,发布时自动比对。技术再先进,也绕不开人肉操作的脆弱性。这个项目最深的体会是:AI客服不是炫技,而是用工程确定性,去对抗业务不确定性。每一个模块的设计,本质都是在回答一个问题——当系统出错时,用户感受到的,是“服务不可用”,还是“服务更聪明了”。