Agent 返回 JSON 而不是闲聊:LangChain4j 结构化输出实战

Agent 返回 "CNC-001 温度有点高,建议检查一下"------前端怎么解析?用 POJO 替代 String,让 Agent 返回类型安全的 Java 对象。


问题:String 返回值的困境

默认情况下,Agent 返回的是 String------自然语言,LLM 自由发挥:

java 复制代码
interface IndustrialAssistant {
    String chat(String message);
}

调用结果:

arduino 复制代码
"根据查询结果,CNC-001 设备当前振动值为 4.8mm/s,超出正常范围 0-2.8mm/s,
轴承温度 72°C 也高于正常上限 65°C。初步判断为轴承磨损导致的振动异常,
建议立即安排检修,优先级 HIGH。"

前端要从中提取「设备 ID」「异常指标」「建议操作」「优先级」------全靠正则或二次 LLM 调用。不可靠,也不优雅。


方案:让 Agent 返回 POJO

LangChain4j 支持方法的返回类型直接定义为 Java POJO。框架会把 LLM 的输出自动映射到 POJO 字段。

Step 1:定义输出模型

java 复制代码
@Data
public class DiagnosticResponse {

    @Description("设备ID")
    private String deviceId;

    @Description("设备当前状态:normal/warning/critical")
    private String status;

    @Description("诊断分析结论")
    private String analysis;

    @Description("可能的故障原因,按可能性从高到低排列")
    private List<String> possibleCauses;

    @Description("建议的维修或处理措施")
    private List<String> suggestedActions;

    @Description("优先级:HIGH/MEDIUM/LOW")
    private String priority;

    @Description("是否需要立即处理")
    private Boolean requiresImmediateAction;

    @Description("诊断置信度,0.0-1.0")
    private Double confidence;
}

@Description 注解是核心。 它告诉 LLM 每个字段的含义和取值范围,LLM 会根据这些描述决定填什么值。

Step 2:定义接口

java 复制代码
interface DiagnosticAssistant {
    @SystemMessage("""
            你是一个工业设备故障诊断专家。你必须使用提供的工具查询设备数据,
            然后基于工具返回的结构化数据进行诊断分析,不要凭猜测回答。
            """)
    @UserMessage("""
            请对设备 {{deviceId}} 进行全面的故障诊断分析:
            1. 查询设备告警信息
            2. 查询设备历史遥测数据
            3. 基于以上数据生成诊断报告
            """)
    DiagnosticResponse diagnose(@V("deviceId") String deviceId);
}

注意返回类型从 String 变成了 DiagnosticResponse。@UserMessage 里的 {{deviceId}} 是模板占位符,@V("deviceId") 绑定方法参数。

Step 3:调用

java 复制代码
DiagnosticResponse result = deviceAgent.diagnose("CNC-001");
// result.getDeviceId()        → "CNC-001"
// result.getStatus()          → "warning"
// result.getPriority()        → "HIGH"
// result.getPossibleCauses()  → ["轴承磨损", "转子不平衡"]

前端直接反序列化,不需要做任何字段提取。API 契约由 POJO 定义------字段类型(String/Boolean/Double/List)本身就是文档。


String vs POJO 对比

维度 String 返回 POJO 返回
前端解析 正则/手工提取 直接反序列化
类型安全 无,全靠约定 编译期校验
API 文档 注释靠人维护 POJO 即文档
灵活性 高,LLM 自由发挥 受字段约束
LLM 填充质量 --- 依赖 @Description 质量
适用场景 聊天式交互、调试 业务接口、结构化诊断

实际效果对比

同一个诊断请求「CNC-001 振动异常是什么原因?」:

String 返回:

erlang 复制代码
根据查询结果,CNC-001 设备当前振动值为 4.8mm/s,超出正常范围...
初步判断为轴承磨损导致的振动异常,建议立即安排检修...

前端要从中提取结构化信息------几乎不可能可靠地做到。

POJO 返回(JSON 序列化后):

json 复制代码
{
  "deviceId": "CNC-001",
  "status": "warning",
  "analysis": "振动值4.8mm/s超出正常范围0-2.8mm/s,轴承温度72°C高于正常上限65°C,判断为轴承磨损导致的振动异常",
  "possibleCauses": ["轴承磨损", "转子不平衡"],
  "suggestedActions": ["立即安排轴承检查", "校验联轴器对中参数", "生成维修工单"],
  "priority": "HIGH",
  "requiresImmediateAction": true,
  "confidence": 0.85
}

前端可以直接用 result.priority 决定 UI 上的告警颜色,用 result.suggestedActions 渲染操作按钮列表。


@Description 的写法技巧

@Description 是 LLM 理解字段含义的唯一依据。写好它直接影响 POJO 的填充质量:

java 复制代码
// 差 --- LLM 不知道取值范围
@Description("状态")
private String status;

// 好 --- 明确取值范围
@Description("设备当前状态:normal/warning/critical")
private String status;

// 差 --- 太笼统
@Description("数据列表")
private List<String> data;

// 好 --- 说明内容和排序
@Description("可能的故障原因,按可能性从高到低排列")
private List<String> possibleCauses;

// 差 --- 没有数值范围
@Description("置信度")
private Double confidence;

// 好 --- 有范围
@Description("诊断置信度,0.0-1.0")
private Double confidence;

经验规则@Description 里包含三个信息------字段含义、值类型(枚举/范围)、排序规则(如有)。


什么时候用 POJO,什么时候用 String?

场景 推荐 原因
工单生成 POJO 工单号、设备ID、优先级、操作步骤------全是结构化字段
告警诊断 POJO 诊断结论 + 建议操作 + 置信度,下游系统需要消费
报表查询 POJO 字段固定,前端需要一个确定的 JSON 结构
日常闲聊 String 不需要结构化,让 LLM 自由发挥
调试/探索 String POJO 字段设计还没定型,先跑通流程再建模
多模态输出 String 如果需要混合文字+图片+表格,String 更灵活

一个实用策略:同时保留两个接口

java 复制代码
interface IndustrialAssistant {
    String chat(String message);           // 自由对话
    DiagnosticResponse diagnose(...);      // 结构化诊断
}

同一个 Agent 可以同时暴露多个方法------每个方法对应一种输出模式。其他开发团队按需调用。


一句话总结

让 Agent 返回 POJO,不是"限制 LLM 的自由",而是把 LLM 的输出变成下游系统可消费的数据。String 是人看的,POJO 是代码读的。工业 Agent 的输出,大部分应该被代码消费。


本文由 LaoLiang 原创,首发于掘金/知乎/微信公众号。转载请联系作者。

相关推荐
MetrixAeroCore2 小时前
中东物联网卡行业适配指南|地域组网差异与合规落地解析(MetrixAeroCore)
物联网
砍材农夫2 小时前
物联网实战:Spring Boot + Netty 搭建 MQTT 统一接入层
java·网络·spring boot·后端·物联网·spring
国产化创客2 小时前
嵌入式视觉完整技术体系--ESP32/K230/RDK-X5/树莓派四层架构全解析
嵌入式硬件·物联网·架构·开源·智能硬件
打小就很皮...2 小时前
基于 Python + LangChain + React 实现智能发票识别与验真系统实战
前端·react.js·langchain·ocr·发票识别
颜酱15 小时前
让 Agent 不再失忆:LangChain 短期记忆实战
langchain·agent
装不满的克莱因瓶18 小时前
了解 LangChain 中的 LLM 与 ChatModel 的差异
人工智能·python·ai·langchain·llm·agent·chatmodel
颜酱18 小时前
LangChain 工具调用:从原理、入门到落地
langchain·llm
CSDN官方博客18 小时前
「谁说嵌入式只是调包和焊板子?」—— 2026嵌入式全栈技术征锋令
嵌入式硬件·物联网·embedding
swipe19 小时前
别再把关系库和向量库拆开了:PostgreSQL 搭建 AI 长期记忆层实战
面试·langchain·llm