把补参从模板里拿出来:Agent Clarify 改造成结构化 Tool Call
这次在改工业 AI 助手的编排层时,前面已经把 LangGraph + checkpoint 那层黑盒拆掉了,但 clarify 这件事还是不顺。问题不在“能不能让模型问用户要参数”,而在“这句补参话术到底应该由谁写”。
一开始很容易想到两条路:
- 继续把补参文案硬编码在工具 schema 里,比如
x-clarifyTemplate - 让模型自己在普通
content里判断这是补参还是最终回答
这两条路都能跑,但都不够稳。前者维护成本高,后者运行时语义太虚。最后收敛下来的做法是把 clarify 拆成两步:
- 第一步,模型通过原生 function calling 明确声明“缺了什么参数”
- 第二步,系统再让模型根据这些缺参元数据生成真正给用户看的澄清话术
这篇就只写这件事。
原来的 clarify 为什么不好维护
当前这类业务工具本身会带一些字段元数据,比如:
- 内部字段名:
fnCode - 中文业务名:
炉号 - 参数说明:需要用户明确指定哪台炉
- 有时还会带一些
x-*扩展字段
问题出在最后一类。
如果把 x-clarifyTemplate 当成长期方案,工具 schema 很快就会开始长成下面这样:
x-clarifyTemplatex-userLabelx-examplex-normalizationRule- 还有各种和 prompt 一样长的说明文字
这会带来三个直接问题。
第一,文案维护会散。
工具一多,澄清句子就会分散在各个 schema 里。后面你想统一语气、统一产品口径、统一“示例怎么写”,改起来会很烦。
第二,schema 会开始承载不该它承载的东西。
工具 schema 应该回答的是“参数长什么样、哪些必填、哪些可枚举”,不是“缺参数时这句话怎么说才更自然”。
第三,多轮上下文会越来越难处理。
同样缺 fnCode,如果用户上一轮已经提到“查今天的炉次情况”,和用户这一轮直接问“帮我查生产情况”,澄清文案不应该完全一样。模板很难把这种上下文差异处理好。
另一条路为什么也不够稳
另一种更激进的想法是:不用专门的 clarify 语义,直接让模型在普通 content 里输出一句补参话术。
这样表面上更省事,因为 loop 会很像这样:
1 | |
这里的问题不是功能做不到,而是系统很难稳定判断:
- 这是最终回答
- 还是一条待恢复的澄清问题
如果两者都走裸 content,运行时就得靠额外 prompt 约束或者字符串规则猜。这样一来:
- 事件语义会变糊
pending_action很难稳定落库- 前端也不知道当前这轮到底是 final 还是 clarification
- 以后做 resume、审计、统计都会变难
这和前面拆黑盒的目标是反着来的。
这次确定下来的结构
最后比较稳的做法其实很简单:让 clarify 变成一个显式的结构化信号,但不要让它负责写文案。
第一步:用一个本地 Tool 表达“缺参数”
给模型额外提供一个本地 tool,比如 request_clarification。
它不去调用 Java MCP,不查外部数据,也不拼用户话术。它只负责告诉运行时三件事:
- 当前不能继续查证据
- 缺了哪些参数
- 这些参数的业务名字是什么
结构大概像这样:
1 | |
这里最关键的是:
name给运行时用label给用户表达用description给 LLM 生成澄清文案时参考
这个 tool call 一出来,系统就知道这是 clarify,不会和 final 混掉。
第二步:再让模型专门生成一条澄清话术
真正给用户看的那句话,不再写死在 schema 里,而是用另一个很小的 prompt 现场生成。
输入内容可以很克制:
- 用户原问题
- 缺参列表
- 每个参数的中文业务名
- 当前用户可见的候选范围
- 几条写作约束
例如:
1 | |
模型输出:
1 | |
这句就是最后发给前端的 clarification_needed.content。
这套结构为什么更稳
我最后比较看重的是两个边界。
1. “缺了什么”必须结构化
这个判断不能再交给第二次 LLM 去猜。
缺参判断一旦不结构化,后面就会出现一串连锁问题:
- 这轮到底是不是 clarify
- 要不要写
pending_action - resume 时应该拿哪些字段合并用户补充输入
- 前端该不该渲染成补参态
所以第一层必须是机器可判定的结构。
2. “怎么问用户”可以交给 LLM
这部分反而适合交给模型做,因为它天然擅长:
- 把同一个参数换成更自然的业务表达
- 根据当前上下文调整话术
- 根据候选值给用户几个容易理解的示例
运行时不需要维护一堆模板,也不用把语言风格散落在工具 schema 里。
改完以后,loop 会变成什么样
编排会比现在干净很多。主循环只需要处理三种结果:
1 | |
这套 loop 跟 nanobot 那种原生 function calling 骨架已经很接近了,但又保留了工业助手需要的补参恢复语义。
这次我不想再让 schema 扛文案了
这轮最值钱的地方,不是“加了一个 clarify tool”,而是把责任重新切清楚了。
工具 schema 回到它该做的事:
- 描述参数
- 提供字段级业务标签
- 提供必要的约束和可选值
LLM 去做它更适合的事:
- 把结构化缺参信息转成自然对话
运行时负责把这两件事接起来:
- 明确记录 clarify 语义
- 明确保存 resume 所需状态
这样后面就算要换模型、换文风、换联调环境,也不会再去一堆 x-clarifyTemplate 里翻文案。
代价也得说清楚
这套方案不是没有代价。
第一,clarify 分支从“一次 LLM 调用”变成了“两次”。
第一次做 function calling 决策,第二次生成澄清文案。虽然第二次 prompt 很小,但它确实多了一次模型调用。
第二,对 provider 能力有要求。
模型必须稳定支持原生 function calling,否则这条链路会退化回 JSON 解析。
第三,字段级元数据还是要维护。
虽然不再维护整句模板,但 label/description/examples 这些基础业务元数据还是要保持干净,不然第二次生成的澄清话术也不会好。
这些代价都能接受,因为它们换回来的是更清楚的运行时边界和更低的长期维护成本。
收口
这次 clarify 的改造,真正让我满意的点不是“代码更短”,而是 finally 把三件事分开了:
- 模型判断需不需要继续查证据
- 运行时判断当前是不是 clarify
- 模型负责把缺参信息说成人话
这三件事混在一起时,代码会越来越像 prompt 工程。把它们拆开以后,编排才更像一个可以长期维护的系统。