知行札记
专题技术实践AI 工程实践

会话界面与流式状态

围绕任务身份、消息版本、内容安全和终态建立可靠的聊天与工具反馈界面。

消息是有身份与版本的内容

会话保存的是用户消息、模型结果、工具请求与工具回执。模型生成中的文本是当前消息的部分内容;消息完成后才能作为最终回答进入后续流程。用户编辑旧问题时会形成新的上下文分支,需要记录父消息或所用上下文版本。

界面可使用 idle → submitting → streaming → completed 的正常路径,以及 failed、cancelled 终态。网络断开只说明客户端失去连接,服务端任务可能仍在运行。用户恢复连接后用任务 ID 查询状态,避免再次点击提交制造第二次调用。重新生成回答应产生新消息版本,让用户知道比较的是哪个结果。

后端无关的组件库和绑定特定流协议的 hooks 处在不同层次。选择组件前先确认消息类型、工具事件和流协议是否适配已有服务。assistant-ui 概览 可作为组件与 runtime 的候选入口;使用 AI SDK UI 时按其消息协议接入后端。组件展示能力不能替代服务端任务治理。

流事件需要关联和终态

SSE、WebSocket 或 HTTP 流都可以承载模型事件。每个事件至少关联任务、消息及顺序;文本增量、工具调用增量、工具结果、用量和完成事件有各自类型。供应商事件可能按内容块或索引拆分,适配层先归并,再输出应用协议。

type StreamEvent =
  | { type: 'text'; taskId: string; messageId: string; seq: number; delta: string }
  | { type: 'tool-result'; taskId: string; callId: string; seq: number; result: unknown }
  | { type: 'done'; taskId: string; messageId: string; seq: number }
  | { type: 'error'; taskId: string; seq: number; code: string };

这个类型表达事件契约。恢复传输时,还需要服务端定义事件 ID 的保留期限、是否可以续传和重复事件去重方式。浏览器收到旧任务的晚到事件时,按任务 ID 更新对应消息;不要把它附加到用户刚开始的新回答中。

结构化输出在流中可能是部分 JSON。可以提供预览,最终消费以完成后的验证结果为准。工具参数也应在完整接收、解析与授权后执行。视觉上“参数已出现”不能触发写业务系统。

AI SDK 的服务端与 React 接线

以下为 AI SDK transport 接线示例,依赖 ai、@ai-sdk/react 与 React。服务端 route 函数适用于采用标准 Request/Response 的框架;鉴权、限流和消息大小校验在调用模型前完成。模型标识从服务端环境读取。

import { convertToModelMessages, streamText } from 'ai';

export async function POST(request: Request) {
  const { messages } = await request.json();
  // 此处应核对会话身份、消息形状、附件与总输入预算。
  const model = process.env.AI_MODEL;
  if (!model) throw new Error('AI_MODEL is required');
  const result = streamText({
    model,
    messages: await convertToModelMessages(messages),
    abortSignal: request.signal,
    onError: ({ error }) => {
      // 写入带 runId 的服务端错误记录,正文与凭据需脱敏。
      console.error(error instanceof Error ? error.name : 'stream_error');
    },
  });
  return result.toUIMessageStreamResponse();
}
'use client';
import { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';

export function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage, status, stop, error } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
  });
  const busy = status === 'submitted' || status === 'streaming';
  return (
    <section>
      {messages.map((message) => (
        <p key={message.id}>
          {message.parts.map((part) => part.type === 'text' ? part.text : '').join('')}
        </p>
      ))}
      <form onSubmit={(event) => {
        event.preventDefault();
        if (busy || !input.trim()) return;
        sendMessage({ text: input });
        setInput('');
      }}>
        <input value={input} onChange={(event) => setInput(event.target.value)} />
        <button disabled={busy || !input.trim()}>发送</button>
        {busy && <button type="button" onClick={() => stop()}>停止</button>}
      </form>
      {error && <p role="alert">请求未完成,请查看已保存的结果后重试。</p>}
    </section>
  );
}

这是文字路径的教学示例,未运行页面。真实应用需渲染工具和文件 parts,并处理审批、引用和失败。useChat 的 transport 与输入状态约定在不同大版本间有迁移成本;React 组件不能直接搬到其他框架。AI SDK useChat 参考。

显示内容保持清晰与受控

Markdown 渲染需要控制原始 HTML、链接协议与外部图片。来源链接带来源名称和定位,让用户能核对断言。工具结果中过长的原始数据可折叠,关键状态与业务回执放在可见位置。读取中、等待审批、执行中和结果未知分别表达;“完成”应对应外部效果已确认的任务。

自动滚动只在用户仍接近列表底部时进行。用户向上阅读时显示有新消息的提示,避免反复抢位置。重试按钮对应失败消息,停止按钮对应运行中的任务;界面无需同时堆叠含义相近的多个操作。

上传文件先显示解析状态、文件身份和可用范围。多模态预览可与模型实际收到的文件版本不同,必须绑定同一个资源 ID。删除会话、删除上传文件和撤销外部动作是不同操作,由各自的存储和权限边界处理。

取消和审批的结果落到状态里

“停止生成”要将取消信号传到服务端和供应商适配器。服务端仍可能已产生费用,已完成的工具动作也会保留。取消后查询尚未确认的动作回执,界面据此展示任务实际结果。

审批界面应呈现操作对象、具体参数、影响范围和审批有效期。审批通过后若参数改变,应用重新核对授权范围。让用户确认一句抽象的“允许 Agent 执行”无法表达具体业务授权。

验收可以覆盖同一任务重复提交、旧事件晚到、流中断、用户切换会话、编辑历史、工具审批超时和取消后的外部回执。本页列出需要验证的场景,本轮没有进行浏览器或实际应用观察。

工具审批进入同一消息流

审批事件携带工具调用 ID、工具名、经过验证的参数摘要、目标资源和有效期限。用户确认后,服务端重新核对当前身份、参数和资源版本,再恢复该调用。修改参数、换用户或资源状态发生变化时,原审批是否仍有效需由策略判断。

AI SDK 7 在调用或 agent 配置上用 toolApproval 声明审批策略,客户端通过审批响应恢复;v6 的工具级 needsApproval 在 v7 已弃用。assistant-ui 的工具界面和 CopilotKit 的人在环路能力提供呈现与传输原语。实际授权在服务器工具入口执行。MCP Apps 的工具 UI 还需 iframe 隔离和来源核对;AG-UI 描述 agent 与用户界面的事件,MCP 描述工具接入,两者不互相替代。AI SDK 7 审批迁移、AG-UI。

最后更新于

本页目录