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 并直接执行高权限动作。交互越强,边界越要清楚。