第三章 05:3.7 结构化输出与程序化消费

第三章 05:3.7 结构化输出与程序化消费

课程专题 :《AI Agents 开发实践》

原文链接 :https://s.juejin.cn/ds/TAqu_hbSmd8/

章节 :第三章 3.7 结构化输出与程序化消费

作者:Java 开发者转型 AI 全栈


3.7.1 自由文本输出的局限

前面几节已经把调用流程整理得更清楚了,但如果模型输出仍然是一段自由文本,它进入程序后的价值仍然有限。

问题 :3.6 的 chain-invoke 返回的 result 是字符串:

json 复制代码
{
  "result": "{\n  \"action\": \"绑定手机号\",\n  \"constraints\": [\"必须\", \"至少 8 位\"],\n  \"entities\": [\"用户\", \"手机号\", \"密码\"]\n}"
}

注意 result 的值------它长得像 JSON,但本质仍是字符串 (外层有引号)。如果模型哪天多输出一句"希望对你有帮助",你的 JSON.parse() 就崩了。


3.7.2 共享字段结构的定义

因此,下一步要做的,就是先把字段结构明确下来,并沉淀到共享 contract 中。

Java 对照

java 复制代码
// 反面:模型返回字符串,手动解析(脆弱)
String modelOutput = callModel(prompt);  // "{\"action\":\"注册\",...}"
JSONObject json = new JSONObject(modelOutput);  // 模型多输出一句就崩
String action = json.getString("action");

// 正面:用 DTO + Jackson 反序列化(类型安全)
public class RequirementResult {
    private String action;
    private List<String> constraints;
    private List<String> entities;
    // getter/setter
}

public RequirementResult extract(String input) {
    String modelOutput = callModel(prompt);
    return objectMapper.readValue(modelOutput, RequirementResult.class);
    // 字段类型稳定,程序可断言
}

LangChain 做法

用 zod 定义 schema(相当于 Java 的 DTO),把 schema 放到 @autix/contracts 共享包(前后端共用同一份结构)。

修改 packages/contracts/src/index.ts

在文件末尾追加(先确保已安装 zod:bun add zod --cwd packages/contracts):

typescript 复制代码
// ═══════════════════════════════════════════════════════════
// 3.7 结构化输出:需求抽取契约(schema 即契约,前后端共用)
// ═══════════════════════════════════════════════════════════

import { z } from 'zod';

/** 需求抽取入参 */
export const RequirementSchema = z.object({
  input: z.string().min(1),
});

/** 需求抽取结构化结果 */
export const RequirementResultSchema = z.object({
  action: z.string().describe('唯一核心动作'),
  constraints: z.array(z.string()).describe('明确约束条件'),
  entities: z.array(z.string()).describe('关键实体'),
});

export type RequirementResult = z.infer<typeof RequirementResultSchema>;

3.7.3 withStructuredOutput() 的使用方式

有了共享 schema 之后,就可以让模型直接朝着这个结构输出,而不是再依赖后置字符串清洗。

services/chat/src/llm/requirement.service.ts

typescript 复制代码
// services/chat/src/llm/requirement.service.ts
// 3.7 结构化输出 ------ 让模型直接朝着共享 schema 输出,而不是依赖后置字符串清洗

import { Injectable } from '@nestjs/common';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import {
  RequirementResultSchema,
  type RequirementResult,
} from '@autix/contracts';
import { createChatModel } from './model.factory';
import {
  REQUIREMENT_SYSTEM_PROMPT,
  REQUIREMENT_USER_TEMPLATE,
} from './prompts/requirement.prompt';

@Injectable()
export class RequirementService {
  private model = createChatModel();

  private prompt = ChatPromptTemplate.fromMessages([
    ['system', REQUIREMENT_SYSTEM_PROMPT],
    ['human', REQUIREMENT_USER_TEMPLATE],
  ]);

  async extract(input: string): Promise<RequirementResult> {
    const messages = await this.prompt.formatMessages({ input });
    const structuredModel = this.model.withStructuredOutput(
      RequirementResultSchema,
    );
    return structuredModel.invoke(messages);
  }
}

修改 services/chat/src/llm/llm.module.ts

把 RequirementService 加到 providers 和 exports:

typescript 复制代码
import { Module } from '@nestjs/common';
import { LlmService } from './llm.service';
import { LlmController } from './llm.controller';
import { RequirementService } from './requirement.service';

@Module({
  providers: [LlmService, RequirementService],
  controllers: [LlmController],
  exports: [LlmService, RequirementService],
})
export class LlmModule {}

修改 services/chat/src/llm/llm.controller.ts

顶部加 import:

typescript 复制代码
import { RequirementService } from './requirement.service';

构造函数加注入:

typescript 复制代码
constructor(
  private readonly llmService: LlmService,
  private readonly requirementService: RequirementService,
) {}

类里新增路由(放在最后):

typescript 复制代码
// 3.7.4 POST /api/langchain/structured(结构化输出,直接返回 schema 对象)
@Post('structured')
async structured(@Body() body: { input: string }) {
  return this.requirementService.extract(body.input);
}

3.7.4 结构化输出的工程价值

验收测试

cmd 复制代码
curl.exe -s -X POST http://localhost:4001/api/langchain/structured -H "Content-Type: application/json" -d "{\"input\":\"用户注册时必须绑定手机号,密码至少 8 位\"}"

期望返回:

json 复制代码
{
  "action": "绑定手机号",
  "constraints": ["必须", "至少 8 位"],
  "entities": ["用户", "手机号", "密码"]
}

关键对比

3.6 chain-invoke 3.7 structured
{"result":"{\n \"action\": ..."} {"action":"绑定手机号","constraints":[...],"entities":[...]}
result 是字符串(外层有引号) 直接是 JSON 对象(无外层引号)
程序需要 JSON.parse(result) 程序直接 result.action 访问
模型多输出一句就崩 字段类型由 zod schema 保证稳定

这就是 withStructuredOutput() 的核心价值:模型输出从"自由文本"变成"可被程序稳定消费的结构"。


工程价值

到这里,模型结果才真正开始具备"可被程序稳定消费"的条件。

  • 可断言 :测试里可以写 expect(result.action).toBe('绑定手机号')
  • 可消费 :下游代码可以直接 result.constraints.map(...)
  • 可测试:回归测试可以批量跑,对比前后输出差异

注意:解析兜底

即使已经声明返回 JSON,也仍可能出现输出夹杂解释文本的情况。这种问题需要通过提示约束、解析兜底和测试样例一起处理。

提示约束:在 system prompt 里明确要求"输出必须符合 schema,不要输出解释"

解析兜底 :如果模型偶尔还是输出了解释文本,可以在 Service 层加一个 JSON 提取逻辑(从输出中截取第一个 {...} 块)

测试样例:准备一组典型输入,每次改 prompt 或换模型后批量重跑,对比输出差异


判断标准

什么时候该上结构化输出?

当模型结果需要作为接口返回值、下游输入或测试断言的一部分,而不是仅供人类阅读时。


下一章

3.8 工具调用机制的接入:让模型不只生成内容,还能主动调用外部函数。

敬请期待!


回到索引 :第三章博客索引

相关推荐
周末程序猿2 小时前
翻译|一份给"与模型共事的人类"的教程
人工智能
原子延迟2 小时前
iLovePDF免费吗?不限次数的PDF压缩5款(2026)
图像处理·人工智能·计算机视觉·pdf
DongQiShanRen2 小时前
宇宙(上):分身创建——三条件触发与差异化演化
人工智能·深度学习·自然语言处理·数据挖掘
2601_962780912 小时前
2027校招:数学与应用数学转产品运营,如何把数据能力变成项目证据?
大数据·人工智能·产品运营
龍德明宇2 小时前
机器人会冷吗,会饿吗?-龍德明宇
人工智能·机器人·大语言模型llm·负主体性·ai存在论
云和数据.ChenGuang2 小时前
AI 工程师相关就业岗位(细分,偏工程落地,由底层到上层)
服务器·开发语言·人工智能·python
虎鲸不是鱼2 小时前
【虎鲸进化论 15】上下文工程:长周期项目的接力
人工智能·自动化·大模型·llm·自进化agent
余槐i2 小时前
近期技术热点:从异常现象到根因定位的完整排查
人工智能·深度学习·机器学习
2601_965454232 小时前
AI视频制作:券商研报怎么转口播视频:PDF 解读、配音和图表自动成片的方法
人工智能·pdf·音视频