文章目录
-
- 一、端云通信架构与选型
-
- [1.1 通信方式对比与适用场景](#1.1 通信方式对比与适用场景)
- [1.2 场景与通信架构映射](#1.2 场景与通信架构映射)
- [1.3 MQTT 在端云架构中的定位](#1.3 MQTT 在端云架构中的定位)
- [1.4 会话与消息的契约设计(REST)](#1.4 会话与消息的契约设计(REST))
- [1.5 建连与鉴权架构](#1.5 建连与鉴权架构)
- [1.6 协议选型决策表](#1.6 协议选型决策表)
- 二、智能体应用架构与场景映射
-
- [2.2 车机端云架构与具身智能架构](#2.2 车机端云架构与具身智能架构)
- [2.3 云端编排与端侧能力抽象(Tool 化)--- 以路径规划为例](#2.3 云端编排与端侧能力抽象(Tool 化)— 以路径规划为例)
- 三、架构与实现中的常见坑
-
- [3.1 协议与实现层](#3.1 协议与实现层)
- [3.2 多智能体编排层](#3.2 多智能体编排层)
- [3.3 端侧与网络边界](#3.3 端侧与网络边界)
- [3.4 流式与状态一致性](#3.4 流式与状态一致性)
- 四、架构落地的代码示例
-
- [4.1 云端服务层(Python)](#4.1 云端服务层(Python))
- [4.2 Web 端架构示例(React)](#4.2 Web 端架构示例(React))
- [4.3 移动端/车机端架构要点(Android)](#4.3 移动端/车机端架构要点(Android))
- [4.4 MQTT 端云架构示例](#4.4 MQTT 端云架构示例)
- [4.5 多智能体编排与端侧推送](#4.5 多智能体编排与端侧推送)
- [4.6 端侧能力 Tool 化架构(路径规划示例)](#4.6 端侧能力 Tool 化架构(路径规划示例))
- 五、多智能体架构下的端云协作设计
- 六、端云状态架构:同步、主动推送与流式传输
-
- [6.1 状态同步架构](#6.1 状态同步架构)
- [6.2 服务端主动推送架构](#6.2 服务端主动推送架构)
- [6.3 流式传输架构(TTS、占位符)](#6.3 流式传输架构(TTS、占位符))
- 七、架构与工程建议
-
- [7.1 既有 Agent 架构的接入方式](#7.1 既有 Agent 架构的接入方式)
- [7.2 架构参考清单与延伸阅读](#7.2 架构参考清单与延伸阅读)
- [7.3 异步与并发设计(Python)](#7.3 异步与并发设计(Python))
- 八、架构补充要点(协议选型、弱网、顺序、连接数、运维、安全)
-
- [8.1 协议选型决策架构](#8.1 协议选型决策架构)
- [8.2 弱网与离线架构策略](#8.2 弱网与离线架构策略)
- [8.3 消息顺序与去重架构](#8.3 消息顺序与去重架构)
- [8.4 连接规模与成本架构](#8.4 连接规模与成本架构)
- [8.5 可观测与运维架构](#8.5 可观测与运维架构)
- [8.6 MQTT 安全架构与规范](#8.6 MQTT 安全架构与规范)
- 九、架构小结与参考
- 十、附件:可运行代码
-
- [附件一:REST + SSE 流式后端(backend_sse_stream.py)](#附件一:REST + SSE 流式后端(backend_sse_stream.py))
- [附件二:Planner 调度端侧 Tool 云端后端(backend.py)](#附件二:Planner 调度端侧 Tool 云端后端(backend.py))
- 附件三:端侧模拟客户端(device_client.py)
面向读者:架构师、AI 全栈选手;需要做端云通信选型、多智能体编排与端侧能力抽象(如 React/安卓 + Python 智能体云服务),并能从架构到落地一条线打通的读者。
本文从通信架构、多智能体编排到端云落地 展开:先讲端云通信方式选型与契约设计(一),再讲智能体应用架构与场景映射(二)、云端编排与端侧能力 Tool 化(2.3),接着归纳架构与实现中的常见坑(三),给出架构落地的代码示例与文末可运行附件(四),然后是多智能体架构下的端云协作设计(五)、端云状态与流式传输(六),以及架构与工程建议(七)、架构补充要点(八),最后为小结与参考(九)及附件可运行代码(十)。与从 0 到 1 量产 Agent 落地:一份架构师视角的实践建议互补:该文偏架构决策清单,本文偏协议设计、多端对接与可复制代码。
端云智能体总体架构图(下图覆盖端侧形态、通信选型、鉴权与签名、多智能体编排与端侧 Tool 通道,后文各节与之对应):
云端服务
端侧
WebSocket / MQTT
WebSocket / MQTT
端侧能力 Tool 化
tool_call 下发
tool_result 回传
建连与安全
鉴权 token/session
签名/验签 可选
通信层(按场景选型)
REST 会话/消息
SSE 流式
WebSocket
MQTT
Web / React
Android / 车机
IoT 设备
会话与消息 API
Planner / 主智能体
Sub-Agent
- 端侧:Web(React)、Android/车机、IoT,统一通过 REST/SSE/WebSocket/MQTT 与云端交互。
- 通信层 :按场景选型(1.1、1.6);长连接在建连时经鉴权与可选签名(1.5)。
- 云端 :会话与消息 API(1.4)→ Planner 与 Sub-Agent 多智能体编排(二、五)→ 需端侧执行时经 端侧 Tool 通道 下发 tool_call、收 tool_result(2.3、4.6),车机/IoT 通过 WebSocket 或 MQTT 与云端保持长连。
一、端云通信架构与选型
建议按「请求-响应 vs 长连接 vs 异步任务」来选型。
1.1 通信方式对比与适用场景
| 方式 | 典型用途 | 优点 | 缺点/注意 |
|---|---|---|---|
| REST/HTTP | 创建会话、发消息、拉历史、配置 | 简单、无状态、易缓存、多端统一 | 长回复需轮询或配合 SSE/WS |
| SSE (Server-Sent Events) | 流式文本、进度事件、服务端主动推送(单向) | 单向流式、自动重连、基于 HTTP | 仅服务端→客户端;需考虑 proxy/超时 |
| WebSocket | 双向流式、实时对话、推送、多通道 | 双向、低延迟、可复用连接 | 连接管理、心跳、重连、部分网关限制 |
| 轮询 + REST | 异步任务结果(如 202 + task_id) | 实现简单、兼容性好 | 延迟与无效请求多,不适合强实时 |
| gRPC/HTTP2 | 高性能、多语言、流式(可选) | 性能好、强类型 | 浏览器需 grpc-web,安卓原生友好 |
| MQTT | 车机/IoT 指令与状态推送、弱网/离线友好 | 轻量、Pub/Sub、QoS、断线重连、省电省流量 | 浏览器需 MQTT over WS;后端需 Broker;需约定 topic 与 payload 规范 |
1.2 场景与通信架构映射
推荐方式
场景
流式回复
车机或IoT弱网离线推送
多智能体进度推送
简单请求响应
SSE 或 WebSocket
MQTT 或 WebSocket
WebSocket / SSE event / MQTT topic
REST
- 流式回复:SSE 或 WebSocket,前端逐 chunk 渲染。
- 车机/IoT 弱网、离线推送:MQTT 或 WebSocket;车机弱网还可配合短超时、重试与可选离线兜底。
- 多智能体进度推送:WebSocket、SSE event 分类型,或 MQTT 按 topic 分类型。
- 简单请求响应:REST(创建会话、发消息、拉历史、配置)。
1.3 MQTT 在端云架构中的定位
典型场景:车机、嵌入式、IoT。
- 机制:发布/订阅、QoS 0/1/2、Last Will、持久会话与离线消息。
- Topic 示例 :端订阅
user/{user_id}/agent/events或vehicle/{vin}/commands,后端智能体/多智能体某步完成后向对应 topic publish。 - 何时选 MQTT:多端/多设备、弱网、已有 MQTT 基础设施、车厂规范要求时,优先考虑;与 WebSocket 相比,MQTT 更省电省流量、Broker 解耦、便于多端订阅同一会话。
架构示意:
React Web
MQTT over WebSocket
Android 车机
MQTT Client
MQTT Broker
Python 智能体服务
1.4 会话与消息的契约设计(REST)
与延伸阅读《从 0 到 1 量产 Agent 落地:架构师视角的实践建议》第十三节对齐,便于多端共用。
- 创建会话 :
POST /v1/sessions,请求体{ "user_id": "xxx" },响应{ "session_id", "trace_id" }。 - 发送消息 :
POST /v1/sessions/{session_id}/messages,请求体{ "message", "options?" },响应{ "trace_id", "content", "status", "need_human?" }。 - 拉取历史 :
GET /v1/sessions/{session_id}/messages?limit=20&before=msg_id。
统一请求头/体建议:trace_id(可选,前端生成或由网关生成)、client_type(web / android_vehicle)便于后端限流与审计。
1.5 建连与鉴权架构
长连接(SSE、WebSocket、MQTT)必须在握手/建连阶段做鉴权,未通过则拒绝连接,避免未授权端占用连接或串号。
- WebSocket :建连时在 HTTP 升级请求中带
Authorization: Bearer <token>或 Cookie;服务端在accept前校验 token/session,无效则返回 401 并关闭连接。 - SSE:首包为 HTTP 请求,在请求头中带 token/session;服务端校验通过后再开始推送流,否则返回 401。
- MQTT:CONNECT 时带 username/password 或客户端证书;Broker 校验通过后再允许订阅/发布,并可按 topic 做细粒度权限。
- REST :无「握手」概念,每次请求在 Header 或 body 带鉴权信息即可;长连接则务必在建连时先鉴权再进入业务。
- 请求与消息签名(可选) :对敏感请求或消息体做签名 ,服务端验签 防篡改、防伪造。secret 为端与云端共享的密钥,可预置到设备、登录后下发或由 KMS 等安全下发,仅用于 HMAC 计算,不得泄露。常见做法:用 secret 对「方法+路径+body+timestamp」做 HMAC-SHA256,请求头带
X-Signature、X-Timestamp(或 nonce),服务端用同一密钥重算并比对;带 timestamp 可防重放(如 5 分钟内有效)。签名内容可包含 device_id 或 token(session_id) :参与 HMAC 计算后,请求即与设备/会话绑定,可防串号与跨设备重放。REST 在 Header 带签名(及X-Device-Id、X-Token等);WebSocket/MQTT 可在 payload 内带signature、device_id、token字段,云端/端侧收到后验签再处理。车机、开放环境或高安全场景建议启用。
REST 请求签名与验签示例(Python):
python
# 端侧:对 method+path+timestamp+device_id+token+body 做 HMAC,请求头带 X-Signature、X-Timestamp,可选 X-Device-Id、X-Token
import hmac
import hashlib
import time
def sign_request(secret: bytes, method: str, path: str, body: bytes, device_id: str = "", token: str = "") -> tuple[str, str]:
ts = str(int(time.time()))
raw = f"{method}\n{path}\n{ts}\n{device_id}\n{token}\n".encode() + body
sig = hmac.new(secret, raw, hashlib.sha256).hexdigest()
return sig, ts
# 请求时:sig, ts = sign_request(secret, "POST", "/v1/sessions/xxx/messages", body, device_id="DEV001", token="sess_xxx")
# headers["X-Signature"], headers["X-Timestamp"] = sig, ts
# 可选:headers["X-Device-Id"], headers["X-Token"] = device_id, token
python
# 服务端:用同一 secret 及相同的 device_id、token 重算并比对,并校验 timestamp 在有效窗口内(如 5 分钟)
def verify_request(secret: bytes, method: str, path: str, body: bytes, signature: str, timestamp: str,
device_id: str = "", token: str = "", window_sec: int = 300) -> bool:
now = int(time.time())
if abs(now - int(timestamp)) > window_sec:
return False # 防重放
raw = f"{method}\n{path}\n{timestamp}\n{device_id}\n{token}\n".encode() + body
expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# 验签时 device_id、token 从请求头 X-Device-Id、X-Token 或 Cookie/body 取,须与端侧参与签名时一致
1.6 协议选型决策表
| 是否需要服务端主动推? | 是否车机/IoT/弱网? | 是否已有 MQTT? | 推荐方式 |
|---|---|---|---|
| 否 | --- | --- | REST |
| 是 | 否 | 否 | SSE 或 WebSocket |
| 是 | 是 | 否 | WebSocket + 短超时/重试,或 MQTT |
| 是 | 是 | 是 | MQTT |
二、智能体应用架构与场景映射
按「单智能体 vs 多智能体」与「端形态」做简要推荐;可与文末附件可运行代码对应。
| 场景 | 推荐架构 | 通信方式建议 |
|---|---|---|
| 客服/FAQ | 单 Agent(ReAct 或工具调用) | REST + 可选 SSE 流式回复;Web/车机均可,注意转人工与超时 |
| 多步任务/审批/数据分析 | 规划-执行-验证或流水线多 Agent | REST 发起 + SSE/WebSocket 推送「当前步骤」与进度 |
| 多专家综合(投资/研报) | 多 Agent 并行后汇总(Ensemble) | 同上;端侧可展示「分析师 A/B/C 已就绪」等占位或进度 |
| 车载语音助手 | 单 Agent + 车机端精简与 TTS | 短回复 + TTS 流式 + 关键信息占位符;REST + SSE/WS 或 MQTT |
| 车机端云协作 | Planner + 端侧 SDK 作为 Tool(见下 2.2、2.3) | 云端 Planner/Sub-Agent 通过 WebSocket/MQTT 向端侧下发 function call,端侧 APK/SDK 执行(如高德路径、传感器)并回传结果 |
| 机器人具身智能 | 云端 Mind/Planner + 端侧执行器与传感器 | 云端决策与规划,端侧执行动作、上报感知(视觉/力觉/定位);双向流式或 MQTT 指令/状态同步 |
| 需人工兜底(医疗/法律/金融) | 任意架构 + 转人工规则 | 前后端约定 need_human、转人工入口与工单闭环 |
2.2 车机端云架构与具身智能架构
车机端云协作 :车机/手机上的能力(高德/百度路径规划、红绿灯数量、实时路况、本地传感器、HMI)往往比云端开放 API 更丰富。典型模式是云端 Planner(Mind)负责理解意图与拆任务,具体「查路线、选红绿灯最少」等执行依赖端侧 SDK 。端侧以 Tool 形式暴露给云端:云端下发「function call」式请求(如 amap_route_options(origin, destination, strategy="fewest_traffic_lights")),端侧 APK/SDK 执行后把结果回传,云端 Sub-Agent 再汇总生成回复或下一步指令。通信可用 WebSocket(端保持长连)或 MQTT(端订阅指令 topic、发布结果 topic)。
机器人具身智能:云端负责「想」(规划、推理、多步任务分解),端侧负责「做」(运动控制、抓取、导航)和「感」(相机、力觉、定位)。端侧上报状态与感知摘要,云端下发动作序列或实时指令;对延迟敏感的场景可用边缘轻量模型 + 云端兜底。通信上常用双向流式(WebSocket/gRPC)或 MQTT(指令 topic / 状态 topic 分离),需约定指令格式、超时与安全边界(急停、权限)。
2.3 云端编排与端侧能力抽象(Tool 化)--- 以路径规划为例
问题 :用户说「帮我选一条红绿灯最少的路线回家」。云端有 Planner 和地图 Sub-Agent,但云端地图 API 能力不如端侧高德/百度 SDK (如按红绿灯数排序、实时路况、偏好设置)。需要端侧 APK/SDK 作为 Tool :由云端下发「路径规划」的 function call,端侧执行本地 SDK 后把结果回传。若为语音入口,端侧需先经 ASR(语音识别) 转成文本再上行。
端侧 SDK 分层(逻辑关系):
- 端侧主链路 SDK :负责建连、收发 tool_call/tool_result、ASR/TTS 调度、会话与 UI 等;收到云端下发的 tool_call 后,按
tool名调度对应能力 SDK。 - 地图 Agent SDK :端侧主链路 SDK 中的地图能力抽象层,对外统一接口(如
getRouteOptions(origin, dest, strategy)),对接云端地图 Sub-Agent 的 tool_call;内部封装具体厂商实现。 - 百度/高德 车机版 SDK:地图 Agent SDK 所依赖的具体实现,按车厂或配置选用百度车机版、高德车机版等,由地图 Agent SDK 统一封装后对主链路暴露。
即:端侧主链路 SDK ⊃ 地图 Agent SDK ⊃ 百度/高德 车机版 SDK。
完整链路(含语音入口时的 ASR):
地图Sub-Agent Planner主智能体 云端 端侧 ASR 端侧 APK/SDK User 地图Sub-Agent Planner主智能体 云端 端侧 ASR 端侧 APK/SDK User alt [语音输入] 语音「红绿灯最少路线回家」或直接文本 音频流 文本 上行请求 (session_id, trace_id, message 文本) 意图理解与任务分解 生成 task: 路径规划 调用地图 Sub-Agent 需要端侧路线数据 tool_call: amap_route_options(origin, dest, strategy) 地图 Agent SDK(封装高德/百度车机版)获取路线列表 tool_result: routes[], traffic_lights_count 汇总推荐路线与话术 最终回复 下行回复 + 可选导航指令 TTS + 地图展示
最佳实践要点:
- 协议 :云端→端侧用 tool_call (含
request_id、tool名、params);端侧→云端用 tool_result (含同request_id、result或error)。与 LLM function call 对齐,便于 Planner/Sub-Agent 复用同一套抽象。 - 连接与鉴权 :端侧与云端保持长连接(WebSocket 或 MQTT),建连时带
session_id/device_id/token;云端按 session 找到对应设备再下发 tool_call,避免串号。 - 超时与重试:云端对单次 tool_call 设超时(如 10s);端侧 SDK 超时或失败时返回 tool_result error,云端可重试或降级(如用云端 API 兜底)。
- trace_id:从用户请求到 Planner → MapAgent → tool_call → tool_result 全链透传,便于排障与可观测。
代码与可运行示例 :见下文 第四节 4.6 端侧能力 Tool 化架构(路径规划示例) ;完整可运行代码见文末附件二、附件三。
三、架构与实现中的常见坑
按「协议与实现」「多智能体」「端侧与网络」「流式与状态」归纳,与《从 0 到 1 量产 Agent 落地:架构师视角的实践建议》中的痛点与可避免的坑对应。
3.1 协议与实现层
| 坑 | 现象 | 建议 |
|---|---|---|
| 接口契约不一致 | 前后端字段/错误码/流式 event 名对不齐,联调反复改 | 先定 OpenAPI/类型定义,Web 与 Android 共用 |
| 无 trace_id 或未透传 | 用户反馈「答错了」无法查日志 | 请求头或 body 带 trace_id,后端全链路透传 |
| 超时只做一端 | 服务端已放弃,客户端还在等;或反过来 | 两端都设超时,且客户端超时略大于服务端 |
3.2 多智能体编排层
| 坑 | 现象 | 建议 |
|---|---|---|
| 端侧只当「一个请求一个回复」 | 多步/多 Agent 耗时长,白屏或超时 | 用 SSE/WebSocket/MQTT 推送「当前节点/步骤」与中间结果(占位符、进度条) |
| 多 Agent 链路过长无取消 | 用户取消或离开时资源不释放 | 后端支持 cancel(如 task_id + DELETE),前端离开页或点「取消」时调用 |
| 多智能体 trace 不统一 | 每个子 Agent 各打各的日志,无法串成一条链 | 同一 request 共享 trace_id,全链透传 |
3.3 端侧与网络边界
| 坑 | 现象 | 建议 |
|---|---|---|
| 弱网/断网无提示 | 车机或移动端一直转圈 | 短超时、重试策略、明确「网络异常,请重试」与「转人工」入口 |
| 前端不处理 4xx/5xx 与断线 | 用户以为「坏了」 | 统一错误态与重试/转人工入口 |
| 车机长文本一次渲染 | 内存与注意力问题 | 后端按端类型返回「摘要 + 关键信息」或 TTS 用短句流式 |
3.4 流式与状态一致性
| 坑 | 现象 | 建议 |
|---|---|---|
| 流式中断只当失败 | 网络抖动导致 SSE/WS 断,整段重来 | 设计「可恢复」:last_event_id、重连后补发或从 checkpoint 续传(若业务允许) |
| TTS 与文本不同步 | 先出全文再 TTS,体验差 | 设计「文本块 + 对应 TTS 流」或服务端推送「可播片段」顺序,端侧边收边播 |
| 占位符与最终数据不一致 | 多 Agent 阶段用占位符,最终替换时字段或顺序错乱 | 约定占位符协议:先推 { "type": "placeholder", "id": "step_1", "label": "技术分析中" },再推 { "type": "result", "id": "step_1", "payload": {...} } 同 id 替换,前端按 id 更新 UI |
四、架构落地的代码示例
以下为端云通信与多智能体编排的「最小可运行」思路(文末附件为独立可运行脚本,可与 LangGraph 等图编排结合使用)。可运行代码见文末「十、附件:可运行代码」 :附件一为 REST 会话/消息与 SSE 流式后端(模拟多智能体步骤与 chunk),保存为 backend_sse_stream.py 后运行 uvicorn backend_sse_stream:app --reload 即可联调前端。
REST 会话/消息 + SSE 流式整体时序(对应 4.1 与附件一):
多智能体 stream 后端 API 前端/客户端 多智能体 stream 后端 API 前端/客户端 loop [SSE 流式] POST /v1/sessions session_id, trace_id POST /v1/sessions/{id}/messages (message) content (或引导走 /stream) GET /v1/sessions/{id}/stream?message=xxx agent_stream 事件 step / delta / done event: step | message | done + data
4.1 云端服务层(Python)
REST:创建会话、发消息
python
# FastAPI 示例:会话与消息(REST 契约见 1.4)
from uuid import uuid4
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class SendMessageRequest(BaseModel):
message: str
options: dict | None = None # 可选:client_type、流式开关等
@app.post("/v1/sessions")
def create_session(user_id: str, x_trace_id: str | None = Header(None)):
"""创建会话,返回 session_id 与 trace_id,供后续消息与全链追踪使用。"""
session_id = f"sess_{uuid4().hex[:16]}"
trace_id = x_trace_id or str(uuid4())
return {"session_id": session_id, "trace_id": trace_id}
@app.post("/v1/sessions/{session_id}/messages")
def send_message(session_id: str, body: SendMessageRequest, x_trace_id: str | None = Header(None)):
"""发送消息,调用 Agent 得到回复;trace_id 可从前端传入或由本端生成。"""
trace_id = x_trace_id or str(uuid4())
# 此处调用 Agent 的 invoke,得到 content, status, need_human(是否转人工)
content, status, need_human = agent_invoke(session_id, body.message, trace_id)
return {"trace_id": trace_id, "content": content, "status": status, "need_human": need_human}
SSE 流式:步骤事件与文本 chunk
python
# SSE 流式:多智能体步骤与文本 chunk,按 event 类型推送(step / message / done)
from fastapi.responses import StreamingResponse
import json
async def stream_agent_events(session_id: str, message: str, trace_id: str):
"""异步生成器:从 agent_stream 取事件,按 SSE 格式 yield。"""
async for event in agent_stream(session_id, message, trace_id):
if event.get("type") == "step":
yield f"event: step\ndata: {json.dumps(event)}\n\n" # 步骤/占位符
elif event.get("type") == "delta":
yield f"event: message\ndata: {json.dumps(event)}\n\n" # 文本增量
elif event.get("type") == "done":
yield f"event: done\ndata: {json.dumps(event)}\n\n"
return
@app.get("/v1/sessions/{session_id}/stream")
async def stream_chat(session_id: str, message: str, trace_id: str | None = None):
"""SSE 端点:前端用 EventSource 订阅,收到 step / message / done 后更新 UI。"""
return StreamingResponse(
stream_agent_events(session_id, message, trace_id or str(uuid4())),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
WebSocket:双向消息与占位符/最终结果
python
# WebSocket:建连时鉴权,收消息后按 agent_stream 推送 step/placeholder/result/delta
from fastapi import WebSocket
@app.websocket("/v1/ws/{session_id}")
async def ws_chat(websocket: WebSocket, session_id: str):
# 握手阶段鉴权(见 1.5):未通过则 close 并 return,不 accept
token = websocket.headers.get("Authorization") or websocket.cookies.get("session")
if not validate_token(token, session_id):
await websocket.close(code=4001)
return
await websocket.accept()
async for msg in websocket.iter_json():
if msg.get("action") == "send":
# 逐条推送事件,前端按 type 更新步骤条或内容
async for event in agent_stream(session_id, msg["message"], msg.get("trace_id")):
await websocket.send_json(event) # type: placeholder | result | delta
异步任务:202 + task_id + 轮询或推送
python
# 异步任务:POST 返回 202 + task_id,客户端轮询 GET 或通过 SSE/WS 等结果推送
@app.post("/v1/tasks")
def create_task(body: TaskRequest):
"""提交任务,立即返回 202,不等待执行完成。"""
task_id = enqueue_agent_task(body)
return JSONResponse(status_code=202, content={"task_id": task_id})
@app.get("/v1/tasks/{task_id}")
def get_task(task_id: str):
"""轮询任务状态与结果;完成后可配合 SSE/WebSocket 推送避免轮询。"""
status, result = get_task_status(task_id)
return {"task_id": task_id, "status": status, "result": result}
4.2 Web 端架构示例(React)
REST 发消息 + loading/错误/重试
js
// REST 发消息:带 trace_id 便于排障,处理 need_human 时展示转人工入口
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const traceId = useRef(crypto.randomUUID()).current; // 同一会话复用,便于后端串联日志
async function sendMessage() {
setLoading(true); setError(null);
try {
const res = await fetch(`/v1/sessions/${sessionId}/messages`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Trace-Id": traceId },
body: JSON.stringify({ message: input }),
});
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
appendMessage(data.content);
if (data.need_human) showTransferHuman(); // 后端标记需转人工时展示入口
} catch (e) {
setError("发送失败,请重试或转人工");
} finally {
setLoading(false);
}
}
EventSource 消费 SSE:step 与 message
js
// EventSource 消费 SSE:step 更新步骤条,message 追加内容,done 关闭连接
const es = new EventSource(`/v1/sessions/${sessionId}/stream?message=${encodeURIComponent(msg)}&trace_id=${traceId}`);
es.addEventListener("step", (e) => {
const data = JSON.parse(e.data);
setSteps((s) => [...s, { id: data.step_id, label: data.label, status: data.status }]);
});
es.addEventListener("message", (e) => {
const data = JSON.parse(e.data);
if (data.delta) setContent((c) => c + data.delta); // 流式追加文本
});
es.addEventListener("done", () => es.close());
es.onerror = () => { setError("连接中断,可重试"); es.close(); };
Web 端与后端交互时序(REST 建会话/发消息 + EventSource 消费 SSE):
后端 React 前端 用户 后端 React 前端 用户 EventSource 长连接 loop [SSE 事件] 输入并发送 POST /v1/sessions (可选 X-Trace-Id) session_id, trace_id GET /v1/sessions/{id}/stream?message=xxx&trace_id=xxx event: step (步骤/占位符) 更新步骤条 event: message (delta 文本) 追加流式内容 event: done 展示最终结果
4.3 移动端/车机端架构要点(Android)
- REST :OkHttp/Retrofit 调用
POST /v1/sessions/{id}/messages,短超时与重试(如指数退避)。 - SSE/WebSocket :OkHttp 的
EventSource或 WebSocket 消费流式与推送;按event.step/event.message更新步骤与内容。 - MQTT :Paho 等库连接 Broker,订阅
user/{id}/agent/events接收步骤/结果,发布user/{id}/agent/request发消息(车机场景);弱网下短超时与重试。 - TTS:收到文本片段后「收到一段播一段」的简单逻辑,避免长文本一次播报。
4.4 MQTT 端云架构示例
MQTT 端云数据流(车机/IoT 场景:REST 入参,结果经 MQTT 推送):
云端
端侧
subscribe topic
event: step/message/done
Web/车机 MQTT Client
REST API 入参
智能体执行
MQTT Publish
MQTT Broker
后端(Python paho-mqtt) :智能体/多智能体某步完成时向 agent/events/{session_id} publish;payload 与 SSE/WS 的 event 格式统一(trace_id、step_id、status、payload)。可选:REST 入参 → 异步执行 → MQTT 推送结果。
后端示例:智能体某步完成后向 MQTT 推送事件
python
# 依赖:pip install paho-mqtt
import json
import paho.mqtt.client as mqtt
BROKER = "localhost" # 或实际 Broker 地址
PORT = 1883
def on_connect(client, userdata, flags, reason_code, properties=None):
if reason_code == 0:
print("MQTT 已连接")
def create_mqtt_client():
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.connect(BROKER, PORT, 60)
client.loop_start()
return client
# 智能体/多智能体某步完成时调用:向 session 对应 topic 推送,格式与 SSE/WS 的 event 一致
def publish_agent_event(client: mqtt.Client, session_id: str, event: dict):
topic = f"agent/events/{session_id}"
payload = json.dumps(event, ensure_ascii=False)
client.publish(topic, payload, qos=1)
# 示例:模拟多智能体 stream 中每步完成后推送
def on_agent_step_done(client: mqtt.Client, session_id: str, trace_id: str, step_id: str, label: str):
publish_agent_event(client, session_id, {
"trace_id": trace_id,
"step_id": step_id,
"agent_or_node": step_id,
"status": "done",
"label": label,
})
def on_agent_message_delta(client: mqtt.Client, session_id: str, trace_id: str, delta: str):
publish_agent_event(client, session_id, {"trace_id": trace_id, "type": "delta", "delta": delta})
def on_agent_done(client: mqtt.Client, session_id: str, trace_id: str):
publish_agent_event(client, session_id, {"trace_id": trace_id, "type": "done"})
端侧示例:Python 订阅同一 topic 收事件(车机/网关可用;Web 端多用 MQTT over WebSocket + JS 库)
python
# 端侧:订阅 agent/events/{session_id},按 step_id/type 更新步骤条或内容
import json
import paho.mqtt.client as mqtt
def on_message(client, userdata, msg):
payload = json.loads(msg.payload.decode())
if payload.get("type") == "done":
print("会话结束", payload.get("trace_id"))
return
if payload.get("step_id"):
print("步骤:", payload.get("status"), payload.get("label"))
if payload.get("delta"):
print("流式内容:", payload["delta"], end="", flush=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.subscribe("agent/events/your_session_id", qos=1)
client.loop_forever()
前端 :Web 可用 MQTT over WebSocket 连接同一 Broker,React 订阅 session 相关 topic;安卓/车机 可用 Paho Android 等 MQTT 客户端直连 Broker,订阅同一 topic。收到 message 后均按 payload 的 step_id/agent 更新步骤/占位符/最终结果。
4.5 多智能体编排与端侧推送
多智能体 stream 与端侧展示对应关系:
端侧
推送
后端
LangGraph app.stream
step / placeholder / result / delta
SSE
WebSocket
MQTT topic
步骤条
占位符
最终结果
后端 :用 LangGraph 的 app.stream(state) 循环,每步将 node_name、state 摘要或占位符通过 SSE/WebSocket/MQTT 推到前端(与多智能体图结构对应)。
前端 :根据 event.step 或 event.agent 更新「当前执行到哪一 Agent」与占位符,最后用最终结果替换;若走 MQTT,则按 topic 或 payload 中的 step_id/agent 同理更新。
4.6 端侧能力 Tool 化架构(路径规划示例)
场景:Planner 主智能体调度地图 Sub-Agent,Sub-Agent 需要「红绿灯最少路线」等数据,由端侧地图 Agent SDK(封装高德/百度车机版) 执行并回传。云端以 function call 形式下发 tool_call,端侧执行后回传 tool_result。
Planner → 端侧 Tool 调用时序(对应附件二、三):
端侧 (车机/SDK) Map Sub-Agent 后端 (Planner) 用户 端侧 (车机/SDK) Map Sub-Agent 后端 (Planner) 用户 本地高德/百度 SDK 执行 POST /messages "红绿灯最少路线回家" planner_handle 识别意图 map_agent_get_routes(origin, dest, strategy) tool_call (amap_route_options, request_id) tool_result (routes, recommend_index) 汇总路线与话术 content 推荐路线与导航提示
协议约定(WebSocket 或 MQTT payload):
- 云端 → 端侧 tool_call :
{ "type": "tool_call", "request_id": "uuid", "trace_id": "xxx", "tool": "amap_route_options", "params": { "origin": "经度,纬度", "destination": "经度,纬度", "strategy": "fewest_traffic_lights" } } - 端侧 → 云端 tool_result :
{ "type": "tool_result", "request_id": "uuid", "trace_id": "xxx", "result": { "routes": [{ "name": "路线A", "distance": "12km", "traffic_lights": 8 }], "recommend_index": 0 } }或{ "type": "tool_result", "request_id": "uuid", "error": "timeout" }
后端要点 :维护 session_id → WebSocket 映射;Planner 调用 Map Sub-Agent 时,Map Agent 的「工具」实为「向该 session 对应设备下发 tool_call 并等待 tool_result」(带超时与 trace_id)。
端侧要点 :长连建立后,监听 tool_call;根据 tool 名分发到本地 SDK(如 amap_route_options 调高德路径规划),执行完毕后发送 tool_result。
完整可运行代码见文末附件二(云端 backend)、附件三(端侧 device_client)。下面为关键片段。
后端:设备连接与下发 tool_call
python
# 维护 session_id -> WebSocket,便于按会话向端侧下发 tool_call
device_connections: dict[str, WebSocket] = {}
async def wait_tool_result(session_id: str, request_id: str, tool: str, params: dict, trace_id: str, timeout: float = 10.0):
"""向该 session 对应设备下发 tool_call,并等待 tool_result(带超时)。"""
ws = device_connections.get(session_id)
if not ws:
raise RuntimeError("device not connected")
await ws.send_json({
"type": "tool_call", "request_id": request_id, "trace_id": trace_id,
"tool": tool, "params": params,
})
# 等待端侧回传 tool_result:可用 asyncio.Future + 设备 WS 收到消息时 set_result,外层 wait_for 超时
return await wait_for_tool_result_in_session(session_id, request_id, timeout)
后端:Map Sub-Agent 调用「端侧工具」
python
# Map Sub-Agent 调用端侧「高德路径」工具:下发 tool_call,等待 tool_result
async def map_agent_tool_route_options(origin: str, destination: str, strategy: str, session_id: str, trace_id: str):
request_id = str(uuid.uuid4())
result = await wait_tool_result(
session_id, request_id, "amap_route_options",
{"origin": origin, "destination": destination, "strategy": strategy},
trace_id,
)
return result # { "routes": [...], "recommend_index": 0 } 或含 error
端侧:接收 tool_call 并调用本地 SDK 后回传
python
# 端侧 WebSocket 客户端(或 Android 内对应逻辑):收 tool_call,调本地 SDK,回传 tool_result
async def on_message(ws, message):
data = json.loads(message)
if data.get("type") == "tool_call":
rid, tool, params = data["request_id"], data["tool"], data["params"]
if tool == "amap_route_options":
# 实际车机中调用地图 Agent SDK(封装高德/百度车机版),此处用模拟
routes = mock_amap_route_options(params["origin"], params["destination"], params["strategy"])
await send_json(ws, {"type": "tool_result", "request_id": rid, "trace_id": data["trace_id"], "result": routes})
else:
await send_json(ws, {"type": "tool_result", "request_id": rid, "error": "unknown_tool"})
五、多智能体架构下的端云协作设计
- 调度模式与端侧展示:路由/流水线/黑板/并行汇总(见延伸阅读《从 0 到 1 量产 Agent 落地...》第十节)→ 端侧分别对应「步骤条」「阶段进度」「占位符列表」「多路进度 + 汇总结果」。
- 契约 :云端推送的「步骤/进度」事件建议统一格式,例如
{ "trace_id", "step_id", "agent_or_node", "status", "payload?" },便于 Web 与车机共用。 - 超时与取消:整链超时、单步超时;用户取消时端侧发 cancel,云端终止未完成子任务并释放 LLM/工具调用(见延伸阅读该文第十节)。
- 可观测:同一 trace_id 贯穿多 Agent,日志/排查时可按 trace 查全链(见延伸阅读该文第七节)。
时序示意:
Agent2 Agent1 Backend Frontend User Agent2 Agent1 Backend Frontend User 发送消息 POST /messages (trace_id) 多智能体 stream SSE/WS: step placeholder step_1 执行 完成 SSE/WS: result step_1 执行 完成 SSE/WS: result step_2, final 更新步骤条与最终结果
六、端云状态架构:同步、主动推送与流式传输
6.1 状态同步架构
- 会话与历史:以 session_id 为维度,后端存历史;端侧拉取或分页加载,避免单页 DOM 过重。
- 乐观更新与回滚:端侧可先展示「发送中」,失败再回滚并提示重试;敏感操作不做乐观执行,等后端确认。
- 多端同会话:若同一用户多端(Web + 车机)共享会话,需约定谁可写、冲突策略(如以最新写入为准或提示「他端正在输入」)。
6.2 服务端主动推送架构
- 场景:定时任务完成、审核结果、多 Agent 某步完成等,需要服务端主动通知端。
- 实现 :SSE(单向)、WebSocket(双向)或 MQTT(Pub/Sub,尤适合车机/IoT:QoS、持久会话、离线消息);若端不在线,可存「待推送」队列,端重连后拉取或通过 SSE/WS/MQTT 补发。
- 鉴权:网络握手阶段必须鉴权------WS/SSE 建连时校验 token 或 session,未通过则拒绝连接;MQTT 建连时校验 username/password 或证书。按 user_id/session_id 推送,避免串号。
- 连接保活与端侧可等待时长 :SSE 与 WebSocket 在「端侧挂着等推送」时的可等待时长受中间层(代理、负载均衡、CDN)空闲超时 影响------一段时间无数据则连接可能被掐断。SSE 无内置保活,长时间不推任何事件时易断;建议应用层做心跳 (如每 15--30s 发一条注释或空事件,如
: keepalive\n\n),以延长可等待时长。WebSocket 协议有 ping/pong 帧可保活,配合合理配置可维持较长时间(如小时级);无保活时同样受代理空闲超时限制。选型时若需端侧长时间静默等待推送,SSE 需显式加心跳,WebSocket 建议开启 ping/pong 或应用层心跳。
6.3 流式传输架构(TTS、占位符)
- 文本流式 :SSE/WebSocket 推送 LLM chunk,前端逐字或逐段渲染;需约定 chunk 格式(如
data: {"delta":"..."})与 end 事件。 - TTS 流式:方案一------后端边生成文本边调用 TTS,按「句子或片段」推送音频流(或 URL);方案二------端侧收到文本片段后本地 TTS。可对比「延迟 vs 实现复杂度 vs 车机资源」。
- 占位符数据 :多智能体/多步骤场景下,先推
{ "type": "placeholder", "id": "step_1", "label": "技术分析中" },再推{ "type": "result", "id": "step_1", "payload": {...} };前端用 id 匹配并替换。
TTS 流式路径示意:
Frontend TTS Backend Frontend TTS Backend 生成文本块1 合成片段1 音频1 推送文本块1 + 音频1或URL 边收边播 生成文本块2 推送文本块2...
七、架构与工程建议
- 鉴权与多端 :长连接(WS/SSE/MQTT)在握手/建连时 校验 token 或证书,未通过则拒绝连接。Web 用登录态/Token,车机用设备 ID/车架号/厂商账号;请求头或 body 带
client_type(web / android_vehicle),后端按端做限流与审计。 - 版本与兼容 :API 版本(如
/v1/)与 event 格式版本;新增字段做兼容,避免老端解析失败。 - 测试策略:契约测试(OpenAPI + 示例请求/响应);弱网/断线模拟(Charles/Proxyman);多智能体 mock 各节点延迟,验证前端超时与取消逻辑。
- 安全:输入长度与敏感词、输出过滤、敏感操作二次确认;WS/SSE 同源与 CORS,WSS 生产必用。
- 可观测:前端在控制台或上报里带上 trace_id,方便用户反馈时根据 trace 查后端日志。
7.1 既有 Agent 架构的接入方式
若已有基于 LangGraph(或同类框架)的多智能体图,只需在图执行外再包一层通信即可对端提供服务。做法:在 app.stream(state) 的循环中,每执行完一个节点或每产生一次 state 更新,将当前节点名、步骤摘要或占位符封装成统一格式的 event,通过 SSE(StreamingResponse 逐条 yield)、WebSocket(await websocket.send_json(event))或 MQTT(向 session 对应 topic publish)推送到前端或设备。前端按 event.step_id / event.agent_or_node 更新步骤条或占位符,最后用最终结果替换。这样无需改动图内逻辑,仅增加「事件上报」的一层即可。
7.2 架构参考清单与延伸阅读
与延伸阅读《从 0 到 1 量产 Agent 落地:架构师视角的实践建议》可对照阅读的章节如下,便于从架构决策到实现细节一条线打通:
| 该文(延伸阅读)章节 | 对应主题 |
|---|---|
| 第十三节 | 后端工程:API 设计、鉴权多端、与 Agent 解耦、部署扩缩容 |
| 第十四节 | 前端与多端:Web 流式与错误态、车机弱网与驾驶安全、统一协议与 trace 透传 |
| 第十节 | 多智能体调度:何时拆多 Agent、调度模式(路由/流水线/黑板/并行汇总)、超时与失败策略 |
| 第七节 | 可观测:trace、结构化日志、指标与采样 |
| 第十一节 | 人工兜底与审核:转人工规则、工单闭环 |
7.3 异步与并发设计(Python)
端云通信与多智能体场景下,异步与并发 能显著提升吞吐与响应:多路长连接复用、多 Sub-Agent 并行、端侧多 tool 等待等。Python 侧建议以 asyncio + async/await 为主线,必要时配合线程/进程。
何时用异步
- 长连接多路复用 :SSE/WebSocket 服务端需同时维护大量连接,用
async def处理每个连接,单进程内通过事件循环复用,避免一连接一线程。 - 流式与等待 :
StreamingResponse内用async for逐条 yield;等待端侧tool_result用asyncio.wait_for(fut, timeout),不阻塞其他请求。 - 多智能体并行 :多个 Sub-Agent 或多次 LLM/工具调用可并行时,用
asyncio.gather或asyncio.create_task并发执行,再汇总结果。
何时用并发(多任务)
- CPU 密集或阻塞 IO :若某步为 CPU 密集或调用阻塞库(如部分同步 HTTP/DB),可放入
asyncio.to_thread()或run_in_executor,避免阻塞事件循环。 - 多进程:若需多核并行(如多路 LLM 同时推理),可用多进程 + 消息队列,与 asyncio 主进程解耦;或 uvicorn 多 worker(每 worker 一进程)。
Python 异步要点
- FastAPI :路由用
async def时自动跑在 asyncio 事件循环中;StreamingResponse、WebSocket 的receive/send均为异步,直接await即可。 - 超时与取消 :
asyncio.wait_for(coro, timeout)做单次超时;用户取消请求时,可task.cancel()并配合try/except asyncio.CancelledError做清理。Python 3.11+ 可用asyncio.TaskGroup统一管理子任务与取消传播。 - 限流 :用
asyncio.Semaphore(n)限制并发数(如同时调 LLM 的请求数),在进入关键路径前async with sem:。 - 避免阻塞事件循环 :在
async def内不要直接调用阻塞 IO(如requests.get、同步 DB 驱动);可改为httpx.AsyncClient、异步 DB 驱动,或await asyncio.to_thread(blocking_fn, ...)。
多 Sub-Agent 并行示例(asyncio.gather)
python
# 多专家并行再汇总(Ensemble):gather 并发执行,return_exceptions 防止单点失败拖垮整组
async def run_ensemble(session_id: str, query: str, trace_id: str) -> dict:
async def call_one(name: str, prompt: str):
return await llm_ainvoke(prompt) # 假设为异步 LLM 调用
results = await asyncio.gather(
call_one("技术分析师", f"从技术面分析:{query}"),
call_one("新闻分析师", f"从新闻面分析:{query}"),
call_one("财务分析师", f"从财务面分析:{query}"),
return_exceptions=True, # 某一路异常时返回 Exception 而非抛错,便于合并时过滤
)
return merge_results(results) # 合并或选优,需处理 results 中的 Exception
限流示例(Semaphore)------ 一套完整用法
业务中不再直接 调用 llm_ainvoke,统一通过下面的限流包装调用,保证同时进行中的 LLM 调用不超过设定值。
python
import asyncio
# 1)全局限流器:最多允许 10 个 LLM 调用同时进行,可按部署规模调整
LLM_MAX_CONCURRENT = 10
llm_sem = asyncio.Semaphore(LLM_MAX_CONCURRENT)
async def llm_ainvoke(prompt: str) -> str:
"""假设的异步 LLM 调用(实际替换为你的 ChatOpenAI/LangChain 等)。"""
await asyncio.sleep(0.5) # 模拟网络
return f"[LLM] {prompt[:20]}..."
# 2)限流包装:所有需要调 LLM 的地方都走这个函数,不要直接调 llm_ainvoke
async def limited_llm_call(prompt: str, timeout: float = 60.0) -> str:
async with llm_sem: # 超过 10 个并发时在此排队等待
return await asyncio.wait_for(llm_ainvoke(prompt), timeout=timeout)
# 3)在 FastAPI 发消息接口里用限流包装
async def agent_invoke(session_id: str, message: str, trace_id: str) -> str:
# 原来:return await llm_ainvoke(...)
return await limited_llm_call(f"session={session_id} trace={trace_id} user: {message}")
# 4)在流式/多智能体里同样用限流包装
async def agent_stream(session_id: str, message: str, trace_id: str):
yield {"type": "step", "step_id": "think", "status": "running"}
content = await limited_llm_call(message, timeout=30.0) # 流式场景可设短超时
yield {"type": "delta", "delta": content}
yield {"type": "done"}
要点:凡是要调 LLM 的地方都写 await limited_llm_call(...),不要写 await llm_ainvoke(...) ,这样 Semaphore 才能统一限流;并发超过 10 的请求会在 async with llm_sem 处排队,不会打满上游或本机。
小结:IO 密集、多连接、多等待 用 asyncio + async/await;CPU 密集或阻塞调用 用 to_thread/run_in_executor 或多进程;多 Agent 并行 用 gather/create_task;限流与超时 用 Semaphore 与 wait_for。
八、架构补充要点(协议选型、弱网、顺序、连接数、运维、安全)
8.1 协议选型决策架构
按下面三个问题可快速定通信方式:
| 问题 | 是 → 推荐 | 否 → 推荐 |
|---|---|---|
| 是否需要服务端主动推(进度、推送、流式)? | 是 → 需长连接 | 否 → REST 即可 |
| 是否车机/IoT/弱网/离线场景? | 是 → 优先 MQTT 或 WebSocket + 短超时与重试 | 否 → SSE 或 WebSocket 均可 |
| 是否已有 MQTT Broker 或车厂要求 MQTT? | 是 → 用 MQTT | 否 → WebSocket 或 SSE |
综合建议:简单请求响应 用 REST;流式回复或进度推送 用 SSE(单向)或 WebSocket(双向);车机/弱网/离线 在 SSE/WS 基础上加短超时、重试与离线兜底,或直接用 MQTT;异步任务结果可用 202 + task_id + 轮询,或任务完成后通过 SSE/WS/MQTT 推送。
8.2 弱网与离线架构策略
- 弱网:请求超时设短(如 8--15s),重试采用指数退避(1s、2s、4s...),并设最大重试次数;前端明确提示「网络异常,请重试」与「转人工」入口,避免长时间白屏。
- 请求队列 :端侧可对发送失败的消息做本地队列,网络恢复后按序重试;重试时带幂等键(如
client_msg_id),避免重复落库或重复执行。 - 离线:端不在线时,服务端可将待推送消息写入「待推送」队列,端重连后通过拉取接口或 SSE/WS/MQTT 首包补发;冲突策略需约定(如以服务端为准或按时间戳合并)。
- 车机离线兜底:在无网或超时情况下,可预置本地规则或缓存回复(如「当前无网络,请稍后再试」或常用 FAQ 缓存),避免用户无反馈。
8.3 消息顺序与去重架构
多智能体事件经重连、重试后可能乱序或重复到达端侧。建议:每条事件带 sequence_id 或 event_id,端侧按 sequence 排序后再应用,或对已处理的 event_id 做去重;对「标记已读」「状态更新」等关键操作设计为幂等(同一 request_id 多次执行结果一致)。这样可保证 UI 状态一致、不重复渲染。
8.4 连接规模与成本架构
SSE/WebSocket/MQTT 均为长连接,单机连接数受限于进程 fd 与 Broker 规格。建议:按用户或设备维度限制单用户最大连接数(如 1 设备 1 连接);设置心跳与空闲断开(如 5 分钟无数据则服务端主动断开),减少僵尸连接------SSE 无内置保活、长静默易被代理掐断,需应用层心跳(见 6.2);WebSocket 可开 ping/pong 或应用层心跳。MQTT 使用 Broker 的持久会话与 clean session 策略时要评估内存与成本。这样可避免单机连接打满导致新用户无法建连。
8.5 可观测与运维架构
长连接场景下建议监控:连接数 (当前在线、按端类型分布)、重连率 、消息端到端延迟 (从后端发出到端侧展示)、SSE/WS/MQTT 断线率与断线原因(超时、网络、服务重启)。配置告警(如连接数超阈值、断线率突增)与大盘,便于量产稳定与故障快速定位。
8.6 MQTT 安全架构与规范
- 传输 :生产环境使用 TLS(MQTT over TLS),避免明文传输。
- 认证:CONNECT 时使用 username/password 或客户端证书;Broker 校验通过后再允许订阅/发布。
- 权限 :按 user_id、device_id 或 vehicle(VIN)做 topic 隔离(如
user/{user_id}/agent/events),避免跨用户收到他人消息。 - 消息签名与验签 :对 payload 做签名 (如 HMAC-SHA256(secret, payload) 或对关键字段序列化后签名),接收方验签 后再处理,可防篡改与伪造。适用于指令类、敏感数据推送;与 1.5 中「请求与消息签名」同一思路,MQTT 下在 payload 内带
signature或单独 signature 字段即可。 - 车厂规范:若车厂要求 MQTT,需遵循其约定的 topic 命名与 payload 格式,便于对接与过检。
九、架构小结与参考
可直接对照落地的清单:
- 通信方式 :REST/SSE/WebSocket/轮询/gRPC/MQTT 按场景选型;车机/IoT 弱网与离线重点考虑 MQTT;协议选型见 8.1。
- 握手与鉴权 :长连接在建连时 必须鉴权(WS/SSE 校验 token,MQTT 校验 username/password 或证书),未通过则拒绝连接;高安全或车机场景可启用请求/消息签名与验签 (见 1.5 、8.6)。
- 场景 :单 Agent 与多 Agent 对应不同端侧展示(步骤条、占位符、多路进度);车载注意 TTS 与驾驶安全;车机端云协作、具身智能、Planner 调度端侧 SDK 见 2.2 / 2.3。
- 坑:契约一致、trace_id 全链透传、双端超时、多智能体进度推送与取消、弱网提示、流式可恢复、占位符按 id 替换。
- 代码 :后端 REST/SSE/WebSocket/MQTT、前端 React EventSource/WS、Android OkHttp/MQTT、多智能体 stream 推送;端侧 SDK 作为 Tool 见 4.6 ,可运行代码见文末附件。
- 补充 :弱网与离线(8.2)、消息顺序与去重(8.3)、连接规模与成本(8.4)、可观测与运维(8.5)、MQTT 安全(8.6);既有 Agent 图接入见 7.1 ,Python 异步与并发设计见 7.3。
参考:延伸阅读《从 0 到 1 量产 Agent 落地:架构师视角的实践建议》第十三、十四、十、七、十一节;多智能体图结构可参考 LangGraph 等实现。
延伸阅读(CSDN 博文):
十、附件:可运行代码
以下代码可直接复制保存为对应文件名后运行,无需新建工程目录;依赖:pip install fastapi uvicorn websockets(附件二、三需要 websockets)。
附件一:REST + SSE 流式后端(backend_sse_stream.py)
提供 REST 会话/消息与 SSE 流式接口(模拟多智能体步骤与 chunk)。运行:uvicorn backend_sse_stream:app --reload,然后可用浏览器或 EventSource 请求 /v1/sessions/{session_id}/stream?message=xxx。
附件一数据流示意:
backend_sse_stream 客户端 backend_sse_stream 客户端 loop [event stream] alt [同步消息] [SSE 流式] POST /v1/sessions session_id, trace_id POST /v1/sessions/{id}/messages content, status, need_human GET /v1/sessions/{id}/stream?message=xxx event: step (running/done) event: message (delta) event: done
python
# -*- coding: utf-8 -*-
"""
REST + SSE 流式后端:会话/消息 + 多智能体步骤与 chunk 模拟
运行:uvicorn backend_sse_stream:app --reload
"""
import asyncio
import json
import uuid
from fastapi import FastAPI, Header
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
app = FastAPI(title="Agent API (REST + SSE)")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
class CreateSessionRequest(BaseModel):
user_id: str = "default"
class SendMessageRequest(BaseModel):
message: str
options: dict | None = None # 可选:client_type、流式开关等
@app.post("/v1/sessions")
def create_session(body: CreateSessionRequest | None = None, x_trace_id: str | None = Header(None)):
"""创建会话,返回 session_id、trace_id(契约见 1.4);请求体可选 { \"user_id\": \"xxx\" }。"""
user_id = body.user_id if body else "default"
session_id = f"sess_{uuid.uuid4().hex[:16]}"
trace_id = x_trace_id or str(uuid.uuid4())
return {"session_id": session_id, "trace_id": trace_id}
@app.post("/v1/sessions/{session_id}/messages")
def send_message(session_id: str, body: SendMessageRequest, x_trace_id: str | None = Header(None)):
"""同步回复;实际可在此调用 agent_invoke,或引导前端走 /stream。"""
trace_id = x_trace_id or str(uuid.uuid4())
content = f"[模拟回复] 收到:{body.message}"
return {"trace_id": trace_id, "content": content, "status": "ok", "need_human": False}
async def stream_agent_events(session_id: str, message: str, trace_id: str):
"""SSE 异步生成器:先推 step(running→done),再推 message delta,最后 done。"""
steps = [("step_1", "技术分析中"), ("step_2", "新闻分析中"), ("step_3", "汇总报告")]
for step_id, label in steps:
yield f"event: step\ndata: {json.dumps({'trace_id': trace_id, 'step_id': step_id, 'agent_or_node': step_id, 'status': 'running', 'label': label})}\n\n"
await asyncio.sleep(0.3)
for step_id, label in steps:
yield f"event: step\ndata: {json.dumps({'trace_id': trace_id, 'step_id': step_id, 'status': 'done', 'label': label})}\n\n"
await asyncio.sleep(0.1)
for word in ["多智能体", "分析", "完成", "。"]:
yield f"event: message\ndata: {json.dumps({'delta': word})}\n\n"
await asyncio.sleep(0.2)
yield f"event: done\ndata: {json.dumps({'trace_id': trace_id})}\n\n"
@app.get("/v1/sessions/{session_id}/stream")
async def stream_chat(session_id: str, message: str, trace_id: str | None = None):
"""SSE 端点:前端 EventSource 订阅,按 step / message / done 更新 UI。"""
tid = trace_id or str(uuid.uuid4())
return StreamingResponse(
stream_agent_events(session_id, message, tid),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive"},
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
附件二:Planner 调度端侧 Tool 云端后端(backend.py)
设备通过 WebSocket /v1/device/ws?session_id=xxx 连接;用户通过 POST /v1/sessions/{id}/messages 发消息;Planner 解析意图后调用 Map Sub-Agent,向端侧下发 tool_call 并等待 tool_result。运行:uvicorn backend:app --reload --port 8000(需先启动,再运行附件三)。
附件二数据流示意:
端侧设备 device_ws map_agent_get_routes planner_handle backend (FastAPI) 用户/客户端 端侧设备 device_ws map_agent_get_routes planner_handle backend (FastAPI) 用户/客户端 POST /v1/sessions/{id}/messages WebSocket /v1/device/ws?session_id=xxx device_connections[session_id]=ws planner_handle(message) map_agent_get_routes(...) wait_tool_result → send tool_call tool_call (amap_route_options) tool_result (routes) Future.set_result(result) result content content 推荐路线
python
# -*- coding: utf-8 -*-
"""
Planner 调度端侧 SDK 作为 Tool:云端后端
运行:uvicorn backend:app --reload --port 8000
"""
import asyncio
import json
import uuid
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Header
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI(title="Planner + Device Tool (Path Planning)")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# session_id -> 设备 WebSocket;端侧连 /v1/device/ws?session_id=xxx 后写入
device_connections: dict[str, WebSocket] = {}
# request_id -> Future,设备回传 tool_result 时 set_result,wait_tool_result 据此返回
pending_tool_results: dict[str, asyncio.Future] = {}
@app.websocket("/v1/device/ws")
async def device_ws(websocket: WebSocket, session_id: str):
"""设备长连:注册到 device_connections,循环收 tool_result 并完成对应 Future。"""
await websocket.accept()
device_connections[session_id] = websocket
try:
while True:
raw = await websocket.receive_text()
data = json.loads(raw)
if data.get("type") == "tool_result":
rid = data.get("request_id")
if rid and rid in pending_tool_results:
fut = pending_tool_results.pop(rid, None)
if fut and not fut.done():
fut.set_result({"result": data.get("result"), "error": data.get("error")})
except WebSocketDisconnect:
pass
finally:
device_connections.pop(session_id, None)
async def wait_tool_result(
session_id: str, request_id: str, tool: str, params: dict, trace_id: str, timeout: float = 10.0
) -> dict:
"""向该 session 设备下发 tool_call,等待 tool_result(asyncio.wait_for 超时)。"""
ws = device_connections.get(session_id)
if not ws:
return {"error": "device_not_connected", "result": None}
fut = asyncio.get_event_loop().create_future()
pending_tool_results[request_id] = fut
try:
await ws.send_json({
"type": "tool_call", "request_id": request_id, "trace_id": trace_id,
"tool": tool, "params": params,
})
done = await asyncio.wait_for(fut, timeout=timeout)
return done
except asyncio.TimeoutError:
pending_tool_results.pop(request_id, None)
return {"error": "timeout", "result": None}
finally:
pending_tool_results.pop(request_id, None)
async def map_agent_get_routes(origin: str, destination: str, strategy: str, session_id: str, trace_id: str) -> dict:
"""地图 Sub-Agent:下发 amap_route_options 的 tool_call,等待端侧 tool_result。"""
request_id = str(uuid.uuid4())
return await wait_tool_result(
session_id, request_id, "amap_route_options",
{"origin": origin, "destination": destination, "strategy": strategy},
trace_id,
)
async def planner_handle(message: str, session_id: str, trace_id: str) -> str:
"""简化 Planner:识别路径规划意图则调 map_agent_get_routes,否则提示仅支持路径规划。"""
msg_lower = message.strip().lower()
if any(k in msg_lower for k in ("路线", "回家", "红绿灯", "导航", "怎么走")):
origin, destination = "116.397128,39.916527", "116.481028,39.989643"
strategy = "fewest_traffic_lights" if "红绿灯" in msg_lower else "default"
out = await map_agent_get_routes(origin, destination, strategy, session_id, trace_id)
if out.get("error"):
return f"[路径规划] 端侧未响应或超时:{out.get('error')},请确认设备已连接。"
result = out.get("result") or {}
routes = result.get("routes", [])
recommend_index = result.get("recommend_index", 0)
if not routes:
return "未获取到可用路线,请检查端侧 SDK 或网络。"
rec = routes[recommend_index] if recommend_index < len(routes) else routes[0]
return f"已为您选择红绿灯较少的路线:{rec.get('name', '推荐路线')},约 {rec.get('distance', 'N/A')},红绿灯约 {rec.get('traffic_lights', 'N/A')} 个。请在地图上确认后开始导航。"
return f"[Planner] 收到:{message}。当前示例仅支持路径规划类请求(如「红绿灯最少的路线回家」)。"
class CreateSessionRequest(BaseModel):
user_id: str = "default"
class SendMessageRequest(BaseModel):
message: str
options: dict | None = None
@app.post("/v1/sessions")
def create_session(body: CreateSessionRequest | None = None, x_trace_id: str | None = Header(None)):
"""创建会话(契约见 1.4);请求体可选 { \"user_id\": \"xxx\" }。"""
session_id = f"sess_{uuid.uuid4().hex[:16]}"
trace_id = x_trace_id or str(uuid.uuid4())
return {"session_id": session_id, "trace_id": trace_id}
@app.post("/v1/sessions/{session_id}/messages")
async def send_message(session_id: str, body: SendMessageRequest, x_trace_id: str | None = Header(None)):
trace_id = x_trace_id or str(uuid.uuid4())
content = await planner_handle(body.message, session_id, trace_id)
return {"trace_id": trace_id, "content": content, "status": "ok", "need_human": False}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
附件三:端侧模拟客户端(device_client.py)
连接云端 WebSocket,接收 tool_call 后用模拟路线数据回传 tool_result(实际车机中此处调用高德/百度地图 SDK)。先启动附件二后端,再运行:python device_client.py。
附件三数据流示意:
device_ws 附件二 backend send_user_message() device_loop() create_session() main() device_ws 附件二 backend send_user_message() device_loop() create_session() main() create_session() POST /v1/sessions session_id session_id 后台线程 asyncio.run(device_loop(session_id)) WebSocket 连接 ?session_id=xxx sleep(1) 等设备连上 send_user_message(session_id, "红绿灯最少路线回家") POST /v1/sessions/{id}/messages tool_call (amap_route_options) 收到 tool_call mock_amap_route_options() tool_result (routes) set_result → 后端继续 content 打印后端回复
python
# -*- coding: utf-8 -*-
"""
端侧模拟客户端:接收 tool_call,用模拟数据回传 tool_result
先启动附件二后端,再运行:python device_client.py
"""
import asyncio
import json
import threading
import urllib.request
try:
import websockets
except ImportError:
websockets = None
BASE, WS_BASE = "http://127.0.0.1:8000", "ws://127.0.0.1:8000"
def mock_amap_route_options(origin: str, destination: str, strategy: str) -> dict:
"""模拟高德路径规划;车机中改为调用地图 Agent SDK(封装高德/百度车机版)。"""
if strategy == "fewest_traffic_lights":
routes = [
{"name": "路线A(红绿灯最少)", "distance": "12km", "traffic_lights": 5, "duration": "28分钟"},
{"name": "路线B", "distance": "11km", "traffic_lights": 12, "duration": "25分钟"},
]
recommend_index = 0
else:
routes = [{"name": "推荐路线", "distance": "11km", "traffic_lights": 12, "duration": "25分钟"}]
recommend_index = 0
return {"routes": routes, "recommend_index": recommend_index}
def create_session(user_id: str = "default") -> str:
"""创建会话,返回 session_id,供设备 WS 与发消息使用;请求体与 1.4 契约一致 { \"user_id\": \"xxx\" }。"""
req = urllib.request.Request(
f"{BASE}/v1/sessions",
data=json.dumps({"user_id": user_id}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=5) as r:
return json.loads(r.read().decode())["session_id"]
def send_user_message(session_id: str, message: str) -> str:
"""POST 发用户消息,返回云端 Planner 的回复内容。"""
req = urllib.request.Request(
f"{BASE}/v1/sessions/{session_id}/messages",
data=json.dumps({"message": message}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=15) as r:
return json.loads(r.read().decode()).get("content", "")
async def device_loop(session_id: str):
"""端侧 WebSocket:连上后循环收 tool_call,按 tool 名调 mock(或真实 SDK),回传 tool_result。"""
if not websockets:
print("请安装: pip install websockets")
return
uri = f"{WS_BASE}/v1/device/ws?session_id={session_id}"
async with websockets.connect(uri, ping_interval=20, ping_timeout=10) as ws:
while True:
try:
raw = await asyncio.wait_for(ws.recv(), timeout=60)
except asyncio.TimeoutError:
continue
data = json.loads(raw)
if data.get("type") == "tool_call":
rid, tool, params = data.get("request_id"), data.get("tool", ""), data.get("params", {})
result = mock_amap_route_options(params.get("origin", ""), params.get("destination", ""), params.get("strategy", "default")) if tool == "amap_route_options" else None
err = None if tool == "amap_route_options" else "unknown_tool"
await ws.send(json.dumps({"type": "tool_result", "request_id": rid, "trace_id": data.get("trace_id", ""), "result": result, "error": err}))
def main():
"""先建会话,再在后台线程跑 device_loop,最后发一条用户消息触发云端 tool_call。"""
session_id = create_session()
if websockets:
threading.Thread(target=lambda: asyncio.run(device_loop(session_id)), daemon=True).start()
import time
time.sleep(1.0) # 等设备 WS 连上后再发消息
content = send_user_message(session_id, "帮我选红绿灯最少的路线回家")
print("后端回复:", content)
if __name__ == "__main__":
main()