跳到主要内容

前后端通信协议

HTTP Agent 完全兼容 AG-UI 协议,使用 SSE 实现前后端实时流式通信。

可直接使用 AG-UI TypeScript SDK 的 HttpAgent 进行接入:

import { HttpAgent } from "@ag-ui/client";

const agent = new HttpAgent({
url: "https://your-endpoint/send-message",
headers: {
Authorization: "Bearer your-api-key",
},
});

POST /send-message

请求体

请求体遵循 AG-UI 协议的 RunAgentInput 结构:

interface RunAgentInput {
threadId: string; // 会话线程 ID
runId: string; // 本次运行的唯一 ID
parentRunId?: string; // 可选,父运行 ID(用于分支/回溯场景)
state?: any; // 可选,当前状态
messages: Message[]; // 消息历史
tools: Tool[]; // 客户端工具列表
context: Context[]; // 上下文信息
forwardedProps?: any; // 可选,透传属性
}

说明:

  • threadId 整个对话保持一致,服务端通过该 ID 维护会话状态
  • runId 每次调用生成新的唯一 ID
  • messages 消息列表

消息类型

type Message =
| DeveloperMessage
| SystemMessage
| AssistantMessage
| UserMessage
| ToolMessage;

interface DeveloperMessage {
id: string;
role: "developer";
content: string;
name?: string;
}

interface SystemMessage {
id: string;
role: "system";
content: string;
name?: string;
}

interface AssistantMessage {
id: string;
role: "assistant";
content?: string;
name?: string;
toolCalls?: ToolCall[];
}

interface UserMessage {
id: string;
role: "user";
content: string | InputContent[];
name?: string;
}

interface ToolMessage {
id: string;
role: "tool";
content: string;
toolCallId: string;
error?: string;
}

interface ToolCall {
id: string;
type: "function";
function: { name: string; arguments: string };
}

// 多模态输入内容
type InputContent =
| { type: "text"; text: string }
| { type: "binary"; mimeType: string; id?: string; url?: string; data?: string; filename?: string };

工具定义

interface Tool {
name: string;
description: string;
parameters: any; // JSON Schema
}

上下文定义

interface Context {
description: string;
value: string;
}

示例请求:

{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run_001",
"messages": [
{ "id": "msg_1", "role": "user", "content": "今天北京天气怎么样?" }
],
"tools": [
{
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
],
"context": [],
"state": {}
}

响应事件

响应为 SSE(Server-Sent Events) 流,格式如下:

  • 每个事件:data: {json}\n\n(JSON 后跟两个换行符)

AG-UI 协议定义了以下事件类型:

事件类型说明
RUN_STARTEDAgent 运行开始
RUN_FINISHEDAgent 运行结束
RUN_ERRORAgent 运行错误
TEXT_MESSAGE_START文本消息开始
TEXT_MESSAGE_CONTENT文本内容增量
TEXT_MESSAGE_END文本消息结束
TOOL_CALL_START工具调用开始
TOOL_CALL_ARGS工具参数增量
TOOL_CALL_END工具调用结束
TOOL_CALL_RESULT工具执行结果
STATE_SNAPSHOT状态快照
STATE_DELTA状态增量更新(JSON Patch)
MESSAGES_SNAPSHOT消息历史快照
STEP_STARTED步骤开始
STEP_FINISHED步骤结束

TOOL_CALL_ARGS 拼接规则:

工具参数以流式增量传输,客户端需将同一 toolCallId 的多个 TOOL_CALL_ARGS 事件的 delta 字段按顺序拼接,最终得到完整的 JSON 字符串。

// 事件流
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_1","delta":"{\"cit"}
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_1","delta":"y\":\"北"}
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_1","delta":"京\"}"}

// 拼接结果
"{\"city\":\"北京\"}" → JSON.parse() → { city: "北京" }

注意: 当存在多个并行工具调用时,不同 toolCallId 的事件可能交错出现,客户端应使用 toolCallId 区分和追踪每个工具调用。

事件结构

// 生命周期事件
interface RunStartedEvent {
type: "RUN_STARTED";
threadId: string;
runId: string;
parentRunId?: string;
timestamp?: number;
}

interface RunFinishedEvent {
type: "RUN_FINISHED";
threadId: string;
runId: string;
result?: any;
timestamp?: number;
}

interface RunErrorEvent {
type: "RUN_ERROR";
message: string;
code?: string;
timestamp?: number;
}

// 文本消息事件
interface TextMessageStartEvent {
type: "TEXT_MESSAGE_START";
messageId: string;
role: "developer" | "system" | "assistant" | "user";
}

interface TextMessageContentEvent {
type: "TEXT_MESSAGE_CONTENT";
messageId: string;
delta: string; // 非空文本增量
}

interface TextMessageEndEvent {
type: "TEXT_MESSAGE_END";
messageId: string;
}

// 工具调用事件
interface ToolCallStartEvent {
type: "TOOL_CALL_START";
toolCallId: string;
toolCallName: string;
parentMessageId?: string;
}

interface ToolCallArgsEvent {
type: "TOOL_CALL_ARGS";
toolCallId: string;
delta: string;
}

interface ToolCallEndEvent {
type: "TOOL_CALL_END";
toolCallId: string;
}

interface ToolCallResultEvent {
type: "TOOL_CALL_RESULT";
messageId: string;
toolCallId: string;
content: string;
}

// 状态管理事件
interface StateSnapshotEvent {
type: "STATE_SNAPSHOT";
snapshot: any;
}

interface StateDeltaEvent {
type: "STATE_DELTA";
delta: JsonPatchOperation[]; // RFC 6902 JSON Patch
}

interface MessagesSnapshotEvent {
type: "MESSAGES_SNAPSHOT";
messages: Message[];
}

// 步骤事件
interface StepStartedEvent {
type: "STEP_STARTED";
stepName: string;
}

interface StepFinishedEvent {
type: "STEP_FINISHED";
stepName: string;
}

响应示例

data: {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_1","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_1","delta":"让我查询一下"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_1"}
data: {"type":"TOOL_CALL_START","toolCallId":"call_123","toolCallName":"get_weather","parentMessageId":"msg_1"}
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_123","delta":"{\"city\":\"北京\"}"}
data: {"type":"TOOL_CALL_END","toolCallId":"call_123"}
data: {"type":"TOOL_CALL_RESULT","messageId":"msg_2","toolCallId":"call_123","content":"晴天,25°C"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_3","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_3","delta":"北京今天是晴天,温度25°C。"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_3"}
data: {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001"}

前端工具 vs 服务端工具

Agent 可以同时使用前端工具服务端工具

特征前端工具服务端工具
定义位置前端,通过 tools 参数传入服务端 Agent 内部
执行位置前端(浏览器)服务端
结果传递前端通过 ToolMessage 发回服务端返回 TOOL_CALL_RESULT 事件

前端判断逻辑:

  1. 收到 TOOL_CALL_END 后,检查工具名称是否在前端传入的 tools 列表中
  2. 如果在列表中:这是前端工具,等待 RUN_FINISHED,然后前端执行工具,将结果作为 ToolMessage 发送
  3. 如果不在列表中:这是服务端工具,等待 TOOL_CALL_RESULT 事件

时序图

基本文本响应

前端工具调用流程

前端通过 tools 参数传递工具定义。当 Agent 调用前端工具时,前端负责执行并将结果发回。

服务端工具调用流程

服务端工具由服务端定义和执行,前端无需处理工具执行逻辑。

人机交互(Human-in-the-Loop)

通过前端工具实现人机交互。当需要用户输入时,Agent 调用一个前端定义的工具,前端执行该工具时向用户展示 UI 并收集输入。

示例:确认工具定义

const confirmTool = {
name: "confirmAction",
description: "请求用户确认操作",
parameters: {
type: "object",
properties: {
action: { type: "string", description: "需要确认的操作" },
importance: { type: "string", enum: ["low", "medium", "high", "critical"] }
},
required: ["action"]
}
};

场景说明

场景 1:纯对话

请求:

{
"threadId": "thread_001",
"runId": "run_001",
"messages": [
{ "id": "msg_1", "role": "user", "content": "你好" }
],
"tools": [],
"context": []
}

响应:

data: {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_2","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_2","delta":"你好"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_2","delta":"!有什么可以帮你的吗?"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_2"}
data: {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001"}

场景 2:前端工具调用

前端定义了 search_local_files 工具(只有浏览器能访问本地文件)。

第一轮请求:

{
"threadId": "thread_003",
"runId": "run_003",
"messages": [
{ "id": "msg_1", "role": "user", "content": "帮我搜索本地的报告文件" }
],
"tools": [
{
"name": "search_local_files",
"description": "搜索用户本地文件",
"parameters": { "type": "object", "properties": { "keyword": { "type": "string" } } }
}
],
"context": []
}

第一轮响应:

data: {"type":"RUN_STARTED","threadId":"thread_003","runId":"run_003"}
data: {"type":"TOOL_CALL_START","toolCallId":"call_002","toolCallName":"search_local_files"}
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_002","delta":"{\"keyword\":\"报告\"}"}
data: {"type":"TOOL_CALL_END","toolCallId":"call_002"}
data: {"type":"RUN_FINISHED","threadId":"thread_003","runId":"run_003"}

前端处理:

  1. 从事件中收集 toolCalls{id: "call_002", type: "function", function: {name: "search_local_files", arguments: "{\"keyword\":\"报告\"}"}}
  2. 执行工具,获得结果
  3. 发送第二轮请求

第二轮请求:

{
"threadId": "thread_003",
"runId": "run_004",
"messages": [
{ "id": "msg_1", "role": "user", "content": "帮我搜索本地的报告文件" },
{
"id": "msg_2",
"role": "assistant",
"toolCalls": [
{ "id": "call_002", "type": "function", "function": { "name": "search_local_files", "arguments": "{\"keyword\":\"报告\"}" } }
]
},
{
"id": "msg_3",
"role": "tool",
"toolCallId": "call_002",
"content": "[\"2024年度报告.pdf\", \"Q3报告.docx\"]"
}
],
"tools": [...],
"context": []
}

第二轮响应:

data: {"type":"RUN_STARTED","threadId":"thread_003","runId":"run_004"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_4","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_4","delta":"找到了 2 个文件:2024年度报告.pdf 和 Q3报告.docx"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_4"}
data: {"type":"RUN_FINISHED","threadId":"thread_003","runId":"run_004"}

场景 3:服务端工具调用

服务端定义了 get_weather 工具,前端无需传递该工具。

请求:

{
"threadId": "thread_002",
"runId": "run_002",
"messages": [
{ "id": "msg_1", "role": "user", "content": "北京天气怎么样?" }
],
"tools": [],
"context": []
}

响应:

data: {"type":"RUN_STARTED","threadId":"thread_002","runId":"run_002"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_2","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_2","delta":"让我查一下"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_2"}
data: {"type":"TOOL_CALL_START","toolCallId":"call_001","toolCallName":"get_weather","parentMessageId":"msg_2"}
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_001","delta":"{\"city\":\"北京\"}"}
data: {"type":"TOOL_CALL_END","toolCallId":"call_001"}
data: {"type":"TOOL_CALL_RESULT","messageId":"msg_tool_1","toolCallId":"call_001","content":"晴天,25°C"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_3","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_3","delta":"北京今天晴天,25°C。"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_3"}
data: {"type":"RUN_FINISHED","threadId":"thread_002","runId":"run_002"}

说明:

  • 前端收到 TOOL_CALL_START,工具名 get_weather 不在前端 tools 列表中
  • 前端等待 TOOL_CALL_RESULT 事件,直接展示结果
  • 无需前端执行工具或发送额外请求

场景 4:人机交互(Human-in-the-Loop)

通过前端工具实现用户确认。

第一轮请求:

{
"threadId": "thread_004",
"runId": "run_005",
"messages": [
{ "id": "msg_1", "role": "user", "content": "删除所有临时文件" }
],
"tools": [
{
"name": "confirmAction",
"description": "请求用户确认危险操作",
"parameters": {
"type": "object",
"properties": {
"action": { "type": "string" },
"count": { "type": "number" }
},
"required": ["action"]
}
}
],
"context": []
}

第一轮响应:

data: {"type":"RUN_STARTED","threadId":"thread_004","runId":"run_005"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_2","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_2","delta":"即将删除 15 个临时文件"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_2"}
data: {"type":"TOOL_CALL_START","toolCallId":"call_003","toolCallName":"confirmAction","parentMessageId":"msg_2"}
data: {"type":"TOOL_CALL_ARGS","toolCallId":"call_003","delta":"{\"action\":\"删除临时文件\",\"count\":15}"}
data: {"type":"TOOL_CALL_END","toolCallId":"call_003"}
data: {"type":"RUN_FINISHED","threadId":"thread_004","runId":"run_005"}

客户端处理:

  1. 解析 confirmAction 工具调用参数
  2. 显示确认对话框
  3. 用户点击"确认"
  4. 发送包含工具结果的新请求

第二轮请求:

{
"threadId": "thread_004",
"runId": "run_006",
"messages": [
{ "id": "msg_1", "role": "user", "content": "删除所有临时文件" },
{
"id": "msg_2",
"role": "assistant",
"content": "即将删除 15 个临时文件",
"toolCalls": [
{ "id": "call_003", "type": "function", "function": { "name": "confirmAction", "arguments": "{\"action\":\"删除临时文件\",\"count\":15}" } }
]
},
{
"id": "msg_3",
"role": "tool",
"toolCallId": "call_003",
"content": "confirmed"
}
],
"tools": [...],
"context": []
}

第二轮响应:

data: {"type":"RUN_STARTED","threadId":"thread_004","runId":"run_006"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_4","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_4","delta":"已删除 15 个临时文件。"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_4"}
data: {"type":"RUN_FINISHED","threadId":"thread_004","runId":"run_006"}