
在企业 NL2SQL 的落地里单轮问答的准确率往往不是最难的部分。真正决定它能不能被当成工具用的是第二轮之后——用户不会每次都说完整他会说那这个月呢按楼栋拆一下算了看看收缴率。这篇文章整理我们在多轮追问上的设计思路怎么表示对话状态、怎么做指代消解、怎么裁剪上下文、什么时候必须反问而不是硬答。文中 YAML 与伪代码均为示意用于说明结构不是任何产品的源码。一、多轮比单轮难在哪把用户真实说出来的追问归个类基本是三种追问类型典型说法单轮方案的失败方式指代型那这个月呢缺时间基准模型只能猜一个日期省略型按楼栋拆一下缺分组维度模型复述上一轮结果切换型算了看看收缴率继承了上一轮的对象与过滤条件两个问题混在一起算三种失败的共同根源是同一件事系统没有把上一轮已经确认了什么当成结构化数据保存下来而是把历史对话原文一整段塞回给模型让模型自己从自然语言里再理解一遍。自然语言里再理解一遍就意味着同样的信息第二次理解的结果可能和第一次不一样。二、核心设计显式槽位而不是拼接历史我们的做法是把对话状态从文本变成结构。每一轮结束后把这一轮已经确认的查询要素提取成槽位slots下一轮只带槽位不带原文。# 对话状态示意 session_id: s_20260928_001 turn: 3 slots: object: park_building_resource # 必须是语义层注册过的对象 metrics: [vacant_count] # 必须是语义层注册过的度量 dimensions: [building_no] filters: park_id: P001 status: VACANT time_range: 2026-08 confirmed: true # 口径是否已经与用户确认过 last_sql: SELECT ... # 上一轮真实执行的查询三个必须坚持的约束槽位里只能放语义层可识别的标识。对象的内部名、字段名、枚举值都来自语义层注册模型不能自己创造一个槽位。做不到这一点槽位化就退化成了另一种自由文本。每轮重新构造提示词输入 槽位快照 本轮问题 必要的规则而不是 N 轮对话原文。这直接决定了 token 成本是否随轮次线性增长。槽位快照可回放。有了结构化状态任何一轮都可以复现——这在排查为什么第四轮答错了时非常关键。三、指代消解把代词映射到槽位而不是让模型自由联想指代消解在多轮里的正确姿势不是语义分析而是最小补全只有当本轮问题缺少最小必需槽位时才用历史槽位回填用户在本轮显式给出的要素永远覆盖历史值。# 示意最小补全式回填 PRONOUN_MAP {它: object, 这个: object, 这些: dimensions, 那: time_range} def fill_slots(new_query, slots): draft parse(new_query) # 只解析本轮 for word, slot_key in PRONOUN_MAP.items(): if word in new_query and slot_key not in draft: draft[slot_key] slots.get(slot_key) # 只在缺失时回填 for key, val in draft.items(): if val is not None: slots[key] val # 显式值覆盖历史 return slots关键在最后两行回填是兜底覆盖是默认。如果用户说那这个月呢回填time_range其他槽位保持如果用户说按楼栋拆一下只看 1 号楼filters被显式替换而不追加——否则过滤条件会一轮轮累积最后查出来的结果和用户想的完全不是一回事。四、上下文裁剪全量历史是负债不是保险三种常见策略的对比策略token 成本稳定性可排查性全量历史原文拼接随轮次线性增长差旧口径残留、多意图混叠差无法定位是哪一轮带偏的最近 N 轮原文固定上限但会截断关键约束中N 取小了丢约束取大了仍有噪音中槽位化状态 最近一轮基本恒定好约束显式、互斥可校验好状态可回放我们选第三种。除了成本更重要的是可控性槽位是结构化的所以可以在构造提示词之前做校验——比如用户既要按楼栋分组、又要求明细不含楼栋这种冲突能在发模型之前就拦下来而不是等答案出来再发现不对。另外裁剪掉的是原文不是依据。上一轮真实执行的last_sql要保留因为用户追问这个数怎么来的时系统需要能指回具体那一次查询。五、话题切换检测与槽位重置多轮里最隐蔽的一类错误是把换了个问题当成继续上一个问题。用户说算了看看收缴率——对象从房源变成了账单如果槽位没重置park_id、building_no这些过滤条件会被错误继承。我们的判定规则很朴素按优先级依次检查本轮问题命中的语义层对象与当前槽位不一致 → 重置出现另外 / 换个 / 重新 / 不看这个了等切换意图词 → 重置本轮问题能独立成句对象、度量、时间都齐全且与当前槽位无交叠 → 倾向重置。重置不是清空会话而是清空继承关系历史提问仍可回溯但不参与本轮查询构造。六、澄清策略什么时候必须反问而不是硬答多轮能力成熟度的一个标志是知道什么时候不该答。我们把澄清分成三档触发条件动作例子有明确默认口径、且影响面小静默取默认在答案来源里标注所用口径未指定时间 → 取当月口径存在分支、且会影响结论显式确认给出可选口径让用户选在租是否含空置待租对象不唯一 / 时间缺失 / 指代无法解析必须反问一次只问一个关键项你说的这个指园区还是楼栋第三档最容易被做坏。两个反方向都要避免一是硬答猜一个对象就开始查返回一个看似合理的结果二是反问过度每一句都确认一次用户很快就烦了。我们的经验是一次只问一个最关键项并且把候选值列出来让用户点选而不是让用户重新描述一遍。确认过的口径要写进槽位的confirmed标记短时间内同类问题不再重复问。七、一条完整的追问链路用一个园区场景把上面几节串起来。用户连续四轮槽位变化如下轮次用户输入槽位变化系统动作1目前园区还有多少空置房源新建object房源metrics空置数time当月执行查询返回三层答案2按楼栋拆一下追加dimensionsbuilding_no复用其余槽位仅改分组3只看 1 号楼覆盖filters.building_no1而非追加复用过滤条件替换4这些房源里合同到期的有多少不重置object 仍为房源新增 metrics合同到期数跨对象取数走关联第 3 轮是覆盖而不是追加第 4 轮是新增度量而不是重置——这两个判断做错任意一个用户拿到的数就是错的。八、五个常见反模式全量历史塞进提示词。看起来最省事实际是成本与不稳定性的双输。多轮一长规则和旧约束互相干扰出错了也查不出是哪一轮带偏的。槽位用自然语言存。存成用户想看1号楼空置的房子下一轮还得再理解一遍——等于没做槽位化。把追问当重新提问。每轮都从零解析用户就得像第一次一样把条件说全多轮体验直接消失。没有话题重置。切换问题后旧过滤条件仍在生效这是最难被用户发现、也最容易导致错误决策的一类 Bug。反问过度。把澄清做成什么都问一遍用户体验比答错还差。九、小结多轮设计的检查清单检查项达标标准对话状态结构化槽位字段来自语义层注册提示词构造槽位快照 本轮问题不拼接 N 轮原文回填规则缺失才回填显式值覆盖历史话题切换有明确的重置判定规则冲突检测发模型之前做槽位冲突校验澄清策略三档分级一次只问一个关键项可回溯任一槽位快照可回放last_sql 可查如果说单轮问答证明的是模型能不能听懂一句话那多轮追问证明的是它能不能被当成工具用一整天。前者是能力演示后者才是工程。在多轮这件事上答得快永远排在答得准、答得可核对之后。文中产品截图取自演示环境案例数据仅作示意YAML 与伪代码用于说明设计结构非实际源码。欢迎在评论区交流你们在多轮追问与上下文管理上的做法与踩过的坑。