1. 从闲聊到精准数据:为什么我们需要结构化输出?

最近在折腾几个AI应用的原型,发现一个挺普遍的问题:当你让大模型帮你处理一些稍微复杂点的任务时,它给你的回复,十有八九是一段“人类友好”的自然语言。比如,你问它:“帮我分析一下这份合同里的关键条款,包括甲方、乙方、合同金额和截止日期。” 它可能会给你一段非常流畅、甚至带点格式的文本回复:“好的,已为您分析。这份合同的甲方是XX公司,乙方是YY公司,合同总金额为100万元人民币,约定的项目截止日期是2024年12月31日。请注意,其中还涉及了保密条款和违约责任……”

这段回复读起来很舒服,对吧?但如果你是一个开发者,想把这段信息塞进你自己的数据库里,或者传递给下一个自动化流程(比如自动生成付款提醒、更新项目看板),你就傻眼了。你得写一堆复杂的正则表达式,或者用更高级的NLP模型去“猜”和“抠”出“XX公司”、“100万元”、“2024-12-31”这些关键信息。这个过程不仅繁琐、容易出错,而且极度脆弱——模型换个说法,你的解析逻辑可能就崩了。

这就是“闲聊式”输出的痛点:它适合人看,不适合机器用。而在企业级应用、自动化工作流(Agent)中,我们需要的是机器可读、结构清晰、格式固定的数据。 JSON(JavaScript Object Notation) 正是解决这个问题的银弹。它是一种轻量级的数据交换格式,简单、清晰、被几乎所有编程语言原生支持。如果我们能让AI模型直接返回JSON,比如 {“partyA”: “XX公司”, “partyB”: “YY公司”, “amount”: 1000000, “currency”: “CNY”, “deadline”: “2024-12-31”} ,那么后续的处理就变成了简单的JSON解析,稳定、高效、零歧义。

所以, 结构化输出 的核心价值,就是 在AI的“灵活性”与程序的“确定性”之间架起一座桥梁 。它让大模型的强大理解与生成能力,能够以标准化的方式无缝嵌入到现有的、依赖确定数据格式的软件系统中。而 LangChain4j ,作为Java生态中连接大模型与应用的明星框架,提供了一套优雅且强大的工具来实现这一点。今天,我们就来深入聊聊,如何利用LangChain4j,让你的Agent不再“闲聊”,而是精准地“吐”出你想要的JSON数据。

2. LangChain4j 结构化输出核心: StructuredPrompt OutputParser

在LangChain4j中,实现结构化输出的核心思想是“约定大于配置”。它通过两个关键组件协同工作: StructuredPrompt (结构化提示)和 OutputParser (输出解析器)。简单理解, StructuredPrompt 负责“教”模型应该输出什么样的格式,而 OutputParser 则负责“确保”模型输出的内容能被正确解析成我们想要的Java对象。

2.1 StructuredPrompt :给模型的格式说明书

StructuredPrompt 是一个注解,你把它加在你自定义的一个接口上。这个接口的每一个方法,都代表了你希望模型返回的JSON对象中的一个字段。LangChain4j在背后会将这些方法签名和注解信息,转换成一段清晰的指令,插入到最终发送给大模型的提示词(Prompt)中。

举个例子,假设我们要让AI从一段产品描述中提取信息。我们首先定义一个“产品信息”的结构:

import dev.langchain4j.model.input.structured.StructuredPrompt;

@StructuredPrompt({
    “请从以下产品描述中提取关键信息,并以JSON格式返回。”,
    “描述:{{it}}” // {{it}} 是一个占位符,运行时会被实际的用户输入替换
})
public interface ProductInfo {
    String productName();
    String brand();
    Double price();
    List<String> keyFeatures();
}

注意这个 ProductInfo 接口:

  1. 方法名直接对应JSON键 productName() 对应JSON中的 ”productName” 键。
  2. 返回类型定义数据类型 String , Double , List<String> 清晰地指明了每个字段期望的数据类型。
  3. @StructuredPrompt 注解 :这里定义了给模型的“系统指令”。 {{it}} 是LangChain4j的模板变量,代表用户输入的实际文本。

当这个接口被使用时,LangChain4j生成的最终Prompt会类似于:

你是一个信息提取助手。请严格按照以下JSON格式输出,不要添加任何其他解释。
格式:{“productName”: “string”, “brand”: “string”, “price”: number, “keyFeatures”: [“string”, “string”, …]}
请从以下产品描述中提取关键信息,并以JSON格式返回。
描述:`用户输入的实际产品描述文本`

这种指令非常明确,大大提高了模型返回合规JSON的概率。

2.2 OutputParser :从文本到对象的转换器

即使有清晰的指令,大模型偶尔也可能“放飞自我”,在JSON前后加上一些说明文字,或者格式略有瑕疵。 OutputParser 的作用就是处理这些“不完美”的响应,将其规整并反序列化成我们定义的Java接口的代理实例。

在LangChain4j中,你通常不需要直接操作 OutputParser 。当你通过 AiServices 创建AI服务时,框架已经为你集成了默认的解析逻辑。它的工作流程大致如下:

  1. 获取模型的原始文本响应。
  2. 尝试在响应中定位JSON字符串(通常通过查找第一个 { 和最后一个 } )。
  3. 使用JSON库(如Jackson)将定位到的JSON字符串解析成一个 Map<String, Object>
  4. 根据你定义的接口(如 ProductInfo ),将Map中的值映射到接口方法的返回值上,并动态创建一个实现了该接口的代理对象。

当你调用代理对象的方法时,实际上是从这个内存中的Map里取值。这个过程对开发者是透明的,你拿到手的就是一个标准的Java对象,可以直接调用 getter (即接口方法)。

2.3 两者协作:一个完整的流程视图

让我们把这两个组件串起来,看看一次完整的结构化调用是如何发生的:

  1. 开发者定义结构 :你创建了一个带有 @StructuredPrompt 的接口 MyStructure
  2. 构建AI服务 :你使用 AiServices.builder() 创建服务,并指定模型和这个接口。
  3. 用户发起请求 :用户输入一段文本,例如“ 帮我分析这个句子:苹果公司发布了售价999美元的iPhone 15,特点是灵动岛和USB-C接口。
  4. 框架组装Prompt :LangChain4j将 @StructuredPrompt 中的指令模板和用户输入结合,生成最终Prompt发送给大模型。
  5. 模型返回文本 :大模型返回类似“ {“productName”: “iPhone 15”, “brand”: “苹果公司”, “price”: 999, “keyFeatures”: [“灵动岛”, “USB-C接口”]} ”的文本(理想情况下)。
  6. 框架解析响应 OutputParser 提取出JSON部分,并创建 MyStructure 的代理实例。
  7. 开发者使用结果 :你获得一个 MyStructure 对象,调用 productName() 直接得到“iPhone 15”。

这个流程将不确定的自然语言输出,转化为了高度确定的、类型安全的Java对象,是构建可靠AI Agent的基石。

3. 实战:构建一个合同条款提取Agent

光说不练假把式。我们现在就来构建一个真实的、微型的Agent,它的唯一任务就是从一段非结构化的合同文本中,提取出结构化的关键条款信息。我们将使用OpenAI的GPT-4模型(你也可以替换为其他兼容模型,如Ollama本地模型)。

3.1 环境准备与依赖引入

首先,创建一个新的Maven或Gradle项目。核心依赖是 langchain4j-open-ai ,它包含了LangChain4j核心以及OpenAI客户端的集成。我们也会用 lombok 来简化POJO的创建。

Maven pom.xml 关键依赖:

<dependencies>
    <!-- LangChain4j OpenAI 集成 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai</artifactId>
        <version>0.30.0</version> <!-- 请使用最新版本 -->
    </dependency>
    <!-- Lombok 用于生成Getter/Setter等 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
    <!-- 日志框架(可选,但推荐) -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.9</version>
    </dependency>
</dependencies>

Gradle build.gradle 关键依赖:

dependencies {
    implementation ‘dev.langchain4j:langchain4j-open-ai:0.30.0’
    compileOnly ‘org.projectlombok:lombok:1.18.30’
    annotationProcessor ‘org.projectlombok:lombok:1.18.30’
    implementation ‘org.slf4j:slf4j-simple:2.0.9’
}

注意 :版本号请务必查询LangChain4j官方GitHub仓库,使用最新的稳定版。这个领域迭代非常快。

3.2 定义数据结构:合同条款POJO

我们需要明确要从合同里提取什么。定义一个 ContractClause 接口,并使用 @StructuredPrompt 注解。这里我选择用接口而不是类,是因为LangChain4j的 AiServices 默认期望与接口配合工作来创建动态代理。

import dev.langchain4j.model.input.structured.StructuredPrompt;
import java.time.LocalDate;
import java.util.List;

@StructuredPrompt({
    “你是一个专业的合同分析助手。请从以下合同文本中,精确提取以下关键条款信息。请确保金额只提取数字,日期格式为YYYY-MM-DD。除了以下信息,不要提取其他内容。”,
    “合同文本:{{it}}”
})
public interface ContractClause {
    // 甲方名称
    String partyA();
    // 乙方名称
    String partyB();
    // 合同总金额(数字)
    Double totalAmount();
    // 币种,如 CNY, USD
    String currency();
    // 合同签署日期
    LocalDate signingDate();
    // 项目截止日期
    LocalDate deadline();
    // 关键责任条款列表
    List<String> keyResponsibilities();
    // 付款方式描述
    String paymentTerms();
}

关键点解析:

  1. LocalDate 类型 :LangChain4j的 OutputParser 能够处理常见的Java类型,包括 LocalDate 。它会尝试将模型返回的日期字符串(如“2023-10-01”)自动转换。这比我们手动用 String 接收再解析要安全方便得多。
  2. List<String> 类型 :对于列表型字段,模型需要返回一个JSON数组。指令中“关键责任条款列表”的表述会引导模型将多条责任总结为数组项。
  3. 清晰的指令 :指令中强调了“精确提取”、“金额只提取数字”、“日期格式”,这些都能有效约束模型输出,减少后续清洗工作。
  4. {{it}} 占位符 :这是固定的,代表整个用户输入。

3.3 创建并配置AI服务

接下来,我们创建AI服务。你需要一个OpenAI的API Key。

import dev.langchain4j.service.AiServices;
import dev.langchain4j.model.openai.OpenAiChatModel;

public class ContractExtractionAgent {
    public static void main(String[] args) {
        // 1. 创建OpenAI模型实例
        // 请将”your-api-key-here“替换成你的真实API Key
        OpenAiChatModel model = OpenAiChatModel.builder()
                .apiKey(System.getenv(“OPENAI_API_KEY”)) // 推荐从环境变量读取
                .modelName(“gpt-4o”) // 使用GPT-4或GPT-3.5-turbo。结构化输出推荐GPT-4,更稳定。
                .temperature(0.0) // 温度设为0,使输出确定性最高,最适合结构化任务
                .logRequests(true) // 开启请求日志,调试时非常有用
                .logResponses(true)
                .build();

        // 2. 创建AI服务,将模型与我们定义的接口绑定
        ContractClause extractor = AiServices.create(ContractClause.class, model);

        // 3. 准备一段合同文本
        String contractText = “””
                本合同由甲方(委托方):上海云智科技有限公司,与乙方(受托方):北京数创未来人工智能实验室,于2024年5月15日共同签署。
                甲方委托乙方进行‘智能客服系统升级项目’的开发与实施。合同总金额为人民币贰拾伍万元整(¥250,000.00)。
                乙方需在2024年11月30日前完成全部开发、测试并交付上线。
                乙方的主要责任包括:1. 完成系统架构设计与核心模块开发;2. 提供为期一年的免费技术维护;3. 对甲方人员进行两次系统操作培训。
                付款方式:合同签订后7个工作日内,甲方向乙方支付合同总价的50%作为预付款;项目验收合格后15个工作日内,支付剩余的50%。
                “””;

        // 4. 调用服务进行提取
        ContractClause result = extractor.extract(contractText); // 注意:接口方法名需要定义,这里假设为extract

        // 5. 输出结果
        System.out.println(“甲方: “ + result.partyA());
        System.out.println(“乙方: “ + result.partyB());
        System.out.println(“合同金额: “ + result.totalAmount() + “ “ + result.currency());
        System.out.println(“签署日期: “ + result.signingDate());
        System.out.println(“截止日期: “ + result.deadline());
        System.out.println(“关键责任: “ + result.keyResponsibilities());
        System.out.println(“付款方式: “ + result.paymentTerms());
    }
}

等等,这里有个问题! ContractClause 是一个接口,我们并没有定义 extract 这个方法。 AiServices 需要知道调用哪个方法来触发AI分析。我们需要修改接口,增加一个方法。

3.4 完善接口与调用

修正后的 ContractClause 接口:

import dev.langchain4j.model.input.structured.StructuredPrompt;
import dev.langchain4j.service.SystemMessage;
import java.time.LocalDate;
import java.util.List;

// 可以添加一个系统消息,进一步固定模型角色,但非必须
// @SystemMessage(“你是一个严谨的法律文档分析AI,只输出JSON,不进行任何额外解释。”)
@StructuredPrompt({
    “你是一个专业的合同分析助手。请从以下合同文本中,精确提取以下关键条款信息。请确保金额只提取数字,日期格式为YYYY-MM-DD。除了以下信息,不要提取其他内容。”,
    “合同文本:{{it}}”
})
public interface ContractClause {
    // 这个方法才是真正被AiServices调用的入口点。
    // 参数`String contractText`会替换掉 @StructuredPrompt 中的 {{it}}
    ContractClause extractClauses(String contractText);

    // 以下是提取的字段
    String partyA();
    String partyB();
    Double totalAmount();
    String currency();
    LocalDate signingDate();
    LocalDate deadline();
    List<String> keyResponsibilities();
    String paymentTerms();
}

相应地,主程序中的调用也需要修改:

// 4. 调用服务进行提取
ContractClause result = extractor.extractClauses(contractText); // 调用我们定义的方法

// 5. 输出结果
System.out.println(“提取结果:”);
System.out.println(“甲方: “ + result.partyA());
System.out.println(“乙方: “ + result.partyB());
System.out.println(“合同金额: “ + result.totalAmount() + “ “ + result.currency());
System.out.println(“签署日期: “ + result.signingDate());
System.out.println(“截止日期: “ + result.deadline());
System.out.println(“关键责任: “ + result.keyResponsibilities());
System.out.println(“付款方式: “ + result.paymentTerms());

现在,运行这个程序。如果一切顺利,你将看到控制台打印出结构化的信息:

提取结果:
甲方: 上海云智科技有限公司
乙方: 北京数创未来人工智能实验室
合同金额: 250000.0 CNY
签署日期: 2024-05-15
截止日期: 2024-11-30
关键责任: [完成系统架构设计与核心模块开发, 提供为期一年的免费技术维护, 对甲方人员进行两次系统操作培训]
付款方式: 合同签订后7个工作日内,甲方向乙方支付合同总价的50%作为预付款;项目验收合格后15个工作日内,支付剩余的50%。

成功! 一段杂乱的非结构化合同文本,被我们的小Agent精准地转换成了一个结构化的Java对象。你可以轻松地将这个 result 对象序列化成JSON存入数据库,或者传递给工作流中的下一个处理单元。

4. 进阶技巧与避坑指南

上面的例子跑通了基本流程,但在实际生产中,你会遇到各种边界情况和挑战。下面分享一些我踩过坑后总结的进阶技巧。

4.1 处理复杂嵌套结构与可选字段

现实中的数据模型很少是扁平的。比如,合同金额可能包含明细,参与方可能不止两个。LangChain4j同样支持嵌套对象的定义。

示例:定义包含嵌套对象的条款

import dev.langchain4j.model.input.structured.StructuredPrompt;
import java.time.LocalDate;
import java.util.List;

@StructuredPrompt(“从文本中提取合同信息:{{it}}”)
public interface ComplexContract {
    ComplexContract analyze(String text);

    // 嵌套对象:甲方信息
    Party partyA();
    // 嵌套对象:乙方信息
    Party partyB();
    // 合同金额明细(也是一个嵌套对象)
    AmountDetail amountDetail();
    LocalDate deadline();
}

// 定义”参与方“子结构
interface Party {
    String name();
    String unifiedSocialCreditCode(); // 统一社会信用代码,可能为空
    String contactPerson();
}

// 定义”金额明细“子结构
interface AmountDetail {
    Double total();
    String currency();
    Double taxRate(); // 税率,可能为空
    Double taxAmount(); // 税额,可能为空
}

关键点:

  • 嵌套接口 Party AmountDetail 本身也是接口。LangChain4j会递归地处理这些嵌套结构。
  • 可选字段处理 :对于模型中可能不存在的字段(如 taxRate ),如果文本中没有提及,大模型返回的JSON中可能没有这个键,或者值为 null 。LangChain4j的代理会处理这种情况,调用方法时可能返回 null 你需要在自己的业务逻辑中做好空值判断。

4.2 枚举(Enum)类型的映射

对于固定类别的字段,使用 Enum 类型是更安全的选择。例如,合同状态可以是“DRAFT“, “SIGNED“, “TERMINATED“。

@StructuredPrompt(“提取合同状态:{{it}}”)
public interface ContractStatusInfo {
    ContractStatusInfo extract(String text);
    ContractStatus status(); // 使用枚举
}

// 定义枚举
enum ContractStatus {
    DRAFT, UNDER_REVIEW, SIGNED, IN_EFFECT, TERMINATED, UNKNOWN
}

LangChain4j会尝试将模型返回的字符串匹配到枚举值上。如果匹配失败,可能会抛出异常。为了健壮性,可以定义一个 UNKNOWN 枚举项作为兜底,或者在接口方法中使用 String 类型接收,再手动转换。

4.3 指令工程:提高输出稳定性的关键

大模型对指令非常敏感。 @StructuredPrompt 里的文字,直接决定了输出质量。以下是一些撰写指令的黄金法则:

  1. 角色明确 :开头就固定AI的角色,如“你是一个专业的合同分析AI“。
  2. 任务清晰 :明确指出要做什么,“提取以下信息“、“总结为以下几点“。
  3. 格式强制 :必须包含“以JSON格式返回“、“只输出JSON,不要有任何其他文字“这类强约束语句。
  4. 字段说明 :对于容易混淆的字段,可以在指令中附加简短说明。例如,“ currency :使用三位字母代码,如CNY代表人民币,USD代表美元“。
  5. 示例驱动(Few-Shot) :对于极其复杂的结构,可以在指令中给一两个输入输出的例子,这是最强大的约束方式。虽然 @StructuredPrompt 不支持直接内嵌复杂示例,但你可以把例子写在指令文本里。

优化后的指令示例:

你是一个顶尖的法律文档分析AI。你的任务是从用户提供的合同文本片段中,精确提取指定的结构化信息,并输出为一个纯净的JSON对象,不要有任何额外的介绍、总结或解释性文字。

输出必须严格遵守以下JSON格式:
{
  “partyA”: “字符串,甲方全称”,
  “partyB”: “字符串,乙方全称”,
  “totalAmount”: 数字,仅提取金额数字,如250000,
  “currency”: “字符串,货币代码,如CNY”,
  “signingDate”: “字符串,日期格式必须为YYYY-MM-DD”,
  “deadline”: “字符串,日期格式必须为YYYY-MM-DD”,
  “keyResponsibilities”: [“字符串数组,每条责任简明扼要”],
  “paymentTerms”: “字符串,描述付款方式”
}

请注意:
1. 金额只提取纯数字,忽略‘元’、‘人民币’等文字和‘¥’、‘$’等符号。
2. 日期必须统一转换为‘YYYY-MM-DD’格式。
3. 如果某项信息在文本中未找到,则在JSON中将其值设为null。

现在,请分析以下合同文本:
{{it}}

4.4 错误处理与模型“不听话”怎么办?

即使指令再完美,模型也可能返回非JSON内容,或者JSON格式错误。LangChain4j的 OutputParser 内部会尝试修复(如提取首尾 {} 之间的内容),但并非万能。

实战中的处理策略:

  1. 使用 try-catch 包裹调用 :最基础的保护。

    try {
        ContractClause result = extractor.extractClauses(someText);
        // 处理结果
    } catch (Exception e) {
        log.error(“解析合同失败,文本: {}“, someText, e);
        // 降级策略:存入待人工审核队列,或使用更简单的正则进行二次提取
    }
    
  2. 启用详细日志 :创建模型时设置 .logRequests(true).logResponses(true) 。当解析失败时,查看日志里模型实际返回了什么,这是调试指令最关键的依据。

  3. 降级与重试

    • 降级 :对于不重要的场景,可以准备一个“默认值”或“未知”对象作为回退。
    • 重试 :对于重要任务,可以捕获异常后,尝试用更简化的指令或换一个模型(如从 gpt-3.5-turbo 切换到 gpt-4 )重新请求一次。注意设置重试次数和退避策略,避免无限循环和API费用暴涨。
  4. 后置校验与清洗 :即使解析成功,数据也可能有误。例如,金额单位弄错、日期格式不对。在将结果入库或进入下一流程前,增加一道业务规则的校验逻辑。比如,检查金额是否在合理范围内,日期是否在未来。

4.5 性能与成本考量

  1. 模型选择 gpt-4 系列在遵循复杂指令和输出格式上远胜于 gpt-3.5-turbo ,但价格更贵,速度更慢。对于格式简单、任务明确的情况, gpt-3.5-turbo 可能就足够了。需要进行测试和权衡。
  2. 温度(Temperature)参数 务必设置为0或接近0的值(如0.1) 。这个参数控制输出的随机性。对于结构化输出任务,我们需要的是确定性,而不是创造性。
  3. Token消耗 @StructuredPrompt 中的指令会作为系统或用户消息的一部分发送,占用Token。指令越长、越详细,每次请求的成本就越高。需要在指令的清晰度和简洁性之间找到平衡。
  4. 批量处理 :如果需要处理大量文档,避免在循环中同步调用API,这会导致极慢的速度。考虑使用异步客户端,或者利用LangChain4j的 BatchProcessor (如果版本支持)来批量发送请求,但要注意模型的速率限制。

5. 超越基础:动态结构与流式输出

5.1 处理动态字段(高级话题)

有时,我们无法在编译时确定所有字段。比如,从一份简历中提取技能,不同人的技能列表完全不同。虽然LangChain4j的原生 @StructuredPrompt 更适用于固定模式,但我们仍有变通方案。

方案一:使用 Map<String, Object> 类型。 定义一个字段返回 Map ,并在指令中要求模型将动态内容组织成键值对。但这要求模型对Map结构有很好的理解,且后续处理Map的逻辑会变得复杂。

@StructuredPrompt(“提取信息,动态属性放在‘additionalInfo’字段中:{{it}}”)
public interface DynamicResume {
    DynamicResume parse(String text);
    String name();
    Map<String, Object> additionalInfo(); // 用于存放动态字段
}

方案二:设计可扩展的固定结构。 这是更推荐的做法。预先定义好所有可能的字段,但允许为空。例如,为技能、工作经历、项目经验都定义成列表字段。即使简历中没有,返回空列表即可。这牺牲了一点灵活性,但换来了极强的类型安全和处理简便性。

5.2 流式输出(Streaming)与结构化

LangChain4j也支持流式响应,这对于生成长文本非常有用。但对于结构化输出,流式的意义在于 可以一边生成一边进行初步解析和验证 ,或者在生成完整JSON后立即开始处理,而不必等待整个响应文本传输完毕。

使用 OpenAiStreamingChatModel 并配合 StreamingResponseHandler ,你可以在 onComplete 回调中获得完整的响应文本,然后可以将其传递给一个自定义的或框架提供的 OutputParser 进行解析。不过,目前(以0.30.0版本为例) AiServices 对流式结构化输出的直接支持可能不如同步调用那么完善,可能需要更多的手动处理。

一个常见的模式是:使用流式模型获取完整的响应字符串,然后使用 ObjectMapper (如Jackson)或LangChain4j的 Json.JsonCodec 将其反序列化成你的POJO。

OpenAiStreamingChatModel streamingModel = …;
StringBuilder fullResponse = new StringBuilder();

streamingModel.generate(userMessage, new StreamingResponseHandler<AiMessage>() {
    @Override
    public void onNext(String token) {
        fullResponse.append(token);
        // 可以实时显示token
    }
    @Override
    public void onComplete(Response<AiMessage> response) {
        String jsonString = fullResponse.toString();
        // 1. 可能需要清洗jsonString(提取JSON部分)
        // 2. 使用Jackson等库解析为你的Java对象
        ObjectMapper mapper = new ObjectMapper();
        MyPojo result = mapper.readValue(extractedJson, MyPojo.class);
        // 处理result
    }
    // … 其他方法
});

6. 总结与最佳实践心法

让Agent返回JSON而不是闲聊,本质上是将大模型纳入到确定性软件工程范式中的关键一步。通过LangChain4j的结构化输出能力,我们能够构建出可靠、可集成、可维护的AI增强型应用。

回顾整个实战过程,以下是我总结的几条核心心法:

  1. 始于清晰的定义 :在写第一行代码之前,先用纸笔或文档把你希望得到的JSON结构画出来。明确的输出结构是成功的起点。
  2. 指令即契约 @StructuredPrompt 里的文字是你与模型之间的契约。写得越清晰、越无歧义,模型“履约”的可能性就越高。多花时间打磨指令,事半功倍。
  3. 拥抱强类型 :充分利用Java的强类型系统。用 LocalDate Enum List<YourType> ,让编译器和你站在一起,在编译期就能发现很多潜在的类型错误。
  4. 假设输出会出错 :永远不要假设模型返回的JSON是完美的。一定要有异常处理、日志记录和降级方案。对于关键业务,甚至可以考虑引入人工审核环节作为最终保障。
  5. 测试、测试、再测试 :准备一个涵盖各种边界情况的测试用例集:字段缺失、格式异常、输入荒谬、长度极长等。用这些用例反复测试你的Agent,观察其表现,并持续优化你的指令和解析逻辑。
  6. 关注成本与延迟 :结构化输出通常意味着更长的指令(更多Token)和可能需要更强大的模型(如GPT-4),这会增加单次调用的成本和耗时。在产品化时,需要评估这些开销是否在可接受范围内。

最后,记住工具是为人服务的。LangChain4j的结构化输出是一个强大的工具,但它不是魔法。它需要你,开发者,去精心设计数据结构、撰写清晰指令、并构建稳健的错误处理外壳。当你把这些都做到位时,你就会得到一个不再是“闲聊伙伴”,而是真正能融入生产流水线的“智能数据提取员”。

更多推荐