petstore-docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md

15 KiB
Raw Blame History

架构决策:智能预约助手 M0 契约与数据模型

日期2026-08-02
状态:Accepted / Batch 2 implemented — petstore-backend 3c00065
WorkstreamCore Booking Flow
TrackArchitecture
PriorityP0(独立试验流,不高于 RC6 真实门店基线)
Owner AgentSystem Architect
Paired QACore Flow QA
Service Lockdocs:booking-agent-m0-contract+ontology
Architecture Review RequiredYes — completed by this ADR
Data Model Review RequiredYes — proposal and implementation diff completed
Access / Privacy Review RequiredYes — development boundary accepted; live provider and production review pending
RC IncludedNo

关联资产

1. 决策结论

M0 冻结为 「已登录 customer 在已选门店范围内,用文字或语音生成已验证预约草稿,再回填现有普通预约表单」

冻结边界:

  1. 门店不是模型槽位。宠主必须先在 CustAppointmentCreate 选定门店,创建会话时只提交 storeId,服务端验证门店存在。
  2. 模型只输出通过 booking-intent-v1 校验的非权威文本约束,不输出或决定业务 ID、价格、服务事实、号源或预约状态。
  3. 宠物、服务、日期时间和号源全部由 Petstore 后端在 current.userId + session.storeId 范围内重新解析。
  4. M0 没有 /confirm endpoint不调用 AppointmentService.createBooking,不修改 Appointment 表或状态机。
  5. 「带入普通预约」只返回一次页面级回填数据;最终仍由现有 /api/appointment/create 提交。
  6. 原始语音、当轮用户原文、模型请求/响应和完整对话不落库、不进业务事件、不进日志。
  7. 功能开关默认关闭,不进入 phase2-pilot-rc6

2. 为什么先选门店

原方案允许助手在对话内收集门店,但会同时引入门店名称歧义、地理推荐、跨店服务匹配和无门店 BusinessEvent 等问题。当前产品仍是单店 SaaS 试点,不需要把门店发现变成 Agent 能力。

M0 的次级入口仍位于 CustAppointmentCreate,行为调整为:

  1. 未登录:先登录并返回原预约页;
  2. 未选门店:先使用现有门店选择控件;
  3. 已选有效门店:创建智能会话;
  4. 会话内只解析该门店的服务与容量。

这样可以让每个 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 强校验顺序

  1. 响应体必须是单个 JSON object不接受 Markdown fence 或前后说明文字;
  2. JSON Schema 校验通过,未知字段拒绝;
  3. 文本 trim、控制字符过滤和长度校验
  4. 输出出现任意 ID、价格、手机号、号源或预约状态字段时整个响应降级
  5. 服务端时间解析、归属解析和号源查询成功后才更新 BookingDraft
  6. 失败不尝试从残缺 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 到期

不允许的状态:submittingbookedneeds_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 == customerboss/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.sqlpetstore-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 + 30minM0 不滑动续期
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 与清理

  1. 会话固定 30 分钟有效,不因消息滑动延长;
  2. endpoint 发现过期时返回 SESSION_EXPIRED,并以幂等方式标记 expired
  3. 每小时清理任务将到期活动会话标记 expired
  4. expires_at 早于当前时间 24 小时的 session 物理删除;
  5. 清理失败记录数量和错误类别,不记录 draft_json
  6. 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.customerUserIdactorRole=customersource=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-10 Base64 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 必填;
  • 去掉 deletedappointment_id
  • 增加固定 TTL、物理清理和 draft_version 并发规则。

实现 diff 复核通过。 BookingAgentSession JPA、迁移与 mapper 保持上述冻结字段和三个索引;六态与 input_modality 有数据库 CHECKowner 查询持悲观锁,未增加 appointment_iddeleted、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 实现证据

  • Backend3c00065 feat: add booking agent session orchestration
  • 五个 endpoint、JPA 会话、迁移、owner/store 数据边界、版本与 TTL 已实现;
  • 本体 BookingAgentSession、五个动作、三个事件、五条规则和十条关系已从 documented 更新为 anchored
  • 后端全量测试 292 项通过,本体端点 73/73、JPA 实体 17/17无漂移
  • 功能开关仍默认关闭,真实供应商 smoke、Batch 3 前端和生产开启仍未完成。