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:
– redis–0.internal:6379
– redis–1.internal:6379
– redis–2.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: hermes–intent–v2
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: jaeger–collector.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>;
}
上下文版本控制的核心应用场景包括:
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 压缩效果评估
以下是不同压缩策略在实测中的效果对比:
| 轻度压缩 | 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 |
| MIME | 无限制 | 否 | HTML | 否 | 全类型 | IMAP/SMTP | SMTP Auth | |
| 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) = °raded.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/knowledge–base/sse
headers:
Authorization: "Bearer ${MCP_KB_TOKEN}"
auto_sync: true
sync_interval: 30s
# 自定义 MCP 服务器(gRPC 传输)
– id: data_pipeline
transport: grpc
endpoint: data–pipeline.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"
}
]
}
这个编排计划的执行流程是:
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) = <.user_profile {
let section = format!(
"[用户画像]\\n{}\\n",
profile.summary
);
let tokens = self.token_estimator.estimate_text(§ion);
if tokens_injected + tokens <= memory_budget {
injected_sections.push(section);
tokens_injected += tokens;
}
}
// 注入相关长期记忆
if let Some(memories) = <.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(§ion);
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(§ion);
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: gateway–hpa
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 核心能力对比矩阵
| 架构定位 | 统一控制面 | 链式组合 | 多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 | 编写者:风云再起
网硕互联帮助中心



评论前必须登录!
注册