项目背景

这个项目是在一个已有能力的会话 Agent 基础上做的改造。原 Agent 覆盖了 App 内多类客服问题,具备多分类路由能力——不同问题类型对应不同回答框架。

问题出现在用户侧:用户在操作 App 时遇到错误弹窗或异常界面,最自然的求助方式不是打字描述,而是直接把截图发过来。而原 Agent 只处理纯文本,面对截图完全无从应对。

这不是边缘场景。截图往往是最有信息量的输入——比文字描述精准得多,也是用户在移动端的惯用方式。

核心问题拆解

做改造之前先厘清约束:

  1. 原路由框架不能动:已在线上稳定运行,覆盖多个业务分类,动它风险高、成本高
  2. 多轮对话要连续:用户发图后继续追问,Agent 需要记住图片语义
  3. IM 场景格式约束:禁 Markdown、长消息分条,用户都在手机上看
  4. 成本可控:视觉模型比文本模型贵一个数量级,每次图片输入都触发调用

这四个约束基本决定了方案的形状。

方案设计

架构选型:图片理解作为路由前置预处理节点

最直接的思路是"换一个支持多模态的大模型做所有事"。但这意味着重写整个路由框架,而且多模态模型的文本推理能力不一定优于专用文本模型。

最终选的是图片理解作为预处理节点(方案 A)

用户输入(文本 / 图片 / 文本+图片)
    ┌────┴──────────┐
  有图片           无图片
    │                │
    ▼                │
① vision_describe()  │
  视觉模型提取:      │
  - 问题描述         │
  - 初判分类         │
  - 建议解决方案      │
    │                │
    └────┬───────────┘
         │ 纯文字路由上下文(+原始用户文字)
② 现有多分类路由(不改动)
③ 生成最终回复

核心逻辑:把图片转化为结构化文字描述,再交给现有路由。路由框架从它的角度看,输入永远是文字,无需任何改动。

vision_describe() 的输出是一个结构化 JSON:

{
  "description": "截图显示账户绑定页,系统提示该手机号已被其他账户绑定",
  "category": "account_bind_conflict",
  "suggested_solution": "建议用户确认手机号是否有误,或联系客服解绑原账户"
}

三个字段各有用途:descriptioncategory 给路由器做判断依据,suggested_solution 给回答框架提供参考上下文。

路由框架拿到预处理结果后,拼入上下文:

[图片描述] 截图显示账户绑定页,系统提示该手机号已被其他账户绑定
[初判分类] account_bind_conflict
[建议方案] 建议用户确认手机号是否有误,或联系客服解绑原账户
[用户原话] 这怎么办

案例库驱动识别

视觉模型不是"通用看图",而是带着领域知识看图

cases.json 存储了各类已知问题截图的描述和关键词,在 vision_describe() 的 system prompt 里注入这份知识,让模型在识图时具备业务上下文。对截图的识别准确率远高于通用提示。

新增场景只需在 cases.json 加一条记录,不改代码:

{
  "id": "account_bind_conflict",
  "page": "账户绑定页",
  "problem": "手机号已被绑定提示",
  "description": "页面提示该手机号已关联其他账户,无法重复绑定",
  "keywords": ["手机号", "已绑定", "账户冲突", "重复绑定"]
}

多轮对话的历史管理

这是实现中最容易踩坑的地方。

图片的 base64 数据体积很大,如果原样存进对话历史,多轮之后 context 会被撑爆,成本失控。

解法:历史里只存纯文字vision_describe() 的输出被转换成路由上下文字符串后,以 user message 的形式存入历史;原始图片数据不进历史。

历史记录(示例):
turn-1 user:      "[图片描述] 截图显示账户绑定页,手机号已被绑定\n[初判分类] account_bind_conflict..."
turn-1 assistant: "您好,截图显示该手机号已被其他账户绑定,建议先确认号码是否输入正确。"
turn-2 user:      "号码没错,是我之前的旧账号,怎么解绑?"
turn-2 assistant: "..."

用户在 turn-2 发文字追问时,turn-1 的图片语义已经以文字形式留在了历史里,追问可以自然接续,不需要再传图片。

成本与延迟分析

每轮含图消息的 token 消耗

每次用户发图,触发两次 API 调用:

调用 模型 输入 token 输出 token
vision_describe() qwen-vl-max ~1,300~1,800 ~100
路由回复 qwen-plus ~1,050 ~150

图片 token 规则:每 14×14 像素 = 1 token,手机截图经缩放后约 784~1,280 token。

成本估算

vision_describe()(qwen-vl-max):
  输入 1,600 token × ¥0.003/千  ≈ ¥0.0048
  输出   100 token × ¥0.009/千  ≈ ¥0.0009
  小计                          ≈ ¥0.006

路由回复(qwen-plus):
  输入 1,050 token × ¥0.0008/千 ≈ ¥0.00084
  输出   150 token × ¥0.002/千  ≈ ¥0.0003
  小计                          ≈ ¥0.001

每轮含图消息合计               ≈ ¥0.007(约7厘)
月消息量 含图比例 50% 月预估费用
1,000 条 500 条含图 ~¥3.5
10,000 条 5,000 条含图 ~¥35
100,000 条 50,000 条含图 ~¥350

延迟影响

vision_describe() 是串行节点,耗时直接叠加在原有链路上:

  • qwen-vl-max:TTFT 约 2~4 秒,完整 JSON 输出约 4~7 秒
  • qwen-vl-plus(备选):TTFT 约 1~2 秒,成本减半

用户体验优化:图片收到后立即回复"正在分析截图,请稍候……",再异步执行视觉理解。用户感知到的是"已收到,处理中",而非等待。

成本优化策略

  1. 模型降级qwen-vl-plus 替代 qwen-vl-max,成本减半,先评估识别效果
  2. 结果缓存:对相同图片(MD5 哈希)缓存 vision 输出,同一截图多次发送不重复调用
  3. OCR 前置:截图若只需提取文字(纯错误码),走更便宜的 OCR 模型再路由

关键决策回顾

为什么不直接用多模态模型替换整个路由框架?

风险和边际价值的判断。现有路由框架已过线上验证,覆盖多个分类的回答逻辑;全部重写风险远大于在前面加一个预处理节点。而且 vision 模型的文字路由能力不一定比专用文本模型强,两者分工让各自发挥所长。

为什么历史不存图片?

Context 成本是累积问题——多轮之后,每次请求都带着所有历史图片,token 消耗快速失控。“图片只在当轮处理,语义以文字形式持久化"是成本可控的关键。

为什么要注入案例库?

通用视觉模型看到一张 App 弹窗截图,描述往往很宽泛。注入了案例库后,模型带着业务上下文识图,category 字段的准确率显著提升,路由才能准确分发。

结果

  • 支持纯文本、纯图片、图片+文字三种输入形态,路由框架零改动
  • 含图消息端到端成本约 ¥0.007/轮,10 万条/月规模下月成本约 ¥350
  • 新增问题场景只需更新 cases.json,运营人员可自助维护,不依赖开发排期