第四章 结构化输出:用 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 对象时,框架会:
- 使用
jsonschema-generator根据 Java 类生成 JSON Schema(@JsonPropertyDescription/@JsonProperty都会反映到 schema 上) - 将 JSON Schema 作为约束发送给 LLM(通过
response_format参数或 system prompt 注入) - LLM 按照 Schema 格式输出 JSON
- 框架将 JSON 反序列化为 Java 对象
- 将对象放入
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 最佳实践
- 字段名要有意义:LLM 会根据字段名理解应该填什么内容
- 使用合适的类型:数字用
Integer/Double,不要都用String - List 用于多值字段:当一个字段可能有多个值时,使用
List - 加
@JsonPropertyDescription:为每个字段写一句话业务描述,准确率提升明显 - 系统提示词配合:在
sysPrompt中说明输出要求,提高准确率 - 处理异常: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 就能稳定地消费。