266 lines
9.6 KiB
Markdown
266 lines
9.6 KiB
Markdown
# 多智能体协调 API 接口文档
|
||
|
||
后端路由:`offline-backend-20260512/backend/app/gateway/routers/multi_agent.py`
|
||
路由前缀:`/api/multi-agent`
|
||
|
||
## 概览
|
||
|
||
提供一个"总控 + 多个子智能体"的协调编排接口。
|
||
|
||
- **总控(leader)** 智能体:通过 `agent_orchestration` 技能给子智能体派活
|
||
- **子智能体(special)**:执行被派到的具体任务,完成后把交付内容广播给其他线程
|
||
|
||
调用方按下面顺序使用:
|
||
|
||
1. 调 `POST /api/multi-agent/init` 一次性创建协调智能体 + 给每个子智能体建 `thread`,拿到 `thread_ids` 映射
|
||
2. 客户端把 `thread_ids` 保管好,后续每次调 `POST /api/multi-agent/run/stream` 时回传
|
||
3. 先调用 `agent_type="leader"` 拿派活清单,再针对清单里每个子智能体调用 `agent_type="special"`
|
||
|
||
|
||
## 1. 初始化多智能体集群
|
||
|
||
### `POST /api/multi-agent/init`
|
||
|
||
创建协调智能体并为列表中的每个子智能体建立独立的 `thread`,同时把"职责说明"广播到所有子智能体线程作为初始上下文。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"agent_names": ["taiwan-intelligence-gathering-agent", "us-intelligence-gathering-agent"],
|
||
"main_agent_name": "main-controller",
|
||
"model": "deepseek-chat"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `agent_names` | `string[]` | 是 | 已有子智能体的 id 列表(kebab-case,区分大小写不敏感,自动转小写) |
|
||
| `main_agent_name` | `string` | 否 | 要创建的协调智能体名,默认 `main-agent`。重名时会复用已存在的同名智能体 |
|
||
| `model` | `string` | 否 | 协调智能体使用的模型名。不传则用后端默认模型 |
|
||
|
||
#### 响应(200)
|
||
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"main_agent_name": "main-controller",
|
||
"thread_ids": {
|
||
"main-controller": "thread-uuid-xxx",
|
||
"taiwan-intelligence-gathering-agent": "thread-uuid-yyy",
|
||
"us-intelligence-gathering-agent": "thread-uuid-zzz"
|
||
}
|
||
}
|
||
```
|
||
|
||
`thread_ids` 是一个 `{agent_name: thread_id}` 字典。**前端必须把这个对象完整保管下来**,后续调用 stream 接口需要原样回传。
|
||
|
||
#### 错误
|
||
|
||
| 状态码 | 触发条件 |
|
||
|---|---|
|
||
| `400` | `agent_names` 为空 |
|
||
| `401` | 未携带 `Authorization` 头 |
|
||
| `500` | 协调智能体创建失败且不是"已存在"错误 |
|
||
|
||
---
|
||
|
||
## 2. 流式调度
|
||
|
||
### `POST /api/multi-agent/run/stream`
|
||
|
||
通过 SSE(Server-Sent Events)流式驱动一个 leader 或 special 智能体跑一轮对话。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"agent_type": "leader",
|
||
"d_agent_thread_id": {
|
||
"main-controller": "thread-uuid-xxx",
|
||
"taiwan-intelligence-gathering-agent": "thread-uuid-yyy"
|
||
},
|
||
"agent_name": "main-controller",
|
||
"new_message": "请分析特朗普的支持率",
|
||
"skill_stop_names": [],
|
||
"model": "deepseek-chat"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `agent_type` | `"leader"` \| `"special"` | 否 | 默认 `leader`。`leader` 调用协调智能体,`special` 调用子智能体 |
|
||
| `d_agent_thread_id` | `object` | 是 | 由 `/init` 接口返回的 `thread_ids` 映射,原样回传 |
|
||
| `agent_name` | `string` | 是 | 本轮要驱动的智能体名(必须存在于 `d_agent_thread_id` 中) |
|
||
| `new_message` | `string` | 是 | 这轮发给智能体的用户消息 |
|
||
| `skill_stop_names` | `string[]` | 否 | 触发后中断执行的技能名列表(默认空) |
|
||
| `model` | `string` | 否 | 本轮 LLM 模型名,默认 `deepseek-chat` |
|
||
|
||
#### 响应
|
||
|
||
`Content-Type: text/event-stream` 的 SSE 长连接,按到达顺序产出多种数据:
|
||
|
||
##### a. 透传上游事件
|
||
|
||
原样转发底层 `POST /api/threads/{thread_id}/runs/stream` 的事件帧(LangGraph 协议),包含 `event:`、`data:` 等行。前端按 LangGraph SDK 既有方式渲染即可。
|
||
|
||
##### b. 最后一帧:调度状态
|
||
|
||
流结束前会额外产出一帧 `data:` 行,结构因 `agent_type` 而异:
|
||
|
||
**`agent_type == "leader"`:**
|
||
|
||
```
|
||
data: {"status": [["us-intelligence-gathering-agent", "收集美国民众对特朗普的看法"], ["taiwan-intelligence-gathering-agent", "..."]]}
|
||
```
|
||
|
||
`status` 是一个 `[agent_name, task_text]` 二元组数组,代表协调智能体本轮派出去的任务清单。**前端拿到后应当对每个元素再调用一次本接口(`agent_type="special"`)来执行子智能体**。如果数组为空,说明 leader 觉得已有信息足够,不再派活,可以直接展示 leader 的最终文本回答。
|
||
|
||
**`agent_type == "special"`:**
|
||
|
||
```
|
||
data: {"status": "子智能体xxx完成yyy工作"}
|
||
```
|
||
|
||
代表子智能体完成本轮工作并已把交付内容广播给其他线程。
|
||
|
||
#### 副作用(重要)
|
||
|
||
- **leader 调用**:自动把"总控智能体收到用户消息: …"广播到所有非自己的线程;派活后再把"总控智能体给 X 派活, 内容为: …"广播到所有非派活双方的线程
|
||
- **special 调用**:完成后把"子智能体 X 完成 Y 工作, 交付内容为: …"广播到所有非自己的线程
|
||
|
||
这意味着每个线程的对话历史里会逐步累积出全局上下文,无需前端再做合并。
|
||
|
||
#### 错误
|
||
|
||
| 状态码 | 触发条件 |
|
||
|---|---|
|
||
| `400` | `agent_name` 缺失或未在 `d_agent_thread_id` 中 |
|
||
| `401` | 未携带 `Authorization` 头 |
|
||
| `502` | 上游 `runs/stream` 返回非 200 / SSE 解析失败 |
|
||
|
||
---
|
||
|
||
## 3. 前端调用流程示例
|
||
|
||
```ts
|
||
// 1. 拿 token
|
||
const loginResp = await fetch("/api/v1/auth/login/username", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ username: "yourname" }),
|
||
});
|
||
const { access_token } = await loginResp.json();
|
||
const authHeaders = {
|
||
Authorization: `Bearer ${access_token}`,
|
||
"Content-Type": "application/json",
|
||
};
|
||
|
||
// 2. 初始化
|
||
const initResp = await fetch("/api/multi-agent/init", {
|
||
method: "POST",
|
||
headers: authHeaders,
|
||
body: JSON.stringify({
|
||
agent_names: [
|
||
"taiwan-intelligence-gathering-agent",
|
||
"us-intelligence-gathering-agent",
|
||
],
|
||
main_agent_name: "main-controller",
|
||
model: "deepseek-chat",
|
||
}),
|
||
});
|
||
const { thread_ids } = await initResp.json();
|
||
|
||
// 3. 流式调用 leader
|
||
async function streamLeader(message: string, model = "deepseek-chat") {
|
||
const resp = await fetch("/api/multi-agent/run/stream", {
|
||
method: "POST",
|
||
headers: authHeaders,
|
||
body: JSON.stringify({
|
||
agent_type: "leader",
|
||
d_agent_thread_id: thread_ids,
|
||
agent_name: "main-controller",
|
||
new_message: message,
|
||
skill_stop_names: [],
|
||
model,
|
||
}),
|
||
});
|
||
const reader = resp.body!.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buf = "";
|
||
let dispatched: [string, string][] = [];
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
buf += decoder.decode(value, { stream: true });
|
||
const lines = buf.split("\n");
|
||
buf = lines.pop() ?? "";
|
||
for (const line of lines) {
|
||
if (!line.startsWith("data:")) continue;
|
||
try {
|
||
const payload = JSON.parse(line.slice(5).trim());
|
||
if (Array.isArray(payload.status)) {
|
||
dispatched = payload.status;
|
||
}
|
||
// 其余事件按 LangGraph SDK 协议自行渲染
|
||
} catch {
|
||
/* 非 JSON 帧,可忽略或交给现有渲染器 */
|
||
}
|
||
}
|
||
}
|
||
return dispatched;
|
||
}
|
||
|
||
// 4. 对派活清单逐个调用 special
|
||
async function streamSpecial(agentName: string, task: string, model = "deepseek-chat") {
|
||
return fetch("/api/multi-agent/run/stream", {
|
||
method: "POST",
|
||
headers: authHeaders,
|
||
body: JSON.stringify({
|
||
agent_type: "special",
|
||
d_agent_thread_id: thread_ids,
|
||
agent_name: agentName,
|
||
new_message: task,
|
||
skill_stop_names: [],
|
||
model,
|
||
}),
|
||
});
|
||
// 同样按 SSE 解析
|
||
}
|
||
|
||
// 5. 编排:先 leader 派活 → 依次执行 special → 再让 leader 汇总
|
||
const dispatched = await streamLeader("请分析特朗普的支持率");
|
||
for (const [agentName, task] of dispatched) {
|
||
await streamSpecial(agentName, task);
|
||
}
|
||
const next = await streamLeader("根据上面收集到的信息出最终结论");
|
||
// 当 leader 返回 status: [] 时,意味着汇报完毕,可结束循环
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 注意事项
|
||
|
||
- **`model` 字段的取值**:必须是后端 `config.yaml` 中 `models[]` 已配置的模型名(如 `deepseek-chat`、`glm-4`、`gpt-4o` 等)。传错会导致上游 502
|
||
- **`thread_ids` 必须每次回传**:后端不存储这个映射,是无状态接口
|
||
- **协调智能体必须装有 `agent_orchestration` 技能**:`/init` 创建时会自动指定 `skills=["agent_orchestration"]`。后端 `skills/` 目录里必须存在该技能定义,否则 leader 无法派活
|
||
- **同名协调智能体冲突**:`main_agent_name` 重名时后端不会报错("already exists" 走兼容分支),会复用既有智能体——所以同一个用户多次 init 用同一个名字是安全的,但 SOUL 不会被覆盖
|
||
- **SSE 断连**:本接口底层 `on_disconnect: "continue"`,断连后上游 run 会继续跑到结束(不会取消)
|
||
- **并发广播**:广播消息使用 `asyncio.gather` 并发更新所有线程 state,性能上不会随子智能体数线性变慢
|
||
|
||
---
|
||
|
||
## 5. 相关接口(底层依赖)
|
||
|
||
本接口内部转发以下 Gateway 原生接口,了解细节可查:
|
||
|
||
| 接口 | 路由 | 用途 |
|
||
|---|---|---|
|
||
| 创建 thread | `POST /api/threads` | 为每个智能体建立独立会话 |
|
||
| 读 thread state | `GET /api/threads/{id}/state` | 广播前取当前消息列表 |
|
||
| 写 thread state | `POST /api/threads/{id}/state` | 把广播消息追加到 messages |
|
||
| 取 agent 详情 | `GET /api/agents/{id}` | init 时读取子智能体的 name/description |
|
||
| 建 agent | `POST /api/agents` | init 时创建协调智能体 |
|
||
| 流式 run | `POST /api/threads/{id}/runs/stream` | leader/special 的实际执行 |
|