夜雨聆风学习资料网

ARTICLE · 1313606

AgentScope Java 源码解析系列第 4 篇 Service 层|把整个框架部署成平台

AgentScope Java 源码解析系列第 4 篇 Service 层|把整个框架部署成平台:四个平面、三种执行器、一张不变的契约

AgentScope Java 源码解析 · 第 4 篇 / 共 4 篇(完结篇)· Agent as a Service

系列前三篇读的都是「一个 Agent 的内部」——这篇把它部署成五个容器,读最外层:怎么变成一个交给业务方调用的平台。

agentscope-service 五模块 · v2.1.0-BETA1 · 基于 2026-11-04 本机全栈实测

📖 系列导航(完结篇)

第 1 篇 HarnessAgent:「全家桶」不是新引擎,是一摞中间件加一棵会进化的工作区——本篇数据面跑的就是它

第 2 篇 Workspace:Agent 的长期状态不进数据库,进一棵会自己整理自己的文件树——本篇的工作区卷就是给它住的

第 3 篇 Skill:Agent 写的技能要先蹲草稿区、过两道安检、再等一本账发落——本篇的会话里它们照常上岗

第 4 篇 Service 层(本文):把整个框架部署成平台——四个平面、三种执行器、一张不变的契约

系列四篇到此完结。

📌 TL;DR
  • Service 层不是「把 Agent 包一层 HTTP 接口」。compose 起来是 5 个容器:一个 PostgreSQL(cp/rt/dp 三个 schema)+ 四个平面:gateway、control、data、scheduler——而且控制面是 Go 写的(551 个 Go 文件),Java 只负责「跑 Agent」的那两层。官方定位词是 Agent as a Service
  • 对外只认一张契约:Endpoint(输入/输出 JSON Schema、超时、限流、鉴权策略都在字段里)。发布和回滚都是追加一条不可变的 EndpointRelease,公开 slug 永远稳定
  • 一次调用 = EndpointInvocation 状态机:accepted → dispatching → running → waiting(停下来等人审批)→ 五个终态。waiting 就是第 1 篇的权限确认,在平台层有了名字
  • 数据面每跑一轮先拿带围栏令牌的 TurnLease(fenceToken 形如 run:{runId}),开 10 秒心跳汇报存活;事件走「事务内入箱 + at-least-once 出箱」(至少送达一次,丢了重投)的 outbox——进程崩了账不丢,老 attempt 的事件被围栏封死不串台
  • 三种执行器并存:托管 HarnessAgent、外部应用(LangChain、Claude Agent SDK 等经 ASDP 接入)、Runtime Host 纳管 Codex / Claude Code / Qoder / QwenPaw / OpenClaw 五种现成 CLI(2 秒领任务、15 秒心跳、60 秒租约)
  • BETA 说清楚了自己是 BETA:README 自述「项目尚未发布,v4 故意替换了旧执行 schema」;Helm Chart 明说「不承诺零停机数据库升级和 HA」。截至分析日(2026-11-04)v2.1.0 stable 未发布
🎯 如果只记一件事

把 Agent 变成服务,难点不在「跑起来」,在给一次执行建账——谁发布的(不可变 Release)、跑到哪一步了(Invocation 状态机)、过程留了什么痕(事件 outbox)、能不能随时接管(waiting 加围栏中断)。这套「平台化 = 给执行记账」的思路,跟 Agent 没有绑定关系,任何想把内部能力产品化成对外服务的团队都可以照抄。

适合谁读:想把 Agent 交给业务系统去调、正在纠结「包个接口还是上个平台」的后端同学;系列前三篇读过更好,没读也能跟上。预计阅读:主线 14 分钟(3 张按需查阅的表不算在内)。


从五个容器和一个小意外说起

写这篇的实测从拉镜像开始。按官方文档,init-env.sh 生成密钥、docker compose pull、up -d --wait,三步。镜像放在阿里云杭州的 registry,匿名就能拉——但本机 Docker 配了 daocloud 加速器,其中一个底层 blob 连续三次 EOF,第四轮重试才续传成功。这种毛边先记下,正文实测段还有它。

栈起来之后是这样的:postgres、control、data、scheduler、gateway,五个容器。我照着 compose 文件核对各容器健康检查时注意到一处不对劲:其他四个平面查的都是 curl /actuator/health(Spring Boot 的标准姿势),唯独 control 容器是一句 /service-controlplane --healthcheck——一个二进制在自检。再翻 service-controlplane/ 目录:一个 go.mod、551 个 Go 文件、Java 文件为零。一个叫 AgentScope Java 的项目,平台的账房中枢是 Go 写的——这是本篇第一个、也是我卡得最久的一个认知更新。

系列前三篇读的都是「一个 Agent 的内部」:中间件怎么摞(第 1 篇)、记忆怎么存(第 2 篇)、技能怎么转正(第 3 篇)。这篇读最外层:这些内部件怎么被组装成一个给别人调用的服务。也是完结篇,最后一节会把四篇串回一张图。

这层在解决什么问题

选型-11(《AgentScope Java vs Spring AI:塞进 Spring 应用,还是独立成平台》)留了一个尾巴:AgentScope 选了「独立成平台」,那平台长什么样?agentscope-service 就是答案的实体。

没有这一层时,你把一个 HarnessAgent 交给业务方,至少要自己补四件事:谁来鉴权(业务系统的凭证怎么发、怎么轮换);谁来做审计(哪次调用、花了多少 token、产出了什么文件);人怎么介入(Agent 要删库时谁按暂停);结果怎么交付(跑完的产物怎么变成业务系统里可验收的东西)。README 第一句——An Agent as a Service platform——就把定位说完了:submit work through APIs, stay involved, and retrieve reviewable results——提交、盯着、拿回可审查的结果。

所以这个模块真正交付的不是「Agent 的 HTTP 化」,是一套执行账本:发布物(Endpoint)、执行实例(Invocation)、事件流、审批位、产物库。下面按这个顺序拆。

四个平面怎么分工

生产部署形态就是 compose 里那五个容器的关系(下图)。每个平面都有明确的两张清单——管什么、不管什么。README 里那张「owns / does not own」表是我见过最省事的架构文档写法,直接抄意:

图 1 · 四个平面 + 三种执行器 + Invocation 状态机(v2.1.0-BETA1)
平面
语言/规模
管
不管
gateway
Java,209 行
公网入口、鉴权透传、按路径路由
业务状态、Agent 执行
control(控制面)
Go
,551 文件
产品资源、发布、Invocation、公共事件、任务协调、运行时命令
Harness 推理、原生会话事件生成
data(数据面)
Java,23,234 行
托管 Harness 运行时、事件日志、SSE(把事件持续推给调用方的长连接)、Turn 租约、HITL(人审批后放行)、工作队列
直接读产品目录表
scheduler
Java,3,321 行
渠道(IM 出站)、Cron、外发任务、自托管 Worker
推理循环

「不管」那一列比「管」更有信息量:控制面不做推理、数据面不读目录、调度器不碰循环——每条都是一次边界决策。

网关薄到什么程度?GatewayApp.java 连注释带 license 一共 44 行,就是一个 SpringApplication.run。全部个性在 application.yml 的路由表里,有三处我标了记号:

  1. 内部接口在门口就掐死:/api/internal/** 匹配即 SetStatus=404(order -1,优先于一切)。平面之间的内部调用根本不走公网网关,万一有人从外面探测,得到的不是 403 而是 404——不承认它的存在。
  2. 客户端伪造的内部头一律剥掉:default-filters 里两条 RemoveRequestHeader=X-Builder-Internal-Token / X-Builder-Internal-User。注释原话:绝不把客户端自带的内部鉴权头转发进去。
  3. SSE 长连接超时放到 1 小时(response-timeout: 1h),注释写明是为了事件流不被默认超时掐断——事件流是这个平台的「返回值」,掐断等于把函数调用的 return 丢了。

路由的优先级也讲了故事:会话事件(/api/sessions/*/events/**)和数据面工作队列 order 0 先判,因为它们和控制面的 /api/sessions/** 命名空间重叠;/api/outbound/** order 5 必须压过控制面的渠道 CRUD,否则 IM 回调会被路由到错误的家。

为什么这样设计

控制面用 Go、数据面用 Java,是「账本」和「引擎」的分工。 控制面管的是产品资源、发布、调用记录——无状态偏多的 CRUD 和协调逻辑,Go 单二进制部署、镜像小、启动快,还能顺手复用 Kubernetes 的 controller-runtime(CRD 调谐器,本地开发用 CONTROL_PLANE_ENABLE_KUBERNETES=false 关掉)。数据面则必须长在 HarnessAgent 身上——SessionTurnRunner 的 import 列表就是证据:io.agentscope.core.session.*、io.agentscope.harness.agent.HarnessAgent,全是系列第 1 篇读过的那些类。让 Java 写业务、Go 管账,比「全栈一门语言」的教条诚实。

数据面不读目录表,执行材料一律由控制面 resolve 结果带过来。HarnessAgentBuildService 的 javadoc 写得很硬:托管会话的 Agent 实例只从控制面 resolveSession 返回的快照构建(agentSnapshot、workspacePath、definitionFiles),「没有 JPA 目录回退,resolve 必须成功且快照非空」。也就是说数据面没有自己的「事实来源」,只是执行者。好处是多副本数据面天然无目录竞争,坏处是控制面成了单点依赖——它用 DataPlaneSelfRegistration(默认关,builder.dataplane.register-enabled=true 才开)加 15 秒心跳的自愿注册来补发现问题。

发布即追加,而不是原地改。Endpoint 是对外的稳定承诺(slug、输入输出 Schema、超时、限流),EndpointRelease 是每次部署/回滚追加的一条不可变记录(Number 递增、带 Reason 和 ActivatedAt)。要灰度要回滚,都是激活另一条 Release,公开 slug 和契约不动。调用方集成的稳定性不是靠「大家小心别改接口」,是靠数据结构不许改。

一轮对话一把租约,事件入箱出箱。SessionTurnRunner.runTurnAsync(L272 起)的开场是一把 TurnLeaseService.acquireOrConflictFenced——带围栏令牌的排他租约,fenceToken 形如 run:{runId},中断请求必须带上匹配的令牌才生效,「陈旧的中断信号永远不能打断新一轮」。拿到租约后先 beginManagedExecution 向控制面立账,再起一个 10 秒一次的心跳(L397-407),跑完 endManagedExecution 销账。事件侧配 ManagedEventOutbox:事件必须在源事务内入箱(不在事务里直接抛 IllegalStateException),由后台任务 at-least-once 地投递到控制面;每个 attempt 有围栏,sealed 之后「永久封条」,晚到的旧事件想改账会被拒。

跟我读源码:四个入口和三张表

必读文件按顺序(Java 侧在 agentscope-service/ 下,Go 侧在 service-controlplane/ 下)。第一次读只精读下面第一张表,其余两张按需查阅:

文件
规模
为什么必读
service-gateway/.../application.yml
~100 行
整个系统的流量地图,路由表就是架构图
service-dataplane/.../managed/SessionTurnRunner.java
1,088 行
一轮对话的全部生命周期:租约、立账、心跳、中断、收尾
service-dataplane/.../catalog/HarnessAgentBuildService.java
~700 行
控制面快照怎么变成一个活的 HarnessAgent(承接第 1 篇)
service-controlplane/.../model/agent_endpoint.go
模型文件
对外契约三件套:Endpoint / EndpointRelease / EndpointCredential

表 1(核心,必读):Endpoint——对外服务的全部承诺(agent_endpoint.go L54-79)

字段
含义
Slug
公开稳定标识,调用方 URL 里看到的就是它;发布/回滚都不变
TargetType
 / TargetRef
指向谁执行:托管 Agent、外部应用或 hosted runtime
InvocationMode
调用模式(Job 后台任务 / Conversation 会话式)
InputSchema
 / OutputSchema
输入输出的 JSON Schema,输出也校验(ValidateResult 拿 OutputSchema 验交付物)
TimeoutSeconds
 / MaxPayloadBytes
超时与负载上限,超时终态是 timed_out
AuthPolicy
 / RateLimit
鉴权与限流策略,策略也是契约的一部分
ActiveReleaseID
 / ActiveRelease
当前激活的发布版本(追加式,见下)
Version
 / ArchivedAt
乐观锁版本号 / 归档时间(下线不删除)

表 2(按需查阅):EndpointInvocation 状态机(L128-137)

accepted → dispatching → running → waiting / cancel_requested,终态五个:completed / partial_succeeded / failed / cancelled / timed_out(contract.go 的 Terminal() L103-105 圈定)。waiting 就是等人审批的暂停态;partial_succeeded 这个终态值得注意——多 Agent 协作里「跑了一半、部分成功」被当成一等公民,而不是硬凑成成功或失败。

表 3(按需查阅):EndpointCredential(同文件)

应用方凭证:KeyPrefix(可展示的前缀)+ SecretHash(只存哈希)+ SecretCiphertext(AES-GCM 落库加密,仅供授权 reveal,正常响应里不序列化)+ RotatedFrom(轮换链)+ 状态三值 active/revoked/expired。凭证不亮明文、可轮换可吊销,这是给业务系统发钥匙的标准做法。

另有三处小而硬的细节:invocation/contract.go 编译 JSON Schema 时把外部 $ref 直接禁掉(LoadURL 返回错误:"Service contracts cannot cause server-side network requests through $ref"——契约文件不能驱动服务端发网络请求,防 SSRF);EndpointRelease.Contract 字段标了 json:"-",契约内容不随响应外泄;helm values 里 workspaces 卷必须 ReadWriteMany(控制、数据、调度三个 pod 共享同一工作区),这是四平面共享文件的前提。

一次完整调用走一遍(带实测输出)

实测环境:Windows 11 + Docker 28.3.0,官方镜像 sca-registry.cn-hangzhou.cr.aliyuncs.com/agentscope/as-{controlplane,dataplane,scheduler,gateway}:2.1.0-BETA1,init-env.sh 生成的随机密钥,docker compose up -d --wait。

[1] docker compose ps(--wait 全绿后)    agentscope-service-db-1         Up (healthy)    agentscope-service-control-1    Up (healthy)  # Go 自检    agentscope-service-data-1       Up (healthy)   # curl /actuator/health    agentscope-service-scheduler-1  Up (healthy)    agentscope-service-gateway-1    Up (healthy)   127.0.0.1:18080->8080

然后走一遍最小链路:登录拿 token → 建托管 Agent → 建会话 → 发一条消息 → 看结果。

[2] curl -X POST :18080/api/auth/login {admin / init-env 生成的随机密码}    -> {"token":"eyJhbGciOi..."}   # 281 字符 JWT[3] POST /api/v1/agents {"agentKey":"hands-on","binding":{"kind":"managed"},"definition":{"model":"qwen-plus",...}}   -> 201[4] POST /api/v1/agent-sessions {"target":{"type":"agent","id":"..."}} -> 201[5] POST /api/v1/agent-sessions/{sid}/turns  (Idempotency-Key: handson-002)    {"message":"hello, introduce yourself in one sentence"}    -> 202 {"id":"7b6531d7-...","eventsUrl":".../events/stream",             "snapshotUrl":"..."}

turn 被受理后返回一张回执:turn id、事件流地址(SSE)、快照地址。之后一路代码是这样走的:网关把 /api/v1/agent-sessions 转给控制面(Go)记账、建会话;数据面 SessionTurnRunner.runTurnAsync 先做已删会话护栏检查,拿 TurnLease(围栏令牌),beginManagedExecution 立账,状态置 RUNNING;HarnessAgentBuildService 用控制面 resolve 来的快照现场拼一个 HarnessAgent(实例 ID 前缀 dpa-,缓存在 ConcurrentHashMap);然后才是第 1 篇读过的那摞中间件跑推理循环,每 10 秒一次心跳上报,事件在源事务里进 ManagedEventOutbox 再异步镜像给控制面。

我这条消息故意没配模型 key,想看平台在「跑不起来」时怎么表现。8 秒后查快照:

[6] GET /api/v1/agent-sessions/{sid}/snapshot    "runtimeStatus": "terminated",    "status": "active",    "stopReason": {"error": {        "code": "turn_failed",        "message": "Failed to create model for id: qwen-plus: Environment                    variable DASHSCOPE_API_KEY is required to auto-create                    model: qwen-plus",        "retry_status": "not_retrying", "type": "api_error"}}[L] 数据面容器日志(docker compose logs data)    2026-11-04T12:37:19.619Z WARN [boundedElastic-1]    i.a.b.web.managed.SessionTurnRunner:    Managed session turn failed: sessionId=sess_d2f5331e22e1,    error=Failed to create model for id: qwen-plus: ...

两处细节值得停一下。stopReason.code 是 turn_failed——正是 SessionTurnRunner L439 failTurn(session, ex, "turn_failed", ...) 里那个字符串,日志的类名也对得上,一条调用链从源码读到了实物。更值得注意的是 "status": "active":会话没废,失败被记在账上,配上 key 发下一条消息即可复用同一个会话——执行失败被当成账目而不是灾难,这是「账本」设计的直接好处。

🧾 毛边一则:第一次用 Git Bash 直接在命令行里发中文消息,被 Postgres 拒收 invalid byte sequence for encoding "UTF8": 0xc4 0xe3——0xc4 0xe3 是「你」的 GBK 编码,是我这台 Windows 壳子的编码问题,不是平台的;改用 UTF-8 文件体重发即受理。

最后,因为没配模型、也没接沙箱,HITL 分支这次实测没触发到——waiting 状态和审批回调链路(resumeDurableTurnAsync 处理 kind:"confirmation" 的回答,L221-266)我只读了代码没跑通,这条标记为待核实,等补了模型 key 再验。另一个没验到的:SSE 事件流(/events/stream)在这次失败场景下我没消费到条目(快照里账是齐的,事件镜像为什么没到列表没查清),同样标待核实。

调试断点放哪

断点 1gateway 的 application.yml:先看流量去哪了。路由 order(-1 / 0 / 5)决定同名前缀归谁,调试「为什么这个请求 404/去了错误的平面」从这里开始。

断点 2SessionTurnRunner.runTurnAsync L272-472:租约获取 → beginManagedExecution → 心跳调度 → runTurn → finally 销账。观察 activeTurnLeases 和 fenceToken 的匹配,中断类 bug 都在这条线上。

断点 3HarnessAgentBuildService 的 build 路径:盯 agentCache 的 key 构成和 resolve payload 的三个字段(agentSnapshot / workspacePath / definitionFiles),「Agent 配置改了不生效」多半是快照缓存问题。

断点 4ManagedEventOutbox.enqueue:确认事件在事务内入箱、围栏未 sealed;「事件丢了/串了」先查这里和 SessionEventFenceRepository。

断点 5Go 侧 internal/invocation/:contract.go(契约与终态判定)、journal.go(调用日志)——对外 API 行为对不上预期时看这里。

怎么扩展

✅ 应该改(推荐路径)

给 Runtime Host 接一个新的 CLI provider。internal/runtimehost/provider/ 下 codex、anthropiccli、qoder、qwenpaw、openclaw 五个目录就是五个范本,照一个抄一个即可把你们的内部 CLI 纳管进平台。

✅ 应该改(推荐路径)

接自家 IM 出站渠道。scheduler 的 ChannelManager + ChannelRuntimeCatalog 是留好的插口,OutboundTool 还让 Agent 自己把消息发到渠道里(OutboundService.send 同时服务 HTTP 控制器和 Agent 工具两个入口)。

⚠️ 不应该改(有更优替代)

别往 gateway 里加业务逻辑。它 209 行是刻意的设计,鉴权在各平面自己 enforce;要加统一逻辑,加在目标平面上。

⚠️ 不应该改(有更优替代)

别给数据面开目录表的直读捷径。所有执行材料走控制面 resolve,「数据面没有事实来源」是它多副本水平扩展的前提。

🚫 千万不要改(动了会破坏不变量)

EndpointRelease 的不可变性。发布/回滚只能是追加,绝不能原地改 Schema——公开 slug 的稳定是调用方信任的地基,改一次,所有接入方的「契约校验」都变成摆设。

🚫 千万不要改(动了会破坏不变量)

别把 /api/internal/** 从网关的 404 名单里放出来,也别转发 X-Builder-Internal-Token 头。这两道是平面间信任边界,放开等于把内部 API 暴露成公网 API。

最值得抄走的设计

「不管什么」清单和代码一样重要。 这个仓库每个平面的职责都配了明确的 does-not-own 列表。多数架构文档只写「负责什么」,不写「明确不负责什么」——后者才是防止边界腐烂的机制。抄走:给你的每个服务写两张清单。

发布即追加(append-only release)。 对外契约的每次变更都是一条不可变记录,回滚是「激活旧记录」而不是「改回去」。这招不新鲜(数据库 migration、基础设施即代码都这么干),但用在「Agent 服务发布」上刚刚好,因为 Agent 的行为定义是软的、易变的,更需要一个硬的发布史来兜底。

围栏令牌(fence token)。 TurnLease 不只是「我有锁」,而是「我的锁带编号,中断信号必须报出编号才生效」。分布式系统里最难的一类 bug 是「旧信号打断新任务」,fence 是教科书解法,这里是我在 Java Agent 框架里见到的最完整的落地(租约 + 心跳 + 围栏 + 事件封条四件套)。

BETA 期的诚实标注。 README 写明「项目尚未发布、v4 故意替换旧执行 schema」;helm Chart 明说单副本、Recreate 更新、「不承诺零停机升级和 HA」;本地开发栈的演示账号标了「仅限本地」。把边界写在明处,和第 3 篇扫描器 javadoc 自认「不是安全边界」一脉相承——这套仓库的文档纪律值得单独学。

没覆盖的部分:Console 前端(frontend/)、Kubernetes CRD 调谐器(internal/controller,本地栈关掉了)、ASDP gRPC 接入协议、Team/Workflow 编排——前两个没有 K8s 环境,后两个没跑通真实场景,我不想凭静态阅读硬写;想看的留言,v2.1.0 stable 出来后我补一篇升级实测。

系列四篇,串成一句话

💡 完结回顾

HarnessAgent 是一摞可以拆装中间件包着的推理循环(第 1 篇);它的长期状态不住数据库,住一棵流水账加 LLM 定期精编的文件树(第 2 篇);它自己学的技能先蹲草稿区、过两道安检、被一本使用账本决定去留(第 3 篇);而这一切最终被打包成四个平面加一张不可变契约,作为一个平台交给业务方(本篇)。从里往外读是这四篇的顺序,从外往里读就是一次调用的真实路径。

建议的阅读顺序

先 gateway 的 application.yml(十分钟建立全局地图),再 agent_endpoint.go 的三个模型(对外契约),然后精读 SessionTurnRunner.runTurnAsync(执行生命周期的全部难点),最后 HarnessAgentBuildService(快照到实例)。Go 侧 internal/invocation/ 三个文件放第二遍;scheduler 除非你要接 IM 渠道,可以最后看。

读完后你该带走什么

为什么需要它(把执行变成可交付、可审计、可接管的账本)、它怎么跑(四平面分工 + 一轮一租约一心跳 + 事件出箱)、你该改哪(provider 和渠道是留好的口子,Release 不可变和内部 API 边界是命门)。以及一个可以带走的判断:评估任何「Agent 平台」类产品,先看它有没有这五样——发布契约、执行状态机、事件流、审批位、凭证管理。缺哪样,哪样的成本最终会转嫁到接入方头上。

#agentscopejava #agent


聊聊:你们的 Agent 现在是怎么交给业务方的——直接包个 HTTP 接口、塞进现有系统、还是也上了类似平台?踩过「演示能跑、一上线就废」的坑吗?想深聊的同学:三种执行器(托管 Harness / 接外部应用 / 纳管 Claude Code 这类 CLI),你会先上哪种、为什么?

追更:Java 源码系列四篇完结,总目录会整理成一篇索引;v2.1.0 stable 发布后计划补一篇升级实测,关注不迷路。

裂变:转发到 Java 技术群或朋友圈,帮同样在搞 Agent 平台化的朋友省一次源码考古。

     AgentScope Java 源码解析系列 · 第 4 篇 Service 层 · 完结   

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料