AI 对话助手如何实现动态交互?一文讲清 AG-UI、SSE、组件与工具调用

先看产品:能提问、能选择、能继续办事的 AI 助手

用户在同一个聊天窗口里描述目标;助手缺信息时展示选择卡,拿到答案后继续查询;需要写入业务系统时,展示操作摘要并等待用户确认。

自行绘制的产品效果示意,参考常见聊天助手的消息气泡与会话内卡片布局;客户数量与操作均为教学样例。

产品目标: 让用户沿着一段对话完成"表达需求 → 补齐条件 → 获得结果 → 确认操作",每一步都能看见当前状态,并知道下一步需要自己做什么。

本文以虚构的信贷业务样例说明通用交互实现,界面和客户数量均为教学数据,不涉及真实客户信息。

文章目录

系统由前端、后端、大模型组成

核心不是后端猜用户想点哪个控件,而是:模型按已登记的工具 Schema 返回结构化调用;Agent Runtime 按工具名分流;前端用预先登记的组件渲染;用户答案再以协议消息交回运行时。

贯穿案例: 用户说"帮我筛选授信未用客户"。上下文里缺少机构,助手发起动态追问;用户选"城东支行";后端核验权限并查询;模型总结结果。

三层职责与消息主路径。HTTP 是传输,SSE 是流式承载,AG-UI 约定事件和消息语义。

前端

维护消息列表和组件注册表;发起运行;消费事件;收集点击、选择等用户输入。

后端 Agent Runtime

校验会话与权限;组合上下文和工具;调用模型;执行后端业务工具;保存和恢复运行状态。

大模型

读取上下文和工具 Schema,产生自然语言、工具调用或结构化参数。模型提出意图,应用决定是否允许执行。

三个关联键: threadId 标识对话;runId 标识一次运行;toolCallId 关联工具调用与对应结果。

AG-UI 是前端和 Agent 后端共同遵守的消息约定

AG-UI 全称 Agent--User Interaction Protocol。 它是开放、基于事件的应用交互协议,规定运行状态、文字、工具调用和用户回应的消息结构与处理语义。前端和 Agent 后端按同一份规范实现,就能正确交换这些信息。AG-UI 官方定义

把响应流里的一条消息拆开看

text 复制代码
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m-1","delta":"你好"}
│     └── AG-UI 事件 JSON:事件类型、消息编号、内容片段
└── SSE 的 data 字段前缀;一条事件以空行结束

data: 前缀是 SSE 的字段格式;后面的 JSON 是 AG-UI 定义的事件。这条事件要求前端:找到 m-1 号消息,把"你好"追加到它的文本里。

这段内容 属于哪一层 具体含义
POST /agent/run HTTP/1.1 200 OK HTTP 发起请求、返回成功响应;/agent/run 是本文自定义地址,AG-UI 不强制固定路由。
请求 JSON 中的 threadId / runId / messages / tools / context / forwardedProps AG-UI 的 RunAgentInput 标识对话、运行、输入消息和本轮可用的客户端工具。请求体也体现 AG-UI 约定。
Content-Type: text/event-stream data: ... 和空行 SSE HTTP 响应头声明事件流;响应正文用 SSE 字段与空行划分一条条事件。
{"type":"TEXT_MESSAGE_CONTENT", ...} AG-UI 事件 事件名、字段结构及处理规则由协议定义;m-1、实际文本等字段值由本次运行产生。

前端读懂 type,才知道怎样更新界面

AG-UI 事件 前端对应行为
RUN_STARTED 记录 threadId / runId,显示运行中状态。
TEXT_MESSAGE_START 按 messageId 创建助手消息气泡。
TEXT_MESSAGE_CONTENT 把 delta 追加到对应消息气泡;同一条消息可收到多次。
TEXT_MESSAGE_END 标记这条文本消息接收完毕。
RUN_FINISHED 标记本轮结束;若 outcome 是 interrupt,进入等待用户回应的状态。

AG-UI 约定结构与含义: 只有 SSE 的 data:,还不能表达"追加文字"或"调用工具";里面采用 AG-UI 的事件结构后,前端才可按标准事件分发。工具名 ask_user 和组件样式属于本产品的实现。

HTTP 负责请求响应;SSE 在响应中承载事件;AG-UI 定义事件内容。REST 是接口设计风格,可以与这套交互共存。AG-UI 事件规范 · 输入与消息类型

一次 HTTP 响应里,内容陆续到达

SSE 全称 Server-Sent Events,服务端发送事件。 服务器开始返回响应后保持连接,把事件逐条写入同一个响应体;前端边接收边更新界面。下面展示"你好,我能帮你做什么?"分成三段返回。实际发送间隔由生成与发送过程决定。

http 复制代码
POST /agent/run
Content-Type: application/json

{"threadId":"th-demo","runId":"run-1","messages":[{"id":"u-1","role":"user","content":"你好"}],"tools":[],"context":[],"forwardedProps":{}}

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"type":"RUN_STARTED","threadId":"th-demo","runId":"run-1"}

data: {"type":"TEXT_MESSAGE_START","messageId":"m-1","role":"assistant"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m-1","delta":"你好"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m-1","delta":",我能帮你"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"m-1","delta":"做什么?"}

data: {"type":"TEXT_MESSAGE_END","messageId":"m-1"}

data: {"type":"RUN_FINISHED","threadId":"th-demo","runId":"run-1"}

线上正文就是这些文本: 每条 data: 后面跟事件数据,以空行结束一条事件。代码块中的空行有实际作用。SSE 使用 UTF-8 文本,允许数据是普通文字或 JSON;本例数据采用 AG-UI 的 JSON 事件结构。

陆续收到的内容片段 前端累计显示
第一段 delta:你好 你好
第二段 delta:,我能帮你 你好,我能帮你
第三段 delta:做什么? 你好,我能帮你做什么?

前端消费顺序: 接收网络字节 → 流式解码文本 → SSE 解析器组装完整事件 → JSON 解析 → 按 AG-UI 的 type 分发 → 把 delta 追加到气泡。一次网络读取可能只有半条事件,也可能包含多条;不能把每次读取的内容直接当完整 JSON。生产实现优先使用 SDK 或成熟 SSE 解析器。

两个方向要说清: SSE 本身是服务端到浏览器的单向流。用户回答问题时,浏览器再发一个 HTTP POST;完整对话由这两种方向的消息共同完成。原生 EventSource 发起 GET;本文的 POST 流通常由 SDK 或 fetch + ReadableStream 消费。

服务端需要及时发送并避免中间层把事件攒到最后一起返回。SSE 的可选字段还包括 event、id、retry;这些 SSE 字段与 JSON 里的 AG-UI type / messageId 各有用途。SSE 格式与使用说明

前端怎样启动一轮对话

浏览器通过 HTTP POST 发给 Agent 接口。这里 tools 只放客户端提供的交互能力,例如 ask_user;查询客户的业务工具在后端单独注册,不能让浏览器直接调用数据库。

http 复制代码
POST /agent/run
Content-Type: application/json

{
  "threadId": "th-001",
  "runId": "run-001",
  "messages": [{"id":"m-1","role":"user","content":"帮我筛选授信未用客户"}],
  "tools": [{
    "name": "ask_user",
    "description": "缺少必需条件时向用户收集信息",
    "parameters": {
      "type": "object",
      "properties": {
        "field": {"type":"string","enum":["org_id","period"]},
        "question": {"type":"string"}
      },
      "required": ["field","question"]
    }
  }],
  "context": [],
  "forwardedProps": {}
}

Agent Runtime 把历史消息、系统规则和可用工具交给大模型。模型并不是回一句"请选机构"就让前端猜;它会按函数调用格式给出工具名和参数。

text 复制代码
模型服务响应(OpenAI 兼容格式,省略无关字段)
{
  "choices": [{
    "message": {
      "role": "assistant",
      "tool_calls": [{
        "id":"call-17", "type":"function",
        "function": {
          "name":"ask_user",
          "arguments":"{\"field\":\"org_id\",\"question\":\"请选择分析机构\"}"
        }
      }]
    },
    "finish_reason":"tool_calls"
  }]
}

为什么后端"死代码"也能灵活? 代码里预先实现的是工具注册、Schema 校验和分发规则;每轮"调用哪个工具、参数是什么"由模型按 Schema 生成。运行时按 name 精确匹配登记表,不靠搜索"确认""机构"等自然语言关键词。

但"缺什么业务条件"不能全靠模型猜:后端根据任务必填字段和现有上下文做确定性校验;模型负责用自然语言组织追问。若机构可选范围由登录人权限决定,模型不知道名单,后端权限服务才是来源。

协议传事件,前端组件负责显示

AG-UI 给的是工具、消息和事件结构,不会替产品提供授权卡片、下拉框的视觉样式。前端工程师预先实现组件,再登记"什么工具/字段对应什么组件"。模型只填受限参数;不允许它返回任意 HTML 或任意组件名。

结构化意图 前端预设组件 数据与权限由谁负责
field=org_id 机构下拉框 SelectField 前端调用带登录态的选项接口;后端按当前用户权限返回机构列表,并在提交后再校验。
field=period 单选、日期或范围选择器 控件类型和可选范围由字段配置决定;模型不提供真实业务选项。
confirm_action 操作摘要 + 同意/取消的 ApprovalCard 后端持有待执行 action;确认后仍由后端核验、记录审计并执行。
request_authorization AuthorizationCard / 连接账号按钮 后端生成 OAuth/SSO 授权地址并核验回调;按钮点击本身不代表授权完成。

前端注册表做三件事: ① 校验工具名和参数 Schema;② 将字段或工具名映射到已实现的组件;③ 组件提交时调用受控接口或把回答交回 Agent。遇到未知工具、未知字段或非法参数,应拒绝渲染/执行并显示安全错误。

下拉框还是单选,推荐由前端字段元数据决定,例如 org_id → SelectField。如果让模型给 ui,也只能从 select / radio 这样的枚举里选,前端仍要二次校验。问题文本可动态生成,组件种类和权限边界保持可控。

流式响应把工具调用拆成一组可识别事件

HTTP 仍然是浏览器到后端的请求协议;请求使用 POST,响应可以用 SSE 持续返回。AG-UI 规定每条事件的类型和字段。前端收到同一个 toolCallId 的参数片段后按顺序拼起来:

http 复制代码
Content-Type: text/event-stream

data: {"type":"RUN_STARTED","threadId":"th-001","runId":"run-001"}

data: {"type":"TOOL_CALL_START","toolCallId":"call-17","toolCallName":"ask_user"}

data: {"type":"TOOL_CALL_ARGS","toolCallId":"call-17","delta":"{\"field\":"}

data: {"type":"TOOL_CALL_ARGS","toolCallId":"call-17","delta":"\"org_id\",\"question\":\"请选择分析机构\"}"}

data: {"type":"TOOL_CALL_END","toolCallId":"call-17"}

data: {"type":"RUN_FINISHED","threadId":"th-001","runId":"run-001"}

TOOL_CALL_END 只说明参数片段传完了。前端解析 JSON、按注册表找到 ask_user,再根据 field=org_id 渲染下拉组件;它不表示用户已经作答。

用户选择后,前端把 ToolMessage 交回去

用户选了"城东支行",前端以同一 threadId、新 runId 再发一轮。下面只展示本轮 RunAgentInput 的核心字段;生产请求还会带客户端 tools、context 等字段,完整历史或服务端会话引用由具体集成决定。

http 复制代码
POST /agent/run
{
  "threadId": "th-001",
  "runId": "run-002",
  "messages": [
    {"id":"m-2","role":"assistant","toolCalls":[{"id":"call-17","type":"function","function":{"name":"ask_user","arguments":"{\"field\":\"org_id\",\"question\":\"请选择分析机构\"}"}}]},
    {"id":"m-3","role":"tool","toolCallId":"call-17","content":"{\"selectedId\":\"org_101\"}"}
  ]
}

后端收到后用登录会话重新校验 org_101 是否可访问,再把工具结果放进模型上下文。绝不能仅相信前端传回的 label 或模型生成的权限信息。

后端业务工具是另一条路线: 模型调用服务端注册的 query_unused_credit_customers → Runtime 按后端 allowlist 找 handler → 校验身份、机构、参数 → 调业务系统 → 结果以 tool message / ToolCallResult 关联原 toolCallId → 再调模型整理答案。

普通文字回答也是同一条 SSE 通道

http 复制代码
data: {"type":"TEXT_MESSAGE_START","messageId":"msg-42","role":"assistant"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-42","delta":"筛选完成,找到 18 位客户。"}

data: {"type":"TEXT_MESSAGE_END","messageId":"msg-42"}

前端按 messageId 将 delta 逐段追加到同一个聊天气泡;上面的工具事件与普通文本事件都走同一 SSE 流,事件类型决定该如何更新界面。

审批适合显式暂停与恢复

例如模型准备"创建授信跟进任务",这是后端写操作。Runtime 不应看到模型 tool_call 就立刻执行,而应先生成待审批 action,让 AG-UI 以 interrupt 结束本轮;前端根据 interrupt 的类型显示确认卡。

text 复制代码
响应流中的暂停事件(关键字段)
{
  "type": "RUN_FINISHED", "threadId": "th-009", "runId": "run-008",
  "outcome": {"type":"interrupt","interrupts":[{
    "id":"int-5", "reason":"tool_call", "toolCallId":"call-9",
    "message":"确认创建授信跟进任务?",
    "responseSchema":{"type":"object","properties":{"approved":{"type":"boolean"}},"required":["approved"]}
  }]}
}

用户确认后,前端在同一对话启动新 run,并通过 resume 带回 interruptId 和回答。后端校验结构与权限,再执行或拒绝该 action;执行结果回到工具调用链。普通追问可走上文的客户端工具 + ToolMessage;涉及人工审批时,interrupt/resume 能清楚表达"运行已暂停,等待人决定"。

http 复制代码
POST /agent/run
{"threadId":"th-009","runId":"run-009","resume":[
  {"interruptId":"int-5","status":"resolved","payload":{"approved":true}}
]}

动态从哪里来,哪些仍由工程师控制

动态内容 谁产生 / 谁校验
缺哪个字段、问题怎么措辞、调用什么工具 大模型按上下文和工具 Schema 产生结构化调用;Runtime 校验工具名与参数。
问题组件、下拉/单选/确认样式 前端组件库与字段注册表预先实现;模型只触发允许的组件能力,不生成 UI 代码。
机构列表、用户权限、是否允许执行 后端身份、权限与业务服务决定;每次请求重新校验。
等待、恢复、失败、重复提交 Agent Runtime 保存 thread/run/interrupt 状态;用幂等键、超时和错误事件处理边界情况。

可以这样概括: "我把交互建模为受 Schema 约束的工具调用。前端预先实现组件并注册客户端工具,AG-UI 把模型产生的 tool call 通过事件流传到前端;用户输入再以 ToolMessage 或 interrupt/resume 回到同一个 thread。Agent Runtime 负责模型循环、后端工具执行、权限校验和状态恢复。这样交互内容可动态变化,UI 和业务权限仍由工程规则控制。"

协议细节可核对:AG-UI Tools(客户端工具、参数 Schema、ToolMessage);AG-UI Events(SSE 事件、文本和工具调用事件);AG-UI Interrupts(暂停与 resume);AG-UI Core Types(RunAgentInput 与消息结构)。

相关推荐
丁希希哇1 小时前
Transformer中的注意力机制(Attention)
人工智能·机器学习·transformer·大语言模型
国际学术会议-杨老师1 小时前
2027年人文发展、城市管理与数字化社会国际会议(HUCD 2027)
大数据·人工智能·人文发展
田里的水稻1 小时前
PyTorch_torchvision---数据集下载\TensorBoard可视化
人工智能·pytorch·python·深度学习·机器学习
科技每日热闻1 小时前
电气设备综合维保服务商怎么选?从预防性维护到数字化运维,看懂五个关键能力——企业选择维保合作伙伴,不能只看“坏了能不能修”,更要看能否覆盖资产与系统的长期运营
大数据·运维·人工智能
Dotrust东信创智1 小时前
HIL台架上的“无人夜班“:东信创智用AI重塑汽车软件测试全链路——测试不再是项目的瓶颈,而是交付的底气
人工智能·汽车
电子制造自留地1 小时前
AI服务器高多层PCB设计:阻抗控制、层间对位与孔金属化工程要点
大数据·网络·人工智能·科技·pcb工艺
QYR-分析1 小时前
汽车车窗调节器市场研究:成熟零部件赛道,模块化集成成为核心方向
人工智能·汽车
星云低代码开发平台2 小时前
ERP 与 CRM 客户信用核对:业务键、时点和权限验收
大数据·数据库·人工智能
TF男孩2 小时前
老AI人面对MCP、Skills等新名词
人工智能·程序员