1. 从单体架构到解耦设计:托管代理的演进之路
在构建AI代理系统的过程中,我们常常面临一个根本性矛盾:系统设计时所做的假设,往往随着模型能力的提升而迅速过时。这让我想起2019年Rich Sutton提出的"苦涩教训"——那些试图将人类知识固化到系统中的尝试,最终都会被更强大的计算能力所超越。Anthropic团队在开发Claude系列模型时,深刻体会到了这一教训。
最初的设计采用单体架构,将会话管理、模型推理和工具执行全部塞进同一个容器。这种设计在早期确实简单直接,但随着系统规模扩大,问题逐渐暴露:容器崩溃导致会话丢失、调试困难、安全边界模糊。更糟糕的是,我们为特定模型版本(如Claude Sonnet 4.5)所做的优化,在更强大的Claude Opus 4.5上反而成了负担。
关键教训:任何基于"当前模型不能做什么"的假设进行的优化,都可能在未来成为技术债务。
2. 托管代理的三层核心架构
2.1 会话层:不可变的事件日志
会话层作为系统的"记忆",采用仅追加(append-only)的事件日志设计。这种不可变的数据结构带来了几个关键优势:
- 可恢复性:即使框架崩溃,新实例只需读取日志即可恢复状态
- 审计追踪:所有操作都有完整记录,便于问题排查
- 时间旅行:可以查询历史任意时间点的系统状态
实际实现中,我们使用分片存储策略,将会话日志按时间窗口分割存储。例如:
python复制class SessionLog:
def __init__(self, session_id):
self.shards = {} # {timestamp: log_shard}
def append(self, event):
current_shard = self._get_current_shard()
current_shard.write(json.dumps(event) + "\n")
def query(self, start_time=None, end_time=None):
# 返回指定时间范围内的事件
2.2 框架层:无状态的控制循环
框架层是系统的"大脑",负责调用模型并路由工具请求。解耦后的框架具有以下特点:
- 无状态设计:所有状态都存储在会话层
- 标准工具接口:通过统一的
execute(name, input)方法与执行环境交互 - 错误恢复:框架崩溃后可以无损重启
我们采用指数退避重试机制处理工具调用失败:
python复制def execute_with_retry(tool_name, input, max_retries=3):
base_delay = 0.1
for attempt in range(max_retries):
try:
return execute(tool_name, input)
except ToolError as e:
delay = base_delay * (2 ** attempt)
time.sleep(delay)
raise MaxRetriesExceeded()
2.3 沙箱层:隔离的执行环境
沙箱层作为系统的"双手",负责安全地执行代码和文件操作。解耦后我们实现了:
- 按需配置:只有在需要时才启动沙箱容器
- 安全隔离:凭证与执行环境物理分离
- 多实例支持:可以同时连接多个不同类型的沙箱
安全方面,我们采用最小权限原则配置沙箱:
bash复制# Docker运行沙箱的示例配置
docker run --rm \
--cpu-shares 512 \
--memory 1g \
--read-only \
--tmpfs /tmp:rw,size=100m \
sandbox-image
3. 解耦架构的实现细节
3.1 网络通信设计
组件间通信采用gRPC协议,主要考虑以下因素:
- 性能:二进制协议比HTTP/JSON更高效
- 流式支持:适合长时间运行的任务
- 多语言支持:方便不同组件用不同语言实现
服务定义示例:
protobuf复制service AgentService {
rpc Execute (ToolRequest) returns (ToolResponse);
rpc GetEvents (EventQuery) returns (stream Event);
rpc EmitEvent (Event) returns (Ack);
}
message ToolRequest {
string name = 1;
bytes input = 2;
}
3.2 会话存储策略
针对不同规模的会话,我们采用分层存储策略:
| 会话大小 | 存储介质 | 访问延迟 | 成本 |
|---|---|---|---|
| <1MB | 内存缓存 | <1ms | 高 |
| 1MB-10MB | SSD | ~100μs | 中 |
| >10MB | 对象存储 | ~10ms | 低 |
迁移策略基于最近访问时间和修改频率自动执行。
3.3 工具调用协议
工具调用采用标准的请求-响应模式,但增加了以下扩展:
- 流式输出:支持长时间运行任务的渐进式输出
- 元数据传递:可以在不影响接口稳定性的情况下扩展功能
- 内容协商:支持不同编码格式的输入输出
请求示例:
json复制{
"tool": "git.clone",
"input": {
"repo": "https://github.com/example/repo.git",
"branch": "main"
},
"metadata": {
"timeout": "30s",
"priority": "high"
}
}
4. 安全设计与凭证管理
4.1 凭证隔离模式
我们实现了两种凭证管理模式:
仓库绑定模式
- 沙箱初始化时克隆仓库
- 配置本地git remote使用短期令牌
- 所有git操作在沙箱内完成
保险库代理模式
- 凭证存储在独立的保险库服务
- 专用代理处理敏感操作
- 框架只看到操作结果,不接触凭证
4.2 安全边界实施
我们在多个层面建立安全防护:
| 层级 | 防护措施 | 示例 |
|---|---|---|
| 网络 | 微隔离 | 沙箱只能与框架通信 |
| 系统 | 只读根文件系统 | 除/tmp外不可写 |
| 运行时 | seccomp BPF | 限制系统调用 |
| 应用 | 输入消毒 | 检查代码注入尝试 |
5. 性能优化实践
5.1 TTFT优化策略
降低首token响应时间的措施:
- 延迟加载沙箱:只有工具调用时才启动
- 并行初始化:模型加载与会话恢复同时进行
- 上下文预取:预测可能需要的会话片段
优化前后对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| p50 TTFT | 1200ms | 450ms | 62.5% |
| p95 TTFT | 3500ms | 250ms | 92.8% |
5.2 资源利用率提升
解耦架构带来的资源效益:
- 容器利用率:从30%提升至75%
- 冷启动频率:减少80%
- 故障恢复时间:从分钟级降至秒级
6. 错误处理与恢复机制
6.1 框架级错误处理
我们定义了分级错误处理策略:
| 错误类型 | 处理方式 | 重试策略 |
|---|---|---|
| 瞬时错误 | 自动重试 | 指数退避 |
| 逻辑错误 | 报告模型 | 由模型决定 |
| 系统错误 | 重启框架 | 最多3次 |
6.2 会话一致性保证
为确保崩溃后状态一致,我们采用:
- 写前日志:操作前先记录意图
- 校验和:定期验证日志完整性
- 快照:定时保存检查点
恢复流程:
- 加载最近的快照
- 重放快照后的日志
- 验证最终状态
7. 部署架构与扩展性
7.1 多区域部署
我们采用"框架近用户,沙箱近数据"的原则:
code复制用户 → 边缘框架 → 区域会话存储
↓
中心沙箱池
7.2 自动扩展策略
基于负载的自动扩展配置:
yaml复制autoscaling:
framework:
min: 3
max: 100
metrics:
- type: CPU
target: 60%
- type: RPS
target: 100
sandbox:
prewarm: 5
max: 50
idle_timeout: 5m
8. 监控与可观测性
8.1 关键指标监控
我们跟踪的核心指标:
- 工具调用延迟:p50/p95/p99
- 会话加载时间:按大小分段统计
- 错误率:按类型和组件分类
- 资源利用率:CPU/内存/网络
8.2 分布式追踪实现
使用OpenTelemetry实现端到端追踪:
code复制框架Span → 模型Span → 工具Span
↓
会话存储Span
9. 开发者体验优化
9.1 本地测试工具
提供本地模拟环境:
- 模拟沙箱:不需要真实容器
- 录制/回放:保存测试会话
- 流量镜像:复制生产流量测试
9.2 调试支持
增强的调试功能:
- 时间旅行调试:检查历史状态
- 假设分析:"如果当时..."模拟
- 交互式修复:修改后继续执行
10. 架构演进的经验总结
经过这次架构改造,我们获得了几个关键认知:
- 接口比实现长寿:稳定的接口允许底层不断进化
- 无状态设计解放扩展性:状态外置使组件可以随意替换
- 安全需要结构保证:不能依赖模型的"自觉"
- 性能源于解耦:独立扩展各组件才能优化资源利用
这套架构已经支撑Claude处理日均数十亿次工具调用,同时保持了秒级的故障恢复能力。最重要的是,它为未来模型能力的提升预留了充足空间——无论Claude变得多强大,这套基础设计都能适应。
