数据结构化输出控制操作

1、前言

在上一篇文章中,已经完成了第一次模型请求AI Agent:三种主流方式请求单次模型请求,相信大家也发现了很多问题。

1)我需要怎么拿到自己想要的数据格式?
2)我要求返回固定数据格式是否有用?
3)为什么要配置response_format?
4)我已经要求返回JSON,并且也配置了response_format,还需要做什么呢?
5)应用在这个环节的意义?

这次我转换为另外一个叙事流程,先从由简到难层层递进的方式将基础理论和我个人理解讲解清楚,然后在附加部分代码,以便于读者通过代码更深刻理解基础理论,借此降低大家阅读难度并提升阅读体验。

最后,还是那句,这是我的学习笔记,有可能有遗漏有偏差,欢迎大家与我交流

2、前篇回顾

这篇笔记围绕"单次模型请求"展开。我首先理清了 Vibe Coding 与 AI 应用的区别:用 AI 生成代码只是一种开发方式,真正的 AI应用是项目交付后,模型仍在功能流程中参与理解、判断、推理和行动,哪怕只涉及一个小模块。同时我也提到 Vibe Coding带来的效率提升与职场冲击——能力要求正从"会写代码"转向"会读代码、辨别幻觉"。随后我用同一个问题"什么是人工智能?",分别通过 LangChain、OpenAI SDK、Agently 三种方式完成了对 Kimi模型的请求,并附上完整码与真实返回结果。OpenAI SDK 最接近裸请求、结构透明,但要自己处理参数细节(我就踩了 max_tokens 过小导致回答截断的坑);LangChain 封装成 AIMessage 对象,生态强大但概念繁多、学习曲线最陡;Agently链式调用最简洁,还能声明式定义结构化输出,但黑盒感较强。对比结论是:学习阶段推荐用 OpenAI SDK 看清请求的每个细节,做复杂项目时则借助 LangChain 或 Agently的生态与抽象能力。下一篇将记录结构化输出控制的学习过程。

3、架构选择与原因

1)一次请求的能力与不足

这个是之前一个老师总结出来的,我看后深受启发决定依旧采用这个表格

直接请求时可以控制 仍需开发者自行处理
模型、endpoint、鉴权和超时 连接失败、限流、超时和重试策略
system/user 消息和上下文 Prompt 是否包含完整任务事实与结构说明
temperature、max tokens 等生成参数 原始文本如何保存、截断和观测
供应商支持的 JSON mode / response format JSON 定位、解析错误和供应商差异
希望模型生成哪些字段 字段类型、必填、空值、范围和嵌套结构校验
请求一次模型生成 跨字段业务规则、事实忠实、权限和真实写入
2)学习阶段架构选择

我选择使用 Agently 框架。因为我原本是一名 Android 开发,Agently 的链式写法在 Android 开发中比较常见,理解难度相对较低,而且Agently官方文档相比另外两个框架,阅读成本更低一些。

我个人也在使用后总结了几点优势:

- 相比较 OpenAI 抽象层级更高,会少写很多样板代码
- 相比较 LangChain 需要理解的概念会少很多,并且阅读流畅度更高
- 相比较 LangChain 如 Chain、Prompt、Template等一系列概念,因为迭代快,教程很容易过时,Agently相对稳定一些
- Agently 是三个架构当中我认为是结构化最清晰的
- 最后也是比较重要的一点就是国内生态友好度最高

Agently 的优势是"用最少的代码、最低的概念成本拿到结构化结果",适合快速构建以 LLM输出为核心的应用;需要庞大生态时 LangChain 仍是首选,想看清底层时 OpenAI SDK 不可替代。

3)数据结构化的意义

模型的原生输出是自然语言文本,而程序能处理的是确定性的数据结构,这两者之间天然存在一道鸿沟。数据结构化解决的就是这道鸿沟,它的意义我个人理解有三层:

1)让模型输出从"给人看"变成"给程序用"

自由文本只有人能读,程序想从中提取一个字段只能靠正则硬抠,措辞一变就崩。结构化之后(比如 JSON),输出变成了程序可直接消费的 dict/list,可以入库、可以渲染、可以驱动下游逻辑。

2)把不确定性收敛到可控范围

模型的生成本质是概率性的,同样的输入每次输出都不一样。如果放任输出为自由文本,这种不确定性会扩散到整个系统;而结构一旦固定下来,不确定的就只剩"字段里填什么内容",系统其他部分有了稳定的对接面。

3)是 AI 应用成立的前提

上一篇说过,AI 应用的判断标准是模型是否参与功能流程。而参与流程的前提是输出能被流程里的其他环节读懂——靠的就是结构化。可以说没有结构化,LLM 只能停留在聊天框里。

一句话总结:数据结构化是把模型"说的话"翻译成程序"能用的数据",是 LLM 能力接入真实业务的第一块基石。

4)输出控制的意义

如果说数据结构化回答的是"输出要长成什么样",那输出控制回答的就是"怎么保证每次都长成这样"。一次请求能拿到理想的 JSON 不代表一百次都能,输出控制的意义正在于把"偶尔对"变成"稳定对":

1)稳定性是工程化的前提

Demo 阶段模型返回什么都能接受,生产阶段任何一次格式跑偏都是线上事故。输出控制(prompt 约束 + response_format + 解析校验的层层设防)就是把模型的概率性生成,变成系统中确定性的一环。

2)控制是分层的,一层管不住还有下一层

结合上面表格可以看出:prompt 要求是软约束,管"模型知不知道要干什么";response_format 是 API 层硬约束,管"返回的是不是合法 JSON";解析后的字段校验是应用层兜底,管"数据能不能真正用"。三层各司其职,缺了哪一层,风险就漏到哪一层。

3)控制也有边界

输出控制只能保证"格式对",保证不了"内容真"——字段值是否真实、是否符合业务规则,仍在模型能力之外,需要开发者自行处理。认清这个边界,才不会对控制手段产生不切实际的信任。

一句话总结:输出控制是用分层的约束手段对抗模型生成的不确定性,把"偶尔对"收敛为"稳定对",这是 AI 应用从 Demo 走向生产的分水岭。

4、实例

import os
from dotenv import load_dotenv, find_dotenv

from agently import Agently

# 加载项目根目录下的 .env 环境变量配置文件
# find_dotenv() 会自动向上查找最近的 .env 文件
load_dotenv(find_dotenv())

# 从环境变量中读取 Kimi API 密钥,没有则直接抛错终止,
# 因为后续的真实模型请求必须依赖该密钥
api_key = os.getenv("KIMI_API_KEY")
if not api_key:
    raise RuntimeError("未找到 KIMI_API_KEY,否则无法运行真实模型请求。")
# 读取模型名称与接口地址(OpenAI 兼容协议的 base_url)
model_name = os.getenv("MODEL_NAME", "")
base_url = os.getenv("KIMI_BASE_URL", "")
# 打印当前加载到的配置,便于排查环境变量是否生效
print(f"Model Name: {model_name}")
print(f"Base URL: {base_url}")


def build_model():
    """配置 Agently 底层使用的大模型服务(OpenAI 兼容协议)。"""
    Agently.set_settings(
        "OpenAICompatible",
        {
            "base_url": base_url,  # 模型服务的接口地址
            "api_key": api_key,    # 鉴权密钥
            "model": model_name,   # 使用的模型名称
        },
    )


def main():
    # 初始化模型配置
    build_model()
    # 创建 Agent,并通过链式调用组装请求:
    # system 设定角色,input 传入用户问题,
    # output 声明期望的结构化输出格式(JSON)
    result = (
        Agently.create_agent()
        .system("你是一个有帮助的助手。")
        .input("什么是人工智能?")
        .output(
            {
                "content": (str, "一句话定位"),
                "highlights": [
                    {
                        "title": (str, "亮点标题"),
                        "detail": (str, "一句话描述"),
                        "url": (str, "亮点链接"),
                        "image": (str, "亮点图片链接"),
                        "high_contents": (list, "亮点内容"),
                        "tags": (list, "亮点标签"),
                        "score": (float, "亮点评分,0 到 1 之间"),
                    }
                ],
            },
            format="json",  # 要求模型按 JSON 格式返回,Agently 会自动解析成上述结构
        )
        .start()  # 发起请求并等待返回结果
    )
    print(f"LLM Response: {result}")


if __name__ == "__main__":
    main()

请求后结果:

{
  'content': '人工智能是让机器模拟、延伸并增强人类感知、学习、推理、决策与创造能力的技术体系。',
  'highlights': [{
    'title': '核心定义',
    'detail': 'AI 通过数据、算法与算力,让系统完成过去需要人类智能才能完成的任务。',
    'url': 'https://en.wikipedia.org/wiki/Artificial_intelligence',
    'image': '',
    'high_contents': ['感知环境', '学习规律', '推理决策', '生成内容'],
    'tags': ['定义', '机器学习', '智能系统'],
    'score': 0.95
  }, {
    'title': '关键技术分支',
    'detail': '主要包括机器学习、深度学习、自然语言处理、计算机视觉、知识图谱与机器人等方向。',
    'url': 'https://www.britannica.com/technology/artificial-intelligence',
    'image': '',
    'high_contents': ['机器学习', '深度学习', 'NLP', '计算机视觉'],
    'tags': ['技术栈', '深度学习', 'NLP'],
    'score': 0.92
  }, {
    'title': '典型应用',
    'detail': 'AI 已广泛用于搜索推荐、智能助理、医疗影像、自动驾驶、工业质检与代码生成等场景。',
    'url': 'https://en.wikipedia.org/wiki/Applications_of_artificial_intelligence',
    'image': '',
    'high_contents': ['智能问答', '图像识别', '自动驾驶', '辅助编程'],
    'tags': ['应用', '产业', '生成式AI'],
    'score': 0.9
  }, {
    'title': '风险与治理',
    'detail': 'AI 也带来偏见、幻觉、隐私、安全与就业影响,需要评估、监管与负责任设计。',
    'url': 'https://en.wikipedia.org/wiki/Ethics_of_artificial_intelligence',
    'image': '',
    'high_contents': ['算法偏见', '数据隐私', '可控安全', '透明问责'],
    'tags': ['伦理', '治理', '安全'],
    'score': 0.88
  }]
}

本节通过一个完整实例演示了 Agently 的结构化输出能力:在 .output() 中用 (类型, 描述) 元组声明一个包含字符串、列表、浮点数及嵌套数组的 7 字段输出结构,并指定 format="json",框架即可自动完成 prompt 约束、JSON 解析和类型适配,调用方直接拿到可消费的 Python dict。

实例验证了声明式输出对复杂嵌套结构的控制力——返回数据字段齐全、类型正确、结构稳定。但结果也暴露了一个边界:框架只能保证"格式对",不能保证"内容真"(如 image 字段为空、url 可能为模型杜撰),数据的事实性校验仍需开发者自行处理,这也正是第 3 节表格中强调的"直接请求可控"与"仍需自行处理"的分界线。

5、问题回答

1)我需要怎么拿到自己想要的数据格式?

答:结合第 4 节的实例来看,拿到自己想要的数据格式,核心就是"声明结构、约束格式、框架解析"这三步:

第一步:用结构声明告诉模型"我要什么"

在 Agently 中,这一步通过 .output() 完成。声明的写法是 (类型, 描述) 元组:

.output({
    "content": (str, "一句话定位"),
    "highlights": [
        {
            "title": (str, "亮点标题"),
            "score": (float, "亮点评分,0 到 1 之间"),
            "tags": (list, "亮点标签"),
        }
    ],
})

这里有两个关键点:

  • 结构即文档:key 名、类型、描述文字本身就是写给模型看的指令。模型看到 score: (float, "亮点评分,0 到 1 之间"),就知道要生成一个浮点数字段。所以描述写得越清楚,返回越靠谱。
  • 支持嵌套:实例中 highlights 是一个数组,数组里每个元素又是包含 7 个字段的对象,这种复杂嵌套同样只需声明一次。

第二步:用 format 参数约束"按什么格式返回"

.output({...}, format="json") 中的 format="json" 会要求模型以 JSON 格式输出,避免模型自由发挥返回一大段散文,导致后续无法解析。这一步对应的是模型侧的 JSON mode / response_format 能力(问题 3 会细说)。

第三步:框架自动解析,直接消费结果

模型返回的 JSON 文本会被 Agently 自动解析成 Python 的 dict/list,字段类型与声明一致(score 直接就是 float,tags 直接就是 list)。也就是说拿到结果后不需要再做 json.loads 或正则提取,直接 result["highlights"][0]["score"] 就能用。

一个需要清醒的边界

从实例的返回结果也能看到:框架只能保证"格式对",不能保证"内容真"。比如 image 字段返回了空字符串、url 可能是模型杜撰的链接。结构声明解决的是"数据长什么样"的问题,数据内容的校验(字段是否为空、链接是否真实、数值是否合理)仍然需要开发者自己兜底——这正是第 3 节表格中"希望模型生成哪些字段"可控、而"字段校验与事实忠实"仍需自行处理的分界线。

一句话总结:在 Agently 里,想要什么样的数据格式,就在 .output() 里把它声明出来,再配上 format="json",剩下的 prompt 构造和结果解析交给框架。

2)我要求返回固定数据格式是否有用?

答:有用,但不能只靠它。

先说"有用"的部分。在 prompt 里明确要求返回固定格式,实际效果是明显的:

  • 绝大多数情况下模型会照办。现在主流模型对指令的遵循能力已经很强,简单场景下光靠一句"请用 JSON 返回"就能拿到像样的结果。
  • 要求本身就是在给模型提供上下文。你声明了字段名、类型和描述(如第 4 节实例中的 (float, "亮点评分,0 到 1 之间")),模型才知道每个字段该填什么、填成什么样。没有这个声明,模型连"你想要什么"都无从猜起。
  • 描述质量直接影响返回质量。要求写得越具体,返回越稳定;写得含糊,模型就自由发挥。

再说"不能只靠它"的部分。prompt 要求本质上是和模型商量,不是强制:

  • 模型可能在 JSON 外面包裹解释文字或 Markdown 代码块
  • 复杂嵌套结构下,字段缺失、类型跑偏的概率会上升
  • 同样的 prompt,换个模型、换个供应商,表现可能完全不一样(第 3 节表格里的"供应商差异"说的就是这件事)

所以我对这个问题的理解是:prompt 要求是必要不充分条件。它是整个结构化输出的第一道工序,决定了模型"知不知道要干什么";但要做到稳定可用,还得叠加 response_format 的硬约束(问题 3)和解析后的字段校验(问题 4)。

一句话总结:要求返回固定格式有用,它是基础;但它是"软约束",只能作为结构化输出的第一道防线,不能是唯一一道。

3)为什么要配置response_format?

答:先说结论:只在 prompt 里"口头要求"模型返回 JSON 是靠不住的,response_format 是在模型 API 层面给输出加的一道硬约束。

1)光靠 prompt 要求,模型不一定听话

很多初学者(包括我一开始)会觉得:我在 prompt 里写了"请用 JSON 格式返回",模型就会照办。实际跑多了就会发现各种意外:

  • 模型在 JSON 前后加了解释性文字:"好的,这是你要的 JSON:json {...} ",导致 json.loads 直接报错
  • 模型自作主张加了 Markdown 代码块标记
  • 生成到一半 token 用完,JSON 没闭合(上一篇就踩过 max_tokens 截断的坑)

也就是说,prompt 里的要求只是"软约束",取决于模型的指令遵循能力和采样随机性。

2)response_format 是 API 层的"硬约束"

配置了 response_format={"type": "json_object"}(即 JSON mode)之后,模型在生成时会被强制输出合法的 JSON 文本,不会出现闲聊式的前缀后缀。这是模型服务端在解码层面做的限制,比 prompt 里的文字要求可靠得多。

在第 4 节的实例里,.output({...}, format="json") 这个 format="json" 参数,最终就是 Agently 帮我们映射到了底层请求的 response_format 上——框架替我们做了配置,但这个配置本身是必须存在的。

3)为什么不能只靠框架的解析能力?

有人会想:既然 Agently 能解析,那模型随便返回什么让它解析不就行了?问题在于解析的前提是"模型返回的东西里确实有一段完整、合法的 JSON"。response_format 保证的是解析的输入是可靠的,框架的解析能力才有意义。两者是先后手关系,不是二选一。

4)一个实际收益:省钱省 token

模型不用生成"好的,这是结果:"之类的客套话,也不用 Markdown 代码块包裹,输出更紧凑,token 消耗更少。

一句话总结:prompt 要求返回 JSON 是"商量",response_format 是"规定"。想让结构化输出稳定可用,两个都要有——这也是问题 4 要接着展开的。

4)我已经要求返回JSON,并且也配置了response_format,还需要做什么呢?

答:

前两步(prompt 要求 + response_format)解决的都是"模型返回什么"的问题。但拿到 JSON 之后到真正把数据用起来,中间还有一段路,这段路正好就是第 3 节表格右列的内容。我自己的梳理是需要做这五件事:

1)字段级校验:类型、必填、空值、范围

response_format 只保证返回的是合法 JSON,不保证字段符合你的业务预期。实际中会遇到:

  • 必填字段缺失或给了空值(第 4 节实例里 image 就是空字符串)
  • 类型对了但值不合理(score 声明了 float,模型给个 9.5 还是 0.95?范围没有约定)
  • 嵌套数组长度不符合预期(要 4 条 highlights,给了 2 条)

所以解析后还需要一层校验逻辑(比如用 Pydantic 定义模型再验证一遍),Agently 在解析时也会做一层类型适配,但业务级的规则仍要自己写。

2)事实忠实性校验

这是最容易被忽略的一点:JSON 结构完美 ≠ 内容是真的。实例里的 url 字段看起来有模有样,但很可能是模型杜撰的链接。如果业务对内容真实性有要求(要展示、要入库、要跳转),必须额外校验,或者干脆不让模型生成这类字段,改由程序自己补充。

3)异常处理与重试

网络连接失败、限流、超时、max_tokens 截断导致 JSON 没闭合……这些都是 response_format 管不了的事。需要明确重试策略:重试几次、间隔多久、重试还失败怎么办(降级?报错?给默认值?)。

4)原始结果的保存与观测

线上排查问题全靠现场。建议在解析前把模型的原始返回文本落日志:请求参数、原始响应、解析结果、校验失败原因。否则线上出现脏数据时,根本分不清是模型生成错了、解析错了还是校验漏了。

5)跨字段的业务规则校验

单字段合法不代表整体合法。比如"开始时间必须早于结束时间""折扣价必须小于原价"这类跨字段规则,模型和框架都不会替你保证,只能在应用层兜底。

一句话总结:要求 JSON + response_format 解决的是"格式合法",从"格式合法"到"数据可用"之间,还有字段校验、事实校验、异常重试、日志观测、业务规则这五道关卡,它们全部在模型能力之外,是开发者的责任区。

5)应用在这个环节的意义?

答:

这个问题我自己的理解是:结构化输出是 LLM 和传统程序之间的"翻译层",AI 应用能不能成立,很大程度上就看这一环做得扎不扎实。

1)没有结构化输出,模型的回答只能"给人看"

自由文本的宿命是被人阅读。人可以容忍废话、可以脑补格式、可以忽略多余的表情符号,但程序不行——程序要的是确定性的数据结构。如果不做结构化控制,模型返回一大段散文,后端想从里面提取一个评分、一个链接,只能靠正则硬抠,脆弱到改一个措辞就崩。

2)有了结构化输出,模型才能"接入"业务流程

拿到稳定的 dict/list 之后,模型的输出才真正变成了程序可以消费的数据:

  • 直接入库,写进数据库表
  • 直接渲染,绑到前端页面的字段上
  • 直接驱动下游逻辑,比如按 score 排序过滤、按 tags 做推荐
  • 作为下一个 Agent / 下一次模型调用的输入,串成链路

这也呼应了上一篇对 AI 应用的定义:模型要在功能流程中参与理解、判断、推理和行动。而"参与流程"的前提,就是它的输出能被流程里的其他环节读懂——靠的就是结构化。

3)这一步是 AI 应用从 Demo 走向生产的关键分水岭

Demo 阶段,模型返回什么都能接受,截屏发群里就行;生产阶段,输出要进库、要展示、要触发动作,任何一次格式跑偏都是线上事故。所以前四个问题(怎么拿格式、prompt 约束、response_format、校验兜底)合起来,本质上就是在回答同一个问题:怎么把模型不确定的生成能力,变成系统里确定性的一环。

一句话总结:结构化输出不是一个锦上添花的技术细节,而是 LLM 能力接入真实业务的必经之路——它把"模型说的话"翻译成"程序能用的数据",AI 应用这个概念的落地,正是从这一环开始的。

6、结束语

这篇笔记围绕"单次模型请求数据结构化输出控制操作"展开。我以"一次请求的能力与不足"和架构选择的理由开篇,由此引出本文的核心问题:单次请求的表达能力有限,需要借助框架的约束手段,才能得到实际开发中需要的数据结构。为了达成这个目的,我引入了框架的概念——通过框架的能力,把原本杂乱无序的输出控制为目标数据结构。随后我用 Agently 框架给出了一个完整实例,与前面的概念前后呼应,以确保最大限度加深对结构化输出的认知。最后,我将开篇提出的问题逐一回答,把一个简单的 Demo 升级为企业级能力的分析。

从这篇文章开始,我也开始做了一些调整,将上一篇文章的多种方式转为一种我最熟悉的展示示例方式,并加入了注释,方便大家阅读代码,同时,也增加了提问开篇和末尾的问题回答的方式,以这种前后呼应的方法将内容层层递进。

下一篇会记录自己学习从请求开始到数据处理全流程,这篇是前两篇文章的总结和整合。

从注册模型 -> 注册架构 -> 数据结构化约束 -> 发出请求 -> 数据格式化 -> 数据验证 -> 打印符合要求数据, 以这个完整流程展示。

感谢大家能抽出宝贵的时间将文章读到最后,希望通过我的文章给大家带来一些帮助。

同样,我也是希望通过文章的手段将自己的学到的知识加深记忆,也在写文章的过程中希望将一些不太清晰的知识巩固下来。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐