第20章 结构化输出

第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类型说明
Stringstring文本
Integer, intinteger整数
Long, longinteger长整数
Double, doublenumber浮点数
Float, floatnumber单精度浮点
Boolean, booleanboolean布尔值
Listarray数组/列表
Mapobject键值对映射
自定义类object嵌套对象
Enumstring枚举值

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的结构化输出功能:

  1. 基本使用:通过agent.call(msg, SchemaClass.class)获取结构化输出
  2. Schema设计:使用Java类定义输出结构,支持Jackson注解增强
  3. StructuredOutputHook:通过Hook机制确保模型调用generate_response工具
  4. 实际应用:信息提取、分类系统、决策支持等场景
  5. 错误处理:处理解析失败和验证输出
  6. 性能优化:Schema缓存和批量处理

结构化输出使Agent的响应可预测、可解析,是构建生产级AI应用的重要能力。

← 返回目录