diff --git a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java index 20810c6c8..bd9fdb8ea 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -1038,7 +1038,17 @@ private Mono doStructuredCall(List msgs, Class targetClass, JsonNod ? model.supportsNativeStructuredOutputWithTools() : model.supportsNativeStructuredOutput(); if (useNative) { - return doNativeStructuredCall(msgs, jsonSchema); + return doNativeStructuredCall(msgs, jsonSchema) + .onErrorResume( + e -> { + log.warn( + "Native structured output failed ({}) — falling back to" + + " synthetic tool path", + e.getMessage() != null + ? e.getMessage() + : e.getClass().getSimpleName()); + return doFallbackStructuredCall(msgs, jsonSchema); + }); } return doFallbackStructuredCall(msgs, jsonSchema); } @@ -1075,11 +1085,21 @@ private Mono doNativeStructuredCall(List msgs, Map jso .strict(true) .build()); + int contextSizeBefore = scope.state.contextMutable().size(); + return scope.doCallInner(msgs) .flatMap( result -> { Msg out = wrapNativeStructuredResult(result); return saveStateToSession(scope).thenReturn(out); + }) + .doOnError( + e -> { + List ctx = scope.state.contextMutable(); + while (ctx.size() > contextSizeBefore) { + ctx.remove(ctx.size() - 1); + } + scope.nativeResponseFormat = null; }); }); } @@ -1912,7 +1932,7 @@ private Mono reasoning(int iter, boolean ignoreMaxIters) { event.getEffectiveGenerateOptions() != null ? event.getEffectiveGenerateOptions() : buildGenerateOptions(); - if (nativeResponseFormat != null) { + if (nativeResponseFormat != null && soTool == null) { options = GenerateOptions.mergeOptions( GenerateOptions.builder() diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java index 6aeb0d9ba..f69b82b2f 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeToolsHelper.java @@ -15,6 +15,7 @@ */ package io.agentscope.core.formatter.dashscope; +import io.agentscope.core.formatter.ResponseFormat; import io.agentscope.core.formatter.dashscope.dto.DashScopeFunction; import io.agentscope.core.formatter.dashscope.dto.DashScopeParameters; import io.agentscope.core.formatter.dashscope.dto.DashScopeTool; @@ -103,6 +104,12 @@ public void applyOptions( if (parallelToolCalls != null) { params.setParallelToolCalls(parallelToolCalls); } + + ResponseFormat responseFormat = + getOption(options, defaultOptions, GenerateOptions::getResponseFormat); + if (responseFormat != null) { + params.setResponseFormat(responseFormat); + } } /** diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index ce16310cc..ba57cf0b0 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -61,6 +61,7 @@ public class DashScopeChatModel extends ChatModelBase { private final boolean stream; private final Boolean enableThinking; // nullable private final Boolean enableSearch; // nullable + private Boolean nativeStructuredOutput; // nullable, set by Builder private final EndpointType endpointType; private final GenerateOptions defaultOptions; private final Formatter formatter; @@ -152,6 +153,7 @@ public DashScopeChatModel( this.stream = enableThinking != null && enableThinking ? true : stream; this.enableThinking = enableThinking; this.enableSearch = enableSearch; + this.nativeStructuredOutput = null; this.endpointType = endpointType != null ? endpointType : EndpointType.AUTO; this.defaultOptions = defaultOptions != null ? defaultOptions : GenerateOptions.builder().build(); @@ -384,7 +386,13 @@ public String getModelName() { @Override public boolean supportsNativeStructuredOutput() { - return true; + if (Boolean.TRUE.equals(enableThinking)) { + return false; + } + if (nativeStructuredOutput != null) { + return nativeStructuredOutput; + } + return false; } public static class Builder { @@ -401,6 +409,7 @@ public static class Builder { private boolean enableEncrypt = false; private ProxyConfig proxyConfig; private int contextWindowSize = -1; + private Boolean nativeStructuredOutput; private Boolean nativeStructuredOutputWithTools; /** @@ -631,13 +640,33 @@ public Builder contextWindowSize(int contextWindowSize) { return this; } + /** + * Sets whether this model supports native structured output via {@code response_format} + * with {@code json_schema} type. + * + *

Defaults to {@code false}. DashScope's native endpoint only supports + * {@code json_object} (free-form JSON), not {@code json_schema} (strict schema + * validation). When {@code false}, the framework uses the {@code generate_response} + * tool fallback for structured output requests. + * + *

Set to {@code true} only if your model/endpoint is confirmed to support + * {@code json_schema} in {@code response_format}. + * + * @param nativeStructuredOutput true to enable native json_schema path + * @return this builder instance + */ + public Builder nativeStructuredOutput(boolean nativeStructuredOutput) { + this.nativeStructuredOutput = nativeStructuredOutput; + return this; + } + /** * Sets whether this model correctly handles native structured output * ({@code response_format}) alongside tool calling. * - *

Defaults to {@code true}, which is correct for Qwen models on DashScope. - * Set to {@code false} for third-party models hosted on DashScope that - * prioritise {@code response_format} over tool invocations. + *

Defaults to {@code false} (inherits from + * {@link #nativeStructuredOutput(boolean)}). Set to {@code true} only for models + * that support both {@code response_format} and tool calling simultaneously. * * @param nativeStructuredOutputWithTools false to use fallback when tools are present * @return this builder instance @@ -699,6 +728,9 @@ public DashScopeChatModel build() { contextWindowSize >= 0 ? contextWindowSize : ModelContextWindows.lookup(modelName, ModelContextWindows.DASHSCOPE)); + if (nativeStructuredOutput != null) { + model.nativeStructuredOutput = nativeStructuredOutput; + } if (nativeStructuredOutputWithTools != null) { model.setNativeStructuredOutputWithTools(nativeStructuredOutputWithTools); } diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/OpenAIResponseParser.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/OpenAIResponseParser.java index 4a41e1aa9..1e4e94023 100644 --- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/OpenAIResponseParser.java +++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/OpenAIResponseParser.java @@ -250,12 +250,22 @@ protected ChatResponse parseCompletionResponse(OpenAIResponse response, Instant Map argsMap = new HashMap<>(); if (!arguments.isEmpty()) { - @SuppressWarnings("unchecked") - Map parsed = - JsonUtils.getJsonCodec() - .fromJson(arguments, Map.class); - if (parsed != null) { - argsMap.putAll(parsed); + try { + @SuppressWarnings("unchecked") + Map parsed = + JsonUtils.getJsonCodec() + .fromJson(arguments, Map.class); + if (parsed != null) { + argsMap.putAll(parsed); + } + } catch (Exception parseEx) { + log.warn( + "Failed to parse tool call arguments as JSON;" + + " preserving raw arguments: id={}," + + " name={}, error={}", + toolCallId, + name, + parseEx.getMessage()); } } diff --git a/docs/v2/en/docs/building-blocks/model.md b/docs/v2/en/docs/building-blocks/model.md index 179679088..329fd40e2 100644 --- a/docs/v2/en/docs/building-blocks/model.md +++ b/docs/v2/en/docs/building-blocks/model.md @@ -168,20 +168,54 @@ WeatherInfo info = msg.getStructuredData(WeatherInfo.class); How it works: the framework synthesizes a forced structured tool call from the target class, validates and repairs the model output, and writes the result into `Msg.metadata` under the `structured_output` key, so `getStructuredData(Class)` can deserialize it directly. Complete example: `agentscope-examples/documentation/.../structuredoutput/StructuredOutputExample.java`. -> **Structured output with tool calling** -> -> When an agent has both tools and structured output, some OpenAI-compatible providers (e.g. Kimi, Deepseek) prioritise the `response_format` constraint and skip tool calling entirely. If you encounter this, set `nativeStructuredOutputWithTools(false)` when building the model — the framework will use a synthetic tool approach for structured output, fully compatible with the ReAct tool-calling loop: -> -> ```java -> OpenAIChatModel model = OpenAIChatModel.builder() -> .apiKey("...") -> .baseUrl("https://api.moonshot.cn/v1") -> .modelName("moonshot-v1-8k") -> .nativeStructuredOutputWithTools(false) -> .build(); -> ``` -> -> `DashScopeChatModel` supports this option as well. For native OpenAI models (GPT-4o, etc.) the default behavior handles both correctly — no configuration needed. +#### Structured output path selection + +The framework provides two structured output paths: + +| Path | Condition | Mechanism | +|------|-----------|-----------| +| **Native** | `supportsNativeStructuredOutput() = true` | Uses `response_format` + `json_schema` for direct JSON output | +| **Fallback** (default) | `supportsNativeStructuredOutput() = false` | Injects a `generate_response` synthetic tool; model returns structured data via tool call | + +If the native path fails (e.g. model returns HTTP 400), the framework **automatically falls back** to the synthetic tool path — no user intervention needed. + +#### Default behavior per provider + +| Provider | `supportsNativeStructuredOutput` | Notes | +|----------|----------------------------------|-------| +| OpenAI (GPT-4o, etc.) | `true` | Native `json_schema` support | +| OpenAI (DeepSeek/GLM formatter) | `false` | Not supported; auto-fallback | +| DashScope | `false` | Native endpoint only supports `json_object`, not `json_schema`; fallback by default | +| Anthropic | `false` (default) | — | + +> **DashScope users**: Thinking mode (`enableThinking(true)`) does not support structured output at all — the framework forces the fallback path. + +#### Explicit configuration + +If you confirm your model/endpoint supports `json_schema`, enable the native path via builder: + +```java +DashScopeChatModel model = DashScopeChatModel.builder() + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + .modelName("qwen-plus") + .nativeStructuredOutput(true) // explicitly enable native json_schema path + .build(); +``` + +#### Structured output with tool calling + +When an agent has both tools and structured output, some OpenAI-compatible providers (e.g. Kimi, Deepseek) prioritise the `response_format` constraint and skip tool calling entirely. Set `nativeStructuredOutputWithTools(false)` to resolve this: + +```java +OpenAIChatModel model = OpenAIChatModel.builder() + .apiKey("...") + .baseUrl("https://api.moonshot.cn/v1") + .modelName("moonshot-v1-8k") + .nativeStructuredOutputWithTools(false) + .build(); +``` + +`DashScopeChatModel` supports this option as well. For native OpenAI models (GPT-4o, etc.) the default behavior handles both correctly — no configuration needed. ### Formatter diff --git a/docs/v2/zh/docs/building-blocks/model.md b/docs/v2/zh/docs/building-blocks/model.md index e2959860f..d31313dd2 100644 --- a/docs/v2/zh/docs/building-blocks/model.md +++ b/docs/v2/zh/docs/building-blocks/model.md @@ -168,20 +168,54 @@ WeatherInfo info = msg.getStructuredData(WeatherInfo.class); 实现细节:框架会基于目标 Class 合成强制结构化的工具调用,再校验并修复模型输出,最后把结果挂到 `Msg.metadata` 的 `structured_output` 字段,供 `getStructuredData(Class)` 直接反序列化。完整示例:`agentscope-examples/documentation/.../structuredoutput/StructuredOutputExample.java`。 -> **结构化输出与工具调用共存** -> -> 当 Agent 同时注册了工具并请求结构化输出时,部分 OpenAI 兼容 API(如 Kimi、Deepseek 等)会优先遵循 `response_format` 约束而跳过工具调用。如果遇到此问题,在构建 Model 时设置 `nativeStructuredOutputWithTools(false)`,框架将改用合成工具方式输出结构化结果,与工具调用完全兼容: -> -> ```java -> OpenAIChatModel model = OpenAIChatModel.builder() -> .apiKey("...") -> .baseUrl("https://api.moonshot.cn/v1") -> .modelName("moonshot-v1-8k") -> .nativeStructuredOutputWithTools(false) -> .build(); -> ``` -> -> `DashScopeChatModel` 同样支持此配置。对于 OpenAI 原生模型(GPT-4o 等)无需设置,默认行为即可正确处理。 +#### 结构化输出路径选择 + +框架提供两条结构化输出路径: + +| 路径 | 条件 | 机制 | +|------|------|------| +| **Native** | `supportsNativeStructuredOutput() = true` | 通过 `response_format` + `json_schema` 让模型直接输出合规 JSON | +| **Fallback**(默认) | `supportsNativeStructuredOutput() = false` | 注入 `generate_response` 合成工具,模型通过 tool call 返回结构化数据 | + +当 native 路径失败(如模型返回 400),框架会**自动降级**到 fallback 路径,无需用户干预。 + +#### 各 Provider 默认行为 + +| Provider | `supportsNativeStructuredOutput` | 说明 | +|----------|----------------------------------|------| +| OpenAI (GPT-4o 等) | `true` | 原生支持 `json_schema` | +| OpenAI (DeepSeek/GLM formatter) | `false` | 不支持,自动走 fallback | +| DashScope | `false` | DashScope 原生端点仅支持 `json_object`,不支持 `json_schema`;框架默认走 fallback | +| Anthropic | `false`(默认) | — | + +> **DashScope 用户注意**:DashScope 的思考模式(`enableThinking(true)`)不支持结构化输出,框架会强制走 fallback 路径。 + +#### 显式配置 + +如果确认你的模型/端点支持 `json_schema`,可以通过 builder 开启 native 路径: + +```java +DashScopeChatModel model = DashScopeChatModel.builder() + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + .modelName("qwen-plus") + .nativeStructuredOutput(true) // 显式开启 native json_schema 路径 + .build(); +``` + +#### 结构化输出与工具调用共存 + +当 Agent 同时注册了工具并请求结构化输出时,部分 OpenAI 兼容 API(如 Kimi、Deepseek 等)会优先遵循 `response_format` 约束而跳过工具调用。设置 `nativeStructuredOutputWithTools(false)` 可解决此问题: + +```java +OpenAIChatModel model = OpenAIChatModel.builder() + .apiKey("...") + .baseUrl("https://api.moonshot.cn/v1") + .modelName("moonshot-v1-8k") + .nativeStructuredOutputWithTools(false) + .build(); +``` + +`DashScopeChatModel` 同样支持此配置。对于 OpenAI 原生模型(GPT-4o 等)无需设置。 ### Formatter