给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了

说实话,接这个活的时候我心里没底。

上个月产品经理找我,说客服那边扛不住了------每天800多条工单,4个客服轮班都回复不过来,用户满意度跌到了72%。领导的意思是:你不是搞技术的吗,搞个AI客服试试。

我当时的第一反应是:又要造轮子了。之前公司搞过一轮智能客服,用的某商业方案,花了大几万,结果回答得驴唇不对马嘴,上线两周就下线了。这次让我搞,我心里其实挺虚的。

但这次有个不一样的地方------Spring AI 1.0在5月份正式GA了。我之前一直有关注这个项目,从0.8版本就开始试,那时候bug不少,API也变来变去。但1.0 GA之后,我重新过了一遍,发现真的能用了。

下面是整个过程的记录,不美化,踩的坑也一并写出来。


先说结论

花了3天,搭了个AI客服Agent,能自动回复60%的常见问题,准确率85%左右(比之前那套商业方案强太多了)。领导看了demo直接拍板上线试运行。

技术栈很简单:Spring Boot 4 + Spring AI 1.0 + DeepSeek API + Redis做向量缓存。


Day 1:跑通最小闭环

第一天的目标就一个------让AI能回答公司业务问题。

之前的商业方案为什么烂?因为它只有一个大模型API,啥上下文都不给,直接让模型回答"怎么退货"这种问题。大模型又不知道你们公司的退货政策,只能瞎编。

所以核心思路是RAG(检索增强生成):先把公司知识库灌进去,用户提问时先检索相关文档,再把文档和问题一起丢给大模型。

Spring AI 1.0做这个事情其实不复杂。先加依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>

然后在application.yml里配一下DeepSeek的API(Spring AI的OpenAI兼容接口可以直接用DeepSeek):

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
          temperature: 0.3  # 客服场景温度调低,回答更稳定

temperature这个参数很关键。一开始我用的默认0.7,结果同样的问题问两次,回答不一样,客服主管直接提意见了。后来调到0.3,回答就稳定多了。

接下来是把知识库文档灌进Redis向量库。客服那边有一堆Word文档和PDF,都是产品FAQ、退换货政策、常见故障排查这些。Spring AI 1.0自带的文档读取器可以直接读PDF:

java 复制代码
@Service
public class KnowledgeImportService {

    private final VectorStore vectorStore;

    public void importKnowledgeBase(String filePath) {
        // 读PDF
        var reader = new PagePdfDocumentReader(filePath);
        var docs = reader.get();

        // 切块,每块大概500 token
        var splitter = new TokenTextSplitter(500, 200, 10, 5000, true);
        var chunks = splitter.apply(docs);

        // 写入Redis向量库
        vectorStore.add(chunks);
        
        log.info("导入完成,共{}个文档块", chunks.size());
    }
}

这块我踩了个坑。一开始切块太小(200 token),切得稀碎,检索出来的文档块上下文不完整,AI回答经常断章取义。后来调成500 token,好多了。这个参数没有标准答案,得根据你的文档类型试。

第一天结束,最小闭环跑通了------用户输入问题,系统检索知识库,调用大模型,返回回答。虽然还很粗糙,但至少能回答出"你们的退货政策是什么"这种问题了。


Day 2:加Function Calling,让它能干活

光能回答问题不够,客服场景里有个高频需求:查订单状态。

用户说"我的订单到哪了",光靠RAG是不行的------订单数据在数据库里,知识库里没有。这时候就要用Spring AI的Function Calling了。

说白了就是让AI能调用你的Java方法。定义一个函数:

java 复制代码
@Bean
@Description("根据订单号查询订单状态和物流信息")
public Function<OrderQuery, OrderInfo> queryOrder(OrderService orderService) {
    return query -> orderService.queryOrderInfo(query.orderNo());
}

public record OrderQuery(String orderNo) {}
public record OrderInfo(String status, String logistics, String estimatedArrival) {}

然后在ChatClient里把这个函数绑上去:

java 复制代码
var response = chatClient.prompt()
    .system("""
        你是公司的AI客服助手。回答用户问题时要:
        1. 基于知识库内容回答,不要编造
        2. 如果用户提供了订单号,调用queryOrder查询订单状态
        3. 语气友善但简洁,不要说太多废话
        """)
    .user(userMessage)
    .functions("queryOrder")
    .call()
    .content();

效果是这样的------用户说"帮我查下订单DD20260720001到哪了",AI会自动识别出这是查订单的意图,提取订单号,调用queryOrder方法,拿到结果后组织成自然语言回复。

这比之前那套商业方案强太多了。之前那套需要写一堆意图识别的规则,现在大模型自己就能理解意图。

但这里也有个坑。Function Calling有个限制:函数的参数和返回值会被序列化成JSON传给大模型。如果你的返回值太大(比如返回了一个包含100个字段的巨型DTO),token消耗会暴涨,而且模型容易"看花眼",抓不住重点。所以返回值要精简,只返回AI需要的信息。

java 复制代码
// 别这么干 ------ 返回整个Order实体
public record OrderInfo(Order order) {}  // Order有50个字段...

// 应该这么干 ------ 只返回需要的字段
public record OrderInfo(String status, String logistics, String estimatedArrival) {}

Day 3:加记忆、加兜底、加监控

第三天主要是打磨细节。

对话记忆。用户经常连续问几个问题,比如"退货政策是什么" → "那运费谁出" → "多久能到账"。第二第三个问题没有上下文是答不了的。Spring AI 1.0的记忆功能:

java 复制代码
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) {
    return builder
        .defaultSystem("你是公司的AI客服助手...")
        .defaultAdvisors(new MessageChatMemoryAdvisor(memory))
        .build();
}

加上这个之后,AI能记住同一会话里的上下文了。这里我用的Redis-backed的ChatMemory实现,因为默认的InMemoryChatMemory重启就没了。

兜底机制。这是我觉得最重要的一个环节。AI不是万能的,碰到回答不了的问题,不能让它瞎编。我在system prompt里加了硬性约束:

arduino 复制代码
如果知识库中没有相关信息,必须回复:"这个问题我暂时无法回答,正在为您转接人工客服。"
绝对不要编造答案。

然后在代码里检测这个回复,自动触发转人工的流程:

java 复制代码
if (response.contains("转接人工客服")) {
    // 自动创建工单,转给人工客服
    ticketService.createEscalationTicket(userId, userMessage, conversationId);
}

这个兜底逻辑上线后,客服那边的反馈好了很多------至少不会出现AI瞎回答导致用户投诉的情况了。

监控。这个容易忽略但很重要。你总得知道AI回答了多少问题、准确率多少、转人工率多少。Spring AI 1.0集成了Micrometer,可以直接在application.yml里开:

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

然后我写了个简单的统计面板,每天看这些数据:

  • AI回复总数 / 转人工数(转人工率)
  • 平均响应时间
  • 用户满意度(客服那边的后续标记)

上线一周后的真实数据

指标 AI客服 之前
日处理工单 520条 800条人工
自动解决率 62% 0%(全部人工)
平均响应时间 3秒 15分钟
用户满意度 83% 72%
转人工率 38% ---

62%的自动解决率说实话比我预期高。剩下38%转人工的,主要是复杂投诉和需要退款操作的场景------这些本来就该让人来处理。

满意度从72%到83%,提升了11个点。最大的改善是响应速度,之前用户问个问题等15分钟,现在3秒就有回复。


踩的坑和教训

坑1:知识库质量决定一切。 AI回答准不准,80%取决于知识库。一开始客服给我的文档乱七八糟,有2019年的旧政策没更新的,有同一个问题写了三份不同版本的。花了大半天清理文档,AI回答准确率立刻上了一个台阶。

坑2:DeepSeek偶尔会抽风。 有一次用户问了个完全无关的问题(问天气),AI一本正经地用公司退货政策格式回答了天气问题。后来在system prompt里加了约束:"只回答与公司业务相关的问题,无关问题礼貌拒绝。"

坑3:并发量上来后API限流。 DeepSeek的API有并发限制,高峰期偶尔会429。加了个本地队列做削峰,问题不大。如果你用的是商业API,记得算好成本------我们每天500+次调用,月费大概在200-300块,比人工客服便宜太多了。


技术选型的一些思考

有人可能会问,为什么不用LangChain4j?

我对比过。LangChain4j确实也不错,社区活跃,功能也全。但我选Spring AI有两个原因:

第一,和Spring Boot的无缝集成。依赖注入、配置管理、Actuator监控这些全都是现成的,不需要额外学习成本。对我们这种已经是Spring Boot技术栈的团队来说,几乎零成本接入。

第二,Spring AI 1.0 GA之后的稳定性明显好了。之前0.8版本我试过,API变动太频繁,升级一次就得改一堆代码。1.0之后API稳定了,文档也完整了,敢用在生产环境了。

当然,如果你的项目不是Spring Boot技术栈,LangChain4j可能更合适。技术选型没有绝对的对错,适合自己团队就行。


下一步计划

目前这个AI客服还是"单轮检索+函数调用"的模式,比较简单。接下来想做的几个方向:

  1. 多轮对话的状态管理------比如用户说"我要退货",AI能引导用户走完整个退货流程,而不是一问一答
  2. 接入更多Function------比如直接帮用户创建退货单、查询物流详情
  3. 效果评估自动化------目前准确率是人工抽检的,想搞一套自动化的评估体系

如果有人也在搞类似的东西,欢迎交流。说实话这个领域变化太快了,Spring AI几乎每两周一个版本,我也在持续学习中。


如果这篇文章对你有点用,帮忙点个关注。不为了别的,就是想记录一下踩坑的过程,万一有人也踩了同样的坑,至少能少走点弯路。

《卷毛的技术笔记》,一个9年Java开发的真实技术笔记。

相关推荐
曹牧1 小时前
Eclipse 批量文本替换
java·ide·eclipse
Darren2451 小时前
MySQL索引执行计划不走索引下推
后端
Ai拆代码的曹操1 小时前
opencode 源码调试环境搭建:bun install → F5 断点全流程
ai编程·opencode·源码拆解
程序员清风2 小时前
OpenAI官方发布最新提示词技巧!
java·后端·面试
梅头脑2 小时前
写了3年CRUD,volatile和synchronized的区别还是答不清——直到我画出了这张三性两锁边界图
java
暮暮祈安2 小时前
Celery 新手入门指南
java·数据库·python·flask·httpx
栩栩云生2 小时前
命令行的门槛从"会写"变成了"会拦"
安全·ai编程·命令行
码事漫谈2 小时前
人机协同的三重范式:HITL、HOTL与HOOTL
后端
武子康2 小时前
Inkling 975B 说明“开放权重“与“普通开发者本地运行“已经分离,内容重点应是部署容量和运行时边界
前端·人工智能·后端