15 KiB
架构决策:智能预约助手 M0 契约与数据模型
日期:2026-08-02
状态:Accepted / Batch 2 implemented — petstore-backend3c00065
Workstream:Core Booking Flow
Track:Architecture
Priority:P0(独立试验流,不高于 RC6 真实门店基线)
Owner Agent:System Architect
Paired QA:Core Flow QA
Service Lock:docs:booking-agent-m0-contract+ontology
Architecture Review Required:Yes — completed by this ADR
Data Model Review Required:Yes — proposal and implementation diff completed
Access / Privacy Review Required:Yes — development boundary accepted; live provider and production review pending
RC Included:No
关联资产
1. 决策结论
M0 冻结为 「已登录 customer 在已选门店范围内,用文字或语音生成已验证预约草稿,再回填现有普通预约表单」。
冻结边界:
- 门店不是模型槽位。宠主必须先在
CustAppointmentCreate选定门店,创建会话时只提交storeId,服务端验证门店存在。 - 模型只输出通过
booking-intent-v1校验的非权威文本约束,不输出或决定业务 ID、价格、服务事实、号源或预约状态。 - 宠物、服务、日期时间和号源全部由 Petstore 后端在
current.userId + session.storeId范围内重新解析。 - M0 没有
/confirmendpoint,不调用AppointmentService.createBooking,不修改Appointment表或状态机。 - 「带入普通预约」只返回一次页面级回填数据;最终仍由现有
/api/appointment/create提交。 - 原始语音、当轮用户原文、模型请求/响应和完整对话不落库、不进业务事件、不进日志。
- 功能开关默认关闭,不进入
phase2-pilot-rc6。
2. 为什么先选门店
原方案允许助手在对话内收集门店,但会同时引入门店名称歧义、地理推荐、跨店服务匹配和无门店 BusinessEvent 等问题。当前产品仍是单店 SaaS 试点,不需要把门店发现变成 Agent 能力。
M0 的次级入口仍位于 CustAppointmentCreate,行为调整为:
- 未登录:先登录并返回原预约页;
- 未选门店:先使用现有门店选择控件;
- 已选有效门店:创建智能会话;
- 会话内只解析该门店的服务与容量。
这样可以让每个 session 和漏斗事件都有明确 storeId,并阻止模型扩大门店数据范围。
3. booking-intent-v1 冻结
模型输出以 contracts/booking-intent-v1.schema.json 为唯一机器契约。
3.1 字段语义
| 字段 | 语义 | 是否业务权威 |
|---|---|---|
schemaVersion |
固定 booking-intent-v1 |
是,仅契约版本 |
intent |
book / modify / end / fallback |
否,状态机重新判定 |
petQuery |
宠物展示名搜索词 | 否 |
serviceQuery |
服务名称/同义表达搜索词 | 否 |
dateExpression |
用户日期表达 | 否 |
timeWindow |
HH:mm 起止偏好 |
否 |
remark |
用户希望带入预约的短备注 | 否,可编辑且限 200 字 |
clearFields |
用户明确要求清空的草稿字段 | 否,服务端只允许白名单字段 |
ambiguities |
模型观察到的歧义类型 | 否,服务端重新确认 |
nextAction |
下一步建议 | 否,编排器可忽略 |
3.2 强校验顺序
- 响应体必须是单个 JSON object,不接受 Markdown fence 或前后说明文字;
- JSON Schema 校验通过,未知字段拒绝;
- 文本 trim、控制字符过滤和长度校验;
- 输出出现任意 ID、价格、手机号、号源或预约状态字段时整个响应降级;
- 服务端时间解析、归属解析和号源查询成功后才更新
BookingDraft; - 失败不尝试从残缺 JSON 猜字段,返回
AGENT_UNAVAILABLE并保留旧草稿。
3.3 时间口径
- 时区固定
Asia/Shanghai; - 支持绝对日期、今天/明天/后天、本周 X/下周 X、精确时间、上午/下午、X 点以前/以后;
- 「过几天」「最近」「周末都行」等表达不得猜成绝对日期,必须返回日期选择项;
appointmentTime只有在现有容量服务返回真实 slot 后才能写入草稿;- 对外时间字符串统一为
yyyy-MM-dd'T'HH:mm:ss,语义为Asia/Shanghai本地时间。
4. M0 会话状态机
stateDiagram-v2
[*] --> collecting: create session
collecting --> collecting: 信息仍不足
collecting --> proposing: 可搜索真实号源
proposing --> collecting: 修改条件或需要澄清
proposing --> confirmable: 选择真实 slot
confirmable --> collecting: 用户修改草稿
collecting --> fallback: 用户回填表单或 LLM 降级
proposing --> fallback: 用户回填表单或 LLM 降级
confirmable --> fallback: 带入普通预约
collecting --> cancelled: 用户结束
proposing --> cancelled: 用户结束
confirmable --> cancelled: 用户结束
collecting --> expired: TTL 到期
proposing --> expired: TTL 到期
confirmable --> expired: TTL 到期
不允许的状态:submitting、booked、needs_reselection。终态 fallback / expired / cancelled 不接受新消息或转写。
每次消息请求必须带客户端当前 draftVersion。服务端只在版本相等时更新,成功后加一;版本冲突返回 DRAFT_VERSION_CONFLICT,不得静默覆盖。
5. API 冻结
唯一契约是 contracts/booking-agent-m0.openapi.yaml。M0 只注册五个 endpoint:
| Method | Endpoint | 结果 |
|---|---|---|
POST |
/api/booking-agent/sessions |
在已选 storeId 下创建 customer 会话 |
POST |
/api/booking-agent/sessions/{sessionId}/messages |
提交当轮已确认文字并刷新草稿 |
POST |
/api/booking-agent/sessions/{sessionId}/transcriptions |
转写短音频,文字返回输入框,不自动发送 |
POST |
/api/booking-agent/sessions/{sessionId}/fallback |
返回已验证的部分/完整草稿并结束会话 |
DELETE |
/api/booking-agent/sessions/{sessionId} |
结束本人会话 |
5.1 身份与资源隐藏
- 所有 endpoint 都必须经过
AuthInterceptor; - controller 再要求
current.role == customer,boss/staff 返回FORBIDDEN; - session 每次以
sessionId + current.userId查询;不存在和他人 session 统一返回SESSION_NOT_FOUND; storeId只在创建时接收并验证,后续从 session 读取;- 音频转写 endpoint 必须挂在 session 下,客户端不得上传宠物/服务词表。
5.2 响应语义
沿用 { code, message?, bizCode?, data? }。当前前端以 body code 判断业务结果;除 AuthInterceptor 的 HTTP 401 外,M0 不借机重构全局 HTTP status 行为。
草稿中的 storeId / petId / serviceTypeId / appointmentTime 是经过后端重新解析的事实;客户端回填后,现有预约创建服务仍会重新执行最终权限、服务和容量校验。
5.3 业务码
| body code | bizCode | 语义 |
|---|---|---|
| 401 | UNAUTHENTICATED |
session token 缺失或失效 |
| 403 | FORBIDDEN |
非 customer 调用 |
| 503 | AGENT_DISABLED |
功能开关关闭 |
| 404 | STORE_NOT_FOUND |
创建时目标门店无效 |
| 404 | SESSION_NOT_FOUND |
session 不存在或不属于当前 customer |
| 410 | SESSION_EXPIRED |
会话 TTL 到期 |
| 409 | SESSION_TERMINAL |
会话已 fallback/cancelled |
| 409 | DRAFT_VERSION_CONFLICT |
客户端草稿版本过旧 |
| 400 | INVALID_INPUT |
空文本、超长或请求结构无效 |
| 400 | INVALID_AUDIO |
空音频、格式/MIME/文件头不匹配或时长非法 |
| 413 | AUDIO_TOO_LARGE |
超过 3 MB |
| 429 | RATE_LIMITED |
customer 请求频率超限 |
| 503 | AGENT_UNAVAILABLE |
LLM 超时、无效 JSON/schema 或外部失败 |
| 503 | ASR_UNAVAILABLE |
ASR 超时或外部失败 |
宠物/服务歧义、无号源和未选时段是正常 200 会话响应,不用异常码表达。
6. 数据模型冻结
已实现迁移:backend/db/migrations/20260802_create_booking_agent_session.sql(petstore-backend 3c00065)。
6.1 t_booking_agent_session
| 字段 | 类型 | 规则 |
|---|---|---|
id |
BIGINT | 自增主键,只在后端和 BusinessEvent.aggregate_id 使用 |
session_id |
VARCHAR(36) | UUID,唯一,对客户端暴露 |
customer_user_id |
BIGINT | 必须是创建会话的 customer |
store_id |
BIGINT | M0 必填;创建时验证存在,后续不接受覆盖 |
status |
VARCHAR(24) | 仅六个 M0 状态 |
draft_json |
TEXT | 最小已验证草稿,不存当轮原文/模型响应 |
draft_version |
INT | 初始 0,每次成功草稿更新加一 |
entry_source |
VARCHAR(32) | M0 固定 appointment_create |
input_modality |
VARCHAR(16) | text / voice / mixed,首次成功消息后派生 |
expires_at |
DATETIME | 创建时固定 now + 30min,M0 不滑动续期 |
create_time / update_time |
DATETIME | 服务端时间 |
索引:
UNIQUE uk_booking_agent_session_public_id (session_id);INDEX idx_booking_agent_customer_status_expire (customer_user_id, status, expires_at);INDEX idx_booking_agent_status_expire (status, expires_at)。
不增加:
appointment_id;t_booking_agent_turn;- 原始语音/转写/提示词/模型响应列;
deleted。会话是短期对象,使用明确终态并按 TTL 物理清理。
6.2 TTL 与清理
- 会话固定 30 分钟有效,不因消息滑动延长;
- endpoint 发现过期时返回
SESSION_EXPIRED,并以幂等方式标记expired; - 每小时清理任务将到期活动会话标记
expired; expires_at早于当前时间 24 小时的 session 物理删除;- 清理失败记录数量和错误类别,不记录
draft_json; BusinessEvent是独立低敏事实,不随短期 session 删除。
7. 业务事件冻结
M0 只新增三个首次漏斗事实:
| 事件 | 触发 | 幂等键 | metadata 白名单 |
|---|---|---|---|
booking_agent_started |
session 创建成功 | booking_agent_started:{sessionDbId} |
entrySource |
booking_agent_draft_ready |
session 首次进入 confirmable |
booking_agent_draft_ready:{sessionDbId} |
inputModality |
booking_agent_fallback |
用户回填表单或 LLM 降级 | booking_agent_fallback:{sessionDbId} |
reason=user/llm_unavailable |
事件使用:
storeId=session.storeId;aggregateType=booking_agent_session;aggregateId=session.id;actorUserId=session.customerUserId、actorRole=customer、source=customer;- 已有
StoreCustomer时可关联,不能只为启动助手创建客户主档。
禁止把输入原文、转写文本、备注、宠物名、服务名、供应商 request ID 或错误原文写入 metadata。
M0 不写 booking_agent_confirmed,也不修改现有 appointment_created metadata。精确联结「助手草稿 -> 表单创建」需要另行设计服务端一次性交接机制。
8. 外部调用与隐私门禁
8.1 语音
- 单段不超过 60 秒、3 MB;客户端预检,服务端按字节、文件头、MIME 和解析时长复核;
- ASR 使用
qwen3-asr-flash-2026-02-10Base64 Data URL,不中转 OSS; - 上下文只包含当前门店服务名和当前 customer 的宠物展示名;
- 返回转写后立即释放音频字节,不缓存、不写文件、不返回供应商 request ID;
- 转写必须由用户编辑/确认后才能调用 messages。
8.2 LLM
- 使用固定
qwen-plus-2025-12-01、非思考、非流式、无工具、无联网; - LLM 超时 3 秒,ASR 超时 5 秒;不做多次自动重试;
- 请求只包含当次文字、最小草稿和枚举说明,不包含手机号、token、客户时间线或其他门店数据;
- API Key、Workspace ID 和 Authorization header 不进 Git、测试 fixture、聊天、截图或日志。
8.3 频率门禁
M0 默认每个 customer:
- 10 分钟内最多创建 5 个 session;
- 10 分钟内最多提交 30 条 message;
- 10 分钟内最多转写 10 段音频。
超限返回 RATE_LIMITED,普通预约继续可用。当前单实例可先使用进程内限流;进入多实例前必须切换共享限流状态。
9. Review 结论
Architecture Review
通过,可进入实现。 条件是严格保留五个 endpoint、无 /confirm、门店先选、普通表单独立可用、feature flag 默认关闭。
Data Model Review
通过提案,可由 Backend Core 实现迁移。 相比原 brief 的修订:
- 使用
id BIGINT内部主键 +session_id UUID外部唯一键; store_id从可空改为 M0 必填;- 去掉
deleted和appointment_id; - 增加固定 TTL、物理清理和
draft_version并发规则。
实现 diff 复核通过。 BookingAgentSession JPA、迁移与 mapper 保持上述冻结字段和三个索引;六态与 input_modality 有数据库 CHECK,owner 查询持悲观锁,未增加 appointment_id、deleted、turn 表或原文列。迁移测试、版本/TTL/清理测试均已落地。
Access / Privacy Review
开发边界通过,真实供应商 smoke 与生产开启未通过。 不需要真实密钥即可实现接口、stub、状态机和测试;真实调用前仍需完成供应商数据处理确认、小程序隐私告知、费用告警和已知情语音样本评测。
10. 后果与风险
正向结果:
- M0 不增加第二条预约写路径,模型失败不会污染
Appointment; - session 可短期恢复结构化草稿,又不形成对话原文数据库;
- OpenAPI、JSON Schema 和本体可以直接驱动测试与 review;
- 后续 M1 必须另立 ADR 才能新增 confirm/appointment 关联。
已知限制:
- 门店必须先选,无法用一句话同时完成「找店 + 预约」;
- M0 不精确统计回填后最终创建转化;
- 进程内限流只适用于当前单实例;
- 供应商效果仍需真实语料验证,当前推荐不是生产准入结论。
11. 实现门禁
编码 PR 必须同时满足:
- 引用本 ADR、OpenAPI、JSON Schema 和对应
ontologyRefs; - provider 单测只访问本地 HTTP stub;
- 后端测试证明整个 M0 不调用
AppointmentMapper.save; - 未配置密钥或 feature flag 关闭时应用正常启动、普通预约正常;
- 本体从
documented更新为anchored只能与代码和测试同一批完成; - 真实供应商 smoke、生产配置和 RC 纳入必须重新授权。
12. Batch 2 实现证据
- Backend:
3c00065 feat: add booking agent session orchestration; - 五个 endpoint、JPA 会话、迁移、owner/store 数据边界、版本与 TTL 已实现;
- 本体
BookingAgentSession、五个动作、三个事件、五条规则和十条关系已从documented更新为anchored; - 后端全量测试 292 项通过,本体端点 73/73、JPA 实体 17/17,无漂移;
- 功能开关仍默认关闭,真实供应商 smoke、Batch 3 前端和生产开启仍未完成。