Skip to main content
生成式UI允许Agent在您的应用中动态生成并渲染UI组件,创造丰富的交互体验。Agent能根据对话上下文触发图表、表单或卡片等组件的渲染。 AG-Kit提供以下主要方式实现生成式UI:
  1. 客户端工具 - 定义客户端工具的完整流程,包括名称、参数、处理器以及如何渲染客户端工具的运行过程
  2. 服务端工具 - 定义如何渲染服务端工具的运行过程
  3. 人工干预中断 - 当Agent暂停并请求用户输入时,定义恢复逻辑及如何渲染中断界面
详细功能请参阅前端操作人工干预 本指南重点介绍动态UI组件的构建与渲染。

生成式UI概念

在生成式UI中,出于安全考虑,Agent不会生成原始JSX代码,而是通过调用工具或发送结构化负载的中断来触发客户端预定义的渲染逻辑。客户端随后使用这些数据动态渲染React组件。 核心优势:
  • 基于Agent逻辑的动态界面
  • 支持表单和图表等交互元素

组件注册

useChat提供便捷方式定义渲染逻辑。

工具注册(客户端工具 & 服务端工具)

对于客户端工具,使用带render(仅展示)或renderAndWaitForResponse(需返回结果)的clientTool。它们接收{input}(后者额外接收submitToolResult)并返回JSX。 简单示例(仅展示):
import { clientTool } from '@ag-kit/ui-react';
import { z } from 'zod';

const myTool = clientTool({
  name: 'show_info',
  description: 'Show info card',
  parameters: z.object({ text: z.string() }),
  render: ({ input }) => <div>{input.text}</div>
});

// Pass to useChat: tools: [myTool]
对于服务端工具,使用带renderserverTool。它们接收{input}并返回JSX。 简单示例:
import { serverTool } from '@ag-kit/ui-react';
import { z } from 'zod';

const myTool = serverTool({
  name: 'show_info',
  parameters: z.object({ text: z.string() }),
  render: ({ input }) => <div>{input.text}</div>
});

中断注册(人工干预)

当Agent需要暂停并通过自定义UI获取用户输入时使用此方式。 API:在useChat选项中提供interrupt.renderWithResume函数。它接收{interrupt, resume}并返回JSX。调用resume(payload)可返回数据。 简单示例:
import { useChat } from '@ag-kit/ui-react';

useChat({
  interrupt: {
    renderWithResume({ interrupt, resume }) {
      return (
        <button onClick={() => resume('User confirmed')}>
          Confirm
        </button>
      );
    }
  }
});

渲染生成式UI

若已按上述方式注册渲染逻辑,UI将自动出现在消息流中。 在消息渲染代码中,对于类型为’tool-call’或’interrupt’的部分,只需调用part.render?.()即可显示组件。 示例:
import { useChat } from '@ag-kit/ui-react';

const { uiMessages } = useChat(/* omitted options */);

{uiMessages.map((m, i) => (
  <div key={i}>
    {m.parts.map((p, j) => {
      if (p.type === 'text') return <p>{p.text}</p>;
      if (p.render) return <div key={j}>{p.render()}</div>;
      // 处理其他类型
    })}
  </div>
))}
其余工作由useChat钩子自动处理。

示例:用客户端工具渲染图表

定义渲染图表的工具:
// 假设已有Chart组件
import { clientTool } from '@ag-kit/ui-react';
import { Chart } from '@/components/Chart'; // 您的图表组件
import { z } from 'zod';

const renderChart = clientTool({
  name: 'render_chart',
  description: 'Render a dynamic chart',
  parameters: z.object({
    title: z.string(),
    type: z.enum(['bar', 'line', 'pie']),
    data: z.array(z.object({
      label: z.string(),
      value: z.number()
    }))
  }),
  renderAndWaitForResponse: ({ input, submitToolResult }) => (
    <div>
      <h3>{input.title}</h3>
      <Chart type={input.type} data={input.data} />
      <button 
        onClick={() => submitToolResult('Chart viewed')}
      >
        Close
      </button>
    </div>
  ),
});
在聊天组件中:
import { useChat } from '@ag-kit/ui-react';

const { uiMessages } = useChat(/* omitted options */);

{uiMessages.map((m, i) => (
  <div key={i}>
    {m.parts.map((p, j) => {
      if (p.type === 'tool-call' && p.render) {
        return <div key={j}>{p.render()}</div>;
      }
      // 其他消息类型...
    })}
  </div>
))}
传入useChat:clientTools: [renderChart]

示例:渲染服务端工具

定义获取城市推荐购物中心的服务端工具:
import { serverTool } from '@ag-kit/ui-react';
import { z } from 'zod';

const getRecommendedShoppingMalls = serverTool({
  name: 'get_recommended_shopping_malls',
  parameters: z.object({ city: z.string() }),
  render: ({ input }) => <div>{input.city}</div>
});

示例:用中断渲染表单

定义渲染动态表单的中断处理器:
// 在useChat选项中
interrupt: {
  renderWithResume({ interrupt, resume }) {
    const { fields, title } = interrupt.payload; // 假设负载格式: { title: string, fields: Array&lt;{name: string, type: string}&gt; }
    
    const [formData, setFormData] = useState({});
    
    const handleSubmit = () => resume(formData);
    
    return (
      <div className="border p-4 rounded-lg">
        <h3>{title}</h3>
        {fields.map(field => (
          <div key={field.name}>
            <label>{field.name}</label>
            <input 
              type={field.type} 
              onChange={e => setFormData(d => ({...d, [field.name]: e.target.value}))}
              className="border p-2 w-full"
            />
          </div>
        ))}
        <button 
          onClick={handleSubmit}
          className="mt-2 px-4 py-2 bg-green-500 text-white rounded"
        >
          提交
        </button>
      </div>
    );
  },
},