新闻详情

新闻详情

首页 / 资讯中心 / 详情

[AI工程]Spring AI 第十七篇:对话式 UX 重构——从 8 字段表单到多轮槽位填充,哪些环节真的值得改

发布时间:2026/9/28 4:16:02来源:尧图网络
[AI工程]Spring AI 第十七篇:对话式 UX 重构——从 8 字段表单到多轮槽位填充,哪些环节真的值得改
上一篇把航司客服做成 RAG 问答之后产品同学提了个看起来很小的需求能不能让用户一句话就把改签办完别再让他填那张 8 个字段的表。我一开始的判断是这不就是多加几轮追问吗两小时的活。真做起来才发现难的不是让模型多问几句而是三件事同时成立模型得知道还缺哪个字段、得知道哪些字段不能由它自己填、还得在用户中途说算了不退了的时候干净地退出而不留下一半状态。更麻烦的是 2.0 的对话记忆接口和 1.x 长得不一样——ChatMemory.get(conversationId, lastN)这个签名在新版本里已经没了窗口大小改由构造参数决定。所以这一篇不谈聊天界面多好看只谈后端要还的债意图与槽位怎么用结构化输出落地、多轮状态存在哪、流式和中断怎么处理、以及哪些表单压根不该改成对话。第十七篇对话式 UX 重构从 8 字段表单到多轮槽位填充到底哪些环节值得改1. 对话式不是少几个输入框先把判据说清楚否则很容易做成一个更难用的表单。表单和对话的差别不在交互形式在谁持有流程状态表单状态在前端 数据库 用户填 8 格 -- 一次提交 -- 校验 -- 落库 对话状态在服务端会话 用户说话 -- 模型抽字段 -- 缺什么问什么 -- 确认 -- 落库 ↑ 这一层是你新写的代码不是框架给的维度表单对话式谁承担成本字段缺失前端红框提示用户自己补模型判断缺哪个槽主动追问后端要存当前已收集到什么字段非法正则 / 类型校验即时模型可能给出2026-13-45服务端必须二次校验不能信模型中途改主意刷新页面即回到干净状态算了不改成后天了要能撤销 pending 动作会话状态机要可回退上下文引用不支持上次那个单号要重打天然支持记忆窗口 隔离见第 3 节用户输入成本高8 格2 个下拉1 个日期控件低一句话—可测性高E2E 脚本稳定低同输入不同问法评测集见第十二篇判断标准我浓缩成一句字段多、有依赖关系、用户记不住规则的场景才值得改两三个字段能选完的别改。场景建议理由改签订单号 / 目标日期 / 差价确认 / 座位偏好✅ 改4 个字段有依赖先有单才能改且目标日期用户会说话表达“下周三下午”修改手机号❌ 不改2 个字段一次表单更快且涉敏感操作走对话反而多一轮报销单十几字段 明细行✅ 改但保留表单兜底明细行用自然语言录入省大量时间金额校验仍需前端展示密码重置❌ 不改安全路径不该引入模型判断见第十八篇笔者后端 架构的顺序是先做对话填单 表单确认这种混合模式跑三个月看用户是否真的少打电话再考虑把整条流程改成纯对话。2. 槽位抽取用结构化输出不要用多轮自由聊这是全篇最值钱的一节把 NLU 那套意图分类 实体抽取换成一次带 schema 的模型调用。2.1 传统 NLU 的三件套在 LLM 时代塌缩成一步传统ASR/文本 -- 意图分类模型 -- 实体抽取NER-- 槽位映射表 -- 状态机追问 现在文本 -- 一次 LLM 调用system 里写清 schema-- Record 对象省掉的是意图分类器和槽位映射表没省掉的是追问逻辑——它从模型自己决定问什么变成你根据缺失字段决定问什么。别指望模型在一个自由对话里既理解又推进流程那会把不可控的地方堆得最多。2.0.1 的落点就是.call().entity(...)ChatClient$CallResponseSpec有entity(ClassT)、entity(ParameterizedTypeReferenceT)、entity(StructuredOutputConverterT)三族重载配合一个 RecordpublicrecordChangeFlightIntent(NullableStringbookingCode,// 不给 required缺失就是要问不是报错NullableStringtargetDate,// ISO yyyy-MM-dd归一化在服务端做NullableStringseatPreference,StringuserUtterance){// 原话留一份用于审计与追问复述}ServicepublicclassSlotFillingService{privatestaticfinalStringSYSTEM 你是航空客服的意图抽取器。只做抽取不做回答。 规则 1. 只输出 JSON字段缺失时留空严禁编造订单号或日期 2. 相对日期下周三按 today 参数换算成 yyyy-MM-dd 3. 用户没提到的槽位必须为空不要根据常识补全。 ;publicChangeFlightIntentextract(StringuserText,StringconversationId){returnthis.chatClient.prompt().system(SYSTEM).user(u-u.text(userText).param(today,LocalDate.now().toString()).param(conversationId,conversationId)).advisors(a-a.param(ChatMemory.CONVERSATION_ID,conversationId)).call().entity(ChangeFlightIntent.class);}}ToolParam那套 required 语义在这里不适用这是输出不是工具入参但**“宁缺毋滥必须写在 system 里**——模型最擅长的就是把缺失字段漂亮地编出来。这也是第七篇讲的 schema 校验之外的第二道语义层的不许补全”。2.2 追问由代码生成不由模型生成拿到 intent 之后缺什么、先问哪个用普通 Java 决定publicDialogueTurnadvance(StringconversationId,StringuserText){ChangeFlightIntentcollectedthis.state.merge(conversationId,this.slotFillingService.extract(userText,conversationId));OptionalStringmissingfirstMissing(collected);// 顺序 业务优先级if(missing.isPresent()){returnDialogueTurn.ask(missing.get(),collected);// 问法固定模板别每次让模型现编}Validationverdictthis.bookingService.validateChange(collected);// 日期合法性、票种是否可改if(!verdict.ok()){returnDialogueTurn.reject(verdict.reason());// 拒绝理由来自服务端不来自模型}returnDialogueTurn.confirm(collected,verdict.diffAmount());// 复述式确认卡片}两个刻意的选择问法用模板。让模型自由组织追问话术同一会话三轮三种问法前端气泡很难看评测也没法写断言。把话术收敛到messages.properties里模型只负责抽取。校验永远在服务端。2026-13-45这种日期模型会照样输出票种不可改这种业务规则模型不可能知道。对话式的聪明只体现在理解输入不体现在判断合法性。StructuredOutputValidationAdvisor在spring-ai-client-chat的 advisor 包里可以再加一层输出不符合 schema 时自动带错误信息重试。它管格式不管语义——这两件事别混。3. 多轮状态ChatMemory在 2.0.1 的真实形状这一节先讲版本断层因为照抄 1.x 示例会直接编译不过或者语义变了。3.1 三处必须知道的差异事项1.x 常见写法2.0.1 实际影响取历史chatMemory.get(conversationId, lastN)ChatMemory只有get(String)——没有 lastN 重载窗口大小改由MessageWindowChatMemory.builder().maxMessages(n)决定是构造期配置不是读取参数隔离会话有的示例漏传conversationId文档明确省略该参数运行时抛IllegalArgumentException没有默认值每个请求都要.advisors(a - a.param(ChatMemory.CONVERSATION_ID, id))Advisor 构造直接new MessageChatMemoryAdvisor(chatMemory)多个重载MessageChatMemoryAdvisor.builder(ChatMemory)起 builder.order(int)默认HIGHEST_PRECEDENCE 200与工具 Advisor 的顺序要显式想清楚3.3接口本身很小javap全量publicinterfaceChatMemory{StringCONVERSATION_IDconversationId;defaultvoidadd(StringconversationId,Messagemessage);// 委托给 List 版本voidadd(StringconversationId,ListMessagemessages);ListMessageget(StringconversationId);voidclear(StringconversationId);}publicinterfaceChatMemoryRepository{ListStringfindConversationIds();ListMessagefindByConversationId(StringconversationId);voidsaveAll(StringconversationId,ListMessagemessages);voiddeleteByConversationId(StringconversationId);}也就是说记忆的策略在MessageWindowChatMemory截多少条存储在ChatMemoryRepository存哪两件事已经彻底拆开。2.0.1 官方 repository starter 有五种spring-ai-starter-model-chat-memory-repository-{jdbc,redis,cassandra,neo4j,mongodb}另有 in-memory 实现具体选型对比见 第六篇。BeanChatMemorychatMemory(JdbcChatMemoryRepositoryrepo){returnMessageWindowChatMemory.builder().chatMemoryRepository(repo).maxMessages(20)// 一轮用户 助手是 2 条20 条 ≈ 10 轮.build();}maxMessages对对话式填单特别关键窗口太小第 5 轮追问时模型已经不记得用户第 1 轮说的订单号了窗口太大Token 和成本线性上涨第三篇、第十三篇。我的经验值槽位数量 × 2 4起步再按评测集调。3.2 真正该单独存的不是聊天记录对话式流程有个陷阱把当前收集到哪些字段寄托在模型从历史里重新读出来。窗口一裁、或者用户插了一句闲聊抽取就飘。正确做法是会话状态与消息历史分开存// 消息历史给模型看ChatMemoryRedis / JDBC// 收集状态给代码看自己的表带 TTLDocument(dialogue_slot_state)// 或一张 MySQL 表publicclassDialogueState{IdStringconversationId;StringflowId;// CHANGE_FLIGHTChangeFlightIntentcollected;// 已收集的槽位InstantupdatedAt;intaskCount;// 追问次数超过 3 次转人工StringpendingActionId;// 待确认动作见第十八篇的 decision}这样用户说算了只要把pendingActionId清掉、状态置空不依赖模型理解算了之后历史里那句话还在不在。TTL必设。Redis 的话直接EXPIRE用 JDBC 的话加个定时清理。会话状态留着不动半年后会有一堆半死流程等着人工排查。3.3 记忆 工具在同一轮里不打架第十一篇 里那句难的是让记忆、检索、工具各只干自己该干的那一次在 2.0.1 有具体的机制支撑。ChatClient.Builder自动注册工具 Advisor 时会检查链路上有没有记忆 AdvisorbooleanhasDownstreamMemoryAdvisorthis.advisors.stream().anyMatch(a-ainstanceofMemoryAdvisora.getOrder()configuredOrder);this.advisors.add(this.toolCallingAdvisorBuilder.copy().conversationHistoryEnabled(!hasDownstreamMemoryAdvisor)// ← 有外层记忆就关掉内部历史.build());数值关系是这样的ToolCallingAdvisor.DEFAULT_ORDER HIGHEST_PRECEDENCE 300记忆 Advisor 默认HIGHEST_PRECEDENCE 200Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER。200 300所以默认配置下记忆 Advisor 在外层框架据此关掉工具循环的内部历史避免每一轮工具迭代都重写一遍会话记忆。如果你把记忆 Advisor 的 order 手调到 400想让它只作用于最后一次模型调用这个自动判断会反过来——conversationHistoryEnabled变true工具循环自己维护历史而记忆在循环内层反复读写。这类顺序改了个数字行为静默变化的坑我在第十一篇 5.2 记过一次对话式场景更容易踩到因为追问轮数天然多。4. 流式、中断、重定向对话式对延迟极其敏感一个追问要 3 秒才开口用户就以为卡住了。4.1 端点2.0.1 的流式出口是Flux.stream().content()返回FluxString.chatClientResponse()返回FluxChatClientResponseWebFlux 侧直接吐 SSE 就行GetMapping(value/api/assistant/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxServerSentEventStringstream(RequestParamStringconversationId,RequestParamStringtext,Authenticationauth){FluxServerSentEventStringtokensthis.chatClient.prompt().user(text).advisors(a-a.param(ChatMemory.CONVERSATION_ID,conversationId)).toolContext(Map.of(userId,auth.getName())).stream().content().map(chunk-ServerSentEvent.builder(chunk).event(token).build());returntokens.concatWith(Flux.just(ServerSentEvent.Stringbuilder([DONE]).event(done).build()));}细节和坑见 第三篇多模型与流式MCP 传输层那边 SSE 已经在 2.0.0 起被标记 deprecated那是另一件事别把两处混起来看。4.2 流式 工具轮次显示什么流式下工具执行会在中间产生若干轮模型调用。ToolCallingAdvisor的流式实现内部要聚合消息再决定是否继续循环所以你在Flux里看到的顺序不是模型一字一句往外蹦而是先可能有一段工具过程、然后才是正文。前端别按纯文本流渲染否则气泡会闪回。我的做法是三类事件分开event: tool_start data: {name:getBookingDetails,arguments:{...}} ← 折叠显示 event: tool_end data: {name:getBookingDetails,durationMs:312} ← 只给耗时与名字不给原文 event: token data: 正文增量 event: action_card data: {type:confirm,actionId:pa-7f3,summary:改签到 2026-10-03}tool_end里刻意不回传工具返回值全文那里面常有乘客姓名和证件号前端用不到且它一旦进浏览器 DOM 就等于出网边界又外移了一层。要看明细去点审计页需要另一个权限。这也和第十八篇的观测内容开关是同一个原则。4.3 中断和改主意用户点停止、或者在流式过程中又发了一句话都要能收敛// 前端停止 关闭 EventSource服务端要在订阅取消时把工具链掐掉returntokens.doOnCancel(()-this.dialogueState.markAborted(conversationId));三件事必须做Flux取消不等于工具停止。正在执行的工具方法是自己跑的要往下传中断协程式取消在 Java 里最实用的做法是给工具方法一个可查的abortFlag或让 Service 层的调用带超时。长耗时工具导出、批处理尤其。半截状态要有名字。我用的就是 3.2 里的pendingActionIdmarkAborted只做一件事——清空它。历史消息保持原样不删。改主意要能识别。在 slot 抽取的 system 里加一条用户表达撤销意图时输出intentABORT。这比在历史里让模型自己想起来稳。4.4 重连与幂等SSE 断了要重连重连后不能把上一段重复执行一遍。做法每次用户输入带一个turnId客户端生成 UUID服务端按turnId幂等——重复提交直接返回已缓存的那轮结果。这个开关一旦漏掉用户网络抖一下就能改签两次。5. 前端消息流怎么渲染才不像玩具后端作者视角只讲接口约定不讲组件选型。气泡类型触发渲染要求用户本地即时立即上屏不等服务端失败再标红重试助手正文event: token增量打字机效果光标只在最后一个 token 气泡上工具过程tool_start/tool_end默认折叠一行正在查询订单…312ms展开只显示脱敏摘要确认卡片action_card结构化渲染按钮回调actionId不走文本输入追问服务端模板文案与正文同样式但带跳过入口系统提示校验失败 / 转人工与助手气泡视觉区分避免用户以为是模型在拒绝三条约定确认动作绝不能只有打字确认这条路。卡片按钮回传actionId服务端按 ID 执行落库参数第十八篇 5.2 的原因别让用户确认的内容和实际执行的不是同一份。消息体带turnIdcreatedAt。重连、乱序、翻页都要靠它。不要展示模型的思考过程文本。它经常包含未经校验的业务判断“我看这单应该可以免费退”一旦被截图就是客诉证据。要展示推理走第十三篇的 trace 页面给内部人员看。Q要不要在前端做意图路由比如猜用户想改签就直接开对话可以让前端做建议猜到了就把流程名作为参数带上减少抽取歧义但路由结论必须由服务端抽取给出。前端猜错的代价是用户被卡在错误流程里比多问一句难受得多。6. 混合模式我更推荐先做这个纯对话在真实业务里最常见的失败不是技术问题是用户不敢看不清要提交什么、改了什么、能不能撤回。所以我推荐的落地形态是对话输入 表单确认用户帮我改到下周三下午靠窗 ↓ [模型] 抽取 → {targetDate: 2026-10-07, seatPreference: WINDOW, bookingCode: null} ↓ [服务端] 缺 bookingCode → 查该用户近期订单可信 userId见第十八篇→ 唯一命中就补上 ↓ [渲染] 表单卡片出发日期 09-30 → 10-07周三下午 | 座位 靠窗 | 差价 ¥240 ↓ [用户] 点确认 → 服务端按落库参数执行不再问模型用户得到对话的省事也保留表单的可见性和可控性。三个实现要点补槽优先用数据不用追问。能从该用户名下唯一在途订单推出的字段就别问用户。追问次数是这套体验的头号杀手我在DialogueState.askCount上设了 3 次上限超过直接转人工。卡片是结构化 DTO不是 HTML 字符串。后端出ChangeConfirmCard前端渲染评测时断言 DTO 就够不用碰 DOM。对话与表单共享同一个校验入口。否则两条路径的校验规则一定会漂移最后变成网页上能提交、对话里报错。7. 上线自检清单#检查项怎么验不过关的表征1每个请求都传CONVERSATION_ID漏传必须抛IllegalArgumentException这是框架行为别 catch 掉会话串号A 看到 B 的订单2槽位状态独立于消息历史手工把maxMessages调到 4流程仍能走完换个说法就忘了订单号3模型不许补全缺失字段单测只说我要改签intent 三个字段必须全空冒出一个不存在的 bookingCode4校验只在服务端断言2026-13-45会被拒模型给的日期直接进库5确认走actionId断言确认执行不再调模型用户确认 A 参数执行了 B6流式事件分型前端能区分 token / tool / card 三类工具执行时正文气泡闪回7取消能停工具断开工后长耗时工具在超时内终止停止按钮只停显示8turnId幂等同turnId重发两次只有一次生效网络抖动导致重复改签9追问轮数上限三轮问不全转人工用户和模型互相打转最后打电话10有对话流程的评测集第十二篇那套用例里加改主意“插话”信息不全三类改 prompt 后通过率悄悄掉最后总结对话式重构的成本不在模型调用在状态机槽位状态、待确认动作、追问计数、幂等键全是普通 Java 代码框架不会替你写。槽位抽取交给一次.entity(Record.class)调用追问话术和校验规则留在代码里这是我把不可控范围压到最小的分界线。2.0.1 的ChatMemory读取接口去掉了lastN窗口在MessageWindowChatMemory.maxMessages上定CONVERSATION_ID漏传直接抛异常——照抄 1.x 示例一定翻车这两处是硬断层。记忆与工具同轮时ChatClient.Builder会按 order 关系自动决定conversationHistoryEnabled手改记忆 Advisor 的 order 会连带改变工具循环的历史行为这类静默变化比报错危险。流式要分三类事件token / 工具过程 / 动作卡片工具返回值不进前端重连要有turnId幂等。表单 对话的混合模式是我的默认推荐对话负责省输入表单负责看得懂、可撤回。对于后端 / 架构开发者我个人更关注判据本身字段少、无依赖、涉安全的流程就别改对话式了——这一篇里的改动清单用在两三处真正高频的入口上收益比全站改造大得多。参考资料 致谢[1] Spring AI Reference2.0.1[2] Spring AI - Chat Memory[3] Spring AI - Advisors[4] spring-projects/spring-ai - GitHub[5] Spring AI 第三篇多模型、流式输出与工具调用[6] Spring AI 第五篇Advisor 对话拦截[7] Spring AI 第六篇对话记忆的数据库与 Redis 实现[8] Spring AI 第七篇结构化输出[9] Spring AI 第十一篇基于航空智能客服的 RAG 实战
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Vulkan快速上手实战:核心概念与踩坑记录 2026/9/28 5:33:49

Vulkan快速上手实战:核心概念与踩坑记录

Vulkan这个名字,圈内人听了都知道分量。它是 Khronos Group 维护的图形与计算 API,2016 年发布至今,基本成了底层 GPU 编程绕不开的话题。它能做什么?一句话概括:它让你直接控制 GPU 的工作方式,从资源分配…

阅读更多 →
SpringBoot+Vue+MySQL物品租赁系统毕设全流程:从数据库设计到部署 2026/9/28 5:33:49

SpringBoot+Vue+MySQL物品租赁系统毕设全流程:从数据库设计到部署

物品租赁系统这个题目,在每年的毕业设计选题里都能见到,而且是那种一眼看去平平无奇、细琢磨却五脏俱全的类型。SpringBoot做后端、Vue做前端、MySQL存数据,这一套组合几乎就是当前Java后端方向课程设计和毕业设计的主流标配。选它的人&#…

阅读更多 →
Jupyter Notebook机器学习案例实战:从数据预处理到模型评估 2026/9/28 5:33:49

Jupyter Notebook机器学习案例实战:从数据预处理到模型评估

简介:基于Jupyter Notebook的机器学习基本模型算法教程,系统讲解从数据预处理到模型调优的完整流程,适合希望通过Python快速上手数据分析与建模的初学者及开发者。内容围绕NumPy、Pandas、Scikit-learn展开,覆盖线性回归、逻辑回归…

阅读更多 →
PSO-CNN回归预测:用粒子群算法自动优化CNN超参数 2026/9/28 5:33:49

PSO-CNN回归预测:用粒子群算法自动优化CNN超参数

简介:基于粒子群算法优化卷积神经网络(PSO-CNN)的Matlab完整源码,面向多变量输入的回归预测任务,支持多输入单输出结构,适合需要自动确定CNN超参数的科研与工程人员,也可用于能源、经济、环境等…

阅读更多 →
从零搭建网站,如何优化网站图片大小才能不卡? 2026/9/28 5:33:49

从零搭建网站,如何优化网站图片大小才能不卡?

从零搭建网站,如何优化网站图片大小才能不卡? 域名解析和服务器配置往往让新手头疼,但这只是冰山一角。真正拖慢打开速度、导致用户流失的,通常是那些未被压缩的海量图片。很多站长花大价钱买了高端服务器,结果网站加载还是像蜗牛一样,问题就出在图片资…

阅读更多 →
Python主观题自动阅卷系统:基于关键词与相似度的实战方案 2026/9/28 5:33:41

Python主观题自动阅卷系统:基于关键词与相似度的实战方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉