【AgentScope Java新手村系列】(4)结构化输出
来源:互联网
时间:2026-06-12 07:26:41
第四章 结构化输出:用 JSON Schema 让 Agent 直接返回 Ja va POJO
在实际的 Agent 开发中,有个问题迟早会撞到你面前:Agent 输出的自然语言虽然“像人话”,但在系统对接时却需要一遍遍地做文本解析、正则匹配、字段提取。这不仅低效,还容易出错。做 Agent 开发的老手都知道,真正好用的方案是让 Agent 从一开始就输出结构化的数据,直接映射到你代码里的 Ja va 对象。4.1 为什么需要结构化输出
默认情况下,Agent 返回的是自然语言文本。但在很多场景下,我们需要 Agent 返回结构化的数据:
- 从文本中提取信息(姓名、邮箱、电话)
- 分类任务(情感分类、意图识别)
- 数据生成(产品描述、测试数据)
AgentScope Ja va 支持让 Agent 返回指定 Ja va 类型的数据。2.0 沿用 1.x 的 @StructuredOutput JSON Schema 机制,并在 Msg 上提供 getStructuredData(Class) 读取入口。这套方案的好处是——你只需要定义好 Ja va 类,剩下的框架帮你处理。
4.2 基本用法
**定义输出类型** 先来看一个典型的输出类型定义。假设我们要从用户描述中提取产品需求: ```ja va public static class ProductRequirements { public String productType; public String brand; public Integer minRam; public Double maxBudget; public Listpublic 修饰(或者提供 getter/setter,但 public 是捷径)
- 支持基本类型、String、List、嵌套对象
- 2.0 推荐在字段上加 com.fasterxml.jackson.annotation.JsonPropertyDescription,这样生成的 JSON Schema 描述更清晰——别小看这一行注解,LLM 的填充准确率能提升不少
**调用时指定类型**
定义好类之后,调用时只需要把 Class 对象传给 agent.call(...):
```ja va
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 的输出格式,最后反序列化成你想要的 Ja va 对象。整个过程对开发者来说几乎是透明的。
4.3 完整示例
下面给一个可运行的完整示例,使用 DeepSeek 模型做演示: ```ja va 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 ja va.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 List4.4 流式结构化输出
如果你需要更好的流式体验,可以使用streamEvents() 方法拿到结构化的输出事件——这在响应式架构里特别顺手:
```ja va
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;
Fluxagent.stream(msg, opts, type) 虽然仍可工作,但已经标注了 @Deprecated(forRemoval = true),新代码建议直接用 streamEvents(...)。
4.5 支持的字段类型
框架支持的字段类型还挺全面,基本上覆盖了日常开发需要的: | Ja va 类型 | JSON Schema 类型 | |-----------|-----------------| |String | string |
| Integer, int | integer |
| Double, double, Float, float | number |
| Boolean, boolean | boolean |
| List | array |
| Map | object |
| 嵌套对象 | object |
| Ja va record | object(2.0 起官方推荐用 record,更简洁) |
**4.5.1 用 record 简化定义**
如果你用 Ja va 17 开发,推荐直接用 record 来定义输出类型——零样板代码,字段自带 getter:
```ja va
public record ProductRequirements(
@JsonPropertyDescription("产品类型,例如 laptop / phone / tablet")
String productType,
@JsonPropertyDescription("品牌,例如 Apple / Dell / Lenovo")
String brand,
@JsonPropertyDescription("最小内存,单位 GB")
Integer minRam,
@JsonPropertyDescription("最高预算,单位美元")
Double maxBudget,
@JsonPropertyDescription("用户提到的特性关键词列表")
List@JsonPropertyDescription 之后,生成的 JSON Schema 描述会带上字段说明——LLM 填充时知道每个字段的“业务含义”,准确率提升非常明显。
**4.5.2 嵌套对象示例**
遇到复杂数据结构也不需要慌,直接嵌套对象即可:
```ja va
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 List4.6 工作原理
把幕后流程简单梳理一下,当你传入一个Class 对象时,框架会做这几件事:
1. 使用 jsonschema-generator 根据 Ja va 类生成 JSON Schema(@JsonPropertyDescription / @JsonProperty 都会反映到 schema 上)
2. 将 JSON Schema 作为约束发送给 LLM(通过 response_format 参数或 system prompt 注入)
3. LLM 按照 Schema 格式输出 JSON
4. 框架将 JSON 反序列化为 Ja va 对象
5. 将对象放入 Msg 的结构化数据字段(msg.getStructuredData(Class) 读取)
整个过程对用户是透明的,你只需要定义 Ja va 类即可。
**4.6.1 模型兼容性说明**
关于模型兼容性,需要留意一件事:结构化输出有两种实现方式,它们对模型的要求不同:
| 方式 | 原理 | 模型兼容性 |
|------|------|-----------|
| API 参数约束(agent.call(msg, SomeClass.class, rt)) | 框架向 API 发送 response_format 参数,强制服务器校验输出为 JSON | 仅 OpenAI 等部分模型支持 |
| 提示词驱动(本章示例的做法) | 在 UserMessage 中写“请输出 JSON 格式:{...}”,让 LLM 按格式输出 | 所有模型都支持 |
本章的完整示例采用提示词驱动方式,因此不挑模型——DeepSeek、通义千问等均可正常使用。
如果你误用了方式一,不支持的模型会报:"This response_format type is una vailable now",此时换成本章示例的提示词驱动方式即可。
4.7 最佳实践
写到最后,整理几条实际项目里积累下来的经验: - **字段名要有意义**:LLM 会根据字段名理解应该填什么内容,比如productType 就比 type 更明确
- **类型要选对**:数字用 Integer / Double,不要为了省事全都用 String
- **多值字段用 List**:如果一个字段可能有多个值,用 List 而非 String
- **加 @JsonPropertyDescription**:为每个字段写一句话业务描述,这是成本最低的准确率提升手段
- **系统提示词配合**:在 sysPrompt 中说明输出要求,能让 LLM 少“猜”很多
- **处理异常**:LLM 的输出不一定每次都符合预期,养成 try-catch 的习惯:
```ja va
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 自动拿到强类型数据:
```ja va
// 主 agent 调用子 agent,子 agent 内部 call(..., Report.class, ctx) 返回 Report
// 主 agent 拿到的 tool_result 是 Report 的 JSON 序列化
// 主 agent 的下一轮推理基于这份结构化结果继续
```
在 workspace/subagents/researcher.md 里,可以显式说明子 agent 的输出 schema(用自然语言描述即可),这样主 agent 就能稳定地消费子 agent 返回的结构化结果。这种协作模式在复杂的多层任务中特别有用——每个子 agent 的产出都是强类型的,主 agent 不用再操心解析。