项目背景
这个项目是在一个已有能力的会话 Agent 基础上做的改造。原 Agent 覆盖了 App 内多类客服问题,具备多分类路由能力——不同问题类型对应不同回答框架。
问题出现在用户侧:用户在操作 App 时遇到错误弹窗或异常界面,最自然的求助方式不是打字描述,而是直接把截图发过来。而原 Agent 只处理纯文本,面对截图完全无从应对。
这不是边缘场景。截图往往是最有信息量的输入——比文字描述精准得多,也是用户在移动端的惯用方式。
核心问题拆解
做改造之前先厘清约束:
- 原路由框架不能动:已在线上稳定运行,覆盖多个业务分类,动它风险高、成本高
- 多轮对话要连续:用户发图后继续追问,Agent 需要记住图片语义
- IM 场景格式约束:禁 Markdown、长消息分条,用户都在手机上看
- 成本可控:视觉模型比文本模型贵一个数量级,每次图片输入都触发调用
这四个约束基本决定了方案的形状。
方案设计
架构选型:图片理解作为路由前置预处理节点
最直接的思路是"换一个支持多模态的大模型做所有事"。但这意味着重写整个路由框架,而且多模态模型的文本推理能力不一定优于专用文本模型。
最终选的是图片理解作为预处理节点(方案 A):
用户输入(文本 / 图片 / 文本+图片)
│
┌────┴──────────┐
有图片 无图片
│ │
▼ │
① vision_describe() │
视觉模型提取: │
- 问题描述 │
- 初判分类 │
- 建议解决方案 │
│ │
└────┬───────────┘
│ 纯文字路由上下文(+原始用户文字)
▼
② 现有多分类路由(不改动)
│
▼
③ 生成最终回复
核心逻辑:把图片转化为结构化文字描述,再交给现有路由。路由框架从它的角度看,输入永远是文字,无需任何改动。
vision_describe() 的输出是一个结构化 JSON:
{
"description": "截图显示账户绑定页,系统提示该手机号已被其他账户绑定",
"category": "account_bind_conflict",
"suggested_solution": "建议用户确认手机号是否有误,或联系客服解绑原账户"
}
三个字段各有用途:description 和 category 给路由器做判断依据,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 秒,成本减半
用户体验优化:图片收到后立即回复"正在分析截图,请稍候……",再异步执行视觉理解。用户感知到的是"已收到,处理中",而非等待。
成本优化策略
- 模型降级:
qwen-vl-plus替代qwen-vl-max,成本减半,先评估识别效果 - 结果缓存:对相同图片(MD5 哈希)缓存 vision 输出,同一截图多次发送不重复调用
- OCR 前置:截图若只需提取文字(纯错误码),走更便宜的 OCR 模型再路由
关键决策回顾
为什么不直接用多模态模型替换整个路由框架?
风险和边际价值的判断。现有路由框架已过线上验证,覆盖多个分类的回答逻辑;全部重写风险远大于在前面加一个预处理节点。而且 vision 模型的文字路由能力不一定比专用文本模型强,两者分工让各自发挥所长。
为什么历史不存图片?
Context 成本是累积问题——多轮之后,每次请求都带着所有历史图片,token 消耗快速失控。“图片只在当轮处理,语义以文字形式持久化"是成本可控的关键。
为什么要注入案例库?
通用视觉模型看到一张 App 弹窗截图,描述往往很宽泛。注入了案例库后,模型带着业务上下文识图,category 字段的准确率显著提升,路由才能准确分发。
结果
- 支持纯文本、纯图片、图片+文字三种输入形态,路由框架零改动
- 含图消息端到端成本约 ¥0.007/轮,10 万条/月规模下月成本约 ¥350
- 新增问题场景只需更新
cases.json,运营人员可自助维护,不依赖开发排期