切换主题
第 5 章:结构化输出,让 Agent 别把表格写成散文
前几章的 Agent 已经能聊、能用工具、能记住上下文。但真实业务里,我们经常不只要一段回答,还要稳定字段:意图、紧急程度、联系人、订单号、评分、分类结果。模型可以自由发挥,系统不能自由崩。
本章边界
这一章讲“如何让模型尽量按结构输出,以及 Java 侧如何解析、校验、兜底”。请先记住一句话:outputType(...) 不是魔法按钮,它不会保证 call(...) 直接返回 Java record。
1. 生活类比:别让厨师把菜单写成散文
你去餐厅点套餐,后厨不能回你一篇《我与番茄炒蛋的故事》。收银系统需要的是:菜名、数量、价格、备注。
结构化输出也是这个道理:
| 餐厅系统 | Agent 概念 | 作用 |
|---|---|---|
| 点菜单模板 | outputType(...) / schema | 告诉模型应该有哪些字段 |
| 菜品备注 | prompt / instruction | 告诉模型缺失、异常时怎么处理 |
| 取餐口编号 | outputKey(...) | 告诉后续流程从哪里取结果 |
| 收银校验 | JSON 解析和字段校验 | 防止错误格式进入业务系统 |
2. 最小示例:抽取联系人
java
public record ContactInfo(String name, String email, String phone) {}
ReactAgent contactAgent = ReactAgent.builder()
.name("contact_agent")
.model(chatModel)
.instruction("从用户输入中抽取联系人信息。缺失字段用空字符串。")
.outputType(ContactInfo.class)
.outputKey("contact")
.build();
AssistantMessage answer = contactAgent.call("张三,邮箱 zhangsan@example.com,电话 13800000000");
String json = answer.getText();这里最容易误会的是最后两行。返回值仍然是 AssistantMessage,你拿到的通常还是文本。outputType(ContactInfo.class) 的价值,是帮助框架把格式要求告诉模型,让模型更可能输出 ContactInfo 形状的 JSON。
也就是说:
text
outputType 负责“提醒模型按格式答”
业务代码负责“解析、校验、兜底”3. 结构化输出的真实闭环
生产里不要把结构化输出写成“一行配置”。它应该是一个小闭环:
示例解析可以这样写:
java
ContactInfo parseContact(AssistantMessage answer, ObjectMapper mapper) {
try {
ContactInfo contact = mapper.readValue(answer.getText(), ContactInfo.class);
if (contact.email() == null || !contact.email().contains("@")) {
throw new IllegalArgumentException("email invalid");
}
return contact;
}
catch (Exception ex) {
throw new IllegalStateException("结构化输出解析失败,需要重试或转人工", ex);
}
}如果你的后续 Graph 节点要继续使用这个结果,可以用 outputKey(...) 把输出放到状态里。但要记住:状态里保存什么对象,取决于框架节点和你的解析逻辑,不要想当然地认为它已经是强类型业务对象。
4. 什么时候适合结构化输出
| 场景 | 是否适合 | 原因 |
|---|---|---|
| 意图分类 | 适合 | 结果集合比较稳定 |
| 联系人抽取 | 适合 | 字段清楚,便于校验 |
| 评分打标 | 适合 | 可以约束范围和格式 |
| 长篇解释 | 不一定 | 自然语言更灵活 |
| 创意写作 | 不适合强约束 | 结构会压掉表达空间 |
一句经验:需要进入数据库、流程分支、规则判断的结果,优先结构化;只是给人看的解释,可以保留自然语言。
5. 新手容易误解什么
误解一:outputType(...) 等于反序列化一定成功。
别把它当保险箱。它更像“答题纸模板”,模型仍然可能写歪。
误解二:所有输出都应该结构化。
不是。结构化输出适合抽取、分类、评分、路由;开放问答和解释型内容,不必硬塞进 JSON。
误解三:只要模型输出 JSON 就万事大吉。
JSON 语法正确不代表业务正确。邮箱格式、枚举范围、分数上下限、必填字段,都要校验。
6. 本章小结
结构化输出的工程心法:
text
先约束格式,再解析文本,再校验字段,最后决定重试还是兜底。下一章我们给 Agent 加检查站和切面:Hooks 与 Interceptors。
7. 练习题
- 定义
ProductReviewSummary,包含sentiment、score、keywords三个字段,让 Agent 从评论里抽取。 - 给
ContactInfo写校验规则:邮箱必须包含@,手机号为空时允许通过。 - 设计一个“解析失败后重试一次”的 prompt,要求模型只返回 JSON,不要解释。
课后源码索引:想验证实现时再打开
| 你想验证的结论 | 源码锚点 |
|---|---|
| 官方结构化输出示例 | examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/StructuredOutputExample.java,类 StructuredOutputExample,方法 basicJsonSchema()、complexNestedSchema()、outputTypeContactInfo()、outputTypeProductReview()、comprehensiveExample() |
| 模板渲染示例 | examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/AgentsExample.java,类 AgentsExample,方法 customTemplateRendererExample() |
| Builder 暴露的结构化配置 | spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java,类 Builder,方法 outputType(...)、outputSchema(...)、outputKey(...)、inputType(...)、includeContents(...) |
| outputType 如何生成格式说明 | spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java,类 DefaultBuilder,方法 build() |
| schema 和模板如何进入模型请求 | spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/node/AgentLlmNode.java,类 AgentLlmNode,方法 augmentUserMessage(...)、renderTemplatedUserMessage(...)、apply(...) |