当前为游客模式。公开章节: 00, 01 。关注「公众号」后可解锁: 03, 04 。其余章节仍需登录。
目录导航

01 先跑一个最小 Agent

这篇只做一件事:
搭一个不依赖业务知识的 A2A 最小服务,让前端能收流式事件。

本文内容对照提交:

  • data-agent-backend/src/main/kotlin/io/github/qifan777/server/a2a/A2AConfiguration.kt
  • data-agent-backend/src/main/kotlin/io/github/qifan777/server/a2a/A2AController.kt
  • data-agent-backend/src/main/kotlin/io/github/qifan777/server/a2a/ToyAgentExecutor.kt

1. 本文目标

完成后你应该具备这 3 个能力:

  1. 知道 A2A 服务最小需要哪些端点与配置
  2. 能发起一次流式请求并收到 artifact-update
  3. 能把这套骨架迁移到任意业务场景

2. 最小链路

Frontend -> /api/.well-known/agent-card.json -> /api/a2a/jsonrpc -> 后端 A2AController -> JSONRPCHandler -> ToyAgentExecutor -> SSE

只保留协议,不引入 SQL、RAG、知识库。

3. 后端最小必备

3.1 依赖

implementation("io.github.a2asdk:a2a-java-sdk-transport-jsonrpc:0.3.2.Final")

这个依赖会把 A2A Java SDK 的 JSON-RPC 传输层一起带进来,后面 JSONRPCHandler 就来自这里。

3.2 AgentCard + JSONRPCHandler

提交里的 A2AConfiguration 做了 4 件关键事情:

  1. capabilities.streaming(true)
  2. url(...) 指向前端可访问的 http://localhost:3500/api/a2a/jsonrpc
  3. additionalInterfaces(...) 补充 JSON-RPC 接口信息
  4. DefaultRequestHandler.create(...) 创建 JSONRPCHandler

对应代码可以直接理解成:

@Bean
open fun agentCard(): AgentCard {
    return AgentCard.Builder()
        .name("sqlAgent")
        .capabilities(
            AgentCapabilities.Builder()
                .streaming(true)
                .pushNotifications(true)
                .stateTransitionHistory(true)
                .build()
        )
        .url("http://localhost:3500/api/a2a/jsonrpc")
        .additionalInterfaces(
            listOf(AgentInterface("JSONRPC", "http://localhost:3500/api/a2a/jsonrpc"))
        )
        .build()
}

这里最容易忽略的一点是:
AgentCard.url 不是随便填的,它必须是前端真的能访问到的地址。所以教程里写成 3500/api/...,而不是后端自己的 9933/a2a/...

3.3 两个 HTTP 入口

提交里不是 GraphController,而是 A2AController 提供:

  1. GET /.well-known/agent-card.json
  2. POST /a2a/jsonrpc

其中:

  • GET 直接返回 jsonRpcHandler.agentCard
  • POST 同时声明 application/jsontext/event-stream
  • 控制器会先解析 JSON-RPC 请求体,再判断这是不是流式方法

关键代码如下:

@GetMapping(value = ["/.well-known/agent-card.json"], produces = [MediaType.APPLICATION_JSON_VALUE])
fun agentJson(): AgentCard {
    return jsonRpcHandler.agentCard
}

@PostMapping(
    value = ["/a2a/jsonrpc"],
    produces = [MediaType.TEXT_EVENT_STREAM_VALUE, MediaType.APPLICATION_JSON_VALUE]
)
fun handleRequest(@RequestBody body: String): Any {
    ...
}

这个 handleRequest 的核心判断是:

streaming = method != null && (
    SendStreamingMessageRequest.METHOD == method.asText() ||
        TaskResubscriptionRequest.METHOD == method.asText()
)

也就是说,只有流式消息发送和任务重订阅两种方法会进入 SSE 分支。

4. 一个最小 Executor 思路

先做“玩具执行器”:收到文本就回一段固定结果,不做业务处理。

提交里的 ToyAgentExecutor 就是这个最小版本:

override fun execute(context: RequestContext, eventQueue: EventQueue) {
    val taskUpdater = TaskUpdater(context, eventQueue)
    val text = ((context.message?.parts?.first { it is TextPart }) as TextPart).text

    taskUpdater.addArtifact(
        listOf(TextPart("hello from a2a: $text")),
        "1",
        "TOY_HELLO_NODE",
        mapOf("outputType" to "GRAPH_NODE_STREAMING")
    )
    taskUpdater.complete()
}

这段代码有 3 个关键信号:

  1. context.message.parts 里读用户输入
  2. taskUpdater.addArtifact(...) 发出一个名字为 TOY_HELLO_NODE 的 artifact
  3. taskUpdater.complete() 结束任务

前端真正依赖的是下面这组事件顺序:

  1. task
  2. artifact-update
  3. status-update(completed)

task 事件由 A2A SDK 侧的任务生命周期管理产生;我们在业务代码里最直接控制的是 artifact-update 和最终完成态。

这里还有一个很实用的细节:

mapOf("outputType" to "GRAPH_NODE_STREAMING")

虽然这是 toy demo,但提前把 outputType 放进 metadata,后面接 Graph 节点渲染时可以直接复用这套约定。

5. 本地启动

# backend
./gradlew bootRun

# frontend
pnpm install
pnpm dev

默认:

  • 前端:http://localhost:3500
  • 后端:http://localhost:9933

这里有一个隐含前提:
前端需要把 /api 代理到后端,所以浏览器访问的是 3500/api/...,真正处理请求的是 9933 上的 Spring Boot。

6. 第一轮验证

先验证服务发现:

curl http://localhost:3500/api/.well-known/agent-card.json

你应该能看到这些关键字段:

  • name
  • capabilities.streaming = true
  • url = http://localhost:3500/api/a2a/jsonrpc

再验证 JSON-RPC 入口。当前提交里前端没有额外封装调试页,最简单的方式就是跑起前端页面后发送一句话:

  1. 页面发消息
  2. 网络出现 /api/a2a/jsonrpc
  3. 响应类型是 text/event-stream
  4. 后端进入 ToyAgentExecutor.execute(...)

7. 常见问题

1) agent card 404

  • 后端没启动
  • 前端代理 /api -> 9933 配置错误

2) 只有一次性返回,没有流式

  • capabilities.streaming 不是 true
  • 控制器没有命中 SendStreamingMessageRequest
  • controller 没按 SSE 返回 SseEmitter

3) 前端收不到 artifact

  • executor 没有 addArtifact(...)
  • taskUpdater.complete() 未调用
  • artifact.name 和前端约定的节点名对不上

8. 本章完成标准

满足以下两条即可进入下一篇:

  1. 前端能稳定收到 artifact-update
  2. 你可以解释最小 A2A 链路中的每一跳

如果你要把这篇记成一句话,可以记成:

AgentCard 负责发现,A2AController 负责接协议,JSONRPCHandler 负责分发,ToyAgentExecutor 负责产出 artifact。