文心 5.0 Preview 智能体「任务托管」落地自研编排层 vs 厂商托管 vs 开源框架我们如何选型上周接手一个 B2B「AI 数字员工」 SaaS 项目核心诉求是把文心 5.0 Preview 的「任务托管」能力——长跑任务、跨设备状态同步、多模态中间产物落盘——包装成标准化 API 供前端调用。技术栈定在 Spring Boot 3.3.2、JDK 21.0.4、Redis 7.4.1、Netty 4.1.115 Final网关层用 Spring Cloud Gateway 4.1.5。百度千帆 SDK 版本锁在 0.3.82026-04-30 发布适配 5.0 Preview。背景不是简单的 API 网关转发文心 5.0 Preview 发布后智能体不再是单轮问答而是变成了「可托管的异步进程」用户在 PC 网页端下发「生成季度财报分析视频」智能体会拆解为爬取数据、生成脚本、调用视频生成模型、合成字幕等十几个子任务跑个 20 分钟。用户中途关掉网页换手机打开 App进度条、中间生成的图表、甚至视频预览帧必须无感衔接。这带来三个硬约束状态机持久化任务上下文含多模态中间产物 URL、Token 用量、子任务 DAG必须落地且支持断点续跑。跨端会话粘性WebSocket 长连接在 PC、App、小程序间无感迁移心跳、重连、消息幂等全得自己兜底。审计与计费隔离每个租户的任务执行轨迹要留存半年计费按「子任务类型 × 耗时 × 显存占用」分级结算厂商侧只返回总 Token颗粒度不够。厂商原生托管平台百度智能体开发平台提供了开箱即用的任务队列和推送但数据不出 VPC、计费颗粒度粗、自定义插件只能用沙箱 JS不符合合规和扩展性要求。开源方向调研了 Dify 0.11.0、Coze 插件化改造版、LangGraph 0.2.0要么多租户隔离弱要么状态后端耦合 PostgreSQL 难以接入我们现有的 Redis Cluster ClickHouse 架构。过程三条路径的正面硬刚把三个方案拉到同一个压测环境跑了两周场景模拟 500 并发长任务平均 15 分钟混合 20% 短任务30 秒记录 P99 延迟、状态同步失败率、运维介入次数。| 维度 | 百度原生托管平台 | Dify 0.11.0 二次开发 |自研编排层 千帆 SDK||------|------------------|----------------------|---------------------------|| 状态持久化位置 | 厂商侧不可见 | PostgreSQL (JSONB) |Redis Cluster (Hash Stream) ClickHouse|| 跨设备会话迁移 | 依赖厂商账号体系 | 需改造 WebSocket 管理器 |自研 Session Registry 基于 Redis Lua|| 计费颗粒度 | 总 Token / 次 | 仅支持 Token 计费 |子任务级类型 × 耗时 × 资源规格|| 自定义插件运行时 | 沙箱 JS (ES2022) | Python Sandbox / WASM |Java 进程内加载 / K8s Job 灵活|| 合规数据不出 VPC | ❌ 托管在百度云 | ✅ 私有化部署 |✅ 完全自控|| 开发改造周期 | 1 周对接文档 | 6 周重构多租户/状态机 |3 周复用现有基建|| P99 状态同步延迟 | ~1.2 s (依赖厂商推送) | ~800 ms (PG 写入瓶颈) |~120 ms (Redis Stream 本地消费)|| 运维介入次数/周 | 0 (黑盒) | 3 (PG 锁死、迁移脚本报错) |0 (全链路可观测)|约束条件倒逼选型数据不出 VPC → 直接淘汰原生托管。计费模型已在合同写死改不了 → Dify 计费插件改造成本高于重写。团队只有 Java 技栈不想引入 Python/Go 运维负担 → LangGraph 放弃。现有 Redis Cluster 7.4.1 已跑熟ClickHouse 24.3 做日志分析成熟 → 复用边际成本最低。最终决定自研轻量编排层只调用千帆 SDK 的ChatCompletion与AgentRun接口状态机、会话粘性、计费埋点全在自己进程里闭环。核心代码状态机与跨端会话注册表1. 基于 Redis Stream 的任务状态机支持断点续跑、幂等重试java// TaskStateMachine.java// 依赖spring-data-redis 3.3.2, lettuce-core 6.3.1.RELEASEComponentRequiredArgsConstructorpublic class TaskStateMachine {private final ReactiveRedisTemplate redis;private final ObjectMapper mapper new ObjectMapper();private final MeterRegistry meterRegistry;// 状态流Keytask:state:{taskId}, ValueHash{currentNode, payload, retryCount, updatedAt}// 事件流Keytask:events:{taskId}, Stream// 分布式锁Keytask:lock:{taskId}, TTL30s, 看门狗自动续期public Mono transit(String taskId, String targetNode, Object payload) {String stateKey task:state: taskId;String lockKey task:lock: taskId;return redis.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(30)) // 简化版生产用 Redisson.flatMap(acquired - {if (!acquired) {return Mono.error(new IllegalStateException(任务正在执行中: taskId));}return redis.opsForHash().entries(stateKey).collectMap(Map.Entry::getKey, Map.Entry::getValue).flatMap(state - {// 幂等校验同一节点、同一 payloadHash 不重复执行String payloadHash DigestUtils.md5DigestAsHex(mapper.writeValueAsBytes(payload));if (targetNode.equals(state.get(currentNode)) payloadHash.equals(state.get(payloadHash))) {log.info(幂等拦截: taskId{}, node{}, taskId, targetNode);return Mono.empty();}// 持久化新状态Map newState Map.of(currentNode, targetNode,payload, mapper.writeValueAsString(payload),payloadHash, payloadHash,retryCount, 0,updatedAt, String.valueOf(Instant.now().toEpochMilli()));return redis.opsForHash().putAll(stateKey, newState).then(publishEvent(taskId, targetNode, payload, STARTED)).doOnSuccess(v - meterRegistry.counter(task.transit.success, node, targetNode).increment());}).doFinally(sig - redis.delete(lockKey).subscribe());});}private Mono publishEvent(String taskId, String node, Object payload, String phase) {TaskEvent event new TaskEvent(taskId, node, payload, phase, Instant.now());return redis.opsForStream().add(task:events: taskId, mapper.writeValueAsString(event)).then();}Data AllArgsConstructor NoArgsConstructorstatic class TaskEvent {String taskId; String node; Object payload; String phase; Instant timestamp;}}关键点用 Redis Hash 存「当前快照」Stream 存「全量事件流」ClickHouse 通过 Kafka Connect 同步 Stream 做审计。幂等键设计为currentNode payloadHash解决网关重试、客户端重连导致的重复触发。分布式锁这里用setIfAbsent演示生产环境必须上 Redisson 3.2.6 的看门狗机制防止长任务锁过期。2. 跨端会话注册表WebSocket 会话与任务绑定支持踢旧端、无感迁移java// SessionRegistry.java// 依赖spring-webflux 6.1.10, netty-handler 4.1.115.FinalComponentRequiredArgsConstructorpublic class SessionRegistry {private final ReactiveRedisTemplate redis;private final ObjectMapper mapper;// 用户在线端集合: Keyuser:sessions:{userId}, ValueHash{deviceId - SessionInfo(JSON)}// 任务订阅关系: Keytask:subscribers:{taskId}, ValueSet// 设备心跳: Keydevice:heartbeat:{deviceId}, TTL45spublic Mono bindTask(String taskId, String userId, String deviceId) {String subKey task:subscribers: taskId;String sessKey user:sessions: userId;return redis.opsForSet().add(subKey, deviceId).then().then(redis.expire(subKey, Duration.ofHours(24))) // 任务最长 24h.then(redis.opsForHash().put(sessKey, deviceId,mapper.writeValueAsString(new SessionInfo(deviceId, taskId, Instant.now()))));}public Mono getActiveDevices(String taskId) {return redis.opsForSet().members(task:subscribers: taskId).collectList();}// 进度推送由 Netty 事件循环调用非阻塞public Mono pushProgress(String taskId, Object progressPayload) {return getActiveDevices(taskId).flatMap(devices - Flux.fromIterable(devices).flatMap(deviceId - sendToDevice(deviceId, progressPayload)).onErrorResume(e - {log.warn(推送失败 deviceId{}, err{}, deviceId, e.getMessage());return Mono.empty(); // 单设备失败不影响其他})).then();}private Mono sendToDevice(String deviceId, Object payload) {// 实际项目中通过 ChannelId 映射找到 Netty Channel 写入// 这里简化为发 Redis Stream 由独立 Push Worker 消费String pushKey push:outbound: deviceId;return redis.opsForStream().add(pushKey, mapper.writeValueAsString(payload)).then();}Data AllArgsConstructor NoArgsConstructorstatic class SessionInfo {String deviceId; String currentTaskId; Instant lastActive;}}跨端迁移逻辑用户在手机端打开 App建立新 WebSocket 连接后前端上报deviceId后端调用bindTask。旧端PC收到SESSION_MIGRATED推送后主动关闭连接或由服端在pushProgress发现旧deviceId无响应时自动清理。心跳 Keydevice:heartbeat:{deviceId}TTL 45s配合 NettyIdleStateHandler双向保活。效果上线三周的真实数据| 指标 | 上线前 (对接原生托管 Demo) | 上线后 (自研编排层) | 备注 ||------|---------------------------|---------------------|------|| 任务状态同步 P99 延迟 | 1.8 s (厂商推送抖动) |112 ms| Redis Stream 本地消费去掉公网跳转 || 跨设备迁移成功率 | 82% (依赖厂商账号同步) |99.6%| 自研注册表 幂等设计 || 计费对账差异率 | 15% (仅总 Token) |0.03%| 子任务级埋点入 ClickHouse日对账自动化 || 运维手工干预/周 | 4 次 (黑盒排查) |0 次| 全链路 TraceID 打通Grafana 看板覆盖编排层全节点 || 新增插件接入耗时 | 不支持 (沙箱受限) |2 天| Java 进程内加载共享连接池/配置中心 |有个细节挺扎心压测时发现 Redis Stream 消费组如果不显式XACK重启后会重复消费导致计费重复。加了XACK后单任务平均多 0.3 ms RTT可接受。另外千帆 SDK 0.3.8 的AgentRun接口偶发返回429但Retry-After头为空代码里加了指数退避 抖动配合 Resilience4j 2.2.0 的RetryConfig.custom().maxAttempts(3).waitDuration(Duration.ofSeconds(2)).exponentialBackoffMultiplier(2).build()才稳住。总结文心 5.0 Preview 的「任务托管」把智能体变成了有状态的长跑进程状态归属权是架构选型的核心杠杆。厂商托管省开发但丧失控制权开源框架通用但改造成本高、技术栈发散。我们场景下——数据不出 VPC、计费模型非标、团队纯 Java、现有 Redis/ClickHouse 基建成熟——自研轻量编排层是边际收益最高的路径。核心经验三条状态机落 Redis Hash Stream快照查询快、事件流审计全、重放方便别再用关系型表存 JSONB 当状态机。跨端会话绑定任务而非用户task:subscribers设计天然支持多设备并行订阅、单设备平滑迁移踢旧端只需删 Set 成员。计费埋点下沉到编排层每个节点别信厂商总 Token子任务级类型 × 耗时 × 规格才是对账硬通货。下一步打算把编排层抽象成内部 Starter把TaskStateMachine、SessionRegistry、MeterBinder封装好新接入智能体只需写 YAML 定义 DAG不写 Java 代码。这才是后端该有的复用姿势。#后端 #Java #SpringBoot #Redis #架构设计你在实际项目中有遇到类似问题吗欢迎在评论区分享你的经验和解决方案。