ysoseri.us

实习日志 / 中文 / 报案端前端 / 系统显示,模型说话:报案端从加功能到做收纳

系统显示,模型说话:报案端从加功能到做收纳

中文 | English

9 月 30 日,我在本地把匿名报案从头走了一遍。报案人的主线本来只有一条:用自己的话讲经过,Agent 追问缺的,随手核对记下的内容,最后提交。走下来,这条线断在几个地方:记下的信息客户看不到,进度又不准;一个"确认"按钮会把人直接推进收款;没登录就不能传照片;手机上对话被挤到了屏幕下半截。此外还有一串小毛病,比如 Enter 不发送、标题显示内部编号。

系统显示,模型说话

当时系统记下的信息放在右侧约 400px 宽的侧栏里,内容长,要上下滚动,客户在聊天时根本注意不到。客户刚说过的话在那里被标成 "Needs review",读起来像说错了什么。进度在标题下、侧栏顶部、概览卡里出现了三次,而且客户讲完经过和地点以后三处都还是 0,因为 AI 提议、尚未确认的事实不计入进度。为了让客户知道记下了什么,Agent 每一轮都把记下的值逐项复述一遍。复述占篇幅,也不可靠,模型转述的值和系统里存的值可能对不上。

改造方案定了一条原则:记下了什么、完成了多少,由界面按系统数据直接显示;模型只负责自然地聊天和追问。具体的形态是 Agent 回复下面的一张"刚记下的"卡片:

Agent: Glad no one's hurt and the car still drives. Did the other driver accept fault?
┌──────────────────────────────────────────┐
│ Just noted                  5 of 7 noted · 2 to check │
│  · Location: Queen St, at the lights      │
│  · Vehicle: drivable                      │
│  · Other vehicle: yes                     │
│  [ Looks right ]   [ Edit ]               │
└──────────────────────────────────────────┘

卡片只显示这一轮新增或变更的字段。右上角是界面上唯一的进度,客户说了但还没确认的事实也算进去,同时注明待核对的数量。"Looks right" 一次确认整张卡,复用现有的 POST /claims/{id}/form/confirmations;客户不理卡片、直接接着聊也可以,下一张卡出现后旧卡收成一行"3 项待核对"。客户自己说的内容,状态叫 "To check",不叫 "Needs review"。

Agent 那边的要求随之变成:两句以内,最多一个问题,不复述,目标 240 字符。硬上限仍然是 500,因为 OpenAI 的严格模式不支持 maxLength,设成 240 会让超长回复整轮失败。

同一个 PR(#1160)还把答题卡固定了下来。所有字段共用一种格子,每个注册表版本一份;本轮不允许的字段或形状不对的值,单独丢掉并记录,其余照常生效,不再整轮失败。按仓库的估算方法,首轮车险请求的答题卡从 4,508 token 降到 2,069 token。

用 10 个案例(追尾、停车场被撞、冰雹、水管爆裂、厨房起火、手机丢失等)各跑 2 轮做对比:

改前第一次提交最终版本
失败轮数0 / 200 / 200 / 20
回复长度中位数(字符)17294108
复述比例0.320.070.14
客套话收尾200
耗时中位数(秒)3.443.633.07
记下的字段总数615465

复述比例按"客户原话的三词短语在回复中出现的比例"算。第一次提交回复短了,字段却少了约 12%。原因是答题卡固定以后,v8 模型唯一能看到枚举可选值的地方没了:本以为 field_value_contracts 在上下文里,实际只在 v7 的路径上。模型猜错的枚举值,被新的"丢弃不合规字段"规则丢掉了。修法是在上下文里加 writable_fields,列出本轮可写的字段,按类型分组,枚举字段附上可选值,和校验用同一份字段合同,字段数回到 65。判断"模型看得到什么",要读实际发出去的请求,不能读相邻路径的代码。

点确认,就要银行账号

原来的流程里,客户在侧栏点一下"确认",字段一齐,前端就在对话里插入收款卡片,看上去就是刚确认完,AI 立刻开始要银行账号。收款这一步本身也多余。收款账户、联系方式、保单、车辆和房产,个人资料里都有(/account/profile、/account/policies、/account/payment-destinations、/account/assets),也有"报案时自动带入"的偏好,报案流程却要人在 Step 3 再选一遍。

提交路径改成:字段齐了,对话里出现一张"准备提交"卡片,点开是一页核对:事故摘要、材料清单、收款账户(默认取资料里的默认账户,显示成 "ANZ ••••1234 · Change")、联系方式、同意条款、提交按钮。条款的勾选框在客户打开条款之前不能勾,提示 "Open the terms to continue"。提交按钮依次调用现有的三个接口:保存收款与联系方式、同意条款、创建理赔。后端的步骤划分不变,前端不再把它们拆成单独的两步,Agent 也不再主动推进收款。

未登录的人点"查看并提交",先弹登录或注册,登录后用现有的 /claims/{id}/promote 把理赔单转到账号下,回到同一张核对页。

没登录也能拍照

匿名报案是仿照 ChatGPT 的做法设计的,给刚出事、着急的人用,不用先注册就能开始讲。可传照片被前后端各拦了一道:前端没有登录令牌就直接报 "Sign in before uploading a file",后端的 require_durable_claimant 也拒绝匿名会话上传内容。结果是客户最需要拍照的时候被要求登录。仓库自己的前端质量标准里其实写过,文件持久化需要认证,但匿名上传可以在当前 session 里处理,只要讲清楚它的生命周期。

所以匿名时的文件,我定的做法是放进一个和匿名 session 绑定的暂存区,session 不过期,文件就一直在。键就是浏览器本地保存的 X-Northwind-Anonymous-Session,同一个 session 下的理赔单都能引用暂存区里的文件,Agent 也能读这些照片。登录时 promote 一并把被引用的文件转成正式材料,没被引用的留在暂存区。

限额是单个文件 10MB,每个匿名 session 最多 20 个文件、共 100MB,防止被拿来当网盘。超出时提示 "This report already holds 20 files. Sign in to add more." 客户清掉浏览器数据或换设备,就拿不回这个 session,这和匿名对话本身的行为一致。后端要改的是把 require_durable_claimant 的适用范围收窄到账号级材料,新增以 session 为键的暂存存储,复用现有的对象存储和图片就绪流程。这一块涉及身份和隐私,单独做,失败路径和测试按高风险类别要求补齐。

模型选择器只说产品名

输入框里的下拉框原来显示 openrouter-gpt4o… 这类配置代码。选择器保留,但只显示产品名:

  • Northwind Assist:主力云端模型。"Fast. Reads your photos and documents."
  • Northwind Private:本地部署的 Qwen。"Text only. Your details stay on Northwind's own servers."

Private 必须写明只处理文字,因为 qwen-local 的配置是 image_input: false、document_input: false,不写清楚,客户会以为 AI 看过了照片。

两个塞满的仓库

这些改动陆续落地以后,到 10 月 2 日,界面又出现了另一种问题:功能是一项一项加出来的,没有统一的摆放规则。聊天区什么都放,回复下面有表格式的事实卡、彩色回执、服务同意大卡片、流程提示卡,中间还会插入整张收款表单、资料带入面板、6 种系统事件卡,一屏里同时有七八种视觉形态。右侧面板也什么都放:待办、带进度条的概览卡、分组清单、下一步、文件、第三方服务入口。进度又在标题栏、事实卡右上角、面板概览卡里各出现了一次。

这一轮的性质是收纳:不删功能,只做归位、合并和减重。先定 8 条规则,后面每一项的去处都由规则推出来:

  1. 聊天只放对话。一条 Agent 回复是文字加至多一行附件条,附件条由小标签组成(事实、回执、服务、文件判断),默认收起,点开才显示细节。
  2. 要填写、要核对、要读长内容的,都去面板的置顶区,聊天里只留一个入口标签。
  3. 状态只在标题栏出现一处。进度、阶段、人工协助状态合成一个状态区。
  4. 系统事件是对话流里的一条分隔线加一句话。
  5. 面板是档案。默认关闭,常驻三区依次是待办、已记下、文件;置顶区同一时刻只放一项。
  6. 只用一种强调色表示"需要你做",其余信息一律中性。
  7. 页头只放身份和出口:品牌、返回、一个求助入口。
  8. 工具页不挤进对话的中间区。

照这几条,"刚记下的"卡片从一张占半屏的表格,收成附件条里的一组事实标签:"Dominion Road · Today about 5

· No one hurt",有待核对的加一个小点,点开在原地展开成可改的精简列表。服务同意卡变成一个标签"Towing · needs your OK ›",点开在面板置顶显示完整的同意卡。条款引用做成可点的标签,点开在置顶区显示条款原文;回合记录里的 cited_clauses 只保留这一轮实际检索到的条款。"外部服务"整页删掉,内容进了面板。

这一轮先做成一个直接引用 token 库的 HTML 原型,按场景分帧定下形态,再落到代码里(#1184)。视觉数值照旧全部取自 frontend/shared/design-tokens.css,这一轮新增了 --claimant-chip-*、--claimant-title-bar-height、--claimant-rail-width、--claimant-case-sheet-half-height 等 token。

手机上只剩对话

手机上原来是整页滚动:页头、History / Chat / Details 三个标签、换成两行的内部编号标题、进度条依次堆下来,占掉屏幕上方约 40%,第一条消息在那之后,输入框的工具栏常常在首屏之外。想看记下的信息,要切到 Details 标签,也就是离开对话。响应式布局只在早期做过,后来的改动都没跟上。

手机上没地方放侧栏,对话里的事实标签就是主要的核对方式。宽度小于 640 时,对话区单独滚动,输入框固定在底部,留出 env(safe-area-inset-bottom) 的安全区,键盘弹出时跟着上移;标题栏收成一行:返回菜单、短标题、状态;面板改成底部抽屉,停在半屏或全屏两个高度,下拉关闭,有置顶项时自动停在半屏。验收的宽度是 390、640 和 980 两侧,以及 1366。

员工不在收集期间进来

收纳的时候有一个问题绕不开:客户途中提出的、Agent 答不了的问题,放哪?原来的做法是发起人工交接,员工进到会话里来。客户一多,员工端就会被大量临时问题淹没。

10 月 3 日我定的规则是:员工不在案件收集期间进入会话。途中 Agent 答不了的问题、需要专业判断的点、客户的一般联系请求,都记进待答清单,每项附客户原话和 Agent 的建议答复及依据,客户会被告知"提交后由理赔团队处理"。紧急安全照旧直通交接;客户明确而强烈地要求人工,或一般请求被暂缓后再次坚持,也直通。这个判断交给 Agent,写在提示词里,Runtime 不写死规则。客户端的求助按钮发出的是一条普通消息,同样由 Agent 判断。

员工接手案件时,先弹出待答清单,每项可以"同意并发出""修改后发出""驳回"(写原因),联系请求标为"已联系"。答复以负责人的身份出现在客户的对话里。后端的结构是:

northwind_questions:
  kind / question(客户口吻)/ claimant_wording / proposed_answer / basis / contact_preference

POST /workbench/claims/{id}/questions/{qid}/answer
  decision: approve | edit | decline | contacted

客户端只看到 question 和状态。立案后负责人发过言,Agent 就让路;有待答事项或客户回复的已立案案件,回到 processing 队列。

后来回到那条主线

9 月 30 日那一轮修的是断点:看不到的记录、半路冒出来的收款、不能拍照、挤在下半截的手机对话。10 月 2 日那一轮修的是堆积:每个功能单独看都合理,放在一起就把对话淹没了。两轮用的是同一个判断。客户的注意力在对话上,系统知道的事由界面准确地显示一次,模型只负责说话;凡是需要客户做点什么的,放到客户正在读的那条消息旁边,或者面板最上面那一格。