The Responses API and Built-in Tools: Function Calling, Web Search / File Search / Computer Use, Structured Output
Look one layer below a finished product like Codex: how the Responses API organizes the model, tools, and multi-turn state, and exactly how it differs from Chat Completions.
今日目标
- 能说清 Responses API 与 Chat Completions 在输入形态、输出形态、状态管理上的三处区别
- 能写一个带函数调用与 web search 的最小脚本,并把 function_call_output 正确回填
- 能用 json_schema 严格模式拿到结构化输出,并处理 refusal 分支
前两天你用的是成品。今天拆开看一层:Codex 这类产品跟模型说话用的接口是什么样的,工具是怎么接进去的,多轮状态放在哪。读完做完,回到页面顶部把三条目标勾掉。
小白版讲解
从 messages 到 items:换掉的不只是字段名
还是两位搭档的类比。你给两家公司各派了一件活,两家的工单格式不一样。老格式(Chat Completions)是一张聊天记录:一条条消息,每条标着谁说的;新格式(Responses API)更像一份工作日志:里面不只有「说了什么」,还有「查了什么」「调了哪个工具」「工具回了什么」,每一项都是一个带类型的条目。OpenAI 把后者叫 items——输入是 items 的数组,输出也是 items 的数组,消息只是其中一种类型。
三处差别值得记准,因为它们决定了你写代码的方式:
- 输入形态:Chat Completions 收
messages数组,系统提示是数组里的第一条;Responses 收input(可以是一个字符串,也可以是 items 数组),系统级指令单独放在顶层的instructions字段。 - 输出形态:Chat Completions 返回
choices,你要取choices[0].message.content;Responses 返回output数组,里面按顺序是模型这一轮做的每件事(工具调用、搜索、消息),SDK 额外给了一个output_text助手直接拿到最终文本。 - 状态管理:Chat Completions 是无状态的,历史每轮由你重发;Responses 默认
store为 true,会把这次响应存起来,下一轮只要传previous_response_id就能接着聊,不用重发历史。你也可以关掉存储自己维护历史——两种做法的取舍最后一节讲。
最小的一次调用长这样:
import OpenAI from 'openai'
const client = new OpenAI() // 读环境变量 OPENAI_API_KEY
const response = await client.responses.create({
model: process.env.OPENAI_MODEL ?? '',
instructions: '你是一个 TODO API 项目的助手,回答简短。',
input: '用一句话解释什么是输入校验',
})
console.log(response.output_text)import os
from openai import OpenAI
client = OpenAI() # 读环境变量 OPENAI_API_KEY
response = client.responses.create(
model=os.environ["OPENAI_MODEL"],
instructions="你是一个 TODO API 项目的助手,回答简短。",
input="用一句话解释什么是输入校验",
)
print(response.output_text)跟 30 天课 D1 那个 Chat Completions 请求对着看:messages 变成了 input 加 instructions,choices[0].message.content 变成了 output_text。模型 id 这门课一律从环境变量 OPENAI_MODEL 读——OpenAI 的型号更新很快,写死在正文里几个月就过期,以官方文档当前推荐为准。
为什么要换格式?因为 Chat Completions 的「聊天记录」模型装不下工具。当模型不只是回话,还要调工具、搜网页、读文件时,把这些动作硬塞进「消息」里会很别扭。items 让每一种动作都有自己的类型,这也是下面几节要讲的内置工具能顺畅接入的前提。
函数调用:声明、请求、回填
搭档需要查你们内部的东西时,他不能自己查,得让你去查再把结果告诉他。函数调用(function calling)就是这个来回:你声明有哪些函数可以调、参数长什么样;模型判断需要时请求调用某个函数并给出参数;你的代码真的去执行,把结果回填给模型,模型再基于结果继续。
三步分别对应三种 item。声明是请求里 tools 数组的一项,类型 function,带 name、description、JSON Schema 形式的 parameters,以及 strict: true 表示参数必须严格符合 schema。模型的请求是输出里一个 function_call 类型的 item,带 call_id(这次调用的编号)和 arguments(JSON 字符串)。回填是你在下一轮的 input 里放一个 function_call_output item,call_id 对上,output 是结果字符串。
import OpenAI from 'openai'
const client = new OpenAI()
const model = process.env.OPENAI_MODEL ?? ''
const todos = [{ id: 1, title: '给 POST /todos 补校验', done: false }]
const tools: OpenAI.Responses.Tool[] = [
{
type: 'function',
name: 'list_todos',
description: '列出当前所有 TODO',
parameters: { type: 'object', properties: {}, additionalProperties: false },
strict: true,
},
]
const first = await client.responses.create({
model,
tools,
input: '现在还有哪些没做完的事?',
})
// 找出模型请求的函数调用,真的去执行,再把结果回填
const calls = first.output.filter((item) => item.type === 'function_call')
const outputs = calls.map((call) => ({
type: 'function_call_output' as const,
call_id: call.call_id,
output: JSON.stringify(todos.filter((t) => !t.done)),
}))
const second = await client.responses.create({
model,
tools,
previous_response_id: first.id, // 接着上一轮,历史不用重发
input: outputs,
})
console.log(second.output_text)import json
import os
from openai import OpenAI
client = OpenAI()
model = os.environ["OPENAI_MODEL"]
todos = [{"id": 1, "title": "给 POST /todos 补校验", "done": False}]
tools = [
{
"type": "function",
"name": "list_todos",
"description": "列出当前所有 TODO",
"parameters": {"type": "object", "properties": {}, "additionalProperties": False},
"strict": True,
}
]
first = client.responses.create(model=model, tools=tools, input="现在还有哪些没做完的事?")
# 找出模型请求的函数调用,真的去执行,再把结果回填
outputs = [
{
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps([t for t in todos if not t["done"]], ensure_ascii=False),
}
for item in first.output
if item.type == "function_call"
]
second = client.responses.create(
model=model,
tools=tools,
previous_response_id=first.id, # 接着上一轮,历史不用重发
input=outputs,
)
print(second.output_text)注意两个工程细节。第一,call_id 必须原样对上,模型靠它把结果和请求配对,一轮里请求了三个函数你就要回填三个。第二,strict: true 加上 additionalProperties: false 是一对,前者让模型的参数严格符合 schema,后者禁止多出字段——少了任何一个,你就得在代码里防御性地处理奇形怪状的参数。这两行代码换来的是:参数解析这一层基本不用写 try/catch。
回头看 D1:Codex 读文件、跑命令,底层就是这个来回,只是它的工具是 shell 和文件系统,而且循环由它自己转。你现在手里这个二十几行的脚本,就是一个最小的「Agent 循环」的一次迭代。
内置工具:web search 与 file search 是平台替你跑的
上一节的函数由你执行。有一类工具太通用了,OpenAI 干脆在服务端替你执行,你只要在 tools 里声明一下——这就是内置工具(built-in tools)。声明方式和函数工具在同一个数组里,区别是 type 不同,而且不需要你回填任何东西。
web search:{ type: 'web_search' }。模型判断需要时自己搜,输出里会多一个 web_search_call item 记录它搜了什么,最终的 message item 里带 annotations,每条 url_citation 指向一个来源。可以用 search_context_size(low / medium / high)控制拉多少网页内容进上下文,用 filters.allowed_domains 限制只搜某些站——做企业内知识问答时这一条很实用。
file search:先把文件上传并放进一个向量库(vector store),然后声明 { type: 'file_search', vector_store_ids: ['...'], max_num_results: 3 }。模型需要时去库里检索,输出里多一个 file_search_call item,消息里的 annotations 是 file_citation。在请求里加 include: ['file_search_call.results'] 可以把检索到的原文片段也一起返回,方便调试「它到底看到了什么」。这基本是一个托管版的 RAG,30 天课 D12 手写的那条流水线,这里被压成了一个字段。
import OpenAI from 'openai'
const client = new OpenAI()
const response = await client.responses.create({
model: process.env.OPENAI_MODEL ?? '',
tools: [{ type: 'web_search', search_context_size: 'low' }],
input: 'Express 里做请求体校验,目前社区常用哪几个库?给出来源。',
})
console.log(response.output_text)
// 把引用来源单独列出来:annotations 在 message item 的 content 里
for (const item of response.output) {
if (item.type !== 'message') continue
for (const part of item.content) {
if (part.type !== 'output_text') continue
for (const a of part.annotations) {
if (a.type === 'url_citation') console.log('来源:', a.url)
}
}
}import os
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model=os.environ["OPENAI_MODEL"],
tools=[{"type": "web_search", "search_context_size": "low"}],
input="Express 里做请求体校验,目前社区常用哪几个库?给出来源。",
)
print(response.output_text)
# 把引用来源单独列出来:annotations 在 message item 的 content 里
for item in response.output:
if item.type != "message":
continue
for part in item.content:
if part.type != "output_text":
continue
for a in part.annotations:
if a.type == "url_citation":
print("来源:", a.url)内置工具和自己写的函数怎么选?口径是:数据在外面且通用的(公网、你上传的文档)用内置工具,数据在你系统里的(数据库、内部服务、业务逻辑)写函数。内置工具省掉了执行和回填,但你控制不了它怎么搜、搜到什么;函数工具样样要自己写,但每一步都在你手里。生产系统里两者几乎总是混用。
computer use:让模型操作界面,以及为什么要关进隔离环境
再往前一步:如果任务是「打开这个后台,把这批工单的状态改掉」,没有 API 可调,只有一个网页界面呢?这就是 computer use 要解决的问题——让模型看屏幕截图、决定点哪里、输入什么。官方文档目前给了两条路:一是让模型写代码去操作界面(比如生成 Playwright 脚本,在隔离环境里执行,环境在多次调用间保持),二是用 { type: 'computer' } 这个计算机工具,模型返回结构化的动作(click、type、scroll、keypress、screenshot 等),你的代码执行后把新的截图送回去,循环往复。
今天不给这段代码,原因是它不该出现在一个「最小脚本」里。文档反复强调的是:把它关进隔离的浏览器或虚拟机,并给一个允许访问的站点与动作清单。一个能点击任何按钮、输入任何文字的模型,在你的日常桌面上是不能跑的——它可能点到「删除全部」,也可能被网页上的一段文字诱导去做别的事(这就是提示词注入在界面操作上的版本)。所以 computer use 的正确起点不是代码,是环境:一个一次性的容器、一个受限的账号、一个白名单。
把它放在这里讲,是为了让你看到内置工具的完整光谱:从最安全的 web search(只读公网),到 file search(只读你给的文件),到函数调用(你写的代码、你控制副作用),再到 computer use(模型直接产生副作用)。越往右能力越强,需要的隔离也越重。D1 讲的 Codex 沙箱,就是这条光谱上「让模型执行 shell」那一格的隔离方案。
结构化输出:strict 模式、parse 助手与 refusal
回到搭档。你让他汇报进度,他写了一大段散文,你还得从里面把数字挑出来——不如给他一张表让他填。结构化输出(structured output)就是那张表:你给一个 JSON Schema,模型的输出保证符合它。
Responses API 里写在 text.format 字段:{ type: 'json_schema', name, schema, strict: true }。strict 是关键——开了它,输出保证能通过 schema 校验,不再是「大概率是合法 JSON」。两边 SDK 都有助手让你少写 schema:TypeScript 用 zodTextFormat 把 zod schema 转过去,Python 直接传 pydantic 模型;然后用 responses.parse 代替 create,结果在 output_parsed 里已经是解析好的对象。
import OpenAI from 'openai'
import { z } from 'zod'
import { zodTextFormat } from 'openai/helpers/zod'
const client = new OpenAI()
const Review = z.object({
verdict: z.enum(['approve', 'request_changes']),
issues: z.array(z.object({ file: z.string(), line: z.number(), note: z.string() })),
})
const response = await client.responses.parse({
model: process.env.OPENAI_MODEL ?? '',
input: '审查这段 diff:……(略)',
text: { format: zodTextFormat(Review, 'review') },
})
// 先看有没有拒答:refusal 是一种独立的 content 类型,不是解析失败
const refused = response.output
.filter((item) => item.type === 'message')
.flatMap((item) => item.content)
.find((part) => part.type === 'refusal')
if (refused) {
console.log('模型拒绝了:', refused.refusal)
} else {
console.log(response.output_parsed?.verdict, response.output_parsed?.issues.length)
}import os
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class Issue(BaseModel):
file: str
line: int
note: str
class Review(BaseModel):
verdict: str # approve / request_changes
issues: list[Issue]
response = client.responses.parse(
model=os.environ["OPENAI_MODEL"],
input="审查这段 diff:……(略)",
text_format=Review,
)
# 先看有没有拒答:refusal 是一种独立的 content 类型,不是解析失败
refused = next(
(
part
for item in response.output
if item.type == "message"
for part in item.content
if part.type == "refusal"
),
None,
)
if refused:
print("模型拒绝了:", refused.refusal)
else:
review = response.output_parsed
print(review.verdict, len(review.issues))refusal 这个分支容易被忽略。当模型因为安全原因拒绝回答时,它不会硬塞一个不合法的 JSON 给你,而是返回一个 refusal 类型的内容块。不处理它,你的代码会拿到一个 output_parsed 为空的响应然后在下游炸掉;处理了,你就能把「模型不愿意」和「模型没做对」区分开——这在做审查、分类这类自动化流水线时是必需的。
strict 模式解决了「格式对不对」,没解决「内容对不对」。schema 保证 verdict 一定是两个枚举值之一,但不保证判断正确。这一点面试里常被追问:结构化输出让你省掉了解析层的防御代码,业务层的校验一行都不能省。
多轮状态:previous_response_id 还是自己带历史
最后一个问题:多轮对话的历史放哪。Responses 给了两条路。
让服务端存:默认 store: true,每次响应都被存下来,下一轮传 previous_response_id 就接上了,请求体只放新内容。好处是请求小、代码简单,第二节的函数调用例子就是这么串的。代价是状态在别人那里:你想「回到第三轮重来」得记住每一轮的 id;你想审计完整对话得另外拉;合规要求数据不出境时这条路走不通。
自己维护:store: false,每轮把完整的 items 数组(包括之前的 function_call 和 function_call_output)作为 input 重发。这就是 30 天课 D1 那套「历史靠你自己搬」,成本随轮数增长,但每一个字节都在你手里,想压缩、想截断、想换模型重放都行。
选哪条?原型和内部工具用前者,省事;面向用户的生产系统多半用后者,或者混合——用 previous_response_id 串短链,同时自己落一份历史做审计与恢复。明天讲的 Agents SDK 把这个选择包成了 sessions,你会看到同一个取舍换了一层外壳。
源码导读
动手实验
代码在 labs/codex-mastery/day-03-responses-tools,starter/ 挖了四个练习点,solution/ 是完整答案。这是一次性脚本,不是 REPL,所以直接带参数跑,不要用管道喂输入。
- 跑
MOCK=1 pnpm start "你好",看到一次最简单的responses.create往返,打印出output_text。 - 实现练习 1 与 2:声明
list_todos函数工具,从输出里筛出function_call,执行后用function_call_output回填,跑MOCK=1 pnpm start "还有哪些没做完"看到 TODO 标题出现在最终回答里。 - 实现练习 3:加
--search时在tools里加web_search,从 message 的 annotations 里把url_citation单独打印。 - 实现练习 4:加
--structured时改用responses.parse与zodTextFormat,先检查refusal再读output_parsed。 - 配好
OPENAI_API_KEY与OPENAI_MODEL,去掉 MOCK 跑一遍,对比 MOCK 输出与真实输出的形状是否一致。
面试题
今天 3 道题在下方题库区,侧重 Responses API 与 Chat Completions 的取舍、内置工具与自定义函数的边界、结构化输出的可靠性。展开后先看「分析过程」再看要点——照着推导练,比背要点管用。
检查清单与明日预告
- 能说清 Responses API 与 Chat Completions 在输入形态、输出形态、状态管理上的三处区别
- 能写一个带函数调用与 web search 的最小脚本,并把 function_call_output 正确回填
- 能用 json_schema 严格模式拿到结构化输出,并处理 refusal 分支
- 能按「能力越强隔离越重」排出 web search、file search、函数调用、computer use 的顺序并说明理由
- 实验的 4 条验收标准全部通过
- 3 道面试题不看要点也能答出至少 2 道
明天(D4)我们用官方的 Agents SDK 把今天手写的循环收进几行 API:Agent 对象装下指令和工具,run 函数替你转循环;然后讨论三个今天还没碰的问题——多个 Agent 之间怎么交接(handoffs)、怎么在入口和出口设护栏(guardrails)、多轮记忆怎么交给 SDK 存(sessions)。先手写再用 SDK 是有意的顺序:你已经知道 function_call 和 function_call_output 的来回是什么,明天看到 SDK 把它们藏起来时,才知道它藏了什么、出问题时该往哪一层找。
Interview questions
How does the Responses API differ from Chat Completions, and what are the common pitfalls when migrating?Responses API 和 Chat Completions 的区别是什么?从 Chat Completions 迁移过去最容易踩什么坑?
Common in ChinaCommon overseasBasic#responses-api#openai#migrationHow to reason about it · think before answering
- This tests whether you have actually migrated code, not whether you can recite field names.
- Split into three axes: input shape (messages array becomes input plus top-level instructions), output shape (choices becomes typed output items with an output_text helper), and state (stateless becomes store by default plus previous_response_id).
- Explain the motivation: a chat-transcript model cannot hold tool actions; items give search, function calls and their outputs distinct types, which is what makes built-in tools possible.
- Name three pitfalls: store defaults to true so compliance-sensitive apps must disable it; output is an array, so read output_text or walk message items; tool results move from role tool messages to function_call_output items keyed by call_id.
- Expect the follow-up: previous_response_id versus self-managed history? Prototypes take the former; production usually keeps its own history for audit and recovery, or mixes both.
分析过程 · 先想清楚再作答
- 这题考的是你有没有真迁移过,而不是能不能背出字段名。只答「新接口更强」会被判为看过文档没写过代码。
- 拆成三个维度:输入形态(messages 数组变成 input 加顶层 instructions)、输出形态(choices 变成按类型排列的 output items,SDK 给 output_text 助手)、状态管理(无状态变成默认 store 加 previous_response_id)。
- 再说为什么要改:聊天记录模型装不下工具动作;items 让搜索、函数调用、回填各有自己的类型,这是内置工具能接进来的前提。
- 迁移坑给三条:默认 store 为 true 意味着数据会被存下来,合规场景要显式关掉;output 是数组不是单个消息,取文本要用 output_text 或遍历 message item;函数调用的回填从 role 为 tool 的消息变成 function_call_output item,call_id 要对上。
- 可预期的追问:previous_response_id 和自己维护历史怎么选?原型用前者省事,生产多半自己落一份历史做审计与恢复,或两者混用。
Key points
- Input: messages become input plus top-level instructions; output: choices become typed output items plus output_text
- State: store defaults to true and previous_response_id chains turns without resending history
- The motivation is distinct item types for tool actions, enabling built-in tools
- Pitfalls: store on by default, output is an array, tool results go back as function_call_output keyed by call_id
答题要点
- 输入:messages 变 input 加顶层 instructions;输出:choices 变按类型排列的 output items 与 output_text
- 状态:默认 store 为 true,用 previous_response_id 接上一轮,不再每轮重发历史
- 改的动机是给工具动作独立的 item 类型,内置工具由此接入
- 迁移坑:store 默认开、output 是数组、回填要用 function_call_output 且 call_id 对上
When do you use platform built-in tools (web search, file search, computer use) versus your own function tools, and why does computer use deserve special treatment?平台内置的工具(web search、file search、computer use)和自己写的函数工具,各适合什么场景?为什么 computer use 要单独对待?
Common in ChinaCommon overseasIntermediate#tools#responses-api#securityHow to reason about it · think before answering
- Two cruxes: who executes the tool, and how large its side effects are; comparing features alone signals no production experience.
- Executor test: built-in tools run server-side, you declare but never fill results and cannot steer the search; function tools run in your code, more work but full control.
- Map to scenarios: external, generic data (the web, your uploaded documents) fits built-ins; data inside your systems (databases, internal services, business logic) needs functions; production mixes both.
- Order by side effects: web search reads the public web, file search reads your files, function calls have whatever side effects your code allows, computer use lets the model act directly; more capability demands heavier isolation.
- Computer use is special because it can click anything, type anything and be steered by on-screen content, so the starting point is an isolated environment, a restricted account and an allow-list, not code.
- Expect the follow-up: built-in file search versus your own RAG? The built-in is a managed pipeline that skips chunking, embedding and retrieval work at the cost of control and observability; build your own when you need custom chunking or reranking.
分析过程 · 先想清楚再作答
- 题眼有两个:一是「谁来执行」,二是「副作用有多大」。只答功能对比不谈执行方与风险,就是没做过工程。
- 先给执行方的判据:内置工具由平台在服务端执行,你只声明、不回填、也控制不了它怎么搜;函数工具由你执行,样样自己写,但每一步都在你手里。
- 落到场景:数据在外面且通用(公网、你上传的文档)用内置工具;数据在你系统里(数据库、内部服务、业务逻辑)写函数;生产系统几乎总是混用。
- 再按副作用排一条光谱:web search 只读公网,file search 只读你给的文件,函数调用的副作用由你的代码决定,computer use 由模型直接产生副作用——越往右能力越强,需要的隔离越重。
- computer use 单独对待的原因:它能点任何按钮、输任何文字,还可能被页面内容诱导,所以正确起点是隔离环境、受限账号和站点与动作白名单,不是代码。
- 可预期的追问:内置的 file search 和自己搭 RAG 怎么选?前者是托管版,省掉切分、向量化、检索三步,代价是可控性与可观测性弱,需要自定义切分或重排时才自己搭。
Key points
- Built-ins run on the platform with no result filling and no steering; functions run in your code with full control
- External generic data suits built-ins, in-system data and business logic need functions, production mixes both
- Rank by side effects: web search, file search, function calls, computer use; more power needs more isolation
- Computer use starts with an isolated environment and an allow-list, not with code
答题要点
- 内置工具由平台执行、不用回填、不可干预;函数工具由你执行、全部可控
- 外部通用数据用内置工具,系统内数据与业务逻辑写函数,生产混用
- 按副作用排序:web search、file search、函数调用、computer use,能力越强隔离越重
- computer use 的起点是隔离环境与白名单,不是代码
What does strict mode in structured outputs solve, what does it not solve, and what must your code still do after receiving the output?结构化输出的 strict 模式解决了什么问题,没解决什么问题?拿到输出之后代码里还要做什么?
Common in ChinaCommon overseasIntermediate#structured-output#responses-api#validationHow to reason about it · think before answering
- This tests the distinction between well-formed and correct; claiming strict mode removes the need for validation is the classic mistake.
- What it solves: strict plus json_schema guarantees the output validates against the schema, so enums are always allowed values, required fields exist and types are right; parsing-layer try/catch and retries can largely go.
- What it does not solve: semantics. The verdict is one of two enums but may be wrong; a number is a number but may be invented. Business validation stays.
- Then refusals: a safety refusal comes back as a refusal content block, not malformed JSON; unhandled, downstream code crashes on an empty parse, handled, you can separate unwilling from incorrect.
- Give the order in code: check refusal, read output_parsed, run business checks (ranges, referenced entities exist, consistency with context), then persist or act.
- Expect the follow-up: schema restrictions under strict? Every object needs additionalProperties false and all fields in required, optional fields become nullable; these constraints are exactly what makes the guarantee possible.
分析过程 · 先想清楚再作答
- 这题考的是对「格式正确」与「内容正确」的区分,答成「有了 strict 就不用校验了」是典型的错误。
- 先说解决了什么:strict 加 json_schema 保证输出一定能通过 schema 校验——枚举只会是给定值、必填字段一定在、类型不会错,解析层的 try/catch 与重试基本可以删掉。
- 再说没解决什么:schema 管不了语义。verdict 一定是两个枚举之一,但判断可能是错的;数字一定是数字,但可能是编的。业务层校验一行不能省。
- 然后是拒答分支:模型因安全原因拒绝时返回 refusal 类型的内容块,而不是硬塞一个不合法的 JSON;不处理它,下游会拿到空的解析结果直接崩,处理了才能区分「不愿意」与「没做对」。
- 给出代码里的顺序:先查 refusal,再读 output_parsed,再做业务校验(范围、引用是否存在、与上下文是否一致),最后才落库或执行。
- 可预期的追问:strict 对 schema 有什么限制?每个对象都要 additionalProperties 为 false、字段都要在 required 里,可选字段用可空类型表达;这些限制正是它能给出保证的原因。
Key points
- Solves: output is guaranteed to match the schema, so parsing defenses can go
- Does not solve: semantic correctness, so business validation stays
- Check refusal first, then output_parsed, then business checks, then persist
- Strict requires additionalProperties false and all fields required, optional fields become nullable
答题要点
- 解决:输出保证符合 schema,解析层防御代码可以删
- 没解决:语义正确性,业务校验一行不能省
- 先查 refusal 再读 output_parsed,再做业务校验,最后落库
- strict 要求 additionalProperties 为 false、字段全在 required 里,可选用可空类型表达
Comments
Sign in to join the discussion
No comments yet — be the first.