【AgentScope Java新手村系列】(4)结构化输出

第四章 结构化输出:用 JSON Schema 让 Agent 直接返回 Java POJO

"让 LLM 返回自然语言容易,让它返回强类型的 Java 对象很难------除非用 JSON Schema 约束。本章演示这个'文本到对象'转换的关键技巧。"

4.1 为什么需要结构化输出

默认情况下,Agent 返回的是自然语言文本。但在很多场景下,我们需要 Agent 返回结构化的数据:

  • 从文本中提取信息(姓名、邮箱、电话)
  • 分类任务(情感分类、意图识别)
  • 数据生成(产品描述、测试数据)

AgentScope Java 支持让 Agent 返回指定 Java 类型的数据。2.0 沿用 1.x 的 @StructuredOutput + JSON Schema 机制,并在 Msg 上提供 getStructuredData(Class) 读取入口。

4.2 基本用法

定义输出类型

复制代码
public static class ProductRequirements {
    public String productType;
    public String brand;
    public Integer minRam;
    public Double maxBudget;
    public List<String> features;

    public ProductRequirements() {}  // 必须有无参构造函数
}

注意事项:

  • 类必须有无参构造函数
  • 字段使用 public 修饰(或提供 getter/setter)
  • 支持基本类型、String、List、嵌套对象
  • 2.0 推荐在字段上加 com.fasterxml.jackson.annotation.JsonPropertyDescription,让生成的 JSON Schema 描述更清晰------LLM 填充准确率更高

调用时指定类型

复制代码
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.agent.RuntimeContext;

UserMessage userMsg = new UserMessage(
        "我需要一台16GB内存、苹果品牌、预算2000美元左右的笔记本电脑");

// 传入 Class 对象
Msg msg = agent.call(userMsg, ProductRequirements.class, RuntimeContext.empty()).block();

// 获取结构化数据
ProductRequirements result = msg.getStructuredData(ProductRequirements.class);
System.out.println("Product: " + result.productType);
System.out.println("Brand: " + result.brand);
System.out.println("RAM: " + result.minRam + " GB");
System.out.println("Budget: $" + result.maxBudget);

2.0 的 call(messages, returnType, ctx) 形式与 1.x 的 call(messages, returnType) 形式并存;新代码请统一传 RuntimeContext

4.3 完整示例

以下方案通过提示词要求返回 JSON + 手动反序列化 ,不依赖 response_format 参数,所有模型(含 DeepSeek)都支持。

复制代码
package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.formatter.openai.OpenAIChatFormatter;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.OpenAIChatModel;
import io.agentscope.core.tool.Toolkit;

import java.util.List;

public class StructuredOutputExample {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /** 从 LLM 回复中提取第一个 JSON 对象,忽略前后的自然语言 */
    private static String extractJson(String raw) {
        int start = raw.indexOf('{');
        int end = raw.lastIndexOf('}');
        if (start != -1 && end > start) {
            return raw.substring(start, end + 1);
        }
        throw new IllegalArgumentException("No JSON found: " + raw);
    }

    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("DEEPSEEK_API_KEY");

        ReActAgent agent = ReActAgent.builder()
                .name("AnalysisAgent")
                .sysPrompt("你是一个智能分析助手,始终输出纯 JSON,不要包含其他文字。")
                .model(OpenAIChatModel.builder()
                        .apiKey(apiKey)
                        .modelName("deepseek-chat")
                        .baseUrl("https://api.deepseek.com")
                        .stream(true)
                        .formatter(new OpenAIChatFormatter())
                        .build())
                .toolkit(new Toolkit())
                .build();

        RuntimeContext ctx = RuntimeContext.empty();

        // 示例 1:提取产品信息
        System.out.println("=== Product Requirements ===");
        String reply1 = agent.call(
                new UserMessage("提取产品需求:我需要一台16GB内存、苹果品牌、"
                        + "预算2000美元左右的笔记本电脑。"
                        + "请输出 JSON:{\"productType\":\"类型\", \"brand\":\"品牌\","
                        + " \"minRam\":16, \"maxBudget\":2000, \"features\":[\"特性\"]}"),
                ctx
        ).block().getTextContent();

        ProductRequirements product = MAPPER.readValue(extractJson(reply1), ProductRequirements.class);
        System.out.println("Product Type: " + product.productType);
        System.out.println("Brand: " + product.brand);
        System.out.println("Min RAM: " + product.minRam + " GB");

        // 示例 2:情感分析
        System.out.println("\n=== Sentiment Analysis ===");
        String reply2 = agent.call(
                new UserMessage("分析情感:这个产品超出了我的预期!质量很棒但配送速度慢。"
                        + "请输出 JSON:{\"sentiment\":\"正面\", \"score\":0.95, \"summary\":\"总结\"}"),
                ctx
        ).block().getTextContent();

        SentimentAnalysis sentiment = MAPPER.readValue(extractJson(reply2), SentimentAnalysis.class);
        System.out.println("Overall: " + sentiment.sentiment);
        System.out.println("Score: " + sentiment.score);
    }

    public static class ProductRequirements {
        public String productType;
        public String brand;
        public Integer minRam;
        public Double maxBudget;
        public List<String> features;
        public ProductRequirements() {}
    }

    public static class SentimentAnalysis {
        public String sentiment;
        public Double score;
        public String summary;
        public SentimentAnalysis() {}
    }
}

4.4 流式结构化输出

也可以通过 streamEvents() 拿到结构化输出(推荐):

复制代码
import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.event.AgentEventType;
import io.agentscope.core.event.AgentEndEvent;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.agent.RuntimeContext;
import reactor.core.publisher.Flux;

Flux<AgentEvent> eventFlux = agent.streamEvents(
        new UserMessage("..."),
        ProductRequirements.class,
        RuntimeContext.empty());

AgentEndEvent end = eventFlux.filter(e -> e.getType() == AgentEventType.AGENT_END)
        .blockLast()
        .map(e -> (AgentEndEvent) e)
        .orElseThrow();

ProductRequirements result = end.getMessage().getStructuredData(ProductRequirements.class);

1.x 风格的 agent.stream(msg, opts, type) 仍可工作但已标注 @Deprecated(forRemoval = true),新代码请用 streamEvents(...)

4.5 支持的字段类型

Java 类型 JSON Schema 类型
String string
Integer, int integer
Double, double, Float, float number
Boolean, boolean boolean
List<T> array
Map<String, Object> object
嵌套对象 object
Java record object(2.0 起官方推荐用 record,更简洁)

4.5.1 用 record 简化定义

2.0 推荐用 Java 17 的 record 来表达输出类型------无样板代码、字段自带 getter:

复制代码
public record ProductRequirements(
        @JsonPropertyDescription("产品类型,例如 laptop / phone / tablet")
        String productType,
        @JsonPropertyDescription("品牌,例如 Apple / Dell / Lenovo")
        String brand,
        @JsonPropertyDescription("最小内存,单位 GB")
        Integer minRam,
        @JsonPropertyDescription("最高预算,单位美元")
        Double maxBudget,
        @JsonPropertyDescription("用户提到的特性关键词列表")
        List<String> features
) {}

加上 @JsonPropertyDescription 之后,生成的 JSON Schema 描述会带上字段说明------LLM 填充时知道每个字段的"业务含义",准确率显著提升。

4.5.2 嵌套对象示例

复制代码
public static class Address {
    public String street;
    public String city;
    public String country;
    public Address() {}
}

public static class Person {
    public String name;
    public Integer age;
    public Address address;        // 嵌套对象
    public List<String> hobbies;   // 列表
    public Person() {}
}

4.6 工作原理

当你传入一个 Class 对象时,框架会:

  1. 使用 jsonschema-generator 根据 Java 类生成 JSON Schema(@JsonPropertyDescription / @JsonProperty 都会反映到 schema 上)
  2. 将 JSON Schema 作为约束发送给 LLM(通过 response_format 参数或 system prompt 注入)
  3. LLM 按照 Schema 格式输出 JSON
  4. 框架将 JSON 反序列化为 Java 对象
  5. 将对象放入 Msg 的结构化数据字段(msg.getStructuredData(Class) 读取)

这个过程对用户是透明的,你只需要定义 Java 类即可。

4.6.1 模型兼容性说明

结构化输出有两种实现方式:

方式 原理 模型兼容性
API 参数约束agent.call(msg, SomeClass.class, rt) 框架向 API 发送 response_format 参数,强制服务器校验输出为 JSON 仅 OpenAI 等部分模型支持
提示词驱动(本章示例的做法) 在 UserMessage 中写"请输出 JSON 格式:{...}",让 LLM 按格式输出 所有模型都支持

本章的完整示例采用提示词驱动方式,因此不挑模型------DeepSeek、通义千问等均可正常使用。

如果误用了方式一(agent.call(msg, SomeClass.class, rt)),不支持的模型会报:

复制代码
"This response_format type is unavailable now"

此时改用本章示例的方式即可。

4.7 最佳实践

  1. 字段名要有意义:LLM 会根据字段名理解应该填什么内容

  2. 使用合适的类型 :数字用 Integer/Double,不要都用 String

  3. List 用于多值字段 :当一个字段可能有多个值时,使用 List

  4. @JsonPropertyDescription:为每个字段写一句话业务描述,准确率提升明显

  5. 系统提示词配合 :在 sysPrompt 中说明输出要求,提高准确率

  6. 处理异常:LLM 输出可能不符合预期,需要 try-catch 处理

    try {
    Msg msg = agent.call(userMsg, ProductRequirements.class, ctx).block();
    ProductRequirements result = msg.getStructuredData(ProductRequirements.class);
    // 使用 result
    } catch (Exception e) {
    System.err.println("Failed to parse structured output: " + e.getMessage());
    }

4.8 2.0 增量:结构化输出与子 agent 协作

如果你在 HarnessAgent 里用子 agent 处理"先调研再汇总"的场景,可以让子 agent 返回结构化结果,主 agent 自动拿到强类型:

复制代码
// 主 agent 调用子 agent,子 agent 内部 call(..., Report.class, ctx) 返回 Report
// 主 agent 拿到的 tool_result 是 Report 的 JSON 序列化
// 主 agent 的下一轮推理基于这份结构化结果继续

workspace/subagents/researcher.md 里可以显式说明子 agent 的输出 schema(用自然语言),主 agent 就能稳定地消费。

相关推荐
何以解忧,唯有..1 小时前
Python 中的继承机制:从基础到高级用法详解
java·开发语言·python
Yiyaoshujuku1 小时前
化合物数据集API接口(数据结构及样例)
java·网络·数据结构
console.log('npc')1 小时前
架构篇 AI 智能体主流架构全梳理
ai·ai编程
plainGeekDev2 小时前
算法刷题笔记:一维DP没那么难,状态想清楚就赢了一半
java·算法·面试
IceBing2 小时前
还在一个个连接 Arthas?这个开源平台支持批量诊断 JVM
java
SL_staff2 小时前
《如何用规则引擎替代if-else?JVS-Rules可视化编排比硬编码强在哪里?》
java·低代码·架构
ZFSS2 小时前
VS Code + Hailuo MCP 使用指南
人工智能·ai·copilot·ai编程·ai写作
AI导出鸭PC端2 小时前
ChatGPT怎么生成word文档?「AI 导出鸭」解决格式丢失痛点
人工智能·ai·chatgpt·word·豆包·ai导出鸭
装不满的克莱因瓶2 小时前
自动微分的原理:计算图与前向传播
人工智能·pytorch·python·数学·ai·微积分·计算图