google-adk-spring-ai 1.2.0 映射机制与 ADK + Spring AI 对接分析
背景
起因是PPT生成Agent需要结构化输出来渲染PPT,开发时让AI分析需求,发现目前adk+springAI又又不支持。故借机学习一下google-adk-spring-ai 1.2.0 的映射机制。
Google ADK Java 版本身有一套模型抽象:
text
LlmAgent
-> BaseLlm
-> LlmRequest
-> LlmResponse
-> Event
-> Session State
Spring AI 也有一套模型抽象:
text
ChatModel
-> Prompt
-> ChatOptions
-> ChatResponse
google-adk-spring-ai:1.2.0 的作用,就是把 ADK 的 BaseLlm 接口适配到 Spring AI 的 ChatModel / StreamingChatModel。
也就是说,它不是一个新的模型实现,而是一层适配器:
text
ADK LlmRequest
-> Spring AI Prompt
-> Spring AI ChatModel.call(...)
-> Spring AI ChatResponse
-> ADK LlmResponse
这篇文章基于本地依赖:
text
com.google.adk:google-adk:1.2.0
com.google.adk:google-adk-spring-ai:1.2.0
结合反编译结果和项目实践,分析官方适配器的映射机制、能力边界,以及在 ADK + Spring AI 下如何补齐结构化输出。
一句话结论
google-adk-spring-ai:1.2.0 官方适配器主要支持:
text
ADK Content -> Spring AI Message
ADK Part -> Spring AI text/media/tool call
ADK tools -> Spring AI ToolCallback
ADK generation config 部分字段 -> Spring AI ChatOptions
Spring AI ChatResponse -> ADK LlmResponse
但它不支持把:
text
GenerateContentConfig.responseSchema
GenerateContentConfig.responseMimeType
自动映射成 Spring AI OpenAI 的:
text
OpenAiChatOptions.responseFormat
因此,如果你在 ADK agent 里配置了 outputSchema,使用官方 new SpringAI(chatModel) 时,schema 不会自动落到 OpenAI-compatible HTTP 请求体的 response_format。
Maven 依赖与包结构
依赖:
xml
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-spring-ai</artifactId>
<version>1.2.0</version>
</dependency>
jar 中核心类:
text
com.google.adk.models.springai.SpringAI
com.google.adk.models.springai.MessageConverter
com.google.adk.models.springai.ConfigMapper
com.google.adk.models.springai.ToolConverter
com.google.adk.models.springai.StreamingResponseAggregator
com.google.adk.models.springai.SpringAIEmbedding
com.google.adk.models.springai.observability.SpringAIObservabilityHandler
com.google.adk.models.springai.error.SpringAIErrorMapper
几个类的职责可以概括为:
| 类 | 职责 |
|---|---|
SpringAI |
ADK BaseLlm 实现,持有 Spring AI ChatModel / StreamingChatModel |
MessageConverter |
ADK LlmRequest 和 Spring AI Prompt 互转;Spring AI ChatResponse 转 ADK LlmResponse |
ConfigMapper |
ADK GenerateContentConfig 部分字段转 Spring AI ChatOptions |
ToolConverter |
ADK BaseTool / FunctionDeclaration 转 Spring AI ToolCallback |
StreamingResponseAggregator |
聚合 Spring AI 流式响应为 ADK 最终响应 |
SpringAIObservabilityHandler |
请求日志、指标、观测能力 |
SpringAIErrorMapper |
Spring AI 异常归一化 |
SpringAI 适配器的核心机制
构造方式
官方适配器提供多种构造方式:
java
new SpringAI(ChatModel chatModel);
new SpringAI(ChatModel chatModel, String modelName);
new SpringAI(StreamingChatModel streamingChatModel);
new SpringAI(StreamingChatModel streamingChatModel, String modelName);
new SpringAI(ChatModel chatModel, StreamingChatModel streamingChatModel, String modelName);
构造时会做几件事:
text
保存 ChatModel
如果 chatModel 同时实现 StreamingChatModel,则保存 streamingChatModel
创建 ObjectMapper
创建 MessageConverter
创建 SpringAIObservabilityHandler
反编译后能看到类似逻辑:
java
this.chatModel = Objects.requireNonNull(chatModel, "chatModel cannot be null");
this.streamingChatModel =
chatModel instanceof StreamingChatModel ? chatModel : null;
this.objectMapper = new ObjectMapper();
this.messageConverter = new MessageConverter(this.objectMapper);
this.observabilityHandler =
new SpringAIObservabilityHandler(createDefaultObservabilityConfig());
因此在 ADK agent 中可以这样使用:
java
LlmAgent agent = LlmAgent.builder()
.name("assistant")
.model(new SpringAI(chatModel))
.instruction("你是一个助手")
.build();
这会让 ADK 把一次 agent 调用交给 Spring AI 的 ChatModel 执行。
非流式调用链路
SpringAI.generateContent(LlmRequest request, boolean stream) 根据 stream 参数选择调用路径。
非流式时,调用链路是:
text
SpringAI.generateContent(llmRequest, false)
-> generateContent(llmRequest)
-> MessageConverter.toLlmPrompt(llmRequest)
-> chatModel.call(prompt)
-> MessageConverter.toLlmResponse(chatResponse)
-> Flowable.just(llmResponse)
反编译后关键逻辑可以概括为:
java
Prompt prompt = messageConverter.toLlmPrompt(llmRequest);
observabilityHandler.logRequest(prompt.toString(), model());
ChatResponse chatResponse = chatModel.call(prompt);
LlmResponse llmResponse = messageConverter.toLlmResponse(chatResponse);
observabilityHandler.logResponse(extractTextFromResponse(llmResponse), model());
return Flowable.just(llmResponse);
这里的关键点是:
text
ADK 的 LlmRequest 并不会直接交给 Spring AI。
必须先由 MessageConverter 转成 Spring AI Prompt。
所以所有能力是否生效,取决于 MessageConverter / ConfigMapper 是否把对应字段映射过去。
流式调用链路
流式时会要求存在 StreamingChatModel:
java
if (stream && streamingChatModel == null) {
return Flowable.error(new IllegalStateException("StreamingChatModel is not configured"));
}
流式链路可以概括为:
text
SpringAI.generateContent(llmRequest, true)
-> generateStreamingContent(llmRequest)
-> MessageConverter.toLlmPrompt(llmRequest)
-> streamingChatModel.stream(prompt)
-> MessageConverter.toLlmResponse(chatResponse, true)
-> StreamingResponseAggregator
-> ADK streaming LlmResponse
流式集成要特别关注 ADK 的 partial / turnComplete 语义。中间 token 应该是:
text
partial = true
turnComplete = false
最终完整响应应该是:
text
partial = false
turnComplete = true
否则 ADK 的 outputKey 可能会过早把残缺 token 写入 session state。
MessageConverter:消息映射机制
MessageConverter 是整个适配层最关键的类。
它负责:
text
ADK LlmRequest -> Spring AI Prompt
Spring AI ChatResponse -> ADK LlmResponse
ADK tools -> Spring AI tool callbacks
LlmRequest 转 Prompt
核心方法:
java
public Prompt toLlmPrompt(LlmRequest request)
反编译后的流程可以整理为:
text
1. 收集 request.getSystemInstructions()
2. 遍历 request.contents()
3. role=system 的内容拼接进 system instructions
4. role=user/model/assistant 的内容转换为 Spring AI Message
5. 用 ConfigMapper 把 request.config 转成 ChatOptions
6. 如果 request.tools 不为空,用 ToolConverter 转成 Spring AI ToolCallback
7. 返回 new Prompt(messages, options)
伪代码:
java
List<Message> messages = new ArrayList<>();
List<String> systemInstructions = new ArrayList<>();
systemInstructions.addAll(request.getSystemInstructions());
for (Content content : request.contents()) {
String role = content.role().orElse("user").toLowerCase();
if ("system".equals(role)) {
systemInstructions.add(extractText(content));
} else {
messages.addAll(toSpringAiMessages(content));
}
}
if (!systemInstructions.isEmpty()) {
messages.add(new SystemMessage(String.join("\n\n", systemInstructions)));
}
ChatOptions options =
configMapper.toSpringAiChatOptions(request.config());
if (!request.tools().isEmpty()) {
List<ToolCallback> tools =
toolConverter.convertToSpringAiTools(request.tools());
options = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.copySomeOptionsFrom(options)
.build();
}
return new Prompt(messages, options);
ADK role 到 Spring AI Message
MessageConverter.toSpringAiMessages(...) 根据 ADK Content.role 分流:
| ADK role | Spring AI Message |
|---|---|
user |
UserMessage |
model |
AssistantMessage |
assistant |
AssistantMessage |
system |
SystemMessage |
未知 role 会抛出异常。
User content 映射
ADK Content.parts 中的不同 part 会被映射为:
| ADK Part | Spring AI 表达 |
|---|---|
text |
UserMessage.text |
inlineData |
Media,使用 ByteArrayResource |
fileData |
Media,使用 URI |
functionResponse |
当前 user content 处理里基本跳过 |
简化逻辑:
java
for (Part part : content.parts()) {
if (part.text().isPresent()) {
text.append(part.text().get());
} else if (part.inlineData().isPresent()) {
media.add(new Media(mimeType, byteArrayResource));
} else if (part.fileData().isPresent()) {
media.add(new Media(mimeType, uri));
}
}
UserMessage.builder()
.text(text.toString())
.media(media)
.build();
这意味着官方适配器可以承载文本和多模态媒体输入,但具体模型是否支持媒体,取决于底层 Spring AI ChatModel 和模型供应商。
Assistant content 映射
ADK model / assistant 内容会转成 Spring AI AssistantMessage。
其中:
| ADK Part | Spring AI 表达 |
|---|---|
text |
AssistantMessage.content |
functionCall |
AssistantMessage.ToolCall |
简化逻辑:
java
if (part.text().isPresent()) {
text.append(part.text().get());
}
if (part.functionCall().isPresent()) {
FunctionCall call = part.functionCall().get();
toolCalls.add(new AssistantMessage.ToolCall(
call.id().orElseThrow(),
"function",
call.name().orElseThrow(),
toJson(call.args().orElse(Map.of()))
));
}
这让 ADK 的函数调用历史可以被转换成 Spring AI 可理解的 assistant tool call 历史。
ChatResponse 转 LlmResponse
MessageConverter.toLlmResponse(...) 会把 Spring AI 的 ChatResponse 转回 ADK 的 LlmResponse。
关键流程:
text
1. 取 ChatResponse.getResult()
2. 取 Generation.getOutput(),即 AssistantMessage
3. AssistantMessage 转 ADK Content
4. 如果是流式,计算 partial / turnComplete
5. 构造 LlmResponse
伪代码:
java
Generation generation = chatResponse.getResult();
AssistantMessage output = generation.getOutput();
Content content = convertAssistantMessageToContent(output);
boolean partial =
stream && isPartialResponse(output);
boolean turnComplete =
!stream || isTurnCompleteResponse(chatResponse);
return LlmResponse.builder()
.content(content)
.partial(partial)
.turnComplete(turnComplete)
.build();
这也是为什么 Spring AI 流式输出适配时,partial 语义非常重要。
ConfigMapper:生成参数映射机制
ConfigMapper 负责把 ADK 的:
java
Optional<GenerateContentConfig>
映射成 Spring AI 的:
java
ChatOptions
支持映射的字段
toSpringAiChatOptions(...) 支持的字段包括:
ADK GenerateContentConfig |
Spring AI ChatOptions |
|---|---|
temperature |
temperature |
maxOutputTokens |
maxTokens |
topP |
topP |
topK |
topK |
stopSequences |
stopSequences |
presencePenalty |
目前 lambda 空实现,实际没有设置 |
frequencyPenalty |
目前 lambda 空实现,实际没有设置 |
反编译后能看到:
java
config.temperature().ifPresent(value -> builder.temperature(value.doubleValue()));
config.maxOutputTokens().ifPresent(builder::maxTokens);
config.topP().ifPresent(value -> builder.topP(value.doubleValue()));
config.topK().ifPresent(value -> builder.topK(value.intValue()));
config.stopSequences()
.filter(list -> !list.isEmpty())
.ifPresent(builder::stopSequences);
但 presencePenalty 和 frequencyPenalty 对应的 lambda 是空方法:
java
private static void lambda$toSpringAiChatOptions$5(Float value) {
return;
}
private static void lambda$toSpringAiChatOptions$4(Float value) {
return;
}
所以在 google-adk-spring-ai:1.2.0 中,不能认为所有 ADK generation config 都完整映射到了 Spring AI。
createDefaultChatOptions
ConfigMapper.createDefaultChatOptions() 的默认值是:
java
ChatOptions.builder()
.temperature(0.7)
.maxTokens(1000)
.build();
但在 MessageConverter.toLlmPrompt(...) 的主流程中,是否使用默认 options 要看调用路径。通常 request 有 config 时使用映射后的 options;没有 config 时可能返回 null options。
isConfigurationValid 的关键信号
ConfigMapper.isConfigurationValid(...) 非常关键。
反编译后可见,它会明确把以下配置判为不支持:
java
if (config.responseSchema().isPresent()) {
return false;
}
if (config.responseMimeType().isPresent()) {
return false;
}
也就是说,官方 Spring AI 适配器在 1.2.0 版本里不是忘记映射 responseSchema,而是它的配置校验层也把 responseSchema / responseMimeType 归类为不支持。
这对结构化输出的影响很直接:
text
ADK LlmAgent.outputSchema(...)
-> LlmRequest.config.responseSchema 存在
-> ConfigMapper 不映射 responseSchema
-> Spring AI Prompt.options 没有 responseFormat
-> OpenAI HTTP body 没有 response_format
ToolConverter:工具映射机制
ToolConverter 负责把 ADK tools 转成 Spring AI tools。
ADK tool 的核心信息来自:
java
BaseTool.declaration()
也就是 FunctionDeclaration。
工具注册表
createToolRegistry(...) 会遍历 ADK tools:
java
for (BaseTool tool : tools.values()) {
if (tool.declaration().isPresent()) {
FunctionDeclaration declaration = tool.declaration().get();
registry.put(tool.name(), new ToolMetadata(
tool.name(),
tool.description(),
declaration
));
}
}
Schema 转 Spring AI 工具入参 schema
convertSchemaToSpringAi(Schema) 会处理:
| ADK Schema | Spring AI schema map |
|---|---|
type |
type |
description |
description |
properties |
properties |
required |
required |
类型会转换为 JSON Schema 常见小写值:
| ADK Type | JSON Schema type |
|---|---|
STRING |
string |
NUMBER |
number |
INTEGER |
integer |
BOOLEAN |
boolean |
ARRAY |
array |
OBJECT |
object |
简化逻辑:
java
Map<String, Object> result = new HashMap<>();
schema.type().ifPresent(type ->
result.put("type", convertTypeToString(type)));
schema.description().ifPresent(description ->
result.put("description", description));
schema.properties().ifPresent(properties ->
result.put("properties", convertedProperties));
schema.required().ifPresent(required ->
result.put("required", required));
ADK tool 转 Spring AI ToolCallback
convertToSpringAiTools(...) 会把 ADK BaseTool 转成 Spring AI FunctionToolCallback:
java
FunctionToolCallback.builder(tool.name(), function)
.description(tool.description())
.inputType(Map.class)
.inputSchema(schemaJson)
.build();
这里有一个非常重要的区别:
text
ToolConverter 会映射工具入参 schema。
ConfigMapper 不会映射模型输出 responseSchema。
所以不能因为 tool schema 能生效,就推断 outputSchema 也能生效。两条链路完全不同。
ADK + Spring AI 的完整运行机制
一个典型调用链:
text
用户消息
-> Runner
-> InvocationContext
-> LlmAgent.runAsync
-> BaseLlmFlow
-> LlmRequest
-> SpringAI.generateContent
-> MessageConverter.toLlmPrompt
-> ChatModel.call
-> ChatResponse
-> MessageConverter.toLlmResponse
-> Event
-> SessionService.appendEvent
-> outputKey 写入 state
在这条链路中,google-adk-spring-ai 只负责中间这段:
text
LlmRequest <-> Prompt
LlmResponse <-> ChatResponse
Tools <-> ToolCallback
而不负责:
text
把所有 ADK GenerateContentConfig 字段完整映射到每一种 Spring AI provider 的专有 options
这是一个合理的设计取舍。Spring AI 是统一抽象,但不同模型 provider 的结构化输出参数并不统一:
| Provider | 结构化输出方式 |
|---|---|
| OpenAI | response_format |
| Gemini | response schema / MIME type |
| Anthropic | tool use / JSON discipline |
| Ollama | format / provider-specific options |
因此 google-adk-spring-ai 的通用 ChatOptions 映射层无法天然覆盖所有 provider-specific structured output。
结构化输出为什么不会自动生效
ADK outputSchema 的核心链路
在 ADK 核心里:
java
LlmAgent.builder()
.outputSchema(schema)
.build();
最终会让 LlmRequest 带上:
text
GenerateContentConfig.responseSchema = schema
GenerateContentConfig.responseMimeType = application/json
这说明 ADK 核心是支持结构化输出的。
Spring AI OpenAI 的结构化输出链路
Spring AI OpenAI 要让模型按 JSON Schema 输出,需要设置:
java
OpenAiChatOptions.builder()
.responseFormat(ResponseFormat.builder()
.type(ResponseFormat.Type.JSON_SCHEMA)
.jsonSchema(...)
.build())
.build();
最终 HTTP body 里应出现:
json
{
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "result",
"strict": true,
"schema": {}
}
}
}
官方适配器缺少的桥
官方 google-adk-spring-ai:1.2.0 的主流程是:
text
LlmRequest.config
-> ConfigMapper.toSpringAiChatOptions
-> ChatOptions
但 ChatOptions 是 Spring AI 通用接口,没有 OpenAI 专用的 responseFormat 字段。
官方适配器也没有做:
text
GenerateContentConfig.responseSchema
-> OpenAiChatOptions.responseFormat
因此结构化输出链路断在这里:
text
ADK outputSchema 存在
Spring AI Prompt.options 不包含 responseFormat
OpenAI-compatible request body 不包含 response_format
示例一:官方适配器普通问答
普通问答可以直接使用官方适配器:
java
ChatModel chatModel = openAiChatModel();
LlmAgent agent = LlmAgent.builder()
.name("chatAgent")
.model(new SpringAI(chatModel))
.instruction("你是一个中文助手。")
.build();
请求流转:
text
ADK user Content
-> MessageConverter
-> Spring AI UserMessage
-> ChatModel.call
-> ChatResponse
-> ADK LlmResponse
这类场景没有特殊 provider options,官方适配器足够。
示例二:官方适配器工具调用
如果 ADK agent 有 tools:
java
LlmAgent agent = LlmAgent.builder()
.name("toolAgent")
.model(new SpringAI(chatModel))
.instruction("你可以调用工具完成任务。")
.tools(getWeatherTool)
.build();
适配器会做:
text
ADK BaseTool
-> FunctionDeclaration
-> ToolConverter.convertToSpringAiTools
-> Spring AI FunctionToolCallback
-> ToolCallingChatOptions
工具参数 schema 会通过 inputSchema(...) 写入 Spring AI tool callback。
这说明 google-adk-spring-ai 对工具映射是有实际支持的,尤其是 function declaration 的参数 schema。
示例三:官方适配器 outputSchema 不会自动落地
假设写:
java
Schema outputSchema = Schema.builder()
.type("OBJECT")
.properties(Map.of(
"capital",
Schema.builder()
.type("STRING")
.description("首都")
.build()))
.required("capital")
.build();
LlmAgent agent = LlmAgent.builder()
.name("capitalAgent")
.model(new SpringAI(chatModel))
.instruction("给定国家,只返回 JSON。")
.outputSchema(outputSchema)
.outputKey("found_capital")
.build();
ADK 内部请求会有:
text
LlmRequest.config.responseSchema present
LlmRequest.config.responseMimeType = application/json
但官方 Spring AI 适配器生成的 Prompt.options 不会包含:
text
OpenAiChatOptions.responseFormat
因此 OpenAI 兼容请求体不会自动出现:
json
{
"response_format": {
"type": "json_schema"
}
}
最终模型仍然可能返回:
text
中国的首都是北京。
或者返回字段不完整的 JSON。
项目中的补齐方案
在项目中,可以不用官方 SpringAI,而是自定义一个 BaseLlm 包装 Spring AI ChatModel。
核心思想:
text
先复用官方 MessageConverter 把 ADK LlmRequest 转成 Prompt
再把 ADK responseSchema 映射成 OpenAiChatOptions.responseFormat
最后调用 chatModel.call(patchedPrompt)
示例:
java
public class OutputKeySafeSpringAI extends BaseLlm {
private final ChatModel chatModel;
private final MessageConverter messageConverter;
private final ResponseSchemaMapper responseSchemaMapper;
public OutputKeySafeSpringAI(ChatModel chatModel) {
super(extractModelName(chatModel));
this.chatModel = chatModel;
this.messageConverter = new MessageConverter(new ObjectMapper());
this.responseSchemaMapper = new ResponseSchemaMapper();
}
private Flowable<LlmResponse> generateBlockingContent(LlmRequest llmRequest) {
return Flowable.fromCallable(() -> {
Prompt prompt = messageConverter.toLlmPrompt(llmRequest);
Prompt patchedPrompt = responseSchemaMapper.apply(prompt, llmRequest);
ChatResponse response = chatModel.call(patchedPrompt);
return messageConverter.toLlmResponse(response);
});
}
}
关键就在:
java
Prompt patchedPrompt = responseSchemaMapper.apply(prompt, llmRequest);
ResponseSchemaMapper
ResponseSchemaMapper 读取 ADK 的 responseSchema:
java
Schema responseSchema = llmRequest.config()
.flatMap(GenerateContentConfig::responseSchema)
.orElse(null);
如果不存在 schema,原样返回 prompt:
java
if (responseSchema == null) {
return prompt;
}
如果存在 schema,则构造 Spring AI OpenAI 的 ResponseFormat:
java
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormat.Type.JSON_SCHEMA)
.jsonSchema(ResponseFormat.JsonSchema.builder()
.name("ppt_project")
.schema(toSchemaMap(responseSchema))
.strict(Boolean.TRUE)
.build())
.build();
再合并到 OpenAiChatOptions:
java
OpenAiChatOptions patchedOptions =
mergeOptions(prompt.getOptions(), llmRequest.config().orElse(null), responseFormat);
return new Prompt(prompt.getInstructions(), patchedOptions);
最终链路变成:
text
ADK LlmRequest.config.responseSchema
-> ResponseSchemaMapper
-> OpenAiChatOptions.responseFormat
-> Spring AI OpenAiChatModel.createRequest
-> ChatCompletionRequest.response_format
-> HTTP body
Agent 装配示例
项目中可以在 agent 装配阶段根据配置生成 ADK schema:
java
LlmAgent.Builder builder = LlmAgent.builder()
.name(agentConfig.getName())
.description(agentConfig.getDescription())
.model(new OutputKeySafeSpringAI(chatModel))
.instruction(agentConfig.getInstruction())
.outputKey(agentConfig.getOutputKey());
resolveOutputSchema(agentConfig).ifPresent(builder::outputSchema);
schema 可以由 DTO 生成:
java
static Optional<Schema> resolveOutputSchema(Agent agentConfig) {
if (!Boolean.TRUE.equals(agentConfig.getStructuredOutput())) {
return Optional.empty();
}
Class<?> schemaClass = Class.forName(agentConfig.getOutputSchemaClass());
return Optional.of(FunctionCallingUtils.buildSchemaFromType(schemaClass));
}
对应 YAML:
yaml
agents:
- name: pptGenerator
description: 生成前端可直接渲染的 PPT 项目 JSON
instruction: |
你是一个 PPT 设计专家。
只输出完整 PptProjectDTO JSON。
outputKey: ppt_project
structuredOutput: true
outputSchemaClass: org.cheese.api.dto.PptProjectDTO
注意,这里的 structuredOutput / outputSchemaClass 是项目自己的配置约定,不是 google-adk-spring-ai 自动识别的 YAML 能力。
如何判断结构化输出是否真的生效
要分三层看。
第一层:ADK request 是否有 responseSchema
在 ADK plugin 里打印:
java
boolean present = request.config()
.flatMap(GenerateContentConfig::responseSchema)
.isPresent();
期望日志:
text
Response Schema: present
如果这里是 absent,说明 agent 装配时没有设置 outputSchema。
第二层:Spring AI Prompt options 是否有 responseFormat
如果使用官方 SpringAI,这里通常没有。
如果使用自定义 ResponseSchemaMapper,应该能从 Prompt.getOptions() 看到:
text
OpenAiChatOptions.responseFormat.type = JSON_SCHEMA
第三层:HTTP body 是否有 response_format
最终要看请求体:
json
{
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "ppt_project",
"strict": true,
"schema": {
"type": "object",
"properties": {}
}
}
}
}
判断表:
| 现象 | 结论 |
|---|---|
| ADK responseSchema absent | agent 没有设置 outputSchema |
| ADK responseSchema present,但 HTTP 无 response_format | 缺少 ADK -> Spring AI provider options 桥接 |
| HTTP 有 response_format,但输出仍不符合 | 模型或 OpenAI-compatible 网关不支持,或 schema 太复杂 |
| 输出符合 JSON Schema,但业务不可渲染 | 需要业务 verifier / repairer |
官方适配器的能力边界
google-adk-spring-ai:1.2.0 适合:
text
普通文本对话
系统指令 + 用户消息转换
多模态 media 的基础映射
Spring AI ToolCallback 工具调用
基础 generation config 映射
Spring AI ChatResponse 转 ADK LlmResponse
不适合直接期待:
text
ADK outputSchema 自动变成 OpenAI response_format
所有 GenerateContentConfig 字段完整映射
所有 provider-specific options 自动支持
Live connection 支持
复杂结构化输出自动修复
其中 live connection 在 SpringAI.connect(...) 里直接抛出:
java
throw new UnsupportedOperationException(
"Live connection is not supported for Spring AI models.");
为什么官方没有直接映射 responseSchema
从架构上看,这是因为 google-adk-spring-ai 面向的是 Spring AI 的通用模型抽象,而结构化输出是 provider-specific 能力。
ADK 的结构化输出字段:
text
GenerateContentConfig.responseSchema
GenerateContentConfig.responseMimeType
更接近 Gemini / Google GenAI 的配置模型。
Spring AI OpenAI 的结构化输出字段:
text
OpenAiChatOptions.responseFormat
ResponseFormat.Type.JSON_SCHEMA
是 OpenAI provider 专有 options。
如果官方适配器在通用 ConfigMapper 里直接依赖 OpenAiChatOptions,就会让 google-adk-spring-ai 从通用适配器变成 OpenAI 专用适配器。这可能是官方当前没有做这层映射的主要原因。
因此业务项目中更合理的方式是:
text
保留官方 MessageConverter 的通用消息转换能力
在项目自己的模型包装层里按 provider 补充专有 options
推荐实现模式
模式一:普通 agent 使用官方 SpringAI
适合不需要结构化输出的 agent:
java
.model(new SpringAI(chatModel))
例如:
text
普通问答
总结
分类
不严格依赖 JSON 结构的回复
模式二:结构化输出 agent 使用自定义 BaseLlm
适合需要 JSON Schema 的 agent:
java
.model(new OutputKeySafeSpringAI(chatModel))
例如:
text
PPT 项目 JSON
Draw.io 结构化中间结果
表单抽取
工作流状态对象
前端可直接渲染的数据结构
自定义 wrapper 可以复用官方组件:
text
MessageConverter
StreamingResponseAggregator
SpringAIErrorMapper
但要自己补:
text
ResponseSchemaMapper
provider-specific options merge
HTTP request logging
schema verifier / repairer
模式三:按 provider 做 mapper 策略
如果系统未来支持多个 Spring AI provider,可以把 mapper 做成策略:
java
interface ResponseSchemaBridge {
boolean supports(ChatModel chatModel);
Prompt apply(Prompt prompt, LlmRequest request);
}
OpenAI 兼容模型:
text
GenerateContentConfig.responseSchema
-> OpenAiChatOptions.responseFormat(JSON_SCHEMA)
Gemini Spring AI 模型:
text
GenerateContentConfig.responseSchema
-> Gemini provider-specific options
不支持结构化输出的模型:
text
降级为 prompt JSON instruction
后端 verifier + retry repair
排障清单
1. 确认使用的是官方 SpringAI 还是自定义 wrapper
如果代码里是:
java
.model(new SpringAI(chatModel))
则 outputSchema 不会自动映射到 OpenAI response_format。
如果代码里是:
java
.model(new OutputKeySafeSpringAI(chatModel))
则看自定义 wrapper 是否调用了:
java
responseSchemaMapper.apply(prompt, llmRequest)
2. 确认 ADK request 有 schema
日志应有:
text
Response Schema: present
3. 确认 HTTP 请求体有 response_format
开启请求日志后查:
text
"response_format"
"type":"json_schema"
"strict":true
4. 确认模型或网关支持 JSON Schema
OpenAI-compatible 接口不代表完整支持 OpenAI structured outputs。
如果请求体已经有 response_format,但模型仍然返回残缺 JSON,需要检查:
text
当前模型是否支持 json_schema
网关是否透传 response_format
schema 是否过大或包含不支持字段
max_tokens 是否太小导致截断
5. 保留后端 repairer
即使结构化输出生效,也要保留后端修复:
text
固定字段归一化
空 slides 修复
空 elements 修复
transform 越界修复
业务字段默认值补齐
因为 JSON Schema 只能保证结构,不保证 PPT 业务质量。
总结
google-adk-spring-ai:1.2.0 是一个通用适配器,它把 ADK 的模型请求转换为 Spring AI 的模型请求。
它的核心映射包括:
text
LlmRequest -> Prompt
Content -> Message
Part -> text/media/tool call
GenerateContentConfig 部分字段 -> ChatOptions
BaseTool -> ToolCallback
ChatResponse -> LlmResponse
但它没有把 ADK 的结构化输出:
text
responseSchema / responseMimeType
映射成 Spring AI OpenAI 的:
text
responseFormat / response_format
因此 ADK + Spring AI 要真正支持结构化输出,推荐使用项目自定义 wrapper:
text
官方 MessageConverter
+ 自定义 ResponseSchemaMapper
+ provider-specific OpenAiChatOptions.responseFormat
+ HTTP 日志验证
+ 后端 verifier / repairer
这样既能复用官方适配器的消息、工具、响应转换能力,又能补齐 OpenAI-compatible 模型的结构化输出能力。