需求拆解助手:OpenAI API 结构化输出实践

📅 2026/7/15 3:19:25
需求拆解助手:OpenAI API 结构化输出实践
需求拆解助手OpenAI API 结构化输出实践需求拆解助手的目标是把一段自然语言需求转换成后续系统可直接消费的结构化 JSON。真正需要解决的问题不是“模型能不能回答”而是模型输出是否具备稳定协议字段固定、类型明确、异常可恢复、结果可判断。围绕一个最小可运行脚本展开重点看结构化输出的完整链路输出协议、Prompt 约束、OpenAI 请求封装、JSON Schema、模型降级、JSON 解析、本地校验、修复重试和兜底返回。1. 输出协议先于 Prompt需求拆解的输入是一段需求文本输出固定为五个字段字段类型说明statusstringexecutable、needs_clarification或refusedbackgroundstring需求背景信息不足时说明缺失内容goalsstring[]可验收目标tasksstring[]执行任务或澄清动作risksstring[]范围、依赖、数据、安全、上线或验收风险status是协议里的关键字段。它把模型输出从“文本回答”变成“可分支处理的数据”executable表示可以继续拆解needs_clarification表示信息不足refused表示无法处理或不应处理。对应的BREAKDOWN_SCHEMA核心定义如下BREAKDOWN_SCHEMA{type:object,additionalProperties:False,properties:{status:{type:string,enum:[executable,needs_clarification,refused],},background:{type:string},goals:{type:array,items:{type:string}},tasks:{type:array,items:{type:string}},risks:{type:array,items:{type:string}},},required:[status,background,goals,tasks,risks],}这里有三个核心约束required保证关键字段不能缺。enum限制status只能取约定值。additionalProperties: False禁止模型输出未约定字段。先定义输出协议再写 Prompt 和调用代码才能让结构化输出具备工程边界。2. Prompt 只描述任务和边界Prompt 不需要堆砌复杂话术只需要把角色、任务、字段和边界说清楚你是一个需求拆解助手。请把用户输入的需求拆成结构化 JSON。 必须输出 JSON并且只输出 JSON。字段必须包含 - statusexecutable、needs_clarification 或 refused - background一句话概括背景信息不足时说明还缺什么背景 - goals目标数组写成可验收目标 - tasks任务数组写成可执行步骤 - risks风险数组覆盖范围、依赖、数据、安全、上线或验收风险 如果输入太短、目标不清或缺少上下文status 使用 needs_clarification 不要编造缺失事实把需要补充的问题放进 tasks 或 risks。这段 Prompt 的重点是最后两句。需求不完整时模型不能替用户补业务背景而是必须进入needs_clarification状态把缺失信息显式暴露出来。3. OpenAI 请求封装是核心入口结构化输出最终还是一次模型请求。脚本把请求拆成三层创建客户端、构造消息、发起调用。客户端从环境变量读取密钥和服务地址defcreate_client():fromdotenvimportfind_dotenv,load_dotenvfromopenaiimportOpenAI load_dotenv(find_dotenv(),overrideTrue)returnOpenAI(api_keyos.environ[OPENAI_API_KEY],base_urlos.environ[OPENAI_BASE_URL],)消息构造只做一件事把系统约束和用户需求放进 Chat Completions 的messagesdefbuild_messages(requirement):return[{role:system,content:SYSTEM_PROMPT},{role:user,content:f需求文本\n{requirement}},]真正的核心请求封装是request_breakdowndefrequest_breakdown(client,requirement,model,response_format):responseclient.chat.completions.create(modelmodel,messagesbuild_messages(requirement),temperature0,response_formatresponse_format,)returnresponse.choices[0].message.contentor这里有两个关键点temperature0降低输出随机性让同类需求的拆解结果更稳定。response_format由调用方传入可以是强约束的json_schema也可以是兼容降级的{type: json_object}。也就是说request_breakdown是整条链路里连接 Prompt、模型端点和结构化输出协议的入口。后面的 Schema、降级、修复逻辑都是围绕这次原始模型输出继续处理。4. 用 JSON Schema 约束模型输出请求模型时脚本优先使用response_format的json_schema模式defbuild_json_schema_response_format():return{type:json_schema,json_schema:{name:requirement_breakdown,strict:True,schema:BREAKDOWN_SCHEMA,},}这一步的作用是把自然语言约束转成接口级约束。Prompt 负责让模型理解任务Schema 负责让输出贴近调用方定义的数据结构。完整调用链路可以概括为需求文本 - messages - chat.completions.create - 原始文本 - JSON 解析 - 字段校验 - 最终结果只有经过解析和校验后的结果才应该进入后续业务逻辑。5. 兼容不同模型端点实际使用中并不是所有 OpenAI-compatible 端点都完整支持json_schema。因此脚本采用两层模型请求策略顺序模式作用1json_schema强约束输出结构2json_object退一步保证返回 JSON 对象核心代码如下try:raw_textrequest_breakdown(client,requirement,model,build_json_schema_response_format(),)exceptException:raw_textrequest_breakdown(client,requirement,model,{type:json_object},)这不是业务兜底而是模型能力兼容优先使用强约束端点不支持时再退回普通 JSON 模式。退回后仍然必须经过本地解析和字段校验。6. 先解析再校验模型返回的是字符串不是业务对象。即使使用了结构化输出脚本仍然先把文本转成 JSON再交给校验函数defparse_breakdown(text):datajson.loads(extract_json_text(text))returnvalidate_breakdown(data)extract_json_text处理两类常见情况模型返回纯 JSON或者返回 Markdown fenced JSON。defextract_json_text(text):valuetext.strip()ifvalue.startswith():linesvalue.splitlines()iflen(lines)3andlines[-1].strip():value\n.join(lines[1:-1]).strip()startvalue.find({)endvalue.rfind(})ifstart-1orend-1orendstart:raiseJSONDecodeError(No JSON object found,value,0)returnvalue[start:end1]这一层的作用是把“模型输出文本”转换成“待校验 JSON”。如果文本里找不到 JSON object会直接抛出JSONDecodeError进入后续 JSON 修复流程。7. 本地校验负责最后一道边界模型返回 JSON 不代表结果可信。脚本会先抽取 JSON再校验字段和类型defvalidate_breakdown(data):ifnotisinstance(data,dict):raiseValueError(模型输出不是 JSON object)statusdata.get(status)ifstatusnotinVALID_STATUSES:raiseValueError(status 必须是 executable、needs_clarification 或 refused)backgrounddata.get(background)ifnotisinstance(background,str)ornotbackground.strip():raiseValueError(background 必须是非空字符串)return{status:status,background:background.strip(),goals:validate_string_list(data,goals),tasks:validate_string_list(data,tasks),risks:validate_string_list(data,risks),}校验的价值在于把不稳定输出拦在业务入口之前。只要字段缺失、状态非法、列表类型不对就不能继续当作有效结果使用。8. 修复与兜底必须成体系结构化输出应用不能只考虑成功路径。这个脚本把异常处理分成三类场景处理方式结果输入为空不请求模型直接本地兜底needs_clarification模型返回非标准 JSON请求一次 JSON 修复修复后重新解析修复后仍不可用返回本地兜底结构固定字段完整返回主流程如下defanalyze_requirement(client,requirement,modelDEFAULT_MODEL):ifnotrequirement.strip():returnbuild_local_fallback(requirement,需求为空无法可靠拆解。)try:raw_textrequest_breakdown(client,requirement,model,build_json_schema_response_format(),)exceptException:raw_textrequest_breakdown(client,requirement,model,{type:json_object},)try:returnparse_breakdown(raw_text)except(JSONDecodeError,ValueError)asexc:try:repaired_textrequest_json_repair(client,raw_text,model)returnparse_breakdown(repaired_text)exceptException:returnbuild_local_fallback(requirement,f模型输出解析失败{exc})这里的 JSON 修复不是重新拆一遍需求而是把模型的原始输出修正为符合协议的 JSON再走同一套解析和校验逻辑defrequest_json_repair(client,raw_text,model):responseclient.chat.completions.create(modelmodel,messages[{role:system,content:(你是 JSON 修复器。把输入修复成符合需求拆解 schema 的 JSON只输出 JSON。),},{role:user,content:raw_text},],temperature0,response_format{type:json_object},)returnresponse.choices[0].message.contentor修复后的内容仍然会执行parse_breakdown(repaired_text)。如果字段缺失、类型错误或状态非法依然会被validate_breakdown拦截。也就是说修复只提供一次恢复机会不会绕过本地校验。兜底返回也必须遵守同一个输出协议defbuild_local_fallback(requirement,reason):summaryrequirement.strip()or未提供需求文本iflen(summary)80:summarysummary[:77]...return{status:needs_clarification,background:输入需求需要进一步澄清以下为保守拆解。,goals:[f澄清并确认需求目标{summary}],tasks:[补充业务背景和目标用户,明确验收标准和边界范围,确认依赖系统、上线时间和优先级,],risks:[reason],}这段逻辑的重点不是“随便返回一个错误”而是保证失败时也返回同一份结构。调用方不需要额外处理异常文本只需要根据status和risks继续分支。9. 小结需求拆解助手体现的是一条完整的 LLM 结构化输出链路协议设计 - Prompt 约束 - 请求封装 - Schema 输出 - JSON 解析 - 本地校验 - 修复重试 - 兜底返回其中最重要的是协议一致性。成功时返回结构化结果失败时也返回同样结构的needs_clarification。只有做到这一点模型输出才能稳定接入后续系统而不是停留在一次性的文本回答。