智能体端云协同架构指南:通信设计、多智能体编排与落地

文章目录

    • 一、端云通信架构与选型
      • [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 安全架构与规范)
    • 九、架构小结与参考
    • 十、附件:可运行代码

面向读者:架构师、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/eventsvehicle/{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-SignatureX-Timestamp(或 nonce),服务端用同一密钥重算并比对;带 timestamp 可防重放(如 5 分钟内有效)。签名内容可包含 device_id 或 token(session_id) :参与 HMAC 计算后,请求即与设备/会话绑定,可防串号与跨设备重放。REST 在 Header 带签名(及 X-Device-IdX-Token 等);WebSocket/MQTT 可在 payload 内带 signaturedevice_idtoken 字段,云端/端侧收到后验签再处理。车机、开放环境或高安全场景建议启用。

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_idtool 名、params);端侧→云端用 tool_result (含同 request_idresulterror)。与 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.stepevent.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_resultasyncio.wait_for(fut, timeout),不阻塞其他请求。
  • 多智能体并行 :多个 Sub-Agent 或多次 LLM/工具调用可并行时,用 asyncio.gatherasyncio.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限流与超时Semaphorewait_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_idevent_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 格式,便于对接与过检。

九、架构小结与参考

可直接对照落地的清单

  1. 通信方式 :REST/SSE/WebSocket/轮询/gRPC/MQTT 按场景选型;车机/IoT 弱网与离线重点考虑 MQTT;协议选型见 8.1
  2. 握手与鉴权 :长连接在建连时 必须鉴权(WS/SSE 校验 token,MQTT 校验 username/password 或证书),未通过则拒绝连接;高安全或车机场景可启用请求/消息签名与验签 (见 1.58.6)。
  3. 场景 :单 Agent 与多 Agent 对应不同端侧展示(步骤条、占位符、多路进度);车载注意 TTS 与驾驶安全;车机端云协作、具身智能、Planner 调度端侧 SDK 见 2.2 / 2.3
  4. :契约一致、trace_id 全链透传、双端超时、多智能体进度推送与取消、弱网提示、流式可恢复、占位符按 id 替换。
  5. 代码 :后端 REST/SSE/WebSocket/MQTT、前端 React EventSource/WS、Android OkHttp/MQTT、多智能体 stream 推送;端侧 SDK 作为 Tool 见 4.6 ,可运行代码见文末附件
  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()
相关推荐
一战成名9969 小时前
深度解析 CANN 模型转换工具链:从 ONNX 到 OM
人工智能·学习·安全·开源
BJ_Bonree9 小时前
4月17日,博睿数据受邀出席GOPS全球运维大会2026 · 深圳站!
大数据·运维·人工智能
ujainu9 小时前
CANN仓库中的AIGC能效-性能协同优化:昇腾AI软件栈如何实现“既要又要还要”的工程奇迹
人工智能·aigc
2501_944934739 小时前
大专大数据管理与应用专业,怎么自学数据治理相关知识?
人工智能
芷栀夏9 小时前
CANN ops-math:从矩阵运算到数值计算的全维度硬件适配与效率提升实践
人工智能·神经网络·线性代数·矩阵·cann
Yuer20259 小时前
为什么说在真正的合规体系里,“智能”是最不重要的指标之一。
人工智能·edca os·可控ai
一切尽在,你来9 小时前
1.4 LangChain 1.2.7 核心架构概览
人工智能·langchain·ai编程
Giggle12189 小时前
外卖 O2O 系统怎么选?从架构到部署方式的完整拆解
大数据·架构
爱吃大芒果9 小时前
CANN ops-nn 算子开发指南:NPU 端神经网络计算加速实战
人工智能·深度学习·神经网络