
1. 客服工单智能处理的痛点与 Spring AI Alibaba Graph 的切入逻辑做过客服系统的人都有一个共识工单处理这件事表面上是个流程问题骨子里是个语义理解问题。用户提交一段文字系统需要判断这到底是个退款诉求、技术故障、还是物流查询然后决定走哪条处理链路、调用哪些后端服务、要不要转人工、优先级怎么定。传统做法是关键词匹配加规则引擎写几百条 if-else上线三个月后没人敢改因为改一条规则可能影响另外三条业务线。Spring AI Alibaba Graph 解决的就是这个层面的问题。它把大模型能力编排成一张有向图每个节点是一个处理单元边是流转条件整张图就是一个可观测、可调试、可版本管理的智能决策流程。我拿客服工单场景做了一轮完整落地从工单接入、意图识别、情绪分析、自动分派到回复生成整条链路跑通之后人工介入率从原来的 78% 降到了 23% 左右而且规则维护成本大幅下降。这篇文章面向的是已经了解 Spring Boot、对 Spring AI 有基本认知、想把手头客服系统做智能化升级的后端开发。如果你还在纠结要不要上大模型或者上了大模型但不知道怎么把它嵌进现有业务流程这篇实战记录应该能帮你少走一些弯路。全文会从架构设计、核心节点实现、参数调优、踩坑排查几个维度展开代码和配置都是可以直接参考的。2. 整体架构设计与技术选型拆解2.1 为什么选 Graph 而不是 ChainSpring AI 本身提供了 Chain 模式把多个处理步骤串成一条线。但客服工单场景有个特点它不是线性的。一个工单进来可能先做意图分类根据分类结果走不同的分支——退款走退款流程技术故障走诊断流程投诉走升级流程。而且有些分支需要回退比如意图识别置信度不够要回到上一级重新判断。这种带分支、带循环、带条件跳转的编排需求Chain 模式表达起来很别扭Graph 模式天然适配。Graph 的核心抽象是 Node 和 Edge。Node 负责执行具体逻辑Edge 定义流转方向。条件边可以绑定一个判断函数根据当前状态决定走哪条路。这个模型和客服工单的实际处理逻辑几乎是一一对应的设计阶段画一张流程图代码阶段直接翻译成 Graph 定义中间没有语义损耗。2.2 节点划分与职责边界我把整张图拆成了七个核心节点每个节点职责单一方便单独测试和替换工单预处理节点清洗文本、提取元数据用户 ID、订单号、历史工单数、做敏感信息脱敏意图识别节点调用大模型做多分类输出意图标签和置信度情绪分析节点判断用户情绪等级平静、不满、愤怒决定优先级知识检索节点根据意图从向量库检索相关 FAQ 和解决方案自动分派节点结合意图、情绪、技能组负载决定分派目标回复生成节点基于检索结果和意图生成候选回复人工审核节点低置信度或高情绪工单转人工附带 AI 建议节点之间的边分两类普通边直接流转条件边根据状态字段判断。比如意图识别节点输出置信度低于 0.7条件边直接跳到人工审核节点跳过后续自动流程。2.3 状态管理的设计考量Graph 执行过程中需要一个共享状态对象在节点间传递数据。我用的是一个可变的TicketContext类里面包含原始工单文本、清洗后文本、意图标签、置信度、情绪分数、检索结果、分派建议、生成回复等字段。每个节点读取自己需要的字段写入自己产出的字段。这里有个设计决策值得展开状态对象要不要做成不可变的不可变的好处是线程安全、便于回溯但每次节点执行都要创建新对象在工单量大时内存开销明显。我最终选择了可变对象加字段级版本号的方式既保证了并发安全又避免了频繁对象创建。具体做法是在TicketContext里给每个字段配一个version标记节点写入时递增版本号读取时校验版本防止脏读。2.4 模型选型与成本控制意图识别和回复生成对模型能力要求不同。意图识别是分类任务用轻量模型就够我选的是通义千问的 turbo 版本响应快、成本低。回复生成需要一定的语言组织能力用的是 plus 版本。情绪分析可以用更小的模型甚至用微调过的小模型本地部署。成本控制这块有个实操经验意图识别节点不要每次都用大模型。我先用规则做一层粗筛命中明确关键词的工单直接打标签只有规则无法判断的才走模型。实测下来大约 40% 的工单可以被规则层拦截模型调用量直接降了四成。3. 核心节点实现与关键参数调优3.1 工单预处理节点的清洗策略预处理看起来简单实际上坑很多。用户提交的工单文本里经常夹杂 HTML 标签、表情符号、重复标点、无意义字符。如果直接丢给模型不仅浪费 token还会干扰意图判断。我的清洗流程分四步第一步用正则去掉 HTML 标签和特殊字符第二步做全角半角转换第三步压缩连续重复字符比如“好好好好好”压缩成“好”第四步做长度截断超过 2000 字符的截断并标记。脱敏这块要特别注意。用户工单里经常包含手机号、身份证号、银行卡号。我在预处理节点加了一层正则匹配把这些敏感信息替换成占位符比如[PHONE]、[ID_CARD]。这样后续节点和日志里都不会出现明文敏感信息合规上更稳妥。public class TicketPreprocessNode implements NodeTicketContext { private static final Pattern HTML_PATTERN Pattern.compile([^]); private static final Pattern PHONE_PATTERN Pattern.compile(1[3-9]\\d{9}); private static final Pattern ID_CARD_PATTERN Pattern.compile(\\d{17}[\\dXx]); Override public TicketContext apply(TicketContext context) { String raw context.getRawText(); String cleaned HTML_PATTERN.matcher(raw).replaceAll(); cleaned PHONE_PATTERN.matcher(cleaned).replaceAll([PHONE]); cleaned ID_CARD_PATTERN.matcher(cleaned).replaceAll([ID_CARD]); cleaned normalizeFullWidth(cleaned); cleaned compressRepeatedChars(cleaned); if (cleaned.length() 2000) { cleaned cleaned.substring(0, 2000); context.setTruncated(true); } context.setCleanedText(cleaned); return context; } }注意脱敏正则要放在清洗之后、截断之前。如果先截断再脱敏可能把手机号截成两半导致脱敏失败。3.2 意图识别节点的 Prompt 工程意图识别是整个流程的分水岭它错了后面全错。我在这个节点上花了最多时间调优。核心是 Prompt 的设计我试过三种方案第一种是直接让模型输出意图标签简单粗暴但模型经常输出一些不在预设列表里的标签解析起来很麻烦。第二种是让模型输出 JSON 格式包含意图和置信度解析稳定但模型偶尔会输出非法 JSON。第三种是让模型从预设列表中选择并输出固定格式的标签加分数我在 Prompt 里明确列出所有可选标签和判断标准。最终采用的是第三种方案的变体Prompt 结构如下你是一个客服工单意图分类器。请根据用户工单内容从以下意图中选择最匹配的一个 - REFUND退款退货相关 - TECHNICAL技术故障、功能异常 - LOGISTICS物流查询、配送问题 - COMPLAINT投诉、不满、要求赔偿 - CONSULT产品咨询、使用方法询问 - OTHER无法归入以上类别 输出格式意图标签|置信度0-1之间的小数 示例REFUND|0.92 用户工单内容 {cleanedText}这个 Prompt 的关键点在于标签定义要带简短说明帮助模型理解边界输出格式要极简方便解析示例要给一个锚定输出风格。置信度的校准也很重要。模型输出的置信度往往偏高我做了后处理如果模型输出的置信度在 0.6 到 0.8 之间统一降 0.1因为实测这个区间的准确率确实偏低。这个校准系数是根据 500 条标注数据回归出来的。3.3 情绪分析节点的轻量化实现情绪分析不需要太重的模型。我的做法是用一个小的分类模型输入是清洗后的文本输出是三个等级平静、不满、愤怒。训练数据来自历史工单的人工标注大概 3000 条就够用了。如果不想自己训练模型也可以用大模型做 few-shot 分类但成本会高一些。我的建议是工单量日均低于 1000 的直接用大模型 few-shot高于 1000 的值得花时间训一个小模型本地部署。情绪分数会影响后续的分派策略。愤怒工单直接标记为高优先级跳过自动回复直接转人工并且附带情绪安抚话术建议。不满工单走正常流程但优先级提升。平静工单走全自动流程。3.4 知识检索节点的向量库选型知识检索用的是向量相似度搜索。我选的是 Milvus 作为向量库主要是考虑到它支持标量过滤和向量检索混合查询可以在检索时加上意图标签作为过滤条件提高召回准确率。Embedding 模型用的是通义千问的 text-embedding 系列维度 1536。知识库的构建流程是把历史 FAQ、解决方案文档、产品手册切分成 500 字左右的 chunk每个 chunk 生成 embedding 存入 Milvus同时保留原文和来源信息。检索时的参数调优有个经验topK 不要设太大3 到 5 就够了。设太大反而会引入噪声影响回复生成质量。相似度阈值设在 0.75 左右低于这个值的检索结果直接丢弃宁可让模型基于通用知识回答也不要用不相关的知识误导。public class KnowledgeRetrievalNode implements NodeTicketContext { private final VectorStore vectorStore; Override public TicketContext apply(TicketContext context) { String query context.getCleanedText(); SearchRequest request SearchRequest.builder() .query(query) .topK(5) .similarityThreshold(0.75) .filterExpression(intent context.getIntent() ) .build(); ListDocument docs vectorStore.similaritySearch(request); context.setRetrievedDocs(docs); return context; } }提示filterExpression 里的字段名要和 Milvus collection 的 schema 一致否则会报字段不存在的错误。建议在知识入库时就打好 intent 标签检索时直接过滤。3.5 自动分派节点的决策逻辑分派节点要综合考虑三个因素意图类型、情绪等级、技能组当前负载。我用的是一个加权评分模型意图匹配度权重 0.5技能组是否擅长处理该意图情绪紧急度权重 0.3愤怒工单优先分派给资深客服负载均衡权重 0.2当前排队工单少的技能组优先每个技能组有一个能力向量记录它擅长处理的意图和对应的能力分。分派时计算工单需求向量和技能组能力向量的余弦相似度再乘以权重得到综合得分选最高分的技能组。这个模型的好处是可解释性强每个工单为什么分到某个组都能追溯到具体得分。出问题时排查起来很方便。4. 完整实操流程与关键环节实现4.1 环境准备与依赖配置先说一下基础环境。JDK 17 是必须的Spring AI Alibaba Graph 依赖 Spring Boot 3.2 以上版本。Maven 依赖主要有三个dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-graph-core/artifactId version1.0.0-M5/version /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0-M5/version /dependency dependency groupIdio.milvus/groupId artifactIdmilvus-sdk-java/artifactId version2.4.0/version /dependency配置文件里需要配好模型 API Key、Milvus 连接信息、以及 Graph 相关的线程池参数。线程池这块有个坑Graph 默认用的是 ForkJoinPool在高并发场景下容易和业务线程池互相干扰。我建议单独配一个 ThreadPoolTaskExecutor在 Graph 配置里指定使用。spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-turbo temperature: 0.1 vectorstore: milvus: client: host: localhost port: 19530 collection-name: ticket_knowledge embedding-dimension: 1536 graph: executor: core-pool-size: 8 max-pool-size: 32 queue-capacity: 200注意temperature 设 0.1 是为了让意图分类的输出稳定。回复生成节点可以单独设 0.7让语言更自然。不同节点用不同模型参数这个在 Graph 里可以通过节点级别的配置覆盖。4.2 Graph 定义与节点注册Graph 的定义我用的是编程式配置比 YAML 更灵活也方便做条件判断。核心代码如下Configuration public class TicketGraphConfig { Bean public StateGraphTicketContext ticketGraph( TicketPreprocessNode preprocessNode, IntentRecognitionNode intentNode, EmotionAnalysisNode emotionNode, KnowledgeRetrievalNode retrievalNode, AutoDispatchNode dispatchNode, ReplyGenerationNode replyNode, HumanReviewNode humanNode) { StateGraphTicketContext graph new StateGraph(TicketContext::new); graph.addNode(preprocess, preprocessNode); graph.addNode(intent, intentNode); graph.addNode(emotion, emotionNode); graph.addNode(retrieval, retrievalNode); graph.addNode(dispatch, dispatchNode); graph.addNode(reply, replyNode); graph.addNode(human, humanNode); graph.addEdge(preprocess, intent); graph.addEdge(intent, emotion); graph.addConditionalEdge(emotion, context - { if (context.getConfidence() 0.7 || context.getEmotionLevel() EmotionLevel.ANGRY) { return human; } return retrieval; }); graph.addEdge(retrieval, dispatch); graph.addEdge(dispatch, reply); graph.addEdge(reply, human); graph.setEntryPoint(preprocess); graph.setFinishPoint(human); return graph; } }这段配置里条件边是核心。情绪分析节点执行完后根据置信度和情绪等级决定走人工还是走自动流程。这个判断逻辑可以随时调整比如业务初期置信度阈值设高一点等模型效果稳定了再降低。4.3 状态对象的字段设计与序列化TicketContext的字段设计直接影响 Graph 的可观测性。我的字段清单如下字段名类型说明写入节点rawTextString原始工单文本入口cleanedTextString清洗后文本preprocessintentString意图标签intentconfidencedouble意图置信度intentemotionLevelEnum情绪等级emotionretrievedDocsList检索结果retrievaldispatchGroupString分派技能组dispatchgeneratedReplyString生成回复replyneedHumanboolean是否转人工各节点traceIdString链路追踪 ID入口序列化这块要注意Graph 执行过程中状态对象可能在节点间传递多次如果字段里有大对象比如检索到的文档列表序列化开销会很大。我的做法是检索结果只存文档 ID 和摘要完整内容按需从向量库拉取。4.4 人工审核节点的兜底设计人工审核节点是整张图的终点但不是简单的结束。它要做三件事把 AI 处理结果推送给人工客服、记录 AI 建议和人工实际处理的差异、把差异数据回流到训练集。推送这块我用的是 WebSocket人工客服的工作台实时收到工单和 AI 建议。AI 建议包括推荐意图、推荐回复、相关知识链接、情绪提示。客服可以选择采纳、修改或忽略。差异记录是后续优化的关键。每次人工修改了 AI 的意图判断或回复内容系统都会记录一条 diff。积累到一定量后用这些数据做 few-shot 示例更新或者微调模型。我实测下来运行一个月后意图识别的准确率从初始的 82% 提升到了 91%主要就是靠这个回流机制。5. 常见问题与排查技巧实录5.1 模型输出格式不稳定的处理这是最常见的问题。明明 Prompt 里写了输出格式模型偶尔还是会输出多余的解释文字。我的解决方案是三层防护第一层在 Prompt 里强调“只输出格式要求的内容不要任何额外解释”第二层在解析时用正则提取容忍一定程度的格式偏差第三层如果解析失败重试一次重试时在 Prompt 里加上“上次输出格式错误请严格按照格式输出”。重试机制要设上限最多重试两次。两次都失败就降级到规则匹配不要让整个流程卡住。5.2 向量检索召回不准的排查思路检索不准通常有三个原因Embedding 模型不适合当前语料、chunk 切分粒度不对、相似度阈值设得不合理。排查时先看检索结果和 query 的实际相似度分数。如果分数普遍偏低低于 0.6说明 Embedding 模型可能不适合考虑换模型或做微调。如果分数高但结果不相关说明 chunk 切分有问题可能是切得太碎导致语义不完整或者切得太大导致噪声太多。如果分数分布正常但 topK 里混入了不相关结果调高相似度阈值。我踩过的一个坑是知识库里的文档格式不统一有的带 Markdown 标记有的带 HTML 标签导致 Embedding 质量参差不齐。后来统一做了清洗检索准确率明显提升。5.3 高并发下的性能瓶颈工单量上来之后最先扛不住的是模型调用。每个工单要调两次模型意图识别和回复生成如果 QPS 到 50就是每秒 100 次模型调用。这个量级下API 限流和响应延迟都会成为问题。我的优化措施有三个第一意图识别加缓存相同或相似的工单文本直接命中缓存缓存用 Caffeine 做本地缓存TTL 设 10 分钟第二回复生成做异步化不阻塞主流程先生成一个简版回复返回完整回复后续推送第三模型调用加熔断降级用 Resilience4j 做熔断失败率超过阈值自动降级到规则引擎。问题现象可能原因排查方法解决方案意图识别超时模型 API 响应慢查看 API 调用日志加超时配置降级到规则检索结果为空向量库连接异常检查 Milvus 健康状态重连或降级到关键词检索回复内容重复缓存 key 冲突检查缓存 key 生成逻辑用文本 hash 做 key分派结果异常技能组负载数据过期检查负载数据更新时间加缓存过期时间状态字段丢失节点并发写入冲突查看字段版本号加乐观锁重试5.4 成本控制的实操经验模型调用成本是智能客服系统的主要运营成本。除了前面提到的规则粗筛和缓存还有几个省钱技巧意图识别用 turbo 模型回复生成用 plus 模型这个前面说过了。另外回复生成可以限制 max_tokens客服回复不需要长篇大论200 字以内就够了。还有非工作时间的工单可以延迟处理批量调用模型利用批量折扣。我算过一笔账日均 5000 工单优化前每月模型成本大约 3000 元优化后降到 1200 元左右。主要降幅来自规则粗筛省 40%和缓存命中省 25%。5.5 人工审核节点的体验优化人工客服对 AI 建议的接受度是个渐进过程。初期客服不信任 AI每个工单都要自己重新判断AI 建议形同虚设。我的做法是初期只展示 AI 建议但不强制让客服自己对比运行两周后统计 AI 建议的准确率在界面上展示“AI 建议准确率 85%”这样的数据建立信任一个月后对于高置信度的工单默认采纳 AI 建议客服只需确认。这个渐进策略很有效客服的抵触情绪明显降低采纳率从初期的 30% 提升到了 75%。6. 上线后的效果评估与迭代方向系统上线三个月核心指标变化如下工单平均处理时长从 45 分钟降到 12 分钟人工介入率从 78% 降到 23%客服满意度内部调研从 3.2 分提升到 4.1 分5 分制用户满意度工单评价从 3.8 分提升到 4.3 分。迭代方向上我目前在尝试两个事情一是把 Graph 的节点做细比如把意图识别拆成粗分类和细分类两级粗分类用规则细分类用模型进一步降低成本二是引入反馈学习把人工修改的数据自动生成 few-shot 示例动态更新 Prompt让模型持续进化。这套方案不是银弹它适合工单量大、意图相对固定、有历史数据积累的场景。如果工单量很小或者意图极其发散上这套系统的投入产出比可能不划算。我的建议是先用规则跑一段时间积累数据等数据量够了再上模型。