第20章 结构化输出
在许多应用场景中,我们需要Agent返回结构化的数据而非自由文本。AgentScope-Java提供了强大的结构化输出功能,让Agent能够生成符合指定Schema的JSON数据。本章将详细介绍结构化输出的使用方法。
20.1 结构化输出概述
20.1.1 什么是结构化输出
┌─────────────────────────────────────────────────────────────────┐
│ 自由文本 vs 结构化输出 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 自由文本输出: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 用户: "分析这条评论的情感" │ │
│ │ Agent: "这条评论整体是积极的。作者表达了对产品质量 │ │
│ │ 的满意,但也提到了配送时间较长的问题..." │ │
│ │ │ │
│ │ → 难以程序化处理 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 结构化输出: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 用户: "分析这条评论的情感" │ │
│ │ Agent: { │ │
│ │ "sentiment": "positive", │ │
│ │ "positiveScore": 0.75, │ │
│ │ "negativeScore": 0.15, │ │
│ │ "topics": ["product_quality", "shipping"], │ │
│ │ "summary": "用户对产品满意但对配送有不满" │ │
│ │ } │ │
│ │ → 易于程序处理、存储和分析 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
20.1.2 应用场景
| 场景 | 说明 | 示例Schema |
|---|---|---|
| 信息提取 | 从非结构化文本中提取结构化信息 | ContactInfo, ProductInfo |
| 数据分类 | 将输入分类并返回类别标签 | Category, Label |
| 情感分析 | 分析文本情感并返回评分 | SentimentResult |
| 决策输出 | 返回决策结果和理由 | Decision, Recommendation |
| 表单填充 | 自动填充表单字段 | FormData, ApplicationForm |
20.1.3 实现机制
AgentScope-Java通过generate_response工具实现结构化输出:
┌─────────────────────────────────────────────────────────────────┐
│ 结构化输出工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户调用 agent.call(msg, SchemaClass.class) │
│ ↓ │
│ 2. 框架生成 generate_response 工具(基于Schema) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ @Tool(name = "generate_response") │ │
│ │ 参数: 根据SchemaClass的字段动态生成 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 3. Agent推理并调用 generate_response 工具 │
│ ↓ │
│ 4. StructuredOutputHook 拦截结果 │
│ - 验证输出格式 │
│ - 如果未调用工具,添加提示重试 │
│ - 压缩中间消息 │
│ ↓ │
│ 5. 返回包含结构化数据的 Msg │
│ msg.getStructuredData(SchemaClass.class) │
│ │
└─────────────────────────────────────────────────────────────────┘
20.2 基本使用
20.2.1 定义Schema类
/**
* 产品需求Schema
* 用于从用户描述中提取产品需求
*/
public class ProductRequirements {
public String productType; // 产品类型
public String brand; // 品牌偏好
public Integer minRam; // 最低内存(GB)
public Double maxBudget; // 预算上限
public List<String> features; // 需要的特性
// 必须有无参构造器用于反序列化
public ProductRequirements() {}
}
/**
* 联系人信息Schema
*/
public class ContactInfo {
public String name; // 姓名
public String email; // 邮箱
public String phone; // 电话
public String company; // 公司
public ContactInfo() {}
}
/**
* 情感分析结果Schema
*/
public class SentimentAnalysis {
public String overallSentiment; // "positive", "negative", "neutral"
public Double positiveScore; // 0.0 - 1.0
public Double negativeScore; // 0.0 - 1.0
public Double neutralScore; // 0.0 - 1.0
public List<String> keyTopics; // 关键主题
public String summary; // 总结
public SentimentAnalysis() {}
}
20.2.2 调用结构化输出
// 创建Agent
ReActAgent agent = ReActAgent.builder()
.name("AnalysisAgent")
.model(OpenAIChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build())
.sysPrompt("你是智能分析助手,根据用户需求提取结构化信息。")
.build();
// 用户查询
String query = "我想买一台笔记本电脑,要求至少16GB内存," +
"最好是苹果品牌,预算2万元左右,需要轻便便于出差";
Msg userMsg = Msg.user("从以下描述中提取产品需求:" + query);
// 调用结构化输出(阻塞式)
Msg response = agent.call(userMsg, ProductRequirements.class).block();
// 获取结构化数据
ProductRequirements result = response.getStructuredData(ProductRequirements.class);
// 使用提取的数据
System.out.println("产品类型: " + result.productType);
System.out.println("品牌偏好: " + result.brand);
System.out.println("最低内存: " + result.minRam + "GB");
System.out.println("预算上限: " + result.maxBudget);
System.out.println("需要特性: " + result.features);
20.2.3 流式结构化输出
import io.agentscope.core.agent.Event;
import io.agentscope.core.agent.StreamOptions;
import reactor.core.publisher.Flux;
// 流式调用结构化输出
Flux<Event> eventFlux = agent.stream(
userMsg,
StreamOptions.defaults(),
ProductRequirements.class
);
// 处理事件流
eventFlux.subscribe(event -> {
switch (event.getType()) {
case REASONING_CHUNK -> {
// 处理推理过程中的文本块
System.out.print(event.getChunk());
}
case TOOL_USE -> {
// 工具调用事件
System.out.println("\n[调用工具: " + event.getToolName() + "]");
}
case COMPLETE -> {
// 完成事件,获取结构化结果
ProductRequirements result = event.getMessage()
.getStructuredData(ProductRequirements.class);
System.out.println("\n提取结果: " + result);
}
}
});
// 或者直接获取最终结果
Event lastEvent = agent.stream(
userMsg,
StreamOptions.defaults(),
ProductRequirements.class
).blockLast();
ProductRequirements result = lastEvent.getMessage()
.getStructuredData(ProductRequirements.class);
20.3 Schema设计
20.3.1 支持的数据类型
| Java类型 | JSON类型 | 说明 |
|---|---|---|
| String | string | 文本 |
| Integer, int | integer | 整数 |
| Long, long | integer | 长整数 |
| Double, double | number | 浮点数 |
| Float, float | number | 单精度浮点 |
| Boolean, boolean | boolean | 布尔值 |
| List | array | 数组/列表 |
| Map | object | 键值对映射 |
| 自定义类 | object | 嵌套对象 |
| Enum | string | 枚举值 |
20.3.2 使用注解增强Schema
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/**
* 使用Jackson注解增强Schema描述
*/
public class OrderAnalysis {
@JsonProperty("order_id")
@JsonPropertyDescription("订单唯一标识符")
public String orderId;
@JsonProperty("total_amount")
@JsonPropertyDescription("订单总金额,单位为人民币元")
public Double totalAmount;
@JsonProperty("order_status")
@JsonPropertyDescription("订单状态: pending, processing, shipped, delivered, cancelled")
public OrderStatus status;
@JsonProperty("items")
@JsonPropertyDescription("订单包含的商品列表")
public List<OrderItem> items;
@JsonProperty("risk_level")
@JsonPropertyDescription("风险等级: low, medium, high")
public RiskLevel riskLevel;
public OrderAnalysis() {}
}
public class OrderItem {
@JsonPropertyDescription("商品名称")
public String name;
@JsonPropertyDescription("商品数量")
public Integer quantity;
@JsonPropertyDescription("商品单价")
public Double unitPrice;
public OrderItem() {}
}
public enum OrderStatus {
@JsonProperty("pending") PENDING,
@JsonProperty("processing") PROCESSING,
@JsonProperty("shipped") SHIPPED,
@JsonProperty("delivered") DELIVERED,
@JsonProperty("cancelled") CANCELLED
}
public enum RiskLevel {
@JsonProperty("low") LOW,
@JsonProperty("medium") MEDIUM,
@JsonProperty("high") HIGH
}
20.3.3 嵌套和复杂结构
/**
* 复杂嵌套结构示例
*/
public class DocumentAnalysis {
@JsonPropertyDescription("文档标题")
public String title;
@JsonPropertyDescription("文档类型: report, article, email, contract")
public String documentType;
@JsonPropertyDescription("文档元数据")
public DocumentMetadata metadata;
@JsonPropertyDescription("提取的实体列表")
public List<Entity> entities;
@JsonPropertyDescription("文档摘要")
public Summary summary;
public DocumentAnalysis() {}
}
public class DocumentMetadata {
public String author;
public String createdDate;
public String language;
public Integer wordCount;
public List<String> keywords;
public DocumentMetadata() {}
}
public class Entity {
public String text; // 实体文本
public String type; // 实体类型: person, organization, location, date
public Double confidence; // 置信度 0.0-1.0
public Integer startOffset; // 起始位置
public Integer endOffset; // 结束位置
public Entity() {}
}
public class Summary {
public String shortSummary; // 一句话摘要
public String detailedSummary; // 详细摘要
public List<String> keyPoints; // 关键点列表
public Summary() {}
}
20.4 StructuredOutputHook详解
20.4.1 工作机制
/**
* StructuredOutputHook - 结构化输出处理Hook
*
* 核心功能:
* 1. PreReasoning: 在TOOL_CHOICE模式下强制选择generate_response工具
* 2. PostReasoning: 检查是否调用了工具,未调用则添加提示重试
* 3. PostActing: 检测generate_response完成,停止Agent
* 4. PostCall: 压缩内存,移除中间消息
*/
public class StructuredOutputHook implements Hook {
public static final String TOOL_NAME = "generate_response";
private static final int MAX_RETRIES = 3;
private final StructuredOutputReminder reminderMode;
private final GenerateOptions baseOptions;
private final Memory memory;
private boolean completed = false;
private Msg resultMsg = null;
private int retryCount = 0;
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PreReasoningEvent e) {
handlePreReasoning(e);
} else if (event instanceof PostReasoningEvent e) {
handlePostReasoning(e);
} else if (event instanceof PostActingEvent e) {
handlePostActing(e);
} else if (event instanceof PostCallEvent e) {
handlePostCall(e);
}
return Mono.just(event);
}
private void handlePostReasoning(PostReasoningEvent event) {
Msg msg = event.getReasoningMessage();
if (msg == null) return;
// 检查是否调用了工具
boolean hasCall = !msg.getContentBlocks(ToolUseBlock.class).isEmpty();
if (!hasCall && retryCount < MAX_RETRIES) {
retryCount++;
// 添加提示消息重新推理
event.gotoReasoning(createReminderMessage(reminderMode));
}
}
private void handlePostActing(PostActingEvent event) {
ToolUseBlock toolUse = event.getToolUse();
if (toolUse != null && TOOL_NAME.equals(toolUse.getName())) {
ToolResultBlock result = event.getToolResult();
if (result != null && isSuccess(result)) {
completed = true;
resultMsg = event.getToolResultMsg();
event.stopAgent(); // 停止Agent执行
}
}
}
@Override
public int priority() {
return 50; // 较高优先级
}
}
20.4.2 提示模式
/**
* 结构化输出提示模式
*/
public enum StructuredOutputReminder {
/**
* PROMPT模式:通过文本提示引导模型调用工具
* 适用于大多数模型
*/
PROMPT,
/**
* TOOL_CHOICE模式:强制模型选择特定工具
* 适用于支持tool_choice参数的模型
*/
TOOL_CHOICE
}
// 配置提示模式
ReActAgent agent = ReActAgent.builder()
.name("Agent")
.model(model)
.structuredOutputReminder(StructuredOutputReminder.TOOL_CHOICE)
.build();
20.5 实际应用示例
20.5.1 信息提取系统
/**
* 简历信息提取
*/
public class ResumeExtractor {
private final ReActAgent agent;
public ResumeExtractor(Model model) {
this.agent = ReActAgent.builder()
.name("ResumeParser")
.model(model)
.sysPrompt("你是专业的简历分析师,从简历文本中提取结构化信息。")
.build();
}
public ResumeInfo extract(String resumeText) {
Msg msg = Msg.user("请从以下简历中提取信息:\n\n" + resumeText);
Msg response = agent.call(msg, ResumeInfo.class).block();
return response.getStructuredData(ResumeInfo.class);
}
// Schema定义
public static class ResumeInfo {
public PersonalInfo personalInfo;
public List<Education> educations;
public List<WorkExperience> workExperiences;
public List<String> skills;
public List<String> certifications;
public String summary;
}
public static class PersonalInfo {
public String name;
public String email;
public String phone;
public String location;
public String linkedIn;
}
public static class Education {
public String school;
public String degree;
public String major;
public String startDate;
public String endDate;
public Double gpa;
}
public static class WorkExperience {
public String company;
public String title;
public String startDate;
public String endDate;
public List<String> responsibilities;
public List<String> achievements;
}
}
// 使用
ResumeExtractor extractor = new ResumeExtractor(model);
ResumeInfo info = extractor.extract(resumeText);
System.out.println("姓名: " + info.personalInfo.name);
System.out.println("技能: " + String.join(", ", info.skills));
for (WorkExperience exp : info.workExperiences) {
System.out.println("工作经历: " + exp.company + " - " + exp.title);
}
20.5.2 智能分类系统
/**
* 客户工单分类
*/
public class TicketClassifier {
private final ReActAgent agent;
public TicketClassifier(Model model) {
this.agent = ReActAgent.builder()
.name("TicketClassifier")
.model(model)
.sysPrompt("""
你是客户服务工单分类专家。
分析工单内容,判断类别、优先级和情感。
""")
.build();
}
public TicketClassification classify(String ticketContent) {
Msg msg = Msg.user("请分类以下工单:\n\n" + ticketContent);
return agent.call(msg, TicketClassification.class)
.block()
.getStructuredData(TicketClassification.class);
}
public static class TicketClassification {
@JsonPropertyDescription("主要类别: billing, technical, account, shipping, other")
public String category;
@JsonPropertyDescription("子类别,更具体的分类")
public String subcategory;
@JsonPropertyDescription("优先级: P1(紧急), P2(高), P3(中), P4(低)")
public String priority;
@JsonPropertyDescription("客户情感: angry, frustrated, neutral, satisfied")
public String sentiment;
@JsonPropertyDescription("是否需要人工处理")
public Boolean requiresHumanAgent;
@JsonPropertyDescription("推荐的处理流程")
public String recommendedAction;
@JsonPropertyDescription("提取的关键实体")
public List<String> entities;
@JsonPropertyDescription("置信度 0.0-1.0")
public Double confidence;
}
}
// 使用
TicketClassifier classifier = new TicketClassifier(model);
String ticket = "我付了款但是订单状态还是未支付,已经过了3天了,太气人了!";
TicketClassification result = classifier.classify(ticket);
System.out.println("类别: " + result.category);
System.out.println("优先级: " + result.priority);
System.out.println("情感: " + result.sentiment);
System.out.println("需要人工: " + result.requiresHumanAgent);
20.5.3 决策支持系统
/**
* 贷款申请评估
*/
public class LoanEvaluator {
private final ReActAgent agent;
public LoanEvaluator(Model model) {
this.agent = ReActAgent.builder()
.name("LoanEvaluator")
.model(model)
.sysPrompt("""
你是贷款风险评估专家。
根据申请人信息评估贷款风险并给出建议。
注意:这只是初步筛选,最终决策需要人工审批。
""")
.build();
}
public LoanEvaluation evaluate(LoanApplication application) {
String prompt = String.format("""
请评估以下贷款申请:
申请金额: %s 元
申请人年收入: %s 元
信用评分: %s
负债率: %s%%
工作年限: %s 年
房产状况: %s
""",
application.amount,
application.annualIncome,
application.creditScore,
application.debtRatio,
application.yearsEmployed,
application.propertyStatus
);
return agent.call(Msg.user(prompt), LoanEvaluation.class)
.block()
.getStructuredData(LoanEvaluation.class);
}
public static class LoanApplication {
public Double amount;
public Double annualIncome;
public Integer creditScore;
public Double debtRatio;
public Integer yearsEmployed;
public String propertyStatus;
}
public static class LoanEvaluation {
@JsonPropertyDescription("风险等级: low, medium, high, very_high")
public String riskLevel;
@JsonPropertyDescription("建议决策: approve, conditional_approve, manual_review, reject")
public String recommendation;
@JsonPropertyDescription("风险评分 0-100,越低越好")
public Integer riskScore;
@JsonPropertyDescription("正面因素列表")
public List<String> positiveFactors;
@JsonPropertyDescription("负面因素列表")
public List<String> negativeFactors;
@JsonPropertyDescription("如果批准,建议的最高额度")
public Double suggestedMaxAmount;
@JsonPropertyDescription("如果条件批准,需要满足的条件")
public List<String> conditions;
@JsonPropertyDescription("详细评估说明")
public String explanation;
}
}
20.6 错误处理
20.6.1 常见错误及处理
// 结构化输出可能的异常情况
try {
Msg response = agent.call(userMsg, MySchema.class).block();
MySchema result = response.getStructuredData(MySchema.class);
// 使用结果
processResult(result);
} catch (StructuredOutputException e) {
// 结构化输出失败
System.err.println("无法生成结构化输出: " + e.getMessage());
// 获取原始响应文本作为fallback
String rawText = e.getRawResponse();
handleFallback(rawText);
} catch (JsonProcessingException e) {
// JSON解析失败
System.err.println("JSON解析失败: " + e.getMessage());
} catch (Exception e) {
// 其他错误
System.err.println("发生错误: " + e.getMessage());
}
20.6.2 验证结构化输出
/**
* 带验证的结构化输出处理
*/
public class ValidatedExtractor<T> {
private final ReActAgent agent;
private final Class<T> schemaClass;
private final Validator<T> validator;
@FunctionalInterface
public interface Validator<T> {
ValidationResult validate(T data);
}
public record ValidationResult(boolean valid, List<String> errors) {}
public T extract(String input) {
Msg response = agent.call(Msg.user(input), schemaClass).block();
T result = response.getStructuredData(schemaClass);
// 验证结果
ValidationResult validation = validator.validate(result);
if (!validation.valid()) {
throw new ValidationException(
"结构化输出验证失败: " + String.join(", ", validation.errors())
);
}
return result;
}
}
// 使用示例
ValidatedExtractor<ContactInfo> extractor = new ValidatedExtractor<>(
agent,
ContactInfo.class,
info -> {
List<String> errors = new ArrayList<>();
if (info.email != null && !info.email.contains("@")) {
errors.add("邮箱格式无效");
}
if (info.phone != null && info.phone.length() < 10) {
errors.add("电话号码格式无效");
}
return new ValidationResult(errors.isEmpty(), errors);
}
);
20.7 性能优化
20.7.1 Schema缓存
/**
* Schema缓存管理器
*/
public class SchemaCache {
private static final Map<Class<?>, String> schemaCache = new ConcurrentHashMap<>();
/**
* 获取或生成Schema JSON
*/
public static String getSchema(Class<?> clazz) {
return schemaCache.computeIfAbsent(clazz, SchemaCache::generateSchema);
}
private static String generateSchema(Class<?> clazz) {
ObjectMapper mapper = new ObjectMapper();
JsonSchemaFactory factory = JsonSchemaFactory.getInstance();
try {
JsonSchema schema = factory.generateSchema(clazz);
return mapper.writeValueAsString(schema);
} catch (Exception e) {
throw new RuntimeException("Failed to generate schema", e);
}
}
}
20.7.2 批量处理
/**
* 批量结构化输出处理
*/
public class BatchExtractor<T> {
private final ReActAgent agent;
private final Class<T> schemaClass;
private final int batchSize;
public List<T> extractBatch(List<String> inputs) {
return Flux.fromIterable(inputs)
.buffer(batchSize)
.flatMap(batch -> processBatch(batch))
.collectList()
.block();
}
private Flux<T> processBatch(List<String> batch) {
return Flux.fromIterable(batch)
.flatMap(input -> agent.call(Msg.user(input), schemaClass)
.map(msg -> msg.getStructuredData(schemaClass))
.onErrorResume(e -> {
log.warn("提取失败: {}", e.getMessage());
return Mono.empty();
}));
}
}
20.8 最佳实践
20.8.1 Schema设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 字段命名清晰 | 使用描述性名称 | orderStatus vs status |
| 提供描述 | 使用@JsonPropertyDescription | 说明字段含义和取值范围 |
| 适当使用枚举 | 限定值域的字段用枚举 | OrderStatus, Priority |
| 避免过度嵌套 | 层级不超过3-4层 | 适当扁平化结构 |
| 合理设置可选 | 非必需字段标记为可选 | 使用包装类型如Integer |
20.8.2 提示词优化
// 好的提示词示例
String prompt = """
请从以下文本中提取联系人信息。
提取规则:
- 姓名:完整的人名
- 邮箱:有效的电子邮件地址
- 电话:包含区号的完整电话号码
- 公司:公司或组织的名称
如果某个字段无法确定,请填写null。
文本内容:
%s
""".formatted(inputText);
// 避免模糊的提示词
// 不好: "分析这段文字"
// 好: "从以下客户反馈中提取情感倾向和关键问题点"
20.8.3 处理不确定性
/**
* 带置信度的结构化输出
*/
public class ExtractedEntity {
public String value;
@JsonPropertyDescription("提取的置信度 0.0-1.0")
public Double confidence;
@JsonPropertyDescription("提取来源:extracted(提取), inferred(推断), default(默认)")
public String source;
}
public class RobustExtraction {
public String primaryValue;
public List<String> alternatives; // 备选值
public Double confidence;
public String reasoning; // 推理过程
}
20.9 本章小结
本章详细介绍了AgentScope-Java的结构化输出功能:
- 基本使用:通过
agent.call(msg, SchemaClass.class)获取结构化输出 - Schema设计:使用Java类定义输出结构,支持Jackson注解增强
- StructuredOutputHook:通过Hook机制确保模型调用
generate_response工具 - 实际应用:信息提取、分类系统、决策支持等场景
- 错误处理:处理解析失败和验证输出
- 性能优化:Schema缓存和批量处理
结构化输出使Agent的响应可预测、可解析,是构建生产级AI应用的重要能力。