news 2026/9/8 18:04:36

大模型结构化输出完整链路:从请求到可靠数据的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型结构化输出完整链路:从请求到可靠数据的工程实践

先说一个我自己的感受。最近半年做 AI 应用落地,几乎每天都在跟“单次模型请求与数据结构化输出完整链路”打交道。表面上看,这事不就是把用户输入发给大模型,拿到返回结果再丢给下游吗?可真到了生产环境,你会发现这条链路里每一个环节都能把你绊一跤。提示词写得不够稳,模型给你吐一堆废话;JSON 格式没约束好,字段说缺就缺;网络超时、输出截断、类型错误……任何一个环节没兜住,线上服务就直接崩了。

这篇文章我会从一次真实的模型请求出发,把从输入组装、请求发送、结构化约束、响应解析、校验兜底到异常排查的完整链路拆开讲透。没有炫技,全部是能直接复用的工程方案和踩坑记录。适合刚接触大模型 API 的初学者,也适合那些已经接入了接口但被脏数据折磨的开发者。看完你至少能少走三个月的弯路。

1. 一条完整链路:从用户输入到可靠数据的旅程

1.1 链路不是“发请求收结果”这么简单

很多人第一次接入模型接口时,脑子里对链路的想象是一条直线:“收到请求 → 调模型 → 拿结果 → 返回”。实际上,真实生产环境里的完整链路要复杂得多,大概可以拆成六个阶段:

  • 输入准备:把用户原始输入转换成模型能理解的结构,包括系统提示词、用户消息、历史上下文、需要输出的字段定义。
  • 请求发送:配置模型名称、温度、最大 token 数、超时时间等参数,真正调用远端模型服务。
  • 结构化约束:通过 System Prompt、JSON Schema、Function Calling 等机制,告诉模型“我要的是 JSON,不是聊天文本”。
  • 响应接收:拿到模型返回后,先判断 HTTP 状态、网络是否正常、是否触发限流。
  • 内容解析:把模型返回的文本或工具调用参数解析成内存中的数据结构。
  • 校验与兜底:对字段缺失、类型错误、枚举越界、逻辑矛盾做二次校验,必要时触发修复或重试,最终输出一份可信数据交给业务逻辑。

上面任何一步出了岔子,整条链路都会产生脏数据。我见过最典型的场景是:解析层只做了json.loads,模型偶尔多输出一段解释文字,直接把整个服务打挂。这类问题看日志的时候非常费劲,因为你以为问题在解析层,其实源头在提示词约束不严。

1.2 为什么必须追求结构化输出,而不是让模型“自由发挥”

大模型本质上是一个概率性的文本生成器,它擅长的东西是“接话”,不是“严格按数据库字段返回”。早期不少团队直接让模型用自然语言回答,再把文本丢给正则去匹配,结果就是维护成本极其夸张。

举个例子,你在做一个客服工单自动分类系统,如果模型返回的是“这个用户反馈说物流太慢,想要退货,看起来挺着急的”,你后续要做意图识别、紧急程度判断、关联订单号,就全部要再做一轮 NLP 解析,准确率还不可控。反过来,如果你在一开始就要求模型输出下面这样的 JSON:

{ "order_id": "SO-20250112-001", "issue_category": "物流", "urgency": "高", "need_refund": true, "summary": "用户反馈快递已到达三天但未派送,要求尽快处理", "tags": ["物流慢", "催派送"] }

下游系统几乎不用做任何适配,直接就能落库、告警、流转工单。所以“数据结构化输出”从来不是一个锦上添花的需求,而是把大模型从“聊天玩具”变成“可靠数据生产者”的核心关卡。而单次请求的质量,直接决定了这条结构化链路能不能在真实业务中站稳脚跟。

2. 结构化输出的主流方案与选型逻辑

如果你去看各大模型服务商的技术文档,会发现“结构化输出”有好几种叫法,有的叫 JSON Mode,有的叫 Structured Output,有的叫 Function Calling。虽然名字不同,背后的设计思路都可以归纳成三种。

2.1 方案一:提示词约束 + 客户端强制解析

这个方案看起来最简单:在 System Prompt 里写死“你必须严格输出 JSON,不要输出任何其他内容”,然后在客户端调用时指定response_format={"type": "json_object"}。模型在采样过程中会收到一个额外的语法约束,使得输出尽可能往合法 JSON 方向靠拢。

它的优点是通用性强,几乎任何模型服务商都支持这种方式,实现成本极低。但缺点也很明显:JSON 内部的字段结构、类型、必填项全靠提示词约定,模型约束力度有限。你告诉它urgency只能是“低/中/高”,它心情不好还是可能给你一个“有点着急”。而且有些模型会为了凑 JSON 结构,自己发挥一些你根本没用过的字段名,解析层一不注意就拿到一个意外的 key。

2.2 方案二:Function Calling / Tools 机制

Function Calling 的思路比较巧妙。你不需要让模型直接回答“数据是什么”,而是给模型定义一个或多个“工具函数”。当你问“帮我提取订单信息”时,模型的回复不是一个 JSON 字符串,而是一个结构化的工具调用请求,比如get_order_info(order_id="SO-001")。调用的参数本身就是结构化的,而且由模型服务端做了格式约束。

我早期做工单系统时用的就是这种方案。定义一个create_ticket(order_id, category, urgency, ...)函数,把每个字段的类型、枚举值、描述都写清楚,模型在调用的时候会尽量按照你的 Schema 来填参数。相比纯提示词,稳定性提升了一个档次。缺点是接入成本稍微高一点,而且某些平台的 Function Calling 实现有字段上限,参数很多时偶尔会出现静默截断。

2.3 方案三:原生 JSON Schema 约束(Structured Outputs)

这是目前我认为最接近“根治”的方案,也是 OpenAI 在 2024 年下半年开始推广的 Structured Outputs。它允许你直接传入一个 JSON Schema,模型在解码阶段就严格遵循这个 Schema 来逐 token 生成内容。简单说,模型不是“试着输出合法 JSON”,而是“被约束只能输出符合 Schema 的合法 JSON”,从生成机制上规避了格式错误。

下面是同一份订单提取需求的 Schema,和方案一里的“靠嘴说”完全是两个量级:

{ "name": "extract_order", "schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式形如 SO-20250112-001" }, "issue_category": { "type": "string", "enum": ["退款", "物流", "质量问题", "其他"] }, "urgency": { "type": "string", "enum": ["低", "中", "高"], "description": "根据用户情绪与时效要求判断" }, "need_refund": { "type": "boolean" }, "summary": { "type": "string", "description": "用一句话概括用户问题" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "问题标签,最多 3 个" } }, "required": ["order_id", "issue_category", "urgency", "need_refund", "summary"] } }

模型在生成need_refund时,只会输出truefalse,而不是“是”“要”“肯定要退”之类的自然语言。这种确定性带来的收益,到了下游数据清洗和自动化决策阶段会被无限放大。

2.4 选型建议:别一上来就上最重的方案

很多开发者一看到 JSON Schema 就热血沸腾,恨不得所有场景都切过去。但我的实际经验是,不同场景应该选不同方案:

场景类型推荐方案理由
简单文本分类、情感判断提示词 + 客户端解析字段少,成本低,不需要额外复杂配置
多字段信息抽取、数据入库Function Calling / JSON Schema可控性高,字段校验严格
需要模型决定调用哪个工具的 Agent 场景Function Calling天然支持多工具分流
极端敏感的支付、医疗数据结构化JSON Schema + 后端二次校验生成约束 + 业务规则双重保障

有一类特殊场景要提醒:如果你用的模型服务商不支持 JSON Schema,却硬要在客户端模拟,往往是自欺欺人。提示词写得再漂亮,模型该乱写还是乱写,这时候不如老老实实用 Function Calling,或者干脆把 Schema 里的核心枚举值全部塞进提示词并配合少量案例(few-shot),先保证能用,再追求完美。

3. 实操过程:手写一次完整的结构化请求调用

理论部分讲再多,不如把链路亲手跑一遍。下面我以一个“电商客服工单信息自动抽取”真实案例为准,带你走一遍完整实现。

3.1 环境准备与客户端配置

假设你已经具备 Python 环境和 API Key。我习惯将所有模型相关配置放到环境变量中,避免代码里硬编码。安装依赖只需要一个官方客户端库:

pip install openai

创建config.py放公共配置:

import os client = OpenAI( api_key=os.getenv("MODEL_API_KEY"), base_url=os.getenv("MODEL_API_BASE") ) MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini") TIMEOUT_SECONDS = 30 MAX_RETRIES = 2

这里把base_url单独留出来,是因为很多团队的模型服务是自建网关,并不会直接连官方域名。后续只要切换环境变量,就能在测试和生产环境之间平滑迁移。超时和重试参数务必统一管理,不要散落在各个调用点。

3.2 输入组装:System Prompt 与 Schema 配合

输入组装是整个链路的地基。我的做法是把 System Prompt 拆成“角色 + 任务 + 输出红线”三段,然后再单独挂 Schema。这样后续要调节模型行为时,不用翻整个 Prompt。

system_prompt = """ 你是电商客服工单系统的信息抽取助手。 你需要从用户对话中提取结构化工单信息,并严格按照 JSON Schema 输出。 输出红线: 1. 只输出符合 Schema 的 JSON,不要输出任何解释、前缀、后缀。 2. 如果用户消息中缺少某个必填字段,请基于上下文合理推断,不要臆造无法确定的订单号。 3. 当用户情绪激烈或明确要求尽快处理时,urgency 标记为"高"。 4. summary 必须是一句对用户问题的主旨概括,控制在 30 字以内。 """.strip() user_message = ( "我的订单 SO-20250112-001 显示已经在派送了," "但是已经三天没动静,快递员电话也打不通," "再不来我就申请退款了,真的很气人。" )

用户消息故意写得口语化,且夹带订单号、退款意图、强烈情绪,模型需要同时完成实体抽取、意图判断、情感分级三个任务。注意,此时千万不要把 user 消息写成“请帮我提取订单号、分类、紧急程度……”,那样会让模型陷入解释模式,总想先复述任务再给结果。

3.3 发起单次模型请求:结构化参数注入

如果你使用的是支持 JSON Schema 的接口,可以这样发起请求:

schema = { "type": "json_schema", "json_schema": { "name": "ticket_extraction", "strict": True, "schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,若无法确定则置为 null" }, "issue_category": { "type": "string", "enum": ["退款", "物流", "质量问题", "其他"] }, "urgency": { "type": "string", "enum": ["低", "中", "高"] }, "need_refund": { "type": "boolean" }, "summary": { "type": "string" } }, "required": [ "order_id", "issue_category", "urgency", "need_refund", "summary" ], "additionalProperties": False } } } resp = client.chat.completions.create( model=MODEL_NAME, temperature=0.1, max_tokens=512, timeout=TIMEOUT_SECONDS, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ], response_format=schema, )

几个参数的选择逻辑我说一下:

  • temperature=0.1:结构化抽取任务是确定性的信息转换,不是创意写作,温度越低越稳定。这里不能设成 0,因为部分模型中温度 0 会导致采样规律过于固化,偶尔出现重复片段,0.1 到 0.2 是我常用区间。
  • max_tokens=512:工单信息字段有限,512 个 token 理论上足够。设得太大会增加成本,设得太小容易出现输出截断,这个值最好是估算的 1.5 倍以上。
  • response_format=schema:关键所在,它会让服务端在解码阶段就约束模型输出。不是所有服务都支持这种传法,不支持时降级用response_format={"type": "json_object"},或者在提示词里做兜底。

3.4 响应后处理:解析、补全与业务校验

拿到响应后,不要直接相信resp.choices[0].message.content就是一个合法 JSON。稳妥的解析逻辑应该是:

import json from pydantic import BaseModel, Field, ValidationError class TicketInfo(BaseModel): order_id: str | None = Field(default=None, description="订单号") issue_category: str urgency: str need_refund: bool summary: str content = resp.choices[0].message.content.strip() try: data = json.loads(content) except json.JSONDecodeError: # 极小概率仍会遇到被围栏包裹或混入杂质的情况 content = content.removeprefix("```json").removeprefix("```").removesuffix("```").strip() data = json.loads(content) try: ticket = TicketInfo(**data) except ValidationError as e: print("字段校验失败,原始数据:", data) print("错误详情:", e.json()) raise

两个关键点:

第一,additionalProperties虽然没有传False,但服务端如果已经严格约束,模型不会多增字段。不过一旦你换了不支持的模型服务商,这条防线就得靠 Pydantic 补上。Pydantic 的模型默认会忽略多余字段,不会抛错,所以最好在配置里把model_config设置为extra="forbid"

第二,order_id允许为null,这是刻意的。模型如果实在抽不到订单号(比如用户压根没提),编一个假订单号比返回 null 危害大得多。下游业务如果发现order_id为空,可以走人工补录流程,而不是拿着一个假 ID 去查订单系统。

3.5 完整调用函数的整合

把上面的环节组装成一个独立函数,方便多个业务方复用:

def extract_ticket(user_message: str) -> TicketInfo: resp = client.chat.completions.create( model=MODEL_NAME, temperature=0.1, max_tokens=512, timeout=30, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ], response_format=schema, ) content = resp.choices[0].message.content.strip() data = json.loads(content) return TicketInfo(**data)

写到这里,一个最小可运行的单次模型结构化请求链路就通了。不过,真实业务里链路不会只跑一次就完事,你还会面对各种莫名其妙的边界情况。

4. 实测高频问题与排查思路

下面这些问题是我在生产环境里遇到过的,或者帮别人排查时见过的,每个都伴随真实代价。整理成速查表方便你对照。

现象直接原因根因解决方案
JSON 解析报错,内容里出现 ```json 围栏提示词没写死,模型按 Markdown 习惯输出使用了不严格的响应格式模式response_format强制 JSON;解析层增加围栏剥离兜底
字段缺失,比如没有order_id模型自行判断“不确定就省略”Schema 未标记 required,或提示词未强调缺失处理策略Schema 中显式声明 required;允许 null 但禁止缺 key
urgency返回了“一般着急”提示词没给枚举边界只写了“请返回紧急程度”,没给出候选值用 JSON Schema 的 enum,并把候选值描述写清楚
输出被截断,JSON 不完整max_tokens设置过小估算不足按最大字段长度估算后乘 1.5,再留余量
偶发网络超时或 5xx远端服务不稳定未做重试启用客户端内置重试策略,配合指数退避
返回字段多出extra_info模型自行发挥Schema 未设置additionalProperties: falseSchema 中显式关闭额外字段,或解析层过滤

接下来挑三个典型问题详细展开。

4.1 模型输出被 Markdown 围栏包裹

这个问题在接入一些国产模型或开源模型时特别常见。即使你在 Prompt 里写了“只输出 JSON”,模型也可能因为训练数据中大量存在代码块,最后给你一个这样的结果:

```json { "order_id": "SO-20250112-001" } ```

当客户端把整段拿去json.loads,直接抛JSONDecodeError。初步排查时你可能会怀疑网络或接口问题,但打印原始内容才发现多了三行围栏。

解决办法分两层。第一层是在请求时启用response_format的 JSON 模式,让服务端帮你过滤掉非 JSON 的生成路径;第二层是在解析层做一次“体检”:如果content{开头且以}结尾,直接解析,否则剥离首尾的 ``` 围栏后再解析。防御式编程在这里不是过度设计。

4.2 Schema 声明了 required 却仍然缺字段

严格模式下,服务端既然承诺了生成 Schema 合法,理论上不会缺 required 字段。但我踩过一个坑:某个模型网关并没有真正把 Schema 传给底座模型,只是在外层做了一次“看起来像是校验”的包装。结果是每次响应都是合法的 JSON,但内部字段随机缺失,有时有order_id,有时没有。

排查这类问题,不能只看 Surface Level 的 JSON 是否合法,还要对每个 required 字段做存在性断言。我现在的做法是引入 Pydantic 做强类型模型,字段缺失直接抛校验错误,让上层调用方感知到“这次结果不可信”。必要时再用带占位符的消息重新请求一次,例如提示模型“你上次的回复缺少订单号,请根据原对话补充完整”。这种自动修复机制大概能挽回 60% 以上的失败请求。

4.3 响应 JSON 合法,但内容与业务冲突

最隐蔽的问题往往不是格式错误,而是语义漂移。有一次我们让模型提取“退款原因”,结果它把用户对快递员的不满情绪提取成了“产品损坏”。格式完全合法,Schema 校验全过,但数据一旦进入工单系统就会产生错误流转。这类问题无法靠格式约束解决,必须在链路里增加“语义校验层”。

我的经验是在 Schema 描述上下足功夫,把每个字段的业务边界写透。比如退款原因字段描述不要只写“退款原因”,要写成“用户明确表达的退款动机,区分物流类、产品类、服务类,不得把物流不满归类为产品损坏”。如果业务允许,还可以在 System Prompt 里加入一正一反两个 few-shot 示例,模型对边界的把握会明显变强。

5. 链路工程化:稳定性、可观测性与降级方案

5.1 给每次请求一个可追踪的 request_id

单次模型请求在链路里不是一个孤立事件。它上游承接用户输入,下游联动工单系统,中间出问题时要能快速定位到是哪个环节、哪个参数、哪条 Prompt 导致。所以我都会在封装函数入口生成一个request_id(UUID),随业务上下文一路透传,并打进日志。

import uuid def extract_ticket_with_trace(user_message: str, request_id: str | None = None): rid = request_id or uuid.uuid4().hex logger.info("[%s] 开始抽取请求,输入长度=%d", rid, len(user_message)) start_ts = time.time() try: resp = client.chat.completions.create(...) logger.info("[%s] 模型调用成功,耗时=%.2fs", rid, time.time() - start_ts) ticket = TicketInfo(**json.loads(resp.choices[0].message.content)) logger.info("[%s] 结构化结果为: %s", rid, ticket.model_dump_json()) return ticket except Exception as e: logger.error("[%s] 链路失败: %s", rid, e) raise

日志里有了request_id,再配合 Trace ID 打到模型服务商的排查后台,定位问题就变得很快。这个步骤看起来琐碎,却是把“单次请求质量”提升为“系统 SLA”的关键一步。

5.2 模型输出质量抽检与回归

链路跑通只是起点,模型的输出质量会随时间波动。今天 Prompt 表现很好,下周模型服务商更新了底座版本,或者某个线上案例出现了新的表达方式,都可能让抽取准确率掉几个点。

我建议在链路旁路搭一个“影子日志库”,每次请求的原始输入、模型原始输出、最终结构化结果都存一份。每天用规则或轻量级脚本对样本做一次人工抽检,统计字段抽取准确率、分类一致率、语义漂移率。一旦发现劣化趋势,就回到 Prompt 或 Schema 上做针对性调优,并在测试集上跑回归。没有这个环节,你很难发现质量劣化,通常是业务方先抱怨,你才知道出了问题。

5.3 超时、重点与缓存降级

模型接口不稳定,是不少团队上线后才意识到的问题。单次调用 30 秒甚至更久不返回,放在同步接口里就是灾难。我的建议是:

  • 设置合理的超时:一般业务场景 15~30 秒较长,超过直接失败,不要让请求无限挂起。
  • 启用自动重试:对网络错误、5xx、限流状态码重试 2 次,使用指数退避。但注意,重复请求如果模型已经成功但响应丢失,可能会产生重复数据,所以在写操作类业务上要配合幂等键。
  • 增加缓存层:对完全相同的输入做短时间缓存,降低调用成本和延迟。用户重复提交相同问题时,直接命中缓存。
  • 准备降级方案:当模型服务整体不可用时,退回规则引擎或人工处理队列。别让核心业务完全依赖第三方模型接口,这是我在生产事故中学到的最深的一课。

工程化链路的意义就在于,模型能力再强,没有稳定性和可观测性托底,生产环境依然像走钢丝。

6. 一些不会写进官方文档的实战体会

做到最后,我想分享几个只会在长期调接口过程中感受到的东西。

第一个体会是:Prompt 写得再好,也不如把 Schema 和校验逻辑做扎实。前几个月我做客服系统时,80% 的解析问题都出在“我以为模型不会犯错”这个念头上面。后来把 Schema 的additionalProperties全部设成false,再叠加 Pydantic 强类型校验,脏数据率直接降了一个数量级。不要奢望模型“听话”,要用机制把它的乱来空间收窄。

第二个体会是:单次请求的链路设计,一定要从“下一次会被复用”的角度去思考。今天你只在工单场景做信息抽取,明天可能要做到邮件自动分类、售后归因分析、评论情感洞察。如果每个场景都重新写一套解析逻辑,维护成本会像滚雪球一样膨胀。把“输入 Schema + 输出强校验 + 重试修复 + 日志追踪”这套骨架沉淀成公共库,新场景接入只需要换 Prompt 和 Schema,能省下大量重复劳动。

第三个体会是:永远要留一条人工兜底的路。任何基于概率模型的结构化输出,无论约束多严格,理论上都有失败的可能。对低风险场景,失败后重试两三次就够了;对高风险场景,设置置信度阈值或人工确认机制,可能比继续逼模型更靠谱。

我后来在项目里还经常会加一个小技巧:让模型同时返回一个confidence_score,表示它对抽取结果的自信程度。低于 0.6 的记录自动进入人工复核池。这个字段虽然简单,却让链路从“完全自动化”变成了“自动化 + 可干预”,线上问题少了很多。如果你正在设计类似的系统,我建议也留出这样一个软性字段。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 18:04:19

FPGA测控程序架构设计:从分层框架到数据流的工程实践

做FPGA的工程师,基本都逃不过测控程序这类需求。我这里说的测控程序,不是视频流或者高速基带那种一路流水处理,而是指要跟外部设备握手、听上位机使唤、把测量结果和控制反馈按节奏送出去的一套逻辑工程。代码量往往不大,可一旦命…

作者头像 李华
网站建设 2026/9/8 18:02:50

AI Skills实战:在腾讯云上让Agent按流程稳定干活

写Agent写了快两年,我最大的体会是:别人口中"啥都能干"的Agent,一落到自己的业务里就原形毕露。让它写个段子没问题,让它按固定流程处理数据、调用内部接口、产出符合规范的报告,它就开始自由发挥&#xff0…

作者头像 李华
网站建设 2026/9/8 18:02:33

writing-skills - discipline

name: discipline-name description: >- 当 [违规前情况] 时使用。 metadata: category: discipline triggers: 新功能、代码变更、实现 规则名称 铁律 [单句绝对规则] 违反字面规定即是违反精神。 规则 始终 [步骤 1]绝不 [步骤 2][步骤 3] 违规 [规则之前的操作]&#xff…

作者头像 李华
网站建设 2026/9/8 18:00:56

5.8头文件

头文件包含函数原型&#xff0c;数据类型和常量。宏在第13章讲。用户自定义头文件用#include预处理指令#include "square.h"13.2呈现更多细节。<assert.h>包含添加诊断测试辅助程序调试的信息。<ctype.h>测试字符某些特性的函数原型&#xff0c;以及字母…

作者头像 李华
网站建设 2026/9/8 18:00:00

海康门禁对讲设备技能接入萤石蓝海AIoT一站式工作台:4款终端多端应用一站生成

一、引言萤石蓝海AIoT一站式工作台新增四款海康门禁对讲产品线设备技能接入——可视对讲门口机、可视对讲室内机、人员通道闸机、护士站终端&#xff0c;覆盖通行认证、可视对讲、信息发布、呼叫管理、防区报警五大核心能力域。开发者通过技能组合与AI生成&#xff0c;可快速搭…

作者头像 李华