云计算百科
云计算领域专业知识百科平台

Hermes Agent Gateway统一控制面架构深度解析

Gateway统一控制面架构深度解析

文档定位:本文档是 Hermes Agent 自进化智能体高阶实战知识体系中「产品定位与核心架构」章节的第二个拆解点,聚焦于 Gateway 统一控制面的架构设计、核心机制与工程实现。

适用读者:架构设计人员、AI Agent 平台开发者、后端高级工程师、技术决策者

前置依赖:建议先阅读《01_产品定位与系统全景》以建立整体认知框架


👉写博客的朋友可以看看这个,助力内容创作,懂技术更懂增长,开发者与企业的创作增长引擎 !👈👉点击进入

目录

  • 第一章 Gateway 架构总览
    • 1.1 设计理念
    • 1.2 核心职责矩阵
    • 1.3 在系统中的中枢地位
    • 1.4 架构分层视图
    • 1.5 技术选型概览
  • 第二章 会话管理器深度解析
    • 2.1 多轮对话上下文维护
    • 2.2 会话 ID 路由机制
    • 2.3 上下文窗口压缩策略
    • 2.4 并行会话隔离方案
    • 2.5 会话生命周期管理
  • 第三章 渠道适配器架构
    • 3.1 适配器模式设计
    • 3.2 消息格式归一化流程
    • 3.3 多平台协议差异处理
    • 3.4 回调机制与异步消息处理
    • 3.5 渠道健康检查与故障转移
  • 第四章 工具注册表设计
    • 4.1 工具注册与发现机制
    • 4.2 MCP 协议集成
    • 4.3 内置工具与自定义工具统一管理
    • 4.4 权限校验与调用审计
    • 4.5 工具版本管理与热更新
  • 第五章 核心路由引擎
    • 5.1 意图识别算法
    • 5.2 任务编排策略
    • 5.3 记忆调度机制
    • 5.4 安全沙箱集成
  • 第六章 Gateway 与其他模块的交互
    • 6.1 与记忆系统的协作
    • 6.2 与模型路由器的协作
    • 6.3 与自进化引擎的协作
  • 第七章 Gateway 性能与扩展性
    • 7.1 并发处理能力
    • 7.2 水平扩展方案
    • 7.3 故障恢复机制
  • 第八章 与其他 Agent 框架的 Gateway 对比
    • 8.1 架构哲学对比
    • 8.2 核心能力对比矩阵
    • 8.3 场景适用性分析

第一章 Gateway 架构总览

1.1 设计理念

Gateway 统一控制面是 Hermes Agent 架构体系中的中枢神经系统,其设计理念根植于以下五个核心原则:

1.1.1 单一入口原则(Single Entry Point)

所有外部渠道——无论是 Web 客户端、移动应用、即时通讯平台还是 API 调用——都必须经过 Gateway 这唯一的入口点。这一设计带来了三个关键收益:

  • 统一安全边界:所有请求经过同一套认证、鉴权、限流、审计管线,消除了多入口带来的安全盲区
  • 协议归一化:不同渠道的异构协议在 Gateway 层面被统一翻译为内部标准消息格式,下游模块无需感知渠道差异
  • 可观测性收敛:全链路追踪、指标采集、日志聚合在同一层完成,为系统提供端到端的事务可见性

1.1.2 控制面与数据面分离(Control-Data Plane Separation)

Gateway 严格遵循云原生架构中控制面与数据面分离的设计范式:

┌─────────────────────────────────────────────────────────────────────┐
│ Hermes Agent 系统全景 │
│ │
│ ┌─────────────────┐ 控制信号 ┌─────────────────────────────┐ │
│ │ │ ─────────> │ │ │
│ │ 控制面(Gateway) │ │ 数据面 (执行层) │ │
│ │ │ <───────── │ │ │
│ │ · 会话管理 │ 状态回传 │ · 模型推理 │ │
│ │ · 渠道适配 │ │ · 工具执行 │ │
│ │ · 工具调度 │ │ · 记忆读写 │ │
│ │ · 意图路由 │ │ · 外部API调用 │ │
│ │ · 策略决策 │ │ · 知识检索 │ │
│ └────────┬────────┘ └─────────────────────────────┘ │
│ │ │
│ │ 控制指令 │
│ v │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 基础设施层 │ │
│ │ 消息队列 · 分布式缓存 · 对象存储 · 日志中心 · 监控 │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

控制面负责「做什么」和「怎么调度」——决定请求的路由方向、编排策略、安全策略;数据面负责「实际执行」——模型推理、工具调用、记忆持久化。这种分离使得控制逻辑可以独立演进、独立部署、独立扩展,同时为数据面的多副本水平扩展提供了前提条件。

1.1.3 声明式配置优于硬编码(Declarative over Imperative)

Gateway 的所有可变行为——渠道映射、工具路由规则、限流策略、安全沙箱策略——均通过声明式配置定义,而非硬编码在业务逻辑中。这使得运维人员可以在不修改代码的情况下调整系统行为:

# gateway-config.yaml — Gateway 声明式配置示例
gateway:
server:
port: 8080
worker_threads: 16
max_connections: 10000
request_timeout: 120s
grpc:
enabled: true
port: 9090
max_recv_msg_size: 16MB

session:
default_ttl: 3600s
max_concurrent_per_user: 5
context_window:
max_tokens: 32768
compression_threshold: 0.75
compression_strategy: sliding_window_summary
storage:
type: redis_cluster
endpoints:
redis0.internal:6379
redis1.internal:6379
redis2.internal:6379
key_prefix: "hermes:session:"

channels:
name: web
adapter: WebSocketAdapter
enabled: true
rate_limit:
requests_per_minute: 120
burst: 30
name: feishu
adapter: FeishuAdapter
enabled: true
rate_limit:
requests_per_minute: 60
callback:
url: https://gateway.internal/feishu/callback
verify_token: ${FEISHU_VERIFY_TOKEN}
name: dingtalk
adapter: DingTalkAdapter
enabled: true
rate_limit:
requests_per_minute: 60
name: wechat_work
adapter: WeComAdapter
enabled: true
name: slack
adapter: SlackAdapter
enabled: false
name: telegram
adapter: TelegramAdapter
enabled: false

routing:
intent_model: hermesintentv2
fallback_strategy: llm_based
max_orchestration_depth: 8
parallel_task_limit: 4
timeout_per_task: 30s

tools:
registry:
type: dynamic
refresh_interval: 30s
sandbox:
enabled: true
engine: firecracker
memory_limit: 512MB
cpu_limit: 1.0
network: restricted
allowed_domains:
api.internal
"*.hermes.ai"
audit:
enabled: true
log_retention: 90d
sensitive_params_mask: true

observability:
tracing:
enabled: true
exporter: otlp
endpoint: jaegercollector.observability:4317
sampling_rate: 0.1
metrics:
enabled: true
exporter: prometheus
port: 9091
logging:
level: info
format: json
output: file
path: /var/log/hermes/gateway.log
rotation:
max_size: 100MB
max_files: 10
max_age: 30d

1.1.4 渐进式退化原则(Graceful Degradation)

Gateway 被设计为在任何单点故障下都能保持核心功能的可用性。当记忆系统不可用时,Gateway 退化为无记忆模式继续服务;当某个渠道适配器故障时,其他渠道不受影响;当模型路由器超时时,Gateway 使用本地缓存的最优模型配置执行降级推理。这一原则确保了系统在恶劣条件下的韧性。

1.1.5 可插拔架构原则(Pluggable Architecture)

Gateway 的每一个核心组件——渠道适配器、工具执行器、意图识别器、记忆后端——都通过标准接口定义,实现可插拔替换。这意味着用户可以用自定义实现替换任何一个组件,而无需修改 Gateway 核心代码:

┌──────────────────────────────────────────────────────────────────────────┐
│ Gateway 可插拔架构 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ Channel │ │ Tool │ │ Intent │ │ Memory │ │
│ │ Adapter SPI │ │ Executor SPI │ │ Detector SPI │ │ Store SPI│ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ 内置实现 │ │ 内置实现 │ │ 内置实现 │ │ 内置实现 │ │
│ │ Web │ │ Code │ │ Rule │ │ Redis │ │
│ │ Feishu │ │ Search │ │ ML Model │ │ Postgres│ │
│ │ Slack │ │ HTTP │ │ LLM Based│ │ Vector │ │
│ │ … │ │ MCP │ │ Hybrid │ │ Hybrid │ │
│ └─────────┘ └─────────┘ └──────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 用户扩展 │ │ 用户扩展 │ │ 用户扩展 │ │ 用户扩展 │ │
│ │ Custom │ │ Custom │ │ Custom │ │ Custom │ │
│ │ Adapter │ │ Tool │ │ Detector │ │ Store │ │
│ └─────────┘ └─────────┘ └──────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────────────────┘

1.2 核心职责矩阵

Gateway 承担着四大核心职责,每一项职责都对应一组具体的子功能和工程要求:

职责域子功能工程要求优先级
会话管理 上下文维护 低延迟读写(<5ms P99),支持多轮对话的增量更新 P0
会话路由 基于会话ID的精确路由,支持跨节点会话亲和性 P0
上下文压缩 在Token预算内保留最大信息密度 P1
并行隔离 同一用户的多个会话互不干扰 P0
渠道适配 协议归一化 12+平台消息格式统一翻译 P0
回调处理 异步消息接收与处理,保证至少一次投递 P0
健康检查 渠道可用性监测与自动故障转移 P1
限流熔断 按渠道粒度的流量控制与熔断保护 P1
工具调度 工具注册 动态注册与发现,支持热更新 P0
权限校验 基于RBAC的工具调用权限控制 P0
沙箱隔离 代码执行类工具的沙箱隔离 P0
调用审计 全量调用日志记录与审计追踪 P1
路由引擎 意图识别 基于规则+ML+LLM的三级意图识别 P0
任务编排 支持串行、并行、条件分支的任务编排 P0
记忆调度 根据意图决定记忆的读写策略 P1
降级策略 模型不可用时的自动降级 P1

1.3 在系统中的中枢地位

Gateway 在 Hermes Agent 系统中的位置可以用「六边形中枢」来形容——它向上对接所有外部渠道,向下调度所有内部能力模块,横向贯穿记忆系统与模型路由器,同时接受自进化引擎的策略指导:

┌─────────────────────┐
│ 自进化引擎 │
│ (Self-Evolution) │
│ · 策略优化 │
│ · 参数调优 │
│ · 能力评估 │
└─────────┬───────────┘
│ 策略下发
v
┌───────────┐ 请求 ┌─────────────────────────────────┐ 调用 ┌───────────────┐
│ │ ═══════> │ │ ════════> │ │
│ Web UI │ │ │ │ 模型路由器 │
│ │ <═══════ │ G A T E W A Y │ <════════ │ (Model │
├───────────┤ 响应 │ │ 结果 │ Router) │
│ │ │ 统 一 控 制 面 │ │ │
│ 飞书/钉钉 │ ═══════> │ ┌───────────┐ ┌──────────────┐ │ ├───────────────┤
│ │ │ │ 会话管理器 │ │ 渠道适配器集群 │ │ │ │
├───────────┤ │ │ Session │ │ Adapters │ │ │ 记忆系统 │
│ │ │ │ Manager │ │ │ │ │ (Memory │
│ Slack/ │ ═══════> │ └───────────┘ └──────────────┘ │ │ System) │
│ Telegram │ │ ┌───────────┐ ┌──────────────┐ │ │ │
│ │ │ │ 工具注册表 │ │ 核心路由引擎 │ │ ════════> ├───────────────┤
├───────────┤ │ │ Tool │ │ Routing │ │ │ │
│ │ │ │ Registry │ │ Engine │ │ │ 工具执行层 │
│ API/SDK │ ═══════> │ └───────────┘ └──────────────┘ │ │ (Tool │
│ │ │ │ │ Executors) │
│ │ └─────────────────────────────────┘ └───────────────┘
│ │ │
│ │ 基础设施层
│ │ ┌─────────────┴──────────────┐
│ │ │ Redis · Kafka · S3 · ES │
│ │ │ Jaeger · Prometheus · ELK │
│ │ └────────────────────────────┘
└───────────┘

从数据流的角度看,Gateway 是所有请求的必经之路。一个典型的请求生命周期如下:

用户消息 → 渠道适配器(协议归一化) → 会话管理器(上下文加载)
→ 核心路由引擎(意图识别+任务编排)
→ [并行] 模型路由器(LLM推理) + 记忆系统(记忆检索)
→ 工具注册表(工具调用) → 结果聚合
→ 会话管理器(上下文更新) → 渠道适配器(格式转换) → 用户响应

1.4 架构分层视图

Gateway 内部采用清晰的六层架构,每层职责单一、边界明确:

┌─────────────────────────────────────────────────────────────────────┐
│ Layer 6: 接入层 (Access Layer) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ HTTP/ │ │ gRPC │ │ WebSocket│ │ Webhook │ │ SDK │ │
│ │ REST │ │ Server │ │ Server │ │ Handler │ │ Gateway │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ Layer 5: 协议适配层 (Protocol Adaptation Layer) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Web Adapter │ │ Feishu │ │ DingTalk │ │ Slack │ │
│ │ │ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ WeCom Adapter│ │ Telegram │ │ Discord │ │ Email │ │
│ │ │ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ WhatsApp │ │ Teams │ │ Custom… │ │
│ │ Adapter │ │ Adapter │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ Layer 4: 会话管理层 (Session Management Layer) │
│ ┌─────────────┐ ┌───────────────┐ ┌───────────────┐ ┌────────────┐ │
│ │ Session │ │ Context │ │ Session │ │ TTL │ │
│ │ Store │ │ Compressor │ │ Router │ │ Manager │ │
│ └─────────────┘ └───────────────┘ └───────────────┘ └────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ Layer 3: 路由编排层 (Routing & Orchestration Layer) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Intent │ │ Task │ │ Memory │ │ Safety │ │
│ │ Detector │ │ Orchestrator │ │ Scheduler │ │ Sandbox │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ Layer 2: 工具调度层 (Tool Orchestration Layer) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Tool │ │ Permission │ │ Tool │ │ Audit │ │
│ │ Registry │ │ Checker │ │ Dispatcher │ │ Logger │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ Layer 1: 基础设施层 (Infrastructure Layer) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Cache │ │ Queue │ │ Storage │ │ Config │ │ Monitor │ │
│ │ (Redis) │ │ (Kafka) │ │ (S3/DB) │ │ Center │ │ Agent │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘

各层职责详解

Layer 6 — 接入层:负责网络协议的接入与连接管理。支持 HTTP/REST、gRPC、WebSocket 三种主要协议,同时提供 Webhook 回调端点和 SDK 直连通道。接入层实施 TLS 终止、连接池管理、请求大小限制等基础网络策略。

Layer 5 — 协议适配层:将各渠道的私有协议适配为 Gateway 内部统一消息格式。每个适配器实现标准的 ChannelAdapter SPI 接口,负责入站消息解析和出站消息格式化。适配器之间完全隔离,互不影响。

Layer 4 — 会话管理层:维护多轮对话的上下文状态。Session Store 负责会话数据的持久化与缓存;Context Compressor 在上下文接近 Token 预算时执行压缩;Session Router 根据会话 ID 将请求路由到正确的处理节点;TTL Manager 管理会话的生命周期过期。

Layer 3 — 路由编排层:Gateway 的大脑。Intent Detector 识别用户意图;Task Orchestrator 根据意图编排任务执行计划;Memory Scheduler 决定何时读写记忆;Safety Sandbox 为高风险操作提供隔离执行环境。

Layer 2 — 工具调度层:管理所有可用工具的注册、发现、调度与审计。Tool Registry 维护工具元数据;Permission Checker 校验调用权限;Tool Dispatcher 执行实际工具调用;Audit Logger 记录全量调用日志。

Layer 1 — 基础设施层:为上层提供分布式缓存、消息队列、对象存储、配置管理和监控代理等基础能力。

1.5 技术选型概览

组件技术选型选型理由
开发语言 Rust (核心) + Python (工具层) Rust 提供内存安全与零成本抽象;Python 提供工具生态兼容性
异步运行时 Tokio 业界成熟的 Rust 异步运行时,支持高并发 IO
通信协议 gRPC (内部) + HTTP/2 (外部) gRPC 提供高效内部通信;HTTP/2 兼容外部客户端
序列化 Protocol Buffers 高效二进制序列化,跨语言支持
会话存储 Redis Cluster 亚毫秒级读写,支持持久化与集群模式
消息队列 Apache Kafka 高吞吐量、持久化、支持精确一次投递
配置中心 etcd 强一致性 KV 存储,支持 Watch 机制
可观测性 OpenTelemetry + Jaeger + Prometheus CNCF 标准,全链路追踪+指标+日志
沙箱引擎 Firecracker microVM AWS 开源,毫秒级启动,强隔离性
工具协议 MCP (Model Context Protocol) Anthropic 提出的开放标准,生态丰富

第二章 会话管理器深度解析

会话管理器是 Gateway 中最核心的组件之一,它承担着维护多轮对话上下文完整性、保障会话隔离性、优化上下文窗口利用率的关键职责。在一个典型的 Agent 系统中,会话管理的质量直接决定了用户交互的连贯性和推理质量的上限。

2.1 多轮对话上下文维护

2.1.1 上下文数据模型

Hermes Agent 的会话上下文采用分层结构进行组织,每一层承载不同粒度的信息:

/// 会话上下文的核心数据结构
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionContext {
/// 会话唯一标识
pub session_id: SessionId,
/// 用户标识
pub user_id: UserId,
/// 渠道来源
pub channel: ChannelType,
/// 会话创建时间
pub created_at: DateTime<Utc>,
/// 最后活跃时间
pub last_active_at: DateTime<Utc>,
/// 会话状态
pub status: SessionStatus,
/// 对话消息序列(按时间排序)
pub messages: Vec<Message>,
/// 会话级元数据
pub metadata: SessionMetadata,
/// 压缩后的摘要(当上下文被压缩时填充)
pub compressed_summary: Option<Summary>,
/// 活跃工具调用栈
pub active_tool_calls: Vec<ToolCallContext>,
/// 会话级记忆引用
pub memory_refs: Vec<MemoryRef>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message {
/// 消息唯一ID
pub message_id: MessageId,
/// 消息角色:user / assistant / system / tool
pub role: MessageRole,
/// 消息内容(支持多模态)
pub content: MessageContent,
/// 消息时间戳
pub timestamp: DateTime<Utc>,
/// 消息元数据(模型ID、Token消耗、延迟等)
pub metadata: MessageMetadata,
/// 关联的工具调用(如果有)
pub tool_calls: Vec<ToolCall>,
/// 消息状态
pub status: MessageStatus,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MessageContent {
/// 纯文本
Text(String),
/// 多模态内容(文本+图片+文件等)
Multimodal {
text: Option<String>,
images: Vec<ImageContent>,
files: Vec<FileContent>,
attachments: Vec<Attachment>,
},
/// 工具调用结果
ToolResult {
tool_call_id: String,
result: serde_json::Value,
is_error: bool,
},
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionMetadata {
/// 当前使用的模型
pub current_model: Option<String>,
/// 累计 Token 消耗
pub total_tokens_consumed: u64,
/// 对话轮次
pub turn_count: u32,
/// 会话主题(由意图识别自动提取)
pub topic: Option<String>,
/// 用户偏好(从记忆系统加载)
pub user_preferences: HashMap<String, serde_json::Value>,
/// 自定义标签
pub tags: Vec<String>,
}

2.1.2 上下文增量更新机制

每次消息交互后,会话上下文需要进行增量更新。Hermes Agent 采用 Write-Ahead Log (WAL) 机制保证上下文更新的原子性和持久性:

/// 会话上下文增量更新器
pub struct ContextUpdater {
session_store: Arc<SessionStore>,
wal: Arc<WriteAheadLog>,
compressor: Arc<ContextCompressor>,
}

impl ContextUpdater {
/// 追加新消息到会话上下文
pub async fn append_message(
&self,
session_id: &SessionId,
message: Message,
) -> Result<UpdateResult, SessionError> {
// 1. 写入 WAL,保证原子性
let wal_entry = WalEntry::AppendMessage {
session_id: session_id.clone(),
message: message.clone(),
timestamp: Utc::now(),
};
self.wal.append(&wal_entry).await?;

// 2. 更新内存中的会话上下文
let mut session = self.session_store.get(session_id).await?;
session.messages.push(message.clone());
session.metadata.turn_count += 1;
session.last_active_at = Utc::now();

// 3. 计算 Token 使用量
let token_count = self.estimate_tokens(&session);
let token_budget = self.get_token_budget(session_id).await?;

// 4. 判断是否需要压缩
let needs_compression = token_count as f64 / token_budget as f64
> self.compressor.compression_threshold;

if needs_compression {
let compressed = self.compressor.compress(&session).await?;
session.compressed_summary = Some(compressed.summary);
// 保留最近 N 条原始消息 + 压缩摘要
let retain_count = self.compressor.recent_message_window;
session.messages = session.messages.split_off(
session.messages.len().saturating_sub(retain_count)
);
}

// 5. 持久化到 Session Store
self.session_store.put(session_id, &session).await?;

// 6. 标记 WAL 条目为已完成
self.wal.mark_complete(&wal_entry).await?;

Ok(UpdateResult {
session_id: session_id.clone(),
token_count,
token_budget,
compressed: needs_compression,
turn_count: session.metadata.turn_count,
})
}
}

2.1.3 上下文版本控制

为了支持会话回滚和调试,会话管理器为每次上下文变更维护版本号:

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionVersion {
/// 版本号(单调递增)
pub version: u64,
/// 变更类型
pub change_type: ChangeType,
/// 变更时间
pub timestamp: DateTime<Utc>,
/// 变更前快照的引用(仅保留最近 N 个版本)
pub parent_version: Option<u64>,
}

pub enum ChangeType {
MessageAppended,
ContextCompressed,
ToolCallStarted,
ToolCallCompleted,
MetadataUpdated,
SessionResumed,
SessionForked,
}

/// 版本控制操作接口
pub trait SessionVersioning {
/// 获取当前版本
async fn current_version(&self, session_id: &SessionId) -> u64;

/// 回滚到指定版本
async fn rollback_to(
&self,
session_id: &SessionId,
target_version: u64,
) -> Result<SessionContext, SessionError>;

/// 创建会话分支(用于并行探索)
async fn fork(
&self,
session_id: &SessionId,
at_version: u64,
) -> Result<SessionId, SessionError>;

/// 获取变更历史
async fn changelog(
&self,
session_id: &SessionId,
from: Option<u64>,
to: Option<u64>,
) -> Vec<SessionVersion>;
}

上下文版本控制的核心应用场景包括:

  • 会话回滚:当 Agent 执行了错误的工具调用或产生了不理想的回答时,可以回滚到之前的上下文状态,重新执行
  • 并行探索:从同一会话节点分叉出多个分支,每个分支尝试不同的策略,最终选择最优结果合并回主线
  • 调试与审计:通过变更历史精确追踪会话上下文的演变过程,定位问题根因
  • A/B 测试:对同一用户输入,在不同版本的上下文上测试不同的模型或提示词策略
  • 2.2 会话 ID 路由机制

    2.2.1 会话 ID 生成策略

    会话 ID 是 Gateway 路由的核心标识。Hermes Agent 采用 Snowflake 变体 生成全局唯一、时间有序的会话 ID:

    /// 会话 ID 结构(128位)
    /// | 1位符号 | 41位时间戳 | 10位机器ID | 10位渠道ID | 66位序列号 |
    #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
    pub struct SessionId(u128);

    impl SessionId {
    pub fn new(machine_id: u16, channel_id: u16) -> Self {
    let timestamp = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap()
    .as_millis() as u64;

    let sequence = SEQUENCE_COUNTER.fetch_add(1, Ordering::SeqCst);

    let id = ((timestamp as u128) << 87)
    | ((machine_id as u128) << 77)
    | ((channel_id as u128) << 67)
    | (sequence & 0x7FFFFFFFFFFFFF);

    SessionId(id)
    }

    /// 从会话 ID 提取时间戳
    pub fn timestamp(&self) -> u64 {
    (self.0 >> 87) as u64
    }

    /// 从会话 ID 提取机器 ID(用于路由)
    pub fn machine_id(&self) -> u16 {
    ((self.0 >> 77) & 0x3FF) as u16
    }

    /// 从会话 ID 提取渠道 ID
    pub fn channel_id(&self) -> u16 {
    ((self.0 >> 67) & 0x3FF) as u16
    }
    }

    会话 ID 的设计要点:

    • 时间有序:41位时间戳使得会话 ID 天然按创建时间排序,有利于范围查询和时间窗口过滤
    • 机器可路由:10位机器 ID 直接编码在会话 ID 中,无需额外查表即可确定会话归属节点
    • 渠道可追溯:10位渠道 ID 使得从会话 ID 即可识别来源渠道,便于渠道维度的统计和故障排查
    • 序列号充足:66位序列号理论上支持每毫秒 2^66 个会话创建,永远不会溢出

    2.2.2 一致性哈希路由

    Gateway 集群中,会话数据分布在多个节点上。采用一致性哈希环确保会话亲和性——同一个会话的请求始终路由到同一节点:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 一致性哈希路由环 │
    │ │
    │ 0 / 2^128 │
    │ │ │
    │ Node A ●──────┼──────● Node D │
    │ (hash:0x3F..) │ (hash:0xC0..) │
    │ ╱ │ ╲ │
    │ ╱ │ ╲ │
    │ ╱ │ ╲ │
    │ Node B ●─────╱ │ ╲─────● Node C │
    │ (hash:0x10..) │ (hash:0x80..) │
    │ │ │
    │ Session ID hash ──> 沿顺时针找到的第一个节点 │
    │ │
    │ 虚拟节点 (Virtual Nodes): │
    │ Node A: v1, v2, v3, v4, v5 (5个虚拟节点) │
    │ Node B: v1, v2, v3, v4, v5 (5个虚拟节点) │
    │ Node C: v1, v2, v3, v4, v5 (5个虚拟节点) │
    │ Node D: v1, v2, v3, v4, v5 (5个虚拟节点) │
    │ │
    │ 故障转移: Node B 宕机 → 其虚拟节点顺时针迁移到 Node C 和 Node D │
    └──────────────────────────────────────────────────────────────────────┘

    /// 一致性哈希路由器
    pub struct ConsistentHashRouter {
    /// 哈希环:SortedMap<Hash, NodeId>
    ring: Arc<RwLock<BTreeMap<u128, NodeId>>>,
    /// 虚拟节点数
    virtual_nodes: usize,
    /// 节点列表
    nodes: Arc<RwLock<HashMap<NodeId, NodeInfo>>>,
    }

    impl ConsistentHashRouter {
    /// 根据会话 ID 路由到目标节点
    pub async fn route(&self, session_id: &SessionId) -> NodeId {
    let ring = self.ring.read().await;
    let hash = self.hash(session_id);

    // 沿顺时针找到第一个节点
    match ring.range(hash..).next() {
    Some((_, node_id)) => *node_id,
    None => {
    // 环回到第一个节点
    ring.iter().next().map(|(_, id)| *id).unwrap()
    }
    }
    }

    /// 获取会话的备用节点(用于故障转移)
    pub async fn get_backup_nodes(
    &self,
    session_id: &SessionId,
    count: usize,
    ) -> Vec<NodeId> {
    let ring = self.ring.read().await;
    let hash = self.hash(session_id);

    let mut backups = Vec::new();
    let mut seen = HashSet::new();

    for (_, node_id) in ring.range(hash..).chain(ring.iter()) {
    if !seen.contains(node_id) {
    seen.insert(*node_id);
    backups.push(*node_id);
    if backups.len() >= count {
    break;
    }
    }
    }
    backups
    }

    /// 节点加入集群
    pub async fn add_node(&self, node_id: NodeId, node_info: NodeInfo) {
    let mut ring = self.ring.write().await;
    let mut nodes = self.nodes.write().await;

    nodes.insert(node_id, node_info);

    // 添加虚拟节点
    for i in 0..self.virtual_nodes {
    let vhash = self.hash_with_suffix(&node_id, i);
    ring.insert(vhash, node_id);
    }
    }

    /// 节点离开集群
    pub async fn remove_node(&self, node_id: NodeId) {
    let mut ring = self.ring.write().await;
    let mut nodes = self.nodes.write().await;

    nodes.remove(&node_id);

    let to_remove: Vec<u128> = ring
    .iter()
    .filter(|(_, id)| **id == node_id)
    .map(|(h, _)| *h)
    .collect();

    for h in to_remove {
    ring.remove(&h);
    }
    }
    }

    一致性哈希路由的核心优势在于最小化节点变更时的数据迁移。当集群中增加或减少一个节点时,平均只有 1/N(N为节点数)的会话需要迁移,而非全部重新分布。每个物理节点配置 5-10 个虚拟节点,可以进一步均衡数据分布,避免热点问题。

    2.2.3 会话亲和性与跨节点迁移

    当持有某会话的节点发生故障时,Gateway 需要将该会话迁移到备用节点。迁移过程需要保证三个关键属性:

  • 零丢失:会话数据必须完整迁移,包括所有消息历史和元数据
  • 最小中断:迁移期间的请求被暂存,迁移完成后立即处理
  • 幂等性:迁移过程可安全重试,不会产生重复数据
  • /// 会话迁移管理器
    pub struct SessionMigrationManager {
    router: Arc<ConsistentHashRouter>,
    session_store: Arc<SessionStore>,
    migration_queue: Arc<MigrationQueue>,
    }

    impl SessionMigrationManager {
    /// 执行会话迁移
    pub async fn migrate_session(
    &self,
    session_id: &SessionId,
    from_node: NodeId,
    to_node: NodeId,
    ) -> Result<MigrationResult, MigrationError> {
    tracing::info!(
    session_id = %session_id,
    from = %from_node,
    to = %to_node,
    "Starting session migration"
    );

    // 1. 在源节点标记会话为「迁移中」状态
    self.session_store
    .set_migration_state(session_id, MigrationState::InProgress)
    .await?;

    // 2. 从源节点读取完整会话数据
    let session_data = self.session_store
    .get_full_session(session_id)
    .await?;

    // 3. 将会话数据写入目标节点
    self.session_store
    .put_to_node(&to_node, session_id, &session_data)
    .await?;

    // 4. 验证数据完整性
    let verify_data = self.session_store
    .get_from_node(&to_node, session_id)
    .await?;

    if verify_data.version != session_data.version {
    return Err(MigrationError::VerificationFailed);
    }

    // 5. 更新路由表
    self.router
    .update_session_route(session_id, to_node)
    .await?;

    // 6. 从源节点删除旧数据
    self.session_store
    .delete_from_node(&from_node, session_id)
    .await?;

    // 7. 标记迁移完成
    self.session_store
    .set_migration_state(session_id, MigrationState::Completed)
    .await?;

    tracing::info!(
    session_id = %session_id,
    "Session migration completed successfully"
    );

    Ok(MigrationResult {
    session_id: session_id.clone(),
    from_node,
    to_node,
    duration: session_data.messages.len() as u64,
    })
    }
    }

    会话迁移的触发场景包括:

    触发条件迁移策略预期中断时间
    节点硬件故障 自动故障转移,从 Redis 副本恢复 < 3秒
    节点滚动升级 优雅迁移,先迁移后下线 < 1秒
    集群扩容 新节点加入触发数据再平衡 0(后台异步)
    负载不均 热点会话迁移到空闲节点 < 2秒
    手动运维 管理员触发指定会话迁移 < 1秒

    2.3 上下文窗口压缩策略

    随着对话轮次增加,上下文 Token 数会逐渐逼近甚至超出模型的最大上下文窗口。Hermes Agent 实现了多级压缩策略,在保留关键信息的同时控制 Token 消耗。

    2.3.1 压缩策略总览

    ┌─────────────────────────────────────────────────────────────────────┐
    │ 上下文压缩策略决策树 │
    │ │
    │ 当前 Token 数 │
    │ │ │
    │ ┌───────────┴───────────┐ │
    │ │ │ │
    │ < 50% 预算 > 50% 预算 │
    │ │ │ │
    │ 不压缩 ┌─────────┴─────────┐ │
    │ │ │ │
    │ < 75% 预算 > 75% 预算 │
    │ │ │ │
    │ 轻度压缩 ┌────┴────┐ │
    │ (消息裁剪) < 90% > 90% │
    │ │ │ │ │
    │ │ 中度压缩 重度压缩 │
    │ │ (摘要+裁剪) (全量摘要+ │
    │ │ │ 仅保留最近3轮) │
    │ v v v │
    │ ┌──────────────────────────────────┐ │
    │ │ 压缩后 Token < 50% 预算 │ │
    │ └──────────────────────────────────┘ │
    └─────────────────────────────────────────────────────────────────────┘

    2.3.2 滑动窗口摘要压缩算法

    这是 Hermes Agent 的核心压缩算法,采用滑动窗口 + 渐进式摘要的方式:

    /// 上下文压缩器
    pub struct ContextCompressor {
    /// 压缩阈值(占预算的比例)
    pub compression_threshold: f64,
    /// 保留最近消息的窗口大小
    pub recent_message_window: usize,
    /// 摘要模型
    pub summary_model: String,
    /// Token 估算器
    pub token_estimator: TokenEstimator,
    }

    #[derive(Clone, Debug, PartialEq)]
    pub enum CompressionStrategy {
    Light,
    Medium,
    Heavy,
    None,
    }

    impl ContextCompressor {
    /// 执行上下文压缩
    pub async fn compress(
    &self,
    session: &SessionContext,
    ) -> Result<CompressionResult, CompressionError> {
    let total_tokens = self.token_estimator.estimate_session(session);
    let budget = self.token_estimator.get_budget(&session.metadata.current_model);

    if (total_tokens as f64 / budget as f64) < self.compression_threshold {
    return Ok(CompressionResult::NoCompressionNeeded);
    }

    // 根据压缩级别选择策略
    let ratio = total_tokens as f64 / budget as f64;
    let strategy = if ratio < 0.75 {
    CompressionStrategy::Light
    } else if ratio < 0.90 {
    CompressionStrategy::Medium
    } else {
    CompressionStrategy::Heavy
    };

    match strategy {
    CompressionStrategy::Light => self.light_compress(session).await,
    CompressionStrategy::Medium => self.medium_compress(session).await,
    CompressionStrategy::Heavy => self.heavy_compress(session).await,
    CompressionStrategy::None => Ok(CompressionResult::NoCompressionNeeded),
    }
    }

    /// 轻度压缩:裁剪工具调用的详细输出
    async fn light_compress(
    &self,
    session: &SessionContext,
    ) -> Result<CompressionResult, CompressionError> {
    let mut compressed_messages = Vec::new();
    let mut tokens_saved = 0u64;

    for msg in &session.messages {
    if msg.role == MessageRole::Tool {
    // 将工具调用的详细输出替换为摘要
    let summary = self.summarize_tool_output(&msg.content).await?;
    tokens_saved += self.token_estimator.estimate_message(msg)
    self.token_estimator.estimate_text(&summary);

    compressed_messages.push(Message {
    content: MessageContent::Text(summary),
    metadata: msg.metadata.clone(),
    ..msg.clone()
    });
    } else {
    compressed_messages.push(msg.clone());
    }
    }

    Ok(CompressionResult::Compressed {
    strategy: CompressionStrategy::Light,
    messages: compressed_messages,
    tokens_saved,
    })
    }

    /// 中度压缩:历史消息摘要 + 保留最近N轮
    async fn medium_compress(
    &self,
    session: &SessionContext,
    ) -> Result<CompressionResult, CompressionError> {
    let split_point = session.messages.len()
    .saturating_sub(self.recent_message_window);

    // 将早期消息生成摘要
    let old_messages = &session.messages[..split_point];
    let recent_messages = &session.messages[split_point..];

    let summary = self.generate_summary(old_messages).await?;

    let mut compressed = vec![Message::system(summary)];
    compressed.extend_from_slice(recent_messages);

    let tokens_saved = self.token_estimator.estimate_messages(old_messages)
    self.token_estimator.estimate(&compressed[0]);

    Ok(CompressionResult::Compressed {
    strategy: CompressionStrategy::Medium,
    messages: compressed,
    tokens_saved,
    })
    }

    /// 重度压缩:全量摘要 + 仅保留最近3轮
    async fn heavy_compress(
    &self,
    session: &SessionContext,
    ) -> Result<CompressionResult, CompressionError> {
    // 保留最近3轮对话(6条消息:3 user + 3 assistant)
    let retain = 6.min(session.messages.len());
    let split_point = session.messages.len() retain;

    let old_messages = &session.messages[..split_point];
    let recent_messages = &session.messages[split_point..];

    // 如果已有之前的摘要,将旧摘要 + 新消息一起压缩
    let summary_input = if let Some(prev_summary) = &session.compressed_summary {
    self.merge_summary(prev_summary, old_messages).await?
    } else {
    self.generate_detailed_summary(old_messages).await?
    };

    let mut compressed = vec![Message::system(summary_input)];
    compressed.extend_from_slice(recent_messages);

    Ok(CompressionResult::Compressed {
    strategy: CompressionStrategy::Heavy,
    messages: compressed,
    tokens_saved: self.token_estimator.estimate_session(session)
    self.token_estimator.estimate_messages(&compressed),
    })
    }

    /// 使用 LLM 生成对话摘要
    async fn generate_summary(
    &self,
    messages: &[Message],
    ) -> Result<String, CompressionError> {
    let prompt = format!(
    r#"请将以下对话历史压缩为简洁的摘要,保留:
    1. 用户的核心需求和目标
    2. 已经做出的关键决策
    3. 重要的上下文信息(人名、技术栈、约束条件等)
    4. 未解决的问题和待办事项

    对话历史:
    {}

    摘要:"#,
    messages.iter()
    .map(|m| format!("[{}]: {}", m.role,
    m.content.as_text().unwrap_or_default()))
    .collect::<Vec<_>>()
    .join("\\n")
    );

    let summary = self.llm_call(&self.summary_model, &prompt).await?;
    Ok(format!("[对话历史摘要]\\n{}", summary))
    }

    /// 合并已有摘要与新消息(增量摘要)
    async fn merge_summary(
    &self,
    prev_summary: &Summary,
    new_messages: &[Message],
    ) -> Result<String, CompressionError> {
    let prompt = format!(
    r#"以下是之前的对话摘要和新增的对话内容,请合并为一个更新的摘要:

    【已有摘要】
    {}

    【新增对话】
    {}

    【更新后的摘要】"#,
    prev_summary.text,
    new_messages.iter()
    .map(|m| format!("[{}]: {}", m.role,
    m.content.as_text().unwrap_or_default()))
    .collect::<Vec<_>>()
    .join("\\n")
    );

    let merged = self.llm_call(&self.summary_model, &prompt).await?;
    Ok(format!("[对话历史摘要(更新)]\\n{}", merged))
    }
    }

    2.3.3 压缩效果评估

    以下是不同压缩策略在实测中的效果对比:

    压缩策略触发条件Token 压缩率信息保留率压缩延迟适用场景
    轻度压缩 50%-75% 预算 15%-25% 95%+ <100ms 工具调用较多的场景
    中度压缩 75%-90% 预算 40%-60% 85%-90% 500ms-2s 长对话(>20轮)
    重度压缩 >90% 预算 70%-85% 70%-80% 1s-3s 超长对话(>50轮)
    不压缩 <50% 预算 0% 100% 0ms 短对话(<10轮)

    信息保留率的衡量方式:压缩前后,使用同一组测试问题评估 Agent 回答的准确率,以未压缩版本的准确率为基准计算保留率。

    2.3.4 压缩策略的选择考量

    压缩策略的选择并非简单的阈值触发,还需要综合考虑以下因素:

    /// 压缩决策上下文
    pub struct CompressionDecisionContext {
    /// 当前 Token 使用量
    pub current_tokens: u64,
    /// 模型上下文窗口大小
    pub context_window: u64,
    /// 对话轮次
    pub turn_count: u32,
    /// 工具调用密度(最近10轮中工具调用的比例)
    pub tool_call_density: f64,
    /// 用户情绪指标(基于最近消息的语义分析)
    pub user_sentiment: Sentiment,
    /// 会话重要性评分
    pub session_importance: f64,
    /// 可用压缩预算(时间)
    pub time_budget: Duration,
    }

    /// 智能压缩策略选择器
    pub fn select_strategy(ctx: &CompressionDecisionContext) -> CompressionStrategy {
    let ratio = ctx.current_tokens as f64 / ctx.context_window as f64;

    // 如果时间预算紧张,选择最快的策略
    if ctx.time_budget < Duration::from_millis(200) {
    return if ratio > 0.9 {
    CompressionStrategy::Medium
    } else {
    CompressionStrategy::Light
    };
    }

    // 如果用户情绪消极(不耐烦),减少压缩延迟
    if ctx.user_sentiment == Sentiment::Negative && ratio < 0.9 {
    return CompressionStrategy::Light;
    }

    // 高重要性会话,使用更保守的压缩
    if ctx.session_importance > 0.8 && ratio < 0.85 {
    return CompressionStrategy::Light;
    }

    // 标准决策路径
    if ratio < 0.5 {
    CompressionStrategy::None
    } else if ratio < 0.75 {
    CompressionStrategy::Light
    } else if ratio < 0.90 {
    CompressionStrategy::Medium
    } else {
    CompressionStrategy::Heavy
    }
    }

    2.4 并行会话隔离方案

    2.4.1 隔离架构

    Hermes Agent 支持同一用户同时持有多个活跃会话,每个会话拥有独立的上下文空间,互不干扰:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 并行会话隔离架构 │
    │ │
    │ ┌──────────────┐ │
    │ │ User A │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ┌────────────┼────────────┐ │
    │ │ │ │ │
    │ Session #1 Session #2 Session #3 │
    │ (飞书渠道) (Web渠道) (API渠道) │
    │ │ │ │ │
    │ ┌────────┴───┐ ┌────┴─────┐ ┌───┴────────┐ │
    │ │ Context │ │ Context │ │ Context │ │
    │ │ · messages │ │ · messages│ │ · messages │ │
    │ │ · tools │ │ · tools │ │ · tools │ │
    │ │ · metadata │ │ · metadata│ │ · metadata │ │
    │ └────────────┘ └──────────┘ └────────────┘ │
    │ │ │ │ │
    │ └────────────┼────────────┘ │
    │ │ │
    │ ┌──────┴───────┐ │
    │ │ 共享记忆层 │ │
    │ │ (User Level) │ │
    │ │ · 用户画像 │ │
    │ │ · 长期记忆 │ │
    │ │ · 偏好设置 │ │
    │ └──────────────┘ │
    └──────────────────────────────────────────────────────────────────────┘

    会话隔离的核心设计原则:

    • 上下文隔离:每个会话拥有独立的消息序列、工具调用栈和元数据,一个会话的上下文变更不会影响其他会话
    • 记忆共享:用户级长期记忆(用户画像、偏好设置、历史决策)在所有会话间共享,但通过只读快照方式访问,避免并发写入冲突
    • 资源隔离:每个会话有独立的 Token 配额、工具调用配额和速率限制,防止单个会话耗尽用户资源
    • 显式共享:会话间的信息共享是显式操作而非自动行为,用户或系统必须明确指定要共享的消息和上下文

    2.4.2 隔离实现

    /// 会话隔离管理器
    pub struct SessionIsolationManager {
    /// 用户会话映射:UserId -> Set<SessionId>
    user_sessions: Arc<RwLock<HashMap<UserId, HashSet<SessionId>>>>,
    /// 会话上下文存储(按 SessionId 隔离)
    session_contexts: Arc<SessionStore>,
    /// 用户级共享记忆
    user_memory: Arc<UserMemoryStore>,
    /// 每用户最大并发会话数
    max_sessions_per_user: usize,
    }

    impl SessionIsolationManager {
    /// 创建新会话(检查并发限制)
    pub async fn create_session(
    &self,
    user_id: &UserId,
    channel: ChannelType,
    ) -> Result<SessionId, IsolationError> {
    let mut user_sessions = self.user_sessions.write().await;

    let active_count = user_sessions
    .get(user_id)
    .map(|s| s.len())
    .unwrap_or(0);

    if active_count >= self.max_sessions_per_user {
    return Err(IsolationError::MaxSessionsExceeded {
    user_id: user_id.clone(),
    current: active_count,
    max: self.max_sessions_per_user,
    });
    }

    let session_id = SessionId::new(
    self.get_machine_id(),
    channel.as_u16(),
    );

    // 创建隔离的上下文空间
    let context = SessionContext::new(
    session_id.clone(),
    user_id.clone(),
    channel,
    );

    self.session_contexts.put(&session_id, &context).await?;

    user_sessions
    .entry(user_id.clone())
    .or_insert_with(HashSet::new)
    .insert(session_id.clone());

    Ok(session_id)
    }

    /// 获取会话上下文(确保隔离性)
    pub async fn get_context(
    &self,
    session_id: &SessionId,
    ) -> Result<SessionContext, IsolationError> {
    // 直接从隔离存储获取,不经过用户级路由
    self.session_contexts
    .get(session_id)
    .await?
    .ok_or(IsolationError::SessionNotFound(session_id.clone()))
    }

    /// 获取用户级共享记忆(所有会话可读)
    pub async fn get_user_memory(
    &self,
    user_id: &UserId,
    ) -> Result<UserMemory, IsolationError> {
    self.user_memory.get(user_id).await
    }

    /// 会话间信息共享(显式操作,非自动)
    pub async fn share_context(
    &self,
    from_session: &SessionId,
    to_session: &SessionId,
    message_ids: &[MessageId],
    ) -> Result<(), IsolationError> {
    // 验证两个会话属于同一用户
    let from_ctx = self.session_contexts.get(from_session).await?.unwrap();
    let to_ctx = self.session_contexts.get(to_session).await?.unwrap();

    if from_ctx.user_id != to_ctx.user_id {
    return Err(IsolationError::CrossUserSharing {
    from_user: from_ctx.user_id,
    to_user: to_ctx.user_id,
    });
    }

    // 仅复制指定消息到目标会话
    let messages_to_share: Vec<Message> = from_ctx.messages
    .iter()
    .filter(|m| message_ids.contains(&m.message_id))
    .cloned()
    .collect();

    let mut to_ctx = to_ctx;
    for msg in messages_to_share {
    // 添加来源标记
    let shared_msg = Message {
    metadata: MessageMetadata {
    shared_from: Some(from_session.clone()),
    ..msg.metadata
    },
    ..msg
    };
    to_ctx.messages.push(shared_msg);
    }

    self.session_contexts.put(to_session, &to_ctx).await?;
    Ok(())
    }
    }

    2.4.3 资源配额管理

    为防止单个用户或会话消耗过多资源,Gateway 实现了多维度资源配额管理:

    # 资源配额配置示例
    quota:
    per_user:
    max_active_sessions: 5
    max_requests_per_minute: 120
    max_tokens_per_hour: 500000
    max_tool_calls_per_day: 1000

    per_session:
    max_messages: 500
    max_tokens: 100000
    max_tool_calls: 50
    max_duration: 24h

    per_channel:
    web:
    max_concurrent_sessions_per_user: 3
    feishu:
    max_concurrent_sessions_per_user: 2
    api:
    max_concurrent_sessions_per_user: 10
    max_rps: 50

    enforcement:
    strategy: reject # reject | queue | degrade
    queue_timeout: 30s
    degrade_message: "当前请求量较大,已切换到简化模式"

    配额管理的执行策略有三种模式:

    • reject(拒绝):超出配额的请求直接返回 429 错误,客户端需要自行处理重试
    • queue(排队):超出配额的请求进入等待队列,在超时时间内有配额释放则继续处理
    • degrade(降级):超出配额后自动降级服务——使用更小的模型、减少工具调用、跳过记忆检索等

    2.5 会话生命周期管理

    2.5.1 生命周期状态机

    ┌───────────┐
    创建 ────> │ Active │ <──── 恢复
    └─────┬─────┘

    ┌───────────┼───────────┐
    │ │ │
    v v v
    ┌──────────┐ ┌──────────┐ ┌──────────┐
    │ Idle │ │ Paused │ │ Migrating│
    │ (超时等待) │ │ (用户暂停) │ │ (节点迁移) │
    └────┬─────┘ └────┬─────┘ └────┬─────┘
    │ │ │
    │ 恢复 <───┘ 完成 ───┘
    │ │ │
    v v v
    ┌──────────┐ ┌──────────┐ ┌──────────┐
    │ Expiring │ │ Active │ │ Active │
    │ (即将过期) │ │ │ │ │
    └─────┬────┘ └──────────┘ └──────────┘

    v
    ┌──────────┐
    │ Archived │
    │ (已归档) │
    └─────┬────┘

    v
    ┌──────────┐
    │ Deleted │
    │ (已删除) │
    └──────────┘

    /// 会话生命周期状态
    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    pub enum SessionStatus {
    /// 活跃状态,正在处理请求
    Active,
    /// 空闲状态,等待用户输入
    Idle { idle_since: DateTime<Utc> },
    /// 暂停状态,用户主动暂停
    Paused { paused_at: DateTime<Utc> },
    /// 迁移中状态,正在跨节点迁移
    Migrating { from_node: NodeId, to_node: NodeId },
    /// 即将过期
    Expiring { expire_at: DateTime<Utc> },
    /// 已归档(从热存储转移到冷存储)
    Archived,
    /// 已删除
    Deleted,
    }

    /// 会话生命周期管理器
    pub struct SessionLifecycleManager {
    /// 空闲超时(自动转为 Idle)
    idle_timeout: Duration,
    /// 过期时间(Idle 后多久转为 Expiring)
    expire_timeout: Duration,
    /// 归档时间(Expiring 后多久转为 Archived)
    archive_timeout: Duration,
    /// 删除时间(Archived 后多久物理删除)
    delete_timeout: Duration,
    /// 热存储
    hot_store: Arc<HotSessionStore>,
    /// 冷存储
    cold_store: Arc<ColdSessionStore>,
    }

    impl SessionLifecycleManager {
    /// 定时扫描会话状态,执行状态转换
    pub async fn run_lifecycle_scan(&self) {
    let mut interval = tokio::time::interval(Duration::from_secs(30));

    loop {
    interval.tick().await;

    // 1. 扫描 Active 会话,检查是否空闲
    let idle_sessions = self.hot_store
    .find_sessions_idle_before(Utc::now() self.idle_timeout)
    .await;
    for session_id in idle_sessions {
    self.transition_to_idle(&session_id).await;
    }

    // 2. 扫描 Idle 会话,检查是否过期
    let expiring_sessions = self.hot_store
    .find_sessions_idle_before(Utc::now() self.expire_timeout)
    .await;
    for session_id in expiring_sessions {
    self.transition_to_expiring(&session_id).await;
    }

    // 3. 扫描 Expiring 会话,执行归档
    let archive_sessions = self.hot_store
    .find_sessions_expiring_before(Utc::now())
    .await;
    for session_id in archive_sessions {
    self.archive_session(&session_id).await;
    }

    // 4. 扫描 Archived 会话,执行物理删除
    let delete_sessions = self.cold_store
    .find_sessions_archived_before(Utc::now() self.delete_timeout)
    .await;
    for session_id in delete_sessions {
    self.delete_session(&session_id).await;
    }
    }
    }

    /// 恢复已归档的会话
    pub async fn restore_session(
    &self,
    session_id: &SessionId,
    ) -> Result<SessionContext, LifecycleError> {
    // 1. 从冷存储读取会话数据
    let session = self.cold_store
    .get(session_id)
    .await?
    .ok_or(LifecycleError::SessionNotFound)?;

    // 2. 写入热存储
    self.hot_store.put(session_id, &session).await?;

    // 3. 更新状态为 Active
    let mut session = session;
    session.status = SessionStatus::Active;
    session.last_active_at = Utc::now();

    Ok(session)
    }
    }

    2.5.2 冷热分离存储策略

    会话数据在不同生命周期阶段存储在不同介质中,以平衡访问速度和存储成本:

    存储层存储介质访问延迟存储成本保留时长适用状态
    热存储 Redis Cluster <1ms 实时 Active, Idle
    温存储 PostgreSQL <10ms 7天 Expiring
    冷存储 S3 对象存储 <500ms 90天 Archived
    删除 Deleted

    冷热分离的关键设计点:

    • 异步迁移:会话从热存储到冷存储的迁移是异步操作,不阻塞主流程
    • 透明恢复:当用户恢复已归档会话时,Gateway 自动从冷存储加载数据到热存储,对用户透明
    • 预加载:系统可以基于用户行为预测,提前将可能恢复的归档会话预加载到热存储
    • 渐进式删除:删除操作先标记为软删除,保留 24 小时的恢复窗口后才执行物理删除

    第三章 渠道适配器架构

    渠道适配器是 Gateway 与外部世界沟通的桥梁。它将各平台千差万别的消息协议统一翻译为 Gateway 内部标准格式,使得核心处理逻辑完全无需感知渠道差异。这一层的设计质量直接决定了系统的可扩展性和多渠道接入效率。

    3.1 适配器模式设计

    3.1.1 核心接口定义

    Hermes Agent 定义了一套统一的渠道适配器 SPI(Service Provider Interface),所有渠道适配器必须实现该接口:

    /// 渠道适配器核心接口
    #[async_trait]
    pub trait ChannelAdapter: Send + Sync {
    /// 适配器名称
    fn name(&self) -> &str;

    /// 支持的渠道类型
    fn channel_type(&self) -> ChannelType;

    /// 初始化适配器
    async fn initialize(&mut self, config: &AdapterConfig)
    -> Result<(), AdapterError>;

    /// 启动适配器(开始接收消息)
    async fn start(&mut self) -> Result<(), AdapterError>;

    /// 停止适配器
    async fn stop(&mut self) -> Result<(), AdapterError>;

    // ========== 入站消息处理 ==========

    /// 接收外部消息,转换为内部标准格式
    async fn receive_message(
    &self,
    raw_message: RawMessage,
    ) -> Result<InboundMessage, AdapterError>;

    /// 验证消息来源合法性(签名校验等)
    async fn verify_message(
    &self,
    raw_message: &RawMessage,
    ) -> Result<bool, AdapterError>;

    // ========== 出站消息处理 ==========

    /// 将内部标准格式消息转换为目标渠道格式
    async fn send_message(
    &self,
    message: OutboundMessage,
    ) -> Result<DeliveryReceipt, AdapterError>;

    /// 发送流式消息(SSE/WebSocket)
    async fn send_stream(
    &self,
    stream: BoxStream<'static, StreamChunk>,
    target: &str,
    ) -> Result<(), AdapterError>;

    // ========== 渠道能力查询 ==========

    /// 查询渠道支持的能力
    fn capabilities(&self) -> ChannelCapabilities;

    /// 健康检查
    async fn health_check(&self) -> Result<HealthStatus, AdapterError>;
    }

    渠道能力描述结构体定义了每个平台支持的功能特性,适配器在初始化时声明自身能力,Gateway 据此做能力感知路由:

    /// 渠道能力描述
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ChannelCapabilities {
    /// 支持文本消息
    pub text: bool,
    /// 支持图片消息
    pub image: bool,
    /// 支持文件消息
    pub file: bool,
    /// 支持音频消息
    pub audio: bool,
    /// 支持视频消息
    pub video: bool,
    /// 支持流式输出
    pub streaming: bool,
    /// 支持卡片消息
    pub card: bool,
    /// 支持按钮交互
    pub buttons: bool,
    /// 支持Markdown
    pub markdown: bool,
    /// 最大消息长度
    pub max_message_length: usize,
    /// 支持消息编辑
    pub edit: bool,
    /// 支持消息删除
    pub delete: bool,
    /// 支持消息引用回复
    pub reply: bool,
    /// 支持At提及
    pub mention: bool,
    }

    3.1.2 适配器注册与加载

    适配器通过工厂模式创建实例,工厂在 Gateway 启动时自动注册:

    /// 适配器注册中心
    pub struct AdapterRegistry {
    /// 已注册的适配器工厂
    factories: RwLock<HashMap<ChannelType, Box<dyn AdapterFactory>>>,
    /// 活跃的适配器实例
    instances: RwLock<HashMap<ChannelType, Arc<dyn ChannelAdapter>>>,
    /// 配置中心
    config_source: Arc<dyn ConfigSource>,
    }

    #[async_trait]
    pub trait AdapterFactory: Send + Sync {
    /// 创建适配器实例
    fn create(&self) -> Box<dyn ChannelAdapter>;
    /// 适配器类型标识
    fn channel_type(&self) -> ChannelType;
    }

    impl AdapterRegistry {
    /// 注册适配器工厂
    pub async fn register_factory(
    &self,
    factory: Box<dyn AdapterFactory>,
    ) -> Result<(), RegistryError> {
    let channel_type = factory.channel_type();
    let mut factories = self.factories.write().await;
    factories.insert(channel_type, factory);
    tracing::info!(channel = ?channel_type, "Adapter factory registered");
    Ok(())
    }

    /// 获取或创建适配器实例
    pub async fn get_or_create(
    &self,
    channel_type: &ChannelType,
    ) -> Result<Arc<dyn ChannelAdapter>, RegistryError> {
    // 先检查是否有活跃实例
    {
    let instances = self.instances.read().await;
    if let Some(instance) = instances.get(channel_type) {
    return Ok(instance.clone());
    }
    }

    // 创建新实例
    let factories = self.factories.read().await;
    let factory = factories.get(channel_type)
    .ok_or(RegistryError::FactoryNotFound(*channel_type))?;

    let mut adapter = factory.create();
    let config = self.config_source
    .get_adapter_config(channel_type)
    .await?;

    adapter.initialize(&config).await?;
    adapter.start().await?;

    let adapter: Arc<dyn ChannelAdapter> = Arc::from(adapter);
    let mut instances = self.instances.write().await;
    instances.insert(*channel_type, adapter.clone());

    Ok(adapter)
    }

    /// 优雅停止指定渠道的适配器
    pub async fn stop_adapter(
    &self,
    channel_type: &ChannelType,
    ) -> Result<(), RegistryError> {
    let mut instances = self.instances.write().await;
    if let Some(adapter) = instances.remove(channel_type) {
    // 等待正在处理的消息完成
    adapter.stop().await?;
    tracing::info!(channel = ?channel_type, "Adapter stopped gracefully");
    }
    Ok(())
    }
    }

    3.1.3 适配器实现示例:飞书适配器

    以下是一个完整的飞书渠道适配器实现示例,展示了如何将飞书的事件回调机制适配为 Gateway 标准接口:

    /// 飞书适配器工厂
    pub struct FeishuAdapterFactory;

    #[async_trait]
    impl AdapterFactory for FeishuAdapterFactory {
    fn create(&self) -> Box<dyn ChannelAdapter> {
    Box::new(FeishuAdapter::new())
    }
    fn channel_type(&self) -> ChannelType {
    ChannelType::Feishu
    }
    }

    /// 飞书渠道适配器
    pub struct FeishuAdapter {
    app_id: String,
    app_secret: String,
    encrypt_key: Option<String>,
    verification_token: String,
    http_client: reqwest::Client,
    tenant_access_token: Arc<RwLock<Option<(String, Instant)>>>,
    }

    #[async_trait]
    impl ChannelAdapter for FeishuAdapter {
    fn name(&self) -> &str { "feishu" }
    fn channel_type(&self) -> ChannelType { ChannelType::Feishu }

    async fn initialize(&mut self, config: &AdapterConfig)
    -> Result<(), AdapterError>
    {
    self.app_id = config.get("app_id")?;
    self.app_secret = config.get("app_secret")?;
    self.encrypt_key = config.get_optional("encrypt_key");
    self.verification_token = config.get("verification_token")?;
    Ok(())
    }

    async fn receive_message(
    &self,
    raw: RawMessage,
    ) -> Result<InboundMessage, AdapterError> {
    let feishu_event: FeishuEvent = serde_json::from_slice(&raw.body)
    .map_err(|e| AdapterError::ParseError(e.to_string()))?;

    match feishu_event.event_type.as_str() {
    "im.message.receive_v1" => {
    let msg_data = &feishu_event.event.message;

    // 解析消息内容(飞书消息内容为JSON字符串)
    let content: FeishuMessageContent = serde_json::from_str(
    &msg_data.content
    ).unwrap_or_default();

    let normalized = NormalizedContent {
    text: content.text.or_else(|| {
    serde_json::from_str::<serde_json::Value>(&msg_data.content)
    .ok()
    .and_then(|v| v.get("text")
    .and_then(|t| t.as_str())
    .map(String::from))
    }),
    attachments: self.parse_feishu_attachments(msg_data)
    .unwrap_or_default(),
    mentions: self.parse_mentions(&feishu_event.event)
    .unwrap_or_default(),
    reply_to: msg_data.parent_id.clone(),
    };

    Ok(InboundMessage {
    source_message_id: msg_data.message_id.clone(),
    session_id: None,
    sender: SenderInfo {
    user_id: feishu_event.event.sender.sender_id.open_id,
    user_name: None,
    avatar: None,
    },
    content: normalized,
    metadata: InboundMetadata {
    channel: ChannelType::Feishu,
    chat_id: msg_data.chat_id.clone(),
    chat_type: self.parse_chat_type(&msg_data.chat_type),
    timestamp: msg_data.create_time.parse().unwrap_or(0),
    raw_ref: raw.id,
    },
    })
    }
    _ => Err(AdapterError::UnsupportedEventType(
    feishu_event.event_type
    ))
    }
    }

    async fn send_message(
    &self,
    message: OutboundMessage,
    ) -> Result<DeliveryReceipt, AdapterError> {
    let token = self.get_tenant_access_token().await?;

    // 根据渲染提示选择消息格式
    let (msg_type, content) = match message.render_hint.format {
    MessageFormat::Markdown => {
    ("text", serde_json::json!({
    "text": message.content.text.unwrap_or_default()
    }).to_string())
    }
    MessageFormat::Interactive => {
    ("interactive",
    self.build_feishu_card(&message).await?.to_string())
    }
    _ => {
    ("text", serde_json::json!({
    "text": message.content.text.unwrap_or_default()
    }).to_string())
    }
    };

    let req_body = serde_json::json!({
    "receive_id": message.target,
    "msg_type": msg_type,
    "content": content,
    });

    let resp = self.http_client
    .post("https://open.feishu.cn/open-apis/im/v1/messages")
    .header("Authorization", format!("Bearer {}", token))
    .header("Content-Type", "application/json")
    .query(&[("receive_id_type", "chat_id")])
    .json(&req_body)
    .send()
    .await
    .map_err(|e| AdapterError::SendError(e.to_string()))?;

    let resp_body: serde_json::Value = resp.json().await
    .map_err(|e| AdapterError::SendError(e.to_string()))?;

    let message_id = resp_body
    .get("data")
    .and_then(|d| d.get("message_id"))
    .and_then(|m| m.as_str())
    .unwrap_or("")
    .to_string();

    Ok(DeliveryReceipt {
    message_id,
    status: DeliveryStatus::Sent,
    timestamp: Utc::now(),
    })
    }

    fn capabilities(&self) -> ChannelCapabilities {
    ChannelCapabilities {
    text: true,
    image: true,
    file: true,
    audio: false,
    video: false,
    streaming: false,
    card: true,
    buttons: true,
    markdown: true,
    max_message_length: 30000,
    edit: true,
    delete: true,
    reply: true,
    mention: true,
    }
    }

    async fn health_check(&self) -> Result<HealthStatus, AdapterError> {
    // 尝试获取 tenant_access_token 作为健康检查
    match self.get_tenant_access_token().await {
    Ok(_) => Ok(HealthStatus::Healthy),
    Err(e) => Ok(HealthStatus::Unhealthy {
    reason: e.to_string(),
    since: Utc::now(),
    }),
    }
    }
    }

    impl FeishuAdapter {
    /// 获取或刷新 tenant_access_token
    async fn get_tenant_access_token(&self) -> Result<String, AdapterError> {
    // 检缓存的 token 是否有效
    {
    let cache = self.tenant_access_token.read().await;
    if let Some((token, expiry)) = cache.as_ref() {
    if expiry > &Instant::now() {
    return Ok(token.clone());
    }
    }
    }

    // 申请新 token
    let resp = self.http_client
    .post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal")
    .json(&serde_json::json!({
    "app_id": self.app_id,
    "app_secret": self.app_secret,
    }))
    .send()
    .await
    .map_err(|e| AdapterError::AuthError(e.to_string()))?;

    let body: serde_json::Value = resp.json().await
    .map_err(|e| AdapterError::AuthError(e.to_string()))?;

    let token = body.get("tenant_access_token")
    .and_then(|t| t.as_str())
    .ok_or(AdapterError::AuthError("Missing token".into()))?
    .to_string();

    let expire = body.get("expire")
    .and_then(|e| e.as_u64())
    .unwrap_or(7200);

    // 缓存 token(提前60秒过期)
    let mut cache = self.tenant_access_token.write().await;
    *cache = Some((
    token.clone(),
    Instant::now() + Duration::from_secs(expire 60),
    ));

    Ok(token)
    }

    /// 构建飞书交互卡片
    async fn build_feishu_card(
    &self,
    message: &OutboundMessage,
    ) -> Result<serde_json::Value, AdapterError> {
    let text = message.content.text.as_deref().unwrap_or("");

    Ok(serde_json::json!({
    "config": {
    "wide_screen_mode": true
    },
    "elements": [
    {
    "tag": "div",
    "text": {
    "content": text,
    "tag": "lark_md"
    }
    }
    ]
    }))
    }
    }

    3.2 消息格式归一化流程

    3.2.1 归一化管线

    消息从外部渠道进入 Gateway 后,需要经过一条归一化管线,将异构格式统一为内部标准格式:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 消息格式归一化管线 │
    │ │
    │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
    │ │ 原始消息 │──>│ 安全验证 │──>│ 格式解析 │──>│ 内容归一化│ │
    │ │ (Raw) │ │ (Verify) │ │ (Parse) │ │ (Normalize)│ │
    │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
    │ │ │
    │ v │
    │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
    │ │ 消息分发 │<──│ 元数据 │<──│ 内容增强 │<──│ 标准消息 │ │
    │ │ (Dispatch)│ │ (Enrich) │ │ (Enhance)│ │ (Standard)│ │
    │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
    │ │
    │ 归一化步骤详解: │
    │ │
    │ 1. 安全验证: 签名校验 → 频率限制 → 内容安全检查 │
    │ 2. 格式解析: JSON/XML/Form 解析 → 渠道特定结构提取 │
    │ 3. 内容归一化: 文本提取 → 附件下载 → 多模态转换 │
    │ 4. 内容增强: 语言检测 → 意图预判 → 上下文关联 │
    │ 5. 元数据填充: 时间戳标准化 → 用户信息补全 → 会话映射 │
    │ 6. 消息分发: 路由到会话管理器 → 进入处理管线 │
    └──────────────────────────────────────────────────────────────────────┘

    3.2.2 归一化处理器实现

    归一化管线采用责任链模式,每个阶段独立处理并传递给下一阶段:

    /// 消息归一化管线
    pub struct NormalizationPipeline {
    stages: Vec<Box<dyn NormalizationStage>>,
    }

    #[async_trait]
    pub trait NormalizationStage: Send + Sync {
    fn name(&self) -> &str;
    async fn process(
    &self,
    message: &mut PipelineMessage,
    ) -> Result<(), PipelineError>;
    }

    /// 归一化管线消息(在管线各阶段间传递)
    pub struct PipelineMessage {
    pub raw: RawMessage,
    pub verified: bool,
    pub parsed: Option<ParsedMessage>,
    pub normalized: Option<NormalizedContent>,
    pub enriched: Option<EnrichedContent>,
    pub inbound: Option<InboundMessage>,
    }

    impl NormalizationPipeline {
    pub async fn execute(
    &self,
    raw: RawMessage,
    ) -> Result<InboundMessage, PipelineError> {
    let mut message = PipelineMessage {
    raw,
    verified: false,
    parsed: None,
    normalized: None,
    enriched: None,
    inbound: None,
    };

    for stage in &self.stages {
    stage.process(&mut message).await.map_err(|e| {
    tracing::error!(
    stage = stage.name(),
    error = %e,
    "Pipeline stage failed"
    );
    e
    })?;
    }

    message.inbound.ok_or(PipelineError::IncompletePipeline)
    }
    }

    以下是两个关键阶段的实现:

    /// 阶段1:安全验证
    pub struct SecurityVerificationStage {
    rate_limiter: Arc<RateLimiter>,
    content_safety: Arc<ContentSafetyChecker>,
    }

    #[async_trait]
    impl NormalizationStage for SecurityVerificationStage {
    fn name(&self) -> &str { "security_verification" }

    async fn process(&self, msg: &mut PipelineMessage)
    -> Result<(), PipelineError>
    {
    // 频率限制检查
    self.rate_limiter.check(&msg.raw.source).await?;

    // 内容安全检查(敏感词、恶意链接等)
    if let Some(text) = &msg.raw.text_preview {
    self.content_safety.check(text).await?;
    }

    msg.verified = true;
    Ok(())
    }
    }

    /// 阶段3:内容归一化
    pub struct ContentNormalizationStage {
    attachment_downloader: Arc<AttachmentDownloader>,
    media_processor: Arc<MediaProcessor>,
    }

    #[async_trait]
    impl NormalizationStage for ContentNormalizationStage {
    fn name(&self) -> &str { "content_normalization" }

    async fn process(&self, msg: &mut PipelineMessage)
    -> Result<(), PipelineError>
    {
    let parsed = msg.parsed.as_ref().unwrap();
    let mut attachments = Vec::new();

    for att in &parsed.attachments {
    match att.att_type {
    AttachmentType::Image => {
    let downloaded = self.attachment_downloader
    .download(&att.url).await?;
    let processed = self.media_processor
    .process_image(&downloaded).await?;
    attachments.push(NormalizedAttachment {
    att_type: AttachmentType::Image,
    url: processed.url,
    thumbnail_url: processed.thumbnail_url,
    metadata: Some(serde_json::json!({
    "width": processed.width,
    "height": processed.height,
    "size": processed.size,
    })),
    });
    }
    AttachmentType::File => {
    let downloaded = self.attachment_downloader
    .download(&att.url).await?;
    attachments.push(NormalizedAttachment {
    att_type: AttachmentType::File,
    url: downloaded.url,
    thumbnail_url: None,
    metadata: Some(serde_json::json!({
    "filename": att.filename,
    "size": att.size,
    "mime_type": att.mime_type,
    })),
    });
    }
    _ => {
    attachments.push(NormalizedAttachment {
    att_type: att.att_type,
    url: att.url.clone(),
    thumbnail_url: None,
    metadata: None,
    });
    }
    }
    }

    msg.normalized = Some(NormalizedContent {
    text: parsed.text.clone(),
    attachments,
    mentions: parsed.mentions.clone(),
    reply_to: parsed.reply_to.clone(),
    });

    Ok(())
    }
    }

    3.2.3 内容增强阶段

    内容增强是归一化管线中最具智能性的阶段,它在消息进入核心处理之前完成预分析:

    /// 阶段4:内容增强
    pub struct ContentEnhancementStage {
    language_detector: Arc<LanguageDetector>,
    intent_predictor: Arc<IntentPredictor>,
    session_resolver: Arc<SessionResolver>,
    }

    #[async_trait]
    impl NormalizationStage for ContentEnhancementStage {
    fn name(&self) -> &str { "content_enhancement" }

    async fn process(&self, msg: &mut PipelineMessage)
    -> Result<(), PipelineError>
    {
    let normalized = msg.normalized.as_ref().unwrap();

    // 语言检测
    let language = if let Some(text) = &normalized.text {
    self.language_detector.detect(text).await
    } else {
    Language::Unknown
    };

    // 快速意图预判(轻量级模型,仅用于路由提示)
    let intent_hint = if let Some(text) = &normalized.text {
    Some(self.intent_predictor.quick_predict(text).await?)
    } else {
    None
    };

    // 会话解析:根据渠道ID+用户ID查找或创建会话
    let session_id = self.session_resolver
    .resolve_or_create(&msg.raw).await?;

    msg.enriched = Some(EnrichedContent {
    language,
    intent_hint,
    session_id,
    priority: self.calculate_priority(&normalized),
    });

    Ok(())
    }
    }

    内容增强阶段的设计价值在于:通过将语言检测、意图预判等轻量级分析前置到归一化管线,核心路由引擎可以直接利用这些预计算结果,减少重复计算,提升整体处理效率。

    3.3 多平台协议差异处理

    3.3.1 平台差异对比矩阵

    以下是 Hermes Agent 支持的各主流平台在关键协议维度的差异对比:

    平台消息格式最大长度流式支持富文本卡片/按钮附件类型回调机制鉴权方式
    Web UI JSON 无限制 SSE/WS Markdown 自定义组件 全类型 WebSocket JWT
    飞书 JSON 30K Markdown 交互卡片 图片/文件 Webhook App Token
    钉钉 JSON 20K Markdown ActionCard 图片/文件 Webhook AppKey/Secret
    企业微信 XML/JSON 2K 文本卡片 图片/文件 Webhook Token+AES
    Slack JSON 40K mrkdwn Blocks API 全类型 Events API OAuth Token
    Telegram JSON 4096 是(流式编辑) HTML/MD Inline KB 全类型 Long Polling Bot Token
    Discord JSON 2000 是(流式编辑) Markdown Components 全类型 Gateway WS Bot Token
    Email MIME 无限制 HTML 全类型 IMAP/SMTP SMTP Auth
    WhatsApp JSON 65536 模板按钮 图片/文档 Webhook Bearer Token
    Teams JSON 28K Markdown Adaptive Card 全类型 Webhook OAuth
    Telegram Bot JSON 4096 HTML/MD Inline KB 全类型 Webhook/Poll Bot Token
    Twitter DM JSON 10000 Quick Reply 图片/视频 Webhook OAuth 1.0a
    API/SDK JSON 无限制 原始 原始 全类型 HTTP回调 API Key

    3.3.2 协议差异处理策略

    面对如此多样的平台协议差异,Gateway 采用了三层处理策略:

    第一层:格式适配层。每个适配器负责将平台特定格式解析为内部标准格式。例如,飞书的消息内容是 JSON 字符串嵌套在 JSON 中,钉钉使用不同的字段命名,企业微信可能使用 XML 格式。适配器内部处理这些格式差异,对外输出统一的 NormalizedContent。

    第二层:能力降级层。当 Agent 产生的输出内容超出目标渠道的能力限制时(如飞书不支持流式输出、企业微信不支持 Markdown),Gateway 自动执行能力降级:

    /// 能力降级处理器
    pub struct CapabilityDegrader {
    /// 各渠道能力缓存
    capabilities_cache: Arc<RwLock<HashMap<ChannelType, ChannelCapabilities>>>,
    }

    impl CapabilityDegrader {
    /// 根据目标渠道能力降级输出内容
    pub async fn degrade(
    &self,
    content: &NormalizedContent,
    target_channel: ChannelType,
    ) -> Result<NormalizedContent, DegradeError> {
    let caps = self.capabilities_cache.read().await
    .get(&target_channel)
    .cloned()
    .ok_or(DegradeError::UnknownChannel(target_channel))?;

    let mut degraded = content.clone();

    // Markdown 降级
    if !caps.markdown && degraded.text.is_some() {
    let plain = self.strip_markdown(degraded.text.as_ref().unwrap());
    degraded.text = Some(plain);
    }

    // 消息长度截断
    if let Some(text) = &degraded.text {
    if text.len() > caps.max_message_length {
    let truncated = self.smart_truncate(text, caps.max_message_length);
    degraded.text = Some(truncated);
    }
    }

    // 附件类型过滤
    degraded.attachments.retain(|att| {
    match att.att_type {
    AttachmentType::Image => caps.image,
    AttachmentType::File => caps.file,
    AttachmentType::Audio => caps.audio,
    AttachmentType::Video => caps.video,
    }
    });

    Ok(degraded)
    }

    /// 智能截断(在句子边界截断)
    fn smart_truncate(&self, text: &str, max_len: usize) -> String {
    if text.len() <= max_len {
    return text.to_string();
    }

    let truncated = &text[..max_len];

    // 尝试在最后一个句号/换行处截断
    if let Some(pos) = truncated.rfind(|c: char| c == '\\n' || c == '。' || c == '.') {
    let mut result = truncated[..pos].to_string();
    result.push_str("\\n…[消息已截断]");
    result
    } else {
    let mut result = truncated.to_string();
    result.push_str("…[消息已截断]");
    result
    }
    }

    /// 去除 Markdown 格式标记
    fn strip_markdown(&self, text: &str) -> String {
    text.replace("**", "")
    .replace("*", "")
    .replace("`", "")
    .replace("#", "")
    .replace(">", "")
    .replace("-", "•")
    .replace("[", "")
    .replace("]", "")
    }
    }

    第三层:协议补偿层。对于某些渠道不支持的功能,Gateway 提供协议级补偿。例如,当目标渠道不支持流式输出时,Gateway 将流式输出缓存为完整消息后一次性发送;当渠道不支持消息编辑时,Gateway 通过发送新消息+删除旧消息的方式模拟编辑效果。

    3.3.3 消息长度超限处理

    不同平台对消息长度有不同限制,当 Agent 产生的回复超过限制时,Gateway 采用分片发送策略:

    /// 消息分片器
    pub struct MessageSplitter {
    /// 各渠道最大消息长度
    max_lengths: HashMap<ChannelType, usize>,
    /// 分片策略
    strategy: SplitStrategy,
    }

    pub enum SplitStrategy {
    /// 按段落分片
    ByParagraph,
    /// 按句子分片
    BySentence,
    /// 按代码块边界分片
    ByCodeBlock,
    /// 智能分片(综合考虑多种边界)
    Smart,
    }

    impl MessageSplitter {
    pub fn split(
    &self,
    content: &str,
    channel: ChannelType,
    ) -> Vec<String> {
    let max_len = self.max_lengths.get(&channel)
    .copied()
    .unwrap_or(4000);

    if content.len() <= max_len {
    return vec![content.to_string()];
    }

    match self.strategy {
    SplitStrategy::Smart => self.smart_split(content, max_len),
    SplitStrategy::ByParagraph => self.split_by_delimiter(content, max_len, "\\n\\n"),
    SplitStrategy::BySentence => self.split_by_delimiter(content, max_len, "。"),
    SplitStrategy::ByCodeBlock => self.split_by_code_block(content, max_len),
    }
    }

    fn smart_split(&self, content: &str, max_len: usize) -> Vec<String> {
    let mut chunks = Vec::new();
    let mut current = String::new();

    // 按行遍历,优先在代码块边界分片
    let mut in_code_block = false;

    for line in content.lines() {
    if line.trim_start().starts_with("```") {
    in_code_block = !in_code_block;
    }

    if current.len() + line.len() + 1 > max_len {
    if !current.is_empty() {
    chunks.push(current.clone());
    current.clear();
    }

    // 如果当前在代码块中,添加代码块标记
    if in_code_block {
    current.push_str("```\\n");
    }
    }

    current.push_str(line);
    current.push('\\n');
    }

    if !current.is_empty() {
    chunks.push(current);
    }

    chunks
    }
    }

    3.4 回调机制与异步消息处理

    3.4.1 回调架构总览

    许多即时通讯平台采用 Webhook 回调机制推送消息,这是一种异步、被动的消息接收模式。Gateway 需要处理回调消息的接收、验证、去重和有序处理:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ Webhook 回调处理架构 │
    │ │
    │ ┌──────────┐ HTTPS POST ┌──────────────┐ │
    │ │ 飞书平台 │ ═══════════════> │ │ │
    │ └──────────┘ │ Webhook │ │
    │ ┌──────────┐ │ Receiver │ │
    │ │ 钉钉平台 │ ═══════════════> │ │ │
    │ └──────────┘ └──────┬───────┘ │
    │ ┌──────────┐ │ │
    │ | 企微平台 | ═══════════════> │ │
    │ └──────────┘ v │
    │ ┌──────────────┐ │
    │ │ Signature │ │
    │ │ Verifier │ │
    │ └──────┬───────┘ │
    │ │ │
    │ v │
    │ ┌──────────────┐ │
    │ │ Dedup │ │
    │ │ Cache │ │
    │ └──────┬───────┘ │
    │ │ │
    │ v │
    │ ┌──────────────┐ │
    │ │ Message │ │
    │ │ Queue │ (Kafka) │
    │ └──────┬───────┘ │
    │ │ │
    │ ┌──────────────┼──────────────┐ │
    │ │ │ │ │
    │ v v v │
    │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
    │ │ Worker 1 │ │ Worker 2 │ │ Worker N │ │
    │ └──────────┘ └──────────┘ └──────────┘ │
    └──────────────────────────────────────────────────────────────────────┘

    3.4.2 回调处理实现

    /// Webhook 回调处理器
    pub struct WebhookHandler {
    /// 渠道适配器注册表
    adapters: Arc<AdapterRegistry>,
    /// 消息去重缓存
    dedup_cache: Arc<DedupCache>,
    /// 消息队列
    message_queue: Arc<MessageQueue>,
    /// 签名验证器
    signature_verifier: Arc<SignatureVerifier>,
    }

    impl WebhookHandler {
    /// 处理 Webhook 回调
    pub async fn handle_callback(
    &self,
    channel: ChannelType,
    headers: HeaderMap,
    body: Bytes,
    ) -> Result<CallbackResponse, CallbackError> {
    // 1. 获取渠道适配器
    let adapter = self.adapters.get_or_create(&channel).await?;

    // 2. 构造原始消息
    let raw = RawMessage {
    source: channel,
    headers: headers.clone(),
    body: body.clone(),
    id: headers.get("x-request-id")
    .and_then(|v| v.to_str().ok())
    .unwrap_or("")
    .to_string(),
    timestamp: Utc::now(),
    text_preview: None,
    };

    // 3. 验证消息签名
    let verified = adapter.verify_message(&raw).await?;
    if !verified {
    return Err(CallbackError::SignatureVerificationFailed);
    }

    // 4. 消息去重(基于消息ID)
    let message_key = format!("{}:{}", channel, raw.id);
    if self.dedup_cache.exists(&message_key).await? {
    tracing::warn!(
    channel = ?channel,
    message_id = %raw.id,
    "Duplicate callback detected, skipping"
    );
    return Ok(CallbackResponse::Duplicate);
    }

    // 5. 立即返回 200 ACK(不等处理完成)
    // 消息异步处理

    // 6. 将消息投入队列异步处理
    self.message_queue
    .enqueue(MessageQueueItem {
    channel,
    raw_message: raw,
    enqueued_at: Utc::now(),
    })
    .await?;

    // 7. 标记去重
    self.dedup_cache
    .set(&message_key, "1", Duration::from_secs(300))
    .await?;

    Ok(CallbackResponse::Accepted)
    }
    }

    /// 异步消息消费器
    pub struct AsyncMessageConsumer {
    adapters: Arc<AdapterRegistry>,
    pipeline: Arc<NormalizationPipeline>,
    session_manager: Arc<SessionIsolationManager>,
    routing_engine: Arc<RoutingEngine>,
    }

    impl AsyncMessageConsumer {
    /// 消费队列中的消息
    pub async fn consume(&self, item: MessageQueueItem) {
    let result = self.process_message(item).await;

    if let Err(e) = result {
    tracing::error!(error = %e, "Message processing failed");
    // TODO: 重试逻辑或死信队列
    }
    }

    async fn process_message(
    &self,
    item: MessageQueueItem,
    ) -> Result<(), ProcessingError> {
    // 1. 获取适配器
    let adapter = self.adapters
    .get_or_create(&item.channel).await?;

    // 2. 通过归一化管线处理
    let inbound = self.pipeline.execute(item.raw_message).await?;

    // 3. 获取或创建会话
    let session_id = inbound.metadata.session_id
    .clone()
    .ok_or(ProcessingError::MissingSessionId)?;

    // 4. 加载会话上下文
    let mut context = self.session_manager
    .get_context(&session_id).await?;

    // 5. 将用户消息追加到上下文
    context.messages.push(Message {
    message_id: MessageId::new(),
    role: MessageRole::User,
    content: MessageContent::Text(
    inbound.content.text.unwrap_or_default()
    ),
    timestamp: Utc::now(),
    metadata: MessageMetadata::default(),
    tool_calls: vec![],
    status: MessageStatus::Sent,
    });

    // 6. 路由到核心处理引擎
    let response = self.routing_engine
    .process(&session_id, &mut context).await?;

    // 7. 通过适配器发送响应
    adapter.send_message(OutboundMessage {
    target: inbound.metadata.chat_id,
    content: response.content,
    render_hint: response.render_hint,
    streaming: response.streaming,
    message_id: None,
    }).await?;

    Ok(())
    }
    }

    3.4.3 消息有序性保证

    在异步处理架构下,保证同一会话内消息的有序处理是一个关键挑战。Gateway 通过会话级锁和分区有序队列实现消息有序性:

    /// 会话级有序处理器
    pub struct OrderedSessionProcessor {
    /// 会话锁(确保同一会话的消息串行处理)
    session_locks: Arc<DashMap<SessionId, Arc<tokio::sync::Mutex<()>>>>,
    /// 处理器
    processor: Arc<AsyncMessageConsumer>,
    }

    impl OrderedSessionProcessor {
    pub async fn process_ordered(
    &self,
    session_id: &SessionId,
    item: MessageQueueItem,
    ) -> Result<(), ProcessingError> {
    // 获取会话级锁
    let lock = self.session_locks
    .entry(session_id.clone())
    .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
    .clone();

    let _guard = lock.lock().await;

    // 串行处理同一会话的消息
    self.processor.process_message(item).await
    }
    }

    有序性保证的策略分层:

    层级策略保证范围性能影响
    队列层 按会话ID分区 同一会话消息进入同一分区 无额外开销
    消费层 会话级互斥锁 同一会话消息串行处理 锁等待 <1ms
    应用层 乐观并发控制 检测并发修改并重试 冲突率 <0.1%

    3.5 渠道健康检查与故障转移

    3.5.1 健康检查机制

    Gateway 对每个渠道适配器实施持续的健康监测,及时发现渠道故障并触发转移:

    /// 渠道健康检查器
    pub struct ChannelHealthChecker {
    /// 健康状态缓存
    health_status: Arc<RwLock<HashMap<ChannelType, HealthState>>>,
    /// 检查间隔
    check_interval: Duration,
    /// 不健康阈值(连续失败次数)
    unhealthy_threshold: u32,
    /// 恢复阈值(连续成功次数)
    healthy_threshold: u32,
    }

    #[derive(Clone, Debug)]
    pub struct HealthState {
    pub status: HealthStatus,
    pub consecutive_failures: u32,
    pub consecutive_successes: u32,
    pub last_check_at: DateTime<Utc>,
    pub last_failure_reason: Option<String>,
    pub latency_history: VecDeque<Duration>,
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    pub enum HealthStatus {
    Healthy,
    Degraded { success_rate: f64 },
    Unhealthy { since: DateTime<Utc> },
    }

    impl ChannelHealthChecker {
    /// 启动持续健康检查
    pub async fn run_checks(&self, adapters: Arc<AdapterRegistry>) {
    let mut interval = tokio::time::interval(self.check_interval);

    loop {
    interval.tick().await;

    let channels = self.get_all_channels().await;

    for channel in channels {
    let adapter = match adapters.get_or_create(&channel).await {
    Ok(a) => a,
    Err(_) => continue,
    };

    let check_result = self.check_single(&channel, &adapter).await;
    self.update_health_status(&channel, check_result).await;
    }
    }
    }

    async fn check_single(
    &self,
    channel: &ChannelType,
    adapter: &Arc<dyn ChannelAdapter>,
    ) -> CheckResult {
    let start = Instant::now();

    match adapter.health_check().await {
    Ok(HealthStatus::Healthy) => CheckResult {
    success: true,
    latency: start.elapsed(),
    reason: None,
    },
    Ok(HealthStatus::Unhealthy { reason, since }) => CheckResult {
    success: false,
    latency: start.elapsed(),
    reason: Some(format!("Unhealthy since {}: {}", since, reason)),
    },
    Err(e) => CheckResult {
    success: false,
    latency: start.elapsed(),
    reason: Some(e.to_string()),
    },
    }
    }

    async fn update_health_status(
    &self,
    channel: &ChannelType,
    result: CheckResult,
    ) {
    let mut states = self.health_status.write().await;
    let state = states.entry(*channel).or_insert(HealthState {
    status: HealthStatus::Healthy,
    consecutive_failures: 0,
    consecutive_successes: 0,
    last_check_at: Utc::now(),
    last_failure_reason: None,
    latency_history: VecDeque::with_capacity(100),
    });

    // 记录延迟
    state.latency_history.push_back(result.latency);
    if state.latency_history.len() > 100 {
    state.latency_history.pop_front();
    }

    state.last_check_at = Utc::now();

    if result.success {
    state.consecutive_successes += 1;
    state.consecutive_failures = 0;

    // 恢复判定
    if state.consecutive_successes >= self.healthy_threshold {
    state.status = HealthStatus::Healthy;
    }
    } else {
    state.consecutive_failures += 1;
    state.consecutive_successes = 0;
    state.last_failure_reason = result.reason;

    // 故障判定
    if state.consecutive_failures >= self.unhealthy_threshold {
    state.status = HealthStatus::Unhealthy {
    since: Utc::now(),
    };

    tracing::error!(
    channel = ?channel,
    reason = ?result.reason,
    "Channel marked as unhealthy"
    );

    // 触发故障转移
    self.trigger_failover(channel).await;
    }
    }
    }

    /// 触发渠道故障转移
    async fn trigger_failover(&self, channel: &ChannelType) {
    // 1. 通知所有正在使用该渠道的会话
    // 2. 将待发送消息转移到备用渠道或队列
    // 3. 发送告警通知
    tracing::warn!(
    channel = ?channel,
    "Initiating channel failover"
    );
    }
    }

    3.5.2 故障转移策略

    当渠道不可用时,Gateway 根据配置的故障转移策略处理待发送消息:

    策略描述适用场景用户体验
    queue 消息暂存队列,渠道恢复后发送 短暂故障(<5分钟) 延迟接收
    fallback_channel 转移到备用渠道发送 有明确备用渠道 收到来自不同渠道的消息
    notify 通知用户渠道不可用 长时间故障 收到故障通知
    discard 丢弃非关键消息 实时性要求低的场景 无感知

    # 故障转移配置示例
    failover:
    feishu:
    strategy: queue
    max_queue_size: 1000
    queue_timeout: 300s
    fallback_channel: web
    notify_after: 60s

    dingtalk:
    strategy: fallback_channel
    fallback_channel: feishu

    web:
    strategy: queue
    max_queue_size: 5000
    queue_timeout: 600s

    email:
    strategy: discard
    discard_after: 300s


    第四章 工具注册表设计

    工具注册表是 Gateway 中管理 Agent 可用工具的核心组件。它负责工具的注册、发现、版本管理、权限控制和调用审计,是 Agent 能力扩展的基础设施。

    4.1 工具注册与发现机制

    4.1.1 工具元数据模型

    每个工具在注册表中都有一份完整的元数据描述,定义了工具的身份、能力、接口和约束:

    /// 工具元数据
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ToolDescriptor {
    /// 工具唯一标识
    pub tool_id: ToolId,
    /// 工具名称(人类可读)
    pub name: String,
    /// 工具描述(供LLM理解工具用途)
    pub description: String,
    /// 工具版本
    pub version: SemVer,
    /// 工具类别
    pub category: ToolCategory,
    /// 工具提供者
    pub provider: ToolProvider,
    /// 输入参数Schema (JSON Schema)
    pub input_schema: serde_json::Value,
    /// 输出格式Schema
    pub output_schema: serde_json::Value,
    /// 执行超时
    pub timeout: Duration,
    /// 是否需要沙箱
    pub requires_sandbox: bool,
    /// 所需权限
    pub required_permissions: Vec<Permission>,
    /// 速率限制
    pub rate_limit: Option<RateLimit>,
    /// 工具状态
    pub status: ToolStatus,
    /// 标签
    pub tags: Vec<String>,
    /// 使用统计
    pub stats: ToolStats,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub enum ToolCategory {
    /// 代码执行
    CodeExecution,
    /// 网络请求
    HttpRequest,
    /// 文件操作
    FileOperation,
    /// 数据库操作
    Database,
    /// 搜索检索
    Search,
    /// 知识库查询
    KnowledgeQuery,
    /// 图像处理
    ImageProcessing,
    /// 数据分析
    DataAnalysis,
    /// 外部API
    ExternalApi,
    /// MCP工具
    McpTool,
    /// 自定义
    Custom(String),
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ToolStats {
    /// 总调用次数
    pub total_calls: u64,
    /// 成功次数
    pub successful_calls: u64,
    /// 失败次数
    pub failed_calls: u64,
    /// 平均延迟(毫秒)
    pub avg_latency_ms: f64,
    /// P99延迟(毫秒)
    pub p99_latency_ms: f64,
    /// 最后调用时间
    pub last_called_at: Option<DateTime<Utc>>,
    }

    /// JSON Schema 示例:HTTP请求工具的输入定义
    pub fn http_request_tool_schema() -> serde_json::Value {
    serde_json::json!({
    "type": "object",
    "properties": {
    "method": {
    "type": "string",
    "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"],
    "description": "HTTP method"
    },
    "url": {
    "type": "string",
    "format": "uri",
    "description": "Request URL"
    },
    "headers": {
    "type": "object",
    "description": "Request headers",
    "additionalProperties": { "type": "string" }
    },
    "body": {
    "type": "string",
    "description": "Request body (JSON string)"
    },
    "timeout": {
    "type": "integer",
    "minimum": 1,
    "maximum": 120,
    "default": 30,
    "description": "Timeout in seconds"
    }
    },
    "required": ["method", "url"]
    })
    }

    4.1.2 工具注册表实现

    /// 工具注册表
    pub struct ToolRegistry {
    /// 已注册工具:ToolId -> ToolDescriptor
    tools: Arc<RwLock<HashMap<ToolId, ToolDescriptor>>>,
    /// 工具执行器:ToolId -> Box<dyn ToolExecutor>
    executors: Arc<RwLock<HashMap<ToolId, Arc<dyn ToolExecutor>>>>,
    /// 名称索引:name -> Set<ToolId>(支持同名不同版本)
    name_index: Arc<RwLock<HashMap<String, HashSet<ToolId>>>>,
    /// 标签索引:tag -> Set<ToolId>
    tag_index: Arc<RwLock<HashMap<String, HashSet<ToolId>>>>,
    /// 配置中心(监听工具配置变更)
    config_source: Arc<dyn ConfigSource>,
    }

    #[async_trait]
    pub trait ToolExecutor: Send + Sync {
    /// 执行工具
    async fn execute(
    &self,
    input: serde_json::Value,
    context: &ExecutionContext,
    ) -> Result<ToolOutput, ToolError>;

    /// 健康检查
    async fn health_check(&self) -> Result<bool, ToolError>;
    }

    /// 工具执行上下文
    #[derive(Clone, Debug)]
    pub struct ExecutionContext {
    pub session_id: SessionId,
    pub user_id: UserId,
    pub tool_call_id: String,
    pub sandbox_id: Option<String>,
    pub deadline: Instant,
    pub permissions: Vec<Permission>,
    }

    /// 工具输出
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ToolOutput {
    pub success: bool,
    pub result: serde_json::Value,
    pub error: Option<ToolErrorInfo>,
    pub metadata: OutputMetadata,
    }

    impl ToolRegistry {
    /// 注册新工具
    pub async fn register(
    &self,
    descriptor: ToolDescriptor,
    executor: Arc<dyn ToolExecutor>,
    ) -> Result<(), RegistryError> {
    let tool_id = descriptor.tool_id.clone();
    let name = descriptor.name.clone();

    // 验证工具描述符
    self.validate_descriptor(&descriptor)?;

    // 注册工具描述符
    {
    let mut tools = self.tools.write().await;
    if tools.contains_key(&tool_id) {
    return Err(RegistryError::ToolAlreadyExists(tool_id));
    }
    tools.insert(tool_id.clone(), descriptor.clone());
    }

    // 注册执行器
    {
    let mut execs = self.executors.write().await;
    execs.insert(tool_id.clone(), executor);
    }

    // 更新名称索引
    {
    let mut names = self.name_index.write().await;
    names.entry(name).or_default().insert(tool_id.clone());
    }

    // 更新标签索引
    {
    let mut tags = self.tag_index.write().await;
    for tag in &descriptor.tags {
    tags.entry(tag.clone())
    .or_default()
    .insert(tool_id.clone());
    }
    }

    tracing::info!(
    tool_id = %tool_id,
    name = %descriptor.name,
    version = %descriptor.version,
    "Tool registered successfully"
    );

    Ok(())
    }

    /// 注销工具
    pub async fn unregister(&self, tool_id: &ToolId)
    -> Result<(), RegistryError>
    {
    let descriptor = {
    let mut tools = self.tools.write().await;
    tools.remove(tool_id)
    .ok_or(RegistryError::ToolNotFound(tool_id.clone()))?
    };

    // 移除执行器
    {
    let mut execs = self.executors.write().await;
    execs.remove(tool_id);
    }

    // 更新名称索引
    {
    let mut names = self.name_index.write().await;
    if let Some(ids) = names.get_mut(&descriptor.name) {
    ids.remove(tool_id);
    if ids.is_empty() {
    names.remove(&descriptor.name);
    }
    }
    }

    // 更新标签索引
    {
    let mut tags = self.tag_index.write().await;
    for tag in &descriptor.tags {
    if let Some(ids) = tags.get_mut(tag) {
    ids.remove(tool_id);
    if ids.is_empty() {
    tags.remove(tag);
    }
    }
    }
    }

    tracing::info!(tool_id = %tool_id, "Tool unregistered");
    Ok(())
    }

    /// 工具发现:按名称查找(返回最新版本)
    pub async fn find_by_name(
    &self,
    name: &str,
    ) -> Result<ToolDescriptor, RegistryError> {
    let names = self.name_index.read().await;
    let tool_ids = names.get(name)
    .ok_or(RegistryError::ToolNotFound(name.to_string()))?;

    let tools = self.tools.read().await;
    let mut latest: Option<ToolDescriptor> = None;

    for id in tool_ids {
    if let Some(desc) = tools.get(id) {
    if desc.status != ToolStatus::Disabled {
    match &latest {
    None => latest = Some(desc.clone()),
    Some(current) if desc.version > current.version => {
    latest = Some(desc.clone());
    }
    _ => {}
    }
    }
    }
    }

    latest.ok_or(RegistryError::ToolNotFound(name.to_string()))
    }

    /// 工具发现:按标签查找
    pub async fn find_by_tags(
    &self,
    tags: &[String],
    ) -> Vec<ToolDescriptor> {
    let tag_index = self.tag_index.read().await;
    let tools = self.tools.read().await;

    // 取所有标签的交集
    let mut result_ids: Option<HashSet<ToolId>> = None;
    for tag in tags {
    if let Some(ids) = tag_index.get(tag) {
    match &result_ids {
    None => result_ids = Some(ids.clone()),
    Some(current) => {
    let intersection: HashSet<_> =
    current.intersection(ids).cloned().collect();
    result_ids = Some(intersection);
    }
    }
    } else {
    return vec![]; // 标签不存在,无匹配
    }
    }

    result_ids
    .unwrap_or_default()
    .iter()
    .filter_map(|id| tools.get(id).cloned())
    .filter(|d| d.status == ToolStatus::Active)
    .collect()
    }

    /// 获取所有可用工具(供LLM作为工具列表)
    pub async fn list_available_tools(
    &self,
    user_permissions: &[Permission],
    ) -> Vec<ToolSummary> {
    let tools = self.tools.read().await;

    tools.values()
    .filter(|d| d.status == ToolStatus::Active)
    .filter(|d| {
    // 检查用户是否有足够权限
    d.required_permissions.iter()
    .all(|p| user_permissions.contains(p))
    })
    .map(|d| ToolSummary {
    name: d.name.clone(),
    description: d.description.clone(),
    input_schema: d.input_schema.clone(),
    })
    .collect()
    }
    }

    /// 工具摘要(传递给LLM的精简格式)
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ToolSummary {
    pub name: String,
    pub description: String,
    pub input_schema: serde_json::Value,
    }

    4.2 MCP 协议集成

    4.2.1 MCP 协议概述

    MCP(Model Context Protocol)是 Anthropic 提出的开放标准协议,旨在为 LLM 提供统一的工具、资源和提示词访问接口。Hermes Agent 将 MCP 作为工具层的一等公民协议,实现了完整的 MCP 客户端集成。

    MCP 的核心概念:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ MCP 协议架构 │
    │ │
    │ ┌──────────────┐ MCP Protocol ┌──────────────────────┐ │
    │ │ │ <══════════════════> │ │ │
    │ │ MCP Client │ │ MCP Server │ │
    │ │ (Gateway) │ │ │ │
    │ │ │ · tools/list │ · 工具定义 │ │
    │ │ │ · tools/call │ · 工具执行 │ │
    │ │ │ · resources/list │ · 资源暴露 │ │
    │ │ │ · resources/read │ · 资源读取 │ │
    │ │ │ · prompts/list │ · 提示词模板 │ │
    │ │ │ · prompts/get │ · 提示词生成 │ │
    │ └──────────────┘ └──────────────────────┘ │
    │ │
    │ 传输层: stdio | SSE | WebSocket | gRPC │
    │ 序列化: JSON-RPC 2.0 │
    └──────────────────────────────────────────────────────────────────────┘

    4.2.2 MCP 客户端实现

    /// MCP 客户端
    pub struct McpClient {
    /// 传输层
    transport: Box<dyn McpTransport>,
    /// 已发现的工具缓存
    tools_cache: Arc<RwLock<Vec<McpTool>>>,
    /// 已发现的资源缓存
    resources_cache: Arc<RwLock<Vec<McpResource>>>,
    /// 服务器能力
    server_capabilities: Arc<RwLock<Option<ServerCapabilities>>>,
    /// 请求超时
    request_timeout: Duration,
    }

    #[async_trait]
    pub trait McpTransport: Send + Sync {
    async fn send(&self, message: JsonRpcRequest) -> Result<(), TransportError>;
    async fn recv(&self) -> Result<JsonRpcResponse, TransportError>;
    async fn close(&self) -> Result<(), TransportError>;
    }

    /// stdio 传输实现
    pub struct StdioTransport {
    process: tokio::process::Child,
    stdin: tokio::process::ChildStdin,
    stdout: tokio::process::ChildStdout,
    }

    /// SSE 传输实现
    pub struct SseTransport {
    url: String,
    client: reqwest::Client,
    event_source: Option<EventSource>,
    }

    impl McpClient {
    /// 连接 MCP 服务器并初始化
    pub async fn connect(
    transport: Box<dyn McpTransport>,
    ) -> Result<Self, McpError> {
    let mut client = McpClient {
    transport,
    tools_cache: Arc::new(RwLock::new(vec![])),
    resources_cache: Arc::new(RwLock::new(vec![])),
    server_capabilities: Arc::new(RwLock::new(None)),
    request_timeout: Duration::from_secs(30),
    };

    // 发送 initialize 请求
    let init_response = client.send_request(JsonRpcRequest {
    id: 1.into(),
    method: "initialize".to_string(),
    params: serde_json::json!({
    "protocolVersion": "2024-11-05",
    "capabilities": {
    "roots": { "listChanged": true },
    "sampling": {}
    },
    "clientInfo": {
    "name": "hermes-gateway",
    "version": "1.0.0"
    }
    }),
    }).await?;

    // 解析服务器能力
    let caps: ServerCapabilities = serde_json::from_value(
    init_response.result.get("capabilities").cloned().unwrap_or_default()
    )?;

    *client.server_capabilities.write().await = Some(caps);

    // 发送 initialized 通知
    client.send_notification("notifications/initialized",
    serde_json::json!({})).await?;

    Ok(client)
    }

    /// 发现可用工具
    pub async fn list_tools(&self) -> Result<Vec<McpTool>, McpError> {
    let response = self.send_request(JsonRpcRequest {
    id: self.next_id().await,
    method: "tools/list".to_string(),
    params: serde_json::json!({}),
    }).await?;

    let tools: Vec<McpTool> = serde_json::from_value(
    response.result.get("tools").cloned().unwrap_or_default()
    )?;

    // 更新缓存
    *self.tools_cache.write().await = tools.clone();

    Ok(tools)
    }

    /// 调用工具
    pub async fn call_tool(
    &self,
    name: &str,
    arguments: serde_json::Value,
    ) -> Result<McpToolResult, McpError> {
    let response = self.send_request(JsonRpcRequest {
    id: self.next_id().await,
    method: "tools/call".to_string(),
    params: serde_json::json!({
    "name": name,
    "arguments": arguments,
    }),
    }).await?;

    let result: McpToolResult = serde_json::from_value(
    response.result
    )?;

    Ok(result)
    }

    /// 读取资源
    pub async fn read_resource(
    &self,
    uri: &str,
    ) -> Result<McpResourceContent, McpError> {
    let response = self.send_request(JsonRpcRequest {
    id: self.next_id().await,
    method: "resources/read".to_string(),
    params: serde_json::json!({
    "uri": uri,
    }),
    }).await?;

    let content: McpResourceContent = serde_json::from_value(
    response.result
    )?;

    Ok(content)
    }
    }

    4.2.3 MCP 工具到 Gateway 注册表的映射

    MCP 服务器发现的工具需要自动注册到 Gateway 的工具注册表中,使得核心路由引擎可以统一调度:

    /// MCP 工具注册桥接器
    pub struct McpToolBridge {
    /// MCP 客户端
    mcp_client: Arc<McpClient>,
    /// Gateway 工具注册表
    registry: Arc<ToolRegistry>,
    /// 服务器标识
    server_id: String,
    /// 自动同步间隔
    sync_interval: Duration,
    }

    impl McpToolBridge {
    /// 启动自动同步:将 MCP 服务器的工具同步到注册表
    pub async fn start_sync(&self) {
    let mut interval = tokio::time::interval(self.sync_interval);

    loop {
    interval.tick().await;

    match self.mcp_client.list_tools().await {
    Ok(mcp_tools) => {
    for mcp_tool in mcp_tools {
    let descriptor = self.convert_to_descriptor(&mcp_tool);

    // 注册或更新工具
    match self.registry.get(&descriptor.tool_id).await {
    Ok(existing) if existing.version == descriptor.version => {
    // 版本相同,跳过
    }
    _ => {
    // 注册新版本
    let executor = McpToolExecutor::new(
    self.mcp_client.clone(),
    mcp_tool.name.clone(),
    );
    self.registry.register(
    descriptor,
    Arc::new(executor),
    ).await.ok();
    }
    }
    }

    tracing::info!(
    server_id = %self.server_id,
    tool_count = mcp_tools.len(),
    "MCP tools synced"
    );
    }
    Err(e) => {
    tracing::error!(
    server_id = %self.server_id,
    error = %e,
    "Failed to sync MCP tools"
    );
    }
    }
    }
    }

    /// 将 MCP 工具描述转换为 Gateway 工具描述符
    fn convert_to_descriptor(&self, mcp_tool: &McpTool) -> ToolDescriptor {
    let tool_id = ToolId::new(&format!(
    "mcp:{}:{}", self.server_id, mcp_tool.name
    ));

    ToolDescriptor {
    tool_id,
    name: format!("mcp_{}_{}", self.server_id, mcp_tool.name),
    description: mcp_tool.description.clone(),
    version: SemVer::new(1, 0, 0),
    category: ToolCategory::McpTool,
    provider: ToolProvider::Mcp {
    server_id: self.server_id.clone(),
    tool_name: mcp_tool.name.clone(),
    },
    input_schema: mcp_tool.input_schema.clone(),
    output_schema: serde_json::json!({"type": "object"}),
    timeout: Duration::from_secs(60),
    requires_sandbox: false,
    required_permissions: vec![Permission::UseMcpTools],
    rate_limit: Some(RateLimit {
    max_calls_per_minute: 30,
    max_concurrent: 3,
    }),
    status: ToolStatus::Active,
    tags: vec!["mcp".to_string(), self.server_id.clone()],
    stats: ToolStats::default(),
    }
    }
    }

    /// MCP 工具执行器(实现 ToolExecutor 接口)
    pub struct McpToolExecutor {
    mcp_client: Arc<McpClient>,
    tool_name: String,
    }

    #[async_trait]
    impl ToolExecutor for McpToolExecutor {
    async fn execute(
    &self,
    input: serde_json::Value,
    context: &ExecutionContext,
    ) -> Result<ToolOutput, ToolError> {
    let result = self.mcp_client
    .call_tool(&self.tool_name, input)
    .await
    .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;

    Ok(ToolOutput {
    success: !result.is_error,
    result: result.content.into(),
    error: if result.is_error {
    Some(ToolErrorInfo {
    code: "MCP_TOOL_ERROR".to_string(),
    message: "MCP tool returned error".to_string(),
    })
    } else {
    None
    },
    metadata: OutputMetadata {
    execution_time_ms: 0,
    tokens_used: None,
    },
    })
    }

    async fn health_check(&self) -> Result<bool, ToolError> {
    // 检查 MCP 客户端连接是否活跃
    self.mcp_client.ping().await.map(|_| true)
    }
    }

    4.2.4 MCP 服务器配置

    # MCP 服务器配置示例
    mcp_servers:
    # 本地文件系统 MCP 服务器
    id: filesystem
    transport: stdio
    command: npx
    args:
    "@modelcontextprotocol/server-filesystem"
    "/data/workspace"
    env:
    NODE_ENV: production
    auto_sync: true
    sync_interval: 60s

    # GitHub MCP 服务器
    id: github
    transport: stdio
    command: npx
    args:
    "@modelcontextprotocol/server-github"
    env:
    GITHUB_TOKEN: ${GITHUB_TOKEN}
    auto_sync: true

    # 远程 MCP 服务器(SSE 传输)
    id: knowledge_base
    transport: sse
    url: https://mcp.internal/knowledgebase/sse
    headers:
    Authorization: "Bearer ${MCP_KB_TOKEN}"
    auto_sync: true
    sync_interval: 30s

    # 自定义 MCP 服务器(gRPC 传输)
    id: data_pipeline
    transport: grpc
    endpoint: datapipeline.internal:50051
    tls:
    enabled: true
    cert_file: /etc/hermes/certs/client.crt
    key_file: /etc/hermes/certs/client.key
    ca_file: /etc/hermes/certs/ca.crt
    auto_sync: true

    4.3 内置工具与自定义工具统一管理

    4.3.1 工具分类体系

    Hermes Agent 的工具体系分为三个层级,通过统一的注册表接口实现无缝管理:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 工具分类体系 │
    │ │
    │ ┌──────────────────────────────────────────────────────────────┐ │
    │ │ 内置工具 │ │
    │ │ · 代码执行器 │ │
    │ │ · HTTP请求 ( │ │
    │ │ · 文件读写 ( │ │
    │ │ · 网络搜索 ( │ │
    │ │ · 数据分析 ( │ │
    │ │ · 图像生成 ( │ │
    │ └──────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ v │
    │ ┌──────────────────────────────────────────────────────────────┐ │
    │ │ MCP 工具 │ │
    │ │ · 文件系统 MCP Server │ │
    │ │ · GitHub MCP Server │ │
    │ │ · 知识库 MCP Server │ │
    │ │ · 数据管道 MCP Server │ │
    │ │ · 自定义 MCP Server │ │
    │ └──────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ v │
    │ ┌──────────────────────────────────────────────────────────────┐ │
    │ │ 用户自定义工具 │ │
    │ │ · Python 脚本工具 │ │
    │ │ · Shell 命令工具 │ │
    │ │ · API 封装工具 │ │
    │ │ · Workflow 工具 │ │
    │ │ · 插件式工具 │ │
    │ └──────────────────────────────────────────────────────────────┘ │
    │ │
    │ 统一接口: ToolExecutor trait │
    │ 统一注册: ToolRegistry │
    │ 统一发现: list_available_tools() │
    └──────────────────────────────────────────────────────────────────────┘

    4.3.2 内置工具实现示例:代码执行工具

    /// 代码执行工具
    pub struct CodeExecutionTool {
    sandbox_manager: Arc<SandboxManager>,
    supported_languages: Vec<String>,
    }

    #[async_trait]
    impl ToolExecutor for CodeExecutionTool {
    async fn execute(
    &self,
    input: serde_json::Value,
    context: &ExecutionContext,
    ) -> Result<ToolOutput, ToolError> {
    let language = input.get("language")
    .and_then(|v| v.as_str())
    .ok_or(ToolError::InvalidInput("Missing language".into()))?;

    let code = input.get("code")
    .and_then(|v| v.as_str())
    .ok_or(ToolError::InvalidInput("Missing code".into()))?;

    // 语言检查
    if !self.supported_languages.contains(&language.to_string()) {
    return Err(ToolError::InvalidInput(format!(
    "Unsupported language: {}", language
    )));
    }

    // 在沙箱中执行代码
    let sandbox = self.sandbox_manager
    .acquire_or_create(&context.session_id).await?;

    let result = sandbox.execute_code(language, code).await?;

    // 截断过长的输出
    let stdout = self.truncate_output(&result.stdout, 10000);
    let stderr = self.truncate_output(&result.stderr, 5000);

    Ok(ToolOutput {
    success: result.exit_code == 0,
    result: serde_json::json!({
    "stdout": stdout,
    "stderr": stderr,
    "exit_code": result.exit_code,
    "execution_time_ms": result.execution_time_ms,
    }),
    error: if result.exit_code != 0 {
    Some(ToolErrorInfo {
    code: "EXECUTION_FAILED".to_string(),
    message: format!(
    "Process exited with code {}", result.exit_code
    ),
    })
    } else {
    None
    },
    metadata: OutputMetadata {
    execution_time_ms: result.execution_time_ms,
    tokens_used: Some(result.tokens_used),
    },
    })
    }

    async fn health_check(&self) -> Result<bool, ToolError> {
    self.sandbox_manager.health_check().await
    }
    }

    impl CodeExecutionTool {
    fn truncate_output(&self, output: &str, max_len: usize) -> String {
    if output.len() <= max_len {
    return output.to_string();
    }
    format!(
    "{}\\n…[output truncated, {} bytes total]…",
    &output[..max_len],
    output.len()
    )
    }
    }

    4.3.3 自定义工具注册

    用户可以通过配置文件或 API 动态注册自定义工具:

    # 自定义工具配置
    custom_tools:
    # Python 脚本工具
    name: sentiment_analyzer
    description: "分析文本的情感倾向,返回情感分数和分类"
    type: python_script
    script: |
    import json
    from textblob import TextBlob

    def execute(input_data):
    text = input_data.get("text", "")
    blob = TextBlob(text)
    sentiment = blob.sentiment
    return {
    "polarity": sentiment.polarity,
    "subjectivity": sentiment.subjectivity,
    "classification": "positive" if sentiment.polarity > 0
    else "negative" if sentiment.polarity < 0
    else "neutral"
    }
    input_schema:
    type: object
    properties:
    text:
    type: string
    description: "待分析的文本"
    required: ["text"]
    timeout: 10s
    requires_sandbox: true
    permissions: [UseCodeExecution]

    # API 封装工具
    name: weather_lookup
    description: "查询指定城市的天气信息"
    type: http_api
    config:
    method: GET
    url: "https://api.weatherapi.com/v1/current.json"
    headers:
    "X-API-Key": "${WEATHER_API_KEY}"
    params:
    q: "{{city}}"
    lang: "zh"
    response_mapping:
    temperature: "$.current.temp_c"
    condition: "$.current.condition.text"
    humidity: "$.current.humidity"
    wind_speed: "$.current.wind_kph"
    input_schema:
    type: object
    properties:
    city:
    type: string
    description: "城市名称"
    required: ["city"]
    timeout: 15s
    permissions: [UseExternalApi]

    # Workflow 工具(组合多个工具)
    name: research_pipeline
    description: "研究管线:搜索→分析→总结"
    type: workflow
    steps:
    name: search
    tool: web_search
    input:
    query: "{{topic}}"
    name: analyze
    tool: sentiment_analyzer
    input:
    text: "{{search.result.summary}}"
    depends_on: [search]
    name: summarize
    tool: text_summarizer
    input:
    text: "{{search.result.content}}"
    max_length: 500
    depends_on: [search]
    output:
    search_results: "{{search.result}}"
    sentiment: "{{analyze.result}}"
    summary: "{{summarize.result}}"
    timeout: 60s
    permissions: [UseWebSearch, UseCodeExecution]

    4.4 权限校验与调用审计

    4.4.1 权限模型

    Gateway 采用基于角色的访问控制(RBAC)模型管理工具调用权限:

    /// 权限定义
    #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
    pub enum Permission {
    /// 代码执行
    UseCodeExecution,
    /// 网络请求
    UseHttpRequest,
    /// 文件操作
    UseFileOperation,
    /// 数据库操作
    UseDatabase,
    /// 网络搜索
    UseWebSearch,
    /// 知识库查询
    UseKnowledgeQuery,
    /// 图像处理
    UseImageProcessing,
    /// 外部API
    UseExternalApi,
    /// MCP工具
    UseMcpTools,
    /// 管理员权限
    Admin,
    }

    /// 角色
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct Role {
    pub role_id: String,
    pub name: String,
    pub permissions: HashSet<Permission>,
    pub tool_overrides: HashMap<ToolId, ToolPermission>,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub enum ToolPermission {
    Allowed,
    Denied,
    AllowedWithConditions(Conditions),
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct Conditions {
    pub max_calls_per_session: Option<u32>,
    pub allowed_params: Option<HashMap<String, AllowedValue>>,
    pub time_window: Option<TimeWindow>,
    }

    /// 权限校验器
    pub struct PermissionChecker {
    /// 角色-权限映射
    roles: Arc<RwLock<HashMap<String, Role>>>,
    /// 用户-角色映射
    user_roles: Arc<RwLock<HashMap<UserId, Vec<String>>>>,
    }

    impl PermissionChecker {
    /// 校验用户是否有权调用指定工具
    pub async fn check(
    &self,
    user_id: &UserId,
    tool: &ToolDescriptor,
    input: &serde_json::Value,
    ) -> Result<PermissionCheckResult, PermissionError> {
    // 1. 获取用户角色
    let user_role_ids = self.user_roles.read().await
    .get(user_id)
    .cloned()
    .unwrap_or_default();

    if user_role_ids.is_empty() {
    return Ok(PermissionCheckResult::Denied {
    reason: "User has no roles assigned".to_string(),
    });
    }

    // 2. 聚合用户权限
    let roles = self.roles.read().await;
    let mut aggregated_perms: HashSet<Permission> = HashSet::new();
    let mut tool_overrides: HashMap<ToolId, ToolPermission> = HashMap::new();

    for role_id in &user_role_ids {
    if let Some(role) = roles.get(role_id) {
    aggregated_perms.extend(role.permissions.iter().cloned());
    tool_overrides.extend(role.tool_overrides.iter()
    .map(|(k, v)| (k.clone(), v.clone())));
    }
    }

    // 3. 检查工具特定覆盖
    if let Some(override_perm) = tool_overrides.get(&tool.tool_id) {
    return match override_perm {
    ToolPermission::Denied => Ok(PermissionCheckResult::Denied {
    reason: "Tool explicitly denied for user's role".to_string(),
    }),
    ToolPermission::Allowed => Ok(PermissionCheckResult::Allowed),
    ToolPermission::AllowedWithConditions(conditions) => {
    self.check_conditions(conditions, input).await
    }
    };
    }

    // 4. 检查所需权限
    for required in &tool.required_permissions {
    if !aggregated_perms.contains(required) {
    return Ok(PermissionCheckResult::Denied {
    reason: format!(
    "Missing required permission: {:?}", required
    ),
    });
    }
    }

    Ok(PermissionCheckResult::Allowed)
    }

    async fn check_conditions(
    &self,
    conditions: &Conditions,
    input: &serde_json::Value,
    ) -> Result<PermissionCheckResult, PermissionError> {
    // 检查参数白名单
    if let Some(allowed_params) = &conditions.allowed_params {
    for (param, allowed_value) in allowed_params {
    if let Some(value) = input.get(param) {
    if !allowed_value.matches(value) {
    return Ok(PermissionCheckResult::Denied {
    reason: format!(
    "Parameter '{}' has disallowed value", param
    ),
    });
    }
    }
    }
    }

    // 检查时间窗口
    if let Some(time_window) = &conditions.time_window {
    let now = Utc::now();
    if !time_window.contains(now) {
    return Ok(PermissionCheckResult::Denied {
    reason: format!(
    "Current time outside allowed window: {:?}", time_window
    ),
    });
    }
    }

    Ok(PermissionCheckResult::Allowed)
    }
    }

    pub enum PermissionCheckResult {
    Allowed,
    Denied { reason: String },
    }

    4.4.2 调用审计

    每次工具调用都被完整记录,用于审计、合规和性能分析:

    /// 工具调用审计日志
    pub struct AuditLogger {
    /// 日志写入器(批量写入)
    writer: Arc<BatchLogWriter>,
    /// 敏感参数脱敏器
    masker: Arc<SensitiveDataMasker>,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct AuditEntry {
    /// 审计ID
    pub audit_id: String,
    /// 调用时间
    pub timestamp: DateTime<Utc>,
    /// 会话ID
    pub session_id: SessionId,
    /// 用户ID
    pub user_id: UserId,
    /// 工具ID
    pub tool_id: ToolId,
    /// 工具名称
    pub tool_name: String,
    /// 输入参数(脱敏后)
    pub input: serde_json::Value,
    /// 输出结果(脱敏后)
    pub output: Option<serde_json::Value>,
    /// 是否成功
    pub success: bool,
    /// 错误信息
    pub error: Option<String>,
    /// 执行耗时
    pub duration_ms: u64,
    /// 调用来源(LLM决策/用户指定/系统自动)
    pub source: CallSource,
    /// 沙箱ID
    pub sandbox_id: Option<String>,
    /// 追踪ID
    pub trace_id: String,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub enum CallSource {
    LlmDecision { model: String, reasoning: String },
    UserSpecified,
    SystemAutomatic { trigger: String },
    Workflow { workflow_name: String, step: usize },
    }

    impl AuditLogger {
    /// 记录工具调用
    pub async fn log(&self, entry: AuditEntry) -> Result<(), AuditError> {
    // 脱敏处理
    let masked_entry = AuditEntry {
    input: self.masker.mask(&entry.input).await,
    output: match &entry.output {
    Some(o) => Some(self.masker.mask(o).await),
    None => None,
    },
    ..entry
    };

    // 批量写入
    self.writer.write(masked_entry).await?;
    Ok(())
    }

    /// 查询审计日志
    pub async fn query(
    &self,
    filter: AuditFilter,
    ) -> Result<Vec<AuditEntry>, AuditError> {
    self.writer.query(filter).await
    }
    }

    /// 审计查询过滤器
    pub struct AuditFilter {
    pub session_id: Option<SessionId>,
    pub user_id: Option<UserId>,
    pub tool_id: Option<ToolId>,
    pub start_time: Option<DateTime<Utc>>,
    pub end_time: Option<DateTime<Utc>>,
    pub success_only: Option<bool>,
    pub limit: usize,
    }

    审计日志的典型查询场景:

    — 查询某用户在指定时间段的所有工具调用
    SELECT * FROM audit_logs
    WHERE user_id = 'user_123'
    AND timestamp BETWEEN '2026-07-01' AND '2026-07-07'
    ORDER BY timestamp DESC;

    — 查询失败的工具调用
    SELECT * FROM audit_logs
    WHERE success = false
    AND timestamp > NOW() INTERVAL '24 hours'
    ORDER BY timestamp DESC;

    — 按工具统计调用频次和成功率
    SELECT
    tool_name,
    COUNT(*) as total_calls,
    SUM(CASE WHEN success THEN 1 ELSE 0 END) as successful,
    AVG(duration_ms) as avg_duration
    FROM audit_logs
    WHERE timestamp > NOW() INTERVAL '7 days'
    GROUP BY tool_name;

    4.5 工具版本管理与热更新

    4.5.1 版本管理策略

    /// 工具版本管理器
    pub struct ToolVersionManager {
    /// 版本历史:ToolId -> Vec<VersionEntry>
    version_history: Arc<RwLock<HashMap<ToolId, Vec<VersionEntry>>>>,
    /// 活跃版本:ToolId -> Version
    active_versions: Arc<RwLock<HashMap<ToolId, SemVer>>>,
    /// 灰度发布配置
    rollout_config: Arc<RwLock<HashMap<ToolId, RolloutConfig>>>,
    }

    #[derive(Clone, Debug)]
    pub struct VersionEntry {
    pub version: SemVer,
    pub descriptor: ToolDescriptor,
    pub registered_at: DateTime<Utc>,
    pub status: VersionStatus,
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    pub enum VersionStatus {
    Active,
    Inactive,
    Deprecated { sunset_date: DateTime<Utc> },
    Retired,
    }

    #[derive(Clone, Debug)]
    pub struct RolloutConfig {
    pub strategy: RolloutStrategy,
    pub percentage: u8, // 0-100
    pub target_sessions: Option<Vec<SessionId>>,
    pub target_users: Option<Vec<UserId>>,
    }

    #[derive(Clone, Debug)]
    pub enum RolloutStrategy {
    /// 全量发布
    Full,
    /// 百分比灰度
    Percentage,
    /// 白名单灰度
    Whitelist,
    /// 金丝雀发布
    Canary,
    }

    impl ToolVersionManager {
    /// 注册新版本(不立即激活)
    pub async fn register_version(
    &self,
    descriptor: ToolDescriptor,
    ) -> Result<(), VersionError> {
    let tool_id = descriptor.tool_id.clone();
    let version = descriptor.version.clone();

    let mut history = self.version_history.write().await;
    let entries = history.entry(tool_id.clone()).or_default();

    // 检查版本是否已存在
    if entries.iter().any(|e| e.version == version) {
    return Err(VersionError::VersionAlreadyExists(
    tool_id, version
    ));
    }

    entries.push(VersionEntry {
    version: version.clone(),
    descriptor,
    registered_at: Utc::now(),
    status: VersionStatus::Inactive,
    });

    Ok(())
    }

    /// 激活指定版本(热更新)
    pub async fn activate_version(
    &self,
    tool_id: &ToolId,
    version: &SemVer,
    ) -> Result<(), VersionError> {
    let mut history = self.version_history.write().await;
    let entries = history.get_mut(tool_id)
    .ok_or(VersionError::ToolNotFound(tool_id.clone()))?;

    // 找到目标版本并激活
    for entry in entries.iter_mut() {
    if entry.version == *version {
    entry.status = VersionStatus::Active;
    } else if entry.status == VersionStatus::Active {
    entry.status = VersionStatus::Inactive;
    }
    }

    // 更新活跃版本
    self.active_versions.write().await
    .insert(tool_id.clone(), version.clone());

    tracing::info!(
    tool_id = %tool_id,
    version = %version,
    "Tool version activated"
    );

    Ok(())
    }

    /// 灰度发布:根据配置决定使用哪个版本
    pub async fn resolve_version(
    &self,
    tool_id: &ToolId,
    user_id: &UserId,
    session_id: &SessionId,
    ) -> Result<SemVer, VersionError> {
    // 检查是否有灰度配置
    let rollouts = self.rollout_config.read().await;

    if let Some(config) = rollouts.get(tool_id) {
    match &config.strategy {
    RolloutStrategy::Full => {
    return self.get_latest_version(tool_id).await;
    }
    RolloutStrategy::Whitelist => {
    if let Some(target_users) = &config.target_users {
    if target_users.contains(user_id) {
    return self.get_latest_version(tool_id).await;
    }
    }
    if let Some(target_sessions) = &config.target_sessions {
    if target_sessions.contains(session_id) {
    return self.get_latest_version(tool_id).await;
    }
    }
    }
    RolloutStrategy::Percentage => {
    // 基于用户ID哈希决定是否使用新版本
    let hash = self.hash_user(user_id);
    if hash % 100 < config.percentage as u64 {
    return self.get_latest_version(tool_id).await;
    }
    }
    RolloutStrategy::Canary => {
    // 金丝雀:先小范围测试
    if config.percentage < 10 {
    let hash = self.hash_user(user_id);
    if hash % 100 < config.percentage as u64 {
    return self.get_latest_version(tool_id).await;
    }
    } else {
    return self.get_latest_version(tool_id).await;
    }
    }
    }
    }

    // 默认使用当前活跃版本
    self.active_versions.read().await
    .get(tool_id)
    .cloned()
    .ok_or(VersionError::NoActiveVersion(tool_id.clone()))
    }
    }

    4.5.2 热更新流程

    工具的热更新不需要重启 Gateway,整个流程如下:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 工具热更新流程 │
    │ │
    │ 1. 注册新版本 │
    │ └─> ToolRegistry.register(new_descriptor, new_executor) │
    │ 新版本状态: Inactive │
    │ │
    │ 2. 配置灰度策略 │
    │ └─> RolloutConfig { strategy: Canary, percentage: 5 } │
    │ 仅5%用户使用新版本 │
    │ │
    │ 3. 监控指标 │
    │ └─> 对比新旧版本的成功率、延迟、错误率 │
    │ 如果新版本指标劣化 → 自动回滚 │
    │ 如果新版本指标正常 → 增加灰度比例 │
    │ │
    │ 4. 逐步扩大范围 │
    │ └─> 5% → 10% → 25% → 50% → 100% │
    │ │
    │ 5. 全量激活 │
    │ └─> VersionManager.activate_version(tool_id, new_version) │
    │ 旧版本状态: Deprecated (保留30天后Retired) │
    │ │
    │ 6. 清理旧版本 │
    │ └─> 30天后自动执行 Retirement │
    │ 旧执行器释放资源 │
    └──────────────────────────────────────────────────────────────────────┘

    热更新的关键设计考量:

    • 零停机:整个版本切换过程中不中断任何正在处理的请求
    • 渐进式:从 5% 开始逐步扩大,每步观察至少 10 分钟
    • 可回滚:任何阶段发现问题都能秒级回滚到上一稳定版本
    • 自动监控:系统自动对比新旧版本的关键指标,异常时自动触发回滚
    • 兼容性保证:新版本必须向后兼容旧版本的输入输出格式

    第五章 核心路由引擎

    核心路由引擎是 Gateway 的「大脑」,它接收归一化后的用户消息,识别用户意图,编排任务执行计划,调度记忆与工具资源,最终产出响应。路由引擎的设计质量直接决定了 Agent 的智能水平和响应效率。

    5.1 意图识别算法

    5.1.1 三级意图识别架构

    Hermes Agent 采用三级级联的意图识别架构,在准确率和延迟之间取得最优平衡:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 三级意图识别架构 │
    │ │
    │ 用户输入 │
    │ │ │
    │ v │
    │ ┌──────────────┐ │
    │ │ Level 1: │ 延迟: <1ms │
    │ │ 规则匹配 │ 准确率: 60-70% │
    │ │ (Rule-based) │ 覆盖: 高频明确意图 │
    │ └──────┬───────┘ │
    │ │ 置信度 > 0.9? │
    │ ┌────┴────┐ │
    │ Yes No │
    │ │ │ │
    │ v v │
    │ 返回 ┌──────────────┐ │
    │ 意图 │ Level 2: │ 延迟: 5-20ms │
    │ │ ML分类器 │ 准确率: 85-90% │
    │ │ (ML Model) │ 覆盖: 常见意图 │
    │ └──────┬───────┘ │
    │ │ 置信度 > 0.85? │
    │ ┌────┴────┐ │
    │ Yes No │
    │ │ │ │
    │ v v │
    │ 返回 ┌──────────────┐ │
    │ 意图 │ Level 3: │ 延迟: 100-500ms │
    │ │ LLM推理 │ 准确率: 95%+ │
    │ │ (LLM-based) │ 覆盖: 任意意图 │
    │ └──────┬───────┘ │
    │ │ │
    │ v │
    │ 返回意图 │
    └──────────────────────────────────────────────────────────────────────┘

    5.1.2 Level 1:规则匹配引擎

    /// 规则匹配意图识别器
    pub struct RuleBasedIntentDetector {
    /// 意图规则集
    rules: Vec<IntentRule>,
    /// 编译后的正则表达式缓存
    regex_cache: HashMap<String, Regex>,
    }

    #[derive(Clone, Debug)]
    pub struct IntentRule {
    /// 意图名称
    pub intent: String,
    /// 匹配模式
    pub patterns: Vec<MatchPattern>,
    /// 置信度
    pub confidence: f64,
    /// 优先级(数字越大优先级越高)
    pub priority: i32,
    }

    #[derive(Clone, Debug)]
    pub enum MatchPattern {
    /// 正则匹配
    Regex(String),
    /// 关键词包含
    Keywords(Vec<String>),
    /// 前缀匹配
    Prefix(String),
    /// 精确匹配
    Exact(String),
    }

    impl RuleBasedIntentDetector {
    pub fn detect(&self, input: &str) -> Option<IntentDetection> {
    // 按优先级排序的规则匹配
    let mut matched_rules: Vec<&IntentRule> = self.rules
    .iter()
    .filter(|rule| self.matches(rule, input))
    .collect();

    matched_rules.sort_by(|a, b| b.priority.cmp(&a.priority));

    if let Some(rule) = matched_rules.first() {
    return Some(IntentDetection {
    intent: rule.intent.clone(),
    confidence: rule.confidence,
    source: DetectionSource::Rule,
    extracted_params: self.extract_params(rule, input),
    });
    }

    None
    }

    fn matches(&self, rule: &IntentRule, input: &str) -> bool {
    let input_lower = input.to_lowercase();

    rule.patterns.iter().any(|pattern| {
    match pattern {
    MatchPattern::Regex(re_str) => {
    if let Ok(re) = self.regex_cache.get(re_str) {
    re.is_match(&input_lower)
    } else {
    false
    }
    }
    MatchPattern::Keywords(keywords) => {
    keywords.iter().all(|kw| input_lower.contains(kw))
    }
    MatchPattern::Prefix(prefix) => {
    input_lower.starts_with(&prefix.to_lowercase())
    }
    MatchPattern::Exact(text) => {
    input_lower == text.to_lowercase()
    }
    }
    })
    }
    }

    /// 预定义的意图规则集
    pub fn default_intent_rules() -> Vec<IntentRule> {
    vec![
    IntentRule {
    intent: "code_execution".into(),
    patterns: vec![
    MatchPattern::Keywords(vec!["运行", "代码", "执行"]),
    MatchPattern::Keywords(vec!["run", "code", "execute"]),
    MatchPattern::Regex(r"```(python|javascript|rust|go)".into()),
    ],
    confidence: 0.95,
    priority: 10,
    },
    IntentRule {
    intent: "web_search".into(),
    patterns: vec![
    MatchPattern::Keywords(vec!["搜索", "查找", "查询"]),
    MatchPattern::Keywords(vec!["search", "find", "lookup"]),
    MatchPattern::Prefix("帮我搜".into()),
    ],
    confidence: 0.90,
    priority: 9,
    },
    IntentRule {
    intent: "file_operation".into(),
    patterns: vec![
    MatchPattern::Keywords(vec!["读取", "文件", "保存"]),
    MatchPattern::Keywords(vec!["read", "file", "write", "save"]),
    ],
    confidence: 0.88,
    priority: 8,
    },
    IntentRule {
    intent: "knowledge_query".into(),
    patterns: vec![
    MatchPattern::Keywords(vec!["知识库", "文档", "手册"]),
    MatchPattern::Keywords(vec!["knowledge", "document", "manual"]),
    ],
    confidence: 0.85,
    priority: 7,
    },
    IntentRule {
    intent: "casual_chat".into(),
    patterns: vec![
    MatchPattern::Keywords(vec!["你好", "嗨", "hi", "hello"]),
    MatchPattern::Keywords(vec!["谢谢", "thanks", "thank you"]),
    MatchPattern::Keywords(vec!["再见", "bye", "goodbye"]),
    ],
    confidence: 0.92,
    priority: 5,
    },
    ]
    }

    5.1.3 Level 2:ML 分类器

    当规则匹配的置信度不足时,Gateway 使用轻量级 ML 模型进行意图分类。该模型基于 BERT-tiny 微调,在 CPU 上推理延迟约 5-20ms:

    # 意图分类模型训练脚本 (简化版)
    import torch
    from transformers import BertTokenizerFast, BertForSequenceClassification
    from torch.utils.data import Dataset, DataLoader

    class IntentDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len=128):
    self.texts = texts
    self.labels = labels
    self.tokenizer = tokenizer
    self.max_len = max_len

    def __len__(self):
    return len(self.texts)

    def __getitem__(self, idx):
    text = str(self.texts[idx])
    encoding = self.tokenizer(
    text,
    max_length=self.max_len,
    padding='max_length',
    truncation=True,
    return_tensors='pt'
    )
    return {
    'input_ids': encoding['input_ids'].flatten(),
    'attention_mask': encoding['attention_mask'].flatten(),
    'labels': torch.tensor(self.labels[idx], dtype=torch.long)
    }

    # 意图标签
    INTENT_LABELS = [
    "code_execution", # 代码执行
    "web_search", # 网络搜索
    "file_operation", # 文件操作
    "knowledge_query", # 知识库查询
    "data_analysis", # 数据分析
    "image_generation", # 图像生成
    "translation", # 翻译
    "summarization", # 摘要
    "casual_chat", # 日常闲聊
    "task_planning", # 任务规划
    "tool_orchestration",# 工具编排
    "unknown", # 未知意图
    ]

    class IntentClassifier:
    def __init__(self, model_path: str):
    self.tokenizer = BertTokenizerFast.from_pretrained(model_path)
    self.model = BertForSequenceClassification.from_pretrained(model_path)
    self.model.eval()
    self.labels = INTENT_LABELS

    @torch.no_grad()
    def predict(self, text: str) > dict:
    encoding = self.tokenizer(
    text,
    max_length=128,
    padding='max_length',
    truncation=True,
    return_tensors='pt'
    )

    outputs = self.model(
    input_ids=encoding['input_ids'],
    attention_mask=encoding['attention_mask']
    )

    probs = torch.softmax(outputs.logits, dim=1)
    confidence, predicted = torch.max(probs, dim=1)

    return {
    'intent': self.labels[predicted.item()],
    'confidence': confidence.item(),
    'all_probs': {
    label: prob.item()
    for label, prob in zip(self.labels, probs[0])
    }
    }

    # 推理服务封装
    class IntentDetectionService:
    def __init__(self, model_path: str):
    self.classifier = IntentClassifier(model_path)
    self.confidence_threshold = 0.85

    def detect(self, text: str, context: dict = None) > dict:
    result = self.classifier.predict(text)

    # 结合上下文调整置信度
    if context and 'recent_intents' in context:
    # 如果最近几轮意图一致,提升置信度
    recent = context['recent_intents']
    if recent and recent[1] == result['intent']:
    result['confidence'] = min(
    result['confidence'] + 0.05, 1.0
    )

    result['source'] = 'ml_model'
    result['passed'] = result['confidence'] >= self.confidence_threshold
    return result

    5.1.4 Level 3:LLM 推理

    当 ML 分类器的置信度仍然不足时,Gateway 使用 LLM 进行深度意图推理。这一层级使用专门的提示词模板,引导 LLM 进行结构化的意图分析:

    /// LLM 意图识别器
    pub struct LlmIntentDetector {
    model_router: Arc<ModelRouter>,
    prompt_template: String,
    }

    impl LlmIntentDetector {
    pub async fn detect(
    &self,
    input: &str,
    session_context: &SessionContext,
    ) -> Result<IntentDetection, IntentError> {
    // 构建上下文摘要
    let context_summary = self.build_context_summary(session_context);

    // 获取可用工具列表
    let available_tools = self.get_available_tools_summary().await;

    let prompt = format!(
    r#"你是一个意图识别专家。请分析用户的输入,识别其意图。

    ## 可用意图类型
    – code_execution: 执行代码
    – web_search: 搜索网络信息
    – file_operation: 文件读写操作
    – knowledge_query: 知识库查询
    – data_analysis: 数据分析
    – image_generation: 生成图像
    – translation: 翻译
    – summarization: 内容摘要
    – casual_chat: 日常闲聊
    – task_planning: 任务规划和分解
    – tool_orchestration: 编排多个工具
    – multi_step: 多步骤复杂任务

    ## 对话上下文
    {}

    ## 可用工具
    {}

    ## 用户输入
    {}

    ## 请以JSON格式输出意图分析结果:
    {{
    "intent": "意图名称",
    "confidence": 0.0-1.0,
    "reasoning": "识别理由",
    "required_tools": ["需要的工具列表"],
    "estimated_steps": 估计步骤数,
    "params": {{
    "提取的关键参数": "值"
    }}
    }}"#,
    context_summary,
    available_tools,
    input
    );

    // 使用快速模型进行意图推理
    let response = self.model_router
    .complete(ModelRequest {
    model: "gpt-4o-mini".into(), // 使用快速模型
    messages: vec![Message::user(prompt)],
    temperature: 0.1, // 低温度保证一致性
    max_tokens: 500,
    })
    .await?;

    // 解析LLM输出的JSON
    let detection: IntentDetection = self.parse_llm_response(&response)?;

    Ok(IntentDetection {
    source: DetectionSource::Llm,
    ..detection
    })
    }
    }

    5.1.5 意图识别效果对比

    识别层级平均延迟准确率覆盖率资源消耗适用场景
    Level 1 (规则) <1ms 95% (匹配时) 30-40% 极低 高频明确意图
    Level 2 (ML) 5-20ms 88% 70-80% 低 (CPU) 常见意图分类
    Level 3 (LLM) 100-500ms 96% 100% 中 (GPU/CPU) 复杂/模糊意图
    三级级联 3-15ms (平均) 93% 100% 综合最优

    三级级联的平均延迟远低于单独使用 LLM,因为大部分请求在 Level 1 或 Level 2 就能解决,只有约 15-20% 的请求需要进入 Level 3。

    5.2 任务编排策略

    5.2.1 编排模型

    意图识别完成后,路由引擎根据意图类型编排任务执行计划。Hermes Agent 支持四种编排模式:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 任务编排模式 │
    │ │
    │ 模式1: 串行执行 │
    │ ┌─────┐ ┌─────┐ ┌─────┐ │
    │ │ T1 │───>│ T2 │───>│ T3 │ │
    │ └─────┘ └─────┘ └─────┘ │
    │ 每个任务的输出作为下一个任务的输入 │
    │ │
    │ 模式2: 并行执行 │
    │ ┌─────┐ │
    │ │ T1 │╲ │
    │ └─────┘ ╲ ┌─────┐ │
    │ ┌─────┐ ╳──>│ 聚合 │ │
    │ │ T2 │╱ └─────┘ │
    │ └─────┘ │
    │ ┌─────┐ │
    │ │ T3 │╱ │
    │ └─────┘ 所有任务并行执行,结果聚合后返回 │
    │ │
    │ 模式3: 条件分支 │
    │ ┌─────┐ ┌─────┐ │
    │ │ T1 │──> if──>│ T2 │ │
    │ └─────┘ ╲ └─────┘ │
    │ ╲ │
    │ else ─>┌─────┐ │
    │ │ T3 │ │
    │ └─────┘ │
    │ 根据条件选择执行路径 │
    │ │
    │ 模式4: 循环迭代 │
    │ ┌─────┐ ┌─────────┐ ┌─────┐ ┌──────────┐ │
    │ │ T1 │───>│ 判断条件 │───>│ T2 │───>│ 继续迭代? │ │
    │ └─────┘ └─────────┘ └─────┘ └────┬─────┘ │
    │ ║ │ Yes │
    │ ║ <────────────────────────╛ │
    │ ║ │ No │
    │ v v │
    │ 完成 完成 │
    └──────────────────────────────────────────────────────────────────────┘

    5.2.2 任务编排器实现

    /// 任务编排器
    pub struct TaskOrchestrator {
    tool_registry: Arc<ToolRegistry>,
    model_router: Arc<ModelRouter>,
    max_depth: usize,
    parallel_limit: usize,
    timeout_per_task: Duration,
    }

    /// 任务执行计划
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ExecutionPlan {
    pub plan_id: PlanId,
    pub intent: String,
    pub steps: Vec<ExecutionStep>,
    pub estimated_duration: Duration,
    pub estimated_tokens: u64,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct ExecutionStep {
    pub step_id: String,
    pub step_type: StepType,
    pub tool_name: Option<String>,
    pub input_template: serde_json::Value,
    pub depends_on: Vec<String>,
    pub condition: Option<Condition>,
    pub timeout: Duration,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub enum StepType {
    ToolCall,
    LlmCall,
    MemoryRead,
    MemoryWrite,
    Aggregation,
    Conditional,
    Loop,
    }

    impl TaskOrchestrator {
    /// 根据意图创建执行计划
    pub async fn create_plan(
    &self,
    intent: &IntentDetection,
    context: &SessionContext,
    ) -> Result<ExecutionPlan, OrchestrationError> {
    match intent.intent.as_str() {
    "code_execution" => self.plan_code_execution(intent, context).await,
    "web_search" => self.plan_web_search(intent, context).await,
    "knowledge_query" => self.plan_knowledge_query(intent, context).await,
    "data_analysis" => self.plan_data_analysis(intent, context).await,
    "task_planning" => self.plan_task_decomposition(intent, context).await,
    "tool_orchestration" => self.plan_tool_orchestration(intent, context).await,
    "multi_step" => self.plan_multi_step(intent, context).await,
    "casual_chat" => self.plan_simple_chat(intent, context).await,
    _ => self.plan_default(intent, context).await,
    }
    }

    /// 执行计划
    pub async fn execute_plan(
    &self,
    plan: &ExecutionPlan,
    context: &mut SessionContext,
    ) -> Result<ExecutionResult, OrchestrationError> {
    let mut step_results: HashMap<String, serde_json::Value> = HashMap::new();
    let mut completed_steps: HashSet<String> = HashSet::new();

    // 拓扑排序确定执行顺序
    let execution_order = self.topological_sort(&plan.steps)?;

    for step_id in execution_order {
    let step = plan.steps.iter()
    .find(|s| s.step_id == step_id)
    .unwrap();

    // 检查条件
    if let Some(condition) = &step.condition {
    if !self.evaluate_condition(condition, &step_results)? {
    continue;
    }
    }

    // 等待依赖完成
    for dep in &step.depends_on {
    if !completed_steps.contains(dep) {
    return Err(OrchestrationError::DependencyNotMet(
    step_id, dep.clone()
    ));
    }
    }

    // 渲染输入模板(替换变量引用)
    let input = self.render_template(
    &step.input_template,
    &step_results,
    context,
    )?;

    // 执行步骤
    let result = self.execute_step(step, input, context).await?;
    step_results.insert(step_id.clone(), result);
    completed_steps.insert(step_id);
    }

    // 聚合最终结果
    let final_result = self.aggregate_results(&step_results, &plan.steps)?;

    Ok(ExecutionResult {
    plan_id: plan.plan_id.clone(),
    outputs: final_result,
    step_count: completed_steps.len(),
    status: ExecutionStatus::Completed,
    })
    }

    /// 执行单个步骤
    async fn execute_step(
    &self,
    step: &ExecutionStep,
    input: serde_json::Value,
    context: &mut SessionContext,
    ) -> Result<serde_json::Value, OrchestrationError> {
    match step.step_type {
    StepType::ToolCall => {
    let tool_name = step.tool_name.as_ref().unwrap();
    let tool = self.tool_registry.find_by_name(tool_name).await?;
    let executor = self.tool_registry.get_executor(&tool.tool_id).await?;

    let exec_context = ExecutionContext {
    session_id: context.session_id.clone(),
    user_id: context.user_id.clone(),
    tool_call_id: uuid::Uuid::new_v4().to_string(),
    sandbox_id: None,
    deadline: Instant::now() + step.timeout,
    permissions: vec![], // 从用户角色获取
    };

    let output = executor.execute(input, &exec_context).await?;
    Ok(output.result)
    }

    StepType::LlmCall => {
    let response = self.model_router
    .complete(ModelRequest {
    model: "default".into(),
    messages: vec![Message::user(
    input.get("prompt")
    .and_then(|v| v.as_str())
    .unwrap_or("")
    .to_string()
    )],
    temperature: 0.7,
    max_tokens: 2000,
    })
    .await?;

    Ok(serde_json::json!({
    "response": response.content,
    "tokens_used": response.tokens_used,
    }))
    }

    StepType::MemoryRead => {
    // 从记忆系统读取
    Ok(serde_json::json!({
    "memories": [],
    }))
    }

    StepType::MemoryWrite => {
    // 写入记忆系统
    Ok(serde_json::json!({"success": true}))
    }

    StepType::Aggregation => {
    // 聚合步骤的结果
    Ok(input)
    }

    _ => Ok(serde_json::json!({"status": "skipped"})),
    }
    }

    /// 模板渲染:替换 {{step_id.field}} 格式的变量引用
    fn render_template(
    &self,
    template: &serde_json::Value,
    results: &HashMap<String, serde_json::Value>,
    context: &SessionContext,
    ) -> Result<serde_json::Value, OrchestrationError> {
    match template {
    serde_json::Value::String(s) => {
    let rendered = self.replace_variables(s, results, context)?;
    Ok(serde_json::Value::String(rendered))
    }
    serde_json::Value::Object(map) => {
    let mut rendered = serde_json::Map::new();
    for (k, v) in map {
    rendered.insert(k.clone(),
    self.render_template(v, results, context)?);
    }
    Ok(serde_json::Value::Object(rendered))
    }
    serde_json::Value::Array(arr) => {
    let mut rendered = Vec::new();
    for v in arr {
    rendered.push(self.render_template(v, results, context)?);
    }
    Ok(serde_json::Value::Array(rendered))
    }
    _ => Ok(template.clone()),
    }
    }

    fn replace_variables(
    &self,
    text: &str,
    results: &HashMap<String, serde_json::Value>,
    _context: &SessionContext,
    ) -> Result<String, OrchestrationError> {
    // 替换 {{step_id.field}} 格式的引用
    let mut result = text.to_string();

    // 简化的变量替换逻辑
    for (step_id, value) in results {
    let placeholder = format!("{{{{{}.result}}}}", step_id);
    if let Some(s) = value.as_str() {
    result = result.replace(&placeholder, s);
    } else {
    result = result.replace(&placeholder, &value.to_string());
    }
    }

    Ok(result)
    }
    }

    5.2.3 编排策略示例

    以下是一个复杂任务的多步骤编排示例——“分析某公司的财务数据并生成报告”:

    {
    "plan_id": "plan_001",
    "intent": "data_analysis",
    "steps": [
    {
    "step_id": "fetch_data",
    "step_type": "ToolCall",
    "tool_name": "http_request",
    "input_template": {
    "method": "GET",
    "url": "https://api.finance.internal/companies/{{company_id}}/financials",
    "headers": { "Authorization": "Bearer {{api_token}}" }
    },
    "depends_on": [],
    "timeout": "15s"
    },
    {
    "step_id": "parse_data",
    "step_type": "ToolCall",
    "tool_name": "code_execution",
    "input_template": {
    "language": "python",
    "code": "import json\\ndata = json.loads('''{{fetch_data.result}}''')\\n# 提取关键财务指标\\nmetrics = {\\n 'revenue': data['revenue'],\\n 'profit_margin': data['net_income'] / data['revenue'],\\n 'debt_ratio': data['total_debt'] / data['total_assets']\\n}\\nprint(json.dumps(metrics))"
    },
    "depends_on": ["fetch_data"],
    "timeout": "10s"
    },
    {
    "step_id": "search_benchmarks",
    "step_type": "ToolCall",
    "tool_name": "web_search",
    "input_template": {
    "query": "{{company_name}} 行业平均财务指标 基准"
    },
    "depends_on": [],
    "timeout": "15s"
    },
    {
    "step_id": "analyze",
    "step_type": "LlmCall",
    "input_template": {
    "prompt": "基于以下财务数据和行业基准,进行深度分析:\\n\\n公司数据:{{parse_data.result}}\\n行业基准:{{search_benchmarks.result}}\\n\\n请分析:1.财务健康状况 2.与行业对比 3.风险点 4.建议"
    },
    "depends_on": ["parse_data", "search_benchmarks"],
    "timeout": "30s"
    },
    {
    "step_id": "save_memory",
    "step_type": "MemoryWrite",
    "input_template": {
    "content": "用户请求分析{{company_name}}的财务数据。分析结果:{{analyze.result}}",
    "type": "analysis_result"
    },
    "depends_on": ["analyze"],
    "timeout": "5s"
    },
    {
    "step_id": "generate_report",
    "step_type": "LlmCall",
    "input_template": {
    "prompt": "将以下分析结果整理为格式化的报告:\\n\\n{{analyze.result}}\\n\\n请使用Markdown格式,包含标题、摘要、详细分析、建议等部分。"
    },
    "depends_on": ["analyze"],
    "timeout": "30s"
    }
    ]
    }

    这个编排计划的执行流程是:

  • 并行启动:fetch_data(获取财务数据)和 search_benchmarks(搜索行业基准)同时执行
  • 串行依赖:parse_data(解析数据)等待 fetch_data 完成
  • 汇聚点:analyze(LLM分析)等待 parse_data 和 search_benchmarks 都完成
  • 并行后处理:save_memory(保存记忆)和 generate_report(生成报告)在 analyze 完成后并行执行
  • 最终结果:generate_report 的输出作为最终响应返回给用户
  • 5.3 记忆调度机制

    5.3.1 记忆层级与调度策略

    Hermes Agent 的记忆系统采用三层架构,Gateway 的记忆调度器根据当前任务的意图和上下文,决定何时、从哪个层级、读取或写入什么记忆:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 记忆调度架构 │
    │ │
    │ ┌──────────────────────────────────────────────────────┐ │
    │ │ 工作记忆 (Working Memory) │ │
    │ │ · 当前会话上下文(消息历史) │ │
    │ │ · 活跃工具调用栈 │ │
    │ │ · 临时变量 │ │
    │ │ 存储: Redis (热存储) │ │
    │ │ 寿命: 会话级别 │ │
    │ └──────────────────────────────────────────────────────┘ │
    │ │ │
    │ ┌──────────────────────────────────────────────────────┐ │
    │ │ 短期记忆 (Short-term Memory) │ │
    │ │ · 最近N次会话的摘要 │ │
    │ │ · 用户近期偏好和行为模式 │ │
    │ │ · 临时知识(搜索结果缓存等) │ │
    │ │ 存储: PostgreSQL + Redis Cache │ │
    │ │ 寿命: 7-30天 │ │
    │ └──────────────────────────────────────────────────────┘ │
    │ │ │
    │ ┌──────────────────────────────────────────────────────┐ │
    │ │ 长期记忆 (Long-term Memory) │ │
    │ │ · 用户画像(职业、技能、偏好) │ │
    │ │ · 持久化知识(用户传授的事实、决策) │ │
    │ │ · 语义向量索引(可检索的知识库) │ │
    │ │ 存储: 向量数据库 + S3 + PostgreSQL │ │
    │ │ 寿命: 永久(可手动删除) │ │
    │ └──────────────────────────────────────────────────────┘ │
    └──────────────────────────────────────────────────────────────────────┘

    5.3.2 记忆调度器实现

    /// 记忆调度器
    pub struct MemoryScheduler {
    /// 工作记忆存储
    working_memory: Arc<WorkingMemoryStore>,
    /// 短期记忆存储
    short_term_memory: Arc<ShortTermMemoryStore>,
    /// 长期记忆存储
    long_term_memory: Arc<LongTermMemoryStore>,
    /// 记忆检索模型
    retrieval_model: Arc<MemoryRetrievalModel>,
    }

    /// 记忆调度决策
    #[derive(Clone, Debug)]
    pub struct MemoryDecision {
    /// 是否需要读取工作记忆
    pub read_working: bool,
    /// 是否需要读取短期记忆
    pub read_short_term: bool,
    /// 是否需要读取长期记忆
    pub read_long_term: bool,
    /// 长期记忆检索查询
    pub retrieval_query: Option<String>,
    /// 是否需要写入记忆
    pub write_memory: bool,
    /// 写入类型
    pub write_type: Option<MemoryWriteType>,
    }

    #[derive(Clone, Debug)]
    pub enum MemoryWriteType {
    /// 会话摘要(会话结束时)
    SessionSummary,
    /// 用户偏好更新
    PreferenceUpdate,
    /// 知识存储
    KnowledgeStore,
    /// 决策记录
    DecisionRecord,
    }

    impl MemoryScheduler {
    /// 根据意图和上下文决定记忆调度策略
    pub fn decide(
    &self,
    intent: &IntentDetection,
    context: &SessionContext,
    ) -> MemoryDecision {
    let mut decision = MemoryDecision {
    read_working: true, // 总是读取工作记忆
    read_short_term: false,
    read_long_term: false,
    retrieval_query: None,
    write_memory: false,
    write_type: None,
    };

    match intent.intent.as_str() {
    "casual_chat" => {
    // 闲聊:只读工作记忆,不需要长期记忆
    }

    "knowledge_query" => {
    // 知识查询:需要读取长期记忆
    decision.read_long_term = true;
    decision.retrieval_query = Some(
    intent.extracted_params
    .get("query")
    .cloned()
    .unwrap_or_default()
    );
    }

    "task_planning" | "multi_step" => {
    // 任务规划:需要短期记忆(了解最近做过什么)
    decision.read_short_term = true;
    decision.read_long_term = true;
    decision.retrieval_query = Some(
    self.build_retrieval_query(intent, context)
    );
    }

    "data_analysis" => {
    // 数据分析:可能需要长期记忆(历史分析结果)
    decision.read_short_term = true;
    decision.read_long_term = true;
    }

    _ => {
    // 默认:读取短期记忆
    decision.read_short_term = true;
    }
    }

    // 会话接近结束时,标记需要写入记忆
    if context.metadata.turn_count > 0 &&
    context.metadata.turn_count % 10 == 0 {
    decision.write_memory = true;
    decision.write_type = Some(MemoryWriteType::SessionSummary);
    }

    // 如果用户表达了偏好,标记写入
    if self.detect_preference_expression(&intent, context) {
    decision.write_memory = true;
    decision.write_type = Some(MemoryWriteType::PreferenceUpdate);
    }

    decision
    }

    /// 执行记忆调度
    pub async fn execute(
    &self,
    decision: &MemoryDecision,
    context: &SessionContext,
    ) -> Result<MemoryBundle, MemoryError> {
    let mut bundle = MemoryBundle::default();

    // 读取工作记忆(总是执行)
    bundle.working = Some(
    self.working_memory.get(&context.session_id).await?
    );

    // 读取短期记忆
    if decision.read_short_term {
    bundle.short_term = Some(
    self.short_term_memory
    .get_recent(&context.user_id, Duration::from_secs(7 * 86400))
    .await?
    );
    }

    // 读取长期记忆
    if decision.read_long_term {
    if let Some(query) = &decision.retrieval_query {
    bundle.long_term = Some(
    self.long_term_memory
    .semantic_search(&context.user_id, query, 10)
    .await?
    );
    } else {
    bundle.long_term = Some(
    self.long_term_memory
    .get_user_profile(&context.user_id)
    .await?
    );
    }
    }

    Ok(bundle)
    }

    /// 检测用户是否表达了偏好
    fn detect_preference_expression(
    &self,
    intent: &IntentDetection,
    _context: &SessionContext,
    ) -> bool {
    // 简化的偏好检测逻辑
    let preference_keywords = [
    "我喜欢", "我不喜欢", "请记住", "以后都用",
    "I prefer", "I like", "remember that",
    ];

    if let Some(text) = intent.extracted_params.get("text") {
    return preference_keywords.iter()
    .any(|kw| text.contains(kw));
    }

    false
    }

    fn build_retrieval_query(
    &self,
    intent: &IntentDetection,
    context: &SessionContext,
    ) -> String {
    // 基于意图和最近对话构建检索查询
    let recent_topic = context.metadata.topic.as_deref().unwrap_or("");
    format!("{} {}", recent_topic, intent.intent)
    }
    }

    /// 记忆包(包含从各层级读取的记忆)
    #[derive(Default, Clone, Debug)]
    pub struct MemoryBundle {
    pub working: Option<WorkingMemory>,
    pub short_term: Option<ShortTermMemory>,
    pub long_term: Option<LongTermMemory>,
    }

    5.3.3 记忆注入策略

    读取到的记忆需要以适当的方式注入到 LLM 的上下文中,既不能占用过多 Token,又要确保相关信息被有效利用:

    /// 记忆注入器
    pub struct MemoryInjector {
    /// Token 估算器
    token_estimator: TokenEstimator,
    /// 记忆 Token 预算(占上下文窗口的比例)
    memory_token_budget: f64,
    }

    impl MemoryInjector {
    /// 将记忆注入到 LLM 上下文
    pub fn inject(
    &self,
    messages: &mut Vec<Message>,
    bundle: &MemoryBundle,
    context_window: u64,
    ) -> InjectionResult {
    let mut tokens_injected = 0u64;
    let memory_budget = (context_window as f64 * self.memory_token_budget) as u64;

    let mut injected_sections = Vec::new();

    // 注入长期记忆(用户画像)
    if let Some(lt) = &bundle.long_term {
    if let Some(profile) = &lt.user_profile {
    let section = format!(
    "[用户画像]\\n{}\\n",
    profile.summary
    );
    let tokens = self.token_estimator.estimate_text(&section);
    if tokens_injected + tokens <= memory_budget {
    injected_sections.push(section);
    tokens_injected += tokens;
    }
    }

    // 注入相关长期记忆
    if let Some(memories) = &lt.relevant_memories {
    let section = memories.iter()
    .take(5) // 最多5条
    .map(|m| format!("- {}", m.summary))
    .collect::<Vec<_>>()
    .join("\\n");
    let section = format!("[相关记忆]\\n{}\\n", section);
    let tokens = self.token_estimator.estimate_text(&section);
    if tokens_injected + tokens <= memory_budget {
    injected_sections.push(section);
    tokens_injected += tokens;
    }
    }
    }

    // 注入短期记忆
    if let Some(st) = &bundle.short_term {
    let section = st.recent_summaries.iter()
    .take(3)
    .map(|s| format!("- [{}] {}", s.timestamp.format("%m-%d"), s.summary))
    .collect::<Vec<_>>()
    .join("\\n");
    let section = format!("[近期会话摘要]\\n{}\\n", section);
    let tokens = self.token_estimator.estimate_text(&section);
    if tokens_injected + tokens <= memory_budget {
    injected_sections.push(section);
    tokens_injected += tokens;
    }
    }

    // 将记忆注入为系统消息
    if !injected_sections.is_empty() {
    let memory_block = injected_sections.join("\\n");
    let system_msg = Message::system(format!(
    "以下是关于用户的相关记忆信息,请参考:\\n\\n{}\\n",
    memory_block
    ));

    // 插入到消息列表开头(在原始系统提示之后)
    messages.insert(1, system_msg);
    }

    InjectionResult {
    tokens_injected,
    sections_injected: injected_sections.len(),
    }
    }
    }

    5.4 安全沙箱集成

    5.4.1 沙箱架构

    对于代码执行等高风险工具调用,Gateway 通过安全沙箱提供隔离执行环境。Hermes Agent 选择 Firecracker microVM 作为沙箱引擎:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 安全沙箱架构 │
    │ │
    │ ┌──────────────────────────────────────────────────────────────┐ │
    │ │ Gateway 进程 │ │
    │ │ │ │
    │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
    │ │ │ Tool Call 1 │ │ Tool Call 2 │ │ Tool Call 3 │ │ │
    │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
    │ │ │ │ │ │ │
    │ └─────────┼──────────────────┼──────────────────┼──────────────┘ │
    │ │ │ │ │
    │ v v v │
    │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
    │ │ microVM 1 │ │ microVM 2 │ │ microVM 3 │ │
    │ │ │ │ │ │ │ │
    │ │ · Python │ │ · Node.js │ │ · Shell │ │
    │ │ · 内存:512MB │ │ · 内存:256MB │ │ · 内存:128MB │ │
    │ │ · CPU: 1核 │ │ · CPU: 0.5核 │ │ · CPU: 0.25核│ │
    │ │ · 无网络 │ │ · 受限网络 │ │ · 无网络 │ │
    │ │ · 只读FS │ │ · 临时FS │ │ · 只读FS │ │
    │ └──────────────┘ └──────────────┘ └──────────────┘ │
    │ │
    │ 安全隔离: 进程隔离 + 文件系统隔离 + 网络隔离 + 资源限制 │
    │ 启动延迟: ~125ms (Firecracker microVM) │
    └──────────────────────────────────────────────────────────────────────┘

    5.4.2 沙箱管理器

    /// 沙箱管理器
    pub struct SandboxManager {
    /// Firecracker API 客户端
    fc_client: Arc<FirecrackerClient>,
    /// 沙箱池(预热实例)
    pool: Arc<RwLock<Vec<SandboxInstance>>>,
    /// 活跃实例映射
    active: Arc<RwLock<HashMap<String, SandboxInstance>>>,
    /// 配置
    config: SandboxConfig,
    }

    #[derive(Clone, Debug)]
    pub struct SandboxInstance {
    pub sandbox_id: String,
    pub vm_id: String,
    pub status: SandboxStatus,
    pub created_at: DateTime<Utc>,
    pub socket_path: String,
    pub resources: ResourceLimits,
    pub network_policy: NetworkPolicy,
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    pub enum SandboxStatus {
    Idle,
    Busy,
    Stopping,
    Stopped,
    Failed,
    }

    #[derive(Clone, Debug)]
    pub struct ResourceLimits {
    pub memory_mb: u32,
    pub cpu_cores: f32,
    pub disk_mb: u32,
    pub timeout: Duration,
    }

    #[derive(Clone, Debug)]
    pub struct NetworkPolicy {
    pub mode: NetworkMode,
    pub allowed_domains: Vec<String>,
    pub blocked_ips: Vec<String>,
    }

    #[derive(Clone, Debug)]
    pub enum NetworkMode {
    None, // 无网络
    Restricted, // 受限网络(白名单)
    Full, // 完全网络(不推荐)
    }

    impl SandboxManager {
    /// 获取或创建沙箱实例
    pub async fn acquire_or_create(
    &self,
    session_id: &SessionId,
    ) -> Result<SandboxHandle, SandboxError> {
    // 1. 尝试从池中获取空闲实例
    if let Some(instance) = self.try_acquire_from_pool().await {
    return Ok(SandboxHandle {
    instance,
    manager: Arc::new(self.clone()),
    });
    }

    // 2. 池中没有,创建新实例
    let instance = self.create_sandbox(session_id).await?;

    Ok(SandboxHandle {
    instance,
    manager: Arc::new(self.clone()),
    })
    }

    /// 创建新的沙箱实例
    async fn create_sandbox(
    &self,
    session_id: &SessionId,
    ) -> Result<SandboxInstance, SandboxError> {
    let vm_id = format!("vm-{}-{}", session_id, uuid::Uuid::new_v4());
    let socket_path = format!("/tmp/{}.sock", vm_id);

    // Firecracker VM 配置
    let vm_config = FirecrackerVmConfig {
    vm_id: vm_id.clone(),
    kernel_image: self.config.kernel_image.clone(),
    rootfs: self.config.rootfs.clone(),
    memory_mib: self.config.default_memory_mb,
    vcpu_count: self.config.default_vcpu_count,
    network_interfaces: vec![],
    drives: vec![],
    socket_path: socket_path.clone(),
    };

    // 启动 microVM
    self.fc_client.create_vm(&vm_config).await?;

    // 等待 VM 就绪
    self.wait_for_ready(&socket_path, Duration::from_secs(5)).await?;

    let instance = SandboxInstance {
    sandbox_id: uuid::Uuid::new_v4().to_string(),
    vm_id,
    status: SandboxStatus::Busy,
    created_at: Utc::now(),
    socket_path,
    resources: ResourceLimits {
    memory_mb: self.config.default_memory_mb,
    cpu_cores: self.config.default_vcpu_count as f32,
    disk_mb: self.config.default_disk_mb,
    timeout: self.config.default_timeout,
    },
    network_policy: NetworkPolicy {
    mode: NetworkMode::None,
    allowed_domains: vec![],
    blocked_ips: vec![],
    },
    };

    // 记录活跃实例
    self.active.write().await
    .insert(instance.sandbox_id.clone(), instance.clone());

    Ok(instance)
    }

    /// 在沙箱中执行代码
    pub async fn execute_code(
    &self,
    sandbox: &SandboxInstance,
    language: &str,
    code: &str,
    ) -> Result<ExecutionResult, SandboxError> {
    let start = Instant::now();

    // 通过 VSOCK 或 HTTP 向沙箱发送执行请求
    let request = serde_json::json!({
    "language": language,
    "code": code,
    "timeout": sandbox.resources.timeout.as_secs(),
    });

    let response = self.fc_client
    .execute_in_vm(&sandbox.vm_id, &request)
    .await?;

    let execution_time = start.elapsed();

    Ok(ExecutionResult {
    stdout: response.stdout,
    stderr: response.stderr,
    exit_code: response.exit_code,
    execution_time_ms: execution_time.as_millis() as u64,
    tokens_used: response.tokens_used,
    })
    }

    /// 释放沙箱(归还池或销毁)
    pub async fn release(&self, sandbox_id: &str) -> Result<(), SandboxError> {
    let mut active = self.active.write().await;

    if let Some(mut instance) = active.remove(sandbox_id) {
    // 如果池未满,归还池中复用
    let mut pool = self.pool.write().await;
    if pool.len() < self.config.pool_size {
    instance.status = SandboxStatus::Idle;
    pool.push(instance);
    } else {
    // 池已满,销毁实例
    self.destroy_sandbox(&instance).await?;
    }
    }

    Ok(())
    }

    /// 销毁沙箱实例
    async fn destroy_sandbox(
    &self,
    instance: &SandboxInstance,
    ) -> Result<(), SandboxError> {
    self.fc_client.destroy_vm(&instance.vm_id).await?;
    // 清理临时文件
    let _ = std::fs::remove_file(&instance.socket_path);
    Ok(())
    }

    /// 健康检查
    pub async fn health_check(&self) -> Result<bool, ToolError> {
    let pool = self.pool.read().await;
    let active = self.active.read().await;

    // 检查是否有可用的沙箱实例
    Ok(!pool.is_empty() || !active.is_empty())
    }
    }

    /// 沙箱配置
    #[derive(Clone, Debug)]
    pub struct SandboxConfig {
    pub kernel_image: String,
    pub rootfs: String,
    pub default_memory_mb: u32,
    pub default_vcpu_count: u32,
    pub default_disk_mb: u32,
    pub default_timeout: Duration,
    pub pool_size: usize,
    pub pool_warm_count: usize,
    }

    5.4.3 沙箱安全策略

    # 沙箱安全配置
    sandbox:
    engine: firecracker
    kernel_image: /opt/hermes/vmlinux
    rootfs: /opt/hermes/rootfs.ext4

    defaults:
    memory_mb: 512
    vcpu_count: 1
    disk_mb: 1024
    timeout: 30s

    pool:
    size: 20 # 池最大实例数
    warm_count: 5 # 预热实例数
    idle_timeout: 300s # 空闲实例超时回收

    security:
    # 文件系统安全
    filesystem:
    mode: read_only_overlay # 只读根 + 可写临时层
    temp_dir_size: 256MB
    allowed_paths:
    /tmp
    /workspace
    blocked_paths:
    /etc/shadow
    /root

    # 网络安全
    network:
    default_mode: none # 默认无网络
    restricted:
    allowed_domains:
    pypi.org
    files.pythonhosted.org
    registry.npmjs.org
    blocked_ips:
    10.0.0.0/8
    172.16.0.0/12
    192.168.0.0/16
    max_connections: 10
    connection_timeout: 10s

    # 资源限制
    resources:
    max_processes: 50
    max_file_descriptors: 256
    max_memory_mb: 1024
    max_cpu_time: 60s

    # 系统调用过滤
    syscall_filter:
    enabled: true
    mode: whitelist
    allowed_syscalls:
    read
    write
    open
    close
    mmap
    munmap
    brk
    exit
    exit_group
    # … 更多允许的系统调用

    # 环境变量过滤
    env_filter:
    blocked_prefixes:
    SECRET_
    PRIVATE_
    KEY_
    blocked_names:
    AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY
    DATABASE_URL


    第六章 Gateway 与其他模块的交互

    6.1 与记忆系统的协作

    6.1.1 交互架构

    ┌──────────────────────────────────────────────────────────────────────┐
    │ Gateway 与记忆系统的协作架构 │
    │ │
    │ ┌─────────────┐ ┌─────────────────────┐ │
    │ │ │ 1.记忆调度决策 │ │ │
    │ │ Gateway │ ════════════════> │ │ │
    │ │ 路由引擎 │ │ 记忆系统 │ │
    │ │ │ 2.读取工作记忆 │ (Memory System) │ │
    │ │ │ <════════════════ │ │ │
    │ │ │ │ ┌───────────────┐ │ │
    │ │ │ 3.检索长期记忆 │ │ Working Memory│ │ │
    │ │ │ ════════════════> │ │ (Redis) │ │ │
    │ │ │ │ └───────────────┘ │ │
    │ │ │ 4.返回相关记忆 │ ┌───────────────┐ │ │
    │ │ │ <════════════════ │ │ Short-term │ │ │
    │ │ │ │ │ (PostgreSQL) │ │ │
    │ │ │ 5.记忆注入上下文 │ └───────────────┘ │ │
    │ │ │ (内部操作) │ ┌───────────────┐ │ │
    │ │ │ │ │ Long-term │ │ │
    │ │ │ 6.执行任务 │ │ (Vector DB) │ │ │
    │ │ │ (内部操作) │ └───────────────┘ │ │
    │ │ │ │ │ │
    │ │ │ 7.写入新记忆 │ │ │
    │ │ │ ════════════════> │ │ │
    │ └─────────────┘ └─────────────────────┘ │
    └──────────────────────────────────────────────────────────────────────┘

    6.1.2 交互协议

    Gateway 与记忆系统之间通过 gRPC 进行通信,定义了以下核心 RPC 接口:

    // memory_service.proto
    syntax = "proto3";

    package hermes.memory;

    service MemoryService {
    // 读取工作记忆
    rpc GetWorkingMemory(GetWorkingMemoryRequest) returns (WorkingMemory);

    // 检索长期记忆
    rpc SearchLongTermMemory(SearchRequest) returns (SearchResponse);

    // 获取用户画像
    rpc GetUserProfile(GetUserProfileRequest) returns (UserProfile);

    // 写入记忆
    rpc WriteMemory(WriteMemoryRequest) returns (WriteMemoryResponse);

    // 更新用户画像
    rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UserProfile);

    // 获取最近会话摘要
    rpc GetRecentSummaries(GetRecentSummariesRequest) returns (RecentSummaries);

    // 记忆健康检查
    rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
    }

    message GetWorkingMemoryRequest {
    string session_id = 1;
    }

    message WorkingMemory {
    string session_id = 1;
    repeated Message messages = 2;
    optional string compressed_summary = 3;
    map<string, string> temp_variables = 4;
    }

    message SearchRequest {
    string user_id = 1;
    string query = 2;
    int32 top_k = 3;
    float min_score = 4;
    repeated string memory_types = 5;
    }

    message SearchResponse {
    repeated MemoryItem items = 1;
    }

    message MemoryItem {
    string memory_id = 1;
    string content = 2;
    float relevance_score = 3;
    string memory_type = 4;
    int64 created_at = 5;
    map<string, string> metadata = 6;
    }

    message WriteMemoryRequest {
    string user_id = 1;
    string session_id = 2;
    string content = 3;
    string memory_type = 4;
    map<string, string> metadata = 5;
    bool enable_dedup = 6;
    }

    message WriteMemoryResponse {
    string memory_id = 1;
    bool is_duplicate = 2;
    }

    6.1.3 降级策略

    当记忆系统不可用时,Gateway 的降级行为:

    /// 记忆系统降级处理器
    pub struct MemoryDegradationHandler;

    impl MemoryDegradationHandler {
    pub async fn handle_memory_unavailable(
    &self,
    context: &mut SessionContext,
    ) -> MemoryBundle {
    tracing::warn!(
    "Memory system unavailable, degrading to stateless mode"
    );

    // 降级为无记忆模式
    // 1. 工作记忆:仍然可用(来自本地缓存)
    // 2. 短期记忆:跳过
    // 3. 长期记忆:跳过

    MemoryBundle {
    working: Some(WorkingMemory {
    session_id: context.session_id.clone(),
    messages: context.messages.clone(),
    compressed_summary: context.compressed_summary
    .as_ref().map(|s| s.text.clone()),
    temp_variables: HashMap::new(),
    }),
    short_term: None,
    long_term: None,
    }
    }

    pub async fn handle_memory_timeout(
    &self,
    partial_result: PartialMemoryBundle,
    ) -> MemoryBundle {
    // 使用已获取的部分结果,缺失部分用空值填充
    tracing::warn!(
    "Memory system timeout, using partial results"
    );

    MemoryBundle {
    working: partial_result.working,
    short_term: partial_result.short_term,
    long_term: None, // 长期记忆通常最慢,超时概率最高
    }
    }
    }

    6.2 与模型路由器的协作

    6.2.1 交互架构

    ┌──────────────────────────────────────────────────────────────────────┐
    │ Gateway 与模型路由器的协作架构 │
    │ │
    │ ┌─────────────┐ ┌─────────────────────┐ │
    │ │ │ 1.推理请求 │ │ │
    │ │ Gateway │ ════════════════> │ │ │
    │ │ 路由引擎 │ (model_hint, │ 模型路由器 │ │
    │ │ │ messages, │ (Model Router) │ │
    │ │ │ constraints) │ │ │
    │ │ │ │ ┌───────────────┐ │ │
    │ │ │ 2.模型选择 │ │ 路由决策引擎 │ │ │
    │ │ │ <════════════════ │ │ · 能力匹配 │ │ │
    │ │ │ (selected_model) │ │ · 成本优化 │ │ │
    │ │ │ │ │ · 延迟优先 │ │ │
    │ │ │ 3.推理执行 │ └───────────────┘ │ │
    │ │ │ ════════════════> │ ┌───────────────┐ │ │
    │ │ │ │ │ 模型池 │ │ │
    │ │ │ 4.流式响应 │ │ · GPT-4o │ │ │
    │ │ │ <════════════════ │ │ · Claude 3.5 │ │ │
    │ │ │ (token by token) │ │ · Gemini 1.5 │ │ │
    │ │ │ │ │ · 本地模型 │ │ │
    │ │ │ 5.使用统计 │ └───────────────┘ │ │
    │ │ │ <════════════════ │ │ │
    │ └─────────────┘ └─────────────────────┘ │
    └──────────────────────────────────────────────────────────────────────┘

    6.2.2 模型选择策略

    Gateway 在向模型路由器发起请求时,会附带模型选择提示,帮助路由器做出最优决策:

    /// 模型请求
    pub struct ModelRequest {
    /// 模型提示(非强制指定)
    pub model: ModelHint,
    /// 消息列表
    pub messages: Vec<Message>,
    /// 温度
    pub temperature: f32,
    /// 最大Token数
    pub max_tokens: u32,
    /// 优先级
    pub priority: RequestPriority,
    /// 约束条件
    pub constraints: ModelConstraints,
    }

    /// 模型提示
    pub enum ModelHint {
    /// 使用默认模型
    Default,
    /// 指定模型
    Specific(String),
    /// 快速模型(低延迟)
    Fast,
    /// 强力模型(高质量)
    Powerful,
    /// 代码专用
    Code,
    /// 推理专用
    Reasoning,
    }

    /// 模型约束
    pub struct ModelConstraints {
    /// 最大延迟
    pub max_latency: Option<Duration>,
    /// 最大成本(每1000Token)
    pub max_cost_per_1k: Option<f64>,
    /// 最低质量要求
    pub min_quality: Option<f64>,
    /// 上下文窗口需求
    pub context_window_needed: u64,
    /// 是否需要函数调用能力
    pub needs_function_calling: bool,
    /// 是否需要视觉能力
    pub needs_vision: bool,
    }

    /// 请求优先级
    pub enum RequestPriority {
    /// 低优先级(可排队)
    Low,
    /// 普通优先级
    Normal,
    /// 高优先级(优先调度)
    High,
    /// 紧急(最高优先级,可抢占)
    Urgent,
    }

    模型路由器根据 Gateway 提供的提示和约束,综合考虑以下因素选择最优模型:

    决策因素权重说明
    能力匹配 0.30 模型是否具备所需能力(函数调用、视觉等)
    延迟 0.25 模型的平均响应延迟
    成本 0.20 每1000 Token 的价格
    质量 0.15 模型在相关任务上的基准评分
    可用性 0.10 模型当前是否可用,负载情况

    6.3 与自进化引擎的协作

    6.3.1 交互架构

    自进化引擎是 Hermes Agent 的差异化能力之一。它持续分析 Gateway 的运行数据,优化路由策略、工具配置和系统参数:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ Gateway 与自进化引擎的协作架构 │
    │ │
    │ ┌─────────────┐ ┌─────────────────────┐ │
    │ │ │ 1.运行数据上报 │ │ │
    │ │ Gateway │ ════════════════> │ │ │
    │ │ │ (指标、日志、 │ 自进化引擎 │ │
    │ │ │ 追踪数据) │ (Self-Evolution) │ │
    │ │ │ │ │ │
    │ │ │ 2.策略下发 │ ┌───────────────┐ │ │
    │ │ │ <════════════════ │ │ 数据分析器 │ │ │
    │ │ │ (优化后的 │ │ · 性能分析 │ │ │
    │ │ │ 路由规则、 │ │ · 质量评估 │ │ │
    │ │ │ 参数配置、 │ │ · 异常检测 │ │ │
    │ │ │ 模型选择策略) │ └───────┬───────┘ │ │
    │ │ │ │ │ │ │
    │ │ │ 3.热加载配置 │ ┌───────v───────┐ │ │
    │ │ │ (无需重启) │ │ 策略优化器 │ │ │
    │ │ │ │ │ · 路由优化 │ │ │
    │ │ │ 4.AB测试反馈 │ │ · 参数调优 │ │ │
    │ │ │ ════════════════> │ │ · 模型选择 │ │ │
    │ └─────────────┘ │ └───────┬───────┘ │ │
    │ │ │ │ │
    │ │ ┌───────v───────┐ │ │
    │ │ │ 策略下发器 │ │ │
    │ │ │ · 配置推送 │ │ │
    │ │ │ · AB测试管理 │ │ │
    │ │ └───────────────┘ │ │
    │ └─────────────────────┘ │
    └──────────────────────────────────────────────────────────────────────┘

    6.3.2 数据上报与策略接收

    /// 自进化引擎交互客户端
    pub struct EvolutionClient {
    /// gRPC 客户端
    client: Arc<EvolutionServiceClient>,
    /// 数据上报缓冲区
    report_buffer: Arc<RwLock<Vec<EvolutionDataPoint>>>,
    /// 当前生效的策略版本
    active_strategy_version: Arc<RwLock<u64>>,
    }

    /// 运行数据点
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct EvolutionDataPoint {
    pub timestamp: DateTime<Utc>,
    pub session_id: SessionId,
    pub intent: String,
    pub model_used: String,
    pub tools_used: Vec<String>,
    pub latency_ms: u64,
    pub tokens_consumed: u64,
    pub user_satisfaction: Option<f64>,
    pub success: bool,
    pub error: Option<String>,
    }

    /// 自进化策略
    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct EvolutionStrategy {
    pub strategy_version: u64,
    pub routing_rules: Vec<OptimizedRoutingRule>,
    pub model_preferences: HashMap<String, String>,
    pub tool_priorities: HashMap<String, f64>,
    pub parameter_overrides: HashMap<String, serde_json::Value>,
    pub ab_test_config: Option<ABTestConfig>,
    }

    impl EvolutionClient {
    /// 批量上报运行数据
    pub async fn report_batch(
    &self,
    ) -> Result<(), EvolutionError> {
    let mut buffer = self.report_buffer.write().await;
    if buffer.is_empty() {
    return Ok(());
    }

    let data_points = std::mem::take(&mut *buffer);
    drop(buffer);

    self.client.report_data(ReportRequest {
    data_points,
    }).await?;

    Ok(())
    }

    /// 接收策略更新
    pub async fn receive_strategy_update(
    &self,
    ) -> Result<Option<EvolutionStrategy>, EvolutionError> {
    let current_version = *self.active_strategy_version.read().await;

    let response = self.client
    .get_latest_strategy(GetStrategyRequest {
    current_version,
    })
    .await?;

    if response.strategy.is_none() {
    return Ok(None);
    }

    let strategy = response.strategy.unwrap();

    // 更新版本号
    *self.active_strategy_version.write().await = strategy.strategy_version;

    Ok(Some(strategy))
    }

    /// 应用策略到 Gateway
    pub async fn apply_strategy(
    &self,
    strategy: &EvolutionStrategy,
    gateway: &Arc<Gateway>,
    ) -> Result<(), EvolutionError> {
    // 1. 更新路由规则
    for rule in &strategy.routing_rules {
    gateway.routing_engine
    .update_rule(rule).await?;
    }

    // 2. 更新模型偏好
    for (intent, model) in &strategy.model_preferences {
    gateway.routing_engine
    .set_model_preference(intent, model).await;
    }

    // 3. 更新工具优先级
    for (tool, priority) in &strategy.tool_priorities {
    gateway.tool_registry
    .set_tool_priority(tool, *priority).await;
    }

    // 4. 应用参数覆盖
    for (key, value) in &strategy.parameter_overrides {
    gateway.config
    .override_parameter(key, value.clone()).await?;
    }

    // 5. 配置AB测试(如果有)
    if let Some(ab_config) = &strategy.ab_test_config {
    gateway.ab_test_manager
    .start_experiment(ab_config).await?;
    }

    tracing::info!(
    version = strategy.strategy_version,
    "Evolution strategy applied successfully"
    );

    Ok(())
    }
    }

    6.3.3 策略优化示例

    自进化引擎通过分析历史数据,持续优化 Gateway 的路由策略。以下是一个优化示例:

    {
    "strategy_version": 42,
    "routing_rules": [
    {
    "intent": "data_analysis",
    "optimized_model": "claude-3.5-sonnet",
    "reason": "在数据分析任务上,Claude 3.5 Sonnet 的准确率比 GPT-4o 高 8%,且成本更低",
    "confidence": 0.92,
    "sample_size": 1500
    },
    {
    "intent": "code_execution",
    "optimized_model": "gpt-4o",
    "reason": "代码执行任务的函数调用准确率 GPT-4o 领先 12%",
    "confidence": 0.95,
    "sample_size": 3200
    },
    {
    "intent": "casual_chat",
    "optimized_model": "gpt-4o-mini",
    "reason": "闲聊任务对模型能力要求低,使用 mini 模型可降低 80% 成本,质量差异 <3%",
    "confidence": 0.88,
    "sample_size": 5800
    }
    ],
    "tool_priorities": {
    "web_search": 0.95,
    "code_execution": 0.90,
    "knowledge_query": 0.85
    },
    "parameter_overrides": {
    "session.context_window.max_tokens": 40960,
    "routing.parallel_task_limit": 6,
    "tools.sandbox.timeout": "45s"
    },
    "ab_test_config": {
    "experiment_name": "intent_detection_v3",
    "control_group": "rule_based_v2",
    "treatment_group": "ml_model_v3",
    "traffic_split": 0.2,
    "metrics": ["accuracy", "latency", "user_satisfaction"],
    "duration": "7d"
    }
    }


    第七章 Gateway 性能与扩展性

    7.1 并发处理能力

    7.1.1 并发架构

    Gateway 基于 Rust + Tokio 异步运行时构建,采用事件驱动架构实现高并发处理。以下是 Gateway 在不同负载级别下的并发处理能力指标:

    ┌──────────────────────────────────────────────────────────────────────┐
    │ Gateway 并发处理能力 │
    │ │
    │ 负载级别 QPS 平均延迟 P99延迟 CPU 内存 │
    │ ───────── ─────── ───────── ──────── ───── ────── │
    │ 轻负载 100 45ms 120ms 15% 512MB │
    │ 中负载 500 65ms 200ms 45% 1.2GB │
    │ 高负载 1000 95ms 350ms 72% 2.1GB │
    │ 峰值负载 2000 180ms 650ms 91% 3.5GB │
    │ 极限负载 3500 420ms 1200ms 98% 4.8GB │
    │ │
    │ 测试环境: │
    │ · 16核 CPU (AMD EPYC 7763) │
    │ · 64GB 内存 │
    │ · 10Gbps 网络 │
    │ · Redis Cluster (6节点) │
    │ · Kafka (3节点) │
    │ · 模型推理延迟不计入(使用模拟响应) │
    └──────────────────────────────────────────────────────────────────────┘

    7.1.2 并发优化策略

    /// Gateway 并发优化配置
    pub struct ConcurrencyConfig {
    /// Tokio worker 线程数
    pub worker_threads: usize,
    /// 阻塞线程池大小
    pub blocking_threads: usize,
    /// 每连接并发请求数
    pub max_concurrent_requests_per_connection: usize,
    /// 全局并发请求上限
    pub max_concurrent_requests: usize,
    /// 请求队列容量
    pub request_queue_capacity: usize,
    /// 连接池配置
    pub connection_pools: ConnectionPoolConfig,
    }

    /// 连接池配置
    pub struct ConnectionPoolConfig {
    /// Redis 连接池
    pub redis: PoolConfig {
    min_connections: 10,
    max_connections: 100,
    connection_timeout: 3s,
    idle_timeout: 300s,
    },
    /// gRPC 连接池
    pub grpc: PoolConfig {
    min_connections: 5,
    max_connections: 50,
    connection_timeout: 5s,
    idle_timeout: 600s,
    },
    /// HTTP 连接池
    pub http: PoolConfig {
    min_connections: 10,
    max_connections: 200,
    connection_timeout: 3s,
    idle_timeout: 90s,
    },
    }

    Gateway 的并发优化策略包括以下几个层面:

    1. 异步IO多路复用:所有IO操作(网络、磁盘、数据库)均使用 async/await 异步模型,单线程可处理数千个并发连接,线程切换开销趋近于零。

    2. 零拷贝消息传递:在消息归一化管线和任务编排器之间,使用 Bytes 类型传递消息体,避免不必要的数据拷贝。

    3. 分片锁设计:对于高频访问的共享数据结构(如会话注册表、工具注册表),采用分片锁(Sharded Lock)替代全局锁,将锁竞争降低到 1/N。

    /// 分片锁实现
    pub struct ShardedMap<K, V, const N: usize> {
    shards: [RwLock<HashMap<K, V>>; N],
    }

    impl<K: Hash + Eq + Clone, V: Clone, const N: usize> ShardedMap<K, V, N> {
    fn get_shard(&self, key: &K) -> &RwLock<HashMap<K, V>> {
    let mut hasher = DefaultHasher::new();
    key.hash(&mut hasher);
    let hash = hasher.finish() as usize;
    &self.shards[hash % N]
    }

    pub async fn get(&self, key: &K) -> Option<V> {
    let shard = self.get_shard(key);
    shard.read().await.get(key).cloned()
    }

    pub async fn insert(&self, key: K, value: V) {
    let shard = self.get_shard(&key);
    shard.write().await.insert(key, value);
    }
    }

    // 使用示例:32片分片锁
    type SessionShardedMap = ShardedMap<SessionId, SessionContext, 32>;

    4. 背压控制:当下游服务(模型路由器、工具执行器)处理速度跟不上时,Gateway 通过背压机制减慢上游请求的接受速率,避免内存堆积:

    /// 背压控制器
    pub struct BackpressureController {
    /// 当前并发数
    current_concurrent: Arc<AtomicUsize>,
    /// 最大并发数
    max_concurrent: usize,
    /// 高水位线(开始限流)
    high_watermark: usize,
    /// 低水位线(停止限流)
    low_watermark: usize,
    /// 是否处于限流状态
    is_throttling: Arc<AtomicBool>,
    }

    impl BackpressureController {
    pub async fn acquire(&self) -> Result<Permit, BackpressureError> {
    let current = self.current_concurrent.load(Ordering::Relaxed);

    if current >= self.max_concurrent {
    return Err(BackpressureError::MaxConcurrencyReached);
    }

    if current >= self.high_watermark {
    self.is_throttling.store(true, Ordering::Relaxed);
    // 开始拒绝低优先级请求
    }

    self.current_concurrent.fetch_add(1, Ordering::Relaxed);

    Ok(Permit {
    controller: self,
    })
    }

    fn release(&self) {
    let current = self.current_concurrent.fetch_sub(1, Ordering::Relaxed) 1;

    if current <= self.low_watermark {
    self.is_throttling.store(false, Ordering::Relaxed);
    }
    }
    }

    5. 预热与缓存:沙箱实例预热、会话上下文预加载、工具描述符缓存等预热机制确保热路径上的零冷启动延迟。

    7.2 水平扩展方案

    7.2.1 集群架构

    ┌──────────────────────────────────────────────────────────────────────┐
    │ Gateway 集群架构 │
    │ │
    │ ┌──────────────┐ │
    │ │ 负载均衡器 │ │
    │ │ (LB / VIP) │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ┌───────────────┼───────────────┐ │
    │ │ │ │ │
    │ v v v │
    │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
    │ │ Gateway │ │ Gateway │ │ Gateway │ │
    │ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
    │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
    │ │ │ │ │
    │ └───────────────┼───────────────┘ │
    │ │ │
    │ ┌───────────────┼───────────────┐ │
    │ │ │ │ │
    │ v v v │
    │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
    │ │ Redis │ │ Kafka │ │ etcd │ │
    │ │ Cluster │ │ Cluster │ │ Cluster │ │
    │ │ (会话) │ │ (消息) │ │ (配置) │ │
    │ └──────────┘ └──────────┘ └──────────┘ │
    │ │
    │ 扩展方式: │
    │ · 无状态 Gateway 节点可随时增减 │
    │ · 会话数据存储在 Redis Cluster,不依赖单节点 │
    │ · 配置通过 etcd 实时同步到所有节点 │
    │ · 节点间通过 Kafka 传递异步消息 │
    └──────────────────────────────────────────────────────────────────────┘

    7.2.2 无状态设计

    Gateway 节点设计为无状态,所有状态存储在外部系统中:

    状态类型存储位置访问方式一致性要求
    会话上下文 Redis Cluster 读写 强一致性
    工具注册表 etcd + 本地缓存 读写(注册)/只读(查询) 最终一致性
    路由配置 etcd 只读 最终一致性
    审计日志 Kafka → ES 只写 至少一次
    指标数据 Prometheus 只写 尽力而为
    临时缓存 本地内存 读写 无(可丢失)

    7.2.3 自动伸缩

    # Kubernetes HPA 配置
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
    name: gatewayhpa
    namespace: hermes
    spec:
    scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gateway
    minReplicas: 3
    maxReplicas: 20
    metrics:
    type: Resource
    resource:
    name: cpu
    target:
    type: Utilization
    averageUtilization: 70
    type: Resource
    resource:
    name: memory
    target:
    type: Utilization
    averageUtilization: 75
    type: Pods
    pods:
    metric:
    name: gateway_active_sessions
    target:
    type: AverageValue
    averageValue: "500"
    type: Pods
    pods:
    metric:
    name: gateway_request_latency_p99
    target:
    type: AverageValue
    averageValue: "500"
    behavior:
    scaleUp:
    stabilizationWindowSeconds: 30
    policies:
    type: Percent
    value: 50
    periodSeconds: 60
    type: Pods
    value: 4
    periodSeconds: 60
    selectPolicy: Max
    scaleDown:
    stabilizationWindowSeconds: 300
    policies:
    type: Percent
    value: 25
    periodSeconds: 120
    selectPolicy: Min

    自动伸缩的触发条件:

    指标扩容阈值缩容阈值评估窗口
    CPU 使用率 >70% <40% 60秒
    内存使用率 >75% <45% 60秒
    活跃会话数/节点 >500 <200 30秒
    P99 请求延迟 >500ms <200ms 30秒
    请求队列深度 >50 <10 15秒

    7.3 故障恢复机制

    7.3.1 故障检测

    /// 故障检测器
    pub struct FailureDetector {
    /// 节点健康状态
    node_health: Arc<RwLock<HashMap<NodeId, NodeHealth>>>,
    /// 心跳超时
    heartbeat_timeout: Duration,
    /// 心跳间隔
    heartbeat_interval: Duration,
    /// Phi Accusal 故障检测器参数
    phi_threshold: f64,
    }

    #[derive(Clone, Debug)]
    pub struct NodeHealth {
    pub node_id: NodeId,
    pub status: NodeStatus,
    pub last_heartbeat: DateTime<Utc>,
    pub phi_value: f64,
    pub heartbeat_history: VecDeque<Duration>,
    }

    impl FailureDetector {
    /// 使用 Phi Accusal 算法检测节点故障
    pub async fn check_node(&self, node_id: &NodeId) -> NodeStatus {
    let health = self.node_health.read().await;

    if let Some(h) = health.get(node_id) {
    let time_since_last = Utc::now()
    .signed_duration_since(h.last_heartbeat)
    .to_std()
    .unwrap_or_default();

    // 计算 Phi 值
    let phi = self.calculate_phi(&h.heartbeat_history, time_since_last);

    if phi > self.phi_threshold {
    return NodeStatus::Suspected;
    }

    if time_since_last > self.heartbeat_timeout * 3 {
    return NodeStatus::Dead;
    }

    return NodeStatus::Alive;
    }

    NodeStatus::Unknown
    }

    /// 计算 Phi 值(基于心跳历史的统计检测)
    fn calculate_phi(
    &self,
    history: &VecDeque<Duration>,
    current_delay: Duration,
    ) -> f64 {
    if history.len() < 10 {
    return 0.0; // 历史数据不足
    }

    // 计算心跳间隔的均值和标准差
    let intervals: Vec<f64> = history.iter()
    .map(|d| d.as_millis() as f64)
    .collect();

    let mean = intervals.iter().sum::<f64>() / intervals.len() as f64;
    let variance = intervals.iter()
    .map(|x| (x mean).powi(2))
    .sum::<f64>() / intervals.len() as f64;

    let std_dev = variance.sqrt();

    if std_dev == 0.0 {
    return if current_delay.as_millis() as f64 > mean {
    f64::INFINITY
    } else {
    0.0
    };
    }

    // Phi = -log10(P(x > now – last_heartbeat))
    let delay = current_delay.as_millis() as f64;
    let phi = 1.0 *
    ((1.0 / (std_dev * (2.0 * std::f64::consts::PI).sqrt())) *
    (((delay mean).powi(2)) / (2.0 * variance))).ln();

    phi
    }
    }

    7.3.2 故障恢复流程

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 故障恢复流程 │
    │ │
    │ 1. 故障检测 │
    │ └─> Phi Accusal 检测器发现 Node 2 心跳超时 │
    │ Phi > 8.0 (阈值) → 标记为 Suspected │
    │ │
    │ 2. 验证与确认 │
    │ └─> 其他节点对 Node 2 执行 TCP 探测 │
    │ 多数节点确认 → 标记为 Dead │
    │ │
    │ 3. 影响评估 │
    │ └─> 查询 Node 2 上的活跃会话列表 │
    │ 评估影响范围:N 个活跃会话需要迁移 │
    │ │
    │ 4. 自动恢复 │
    │ ├─> 4a. 会话迁移:从 Redis 副本恢复到备用节点 │
    │ ├─> 4b. 请求重路由:一致性哈希环更新,新请求路由到其他节点 │
    │ ├─> 4c. 队列重分配:Node 2 的 Kafka 分区消费者重平衡 │
    │ └─> 4d. 告警通知:发送故障通知到运维通道 │
    │ │
    │ 5. 恢复验证 │
    │ └─> 验证所有迁移会话的数据完整性 │
    │ 验证新节点的请求处理正常 │
    │ │
    │ 6. 节点恢复(如果 Node 2 恢复) │
    │ └─> 重新加入集群 → 数据同步 → 逐步承接流量 │
    └──────────────────────────────────────────────────────────────────────┘

    7.3.3 关键指标的恢复时间目标

    故障类型检测时间恢复时间数据影响恢复策略
    Gateway 节点崩溃 <5s <10s 请求自动重路由
    Redis 主节点故障 <3s <30s 无(副本接管) Sentinel 自动故障转移
    Kafka Broker 故障 <10s <60s 无(多副本) 分区重平衡
    模型服务不可用 <1s <5s 降级服务 自动切换备用模型
    网络分区 <15s <120s 可能部分超时 拜占庭容错处理
    整机房断电 N/A <600s 无(跨可用区) 跨AZ自动切换

    第八章 与其他 Agent 框架的 Gateway 对比

    8.1 架构哲学对比

    Hermes Agent 的 Gateway 与业界主流 Agent 框架的控制层在设计哲学上有本质差异。以下从五个维度进行对比分析:

    8.1.1 LangChain Router

    LangChain 的 Router 模块采用链式组合的设计哲学,将路由逻辑表达为一系列可组合的 Chain。其核心思想是通过 LCEL(LangChain Expression Language)将意图识别、记忆检索、模型调用等环节串联为声明式的处理链。

    优势:组合灵活,开发者可以快速拼装出不同的处理流程;社区生态丰富,有大量预置的 Chain 和 Tool 可用。

    劣势:缺乏统一的会话管理,状态分散在各 Chain 中;渠道适配需要自行实现;缺乏生产级的高可用和水平扩展能力;性能受 Python GIL 限制。

    8.1.2 AutoGen Orchestrator

    AutoGen 的 Orchestrator 采用多 Agent 协作的设计哲学,核心是 GroupChat 机制——多个 Agent 在一个组内通过消息传递协作完成任务。Orchestrator 负责管理 Agent 间的消息路由和终止条件。

    优势:多 Agent 协作模型天然适合复杂任务分解;支持人类 Agent 参与(Human-in-the-loop);对话历史自动管理。

    劣势:面向研究而非生产,缺乏渠道适配、限流、审计等生产级能力;单进程执行,无水平扩展方案;会话状态管理简单,无压缩和隔离机制。

    8.1.3 CrewAI Manager

    CrewAI 的 Manager 采用角色驱动的设计哲学,将每个 Agent 定义为具有特定角色(Role)、目标(Goal)和背景故事(Backstory)的「船员」。Manager 负责任务分配和进度管理。

    优势:角色抽象直观,易于理解和使用;任务分配灵活,支持串行和并行;内置任务结果验证机制。

    劣势:缺乏独立的 Gateway 层,路由逻辑与业务逻辑耦合;不支持多渠道接入;无会话隔离和上下文压缩;工具管理简单,无 MCP 集成。

    8.1.4 Hermes Agent Gateway

    Hermes Agent 的 Gateway 采用统一控制面的设计哲学,将所有控制逻辑——会话管理、渠道适配、工具调度、意图路由——收敛到一个独立的中枢组件,通过标准接口与数据面交互。

    核心差异:Hermes Gateway 是唯一将控制面与数据面完全分离的架构,这使得控制逻辑可以独立演进和扩展;唯一内置自进化引擎集成的架构,支持策略的自动优化和热更新;唯一提供完整多渠道适配(12+平台)的生产级架构。

    8.2 核心能力对比矩阵

    能力维度Hermes GatewayLangChain RouterAutoGen OrchestratorCrewAI Manager
    架构定位 统一控制面 链式组合 多Agent协作 角色驱动
    开发语言 Rust + Python Python Python Python
    会话管理 三层架构(工作/短期/长期) 简单内存存储 GroupChat历史 任务级状态
    上下文压缩 三级压缩策略 无内置 无内置 无内置
    并行会话隔离 完整隔离方案
    渠道适配 12+平台内置 需自行实现
    消息归一化 6阶段管线
    工具注册 动态注册+版本管理 静态定义 静态定义 静态定义
    MCP集成 原生支持 通过插件
    工具沙箱 Firecracker microVM
    权限控制 RBAC + 条件控制
    调用审计 全量审计+脱敏
    意图识别 三级级联(规则+ML+LLM) LLM-based LLM-based LLM-based
    任务编排 串行/并行/条件/循环 链式串行 消息传递 串行/并行
    记忆调度 智能调度+注入 手动管理 自动但不智能
    自进化 内置集成
    水平扩展 无状态+一致性哈希
    故障恢复 自动故障转移
    可观测性 全链路追踪+指标+日志 基础日志 基础日志 基础日志
    配置管理 声明式+热更新 代码配置 代码配置 代码配置
    生产就绪 部分

    8.3 场景适用性分析

    8.3.1 适用场景对比

    ┌──────────────────────────────────────────────────────────────────────┐
    │ 场景适用性雷达图 │
    │ │
    │ 多渠道接入 │
    │ 5 │
    │ 4 │ 5 Hermes │
    │ 3 │ 4 LangChain │
    │ 2 │ 3 AutoGen │
    │ 1 │ 2 CrewAI │
    │ 生产级─────────────────┼───────────────── 高并发 │
    │ 1 │ 2 │
    │ 2 │ 3 │
    │ 3 │ 4 │
    │ 4 │ 5 │
    │ 5 │
    │ 多Agent协作 工具生态 │
    │ │
    │ 评分标准: 1=不支持 2=基础 3=可用 4=优秀 5=卓越 │
    │ │
    │ Hermes Agent: 生产5 | 多渠道5 | 高并发5 | 多Agent3 | 工具生态4 │
    │ LangChain: 生产3 | 多渠道2 | 高并发2 | 多Agent3 | 工具生态5 │
    │ AutoGen: 生产2 | 多渠道1 | 高并发1 | 多Agent5 | 工具生态3 │
    │ CrewAI: 生产2 | 多渠道1 | 高并发1 | 多Agent4 | 工具生态3 │
    └──────────────────────────────────────────────────────────────────────┘

    8.3.2 选型建议

    场景推荐框架推荐理由
    企业级多渠道 Agent 平台 Hermes Agent 完整的多渠道适配、会话管理、权限控制和水平扩展能力
    快速原型开发 LangChain 丰富的预置组件和社区生态,最快搭建可用的 Agent
    多 Agent 协作研究 AutoGen 原生的多 Agent 对话机制,适合研究 Agent 间协作模式
    角色扮演型 Agent CrewAI 角色抽象直观,适合需要多角色分工的场景
    高并发生产部署 Hermes Agent Rust 核心 + 无状态设计 + 自动伸缩,支撑高并发
    工具密集型任务 Hermes Agent 完整的工具注册表 + MCP 集成 + 沙箱隔离
    教育和演示 LangChain / CrewAI 上手门槛低,代码量少,易于理解

    8.3.3 演进趋势

    从架构演进的角度看,Agent 框架的 Gateway 层正在经历以下趋势:

  • 控制面/数据面分离:从 LangChain 的链式耦合,到 Hermes 的控制面独立,分离趋势明显。这使得控制逻辑可以独立扩展和演进。

  • 多渠道原生支持:随着 Agent 从开发工具走向生产系统,多渠道接入(IM、Web、API、语音)成为必备能力。Hermes 的 12+ 平台适配器是这一趋势的体现。

  • 记忆系统智能化:从简单的对话历史存储,到三层记忆架构 + 智能调度,记忆系统正在成为 Agent 的核心差异化能力。

  • 自进化能力:从静态配置到动态优化,自进化引擎使得 Agent 系统能够基于运行数据持续改进路由策略和参数配置。

  • 生产级特性:权限控制、调用审计、故障恢复、水平扩展等生产级特性正在从「加分项」变为「必选项」。

  • MCP 标准化:MCP 协议正在成为工具层的统一标准,未来 Agent 框架的工具生态将围绕 MCP 展开。


  • 附录 A:术语表

    术语全称说明
    Gateway 统一控制面,Hermes Agent 的中枢组件
    SPI Service Provider Interface 服务提供者接口,可插拔组件的标准接口
    WAL Write-Ahead Log 预写日志,保证数据更新原子性的机制
    MCP Model Context Protocol 模型上下文协议,Anthropic 提出的开放标准
    RBAC Role-Based Access Control 基于角色的访问控制
    HPA Horizontal Pod Autoscaler Kubernetes 水平 Pod 自动伸缩器
    microVM Micro Virtual Machine 微虚拟机,轻量级隔离技术
    Phi Accusal 基于 Phi 值的故障检测算法
    SSE Server-Sent Events 服务器发送事件,流式通信协议
    LCEL LangChain Expression Language LangChain 表达式语言

    附录 B:配置参数速查

    参数默认值说明调优建议
    worker_threads CPU核数 Tokio工作线程数 IO密集型保持默认,CPU密集型可增加
    max_connections 10000 最大连接数 根据内存调整,每个连接约4KB
    session.default_ttl 3600s 会话默认存活时间 根据业务场景调整
    session.max_concurrent_per_user 5 每用户最大并发会话 防止单用户资源占用
    context_window.max_tokens 32768 上下文窗口大小 根据使用的模型调整
    compression_threshold 0.75 压缩触发阈值 降低则更频繁压缩,增加Token利用率
    routing.max_orchestration_depth 8 最大编排深度 防止无限递归
    routing.parallel_task_limit 4 并行任务上限 根据下游服务承受能力调整
    tools.sandbox.memory_limit 512MB 沙箱内存限制 根据代码执行需求调整
    tools.sandbox.timeout 30s 沙箱执行超时 防止恶意长时间执行
    observability.tracing.sampling_rate 0.1 追踪采样率 生产环境建议0.01-0.1

    附录 C:Gateway 请求处理全链路追踪示例

    以下是一次完整请求的处理链路,展示了 Gateway 各组件的协作时序:

    时间线 (毫秒)

    0ms ──> [接入层] 收到 HTTP 请求 (POST /api/chat)
    0.5ms ──> [接入层] TLS 终止 + 请求解析
    1ms ──> [适配层] WebAdapter.receive_message()
    1.5ms ──> [归一化] 安全验证 → 频率限制检查通过
    2ms ──> [归一化] 格式解析 → JSON 解析成功
    2.5ms ──> [归一化] 内容归一化 → 文本提取完成
    3ms ──> [归一化] 内容增强 → 语言检测:zh, 意图预判:data_analysis
    3.5ms ──> [归一化] 会话解析 → session_id: 0x3F…A1
    4ms ──> [会话层] 加载会话上下文 (Redis GET)
    5ms ──> [会话层] 上下文加载完成 (12轮历史)

    5.5ms ──> [路由层] 意图识别 Level 1 (规则匹配) → 置信度 0.6
    6ms ──> [路由层] 意图识别 Level 2 (ML模型) → 置信度 0.82
    6.5ms ──> [路由层] 意图识别 Level 3 (LLM推理) → 置信度 0.95
    │ 意图: data_analysis, 工具: [http_request, code_execution, web_search]

    7ms ──> [路由层] 创建执行计划 (6步: 2并行+4串行)

    7.5ms ──> [记忆层] 记忆调度决策: 读工作+短期+长期
    8ms ──> [记忆层] 并行读取记忆
    12ms ──> [记忆层] 记忆读取完成 (工作:5ms, 短期:4ms, 长期:7ms)
    13ms ──> [记忆层] 记忆注入上下文 (450 tokens)

    14ms ──> [编排层] 并行启动 Step 1 (fetch_data) + Step 3 (search_benchmarks)

    14ms ──> [工具层] Step 1: HTTP请求工具调用
    145ms ──> [工具层] Step 1 完成 (延迟: 131ms)

    14ms ──> [工具层] Step 3: Web搜索工具调用
    89ms ──> [工具层] Step 3 完成 (延迟: 75ms)

    145ms ──> [编排层] Step 2: 代码执行 (依赖 Step 1)
    145ms ──> [工具层] 沙箱获取 (池中预热实例)
    152ms ──> [工具层] 代码执行开始
    185ms ──> [工具层] Step 2 完成 (延迟: 40ms)

    185ms ──> [编排层] Step 4: LLM分析 (依赖 Step 2 + Step 3)
    185ms ──> [模型层] 模型路由器选择: claude-3.5-sonnet
    190ms ──> [模型层] 推理请求发出

    190ms ──> [模型层] 流式响应开始
    190ms ──> [适配层] 流式输出开始 (SSE)
    … (逐 token 流式输出)
    2150ms ──> [模型层] 流式响应完成

    2150ms ──> [编排层] 并行启动 Step 5 (记忆写入) + Step 6 (报告生成)
    2155ms ──> [记忆层] Step 5: 记忆写入完成
    2180ms ──> [模型层] Step 6: 报告生成完成

    2180ms ──> [会话层] 更新会话上下文
    2185ms ──> [会话层] 上下文更新完成 (13轮)

    2185ms ──> [审计层] 记录工具调用审计日志 (3个工具调用)
    2187ms ──> [审计层] 审计日志写入完成

    2187ms ──> [可观测] 上报指标 (延迟、Token、工具调用)
    2190ms ──> [接入层] 响应发送完成

    总延迟: 2190ms
    · Gateway 内部处理: 305ms (14%)
    · 工具调用: 246ms (11%)
    · 模型推理: 1990ms (91%)
    · I/O 等待: 并行化后已最小化


    编写时间 2026-07-07 | 编写者:风云再起

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Hermes Agent Gateway统一控制面架构深度解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!