AgentScope Java新手教程第4篇:结构化输出

作者:技术拾光

时间:2026-06-12

浏览:335

Agent开发中通过JSONSchema让Agent直接返回JavaPOJO,可避免文本解析的低效与错误。定义Java类(需无参构造函数及Public字段)后,调用时传入Class对象,框架自动生成JSONSchema约束LLM输出并反序列化。支持流式输出及Javarecord简化定义,提升准确率。

文章目录

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

在实际的 Agent 开发中,开发者迟早会遇到一个常见问题:Agent 输出的自然语言虽然“像人话”,但在系统对接时需要反复进行文本解析、正则匹配和字段提取。这种方式不仅效率低下,还容易出错。有经验的 Agent 开发者都知道,更好的解决方案是让 Agent 从一开始就输出结构化的数据,直接映射到代码中的 Java 对象,从而避免手工解析的繁琐工作。

4.1 为什么需要结构化输出

默认情况下,Agent 返回的是自然语言文本。但在很多场景中,我们需要 Agent 返回结构化的数据: AgentScope Java新手教程第4篇:结构化输出 - 从文本中提取信息(姓名、邮箱、电话) - 分类任务(情感分类、意图识别) - 数据生成(产品描述、测试数据) AgentScope Java 支持让 Agent 返回指定的 Java 类型数据。2.0 版本沿用了 1.x 的 @StructuredOutput JSON Schema 机制,并在 Msg 上提供了 getStructuredData(Class) 读取入口。这套方案的好处在于:开发者只需定义好 Java 类,其余工作由框架自动处理。

4.2 基本用法

**定义输出类型** 来看一个典型的输出类型定义。假设要从用户描述中提取产品需求:
public static class ProductRequirements {
    public String productType;
    public String brand;
    public Integer minRam;
    public Double maxBudget;
    public List features;
    public ProductRequirements() { } // 必须有无参构造函数
}
几个关键点: - 类必须有一个无参构造函数,这是反序列化的基础。 - 字段用 public 修饰(或者提供 getter/setter,但 public 更便捷)。 - 支持基本类型、String、List 和嵌套对象。 - 2.0 推荐在字段上加 com.fasterxml.jackson.annotation.JsonPropertyDescription,这样生成的 JSON Schema 描述更加清晰——不要小看这一行注解,它能显著提升 LLM 的填充准确率。 **调用时指定类型** 定义好类之后,调用时只需把 Class 对象传给 agent.call(...):
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);
注意:这里传入的是 ProductRequirements.class,Agent 内部会自动根据这个类的结构生成 JSON Schema,约束 LLM 的输出格式,最后反序列化为目标 Java 对象。整个过程对开发者几乎是透明的。

4.3 完整示例

下面是一个可运行的完整示例,使用 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("=== 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 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 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 类型
Stringstring
Integer, intinteger
Double, double, Float, floatnumber
Boolean, booleanboolean
Listarray
Mapobject
嵌套对象object
Java recordobject(2.0 开始官方推荐使用 record,更简洁)
**4.5.1 用 record 简化定义** 如果使用 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 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 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、通义千问等均可正常使用。 如果误用方式一,不支持的模型会报错:"This response_format type is unavailable now",此时换用本章示例的提示词驱动方式即可。

4.7 最佳实践

最后整理几条实际项目积累的经验: - **字段名要有意义**:LLM 会根据字段名理解应填写的内容,例如 productType 比 type 更明确。 - **类型要选对**:数字使用 Integer 或 Double,不要为了省事全部使用 String。 - **多值字段用 List**:如果一个字段可能有多个值,使用 List 而非 String。 - **加 @JsonPropertyDescription**:为每个字段写一句业务描述,这是成本最低的准确率提升手段。 - **系统提示词配合**:在 sysPrompt 中说明输出要求,能让 LLM 少“猜测”很多。 - **处理异常**: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 就能稳定地消费子 agent 返回的结构化结果。这种协作模式在复杂的多层任务中特别有用——每个子 agent 的产出都是强类型的,主 agent 不再需要操心解析工作。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章