用 AG-UI 把 Bedrock AgentCore 代理变成可交互前端

2026-07-01 28 预计阅读时间: 1 分钟
来源: aws.amazon.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

AI 代理如果只会返回一段文本,很快会撞到产品边界:用户需要确认操作、编辑中间状态、查看结构化结果,甚至让代理临时生成一个小界面。AG-UI 的价值就在这里:它把代理运行过程中的状态、事件和 UI 意图变成前端可以消费的协议层。来源文章讨论了 AG-UI 如何接入 Amazon Bedrock AgentCore 的 Fullstack AgentCore Solution Template,也就是 FAST,并进一步用 CopilotKit 支持 generative UI、共享状态和 human-in-the-loop 交互。

AG-UI 解决的不是“聊天框好看一点”

在 AgentCore 这类代理运行环境里,后端代理往往负责推理、工具调用、状态推进;前端则负责把过程展示出来,并在关键节点收集人的输入。如果两边只靠一条自然语言消息传来传去,问题会很快变复杂:

  • 代理想展示一个表格、表单或确认卡片,前端不知道该渲染什么。
  • 用户改了 UI 状态,代理不知道哪些字段被改过。
  • 工具调用需要人工批准,后端只能暂停,前端却缺少标准事件来展示审批动作。

AG-UI 的定位是给这些交互一个协议形状。它不是简单替代聊天 API,而是让代理可以发出“状态变化”“需要用户确认”“渲染某个组件”这类事件。FAST 作为全栈模板,适合承载这种前后端协作:AgentCore 管代理运行,前端订阅 AG-UI 事件并渲染交互界面。

CopilotKit 补上的三块能力

来源摘要提到 CopilotKit 在 AG-UI 和 FAST 之上扩展了 generative UI、shared state 和 human-in-the-loop。可以把它们理解成三个产品能力,而不是三个装饰性功能。

Generative UI 让代理不只生成文本,还能生成界面意图。例如代理分析完云资源账单后,不只是说“这些实例可以优化”,而是让前端展示一个可勾选的建议列表。

Shared state 让前端和代理共享一个可追踪的工作状态。用户在界面里修改预算、区域或过滤条件,代理下一步推理时可以基于这些状态继续,而不是重新猜测用户上下文。

Human-in-the-loop 则把人工审批变成流程的一部分。比如代理准备提交变更、触发部署或调用高影响工具时,可以先发出一个等待确认的事件,前端展示批准和拒绝按钮,用户的选择再回流到代理。

可以这样实践:先用本地 AG-UI 事件流打通界面

下面是一个最小可改造示例。它不声称是 FAST 或 CopilotKit 的完整实现,而是演示一个常见落地方式:后端输出 AG-UI 风格事件,前端根据事件渲染生成式 UI,并把人工确认结果发回后端。接入真实 AgentCore/FAST 时,可以把 /api/agent 替换为你的 AgentCore 代理入口,把事件结构对齐到项目采用的 AG-UI 定义。

先创建一个小项目:

npm create vite@latest ag-ui-agent-demo -- --template react-ts
cd ag-ui-agent-demo
npm install
npm run dev

把下面的组件放到 src/App.tsx。运行前无需连接 AWS,它用本地模拟事件展示前端处理方式;真正接入时,把 mockAgentRun() 替换成读取后端流式响应的函数。

import { useState } from "react";
import "./App.css";

type AgentEvent =
  | { type: "message"; text: string }
  | { type: "state"; patch: { budget?: number; region?: string } }
  | {
      type: "ui";
      component: "approval-card";
      props: { title: string; summary: string; actionId: string };
    };

type SharedState = {
  budget: number;
  region: string;
  approvedActions: string[];
};

async function mockAgentRun(onEvent: (event: AgentEvent) => void) {
  onEvent({ type: "message", text: "正在分析当前部署计划。" });
  await new Promise((resolve) => setTimeout(resolve, 500));

  onEvent({ type: "state", patch: { budget: 1200, region: "us-east-1" } });
  await new Promise((resolve) => setTimeout(resolve, 500));

  onEvent({
    type: "ui",
    component: "approval-card",
    props: {
      title: "需要人工确认",
      summary: "代理建议在 us-east-1 创建一组新的 Bedrock AgentCore 资源。",
      actionId: "create-agentcore-resources"
    }
  });
}

export default function App() {
  const [messages, setMessages] = useState<string[]>([]);
  const [sharedState, setSharedState] = useState<SharedState>({
    budget: 0,
    region: "",
    approvedActions: []
  });
  const [approval, setApproval] = useState<AgentEvent | null>(null);

  async function runAgent() {
    setMessages([]);
    setApproval(null);

    await mockAgentRun((event) => {
      if (event.type === "message") {
        setMessages((items) => [...items, event.text]);
      }

      if (event.type === "state") {
        setSharedState((current) => ({ ...current, ...event.patch }));
      }

      if (event.type === "ui" && event.component === "approval-card") {
        setApproval(event);
      }
    });
  }

  function approve(actionId: string) {
    setSharedState((current) => ({
      ...current,
      approvedActions: [...current.approvedActions, actionId]
    }));
    setMessages((items) => [...items, `用户已批准:${actionId}`]);
    setApproval(null);
  }

  return (
    <main style={{ maxWidth: 760, margin: "40px auto", fontFamily: "system-ui" }}>
      <h1>AG-UI Agent Demo</h1>
      <button onClick={runAgent}>运行代理</button>

      <h2>共享状态</h2>
      <pre>{JSON.stringify(sharedState, null, 2)}</pre>

      <h2>代理消息</h2>
      {messages.map((message, index) => (
        <p key={index}>{message}</p>
      ))}

      {approval?.type === "ui" && (
        <section style={{ border: "1px solid #ccc", padding: 16, borderRadius: 8 }}>
          <h2>{approval.props.title}</h2>
          <p>{approval.props.summary}</p>
          <button onClick={() => approve(approval.props.actionId)}>批准</button>
        </section>
      )}
    </main>
  );
}

如果你的后端已经能返回流式事件,可以把模拟函数改成 fetch 读取 NDJSON。下面的写法适合改造为 AgentCore 前端适配层,重点是把每一行 JSON 事件交给同一个 onEvent 分发器:

async function runRemoteAgent(onEvent: (event: AgentEvent) => void) {
  const response = await fetch("/api/agent", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ input: "检查部署计划,并在需要时请求人工确认" })
  });

  if (!response.body) throw new Error("response body is empty");

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (line.trim()) onEvent(JSON.parse(line) as AgentEvent);
    }
  }
}

接入 FAST 和 AgentCore 时要盯住边界

真正部署到 Amazon Bedrock AgentCore 时,建议把协议、状态和权限边界分清楚。

协议边界:前端只认识 AG-UI 事件,不直接绑定某个代理内部实现。这样代理从“成本分析”换成“发布审批”,前端仍然可以复用消息、状态和确认组件。

状态边界:共享状态要有 schema。不要让代理随意写任意对象,否则前端会出现难以复现的渲染错误。对预算、区域、资源 ID、审批状态这类字段,应该明确类型和默认值。

权限边界:human-in-the-loop 不是弹一个按钮就结束。高影响动作需要记录谁批准、批准了什么、批准时的上下文是什么。尤其是云资源创建、删除、部署发布这类操作,审批事件应该进入审计链路。

采用建议

如果你已经在 Bedrock AgentCore 上构建代理,AG-UI 值得作为前端协议层尽早引入。它能让 FAST 这类全栈模板从“聊天页面”升级成“代理工作台”:代理负责推理和行动,前端负责状态、组件和人工决策。

落地时可以按三步走:先把代理输出标准化为事件流;再引入 CopilotKit 处理生成式 UI 和共享状态;等流程稳定后,把人工确认、审计和权限策略接到生产系统。不要一开始就让代理生成任意 UI 并直接执行高权限动作。交互越强,边界越要清楚。


相关推荐