当前位置: 首页 > news >正文

Spring Boot + Spring AI 实战:从聊天接口到 Function Calling,支撑日均千万级智能客服的架构演进

Spring Boot + Spring AI 实战:从聊天接口到 Function Calling,支撑日均千万级智能客服的架构演进

很多团队第一次把大模型接进客服系统时,都会先做一个最小闭环:前端把用户问题发到/chat,后端调用模型,模型返回一段自然语言答案。这个方案在演示阶段几乎总是成立,因为它足够快、足够直观,也最容易让业务方看到“AI 已经能说话了”。

但只要一上真实业务,这个方案就会立刻暴露边界。

某电商客服场景里,日均会话量已经超过 1200 万次,用户问的不只是“退货规则是什么”,还会问:

  • “我尾号 3321 的订单发货了吗?”
  • “帮我给 ORD20231201 申请退款。”
  • “这张优惠券为什么不能用?”
  • “今天退款工单积压了多少?”

这类问题有一个共同点:答案不在模型里,而在订单中心、售后中心、优惠券中心和运营后台里。模型如果拿不到真实数据,就只能猜。对客服系统来说,猜错不是体验问题,而是业务风险。

所以这篇文章不打算把 Spring AI 写成一个“接模型很方便”的入门 Demo,而是聚焦一个更实际的问题:

一个最初只会聊天的 Spring Boot 智能客服,为什么最终必须演进到 Function Calling,以及这个演进过程中哪些设计是不能省的。

我会重点展开 5 件事:

  • 为什么 Prompt 拼 JSON 在生产里很快会失效
  • Spring AI 的 Function Calling 到底把哪一段链路结构化了
  • 智能客服是怎样从“单接口聊天”一步步演进成“受控工具调用”的
  • 写操作、超时、越权、历史膨胀这些异常分支该怎么处理
  • 什么阶段值得引入这套方案,什么阶段其实没必要

一、先说结论:客服系统的问题,从来不是“不会回答”,而是“不会办事”

在规则引擎时代,客服机器人的主要问题是理解力不够;换成大模型之后,主要问题就变成了执行力不够。

这两个阶段的差别非常大。

第一阶段,系统要解决的是“用户在说什么”。
第二阶段,系统要解决的是“理解之后到底该调哪个系统、带什么参数、以什么权限执行、失败了怎么收口”。

也就是说,智能客服一旦从 FAQ 场景进入真实交易场景,核心矛盾就不再是回答生成,而是下面这几个工程问题:

  • 模型如何拿到可信的业务数据
  • 模型如何触发受控的后端动作
  • 后端如何限制模型只能在允许的能力边界内工作
  • 写操作如何防重、审计和补偿
  • 会话规模上来之后,Token、线程、下游依赖谁先成为瓶颈

如果没有 Function Calling,你仍然可以做一个“很像客服”的机器人;但你做不出一个“真的能处理订单与售后”的客服系统。


二、这套架构不是一步到位长出来的,而是被线上问题逼出来的

这类系统通常会经历四个很典型的阶段。

阶段一:只有/chat,模型负责一切

最初的实现通常非常简单:

  1. 接收用户消息
  2. 拼接 system prompt 和历史消息
  3. 调模型
  4. 返回答案

这个阶段适合验证两件事:

  • 用户是否愿意和 AI 客服交互
  • 模型在咨询类问题上的表达是否足够自然

但它天然有三个限制:

  • 模型无法访问订单、物流、售后等实时数据
  • 模型无法安全执行退款、改单、查券等动作
  • 模型为了“保持有帮助”,会在不知道答案时编造答案

这也是很多团队第一次上线后最直观的感受:它很像一个懂话术的话务员,但不像一个真正接入了业务系统的客服。

阶段二:开始用 Prompt 约束 JSON 输出

不少团队会尝试一个过渡方案:在 Prompt 里要求模型输出固定 JSON,例如:

{"intent":"QUERY_ORDER","orderId":"ORD20231201"}

然后应用解析 JSON,再去调用订单服务。

这个方案比“纯文本聊天”更进一步,但通常撑不过生产:

  • 模型并不总能稳定输出合法 JSON
  • 多意图问题会把单一 JSON 结构挤爆
  • Prompt 中会混入越来越多格式约束,真正留给业务语义的上下文越来越少
  • 一旦需要把能力开放给多个工具,靠文本约束维护会迅速失控

它不是完全不能用,但更像一个过渡层,而不是长期架构。

阶段三:引入 Function Calling,把“决定调用什么”与“真正执行什么”分开

这就是 Spring AI 开始发挥价值的阶段。

Function Calling 的关键不是“让模型能调函数”,而是把原本杂糅在 Prompt 里的几件事拆开:

  • 模型负责意图理解和参数组织
  • 应用负责权限校验、参数校验和执行
  • 下游系统只暴露被允许的业务能力
  • 最终回复仍然由模型组织,但素材来自真实执行结果

一旦边界这样拆开,系统的可靠性就会比“Prompt + JSON”高一个层级。

阶段四:函数能调了,但新的复杂度也来了

Function Calling 并不是终点,它只是让系统进入了“真正工程化”的下一阶段。接下来你很快会碰到这些问题:

  • 模型选错函数
  • 模型传错参数
  • 函数调用太慢,把会话线程拖死
  • 写操作被重复触发
  • 长会话里函数结果太多,Token 爆掉
  • 下游服务失败后,模型用自然语言把失败“粉饰”成成功

所以架构演进的真正逻辑不是:

聊天接口 -> Function Calling -> 大功告成

而是:

聊天接口 -> 结构化工具调用 -> 权限与幂等 -> 超时与降级 -> 审计与观测 -> 会话与成本治理


三、Spring AI 的 Function Calling,究竟解决了什么问题

3.1 它解决的不是“能不能调用接口”,而是“如何稳定地调用接口”

从能力上说,自己解析 JSON 也能调用接口;但从工程上说,Function Calling 解决的是“结构化契约”问题。

它至少把下面四件事做对了:

  1. 把可调用能力显式列出来,而不是藏在 Prompt 说明文字里
  2. 把参数结构交给 Schema 描述,而不是交给自然语言约束
  3. 把执行权保留在应用侧,而不是让模型直接碰业务系统
  4. 把函数结果重新回灌给模型,让最终答复基于真实数据而不是自由发挥

3.2 在 Spring AI 里,这条链路是怎么跑起来的

以“查询订单状态”为例,一次完整调用通常会经历下面几个步骤:

  1. 用户发来“帮我查一下 ORD20231201 发货了吗”
  2. 应用把用户消息、对话历史、当前可用函数元信息一起交给模型
  3. 模型不直接回答,而是返回函数调用意图,例如queryOrder(orderId=ORD20231201)
  4. Spring AI 在本地匹配到对应 Bean 并执行
  5. Bean 内部再去调用订单服务,并完成权限校验、异常兜底、日志审计
  6. 执行结果以结构化内容回传给模型
  7. 模型基于真实结果生成最终自然语言回复

这条链路里最重要的一点是:

模型只参与“理解”和“组织语言”,真正的业务可信性仍由应用保证。

3.3 为什么它比 Prompt 拼 JSON 更适合生产

因为它把不稳定的部分限制在了模型擅长的区域,把必须稳定的部分留在了代码里。

模型擅长的:

  • 理解用户意图
  • 从上下文里提取候选参数
  • 把结果翻译成自然语言

代码必须兜底的:

  • 用户身份与数据归属校验
  • 参数合法性检查
  • 幂等、超时、熔断、补偿
  • 调用审计、链路追踪、错误分类

这条边界如果不划清,系统上线后一定会在“看起来像成功,实际上没成功”这个坑里摔跟头。


四、适合客服系统的,不是“开放所有函数”,而是“按意图暴露最小能力集”

很多刚接入 Function Calling 的项目容易犯一个错:把所有工具一股脑注册进去,然后交给模型自己选。

这在演示里很酷,在生产里很危险。

原因很简单:

  • 工具越多,模型误选概率越高
  • 工具描述越相近,模型区分难度越大
  • 不同用户、不同上下文,本来就不该看到相同的能力集合
  • 写操作与读操作的风险级别完全不同,不能被同一套策略放行

客服场景里更稳妥的做法通常是两层控制。

第一层:先做轻量意图裁剪,再决定本轮暴露哪些函数

例如:

  • 规则说明类问题:只开放知识检索函数
  • 商品咨询类问题:只开放商品查询函数
  • 订单状态类问题:开放订单查询,不开放退款
  • 明确的售后申请:开放退款或售后创建函数,但需要更严格校验

这样做的收益很直接:

  • 缩小模型选择空间
  • 降低误调用不存在函数的概率
  • 让高风险函数只在必要时进入上下文
  • 减少函数描述占用的 Token

第二层:即使函数被暴露,执行时仍然要再次校验

也就是说,“模型有资格提出调用请求”,不等于“调用一定会被执行”。

例如退款函数至少要验证:

  • 当前用户是否为订单归属人
  • 订单状态是否允许退款
  • 是否已存在处理中或已完成的售后工单
  • 本次请求是否重复提交

这一层不能依赖 Prompt,也不能依赖模型自觉。


五、一个更贴近生产的实现方式

下面这套实现思路,适合大部分基于 Spring Boot + Spring AI 的客服场景。重点不是代码写法有多炫,而是哪些边界不能省略。

5.1 先定义输入输出模型,而不是直接让函数接收一堆字符串

publicrecordOrderQueryRequest(StringorderId){}publicrecordOrderQueryResponse(StringorderId,Stringstatus,Stringmessage){}publicrecordRefundRequest(StringorderId,Stringreason){}publicrecordRefundResponse(Stringstatus,StringrefundId,Stringmessage){}

这样做的意义不是“代码更优雅”,而是:

  • Schema 更清晰,模型更容易生成稳定参数
  • 后续扩展字段时兼容性更好
  • 参数校验、日志脱敏、错误映射都更容易落地

5.2 Function Bean 的关键,不是能调通,而是把安全和失败分支写在里面

@ConfigurationpublicclassCustomerServiceFunctionConfig{@Bean@Description("根据订单号查询订单状态。仅支持查询当前登录用户自己的订单。")publicFunction<OrderQueryRequest,OrderQueryResponse>queryOrder(OrderServiceFeignClientorderClient){returnrequest->{StringuserId=SecurityContextHolder.getContext().getAuthentication().getName();if(request==null||request.orderId()==null||request.orderId().isBlank()){returnnewOrderQueryResponse(null,"INVALID_ARGUMENT","缺少有效订单号");}try{if(!orderClient.verifyOwner(request.orderId(),userId)){returnnewOrderQueryResponse(request.orderId(),"FORBIDDEN","无权查询该订单");}returnorderClient.queryOrder(request.orderId());}catch(Exceptionex){returnnewOrderQueryResponse(request.orderId(),"SYSTEM_ERROR","订单系统繁忙,请稍后重试");}};}@Bean@Description("为当前用户的订单申请退款。仅在订单满足售后条件时允许调用。")publicFunction<RefundRequest,RefundResponse>applyRefund(RefundServicerefundService){returnrequest->{StringuserId=SecurityContextHolder.getContext().getAuthentication().getName();if(request==null||request.orderId()==null||request.orderId().isBlank()){returnnewRefundResponse("INVALID_ARGUMENT",null,"缺少有效订单号");}returnrefundService.submit(userId,request.orderId(),request.reason());};}}

这里真正不能省的有四件事:

  • 参数校验要在函数里做,不能假设模型一定传对
  • 权限校验要在函数里做,不能假设模型不会越权
  • 失败返回要结构化,不能把异常栈直接抛回模型
  • 写操作要交给领域服务处理幂等和状态流转,不能在聊天层直接“改库”

5.3 聊天服务层要做的,不只是调用模型,还要决定本轮开放哪些函数

@ServicepublicclassIntelligentChatService{privatefinalChatClientchatClient;privatefinalConversationMemoryServicememoryService;publicIntelligentChatService(ChatClient.Builderbuilder,ConversationMemoryServicememoryService){this.chatClient=builder.build();this.memoryService=memoryService;}publicStringchat(StringsessionId,StringuserId,StringuserMessage){List<String>candidateFunctions=routeFunctions(userMessage);List<Message>history=memoryService.load(sessionId);Stringanswer=chatClient.prompt().system(""" 你是电商智能客服。 若问题涉及订单、售后、优惠券等系统能力,只能通过已提供的函数获取事实,不允许自行编造结果。 若函数返回失败,必须如实告知用户当前无法完成。 """).messages(history).user(userMessage).functions(candidateFunctions.toArray(String[]::new)).
http://www.jsqmd.com/news/1237560/

相关文章:

  • ASSOCIATED RESEARCH 7564SA 分析仪
  • Webhook 实战:准入控制与默认值注入
  • 国产化 ECU 刷写上位机(CAN FD)开发与实操指南
  • 基于AI与本地知识库的游戏动态攻略生成系统设计与实现
  • 小程序毕设项目:基于 SpringBoot 的基层社区养老保障资源整合平台 社区老人健康保障与帮扶服务小程序(源码+文档,讲解、调试运行,定制等)
  • C++编程竞赛中排列组合计算:从数学原理到高效代码实现
  • 第35章 开源贡献与持续成长
  • Java反应式编程核心原理与实践指南
  • 私域邦网络推三返一合规模式系统开发
  • 主权校验码技术解析:从加密算法到跨境应用
  • 哔咔漫画离线阅读终极指南:如何用免费工具快速构建个人漫画图书馆
  • 物业厂区对讲机选型与组网方案,黑龙江单工科技打造高效内部通信体系
  • AI 电动家居用品智能功率 覆盖电机驱动、加热控制、智能电源管理的完整选型方案
  • 2026常州市手机维修推荐 优质服务商实用指南 - 谁都没有我好看
  • iPhone邮件分类优化与手动修正全指南
  • 2026年门票印刷新趋势:性价比与质量兼得的供应商选择指南 - 米諾
  • 直播预约|基于 openJiuwen 的多 Agent 项目实战与系统构建
  • SAP PP 一些意料之外的报错
  • 固态储氢技术在快递物流行业的应用与突破
  • 数据集成平台哪家好?带你看懂ETLCloud和帆软的不同
  • Android功耗系列专题理论之十八:整机续航评估思路
  • 深入解析C2000 ePWM模块:从寄存器到实战配置指南
  • 2026年C#上位机开发:AI集成与跨平台技术趋势
  • Unity UGUI按钮透明区域点击穿透:原理、实现与性能优化
  • 2026 厦门翔安防水补漏公司排名推荐 卫生间屋顶地下室根治指南 - 苏易房屋修缮
  • 嵌套滚动处理 - 鸿蒙FlutterNestedScrollView应用
  • 都叫源头工厂,怎么知道是真工厂还是“皮包公司”?
  • 信息安全系统访问控制
  • Agent Runtime 解耦:从 Context Window 到事件日志的工程演进
  • 【Springboot毕设全套源码+文档】基于springboot旅游出行指南系统的设计与实现(丰富项目+远程调试+讲解+定制)