问数时让模型选工具,为什么只让它选、不让它填参数?选错了怎么降级?
简化版
路由这一步,模型只输出它最擅长、也最容易被核对的东西:选哪个工具(编码)、问的是哪个对象(名称)、日期范围、以及改写后的完整问句。对象的主键、工具的具体入参由代码根据这些结果去查库、组装——模型没有机会传错主键或编造参数。返回后代码逐个字段校验:工具编码必须在启用清单里,对象名称必须能在库里定位到。选错了不判死:选了不存在或已停用的工具,换兜底工具重来一次;固定工具执行失败,也换兜底再跑一次;只降级一次,不循环重试,两次都失败就写明停在哪一步、为什么停。
详细版
一次路由调用,同时产出:
toolCode 选哪个工具(只能从启用清单里选)
subjectName 问的是哪个对象(名称,不是 ID)
startDate/endDate 问题里明确的日期范围,没有就为空
rewrittenQuestion 结合历史对话改写后的完整问句
代码逐项核对:
toolCode 在启用清单里? 否 → 换兜底工具
subjectName 库里能定位? 查不到 → 停「对象未找到」;查到多个 → 列候选让用户点选
日期 格式合法? 否 → 视为未填
→ 代码用对象 ID、日期组装入参,调用工具
→ 固定工具执行失败 → 换兜底工具再执行一次 → 仍失败 → 记录停在哪、为什么
| 谁负责 | 做什么 |
|---|---|
| 模型 | 理解意图、选工具、认出对象名称、消解指代、改写问句 |
| 代码 | 核对工具是否启用、名称换成 ID、组装入参、控制降级次数 |
完整版教学
一、为什么不让模型直接填参数
最直接的做法是把工具的入参定义(像 Function Calling 那样)交给模型,让它直接填 {"movieId": 1023, "startDate": "..."}。在数据分析场景里,这样做有几个风险:
主键编造:模型不知道「某部影片」在库里的 ID,只能猜一个数字,猜错了查到的是另一部影片
静默错误:错的 ID 照样能查出数据,不会报错,用户看到的是另一部影片的票房
参数越界:模型可能填出工具不支持的参数组合
名称和主键的区别在于可核对:模型说「影片叫某某」,代码拿名称去库里查,查不到、查到多个都能明确处理;模型说「ID 是 1023」,代码没法知道它是不是用户说的那一部。所以原则是:模型输出人类可读的名称,代码负责把名称变成主键。
记忆钩子:让模型给「名字」,由代码换「ID」。名字能核对,ID 只能信。
二、名称定位:查不到、查到多个都要有出路
名称换 ID 这一步要处理三种情况:
| 情况 | 处理 |
|---|---|
| 恰好一个 | 直接用 |
| 一个都没有 | 停在「对象未找到」,提示用户换个说法 |
| 多个(同名或模糊匹配到几个) | 停在「对象不唯一」,列出候选(附年份、类型等区分信息),用户点一个后带着 ID 重问 |
查找一般分两步:先按名称完全相等查,命中的就是用户说的那个,即使同名多个也让用户挑;查不到再模糊匹配,给「记不清全名」的情况兜底,候选数量要有上限。用户点选候选后,同一个问题带着对象 ID 重问一次,这次不再需要猜名称。
三、多轮追问:指代改写和选工具放在同一次调用里
用户第二轮说「那它的评分呢」,系统需要知道「它」是谁、「评分」用哪个工具。做法是把改写和选工具合在一次路由调用里:
输入:用户原话 + 最近几轮历史(问题与结论)+ 启用的工具清单
输出:改写后的完整问句 + 工具编码 + 对象名称 + 日期
一个细节:输入里的「当前问题」必须是用户原话,不能是改写后的问题——改写正是这次调用要产出的东西,不能拿它当输入。历史轮次要有上限(比如最近 3 轮),太长既费 Token 又容易让模型把早先的对象当成当前指代;没有历史时写「无」,比留空更不容易让模型误会参数漏传了。
四、降级策略:只降一次,按固定顺序
路由和执行都可能出错,降级顺序要写死:
第一次 执行模型选中的工具
选错或执行失败 → 换兜底工具(Text-to-SQL)再执行一次
兜底也失败 → 记录停在哪一步、两次各自失败的原因,结束
为什么不循环重试?固定工具答不上来,通常说明问题落在它的口径之外,同一个工具再试一遍结果还是一样,白白多花一次工具调用;换一个工具才是有效的第二次尝试。循环重试还会让耗时和成本不可控。对比一下「某个固定工具的 SQL 在这个问题上查不出数」时两种策略的结果:
同一工具重试 3 次:同样的入参、同样的 SQL 再执行 3 次 → 3 次都查不出数,问题原地不动
降级一次: 换兜底工具 → 模型按这个问题现写 1 条 SQL → 校验、执行
→ 查出数则结束;执行报错最多再纠错 1 次
固定工具的 SQL 是写死的,入参不变时重跑多少次结果都一样,重试只是在浪费时间;兜底这一次虽然要多调一次模型,却是换了一条真正不同的路。
还要区分一种情况:兜底工具本身被停用。这时系统确实答不了,提示应该面向管理员(「兜底工具已停用」),因为用户换什么问法都解决不了。
五、模型的每一个输出都要校验
路由结果是一段 JSON,返回后逐字段清洗和校验:
toolCode 去空格,必须能在当前启用清单里找到
subjectType 必须是系统认识的对象类型
subjectName 去掉书名号等修饰,只留名称本身
startDate/endDate 必须是合法日期格式,否则按未填处理
rewrittenQuestion 不能为空,否则回退为用户原话
这一层看似琐碎,却决定了系统是否稳定:模型偶尔会带多余符号、写错日期格式、选一个已经停用的工具,每一种都应该有确定的处理方式,而不是抛异常让整轮问答失败。
六、过程要可见
问数页面上,每一轮回答都应该展示:用了哪个工具、系统把问题理解成了什么(改写后的问句)、是否走了兜底;走了兜底的,可以展开看生成的 SQL、校验结果、纠错痕迹。用户看到「理解为:某某片最近 7 天的每日票房」,发现理解错了可以立即换说法,这比给一个看似正确的数字更有价值。
七、Function Calling 和这种路由方式的关系
这种路由本质上仍是工具调用,区别在于参数的来源:
| 标准 Function Calling | 选工具 + 代码填参数 | |
|---|---|---|
| 模型输出 | 工具名 + 完整参数 | 工具名 + 名称类信息 |
| 参数来源 | 模型生成 | 代码查库、组装 |
| 适合 | 参数模型能直接从问题里得到(城市名、关键词) | 参数需要查库才能确定(主键、内部编码) |
参数能从用户原话里直接抄出来的,交给模型填没问题;参数需要查库才能确定的,就应该交给代码。
八、常见误区与追问
- 误区:模型能力强,直接让它填对象 ID 就行。 模型不知道库里的 ID,猜错了能静默查出另一个对象的数据,应该让它给名称、由代码换 ID。
- 误区:模型选了不存在的工具,直接返回失败。 应该换兜底工具重来一次,只有兜底工具也不可用时才真正答不了。
- 误区:失败了就多重试几次同一个工具。 同一工具失败多半是问题超出口径,换工具才是有效重试,而且只降级一次。
- 误区:多轮对话先单独调一次改写,再调一次选工具。 改写和选工具可以合在一次调用里,输入用用户原话,省一次调用也避免两步理解不一致。
- 误区:名称模糊匹配到多个时,默认取第一个。 取错了是静默错误,应该列出候选让用户选。
- 追问:兜底工具被停用了怎么办? 这时确实答不了,提示面向管理员,而不是让用户换问法。
- 追问:历史轮次要带多少? 设上限(比如 3 轮),只带能进历史的有效轮次,太多会费 Token 并干扰指代判断。
九、加强记忆
问数路由让模型只输出能被核对的东西:工具编码、对象名称、日期、改写问句;对象 ID 和工具入参由代码查库组装,避免模型编造主键导致静默查错。返回后逐字段校验:工具必须在启用清单,名称查不到停「未找到」、查到多个列候选让用户点选。多轮指代改写和选工具合在一次调用,输入用用户原话,历史设上限。降级只一次:选错或执行失败就换兜底再跑一次,还失败就写明停在哪、为什么;兜底被停用时提示面向管理员。页面展示用了哪个工具、理解成了什么、是否走了兜底。