结构化信息采集

Agent 发现缺失信息时,不只用文本提问,而是发出结构化采集事件,前端渲染按钮/表单/滑块,用户点击后写入结构化状态。比纯文本问答稳定得多。

#type / concept #status / evergreen #tech / ai #tech / dev / frontend

[!info] related notes

结构化信息采集

Agent 发现缺失信息时,不只用文本提问(“是否疼痛?”),而是发出结构化采集事件(ask_user_options),前端渲染按钮/表单/滑块,用户点击后写入结构化状态。这比纯文本问答稳定得多,因为:用户输入有明确语义、不需要 LLM 解析自然语言、直接写入 consultation_state。

传统做法 vs 结构化采集

传统:纯文本问答

AI:你的膝盖下坠感是否伴随疼痛?
用户:没有
AI:(需要从"没有"推断出 pain=none,可能推断错误)

问题:

  • “没有”指的是没有疼痛,还是没有肿胀,还是两个都没有?
  • 用户可能说”不疼""没有疼痛""还好""不太疼”,模型每次都要解析
  • 解析结果不确定,可能出错

结构化:选项式采集

AI:你的膝盖下坠感是否伴随疼痛?
    [没有疼痛] [轻微疼痛] [中等疼痛] [明显疼痛] [我不确定]

用户:(点击"轻微疼痛")

→ 系统直接写入 {"pain_level": "mild", "source": "user_option_click"}

优势:

  • 用户输入有明确语义,不需要 LLM 解析
  • 直接写入 consultation_state,100% 准确
  • 前端渲染一致,用户体验好

设计模式

1. Agent 发出采集事件

当 Agent 检测到 missing_slots 不为空时,发出 ask_user_options SSE 事件:

{
  "type": "ask_user_options",
  "payload": {
    "question": "你的膝盖下坠感是否伴随疼痛?",
    "field": "pain_level",
    "options": [
      {"label": "没有疼痛", "value": "none", "icon": "✅"},
      {"label": "轻微疼痛", "value": "mild", "icon": "😐"},
      {"label": "中等疼痛", "value": "moderate", "icon": "😣"},
      {"label": "明显疼痛", "value": "severe", "icon": "😰"}
    ],
    "allow_free_text": true,
    "free_text_placeholder": "请描述你的疼痛程度..."
  }
}

2. 前端渲染

前端根据事件类型渲染对应的 UI 组件:

payload 类型渲染方式
options(2~5 个)按钮组
options(6+ 个)下拉选择
scale(1~10)滑块
boolean是/否按钮
free_text_only输入框

3. 用户交互

用户可以:

  • 点击选项 — 直接发送结构化答案
  • 输入自由文本 — 同时记录结构化标记和原始文本
  • 跳过 — 标记为 “user_skipped”,不写入状态

4. 结构化答案回传

用户点击后,前端发送:

{
  "type": "structured_answer",
  "field": "pain_level",
  "value": "mild",
  "display_text": "轻微疼痛",
  "source": "user_option_click",
  "turn_id": "turn_0197..."
}

5. 状态写入

后端收到结构化答案后:

  1. 直接写入 consultation_state.pain_level = "mild"
  2. 更新 missing_slots(移除 “pain_level”)
  3. 更新 confidence.pain_level = 1.0(用户直接选择,置信度最高)
  4. 作为用户消息保存到 messages(可选,保留对话完整性)

SSE 事件协议扩展

在现有 SSE 事件类型基础上扩展:

事件类型说明新增
text_delta文本增量-
message_done消息完成-
extracted_info提取的结构化信息-
ask_user_options结构化采集请求
knowledge_reference知识库引用
consultation_state_patch状态增量更新
error错误-

前端实现

前端 useSSEProcessor.ts 可以把 ask_user_options 事件转成 assistant-ui 的自定义 message part:

// 伪代码
function MessagePartRenderer({ part }) {
  switch (part.type) {
    case 'text':
      return <Markdown>{part.text}</Markdown>;
    case 'ask_user_options':
      return (
        <div className="options-group">
          <p className="question">{part.question}</p>
          <div className="options">
            {part.options.map(opt => (
              <Button
                key={opt.value}
                onClick={() => sendStructuredAnswer(part.field, opt.value)}
              >
                {opt.icon} {opt.label}
              </Button>
            ))}
          </div>
          {part.allow_free_text && (
            <FreeTextInput
              placeholder={part.free_text_placeholder}
              onSubmit={(text) => sendFreeText(part.field, text)}
            />
          )}
        </div>
      );
  }
}

MCP Elicitation 的借鉴

MCP(Model Context Protocol)的 Elicitation 设计正是这个方向:服务端可以向客户端请求额外用户信息,支持表单模式和 URL 模式,客户端负责展示交互界面、让用户检查和取消。

虽然不一定完整接 MCP,但可以借鉴它的思想:

  • Agent 不直接假设用户会以什么格式回答
  • 采集请求是结构化的,不是自然语言
  • 客户端负责渲染,服务端负责语义

设计要点

要点说明
选项数量2~5 个最佳,超过 5 个用下拉
允许自由文本始终提供”其他”或自由输入入口
跳过能力用户可以跳过不回答
置信度选项点击 > 自由文本解析
保留原文自由文本同时记录原文和解析结果
渐进式不要一次问太多,每轮 1~2 个问题
创建于 2026/6/30 更新于 2026/7/15