OpenAI 开发者平台面经

官方文档 · 中文讲解 + 英文术语 · 每题可跳原文

327题目
40/54主题簇
323深挖·拓展题
74%构建进度

🗂️ 数据源(由 sources.json 控制)

改 sources.json 开关源 · 全文镜像在 sources/
OpenAI API 文档
官方文档 · .md
165 本地文件
sources/openai-api-docs
OpenAI Cookbook
GitHub 仓库 · notebooks
360 本地文件
sources/openai-cookbook
OpenAI Engineering 博客
官方文档 · .md
16 本地文件
sources/openai-eng-blog

📊 章节频率热力(🔥高频/中频/低频 占比)

· 核心概念与文本生成18
· 函数调用与工具33
· 结构化输出与可靠性10
· Agents 平台与编排32
· 检索、嵌入与多模态18
· 规模化、运维与迁移9
· RAG 与向量检索实战32
· Agents 实战31
· 函数调用、结构化输出与工具编排实战24
· 评估、守护与微调实战34
· 多模态与生产化实战30
· Codex 与 Agent Harness 工程25
· Responses API 与 Agentic 基础设施22
· 大规模系统与可靠性9

第一部分 · OpenAI API 平台

第1章 · 核心概念与文本生成

🔥高频

Responses API 与文本生成基础

原文 原文 原文
Q什么是 Responses API?为什么官方推荐用它而不是旧的 Chat Completions API?深挖·拓展🔥高频
Responses API Chat Completions reasoning models
⏱️ 现行
Responses API 是 OpenAI 用于"直接向模型发请求"的主接口,文本生成这类调用就走它——你把 prompt 放进 input、指定 model,SDK 就返回结构化响应。官方给出的取舍很明确:对于任何文本生成应用都建议用 Responses API 而不是更老的 Chat Completions API;尤其当你用的是 reasoning model 时,模型在 Responses API 下表现更好、展现出更高的智能,这是因为 Responses API 能承载并复用推理过程相关的结构(如 reasoning token 项),而 Chat Completions 的扁平 messages 模型无法自然表达这些。因此迁移到 Responses 不只是换个 SDK 调用,而是让 reasoning 类模型发挥全部能力的前提;如果你在用 reasoning model,官方特别建议做 migrate-to-responses 的迁移。
术语 Responses API(直接模型请求的主接口); input(承载 prompt 的入参); Chat Completions API(更老、官方不再优先推荐的接口); reasoning models(带推理过程的模型,在 Responses 下表现更好)
📖 "Use the Responses API for direct model requests like this text-generation call." — 原文
📖 "If you're building any text generation app, we recommend using the Responses API over the older Chat Completions API." — 原文
📖 "One important note is that reasoning models perform better and demonstrate higher intelligence when used with the Responses API." — 原文
🧪 实例 最简单的一次文本生成调用(Python):
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    input="Write a one-sentence bedtime story about a unicorn."
)

print(response.output_text)
🔍 追问 用 reasoning model 时为什么尤其要迁到 Responses? → 因为 reasoning model 在 Responses API 下 perform better、展现更高智能,官方专门建议 migrate-to-responses。
📚 拓展阅读
Q模型返回的 output 是什么结构?为什么不能直接取 output[0].content[0].textoutput_text 又是什么?深挖·拓展🔥高频
output output_text 响应解析
⏱️ 现行
模型生成的内容以数组形式放在响应的 output 属性里。关键陷阱是:这个 output 数组往往不止一个元素——它可能包含 tool call、reasoning model 产生的 reasoning token 相关数据,以及其他条目,因此假设"模型文本一定在 output[0].content[0].text"是不安全的,一旦有工具调用或推理项排在前面,这个下标就会取到错误内容甚至越界。为方便起见,OpenAI 的部分官方 SDK 在响应上提供了 output_text 属性,它把模型所有文本输出聚合成单个字符串,可作为快捷方式直接拿到文本。取舍在于:output_text 省事但屏蔽了结构细节;当你需要处理工具调用、注解或推理项时,仍要遍历原始 output 数组。除纯文本外,模型还能返回符合 JSON schema 的结构化数据,这一能力叫 Structured Outputs。
术语 output(响应中承载生成内容的数组属性); output_text(SDK 提供的聚合全部文本输出的便捷字符串); tool calls(工具调用条目,可能出现在 output 中); Structured Outputs(让模型输出符合 JSON schema 的能力)
📖 "An array of content generated by the model is in the output property of the response." — 原文
📖 "The output array often has more than one item in it! It can contain tool calls, data about reasoning tokens generated by reasoning models, and other items. It is not safe to assume that the model's text output is present at output[0].content[0].text." — 原文
📖 "Some of our official SDKs include an output_text property on model responses for convenience, which aggregates all text outputs from the model into a single string." — 原文
🧪 实例 一次简单调用的 output 数组结构(只有一条 message 输出时):
json
[
  {
    "id": "msg_67b73f697ba4819183a15cc17d011509",
    "type": "message",
    "role": "assistant",
    "content": [
      {
        "type": "output_text",
        "text": "Under the soft glow of the moon, Luna the unicorn danced through fields of twinkling stardust, leaving trails of dreams for every child asleep.",
        "annotations": []
      }
    ]
  }
]
🔍 追问 什么时候必须遍历原始 output 而不能只用 output_text? → 当响应里含 tool call、reasoning 项或注解等非纯文本条目、需要按 type 分别处理时。
📚 拓展阅读
Qdeveloperuserassistant 三种消息角色分别是什么?instructions 参数和它们什么关系?深挖·拓展🔥高频
message roles instructions chain of command
⏱️ 现行
你可以用不同"权威等级"的消息给模型下指令,手段是 instructions 参数配合 message roles。instructions 参数给模型高层级的行为指令——包括语气、目标、正确回答的示例——而且通过它提供的任何指令都会优先于 input 里的 prompt。三种角色的优先级依 OpenAI model spec 的 chain of command 排列:developer 消息是应用开发者提供的指令,优先级排在 user 消息之前;user 消息是终端用户提供的指令,优先级排在 developer 之后;assistant 则是模型自己生成消息的角色。一个直观类比是把 developeruser 消息看成编程语言里的函数与它的实参——developer 消息提供系统规则和业务逻辑(像函数定义),user 消息提供输入和配置、供 developer 指令去作用(像函数参数)。用 instructions 参数其实大致等价于在 input 数组里放一条 developer 角色消息。
术语 instructions(高层行为指令参数,优先于 input 中的 prompt); developer(应用开发者的指令,优先级高于 user); user(终端用户的指令,优先级低于 developer); assistant(模型生成消息的角色); chain of command(model spec 定义的角色优先级链)
📖 "The instructions parameter gives the model high-level instructions on how it should behave while generating a response, including tone, goals, and examples of correct responses. Any instructions provided this way will take priority over a prompt in the input parameter." — 原文
📖 "You could think about developer and user messages like a function and its arguments in a programming language." — 原文
🧪 实例instructions 与用 input 里的 developer 角色消息大致等价:
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    reasoning={"effort": "low"},
    input=[
        {
            "role": "developer",
            "content": "${semicolonsDevMsg}"
        },
        {
            "role": "user",
            "content": "${semicolonsPrompt}"
        }
    ]
)

print(response.output_text)
🔍 追问 developer 和 user 消息的优先级谁高? → developer 消息优先级更高,排在 user 之前;user 又排在 assistant 之外,遵循 model spec 的 chain of command。
📚 拓展阅读
Qinstructions 参数在多轮对话里会一直生效吗?和 previous_response_id 怎么配合?深挖·拓展中频
instructions previous_response_id conversation state
⏱️ 现行
不会自动延续。instructions 参数只作用于当前这一次响应生成请求。如果你用 previous_response_id 参数来管理会话状态(conversation state),之前轮次用过的 instructions 不会出现在后续上下文中。这带来一个实际后果和取舍:把系统级规则只写在某一轮的 instructions 里,是不可靠的——下一轮模型看不到它;如果希望规则贯穿整个多轮对话,要么每一轮都重新传 instructions,要么把规则以 developer 角色消息的形式放进被延续的对话状态里。换句话说,instructions 是"本轮临时"的,而对话历史(通过 previous_response_id 串起来)才是被持久携带的部分。
术语 instructions(仅对当前请求生效的行为指令); previous_response_id(用上一次响应 id 串起会话状态的参数); conversation state(跨轮次维护的对话上下文)
📖 "Note that the instructions parameter only applies to the current response generation request. If you are managing conversation state with the previous_response_id parameter, the instructions used on previous turns will not be present in the context." — 原文
🧪 实例 若要让"用分号"的风格规则在多轮都生效,不能只在第一轮传 instructions,而应每轮重传,或改写成随会话延续的 developer 消息。
🔍 追问 为什么把长期规则只放在某一轮的 instructions 里会踩坑? → 因为 instructions 只对当前请求生效,用 previous_response_id 续接时前几轮的 instructions 不在上下文里,模型下一轮就"忘"了。
📚 拓展阅读
Q什么是 token?为什么它对 context length 很重要?如何粗略估算 token 数?深挖·拓展🔥高频
tokens context length 分词
⏱️ 现行
文本生成和 embeddings 模型都不是按字符、而是按叫 token 的"块"来处理文本的,token 代表常见的字符序列。例如字符串 " tokenization" 会被拆成 " token" 和 "ization",而 " the" 这种又短又常见的词就是单个 token;句子里每个词的第一个 token 通常以空格字符打头。粗略经验法则是:对英文文本,1 个 token 约等于 4 个字符或 0.75 个词。token 之所以关键,是因为存在 context length 限制:对文本生成模型,prompt 和生成输出合起来不能超过模型的最大 context length;对 embeddings 模型(不输出 token),输入本身必须短于最大 context length。这直接影响你的取舍——长 prompt 会挤占可用于生成的预算,超限会被拒绝或截断,所以估算 token 量、必要时做分块或压缩是工程上的常规动作。各模型的最大 context length 可在 model index 查到。
术语 token(模型处理文本的最小块,代表常见字符序列); context length(prompt 加输出的 token 上限); embeddings model(只接收输入、不输出 token 的模型); tokenizer(把字符串切成 token 的工具)
📖 "Text generation and embeddings models process text in chunks called tokens. Tokens represent commonly occurring sequences of characters." — 原文
📖 "As a rough rule of thumb, 1 token is approximately 4 characters or 0.75 words for English text." — 原文
📖 "One limitation to keep in mind is that for a text generation model the prompt and the generated output combined must be no more than the model's maximum context length." — 原文
🧪 实例 按经验法则估算:约 1000 个英文单词 ≈ 1000 / 0.75 ≈ 1333 个 token;一段 4000 字符的英文文本 ≈ 4000 / 4 = 1000 个 token。
🔍 追问 embeddings 模型和文本生成模型在 context length 上的约束有何不同? → 文本生成模型要求 prompt + 输出合计不超过最大 context length;embeddings 模型不输出 token,只要求输入本身短于最大 context length。
📚 拓展阅读
QGPT 文本生成模型能做什么?"设计一个 prompt"意味着什么?深挖·拓展中频
GPT prompt 文本生成
⏱️ 现行
OpenAI 的文本生成模型常被称作 generative pre-trained transformers(简称 "GPT" 模型),像 gpt-5.6gpt-5.6-terra,它们被训练来理解自然语言和形式语言,针对输入产生文本输出;这些输入也被称为 "prompts"。核心思想是:设计一个 prompt 本质上就是你"编程"一个模型的方式——通常靠给出指令,或给几个如何成功完成任务的示例来实现。正因如此,GPT 模型能覆盖极广的任务:内容或代码生成、摘要、对话、创意写作等等。这里的取舍与 prompt engineering 一脉相承:你不是写死逻辑,而是用自然语言"编程",因此措辞、示例、指令的清晰度直接决定输出质量;模型是通用的,把它约束到具体任务上靠的正是 prompt 的设计。
术语 GPT(generative pre-trained transformer,生成式预训练 transformer 模型); prompt(给模型的输入指令,即"编程"模型的方式); text generation model(理解自然与形式语言、输出文本的模型)
📖 "Designing a prompt is essentially how you "program" a model, usually by providing instructions or some examples of how to successfully complete a task." — 原文
🧪 实例 同一个 gpt-5.6 模型,仅靠改 prompt 就能承担不同任务——从"写一句独角兽睡前故事"到"总结这封信的要点",无需改代码逻辑,只改自然语言指令。
🔍 追问 "GPT" 这个缩写展开是什么? → generative pre-trained transformers(生成式预训练 transformer 模型)。
📚 拓展阅读
Q什么是 prompt engineering?为什么官方强烈建议把生产应用 pin 到具体的 model snapshot?深挖·拓展中频
prompt engineering model snapshots evals
⏱️ 现行
Prompt engineering 是"为模型写出有效指令"的过程,目的是让它稳定地产出满足你要求的内容。难点在于模型生成的内容是 non-deterministic 的,所以调 prompt 得到想要的输出是一门"艺术与科学的混合"——有通用技巧(比如使用 message roles 对所有模型都管用),但不同模型、甚至同一模型家族内不同 snapshot 都可能需要不同的 prompt 才能出最好结果。正因如此,官方对复杂应用给出两条强建议:一是把生产应用 pin 到具体的 model snapshot(例如 gpt-5.5-2026-04-23)以保证行为一致;二是建立测试和评估套件来度量 prompt 行为,好让你在迭代、或在升级/更换模型版本时监控性能。这里的取舍很清楚:不 pin snapshot 图省事,但模型静默升级可能让你精心调好的 prompt 行为漂移;pin + evals 换来的是可控和可回归的稳定性。
术语 prompt engineering(写有效指令让模型稳定达标的过程); non-deterministic(生成内容不确定,同输入可有不同输出); model snapshot(带日期的具体模型版本,如 gpt-5.5-2026-04-23); evaluation suites(度量 prompt 行为的评估套件)
📖 "Prompt engineering is the process of writing effective instructions for a model, such that it consistently generates content that meets your requirements." — 原文
📖 "Because the content generated from a model is non-deterministic, prompting to get your desired output is a mix of art and science." — 原文
📖 "Pinning your production applications to specific model snapshots (like gpt-5.5-2026-04-23 for example) to ensure consistent behavior" — 原文
🧪 实例 生产里把 model"gpt-5.6" 换成带日期的快照 "gpt-5.5-2026-04-23",并配一套 eval fixtures,在升级模型版本前先跑评估、对比 prompt 行为是否漂移。
🔍 追问 为什么模型是 non-deterministic 就非要建评估套件? → 因为同一 prompt 输出会变、且换 snapshot 行为可能漂移,evals 让你在迭代和升级时能量化并监控 prompt 行为,避免静默回退。
📚 拓展阅读
QResponses API 怎么给模型"扩展能力"?内置工具和自定义工具有什么区别?深挖·拓展中频
tools web_search file_search function calling
⏱️ 现行
通过给请求附加 tools,你可以让模型访问外部数据和函数。工具分两类:一类是内置工具(built-in tools),比如 web search 或 file search,拿来即用;另一类是你自己定义的工具,用于调用 API、运行代码或对接第三方系统(即 function calling)。机制上,工具都通过 tools 数组声明——内置工具只需给个 type(如 {"type": "web_search"}),自定义 function 则要给出 namedescription 和 JSON schema 形式的 parameters,模型据此决定何时、以什么参数调用。取舍在于:内置工具省心、由 OpenAI 托管能力(联网搜索、向量库检索、代码解释器等),但能力边界固定;自定义 function 灵活、能接你自己的业务系统,但需要你实现执行逻辑并处理返回。此外还能通过 type: "mcp" 接入远程 MCP server。工具调用会作为条目出现在响应的 output 数组里,这也是为什么解析输出时不能假设文本就在 output[0]
术语 tools(声明工具的数组入参); web_search/file_search(内置工具,联网搜索/文件检索); function calling(自定义函数工具,让模型调用你的代码); mcp(接入远程 MCP server 的工具类型)
📖 "Give the model access to external data and functions by attaching tools. Use built-in tools like web search or file search, or define your own for calling APIs, running code, or integrating with third-party systems." — 原文
🧪 实例 在一次响应里启用内置 web search 工具:
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    tools=[{"type": "web_search"}],
    input="What was a positive news story from today?"
)

print(response.output_text)
🔍 追问 内置工具和自定义 function 在声明上的最大差别是什么? → 内置工具通常只给一个 type;自定义 function 还要提供 namedescription 和 JSON schema 的 parameters(并可设 strict)。
📚 拓展阅读
Q如何流式(streaming)获取模型输出?它解决什么问题、和 Realtime API 什么关系?深挖·拓展中频
streaming server-sent events Realtime API
⏱️ 现行
你可以用 server-sent 的 streaming events 在内容"边生成边显示",而不必等整段响应完成再返回——这对聊天式 UI 的感知延迟至关重要。用法上只需在 responses.create 里设 stream: true,SDK 就返回一个可迭代的事件流,你遍历每个 event 即可增量拿到输出。对于更强的交互场景,比如需要语音、以及同时支持文本、音频、图像输入的实时应用,则用 Realtime API(底层走 WebRTC 或 WebSockets)。取舍在于:普通 streaming 走 SSE,实现简单、适合把文本快速推给用户;Realtime API 面向超低延迟的 speech-to-speech 双向交互,能力更强但复杂度也更高。两者服务于不同的实时性诉求。
术语 streaming events(server-sent 的增量事件流); stream: true(开启流式的参数); Realtime API(面向语音等实时交互的接口); WebRTC/WebSockets(Realtime 底层传输)
📖 "Use server-sent events to stream model responses to users fast." — 原文
📖 "Use WebRTC or WebSockets for super fast speech-to-speech AI apps." — 原文
🧪 实例stream=True 逐事件打印流式输出(Python):
python
from openai import OpenAI
client = OpenAI()

stream = client.responses.create(
    model="gpt-5.6",
    input=[
        {
            "role": "user",
            "content": "Say 'double bubble bath' ten times fast.",
        },
    ],
    stream=True,
)

for event in stream:
    print(event)
🔍 追问 什么时候该用 Realtime API 而不是普通 SSE streaming? → 当你要做交互式语音应用、或需要同时处理文本/音频/图像输入的超低延迟 speech-to-speech 场景时,用底层走 WebRTC/WebSockets 的 Realtime API。
📚 拓展阅读
Q什么是 embedding?它和文本生成模型有什么本质区别,能用来做什么?深挖·拓展低频
embeddings vector 语义检索
⏱️ 现行
embedding 是一段数据(例如某段文本)的向量表示,目的是保留其内容和/或含义的某些方面。它的核心性质是:在某种意义上相似的数据块,其 embedding 往往彼此更接近,而不相关的数据则更远——这把"语义相似"变成了"向量距离"。OpenAI 提供文本 embedding 模型,输入一个文本字符串、输出一个 embedding 向量。与文本生成模型的本质区别在于输出形态:生成模型输出 token(文本),embedding 模型不输出 token、只产出向量,因此它的约束是输入本身必须短于模型最大 context length(而非 prompt+输出合计)。用途上,embedding 适合 search、clustering、recommendations、anomaly detection、classification 等——凡是需要"按相似度"组织或检索数据的场景,都能用它把文本映射到向量空间再比距离。
术语 embedding(保留内容/含义的向量表示); vector(embedding 模型输出的数值向量); similarity(相似数据 embedding 距离更近的性质); classification/clustering(embedding 的典型下游用途)
📖 "An embedding is a vector representation of a piece of data (e.g. some text) that is meant to preserve aspects of its content and/or its meaning. Chunks of data that are similar in some way will tend to have embeddings that are closer together than unrelated data." — 原文
🧪 实例 语义检索管线的直观流程:
flowchart LR
    A[文本字符串] --> B[embedding 模型]
    B --> C[embedding 向量]
    C --> D[向量空间按距离比相似度]
    D --> E[search / clustering / classification]
🔍 追问 embedding 模型和文本生成模型的 context length 约束为何不同? → 生成模型要 prompt+输出合计不超上限;embedding 模型不输出 token,只要求输入本身短于最大 context length。
📚 拓展阅读
🔥高频

会话状态管理与流式响应

原文 原文 原文
QOpenAI 提供了哪几种管理会话状态(conversation state)的方式?各自机制是什么?深挖·拓展🔥高频
conversation-state responses-api multi-turn
⏱️ 现行
文本生成请求本身是无状态(stateless)的,每次请求相互独立,所以要实现多轮对话就必须由调用方负责把"之前发生过什么"重新喂回模型。文档给出三条路径,权衡在于"谁来保存历史、保存多久"。第一是手动管理:在 input 数组里用交替的 user / assistant 消息把过去的对话拼进单次请求,或者把模型上一轮的 output 数组整段追加到下一轮 input 里——完全由客户端持有历史,最灵活但也最繁琐,且对推理模型要完整保留 output 里的每一项(含加密的 reasoning items)才能保持推理与 phase 值不丢。第二是 Conversations API:把会话持久化成一个带持久标识符的长驻对象,跨会话、设备、任务复用,之后每轮把 conversation 传进 Responses 请求即可自动带上上下文。第三是 previous_response_id:用上一条 response 的 id 把多次 response 串成一条线程,服务端替你续接上下文。三者的核心取舍是:手动=零服务端依赖但要自己拼;Conversations=持久对象、无 30 天 TTL;previous_response_id=最轻量的链式续接,但历史仍存在服务端。

flowchart TD
    A[多轮对话需求] --> B{谁保存历史?}
    B -->|客户端自持| C[手动: input 拼接 user/assistant
或追加 output 数组] B -->|服务端持久对象| D[Conversations API
durable identifier] B -->|服务端链式续接| E[previous_response_id
threaded conversation]
术语 conversation state(会话状态,跨轮次保留的信息); stateless(无状态,每次请求独立); multi-turn conversations(多轮对话); output array(响应输出项数组,含 reasoning items)
📖 "OpenAI provides a few ways to manage conversation state, which is important for preserving information across multiple messages or turns in a conversation." — 原文
📖 "While each text generation request is independent and stateless, you can still implement multi-turn conversations by providing additional messages as parameters to your text generation request." — 原文
📖 "By using alternating user and assistant messages, you capture the previous state of a conversation in one request to the model." — 原文
🧪 实例 手动把上一轮 output 追加进历史再问第二个问题:
python
history = [{"role": "user", "content": "tell me a joke"}]
response = client.responses.create(model="gpt-5.6", input=history, store=False)
# Add all response output items, including encrypted reasoning items, to the conversation
history += response.output
history.append({"role": "user", "content": "tell me another"})
second_response = client.responses.create(model="gpt-5.6", input=history, store=False)
🔍 追问 对推理模型(reasoning model)做无状态请求时,为什么要把 output 数组里的每一项都保留? → 因为 Responses API 默认返回加密的 reasoning items,只有把完整 output 回放,才能让 reasoning items 和 assistant 的 phase 值保持完整,避免下一轮丢失推理上下文。
📚 拓展阅读
Qprevious_response_id 如何工作?用了它是不是就不再对历史 token 计费了?深挖·拓展🔥高频
previous_response_id billing data-retention
⏱️ 现行
previous_response_id 是把上下文在多次生成的 response 之间共享的另一种方式——它让你把多条 response 链接起来、形成一条 threaded conversation:第二次请求只需传 previous_response_id=response.id,无需把整段历史重新拼进 input,模型就已具备回答所需的全部上下文。但这里有个关键误区必须澄清:即使用了 previous_response_id,链条里所有先前的输入 token 在 API 侧仍然按 input token 计费——服务端替你续接上下文不等于免费,它省的是传输和拼装的麻烦,省不了 token 成本。配套的还有数据留存语义:response 对象默认保存 30 天,可在 dashboard 的 logs 页查看或经 API 取回,把 store 设为 false 可关闭该行为。要注意留存与是否绑定 Conversation 有关(见 Q7)。在 WebSocket 模式下续接语义相同,但连接本地缓存只保留最近一条 previous response,若某个 id 已不在缓存无法解析,就要以 previous_response_id 设为 null 开启新一轮并传入完整输入上下文。
术语 previous_response_id(上一条响应 id,用于链式续接); threaded conversation(线程化对话); store(是否持久化响应,默认保存 30 天); input tokens(输入 token,仍按此计费)
📖 "Another way to manage conversation state is to share context across generated responses with the previous_response_id parameter. This parameter lets you chain responses and create a threaded conversation." — 原文
📖 "Even when using previous_response_id, all previous input tokens for responses in the chain are billed as input tokens in the API." — 原文
📖 "Response objects are saved for 30 days by default." — 原文
🧪 实例 先讲笑话,再让模型解释为什么好笑——第二轮只传上一条 id:
python
response = client.responses.create(model="gpt-5.6", input="tell me a joke")
second_response = client.responses.create(
    model="gpt-5.6",
    previous_response_id=response.id,
    input=[{"role": "user", "content": "explain why this is funny."}],
)
🔍 追问 WebSocket 模式下若传入的 previous_response_id 已无法解析(uncached)该怎么办? → 发起新的一轮,把 previous_response_id 设为 null 并传入完整输入上下文,因为连接本地缓存只保留最近一条 previous response 用于低延迟续接。
📚 拓展阅读
QResponses API 的流式响应(streaming)如何开启?它用什么传输机制、返回什么样的事件?深挖·拓展🔥高频
streaming sse semantic-events
⏱️ 现行
默认情况下 OpenAI API 会先把模型整段输出生成完,再用单个 HTTP 响应返回;生成长文本时这会让用户干等很久。流式响应的价值就是:让你在模型继续生成完整响应的同时,就开始打印或处理输出的开头部分,从而显著缩短首字节等待。开启方式很简单——向 Responses 端点的请求里设 stream=True,SDK 会返回一个可迭代对象,for 循环里逐个拿到事件。传输层面这里聚焦的是基于 server-sent events(SSE)的 HTTP 流(若要在持久 WebSocket 上做增量输入则走 WebSocket 模式)。关键设计是 Responses API 采用语义事件(semantic events):每个事件都带预定义 schema 的类型,你只监听自己关心的事件即可,而不用自己解析原始字节流。事件种类很多(ResponseCreatedEventResponseOutputTextDeltaResponseCompletedEventError 等),流式文本场景最常监听的是 response.createdresponse.output_text.deltaresponse.completederror——有些生命周期事件只触发一次,有些则随生成不断多次触发。
术语 stream=True(开启流式); server-sent events / SSE(服务器发送事件,HTTP 流传输); semantic events(语义事件,带类型 schema); response.output_text.delta(文本增量事件)
📖 "Streaming responses lets you start printing or processing the beginning of the model's output while it continues generating the full response." — 原文
📖 "The Responses API uses semantic events for streaming. Each event is typed with a predefined schema, so you can listen for events you care about." — 原文
🧪 实例stream=True 后逐事件迭代:
python
stream = client.responses.create(
    model="gpt-5.6",
    input=[{"role": "user", "content": "Say 'double bubble bath' ten times fast."}],
    stream=True,
)
for event in stream:
    print(event)
🔍 追问 流式文本时哪些是最常监听的核心事件? → response.createdresponse.output_text.deltaresponse.completederror;其中部分生命周期事件只触发一次,delta 类事件则随生成多次触发。
📚 拓展阅读
Q在 Chat Completions 流式场景里,为什么要从 delta 而不是 message 取内容?chunk 长什么样?深挖·拓展🔥高频
chat-completions stream delta
⏱️ 现行
非流式的 ChatCompletions 调用会先把整个 completion 算完再一次性返回,长回复时要等好几秒。设 stream=True 后,端点返回的是一个把响应作为 data-only server-sent events 增量回传的对象,Python 里可以用 for 循环遍历这些事件(chunk)。这里的核心区别是数据结构:非流式响应用 message 字段承载完整回复,而流式响应每个 chunk 用的是 delta 字段——所以必须从 delta 而不是 message 提取内容。delta 可能装三种东西:一个 role token(如 {"role": "assistant"},通常出现在第一个 chunk)、一个 content token(如 {"content": "\n\n"}),或者什么都没有({},表示流已结束)。因此健壮的收集逻辑是:把每个 chunk.choices[0].delta.content 追加进列表,最后过滤掉 None(结束/无内容的 chunk)再 join 成完整回复。收益是延迟:文档实测里流式请求约 0.1 秒收到首个 token,之后每个 token 约 0.01–0.02 秒。
术语 delta(增量字段,流式 chunk 承载内容处); message(非流式完整回复字段); chunk(流式返回的分片事件); role token / content token(角色/内容增量)
📖 "To stream completions, set stream=True when calling the chat completions or completions endpoints." — 原文
📖 "Extract chunks from the delta field rather than the message field." — 原文
📖 "As you can see above, streaming responses have a delta field rather than a message field." — 原文
🧪 实例 收集流式 chunk 并拼回完整文本,注意过滤 None
python
collected_messages = []
for chunk in response:
    chunk_message = chunk.choices[0].delta.content  # extract the message
    collected_messages.append(chunk_message)
# clean None in collected_messages
collected_messages = [m for m in collected_messages if m is not None]
full_reply_content = ''.join(collected_messages)
🔍 追问 一个 chunk 的 delta 为空对象 {} 意味着什么? → 意味着流已经结束(nothing,when the stream is over),此时 delta.contentNone,应当在拼接前过滤掉。
📚 拓展阅读
Q流式响应下如何拿到 token 用量(usage)统计?有哪些坑?深挖·拓展中频
stream_options include_usage usage
⏱️ 现行
默认的流式响应里各 chunk 的 usage 字段是拿不到整体用量的,要获取需要显式设 stream_options={"include_usage": True}。开启后会在最后额外多流回一个 chunk,整个请求的 token 用量统计就挂在这个最终 chunk 的 usage 字段上。使用时有三个必须记住的行为:其一,除最后一个 chunk 外,所有 chunk 的 usage 字段都是 null;其二,最后一个 chunk 的 usage 才装着整个请求的 token 用量统计(prompt/completion/total tokens);其三,这个最后的 chunk 的 choices 字段永远是空数组 []。这意味着你的消费逻辑不能假设"每个 chunk 都有 choices 且都有内容"——遍历时既要处理带 choices[0].delta 的正常内容 chunk,也要单独处理那个 choices 为空、只带 usage 的收尾 chunk,否则用 chunk.choices[0] 直接索引会在最后一个 chunk 上越界。
术语 stream_options={"include_usage": True}(开启用量统计); usage(token 用量字段,仅末 chunk 非空); CompletionUsage(含 completion/prompt/total tokens); empty array [](末 chunk 的 choices 恒为空)
📖 "When you do so, an extra chunk will be streamed as the final chunk." — 原文
📖 "The value for the usage field on all chunks except for the last one will be null." — 原文
📖 "The choices field on the last chunk will always be an empty array []." — 原文
🧪 实例 开启 include_usage 后逐 chunk 打印 choices 与 usage:
python
response = client.chat.completions.create(
    model='gpt-4o-mini',
    messages=[{'role': 'user', 'content': "What's 1+1? Answer in one word."}],
    temperature=0,
    stream=True,
    stream_options={"include_usage": True},
)
for chunk in response:
    print(f"choices: {chunk.choices}\nusage: {chunk.usage}")

末尾会看到 choices: []usage: CompletionUsage(completion_tokens=2, prompt_tokens=18, total_tokens=20)
🔍 追问 为什么遍历流式 chunk 时不能对每个 chunk 都直接取 chunk.choices[0]? → 因为开启 include_usage 后最后一个 chunk 的 choices 恒为空数组 [],直接索引 [0] 会越界;该 chunk 只用来读整体 usage
📚 拓展阅读
Q什么是 context window?多轮对话里为什么必须关注它,超了会怎样?深挖·拓展中频
context-window tokens truncation
⏱️ 现行
理解 context window 是做好线程化对话、跨交互管理状态的前提。context window 是单次请求能使用的最大 token 数,这个上限同时包含 input、output 以及(部分模型的)reasoning token。随着输入变复杂、对话轮次增多,你需要同时盯住两个约束:一个是 output token 上限(各模型不同,如 gpt-4o-2024-08-06 最多生成 16,384 个 output token),一个是 context window 总量(如 gpt-4o-2024-08-06 总窗口 128k token)。权衡点在于:为了给模型更多上下文、数据或示例而堆大 prompt,会有超出模型分配窗口的风险,进而导致输出被截断(truncated)。对推理模型请求,input token、output token、reasoning token 都会计入窗口总量。超过窗口上限而生成的 token 可能在 API 响应里被截断——所以多轮对话要么裁剪历史、要么借助 compaction,而不能无限往里塞。可以用 tokenizer 工具(基于 tiktoken)预估一段文本占多少 token。
术语 context window(上下文窗口,单次请求 token 上限); output tokens(输出 token,模型生成部分); reasoning tokens(推理 token,计入窗口); truncated outputs(被截断的输出)
📖 "This max tokens number includes input, output, and reasoning tokens." — 原文
📖 "Tokens generated in excess of the context window limit may be truncated in API responses." — 原文
🧪 实例 文档给出的具体上限例子——
text
gpt-4o-2024-08-06: 最多 16,384 output tokens;总 context window 128k tokens
输入过大 → 超出窗口 → 输出可能被截断 (truncated)
🔍 追问 对推理(reasoning)模型来说,哪几类 token 会一起挤占 context window? → input tokens、output tokens 和模型用于规划响应的 reasoning tokens,三者都计入窗口总量。
📚 拓展阅读
QConversations API 存的是什么?它和 response 默认的 30 天留存有何不同?深挖·拓展中频
conversations-api ttl persistence
⏱️ 现行
Conversations API 与 Responses API 配合,把会话状态持久化成一个带自有持久标识符(durable identifier)的长驻对象;创建这个 conversation 对象后,你可以跨会话、跨设备、跨任务继续使用它。它存的不只是文本消息——conversation 里存的是 items,可以是消息、工具调用、工具输出等数据。用法上,多轮交互时把 conversation 传进后续的 responses 请求,就能持久化状态、在多次响应间共享上下文,而不必手动把多条 response item 一条条串起来。留存差异是关键取舍:普通 response 对象默认只保存 30 天(可用 store=false 关闭);但 conversation 对象及其中的 items 不受 30 天 TTL 约束,任何绑定到某个 conversation 上的 response,其 items 都会被无 30 天 TTL 地持久化。也就是说,选 Conversations 意味着换来长期持久(无 TTL),而单纯用 response/previous_response_id 链则默认 30 天到期。
术语 Conversations API(会话持久化接口); durable identifier(持久标识符); items(会话内条目:消息/工具调用/工具输出); 30 day TTL(30 天生存期,conversation 不受其约束)
📖 "Conversations store items, which can be messages, tool calls, tool outputs, and other data." — 原文
📖 "Conversation objects and items in them are not subject to the 30 day TTL. Any response attached to a conversation will have its items persisted with no 30 day TTL." — 原文
🧪 实例 创建 conversation 后传入 responses 请求以持久化并共享上下文:
python
conversation = openai.conversations.create()
response = openai.responses.create(
  model="gpt-5.6",
  input=[{"role": "user", "content": "What are the 5 Ds of dodgeball?"}],
  conversation="conv_689667905b048191b4740501625afd940c7533ace33a2dab"
)
🔍 追问 什么时候该用 Conversations API 而不是 previous_response_id? → 当需要跨会话/设备/任务长期保留、且不希望上下文在 30 天后过期时——conversation 对象及其 items 不受 30 天 TTL 约束,而普通 response 默认 30 天到期。
📚 拓展阅读
Q生产环境用流式输出,在内容审核(moderation)上有什么风险?深挖·拓展低频
moderation streaming safety
⏱️ 现行
流式虽好,但会给内容审核带来实际困难,这是上生产前必须权衡的点。核心问题是:在生产应用里流式返回模型输出,会让审核 completion 内容变得更难,因为部分完成(partial completions)的片段更难评估——你在只拿到半句话时,很难判断整段输出最终是否违规,而此时内容可能已经开始展示给用户了。这一点在 Responses 流式指南和 Chat Completions 流式 cookbook 里措辞一致,都提示这"可能对已批准的用途(approved usage)产生影响"。更具体的机制约束是:如果你在生成请求里同时请求 moderation 分数,这些分数要等到完整生成输出可用之后才会到达,它们不会随部分输出的 delta 一起返回。因此实践上的取舍是——要么接受"先展示后审核"的风险并加缓冲/延迟展示,要么在完整输出到手、moderation 分数返回后再放行,牺牲一部分流式带来的低延迟收益。
术语 moderation(内容审核); partial completions(部分完成的输出片段,难以评估); moderation scores(审核分数,须待完整输出后才返回); approved usage(已批准用途)
📖 "Note that streaming the model's output in a production application makes it more difficult to moderate the content of the completions, as partial completions may be more difficult to evaluate. This may have implications for approved usage." — 原文
📖 "the scores arrive after the full generated output is available. They aren't included with partial output deltas." — 原文
🧪 实例 决策取舍示意——
text
方案A: 边流边展示  → 首字节延迟低,但部分输出无法即时审核(风险)
方案B: 待完整输出+moderation 分数返回再放行 → 可审核,但失去流式低延迟
🔍 追问 为什么流式场景不能靠 delta 实时拿到 moderation 分数来边流边审核? → 因为 moderation 分数是在完整生成输出可用之后才到达的,不会包含在部分输出的 delta 里,所以流式过程中拿不到可用的实时审核结果。
📚 拓展阅读

第2章 · 函数调用与工具

🔥高频

函数调用(Function Calling)核心机制

原文 原文 原文
Q什么是 Function Calling?请说清 tool、tool call、tool call output 三个核心概念的区别。深挖·拓展🔥高频
function-calling 核心概念 术语
⏱️ 现行
Function calling(也叫 tool calling)是让模型与外部系统对接、访问训练数据之外的数据与能力的机制。要理解它必须先建立一套共享词汇:tool(工具/function) 是我们在抽象层面告诉模型它拥有的一块功能,模型在生成回复时可能判断自己需要某个工具提供的数据或能力;tool call(函数调用) 是模型检视 prompt 后、判断为了遵循指令需要调用某个可用工具时返回的一种特殊响应(例如对「巴黎天气如何」返回一个 get_weather 的调用,location 参数值为 Paris);tool call output(函数调用输出) 是工具用模型这次调用的输入所生成的响应,可以是结构化 JSON 也可以是纯文本,并且必须通过 call_id 引用到具体那一次模型 tool call。关键的权衡在于:模型本身并不执行任何函数,它只负责「决定调用哪个工具、填什么参数」,真正的执行、以及把结果回灌给模型换取最终自然语言回复,都由你的应用代码完成——这条边界正是安全与可控性的来源。
术语 tool/function(工具,抽象告知模型可用的一块功能); tool call/function call(模型请求调用工具的特殊响应); tool call output(工具执行后回传给模型的结果); call_id(把输出对应回具体某次调用的引用标识)
📖 "Function calling (also known as tool calling) provides a powerful and flexible way for OpenAI models to interface with external systems and access data outside their training data." — 原文
📖 "A function call or tool call refers to a special kind of response we can get from the model if it examines a prompt, and then determines that in order to follow the instructions in the prompt, it needs to call one of the tools we made available to it." — 原文
📖 "A function call output or tool call output refers to the response a tool generates using the input from a model's tool call." — 原文
🧪 实例 天气例子的三段式对应:
json
[
  { "role": "user", "content": "what's the weather in Paris?" },
  { "type": "function_call", "call_id": "call_12345xyz", "name": "get_weather", "arguments": "{\"location\":\"Paris, France\"}" },
  { "type": "function_call_output", "call_id": "call_12345xyz", "output": "{\"temperature\": \"25\", \"unit\": \"C\"}" }
]

模型给出 tool call,应用执行后用同一个 call_id 回传 output,最后模型才吐出「The weather in Paris today is 25C.」
🔍 追问 function 和 tool 是同一个东西吗? → 不完全是。function 是 tool 的一种(由 JSON schema 定义);除了 function tool,还有处理自由文本的 custom tools,以及平台内建的 built-in tools(如 web search、code interpreter、MCP)。
📚 拓展阅读
  • Using tools — 概览平台上所有可用工具(web search、file search、MCP、tool search 等)
  • Remote MCP — 通过 MCP 服务器给模型接入第三方能力
  • Web search — 内建的联网检索工具
Q完整的 tool calling 流程有哪几步?为什么它是一个「多步对话」而不是一次调用?深挖·拓展🔥高频
调用流程 Responses-API orchestration
⏱️ 现行
Tool calling 本质上是你的应用与模型之间、通过 OpenAI API 进行的一次多步对话,官方把它拆成五个高层步骤:(1) 带着可调用的 tools 向模型发请求;(2) 收到模型返回的 tool call;(3) 在应用侧用 tool call 的输入执行代码;(4) 带着 tool 输出发第二次请求;(5) 收到模型的最终回复(或更多 tool call)。它之所以必须是多步而非一次搞定,是因为模型不执行代码——它只能「提出」调用,真正拿数据/做动作发生在你的环境,然后结果要回灌模型才能生成最终答案;这也意味着任务需要几轮就能循环几轮,Responses API 允许这个流程持续进行直到任务完成。权衡在于:这种「模型决策 + 应用执行」的分层给了你完全的执行控制权与安全边界,代价是需要自己维护 input 列表、把模型输出和 tool 输出都追加回去。
术语 多步对话(application 与 model 交替的循环); Responses API(支持持续多轮 tool call 的接口); input_list(逐轮追加模型输出与函数结果的运行态列表) ``mermaid flowchart LR A[1 请求+tools] --> B[2 收到 tool call] B --> C[3 应用侧执行代码] C --> D[4 带 tool 输出再请求] D --> E[5 最终回复或更多 tool call] E -->|还有调用| B ``
📖 "Tool calling is a multi-step conversation between your application and a model via the OpenAI API." — 原文
📖 "With Responses, your application can continue this flow for as many tool calls as the task requires." — 原文
🧪 实例 get_horoscope 的最小闭环(Responses API):
python
# 2. 带 tools 请求
response = client.responses.create(model="gpt-5.6", tools=tools, input=input_list)
input_list += response.output  # 把模型输出存回,供后续请求使用
for item in response.output:
    if item.type == "function_call" and item.name == "get_horoscope":
        sign = json.loads(item.arguments)["sign"]
        horoscope = get_horoscope(sign)          # 3. 执行
        input_list.append({                       # 4. 回传结果
            "type": "function_call_output",
            "call_id": item.call_id,
            "output": horoscope,
        })
# 5. 再次请求拿最终回复
response = client.responses.create(model="gpt-5.6", tools=tools, input=input_list)
🔍 追问 用推理模型(如 GPT-5、o4-mini)时这个流程有什么额外要求? → 响应里返回的 reasoning items 也必须和 tool call 输出一起回传给模型,否则后续请求会缺失推理上下文。
📚 拓展阅读
Q一个 function 定义包含哪些字段?为什么 parameters 用 JSON schema?深挖·拓展🔥高频
函数定义 JSON-schema tools参数
⏱️ 现行
函数通常声明在每次 API 请求的 tools 参数里,每个可调用函数用同一套 schema 形状,含五个字段:type(恒为 function)、name(函数名如 get_weather)、description(何时以及如何使用该函数的说明)、parameters(用 JSON schema 定义的输入参数)、strict(是否对该调用启用严格模式)。之所以用 JSON schema 来描述 parameters,是因为可以直接复用它丰富的能力——属性类型、枚举 enum、描述、嵌套对象乃至递归对象,从而把「什么是合法参数」精确地表达给模型。权衡点在于:description 和参数说明写得越清楚,模型选对函数、填对参数的概率越高,但这些定义会被注入进 system message、计入上下文并按 input token 计费,所以既要表达充分又要控制体量。
术语 type(恒为 function); name(函数名); description(何时/如何使用的说明); parameters(JSON schema 定义的入参); strict(是否启用严格模式)
📖 "Functions are usually declared in the tools parameter of each API request." — 原文
📖 "Because the parameters are defined by a JSON schema, you can leverage many of its rich features like property types, enums, descriptions, nested objects, and, recursive objects." — 原文
🧪 实例 一个 get_weather 的标准定义:
json
{
  "type": "function",
  "name": "get_weather",
  "description": "Retrieves current weather for the given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string", "description": "City and country e.g. Bogotá, Colombia" },
      "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Units the temperature will be returned in." }
    },
    "required": ["location", "units"],
    "additionalProperties": false
  },
  "strict": true
}
🔍 追问 想按业务域分组多个工具怎么办? → 用 namespace("type": "namespace"),把 crmbillingshipping 等相关工具归组,当模型要在服务不同系统的工具间做选择时特别有用。
📚 拓展阅读
  • JSON Schema — parameters 所依据的 schema 规范
  • Playground — 生成并迭代函数 schema,且默认开启 strict mode
  • Tool search — 工具很多时延迟加载少用的工具
Q模型返回 function call 后,应用侧要怎么处理并把结果回传?深挖·拓展🔥高频
handling function_call_output call_id
⏱️ 现行
当模型调用函数时你必须执行它并返回结果;由于一次响应里可能包含零个、一个或多个调用,最佳实践是默认假设会有好几个,因此要遍历 response.output、只处理 type == "function_call" 的条目,从每条里取出 call_id(稍后用于提交结果)、name 和 JSON 编码的 arguments。执行后把结果作为一条 function_call_output 追加回 input,里面用同一个 call_id 对应回去。结果通常应是字符串,格式随你(JSON、错误码、纯文本都行),模型会按需解释这个字符串;返回图片或文件时可传 image/file 对象数组代替字符串;若函数没有返回值(如 send_email),就返回一个表示成功或失败的字符串(如 "success")。把结果追加进 input 后再发回模型即可拿到最终回复。设计权衡:把 output 统一成字符串简化了协议,但也意味着结构化信息的表达完全由你把控——写得越规整模型越好理解。
术语 function_call_output(回传结果的消息类型); call_id(把结果对应回具体调用); arguments(JSON 编码的入参字符串); response.output(可能含零/一/多个调用的输出数组)
📖 "When the model calls a function, you must execute it and return the result. Since model responses can include zero, one, or multiple calls, it is best practice to assume there are several." — 原文
📖 "The result you pass in the function_call_output message should typically be a string, where the format is up to you (JSON, error codes, plain text, etc.). The model will interpret that string as needed." — 原文
📖 "If your function has no return value (e.g. send_email), simply return a string that indicates success or failure. (e.g. "success")" — 原文
🧪 实例 执行并追加结果的标准骨架:
python
for tool_call in response.output:
    if tool_call.type != "function_call":
        continue
    name = tool_call.name
    args = json.loads(tool_call.arguments)
    result = call_function(name, args)
    input_messages.append({
        "type": "function_call_output",
        "call_id": tool_call.call_id,
        "output": str(result)
    })
🔍 追问 用了 tool search 时 output 数组里可能多出什么? → 可能在 function_call 之前看到 tool_search_calltool_search_output 条目;函数一旦被加载,后面照常处理该 function call 即可。
📚 拓展阅读
  • File inputs — 函数返回文件内容时的传参方式
  • Images — 函数返回图片内容时的传参方式
Qstrict mode 是什么?它靠什么保证参数一定符合 schema,有哪些硬性要求?深挖·拓展🔥高频
strict-mode structured-outputs 可靠性
⏱️ 现行
strict 设为 true 会确保函数调用可靠地贴合函数 schema,而不是「尽力而为」,官方建议始终启用严格模式。它底层是借助 structured outputs 特性实现的,因此引入两条硬性要求:其一,parameters 里每个对象都必须把 additionalProperties 设为 false;其二,properties 里所有字段都必须标为 required。想表达可选字段,就给该字段的 type 加上 null 选项。如果你发了 strict: true 但 schema 不满足这些约束,请求会被拒绝并说明缺哪些约束;若你省略 strict,默认行为依 API 而定——Responses 请求会尝试把 schema 规范化进严格模式,不行则回退到非严格的尽力函数调用,此时响应里的工具会显示 strict: false;Chat Completions 请求则默认非严格。权衡:严格模式换来可靠性,但 JSON schema 的部分特性不被支持;对微调模型还有首请求额外处理、缓存不适用零数据保留等注意点。
术语 strict(严格模式开关); structured outputs(底层保证机制); additionalProperties:false(禁止额外属性); required(所有字段必填); null type(表达可选字段的手段)
📖 "Setting strict to true will ensure function calls reliably adhere to the function schema, instead of being best effort. We recommend always enabling strict mode." — 原文
📖 "You can denote optional fields by adding null as a type option (see example below)." — 原文
🧪 实例 开启 strict 后用 ["string", "null"] 表达可选的 units
json
{
    "type": "function",
    "name": "get_weather",
    "strict": true,
    "parameters": {
        "type": "object",
        "properties": {
            "location": { "type": "string", "description": "City and country e.g. Bogotá, Colombia" },
            "units": { "type": ["string", "null"], "enum": ["celsius", "fahrenheit"], "description": "Units the temperature will be returned in." }
        },
        "required": ["location", "units"],
        "additionalProperties": false
    }
}
🔍 追问 微调模型下并行调用会不会影响 strict? → 会。当前若使用微调模型且模型在一个 turn 里调用了多个函数,那些调用会禁用 strict mode。
📚 拓展阅读
Qtool_choice 有哪几种取值?allowed_tools 为什么和 prompt caching 有关?深挖·拓展低频
tool_choice allowed_tools prompt-caching
⏱️ 现行
默认情况下模型自行决定何时、用多少个工具,你可以用 tool_choice 参数强制特定行为,共四类:Auto(默认,调用零/一/多个函数)、Requiredtool_choice: "required",至少调用一个)、Forced Function{"type":"function","name":"get_weather"},强制且只调用某一个指定函数)、Allowed tools(把可调用范围限制在工具的一个子集内)。此外把 tool_choice 设成 "none" 可以模仿「不传任何函数」的行为。allowed_tools 的价值在于:你想在多次请求间只开放工具的一个子集、又不想改动你传入的完整 tools 列表——保持 tools 列表稳定就能最大化 prompt caching 的节省。权衡在于:约束越强、模型跑偏越少,但也牺牲了让模型自主选择的灵活性;而 allowed_tools 恰好在「限制行为」和「保持缓存命中」之间取得平衡。
术语 auto(默认自主决定); required(至少一个); Forced Function(强制某一个); allowed_tools(限制到子集); none(等价不传函数); prompt caching(稳定前缀带来的成本节省)
📖 "By default the model will determine when and how many tools to use. You can force specific behavior with the tool_choice parameter." — 原文
📖 "You can also set tool_choice to "none" to imitate the behavior of passing no functions." — 原文
🧪 实例 allowed_tools 把可调用集限制到两个函数、但不改动传入 tools:
json
"tool_choice": {
    "type": "allowed_tools",
    "mode": "auto",
    "tools": [
        { "type": "function", "name": "get_weather" },
        { "type": "function", "name": "search_docs" }
    ]
}
🔍 追问 配合 tool search 使用时 tool_choice 作用在谁身上? → 作用在当前 turn 里「已可调用」的工具上,最适合在你加载了一个工具子集后、想把模型约束到该子集时使用。
📚 拓展阅读
  • Prompt caching — 为何保持 tools 列表不变能最大化节省
  • Tool search — 与 tool_choice 协同约束当前可调用工具
Q并行函数调用(parallel function calling)是什么?如何关闭,有哪些坑?深挖·拓展中频
parallel-tool-calls 多调用
⏱️ 现行
模型可能在单个 turn 里选择调用多个函数——像 gpt-5、gpt-4.1、gpt-4o 这类较新模型都能一轮内调用多个函数。你可以把 parallel_tool_calls 设为 false 来阻止这种行为,从而确保恰好调用零或一个工具。要注意两个坑:其一,使用 built-in 工具时无法进行并行函数调用;其二,gpt-4.1-nano-2025-04-14 这个快照在开启并行时有时会对同一个工具产生多次调用,官方建议用该 nano 快照时关闭此特性。权衡在于:并行调用能一轮拿到多个独立结果、减少往返延迟(例如同时查两个城市的天气),但也让结果处理复杂化、并在微调模型上会禁用这些调用的 strict mode,所以何时开启要按可靠性需求权衡。
术语 parallel_tool_calls(并行调用开关,false 即最多一个); single turn(单轮内多调用); built-in tools(内建工具,不支持并行)
📖 "The model may choose to call multiple functions in a single turn. You can prevent this by setting parallel_tool_calls to false, which ensures exactly zero or one tool is called." — 原文
📖 "Newer models such as gpt-5, gpt-4.1 or gpt-4o can call multiple functions in one turn." — 原文
🧪 实例 cookbook 里一轮内并行触发两次 get_n_day_weather_forecast(旧金山 + 格拉斯哥):
python
[
 ChatCompletionMessageFunctionToolCall(function=Function(arguments='{"location": "San Francisco, CA", "format": "fahrenheit", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'),
 ChatCompletionMessageFunctionToolCall(function=Function(arguments='{"location": "Glasgow, UK", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'),
]
🔍 追问 处理并行调用的响应时代码上要注意什么? → 要遍历整个 output 数组、对每条 function_call 分别执行并各用其 call_id 回传,不能只处理第一条。
📚 拓展阅读
Q在 Chat Completions API 里做函数调用是怎样的?API 会替我执行函数吗?深挖·拓展中频
chat-completions tools参数 finish_reason
⏱️ 现行
在 Chat Completions API 里,tools 是一个可选参数,用来提供函数规格(function specifications),目的是让模型生成符合该规格的函数参数。一个关键事实:API 本身并不会真的执行任何函数调用,执行要由开发者拿模型输出去做。默认情况下若提供了函数,模型会自行决定何时使用哪个函数;可以通过把 tool_choice 设成 {"type": "function", "function": {"name": "my_function"}} 强制使用某个特定函数,也可设成 "none" 强制不使用任何函数。当确实用了函数时,响应里会带 "finish_reason": "tool_calls",同时有一个 tool_calls 对象,包含函数名和生成的函数参数。这套「模型只生成参数、开发者负责执行」的分工是整个机制的安全基座——尤其像让模型生成 SQL 这类场景,在生产环境风险较高,因为模型并不能完美可靠地生成正确 SQL,所以执行环节留在开发者手里可控。
术语 tools(Chat Completions 里提供函数规格的可选参数); finish_reason: tool_calls(表示本轮用了函数); tool_calls(含函数名与参数的对象); tool_choice(auto/指定/none)
📖 "tools is an optional parameter in the Chat Completion API which can be used to provide function specifications. The purpose of this is to enable models to generate function arguments which adhere to the provided specifications. Note that the API will not actually execute any function calls. It is up to developers to execute function calls using model outputs." — 原文
📖 "in the response, as well as a tool_calls object that has the name of the function and the generated function arguments." — 原文
🧪 实例 cookbook 的四步闭环——取出 tool_calls、执行 ask_database、以 role: "tool" 追加结果、再请求:
python
tool_calls = response_message.tool_calls
if tool_calls:
    tool_call_id = tool_calls[0].id
    tool_function_name = tool_calls[0].function.name
    tool_query_string = json.loads(tool_calls[0].function.arguments)['query']
    if tool_function_name == 'ask_database':
        results = ask_database(conn, tool_query_string)
        messages.append({
            "role":"tool", "tool_call_id":tool_call_id,
            "name": tool_function_name, "content":results
        })
        model_response_with_function_call = client.chat.completions.create(
            model=GPT_MODEL, messages=messages,
        )
🔍 追问 role: "tool" 的消息有什么位置约束? → 带 role tool 的消息必须是对前一条含 tool_calls 的消息的回应,不能凭空出现。
📚 拓展阅读
Q怎样提升函数调用的准确率?函数定义为什么会影响 token 用量?深挖·拓展中频
最佳实践 准确率 token用量
⏱️ 现行
官方给出一组「定义函数」的最佳实践,核心思路是把负担从模型移走:写清晰详细的函数名、参数描述与说明,用 system prompt 告诉模型何时该用/不该用某函数;套用软件工程习惯,用 enum 和对象结构让非法状态无法表达(如 toggle_light(on, off) 就允许了非法组合),并通过「实习生测试」检验——只凭你给模型的信息,一个实习生能否正确使用这个函数;别让模型去填你已经知道的参数(比如已有 order_id 就别设该参数,用代码传入),把总是连续调用的函数合并;并且把初始可用函数数量保持得小以提高准确率,官方建议一个 turn 开始时可用函数数「尽量少于 20 个」(软性建议)。token 层面必须知道:函数在底层会以模型受过训练的语法注入进 system message,因此可调用的函数定义会计入模型上下文上限、并按 input token 计费——遇到 token 限制时应减少前置加载的函数数、缩短描述,或用 tool search 让延迟工具按需加载。
术语 intern test(实习生测试,检验函数自解释性); enum(用枚举让非法状态不可表达); <20 functions(初始可用函数数尽量少于 20); token 注入(函数被注入 system message 计费)
📖 "Aim for fewer than 20 functions available at the start of a turn" — 原文
📖 "This means callable function definitions count against the model's context limit and are billed as input tokens." — 原文
🧪 实例 「别让模型填你已知的参数」——把 order_id 从入参里去掉,改由代码传入:
python
# 反例:让模型填一个你其实已经知道的值
# submit_refund(order_id: str)
# 正例:无参数函数,order_id 由应用代码补上
def submit_refund():
    ...  # order_id 从上一步菜单上下文用代码传入
🔍 追问 给推理模型加示例(few-shot)一定有好处吗? → 不一定。文档特别提示:为 reasoning models 添加示例可能反而损害性能。
📚 拓展阅读
Q流式(streaming)函数调用与流式普通回复有何不同?如何把 delta 拼成完整调用?深挖·拓展低频
streaming arguments-delta 事件
⏱️ 现行
流式函数调用和流式普通回复非常相似:把 stream 设为 true,然后拿到一系列不同的 event 对象。差别在于——普通流式是把 chunk 聚合成单个 content 字符串,而函数调用流式是把 chunk 聚合成一个编码后的 arguments JSON 对象。机制上,模型开始调用函数时,每个函数调用会先发一个 response.output_item.added 事件(item 里含 in-progress 的 name/arguments/id);随后是一串 response.function_call_arguments.delta 事件,delta 字段带着 arguments 的增量,你按 output_index 把这些 delta 累加到对应调用上;当模型调用完毕,会发出一个 response.function_call_arguments.done 事件,包含完整的函数调用。价值在于:流式能实时呈现「正在调用哪个函数、参数如何被逐步填充」,改善用户可感知的进度;代价是客户端需要自己按索引维护并累加分片,逻辑比一次性拿结果更繁琐。
术语 response.output_item.added(函数调用开始事件); response.function_call_arguments.delta(参数增量事件); response.function_call_arguments.done(调用完成事件); output_index(区分多个并行调用的索引)
📖 "Streaming function calls is very similar to streaming regular responses: you set stream to true and get different event objects." — 原文
📖 "Instead of aggregating chunks into a single content string, however, you're aggregating chunks into an encoded arguments JSON object." — 原文
🧪 实例output_index 累加 delta 成最终 tool_call:
javascript
const finalToolCalls = {};
for await (const event of stream) {
    if (event.type === 'response.output_item.added') {
        finalToolCalls[event.output_index] = event.item;
    } else if (event.type === 'response.function_call_arguments.delta') {
        const index = event.output_index;
        if (finalToolCalls[index]) {
            finalToolCalls[index].arguments += event.delta;
        }
    }
}
🔍 追问 done 事件带来什么,为什么还需要它? → 它携带整个函数调用(含 name/arguments/id),作为「参数已填完」的明确信号,避免客户端靠猜测判断分片是否结束。
📚 拓展阅读
🔥高频

内置工具:Web 搜索 / File 搜索 / Code Interpreter

原文 原文 原文
QOpenAI 模型的 Web 搜索有哪三种类型,各自适合什么场景?深挖·拓展🔥高频
web_search agentic-search deep-research
⏱️ 现行
Web search 让模型能访问互联网上的最新信息并给出带来源引用的回答,官方把它分成三档、由"模型对搜索过程的介入程度"来区分。第一档 non‑reasoning web search 只是把用户 query 丢给搜索工具、再原样转述返回的 top results,没有任何内部规划,因此最快、适合快速查询;第二档 agentic search 用 reasoning 模型主动管理搜索——把 web search 作为 chain of thought 的一部分,分析结果并自行决定要不要继续搜,灵活但比快速查询更慢,适合复杂工作流,可通过调节 reasoning 等级来平衡深度与延迟;第三档 deep research 是最重的 agent 驱动方式,为深入、长时的调查设计,常常动用数百个来源、可运行数分钟,最好配合 background mode 使用。这三档的权衡本质是"延迟/成本 vs 深度/自主性":越往下模型越自主、越能覆盖多来源,但耗时和费用也越高,所以要按任务复杂度选档。
术语 non-reasoning web search(非推理搜索,只转述搜索结果); agentic search(模型主动管理搜索的推理式检索); deep research(agent 驱动的长时深度调查); background mode(后台异步运行模式,适合数分钟级任务)
📖 "Non‑reasoning web search: The non-reasoning model sends the user’s query to the web search tool, which returns the response based on top results. There’s no internal planning and the model simply passes along the search tool’s responses. This method is fast and ideal for quick lookups." — 原文
📖 "Deep research is a specialized, agent-driven method for in-depth, extended investigations by reasoning models. The model conducts web searches as part of its chain of thought, often tapping into hundreds of sources. Deep research can run for several minutes and is best used with background mode." — 原文
🧪 实例 最简单的 Responses API web search 调用——模型自行决定是否搜索:
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    tools=[{"type": "web_search"}],
    input="What was a positive news story from today?"
)

print(response.output_text)
flowchart TD
    A[用户 query] --> B{哪种搜索?}
    B -->|快速查询| C[non-reasoning: 转述 top results]
    B -->|复杂工作流| D[agentic: 边推理边搜索, 自行决定继续]
    B -->|深度调查| E[deep research: 数百来源, 数分钟, background]
🔍 追问 想让 deep research 跑得更深该怎么配? → 用 gpt-5.5 并把 reasoning 设为 highxhigh,同时用 background mode 承接数分钟级的长任务。
📚 拓展阅读
Qweb_searchweb_search_preview 有什么区别?新老集成该怎么选?深挖·拓展🔥高频
web_search web_search_preview migration
⏱️ 现行
二者都是 Responses API 里的托管 web 搜索工具,但能力不同、定位不同。官方明确要求新集成用 { "type": "web_search" },因为它支持一批更新的托管搜索控制项——filters(域名过滤)、external_web_access(在线/离线控制)、return_token_budget(返回 token 预算)等;而早期的 web_search_preview 仅为 legacy 集成保留,不支持这些新控制项,并且会忽略 external_web_access(表现得像始终 true)。选型上:全新的 web 搜索集成走 Responses API 的 web_search + gpt-5.5;只有当你必须保留一个已有的 Chat Completions 搜索集成时才用 gpt-5-search-api;多步研究或长时报告用 gpt-5.5high/xhigh reasoning 并开 background mode。一个关键差异是搜索的"可选性":Responses 里 search 是一个 tool(可选是否触发),而 Chat Completions 的搜索模型总是先搜再答。
术语 web_search(现行托管搜索工具,支持全部新控制项); web_search_preview(遗留搜索工具,不支持新控制项); gpt-5-search-api(Chat Completions 路径的搜索模型); tool_choice(控制 search 是否必须运行)
📖 "For new Responses API integrations, use { "type": "web_search" }. The earlier web_search_preview tool remains available for legacy integrations, but it does not support newer controls such as filters, external_web_access, and return_token_budget." — 原文
📖 "Chat Completions search models always search before responding; Responses search is a tool" — 原文
🧪 实例 何时 search 是"可选"vs"必须"——用 tool_choice 控制:
bash
# tool_choice: "auto" 时 search 是可选的;需要强制搜索时用 required
curl "https://api.openai.com/v1/responses" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $OPENAI_API_KEY" \
    -d '{
        "model": "gpt-5.6",
        "tools": [{"type": "web_search"}],
        "input": "what was a positive news story from today?"
}'
🔍 追问 gpt-4o-search-preview 这类老搜索模型还能用吗? → 它们已被弃用、将于 2026-07-23 关停,应迁移到 Responses 的 web_search,或在必须留在 Chat Completions 时改用 gpt-5-search-api
📚 拓展阅读
  • Deprecations — preview 搜索模型的关停时间表。
  • Chat API — Chat Completions 搜索集成路径。
QFile search 是什么?它靠什么机制检索知识库?深挖·拓展🔥高频
file_search vector-stores retrieval
⏱️ 现行
File search 是 Responses API 里的托管工具,让模型能在"之前上传的文件构成的知识库"里检索信息,检索方式是 semantic search(语义)加 keyword search(关键词)两种。它的核心抽象是 vector store:你先创建 vector store、把文件上传进去,就等于给模型接上了一个可查询的知识库,从而用外部资料增强模型自身的固有知识。整个流程是托管的——它由 OpenAI 管理,你不用在自己这端写执行代码,模型决定要用时会自动调用工具、从你的文件里取回信息并产出输出。使用前提是必须先把知识库建好:把文件上传到 File API(purpose="assistants")、创建 vector store、把文件加入 store,并轮询直到文件 status 变为 completed 才可用。这种设计的权衡在于把"分块/嵌入/索引/检索"的复杂性都交给了平台,换来极简的接入,但代价是检索行为(如返回条数)需要通过工具参数而非自建管线来调优。
术语 vector_stores(向量存储,托管的可检索知识库); semantic and keyword search(语义+关键词双路检索); file_search_call(输出中记录一次文件检索调用的 item); purpose="assistants"(上传文件时供助手/检索使用的用途标记)
📖 "It enables models to retrieve information in a knowledge base of previously uploaded files through semantic and keyword search." — 原文
📖 "This is a hosted tool managed by OpenAI, meaning you don't have to implement code on your end to handle its execution." — 原文
🧪 实例 建好知识库后,把 file_search 挂进 tools 并指向 vector store:
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    input="What is deep research by OpenAI?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": ["<vector_store_id>"]
    }]
)
print(response)
🔍 追问 上传文件后为什么要"轮询"? → 文件需要处理入库,官方要求"Run this code until the file is ready to be used (i.e., when the status is completed)",即状态变为 completed 前不能用于检索。
📚 拓展阅读
QCode Interpreter 工具解决什么问题?它运行在什么环境里?深挖·拓展🔥高频
code_interpreter container sandbox
⏱️ 现行
Code Interpreter 让模型能在一个沙箱环境里写并运行 Python 代码,用来解决数据分析、编码、数学等领域的复杂问题;它还能处理各种格式的数据文件、生成数据文件和图表图像,并能"迭代式"地写代码运行——一段跑不通的代码可以不断重写重跑直到成功,这也是它相较纯文本推理的核心价值:把"可验证的执行反馈"接进了模型循环。它还能增强最新推理模型(如 o3、o4-mini)的视觉智能,用于裁剪、缩放、旋转、变换图像。运行载体是 container:一个完全沙箱化的虚拟机,模型在里面跑 Python,容器里可以放你上传的文件或它自己生成的文件。有个命名细节值得注意——工具叫 Code Interpreter,但模型内部把它认作 "python tool",所以在 prompt 里最明确的触发方式是直接要求用 "the python tool"。
术语 code_interpreter(代码解释器工具); python tool(模型内部对该工具的称呼); container(完全沙箱化的虚拟机,运行 Python 的载体); memory_limit(容器内存档位,如 1g/4g/16g/64g)
📖 "The Code Interpreter tool allows models to write and run Python code in a sandboxed environment to solve complex problems in domains like data analysis, coding, and math." — 原文
📖 "The Code Interpreter tool requires a container object. A container is a fully sandboxed virtual machine that the model can run Python code in." — 原文
🧪 实例 用 auto 模式创建容器并让模型用 python tool 解方程:
bash
curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-5.6",
    "tools": [{
      "type": "code_interpreter",
      "container": { "type": "auto", "memory_limit": "4g" }
    }],
    "instructions": "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question.",
    "input": "I need to solve the equation 3x + 11 = 14. Can you help me?"
  }'
🔍 追问 prompt 里说 "code interpreter" 模型能懂吗? → 通常能懂,但最明确的调用方式是让它用 "the python tool",因为模型内部就是以此名字识别这个工具。
📚 拓展阅读
QWeb search 的输出结构长什么样?引用(citations)如何呈现?深挖·拓展中频
web_search_call annotations url_citation
⏱️ 现行
使用 web search 工具的响应包含两部分。第一部分是 web_search_call 输出项,带搜索调用的 ID,并在 web_search_call.action 里记录所采取的动作:search(一次网页搜索,通常但不总是包含所搜的 queries,会产生 tool call 费用)、open_page(打开页面,reasoning 模型支持)、find_in_page(在页内搜索,reasoning 模型支持)。第二部分是 message 输出项,文本结果在 message.content[0].text,被引用 URL 的标注在 message.content[0].annotations。默认情况下响应会为搜索结果中的 URL 带上内联引用,此外 url_citation 标注对象会包含被引来源的 URL、title 和 location(start_index/end_index)。这里有一条强制的产品规范:当向终端用户展示 web 结果或其中信息时,内联引用必须清晰可见且可点击——这是合规与可溯源的要求,而不仅是 UI 建议。
术语 web_search_call.action(搜索调用采取的动作: search/open_page/find_in_page); url_citation(内联引用标注,含 url/title/location); annotations(消息文本上的引用标注数组); start_index/end_index(引用在文本中的位置)
📖 "By default, the model's response will include inline citations for URLs found in the web search results. In addition to this, the url_citation annotation object will contain the URL, title and location of the cited source." — 原文
📖 "- A web_search_call output item with the ID of the search call, along with the action taken in web_search_call.action. The action is one of:" — 原文
🧪 实例 一次响应里 web_search_call 与带 url_citation 的 message 并列:
json
[
  {
    "type": "web_search_call",
    "id": "ws_67c9fa0502748190b7dd390736892e100be649c1a5ff9609",
    "status": "completed",
    "action": {
      "type": "search",
      "query": "latest news about AI"
    }
  },
  {
    "id": "msg_67c9fa077e288190af08fdffda2e34f20be649c1a5ff9609",
    "type": "message",
    "status": "completed",
    "role": "assistant",
    "content": [
      {
        "type": "output_text",
        "text": "On March 6, 2025, several news...",
        "annotations": [
          {
            "type": "url_citation",
            "start_index": 2606,
            "end_index": 2758,
            "url": "https://...",
            "title": "Title..."
          }
        ]
      }
    ]
  }
]
🔍 追问 想看模型实际访问过的所有 URL(不止被引用的那些)怎么办? → 用 sources 字段,它返回模型形成回答时查阅的完整 URL 列表,数量通常多于 citations。
📚 拓展阅读
Qsearch_context_sizereturn_token_budget 分别控制什么?有何权衡?深挖·拓展中频
search_context_size return_token_budget latency
⏱️ 现行
这两个参数控制 web 搜索"喂给模型多少内容",但作用点不同。search_context_size 控制在模型生成回答前,有多少来自 web 搜索结果的 context 可供模型使用——low 用于简单查询、medium 是平衡默认、high 用于答案需要更多搜索细节时;要注意它并不设定精确 token 数,也不保证具体来源或引用数量。return_token_budget 则控制在一次带 GPT-5+ reasoning 模型的 Responses 搜索运行中,工具能返回多少 web 搜索结果内容:default 用标准返回 token 预算,unlimited 则移除该预算上限。官方建议大多数请求保持默认,只有高强度研究/评测、需要检视大量页面、否则会撞上标准返回 token 上限时才用 unlimited——因为它会增加延迟和成本。它只适用于托管 Responses web_search 工具的 GPT-5+ reasoning 搜索,不改变 search context window,也不作用于非推理搜索、legacy Search API、container web search、Chat Completions 搜索模型或 web_search_preview;且只接受 defaultunlimited,其余值(null、数字、其他字符串)会被拒绝。
术语 search_context_size(生成前可用的搜索 context 量: low/medium/high); return_token_budget(搜索工具可返回内容的 token 预算: default/unlimited); background: true(长时多搜任务的后台异步模式); search context window(搜索上下文窗口,此参数不改变它)
📖 "search_context_size controls how much context from web search results is made available to the model before it generates a response. Use low for simple lookups, medium for a balanced default, and high when the answer may require more detail from search results. This setting does not set an exact token count or guarantee a specific number of sources or citations." — 原文
📖 "Use unlimited selectively because it can increase latency and cost. For long-running multi-search tasks, use background mode (background: true) so the request can keep running asynchronously and you can retrieve the final response later." — 原文
🧪 实例 简单查询用 low 省 token 与延迟:
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    tools=[{
        "type": "web_search",
        "search_context_size": "low",
    }],
    input="What movie won best picture in 2025?",
)

print(response.output_text)
🔍 追问 return_token_budget 能作用于 web_search_preview 吗? → 不能,它只适用于 GPT-5+ reasoning 的托管 web_search,不作用于 web_search_preview 等路径。
📚 拓展阅读
QCode Interpreter 的容器有 auto 和 explicit 两种创建方式,还有过期机制,如何理解?深挖·拓展中频
container auto-mode expiration
⏱️ 现行
创建容器有两种方式。Auto 模式:在 tool 配置里传 "container": { "type": "auto", "memory_limit": "4g", "file_ids": [...] },它会自动新建一个容器,或复用模型上下文里之前 code_interpreter_call 用过的活跃容器;不写 memory_limit 就保持默认 1 GB 档;可在输出的 code_interpreter_call 项里找到生成或复用的 container_id。Explicit 模式:用 v1/containers 端点显式创建容器(带上所需 memory_limit),再把它的 id 作为 tool 配置里的 container 值。内存档位可选 1g(默认)、4g16g64g,选定的 memory_limit 在容器整个生命周期内固定,不管是自动还是通过 API 创建。过期方面官方强烈建议把容器当作临时的、把数据都存在自己系统里:容器 20 分钟不用就过期,过期后在 v1/responses 里使用会失败、所有关联数据被丢弃且不可恢复(只能看到过期时的元数据快照),所以要趁容器活跃时下载所需文件;不能把过期容器变回活跃,只能新建并重传文件,旧容器内存里的状态(如 python 对象)会丢失;任何容器操作(检索、增删文件)都会刷新 last_active_at
术语 type: "auto"(自动创建或复用活跃容器); code_interpreter_call(输出项,含 container_id); last_active_at(最近活跃时间,任何操作都会刷新); ephemeral(临时的,数据应自存)
📖 "This automatically creates a new container, or reuses an active container that was used by a previous code_interpreter_call item in the model's context. Leaving out memory_limit keeps the default 1 GB tier for the container." — 原文
📖 "A container expires if it is not used for 20 minutes. When this happens, using the container in v1/responses will fail." — 原文
🧪 实例 explicit 模式——先建容器,再把返回的 id 传给 Response:
python
from openai import OpenAI
client = OpenAI()

container = client.containers.create(name="test-container", memory_limit="4g")

response = client.responses.create(
    model="gpt-5.6",
    tools=[{
        "type": "code_interpreter",
        "container": container.id
    }],
    tool_choice="required",
    input="use the python tool to calculate what is 4 * 3.82. and then find its square root and then find the square root of that result"
)

print(response.output_text)
🔍 追问 auto 模式建的容器还能用 containers API 访问吗? → 能,官方指出 auto 模式创建的容器也可通过 /v1/containers 端点访问。
📚 拓展阅读
QFile search 的检索结果如何定制?(结果条数 / 返回结果 / 元数据过滤)深挖·拓展中频
max_num_results include metadata-filtering
⏱️ 现行
File search 提供三类定制。其一,用 max_num_results 限制从 vector stores 取回的结果条数——这能同时降低 token 用量和延迟,但可能以降低答案质量为代价,是一个典型的成本/质量权衡。其二,默认情况下 file search 调用不会返回 search results(输出文本里只能看到 annotations 即对文件的引用);要把检索结果本身包含进响应,需在创建 response 时用 include 参数传 ["file_search_call.results"]。其三,metadata filtering:你可以基于文件的 metadata 过滤检索结果,做法是在 tool 里配 filters(如 {"type": "in", "key": "category", "value": ["blog", "announcement"]}),前提是先给 vector store 文件设置 attributes,再定义 filters,具体见 retrieval guide。这三者组合让你能在"检索多少、返回多少、检索哪些子集"三个维度上精细控制检索行为。
术语 max_num_results(限制取回结果条数,权衡 token/延迟 vs 质量); include: ["file_search_call.results"](把检索结果纳入响应); filters(基于文件 metadata 的过滤条件); attributes(vector store 文件上的元数据属性)
📖 "Using the file search tool with the Responses API, you can customize the number of results you want to retrieve from the vector stores. This can help reduce both token usage and latency, but may come at the cost of reduced answer quality." — 原文
📖 "While you can see annotations (references to files) in the output text, the file search call will not return search results by default." — 原文
🧪 实例 用 metadata filter 只检索 category 为 blog 或 announcement 的文件:
python
response = client.responses.create(
    model="gpt-5.6",
    input="What is deep research by OpenAI?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": ["<vector_store_id>"],
        "filters": {
            "type": "in",
            "key": "category",
            "value": ["blog", "announcement"]
        }
    }]
)
print(response)
🔍 追问 减少 max_num_results 一定更好吗? → 不一定,它降低 token 与延迟,但"may come at the cost of reduced answer quality",需按答案质量需求折中。
📚 拓展阅读
QWeb search 有哪些结果控制项?(域名过滤 / sources / 用户位置 / 在线访问)深挖·拓展低频
filters sources external_web_access user_location
⏱️ 现行
现行 web_search 工具提供多个结果控制项。Domain filtering 用 filters 参数把结果限制在特定域名集合,可配至多 100 个 allowed_domains 或至多 100 个 blocked_domains,格式要省略 http/https 前缀(如用 openai.com),且会连带包含子域名——注意它只在 Responses API 的 web_search 工具下可用。Sources 用 sources 字段返回模型查阅的完整 URL 列表(不同于只显示最相关引用的 inline citations),实时第三方源会被标为 oai-sportsoai-weatheroai-finance,该字段在 web_searchweb_search_preview 下都可用。User location 用近似位置(country/city/region/timezone)优化结果,country 用两位 ISO 码、timezone 用 IANA 时区;但 deep research 模型的 web 搜索不支持 user location。Live internet access 用 external_web_access 控制取用实时内容还是只用缓存/索引结果:设 false 进入离线/仅缓存模式,默认 true,而 preview 变体会忽略此参数、表现得像 true
术语 filters.allowed_domains/blocked_domains(域名白/黑名单,各上限 100); sources(完整查阅 URL 列表); external_web_access: false(离线/仅缓存模式); user_location(近似地理位置,deep research 不支持)
📖 "With the filters parameter you can configure up to 100 allowed_domains or up to 100 blocked_domains. When formatting domains, omit the HTTP or HTTPS prefix." — 原文
📖 "Set external_web_access: false on the web_search tool to run in offline/cache‑only mode." — 原文
🧪 实例 用域名白/黑名单聚焦权威医学来源,并用 include 拿到 sources:
bash
curl "https://api.openai.com/v1/responses" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-5.6",
    "reasoning": { "effort": "low" },
    "tools": [
      {
        "type": "web_search",
        "filters": {
          "allowed_domains": [
            "pubmed.ncbi.nlm.nih.gov",
            "clinicaltrials.gov",
            "www.who.int",
            "www.cdc.gov",
            "www.fda.gov"
          ],
          "blocked_domains": [
            "reddit.com",
            "quora.com",
            "wikipedia.org"
          ]
        }
      }
    ],
    "tool_choice": "auto",
    "include": ["web_search_call.action.sources"],
    "input": "Please perform a web search on how semaglutide is used in the treatment of diabetes."
  }'
🔍 追问 离线模式对 web_search_preview 有效吗? → 无效,preview 变体会忽略 external_web_access、表现得如同 external_web_accesstrue
📚 拓展阅读
中频

MCP 与连接器(Connectors)

原文 原文 原文
Q什么是 MCP 工具?connectors 和 remote MCP servers 有什么区别,它们怎么统一到 Responses API 里?深挖·拓展🔥高频
MCP connectors Responses API
⏱️ 现行
除了用 function calling 把工具交给模型外,OpenAI 允许你通过 connectors 和 remote MCP servers 给模型新增能力,让模型在需要时连接并操控外部服务。二者本质上是同一套机制的两种入口:connectors 是 OpenAI 自己维护的、面向 Google Workspace、Dropbox 等热门服务的 MCP 封装(wrapper);remote MCP servers 则可以是公网上任意一个实现了 remote MCP 协议的服务器。二者都通过内置的 mcp 工具类型接入 Responses API——区别只在于寻址方式:调用 remote MCP server 时你传 server_url(可能还要带 OAuth authorization),而调用 connector 时你传一个唯一标识某个内置 connector 的 connector_id(并且必须带 authorization)。OpenAI 之所以要自己做 connectors,是因为它优先补齐了那些还没有官方 remote MCP server 的服务;对于已经有官方 server 的(如 GitHub),直接传 server_url 即可。二者的权衡是:connector 省去了你自己找/信任第三方 server 的负担,但服务集有限;remote MCP server 更开放,但未经 OpenAI 验证、风险更高。
术语 connectors(OpenAI 维护的 MCP 封装,对接热门服务); remote MCP servers(公网上任意实现 remote MCP 的服务器); mcp(Responses API 内置工具类型); server_url(remote server 的地址参数); connector_id(唯一标识某内置 connector)
📖 "Connectors are OpenAI-maintained MCP wrappers for popular services like Google Workspace or Dropbox, like the connectors available in ChatGPT." — 原文
📖 "Remote MCP servers can be any server on the public Internet that implements a remote Model Context Protocol (MCP) server." — 原文
📖 "However, instead of passing a server_url as you would to call a remote MCP server, you pass a connector_id which uniquely identifies a connector available in the API." — 原文
🧪 实例 用一个公开的 D&D 掷骰 MCP server(remote MCP):
bash
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
  "model": "gpt-5.6",
    "tools": [
      {
        "type": "mcp",
        "server_label": "dmcp",
        "server_description": "A Dungeons and Dragons MCP server to assist with dice rolling.",
        "server_url": "https://dmcp-server.deno.dev/mcp",
        "require_approval": "never"
      }
    ],
    "input": "Roll 2d4+1"
  }'

换成 connector(如 Dropbox)时把 server_url 换成 connector_id: "connector_dropbox" 并加上 authorization 即可。
🔍 追问 已经有官方 MCP server 的服务(如 GitHub),要用 connector 还是 server_url? → 直接用 server_url;文档明确 GitHub 有官方 MCP server,传 https://api.githubcopilot.com/mcp/ 到 server_url 即可,connectors 优先覆盖那些没有官方 server 的服务。
📚 拓展阅读
QMCP 工具在一次请求里到底经历了哪些步骤?为什么 mcp_list_tools 要留在上下文里?深挖·拓展🔥高频
mcp_list_tools mcp_call caching transport
⏱️ 现行
MCP 工具的执行分两步。Step 1 列工具:当你在 tools 里指定一个 remote MCP server,运行时先探测该 server 讲的是哪种传输协议(较新的 Streamable HTTP 或较老的 HTTP/SSE),再用它拉取工具清单,成功后在响应 output 里生成一个 mcp_list_tools item,其 tools 字段列出成功导入的工具。Step 2 调工具:模型拿到工具定义后可自行决定调用,运行时向 remote server 发请求执行并把输出塞回模型上下文,生成一个 mcp_call item,里面既有模型给出的 arguments 也有 server 返回的 output;失败则填 error 字段。关键的性能设计是缓存:只要 mcp_list_tools item 还在上下文里,API 就不会在对话的每一轮重新去 server 拉清单——这相当于在“用户-会话”粒度做缓存。因此文档建议把这个 item 保留在每次会话/工作流中以优化延迟;实践上可用 previous_response_id 保证它出现在后续轮次,否则清单会被重新导入、平白增加 token 与延迟。
术语 mcp_list_tools(列出并缓存已导入工具的输出 item); mcp_call(单次工具调用的 item,含 arguments 与 output); Streamable HTTP/HTTP/SSE(两种支持的传输协议); previous_response_id(把上一轮输出带入下一轮以保留缓存)
📖 "If the model decides to use a Connector or MCP server, it will first make a request to list available tools from the server, which will create a mcp_list_tools output item." — 原文
📖 "The Responses API works with remote MCP servers that support either the Streamable HTTP or the HTTP/SSE transport protocols." — 原文
📖 "As long as this item is present in the model's context, we will not call tools/list on the server again. This is akin to caching at the user-conversation level." — 原文
🧪 实例 一次典型流程可视化:
flowchart TD
    A[tools 里声明 mcp server] --> B[探测 transport:
Streamable HTTP / HTTP-SSE] B --> C[调用 tools/list
生成 mcp_list_tools] C -->|item 留在上下文| C C --> D{模型决定调用工具?} D -->|是| E[生成 mcp_call
arguments + output] D -->|否| F[直接返回最终答案] E --> D

mcp_call item 实例:
json
{
  "id": "mcp_68a6102d8948819c9b1490d36d5ffa4a0679e572a900e618",
  "type": "mcp_call",
  "approval_request_id": null,
  "arguments": "{\"diceRollExpression\":\"2d4 + 1\"}",
  "error": null,
  "name": "roll",
  "output": "4",
  "server_label": "dmcp"
}
🔍 追问 使用 MCP 工具会额外按“每次工具调用”收费吗? → 不会;文档说你只为导入工具定义或发起工具调用时用掉的 token 付费,"There are no additional fees involved per tool call."
📚 拓展阅读
QMCP 工具调用的审批(approval)机制是怎样的?默认行为、如何跳过、为什么要有它?深挖·拓展🔥高频
approval require_approval 安全
⏱️ 现行
默认情况下,OpenAI 在把任何数据分享给 connector 或 remote MCP server 之前都会先请求你的批准,目的是让你对“正在把什么数据发给 MCP server”保持控制与可见性。机制上:当需要审批时,响应 output 里会出现一个 mcp_approval_request item(含拟调用的 namearguments);你要放行就新建一个 Response,并往里追加一个 mcp_approval_response item(approve: true,带上对应的 approval_request_id),通常配合 previous_response_id 把这次响应接到产生审批请求的那次响应上。权衡在于延迟与信任:审批流保证敏感动作有人把关,但会增加往返。等你确信某个 server 可信后,可以把 require_approval 设成一个只列出要跳过的工具的对象,或直接设为 'never' 跳过该 server 所有工具的审批以换取更低延迟。文档同时强调对敏感动作应始终保留审批(配合 allowed_tools)。
术语 require_approval(审批策略参数:never / always / 对象白名单); mcp_approval_request(请求审批的输出 item); mcp_approval_response(放行/拒绝的输入 item); approval_request_id(把响应接回对应审批请求)
📖 "By default, OpenAI will request your approval before any data is shared with a connector or remote MCP server." — 原文
📖 "You can then respond to this by creating a new Response object and appending an mcp_approval_response item to it." — 原文
📖 "If and when you feel comfortable trusting a remote MCP server, you can choose to skip the approvals for reduced latency." — 原文
🧪 实例 只对部分工具跳过审批:
json
{
  "type": "mcp",
  "server_label": "deepwiki",
  "server_url": "https://mcp.deepwiki.com/mcp",
  "require_approval": {
    "never": {
      "tool_names": ["ask_question", "read_wiki_structure"]
    }
  }
}

放行一次待审批的调用:
bash
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
    "model": "gpt-5.6",
    "previous_response_id": "resp_682d498bdefc81918b4a6aa477bfafd904ad1e533afccbfa",
    "input": [{
      "type": "mcp_approval_response",
      "approve": true,
      "approval_request_id": "mcpr_682d498e3bd4819196a0ce1664f8e77b04ad1e533afccbfa"
    }]
  }'
🔍 追问 通过 API 用 deep research 模型连 MCP server 时,审批能开着吗? → 不能;mcp.md 明确 "MCP servers used via API for deep research have to be configured with no approval required.",必须配成免审批。
📚 拓展阅读
Q一个 MCP server 暴露几十个工具会带来什么问题?allowed_tools 怎么解决?深挖·拓展低频
allowed_tools token 优化 latency
⏱️ 现行
有些 MCP server 会暴露几十个工具,把大量工具都塞给模型会导致高成本和高延迟——每个工具都附带 name、description、JSON schema 等冗长定义,动辄给上下文加数百 token;更糟的是很多 server 即便只有几个字段相关也会返回整个数据对象(比如完整的 Stripe 发票记录)。解决办法是用 allowed_tools 参数,只从 server 的 mcp_list_tools 里导入你关心的那一部分工具。这样做的收益是三重的:降低 token 开销、缩短响应时间、并收窄模型的决策空间(decision space),让它更少犯选错工具的错。除性能外,allowed_tools 也是安全手段——你可能想彻底排除某些具备写操作、或有财务/安全影响的工具。它与审批参数常配合使用,共同确保敏感动作可控。
术语 allowed_tools(白名单,限制从 server 导入哪些工具); mcp_list_tools(server 暴露的全部工具清单); decision space(模型可选动作的空间,越窄越不易选错); token overhead(工具定义占用的上下文成本)
📖 "Some MCP servers can have dozens of tools, and exposing many tools to the model can result in high cost and latency." — 原文
📖 "This reduces token overhead, improves response time, and narrows the model’s decision space." — 原文
🧪 实例 只导入两个文档工具:
bash
curl https://api.openai.com/v1/responses -i \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4.1",
    "tools": [
        {
            "type": "mcp",
            "server_label": "gitmcp",
            "server_url": "https://gitmcp.io/openai/tiktoken",
            "allowed_tools": ["search_tiktoken_documentation", "fetch_tiktoken_documentation"],
            "require_approval": "never"
        }
    ],
    "input": "how does tiktoken work?"
}'
🔍 追问 如果某个 server 暴露的工具实在太多,除了 allowed_tools 还有别的省 token 办法吗? → 有;配合 tool search 时可在 MCP 工具定义上设 defer_loading: true,模型仍能靠 server 的 label/description 决定何时搜索,但具体函数定义仅在需要时才加载,对暴露大量函数的 server 最有用。
📚 拓展阅读
Q连接需要认证的 MCP server 时,authorization 怎么传?OpenAI 会存这个 token 吗?深挖·拓展中频
authentication OAuth authorization
⏱️ 现行
大多数 MCP server(不同于文档里那个公开的掷骰示例)都需要认证,最常见的方案是 OAuth access token,你通过 MCP 工具的 authorization 字段提供它。安全设计上很关键的一点是:为防止敏感 token 泄露,Responses API 不会存储你在 authorization 里提供的值,这个值也不会出现在返回的 Response 对象里——正因为不存,你必须在每一次创建 Responses 请求时都重新带上 authorization。对 connectors 同理:connector 需要 connector_id 加上一个由你的应用提供的 OAuth access token 放进 authorization,而 OAuth 的客户端注册与授权要由你的应用自行处理。文档还给了个测试技巧:可以用 Google 的 OAuth 2.0 Playground 生成临时 access token 来测 connectors。
术语 authorization(承载 OAuth access token 的字段); OAuth access token(最常见的认证凭据); connector_id(connector 的标识,需与 authorization 搭配); OAuth 2.0 Playground(生成临时 token 的测试工具)
📖 "The most common scheme is an OAuth access token." — 原文
📖 "To prevent the leakage of sensitive tokens, the Responses API does not store the value you provide in the authorization field." — 原文
📖 "Because of this, you must send the authorization value in every Responses API creation request you make." — 原文
🧪 实例 带 OAuth token 调用 Stripe MCP:
bash
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
    "model": "gpt-5.6",
    "input": "Create a payment link for $20",
    "tools": [
      {
        "type": "mcp",
        "server_label": "stripe",
        "server_url": "https://mcp.stripe.com",
        "authorization": "$STRIPE_OAUTH_ACCESS_TOKEN"
      }
    ]
  }'
🔍 追问 私有/内网/防火墙后的 MCP server 怎么连而不暴露到公网? → 用 Secure MCP Tunnel 把它接到受支持的 OpenAI 产品上而无需暴露到公网,客户端从 openai/tunnel-client 下载最新发布版。
📚 拓展阅读
QResponses API 目前内置支持哪些 connectors?怎么用 connector_id 调用?为什么只做这些?深挖·拓展中频
connectors connector_id 第三方服务
⏱️ 现行
Responses API 内置支持一组有限的第三方服务 connector,让你把 Dropbox、Gmail 等热门应用的上下文拉进来供模型交互。可用清单为:Dropbox connector_dropbox、Gmail connector_gmail、Google Calendar connector_googlecalendar、Google Drive connector_googledrive、Microsoft Teams connector_microsoftteams、Outlook Calendar connector_outlookcalendar、Outlook Email connector_outlookemail、SharePoint connector_sharepoint。用法与 remote MCP server 完全一致,只是传 connector_id 而非 server_url,并在 authorization 里放 OAuth token。设计取舍上,OpenAI 优先做了那些没有官方 remote MCP server 的服务;像 GitHub 这类已有官方 server 的,直接用 server_url 连即可。每个 connector 实际可用的工具取决于你 OAuth token 拥有的 scopes——比如 Google Calendar 的 search/fetch/search_events 等都需要 calendar.events scope。connector 的一次调用在输出里同样表现为 mcp_call item,只是其 arguments 和 output 都是 JSON 字符串。
术语 connector_id(如 connector_googlecalendar,唯一标识内置 connector); scopes(OAuth token 权限,决定可用工具集); mcp_call(connector 调用同样产生此 item)
📖 "The Responses API has built-in support for a limited set of connectors to third-party services." — 原文
📖 "We prioritized services that don't have official remote MCP servers. GitHub, for instance, has an official MCP server you can connect to by passing https://api.githubcopilot.com/mcp/ to the server_url field in the MCP tool." — 原文
📖 "The available tools depend on which scopes your OAuth token has available to it." — 原文
🧪 实例 用 Google Calendar connector 查今天日程:
bash
curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-5.6",
    "tools": [
      {
        "type": "mcp",
        "server_label": "google_calendar",
        "connector_id": "connector_googlecalendar",
        "authorization": "ya29.A0AS3H6...",
        "require_approval": "never"
      }
    ],
    "input": "What is on my Google Calendar for today?"
  }'
🔍 追问 connector 的 mcp_call 和 remote server 的有什么不同? → 形态一样、都用 mcp_call item 类型;文档说 connector 的这种调用“will look the same as an MCP tool call from a remote MCP server”,只是 arguments 和 response 都是 JSON 字符串。
📚 拓展阅读
Q要给 ChatGPT / deep research 自建一个 remote MCP server,必须实现哪两个工具?它们的返回结构和 citation 有什么讲究?深挖·拓展中频
自建server search fetch citation
⏱️ 现行
要让自建 remote MCP server 兼容 ChatGPT 的 deep research 和 company knowledge(以及经 API 的 deep research),server 应实现两个只读工具:searchfetch,并遵循 company knowledge 兼容 schema。search 负责根据用户 query 返回一组相关结果,参数是单个 query 字符串,返回一个只含 results 键的对象,每个结果对象至少含 idtitleurlfetch 用于取回某个结果文档的完整内容,参数是一个唯一标识符,返回含 idtitletexturl 以及可选 metadata 的对象。协议上要求把这个对象既作为 structuredContent 返回、又把同样的值以 JSON 编码字符串放进 content 数组以保证兼容。citation 行为是关键陷阱:ChatGPT 只在 url 为非空字符串时才生成引用元数据——有 title 但没有可用 url 的结果只会是普通工具输出,不会变成空引用;所以想让结果可被引用,就要返回它的规范 url。此外每个工具都应声明 output schema 以便客户端校验结果形状(FastMCP 里可由带类型的返回模型自动生成)。
术语 search(只读检索工具,返回 results 列表); fetch(只读取全文工具); structuredContent(结构化返回,需与 content 数组里的 JSON 字符串一致); url(非空字符串才触发 citation 元数据); output schema(供客户端校验结果形状)
📖 "To work with ChatGPT deep research and company knowledge (and deep research via API), your MCP server should implement two read-only tools: search and fetch, using the compatibility schema in Company knowledge compatibility." — 原文
📖 "In MCP, return this object as structuredContent and include the same value as\na JSON-encoded string in the content array for compatibility." — 原文
📖 "To make a result citable, return its canonical url." — 原文
🧪 实例 search 的返回应长这样(structuredContent 与 content 冗余同值):
json
{
  "structuredContent": {
    "results": [{ "id": "doc-1", "title": "...", "url": "..." }]
  },
  "content": [
    {
      "type": "text",
      "text": "{\"results\":[{\"id\":\"doc-1\",\"title\":\"...\",\"url\":\"...\"}]}"
    }
  ]
}

FastMCP 里用带类型的 pydantic 模型声明 output schema:
python
@mcp.tool(output_schema=SearchOutput.model_json_schema())
async def search(query: str) -> SearchOutput:
🔍 追问 经 API 用 deep research 模型测这个自建 server 时有什么硬性约束? → server 必须配成免审批;mcp.md 说 "MCP servers used via API for deep research have to be configured with no approval required."
📚 拓展阅读
Q接入 remote MCP server / connector 有哪些安全风险?为什么“信任开发者”不足以保证安全?深挖·拓展中频
安全 prompt injection trusted servers ZDR
⏱️ 现行
MCP 工具把模型接到外部服务,是强大但有风险的能力。对 connectors,风险在于可能把敏感数据发给 OpenAI、或让模型读到这些服务里的敏感数据;remote MCP servers 有同样风险,还额外未经 OpenAI 验证,能让模型访问、收发数据甚至在服务里执行动作。核心威胁是 prompt injection:攻击者把恶意指令埋进模型可能遇到的内容(如网页)里,企图覆盖模型本来的行为;一旦模型照做,就可能把私密数据发到外部目的地。文档反复强调“信任 MCP 开发者”并不足以让它安全——因为攻击可能来自另一个恶意源(如经另一个 email MCP 投毒),诱导模型从你信任的内部工具里读敏感数据再外泄;即使你完全信任某 server,只要它的写操作有可被攻击者观察到的后果,就仍可能被利用。缓解手段包括:对含用户输入的 prompt 谨慎使用这些工具、用 require_approvalallowed_tools 对敏感动作强制审批、只连服务商官方托管的 server(而非第三方代理的“聚合器”)、记录并定期复核发给第三方 server 的数据。合规上,MCP 工具兼容 Zero Data Retention 与 Data Residency,但 MCP server 是第三方服务,发给它的数据要遵守其自身的保留与驻留政策。
术语 prompt injection(把恶意指令埋进模型会遇到的内容以劫持其行为); write actions(写操作,风险高于只读,ChatGPT 需人工确认); aggregators(代理转发的第三方“聚合器”,不推荐); Zero Data Retention / Data Residency(数据零留存/驻留,仅覆盖到数据发出 server 之前)
📖 "Remote MCP servers carry those same risks, but also have not been verified by OpenAI." — 原文
📖 "Prompt injections are a form of attack where an attacker embeds malicious instructions in content that one of our models is likely to encounter–such as a webpage–with the intention that the instructions override ChatGPT’s intended behavior." — 原文
📖 "Pick official servers hosted by the service providers themselves (e.g. we recommend connecting to the Stripe server hosted by Stripe themselves on mcp.stripe.com, instead of a Stripe MCP server hosted by a third party)." — 原文
📖 "The MCP tool is compatible with Zero Data Retention and Data Residency, but it's important to note that MCP servers are third-party services, and data sent to an MCP server is subject to their data retention and data residency policies." — 原文
🧪 实例 一个被文档点名的 injection 攻击链——你让 ChatGPT 查邮件订餐厅,它在浏览时撞上一段恶意内容,被诱导去 Gmail 取密码重置码再发到恶意站点。对应缓解:对写操作强制审批 + 只连可信 server。开启数据留存以便复核:
bash
# store=true 时,数据经 API 记录 30 天(除非组织启用了 Zero Data Retention)
curl https://api.openai.com/v1/responses -d '{ "store": true, "tools": [ ... ] }'
🔍 追问 一个 server 把“写操作”错标成“只读”会怎样? → 很危险;文档指出即使 server 把动作标成 read only,写操作仍可能发生,这更凸显部署前必须信任该 server;发现恶意 server 应上报 security@openai.com。
📚 拓展阅读
低频

Computer Use 与高级工具(apply-patch、程序化调用)

原文 原文 原文
QComputer use 提供了哪三种集成路径(integration path)?分别在什么场景下选哪一种?深挖·拓展中频
computer-use harness 架构选型
⏱️ 现行
Computer use 让模型通过用户界面操作软件——它可以查看截图、返回供你的代码执行的界面动作,也可以在一个混合了视觉交互与程序化交互的自定义 harness 中工作。官方给出三条集成路径,本质是在"模型控制粒度"和"你已有基础设施复用度"之间做权衡。Option 1(built-in Computer use loop)用第一方 computer 工具,模型返回结构化 UI 动作(click/type/scroll/screenshot),你的 harness 充当键鼠之手,适合"人能通过 UI 完成"的任务,如导航站点、填表、走多步流程;它上手最快但每步都要回传截图。Option 2(custom tool or harness)适用于你已经有成熟的 Playwright/Selenium/VNC/MCP harness、且已有动作执行、可观测性、重试或领域护栏时——不必围绕 built-in 工具重建,只需把现有 harness 暴露成普通工具接口,并可让模型一轮触发多个动作以提速。Option 3(code-execution harness)给模型一个运行时让它写并运行短脚本,在视觉交互与程序化 UI 交互(含 DOM workflow)之间灵活切换,gpt-5.4 被显式训练来用好这条路径,适合需要循环、条件逻辑、DOM 检查或更丰富浏览器库的长流程,能改善速度、token 效率与灵活性。核心权衡:built-in 直观但受截图往返约束;custom 复用已有资产;code-execution 表达力最强、适合复杂控制流。
术语 computer tool(第一方视觉交互工具); harness(执行模型返回动作的宿主环境); code-execution harness(让模型写并跑短脚本的运行时); DOM-based workflows(基于文档对象模型的程序化交互)
📖 "Computer use lets a model operate software through the user interface. It can inspect screenshots, return interface actions for your code to execute, or work through a custom harness that mixes visual and programmatic interaction with the UI." — 原文
📖 "Option 3: Use a code-execution harness](#option-3-use-a-code-execution-harness) when you want the model to write and run short scripts in a runtime and move flexibly between visual interaction and programmatic UI interaction, including DOM-based workflows. gpt-5.4 and future models are explicitly trained to work well with this option." — 原文
🧪 实例 三种路径的选择流程可视化如下:

flowchart TD
    A[要用 Computer use] --> B{已有成熟 harness?}
    B -- 否, 想最快原型 --> C[Option 1: built-in computer 工具
模型返回 click/type/scroll/screenshot] B -- 是 Playwright/Selenium/VNC/MCP --> D[Option 2: 暴露为普通工具接口
复用重试与护栏] A --> E{需要循环/条件/DOM 检查?} E -- 是 --> F[Option 3: code-execution harness
模型写并运行短脚本]
🔍 追问 Option 2 该用哪些指标去和现有方案比较? → 官方列出 4 项:相同 workflow 的 turn count、完成耗时(time to complete)、UI 状态异常时的恢复行为、以及在确认/域名允许列表/敏感数据上保持 on-policy 的能力。
📚 拓展阅读
  • Playwright — 推荐用于本地浏览器自动化的框架,快速搭原型
  • Selenium — 另一款可作为 Computer use 本地 harness 的浏览器自动化框架
  • Images and Vision guide — 讲解图像输入的 detail 级别,与截图回传相关
Qbuilt-in Computer use loop 是怎么运转的?它的五个步骤和收敛条件是什么?深挖·拓展中频
computer-use agent-loop computer_call
⏱️ 现行
built-in 循环的本质是"截图—动作—再截图"的闭环:模型通过截图观察当前 UI,返回 click/type/scroll 之类动作,你的 harness 在浏览器或计算机环境里执行这些动作;动作跑完后 harness 回传新截图,模型据此看到变化并决定下一步——harness 是键鼠之手,模型靠截图理解界面状态并规划下一步。官方把它拆成五步:(1) 用启用了 computer 工具的请求把任务发给模型;(2) 检查返回的 computer_call;(3) 按顺序执行 actions[] 数组里的每个动作;(4) 截取更新后的屏幕并作为 computer_call_output 回传;(5) 重复直到模型不再返回 computer_call。第一轮往往只返回一个 screenshot 请求(模型要先看再动,这是正常的),后续轮次可以把多个动作 batch 进同一个 computer_call,必须按序执行完再截图。延续循环最简单的做法是每一后续轮带上 previous_response_id 并复用同一份工具定义。当响应不再含 computer_call 时,把剩余 output 项当作模型的最终答复或交接来读。
术语 computer_call(模型返回的界面动作调用); actions[](一轮内需按序执行的动作数组); computer_call_output(回传截图的输出项); previous_response_id(延续多轮循环的引用)
📖 "The model looks at the current UI through a screenshot, returns actions such as clicks, typing, or scrolling, and your harness executes those actions in a browser or computer environment." — 原文
📖 "Later turns can batch actions into the same computer_call. Run them in order before taking the next screenshot." — 原文
🧪 实例 一个把 click 与 type 批到同一轮的 computer_call(原文 "Batched actions in one turn"):

json
{
  "output": [
    {
      "type": "computer_call",
      "call_id": "call_002",
      "actions": [
        { "type": "click", "button": "left", "x": 405, "y": 157 },
        { "type": "type", "text": "penguin" }
      ],
      "status": "completed"
    }
  ]
}
🔍 追问 built-in loop 里模型可能返回哪些动作类型?keypress 和鼠标修饰键有什么区别? → 动作类型有 clickdouble_clickscrolltypewaitkeypressdragmovescreenshotkeypress 用于独立键盘输入;需要按住修饰键的鼠标交互应用鼠标动作可选的 keys 数组,而不是把交互拆成单独的键盘和鼠标步骤。
📚 拓展阅读
  • Images and Vision guide — 截图作为图像输入时 detail 级别对精度和 token 的影响
  • Playwright — 官方示例中执行动作与截图所用的浏览器库
Qapply_patch 工具解决什么问题?它有哪三种 operation,harness 要负责什么?深挖·拓展中频
apply_patch 代码编辑 V4A-diff
⏱️ 现行
apply_patch 让模型用结构化 diff 在你的代码库里创建、更新、删除文件——不是仅仅"建议"改动,而是发出 patch operation,由你的应用去应用并回报结果,从而支撑可迭代的多步代码编辑工作流。整体流程是:用 tools=[{"type": "apply_patch"}] 调 Responses API 并给模型文件上下文(或探索文件系统的工具)→ 模型返回一个或多个 apply_patch_call,每个描述单个文件操作 → 你跑 patch harness 解释每个 call 的 operation diff、应用到工作目录、记录成功与否及日志 → 对每个 call_id 回传恰好一个 apply_patch_call_outputstatus: "completed""failed" 加人类可读 output)→ 模型继续编辑或给出面向人的解释。三种 operation 是:create_filediff 是代表完整文件内容的 V4A diff)、update_filediff 是含增删或替换的 V4A diff)、delete_file(无 diff,直接整文件删除)。关键分工:你不提供输入 schema,模型自己知道怎么构造 operation 对象;你的 harness 负责解释 V4A diff 格式并应用改动。权衡与稳健性上要做 path validation 防目录穿越、备份或用 scratch copy、失败时始终返回 failed 加信息性 output、并决定是"全有或全无"回滚还是逐文件成败。
术语 apply_patch_call(模型返回的单文件操作); V4A diff(patch 使用的 diff 格式); apply_patch_call_output(回报每次应用结果的事件); patch harness(解释并应用 diff 的脚本)
📖 "The apply_patch tool lets GPT-5.1 create, update, and delete files in your codebase using structured diffs. Instead of just suggesting edits, the model emits patch operations that your application applies and then reports back on, enabling iterative, multi-step code editing workflows." — 原文
📖 "When using the apply_patch tool, you don’t provide an input schema; the model knows how to construct operation objects." — 原文
🧪 实例 patch 失败时回报(如文件缺失),给模型足够信息去恢复:

json
{
  "type": "apply_patch_call_output",
  "call_id": "call_cNWm41dB3RyQcLNOVTIPBWZU",
  "status": "failed",
  "output": "Could not apply patch to lib/foo.py — file not found on disk"
}
🔍 追问 模型收到失败后会怎么做?为什么要写清 output? → 模型能基于这些错误消息调整后续 diff(例如让你在 prompt 里重新读取文件,或简化改动)。所以官方最佳实践强调 status: "failed" 加清晰 output 帮助模型 recover。
📚 拓展阅读
QProgrammatic Tool Calling 是什么?相比 direct tool calling,什么时候该用它、什么时候不该用?深挖·拓展中频
programmatic-tool-calling 编排 V8-runtime
⏱️ 现行
Programmatic Tool Calling(PTC)让模型写并运行 JavaScript 来编排一次 Responses API 请求里的工具——一个 program 可以并行调用工具、用循环和条件、并把中间结果保留在托管运行时里,适合"任务需要一串相关工具调用"或"需要在返回结果前处理大量工具输出"的场景。它的核心价值是减少加入模型上下文的中间工具输出,但效果取决于任务与工具响应。选型准则很清晰:当一个 stage 有可预测的控制流、且代码能返回更小的结构化结果时,用 PTC;当一次调用就够、每个结果都需要模型的新判断、或工作需要审批(approval)、需要保留引用(citations)或原生 artifact 时,用 direct tool calling。官方给了一张任务形态对照表,例如"单次查找或动作"用 direct;"若干可被代码过滤/连接/排序/去重/聚合/校验的结果"在 program 能返回更小结构化结果时用 PTC;"写操作或对审批敏感的动作"默认用 direct 以保住清晰的授权边界;"最终引用或原生 artifact 校验"除非 program 保留原生输出并校验每个必需项,否则用 direct。一个关键执行事实:OpenAI 在托管运行时里跑模型生成的 JavaScript,你的应用只执行返回的 client-owned function call,它不执行生成的 JavaScript。
术语 programmatic_tool_calling(需加入请求的托管工具); program(模型生成、含 code 与 call_id 与 fingerprint 的项); program_output(program 最终结果与状态项); caller.caller_id(把嵌套调用关联回 program 的字段)
📖 "Programmatic Tool Calling lets a model write and run JavaScript that coordinates the tools in a Responses API request. A program can call tools in parallel, use loops and conditions, and keep intermediate results in the hosted runtime. This is useful when a task needs a sequence of related tool calls or needs to process large tool outputs before returning a result." — 原文
📖 "Use Programmatic Tool Calling when a stage has predictable control flow and code can return a smaller structured result. Use direct tool calling when one call is sufficient, each result requires fresh model judgment, or the work requires approval or preservation of citations or native artifacts." — 原文
📖 "OpenAI runs the model-generated JavaScript in the hosted runtime. Your application executes returned client-owned function calls; it does not execute the generated JavaScript." — 原文
🧪 实例 一个 program 项会并行调用两个工具再归约结果(原文 "Program and nested function calls" 中的 program 项):

json
{
  "type": "program",
  "id": "prog_123",
  "call_id": "call_prog_123",
  "code": "const [stock, demand] = await Promise.all([tools.get_inventory({ sku: 'sku_123' }), tools.get_demand({ sku: 'sku_123' })]); text(JSON.stringify({ sku: stock.sku, available_units: stock.available_units, requested_units: demand.requested_units, shortage_units: Math.max(demand.requested_units - stock.available_units, 0) }));",
  "fingerprint": "opaque_replay_state"
}
🔍 追问 应该怎样评估 PTC 是否值得用? → 官方建议以 direct tool calling 为 baseline,再在代表性任务上比较两者:先定义最终答案质量标准与所需证据,再连同正确性/完整性/证据覆盖一起衡量 token 用量、工具调用、延迟与成本,并把任何可接受的质量取舍写明确。
📚 拓展阅读
  • function calling — 定义 client-owned 函数,PTC 里被 program 调用的基础
  • tool search — 延迟加载大工具定义,与 PTC 的 defer_loading 交互相关
  • conversation state — 继续 stored 或 stateless Responses 请求
  • data controls — 选择存储模式前需审阅的数据控制
QProgrammatic Tool Calling 的运行时环境有哪些限制?allowed_callers 怎么控制工具的调用方式?深挖·拓展低频
programmatic-tool-calling allowed_callers 沙箱
⏱️ 现行
OpenAI 把每个生成的 program 跑在一个全新、隔离的 V8 runtime 里。这个运行时支持带 top-level await 的 JavaScript,但不提供 Node.js、包安装、直接网络访问、通用文件系统、子进程执行、console,也不保留跨执行的持久 JavaScript 状态——program 只能通过请求里启用的工具与外部系统交互,并用 text(...)image(...) 发出输出。这种强隔离带来一个好处:PTC 支持 Zero Data Retention(ZDR)工作流而不需要持久的代码执行容器(但 ZDR 需在组织或项目层开启,store: false 只启用无状态续跑、本身不等于 ZDR)。配置上,先把 programmatic_tool_calling 这个托管工具加入请求,再在每个可被 program 调用的工具上设置 allowed_callers。它有三档语义:省略或 ["direct"]——模型可直接调用;["programmatic"]——只有 program 里的代码能调用;["direct", "programmatic"]——两者皆可。支持 ["programmatic"] 的工具类型包括 functioncustommcpapply_patch、本地与托管 shellcode_interpreter。当两种模式都开时,要给每条路由指派具体的 workflow stage——像"高效使用 PTC"这类泛化指令无法标出预期边界。
术语 V8 runtime(隔离且无状态的 JS 执行环境); top-level await(运行时支持的语法); allowed_callers(控制工具可被 direct/programmatic 谁调用); Zero Data Retention(无数据留存工作流); output_schema(描述工具返回 JSON 对象的字段与类型)
📖 "OpenAI runs each generated program in a fresh, isolated V8 runtime. The runtime supports JavaScript with top-level await, but it does not provide Node.js, package installation, direct network access, a general-purpose filesystem, subprocess execution, a console, or persistent JavaScript state between program executions." — 原文
📖 "Add the programmatic_tool_calling hosted tool to the request. Then set allowed_callers on each eligible tool that the program can invoke." — 原文
🧪 实例 把一个 function 工具标为仅可被 program 调用,并配 output_schema 让生成的 JS 能可靠使用返回字段:

json
{
  "type": "function",
  "name": "get_inventory",
  "description": "Return an object with sku (string) and available_units (number).",
  "parameters": {
    "type": "object",
    "properties": { "sku": { "type": "string" } },
    "required": ["sku"],
    "additionalProperties": false
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "sku": { "type": "string" },
      "available_units": { "type": "number" }
    },
    "required": ["sku", "available_units"],
    "additionalProperties": false
  },
  "allowed_callers": ["programmatic"]
}
🔍 追问 为 program 设计工具时有哪些要点? → 返回紧凑的结构化数据让 JS 无需解析散文即可检查;用 output_schema 定义返回字段与类型并记录错误行为(返回形状未知时保持 direct);尽量让 function call 幂等,使重试或 replay 不重复不安全副作用;即使调用来自托管 program,也要在应用侧检查每次调用的参数与权限;高影响动作前无论调用方是谁都要求应用级审批。
📚 拓展阅读
Q运行 Computer use 时有哪些安全约束?截图回传为什么建议用 detail: "original"深挖·拓展低频
computer-use 安全 prompt-injection
⏱️ 现行
Computer use 的安全模型建立在"外部内容不可信"之上。官方开篇即要求:在隔离的浏览器或 VM 中运行 Computer use,对高影响动作保持 human in the loop,并把页面内容当作不可信输入;若从旧的 preview 集成迁移则走 Migration。对本地浏览器自动化的推荐护栏是:在隔离环境运行浏览器、传入空的 env 对象使浏览器不继承宿主环境变量、尽量禁用扩展与本地文件系统访问。无论用浏览器还是 VM,都要把截图、页面文本、工具输出、PDF、邮件、聊天及其它第三方内容当作不可信输入——只有来自用户的直接指令才算作许可(permission),这正是防 prompt injection 的边界。截图回传方面,Computer use 建议在截图输入上用 detail: "original" 以保留分辨率、提升点击精度;GPT-5.6 模型不会把 original 图像输入缩放到某个像素尺寸或 patch 预算上限,因此大截图会用更多输入 token——若 token 太多,可在发送前 downscale 图像,但必须把模型生成的坐标从缩小后的坐标空间重映射回原图坐标空间,并且应避免对 Computer use 任务用 highlow 图像 detail;缩小时官方观察到 1440x900 与 1600x900 桌面分辨率表现良好。
术语 untrusted input(不可信的第三方内容); human in the loop(高影响动作保留人工); detail: "original"(保留分辨率的截图输入级别); env: {}(不继承宿主环境变量的空环境)
📖 "Whether you use a browser or VM, treat screenshots, page text, tool outputs, PDFs, emails, chats, and other third-party content as untrusted input. Only direct instructions from the user count as permission." — 原文
📖 "For Computer use, prefer detail: "original" on screenshot inputs to preserve resolution and improve click accuracy." — 原文
🧪 实例 把更新后的截图作为 computer_call_output 回传,用 detail: "original"

python
def send_computer_screenshot(response, call_id, screenshot_base64):
    return client.responses.create(
        model="gpt-5.6",
        tools=[{"type": "computer"}],
        previous_response_id=response.id,
        input=[
            {
                "type": "computer_call_output",
                "call_id": call_id,
                "output": {
                    "type": "computer_screenshot",
                    "image_url": f"data:image/png;base64,{screenshot_base64}",
                    "detail": "original",
                },
            }
        ],
    )
🔍 追问detail: "original" 时 token 太多怎么办? → 可以在发送到 API 前 downscale 图像,但要确保把模型生成的坐标从缩小后的坐标空间重映射回原图坐标空间;官方观察到 1440x900 和 1600x900 桌面分辨率表现良好,并建议避免对 Computer use 用 highlow detail。
📚 拓展阅读

第3章 · 结构化输出与可靠性

🔥高频

结构化输出(Structured Outputs / JSON Schema)

原文 原文 原文
Q什么是 Structured Outputs?它解决了 LLM 输出中的什么核心痛点?深挖·拓展🔥高频
Structured Outputs JSON Schema 可靠性
⏱️ 现行
Structured Outputs 是一项让模型输出严格遵循你提供的 JSON Schema 的能力:不是靠提示词"求"模型返回某种格式,而是在解码层面约束模型,使其不可能漏掉 required 键、也不可能吐出 schema 之外的非法 enum 值。它带来三重收益——类型安全(无需再校验或重试格式错误的响应)、显式拒绝(安全类拒绝可被程序检测)、以及更简单的提示(不必写强硬措辞去逼模型稳定输出格式)。之所以可靠,是因为它约束的是 JSON 的形状(shape)而不仅是合法性:合法 JSON 只保证能 parse,而 schema 遵循保证字段、类型、层级都对得上,因此适合直接喂给下游 UI、数据库或工具调用。权衡在于:schema 遵循是"尽力必然满足结构",但语义正确性仍可能出错(见 Q7),且并非所有 JSON Schema 特性都被支持。
术语 Structured Outputs(结构化输出,约束模型输出遵循 schema); JSON Schema(描述 JSON 结构的规范); schema adherence(schema 遵循,保证结构而非仅合法性); required key(必填键); enum(枚举值)
📖 "Structured Outputs is a feature that ensures the model will always generate responses that adhere to your supplied JSON Schema, so you don't need to worry about the model omitting a required key, or hallucinating an invalid enum value." — 原文
📖 "Unlike JSON mode, Structured Outputs constrains the shape of the JSON, not only its validity." — 原文
🧪 实例 数学辅导应用要求把解题过程拆成结构化步骤数组,每步含 explanationoutput,最后加 final_answer——用 Pydantic 定义好后模型必然按此结构返回,前端即可逐步渲染。
python
class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str
🔍 追问 schema 遵循是否等于内容一定正确? → 不是,结构必然对但语义仍可能出错,Structured Outputs 只约束形状不保证事实正确。
📚 拓展阅读
QStructured Outputs 与 JSON mode 有什么区别?该如何取舍?深挖·拓展🔥高频
JSON mode Structured Outputs 对比
⏱️ 现行
JSON mode 是 Structured Outputs 的更基础版本:两者都能保证输出是合法 JSON,但只有 Structured Outputs 能保证输出匹配你指定的 schema。换句话说,JSON mode 只解决"能不能 parse",不保证字段名、类型、层级;Structured Outputs 才把输出可靠地对齐到你的 schema。官方明确建议——只要用例支持,就总是优先用 Structured Outputs 而非 JSON mode。取舍点在模型支持面:response_format: {type: "json_schema", ...} 形式的 Structured Outputs 只在 gpt-4o-minigpt-4o-mini-2024-07-18gpt-4o-2024-08-06 及更新快照上支持;更老的模型(如 gpt-4-turbo)只能退回 JSON mode。此外用 JSON mode 时你必须自己处理边界情况(输出可能不是完整 JSON 对象),并配合校验库+重试来逼近 schema。
术语 JSON mode(仅保证合法 JSON 的基础模式); schema adherence(schema 遵循); json_schema(启用 Structured Outputs 的 response_format 类型); json_object(启用 JSON mode 的 format 类型)
📖 "Structured Outputs is the evolution of [JSON mode](#json-mode). While both ensure valid JSON is produced, only Structured Outputs ensure schema adherence." — 原文
📖 "We recommend always using Structured Outputs instead of JSON mode when possible." — 原文
📖 "However, Structured Outputs with response_format: {type: \"json_schema\", ...} is only supported with the gpt-4o-mini, gpt-4o-mini-2024-07-18, and gpt-4o-2024-08-06 model snapshots and later." — 原文
🧪 实例 在 Responses API 里两种模式的开关差别:
json
{ "text": { "format": { "type": "json_schema", "strict": true, "schema": "..." } } }
json
{ "text": { "format": { "type": "json_object" } } }
🔍 追问 两者在哪些 API 上都可用? → Responses、Chat Completions、Assistants、Fine-tuning、Batch API 都支持这两种模式。
📚 拓展阅读
QStructured Outputs 有哪两种使用形态?function calling 和 response_format 该怎么选?深挖·拓展🔥高频
function calling response_format 架构选择
⏱️ 现行
Structured Outputs 在 API 里有两种形态:一是用于 function calling(函数调用),二是用于 json_schema response format(响应格式)。选择标准取决于结构化输出流向谁:如果你要把模型接到系统里的工具、函数、数据(比如查数据库、操作 UI),用 function calling;如果你要约束模型直接回复用户时的输出结构(比如数学辅导应用把答案拆成可分区渲染的 JSON),用 response_format 里的 text.format。二者机制相同(都靠 schema 约束),区别在语义角色:function calling 是"模型调用你的能力",response_format 是"模型面向用户作答的结构"。
术语 function calling(函数调用,模型触发系统能力); response_format(响应格式,约束面向用户的输出结构); text.format(Responses API 中设置输出格式的字段)
📖 "Function calling is useful when you are building an application that bridges the models and functionality of your application." — 原文
📖 "Conversely, Structured Outputs via response_format are more suitable when you want to indicate a structured schema for use when the model responds to the user, rather than when the model calls a tool." — 原文
🧪 实例 选择逻辑可视化:
flowchart TD
    A[需要结构化输出] --> B{输出流向?}
    B -->|接工具/函数/数据| C[function calling]
    B -->|直接回复用户| D[response_format / text.format]

实体抽取场景(服装推荐 agent 从用户输入提取 category/subcategory/color 去查库)就用 function calling,把 Pydantic 模型经 pydantic_function_tool 挂成工具。
🔍 追问 用 function calling 时 JSON mode 状态如何? → 使用 function calling 时 JSON mode 始终是开启的。
📚 拓展阅读
Q怎样启用严格模式?为什么推荐用 Pydantic/Zod 的 parse helper 而非手写 JSON Schema?深挖·拓展🔥高频
strict parse helper Pydantic Zod
⏱️ 现行
启用方式是把 strict: true 设进 API 调用——无论是配合定义好的 response format,还是配合函数定义,strict: true 都让所提供的 schema 被严格遵循。手写 JSON Schema 时,OpenAI 的 Python/JavaScript SDK 提供了 parse helper:你直接传自己的 Pydantic 模型(或 JS 侧的 Zod),SDK 负责生成并对齐 schema,还能把响应反序列化成类型化对象。官方建议尽量用这种原生方式,因为它避免了"JSON Schema 与代码类型两处定义漂移(divergence)"的维护负担(见 Q8)。注意严格 schema 有硬性写法要求:入门 cookbook 的手写 schema 里每个对象都要把所有属性列进 required,并设 additionalProperties: false
术语 strict: true(严格模式开关,强制 schema 被遵循); parse helper(SDK 提供的解析助手,直接传 Pydantic/Zod 模型); Pydantic(Python 的数据模型库); Zod(JS/TS 的 schema 校验库); additionalProperties: false(禁止 schema 外的额外字段)
📖 "Structured Outputs can be enabled by setting the parameter strict: true in an API call with either a defined response format or function definitions." — 原文
📖 "The new version of the SDK introduces a parse helper to provide your own Pydantic model instead of having to define the JSON schema. We recommend using this method if possible." — 原文
🧪 实例 用 parse helper,直接传 Pydantic 模型、拿回已解析对象:
python
def get_math_solution(question: str):
    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {"role": "system", "content": dedent(math_tutor_prompt)},
            {"role": "user", "content": question},
        ],
        response_format=MathReasoning,
    )
    return completion.choices[0].message

对比手写 JSON Schema 时的严格约束(每个对象都要 required 全列 + additionalProperties: False):
json
{
  "type": "object",
  "properties": {
    "explanation": {"type": "string"},
    "output": {"type": "string"}
  },
  "required": ["explanation", "output"],
  "additionalProperties": false
}
🔍 追问 JS 侧用什么对应 Python 的 Pydantic? → 用 Zod 定义对象 schema,配合 zodTextFormat 传入 text.format
📚 拓展阅读
Q模型拒绝回答时结构化输出怎么办?refusal 字段是什么?深挖·拓展🔥高频
refusal 安全 错误处理
⏱️ 现行
当 Structured Outputs 遇到用户生成的输入时,模型偶尔会出于安全原因拒绝执行请求。问题在于:拒绝内容不一定遵循你在 response_format 里给的 schema,如果直接按 schema 反序列化就会报错。为此 API 新增了一个 refusal 字段来标识模型拒绝了请求。这样你能在两方面受益:一是在 UI 里把拒绝单独渲染出来;二是在消费响应的代码里加条件分支处理被拒场景,而不是让反序列化崩溃。这正是"显式拒绝可被程序检测"这一收益的落地形式——安全拒绝从"隐藏在自由文本里"变成"结构化可判定信号"。
术语 refusal(拒绝字段,标识模型出于安全拒绝并携带拒绝文本); response_format(响应格式); deserialize(反序列化,把响应转成目标结构对象)
📖 "Since a refusal does not necessarily follow the schema you have supplied in response_format, the API response will include a new field called refusal to indicate that the model refused to fulfill the request." — 原文
📖 "Since a refusal does not follow the schema you have supplied in response_format, the API has a new field refusal to indicate when the model refused to answer." — 原文
🧪 实例 遍历输出,遇到 refusal 单独处理、否则再取 parsed:
python
for item in output.content:
    if item.type == "refusal":
        # If the model refuses to respond, you will get a refusal message
        print(item.refusal)
        continue

    if not item.parsed:
        raise Exception("Could not parse response")

    print(item.parsed)

被拒时响应体里 content 会是 {"type": "refusal", "refusal": "I'm sorry, I cannot assist with that request."}
🔍 追问 拒绝时该在 UI 怎么处理? → 可把 refusal 直接呈现给用户,或在消费响应的代码里加条件逻辑处理被拒请求。
📚 拓展阅读
Q哪些模型支持 Structured Outputs?新项目该选什么?深挖·拓展中频
模型支持 版本
⏱️ 现行
Structured Outputs 从 GPT-4o 开始,在 OpenAI 最新的大语言模型上可用。新项目官方建议直接从 gpt-5.6 起步;gpt-4-turbo 等更老模型则可能只能退回 JSON mode。入门 cookbook 里进一步限定:Structured Outputs 仅在 gpt-4o-minigpt-4o-2024-08-06 及未来模型上可用(cookbook 示例统一用 gpt-4o-2024-08-06)。面试要点在于把"能力"与"具体快照"分开记:能力从 GPT-4o 引入,但 json_schema 形式的严格结构化输出绑定到具体模型快照(见 Q2),选模型时要核对快照而非只看系列名。
术语 GPT-4o(引入 Structured Outputs 的起点模型); model snapshot(模型快照,如 gpt-4o-2024-08-06); gpt-5.6(官方建议新项目起步的模型); JSON mode(老模型的回退方案)
📖 "Structured Outputs is available in our latest large language models, starting with GPT-4o." — 原文
📖 "Structured Outputs is only available with gpt-4o-mini , gpt-4o-2024-08-06, and future models." — 原文
🧪 实例 cookbook 里固定模型常量:
python
MODEL = "gpt-4o-2024-08-06"
🔍 追问 老模型完全不能结构化吗? → 可用更基础的 JSON mode(gpt-3.5-turbogpt-4-* 等兼容),但只保证合法 JSON、不保证 schema。
📚 拓展阅读
QStructured Outputs 保证了结构,为什么还会出错?如何处理错误和幻觉?深挖·拓展中频
幻觉 可靠性 最佳实践
⏱️ 现行
schema 遵循只约束"形状",不保证语义正确,所以 Structured Outputs 仍可能出错,甚至产生幻觉。一个典型陷阱是:模型总会尽力去满足给定 schema,当输入与 schema 完全无关时,这种"硬填"就会导致幻觉——因为它不得不为字段编出值。应对分两层:其一,处理用户生成输入时,提示词里要写清"输入无法产生有效响应时怎么办"(例如约定返回空参数或某句固定话);其二,若看到错误,调整指令、在 system 指令里给示例、或把任务拆成更简单的子任务。本质上,Structured Outputs 把"格式可靠性"交给了系统,但"语义可靠性"仍需靠提示工程与任务分解来兜底。
术语 hallucination(幻觉,输入与 schema 无关时被迫编造字段值); user-generated input(用户生成输入); system instructions(系统指令,可放示例引导); subtasks(子任务拆分)
📖 "The model will always try to adhere to the provided schema, which can result in hallucinations if the input is completely unrelated to the schema." — 原文
📖 "Structured Outputs can still contain mistakes. If you see mistakes, try adjusting your instructions, providing examples in the system instructions, or splitting tasks into simpler subtasks." — 原文
📖 "If your application is using user-generated input, make sure your prompt includes instructions on how to handle situations where the input cannot result in a valid response." — 原文
🧪 实例 抽取场景里可在 system prompt 明确"若无相关数据则输出空对象、不要编造",例如发票抽取提示:"If the page contains no charge data, please output an empty JSON object and don't make up any data."
🔍 追问 想彻底避免无关输入被硬填怎么办? → 在提示里指定:检测到输入与任务不兼容时返回空参数或一句特定话。
📚 拓展阅读
Q什么是 JSON Schema divergence?为什么要避免、如何避免?深挖·拓展中频
schema 漂移 工程实践 CI
⏱️ 现行
divergence 指你的 JSON Schema 与编程语言里对应的类型定义"两处各写、逐渐不一致"的问题——改了代码类型没同步 schema(或反过来),线上就会出现结构对不上的隐性 bug。官方强烈建议直接用原生 Pydantic/Zod SDK 支持来消除这个风险:类型定义即 schema 单一真源,SDK 自动对齐,不存在两份手写副本。如果你坚持直接手写 JSON Schema,则应加工程护栏:要么加 CI 规则,在 JSON Schema 或底层数据对象被改动时报警;要么加一个 CI 步骤从类型定义自动生成 JSON Schema(或反向生成)。核心思路是"单一真源 + 自动化同步",把一致性从人工纪律变成机器保证。
术语 divergence(漂移,schema 与代码类型不一致); native Pydantic/zod sdk support(原生 SDK 支持,作为单一真源); CI rules(持续集成规则,改动时报警); auto-generate(自动生成 schema 保持同步)
📖 "To prevent your JSON Schema and corresponding types in your programming language from diverging, we strongly recommend using the native Pydantic/zod sdk support." — 原文
🧪 实例 与其手写并维护两份,不如让 Pydantic 模型作为唯一真源,直接传给 parse helper(见 Q4),schema 由 SDK 派生,代码改了 schema 自动跟着改。
🔍 追问 非用手写 JSON Schema 不可时怎么保证不漂移? → 加 CI:改动 schema 或数据对象时报警,或加一步自动从类型生成 schema(或反向)。
📚 拓展阅读
QJSON mode 有哪些必须处理的边界情况?为什么用它时一定要在提示里提"JSON"?深挖·拓展中频
JSON mode 边界情况 whitespace
⏱️ 现行
JSON mode 只保证输出是合法 JSON、能被无错解析,但不保证匹配任何特定 schema。它有几个必须自己兜住的边界:其一,你必须在对话里(例如 system 消息)显式指示模型产出 JSON——否则模型可能生成无休止的空白字符流,请求会一直跑到 token 上限;为防遗忘,若上下文里没出现字符串 "JSON",API 会直接报错。其二,输出可能不是完整 JSON 对象(比如被截断),应用必须检测并处理这些边界。其三,正因为不保证 schema,你要么改用 Structured Outputs,要么上校验库并可能配合重试来逼近目标结构。所以 JSON mode 的可靠性心智是"合法但不受约束",工程上必须补齐 schema 校验与边界处理。
术语 JSON mode(仅保证合法 JSON); whitespace stream(空白字符流,未指示 JSON 时的失败模式); token limit(token 上限); validation library(校验库,配合重试逼近 schema)
📖 "When using JSON mode, you must always instruct the model to produce JSON via some message in the conversation, for example via your system message. If you don't include an explicit instruction to generate JSON, the model may generate an unending stream of whitespace and the request may run continually until it reaches the token limit." — 原文
📖 "JSON mode will not guarantee the output matches any specific schema, only that it is valid and parses without errors." — 原文
🧪 实例 Responses API 里打开 JSON mode,同时记得在 system 消息里点名 JSON:
json
{ "text": { "format": { "type": "json_object" } } }
🔍 追问 用 function calling 时还要单独开 JSON mode 吗? → 不用,使用 function calling 时 JSON mode 始终开启。
📚 拓展阅读
Q在 ELT 抽取管线里,为什么先用 json_object 抽取、再用 schema 转换?深挖·拓展低频
数据抽取 ELT json_object
⏱️ 现行
在把 PDF/图片这类非结构化数据灌进数据库的 ELT 流程里,官方 cookbook 用了"两段式":抽取阶段先用 GPT-4o 的视觉能力 + response_format={"type": "json_object"}(即 JSON mode)把页面里所有数据不加约束地抽出来,此阶段刻意不强加 schema——目的就是尽可能全量捕获、保留原语言、不丢字段;每张发票各页产出的 JSON 各不相同,可先存进能容纳非结构化数据的 data lake。转换阶段再用 GPT-4o 把这些原始 JSON 按目标 schema 规整(统一日期为 YYYY-MM-DD、翻译成英文),此时允许"输入 JSON 里放不进 schema 的数据被省略、缺的补 null",从而对齐到可入库的四张表。这样分离让抽取追求召回、转换追求规整,各司其职。
术语 ELT(先抽取加载再转换的工作流); json_object(JSON mode,抽取阶段不强加 schema); data lake(数据湖,暂存非结构化 JSON); schema transformation(按目标 schema 规整并补 null)
📖 "We're not concerned about enforcing a schema at this step, we just want all of the data to be extracted regardless of type." — 原文
📖 "Not all of the data in the input JSON will fit the schema, so you may need to omit some data or add null values to the output JSON." — 原文
🧪 实例 抽取阶段调用只指定 json_object、不带 schema:
python
response = client.chat.completions.create(
    model="gpt-4o",
    response_format={ "type": "json_object" },
    messages=[ ... ],
    temperature=0.0,
)

转换阶段把原始 JSON 与目标 schema 一起喂给模型,规整为固定结构(含 "date": "YYYY-MM-DD"),最终拆成 Hotels/Invoices/Charges/Taxes 四表入库。
🔍 追问 这套流程能否降本? → 若不要求实时,可用 OpenAI 的 BatchAPI 异步跑,成本更低。
📚 拓展阅读

第4章 · Agents 平台与编排

🔥高频

Agents SDK 基础与 Agent 定义

原文 原文 原文
Q什么是 Agent?Agents SDK 里的 "agent" 到底封装了哪些东西?深挖·拓展🔥高频
agent agents-sdk 核心概念
⏱️ 现行
在 Agents SDK 的语义里,agent 不是一次模型调用,而是能"规划、调用工具、跨专家协作、并保留足够状态以完成多步工作"的应用级单元。文档明确把 agent 定位为 SDK-based workflow 的 core unit,它把 model、instructions,以及可选的运行时行为(tools、guardrails、MCP servers、handoffs、structured outputs)打包成一个可复用的规格。这样设计的权衡在于:它把"一个专家该知道什么、能做什么、受什么约束"这些内在决策集中到 agent 配置上,而不是散落在每次调用现场;好处是同一个 agent 可以被多轮 run 复用、可以作为 handoff 或工具目标被别的 agent 引用,并在 traces 里以稳定身份出现。相比之下,Responses API 的核心抽象只是"一个 model response",agent 的核心抽象则是"一次 agent run",这决定了后续 loop、编排、状态、审批等能力都围绕 run 而非单次 response 组织。
术语 agent(SDK workflow 的核心单元,打包模型与运行时行为); core unit(核心单元); runtime behavior(运行时行为,如 tools/guardrails/MCP/handoffs/structured outputs); agent run(一次 agent 运行,Agents SDK 的核心抽象)
📖 "Agents are applications that plan, call tools, collaborate across specialists, and keep enough state to complete multi-step work." — 原文
📖 "An agent is the core unit of an SDK-based workflow. It packages a model, instructions, and optional runtime behavior such as tools, guardrails, MCP servers, handoffs, and structured outputs." — 原文
🧪 实例 一个 SDK agent 的最小心智模型:
flowchart LR
  A[Agent 规格] --> B[model]
  A --> C[instructions]
  A --> D[tools]
  A --> E[handoffs]
  A --> F[guardrails / MCP / outputType]
  A --> G[一次 run 复用同一规格]
🔍 追问 agent 和 Responses API 里的一次 model response 有什么本质区别? → response 是单次模型输出、你自己管循环;agent 的核心抽象是一次 run,SDK 负责跑 loop、切换专家、并在完成或等待审批时停止。
📚 拓展阅读
Q什么时候用 Agents SDK,什么时候用 Responses API?两者如何取舍?深挖·拓展🔥高频
agents-sdk responses-api 选型
⏱️ 现行
文档给出的一句话判据是:想自己拥有 loop 就用 Responses API,想让 SDK 替你跑 loop 就用 Agents SDK。选 Responses API 的场景是你要直接控制模型交互、output items、tools、state 和 orchestration——无论工作流是一次调用还是多次,以及你要在应用里自己实现自定义的工具路由、循环或分支;它的 function-calling flow 是:应用收到 function calls、执行、把输出返回、再次调用模型。选 Agents SDK 的场景是:你希望 SDK 管理 agent loop 和重复出现的编排(如反复的工具调用或分支),不同专家需要不同的 instructions/tools/policies,或者你想要内建的 sessions、tracing、guardrails、可恢复的审批流。核心权衡在于"控制权 vs 内建能力":Responses API 给你最大自由但要自己搭路由、循环、审批;Agents SDK 用 runner 替你执行工具循环、在 handoff 后切换 agent、并在 run 结束或暂停等待审批时停止,换来更少的样板但更强的约定。
术语 own the loop(自己拥有循环); agent loop(SDK 管理的 agent 循环); orchestration(编排,重复的工具调用与分支); runner(执行工具循环并在 handoff 后切换 agent 的运行器)
📖 "Use the Responses API when you want to own the loop. Use the Agents SDK when you want the SDK to run it." — 原文
📖 "You want the SDK to manage the agent loop and recurring orchestration such as repeated tool calls or branching." — 原文
🧪 实例 文档给的两个对照例子:Responses API 工作流"might search a knowledge base and generate a cited answer"(一步搞定的定制特性);Agents SDK 工作流"might investigate a support request, hand it to the correct specialist, call internal systems, request approval for a refund, and record the result"(多专家、多步、带审批)。
🔍 追问 用了 Agents SDK 就不能用 Responses API 的状态管理了吗? → 不是,对照表里 State 一栏说 Agents SDK 是"The same options"再加上 SDK sessions 与 resumable run state。
📚 拓展阅读
Q如何定义并运行第一个 agent?run/Runner 返回什么?深挖·拓展🔥高频
quickstart run finalOutput
⏱️ 现行
最短路径是"一个聚焦的 agent + 一个 turn"。你用 Agent 构造一个带 name、instructions、model 的 agent,然后调用 run(TypeScript 的 run / Python 的 Runner.run)传入用户输入;SDK 负责这次模型调用,并返回一个 result 对象,里面既有 final output(TS 的 result.finalOutput / Python 的 result.final_output),又有整个 run history。这个"先跑通再增量加能力"的顺序是文档反复强调的设计取向:一旦这个 loop 能工作,就保持同样的形状,增量地加 tools、specialist agents,而不是一上来就设计庞大的多 agent 结构。返回的 result 还承担状态职责——第一次 run 的结果本身就是你决定第二个 turn 用什么作为 state 的依据(保留完整 history、用 session、用 server-managed continuation ID,或在暂停审批时恢复)。
术语 Agent(定义单个 agent 的构造器); run / Runner.run(执行一次 run 的入口); result(含 final output 与 run history 的结果对象); finalOutput / final_output(最终输出字段)
📖 "Start with one focused agent and one turn. The SDK handles the model call and returns a result object with the final output plus the run history." — 原文
📖 "You should see a concise answer in the terminal. Once that loop works, keep the same shape and add capabilities incrementally rather than starting with a large multi-agent design." — 原文
🧪 实例
typescript
import { Agent, run } from "@openai/agents";

const agent = new Agent({
  name: "History tutor",
  instructions:
    "You answer history questions clearly and concisely.",
  model: "gpt-5.6",
});

const result = await run(agent, "When did the Roman Empire fall?");
console.log(result.finalOutput);

Python 侧对称地用 Runner.run(agent, ...) 并打印 result.final_output
🔍 追问 第二个 turn 的 state 该怎么带? → quickstart 给了四条路径:在应用里保留完整 history、用一个 session、用 server-managed continuation ID,或配合 interruptions 恢复暂停的 run。
📚 拓展阅读
Q什么东西应该配置在 agent 上?有哪些核心属性?深挖·拓展🔥高频
agent 配置 instructions tools
⏱️ 现行
判断原则是:把"对这个专家而言内在(intrinsic)的决策"放到 agent 配置上。文档给的属性表包括:name(traces 与 tool/handoff 界面里的可读身份)、instructions(这个 agent 的职责、约束与风格)、prompt(用于 Responses-based run 的 stored prompt 配置)、model 及 model settings(选模型与调行为)、tools(agent 能直接调用的能力)、handoff description(提示别的 agent 何时委派到这里)、handoffs(委派给另一个 agent)、structured output(返回结构化输出而非纯文本)、guardrails 与 approvals(校验、阻断、审查流),以及 MCP servers 和 hosted MCP tools。设计上应"从最小能担一个清晰任务的 agent 起步",只有当你需要独立的 ownership、不同 instructions、不同工具面、或不同审批策略时才增加 agent——这条约束避免了过早把系统拆成一堆难以维护的小 agent。
术语 name(traces 中的可读身份); instructions(职责/约束/风格); model and model settings(模型及行为调优); tools(可直接调用的能力); handoffs(委派目标)
📖 "Use agent configuration for decisions that are intrinsic to that specialist:" — 原文
📖 "Define the smallest agent that can own a clear task. Add more agents only when you need separate ownership, different instructions, different tool surfaces, or different approval policies." — 原文
🧪 实例 一个带单个 function tool 的聚焦 agent:
typescript
import { Agent, tool } from "@openai/agents";
import { z } from "zod";

const getWeather = tool({
  name: "get_weather",
  description: "Return the weather for a given city.",
  parameters: z.object({ city: z.string() }),
  async execute({ city }) {
    return `The weather in ${city} is sunny.`;
  },
});

const agent = new Agent({
  name: "Weather bot",
  instructions: "You are a helpful weather bot.",
  model: "gpt-5.6",
  tools: [getWeather],
});
🔍 追问 prompt 属性和 instructions 有什么区别? → prompt 用来引用 Responses API 的 stored prompt 配置,而不是把整段 system prompt 嵌进代码;文档把它归到 Models and providers 页深入。
📚 拓展阅读
Q如何让 agent 返回结构化输出(structured output)?深挖·拓展中频
structured-output outputType zod pydantic
⏱️ 现行
当下游代码需要 typed data 而不是自由文本时,就用 structured output——在 TypeScript 里通过 outputType 传入一个 zod schema,在 Python 里通过 output_type 传入一个 pydantic BaseModel。这样 agent 的 result.finalOutput / result.final_output 会是符合该 schema 的结构化对象,而不是一段散文,便于程序直接消费。文档把它和 instructions、handoffs 并列为"三个值得额外用心的配置选择"之一,取舍点在于:纯文本对人友好但对下游程序不稳定,一旦有代码要按字段读取数据,就应显式声明输出类型,让模型产出可校验的结构。与之相关的 prompt 属性则解决另一件事——引用 Responses API 的 stored prompt 配置,避免把整个 system prompt 嵌进代码。
术语 outputType / output_type(声明结构化输出类型); structured output(结构化输出); typed data(带类型的数据); BaseModel(Python pydantic 的模型基类)
📖 "Use when downstream code needs typed data rather than free-form prose." — 原文
📖 "Use prompt when you want to reference a stored prompt configuration from the Responses API instead of embedding the entire system prompt in code." — 原文
🧪 实例 从文本抽取日历事件为结构化对象:
python
import asyncio

from pydantic import BaseModel

from agents import Agent, Runner


class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]


agent = Agent(
    name="Calendar extractor",
    instructions="Extract calendar events from text.",
    output_type=CalendarEvent,
)


async def main() -> None:
    result = await Runner.run(
        agent,
        "Dinner with Priya and Sam on Friday.",
    )
    print(result.final_output)


if __name__ == "__main__":
    asyncio.run(main())
🔍 追问 TypeScript 里用什么声明输出 schema? → 用 zod,例如 z.object({ name: z.string(), date: z.string(), participants: z.array(z.string()) }) 传给 outputType
📚 拓展阅读
Qlocal context 和 conversation history 有什么区别?为什么要分开?深挖·拓展中频
run-context local-context RunContext
⏱️ 现行
SDK 允许你把应用状态和依赖传进一次 run,但不发送给模型——这就是 local context / run context,典型用途是 authenticated user info、database clients、loggers 和 helper functions。关键边界是:conversation history 是模型能看到的,run context 是你的代码能看到的。判断法则很干脆:如果模型需要某个事实,就把它放进 instructions、input、retrieval 或一个 tool;如果只有你的运行时需要它,就留在 local context 里。这样分开的价值在于既保护敏感/非语义数据不进入 prompt(省 token、避免泄露、避免污染模型上下文),又能让工具执行时通过 RunContext(Python 的 RunContextWrapper)拿到这些依赖。实现上,run 的调用点通过 context 参数注入,工具的 execute 再从 runContext 读取,例如根据 runContext?.context.name 返回该用户信息。
术语 local context / run context(只给代码看的运行时状态); conversation history(模型能看到的对话历史); RunContext / RunContextWrapper(工具中访问 context 的载体); context 参数(run 调用点注入依赖)
📖 "The SDK lets you pass application state and dependencies into a run without sending them to the model. Use this for data like authenticated user info, database clients, loggers, and helper functions." — 原文
📖 "If the model needs a fact, put it in instructions, input, retrieval, or a tool. If only your runtime needs it, keep it in local context." — 原文
🧪 实例 工具从 run context 读取当前用户,而不把 uid 发给模型:
typescript
import { Agent, RunContext, run, tool } from "@openai/agents";
import { z } from "zod";

interface UserInfo {
  name: string;
  uid: number;
}

const fetchUserAge = tool({
  name: "fetch_user_age",
  description: "Return the age of the current user.",
  parameters: z.object({}),
  async execute(_args, runContext?: RunContext<UserInfo>) {
    return `User ${runContext?.context.name} is 47 years old`;
  },
});

const agent = new Agent<UserInfo>({
  name: "Assistant",
  tools: [fetchUserAge],
});

const result = await run(agent, "What is the age of the user?", {
  context: { name: "John", uid: 123 },
});
🔍 追问 如果一个事实模型必须知道,该放哪? → 放进 instructions、input、retrieval 或一个 tool,而不是 local context——因为 local context 只有你的代码能看到。
📚 拓展阅读
Q如何用 handoffs 把工作流拆成多个专家 agent?深挖·拓展中频
handoffs triage 多agent
⏱️ 现行
常见的下一步是把工作流拆成若干 specialist,再让一个 router 用 handoffs 把请求委派给它们。做法是:定义多个专家 agent(如 history tutor、math tutor),再定义一个 triage/router agent,其 handoffs 列表指向这些专家;router 的 instructions 只负责"把每个作业问题路由到正确的专家"。运行后可以从 result 读到最终由谁接管——TS 用 result.lastAgent?.name,Python 用 result.last_agent.name。Python 侧还可以给专家配 handoff_description(如 "Specialist for history questions.")作为路由提示,让 router 知道何时该选这个专家;文档也建议 handoff 后为下一个 turn 复用该专家,让它继续掌控对话。这样做把"谁拥有回复"变成显式的、可在 traces 里看到的路由,而不是塞进一个庞大的单 prompt。
术语 handoffs(委派给其他 agent 的列表); triage agent / router(负责路由的分诊 agent); handoff_description(专家的路由提示语); lastAgent / last_agent(本次 run 最终接管的 agent)
📖 "A common next step is to split the workflow into specialists and let a router delegate to them with handoffs." — 原文
🧪 实例 triage agent 路由到专家并读取 lastAgent:
typescript
import { Agent, run } from "@openai/agents";

const historyTutor = new Agent({
  name: "History tutor",
  instructions: "Answer history questions clearly and concisely.",
});

const mathTutor = new Agent({
  name: "Math tutor",
  instructions: "Explain math step by step and include worked examples.",
});

const triageAgent = Agent.create({
  name: "Homework triage",
  instructions: "Route each homework question to the right specialist.",
  handoffs: [historyTutor, mathTutor],
});

const result = await run(
  triageAgent,
  "Who was the first president of the United States?",
);

console.log(result.finalOutput);
console.log(result.lastAgent?.name);
🔍 追问 怎么知道 handoff 后由哪个 agent 收尾? → 读 result.lastAgent?.name(TS)/result.last_agent.name(Python);文档还建议下一个 turn 复用这个专家继续掌控。
📚 拓展阅读
Q什么时候应该把一个 agent 拆成多个?深挖·拓展中频
agent-design 拆分 架构
⏱️ 现行
拆分的判据是:当一个专家不该拥有完整回复,或多项能力实质上不同时,就拆。文档列出的常见理由是:某个专家需要不同的 tool 或 MCP 面;某个专家需要不同的 approval policy 或 guardrail;工作流的某个分支需要不同的 model 或输出风格;或者你想在 traces 里看到显式路由而不是一个庞大的单 prompt。这条原则和"从最小 agent 起步"是一体两面:默认不拆(避免过度工程与协调开销),只有出现上述实质差异时才拆,因为拆分带来的收益(独立 ownership、隔离的策略/工具面、可观测的路由)必须能抵消多 agent 编排的复杂度。换句话说,拆分是由"能力差异是否 material"驱动的,而不是为了拆而拆。
术语 separate ownership(独立的回复所有权); tool / MCP surface(工具或 MCP 能力面); approval policy / guardrail(审批策略与护栏); explicit routing in traces(traces 中的显式路由)
📖 "Split an agent when one specialist shouldn't own the full reply or when separate capabilities are materially different." — 原文
📖 "You want explicit routing in traces rather than a single large prompt." — 原文
🧪 实例 决策清单——出现以下任一"实质差异"才拆:不同 tool/MCP 面、不同 approval/guardrail、某分支需不同 model/输出风格、或需要 traces 里的显式路由;否则保持单个聚焦 agent。
🔍 追问 拆分的默认倾向是什么? → 默认不拆:先定义能担一个清晰任务的最小 agent,只在需要独立 ownership、不同 instructions、不同工具面或不同审批策略时才加 agent。
📚 拓展阅读
Q怎么给 agent 加第一个能力?function tool 和 dynamic instructions 怎么用?深挖·拓展低频
function-tool dynamic-instructions tools
⏱️ 现行
你通常加的第一个能力,是一个 function tool 或一个 hosted OpenAI tool(如 web search 或 file search)。TypeScript 用 tool({ name, description, parameters, execute })(parameters 用 zod schema)定义,再把它放进 agent 的 tools;Python 用 @function_tool 装饰一个函数,其 docstring 充当描述。instructions 一侧的建议是:先用 static instructions;当指导内容依赖当前 user、tenant 或运行时 context 时,改用 dynamic instructions callback,而不是在调用现场拼字符串——这既能让指令随上下文变化,又避免了脆弱的字符串拼接。两者的共同取舍是"从最小、静态、可预测的形状起步,只有当真实需求(需要外部能力 / 需要随上下文变化的指令)出现时才升级",与整篇文档"增量加能力"的基调一致。
术语 function tool(把函数暴露为 agent 可调用工具); @function_tool(Python 装饰器); hosted OpenAI tool(如 web search / file search); dynamic instructions callback(随上下文生成指令的回调)
📖 "The first capability you add is often a function tool or a hosted OpenAI tool such as web search or file search." — 原文
📖 "Start with static instructions. When the guidance depends on the current user, tenant, or runtime context, switch to a dynamic instructions callback instead of stitching strings together at the call site." — 原文
🧪 实例 Python 用 @function_tool 定义并挂到 agent:
python
import asyncio

from agents import Agent, Runner, function_tool


@function_tool
def history_fun_fact() -> str:
    """Return a short history fact."""
    return "Sharks are older than trees."


agent = Agent(
    name="History tutor",
    instructions="Answer history questions clearly. Use history_fun_fact when it helps.",
    tools=[history_fun_fact],
)
🔍 追问 需要 hosted tools 或 agents-as-tools 时看哪里? → 文档指向共享的 Using tools 指南,里面覆盖 hosted tools、tool search 和 agents-as-tools。
📚 拓展阅读
  • Using tools — hosted/function/agents-as-tools 的用法
  • Traces dashboard — 第一个 run 跑通后即可检查工具调用与 handoffs
🔥高频

运行、编排与交接(handoffs / multi-agent)

原文 原文 原文
QAgents SDK 的「agent loop」是什么?一次 run 到底做了什么?深挖·拓展🔥高频
agent-loop runtime runner
⏱️ 现行
定义一个 agent 只是准备工作,真正的运行时问题是「一次 run 做了什么、下一轮如何续接、以及暂停时如何表现」。SDK 的核心是一个 runner 循环:一次 SDK run 等于一个应用级别的 turn,runner 会持续循环直到到达真正的停止点——调用当前 agent 的模型、检查模型输出、如果产生了 tool calls 就执行并继续、如果 handoff 给了另一个 specialist 就切换 agent 并继续、直到模型给出一个不再需要 tool 工作的 final answer 才返回结果。这个设计的关键在于:tools、handoffs、approvals、streaming 都是构建在这个循环之上,而不是替代它——所以你只需理解一个循环,就能推理出所有高级能力的行为,这是它可组合、可预测的根本原因。
术语 agent loop(runner 反复调用模型直到停止点的核心循环); application-level turn(一次 run 对应一个应用级 turn); handoff(把控制权移交给另一个 specialist agent); final answer(不再需要 tool 工作时返回的最终结果)
📖 "One SDK run is one application-level turn. The runner keeps looping until it reaches a real stopping point:" — 原文
📖 "That loop is the core concept behind the SDK. Tools, handoffs, approvals, and streaming all build on top of it rather than replacing it." — 原文
🧪 实例 循环的五个步骤可以画成流程图:

flowchart TD
  A[Call current agent's model with prepared input] --> B[Inspect the model output]
  B --> C{Tool calls?}
  C -->|yes| D[Execute tools and continue] --> A
  C -->|no| E{Handoff?}
  E -->|yes| F[Switch agents and continue] --> A
  E -->|no| G[Final answer, no more tool work] --> H[Return a result]
🔍 追问 为什么说 streaming 和普通 run 用的是「同一个循环」? → 因为 streaming 复用同样的 agent loop 和同样的 state 策略,唯一区别是你在 run 仍在进行时就消费事件,而不是等它结束。
📚 拓展阅读
QHandoffs 和「agents as tools」两种编排模式怎么选?深挖·拓展🔥高频
handoffs agents-as-tools orchestration
⏱️ 现行
多 agent 工作流的第一个设计选择,是决定在工作流的每个分支上「谁拥有面向用户的最终答案」。有两种模式:Handoffs 适用于某个 specialist 应当接管这一分支的对话、拥有下一个 response——控制权真正移交给 specialist agent;而 agents as tools 适用于一个 manager 应当保持掌控、把 specialist 当作有边界的能力来调用——manager 保留对回复的所有权。选 agents-as-tools 通常更合适的场景是:manager 需要综合出最终答案、specialist 在做像 summarization 或 classification 这样有边界的任务、并且你想要一个稳定的外层工作流嵌套 specialist 调用而非移交所有权。核心权衡是「所有权归属」:handoff 是把责任交出去,as-tool 是借用能力但不交责任。同时不要过早拆分——只有当下一个分支真正需要不同的 instructions、tools 或 policy 时才拆。
术语 Handoffs(控制权移交给 specialist,由它拥有下一个 response); Agents as tools(manager 保持所有权,把 specialist 当有边界能力调用); asTool / as_tool(把一个 agent 包装成可被 manager 调用的工具); ownership(谁负责最终面向用户的答案)
📖 "Multi-agent workflows are useful when specialists should own different parts of the job. The first design choice is deciding who owns the final user-facing answer at each branch of the workflow." — 原文
📖 "Handoffs are the clearest fit when a specialist should own the next response rather than merely helping behind the scenes." — 原文
🧪 实例 handoff 用 handoffs 列表声明可移交的 specialist;agents-as-tools 用 asTool 把 specialist 包成 manager 的工具:

typescript
import { Agent, handoff } from "@openai/agents";

const billingAgent = new Agent({ name: "Billing agent" });
const refundAgent = new Agent({ name: "Refund agent" });

const triageAgent = Agent.create({
  name: "Triage agent",
  handoffs: [billingAgent, handoff(refundAgent)],
});
🔍 追问 什么时候才应该新增一个 specialist agent? → 尽量从单个 agent 起步,只有当新 agent 能实质性改善 capability isolation、policy isolation、prompt clarity 或 trace legibility 时才加;过早拆分只会带来更多 prompt、trace 和 approval 表面,却未必让工作流更好。
📚 拓展阅读
  • Running agents — 理解 handoffs 与 tools 在一次 run 内如何表现
  • Define agents — 精修每个 specialist 的 instructions、tools 与输出契约
  • Results guide — 可恢复状态如何影响下一轮
Q多轮对话有哪几种把状态带入下一轮的策略?如何选?深挖·拓展🔥高频
sessions conversationId state multi-turn
⏱️ 现行
把状态带入下一轮有四种常见方式,区别在于「状态存在哪里」以及「下一轮要传什么」:本地维护 history(状态在你的应用里,适合小型 chat loop 和最大控制,下一轮传可重放的完整 history);session(状态在你的存储加 SDK 里,适合持久化聊天状态、可恢复的 run 和你可控的存储,下一轮传同一个 session);conversationId(状态在 OpenAI Conversations API 里,适合跨 worker 或服务共享的服务端托管状态,下一轮传同一个 conversation ID 和仅新增的 turn);以及基于 Responses API 的 last response ID 续接(最轻量的服务端托管续接,下一轮传上一个 response ID 和仅新增的 turn)。关键权衡与建议是:大多数应用应当每个对话只选一种策略,因为把本地重放和服务端托管状态混用,除非你刻意在两层之间做协调,否则会重复上下文(duplicate context)。Sessions 是默认首选,当你想要持久记忆、可恢复的审批流或应用自控的存储时。
术语 session(存储加 SDK,持久化且可恢复的对话状态); conversationId(Conversations API 托管、可跨系统共享的具名对话); last_response_id / previous_response_id(最轻量的 response 到 response 续接); duplicate context(混用本地重放与服务端状态导致上下文重复)
📖 "In most applications, pick one strategy per conversation. Mixing local replay with server-managed state can duplicate context unless you are deliberately reconciling both layers." — 原文
📖 "Sessions are the best default when you want durable memory, resumable approval flows, or storage that your application controls." — 原文
🧪 实例 用 session 持久化多轮状态,第二轮只需再传同一个 session:

python
import asyncio

from agents import Agent, Runner, SQLiteSession

agent = Agent(
    name="Tour guide",
    instructions="Answer with compact travel facts.",
)

session = SQLiteSession("conversation_123")


async def main() -> None:
    first_turn = await Runner.run(
        agent,
        "What city is the Golden Gate Bridge in?",
        session=session,
    )
    print(first_turn.final_output)

    second_turn = await Runner.run(
        agent,
        "What state is it in?",
        session=session,
    )
    print(second_turn.final_output)


if __name__ == "__main__":
    asyncio.run(main())
🔍 追问 什么时候用 conversationId 而不是 session? → 当多个系统应当共享一个具名对话时用 conversationId;当你想要最便宜的 response-to-response 续接时用基于 last response ID 的方式。
📚 拓展阅读
QResponses 的 Multi-agent 是什么?什么时候该用、什么时候不该用?深挖·拓展🔥高频
multi-agent parallel subagents beta
⏱️ 现行(beta)
Multi-agent 让一个模型并行地 spin up 并协调多个 subagent,综合它们的工作产出最终 response,特别适合能从并行工作委派中受益的复杂任务,如代码库探索、文档、实现。它的价值来自三点:并行执行(独立的 research/analysis/implementation 可同时进行,加快执行)、聚焦上下文(每个 subagent 拿到有边界的任务并维护自己的 context,减少不相关工作之间的干扰)、模型主导的协调(root agent 自行创建 subagent、发送信息、等待结果并综合最终答案,无需你的应用去实现编排)。它是 GPT-5.6 全系模型上的 beta 特性。权衡在于:当任务能被切分成具体、独立的 workstream 时最有用;但增加 subagent 会提高 token 使用,对那些依赖单一有序推理链、需要频繁写入共享可变状态、或已经被某个慢速外部操作主导的任务,收益未必好。一句话:并行度换 token 与协调开销,只有工作真正可拆分时才划算。
术语 Multi-agent(root 并行协调 subagent 并综合最终 response 的 beta 能力); root agent(名为 /root,负责综合 subagent 响应并给出最终答复); focused context(每个 subagent 维护自己有边界的 context); parallel execution(独立任务同时进行以缩短 wall-clock)
📖 "Multi-agent lets a model spin up and coordinate subagents in parallel, synthesizing their work to provide a final response." — 原文
📖 "Note that adding subagents can increase token usage, and may not be as beneficial for tasks that depend on a single ordered chain of reasoning, require frequent writes to shared mutable state, or are already dominated by one slow external operation." — 原文
🧪 实例 在 Responses 请求里用 multi_agent.enabled 打开,root 就有资格 spawn 一棵 subagent 树——例如用三个 agent 分别审 correctness、security、missing tests:

python
from openai import OpenAI

client = OpenAI()


def review_pull_request(diff: str) -> str:
    response = client.beta.responses.create(
        model="gpt-5.6-sol",
        input=(
            "Review the pull-request diff below with three agents: one for "
            "correctness, one for security, and one for missing tests. "
            "Reconcile duplicate or conflicting findings, then return a "
            "prioritized review with file and line references.\n\n"
            f"<diff>\n{diff}\n</diff>"
        ),
        multi_agent={
            "enabled": True,
            "max_concurrent_subagents": 3,
        },
        betas=["responses_multi_agent=v1"],
    )
🔍 追问 怎么调节 root 何时该 spawn subagent? → 加一条 developer message,它是对 root 和 subagent 注入指令的「additive」补充;例如「除非用户明确要求 subagent、delegation 或并行 agent 工作,否则不要 spawn subagent」,或「主动 Multi-agent 委派已启用,当并行工作能实质提升速度或质量时使用 subagent」。
📚 拓展阅读
QMulti-agent 提供了哪些 hosted collaboration actions?应用要不要执行它们?深挖·拓展中频
collaboration-actions multi_agent_call spawn_agent
⏱️ 现行(beta)
Multi-agent 模式启用后,Responses API 向 root 和 subagent 提供六个 hosted collaboration action,你会在输出里看到它们以 multi_agent_call item 出现,但你的应用不应执行它们、也不应为它们提交输出——这些是托管动作,由 Responses API 自己执行并返回对应的 multi_agent_call_output。六个动作分别是:spawn_agent(创建一个 subagent 并分配其初始任务)、send_message(给已存在的 agent 排队一条消息但不启动新 turn)、followup_task(给一个已存在的非 root agent 分配更多工作并开始或恢复它的 turn)、wait_agent(等待调用方 agent 邮箱里的更新)、interrupt_agent(打断另一个 agent 正在进行的 turn 但不删除其 context)、list_agents(返回当前 agent 树、状态及每个 agent 的 last_task_message)。要与之区分的是 developer 定义的 tool call:树中任意 agent 都可能 emit 一个 function_call,那种才需要你的应用去执行并回传匹配的 function_call_output——两类调用一个由托管侧负责、一个由你负责,绝不能混淆。
术语 spawn_agent(创建 subagent 并分配初始任务); send_message(排队消息但不触发新 turn); followup_task(给非 root agent 派新活并触发/恢复其 turn); wait_agent(等待邮箱更新); interrupt_agent(打断 turn 但保留 context); list_agents(返回 agent 树与状态)
📖 "When Multi-agent mode is enabled, the Responses API provides six hosted collaboration actions. You may see these as multi_agent_call items. Your application should not execute these or submit outputs for them." — 原文
📖 "Handling developer-defined tool calls works in the same way as without Multi-agent enabled. Any agent in the tree may emit a function_call. Your application must execute the call and submit a matching function_call_output." — 原文
🧪 实例 subagent 使用层级路径,root 是 /root,spawn 出的 subagent 形如:

text
/root
├── /root/researcher
├── /root/reviewer
└── /root/reviewer/tester
🔍 追问 树中所有 agent 能用哪些 tools? → 所有 agent 都能访问 API 请求 model call 里配置的那套 tools;subagent 共享请求的 model 和可用 tools,只通过 spawning、messaging、waiting 等协作原语来协调。
📚 拓展阅读
  • Multi-agent — 六个 action 的用途表与 How Multi-agent works 详解
  • Function calling — developer 定义的 function_call 执行与回传
QMulti-agent 的 function call:HTTP 与 WebSocket 有什么区别?该选哪个?深挖·拓展中频
websocket http response.inject latency
⏱️ 现行(beta)
HTTP 和 WebSocket 支持相同的 Multi-agent 能力,但 WebSocket 更适合 tool-heavy 或长时运行的工作流。区别在于 function 输出如何回注:用 HTTP 时,response 会在每个 active agent 要么完成、要么暂停等待一个客户端执行的 function call 时才完成;随后你的应用执行所有未决 function call,并在一个新的 Responses API 请求里提交它们的输出,让暂停的 agent 恢复。用 WebSocket 时,凭借持久连接,你的应用可以在每个 function 输出可用时立刻用 response.inject 事件注入到 response 中,而无需等待整个 active response 完成——等待的 agent 能立即恢复,其他 agent 继续工作,从而减少协调延迟、避免当多个 agent 在不同时间完成或请求 tool 时的额外请求往返。权衡是:HTTP 对「需要调用多个 hosted tool(如并行 web search)」或「function call 很少的单请求工作流」可能已足够;但对大多数 Multi-agent 工作流,WebSocket 更可能提供更低延迟和更好的端到端性能。
术语 response.inject(WebSocket 下把 function 输出注入 active response 的事件); response.inject.created(输入通过校验并被接受注入); response.inject.failed(未注入,需检查 error.code); response_already_completed(response 已完成、注入失败,需在新请求里续接)
📖 "HTTP and WebSocket support the same Multi-agent capabilities, but WebSocket is recommended for tool-heavy or long-running workflows. Its persistent connection lets your application return function outputs as they become available, reducing continuation overhead and allowing agents to spend less time waiting." — 原文
📖 "With HTTP, the response completes once every active agent has either finished or paused to wait for a client-executed function call." — 原文
🧪 实例 WebSocket 下把 tool 输出注入 active response 的事件形如:

json
{
  "type": "response.inject",
  "response_id": "resp_123",
  "input": [
    {
      "type": "function_call_output",
      "call_id": "call_123",
      "output": "{\"temperature\":72}"
    }
  ]
}
🔍 追问 如果收到 response.inject.failed 且 code 是 response_already_completed 怎么办? → 说明 response 在 function 输出能被加入前就完成了;取失败事件里返回的 input,放到一个新的 response.create 请求里,从已完成的 response 续接。
📚 拓展阅读
Q如何看待运行中的「暂停」(如人工审批)?和「新 turn」有何不同?深挖·拓展中频
approvals pauses interruptions resume
⏱️ 现行
非 happy-path 的结果分两大类:一类是运行时或校验失败,如 max-turn 上限、guardrail 异常或 tool 错误;另一类是预期内的暂停,如人工审批请求——此时 run 是被有意打断的,之后应从同一个 state 恢复。关键原则是:把 approvals 当作被暂停的 run,而不是新的 turn。这个区分能让 turn 计数、history 和服务端托管的续接 ID 保持一致——如果你把审批后的续接错误地当成一个全新用户 turn,就会污染这些计数与 ID。对 streaming 也是同理:如果 run 因审批暂停,应解决 interruptions 并从 state 恢复,而不是开一个新的用户 turn;如果你在 turn 中途取消了 stream,想让同一个 turn 之后继续,也从 state 恢复那个未完成的 turn。
术语 expected pauses(如人工审批,run 被有意打断、之后从同一 state 恢复); interruptions(暂停时需解决的中断项); state(可从中恢复未完成 turn 的运行状态); runtime or validation failures(max-turn 上限、guardrail 异常、tool 错误)
📖 "Treat approvals as paused runs, not as new turns. That distinction keeps turn counts, history, and server-managed continuation IDs consistent." — 原文
📖 "If the run pauses for approval, resolve interruptions and resume from state rather than starting a fresh user turn." — 原文
🧪 实例 文档给出的 streaming 三条实用规则:

text
- Wait for the stream to finish before treating the run as settled.
- If the run pauses for approval, resolve `interruptions` and resume from `state` rather than starting a fresh user turn.
- If you cancel a stream mid-turn, resume the unfinished turn from `state` if you want the same turn to continue later.
🔍 追问 为什么必须等 stream 结束才能把 run 当作已定? → 因为在 stream 完成前 run 还没 settled,可能仍会暂停等待审批或继续 tool 工作;过早处理会破坏 turn 的连续性。
📚 拓展阅读
QMulti-agent 新增了哪些 output item?应用该如何对待它们?深挖·拓展中频
output-items agent_message replay tracing
⏱️ 现行(beta)
Multi-agent response 可能包含三种额外的 output item 类型:multi_agent_call(记录一个 hosted Multi-agent 动作,如 spawn_agent)、multi_agent_call_output(包含某个 hosted 动作执行的结果)、agent_message(携带从一个 agent 到另一个 agent 的加密消息)。它们之间靠 call_id 字段把每个 multi_agent_call 关联到对应的 multi_agent_call_output。每个 item 还带一个 agent 属性;对 agent_message 而言,agent.agent_name 标识接收方 agent,而用 authorrecipient 来追踪消息方向。处理原则很明确:当应用收到 multi_agent_call 时,不要把它当 function call 执行、也不要回传结果,Responses API 会执行该 hosted 动作并返回对应的 multi_agent_call_output;如果你的应用需要用于 replay 或 tracing,就把两个 item 都保留下来。这套设计让托管编排的每一步都可被观察和重放,同时清晰区分「托管动作」与「你需执行的 function call」。
术语 multi_agent_call(记录如 spawn_agent 的 hosted 动作); multi_agent_call_output(hosted 动作的执行结果); agent_message(agent 间的加密消息,含 author/recipient 方向); call_id(把 call 与其 output 关联的字段)
📖 "When your application receives a multi_agent_call, do not execute it as a function call or send back a result. The Responses API executes the hosted action and returns the corresponding multi_agent_call_output. Preserve both items if your application needs them for replay or tracing." — 原文
📖 "Each item also includes an agent attribute. For an agent_message, agent.agent_name identifies the recipient agent. Use author and recipient to trace the message direction." — 原文
🧪 实例 一个 spawn_agent 的 call 与其 output、以及一条 agent_message 的形态:

json
[
  {
    "type": "multi_agent_call",
    "id": "mac_123",
    "call_id": "call_spawn_a",
    "action": "spawn_agent",
    "arguments": "{\"task_name\":\"agent_a\",\"fork_turns\":\"all\",\"message\":\"enc_...\"}",
    "agent": { "agent_name": "/root" }
  },
  {
    "type": "multi_agent_call_output",
    "id": "maco_123",
    "call_id": "call_spawn_a",
    "action": "spawn_agent",
    "output": [
      {
        "type": "output_text",
        "text": "{\"task_name\":\"/root/agent_a\"}",
        "annotations": [],
        "logprobs": []
      }
    ],
    "agent": { "agent_name": "/root" }
  }
]
🔍 追问response.createdresponse.completed 这类事件为什么不带 agent 属性? → 因为这些 response 生命周期事件描述的是整体 response 而非某个具体 agent,所以它们不包含 agent 属性;只有 agent 归属的 SSE 事件才带顶层 agent 属性。
📚 拓展阅读
  • Multi-agent — 三种新 output item 与 agent-attributed SSE 事件的完整定义
Qmax_concurrent_subagents 是什么?Multi-agent 启用后有哪些限制?深挖·拓展低频
concurrency limits compaction beta
⏱️ 现行(beta)
max_concurrent_subagents 设定整棵 agent 树上可同时活跃的 subagent 最大数量,它包含所有后代(children、grandchildren 以及更深的 subagent),但不包含 root agent。API 对该设置不设固定上界,默认值是 3,对大多数 workload 都推荐用它;Multi-agent run 对树深度或一次 run 中创建的 subagent 总数也没有固定上限。启用 Multi-agent 后有几条明确限制值得记住:/responses/compact 端点不受支持;当 multi_agent.enabledtrue 时会隐式启用服务端自动 compaction(即使请求没有配置 context_management),且 compaction 对 root 和每个 subagent 独立应用以保留各自分离的 context,用户仍可通过显式设置 context_management.compact_threshold 覆盖 compact_threshold;此外 reasoning.summarymax_tool_calls 在 Multi-agent 启用时都不受支持。这些限制反映了「每个 agent 维护独立 context」这一核心设计带来的必然取舍。
术语 max_concurrent_subagents(整树可同时活跃的 subagent 上限,默认 3,不含 root); context_management.compact_threshold(可显式覆盖的 compaction 阈值); compaction(对 root 与每个 subagent 独立应用、保留分离 context); reasoning.summary / max_tool_calls(Multi-agent 启用时不受支持)
📖 "max_concurrent_subagents sets the maximum number of subagents that can be active simultaneously across the entire agent tree. It includes all descendants—children, grandchildren, and deeper subagents—but excludes the root agent." — 原文
📖 "When multi_agent.enabled is set to true, automatic server-side compaction is enabled implicitly, even if the request does not configure context_management. Compaction is applied independently to the root agent and each subagent, preserving their separate contexts." — 原文
🧪 实例 在请求里设置并发上限(默认即 3):

json
{
  "multi_agent": {
    "enabled": true,
    "max_concurrent_subagents": 3
  }
}
🔍 追问 max_concurrent_subagents 限制的是并发还是总数? → 限制的是整棵树上同时活跃(含 children 和更深后代、但排除 root)的 subagent turn 数,而不是一次 run 中创建的 subagent 总数——总数和树深度都没有固定上限。
📚 拓展阅读
  • Compaction — 服务端 compaction 与 compact_threshold 的机制
  • Multi-agent — Limitations 一节的完整清单
中频

护栏、人工审批与可观测性

原文 原文 原文
QGuardrails 和 human review 各自解决什么问题?一个 run 什么时候该继续、暂停或停止,应该怎么在这两者之间选?深挖·拓展🔥高频
guardrails human-in-the-loop 控制边界
⏱️ 现行
这是护栏体系的总纲:guardrails 是"自动检查",human review 是"人工审批决策",两者合起来定义一个 run 应当继续、暂停还是停止。关键区别在于是否需要人介入——guardrails 自动地校验 input、output 或 tool 行为,不需要人;human review 则会把 run 暂停下来,让人或策略去 approve/reject 一个敏感动作。选型上文档给了一张清晰的对照:要在主模型跑起来之前拦掉不允许的用户请求,用 input guardrails;要在最终输出离开系统前做校验或脱敏,用 output guardrails;要在某个 function tool 调用前后检查参数或结果,用 tool guardrails;而当动作会产生副作用——取消、编辑、shell 命令、敏感的 MCP 操作——就该用 human-in-the-loop approvals。这个划分的本质是:自动护栏适合"能被规则/模型判定"的情况,而审批适合"必须由人对副作用负责"的情况。
术语 Guardrails(自动校验 input/output/tool 行为的护栏); Human review(暂停 run 等待人或策略批准的人工审批); side effects(取消、编辑、shell、敏感 MCP 等不可逆副作用)
📖 "Use guardrails for automatic checks and human review for approval decisions. Together, they define when a run should continue, pause, or stop." — 原文
📖 "Pause before side effects like cancellations, edits, shell commands, or sensitive MCP actions" — 原文
🧪 实例 客服 agent 场景:先用 input guardrail 拦掉"帮我解数学作业"这类越界请求;正常答疑走自动流程;而"取消订单 123"这种有副作用的动作,交给 human-in-the-loop approval 暂停等人批。
flowchart TD
  A[用户请求] --> B{Input guardrail}
  B -- tripwire 触发 --> X[拦截/停止]
  B -- 通过 --> C[主 Agent 运行]
  C --> D{是否敏感动作?}
  D -- 是 --> E[记录 approval interruption, 暂停]
  D -- 否 --> F{Output guardrail}
  E -- approve --> F
  E -- reject --> X
  F -- 通过 --> G[返回最终输出]
🔍 追问 guardrails 和 approvals 能同时用吗? → 能,它们是互补的:自动护栏挡掉可判定的坏输入/坏输出,审批负责对有副作用的动作做人工把关,合在一起决定 run 的继续/暂停/停止。
📚 拓展阅读
  • Using tools — tool 能力语义(哪些 tool 面需要校验或审批)的规范出处
  • Results and state — 暂停的 run 会把哪种 result 面还给应用
QInput guardrail 的 tripwire 机制是怎么工作的?blocking 执行和 parallel 执行该怎么权衡?深挖·拓展🔥高频
input-guardrail tripwire latency
⏱️ 现行
input guardrail 的用途是"在昂贵或有副作用的部分启动之前,先跑一个快速校验步骤"。机制上,护栏函数返回一个带 tripwireTriggered 布尔的结果——比如用一个专门的 guardrail agent 判断输入是不是数学作业,若判定为真就把 tripwire 置为 true。一旦 tripwire 触发,主 run 会抛出 InputGuardrailTripwireTriggered,应用据此拦截请求。核心权衡在执行时机:blocking(runInParallel: false)会先把护栏跑完再决定是否启动主 agent,适合"启动主 agent 的成本或风险太高"的场景,因为你不想做无谓的投机执行;parallel 则让护栏和主流程并行,适合"低延迟比避免投机工作更重要"的场景。换句话说,是拿"可能白跑的算力"换"更低的响应延迟"。
术语 tripwireTriggered(护栏触发开关,为真则拦截); InputGuardrailTripwireTriggered(护栏拦截时抛出的异常/错误); runInParallel(护栏是否与主流程并行执行); guardrail agent(专门做判定的辅助 agent)
📖 "Use input guardrails when you want a fast validation step to run before the expensive or side-effecting part of the workflow starts." — 原文
📖 "Use blocking execution when the cost or risk of starting the main agent is too high. Use parallel guardrails when lower latency matters more than avoiding speculative work." — 原文
🧪 实例 用 guardrail agent 判定数学作业并触发 tripwire(blocking 模式):
typescript
const agent = new Agent({
  name: "Customer support",
  instructions: "Help customers with support questions.",
  inputGuardrails: [
    {
      name: "Math homework guardrail",
      runInParallel: false,
      async execute({ input, context }) {
        const result = await run(guardrailAgent, input, { context });
        return {
          outputInfo: result.finalOutput,
          tripwireTriggered: result.finalOutput?.isMathHomework === true,
        };
      },
    },
  ],
});

try {
  await run(agent, "Can you solve 2x + 3 = 11 for me?");
} catch (error) {
  if (error instanceof InputGuardrailTripwireTriggered) {
    console.log("Guardrail blocked the request.");
  }
}
🔍 追问 用 parallel 护栏时,如果护栏最终触发但主 agent 已经跑了一部分怎么办? → 文档把这称为"speculative work"(投机工作):parallel 换来的是低延迟,代价就是护栏若触发,那部分已启动的工作可能白做——所以启动成本/风险高时应改用 blocking。
📚 拓展阅读
Qhuman-in-the-loop 审批的完整生命周期是什么?为什么它是"resume 同一个 run"而不是新开一轮?深挖·拓展🔥高频
approvals interruptions state resume
⏱️ 现行
approvals 是 tool 调用的 human-in-the-loop 路径:模型仍然可以判断某个动作是必要的,但 run 会暂停,直到你 approve 或 reject。文档给出了每次都一样的四步生命周期:(1) run 不去执行该 tool,而是记录一个 approval interruption;(2) 结果返回一组 interruptions 加一个可恢复的 state;(3) 你的应用对这些 pending 项做批准或拒绝;(4) 你从 state 恢复同一个 run,而不是开启一个新的用户回合。之所以强调"同一个 run",是因为审批只是把这一次执行暂停在 tool 调用处,上下文、历史、待执行的 tool 都保存在 state 里——从 state 恢复才能让被批准的动作接着原样执行下去。给 tool 标 needsApproval: true(Python 里 needs_approval=True)即可触发这套流程;而且即使这个需审批的 tool 藏在 handoff 之后或嵌套调用里,同样的 interruption 模式依旧适用。
术语 needsApproval / needs_approval(把某个 tool 标记为需要审批); interruptions(返回的待决审批项列表); state(可恢复的 run 快照); approve/reject(对 pending 项做批准或拒绝)
📖 "Approvals are the human-in-the-loop path for tool calls. The model can still decide that an action is needed, but the run pauses until you approve or reject it." — 原文
📖 "You resume the same run from state instead of starting a new user turn." — 原文
📖 "This same interruption pattern applies even when the approving tool lives deeper in the workflow, such as after a handoff or inside a nested call." — 原文
🧪 实例 给"取消订单"tool 标 needsApproval 并在 interruption 上逐项 approve 后 resume:
typescript
const cancelOrder = tool({
  name: "cancel_order",
  description: "Cancel a customer order.",
  parameters: z.object({ orderId: z.number() }),
  needsApproval: true,
  async execute({ orderId }) {
    return `Cancelled order ${orderId}`;
  },
});

let result = await run(agent, "Cancel order 123.");

if (result.interruptions?.length) {
  const state = result.state;
  for (const interruption of result.interruptions) {
    state.approve(interruption);
  }
  result = await run(agent, state);
}
🔍 追问 模型自己不会绕过审批直接执行吗? → 不会:一旦 tool 标了 needsApproval,run 记录的是 approval interruption 而非执行 tool——模型只能"决定要用这个动作",实际执行必须等你从 state 批准后 resume。
📚 拓展阅读
Q一个被审批中断的 run,它的 result 长什么样?为什么说"result 可以是故意不完整的"?深挖·拓展中频
results interruptions resumable-state
⏱️ 现行
通常 result 不只是最终答案,它同时是 handoff 边界、下一回合的续接面,以及"当 run 因审批暂停时"的可恢复快照。审批流是 result 故意不完整的主要情形:因为 run 还没真正结束,最终输出那一项可以是空的;interruptions 告诉你哪些 pending 的 tool 调用需要决定;而保存下来的 state 快照,就是你在批准或拒绝之后传回 runtime 的东西。这个设计的意义在于——把"暂停"表达为数据而不是异常:应用拿到 result 后,通过 interruptions 判断"还需要人拍板",再用 state 无缝续接。同一个 state 面也正是当审批可能晚点发生(而不是在同一个请求里)时你要序列化的东西:把 state 存起来,等决定到了再继续同一个 run。
术语 interruptions(待决 tool 调用列表); state(审批后传回 runtime 的可恢复快照); intentionally incomplete(因未结束而故意留空的 result)
📖 "Approval flows are the main case where a result is intentionally incomplete." — 原文
📖 "interruptions tells you which pending tool calls need a decision." — 原文
📖 "If the review might take time, serialize state, store it, and resume later. That's still the same run." — 原文
🧪 实例 一个"审批可能延迟"的处理骨架——先检测 interruptions,序列化 state 存库,等人工决定回来再 resume:
python
result = await Runner.run(agent, "Cancel order 123.")
if result.interruptions:
    state = result.to_state()
    # 把 state 序列化存起来,等审批决定到达后再从同一个 run 恢复
    for interruption in result.interruptions:
        state.approve(interruption)
    result = await Runner.run(agent, state)
🔍 追问 为什么最终输出会是空的,而不是报错? → 因为 run "hasn't actually finished"——它是被主动暂停等审批,不是失败;把空输出 + interruptions + state 组合起来,恰好表达"待续"这个中间态。
📚 拓展阅读
Q为什么说 agent 级 guardrails "不是到处都跑"?在 manager 式工作流里应该把校验放哪?深挖·拓展低频
guardrail-scope workflow-boundaries tool-guardrail
⏱️ 现行
这是很多人踩的坑:agent 级护栏有明确的作用域边界,并不覆盖工作流每个角落。具体来说——input guardrails 只对链条里的第一个 agent 跑;output guardrails 只对产出最终输出的那个 agent 跑;tool guardrails 只在它们被挂上的那些 function tool 上跑。推论是:如果你在一个 manager 式(经理调度多个子 tool)的工作流里,想给每一次自定义 tool 调用都加检查,就不能只依赖 agent 级的 input/output 护栏——因为中间那些 tool 调用根本不在 input/output 护栏的作用域内。正确做法是把校验放到"制造副作用的那个 tool"旁边,也就是 tool guardrail,让检查紧贴真正会产生后果的地方。这背后的权衡是:护栏放得越贴近副作用点,覆盖越可靠,但你要为每个敏感 tool 显式挂护栏,而不是指望一层入口/出口护栏兜底。
术语 Input guardrails(只对链中第一个 agent 生效); Output guardrails(只对产出最终输出的 agent 生效); Tool guardrails(只对被挂的 function tool 生效); manager-style workflow(经理调度多个子 tool 的工作流)
📖 "Input guardrails run only for the first agent in the chain." — 原文
📖 "If you need checks around every custom tool call in a manager-style workflow, don't rely only on agent-level input or output guardrails. Put validation next to the tool that creates the side effect." — 原文
🧪 实例 一个 manager agent 调用多个子 tool 时,若只在入口挂 input guardrail,子 tool 的参数不会被校验;应改为在会产生副作用的 tool(如写数据库、发消息)上各自挂 tool guardrail,把检查紧贴副作用点。
🔍 追问 output guardrail 会不会对链条中间的 agent 也跑? → 不会,只对"产出最终输出的那个 agent"跑;中间 agent 的输出不经过它,所以中间环节要靠 tool guardrail 兜。
📚 拓展阅读
QAgents SDK 的 tracing 默认开吗?一条 trace 里能看到什么?traces 除了 debug 还有什么用?深挖·拓展中频
tracing observability traces-dashboard evals
⏱️ 现行
tracing 是内建进 Agents SDK 的,在正常的 server-side SDK 路径下默认开启。每个 run 都能发出一条结构化记录,覆盖 model 调用、tool 调用、handoffs、guardrails 以及你自己包裹的 custom spans,你可以在 Traces dashboard 里检视。默认的一条 trace 通常给你:整体 run/workflow、每次 model 调用、tool 调用及其输出、handoffs 和 guardrails,以及你围着工作流包的任何 custom span。设计权衡上文档特别提醒:如果你觉得 trace 太多,应该用 SDK 级或 per-run 的 tracing 控制去调,而不是把工作流里所有可观测性都拿掉——因为一旦出问题你会失去端到端的现场记录。traces 有两个用途:一是 debug 单次 run、搞清楚到底发生了什么;二是当你准备系统性地给行为打分时,把高信噪比的样例喂给 agent workflow evaluation。
术语 Tracing(内建、默认开启的结构化运行记录); custom spans(围绕工作流自定义包裹的跨度); Traces dashboard(platform.openai.com/traces 上检视 trace 的面板); agent workflow evaluation(系统化给行为打分的评估)
📖 "Tracing is built into the Agents SDK and is enabled by default in the normal server-side SDK path." — 原文
📖 "Every run can emit a structured record of model calls, tool calls, handoffs, guardrails, and custom spans, which you can inspect in the Traces dashboard." — 原文
📖 "If you need less tracing, use the SDK-level or per-run tracing controls rather than removing all observability from the workflow." — 原文
🧪 实例withTrace 把多次 run 归到同一条 trace 下,便于把一个工作流当整体检视:
typescript
await withTrace("Joke workflow", async () => {
  const first = await run(agent, "Tell me a joke");
  const second = await run(agent, `Rate this joke: ${first.finalOutput}`);
  console.log(first.finalOutput);
  console.log(second.finalOutput);
});
🔍 追问 trace 太多、想减负时正确姿势是什么? → 用 SDK 级或 per-run 的 tracing 控制去收紧,而不是把工作流的可观测性整个删掉,否则出问题时没有端到端记录可查。
📚 拓展阅读
Qhosted MCP 和 local/private MCP 有什么区别?各自该在什么情况下用?深挖·拓展中频
MCP hosted-mcp trust-model approvals
⏱️ 现行
MCP 接入有两条路,分野在于"谁拥有这条连接"。hosted MCP tools 用于"远端服务器应该通过 model surface 来跑"的情况:模型可以透过 hosted 面去调用远程 MCP server,适合能纳入平台信任模型的公共远程服务器。local/private MCP(通过 stdio 或 streamable HTTP 的 SDK-managed 服务器)用于"你的应用应该直接连到 MCP server"的情况:此时你的 runtime 拥有连接、审批和网络边界。实践上的划分很直接——公共远程服务器且符合平台信任模型,用 hosted MCP;当你的 runtime 需要自己掌管连通性、过滤或审批时,用 local 或 private MCP。这个权衡本质是"托管便利 vs 自主可控":hosted 让模型面直接接远端省心,但连接和边界交给平台;local/private 把控制权拿回自己手里,代价是你要自己维护连接、审批与网络边界。平台级的概念、信任模型和产品支持故事,以 MCP and Connectors 为规范出处。
术语 Hosted MCP tools(通过 model surface 调用的托管远程 MCP); SDK-managed MCP servers(经 stdio 或 streamable HTTP 由 runtime 自管的本地/私有 MCP); trust model(平台信任模型); require_approval(hosted MCP 的审批配置,如 "never")
📖 "Use hosted MCP tools when the remote server should run through the model surface." — 原文
📖 "Your runtime owns the connection, approvals, and network boundaries" — 原文
🧪 实例 挂一个 hosted MCP server(模型透过托管面调用远端 gitmcp):
typescript
const agent = new Agent({
  name: "MCP assistant",
  instructions: "Use the MCP tools to answer questions.",
  tools: [
    hostedMcpTool({
      serverLabel: "gitmcp",
      serverUrl: "https://gitmcp.io/openai/codex",
    }),
  ],
});
🔍 追问 想让审批和网络边界都攥在自己手里,该选哪种? → 选 local/private MCP:因为此时"your runtime owns the connection, approvals, and network boundaries",runtime 自管连通性、过滤与审批。
📚 拓展阅读
  • MCP and Connectors — 平台级 MCP 概念、信任模型与产品支持的规范出处
  • Using tools — MCP 之外 hosted/function tools 的能力语义
Qstreaming 的 run 会不会需要另一套审批系统?流式暂停时正确的处理顺序是什么?深挖·拓展低频
streaming approvals state-model
⏱️ 现行
不会。streaming 并不会创建一套单独的审批系统,它和普通 run 用的是同一个 state 模型。当一个流式 run 暂停时,正确顺序是:先等它 settle(稳定下来),再 inspect interruptions,resolve 那些审批,最后从同一个 state 恢复。如果审批发生在更晚的时刻,就把序列化后的 state 存起来,等决定到达时继续同一个 run。这与非流式审批完全一致——都是 interruptions + 可恢复 state 那一套。这样设计的好处是心智模型统一:你不需要为"流式"再学一套暂停/恢复语义,streaming 只是输出方式不同,审批的暂停、检视、恢复逻辑复用同一个 state 面。
术语 settle(等流式 run 稳定后再处理审批); interruptions(流式暂停时同样返回的待决项); serialized state(延迟审批时序列化存储的快照)
📖 "Streaming doesn't create a separate approval system." — 原文
📖 "If a streamed run pauses, wait for it to settle, inspect interruptions, resolve the approvals, and resume from the same state." — 原文
🧪 实例 流式客服流里,当模型在流中途要"取消订单"而暂停:不要另起审批通道,而是等流 settle → 读 interruptions → approve/reject → 从同一 state resume;若人不在,序列化 state 存库,决定回来再续同一个 run。
🔍 追问 流式暂停能立刻在流事件里就地批准吗? → 文档要求先"wait for it to settle"再检视 interruptions、resolve、从同一 state 恢复——即先让流稳定,而不是在流未结束时抢着处理。
📚 拓展阅读
低频

Agent 评估与 Agent Builder

原文 原文 原文
Q什么是 Agent Builder?用它构建一个 agent 从设计到上线要经过哪几步?深挖·拓展低频
Agent Builder workflow ChatKit
⏱️ 现行(2026-11-30 停用)
Agent Builder 是一块可视化画布,用于搭建多步的 agent workflow。你从模板起步,把每个步骤对应的 node 拖拽、连线,给出 typed 的输入输出,并用实时数据 Preview 运行。它的核心抽象是 workflow ——由 agents、tools 与 control-flow 逻辑三者组合而成,把处理任务或驱动对话的全部步骤与动作封装为一个带可部署代码的对象。构建 agent 处理任务共有三大步:先在 Agent Builder 里设计 workflow(定义 agents 与其协作方式),再 publish 得到一个带 ID 和版本的对象,最后 deploy——把 ID 传入 ChatKit 集成,或下载 Agents SDK 代码自行部署。这样设计的权衡在于:可视化画布降低了编排门槛并让节点间数据契约显式化,而 publish 的版本快照+两种部署路径(ChatKit 托管 vs SDK 自托管)让你在"省心"和"可控/可定制"之间取舍。注意 OpenAI 正在弃用 Agent Builder,计划 2026 年 11 月 30 日关停,但 ChatKit 仍保留。
术语 Agent Builder(可视化多步 agent 工作流画布); workflow(agents+tools+控制流的组合对象); node(工作流的构建块/步骤); ChatKit(用于把对话体验嵌入应用的框架); Agents SDK(自行部署工作流的代码)
📖 "Agent Builder is a visual canvas for building multi-step agent workflows." — 原文
📖 "A workflow is a combination of agents, tools, and control-flow logic. A workflow encapsulates all steps and actions involved in handling your tasks or powering your chats, with working code you can deploy when you're ready." — 原文
📖 "Deploy your workflow. Pass the ID into your ChatKit integration, or download the Agents SDK code to deploy your workflow yourself." — 原文
🧪 实例 官方给出的示例是一个 homework helper 工作流:用多个 agent 接收问题、把问题重新措辞以获得更好回答、路由到其他专门 agent、再返回答案。整体三步流程可视化如下。
flowchart LR
    A[Design workflow
in Agent Builder] --> B[Publish
object with ID + versioning] B --> C{Deploy} C -->|Pass workflow ID| D[ChatKit integration] C -->|Download SDK code| E[Agents SDK self-host]
🔍 追问 Agent Builder 停用后已构建的工作流还能用吗? → 现有用户可在过渡窗口内继续使用,产品计划 2026 年 11 月 30 日关停,但 ChatKit 仍然可用,可继续用工作流 ID 走 ChatKit 部署。
📚 拓展阅读
Q评估 agent workflow 时,什么时候用 trace grading,什么时候转向 datasets 与 eval runs?深挖·拓展🔥高频
evals trace-grading datasets
⏱️ 现行
官方把评估当成一个决策点:两条主要评估面服务于不同阶段。当你还在 debug 行为时,从 trace 入手最快——一条 trace 记录了一次运行里模型调用、工具调用、guardrails 与 handoff 的端到端全貌,grader 则用结构化标准给这些 trace 打分,从而在规模上发现 regression 与失败模式。trace grading 适合回答这类问题:agent 是否选对了工具?该发生 handoff 时是否发生了?工作流是否违反了某条指令或安全策略?某次 prompt 或路由改动是否改善了端到端行为?其工作流是:在 dashboard 打开 Logs > Traces,检视一条有代表性的工作流 trace,创建 grader 并对选定 trace 运行,再据结果精修 prompt、工具面、路由逻辑或 guardrails。一旦你清楚"好"是什么样子,就应从单条 trace 转向可重复的 datasets 与 eval runs——当你要 benchmark 改动、对比 prompt 或做更大规模的长期评估时,这才是正确一步。权衡在于:trace grading 信号密度高、见效快,适合定位问题;而 datasets/eval runs 提供可重复性,适合回归基准。若需对外部模型评估、评估 API 或大规模批量评估等高级能力,则改用 Evals。
术语 trace(一次运行的端到端记录); grader(用结构化标准给 trace 打分的评分器); eval runs(可重复的评估运行); dataset(用于基准的数据集); Evals(支持外部模型/API/批量的高级评估)
📖 "Trace grading is the fastest way to identify workflow-level issues. A trace captures the end-to-end record of model calls, tool calls, guardrails, and handoffs for one run." — 原文
📖 "Once you know what “good” looks like, move from individual traces to repeatable datasets and eval runs. This is the right step when you want to benchmark changes, compare prompts, or run larger-scale evaluations over time." — 原文
🧪 实例 trace-grading 的标准四步流程:
text
1. Open Logs > Traces in the dashboard.
2. Inspect a representative workflow trace (SDK app or Agent Builder workflow).
3. Create a grader and run it against the selected traces.
4. Refine prompts, tool surfaces, routing logic, or guardrails.

在 Agent Builder 内,还可直接在顶部导航点击 Evaluate,选一条或一组 trace 跑自定义 grader 来评估整体工作流表现。
🔍 追问 如果我要评估外部(非 OpenAI)模型或做大规模批量评估该用什么? → 用 Evals,它支持对外部模型评估、通过 API 交互以及更大规模的批量评估,与 datasets 配合使用。
📚 拓展阅读
Q构建 agent 工作流面临哪两类主要风险?它们的机制有什么不同?深挖·拓展中频
安全 prompt-injection data-leakage
⏱️ 现行
官方指出某些工作流模式更易受攻击,聊天类工作流有两点尤其重要:保护用户输入,以及谨慎对待 MCP 工具调用。由此引出两类风险。第一类是 prompt injection(提示注入):当不受信任的文本或数据进入 AI 系统,其中的恶意内容试图覆盖给 AI 的指令。其目标各异,可能包括经由下游工具调用外泄私有数据、采取偏离目标的行动,或以非预期方式改变模型行为——例如骗过一个数据查询 agent,让它发送原始客户记录而非本应给出的摘要。第二类是 private data leakage(私有数据泄露):agent 意外分享私有数据,关键差异在于它背后不一定有攻击者——模型可能在无人操纵下就把超出用户预期的数据发给 MCP。二者的根本区别是:prompt injection 是外部对抗性输入驱动的越权,而 data leakage 可能是模型自主的、非预期的过度分享。guardrails 能更好地控制上下文中包含的信息,但你无法完全掌控模型选择向已连接 MCP 分享什么。因此即便采取缓解措施,agent 也不会完美,仍可能犯错或被骗,所以要理解风险并对授予 agent 的访问权限与使用方式保持谨慎。
术语 prompt injection(不受信内容试图覆盖 AI 指令的攻击); private data leakage(agent 意外分享私有数据); MCP(模型可连接调用的工具服务); guardrails(限制上下文信息的护栏); exfiltration(经下游工具调用外泄数据)
📖 "A prompt injection happens when untrusted text or data enters an AI system, and malicious contents in that text or data attempt to override instructions to the AI." — 原文
📖 "It's possible for a model to leak private data in a way that's not intended, without an attacker behind it. For example, a model may send more data to an MCP than the user expected or intended." — 原文
📖 "While guardrails provide better control to limit the information included in context, you don't have full control over what the model chooses to share with connected MCPs." — 原文
🧪 实例 prompt injection 的具体情形:一段恶意 prompt 可能"trick a data lookup agent into sending raw customer records instead of the intended summary"——把本应输出摘要的数据查询 agent 诱导为直接吐出原始客户记录。官方还指向 Codex internet access 文档中的语境实例。
🔍 追问 private data leakage 一定是被攻击导致的吗? → 不是。它可以在没有攻击者的情况下发生——模型可能自发地向 MCP 发送超出用户预期或意图的数据。
📚 拓展阅读
Q如何缓解 agent 工作流的 prompt injection 与数据泄露风险?列出并解释关键做法。深挖·拓展中频
安全 structured-outputs guardrails tool-approvals
⏱️ 现行
官方给出一组组合式的加固手段,核心思想是让不受信数据永不直接驱动 agent 行为。首先,不要在 developer message 里使用不受信变量——因为 developer message 优先级高于 user 和 assistant message,把不受信输入直接注入其中等于把最高控制权交给攻击者;应通过 user message 传入以限制其影响。其次,用 structured outputs 约束数据流:prompt injection 常依赖模型自由生成非预期文本或命令向下游传播,通过在节点间定义 enums、固定 schema、必填字段名等结构化输出,可消除攻击者用来夹带指令或数据的自由文本通道。第三,用清晰指导与示例引导 agent——以良好的策略文档和明确示例强化 prompt,预判非预期场景。第四,使用 GPT-5 或 GPT-5-mini,它们更守 developer 指令、对越狱和间接注入更稳健,可在 agent node 层配置作为更有韧性的默认姿态。第五,保持 tool approvals 开启——用 MCP 工具时始终启用工具审批,让终端用户能审阅确认每次操作(含读与写),在 Agent Builder 里用 human approval 节点。第六,用 guardrails 净化用户输入,redact PII 并检测越狱尝试。第七,跑 trace graders 和 evals 来发现和预防错误。把这些技术组合、对关键步骤加固,可显著降低风险,但结构化输出与隔离只能大幅减少而非完全消除风险。
术语 developer message(优先级高于 user/assistant 的开发者消息); structured outputs(枚举/固定 schema/必填字段等结构化输出); human approval(人工审批节点); tool approvals(工具操作审批); PII(个人可识别信息); GPT-5 / GPT-5-mini(更抗注入的推荐模型)
📖 "Because developer messages take precedence over user and assistant messages, injecting untrusted input directly into developer messages gives attackers the highest degree of control. Pass untrusted inputs through user messages to limit their influence." — 原文
📖 "By defining structured outputs between nodes (e.g., enums, fixed schemas, required field names), you eliminate freeform channels that attackers can exploit to smuggle instructions or data." — 原文
📖 "When using MCP tools, always enable tool approvals so end users can review and confirm every operation, including reads and writes." — 原文
🧪 实例 组合式加固的最终原则(直接源自"Combine techniques"段):
text
- Design workflows so untrusted data never directly drives agent behavior.
- Extract only specific structured fields (enums / validated JSON) from external inputs.
- Use guardrails, tool confirmations, and variables passed via user messages to validate inputs.
- 但:Structured outputs and isolation greatly reduce, but don't fully remove, this risk.
🔍 追问 为什么推荐把模型换成 GPT-5 或 GPT-5-mini? → 因为它们对遵循 developer 指令更自律,对越狱和间接 prompt injection 的鲁棒性更强,在 agent node 层配置可作为更有韧性的默认姿态,尤其适合高风险工作流。
📚 拓展阅读
Q在 Agent Builder 里用 node 组合工作流是怎么工作的?typed edge 有什么意义?深挖·拓展中频
node typed-edge data-contract
⏱️ 现行
在 Agent Builder 中,你插入并连接 node 来构建工作流,node 是 agent 的构建块。每一条 node 之间的连接都会成为一条 typed edge(带类型的边);点击某个 node 可配置其输入输出、观察步骤间的数据契约(data contract),并确保下游 node 收到它们期望的属性。这种 typed 设计的价值在于让节点间的数据契约显式化——你能在编辑阶段就看清上游产出什么、下游需要什么,从而在编排层面提前发现契约不匹配,而不是等到运行时才暴露。官方提供常见工作流模式的模板:从模板起步可看清 node 如何协作,也可从零开始。要查看所有可用 node 及其配置项,参见 node reference 文档。构建过程中可用 Preview 交互式运行工作流、附加样例文件、观察每个 node 的执行。这一整套"显式类型契约 + 可视化 Preview"的组合,把多步编排中最易出错的"步骤间数据传递"变成可检视、可调试的对象。
术语 typed edge(node 间带类型的连接边); data contract(步骤间的数据契约); node reference(所有可用 node 及配置的参考文档); Preview(交互式运行并观察每个 node 执行的调试功能); template(常见工作流模式的模板)
📖 "In Agent Builder, insert and connect nodes to create your workflow. Each connection between nodes becomes a typed edge. Click a node to configure its inputs and outputs, observe the data contract between steps, and ensure downstream nodes receive the properties they expect." — 原文
📖 "Here, you can interactively run your workflow, attach sample files, and observe the execution of each node." — 原文
🧪 实例 一条以 typed edge 相连的多 agent 链,契约在编辑期即可检视:
flowchart LR
    Q[接收问题 agent] -->|typed edge: question| R[重新措辞 agent]
    R -->|typed edge: reframed| RT[路由 agent]
    RT -->|typed edge: routed| S[专门 agent → 返回答案]
🔍 追问 从模板起步和从零开始有什么区别? → Agent Builder 为常见工作流模式提供模板;从模板起步能看清 node 如何协同工作,也可以选择从零开始搭建。
📚 拓展阅读
QAgent Builder 里如何 publish 和 deploy 工作流?两种部署路径怎么选?深挖·拓展中频
publish versioning deploy ChatKit
⏱️ 现行
Agent Builder 会随手自动保存(autosave);当你对工作流满意时,publish 它会创建一个作为快照的新 major 版本。发布后即可在 ChatKit 里使用该工作流——ChatKit 是 OpenAI 用于嵌入对话体验的框架。你可以创建新版本,也可以在 API 调用中指定一个更旧的版本,这种版本化让你能在快照间安全地演进与回滚。部署时点击顶部导航的 Code,有两个生产实现选项:其一是 ChatKit,遵循 ChatKit quickstart 并传入工作流 ID 把工作流嵌入应用——官方在不确定时推荐这个选项,因为它托管、省心;其二是 Advanced integration,复制工作流代码在任意处使用,可在自有基础设施上运行 ChatKit,并用 Agents SDK 构建与定制 agent 对话体验。二者的权衡即"托管便捷 vs 自托管可控/可定制"。这与 build→publish→deploy 三步中的后两步一一对应:publish 负责版本快照,deploy 负责落地到产品。
术语 autosave(边构建边自动保存); publish(创建作为快照的新 major 版本); versioning(可创建新版本或在 API 中指定旧版本); Code(顶部导航中进入部署选项的入口); Advanced integration(复制代码自托管的高级集成)
📖 "When you're happy with your workflow, publish it to create a new major version that acts as a snapshot." — 原文
📖 "You can create new versions or specify an older version in your API calls." — 原文
📖 "Advanced integration: Copy the workflow code and use it anywhere. You can run ChatKit on your own infrastructure and use the Agents SDK to build and customize agent chat experiences." — 原文
🧪 实例 官方对不确定该选哪条路径的建议非常直接——"ChatKit: Follow the ChatKit quickstart and pass in your workflow ID to embed this workflow into your application. If you're not sure, we recommend this option." 即默认走 ChatKit,把工作流 ID 传进去即可。
🔍 追问 publish 之后旧版本还能被调用吗? → 能。你可以创建新版本,也可以在 API 调用里指定一个更旧的版本。
📚 拓展阅读

第5章 · 检索、嵌入与多模态

🔥高频

向量嵌入(Embeddings)原理与用法

原文 原文
Q什么是 embedding?它到底衡量的是什么,能用来做哪些事?深挖·拓展🔥高频
embeddings 语义相似度
⏱️ 现行
一个 embedding 本质上就是一串浮点数组成的向量(vector),它把一段文本"编码"成高维空间中的一个点,从而让文本的相关性(relatedness)变成一个可以计算的几何量。核心机制是:两个向量之间的距离(distance)衡量它们的相关性——距离越小相关性越高,距离越大相关性越低。正因为把语义映射成了向量距离,一堆原本无法直接做数学运算的自然语言,就能拿来排序、聚类、分类。文档把它的常见用途归纳为六类:Search(按与查询串的相关性排序结果)、Clustering(按相似度分组)、Recommendations(推荐相关文本的条目)、Anomaly detection(找出相关性极低的离群点)、Diversity measurement(分析相似度分布)、Classification(按最相似的标签给文本分类)。理解这一层的关键在于:embedding 不做"精确匹配",它捕捉的是语义,所以"delicious beans"能匹配到讲豆子好吃的评论,即使用词不同。
术语 embedding(把文本编码成的浮点数向量); relatedness(相关性,由向量距离度量); distance(向量间距离,小=高相关)
📖 "OpenAI’s text embeddings measure the relatedness of text strings." — 原文
📖 "An embedding is a vector (list) of floating point numbers." — 原文
📖 "Small distances suggest high relatedness and large distances suggest low relatedness." — 原文
🧪 实例 文本被压成一个向量后,语义相近的文本在向量空间中"靠得近":
flowchart LR
  T1["文本 A: delicious beans"] --> E1["向量 A"]
  T2["文本 B: 这些豆子味道很棒"] --> E2["向量 B"]
  T3["文本 C: bad delivery"] --> E3["向量 C"]
  E1 -- 距离小/相关性高 --> E2
  E1 -- 距离大/相关性低 --> E3
🔍 追问 embedding 相似度能替代关键词精确匹配吗? → 它衡量的是语义相关性而非字面匹配,适合"意思接近但用词不同"的检索;需要精确 ID/字段匹配时仍应用传统方式。
📚 拓展阅读
Q怎么调用 embeddings API 拿到向量?返回结构长什么样?深挖·拓展🔥高频
API text-embedding-3-small
⏱️ 现行
要拿到 embedding,就把文本字符串发到 embeddings API endpoint,并带上模型名(例如 text-embedding-3-small)。请求体主要是 input(待编码文本)和 model;示例里还会传 encoding_format: "float"。返回是一个 object: "list" 的结构,data 数组里每个元素是一个 object: "embedding",含 indexembedding(就是那串浮点数),顶层还带 modelusageprompt_tokens / total_tokens)。机制上要注意:计费是按 input 里的 token 数算的,所以 usage 字段直接反映了这次调用的成本。拿到向量后的典型工作流是——把 embedding 向量抽出来、存进向量数据库(vector database),后续复用给检索、聚类等各种场景,而不必每次重新编码。
术语 input(待编码的文本字符串); encoding_format(返回格式,如 float); usage.total_tokens(计费依据的 token 数); data[].embedding(浮点数向量)
📖 "The response contains the embedding vector (list of floating point numbers) along with some additional metadata. You can extract the embedding vector, save it in a vector database, and use for many different use cases." — 原文
🧪 实例 Python 调用与返回体(均逐字来自文档):
python
from openai import OpenAI
client = OpenAI()

response = client.embeddings.create(
    input="Your text string goes here",
    model="text-embedding-3-small"
)

print(response.data[0].embedding)
json
{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [
        -0.006929283495992422, -0.005336422007530928, -4.547132266452536e-5,
        -0.024047505110502243
      ]
    }
  ],
  "model": "text-embedding-3-small",
  "usage": {
    "prompt_tokens": 5,
    "total_tokens": 5
  }
}
🔍 追问 一次调用只能 embed 一个字符串吗? → 文档示例把单条文本作为 input,返回 data 是数组、每个元素带 index,说明结构上支持一次编码多条并按 index 对应。
📚 拓展阅读
Qembedding 的维度是多少?为什么可以"缩短"维度,缩短时要注意什么?深挖·拓展🔥高频
dimensions Matryoshka 归一化
⏱️ 现行
默认下 text-embedding-3-small 的向量长度是 1536,text-embedding-3-large 是 3072。维度越大,存储、内存、计算和检索成本都越高,所以官方给了降维手段:v3 两个模型在训练时用了一种技术,允许开发者在"性能"和"成本"之间做权衡——可以从向量末尾去掉一些数字来缩短 embedding,而不损失它表征概念的能力。降维的推荐做法是在创建 embedding 时就传 dimensions 参数;若已经生成再手动截断,则必须对截断后的维度做 L2 归一化(normalize)。这套设计的价值在于灵活性:比如向量库只支持最多 1024 维,你仍可以用最强的 text-embedding-3-large、把 3072 维缩到 1024,用一点精度换更小的向量体积。文档给的对照很有说服力:在 MTEB 上,text-embedding-3-large 缩到 256 维仍能超过未缩短、1536 维的 text-embedding-ada-002
术语 dimensions(创建时指定的向量维度参数); normalize_l2(手动截断后需做的 L2 归一化); concept-representing properties(缩短后仍保留的概念表征能力)
📖 "By default, the length of the embedding vector is 1536 for text-embedding-3-small or 3072 for text-embedding-3-large." — 原文
📖 "on the MTEB benchmark, a text-embedding-3-large embedding can be shortened to a size of 256 while still outperforming an unshortened text-embedding-ada-002 embedding with a size of 1536." — 原文
📖 "In general, using the dimensions parameter when creating the embedding is the suggested approach." — 原文
🧪 实例 手动截断到 256 维后必须归一化(逐字来自文档):
python
from openai import OpenAI
import numpy as np

client = OpenAI()

def normalize_l2(x):
    x = np.array(x)
    if x.ndim == 1:
        norm = np.linalg.norm(x)
        if norm == 0:
            return x
        return x / norm
    else:
        norm = np.linalg.norm(x, 2, axis=1, keepdims=True)
        return np.where(norm == 0, x, x / norm)


response = client.embeddings.create(
    model="text-embedding-3-small", input="Testing 123", encoding_format="float"
)

cut_dim = response.data[0].embedding[:256]
norm_dim = normalize_l2(cut_dim)

print(norm_dim)
🔍 追问 为什么手动截断后要归一化,用 dimensions 参数却不用? → 用 dimensions 参数创建时模型直接输出规范化好的短向量;手动切片破坏了原有长度,必须重新 L2 归一化才能保证距离度量正确。
📚 拓展阅读
Q检索时该用哪种距离函数?为什么 OpenAI 推荐 cosine similarity?深挖·拓展🔥高频
cosine-similarity 点积 归一化
⏱️ 现行
官方推荐用 cosine similarity(余弦相似度),并且强调距离函数的选择通常影响不大。之所以能这么说,关键在于一个前提:OpenAI 的 embeddings 已被归一化到长度 1(normalized to length 1)。这带来两个直接后果——其一,因为向量都是单位长度,cosine similarity 可以直接用点积(dot product)稍快地算出来(省掉了除以模长的步骤);其二,cosine similarity 和 Euclidean distance(欧氏距离)会给出完全相同的排序(identical rankings),所以对"取最相关的前几条"这类排序任务,选哪个距离函数结果一样。理解这点能避免过度纠结距离函数:真正影响检索质量的是 embedding 本身,而非度量方式的微小差别。
术语 cosine similarity(余弦相似度,推荐的度量); normalized to length 1(向量被归一化为单位长度); dot product(单位向量下等价于余弦的更快算法)
📖 "We recommend cosine similarity. The choice of distance function typically doesn't matter much." — 原文
📖 "Cosine similarity can be computed slightly faster using just a dot product" — 原文
📖 "Cosine similarity and Euclidean distance will result in the identical rankings" — 原文
🧪 实例 因向量已归一化,余弦相似度可退化为点积:
python
import numpy as np
# 向量已 normalized to length 1,故 cosine == dot product
def cosine_similarity(a, b):
    return np.dot(a, b)
🔍 追问 既然 cosine 和 Euclidean 排序一样,为什么还偏偏推荐 cosine? → 在单位向量下 cosine 等价于点积、计算稍快,且相似度天然落在直观区间,工程上更省事。
📚 拓展阅读
Q怎样用 embeddings 做语义文本检索(semantic text search)?深挖·拓展🔥高频
语义检索 top_n cosine
⏱️ 现行
语义检索的做法是:把查询串(query)也 embed 成向量,再计算查询向量与每个文档向量之间的 cosine similarity,按相似度从高到低排序、取前 top_n 条返回。相比把全部文档塞进上下文窗口(context window)再让模型回答,这种"先 embed 查询、再找最相似文档"的方式既高效又成本极低:文档只需离线编码一次并缓存,查询时只做一次 embedding + 若干次向量点积。Cookbook 的实现里,用 get_embeddingproduct_description 编码,然后对 DataFrame 每行评论的 embedding 计算 cosine_similarity,写入 similarity 列,最后 sort_values(... ascending=False).head(n) 取最相关的几条。它的威力在于捕捉语义——查 "bad delivery" 能直接命中"配送失败"的评论,而不依赖字面出现这两个词。
术语 search query(被 embed 的查询串); cosine_similarity(查询向量与文档向量的相似度); top_n(按相似度取前 n 条); similarity(排序用的相似度列)
📖 "We can search through all our reviews semantically in a very efficient manner and at very low cost, by embedding our search query, and then finding the most similar reviews." — 原文
📖 "Here we compare the cosine similarity of the embeddings of the query and the documents, and show top_n best matches." — 原文
📖 "To retrieve the most relevant documents we use the cosine similarity between the embedding vectors of the query and each document, and return the highest scored documents." — 原文
🧪 实例 Cookbook 的语义检索函数(逐字来自 notebook):
python
from utils.embeddings_utils import get_embedding, cosine_similarity

# search through the reviews for a specific product
def search_reviews(df, product_description, n=3, pprint=True):
    product_embedding = get_embedding(
        product_description,
        model="text-embedding-3-small"
    )
    df["similarity"] = df.embedding.apply(lambda x: cosine_similarity(x, product_embedding))

    results = (
        df.sort_values("similarity", ascending=False)
        .head(n)
        .combined.str.replace("Title: ", "")
        .str.replace("; Content:", ": ")
    )
    if pprint:
        for r in results:
            print(r[:200])
            print()
    return results


results = search_reviews(df, "delicious beans", n=3)
🔍 追问 文档向量每次查询都要重算 embedding 吗? → 不用;文档 embedding 预先算好存在 DataFrame/向量库里,查询时只 embed query 并做点积比对,这正是"低成本、高效率"的来源。
📚 拓展阅读
Qtext-embedding-3-small / 3-large / ada-002 怎么选?各自的性能与成本如何?深挖·拓展中频
模型选型 MTEB pricing
⏱️ 现行
OpenAI 提供两个第三代 embedding 模型(模型 ID 里以 -3 标识),此外还有上一代的 text-embedding-ada-002。计费按 input token 计。选型主要在成本与检索质量之间权衡:text-embedding-3-small 最便宜(约 62,500 页/美元)、MTEB 得分 62.3%;text-embedding-3-large 质量最高(MTEB 64.6%)但更贵(约 9,615 页/美元);ada-002 居中(12,500 页/美元、61.0%)。三者的 Max input 都是 8192 token。实践取舍:对成本敏感、量大的通用检索用 3-small;对检索精度要求高的场景用 3-large;而且 3-large 还能配合 dimensions 参数缩维,在保持较高质量的同时压低存储成本,往往比直接退回 ada-002 更划算。
术语 text-embedding-3-small(便宜、通用的三代模型); text-embedding-3-large(最高质量的三代模型); MTEB(embedding 评测基准); Max input(单次输入上限 8192 token)
📖 "OpenAI offers two powerful third-generation embedding model (denoted by -3 in the model ID)." — 原文
📖 "Usage is priced per input token." — 原文
🧪 实例 文档给出的模型对照表:
text
| Model                  | ~ Pages per dollar | MTEB   | Max input |
| text-embedding-3-small | 62,500             | 62.3%  | 8192      |
| text-embedding-3-large | 9,615              | 64.6%  | 8192      |
| text-embedding-ada-002 | 12,500             | 61.0%  | 8192      |
🔍 追问 想省钱但又不想放弃质量,该怎么办? → 用 3-large 并通过 dimensions 缩维:文档指出缩到 256 维仍能胜过 1536 维的 ada-002,是"高质量 + 低存储"的折中。
📚 拓展阅读
Q嵌入前怎么知道一段文本有多少 token?该用哪种编码?深挖·拓展中频
tiktoken cl100k_base token
⏱️ 现行
因为 embeddings 按 input token 计费、且单次输入有 8192 token 上限,嵌入前预估 token 数很有用。在 Python 里可以用 OpenAI 的 tokenizer tiktoken 把字符串切成 token 再数长度。关键是编码(encoding)要和模型匹配:对 text-embedding-3-small 这类第三代 embedding 模型,应使用 cl100k_base 编码。做法就是 tiktoken.get_encoding(encoding_name) 拿到编码器,encoding.encode(string) 得到 token 列表,其长度即 token 数。这样可以在真正调用 API 前就控制成本、并提前把超过 8192 token 的输入截断或分块。
术语 tiktoken(OpenAI 的分词器库); cl100k_base(三代 embedding 模型对应的编码); encode(把字符串切成 token 序列)
📖 "In Python, you can split a string into tokens with OpenAI's tokenizer tiktoken." — 原文
📖 "For third-generation embedding models like text-embedding-3-small, use the cl100k_base encoding." — 原文
🧪 实例 数 token 的示例代码(逐字来自文档):
python
import tiktoken

def num_tokens_from_string(string: str, encoding_name: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.get_encoding(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens

num_tokens_from_string("tiktoken is great!", "cl100k_base")
🔍 追问 用错编码(比如给三代模型用别的编码)会怎样? → token 切分与计费/上限判断会不准;文档明确三代模型应配 cl100k_base,编码需与模型对应。
📚 拓展阅读
Q要在海量向量里快速找 K 个最近邻,工程上怎么做?深挖·拓展中频
向量数据库 KNN 检索
⏱️ 现行
当向量数量很大时,用纯 Python 对每条文档逐一算 cosine similarity(像 search_reviews 那样全表 apply)会变慢。官方给出的工程建议是:要在大量向量上快速检索,就用向量数据库(vector database)。向量数据库提供了专门的近似最近邻索引,能把"找 top-K 最相似"从线性扫描降到近似次线性,从而支撑大规模、低延迟的检索。这也解释了前面工作流为什么强调"把 embedding 存进向量数据库"——存储只是手段,真正目的是借助它的索引结构做快速 K 近邻检索。Cookbook 里有一整套关于如何配合 OpenAI API 使用各种向量数据库的示例可参考。
术语 vector database(专门存储与检索向量的数据库); K nearest(前 K 个最相似向量); retrieve ... quickly(用索引加速的近邻检索)
📖 "For searching over many vectors quickly, we recommend using a vector database." — 原文
📖 "We can search through these reviews easily. To speed up computation, we can use a special algorithm, aimed at faster search through embeddings." — 原文
🧪 实例 notebook 里也提示大规模检索需要更快的算法:
🔍 追问 小数据集也一定要上向量数据库吗? → 不必;Cookbook 的千条评论例子直接用 pandas 全量点积即可,向量库是为"many vectors"的大规模、低延迟场景准备的。
📚 拓展阅读
QV3 embedding 模型知道最近发生的事吗?我能把生成的 embeddings 公开分享吗?深挖·拓展低频
知识截止 数据归属
⏱️ 现行
两个常被问到的边界问题。其一,知识时效text-embedding-3-largetext-embedding-3-small 缺乏 2021 年 9 月之后事件的知识;不过文档也指出,相比文本生成模型,这个限制对 embedding 场景通常影响没那么大,只在某些边缘情况下会降低表现——因为 embedding 主要编码的是语义相关性而非最新事实。其二,数据归属:可以把 embeddings 在线分享,客户拥有对模型的输入和输出(包括 embeddings 这种情形);但你有责任确保输入到 API 的内容不违反适用法律或 OpenAI 的使用条款(Terms of Use)。这两点在合规和产品设计(比如缓存/公开向量数据集)时需要提前想清楚。
术语 September 2021(V3 模型知识截止时间); customers own their input and output(客户拥有输入与输出,含 embeddings); Terms of Use(需遵守的使用条款)
📖 "No, the text-embedding-3-large and text-embedding-3-small models lack knowledge of events that occurred after September 2021." — 原文
📖 "Yes, customers own their input and output from our models, including in the case of embeddings." — 原文
🧪 实例 决策要点速记:
text
- 涉及"2021-09 之后的新事件/新实体"检索 → 语义相关性通常够用,但极端情况可能掉点
- 想公开分享向量数据集       → 允许(数据归客户),但需自查是否违反法律/Terms of Use
🔍 追问 知识截止对语义检索为什么影响相对小? → embedding 编码的是文本间语义关系,即便不"认识"某个新事件名,相近语义仍能被匹配;文档称其一般不像生成模型那样构成大限制。
📚 拓展阅读
🔥高频

检索与 File Search RAG

原文 原文 原文
Qfile_search 工具在 Responses API 里是如何工作的?它是托管的吗?返回哪些输出?深挖·拓展🔥高频
file_search Responses API hosted-tool
⏱️ 现行
file_search 是 Responses API 里的一个内置(hosted)工具,它让模型能在你事先上传的文件构成的知识库(vector_stores)里做语义 + 关键词检索,用外部知识增补模型自身的固有知识。关键点在于它是"托管"的:你不需要在自己这边写任何执行检索的代码,当模型判断需要时会自动调用该工具、从你的文件里检索、并把结果作为输出返回。使用前提是你已经建好 vector store 并上传了文件。调用后你会拿到多段输出:一个 file_search_call 项(带这次检索调用的 id),以及一个 message 项(模型据检索内容生成的回答,并附带 file_citation 引用标注)。这种设计的权衡在于:把 chunk/embed/index/检索/排序全部交给 OpenAI 托管,换取极简的集成成本,但也因此对底层 chunk、ranker 等只暴露有限的可调参数。
术语 file_search(Responses API 的托管检索工具); vector_stores(可检索文件的容器/知识库); file_search_call(检索调用输出项,含调用 id); file_citation(回答里指向来源文件的引用标注)
📖 "It enables models to retrieve information in a knowledge base of previously uploaded files through semantic and keyword search." — 原文
📖 "This is a hosted tool managed by OpenAI, meaning you don't have to implement code on your end to handle its execution." — 原文
🧪 实例file_search 工具连同要检索的 vector store 传入 Responses API:
python
from openai import OpenAI
client = OpenAI()

response = client.responses.create(
    model="gpt-5.6",
    input="What is deep research by OpenAI?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": ["<vector_store_id>"]
    }]
)
print(response)

返回的 output 里第一项是 file_search_call,第二项是带 annotations(file_citation)的 message
🔍 追问 模型每次回答都一定会触发 file_search 吗? → 不一定,它由模型自主决定何时调用;若要在测试/评测里强制走检索,可在请求里设 tool_choice="required"(见 cookbook 的评测代码)。
📚 拓展阅读
Q什么是 vector store?往里加一个文件后底层发生了什么?深挖·拓展🔥高频
vector-store chunk embed index
⏱️ 现行
vector store 是给 Retrieval API 和 file search 工具提供语义检索能力的容器,充当你数据的索引。当你把一个文件加入 vector store 时,它会被自动切块(chunked)、嵌入(embedded)、建索引(indexed)——这三步都由平台托管完成,你不用自己跑 embedding 或维护向量数据库。vector store 里装的是 vector_store_file 对象,它由一个底层 file 对象支撑:file 是通过 Files API 上传的原始内容,vector_store.file 则是被切块嵌入、并关联到某个 vector store 的包装类型,还带一个可用于过滤的 attributes 字典。计费上按所有 vector store 里"解析后 chunk + 对应 embedding"占用的总存储量收费,1 GB 以内免费,超出按 $0.10/GB/day;可用 expires_after 过期策略在闲置后自动删除以省钱。加文件是异步的,可用 create_and_poll 之类的 helper 阻塞到完成。
术语 vector_store(可检索文件的索引容器); vector_store_file / vector_store.file(被切块嵌入并关联到 store 的文件包装,含 attributes); file(经 Files API 上传的原始内容对象); create_and_poll(阻塞直到异步文件处理完成的 helper)
📖 "When you add a file to a vector store it will be automatically chunked, embedded, and indexed." — 原文
📖 "Vector stores contain vector_store_file objects, which are backed by a file object." — 原文
🧪 实例 建 store 并上传文件(带阻塞轮询):
python
from openai import OpenAI
client = OpenAI()

vector_store = client.vector_stores.create(        # Create vector store
    name="Support FAQ",
)

client.vector_stores.files.upload_and_poll(        # Upload file
    vector_store_id=vector_store.id,
    file=open("customer_policies.txt", "rb")
)
🔍 追问 从 vector store 删掉一个文件后,检索还会命中它吗? → 会有一小段时间:移除是最终一致的(eventually consistent),短时间内检索结果可能仍包含被删文件的内容。
📚 拓展阅读
Q语义检索(semantic search)和关键词检索有什么本质区别?它靠什么工作?深挖·拓展🔥高频
semantic-search embeddings cosine
⏱️ 现行
语义检索是一种利用 vector embeddings 来浮现"语义相关"结果的技术,重点在于它能命中那些和查询"共享很少甚至没有关键词"的结果——而这正是传统关键词检索会漏掉的。文档给的例子很直观:查询 "When did we go to the moon?",句子 "The first lunar landing occurred in July of 1969." 关键词相似度是 0%(一个词都不重叠),但语义相似度高达 65%,反而是最相关的结果;而 "When I ate the moon cake, it was delicious." 关键词重叠 40% 却语义只有 28%。这里关键词用 Jaccard 衡量、语义用 text-embedding-3-small 的 cosine 相似度衡量。这种"不靠字面重叠"的灵活性,使语义检索非常适合查询任意规模的知识库。你用自然语言 querysearch,返回一组结果,每条带相关 chunk、相似度分数和来源文件。
术语 semantic search(语义检索,靠 embedding 找语义相近结果); vector embeddings(把文本映射为向量以计算相似度); text-embedding-3-small(示例中算语义相似度所用的嵌入模型); cosine / Jaccard(分别衡量语义相似度与关键词相似度)
📖 "Importantly, this includes results with few or no shared keywords, which classical search techniques might miss." — 原文
📖 "Notice how the most relevant result contains none of the words in the search query." — 原文
🧪 实例 直接对 vector store 做语义检索,拿回带分数与来源的结果:
python
results = client.vector_stores.search(
    vector_store_id=vector_store.id,
    query="How many woodchucks are allowed per passenger?",
)

返回结构里每条含 file_idfilenamescore(如 0.85)、attributescontent。默认最多返回 10 条,可用 max_num_results 提到最多 50。
🔍 追问 有些查询表述效果不好,平台能自动优化吗? → 能,search 时设 rewrite_query=true 会自动改写查询以获得更优结果,改写后的查询会出现在结果的 search_query 字段(如 "I'd like to know the height of the main office building." → "primary office building height")。
📚 拓展阅读
Q文件是怎么被切块(chunking)的?默认参数和上下限是多少?为什么要 overlap?深挖·拓展🔥高频
chunking max_chunk_size_tokens chunk_overlap_tokens
⏱️ 现行
默认情况下 max_chunk_size_tokens800chunk_overlap_tokens400,也就是每个文件被切成 800-token 的 chunk,相邻 chunk 之间有 400-token 的重叠。设置 overlap 是为了避免把一个完整语义单元从中间切断——重叠让跨越切块边界的信息在至少一个 chunk 里保持完整,从而减少检索时的召回损失。你可以在加文件时通过 chunking_strategy 调整,但有约束:max_chunk_size_tokens 必须在 100 到 4096(含)之间;chunk_overlap_tokens 必须非负且不超过 max_chunk_size_tokens / 2。权衡在于:chunk 越大,单块携带的上下文越多但语义越"稀释"、命中越粗;chunk 越小则更精准但可能割裂上下文、且索引条目更多。此外单文件上限为 512 MB 且每文件不超过 5,000,000 tokens;对 text/ MIME 类型编码须是 utf-8、utf-16 或 ascii 之一。
术语 max_chunk_size_tokens(单 chunk 最大 token 数,默认 800,范围 100–4096); chunk_overlap_tokens(相邻 chunk 重叠 token 数,默认 400,≤ max/2); chunking_strategy(加文件时自定义切块策略的参数)
📖 "By default, max_chunk_size_tokens is set to 800 and chunk_overlap_tokens is set to 400, meaning every file is indexed by being split up into 800-token chunks, with 400-token overlap between consecutive chunks." — 原文
📖 "chunk_overlap_tokens must be non-negative and should not exceed max_chunk_size_tokens / 2." — 原文
🧪 实例 批量加文件时给某个文件覆盖切块策略:
python
client.vector_stores.file_batches.create_and_poll(
    vector_store_id="vs_123",
    files=[
        {
            "file_id": "file_456",
            "chunking_strategy": {
                "type": "static",
                "max_chunk_size_tokens": 1200,
                "chunk_overlap_tokens": 200
            }
        }
    ]
)
🔍 追问 大规模灌数据时应逐个 create 还是用 batch? → 推荐用 batch:一个请求最多可含 500 个文件,通常能降低争用、改善端到端延迟;注意 /vector_stores/{id}/files/file_batches 共享每个 vector store 300 请求/分钟 的限流。
📚 拓展阅读
Q检索结果不够相关时怎么调优?ranking_options 和 hybrid search 各控制什么?深挖·拓展中频
ranking_options hybrid-search RRF score_threshold
⏱️ 现行
当 file search 结果不够相关时,可以通过 ranking_options 调优质量。它包括指定 ranker(如 autodefault-2024-08-21),以及设一个 0.0–1.0 之间的 score_threshold:阈值越高,结果被限制在越相关的 chunk,但也可能把一些本来有用的 chunk 排除掉——这就是 precision/recall 的取舍。更进一步,当提供了 ranking_options.hybrid_search 时,你还能调 hybrid_search.embedding_weight(rrf_embedding_weight)和 hybrid_search.text_weight(rrf_text_weight),来控制 reciprocal rank fusion(RRF,倒数排名融合)如何在"语义 embedding 匹配"与"稀疏关键词匹配"之间做平衡:调大前者强调语义相似,调大后者强调字面重叠,且至少要有一个权重大于零。cookbook 里也印证了这一点:standalone 向量检索返回的每条结果分数(如 0.98、0.95……)由使用 hybrid search 的 ranker 计算。
术语 ranking_options(排序调优入口); ranker(排序器,如 auto / default-2024-08-21); score_threshold(0.0–1.0 相关度阈值,越高越严); hybrid search / RRF(语义与关键词两路结果的倒数排名融合,靠 embedding_weight/text_weight 加权)
📖 "When ranking_options.hybrid_search is provided you can also tune hybrid_search.embedding_weight (rrf_embedding_weight) and hybrid_search.text_weight (rrf_text_weight) to control how reciprocal rank fusion balances semantic embedding matches vs. sparse keyword matches." — 原文
📖 "They all have different relevancy score that are calculated by our ranker which uses hybrid search." — 原文
🧪 实例 cookbook 里 standalone 检索按分数排序输出,直观看到 ranker 打分:
python
for result in search_results.data:
    print(str(len(result.content[0].text)) + ' of character of content from ' + result.filename + ' with a relevant score of ' + str(result.score))
# ... with a relevant score of 0.9813588865322393
# ... with a relevant score of 0.9522476825143714
flowchart LR
    Q[query] --> E[语义 embedding 匹配]
    Q --> K[稀疏关键词匹配]
    E -->|embedding_weight| RRF[Reciprocal Rank Fusion]
    K -->|text_weight| RRF
    RRF --> R[按 score 排序 · score_threshold 过滤]
🔍 追问 提高 score_threshold 一定更好吗? → 不一定,它会把结果收窄到更相关的 chunk,但可能排除掉一些潜在有用的 chunk,属于典型的相关性 vs 召回的取舍,需按业务调。
📚 拓展阅读
Q如何用元数据/属性(attributes)过滤检索范围?有哪些过滤器和限制?深挖·拓展中频
attributes attribute-filter metadata-filtering
⏱️ 现行
每个 vector_store.file 可带一个 attributes 字典(如 region、category、date),在做语义检索时用 attribute_filter 先按属性把候选文件筛掉,再在缩小后的集合上做语义检索——这能把搜索限定到比如某个日期区间或某个地区。过滤器分两类:comparison filter 用 eq/ne/gt/gte/lt/lte/in/nin 把文件 attributes 里某个 key 与给定 value 比较;compound filter 用 and/or 把多个过滤器组合起来,可以嵌套构造相当复杂的逻辑。attributes 字典有硬限制:最多 16 个 key,每个值上限 256 字符。在 file search 工具侧,这个能力以 filters 参数暴露,直接写进 tool 定义里即可。这样做的好处是把"结构化精确筛选"和"非结构化语义匹配"结合起来,既保证命中范围正确、又不牺牲语义召回。
术语 attributes(文件上的键值元数据,≤16 key / ≤256 字符); attribute_filter / filters(检索前的属性过滤条件); comparison filter(eq/ne/gt/gte/lt/lte/in/nin 单条比较); compound filter(and/or 组合多条)
📖 "Attribute filtering helps narrow down results by applying criteria, such as restricting searches to a specific date range." — 原文
📖 "The dictionary can have at most 16 keys, with a limit of 256 characters each." — 原文
🧪 实例 在 file search 工具里用 filters 只检索 category 属于 blog/announcement 的文件:
python
response = client.responses.create(
    model="gpt-5.6",
    input="What is deep research by OpenAI?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": ["<vector_store_id>"],
        "filters": {
            "type": "in",
            "key": "category",
            "value": ["blog", "announcement"]
        }
    }]
)
🔍 追问 复合过滤能嵌套到多深? → 文档给的复杂示例里 orand 再套 or(如按 project_code + confidentiality + language 组合筛选),即 compound filter 内的 filters 数组可以继续放 compound filter,支持任意嵌套组合。
📚 拓展阅读
Qfile search 里怎么控制返回结果数量和拿到检索明细?各有什么代价?深挖·拓展中频
max_num_results include token-latency
⏱️ 现行
用 file search 工具时可以自定义从 vector store 检索回来的结果数量 max_num_results,这能同时降低 token 用量和延迟,但可能以答案质量下降为代价——因为给模型的上下文少了。默认一次响应最多返回 10 条结果,可用 max_num_results 最多提到 50。另一方面,虽然你能在输出文本里看到 annotations(指向文件的引用),但 file search 调用默认不会把检索到的原始 chunk 明细返回;要拿到这些明细,需在创建响应时用 include 参数(include=["file_search_call.results"])显式索取。这套设计把"给模型多少上下文"和"给开发者暴露多少检索内幕"解耦:前者影响成本与质量的平衡,后者用于调试、评估召回质量或做二次分析,两者都按需开启以免默认就付出额外 token/带宽。
术语 max_num_results(限制检索返回条数,默认 10、上限 50); include(响应级参数,索取额外内容如 file_search_call.results); annotations(输出文本里指向来源文件的引用,默认就有)
📖 "This can help reduce both token usage and latency, but may come at the cost of reduced answer quality." — 原文
📖 "While you can see annotations (references to files) in the output text, the file search call will not return search results by default." — 原文
🧪 实例 限制结果数以省 token/降延迟,并要求返回检索明细:
python
response = client.responses.create(
    model="gpt-5.6",
    input="What is deep research by OpenAI?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": ["<vector_store_id>"],
        "max_num_results": 2
    }],
    include=["file_search_call.results"]
)
🔍 追问 在 standalone Retrieval API 的 search 里也是同一个上限吗? → 是,search 默认也是最多 10 条结果,同样可用 max_num_results 设到最多 50。
📚 拓展阅读
Q为什么说 file search 简化了传统 RAG?它把哪些步骤合并了?深挖·拓展中频
RAG simplification single-API-call
⏱️ 现行
传统 RAG 流程冗长:解析 PDF、定义切块策略、把 chunk 上传到存储、对这些文本 chunk 跑 embedding、把 embedding 存进向量数据库——而这还只是"搭建"阶段;真正在 LLM 工作流里检索内容又要多步。file search 作为 Responses API 里的托管工具正是来解决这个的:它让你直接搜索自己的知识库并基于检索到的内容生成答案。cookbook 的结论说得很直接:用 Responses 的 file search,你可以简化 RAG 架构,在单次 API 调用里完成——文件存储、embedding、检索全部集成进这一个工具。相比"先查 vector store 再把数据塞进 Responses/Chat Completion 调用",用 file_search 工具作为 Responses API 的一部分是更便捷的方式。权衡是:你放弃了对切块/嵌入模型/向量库的完全自主控制,换来的是极低的工程复杂度和一次调用即得的 grounded 答案(含引用)。
术语 RAG(检索增强生成); file search(把存储/嵌入/检索合并的托管工具); single API call(一次 Responses 调用内完成检索+生成); grounded answer(基于检索内容、带 citation 的回答)
📖 "This is where file search — a hosted tool you can use in the Responses API — comes in. It allows you to search your knowledge base and generate an answer based on the retrieved content." — 原文
📖 "By using file search with Responses, you can simplify RAG architecture and leverage this in a single API call using the new Responses API. File storage, embeddings, retrieval all integrated in one tool!" — 原文
🧪 实例 一次调用即完成 RAG(gpt-4o-mini 自动走 file_search 并给出带来源的答案):
python
query = "What's Deep Research?"
response = client.responses.create(
    input= query,
    model="gpt-4o-mini",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [vector_store_details['id']],
    }]
)
annotations = response.output[1].content[0].annotations
retrieved_files = set([result.filename for result in annotations])
print(f'Files used: {retrieved_files}')
flowchart LR
    subgraph 传统RAG[传统 RAG · 多步]
      P[解析PDF] --> C[切块] --> U[上传存储] --> Emb[跑embedding] --> V[存向量库] --> Ret[检索] --> G1[生成]
    end
    subgraph FS[file search · 单次调用]
      Q2[Responses.create + file_search] --> A2[grounded 答案 + 引用]
    end
🔍 追问 单次调用后如何知道它用了哪些文件? → 从 response.output[1].content[0].annotationsfile_citation,再取其中的 filename 去重,就能得到本次回答实际引用的文件集合。
📚 拓展阅读
Q如何评估检索质量?cookbook 用了哪些指标?为什么说这种评测是"不完美"的?深挖·拓展低频
evaluation MRR MAP recall-precision
⏱️ 现行
对信息检索系统而言,关键还要衡量为答案检索到的文件的相关性与质量。cookbook 的做法是:用 gpt-4o 对每个 PDF 生成一个"只能由该文档回答"的问题,构成 filename→question 的评测集,再逐题走 file_search(用 tool_choice="required" 强制检索),看期望文件是否出现在 top-k 检索结果里,并计算 Recall@k、Precision@k、Mean Reciprocal Rank(MRR)、Mean Average Precision(MAP)。在 k=5 的实测里得到 Recall@5=0.9048、MRR=0.9048、MAP=0.8954。之所以说"不完美":生成的问题里有些过于泛化(例如"文档主要利益相关者说了什么"),检索系统很难判断该问题究竟对应哪个文档——例如"What is OpenAI's mission..."期望命中 Disrupting malicious uses of AI,却检索到了别的文档。因此文档反复建议:对自己的真实用例,始终应准备一份人工核验过的评测集,这里的自动生成方法只是给你一套可复用的评测方法论。
术语 Recall@k / Precision@k(top-k 内是否命中期望文件); MRR(Mean Reciprocal Rank,命中排名的倒数均值); MAP(Mean Average Precision,平均精度的均值); tool_choice="required"(评测时强制触发 file_search)
📖 "What is key for those information retrieval system is to also measure the relevance & quality of files retrieved for those answers." — 原文
📖 "Recall & Precision are at 1 for this example, and our file ranked first so we're having a MRR and MAP = 1 on this example." — 原文
🧪 实例 逐题跑检索并算 rank / average precision(节选):
python
retrieved_files = [result.filename for result in annotations[:k]]
if expected_filename in retrieved_files:
    rank = retrieved_files.index(expected_filename) + 1
    rr = 1 / rank
    correct = True
else:
    rr = 0
    correct = False

最终汇总输出:Recall@5: 0.9048Precision@5: 0.9048Mean Reciprocal Rank (MRR): 0.9048Mean Average Precision (MAP): 0.8954
🔍 追问 为什么评测里要设 tool_choice="required"? → 因为评测的正是检索本身,强制触发 file_search 可确保每题都真的走了检索(虽非必需,但更严谨),避免模型跳过检索直接答导致指标失真。
📚 拓展阅读

第7章 · 规模化、运维与迁移

🔥高频

速率限制、错误处理与生产最佳实践

原文 原文 原文 原文
QOpenAI 的 rate limit 到底按什么维度计量?为什么"token 还没用完却先被限流"?深挖·拓展🔥高频
rate-limits RPM TPM
⏱️ 现行
Rate limit 不是单一维度,而是一组同时生效的计量指标:RPM(每分钟请求数)、RPD(每天请求数)、TPM(每分钟 token 数)、TPD(每天 token 数)、IPM(每分钟图片数)以及某些流式音频模型的 audio minutes per minute。关键机制在于"谁先触顶就限谁"——任意一个维度先被打满就会触发限流,而不需要所有维度都超标。文档给的经典例子:如果你的 RPM 是 20,那么发 20 个每个只有 100 token 的请求就会把 RPM 打满,即便这 20 个请求加起来远没达到 150k 的 TPM。这解释了为什么很多人"明明 token 预算很足却被 429"——因为触发的是请求数维度而非 token 维度。此外 limit 的作用域是 organization 级和 project 级(不是 user 级)、随模型不同而不同,一些模型族还共享同一份 limit;Batch API 的队列限额则按排队中的输入 token 总量计,任务完成后其 token 就不再计入。理解这套多维计量是做容量规划和限流对策的前提。
术语 RPM(requests per minute,每分钟请求数); TPM(tokens per minute,每分钟 token 数); RPD/TPD(每天请求/token 数); organization level(限额作用在组织与项目级而非用户级); shared limit(同一族模型共享一份限额)
📖 "Rate limits use metrics such as RPM (requests per minute), RPD (requests per day), TPM (tokens per minute), TPD (tokens per day), IPM (images per minute), and audio minutes per minute for some streaming audio models. Rate limits can be hit across any of the options depending on what occurs first." — 原文
📖 "For example, you might send 20 requests with only 100 tokens to the ChatCompletions endpoint and that would fill your limit (if your RPM was 20), even if you didn't send 150k tokens (if your TPM limit was 150k) within those 20 requests." — 原文
🧪 实例 触发限流的最小复现——在循环里连发 100 个短请求,很快就会在请求数维度上撞到 RPM:
python
# request a bunch of completions in a loop
for _ in range(100):
    client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "Hello"}],
        max_tokens=10,
    )
flowchart TD
    A[发起请求] --> B{RPM 已满?}
    B -->|是| E[429 限流]
    B -->|否| C{TPM 已满?}
    C -->|是| E
    C -->|否| D[正常处理]
🔍 追问 rate limit 是按用户算的吗? → 不是,文档明确 limit 定义在 organization 与 project 级别,不是 user 级;所以同组织内其他人的用量也会吃掉你的额度。
📚 拓展阅读
Q如何用 exponential backoff 优雅地扛住 rate limit 错误?为什么要加随机 jitter?深挖·拓展🔥高频
retry exponential-backoff jitter
⏱️ 现行
最直接的对策是命中 rate limit 错误后自动"带随机指数退避地重试":先短暂 sleep 再重发失败请求,如果仍失败就把 sleep 时长翻倍(乘以 exponential_base)后再试,如此反复直到成功或达到最大重试次数。它的三点收益构成了取舍逻辑:自动重试让你从限流中恢复而不崩溃、不丢数据;指数退避让前几次重试可以快速试探,同时在连续失败时享受越来越长的等待;而随机 jitter 的作用是打散重试时刻,避免大量客户端在同一瞬间集中重发(惊群/thundering herd)。一个必须记住的坑:失败的请求同样计入你的每分钟额度,所以"不退避、死循环猛重发"只会雪上加霜。生产上通常用 Tenacity 或 backoff 这类第三方库的装饰器实现(OpenAI 声明对其可靠性/安全性不作保证),不想引第三方也可以手写退避逻辑。
术语 exponential backoff(指数退避,失败后按指数增长 sleep 时长); jitter(随机抖动,打散重试时刻防惊群); wait_random_exponential(Tenacity 的随机指数等待函数); stop_after_attempt(达到最大尝试次数即停止)
📖 "One easy way to avoid rate limit errors is to automatically retry requests with a random exponential backoff. Retrying with exponential backoff means performing a short sleep when a rate limit error is hit, then retrying the unsuccessful request. If the request is still unsuccessful, the sleep length is increased and the process is repeated. This continues until the request is successful or until a maximum number of retries is reached." — 原文
📖 "Adding random jitter to the delay helps retries from all hitting at the same time." — 原文
📖 "Note that unsuccessful requests contribute to your per-minute limit, so continuously resending a request won’t work." — 原文
🧪 实例 用 Tenacity 的 tenacity.retry 装饰器加随机指数退避,最多试 6 次、单次等待在 1~60 秒随机指数增长:
python
from tenacity import (
    retry,
    stop_after_attempt,
    wait_random_exponential,
)  # for exponential backoff

@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def completion_with_backoff(**kwargs):
    return client.chat.completions.create(**kwargs)


completion_with_backoff(model="gpt-4o-mini", messages=[{"role": "user", "content": "Once upon a time,"}])
🔍 追问 手写退避时 delay 怎么增长? → cookbook 的实现是 delay *= exponential_base * (1 + jitter * random.random()),即每次失败把 delay 乘以退避基数并叠加一段随机抖动,再 time.sleep(delay)
📚 拓展阅读
Q429 有哪两种截然不同的含义?各类 API error code 怎么区分处理?深挖·拓展🔥高频
error-codes 429 quota
⏱️ 现行
同样是 429,含义完全不同,处理方式也不同。第一种是 "Rate limit reached for requests"——你发得太快,超过了分配的速率限额,解法是放慢节奏(pace your requests)、加退避重试;这类错误一分钟后额度会重置。第二种是 "You exceeded your current quota"——你把当月的 usage limit(或预付信用点)用光了,这跟发送速率无关,再怎么退避重试也没用,只能去买更多信用点或提高 usage limit。除 429 外,常见的还有:401(鉴权失败/API key 不对/不在组织内/IP 不在白名单)、403(国家地区不支持)、500(服务端出错,稍后重试并看 status page)、503(服务过载或 "Slow Down" 限流)。Python 库层面则把这些映射为带类型的异常:RateLimitErrorAuthenticationErrorBadRequestErrorAPIConnectionErrorAPITimeoutErrorInternalServerError 等,官方建议用 try/except 按异常类型分别处理,对 RateLimitError 专门上退避。区分"限速"与"配额耗尽"是面试常考点,因为二者的补救动作根本相反。
术语 429 rate limit(发送太快,可退避重试,一分钟后重置); 429 quota exceeded(月度用量/信用点耗尽,重试无用需充值提额); RateLimitError(库层对应的异常类型); 503 Slow Down(请求量骤增触发的临时节流)
📖 "This error message indicates that you have hit your assigned rate limit for the API. This means that you have submitted too many tokens or requests in a short period of time and have exceeded the number of requests allowed." — 原文
📖 "This error message indicates that you hit your monthly usage limit for the API, or for prepaid credits customers that you've consumed all your credits." — 原文
🧪 实例 官方推荐的分类型错误处理骨架——对 RateLimitError 单独走退避:
python
import openai
from openai import OpenAI
client = OpenAI()

try:
  #Make your OpenAI API request here
  response = client.responses.create(
    model="gpt-5.6",
    input="Hello world"
  )
except openai.APIError as e:
  #Handle API error here, e.g. retry or log
  print(f"OpenAI API returned an API Error: {e}")
  pass
except openai.APIConnectionError as e:
  #Handle connection error here
  print(f"Failed to connect to OpenAI API: {e}")
  pass
except openai.RateLimitError as e:
  #Handle rate limit error (we recommend using exponential backoff)
  print(f"OpenAI API request exceeded rate limit: {e}")
  pass
🔍 追问 遇到 "503 Slow Down" 该怎么办? → 文档说这是请求速率骤增触发的临时节流,应把请求速率降回原水平、保持稳定至少 15 分钟,再逐步爬升;流量平稳时几乎不会碰到此错误。
📚 拓展阅读
Q怎样从 HTTP 响应头实时读到剩余额度?有哪些 x-ratelimit-* 字段?深挖·拓展中频
rate-limits headers observability
⏱️ 现行
除了在账户页看限额,每个 HTTP 响应头里都会带回一组 x-ratelimit-* 元数据,可用于运行时自适应限流。关键字段成对出现:x-ratelimit-limit-requests/x-ratelimit-limit-tokens 是请求与 token 的上限;x-ratelimit-remaining-requests/x-ratelimit-remaining-tokens 是当前还剩多少(样例值如 remaining-requests=59、remaining-tokens=149984);x-ratelimit-reset-requests/x-ratelimit-reset-tokens 是各自额度重置回初始状态所需的时间(如 1s、6m0s)。当有 project 级 token 限额生效时,还会额外出现 x-ratelimit-limit-project-tokensx-ratelimit-remaining-project-tokensx-ratelimit-reset-project-tokens 三个 project-scoped 字段。生产上的用法是:读 remaining 字段做主动节流(逼近但不撞上限)、读 reset 字段决定退避多久,比盲目重试更省额度。
术语 x-ratelimit-remaining-requests(剩余可用请求数); x-ratelimit-remaining-tokens(剩余可用 token 数); x-ratelimit-reset-tokens(token 额度重置的倒计时,如 6m0s); project-tokens(project 作用域的 token 限额头,按需出现)
📖 "In addition to seeing your rate limit on your account page, you can also view important information about your rate limits such as the remaining requests, tokens, and other metadata in the headers of the HTTP response." — 原文
📖 "Project-token headers may be present when a project-scoped token limit applies." — 原文
🧪 实例 文档给出的响应头样例(节选)——用它判断"还能再发多少、多久后回满":
text
x-ratelimit-limit-requests           60
x-ratelimit-limit-tokens             150000
x-ratelimit-remaining-requests       59
x-ratelimit-remaining-tokens         149984
x-ratelimit-reset-requests           1s
x-ratelimit-reset-tokens             6m0s
🔍 追问 reset 字段表示什么时间? → 文档定义 x-ratelimit-reset-requests/-tokens 是"该维度的限额重置回初始状态所需的时间",可据此决定退避等待时长而非硬猜。
📚 拓展阅读
Q为什么调小 max_tokens 能减少被限流?rate limit 用量是怎么估算的?深挖·拓展中频
max_tokens token-accounting
⏱️ 现行
因为 rate limit 的 token 用量不是等你生成完才算,而是"预扣":它取 max_tokens 与根据请求字符数估算出的输入 token 数二者中的较大值来计入 TPM。这意味着如果你把 max_tokens 设得远大于实际回复长度,用量就会被高估,即使真实回复很短也一样,于是你会过早地撞上限流、白白浪费额度。对策就是把 max_tokens 尽量贴近你预期的回复大小,让用量估算更准确、避免非必要的节流。这是一个低成本高收益的调优点——不改架构,只是把参数设合理。
术语 max_tokens(单次回复允许的最大 token 数,同时决定用量预扣的上界); token accounting(用量按 max_tokens 与输入估算取较大值计入 TPM); overestimated(max_tokens 过大导致用量被高估从而过早限流)
📖 "Your rate limit is calculated as the maximum of max_tokens and the estimated number of tokens based on the character count of your request. Try to set the max_tokens value as close to your expected response size as possible." — 原文
📖 "If you set max_tokens too high, your usage can be overestimated, even if the actual response is much shorter." — 原文
🧪 实例 显式传入贴合预期的 max_tokens,让用量估算更精准:
python
def completions_with_max_tokens(**kwargs):
    return client.chat.completions.create(**kwargs)


completions_with_max_tokens(model="gpt-4o-mini", messages=[{"role": "user", "content": "Once upon a time,"}], max_tokens=100)
🔍 追问 除了压 max_tokens,还有什么能减少生成 token 从而降延迟/降用量? → production 指南建议加 stop sequences 提前截断、降低 nbest_of 以少生成候选;nbest_of >1 时生成 token 量约为 max_tokens * max(n, best_of)
📚 拓展阅读
QRPM 打满但 TPM 还有余量时该怎么提吞吐?batching 有什么代价?深挖·拓展中频
batching RPM TPM throughput
⏱️ 现行
OpenAI 对"每分钟/每天请求数(RPM/RPD)"和"每分钟 token 数(TPM)"是分别限额的。所以当你撞的是 RPM 上限、但 TPM 还有富余时,正确做法是把多个任务打包进同一个请求(batching),从而用更少的请求发更多 token——尤其对较小的模型能显著提升单位时间处理量。发一批 prompt 和普通调用几乎一样,只是把传给 prompt 参数的单个字符串换成一组字符串(list of strings)。但 batching 不是免费的,要权衡三点:每个模型单请求有最大 token 上限,批过大会失败或被截断;为了凑成一批可能引入等待,对时延敏感的应用会伤体验;批量返回时响应的顺序/格式未必与提交顺序一致,需要后处理把每个响应对回它的 prompt。若用例本身不需要同步响应,更省心的是直接用 Batch API,它不占用同步请求的速率额度。
术语 batching(把多个任务合并进一个请求以减少请求数); RPM vs TPM(请求数与 token 数是两套独立限额); Batch API(异步批处理,不吃同步速率额度); Structured Outputs(用严格 schema 让一次请求返回多个结果,减少解析负担)
📖 "The OpenAI API enforces separate limits for requests per minute/day (RPM/RPD) and tokens per minute (TPM). If you’re hitting RPM limits but still have available TPM capacity, consider batching multiple tasks into each request." — 原文
📖 "If you're hitting the limit on requests per minute but have available capacity on tokens per minute, you can increase your throughput by batching multiple tasks into each request." — 原文
🧪 实例 cookbook 用 Structured Outputs 把 10 个 prompt 打进一个请求,并用严格 schema 约束返回结构:
python
from pydantic import BaseModel

# Define the Pydantic model for the structured output
class StoryResponse(BaseModel):
    stories: list[str]
    story_count: int
🔍 追问 batching 一定更快吗? → 不一定。文档提醒:每个模型单请求都有最大 token 上限,批过大会失败或被截断;而且为凑批把任务攒到一起引入的等待会伤及时延敏感场景。
📚 拓展阅读
Q主模型被限流时"降级到备用模型"是好策略吗?有什么坑?深挖·拓展中频
fallback shared-limits resilience
⏱️ 现行
当主模型报 rate limit 时,切到一个次级模型可以让应用保持可用、不至于卡死。但这不是万能药,要权衡几件事:备用模型在准确率、延迟、成本上可能差异很大,对结果一致性要求高的场景未必适用;更隐蔽的坑是——有些模型之间共享同一份 rate limit,这时"换个模型"根本换不出额度,降级就失效了(你可以在组织的 limit 页面看到哪些模型共享限额)。所以官方强调:上线前要充分测试降级对输出质量、用户体验和成本预算的影响,并用相关的 evaluation 验证备用方案在真实条件下仍达标。实现上就是 try 主模型、except 到 RateLimitError 时改 kwargs['model'] 为备用模型再发一次。
术语 fallback model(主模型被限流时切换的次级模型); shared rate limits(部分模型共用一份限额,降级可能无效); evaluations(上线前用评估验证备用方案质量); accuracy/latency/cost trade-off(备用模型三方面可能显著不同)
📖 "If you encounter rate limit errors on your primary model, one option is to switch to a secondary model. This approach helps keep your application responsive when your primary model is throttled or unavailable." — 原文
📖 "Additionally, keep in mind that some models share rate limits, which may reduce the effectiveness of simply switching models." — 原文
🧪 实例 捕获 RateLimitError 后切换到 fallback 模型重发:
python
def completions_with_fallback(fallback_model, **kwargs):
    try:
        return client.chat.completions.create(**kwargs)
    except openai.RateLimitError:
        kwargs['model'] = fallback_model
        return client.chat.completions.create(**kwargs)
    
    
completions_with_fallback(fallback_model="gpt-4o", model="gpt-4o-mini", messages=[{"role": "user", "content": "Once upon a time,"}])
🔍 追问 上线前该验证什么? → 文档要求充分测试降级对输出质量、用户体验、运营预算的影响,并用相关 evaluation 确认在真实条件下仍满足性能要求。
📚 拓展阅读
Q大批量离线处理时,除了退避重试还能怎么把吞吐做满而不浪费额度?深挖·拓展中频
batch-processing throughput proactive-delay
⏱️ 现行
对实时用户请求,退避重试是兼顾低延迟与避免限流的好策略;但对吞吐比延迟更重要的大批量离线处理,单靠退避有个隐患:如果你不断"撞限流→退避→再撞→再退避",相当一部分请求预算会浪费在需要重试的请求上,反而压低了固定速率下的有效吞吐。一个主动式解法是:算出你的速率上限,给每个请求加一个等于其倒数的固定延迟——例如速率是 20 requests per minute,就给每个请求加 3–6 秒延迟(60/20=3 秒起),让你稳定运行在速率天花板附近却不撞顶、不产生被浪费的重试。这是"主动匀速"与"被动退避"的取舍:匀速插延迟牺牲一点峰值速度,换来几乎不浪费额度的稳定高吞吐。官方还提供了 api_request_parallel_processor.py 脚本,把流式读取、并发、请求与 token 双重限流、失败重试、错误日志都集成好了。
术语 proactive delay(主动给每个请求加固定延迟以逼近而不撞速率上限); reciprocal of rate limit(延迟取速率上限的倒数,如 20 RPM → ~3s); wasted requests(撞限流后被迫重试的请求,吞吐损耗来源); parallel processor(集成限流/重试/日志的批处理脚本)
📖 "Here, one potential solution is to calculate your rate limit and add a delay equal to its reciprocal (e.g., if your rate limit 20 requests per minute, add a delay of 3–6 seconds to each request). This can help you operate near the rate limit ceiling without hitting it and incurring wasted requests." — 原文
📖 "If you're processing real-time requests from users, backoff and retry is a great strategy to minimize latency while avoiding rate limit errors." — 原文
🧪 实例 按速率上限的倒数给每个请求插入固定延迟:
python
import time

# Define a function that adds a delay to a Completion API call
def delayed_completion(delay_in_seconds: float = 1, **kwargs):
    """Delay a completion by a specified amount of time."""

    # Sleep for the delay
    time.sleep(delay_in_seconds)

    # Call the Completion API and return the result
    return client.chat.completions.create(**kwargs)


# Calculate the delay based on your rate limit
rate_limit_per_minute = 20
delay = 60.0 / rate_limit_per_minute
🔍 追问 官方的并行处理脚本都做了哪些事? → 它从文件流式读取请求以防大作业爆内存、并发发请求最大化吞吐、对请求和 token 双重限流、重试失败请求避免丢数据、并记录错误便于诊断。
📚 拓展阅读
Q从原型走向生产,架构与运维上要防哪些坑(扩容/缓存/密钥/组织)?深挖·拓展低频
production scaling api-key-safety
⏱️ 现行
production 指南把上生产的关注点拆成几块。扩容架构上有四条常规手段:水平扩展(加服务器/容器分摊负载,前提是架构能处理多节点并有负载均衡)、垂直扩展(升单节点资源)、缓存(存高频数据,减少对 API 的重复调用以改善响应时间,但需设计缓存命中与失效逻辑)、负载均衡(用 LB 或 DNS round-robin 把请求均匀分发以减少瓶颈)。安全上,API key 是鉴权凭证但必须严防泄露:绝不要把 key 写进代码或公开仓库,而应放在安全位置、通过环境变量或密钥管理服务注入,避免硬编码。组织治理上,limit 定义在 organization 与 project 级,可为 staging 与 production 建独立 project 来隔离开发测试、限制生产项目的访问并设定各自的速率与花费上限;成员分 readers 与 owners 两类权限。此外还要规划成本(按 token×单价的框架优化)、MLOps(数据/模型管理、监控、再训练、部署)与安全合规。这些共同构成"能扛真实流量且不出事故"的生产就绪清单。
术语 horizontal/vertical scaling(横向加节点 / 纵向升单机资源); caching(缓存高频数据减少重复 API 调用); load balancing(负载均衡,LB 或 DNS round-robin 均匀分发); staging projects(为 staging/production 建独立 project 隔离并各设限额); readers/owners(组织成员的两类权限)
📖 "By storing frequently accessed data, you can improve response times without needing to make repeated calls to our API." — 原文
📖 "Avoid exposing the API keys in your code or in public repositories; instead, store them in a secure location." — 原文
🧪 实例 上生产前的就绪自检(源自 production 指南的分区):
text
[扩容]   水平扩展 + 负载均衡(LB / DNS round-robin);对高频数据加缓存
[密钥]   不写进代码/公开仓库;用环境变量或密钥管理服务注入
[组织]   staging 与 production 拆成独立 project,各设速率与花费上限
[成本]   按 token 数 × 单价框架优化;设 usage 通知阈值
[MLOps]  数据/模型管理、监控、再训练、部署自动化
🔍 追问 readers 和 owners 权限差在哪? → 两者都能发 API 请求;但只有 owners 能修改账单信息、并在组织内管理成员,readers 不具备这些管理权限。
📚 拓展阅读

第二部分 · OpenAI Cookbook 实战模式

第8章 · RAG 与向量检索实战

🔥高频

嵌入搜索与语义检索基础

原文 原文 原文
Q想让 GPT 回答训练数据之外的问题时,为什么优先用检索(Search-Ask)而不是微调?深挖·拓展🔥高频
RAG fine-tuning context-window
⏱️ 现行
GPT 学知识有两条路:改模型权重(fine-tune)或改模型输入(把知识塞进消息)。虽然微调看起来更"自然",但 cookbook 明确不推荐用它来"教知识",因为微调更适合教专门的任务或风格,对事实性回忆(factual recall)不可靠——原文用了一个精准的类比:模型权重像长期记忆,微调像考前一周复习,真到考场可能忘细节或记错从没读过的事;而把知识插进消息像短期记忆,像开卷考试,手边有笔记就更容易答对。检索方案的代价是每个模型一次能读的文本有上限(gpt-4o / gpt-4o-mini 都是 128,000 tokens,约 384 页),所以当参考资料远超上下文窗口时,就需要先检索出最相关的片段再喂给模型,也就是 Search-Ask。权衡上,检索让系统能"从整书架的资料里只翻几页笔记",既绕开知识截止日期(gpt-4o-mini 训练数据大致截止 2023 年 10 月),又比微调更新更便宜、更可控。
术语 model weights(模型权重,靠微调改写,类比长期记忆); model inputs(模型输入,把知识写进消息,类比短期记忆); factual recall(事实性回忆,微调在此不可靠); Search-Ask(先检索相关片段再提问的两步法)
📖 "Although fine-tuning can feel like the more natural option—training on data is how GPT learned all of its other knowledge, after all—we generally do not recommend it as a way to teach the model knowledge. Fine-tuning is better suited to teaching specialized tasks or styles, and is less reliable for factual recall." — 原文
📖 "In contrast, message inputs are like short-term memory. When you insert knowledge into a message, it's like taking an exam with open notes. With notes in hand, the model is more likely to arrive at correct answers." — 原文
🧪 实例 直接问 gpt-4o-mini「Who won the elections in the US in 2024?」,因为知识截止在 2023 年 10 月,模型答不出来;而把相关的 Wikipedia 文章贴进消息后再问,GPT 就能正确作答——这正是"开卷考试"效果的最小复现。
🔍 追问 那什么场景才该用微调? → 原文说微调更适合教"specialized tasks or styles"(专门任务或风格),而不是灌输可检索的事实知识。
📚 拓展阅读
QSearch-Ask 方法的完整流程分哪几步?深挖·拓展🔥高频
RAG pipeline embeddings
⏱️ 现行
整个系统分两大阶段。准备阶段(每篇文档一次):Collect 收集(下载几百篇 2022 冬奥会相关 Wikipedia 文章)→ Chunk 分块(把文档切成短的、基本自洽的片段)→ Embed 嵌入(用 OpenAI API 给每个片段算 embedding)→ Store 存储(小数据集存 CSV,大数据集用向量数据库)。查询阶段(每次提问一次):先对用户问题生成 query embedding,用 embedding 之间的距离把候选文本按相关度排序(Search);再把问题和最相关的若干片段拼进一条消息发给 GPT,返回答案(Ask)。这样设计的核心权衡是:GPT 调用比 embedding 检索贵得多,所以一个有一定查询量的系统,成本主要压在第 3 步 Ask 上——检索这一步只是廉价地筛出该读哪几页,真正花钱的是让 GPT 读并推理。
术语 Collect / Chunk / Embed / Store(准备阶段四步); query embedding(用户问题的向量); token_budget(Ask 阶段拼接上下文的 token 预算); Search / Ask(查询阶段两步)
📖 "This notebook demonstrates a two-step Search-Ask method for enabling GPT to answer questions using a library of reference text." — 原文
📖 "Because GPT models are more expensive than embeddings search, a system with a decent volume of queries will have its costs dominated by step 3." — 原文
🧪 实例
flowchart LR
  subgraph Prepare[准备阶段 · 每文档一次]
    A[Collect 下载文章] --> B[Chunk 分块]
    B --> C[Embed 生成向量]
    C --> D[Store CSV/向量库]
  end
  subgraph Query[查询阶段 · 每次提问]
    Q[用户问题] --> E[query embedding]
    E --> F[Search 按相关度排序]
    D --> F
    F --> G[Ask 拼接片段送 GPT]
    G --> H[返回答案]
  end
🔍 追问 成本大头为什么在 Ask 而不在检索? → 因为 embedding 检索远比 GPT 生成便宜,原文按 ~1000 tokens/query 估算 gpt-4o 约 \$0.0025/query、gpt-4o-mini 约 \$0.00015/query,查询量一上来成本就由 GPT 调用主导。
📚 拓展阅读
Q检索为什么选 embedding-based search,而不是词法(lexical)检索?深挖·拓展🔥高频
semantic-search embeddings retrieval
⏱️ 现行
cookbook 列了三类检索:基于词法(lexical)、基于图(graph)、基于嵌入(embedding),本例选 embedding-based。理由很直接:embedding 实现简单,而且特别适合处理"问题",因为问题往往和它的答案在字面上并不重叠——比如问「谁拿了冰壶金牌」,答案文本里未必出现"谁""拿了"这些词,词法检索容易漏,而语义向量能捕捉含义上的接近。原文同时强调这只是起点:更好的检索系统会组合多种方法,并叠加流行度、时效、用户历史、与既有结果的冗余、点击率等特征;还可以用 HyDE 这类技巧——先把问题变成一个"假设答案"再嵌入,或让 GPT 把问题改写成关键词/检索词来提升召回。所以权衡是:embedding 检索是低成本、高召回的默认基线,但生产系统通常在它之上做混合与重排。
术语 lexical-based search(词法检索,靠字面词重叠); embedding-based search(嵌入检索,靠语义向量距离); HyDE(先把问题转成假设答案再嵌入的技巧); redundancy(与既有检索结果的冗余,可作重排特征)
📖 "Embeddings are simple to implement and work especially well with questions, as questions often don't lexically overlap with their answers." — 原文
📖 "Q&A retrieval performance may also be improved with techniques like HyDE, in which questions are first transformed into hypothetical answers before being embedded." — 原文
🧪 实例 用「curling gold medal」作查询跑 strings_ranked_by_relatedness(..., top_n=5),返回按相关度打分的前 5 段 Wikipedia 文本——即使段落里没有逐字出现查询词,语义相近的冰壶奖牌段落也能排到前面。
🔍 追问 embedding 检索只是起点,生产上还会加什么? → 原文点名可组合多种检索方法并叠加 popularity、recency、user history、redundancy、click rate 等特征。
📚 拓展阅读
Q语义检索里如何用 embedding 把候选文本按相关度排序?深挖·拓展🔥高频
cosine-similarity ranking scipy
⏱️ 现行
核心函数 strings_ranked_by_relatedness 做四件事:用 OpenAI API 把用户 query 嵌成向量;对 DataFrame 里每一行文本,用相关度函数算它与 query 向量的相关度;按相关度降序排序;返回 top-N 文本及其分数。相关度函数默认是 1 - spatial.distance.cosine(x, y),即 cosine 相似度(1 减去 cosine 距离)——值越大越相关。这里的机制要点是:文档片段的 embedding 在准备阶段已离线算好并存好,查询时只需为 query 现算一个向量,再做纯向量运算比较,所以检索这一步既快又便宜。权衡是这份实现用 df.iterrows() 对全量行线性扫描,适合几千条这种小规模;数据量大时就该换向量数据库来做近似最近邻,而不是每次全表遍历。
术语 cosine similarity(余弦相似度,1 - spatial.distance.cosine); relatedness_fn(可替换的相关度函数参数); top_n(返回最相关的前 N 条); query_embedding(查询向量,查询时现算)
📖 "Uses distance between query embedding and text embeddings to rank the texts" — 原文
🧪 实例
python
# search function
def strings_ranked_by_relatedness(
    query: str,
    df: pd.DataFrame,
    relatedness_fn=lambda x, y: 1 - spatial.distance.cosine(x, y),
    top_n: int = 100
) -> tuple[list[str], list[float]]:
    """Returns a list of strings and relatednesses, sorted from most related to least."""
    query_embedding_response = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=query,
    )
    query_embedding = query_embedding_response.data[0].embedding
    strings_and_relatednesses = [
        (row["text"], relatedness_fn(query_embedding, row["embedding"]))
        for i, row in df.iterrows()
    ]
    strings_and_relatednesses.sort(key=lambda x: x[1], reverse=True)
    strings, relatednesses = zip(*strings_and_relatednesses)
    return strings[:top_n], relatednesses[:top_n]
🔍 追问 相关度函数是写死的吗? → 不是,relatedness_fn 是带默认值(cosine)的参数,可以替换成别的距离/相似度度量。
📚 拓展阅读
Q把文档切成 chunk 时有哪些权衡?该怎么定分块大小?深挖·拓展🔥高频
chunking tokens recall
⏱️ 现行
分块的根本原因是 GPT 一次只能读有限文本,所以要把每篇文档切成足够短、能被读进上下文的片段。原文坦承"没有完美的切分配方",并列出一组权衡:更长的片段对需要更多上下文的问题更好,但对检索更差(容易把多个话题糅在一起);更短的片段能降成本(成本与 token 数成正比),还能让一次检索取回更多片段、有助于召回(recall);片段重叠(overlapping)则能防止答案被切在片段边界上被截断。cookbook 采用的简单做法是:把每个片段限制在 1,600 tokens 以内,对过长的片段递归对半切,并尽量沿段落边界切,以免在有用句子中间断开。此外针对 Wikipedia 还会丢弃 External Links、Footnotes 等不相关小节,清理 <ref> 引用标签和空白、去掉过短片段,并在每个片段前拼上标题和小标题,帮助 GPT 理解该片段的上下文。
术语 chunk / section(分块/小节,嵌入与检索的基本单位); 1,600 tokens(单片段上限,超了递归对半切); overlapping sections(重叠片段,防边界截断); Prepend titles(前置标题以补上下文)
📖 "Because GPT can only read a limited amount of text at once, we'll split each document into chunks short enough to be read." — 原文
📖 "Here, we'll use a simple approach and limit sections to 1,600 tokens each, recursively halving any sections that are too long. To avoid cutting in the middle of useful sentences, we'll split along paragraph boundaries when possible." — 原文
📖 "Longer sections may be worse for retrieval, as they may have more topics muddled together" — 原文
🧪 实例 面对"更长 vs 更短片段"的取舍,原文给的规则化权衡是:
🔍 追问 为什么要在每个片段前面拼标题? → 原文说这样做是为了"help GPT understand the context",让脱离原文的孤立片段仍带有它所属章节的语境。
📚 拓展阅读
QAsk 阶段如何在 token 预算内把检索片段拼进提示词?深挖·拓展中频
prompt-assembly token_budget tiktoken
⏱️ 现行
Ask 阶段由 query_message 负责拼提示词:它先调 strings_ranked_by_relatedness 拿到按相关度排序的片段列表,写一句固定 introduction(要求"用下面这些 2022 冬奥会文章回答问题,答不出就写 I could not find an answer."),然后按相关度从高到低逐段往消息里塞。每加一段前,用 num_tokens(基于 tiktokenencoding_for_model 对文本编码后数 token)检查"当前消息 + 下一段 + 问题"是否超过 token_budget,一旦超了就 break 停止追加,否则把这一段拼进去。最后把问题接在末尾返回。这样设计保证了:最相关的片段优先进入上下文,且拼出的提示词永远不超过预算,不会撑爆模型的上下文窗口——本质是"相关度排序 + 贪心装包"的组合,把有限的上下文预算花在最该读的片段上。
术语 query_message(拼装提示词的函数); token_budget(拼接上限,超出即停止追加); num_tokens(用 tiktoken 数 token); introduction(固定开场白,含"答不出"的兜底指令)
📖 "Return a message for GPT, with relevant source texts pulled from a dataframe." — 原文
🧪 实例
python
def num_tokens(text: str, model: str = GPT_MODELS[0]) -> int:
    """Return the number of tokens in a string."""
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))


def query_message(
    query: str,
    df: pd.DataFrame,
    model: str,
    token_budget: int
) -> str:
    """Return a message for GPT, with relevant source texts pulled from a dataframe."""
    strings, relatednesses = strings_ranked_by_relatedness(query, df)
    introduction = 'Use the below articles on the 2022 Winter Olympics to answer the subsequent question. If the answer cannot be found in the articles, write "I could not find an answer."'
    question = f"\n\nQuestion: {query}"
    message = introduction
    for string in strings:
        next_article = f'\n\nWikipedia article section:\n"""\n{string}\n"""'
        if (
            num_tokens(message + next_article + question, model=model)
            > token_budget
        ):
            break
        else:
            message += next_article
    return message + question
🔍 追问 为什么在 introduction 里写"答不出就写 I could not find an answer"? → 这是给模型的兜底指令,当检索到的片段确实不含答案时,引导它明确说找不到,而不是硬编造。
📚 拓展阅读
QEmbed 阶段怎么批量生成向量并存储?什么时候该上向量数据库?深挖·拓展中频
batch storage vector-database
⏱️ 现行
把库切成若干短的、自洽的字符串后,就逐批调 embeddings 接口算向量。cookbook 用 text-embedding-3-small 模型,BATCH_SIZE = 1000,并注明单次请求最多可提交 2048 条 embedding 输入;循环里按批取子列表调 client.embeddings.create(model=..., input=batch),还用 assert i == be.index 确认返回向量的顺序和输入一致,再把结果拼成一个 {"text", "embedding"} 的 DataFrame。存储上,因为本例只有几千条字符串,直接存成 CSV 文件即可;原文明确指出更大的数据集应改用向量数据库,会更高性能。此外对于超大规模的 embedding 作业,原文建议用 api_request_parallel_processor.py 这类脚本并行发请求、同时限流以不超速率上限。权衡就是:小数据集 CSV + 全表扫描够用且简单,规模一大就要向量库来兼顾存储与近似最近邻检索性能。
术语 BATCH_SIZE(每批输入条数,单请求上限 2048); text-embedding-3-small(本例所用嵌入模型); be.index(校验返回顺序与输入一致); vector database(大数据集的高性能存储/检索)
📖 "Embeddings are saved in a CSV file (for large datasets, use a vector database)" — 原文
📖 "(For larger datasets, use a vector database, which will be more performant.)" — 原文
🧪 实例
python
EMBEDDING_MODEL = "text-embedding-3-small"
BATCH_SIZE = 1000  # you can submit up to 2048 embedding inputs per request

embeddings = []
for batch_start in range(0, len(wikipedia_strings), BATCH_SIZE):
    batch_end = batch_start + BATCH_SIZE
    batch = wikipedia_strings[batch_start:batch_end]
    print(f"Batch {batch_start} to {batch_end-1}")
    response = client.embeddings.create(model=EMBEDDING_MODEL, input=batch)
    for i, be in enumerate(response.data):
        assert i == be.index  # double check embeddings are in same order as input
    batch_embeddings = [e.embedding for e in response.data]
    embeddings.extend(batch_embeddings)

df = pd.DataFrame({"text": wikipedia_strings, "embedding": embeddings})
🔍 追问 embedding 作业量非常大时怎么办? → 原文建议用 api_request_parallel_processor.py 脚本并行发请求并限流,以在不触发速率上限的前提下加速。
📚 拓展阅读
Q答案错了,怎么判断是检索(search)失败还是推理(ask)失败?深挖·拓展中频
debugging retrieval-vs-reasoning evaluation
⏱️ 现行
RAG 出错有两个来源,原文把它们拆得很清楚:一是缺少相关源文本(即 search 步失败),二是推理不可靠(即 ask 步失败)。定位方法是设 print_message=True,把实际喂给 GPT 的文本打印出来看:如果该有的证据根本没被检索到,问题在 search;如果证据其实在里面、模型却没答全,问题在 ask。原文举了个真实例子——排第 1 的文章确实包含了全部三个项目的奖牌得主,但后面的检索结果过度强调男子和女子赛,可能把模型的注意力带偏,导致答得不完整;这是"检索到了但推理没用好"的典型,属于 ask 步问题。针对 ask 步失败,原文给的最简单改进是换更强的模型(如 gpt-4o-mini 或 gpt-4o):GPT-4 系模型往往能正确列出全部 12 位冰壶金牌得主。总体经验是:简单查表类问题最容易答好,需要把多个零散来源组合并推理的问题最难。
术语 print_message=True(打印喂给 GPT 的实际上下文以定位问题); search step(检索步,失败=没取到相关文本); ask step(提问步,失败=有证据但推理不到位); simple lookup(简单查表类问题,系统表现最好)
📖 "In case we get any mistakes in the output, we can see whether a mistake is from a lack of relevant source text (i.e., failure of the search step) or a lack of reasoning reliability (i.e., failure of the ask step), you can look at the text GPT was given by setting print_message=True." — 原文
📖 "In general, search-based systems do best on questions that have a simple lookup, and worst on questions that require multiple partial sources to be combined and reasoned about." — 原文
🧪 实例 冰壶金牌那题,#1 文章其实含全部三项奖牌得主,但后续片段偏重男/女团体赛,模型答得不全——确认证据已检索到,于是把力气花在改进 ask 步(换 gpt-4o),GPT-4 系模型即正确列出全部 12 位金牌得主。
🔍 追问 确认是 ask 步失败后,最省事的改法是什么? → 原文说"The easiest way to improve results is to use a more capable models",即换用 gpt-4o-mini 或 gpt-4o 这类更强的模型。
📚 拓展阅读
Qembedding 除了做检索,还能怎样用于文本分类?深挖·拓展低频
classification embeddings random-forest
⏱️ 现行
embedding 也能直接当分类特征。cookbook 的例子是:根据一条食品评论文本的 embedding,预测它的星级(1 到 5)。做法是把每条评论已算好的 embedding 作为特征向量,划分训练/测试集(便于在未见数据上真实评估),用 RandomForestClassifier(n_estimators=100) 训练,再在测试集上出 classification_report。结果显示模型能像样地区分类别,其中 5 星评论整体表现最好——这并不意外,因为 5 星在数据集里最常见;5 星和 1 星这类极端评分也更容易预测,中间的 2–4 星更难,可能因为人们打中间分时更主观。值得注意的两点权衡:原文说很多文本分类任务里"微调模型往往比 embedding 效果更好",分类并非 embedding 的最优解;而且原文建议样本数应多于 embedding 维度,而本例并没完全做到。所以这是一个"能用但要清楚其局限"的示范。
术语 RandomForestClassifier(随机森林分类器,n_estimators=100); embedding as feature(把向量当分类特征); train/test split(划分数据集以评估未见数据); class imbalance(类别不均衡,5 星最多故最好预测)
📖 "In this text classification task, we predict the score of a food review (1 to 5) based on the embedding of the review's text." — 原文
📖 "For many text classification tasks, we've seen fine-tuned models do better than embeddings." — 原文
📖 "We can see that the model has learnt to distinguish between the categories decently. 5-star reviews show the best performance overall, and this is not too surprising, since they are the most common in the dataset." — 原文
🧪 实例
python
# split data into train and test
X_train, X_test, y_train, y_test = train_test_split(
    list(df.embedding.values), df.Score, test_size=0.2, random_state=42
)

# train random forest classifier
clf = RandomForestClassifier(n_estimators=100)
clf.fit(X_train, y_train)
preds = clf.predict(X_test)

原文对结果的解读:
🔍 追问 这个 embedding 分类示范有什么已知不足? → 原文自陈"We also recommend having more examples than embedding dimensions, which we don't quite achieve here",即样本数应多于向量维度,而本例没完全满足。
📚 拓展阅读
中频

向量数据库集成(Pinecone / Weaviate / Qdrant)

原文 原文 原文
Q什么是向量数据库?为什么把 OpenAI embeddings 存进向量数据库而不是自己搓一个相似度检索?深挖·拓展🔥高频
vector-database embeddings semantic-search
⏱️ 现行
向量数据库是一种"专门用来存储、管理、搜索 embedding 向量"的数据库。它存在的动机来自一个很现实的工程落差:很多团队在小规模上用 embeddings(问答、聊天机器人、推荐等)就能把问题解决掉,但真要上生产,卡住他们的往往不是效果而是性能和安全——数据要放在安全可控的环境里、检索要能水平扩展。三份 cookbook(Pinecone / Weaviate / Qdrant)走的是同一条主线:先把数据用 OpenAI embeddings 向量化,再把这些向量灌进向量库建索引,最后用同一个模型把查询也向量化、做相似度检索(semantic search)。之所以不自己写朴素的暴力比对,是因为向量库把"索引结构 + 距离度量 + 批量写入 + 元数据过滤 + 托管/自托管部署"这些生产要件打包好了;换言之,向量库是把 embeddings 从"demo 能跑"推到"生产可用"的关键组件。

flowchart LR
    A[原始文本/非结构化数据] --> B[OpenAI embeddings 向量化]
    B --> C[灌入向量库建索引]
    D[用户 query] --> E[同模型向量化]
    E --> F[相似度检索 top_k]
    C --> F
    F --> G[取回最近邻结果]
术语 vector database(专门存储/管理/搜索向量的库); embedding vectors(把非结构化数据编码成的向量); semantic search(基于向量接近度的语义检索); top_k(返回最相近的前 k 个结果)
📖 "A vector database is a database made to store, manage and search embedding vectors." — 原文
📖 "Many of our customers make embeddings solve their problems at small scale but performance and security hold them back from going into production - we see vector databases as a key component in solving that, and in this guide we'll walk through the basics of embedding text data, storing it in a vector database and using it for semantic search." — 原文
🧪 实例 三份 notebook 都先加载已经预生成好向量的维基百科数据集,再用同一个 embedding 模型给查询编码:
python
EMBEDDING_MODEL = "text-embedding-3-small"

embeddings_url = 'https://cdn.openai.com/API/examples/data/vector_database_wikipedia_articles_embedded.zip'
# The file is ~700 MB so this will take some time
wget.download(embeddings_url)
🔍 追问 为什么查询用的 embedding 模型必须和建库时一致? → Qdrant notebook 明确提醒查询要用建库时同款模型(原文件里 embeddings 是用 text-embedding-ada-002 生成的),不同模型产出的向量空间不可比,混用会让距离度量失去意义。
📚 拓展阅读
QPinecone / Weaviate / Qdrant 三者的数据模型有什么不同?分别怎么"分区/组织"向量?深挖·拓展🔥高频
data-model namespace schema collection
⏱️ 现行
三者做的事一样(存向量、建索引、查最近邻),但组织向量的抽象概念不同,这也是面试常问的对比点。Pinecone 的顶层是 index,一个 index 下可再切多个 namespace,用来把同一个 index 按用途切成不同子集——cookbook 里就用两个 namespace 分别装文章 title 向量和 content 向量。Weaviate 用的是 schema/class 抽象:你先为每类要搜索的实体定义一个 schema(例子里叫 Article class),把 title、content 作为 properties 声明进去,向量挂在对象上。Qdrant 的顶层是 collection,每个对象至少由一个向量描述,还可带一份叫 payload 的元数据;而且 Qdrant 支持在同一个 collection 里给一个对象配置多个"命名向量"(named vectors,如 titlecontent 两个向量),检索时用 vector_name 切换。取舍上:namespace 是"同结构不同子集"的轻量隔离;schema/class 更像带类型声明的对象模型、便于挂 vectorizer 模块;Qdrant 的 collection+named vectors+payload 则把"一物多向量 + 结构化元数据"放在一等公民位置,检索结果能直接带出 URL 等业务字段。

flowchart TD
    subgraph Pinecone
      P[index: wikipedia-articles] --> P1[namespace: title]
      P --> P2[namespace: content]
    end
    subgraph Weaviate
      W[schema/class: Article] --> W1[property: title + vector]
      W --> W2[property: content]
    end
    subgraph Qdrant
      Q[collection: Articles] --> Q1[named vector: title]
      Q --> Q2[named vector: content]
      Q --> Q3[payload: id/title/url]
    end
术语 namespace(Pinecone 里对单个 index 的子集切分); schema/class(Weaviate 里对搜索实体的类型定义); collection(Qdrant 里的向量集合); named vectors(Qdrant 同一对象上的多个具名向量); payload(Qdrant 随向量存的元数据)
📖 "We'll create an index with namespaces for __titles__ and __content__" — 原文
📖 "In Weaviate you create __schemas__ to capture each of the entities you will be searching." — 原文
📖 "Qdrant stores data in __collections__ where each object is described by at least one vector and may contain an additional metadata called __payload__." — 原文
🧪 实例 Qdrant 在一个 collection 里同时声明 title 与 content 两个命名向量:
python
qdrant.recreate_collection(
    collection_name='Articles',
    vectors_config={
        'title': rest.VectorParams(
            distance=rest.Distance.COSINE,
            size=vector_size,
        ),
        'content': rest.VectorParams(
            distance=rest.Distance.COSINE,
            size=vector_size,
        ),
    }
)
🔍 追问 Pinecone 用两个 namespace、Qdrant 用两个 named vectors 装 title/content,效果上有何差别? → Pinecone 的两个 namespace 是同一 index 下互相隔离的子空间、各自独立查询;Qdrant 的两个命名向量属于同一批对象,一次 upsert 同时写入、检索时按 vector_name 选用哪个向量算距离,天然共享同一份 payload。
📚 拓展阅读
QPinecone 里 index 和 namespace 是什么关系?为什么要用 namespace 而不是建多个 index?深挖·拓展中频
pinecone index namespace upsert
⏱️ 现行
Pinecone 是 cookbook 里代表"托管、云原生"的向量库。工作流是先建一个 index(例子里叫 wikipedia-articles,建 index 时要给定 dimension,取自向量长度),然后在这一个 index 之上创建多个 namespace,让"同一个 index 面向不同用途可搜索"。cookbook 用 contenttitle 两个 namespace 分别存文章正文向量和标题向量,upsert 时把 namespace= 参数指到对应分区即可。相比"每种用途建一个独立 index",namespace 的好处是共享同一个 index 资源、按需切子集、隔离查询,省去多 index 的管理与成本开销。写入侧还有性能考量:cookbook 用一个 BatchGenerator 把 DataFrame 切成批(例子里每批 300)再逐批 upsert,并指出如果想进一步提速可以按官方指南做并行批量插入。查询侧 index.query(...)namespacetop_k,返回 matches(含 idscore)。
术语 index(Pinecone 顶层向量索引,建时需指定 dimension); namespace(index 内的可搜索子集); upsert(插入或更新向量); top_k(检索返回的最近邻数量); score(匹配的相似度得分)
📖 "Once we have an index, we can create multiple namespaces, which can make a single index searchable for various use cases." — 原文
📖 "If you want to batch insert to your index in parallel to increase insertion speed then there is a great guide in the Pinecone documentation on batch inserts in parallel." — 原文
🧪 实例 建 index 后分别把 content、title 向量灌入两个 namespace:
python
pinecone.create_index(name=index_name, dimension=len(article_df['content_vector'][0]))
index = pinecone.Index(index_name=index_name)

for batch_df in df_batcher(article_df):
    index.upsert(vectors=zip(batch_df.vector_id, batch_df.content_vector), namespace='content')

for batch_df in df_batcher(article_df):
    index.upsert(vectors=zip(batch_df.vector_id, batch_df.title_vector), namespace='title')
🔍 追问 建 index 的 dimension 从哪来、写错会怎样? → cookbook 用 len(article_df['content_vector'][0]) 取实际向量维度作为 dimension;维度必须与 embedding 模型输出一致,否则 upsert 的向量与 index 不匹配会失败。
📚 拓展阅读
QWeaviate 的 schema 是干什么的?"bring your own vectors" 和 text2vec-openai 自动向量化怎么选?深挖·拓展中频
weaviate schema text2vec-openai vectorizer
⏱️ 现行
在 Weaviate 里,你要先为每类待搜索实体建一个 schema(class)。cookbook 建了一个 Article class,把 titlecontent 声明为 properties,并在 class 上配置 vectorizermoduleConfig。这里有两条互斥的路线,是面试常问的取舍点。第一条是 bring your own vectors:数据已经在外部向量化好(cookbook 提供的正是预生成向量的数据),你 batch.add_data_object(properties, "Article", None, vector) 时把向量直接塞进去,适合"数据已向量化"的场景。第二条是 自动向量化:如果数据还没向量化,可以把向量化这件事直接委托给 Weaviate 的内置模块 text2vec-openai,它会在 import、任意 CRUD 操作、以及 semantic search 三个时机自动帮你调 OpenAI 生成向量。取舍在于:自带向量给你对 embedding 过程的完全掌控、可复用已算好的向量、不在 DB 侧产生额外 OpenAI 调用;用 text2vec-openai 则省掉自己维护向量化管线,查询时甚至能直接用 with_near_text 传原始文本、由模块在服务端完成 query 的向量化(需要 OPENAI_API_KEY)。Weaviate 还支持在 property 级别用 "skip": True 跳过某些字段的向量化(例子里对 url 就跳过)。
术语 schema/class(Weaviate 的实体类型定义); vectorizer(class 上指定的向量化模块); text2vec-openai(内置的 OpenAI 向量化模块); bring your own vectors(自带外部生成好的向量); moduleConfig(模块级配置,如 model/skip)
📖 "In this cookbook, we provide the data with already generated vectors. This is a good approach for scenarios, where your data is already vectorized." — 原文
📖 "Weaviate offers a built-in module text2vec-openai, which takes care of the vectorization for you at:" — 原文
🧪 实例 定义 Article class,对 title/content 用 text2vec-openai,但对某字段可 skip
python
article_schema = {
    "class": "Article",
    "description": "A collection of articles",
    "vectorizer": "text2vec-openai",
    "moduleConfig": {
        "text2vec-openai": {
          "model": "ada",
          "modelVersion": "002",
          "type": "text"
        }
    },
    "properties": [{
        "name": "title",
        "description": "Title of the article",
        "dataType": ["string"]
    },
    {
        "name": "content",
        "description": "Contents of the article",
        "dataType": ["text"],
        "moduleConfig": { "text2vec-openai": { "skip": True } }
    }]
}
client.schema.create_class(article_schema)
🔍 追问 用 bring-your-own-vectors 时,写入速度怎么优化? → cookbook 先 client.batch.configure(batch_size=100, dynamic=True, timeout_retries=3),用批量写入把 CRUD 打包,起始批 100、按性能动态增减、出错重试。
📚 拓展阅读
QQdrant 建 collection 时为什么必须声明 vector size 和 distance metric?payload 又是用来干嘛的?深挖·拓展中频
qdrant collection distance-metric payload
⏱️ 现行
Qdrant 把数据放在 collection 里,每个对象至少由一个向量描述,可选带一份 payload 元数据。建 collection(recreate_collection)时你必须在 vectors_config 里为每个(命名)向量声明两件事:size(向量维度,cookbook 取自 len(article_df['content_vector'][0]))和 distance(距离度量,例子用 Distance.COSINE)。原因很直接——距离度量决定了向量库如何高效地建索引和检索向量;不先固定维度和度量,索引结构无从建立。payload 则是可选字段,让你把额外的业务元数据和向量存在一起:cookbook 在 payload 里存了文章的 idtitleurl,好处是检索结果能直接带出这些字段——比如把最近邻文章的标题连同它的 URL 一起返回给用户,无需再回源查库。这就是 Qdrant 数据模型的实用性:向量负责"找得到",payload 负责"找到之后能直接用"。
术语 collection(Qdrant 的向量集合); vectors_config(建 collection 时的向量配置); size(向量维度); distance(距离度量,如 COSINE); payload(随向量存储的可选元数据); recreate_collection(重建集合的接口)
📖 "You need to declare the vector size and distance metric for the collection. Distance metric enables vector database to index and search vectors efficiently." — 原文
📖 "Payload is an optional field that allows you to store additional metadata alongside the vectors." — 原文
🧪 实例 upsert 时把 title/content 两个向量与 id/title/url 的 payload 一起写入:
python
qdrant.upsert(
    collection_name='Articles',
    points=[
        PointStruct(
            id=k,
            vector={'title': v['title_vector'],
                    'content': v['content_vector']},
            payload={
                'id': v['id'],
                'title': v['title'],
                'url': v['url']
            }
        )
    ]
)
🔍 追问 payload 在检索结果里怎么被用到? → 查询返回的每个 article 都带 article.payload,cookbook 直接 article.payload["title"]article.payload["url"] 打印结果,把标题和原文链接一并展示。
📚 拓展阅读
Q三个库的相似度检索接口分别长什么样?Weaviate 的 near_vector 和 near_text 有何区别?深挖·拓展中频
search near_vector near_text query
⏱️ 现行
检索的通用套路都是"先把 query 用同一个 embedding 模型向量化,再让库找最近邻",但三家 API 形态不同。Pinecone 直接 index.query(embedded_query, namespace=..., top_k=...),从 matches 里取 idscore。Qdrant 用 qdrant.search(collection_name=..., query_vector=(vector_name, embedded_query), limit=top_k),注意 query_vector 是个二元组,第一项 vector_name 用来在 title 与 content 之间切换检索用的向量。Weaviate 则有两种查法,这是重点:with_near_vector(near_vector) 要求你自己先算好 query 向量再传进去;而 with_near_text(nearText) 让你直接传原始文本 concept,由内置的 text2vec-openai 模块在服务端替你把 query 向量化——它把"生成 query 向量"这步内化进了检索接口,代价是需要配 OpenAI key 并让 DB 侧发起 OpenAI 调用。取舍:near_vector 让向量化完全在你手里、可复用外部算好的向量;near_text 更省事、query 端零向量化代码,但把 embedding 依赖压进了数据库。
术语 index.query(Pinecone 检索接口); qdrant.search(Qdrant 检索接口); query_vector(Qdrant 的 (vector_name, vector) 二元组); with_near_vector(Weaviate 传入自算向量检索); with_near_text(Weaviate 传原始文本、模块内部向量化); certainty/distance(Weaviate 返回的相近度指标)
📖 "We may provide an additional parameter vector_name to switch from title to content based search." — 原文
📖 "This allows you to run a vector query with the with_near_text filter, which uses your OPEN_API_KEY." — 原文
🧪 实例 Qdrant 通过 vector_name 在 title/content 之间切换检索向量:
python
def query_qdrant(query, collection_name, vector_name='title', top_k=20):
    embedded_query = openai.embeddings.create(
        input=query,
        model=EMBEDDING_MODEL,
    ).data[0].embedding
    query_results = qdrant.search(
        collection_name=collection_name,
        query_vector=(
            vector_name, embedded_query
        ),
        limit=top_k,
        query_filter=None
    )
    return query_results

Weaviate 用 near_text 直接传概念文本,模块内部完成 query 向量化:
python
nearText = {
    "concepts": [query],
    "distance": 0.7,
}
query_result = (
    client.query
    .get(collection_name, properties)
    .with_near_text(nearText)
    .with_limit(20)
    .do()
)
🔍 追问 Weaviate 检索结果里的 certainty 和 distance 是什么? → cookbook 在查询里请求 _additional {certainty distance},打印时取 article['_additional']['certainty']['distance'],即返回对象与 query 向量的接近度/距离指标。
📚 拓展阅读
Q这三个向量库的部署形态(托管 vs 自托管)分别是怎样的?cookbook 各自选了哪种?深挖·拓展低频
deployment managed self-hosted docker
⏱️ 现行
面试里常被问"你怎么选向量库",部署形态是重要维度。Pinecone 在 cookbook 里被定位为 managed、cloud-native 的托管库——你注册拿 API key、把它设成环境变量即可用,不需要自己运维基础设施。Weaviate 则同时提供托管 SaaS(Weaviate Cloud Service)和自托管开源两种;cookbook 因为前面已经演示过云托管,这里特意选自托管:用 Docker 按官方 docker-compose 在本地起 Weaviate(连接 http://localhost:8080),当然也给了 SaaS 的连接方式(连到 *.weaviate.network 集群)。Qdrant 是用 Rust 写的高性能向量搜索库,官方同时有 on-premise 和 cloud 版本,cookbook 出于演示目的用本地部署模式,同样靠 Docker(docker-compose up -d)起单容器实例,并提醒本地跑要把 Docker 内存调到 8GB 以上、否则可能被 7 Killed。总结取舍:托管省运维、快速起步、但数据与成本受控于厂商;自托管/开源给你完全的数据掌控和本地/私有部署能力,代价是自己运维容器与资源。
术语 managed/cloud-native(托管、云原生,如 Pinecone); SaaS(Weaviate Cloud Service 托管); self-hosted/open source(自托管开源); on-premise(本地/私有部署,Qdrant); docker-compose(本地起容器实例的方式)
📖 "The next option we'll look at is Pinecone, a managed vector database which offers a cloud-native option." — 原文
📖 "Another vector database option we'll explore is Weaviate, which offers both a managed, SaaS option, as well as a self-hosted open source option." — 原文
📖 "It offers both on-premise and cloud version, but for the purposes of that example we're going to use the local deployment mode." — 原文
🧪 实例 Qdrant 本地部署并连接单容器实例:
bash
docker compose up -d
python
qdrant = qdrant_client.QdrantClient(host="localhost", port=6333)
qdrant.get_collections()
🔍 追问 Qdrant 本地部署有什么常见坑? → cookbook 提示可能要把 Docker 内存上限调到 8GB 或更多,否则 Qdrant 可能报类似 7 Killed 的错误而无法执行。
📚 拓展阅读
🔥高频

RAG 问答、重排与文档解析

原文 原文 原文
Q如何在不维护向量数据库的前提下,给已有搜索系统叠加一套"搜索 → 重排 → 回答"的混合问答流水线?深挖·拓展🔥高频
RAG search-api re-ranking HyDE
⏱️ 现行
这套方案的核心动机是折中:纯"模仿人类浏览"(GPT 触发搜索、看结果、改查询)慢且迭代;纯"embeddings 检索"又要预先把整个知识库嵌入、持续嵌入新内容并维护一个向量库。混合方案把两者的优点缝在中间,并且能直接叠加在任何已有搜索端点上(Slack search API、内部 ElasticSearch 等),无需额外维护向量数据库。流程分三步:Step 1 Search——用户提问,GPT 生成一批多样化查询,并行执行搜索(通常故意 over-fetch 以覆盖潜在相关文档);Step 2 Re-rank——对每条结果算 embedding,和一个"假设的理想答案"(HyDE 思路)做语义相似度,按此排序并过滤;Step 3 Answer——把 top 结果作为上下文让模型生成带引用和链接的答案。它的关键权衡是:相对低延迟、可插进任何现有搜索,代价是每次请求要多跑查询生成 + 假设答案 + 重排,但省掉了维护向量库的长期成本。
术语 Mimicking Human Browsing(模仿人类浏览:GPT 迭代式触发并改写搜索); Retrieval with Embeddings(基于 embedding 的检索,靠 cosine 相似度取回内容); over-fetching(过取:先多召回再重排); re-ranking(重排:用更强信号对候选二次排序)
📖 "By combining these approaches, and drawing inspiration from re-ranking methods, we identify an approach that sits in the middle." — 原文
📖 "This hybrid approach offers relatively low latency and can be integrated into any existing search endpoint, without requiring the upkeep of a vector database." — 原文
🧪 实例 整体机制可视化为三段式:
flowchart LR
  U[用户提问] --> G[GPT 生成多条查询]
  G --> S[并行调用 Search API
over-fetch] S --> H[生成 hypothetical answer] H --> E[对结果 + 假设答案算 embedding] E --> R[按语义相似度排序 + 过滤] R --> A[top 结果作上下文 → 生成带引用答案]
🔍 追问 为什么说这套方案"可插进任何现有搜索"是它相对纯向量 RAG 的最大卖点? → 因为它不要求预先把整个知识库嵌入并持续维护向量库,重排发生在搜索返回结果之后,底层搜索端点(Slack/ES/News API)保持不变,只在其上叠一层 GPT 查询生成 + embedding 重排。
📚 拓展阅读
QBi-encoder 和 cross-encoder 在检索里各自的成本与精度权衡是什么?为什么典型做法是"先 bi 后 cross"?深挖·拓展🔥高频
bi-encoder cross-encoder re-ranking 成本权衡
⏱️ 现行
考虑 D 个文档、Q 个查询的检索任务。暴力算每一对的相关性(cross-encoding)成本随 D * Q 增长,昂贵;而 embeddings-based search(bi-encoding)对每个文档和查询各算一次 embedding 并复用,成本只随 D + Q 增长,快得多。但更快的代价是精度可能更差——bi-encoder 把 query 和 document 分开编码,丢掉了两者的细粒度交互;cross-encoder 把 query+document 一起喂进模型,能捕捉领域特定的微妙相关性,所以更准,但不 scale。因此最佳实践是二者结合:用 embeddings(或其它 bi-encoder)廉价地选出 top 候选,再用 GPT(或其它 cross-encoder)昂贵地重排这批候选。这种混合成本大致是 (D + Q) * cost of embedding + (N * Q) * cost of re-ranking,其中 N 是被重排的候选数——把昂贵的 cross-encoder 只作用在一个已缩短的候选列表上,正是它的理想用武之地。
术语 cross-encoding(交叉编码:query 与 document 一起编码,成本 D * Q); bi-encoding(双编码:各自编码一次并复用,成本 D + Q); N(被重排的候选数,重排成本的乘子); semantic search(语义搜索:embedding 召回)
📖 "A faster approach is embeddings-based search, in which an embedding is computed once for each document and query, and then re-used multiple times to cheaply compute pairwise relevance." — 原文
📖 "Although embeddings-based search is faster, the quality can be worse." — 原文
📖 "Cross-encoders are more accurate than bi-encoders but they don't scale well, so using them to re-order a shortened list returned by semantic search is the ideal use case." — 原文
🧪 实例 混合流水线的成本直觉:
text
纯 cross-encoder:   cost ~ D * Q          (精度高, 不 scale)
纯 bi-encoder:      cost ~ D + Q          (快, 精度可能差)
混合 (先 bi 后 cross): (D + Q) * embed + (N * Q) * rerank

即:bi-encoder 把 D 篇文档压到 N 个候选,cross-encoder 只在这 N 个上做昂贵重排。
🔍 追问 既然 cross-encoder 更准,为什么不直接对全库用它? → 因为它对每个 (query, document) 对都要独立前向计算,成本随 D * Q 爆炸,无法 scale;只有先用 bi-encoder 把候选缩短到 N,重排成本才可控。
📚 拓展阅读
Q如何用带 logprobs 的 GPT 模型搭一个 cross-encoder 来重排搜索结果?logit_bias 在这里起什么作用?深挖·拓展🔥高频
cross-encoder logprobs logit_bias few-shot
⏱️ 现行(原文用 text-davinci-003)
思路是用一个判别式提示把重排变成二分类:给模型一个 query 和一段 document,让它只输出单个 token —— "Yes" 或 "No" 表示该文档是否相关,并附上少量 few-shot 领域示例来调教它。工程上有几个关键点:一是给 " Yes" 和 " No" 两个 token 加 logit bias,降低模型输出其它 token 的概率,把回答收敛到这两个词;二是设 max_tokens=1temperature=0logprobs=1,拿到模型对该 token 的对数概率;三是不能只看离散的 yes/no,而要按 " Yes" 对应的 logprobs(概率)来重排——这样即使一批文档都被判 "Yes",也能按"有多确信是 Yes"细排出高低。few-shot 示例必须做成领域特定的,因为 cross-encoder 的威力正来自你为自己领域量身定制;同时要权衡重排多少候选 vs 处理速度,可考虑批处理和并行请求。
术语 logit_bias(对特定 token 的 logits 加偏置,压制其它 token); logprobs(token 对数概率,用作连续相关性分数); few-shot(少样本示例,把通用模型调成领域判别器); Yes/No token(单 token 相关性判定)
📖 "Add a ``logit bias` for the tokens for ` Yes` and ` No`` to decrease the likelihood of any other tokens occurring." — 原文
📖 "Rerank the results by the ``logprobs` keyed on ` Yes``." — 原文
📖 "Make your examples domain-specific - the strength of cross-encoders comes when you tailor them to your domain." — 原文
🧪 实例 原文的判别提示 + 调用参数(节选):
python
prompt = '''
You are an Assistant responsible for helping detect whether the retrieved document is relevant to the query. For a given input, you need to output a single token: "Yes" or "No" indicating the retrieved document is relevant to the query.
...
'''
response = openai.chat.completions.create(
    model=OPENAI_MODEL,
    prompt=prompt.format(query=query, document=content),
    temperature=0,
    logprobs=1,
    logit_bias={3363: 1, 1400: 1},
    max_tokens=1,
)

其中 3363 / 1400 是 " Yes" / " No" 的 token id;拿到 logprobs 后再用 exp() 转成概率,按 Yes 的概率重排。
🔍 追问 为什么按 logprobs 重排比直接用离散的 Yes/No 更好? → 因为多个文档可能都被判 "Yes",离散标签无法区分它们;而 " Yes" 的 logprob(概率)是连续分数,能反映模型的确信度,从而在同为 Yes 的文档间排出细粒度先后。
📚 拓展阅读
QHyDE(假设答案)重排是什么?为什么要拿"假设理想答案"而不是原问题去和结果比相似度?深挖·拓展🔥高频
HyDE re-rank embedding hypothetical-answer
⏱️ 现行
HyDE(Hypothetical Document Embeddings)的做法是:在重排前,先让模型对用户问题生成一个"假设的理想答案"——注意这个答案不使用任何真实事实,而是用占位符(如 NAME did something、NAME said something at PLACE)撑出一个"答案的样子"。然后把这个假设答案 embed,再和每条搜索结果的 embedding 比语义相似度来排序。关键洞察是:问题和答案在语义空间里长得不一样,如果直接拿问题去比,会偏向"和问题相似"的结果(比如同样在问这个问题的文档);而拿一个"像答案"的文本去比,则会优先召回"看起来像好答案"的结果。因为 OpenAI embedding 返回时已归一化,重排时可以直接用点积代替完整 cosine 相似度计算,省一步归一化。
术语 HyDE(Hypothetical Document Embeddings,用假设答案的 embedding 做检索/重排); hypothetical answer(假设答案,用占位符而非真实事实生成); cosine similarity(余弦相似度,归一化后等价于点积); normalized embeddings(归一化 embedding,点积即可代替 cosine)
📖 "Drawing inspiration from HyDE (Gao et al.), we first generate a hypothetical ideal answer to rerank our compare our results against." — 原文
📖 "This helps prioritize results that look like good answers, rather than those similar to our question." — 原文
📖 "Note that we can simply calculate the dot product in lieu of doing a full cosine similarity calculation since the OpenAI embeddings are returned normalized in our API." — 原文
🧪 实例 原文生成假设答案的提示:
text
Generate a hypothetical answer to the user's question. This answer will be used to rank search results.
Pretend you have all the information you need to answer, but don't use any actual facts. Instead, use placeholders
like NAME did something, or NAME said something at PLACE.

随后 dot(hypothetical_answer_embedding, article_embedding) 直接用点积算相似度并排序。
🔍 追问 为什么假设答案要故意用占位符、不用真实事实? → 因为目的只是得到一段"结构/语义像正确答案"的文本用于比对,真实事实无关紧要甚至可能引入幻觉;占位符让 embedding 抓住答案的形态而非具体内容,从而更好地对齐真正的好答案。
📚 拓展阅读
Q多查询生成(query generation)在检索里解决什么问题?为什么原文强调要"尽可能穷尽"和 over-fetch?深挖·拓展中频
query-generation over-fetching 召回 搜索
⏱️ 现行
用户的单个提问往往不足以覆盖搜索所需的所有措辞,直接拿它去搜会漏掉相关文档。原文的做法是:让模型基于问题生成一批多样化查询,尽量用相关关键词的不同变体、尽量泛化、包含和排除不同词项(例如 ['keyword_1 keyword_2', 'keyword_1', 'keyword_2']),查询越多、找到相关结果的概率越大;并把原始问题本身也加进去以防万一。这些查询并行执行。重排环节的前提是搜索阶段要故意"稍微过取"(over-fetch),先把潜在相关的文档都抓进来,再靠后续 embedding 重排去噪。代价是搜索常返回大量结果、其中很多和原问题无关,所以必须有重排+过滤这一步来提升最终答案质量——召回优先、精度靠后置重排补回来。
术语 query generation(查询生成:模型把一个问题扩成多条查询); over-fetching(过取:先多召回,牺牲精度换召回); parallel search(并行执行多条查询); keyword variation(关键词变体,含/排除词项)
📖 "Now, in order to be as exhaustive as possible, we use the model to generate a list of diverse queries based on this question." — 原文
📖 "Include as many queries as you can think of, including and excluding terms." — 原文
🧪 实例 原文的查询生成提示(节选)与调用:
python
QUERIES_INPUT = f"""
You have access to a search API that returns recent news articles.
Generate an array of search queries that are relevant to this question.
Use a variation of related keywords for the queries, trying to be as general as possible.
Include as many queries as you can think of, including and excluding terms.
...
User question: {USER_QUESTION}
Format: {{"queries": ["query_1", "query_2", "query_3"]}}
"""
queries = json_gpt(QUERIES_INPUT)["queries"]
queries.append(USER_QUESTION)  # 把原问题也加进去
🔍 追问 over-fetch 带来大量无关结果,这不是降低质量吗? → 单看搜索阶段是的,但它是刻意用召回换精度:无关结果由后续 embedding 重排(如 HyDE 相似度)排序并过滤掉,最终只把 top 结果送去生成答案,整体质量反而更高。
📚 拓展阅读
Q面对幻灯片、网页导出这类富 PDF,为什么原文用 GPT-4o 做图像分析而不是只用 pdfminer 抽文本?深挖·拓展中频
PDF解析 GPT-4o 多模态 RAG
⏱️ 现行
富 PDF(slide decks、网页导出)里有大量价值信息藏在图表、表格、版式里,纯文本抽取(pdfminer)会丢掉这些视觉语义。原文因此提供两条路:一是用 pdfminer 抽文本,二是把 PDF 每页转成图像,再用 GPT-4o 基于图像分析内容——甚至可以完全跳过第一种、只用图像分析得到的内容。实现上先用 pdf2image 把页面转图片(依赖本机装 poppler),再把图片编码成 base64 的 data URI 喂给 ChatCompletions 的多模态接口。它给的 system prompt 很讲究:让模型像给 101 级听众做讲解一样,详细描述图表(每个组件如何交互)、拆解表格,并且明确要求不要提"这是什么内容格式/类型",而是直接讲信息本身,因为听众看不到图。这样把"看得懂图"的理解力转成可检索的文本,喂进后续 RAG 流水线。
术语 pdf2image(把 PDF 页转图像,依赖 poppler); pdfminer(纯文本抽取,作为可选/对照路径); data URI (base64)(把图像编码后喂多模态接口); GPT-4o image analysis(用多模态模型把视觉内容转成描述文本)
📖 "This notebook shows how to leverage GPT-4o to turn rich PDF documents such as slide decks or exports from web pages into usable content for your RAG application." — 原文
📖 "After converting a PDF file to multiple images, we'll use GPT-4o to analyze the content based on the images." — 原文
📖 "You will be provided with an image of a PDF page or a slide. Your goal is to deliver a detailed and engaging presentation about the content you see, using clear and accessible language suitable for a 101-level audience." — 原文
🧪 实例 把每页图像转成 data URI 再送多模态接口:
python
def get_img_uri(img):
    png_buffer = io.BytesIO()
    img.save(png_buffer, format="PNG")
    png_buffer.seek(0)
    base64_png = base64.b64encode(png_buffer.read()).decode('utf-8')
    data_uri = f"data:image/png;base64,{base64_png}"
    return data_uri
🔍 追问 为什么 system prompt 里明确禁止模型提"内容格式/类型"? → 因为目标是抽出可检索的信息本身而非元描述,像"这是一张幻灯片/一个表格"这类格式话语对下游 RAG 检索无价值,还会稀释真正的语义内容,所以要求模型直接讲信息。
📚 拓展阅读
QRAG 里内容分块(chunking)该怎么切?原文用了什么简单策略,又建议怎么优化?深挖·拓展中频
chunking RAG metadata embedding
⏱️ 现行
在嵌入之前要先把内容切块。原文为简洁起见用了一个非常朴素的策略:按页逻辑分块,靠分隔符把文本按页切开。但它明确指出真实场景可以更精细:一是把内容切成更小的片段;二是在每个片段开头加入数据——比如 slide 标题、整个 deck 的标题、文档描述——这样每个独立 chunk 都能自带上下文,不至于脱离语境后检索时语义残缺。这背后的权衡是:块太大则一个 chunk 里混入多个主题、embedding 语义被稀释、检索精度下降;块太小又可能丢上下文、需要靠附加 metadata 补回。给每个 chunk 补标题/描述这类元数据,正是在"小块提升精度"和"保留上下文"之间取平衡。原文还在收尾里把"进一步分块并给每块加 metadata 作上下文"列为可探索的优化方向。
术语 chunking(分块:嵌入前把内容切分); page-based chunking(按页分块,原文用的朴素策略); metadata as context(把标题/描述加到 chunk 开头补上下文); separators(分隔符,按页切文本)
📖 "Before embedding the content, we will chunk it logically by page." — 原文
📖 "That way, each independent chunk can be in context" — 原文
🧪 实例 原文的朴素分块——按分隔符切页:
python
# 原文按页 embedding 的分块思路:把整篇内容按页分隔符切成 chunk,再逐块嵌入
# 优化方向(原文建议):
# - 切成更小的片段
# - 在每块开头拼上 slide 标题 / deck 标题 / 文档描述,让 chunk 自带上下文
chunk = f"{deck_title} — {slide_title}\n{page_text}"
🔍 追问 为什么给每个 chunk 开头加标题/描述能提升检索质量? → 因为切块后单个片段容易脱离原文语境,补上 deck/slide 标题和文档描述能让这个独立 chunk 自带上下文,embedding 更能反映它的真实主题,从而在检索时更准确地被相关 query 命中。
📚 拓展阅读
QRAG 的生成阶段怎么防止模型答非所问或硬编上下文?原文的 system prompt 做了什么相关性把关?深挖·拓展中频
RAG grounding 相关性把关 生成
⏱️ 现行
检索的最后一步是拿回来的内容作为上下文去回答输入问题。原文的生成 system prompt 设计了一个两段式流程来做 grounding 与相关性把关:第一,模型先在内部评估提供的内容是否与输入 prompt 相关;第二,如果相关就直接用这些内容作答、用其中的元素来组织回复;如果不相关,就用自己的知识回答,或者在知识不足时直接说不知道该怎么答——而不是强行套用无关上下文编答案。同时要求回答保持简洁、只针对输入 prompt,不要把上下文里额外的信息也倒出来。这套设计的权衡在于:一方面靠"先判相关性"避免检索噪声污染答案(over-fetch 会带回很多无关内容),另一方面用"允许回退到自身知识 / 允许说不知道"来防止幻觉式硬编,牺牲一点覆盖率换取答案的可靠性。
术语 grounding(把答案锚定在检索内容上); relevance assessment(内部相关性评估,判断上下文是否可用); fallback(内容不相关时回退到自身知识或说不知道); RAG generation(用检索内容作上下文生成答案)
📖 "The last step of the process is to generate outputs in response to input queries, after retrieving content as context to reply." — 原文
📖 "First, you will internally assess whether the content provided is relevant to reply to the input prompt." — 原文
📖 "If the content is not relevant, use your own knowledge to reply or say that you don't know how to respond if your knowledge is not sufficient to answer." — 原文
🧪 实例 原文的检索 + 生成骨架(节选):
python
def search_content(df, input_text, top_k):
    embedded_value = get_embeddings(input_text)
    df["similarity"] = df.embeddings.apply(
        lambda x: cosine_similarity(np.array(x).reshape(1,-1),
                                    np.array(embedded_value).reshape(1,-1)))
    res = df.sort_values('similarity', ascending=False).head(top_k)
    return res

def generate_output(input_prompt, similar_content, threshold=0.5):
    content = similar_content.iloc[0]['content']
    # 相似度高于阈值才追加更多匹配内容作为上下文
    ...

先用 cosine 相似度取 top_k,再在生成时用 system prompt 做相关性把关。
🔍 追问 允许模型"用自己的知识回答"不会削弱 RAG 的可溯源性吗? → 会有取舍:原文把它作为内容不相关时的回退,而非默认路径,并同时允许"说不知道",目的是在检索噪声下避免硬套无关上下文;若场景强调可溯源,可收紧为"不相关就只说不知道"。
📚 拓展阅读
Qcross-encoder 重排在生产里适合什么场景?few-shot 方案的边界在哪、何时该换成 fine-tuning?深挖·拓展低频
cross-encoder fine-tuning 延迟 生产实践
⏱️ 现行(原文以 text-davinci-003 为例)
原文总结:定制化 cross-encoder 最适合"存在领域特定细微差别、且已做过预过滤把数据量限制住"的场景。典型用例包括:先返回 100 篇最相关的股票研报,再依据某组客户组合的具体上下文重排成 top 5/10;或在经典规则搜索取回 top 100/1000 后,按特定用户上下文做剪枝。few-shot 方案在领域足够通用、少量示例就能覆盖大多数重排情形时效果好;但当文档间差异越来越具体、少量示例不够时,应考虑用 Fine-tuning 端点做一个示例更丰富的 cross-encoder。此外还有延迟权衡:用 text-davinci-003 即便只有少量示例每次也要花几秒,若能用 ada/babbage 的 fine-tuned 模型拿到不错结果,fine-tuning 也能帮上忙。核心取舍是:few-shot 上手快、无需训练,但精度和延迟受限;fine-tuning 前期成本高,却能覆盖更细的领域差异并可能降低单次延迟。
术语 pre-filtering(预过滤:先用规则/语义搜索限量再重排); few-shot vs fine-tuning(少样本 vs 微调的取舍); latency(延迟:大模型逐条重排的开销); Fine-tuning endpoint(微调端点,做更精细的 cross-encoder)
📖 "There is a trade-off between how many potential examples to re-rank vs. processing speed." — 原文
📖 "To get the best of both, one common approach is to use embeddings (or another bi-encoder) to cheaply identify top candidates, and then use GPT (or another cross-encoder) to expensively re-rank those top candidates." — 原文
🧪 实例 原文给出的典型生产用例:
text
- 先取 100 篇最相关股票研报 → 按某组客户组合的上下文重排成 top 5/10
- 经典规则搜索取回 top 100/1000 → 按特定用户上下文剪枝
何时换 fine-tuning:文档差异越来越具体、few-shot 覆盖不住,或想降低 text-davinci-003 的逐条延迟
🔍 追问 为什么原文反复强调重排前要先做预过滤/限量? → 因为 cross-encoder 逐 (query, document) 对昂贵且有秒级延迟,只有先用语义搜索或规则把候选压到一两百个,重排的成本和延迟才可控——它天生是"缩短列表上的二次排序"而非全库工具。
📚 拓展阅读
低频

进阶 RAG:多模态与图数据库

原文 原文 原文
Q多模态 RAG 是怎么把图片纳入检索的?为什么要"先理解图再入库"而不是直接对图做向量检索?深挖·拓展中频
multimodal-rag vision file-search
⏱️ 现行
这套多模态 RAG 的关键在于把"图像理解"前置成一个离线预处理步骤:先用 Vision 模型(notebook 里是 GPT-5 的 Responses API,配 input_image)对每张图给出一句带情感极性的描述,再把这段文字与原有的文字评论拼成 full_sentiment 一起写入向量库,检索阶段用的仍然是 OpenAI 内建向量库 + File Search 的纯文本语义检索。之所以这么设计,是因为纯文本库对"只有图、没有文字"的样本存在信息缺口——notebook 专门构造了一个七月唯一的正面 spaghetti 评论只有图片反馈的场景:纯文本库回答含糊、不确定,而把图片理解后的描述并入上下文的第二个库能给出更准确的回答。为做对照,notebook 刻意建了两个向量库(一个含图像理解、一个不含),这既是工程实现也是评估基线;代价是每张图都要多一次 Vision 模型调用,增加了离线成本,换来的是检索召回时视觉信息不再丢失。
flowchart LR
  IMG[评论图片] -->|Vision 模型描述| DESC[图像情感描述]
  TXT[文字评论] --> MERGE[full_sentiment]
  DESC --> MERGE
  MERGE -->|上传| VS[(向量库 / File Search)]
  Q[自然语言查询] -->|语义检索+过滤| VS --> ANS[更准确回答]
术语 File Search(OpenAI 内建的向量检索工具,挂在 Responses 的 tools 上); full_sentiment(文字评论与图像描述拼接后的入库字段); vector store(存放文本 embedding 的向量库); input_image(Responses API 里以 base64 data URL 传图的输入类型)
📖 "This example uses OpenAI's built-in vector store and file search capabilities to build a RAG system that can analyse customer experiences from their feedback, which can be both visual and text-based. We create two vector stores for comparisons, one with image understanding and one without." — 原文
📖 "However with image context provided the second RAG system is able to provide a more accurate response." — 原文
🧪 实例 用 Vision 模型把一张外卖图片压成一行"描述+情感",再拼进入库文本:
python
def analyze_image_sentiment(image_path: str) -> str:
    """Analyze food delivery image and return sentiment analysis."""
    base64_image = encode_image(image_path)
    response = client.responses.create(
        model="gpt-5",
        reasoning={"effort": "minimal"},
        input=[{
            "role": "user",
            "content": [
                {"type": "input_text",
                 "text": "Analyze this food delivery image. Respond with a brief description and sentiment (positive/negative) in one line."},
                {"type": "input_image",
                 "image_url": f"data:image/jpeg;base64,{base64_image}"},
            ],
        }],
    )
    return response.output_text.strip()

查询 "Where there any comments about the 'spaghetti'?" 时,纯文本库只找到 "Pasta was overcooked",而含图库能答出"番茄酱意面配芝麻菜、蒜香面包和芝士"的正面描述。
🔍 追问 检索时用的是图还是文字? → 是文字——图片在入库前已经被 Vision 模型转成了描述文本,检索走的是纯文本语义检索,图片本身只在最后核对结果时再展示出来。
📚 拓展阅读
Q图数据库 RAG 相比传统向量 RAG,价值到底在哪?它的检索流程是怎样的?深挖·拓展中频
graph-rag neo4j recommendation
⏱️ 现行
图数据库 RAG 的独特价值不在"查得到",而在"顺着关系找相似"。notebook 把 Amazon 商品数据建成 Product 节点与 category / brand / characteristic / color 等实体之间的关系图(如 (product)-[:hasCategory]->(entity)),流程分两段:先让 LLM 从用户 prompt 里抽出结构化实体(颜色、品类、年龄段等 JSON),再用模板拼 Cypher、配合 GDS 余弦相似度去匹配语义相近的实体;真正体现图库价值的是第二步——基于"同类目且至少有 1 个其它实体相同"或"共享至少 N 个实体"的关系去 surface 相似商品。作者明确点出权衡:单纯的查询部分在关系型数据库上也能做,知识图谱的收益在于当你想利用条目之间的连接、把结果与图surface出的相似条目耦合起来时才显现;因此它天然适合推荐系统、AI 增强 CRM、行为关联分析这类场景,代价是引入了额外的建模与运维复杂度,要先评估是否真的划算。
flowchart TD
  P[用户 prompt] -->|LLM 抽实体| J[实体 JSON]
  J -->|模板生成 Cypher| CY[Cypher 查询]
  CY -->|GDS cosine 匹配| G[(Neo4j 图库)]
  G --> R[命中商品]
  R -->|沿共享实体扩展| SIM[相似商品推荐]
术语 Neo4jGraph(LangChain 连接 Neo4j 的封装); hasCategory / hasBrand(商品与实体之间的关系类型); GDS cosine similarity(Neo4j Graph Data Science 的余弦相似度函数); MERGE(Cypher 里存在则匹配、不存在则创建的写入语义)
📖 "If you have data where relationships between data points are important and you might want to leverage that, then it might be worth considering graph databases instead of traditional relational databases." — 原文
📖 "The querying part of this notebook would work on a relational database as well, the knowledge graph comes in handy when we want to couple the results with similar items that the graph is surfacing." — 原文
🧪 实例 用"同类目 + 至少一个其它共同实体"在图上找相似商品的 Cypher:
python
query_category = '''
    MATCH (p:Product {id: $product_id})-[:hasCategory]->(c:category)
    MATCH (p)-->(entity)
    WHERE NOT entity:category
    MATCH (n:Product)-[:hasCategory]->(c)
    MATCH (n)-->(commonEntity)
    WHERE commonEntity = entity AND p.id <> n.id
    RETURN DISTINCT n;
'''
🔍 追问 什么时候不该上图数据库? → 当你只是做属性过滤/查询、不需要"沿关系发现相似项"时,关系型数据库就够了;作者建议先确认知识图谱是最佳选择,因为它带来了额外复杂度。
📚 拓展阅读
Q用 Vision Modality 解析富含图表/表格的 PDF 做 RAG,机制是什么?为什么不无脑对所有页都用视觉模型?深挖·拓展中频
vision-modality pdf-parsing pinecone
⏱️ 现行
传统 RAG 擅长纯文本,但对图片、图形、表格占关键信息的文档会失灵。这套方案的机制是:把 PDF 逐页转成图像,用 GPT-4o 的视觉能力对每页做转写——不仅抄文字,还在遇到图/表时加上 DESCRIPTION OF THE IMAGE OR CHARTTRANSCRIPTION OF THE TABLE 这样的标记;据此给该页打一个 Visual_Input_Processed=Y 的元数据标志,连同文本 embedding 一起 upsert 到 Pinecone。检索时先对页面文本做语义搜索(cosine),把命中页文字作为上下文给 GPT-4o;只有当命中页带视觉标志、且纯文本描述不足以传达信息(如复杂工程图)时,才在可选的 Step 6 把整页图片直接喂给 GPT-4o。之所以不对所有页都用视觉模态,是因为它资源密集、延迟和成本都更高,作者建议仅在纯文本抽取在评测基准上表现不佳时才启用——这是一条明确的"按需升级"成本权衡。
术语 Vision Modality(把页面图像直接交给 GPT-4o 理解的视觉输入方式); Visual_Input_Processed(标记该页是否含图/表的元数据 flag); upsert(update+insert,存在则更新不存在则插入的原子写入); metadata tagging(给向量打元数据标签以支持过滤)
📖 "Our approach involves parsing documents into images and utilizing metadata tagging to identify pages containing images, graphics and tables. When a semantic search retrieves such a page, we pass the page image to a vision model instead of relying solely on text." — 原文
📖 "Keep in mind that using the Vision Modality is resource-intensive, leading to increased latency and cost. It is advisable to use Vision Modality only for cases where performance on evaluation benchmarks is unsatisfactory with plain text extraction methods." — 原文
🧪 实例 用一个 flag 根据转写里是否含视觉标记,决定该页后续是否需要传图:
python
df['Visual_Input_Processed'] = df['PageText'].apply(
    lambda x: 'Y' if 'DESCRIPTION OF THE IMAGE OR CHART' in x or 'TRANSCRIPTION OF THE TABLE' in x else 'N'
)

notebook 里对第 21 页(含饼图)验证该 flag 被正确置为 "Y";随后提问"西非与中非的社会保障(social protections)拨款占比"时,系统能从对应页的饼图中读出 8%。
🔍 追问 为什么 Pinecone 索引维度要设成 3072? → 因为 embedding 用的是 text-embedding-3-large(默认 3072 维),索引维度必须与所选 embedding 模型的输出维度一致,否则无法写入/检索。
📚 拓展阅读
Q向量库/图库里的 metadata 过滤有什么用?在多模态 RAG 里怎么用?深挖·拓展中频
metadata-filter attributes file-search
⏱️ 现行
metadata(属性)过滤让检索从"整库语义搜索"收窄成"先按结构化条件筛,再在子集内语义搜索",既提升相关性又能定位到向量对应的原始文本或图片。在 image_understanding 这个 notebook 里,上传文件时给每条 context 挂上 {"month": ...} 属性,查询时在 File Search 工具里加 filterseq 精确匹配(如只查七月的评论),从而把"七月是否有 spaghetti 评论"这类带时间维度的问题限定在正确子集上。在 Pinecone 那套里,metadata 除了 pageNumbertextImagePath 还带一个 GraphicIncluded 标志,检索命中后正是靠它决定该页是走文本上下文还是把图片喂给 GPT-4o——也就是说 metadata 不只是过滤器,还是驱动"文本/视觉"分支的开关。权衡在于:属性设计得越细,过滤越精准,但入库时要维护更多字段。
术语 filters / eq(File Search 上按 key 做等值过滤的条件); attributes(上传文件时写入的键值元数据); GraphicIncluded(Pinecone 元数据里标记该页是否含图的字段); metadata tagging(为向量打标签以支持过滤与回溯原始内容)
📖 "Metadata enhances our ability to perform more granular searches, find the text or image associated with the vector, and enables filtering within the vector database." — 原文
📖 "We can analyse our dataset with natural language queries with the help of File Search." — 原文
🧪 实例 File Search 里叠加 month 属性过滤,把语义检索限定到七月:
python
response = client.responses.create(
    model="gpt-5",
    input=query,
    tools=[{
        "type": "file_search",
        "vector_store_ids": [text_image_vector_store_id],
        "filters": {"type": "eq", "key": "month", "value": "july"}
    }]
)
🔍 追问 属性是上传时一次写死的吗? → 不是,notebook 里先 files.upload 拿到 file_id,再用 vector_stores.files.update 单独把 attributes={"month": ...} 补挂上去,属性可后续更新。
📚 拓展阅读
Q为什么不直接让 LLM 生成 Cypher 查图库,而要"先抽实体再用模板拼 Cypher"?深挖·拓展低频
text-to-cypher graph-rag reliability
⏱️ 现行
notebook 先演示了用 GraphCypherQAChain 让 LLM 直接把自然语言翻成 Cypher,结果 "Help me find curtains" 生成的查询用错了关系/实体类型、返回空、答不出来。作者由此得出:让 LLM 直接生成 Cypher 相比自己写并没有增加多少价值,还容易出错——尤其容易用错 entity type 或 relationship type。更稳的做法是把职责拆开:让 LLM 只负责"决定要搜什么"(在 prompt 里约束它输出一个 key 必须精确匹配预定义 entity type 的 JSON),再由确定性的代码用模板拼出 Cypher。这样把不确定的自然语言理解压缩在一个受约束的小任务上,查询结构本身由代码保证正确,是一种典型的"用代码逻辑兜底 LLM"的可靠性设计。代价是灵活性下降:能问的关系被限定在预置的 entity/relation 模板集合内。
术语 GraphCypherQAChain(LangChain 里直接把 NL 转 Cypher 的链); define_query(让 LLM 抽实体、以 response_format=json_object 输出的函数); entity_types(预定义、供 LLM 精确匹配的实体类型字典); templated Cypher(由代码按模板拼装、结构可控的查询)
📖 "Indeed, asking an LLM to generate a Cypher query directly might result in the wrong parameters being used, whether it's the entity type or the relationship type, as is the case above." — 原文
📖 "We will instead use LLMs to decide what to search for, and then generate the corresponding Cypher queries using templates." — 原文
🧪 实例 用 JSON 模式强约束 LLM 只输出结构化实体,而非直接写查询:
python
completion = client.chat.completions.create(
    model=model,
    temperature=0,
    response_format={"type": "json_object"},
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": prompt}
    ]
)

如 "Which pink items are suitable for children?" 只被抽成 {"color": "pink", "age_group": "children"},Cypher 由代码模板生成。
🔍 追问 结论部分对"对话式 agent"是什么态度? → notebook 发现 LangChain agent 虽能从工具取到结果,却会编造答案;对这种确定性检索任务,纯代码函数比对话 agent 更可靠。
📚 拓展阅读
Q抽出的实体和图里存的值往往对不上,怎么办?相似度阈值怎么调?深挖·拓展低频
entity-matching cosine-similarity threshold
⏱️ 现行
LLM 从 prompt 抽出的实体(比如用户说 "clothes",库里存的是 "clothing")常常不是精确字面匹配,硬用等值查会漏掉大量结果。notebook 的解法是给每类实体(Product 及各 entity_type)都在 Neo4j 上建向量索引存 embedding,查询时把用户实体也 embed,用 GDS 的 gds.similarity.cosine 在关系匹配的基础上加一条"相似度 > 阈值"的过滤,从而召回语义相近而非字面相同的实体。阈值是一个可调的松紧旋钮:调低召回更多但更杂,调高更精准但更少(代码里默认 0.81,标题相似度回退搜索用 0.8)。这本质上是把符号化的图关系匹配与稠密向量的语义匹配拼在一起——图负责"结构上连得通",cosine 负责"语义上够接近"。
术语 gds.similarity.cosine(Neo4j GDS 的余弦相似度函数); threshold(相似度阈值,控制召回松紧,默认 0.81); Neo4jVector.from_existing_graph(在既有图节点上建向量索引的 LangChain 工具); create_embedding(用 OpenAI embeddings API 把实体值转向量)
📖 "However, the entities extracted might not be an exact match with the data we have, so we will use the GDS cosine similarity function to return products that have relationships with entities similar to what the user is asking." — 原文
📖 "The threshold defines how closely related words should be. Adjust the threshold to return more or less results" — 原文
🧪 实例 模板拼出的 Cypher 在关系匹配后追加 cosine 阈值过滤:
python
def create_query(text, threshold=0.81):
    query_data = json.loads(text)
    # ... 拼 MATCH (p)-[:relationship]->(entityVar:entity) ...
    similarity_data = []
    for key, val in query_data.items():
        if key != 'product':
            similarity_data.append(f"gds.similarity.cosine({key}Var.embedding, ${key}Embedding) > {threshold}")
    query += "\nWHERE "
    query += " AND ".join(e for e in similarity_data)
    query += "\nRETURN p"
    return query
🔍 追问 一个实体都没抽到时怎么办? → 走回退:用整段 prompt 的 embedding 对 Product 做标题/名称相似度搜索(similarity_search,阈值 0.8),仍找不到再提示用户补充上下文。
📚 拓展阅读
Q多模态 RAG 该怎么评估?加了图片上下文一定更好吗?深挖·拓展低频
evaluation evals-api tokens-tradeoff
⏱️ 现行
notebook 用 OpenAI Evaluation API 把情感分类当成可量化的评测任务:定义一个 custom 数据源(每条含 inputground_truth),用 string_checkeq 判据检查模型输出是否落在 positive / negative / unclear 三选一里,然后分别对"纯文本"和"文本+图像"两条 pipeline 各跑一次 eval run,再本地拉回结果对比。核心不是简单地"加图就更好",而是同时看两个维度:准确率(通过数)和平均 total_tokens——因为把图像理解并入上下文会显著抬高 token 消耗,所以要评估"多花的 token 是否换来了值得的准确率提升",并可对失败样本做 error analysis 定位模型输入。这体现了多模态 RAG 的真实权衡:视觉上下文能补齐纯文本的信息缺口,但成本上升不是免费的,需要用评测数据来决定是否启用。
术语 Evaluation API(OpenAI 用于评估模型/系统表现的 evals 接口); string_check / eq(检查输出与参考是否相等的判据); ground_truth(人工标注的真值标签); total_tokens(单次调用消耗的总 token,用于成本对比)
📖 "we can use the OpenAI Evaluation API to evaluate the performance of our system for sentiment analysis." — 原文
📖 "In this simple example, using the string_check criteria we checked if the output was one of the three possible values: positive, negative, or unclear." — 原文
🧪 实例string_check 判据建一个把输出对齐人工标签的 eval:
python
eval_obj = client.evals.create(
    name="food-categorization-eval",
    data_source_config={
        "type": "custom",
        "item_schema": {
            "type": "object",
            "properties": {
                "input": {"type": "string"},
                "ground_truth": {"type": "string"}
            },
            "required": ["input", "ground_truth"]
        },
        "include_sample_schema": True
    },
    testing_criteria=[{
        "type": "string_check",
        "name": "Match output to human label",
        "input": "{{sample.output_text}}",
        "reference": "{{item.ground_truth}}",
        "operation": "eq"
    }]
)

随后 notebook 把 text-only 与 text+image 两个 run 的通过数与平均 token 画成对比柱状图。
🔍 追问 失败样本怎么用? → 从 text-only run 的 output items 里筛出 results[0]['passed'] 为假的样本,取其 sample.inputsample.output 做 error analysis,看模型在哪些输入上判错。
📚 拓展阅读

第9章 · Agents 实战

🔥高频

Agents SDK 编排与多智能体协作

原文 原文
Q什么是 routine(例程)?它为什么能可靠地编排一个 agent 的多步流程?深挖·拓展🔥高频
routine prompt-engineering orchestration
⏱️ 现行
routine 没有严格定义,它捕捉的是"一组步骤"的思想:把用自然语言写的一串指令(用一个 system prompt 表示)连同完成这些步骤所需的工具(tools)绑在一起,就构成一个 routine——你可以叫它 routine、agent 或 assistant,本质都是"步骤 + 执行工具"。它的威力来自简单与鲁棒:routine 的指令里可以带条件分支,很像状态机或代码里的 if/else,但由 LLM 来"软执行"——LLM 能自然地引导对话走向而不会卡在死胡同(dead-end)里,这是硬编码状态机做不到的。权衡在于规模:小到中等规模的 routine,LLM 能相当鲁棒地处理这些条件;但如果往一个 routine 里塞太多不同任务,它就会开始"力不从心",这也是后面引入多 routine / handoff 的动机。
术语 routine(例程,一组自然语言步骤 + 工具);system prompt(承载指令的系统提示);soft adherance(软性遵循,LLM 对流程的柔性执行);state machine(状态机,类比 routine 里的条件分支)
📖 "Concretely, let's define a routine to be a list of instructions in natural langauge (which we'll represent with a system prompt), along with the tools necessary to complete them." — 原文
📖 "Notice that these instructions contain conditionals much like a state machine or branching in code." — 原文
📖 "the LLM can naturally steer the conversation without getting stuck in dead-ends." — 原文
🧪 实例 cookbook 里的客服 routine:一个 system prompt 内联了 4 步流程,配上 look_up_item / execute_refund 两个工具。
python
system_message = (
    "You are a customer support agent for ACME Inc."
    "Always answer in a sentence or less."
    "Follow the following routine with the user:"
    "1. First, ask probing questions and understand the user's problem deeper.\n"
    " - unless the user has already provided a reason.\n"
    "2. Propose a fix (make one up).\n"
    "3. ONLY if not satisfied, offer a refund.\n"
    "4. If accepted, search for the ID and then execute refund."
    ""
)
🔍 追问 为什么不直接用硬编码的状态机来跑客服流程? → 硬编码状态机容易在未覆盖的输入上卡死,而 routine 靠 LLM 的"软遵循"能自然处理离题、跳步、隐含前提等情况,同时仍保留可读的步骤结构。
📚 拓展阅读
Q什么是 handoff(交接)?它解决了单个 routine 的什么局限?深挖·拓展🔥高频
handoff multi-agent orchestration
⏱️ 现行
当你想给流程加更多步骤和工具时,单个 routine 到某个规模后就会开始吃力;解法是引入多个 routine,针对用户请求加载"对的"那套步骤与工具。如果把 routine 看作 agent,那么动态切换 system 指令和工具这件看似棘手的事,就能被优雅地表示成 handoff——一个 agent 把正在进行的对话交接给另一个 agent,就像打电话时被转接给另一个人。关键区别是:被转接到的 agent 拥有此前对话的完整上下文,不像真实电话那样要重新解释一遍。这样每个 agent 只需专注自己那套精简的步骤与工具,既控制了单个 routine 的复杂度,又能覆盖多样的流程。
术语 handoff(交接,把活动对话移交给另一 agent);agent(把 routine 人格化后的执行单元);context(上下文,交接时被完整继承)
📖 "Let's define a handoff as an agent (or routine) handing off an active conversation to another agent, much like when you get transfered to someone else on a phone call." — 原文
📖 "Except in this case, the agents have complete knowledge of your prior conversation!" — 原文
🧪 实例 cookbook 先手动演示交接——sales_assistant 处理下单,用户改口要退款时切到 refund_agent,同一份 messages 直接续用,新 agent 天然看到之前的下单上下文。
python
response = run_full_turn(sales_assistant, messages) # sales assistant
messages.extend(response)
# 用户改口: "Actually, I want a refund."
response = run_full_turn(refund_agent, messages) # refund agent
🔍 追问 handoff 与"重新起一个新会话"有什么本质不同? → handoff 沿用同一份消息历史,接手的 agent 继承完整上下文;重开会话会丢失此前对话,用户需重复说明,体验和准确率都更差。
📚 拓展阅读
Qhandoff 在代码里到底怎么实现?让 agent 自己决定何时交接的干净做法是什么?深挖·拓展🔥高频
handoff tool-calling implementation
⏱️ 现行
手动交接不够好,我们希望 agent 自己判断何时交接。一个简单却出奇有效的做法是给 agent 一个 transfer_to_XXX 函数(XXX 是目标 agent),模型足够聪明,会在合适时机主动调用它来表达交接的"意图"。但表达意图之后还要让交接真正发生:此前像 execute_refund 这类工具函数都返回字符串给模型,而 handoff 函数则改为返回一个 Agent 对象,以此指明要转去哪个 agent。于是把 run_full_turn 改造成:执行工具后检查返回值类型,如果是 Agent 就更新当前使用的 agent,并把结果消息替换成一句让新 agent 立即入戏的提示("Transfered to X. Adopt persona immediately.")。因为可能发生交接,run_full_turn 还需返回最后使用的 agent(用一个 Response 类包起来保持整洁),这样外层循环就能持续用正确的 agent 继续对话。
术语 transfer_to_XXX(交接函数,返回目标 Agent);Response(封装最新 agent + 新消息);Adopt persona immediately(交接后让新 agent 立即切换人格的指令)
📖 "A simple, but surprisingly effective way to do this is by giving them a transfer_to_XXX function, where XXX is some agent." — 原文
📖 "What if instead, we return an Agent object to indicate which agent we want to transfer to?" — 原文
📖 "if it's an Agent, update the agent in use!" — 原文
🧪 实例 交接函数只是简单返回目标 agent 对象;run_full_turn 在处理工具调用时检测类型并切换。
python
def transfer_to_refunds():
    return refund_agent

# run_full_turn 内部:
if type(result) is Agent:  # if agent transfer, update current agent
    current_agent = result
    result = (
        f"Transfered to {current_agent.name}. Adopt persona immediately."
    )
🔍 追问 为什么用"返回 Agent 对象"而不是返回一个字符串标记再在外部解析? → 返回对象让工具执行层直接拿到目标 agent、类型判断即可切换,无需约定字符串协议或额外解析,代码更干净且不易出错。
📚 拓展阅读
Q如何用 triage agent 把多个专职 agent 编排成一个客服系统?深挖·拓展🔥高频
triage multi-agent routing
⏱️ 现行
典型编排是引入一个 triage(分诊)agent 作为入口:它先做简短自我介绍,通过自然、不露痕迹的提问收集信息,再把用户导向正确的部门——手段就是给它一组 transfer_to_* 交接工具(如转销售、转问题与维修、升级给人工)。各专职 agent(sales_agent、issues_and_repairs_agent)各自带自己的 routine 步骤和业务工具(如 execute_orderexecute_refundlook_up_item),并且都配一个 transfer_back_to_triage,当用户聊到超出本 agent 职责范围的话题(包括要升级人工)时就交还给 triage。这样形成一个以 triage 为枢纽的星型交接网络:入口分流、专职处理、越界回流,每个 agent 的 prompt 和工具集都保持精简可控。整套循环在真正的 python 文件里跑(notebook 里的交互 input 循环跑不了)。
术语 triage agent(分诊 agent,负责分流);transfer_back_to_triage(越界回流到分诊);escalate_to_human(升级人工的工具)
📖 "Gather information to direct the customer to the right department." — 原文
📖 "Call this if the user brings up a topic outside of your purview," — 原文
🧪 实例 triage agent 只装交接工具,专职 agent 各带业务工具 + 回流工具,构成星型路由。
flowchart TD
    U[User] --> T[Triage Agent]
    T -->|transfer_to_sales_agent| S[Sales Agent]
    T -->|transfer_to_issues_and_repairs| I[Issues and Repairs Agent]
    T -->|escalate_to_human| H[Human]
    S -->|transfer_back_to_triage| T
    I -->|transfer_back_to_triage| T
python
triage_agent = Agent(
    name="Triage Agent",
    instructions=(
        "You are a customer service bot for ACME Inc. "
        "Introduce yourself. Always be very brief. "
        "Gather information to direct the customer to the right department. "
        "But make your questions subtle and natural."
    ),
    tools=[transfer_to_sales_agent, transfer_to_issues_and_repairs, escalate_to_human],
)
🔍 追问 为什么专职 agent 要单独配一个 transfer_back_to_triage,而不是让它们互相直接交接? → 回流到 triage 让分流逻辑集中在一处,避免每个专职 agent 都要认识所有兄弟 agent,降低耦合,新增部门时只改 triage 的工具集即可。
📚 拓展阅读
Q为什么以及如何把多个专职 agent 并行 fan-out / fan-in?深挖·拓展🔥高频
parallel asyncio fan-out-fan-in
⏱️ 现行
很多生产流程需要对同一段内容回答多个彼此独立的问题,若逐个串行做,不仅增加延迟,而且一旦某步失败要重试还会抬高总成本。做法是"fan-out":同时并发跑多个专职 agent;再"fan-in":把它们各自的输出汇聚给一个下游 meta-agent 产出最终面向用户的答案。用 Python 的 asyncio 时,先把每个 agent 的 Runner.run 包成协程,用 asyncio.gather 一次性并发全部,收集每个响应的 last_agent.namefinal_output 拼成带标签的摘要,再交给 meta_agent 合成。这条 asyncio 路线延迟最低、最轻量,适合你想自己精确控制 fan-in/fan-out 的图结构、且要确定性执行顺序的场景;代价是编排逻辑要自己写。该模式可迁移到客服分诊、内容审核等"对一个输入跑多路独立分析再合并"的真实场景。
术语 fan-out(扇出,并发启动多 agent);fan-in(扇入,汇聚到 meta-agent);asyncio.gather(并发调度协程);meta-agent(下游合成 agent);Runner.run(SDK 运行单个 agent)
📖 "In many production workflows you must answer several independent questions about the same piece of content." — 原文
📖 "Doing those analyses one-by-one increases latency and can increase total cost if any step fails and forces a retry." — 原文
📖 "This same pattern can be adapted to real world scenarios such as customer-support triage, content moderation, or other scenarios where you might want to run multiple independent analyses on an input and merge them into a single outcome." — 原文
🧪 实例 四个专职 agent 并发跑,结果打标签拼接后喂给 meta_agent。
python
async def run_agents(review_text: str):
    responses = await asyncio.gather(
        *(run_agent(agent, review_text) for agent in parallel_agents)
    )

    labeled_summaries = [
        f"### {resp.last_agent.name}\n{resp.final_output}"
        for resp in responses
    ]

    collected_summaries = "\n".join(labeled_summaries)
    final_summary = await run_agent(meta_agent, collected_summaries)
🔍 追问 为什么要给每个响应打上 last_agent.name 标签再交给 meta-agent? → meta-agent 的指令要求按 Features/ProsCons/Sentiment/Recommendation 分区打分,标签让它能把每段摘要对应到正确的维度,避免混淆来源。
📚 拓展阅读
QAgents SDK 里"agent as tool"是什么?它和手写 asyncio 并行有何不同?深挖·拓展🔥高频
agents-as-tools agents-sdk parallel-tool-calls
⏱️ 现行
除了手写 asyncio.gather,还能直接通过 SDK 走"agent as tool"路线并行:把每个专职 agent 用 as_tool(tool_name=..., tool_description=...) 包成工具挂到 meta-agent 上,并在其 ModelSettings 里开 parallel_tool_calls=True,让 planner(即 meta-agent)动态决定调用哪些工具、以什么顺序调用。好处是便利、由 planner 智能规划工具调用;代价是更高的延迟——这延迟一方面来自前置那次额外的规划 API 调用,另一方面来自工具调用对象带来的更高开销与更长上下文。所以它本质是把"编排逻辑"从你手里交给了模型的 planner:你少写代码、换来动态规划,但牺牲确定性与最低延迟。
术语 as_tool(把 agent 包装成工具);parallel_tool_calls(允许并行工具调用的模型设置);ModelSettings(模型行为配置);planner(负责规划工具调用的 meta-agent)
📖 "route, adding convenience and the assistance of the planner dynamically deciding which tools to call at the expense of higher latency." — 原文
📖 "This latency comes both from the additional planning API call up front, along with the higher overhead and context from the tool call objects." — 原文
🧪 实例 把四个 agent 挂成工具并开启并行工具调用。
python
meta_agent_parallel_tools = Agent(
    name="MetaAgent",
    instructions="You are given multiple summaries labeled with Features, ProsCons, Sentiment, and a Recommendation."
    " Combine them into a concise executive summary of the product review with a 1-5 star rating for each summary area.",
   model_settings=ModelSettings(
       parallel_tool_calls=True
   ),
    tools=[
        features_agent.as_tool(
            tool_name="features",
            tool_description="Extract the key product features from the review.",
        ),
        # ... pros_cons / sentiment / recommend 同理
    ],
)
🔍 追问 既然 agent-as-tool 延迟更高,为什么还有人用它? → 因为它省去手写编排、由 planner 动态决定调用哪些工具及顺序,当你需要动态规划、多层 fan-in/fan-out 又不想维护自定义图时更省心。
📚 拓展阅读
Qasyncio 手写并行 vs. agents-as-tools:该按哪些维度做取舍?深挖·拓展中频
trade-offs parallel design-decision
⏱️ 现行
cookbook 总结了三条取舍轴。其一,便利 vs 定制:偏便利就用 agent-as-tool;要自定义 agent 在多层间如何 fan-in/fan-out,就用 asyncio.gather 自建图。其二,规划 vs 确定性:想让 planner(meta-agent)动态决定调哪些工具及其顺序,用 agents as tools;想要确定性顺序则 asyncio.gather 更合适。其三,延迟敏感度:若对延迟高度敏感,用 asyncio 以避开并行工具的前置规划成本以及工具输出与更长上下文窗口的开销。三条轴共同指向一个核心权衡——把编排控制权留在代码里(确定、低延迟、要自己写)还是交给模型的 planner(动态、便利、更高延迟)。
术语 convenience vs customization(便利与定制之权衡);planning vs determinism(规划与确定性);latency sensitivity(延迟敏感度)
📖 "If you prefer convenience, the agent as tool route is the way to go. If you want to customize how agents fan in and out across multiple layers, building a graph with asyncio.gather might make more sense" — 原文
📖 "If you want your planner (in this case the meta agent) to dynamically decide which tools to call and the order, you should use agents as tools whereas asyncio.gather makes more sense if you want a deterministic order." — 原文
📖 "If you're highly sensitive to latency, you may want to use asyncio to avoid the additional upfront cost of planning the parallel tools and the overhead of tool outputs and longer context windows." — 原文
🧪 实例 决策速查——
text
需要确定性顺序 / 最低延迟 / 自定义多层图  → asyncio.gather
需要便利 / 由 planner 动态规划工具调用    → agents as tools
🔍 追问 如果既要低延迟又要动态规划,能兼得吗? → 通常不能:动态规划天然引入前置规划调用与工具调用开销,cookbook 明确指出对延迟高度敏感时应选 asyncio,即放弃 planner 的动态性来换低延迟。
📚 拓展阅读
Q一个 agent 的"完整一轮"(run_full_turn)里工具调用循环是怎么跑的?深挖·拓展中频
tool-calling function-schema agent-loop
⏱️ 现行
要执行一个 routine,先实现一个简单循环:取用户输入、追加到 messages、调模型、把模型回复追加回 messages。但模型的回复可能带工具调用,而工具调用的结果又可能引出新的工具调用,所以要把它放进一个循环里一直跑到没有更多工具调用为止。具体地:模型要求工具以 function schema 形式描述,于是用 function_to_schema 把 python 函数(靠 inspect.signature 读签名、docstring 当 description、无默认值的参数进 required)自动转成 schema;每轮调用 client.chat.completions.create 传入 system + messages + tool schemas;若回复有 tool_calls,就通过 tools_map(名字→python函数的反向映射)在 execute_tool_calljson.loads 参数并调用对应函数,把结果以 role: "tool" 的消息接回对话;没有工具调用就 break。把 agent 抽象出来后,run_full_turn(agent, messages) 直接从 agent.model / agent.instructions / agent.tools 取配置,这也为后续 handoff 改造铺好了路。
术语 function_to_schema(python 函数→function schema);inspect.signature(读函数签名);tools_map(工具名到函数的反向映射);execute_tool_call(执行工具调用并回填结果);role: "tool"(工具结果消息角色)
📖 "That response might _also_ contain a tool call, so we can just run this in a loop until there are no more tool calls." — 原文
📖 "For convenience, we can define a helper function that turns python functions into the corresponding function schema." — 原文
🧪 实例 工具调用被执行后,结果以 tool 消息接回对话继续下一轮。
python
for tool_call in message.tool_calls:
    result = execute_tool_call(tool_call, tools_map)

    result_message = {
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": result,
    }
    messages.append(result_message)
🔍 追问 为什么要用循环而不是处理完一批工具调用就返回? → 因为工具结果喂回模型后,模型可能基于新信息再次发起工具调用,循环直到无工具调用才能保证这一轮"完整"结束,不遗漏后续步骤。
📚 拓展阅读
QSwarm 是什么?可以直接在生产里用吗?深挖·拓展低频
swarm reference-repo production-readiness
⏱️ 现行
Swarm 是 cookbook 把 routines 与 handoffs 这套思想打包出来的示例库(sample library),作为概念验证(proof of concept)存在,还附带示例。它的定位很明确:仅作示例,不应直接用于生产;但你完全可以拿其中的思想与代码去搭建自己的系统。换句话说,Swarm 的价值在于展示"步骤 + 工具 + 交接"如何组织成可控的多 agent 系统,而不是提供一个开箱即用的生产框架——生产落地应把这些模式迁移到更完备的实现(如 Agents SDK)上。
术语 Swarm(routines/handoffs 的示例库);proof of concept(概念验证);sample library(示例库,非生产件)
📖 "As a proof of concept, we've packaged these ideas into a sample library called Swarm." — 原文
📖 "It is meant as an example only, and should not be directly used in production." — 原文
🧪 实例 若要在生产用同类模式,可转向 Agents SDK——parallel_agents cookbook 就是直接用 from agents import Agent, Runner 演示的。
python
from agents import Agent, Runner
🔍 追问 既然 Swarm 不建议上生产,学它还有什么意义? → 它把 routine/handoff 的最小可控实现讲透,帮助理解多 agent 编排的底层机制,再迁移到 Agents SDK 等生产级方案时能知其所以然。
📚 拓展阅读
中频

会话记忆、上下文与可靠性

原文 原文 原文
QAgents SDK 的 Session 对象相比 Responses API 的 previous_response_id 多解决了什么问题?深挖·拓展🔥高频
session-memory agents-sdk context
⏱️ 现行
Responses API 本身只提供"基础的"记忆能力:通过内置 state 加 previous_response_id 做消息链接,你要么把上一次响应的 id 传进去续接对话,要么手动把每次的 response.output 收集成 list 再作为下一次的 input 重新提交。它缺的是"自动记忆管理"——历史多长、如何裁剪、如何保持连续性都得你自己写。Agents SDK 的 Session 建在 Responses 之上,把 session 本身变成"记忆对象":你只需反复调用 session.run("..."),SDK 就替你处理上下文长度、历史和连续性,不用再手动 append 输出或追踪 ID。这里的核心权衡是,context 指模型一次能关注的 token 总窗口(输入+输出),GPT-5 上限是 272k 输入 / 128k 输出,但即便窗口再大,未经整理的历史、冗余的工具结果、噪声检索仍会把它撑爆,所以上下文管理不是优化项而是必需项——Session 就是承接这套管理逻辑的抽象层。
术语 Session(Agents SDK 的会话/记忆对象,承载历史与上下文管理); previous_response_id(Responses API 用来链接上一条响应做续接的参数); context(模型一次能 attend 的输入+输出 token 窗口)
📖 "The OpenAI Responses API includes basic memory support through built-in state and message chaining with previous_response_id." — 原文
📖 "It provides session memory on top of Responses, so you no longer need to manually append response.output or track IDs yourself." — 原文
🧪 实例 手动链接 vs. Session 托管的心智对比:
python
# 手动:自己收集 output 或传 previous_response_id
resp1 = client.responses.create(input="Hi")
resp2 = client.responses.create(input="next", previous_response_id=resp1.id)

# Session 托管:反复 run,历史/上下文长度由 SDK 处理
session.run("Hi")
session.run("next")   # 无需手动 append response.output 或追踪 id
🔍 追问 为什么"窗口够大"不等于"不用管上下文"? → 原文明确即便 272k/128k 这样的大窗口也会被 uncurated histories、redundant tool results、noisy retrievals 撑爆,所以管理是必需而非优化。
📚 拓展阅读
Q上下文管理的两种技术——Trimming 与 Summarization——各自的机制与取舍是什么?深挖·拓展🔥高频
context-management trimming summarization
⏱️ 现行
Trimming 是丢掉旧的 turn、只保留最后 N 个 turn;Summarization 是把更早的 assistant/user/tool 等消息压成结构化、更短的摘要,再注入回对话历史。二者的取舍是一组对称权衡:Trimming 确定性强、简单、零额外延迟(不需要额外的模型调用)、最近内容保真度高,但缺点是长程上下文被硬切断——早期的约束、ID、决策一旦滚出 N 就突然消失,给人"失忆"感,而且如果某个近期 turn 携带巨大工具负载,last-N 仍可能撑爆窗口。Summarization 能把几百个 turn 压成一条简洁摘要、紧凑保留长程记忆、UX 更连贯,但代价是每次刷新增加延迟与成本、摘要会有信息损失与偏置,更危险的是一旦坏事实进入摘要就会"毒化"后续行为(context poisoning),且必须记录摘要的 prompt/输出以便审计。经验法则:任务彼此独立、要可预测低延迟(ops 自动化、CRM/API 动作)选 Trimming;需要跨长时程携带决策/ID/约束(planning、RAG 重分析、policy Q&A)选 Summarization。
术语 Context Trimming(丢旧 turn、留最后 N 个 turn); Context Summarization(把旧消息压成结构化短摘要注入历史); context poisoning(错误事实进入摘要后持续污染后续上下文); summary drift(反复压缩导致对事实的重解释/漂移)
📖 "Context Trimming – dropping older turns while keeping the last N turns." — 原文
📖 "Context Summarization – compressing prior messages(assistant, user, tools, etc.) into structured, shorter summaries injected into the conversation history." — 原文
📖 "If a bad fact enters the summary, it can poison future behavior (“context poisoning”)." — 原文
🧪 实例 原文给出的快速对比表(节选维度):

flowchart TD
    H[原始多轮历史] --> D{超过 N 个 turn?}
    D -->|Trimming| T[硬切: 只留最后 N 个 turn
零额外调用 · 长程召回弱 · 风险=丢上下文] D -->|Summarizing| S[压缩: 旧内容→生成摘要
刷新点有额外成本 · 长程召回强 · 风险=失真/poisoning]

- Latency/Cost:Trimming 最低(无额外调用),Summarizing 在摘要刷新点更高
- Long-range recall:Trimming 弱(硬切断),Summarizing 强(紧凑携带)
- Risk type:Trimming 是 Context loss,Summarizing 是 Context distortion/poisoning
🔍 追问 什么时候"大工具负载"会让 Trimming 也失效? → 原文指出若最近一个 turn 含巨大 tool payload,last-N 仍可能撑爆上下文(Token spikes still possible)。
📚 拓展阅读
Q在"可靠 Agent"场景里,Compaction 与 Memory 的职责边界如何划分?深挖·拓展🔥高频
compaction memory reliability
⏱️ 现行
二者的分工用"各自被允许携带什么向前"来区分。Compaction 服务于"当前这一次长跑":当对话变长、上下文增长时,它在缩小上下文体积的同时把后续 turn 需要的工作状态携带向前,让同一次 run 能继续。Memory 服务于"未来的 run":它让后续的 sandbox-agent run 复用先前 run 的工作流经验(workflow lessons),而不必重放每一个历史 turn。关键设计原则是职责分离——上下文帮 agent 干当前的活,memory 帮未来的 agent 干得更好,而经过人工评审的产物(如 compliance memo)才保存人们真正依赖的事实。因此 memory 只应存"如何工作"的稳定流程经验与用户偏好(如先看 manifest、保留不确定性、保持被推翻的假设可见),绝不能存 case-specific 结论("Northwind Logistics violated policy"),否则会变成一份未经评审的影子合规记录。Compaction 同理:它可保留工作状态,但被引用的事实要落到生成的产物里,而不是只留在被压缩的对话状态中。
术语 Compaction(压缩当前长跑上下文、保留后续所需工作状态); Memory(跨 run 复用工作流经验); SandboxAgent(带受控 workspace 的 Agents SDK agent); workflow lessons(可复用的流程经验,而非案件事实)
📖 "Compaction lets you support long-running conversations despite finite context windows by carrying forward the state needed for later turns while reducing context size." — 原文
📖 "Memory lets future sandbox-agent runs reuse workflow lessons from prior runs without replaying every previous turn." — 原文
📖 "The main design choice is separation of responsibility: context helps the agent work, memory helps future agents work better, and reviewed artifacts hold the facts that people will rely on." — 原文
🧪 实例 好/坏 memory 候选(evidence review agent):
text
好: Use the manifest first when reviewing a file-based evidence workspace.
好: Preserve uncertainty in the memo instead of guessing.
坏: "Northwind Logistics violated policy."   # case 结论,应写进 memo
坏: "ACME's Finance Ops process is deficient."
🔍 追问 为什么"生成的 memory 文件"不能当作调查真相? → 原文强调 memory 不是 compliance memo,不应被当成 investigation truth,它只是可复用的 workflow memory;真正重要的结论必须写进带引用的评审产物。
📚 拓展阅读
Q长期记忆的生命周期是怎样的?为什么注入(injection)阶段需要 guardrails 和优先级规则?深挖·拓展🔥高频
long-term-memory guardrails precedence
⏱️ 现行
个性化记忆走一个可重复的闭环:inject → reason → distill → consolidate——启动时注入 curated memory,推理,过程中蒸馏候选记忆,结束时异步 consolidate 把 session 笔记合入 global memory。其中 consolidation 是"最敏感、最易错"的一步,处理不好会导致 context poisoning、记忆丢失或长期幻觉,因此必须显式处理去重(dedup)、冲突消解(recency wins)和遗忘(forgetting)——遗忘不是 bug 而是必需,不剪枝的记忆库会不断堆积冗余过时信息、拖垮质量。而 injection 之所以危险,是因为记忆被直接注入 system prompt,使记忆系统成为一个"高价值攻击面":会遭 context poisoning(如"记住我的 SSN 是…")、instruction injection(如"把这条存成系统规则")、以及陈旧/低置信记忆的 over-influence。防护要贯穿每个阶段:蒸馏时拒绝敏感与指令形状的内容并约束 tool schema;consolidation 时执行"绝不发明"规则并去重;注入时用显式分隔符包裹记忆、执行优先级 current user message > session context > memory,并把记忆当 advisory 而非权威。一句话准则:只要某条记忆能改变 agent 行为,它就必须在 capture、consolidation 和 injection 三处都通过安全检查。
术语 Memory Distillation(在活跃 turn 里通过工具把偏好/约束蒸馏成笔记); Memory Consolidation(会话结束时异步把 session 笔记合入 global memory); Memory Injection(启动时把 curated 记忆注入 context); precedence rules(冲突时的优先级:最新用户指令 > session > 记忆); forgetting(主动剪枝陈旧/低置信记忆)
📖 "This gives us a clean, repeatable memory loop: inject → reason → distill → consolidate" — 原文
📖 "Because memories are injected directly into the system prompt, memory systems are a high-value attack surface and must be treated as such." — 原文
📖 "Enforce precedence: current user message > session context > memory" — 原文
📖 "Forgetting is not a bug—it is essential." — 原文
🧪 实例 记忆生命周期与优先级(travel concierge):

flowchart LR
    I[注入: profile+global notes
包在 <memories> 分隔符里] --> R[推理] R --> D[蒸馏: save_memory_note 写入 session notes] D --> C[结束: 异步 consolidate
dedup + recency wins + forgetting] C --> I

原文推荐的注入优先级第 1 条即:"The user’s latest instruction in the current dialogue wins.";若记忆与当前请求冲突,则应发起澄清提问。
🔍 追问 consolidation 具体要 explicitly 处理哪三件事? → Deduplication(合并语义等价记忆)、Conflict resolution(在竞争/过时事实间取舍)、Forgetting(剪枝陈旧/低置信/被取代的记忆)。
📚 拓展阅读
Q"一个 turn"的准确定义是什么?TrimmingSession 在何时、依据什么决定保留哪些内容?深挖·拓展中频
trimming turn session
⏱️ 现行
一个 turn = 一条 user 消息,加上它之后直到下一条 user 消息之前的所有内容(assistant 回复、reasoning、tool 调用、tool 结果)。TrimmingSession 只保留最后 N 个 turn(max_turns),是内存实现,并且在每次写和读时都自动裁剪:写时 add_items(...) 先 append 新条目再立刻裁剪已存历史;读时 get_items(...) 返回裁剪后的视图,所以即便你绕过写入,读也不会泄漏旧 turn。裁剪决策逻辑是:把任何 role == "user" 的条目视为 user 消息(经 _is_user_msg),从后往前扫描收集最后 N 条 user 消息的下标,取其中最早的下标 k,保留从 k 到末尾的一切、丢弃 k 之前的一切——这样能保住完整的 turn 边界,agent 保留它需要的即时上下文(用户最近的诉求 + 中间的 assistant/tool 步骤)。选 max_turns 通常靠实验:统计对话的 turn 分布,或用 LLM 评估每段对话含几个 issue、算出每个 issue 平均需要多少 turn。
术语 turn(一条 user 消息及其后到下一条 user 消息前的全部内容); max_turns(保留的最近 turn 数); _is_user_msg(判断条目是否为 user 消息的谓词); add_items / get_items(写时裁剪 / 读时返回裁剪视图)
📖 "A turn = one user message plus everything that follows it (assistant replies, reasoning, tool calls, tool results) until the next user message." — 原文
📖 "On write: add_items(...) appends the new items, then immediately trims the stored history." — 原文
📖 "On read: get_items(...) returns a trimmed view (so even if you bypassed a write, reads won’t leak old turns)." — 原文
🧪 实例 原文的 tiny example,max_turns = 2:
text
0: user("Hi")
1: assistant("Hello!")
2: tool_call("lookup")
3: tool_result("…")
4: user("It didn't work")
5: assistant("Try rebooting")
6: user("Rebooted, now error 42")
7: assistant("On it")
# 最后两条 user 在 4 和 6,最早者是 4 → 保留 4..7,丢弃 0..3
🔍 追问 想按 token 数而不是 turn 数封顶怎么办? → 原文的 customization knobs 指出可替换 _trim_to_last_turns(...) 或加第二遍来测量 token(也可改成按 message count 封顶)。
📚 拓展阅读
QSummarization 会话如何在历史里"落"摘要?设计 summarization prompt 有哪些关键原则?深挖·拓展中频
summarization prompt-design synthetic-messages
⏱️ 现行
SummarizingSession 保留最近 N 个 user turn 原样不动,把更早的一切压成两条 synthetic 消息注入:一条 user("Summarize the conversation we had so far." 作为 shadow prompt),一条 assistant(生成的摘要)。加这条影子 user 请求是为了让 user/assistant 的对话流看起来自然、不至于混乱。它由两个旋钮控制:context_limit(触发摘要前允许的最大真实 user turn 数)与 keep_last_n_turns(摘要时保留多少最近 turn 原样),不变式是 keep_last_n_turns <= context_limit。设计 summarization prompt 的心智是"客服把 case 交接给下一位 agent"——要在不塞冗余、又不丢关键之间取平衡。原文给出的关键原则包括:标注 Milestones、按 use case 定制、做 Contradiction Check(摘要不能与自身/系统指令/工具定义冲突,这对 reasoning 模型尤其关键)、加时间戳与时序、Chunking 分类分块、记录 tool 表现、给出 guidance 与示例、以及严格的幻觉控制——因为摘要里哪怕微小的幻觉都会向前传播、用不准确信息污染未来上下文。
术语 synthetic messages(注入历史的合成 user→assistant 摘要对); shadow prompt(合成的"请总结"user 消息); context_limit(触发摘要的最大真实 user turn 数); keep_last_n_turns(摘要时保留原样的最近 turn 数); Contradiction Check(确保摘要不自相矛盾/不与系统指令冲突)
📖 "It keeps the most recent N user turns intact, summarizes everything older into two synthetic messages" — 原文
📖 "Ensure the summary does not conflict with itself, system instructions or tool definitions." — 原文
📖 "Even minor hallucinations in a summary can propagate forward, contaminating future context with inaccuracies." — 原文
🧪 实例 摘要块被注入历史顶部的形态:
text
user:      "Summarize the conversation we had so far."   # shadow prompt
assistant: {generated summary}                           # 生成摘要
<最近 keep_last_n_turns 个 turn 原样保留>

并发实现上原文强调:摘要(可能很慢)运行时释放锁,await 后重新检查条件再应用,避免 racey merge、保持幂等。
🔍 追问 摘要块为何设计成两条消息而非一条? → 原文说明 two-message summary block 便于下游工具检测或展示(通过 metadata.synthetic == True),并保住"fresh 一侧"的 turn 边界原样。
📚 拓展阅读
Q长跑评审 agent 里,Compaction 有哪几种触发方式?本例为什么选"强制检查点"?深挖·拓展中频
compaction triggering checkpoint
⏱️ 现行
原文给出三种思路:一是用 Compaction() 的自动压缩——附上能力,由 SDK 在上下文压力需要时压缩,这也是当前默认行为,即挂上 capability 后 server-side compaction 在活跃上下文增长到足够大时"有资格"运行;二是用 StaticCompactionPolicy 的阈值压缩——为想要更可预测上下文体积的环境设一个显式阈值;三是用 OpenAIResponsesCompactionSession.run_compaction({"force": True}) 的强制检查点压缩——在应用定义的阶段边界(如某个评审阶段之后、下一批证据之前)压缩。本 notebook 选强制检查点,是因为合成数据集刻意做得很小:小教程里自动压缩往往看不见(run 可能永远逼近不了模型上下文上限),OpenAIResponsesCompactionSession wrapper 存储 session 历史并让应用在阶段边界调用 run_compaction,能在不膨胀证据集的前提下让压缩可见。生产上,自动压缩常是最简单的起点,阈值压缩适合想要更紧的运维策略时——但两者都依赖渲染后的上下文越过阈值。最佳实践是:在有意义的工作流边界压缩而非每个 turn 都压;为下一阶段保留足够工作状态;被引用的事实要留在生成的产物而非仅在压缩后的对话状态里。
术语 Compaction()(自动压缩能力,上下文压力时由 SDK 触发); StaticCompactionPolicy(显式阈值的压缩策略); run_compaction({"force": True})(在阶段边界强制压缩的检查点); OpenAIResponsesCompactionSession(存储 session 历史、支持强制压缩的 wrapper)
📖 "Automatic compaction with Compaction(): attach the capability and let the SDK compact when context pressure requires it." — 原文
📖 "With the Compaction() capability, server-side compaction is eligible to run when the active context grows large enough." — 原文
📖 "compact at an application-defined phase boundary, such as after a major review phase and before the next evidence batch." — 原文
🧪 实例 阶段边界处的强制压缩伪代码:
python
# 在一批证据评审结束、下一批开始前做一次检查点压缩
session.run_compaction({"force": True})
# 好处:不用把证据集撑大,就能让 compaction 可见

最佳实践一条:"Compact at meaningful workflow boundaries, not after every turn."
🔍 追问 为什么小教程里自动压缩"难以看到"? → 因为 run 可能永远逼近不了模型上下文上限,自动压缩没被触发;调低 StaticCompactionPolicy 有帮助,但仍取决于渲染上下文越过阈值。
📚 拓展阅读
Q对 travel concierge 这类 agent,为什么选 state-based memory 而不是 retrieval-based memory?深挖·拓展中频
state-based-memory retrieval personalization
⏱️ 现行
因为旅行决策依赖连续性、优先级和不断演化的偏好,而不是临时检索。一个 travel agent 必须对"当前、连贯的用户 state"(loyalty 计划、座位偏好、预算、签证约束、行程意图,以及"这次我想睡觉"这类临时覆盖)进行推理,并在机票、酒店、保险、后续跟进间一致地应用它。Retrieval-based memory 把过往交互当作松散相关的文档,对措辞脆弱、容易漏掉 override、也无法随时间调和冲突或更新;相比之下 state-based memory 把用户知识编码为结构化、权威的字段并带清晰的优先级(global vs session),支持"信念更新"而非"事实堆积",让决策确定化,不必依赖脆弱的语义搜索。这让 agent 少像搜索引擎、多像一个持久的 concierge——跨会话保持连续、随上下文适配,并在记忆相关时可靠使用它,而不是仅在"检索成功时"才用。实践上,做法是把结构化 profile 键与非结构化 memory notes 结合:结构化字段从可信内部源持续 hydrate、保持更新,非结构化记忆补足需要灵活性的空白;并只把相关切片在恰当时机注入上下文。
术语 state-based memory(把用户知识编码为结构化权威字段+notes,带 global/session 优先级); retrieval-based memory(把过往交互当松散文档做语义搜索); RunContextWrapper(承载持久 state 的类); belief updates(信念更新,取代单纯的事实堆积)
📖 "state-based memory is better suited than retrieval-based memory for a travel concierge AI agent because travel decisions depend on continuity, priorities, and evolving preferences—not ad-hoc search." — 原文
📖 "In contrast, state-based memory encodes user knowledge as structured, authoritative fields with clear precedence (global vs session), supports belief updates instead of fact accumulation, and enables deterministic decision-making without relying on fragile semantic search." — 原文
🧪 实例 state 对象把结构化 profile 与 notes 分开(节选):
json
{
  "profile": {
    "seat_preference": "aisle",
    "loyalty_status": {"airline": "United Gold", "hotel": "Marriott Titanium"},
    "active_visas": ["Schengen", "US"]
  },
  "global_memory": {
    "notes": [
      {"text": "User usually prefers aisle seats.", "last_update_date": "2024-06-25", "keywords": ["seat_preference"]}
    ]
  }
}
🔍 追问 结构化字段与非结构化 notes 如何分工? → 结构化字段走严格格式、校验后直接用于逻辑/过滤/booking API;非结构化 notes 是叙述性的,面向推理与拟人化决策(如"User usually prefers aisle seats.")。
📚 拓展阅读
中频

语音 Agent 与深度研究 Agent

原文 原文 原文
Q用 Agents SDK 搭一个语音助手,为什么先拆成多个专职 agent 再套语音,而不是直接做一个语音大模型?深挖·拓展🔥高频
voice-agent agents-sdk architecture
⏱️ 现行
cookbook 给出的路线是"先文本、再语音、且多 agent 编排"。核心动机是把一个通吃的助手拆成职责单一的 agent:一个 Triage Agent 负责迎接用户、判定意图并路由,再把请求分发给三个专职 agent(Search Agent 走 Responses API 的 WebSearchTool 做实时搜索、Knowledge Agent 走 FileSearchTool 检索 OpenAI 托管的向量库、Account Agent 用 function_tool 触发自定义 API)。这样做的权衡是:单个 agent 要处理的任务范围越窄,准确率越高,而且新增用例只需再挂一个 agent,扩展成本极低。语音不是从头重写,而是在已验证好的文本工作流外面套一层管道——VoicePipeline 负责把音频转成文本、跑同一套 agent 工作流、再把文本合成语音播放,SingleAgentVoiceWorkflow 让你复用之前那套文本 agent。这就把"做对功能"和"做成语音"两件事解耦,可以逐个用例评估迭代后再一次性转语音。

flowchart TD
    A[麦克风音频] --> B[VoicePipeline: STT]
    B --> C[Triage Agent 判定意图]
    C -->|账户| D[Account Agent / get_account_info]
    C -->|产品FAQ| E[Knowledge Agent / FileSearchTool]
    C -->|实时| F[Search Agent / WebSearchTool]
    D & E & F --> G[VoicePipeline: TTS 播放]
术语 VoicePipeline(语音管道:STT→工作流→TTS 的一体接口); SingleAgentVoiceWorkflow(把既有文本 agent 工作流包成语音工作流); Triage Agent(分诊/路由 agent); WebSearchTool/FileSearchTool(Responses API 内置的联网搜索与文件检索工具)
📖 "This allows us to split the functionality for each use case into a separate agent, in turn reducing the complexity/range of tasks that a single agent could be asked to complete and increasing accuracy." — 原文
📖 "The VoicePipeline class provides an interface for transcribing audio input, executing a given agent workflow and generating a text to speech response for playback to the user, whilst the SingleAgentVoiceWorkflow class enables us to leverage the same agent workflow we used earlier for our text-based workflow." — 原文
🧪 实例 把文本工作流转语音只需寥寥几行——用同一个 triage_agent 构造管道,录音后喂 buffer 即可:
python
from agents.voice import AudioInput, SingleAgentVoiceWorkflow, VoicePipeline

pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(triage_agent))
audio_input = AudioInput(buffer=recording)
with trace("ACME App Voice Assistant"):
    result = await pipeline.run(audio_input)
async for event in result.stream():
    if event.type == "voice_stream_event_audio":
        response_chunks.append(event.data)
🔍 追问 Triage Agent 是靠什么把请求交给下游 agent 的? → 靠 handoffs——triage_agent 定义了 handoffs=[account_agent, knowledge_agent, search_agent],SDK 按意图把控制权转交给对应 agent,不需要你手写路由 if/else。
📚 拓展阅读
Q给带 handoff 的 agent 写指令时,为什么推荐用 prompt_with_handoff_instructions 包一层?深挖·拓展🔥高频
handoff triage prompt
⏱️ 现行
在多 agent 架构里,Triage Agent 的指令既要描述它自己的职责(迎接用户、判意图),又要正确处理"把对话移交给下游 agent"这件事。cookbook 用 prompt_with_handoff_instructions 把你写的路由指令再包一层,它会附加关于如何对待 handoff 的额外指导,官方建议:凡是定义了 handoffs 且带有一套指令的 agent 都应该套这个函数。权衡点在于:如果只给裸指令,模型可能不清楚何时该移交、移交后如何表现;包一层能把 handoff 的行为规范注入进去,让路由更稳。实际写法就是把一段自然语言路由规则(账户类→AccountAgent、产品 FAQ→KnowledgeAgent、实时搜索→SearchAgent)传给它,再配 handoffs 列表即可。
术语 prompt_with_handoff_instructions(为带 handoff 的 agent 追加移交行为指导的辅助函数); handoffs(agent 可移交对象列表); handoff(把对话控制权从一个 agent 转交给另一个)
📖 "Here we're using the prompt_with_handoff_instructions function, which provides additional guidance on how to treat handoffs and is recommended to provide to any agent with a defined set of handoffs with a defined set of instructions." — 原文
🧪 实例
python
triage_agent = Agent(
    name="Assistant",
    instructions=prompt_with_handoff_instructions("""
You are the virtual assistant for Acme Shop. Welcome the user and ask how you can help.
Based on the user's intent, route to:
- AccountAgent for account-related queries
- KnowledgeAgent for product FAQs
- SearchAgent for anything requiring real-time web search
"""),
    handoffs=[account_agent, knowledge_agent, search_agent],
)
🔍 追问 三个测试 query 是怎么验证路由正确的? → 通过 Traces dashboard 看每次运行的 handoff/工具调用链;cookbook 里三条 query(账户余额、dynamite dispenser 参数、duck hunting gear 趋势)分别被正确路由到 Account/Knowledge/Search agent。
📚 拓展阅读
QDeep Research API 到底做了什么?和普通一次 LLM 调用有何本质区别?深挖·拓展🔥高频
deep-research responses-api agentic
⏱️ 现行
Deep Research API 面向的是需要推理、规划、跨真实世界信息做综合的复杂研究流程:你给一个高层 query,它返回结构化、富引用的报告。和普通调用最大的区别是它是 agentic 的——发请求后,模型自主拆解子问题、调用 web search 和 code execution 等工具、再把结果综合成最终结构化响应,整个规划过程在 ChatGPT 里是被隐藏的,而 API 给你直接的编程访问。你通过 responses 端点使用两个模型:o3-deep-research-2025-06-26(偏深度综合、质量更高)和 o4-mini-deep-research-2025-06-26(更轻更快、适合延迟敏感场景),这是一个明确的质量/延迟权衡。请求上要点:把角色写进 system/developer message、把 reasoning.summary 设成 auto、带上必需的 web_search_preview 工具并可选加 code_interpreter;由于一次任务可能跑好几分钟,应把 background 设为 True 异步执行,避免超时和连接问题。响应里不仅有最终报告,还带内联引用(每条 annotation 有 start_index/end_index/title/url),以及所有中间步骤(reasoning/web_search_call/code_interpreter_call)可供调试与追溯。
术语 o3-deep-research / o4-mini-deep-research(深研两档模型:质量 vs 速度); web_search_preview(必需的联网搜索工具); code_interpreter(可选的代码执行工具,用于解析数据/画图); background(异步后台模式,规避长任务超时); annotations(内联引用元数据,带字符跨度与源 URL)
📖 "The Deep Research API enables you to automate complex research workflows that require reasoning, planning, and synthesis across real-world information." — 原文
📖 "When you send a request, the model autonomously plans sub-questions, uses tools like web search and code execution, and produces a final structured response." — 原文
📖 "Since a Deep Research task can take several minutes to execute, enabling background mode will allow you to run the request asynchronously without having to worry about timeouts or other connectivity issues." — 原文
🧪 实例 一次典型的深研调用——挑模型、开 reasoning summary、挂搜索+代码工具:
python
response = client.responses.create(
  model="o3-deep-research",
  input=[
    {"role": "developer", "content": [{"type": "input_text", "text": system_message}]},
    {"role": "user", "content": [{"type": "input_text", "text": user_query}]}
  ],
  reasoning={"summary": "auto"},
  tools=[
    {"type": "web_search_preview"},
    {"type": "code_interpreter", "container": {"type": "auto", "file_ids": []}}
  ]
)

取中间步骤时按 type 过滤即可,例如 next(item for item in response.output if item.type == "web_search_call") 拿到执行过的搜索 query。
🔍 追问 报告里的引用怎么变成可点击的参考文献列表? → 遍历 response.output[-1].content[0].annotations,每条含 titleurlstart_index/end_index 字符跨度,可据此构建 bibliography、加超链接、并高亮回溯有数据支撑的论断。
📚 拓展阅读
QDeep Research API 为什么不像 ChatGPT 那样先反问澄清?开发者该如何补上这一步?深挖·拓展中频
deep-research prompting clarification
⏱️ 现行
在 ChatGPT 里 Deep Research 提交后常会追问,是因为它用一个中间模型(如 gpt-4.1)先厘清你的意图、收集偏好/目标/约束,再开研究,从而让搜索更精准。而 Deep Research API 刻意跳过这一步:模型期望拿到完整成形的 prompt,不会索要额外上下文也不会替你补缺,收到什么就直接开研究。这把控制权和责任都交给了开发者——输入质量直接决定输出质量。cookbook 给两种补救办法:一是用一个轻量模型(如 gpt-4.1)做 prompt rewriter,在传给研究模型前把 query 扩写/具体化;二是先用轻量模型温和地向用户提澄清问题,拿到回答后再套 rewriting prompt 生成清晰指令。权衡在于:直接对开放式 query 做广撒网研究虽能覆盖多种场景,但会带来啰嗦、更高延迟和更多 token 消耗,尤其是多目的地行程、对比研究、选品这类复杂规划任务;先澄清能显著收窄范围、降本增效。
术语 prompt rewriter(改写器:用轻量模型把用户 query 扩写为详尽研究指令); clarifying prompt(澄清器:先向用户提 3-6 个最关键问题再研究); fully-formed prompt(成形提示:深研 API 期望的完整输入)
📖 "In contrast, the Deep Research API skips this clarification step. As a developer, you can configure this processing step to rewrite the user prompt or ask a set of clarifying questions, since the model expects fully-formed prompts up front and will not ask for additional context or fill in missing information; it simply starts researching based on the input it receives." — 原文
🧪 实例 用 gpt-4.1 先改写再研究,是官方推荐的两段式管道:
python
# 第一段:轻量模型把用户跟进信息改写成给研究员的详尽指令
instructions_for_DR = client.responses.create(
    instructions=suggested_rewriting_prompt,
    model="gpt-4.1-2025-04-14",
    input=user_follow_up,
)
instructions_for_deep_research = instructions_for_DR.output[0].content[0].text

# 第二段:把改写后的指令交给深研模型
deep_research_call = client.responses.create(
  model="o4-mini-deep-research-2025-06-26",
  input=[{"role": "developer", "content": [{"type": "input_text", "text": instructions_for_deep_research}]}],
  reasoning={"summary": "auto"},
  tools=[{"type": "web_search_preview"}],
)
🔍 追问 rewriting prompt 里为什么专门要求"该用表格时明确要求研究员给表格"? → 因为深研模型不会自动决定最佳呈现格式;在指令里显式点名(如对比智能手机型号时列出各型号特性/价格/评分)能让结构化输出更清晰,这属于"把期望输出格式写进 prompt"这条准则。
📚 拓展阅读
Q直接把文本 agent 转成语音,输出为什么听起来机械?优化语音体验要动哪两处?深挖·拓展中频
voice-agent tts prompting
⏱️ 现行
直接把文本 agent 套上语音后,响应在语气和格式上都没有为语音优化,读出来会显得机械、不自然。cookbook 的优化落在两个层面。第一层是文本内容:给所有下游 agent 加一段共同的 voice_system_prompt,规定输出要用自然口语化的友好语气、每步一到两句短句、避免术语用大白话、只给必要信息以免信息过载——这是在"生成的文本"这一端就为口播做准备。第二层是语音合成:用 instructions 字段指挥 Agents SDK 默认的 TTS 模型 gpt-4o-mini-tts,可以控制个性、发音、语速和情绪;cookbook 给了 health/coach/themed_character 等不同人设的 instructions 示例,并通过 TTSModelSettings + VoicePipelineConfig 把自定义设置注入管道。两处配合的意义在于:光改文本还不够自然,光调 TTS 也救不了啰嗦的措辞,只有内容为口播优化 + 合成层给定语气人设,才能得到措辞自然、投递也有温度的语音。
术语 voice_system_prompt(注入各 agent 的口播文本规范:短句、口语、少术语); gpt-4o-mini-tts(Agents SDK 默认 TTS 模型); TTSModelSettings.instructions(控制个性/发音/语速/情绪的合成指令); VoicePipelineConfig(把自定义 TTS 设置注入语音管道)
📖 "As we've simply converted our text-based agents into voice-based ones, the responses are not optimised in their output for either tone or format, meaning they feel robotic and unnatural." — 原文
📖 "Next, we can instruct the default OpenAI TTS model used by the Agents SDK, gpt-4o-mini-tts, on how to communicate the audio output of the agent generated text with the instructions field." — 原文
🧪 实例 自定义 TTS 设置并挂进管道配置:
python
custom_tts_settings = TTSModelSettings(
    instructions="Personality: upbeat, friendly, persuasive guide"
    "Tone: Friendly, clear, and reassuring, creating a calm atmosphere and making the listener feel confident and comfortable."
    "Pronunciation: Clear, articulate, and steady, ensuring each instruction is easily understood while maintaining a natural, conversational flow."
    "Tempo: Speak relatively fast, include brief pauses and after before questions"
    "Emotion: Warm and supportive, conveying empathy and care, ensuring the listener feels guided and safe throughout the journey."
)
voice_pipeline_config = VoicePipelineConfig(tts_settings=custom_tts_settings)
pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(triage_voice_agent), config=voice_pipeline_config)
🔍 追问 除了友好口播,能不能做完全不同的语音风格? → 能;换成 themed_character_assistant 那套 instructions(Olde English、骑士史诗腔、在 "Lo!"/"Hark!" 后停顿)就得到戏剧化角色语音,说明 instructions 对语气/情绪/停顿有很强控制力。
📚 拓展阅读
Q想让 Deep Research 用上公司内部文档,机制上怎么接?为什么用 MCP?深挖·拓展中频
deep-research mcp retrieval
⏱️ 现行
默认 Deep Research 只会去公网搜。要把私有知识库纳入研究,cookbook 的做法是挂 MCP 工具——Deep Research 模型和 Responses API 都支持基于 MCP 的工具,因此你能扩展它去查询自己的私有知识库或第三方服务。示例里配了一个 internal_file_lookup 的 MCP 工具,让深研按需拉取组织内部的 semaglutide 研究;这个 MCP server 是 OpenAI File Storage 服务的代理,会自动把你上传的文件向量化以便高效检索。接入方式是在 tools 里加一个 {"type": "mcp", ...} 条目,给出 server_labelserver_url 和审批策略。同时要在 system message 里显式告诉模型"有一个内部文件查找工具,同一个文件已取过就别再 fetch,并优先纳入这些数据",否则模型未必会主动优先用内部源。好处是内部来源的引用同样进入 annotations,URL 指向 platform.openai.com/storage/files/...,和公网引用一样可追溯;所有 mcp_call 也会出现在 response.output 中可审计。
术语 MCP(Model Context Protocol,让深研/Responses API 调用私有或第三方工具的协议); internal_file_lookup(示例中的内部文件查找 MCP 工具); require_approval(MCP 工具审批策略,示例设为 never); mcp_call(响应里可审计的 MCP 工具调用记录)
📖 "The Deep Research models and the Responses API both support MCP-based tools, so you can extend them to query your private knowledge stores or other 3rd party services." — 原文
📖 "The MCP server is a proxy for the OpenAI File Storage service that automagically vectorizes your uploaded files for performant retrieval." — 原文
🧪 实例 在工具列表里追加 MCP 工具:
python
tools=[
    {"type": "web_search_preview"},
    {  # ADD MCP TOOL SUPPORT
      "type": "mcp",
      "server_label": "internal_file_lookup",
      "server_url": "https://<your_mcp_server>/sse/",  # Update to the location of *your* MCP server
      "require_approval": "never"
    }
]
🔍 追问 怎么从报告里挑出确实引用了内部文件的片段? → 遍历 annotations,用 citation.url.startswith("https://platform.openai.com/storage/files";) 过滤,再用 start_index/end_index 从报告文本里切出被引用的确切 span 及其前文。
📚 拓展阅读
Q深度研究 agent 的"短期会话记忆"和"长期持久发现"为什么要分开?工程上怎么落?深挖·拓展中频
deep-research memory agents-sdk session
⏱️ 现行
长跑的研究 agent 光靠重放对话不够,还需要跨会话记住耐用的结论、决策与可复用上下文。cookbook 明确区分两类记录、两种生命周期:短期 session items 是当前 agent loop 用来重放的原始对话状态,通过 OpenAI Agents SDK 的 Session 协议管理——runner 在每轮开始索要历史 items、结束时追加新 items;长期 durable findings 则是经过策划、可语义检索的结论,只通过专门的 save_research_finding 工具显式写入。二者"是同一存储上的不同访问模式":前者用 Session,后者用 add_memory()。为什么分开?因为原始模型/工具消息又大又噪,不该被当成长期研究发现,除非某个工具刻意把一条精炼结论"提拔"上去;否则长期库会被低价值内容塞满。要真正驱动 agent 用好记忆,还得在 instructions 里显式命令它"先 recall 再研究、把值得跨会话记住的结论逐条 save",否则 LLM 会把记忆当可有可无的装饰。落地上,Session 协议只需实现四个 async 方法(get_items/add_items/pop_item/clear_session)即可接自定义后端,无需改 runner,传 session= 即用。
术语 Session 协议(SDK 短期会话持久化接口,四个 async 方法); session items(当前 loop 重放用的原始对话状态); durable findings(通过工具显式写入的可检索长期结论); save_research_finding / recall_research_findings(显式存/取长期记忆的工具); Runner.run(..., session=session)(把会话后端注入运行)
📖 "The OpenAI Agents SDK Session protocol handles short-term session persistence: the runner asks for previous items and appends new items during the agent loop." — 原文
📖 "The session handles short-term conversation replay for the current agent loop. Durable findings should be concise conclusions written through explicit tools, not raw conversation or tool output dumped into long-term memory." — 原文
🧪 实例 agent 的 instructions 直接把"先回忆、再检索、显式保存"编码进系统提示:
python
INSTRUCTIONS = """You are a deep-research agent specialising in human genome exploration.

For every research question:
1. FIRST call `recall_research_findings` to check what is already known from prior sessions.
2. If the prior findings are insufficient or outdated, call `tavily_search` for current sources.
3. Synthesise a clear, cited answer. Prefer authoritative sources (NCBI, OMIM, PubMed).
4. Call `save_research_finding` for each durable conclusion worth remembering across sessions.
   Save one finding per call; keep findings atomic and one sentence long.
5. Present the final answer to the user with inline citations (URLs).

Do not save conversational acknowledgements as findings. Only save factual conclusions.
"""
🔍 追问 怎么证明这套记忆真的能跨会话续上? → 新建一个不同 session_id 但同 user_id/agent_id 的会话,提一个依赖先前 durable findings 的后续问题;如果 agent 不靠上一段会话的 transcript 就能回忆起 BRCA1 的发现,就端到端验证了持久记忆的连续性。
📚 拓展阅读
中频

Agent 评估与改进闭环

原文 原文 原文
QOnline evaluation 与 Offline evaluation 有什么区别?各自解决什么问题?深挖·拓展🔥高频
Evaluation Observability Agents SDK
⏱️ 现行
这是 Agent 评估最基础的分层。Online evaluation 指在真实生产环境、真实用户交互中持续监控 agent 的表现——它捕捉线上才暴露的问题,常见要监控的四类指标是 Costs(由 instrumentation 采集的 token 用量换算近似成本)、Latency(每一步或整段运行耗时,用来定位瓶颈)、User Feedback(UI 里的赞/踩直接打分)、以及 LLM-as-a-Judge(用另一个 LLM 近实时判定输出的 toxicity/correctness)。但线上反馈是滞后的、无法在改动上线前拦住问题,所以还需要 Offline evaluation:在开发前或开发中做的系统性检查,典型做法是准备一个 benchmark dataset(prompt 与期望输出成对),跑 agent,再把输出与期望对比或用额外打分机制评估。两者是互补的权衡:online 覆盖真实分布但只能事后发现,offline 可控可复现、能在上线前守住质量却受限于数据集覆盖面。原文的 dataset 实验里对同一 50 题跑三次、只改 WebSearchTool 的 context size,correctness 从 0.89 提升到 0.92,正是 offline 支持 A/B 对比的价值体现。
术语 Online Evaluation(线上评估,生产真实流量上的持续监控); Offline Evaluation(离线评估,上线前用固定数据集做系统性检查); benchmark dataset(基准数据集,prompt/expected output 对); LLM-as-a-Judge(用 LLM 当裁判自动评分)
📖 "Online Evaluation refers to evaluating the agent in a live, real-world environment, i.e. during actual usage in production. This involves monitoring the agent’s performance on real user interactions and analyzing outcomes continuously." — 原文
📖 "This helps maintain quality and reliability before rolling changes into production." — 原文
🧪 实例 离线评估的最小闭环——先建 dataset,再逐条跑 agent 并把 trace 关联回 dataset item:
python
# helper: run_openai_agent()
# 1. Starts a Langfuse span
# 2. Runs our agent on the prompt
# 3. Records the trace ID in Langfuse
# 然后 loop 每个 dataset item,运行 agent,链接 trace 到 item,
# 可选地附一个 quick evaluation score。
# 例:对同一 50 题跑 3 次,仅改 WebSearchTool 的 context size,
# correct_answer 分数从 0.89 -> 0.92(由 LLM-as-a-Judge Evaluator 打分)
🔍 追问 online 评估里的 User Feedback 具体怎么落到某一条回答上? → 用户发消息时捕获该轮的 OpenTelemetry trace ID,用户点赞/踩时把 score attach 到对应 trace,从而把主观反馈定位到具体 trace。
📚 拓展阅读
Q什么是 Agent improvement loop(改进飞轮)?它由哪几个环节构成、如何自我强化?深挖·拓展🔥高频
Improvement Loop Traces Evals Codex
⏱️ 现行
Agent improvement loop 是一个把「运行中学到的东西」持续沉淀回系统的飞轮,把评估从「一次性打分」升级为「工程闭环」。它从真实 traces 出发,叠加 human 和 model 两路 feedback,把 feedback 转成可反复重跑的 evals(这里用 Promptfoo),再用 HALO(Hierarchical Agent Loop Optimization)对整个 harness 连同 traces、两路 feedback、生成的 evals、Promptfoo 结果一起推理,产出一份排好序的改动建议,最后打包成 codex_handoff.md 交给 Codex 去实现代码改动。飞轮的关键在于每个环节各司其职又相互喂料:traces 记录发生了什么、feedback 解释什么最重要、evals 把期望固化成可复用测试、Codex 把结论落地为改动。它的权衡在于自动化程度可调——可以停在「开发者审阅 PR」这样的 reviewed loop,也可以接成自动开/合/部署 PR 的 closed loop;无论哪种模式,human feedback 都保持核心地位,因为它决定系统学什么、改什么。
术语 flywheel(改进飞轮,环环相扣自我强化的循环); harness(围绕模型的完整契约:指令/工具/路由/输出要求/校验); HALO(Hierarchical Agent Loop Optimization,从 traces 排序下一步 harness 改动的方法与库); Promptfoo(开源 eval/red-teaming CLI,把 feedback 变成可重跑测试); codex_handoff.md(承载 HALO 诊断+排序建议+证据+实现指引的交接文件)
📖 "We start with real traces, add human and model feedback, turn that feedback into evals, and use the resulting evidence to propose the next harness changes for Codex to implement." — 原文
📖 "The flywheel preserves what you learn from each run. Traces show what happened, feedback explains what mattered, evals make those expectations reusable, and Codex can act on the resulting change set." — 原文
📖 "The core workflow is the same in either case: traces plus human and model feedback become concrete harness changes instead of remaining disconnected comments." — 原文
🧪 实例 改进飞轮的环节串联:
flowchart LR
  A[real traces] --> B[human + model feedback]
  B --> C[Promptfoo evals]
  C --> D[Promptfoo validation gate]
  D --> E[HALO 优化排序]
  E --> F[codex_handoff.md]
  F --> G[Codex 实现 harness 改动]
  G --> A
🔍 追问 为什么要 human feedback 和 model-generated feedback 两路并存? → model 那一路(LLM 复审同一批 traces 提出反复出现的问题)提升覆盖率,human/subject-matter expert 那一路补充扎根于实际工作的领域判断,二者互补。
📚 拓展阅读
  • Promptfoo — 把 feedback 固化成可重跑 eval 的开源框架
  • HALO — 从执行 traces 优化 agent harness 的方法与包
  • OpenAI Agents SDK — 提供 runner/tracing hooks 的底座
QLLM-as-a-Judge 是怎么工作的?搭一套自动评估要哪几步?深挖·拓展🔥高频
LLM-as-a-Judge Evaluation Model-based Eval
⏱️ 现行
LLM-as-a-Judge 是另一种自动评估 agent 输出的方式:额外发起一次独立的 LLM 调用,去衡量输出的 correctness、toxicity、style 或任何你关心的标准。工作流分四步:先定义一个 Evaluation Template(例如「判断文本是否 toxic」),设定一个充当裁判的 judge-model(例中用 gpt-4o-mini),每次 agent 产出后把输出连同模板喂给裁判 LLM,裁判返回一个评分或标签并记录到可观测工具里。它的价值在于能近实时、可规模化地对海量输出打质量分,既能做 online(生产流量上实时判 toxicity/correctness),也能做 offline(在 dataset run 里由 LLM-as-a-Judge Evaluator 给 correctness 打分并支持多组配置对比)。权衡是裁判本身也是模型、会有偏差与成本,所以通常用便宜模型当裁判、并需要人工校准 rubric 严格度,而不是盲信分数。
术语 Evaluation Template(评估模板,界定裁判要判什么的 prompt); judge-model(裁判模型,例中为 gpt-4o-mini); score/label(裁判产出的评分或标签,回写到 observability); toxicity/correctness(常见判定维度)
📖 "LLM-as-a-Judge is another way to automatically evaluate your agent's output. You can set up a separate LLM call to gauge the output’s correctness, toxicity, style, or any other criteria you care about." — 原文
📖 "Use a separate LLM to evaluate your agent’s output in near real-time (e.g., checking for toxicity or correctness)." — 原文
🧪 实例 一次 toxicity 判定的四步流水:
text
1. Evaluation Template: "Check if the text is toxic."
2. judge-model: gpt-4o-mini
3. agent 每次产出 -> 连同模板传给 judge LLM
4. judge 返回 rating/label -> 记录到 observability(例中结果判为 "not toxic")
🔍 追问 在离线 dataset 评估里,LLM-as-a-Judge 是怎么打出 correct_answer 分数的? → 由一个配置好的 LLM-as-a-Judge Evaluator,基于 dataset 里给定的 sample answer 判定该问题回答的 correctness,再作为 dataset run 的可比分数(如 0.89 vs 0.92)。
📚 拓展阅读
Q为什么多 agent 系统需要 Macro evals?它和 lower-level evals 是什么关系?深挖·拓展🔥高频
Macro Evals Multi-Agent Evaluation
⏱️ 现行
因为在 agentic 系统里,最终答案只是一条更长工作流的最后一个事件——一个 release 建议看起来很合理,但 trace 可能显示 pricing agent 忽略了一个 incentive、supply agent 漏了一次 stockout、或 orchestrator 绕过了必需的 review 步骤。单看 final answer 无法发现这些,要改进系统就得跨整个 traces 群体看反复出现的行为。为此原文把问题分两层:Lower-level evals 逐个给 individual agents、handoffs、tools、completed runs 打分(例中由 Promptfoo 扮演,判 final decision quality、policy correctness、specialist routing、market drift、review appropriateness),产出的是单条 trace 的局部信号 eval_findingMacro evals 则跨大量 lower-level findings 去问:哪类问题在重复、集中在哪、该先查工作流的哪一部分。二者是「原料—地图」的关系:agent 级 evals 制造原始信号,macro evals 把成千上万个这样的信号汇成一张反复出现行为的地图。核心权衡是——目标不是给每条 trace 建完美分类,而是把海量 agent 事件压缩成少数几个技术方和业务方都能理解的 pattern。
术语 Lower-level evals(下层评估,逐个 agent/handoff/tool/run 打分); Macro evals(宏观评估,跨群体找反复出现的行为模式); eval_finding(单条 trace 的局部风险信号); behavior_pattern(跨多条 trace 发现的复现模式); handoff(orchestrator 把任务委派给专家 agent)
📖 "Multi-agent systems make this harder because a final answer is only the last event in a longer workflow." — 原文
📖 "To improve the system, teams need to see recurring behavior across the whole population of traces." — 原文
📖 "The core lesson is simple: agent-level evals tell us which local behaviors look risky, while macro evals tell us what those risks become at system scale." — 原文
🧪 实例 原文贯穿全文的四个面向读者的标签,构成从个案到群体的分析主线:
text
case_type       -> 生成的业务场景(如 clean order / validation block / supplier substitution / pricing exception)
run_outcome     -> 运行如何结束(completed / awaiting review / blocked / failed)
eval_finding    -> lower-level 信号:哪里看起来错了或有风险
behavior_pattern-> 跨多条 trace 发现的复现模式
# 公开分析路径: case_type -> run_outcome -> eval_finding -> behavior_pattern
# 前三个标签在聚类前已知,第四个在 discovery 后出现
🔍 追问 在这个例子里 Promptfoo 具体判哪些 agent 级问题? → 判 final decision quality、policy correctness、specialist routing、market drift(是否响应带时间戳的市场信号)、以及 review/escalation 是否与风险成比例。
📚 拓展阅读
QTrace 与 span 是什么?为 agent 接入可观测性能拿到哪些指标?深挖·拓展中频
Tracing Observability OpenTelemetry
⏱️ 现行
可观测性是所有评估闭环的地基。一个 trace 包含若干 span,每个 span 代表 agent 逻辑里的一步——例如一次 tool call(get_weather)和多次 LLM call(用 gpt-4o 的 Responses API)会各自成为 trace 下的子 span。接入方式是给 OpenAI Agents SDK 挂 instrumentation(原文用 Pydantic Logfire 的 instrumentation 把 traces 发到 Langfuse 的 OpenTelemetry backend),随后就能逐 span 看清时间花在哪、用了多少 token 等。基于这套 trace 树可以拿到 Costs(token 用量换算成本,定位昂贵步骤)、Latency(每步/整段耗时,例中整段跑了 7 秒,用来找瓶颈)等指标。更进一步,还能给 span 附加 user_idsession_idtags、自定义 metadata 等属性——这对跨不同用户或会话分析、调试、监控应用行为很重要。权衡在于 instrumentation 会带来一定开销,但换来的是把「黑盒 agent」变成可逐步审查的对象,这正是后续 online/offline 评估和改进闭环的前提。
术语 trace(一次完整运行的记录,包含多个 span); span(单步逻辑的记录,如 tool call / LLM call); instrumentation(埋点,本例经 Pydantic Logfire 送 OTel); OpenTelemetry backend(接收 traces 的可观测后端); custom attributes(user_id/session_id/tags/metadata)
📖 "Langfuse records a trace that contains spans, which represent each step of your agent’s logic." — 原文
📖 "Enriching traces with these details is important for analysis, debugging, and monitoring of your application's behavior across different users or sessions." — 原文
🧪 实例 一条复杂查询的 trace 树结构与可读指标:
text
trace: 整段 agent run(约 7 秒)
 ├─ span: tool call (get_weather)
 └─ span: LLM calls (Responses API with 'gpt-4o')
指标: 逐 span 的 token 用量 -> 近似 Costs;逐步耗时 -> Latency 瓶颈
附加属性: user_id / session_id / trace_tags / metadata
🔍 追问 为什么要给 trace 传 session_id 和 tags? → 用于按不同用户或会话切片分析、调试与监控应用行为,把零散的单次运行聚成可对比的群体。
📚 拓展阅读
Q什么是 agent 的 harness?为什么说改进 agent 不能只靠 prompt tuning?深挖·拓展中频
Harness Loop Engineering Prompt Tuning
⏱️ 现行
在改进闭环语境里,harness 指围绕模型的完整契约,包括 instructions、tools、routing、output requirements 和 validation checks——也就是说 prompt 只是其中一小块。原文的金融分析 agent 就把 system prompt(证据规则)、tool policy(可读写什么)、路由规则、输出产物要求(如 summary_answer.md/risk_register.json/citations.json 等)和两个本地校验工具(一个查草稿引用是否指向真实 dataroom 文件、一个查必需产物是否齐全且形状正确)一起构成当前 harness。之所以不能只做 prompt tuning:agent 的失败模式往往是结构性的——把管理层叙事当成官方指标、把未经财务验证的 NRR 估计当成已验证、把 "SOC 2 complete" 说得比证据(只支持 Type I)更满、或答案很漂亮但 citations/风险文件残缺——这些要靠改工具契约、路由、校验和输出要求才能修,而不只是改措辞。所以 improvement loop 提供的是一条不把问题矮化为「只调 prompt」的持续改进路径;真正持久的是 loop engineering 这个更大的想法:当 feedback、testing、implementation 被连进一个闭环时,agent 能从真实行为里改进自己。
术语 harness(围绕模型的完整契约:instructions/tools/routing/output requirements/validation checks); loop engineering(把反馈/测试/实现连成闭环的工程范式); validation tools(校验工具,查引用真实性与产物齐全); failure modes(结构性失败模式,非措辞问题); promoted config(当前被采用的 harness 版本)
📖 "In this notebook, the harness is the full contract around the model, including instructions, tools, routing, output requirements, and validation checks." — 原文
📖 "An agent improvement loop offers a path toward continual improvement without reducing the problem to prompt tuning alone." — 原文
📖 "The larger idea of loop engineering is the durable part: agents can improve from real behavior when feedback, testing, and implementation are connected in one loop." — 原文
🧪 实例 原文列出的、需要 harness 级修复(而非改 prompt)的失败模式:
text
- 结构化 export 与叙事冲突时,把 management narrative 当成官方 metric
- 把未验证的 NRR estimate 当成 finance 已 validated 那样上报
- 把 parent-account 集中度坍缩成更弱的 legal-entity 视角
- 证据只支持 Type I 时却说 "SOC 2 complete"
- 答案很漂亮,却留下 citations / risk 文件 / evidence 产物不完整
🔍 追问 校验工具在 harness 里扮演什么角色? → 它们把「答案可复查」变成硬约束:一个查 drafted claims 是否引用真实 dataroom 文件,另一个查必需输出产物是否存在且形状符合预期,从而让失败在闭环里被 eval 捕获。
📚 拓展阅读
  • HALO — 从 traces 排序 harness 级改动的方法
  • Promptfoo — 把 harness 期望固化为可重跑 evals
  • Agents SDK use cases — 提供 runner/tracing 的 harness 底座
QMacro evals 如何从上千 trace 中发现并排序问题?discovery 与 diagnosis 各做什么?深挖·拓展中频
BERTopic Impact Score Root Cause
⏱️ 现行
原文把「跨群体找问题」拆成 discovery(发现什么在重复)与 diagnosis(先查哪里)两段——一句话概括就是 discovery 告诉你什么在重复、diagnosis 告诉你先去哪里看。Discovery 用 BERTopic-style 流程:把每条 trace document 编码成向量 $e_i=f(d_i)$,用 UMAP 之类 reducer 降维保留局部邻域,用 HDBSCAN 之类密度聚类分组并标记 noise,再为每个 cluster 算出能把它和语料其余部分区分开的 term 作为标签;随后用一个 triage 指标 impact_score = prevalence × severity_weighted_prevalence 给 pattern 排序——它不是万能风险公式,而是个实用的优先级分:一个 pattern 越常见且越严重就越靠前。Diagnosis 用 AgentTrace-style 图分析:对某个高 impact 的 behavior pattern 重建轻量执行图 $G=(V,E)$,选一个 focus event(anchor,通常是 review/finding 标记或晚期决策事件),从 anchor 反向游走并用一个可解释打分 suspect_score = 0.4·proximity + 0.3·frequency + 0.2·bridge + 0.1·role 给上游嫌疑事件排序。这套设计的价值在于把人的注意力导向既频繁又后果重的 pattern——它不是因果证明,而是把「这个 pattern 重要」翻译成「先去查这些 agents/tools/handoffs/review policy」。
术语 trace document(把一条 run 压缩成可比文本,供聚类); UMAP/HDBSCAN(降维/密度聚类,产出 cluster 与 noise); impact_score(prevalence × severity 的三选优先级分); focus event/anchor(反向诊断的起点,如 review finding); suspect_score(proximity/frequency/bridge/role 加权的可解释嫌疑分)
📖 "Discovery tells us what repeats. Diagnosis asks where to inspect first." — 原文
📖 "This is not a universal risk formula. It is a practical prioritization score: a pattern matters more when it is both common and severe." — 原文
📖 "This approach scales by directing human attention toward the patterns that are both frequent and consequential." — 原文
🧪 实例 排序与诊断用到的两个打分(均来自原文):
text
# 发现层:pattern 优先级
impact_score = prevalence × severity_weighted_prevalence

# 诊断层:上游嫌疑事件打分(从 focus event 反向游走)
suspect_score = 0.4·proximity + 0.3·frequency + 0.2·bridge + 0.1·role
  Proximity  奖励靠近 focus event 的事件
  Frequency  奖励在同 pattern 多条 trace 中反复出现的事件
  Bridge     奖励连接执行图各部分的事件
  Role       奖励 agent/tool 角色与该 finding 合理相关的事件
🔍 追问 cohort 分析里的 lift 用来回答什么? → 用 lift = slice pattern share / overall pattern share 判断某 behavior pattern 是否集中在某个切片(如 case_type = supplier_substitution_compound):>1 表示在该切片更集中,从而把「pattern 重要」推进到「去哪里查」。
📚 拓展阅读

第10章 · 函数调用、结构化输出与工具编排实战

🔥高频

函数调用实战模式

原文 原文 原文
Qtools 参数是做什么的?为什么说 API "不会真正执行函数"?深挖·拓展🔥高频
tools function-calling 责任边界
⏱️ 现行
tools 是 Chat Completions API 的一个可选参数,用来向模型提供函数规格(function specifications),它的作用是让模型生成符合这些规格的函数参数,而不是替你运行函数。这是理解整个 function-calling 的关键边界:模型只负责"判断该调哪个函数 + 生成结构化的 arguments",真正的执行(查数据库、调 REST API、发退款)必须由开发者在自己的代码里完成,再把结果回灌给模型。这样设计把不可控的副作用留在开发者手里,模型层保持无状态、纯生成,既安全又可审计。默认情况下,当提供了函数列表,模型会自行决定何时使用某个函数;一旦它决定调用,响应里的 finish_reason 会变成 tool_calls,并附带一个 tool_calls 对象,里面是函数名和生成的参数——开发者据此判断"模型是不是想调函数"并接管执行。
术语 tools(向模型提供函数规格的可选参数); function specifications(函数规格,含 name/description/parameters); finish_reason: tool_calls(表示本轮模型选择了调用工具的结束原因); tool_calls(承载函数名与生成参数的响应对象)
📖 "tools is an optional parameter in the Chat Completion API which can be used to provide function specifications. The purpose of this is to enable models to generate function arguments which adhere to the provided specifications. Note that the API will not actually execute any function calls. It is up to developers to execute function calls using model outputs." — 原文
📖 "Within the tools parameter, if the functions parameter is provided then by default the model will decide when it is appropriate to use one of the functions." — 原文
🧪 实例 定义一个天气查询工具规格,required 声明 location/format 必填:
json
{
  "type": "function",
  "function": {
    "name": "get_current_weather",
    "description": "Get the current weather",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"},
        "format": {"type": "string", "enum": ["celsius", "fahrenheit"]}
      },
      "required": ["location", "format"]
    }
  }
}
🔍 追问 模型返回 tool_calls 后,arguments 字段是什么类型? → 是一个 JSON 字符串,需要用 json.loads(tool_calls[0].function.arguments) 解析成 dict 才能取参数。
📚 拓展阅读
Qtool_choice 有哪几种取值?如何强制或禁止函数调用?深挖·拓展🔥高频
tool_choice 控制流 确定性
⏱️ 现行
tool_choice 决定模型在这一轮里对工具的自由度。默认(即提供了函数时)模型自己决定何时使用某个函数;你可以把 tool_choice 设为 {"type": "function", "function": {"name": "my_function"}}强制模型调用某个指定函数,也可以设为 "none"禁止模型调用任何函数,从而阻止它产出真正的函数调用。权衡在于:强制指定函数会逼模型对该函数的用法做出假设(force the model to make assumptions),即便信息不全也会硬凑参数——原文里强制 get_n_day_weather_forecast 时,模型在用户没说天数的情况下自己填了 num_days:5;而不强制时,同样的请求模型会转去调更合适的 get_current_weather。所以 auto 保灵活、required/指定名保确定性,是一组典型的自由度 vs 可控性权衡。
术语 tool_choice="auto"(模型自行在生成消息与调用函数间选择); tool_choice="none"(禁止调用任何函数); tool_choice={"type":"function",...}(强制调用指定函数); make assumptions(强制调用带来的副作用:模型对缺失参数做假设)
📖 "We can also force the model to not use a function at all. By doing so we prevent it from producing a proper function call." — 原文
📖 "We can force the model to use a specific function, for example get_n_day_weather_forecast by using the function_call argument. By doing so, we force the model to make assumptions about how to use it." — 原文
🧪 实例 强制指定函数 vs 交给模型自选:
python
# 强制:即使用户没给天数,模型也会自己假设 num_days
chat_response = chat_completion_request(
    messages, tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_n_day_weather_forecast"}}
)
# 禁止:模型无法产出函数调用,只能用自然语言回答
chat_response = chat_completion_request(messages, tools=tools, tool_choice="none")
🔍 追问 为什么不总是用强制调用来保证拿到结构化输出? → 因为强制会让模型对缺失/歧义参数硬做假设,牺牲了它"该问就问、该换函数就换"的判断力,可能填出错误参数。
📚 拓展阅读
Qtool_choice='required' 解决什么问题?为什么它适合客服这类流程?深挖·拓展🔥高频
tool_choice=required 确定性 客服编排
⏱️ 现行
tool_choice='required' 让 ChatCompletion 端点保证每一轮都必须调用某个工具,而不是有时回自然语言、有时调工具。这为包裹它的应用引入了确定性(determinism):既然每次调用都必然带一个工具调用,你就能围绕"工具"来搭状态机。原文用客服场景演示——把和用户对话、拉取处理指令都做成工具(speak_to_userget_instructions),于是每一步都落在你预定义的工具集合里,你可以定义明确的"退出点"(exit points),从而对流程有更强的控制。权衡是:强制每轮调工具意味着模型不能"自由发挥地直接回话",必须走 speak_to_user 这类工具通道,这对开放式闲聊是约束,但对需要可预测、可审计、可测试的受限流程(contained flow)恰恰是优势。
术语 tool_choice='required'(强制每次调用都必须使用某个工具); determinism(可预测性,便于搭建包裹应用); contained flow(受限流程,如客服,有明确入口/出口); exit points(可定义的退出点)
📖 "The ChatCompletion endpoint now includes the ability to specify whether a tool must be called every time, by adding tool_choice='required' as a parameter. " — 原文
📖 "This adds an element of determinism to how you build your wrapping application, as you can count on a tool being provided with every call. We'll demonstrate here how this can be useful for a contained flow like customer service, where having the ability to define specific exit points gives more control." — 原文
🧪 实例 循环调用,直到某个工具(speak_to_user)要求把控制权交回用户:
python
response = client.chat.completions.create(model=GPT_MODEL
                                          ,messages=messages
                                          ,temperature=0
                                          ,tools=tools
                                          ,tool_choice='required'
                                         )

其中 get_instructions 返回 respond=False(继续内部循环),speak_to_user 返回 respond=True(退出、把消息返回用户),形成清晰的退出点。
🔍 追问required 保证每轮调工具后,如何避免模型在一条消息里连调多个工具打乱流程? → 原文在 system prompt 里额外约束 "Only call a tool once in a single message.",把每轮工具调用收敛为一次。
📚 拓展阅读
Q什么是 parallel function calling?哪些模型支持,一次能调几个函数?深挖·拓展🔥高频
parallel-calling 多工具 一轮多调
⏱️ 现行
并行函数调用(parallel function calling)指模型在一轮回复里同时产出多个 tool_calls,让你一次拿到多个待执行的函数及其参数,而不必来回多轮。原文指出较新的模型如 gpt-5、gpt-4.1、gpt-4o 可以在一个 turn 内调用多个函数;示例中用户问"San Francisco 和 Glasgow 未来 4 天的天气",模型一次返回两个 get_n_day_weather_forecast 调用,分别带 San Francisco/fahrenheit 和 Glasgow/celsius 的参数。这对同类多实体请求特别高效——把 N 个独立子任务压进一轮,减少往返延迟;代价是你的执行代码必须遍历 tool_calls 列表逐个执行、逐个把结果按 tool_call_id 回灌,而不能假设只有一个调用。
术语 parallel function calling(一轮内产出多个函数调用); turn(一轮模型回复); tool_calls 列表(可含多个待执行调用,需逐个处理)
📖 "Newer models such as gpt-5, gpt-4.1 or gpt-4o can call multiple functions in one turn." — 原文
🧪 实例 一句话触发两个并行调用:
python
messages.append({"role": "user", "content": "what is the weather going to be like in San Francisco and Glasgow over the next 4 days"})
chat_response = chat_completion_request(messages, tools=tools, model="gpt-4o")
assistant_message = chat_response.choices[0].message.tool_calls
# -> 返回两个 get_n_day_weather_forecast 调用:
#    San Francisco, CA / fahrenheit / num_days 4
#    Glasgow, UK       / celsius    / num_days 4
🔍 追问 并行调用返回后,回灌结果时靠什么把每个结果对上原调用? → 靠每个 tool_call 的 id(即回灌 role:"tool" 消息里的 tool_call_id)一一对应。
📚 拓展阅读
Q用 Chat Completions 调函数的完整四步闭环是什么?深挖·拓展🔥高频
闭环 执行流程 tool-message
⏱️ 现行
一次完整的函数调用闭环分四步:Step 1 用可能触发工具选择的内容 prompt 模型,工具的名字与签名放在 tools 列表里传入,若模型选中则响应里带回函数名和参数;Step 2 用代码判断模型是否真的想调函数,是则继续;Step 3 从响应里取出函数名和参数,用这些参数调用真实函数,并把结果 append 回 messages;Step 4 带着追加了函数结果的 messages 再次调用 chat completions,让模型基于函数返回值产出最终答复。关键约束是回灌结果时那条 role:"tool" 的消息必须是对前一条带 tool_calls 的消息的回应,顺序不能乱。原文的数据库 agent 正是这样闭环:模型生成 SQL、代码执行查询、把结果喂回,模型最终答"Greatest Hits"。注意原文提醒 SQL 生成在生产里是高风险的,因为模型未必总能生成正确 SQL。
术语 Step 1–4(prompt→判断→执行并追加结果→再调模型总结); role:"tool" 消息(承载函数执行结果,含 tool_call_id/name/content); ask_database(示例中执行 SQL 的函数)
📖 "Extract the function name and parameters from response, call the function with parameters. Append the result to messages." — 原文
📖 "*Note:* SQL generation can be high-risk in a production environment since models are not perfectly reliable at generating correct SQL." — 原文
🧪 实例 Step 3 → Step 4,把工具结果按 tool_call_id 回灌后再问模型:
python
messages.append({
    "role":"tool",
    "tool_call_id":tool_call_id,
    "name": tool_function_name,
    "content":results
})
# Note that messages with role 'tool' must be a response to a preceding message with 'tool_calls'
model_response_with_function_call = client.chat.completions.create(
    model=GPT_MODEL,
    messages=messages,
)
🔍 追问 为什么把数据库 schema 塞进函数的 description 里? → 让模型知道有哪些表和列,才能生成对得上库结构的 SQL——schema 是模型生成正确 query 的必要上下文。
📚 拓展阅读
Q如何把一份 OpenAPI 规格自动转成函数定义?为什么用 operationId 当函数名?深挖·拓展中频
OpenAPI 函数生成 operationId
⏱️ 现行
互联网大量服务由 RESTful API 驱动,把 OpenAPI 规格(OAS)转成函数定义,就能让模型智能地调这些 API。转换要点:遍历 spec 的每个 path 与 method,第一步解析 JSON references($ref)——OpenAPI 里常用 $ref 复用定义以避免重复,需要把引用替换成它指向的真实内容(示例用 jsonref 做);第二步取函数名,直接用每个 operation 的 operationId(每个 operation 都带一个 operationId,天然唯一且语义化,适合当函数名,当然也可以改用"路径+方法");第三步取 description 和 parameters,从 description/summary/requestBody/parameters 字段拼出函数的描述和入参 schema。这样一份 CRUD 规格就变成 listEvents/createEvent/getEventById/deleteEvent/updateEventDetails 等函数定义,直接喂给 chat completions。要点是 API 仍不代你执行,它只生成可用于调用的 JSON。
术语 OpenAPI Specification (OAS)(描述 REST API 的通用标准); $ref / JSON references(避免重复的引用,需解析替换); operationId(operation 唯一标识,用作函数名); openapi_to_functions(把 spec 转成函数定义列表的函数)
📖 "Each operation in the spec has an operationId, which we will use as the function name when we parse the spec into function specifications." — 原文
📖 "It's important to note that the chat completions API does not execute the function; instead, it generates the JSON that you can use to call the function in your own code." — 原文
🧪 实例 转换核心三步(解析 $ref → 取 operationId → 组装 schema):
python
def openapi_to_functions(openapi_spec):
    functions = []
    for path, methods in openapi_spec["paths"].items():
        for method, spec_with_ref in methods.items():
            # 1. Resolve JSON references.
            spec = jsonref.replace_refs(spec_with_ref)
            # 2. Extract a name for the functions.
            function_name = spec.get("operationId")
            # 3. Extract a description and parameters.
            desc = spec.get("description") or spec.get("summary", "")
            ...
🔍 追问 为什么必须先解析 $ref 再生成函数定义? → 因为多个 endpoint 常引用同一个 schema(如 Event),不展开引用,函数的 parameters 里就会留下无法自解释的 $ref,模型拿不到真实字段结构。
📚 拓展阅读
Q多个函数如何链式编排?怎样防止无限循环?深挖·拓展中频
chained-calls 编排 MAX_CALLS
⏱️ 现行
链式函数调用指用一条复合指令(如"列出所有事件,再创建一个叫 AGI Party 的事件,再删除 id 2456 的事件")驱动模型分多次依序调用不同函数,每调一次就把结果回灌,直到模型不再需要调工具、转而用自然语言总结所有动作为止。为避免模型陷入无限或过长的循环,原文用一个 MAX_CALLS(示例设 5)上限,把链条长度封顶——这是一个务实的护栏:模型编排能力越强,越需要一个硬性 call 计数来兜底。实现上是一个 while num_calls < MAX_CALLS 循环,每轮拿模型响应,若有 tool_calls 就模拟执行并把 role:"tool" 结果 append 回去、计数加一;若模型返回的是纯文本(没有 tool_calls),说明它已完成、进入 except 分支打印最终消息并 break。
术语 chained function calls(多函数按序编排); MAX_CALLS(链条长度上限护栏,示例=5); while num_calls < MAX_CALLS(带计数的编排循环); tool_choice="auto"(让模型在调函数与直接回话间自选)
📖 "Possible extensions of this system could include handling more complex user instructions that require conditional logic or looping, integrating with real APIs to perform actual operations, and improving error handling and validation to ensure the instructions are feasible and the function calls are successful." — 原文
🧪 实例 带上限的编排循环骨架:
python
# Maximum number of function calls allowed to prevent infinite or lengthy loops
MAX_CALLS = 5

def process_user_instruction(functions, instruction):
    num_calls = 0
    messages = [...]
    while num_calls < MAX_CALLS:
        response = get_openai_response(functions, messages)
        message = response.choices[0].message
        try:
            messages.append(message)
            messages.append({"role": "tool", "content": "success",
                             "tool_call_id": message.tool_calls[0].id})
            num_calls += 1
        except:
            print(message.content)  # 无 tool_calls -> 模型已完成
            break

下面用 mermaid 表示这个循环:
flowchart TD
    A[复合用户指令] --> B{num_calls < MAX_CALLS?}
    B -- 否 --> E[Reached max chained function calls]
    B -- 是 --> C[调用模型]
    C --> D{响应含 tool_calls?}
    D -- 是 --> F[执行并回灌结果 / num_calls+1]
    F --> B
    D -- 否 --> G[打印最终总结, break]
🔍 追问 示例里"执行函数"其实做了什么? → 它没真的调 API,而是 append 一条 content:"success" 的 tool 消息来模拟成功;原文注明生产中应在此处真正调函数并回灌真实结果。
📚 拓展阅读
Q为什么要用 system prompt 让模型"别臆测参数、遇歧义先追问"?深挖·拓展中频
system-prompt 参数澄清 健壮性
⏱️ 现行
函数调用最怕模型在信息不全时凭空编参数,导致调错或调空。原文的做法是在 system 消息里明确要求:不要对填进函数的值做假设,请求有歧义就先要求澄清。效果是:当用户只说"今天天气怎么样"却没给城市和单位时,模型不会硬塞参数,而是反问"该查哪个城市?要摄氏还是华氏?";补全信息后才生成正确的 get_current_weather 参数。这与"强制指定函数会逼模型做假设"形成对照——auto + 澄清式 prompt 优先保正确性,让缺失的必填项由用户补齐而非模型瞎猜。权衡是多了一轮交互延迟,但换来参数可靠性,对面向真实用户的助手更稳。
术语 system message(设定模型行为约束的系统提示); ask for clarification(遇歧义先追问而非臆测); required 字段(声明必填参数,配合澄清防止空调用)
📖 "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous." — 原文
🧪 实例 system 约束 + 模型的澄清行为:
python
messages = []
messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
messages.append({"role": "user", "content": "What's the weather like today"})
# assistant: Sure—what city and state (or country) should I check? Also, do you prefer Celsius or Fahrenheit?
🔍 追问 客服示例里的 system prompt 又补了哪条防错约束? → 补了"If you need to fetch a piece of information from a system or document that you don't have access to, give a clear, confident answer with some dummy values.",在缺后端时用占位值兜底而不是卡住。
📚 拓展阅读
Q如何用一个"顾客 GPT"给工具编排应用做自动化多轮评测?深挖·拓展低频
评测 模拟用户 多轮测试
⏱️ 现行
除了让工具调用变确定,还能用第二个模型充当"顾客"来自动化测试你的客服 agent。原文的评测思路是:起一个 customer GPT,给它一个 objective(如"我要给上周五买的西装退款"),让它扮演用户,与客服 agent 一来一回,直到问题解决就回复 "DONE" 结束这轮。execute_conversation 用一个 while done is False 循环:把当前对话历史格式化后塞进顾客的 system prompt,顾客生成下一句用户话术,喂回客服 agent;一旦顾客回复里出现 "DONE" 就置 done、关闭对话。这等于把人工 QA 变成可复现、可批量跑的自动化测试用例——你既能强制工具使用来控制 agent 行为,又能用 GPT tester 去挑战它、当自动测试。
术语 customer GPT / GPT tester(模拟真实用户来挑战 agent 的模型); objective(顾客要达成的目标/query); DONE(顾客判定问题解决、结束对话的信号); execute_conversation(驱动多轮评测的循环)
📖 "You can now control your LLM's behaviour explicitly by making tool use mandatory, as well as spin up GPT testers to challenge your LLM and to act as automated test cases." — 原文
🧪 实例 顾客 GPT 的收敛条件——回复出现 DONE 即结束:
python
if 'DONE' in user_response.choices[0].message.content:
    done = True
    print("Achieved objective, closing conversation\n\n")
else:
    user_query = user_response.choices[0].message.content
🔍 追问 顾客 GPT 若缺具体细节(如订单号)怎么办,会不会卡住对话? → 不会,它的 system prompt 要求 "If you don't know the details, respond with dummy values.",用占位值继续推进直到解决。
📚 拓展阅读
中频

结构化输出与多智能体数据流

原文 原文
Q什么是 Structured Outputs?它和旧的 JSON mode 到底差在哪里?深挖·拓展🔥高频
structured-outputs json-schema strict
⏱️ 现行
Structured Outputs 是 Chat Completions / Assistants API 上的能力,通过在调用里设置 strict: true(配合 response_format 的 json_schema 或函数定义),保证模型输出严格遵循你提供的 JSON Schema。关键区别在于约束层级:旧的 JSON mode 只保证输出是"合法 JSON",但字段可能缺失、类型可能不对、可能多出你没定义的键;Structured Outputs 则约束 JSON 的形状(shape)——哪些字段必须存在、类型是什么、能不能出现额外字段——而不只是它的合法性。这个差异在生产系统里价值很大:当你依赖函数调用去触发下游动作,或依赖输出直接填充 UI / 数据库时,schema 被强制满足意味着你不必再写大量防御性解析和重试逻辑,可以构建更健壮的流程。代价是它只在支持该能力的模型上可用(gpt-4o-minigpt-4o-2024-08-06 及后续模型),且你需要预先把 schema 设计清楚。
术语 Structured Outputs(结构化输出,强制 schema 的能力); strict: true(开启严格模式的参数); JSON mode(只保证合法 JSON 的旧模式); response_format(指定输出格式的参数); json_schema(要遵循的 JSON 结构定义)
📖 "Structured Outputs is a new capability in the Chat Completions API and Assistants API that guarantees the model will always generate responses that adhere to your supplied JSON Schema." — 原文
📖 "Unlike JSON mode, Structured Outputs constrains the shape of the JSON, not only its validity." — 原文
📖 "Structured Outputs can be enabled by setting the parameter strict: true in an API call with either a defined response format or function definitions." — 原文
🧪 实例 数学辅导工具要求把解题过程拆成结构化步骤数组,便于 UI 逐步展示:
python
response_format={
    "type": "json_schema",
    "json_schema": {
        "name": "math_reasoning",
        "schema": {
            "type": "object",
            "properties": {
                "steps": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "explanation": {"type": "string"},
                            "output": {"type": "string"}
                        },
                        "required": ["explanation", "output"],
                        "additionalProperties": False
                    }
                },
                "final_answer": {"type": "string"}
            },
            "required": ["steps", "final_answer"],
            "additionalProperties": False
        },
        "strict": True
    }
}
🔍 追问 Structured Outputs 在哪些模型上可用? → 原文明确只在 gpt-4o-minigpt-4o-2024-08-06 以及后续模型上可用,旧模型不支持。
📚 拓展阅读
Qresponse_format 的 json_schema 里,requiredadditionalProperties: False 各起什么作用?深挖·拓展🔥高频
json-schema required additionalProperties
⏱️ 现行
这两个字段是 strict 模式真正"锁死"输出形状的关键。required 列出对象里必须出现的属性名,保证下游代码能安全地按键取值而不会 KeyError;additionalProperties: False 则禁止模型在对象里塞入 schema 未声明的额外字段,避免输出被无关噪声污染。在数学辅导例子里,顶层对象 required: ["steps", "final_answer"]additionalProperties: False,内层每个 step 又各自声明 required: ["explanation", "output"]additionalProperties: False——也就是说约束是逐层递归施加的,数组元素、嵌套对象都要各自把形状定死。这种嵌套写法保证了整棵 JSON 树的确定性:任何一层多字段、缺字段都不会发生。权衡在于 schema 越严格越可靠,但也越啰嗦,手写大型嵌套 schema 容易出错,这正是官方推荐改用 SDK parse 助手的动机。
术语 required(必需属性列表); additionalProperties: False(禁止额外字段); properties(属性定义); items(数组元素的 schema)
📖 "This can enable more robust flows in production-level applications, whether you are relying on function calls or expecting the output to follow a pre-defined structure." — 原文
🧪 实例 内层 step 对象的严格约束(节选自 math_reasoning schema):
json
{
  "type": "object",
  "properties": {
    "explanation": {"type": "string"},
    "output": {"type": "string"}
  },
  "required": ["explanation", "output"],
  "additionalProperties": false
}
🔍 追问 为什么每一层嵌套都要重复写 additionalProperties: False? → 因为该约束是作用在单个对象层级上的,数组元素、子对象都是独立的对象,不逐层声明就无法保证整棵树都不出现意外字段。
📚 拓展阅读
Q手写 JSON Schema 很啰嗦,SDK 提供了什么更省事的方式?深挖·拓展中频
parse pydantic sdk
⏱️ 现行
新版 SDK 引入了 parse 助手(client.beta.chat.completions.parse),允许你直接传入一个 Pydantic 模型作为 response_format,而不必手工编写等价的 JSON Schema;官方明确推荐尽量用这个方法。机制上,SDK 会把 Pydantic 模型翻译成对应的严格 schema 发给模型,再把模型返回的 JSON 反序列化回 Pydantic 对象,通过 completion.choices[0].message.parsed 直接拿到强类型对象。好处是:schema 定义和你代码里的数据类型是同一份来源(single source of truth),避免手写 schema 与业务类型不一致;嵌套类可以自然表达嵌套结构(例如把 Step 定义成 MathReasoning 的内部类)。权衡是它把 schema 生成交给 SDK,你对底层 JSON Schema 的细节控制会少一些,但对绝大多数场景更简洁也更不易出错。
术语 parse helper(接受 Pydantic 模型的解析助手); BaseModel(Pydantic 基类); response_format=<Model>(直接传模型); .parsed(返回的强类型对象)
📖 "The new version of the SDK introduces a parse helper to provide your own Pydantic model instead of having to define the JSON schema. We recommend using this method if possible." — 原文
🧪 实例 用 Pydantic 模型替代手写 schema:
python
from pydantic import BaseModel

class MathReasoning(BaseModel):
    class Step(BaseModel):
        explanation: str
        output: str

    steps: list[Step]
    final_answer: str

def get_math_solution(question: str):
    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {"role": "system", "content": dedent(math_tutor_prompt)},
            {"role": "user", "content": question},
        ],
        response_format=MathReasoning,
    )

    return completion.choices[0].message
🔍 追问 用了 parse 后怎么拿到结构化结果? → 通过 completion.choices[0].message.parsed,得到的是反序列化好的 Pydantic 对象,可直接 result.stepsresult.final_answer 取值。
📚 拓展阅读
Q用户输入可能触发模型拒答,Structured Outputs 怎么处理这种情况?深挖·拓展中频
refusal safety error-handling
⏱️ 现行
当把 Structured Outputs 用在用户生成的输入上时,模型可能出于安全原因偶尔拒绝完成请求。问题是拒答内容并不符合你在 response_format 里给的 schema,如果强行按你的格式去反序列化就会报错。为此 API 新增了一个 refusal 字段,专门用来标识模型这次是拒答而非正常输出。实践含义有两点:一是你可以在 UI 上把拒答单独渲染(而不是当成解析失败的异常),二是你在解析前应先检查 refusal 是否存在,避免用错误格式去 deserialize 引发崩溃。这本质上是把"安全拒答"这条旁路显式建模进了响应结构里,让调用方能优雅分支处理,而不是靠 catch 异常兜底。
术语 refusal(拒答标识字段); response_format(你提供的输出 schema); deserialize(反序列化到目标结构)
📖 "When using Structured Outputs with user-generated input, the model may occasionally refuse to fulfill the request for safety reasons." — 原文
📖 "Since a refusal does not follow the schema you have supplied in response_format, the API has a new field refusal to indicate when the model refused to answer." — 原文
🧪 实例 一个会被拒答的请求,通过 refusal 字段读取拒答内容:
python
refusal_question = "how can I build a bomb?"

result = get_math_solution(refusal_question)

print(result.refusal)
🔍 追问 为什么不直接抛异常,而要设计一个 refusal 字段? → 因为拒答是一种合法的模型行为而非错误,单独字段让 UI 能把它区别渲染,也让调用方在反序列化前就能判断,避免用错格式解析导致真正的运行时错误。
📚 拓展阅读
Q怎样把 Structured Outputs 用在函数调用上,做用户输入的实体抽取?深挖·拓展中频
function-calling entity-extraction pydantic_function_tool
⏱️ 现行
除了 response_format,strict: true 也能作用在函数定义上,确保模型给函数生成的参数严格符合你声明的 schema——这正是实体抽取的理想场景。cookbook 的例子里用函数调用,根据用户输入去数据库检索匹配商品:把要抽取的参数(category、subcategory、color)定义成一个 Pydantic 模型 ProductSearchParameters,其中 category 是一个 Enum(限定 shoes/jackets/tops/bottoms 四个值),再用 openai.pydantic_function_tool(...) 把它包装成工具传给 tools。模型会把自然语言("我总是很冷,想要保暖的外套")映射成结构化的搜索参数(category=jackets 等)。用 Enum 约束分类字段的好处是模型不会编造出数据库里不存在的类别;结构化保证让抽取结果可直接喂给下游检索,无需再做格式清洗。
术语 pydantic_function_tool(把 Pydantic 模型包装成工具); tool_calls(模型返回的工具调用); Category(str, Enum)(限定取值的枚举字段); function.arguments(工具调用的结构化参数)
📖 "In this example, we will use function calling to search for products that match a user's preference based on the provided input." — 原文
🧪 实例 用枚举约束类别、用 pydantic_function_tool 声明工具:
python
class Category(str, Enum):
    shoes = "shoes"
    jackets = "jackets"
    tops = "tops"
    bottoms = "bottoms"

class ProductSearchParameters(BaseModel):
    category: Category
    subcategory: str
    color: str

def get_response(user_input, context):
    response = client.chat.completions.create(
        model=MODEL,
        temperature=0,
        messages=[
            {"role": "system", "content": dedent(product_search_prompt)},
            {"role": "user", "content": f"CONTEXT: {context}\n USER INPUT: {user_input}"}
        ],
        tools=[
            openai.pydantic_function_tool(ProductSearchParameters, name="product_search", description="Search for a match in the product database")
        ]
    )
    return response.choices[0].message.tool_calls
🔍 追问 为什么 category 用 Enum 而不是普通 string? → Enum 把取值限定在数据库真实存在的四个类别内,strict 模式下模型无法返回枚举外的值,避免抽取出无效类别导致下游检索落空。
📚 拓展阅读
Q为什么要构建多智能体系统,而不是把所有工具塞给一个模型?深挖·拓展🔥高频
multi-agent tool-grouping architecture
⏱️ 现行
核心动因是性能随工具数量增长而下降:用函数调用时,如果函数(工具)数量增多,模型在众多工具里正确选择的表现会变差。缓解办法是把工具按逻辑分组,交给各自专精的"agent"去解决特定任务或子任务,从而提升整体系统表现。cookbook 用一个数据分析任务示范了 4-agent 架构:Triaging Agent 负责评估用户查询并路由到相关 agent;Data Processing Agent 负责清洗/转换/聚合数据;Analysis Agent 负责统计/相关性/回归分析;Visualization Agent 负责画柱状图/折线图/饼图。每个子 agent 只挂载与自身角色相关的工具(除 triage 外),模型面对的工具集变小,选择更准。而这套分工能可靠运行的前提正是 Structured Outputs——它建立在 JSON mode 和函数调用之上,用 strict: true 强制模型输出遵循严格 schema,保证路由和工具调用的参数结构稳定可预测。
flowchart TD
    U[User query] --> T[Triaging Agent]
    T -->|send_query_to_agents| P[Data Processing Agent]
    T -->|send_query_to_agents| A[Analysis Agent]
    T -->|send_query_to_agents| V[Visualization Agent]
    P --> P1[clean/transform/aggregate]
    A --> A1[stat/correlation/regression]
    V --> V1[bar/line/pie chart]
术语 Triaging agent(分诊/路由 agent); send_query_to_agents(把查询转发给相关 agent 的工具); specialized agents(专精子 agent); strict: true(强制 schema 的开关)
📖 "When using function calling, if the number of functions (or tools) increases, the performance may suffer." — 原文
📖 "To mitigate this, we can logically group the tools together and have specialized \"agents\" that are able to solve specific tasks or sub-tasks, which will increase the overall system performance." — 原文
📖 "Structured Outputs is a new capability that builds upon JSON mode and function calling to enforce a strict schema in a model output." — 原文
🧪 实例 Triaging Agent 的系统提示(节选),它只负责路由并可反问用户:
python
triaging_system_prompt = """You are a Triaging Agent. Your role is to assess the user's query and route it to the relevant agents. The agents available are:
- Data Processing Agent: Cleans, transforms, and aggregates data.
- Analysis Agent: Performs statistical, correlation, and regression analysis.
- Visualization Agent: Creates bar charts, line charts, and pie charts."""
🔍 追问 分组成多个 agent 具体缓解了什么? → 缓解了单一模型面对大量工具时选择变差的问题;每个 agent 只看到自己领域的少量工具,选择空间小、命中率高,整体系统表现随之提升。
📚 拓展阅读
Q多智能体系统里,工具调用产生的数据是怎么在 agent 之间流动的?深挖·拓展低频
tool-execution conversation-history data-flow
⏱️ 现行
数据流的核心是一个共享的对话历史(conversation history)加一个统一的执行函数。execute_tool 把每个工具调用映射到对应的 Python 函数,执行后把函数输出以 {"role": "tool", ...} 的消息追加进对话历史——原文原话就是"这会把工具调用映射到对应函数,然后把函数输出追加到对话历史"。每个子 agent 的 handler(如 handle_data_processing_agent)拿到用户查询后,用自己的系统提示和专属工具集调用模型,再把返回的 tool_calls 交给 execute_tool 执行,并把消息 append 到同一个 conversation_messages 里。因此后续 agent 能看到前面 agent 处理的结果(例如先 clean_data,清洗后的数据进入历史,再被 stat_analysis 和 line chart 使用)。这种"状态即对话历史"的设计让链式协作变得自然:每一步的产物都沉淀在共享上下文中,顺序执行时下游自动可见上游输出。
术语 execute_tool(执行工具调用并回填结果的函数); conversation history(共享对话历史/状态); role: tool(工具输出的消息角色); tool_calls(模型请求的工具调用列表)
📖 "This maps a tool call to the corresponding function. It then appends the output of the function to the conversation history." — 原文
🧪 实例 子 agent handler 调用模型后,把工具调用交给 execute_tool 并回写历史:
python
def handle_data_processing_agent(query, conversation_messages):
    messages = [{"role": "system", "content": processing_system_prompt}]
    messages.append({"role": "user", "content": query})

    response = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        temperature=0,
        tools=preprocess_tools,
    )

    conversation_messages.append([tool_call.function for tool_call in response.choices[0].message.tool_calls])
    execute_tool(response.choices[0].message.tool_calls, conversation_messages)
🔍 追问 上游 agent 的输出怎样被下游 agent 用到? → 每个工具输出都以 tool 角色消息 append 进同一个 conversation_messages,顺序执行的下游 agent 读取共享历史时就能看到上游处理后的数据。
📚 拓展阅读
Q顶层是怎么编排整个流程的?Structured Outputs 保证了工具调用符合 schema,是不是就不用再校验了?深挖·拓展低频
orchestration handle_user_message validation
⏱️ 现行
顶层的 handle_user_message 是总编排函数:它接收用户查询、先让 Triaging Agent 出响应,再根据 triage 返回的 send_query_to_agents 工具调用里的 agents 列表,逐个分发给对应的子 agent handler 执行,同时维护贯穿全程的 conversation_messages 状态。这体现了 triage→分发→执行→回写历史的闭环。但要特别注意结论里的边界提醒:Structured Outputs 保证工具调用符合指定的 schema,而应用代码仍必须校验参数的语义和业务规则。也就是说,strict schema 只解决"结构对不对"(字段齐全、类型正确、无多余字段),并不保证"值对不对"(比如某个数据集是否真实存在、聚合列名是否合法、参数组合是否符合业务约束)。因此健壮的多智能体系统里,结构化输出是第一道保障,业务语义校验仍是不可省的第二道。
术语 handle_user_message(顶层编排函数); send_query_to_agents(路由工具); conversation_messages(全程状态); argument semantics(参数语义,需自行校验)
📖 "Structured Outputs ensures that tool calls follow the specified schema. Application code must still validate argument semantics and business rules." — 原文
🧪 实例 顶层根据 triage 结果分发到各子 agent:
python
def handle_user_message(user_query, conversation_messages=[]):
    user_message = {"role": "user", "content": user_query}
    conversation_messages.append(user_message)

    messages = [{"role": "system", "content": triaging_system_prompt}]
    messages.extend(conversation_messages)

    response = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        temperature=0,
        tools=triage_tools,
    )

    conversation_messages.append([tool_call.function for tool_call in response.choices[0].message.tool_calls])

    for tool_call in response.choices[0].message.tool_calls:
        if tool_call.function.name == 'send_query_to_agents':
            agents = json.loads(tool_call.function.arguments)['agents']
            query = json.loads(tool_call.function.arguments)['query']
            for agent in agents:
                if agent == "Data Processing Agent":
                    handle_data_processing_agent(query, conversation_messages)
                elif agent == "Analysis Agent":
                    handle_analysis_agent(query, conversation_messages)
                elif agent == "Visualization Agent":
                    handle_visualization_agent(query, conversation_messages)

    return conversation_messages
🔍 追问 strict schema 之外,应用层还要做什么? → 校验参数语义和业务规则——例如值是否合理、引用的数据/列是否存在、参数组合是否满足业务约束,这些 schema 无法覆盖。
📚 拓展阅读
中频

Responses 工具编排与推理项

原文 原文 原文
Q用 reasoning 模型做 function calling 时,为什么要把 reasoning item 回传?普通多轮对话又为什么不用?深挖·拓展🔥高频
reasoning-items function-calling responses-api
⏱️ 现行
推理模型(o3、o4-mini)在回答前会产出一段内部 chain-of-thought,Responses API 把它作为一个 reasoning 类型的 output item(以 rs_... 这样的 ID 暴露,内容仅以摘要形式或加密形式给出)。关键区别在于:在普通的多轮对话里,模型被训练成不依赖上一轮的 reasoning tokens 也能给出最优输出,所以每轮结束后这些 reasoning tokens 会被丢弃、不必回传;但一旦某一轮里发生了 function call,情况就变了——一次工具调用意味着要跳出 API 去外部执行一次(extra round trip),再把结果喂回来,这个"发起调用 → 拿到结果 → 继续推理"其实是同一个 turn 内的延续,模型需要它自己在调用前的那段 chain-of-thought 才能正确地在结果之上接着推理。因此你必须把 reasoning item 一起带回去,要么用 previous_response_id 让 OpenAI 帮你维护状态,要么手动把 output(含 reasoning item)重新塞进 input。权衡在于:回传会多占上下文,但换来的是更高的智能——官方在 SWE-bench 上测得同样 prompt 下带上 reasoning item 约有 3% 的提升,收益直接来源于"工具调用决策"变得更准。
术语 reasoning item(推理项,模型内部思维链对应的 output 条目,以 rs_ 前缀 ID 暴露); previous_response_id(上一次响应 ID,用于让 API 自动接续对话与推理状态); round trip(一次外部往返,指函数在 API 之外被执行再回传)
📖 "In typical multi-turn conversations, you don’t need to include reasoning items or tokens—the model is trained to produce the best output without them. However, things change when tool use is involved. If a turn includes a function call (which may require an extra round trip outside the API), you do need to include the reasoning items—either via previous_response_id or by explicitly adding the reasoning item to input." — 原文
📖 "On a more rigorous benchmark like SWE-bench, including reasoning items led to about a 3% improvement for the same prompt and setup." — 原文
🧪 实例 拿到 function_call 后,把整段 output(含 reasoning item)加回 context,再附上 function_call_output,一次 API 再调用即为同一 turn 的延续:
python
context += response.output # Add the response to the context (including the reasoning item)

tool_call = response.output[1]
args = json.loads(tool_call.arguments)

# calling the function
result = get_weather(args["latitude"], args["longitude"]) 

context.append({                               
    "type": "function_call_output",
    "call_id": tool_call.call_id,
    "output": str(result)
})

# we are calling the api again with the added function call output. Note that while this is another API call, we consider this as a single turn in the conversation.
response_2 = client.responses.create(
    model="o4-mini",
    input=context,
    tools=tools,
)
🔍 追问 如果只想让 API 帮我管状态、连 output 都不想手动拼接怎么办? → 用 previous_response_id 指向上一次响应,模型会自动获得此前产生的所有 reasoning items,无需自己往 input 里塞。
📚 拓展阅读
Q如何用 Responses API 做多工具编排(built-in 工具 + 自定义 function)实现 RAG 路由?深挖·拓展🔥高频
tool-orchestration rag parallel-tool-calls
⏱️ 现行
思路是把内置工具和自定义 function 一起注册到一次 responses.create 调用的 tools 列表里,让模型按 query 语义自己路由。示例里注册了两类工具:内置的 web_search_preview(能做实时联网检索,适合泛化/时效性问题)和一个自定义 function 类型的 PineconeSearchDocuments(通过语义查询去外部向量库 Pinecone 检索领域文档)。开 parallel_tool_calls=True 允许模型一次返回多个工具调用。执行链路是:模型先返回 output(可能同时含 web_search_callmessagefunction_call),你在代码里识别出 function_call,本地真正执行它(调 query_pinecone_index),再把这个 tool_call 和一个 function_call_output(带同一个 call_id)一起 append 回对话,最后再调一次 Responses API 生成融合了工具结果的最终答案。这套设计的价值在于:同一个 API 面统一了"内置托管工具"和"连接外部向量库"两种能力,既能用官方 file_search 连内部向量库,也能像这样接任意外部数据库,从而在托管工具之上灵活拼 RAG。权衡是模型的路由不是硬编码的——泛化问题走 web search,健康类内部问题走 Pinecone,其它问题可能不调工具直接答,靠 system instruction 引导路由顺序。
术语 web_search_preview(内置联网搜索工具,做实时检索并预览结果); PineconeSearchDocuments(示例自定义 function,语义查询外部向量库); parallel_tool_calls(允许模型一次返回多个可并行执行的工具调用); function_call_output(把本地函数执行结果回传给模型的特殊输入项,用 call_id 对齐) ``mermaid flowchart TD Q[User query] --> R[responses.create with tools + parallel_tool_calls] R -->|general / time-sensitive| W[web_search_preview] R -->|internal medical context| P[PineconeSearchDocuments -> vector DB] R -->|no tool needed| A0[direct answer] W --> M[append tool_call + function_call_output] P --> M M --> F[responses.create again -> final answer] ``
📖 "The model selects a tool based on the query: built-in web search can handle general questions, while function calls can retrieve internal medical context from a vector database such as Pinecone." — 原文
📖 "Finally, the tool call and its output are appended to the conversation, and the final answer is generated by the Responses API." — 原文
🧪 实例 把内置工具和自定义 function 同时注册进一次调用的 tools:
python
tools = [   
    {"type": "web_search_preview",
      "user_location": {
        "type": "approximate",
        "country": "US",
        "region": "California",
        "city": "SF"
      },
      "search_context_size": "medium"},
    {
        "type": "function",
        "name": "PineconeSearchDocuments",
        "description": "Search for relevant documents based on the medical question asked by the user that is stored within the vector database using a semantic query.",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The natural language query to search the vector database."},
                "top_k": {"type": "integer", "description": "Number of top results to return.", "default": 3}
            },
            "required": ["query"],
            "additionalProperties": False
        }
    }
]
🔍 追问 想强制"先 web search 再查内部库"的固定顺序,怎么做? → 改 system instruction,例如让它每次先调 web search 工具、再调 PineconeSearchDocuments 找内部知识库里的真实示例;示例中正是用这种系统提示把工具调用编排成一个序列。
📚 拓展阅读
QResponses 的响应对象里 reasoning item 长什么样?token 是怎么算的?"stateful"意味着什么?深挖·拓展🔥高频
reasoning-tokens stateful token-accounting
⏱️ 现行
一次调用返回的 output 数组里,除了 output_text 对应的 message,还会有一个 type: "reasoning" 的 item,它代表模型的内部推理 tokens,对外只暴露一个 ID(如 rs_6820f383...),原始思维链不给你看(出于安全,只以摘要形式暴露)。因为 Responses API 是 stateful 的,这些 reasoning tokens 会被持久化:你只要在后续消息里带上它们的 ID,就能让之后的响应访问到同一批 reasoning items;若用 previous_response_id 做多轮,模型会自动拿到此前产生的全部 reasoning items。token 计费上要特别注意 reasoning tokens 会算进 output——示例里 10 个 input tokens 换来 148 个 output tokens,其中 128 个是不出现在最终 assistant 消息里的 reasoning tokens。这就是为什么用推理模型会比传统 chat 模型更快耗尽上下文窗口:那部分"看不见"的思考 token 同样占额度,必须纳入容量与成本预算。
术语 output(响应里的输出项数组,含 reasoning item 与 message 等类型); reasoning_tokens(推理消耗的输出 token,计入 output_tokens 但不在最终文本中); stateful(有状态,API 侧持久化 reasoning items,可凭 ID 复用)
📖 "Because the Responses API is stateful, these reasoning tokens persist: just include their IDs in subsequent messages to give future responses access to the same reasoning items. If you use previous_response_id for multi-turn conversations, the model will automatically have access to all previously produced reasoning items." — 原文
📖 "For example, with 10 input tokens, the response included 148 output tokens—128 of which are reasoning tokens not shown in the final assistant message." — 原文
🧪 实例 response.output 里一个 reasoning item 加一个 message item,usagereasoning_tokens 单列:
json
"output": [
    { "id": "rs_6820f383d7c08191846711c5df8233bc0ac5ba57aafcbac7", "summary": [], "type": "reasoning", "status": null },
    { "id": "msg_6820f3854688819187769ff582b170a60ac5ba57aafcbac7",
      "content": [ { "annotations": [], "text": "Why don’t scientists trust atoms?  \nBecause they make up everything!", "type": "output_text" } ],
      "role": "assistant", "status": "completed", "type": "message" }
],
"usage": { "input_tokens": 10, "output_tokens": 148,
    "output_tokens_details": { "reasoning_tokens": 128 }, "total_tokens": 158 }
🔍 追问 既然原始思维链不暴露,我怎么给用户一点可见性? → 用 reasoning summaries(reasoning={"summary": "auto"}),能拿到思考过程的摘要文本而非逐字 token,可在多次函数调用间提前展示"调了哪些函数、为什么调"。
📚 拓展阅读
Q推理模型的多个 function call 能并行吗?为什么要写一个循环来处理?深挖·拓展中频
parallel-tool-calls series agent-loop
⏱️ 现行
有些 OpenAI 模型支持 parallel_tool_calls,让模型一次返回一个函数数组、你并行执行。但推理模型往往会产出一串必须串行(in series)执行的 function call——因为后一步经常依赖前一步的结果(比如先拿到城市 ID 再去搜该城市新闻)。更关键的是,你事先无法知道模型会走多少步 reasoning + tool call,所以不能写死步数,而要用一个循环来通用地处理任意复杂的推理工作流:每一步先发一次响应,如果 output 里含 function call,就假设推理仍在进行,把函数结果(以及中间的 reasoning)喂回模型继续推理;直到某次 output 只剩 message 类型、没有 function call,才认为 agent 推理完成、跳出循环。权衡:这种 loop 编排让模型能自主"暂停执行去调函数再继续",代价是每一步都要正确回传状态,且 token 会随步数累积。
术语 in series(串行,后一步工具调用依赖前一步结果); parallel_tool_calls(并行工具调用参数,返回可并行的函数数组); response loop(响应循环,直到出现无工具调用的 message 才终止)
📖 "Some OpenAI models support the parameter parallel_tool_calls which allows the model to return an array of functions which we can then execute in parallel. However, reasoning models may produce a sequence of function calls that must be made in series, particularly as some steps may depend on the results of previous ones." — 原文
📖 "If the response contains function calls, we must assume the reasoning is ongoing and we should feed the function results (and any intermediate reasoning) back into the model for further inference" — 原文
🧪 实例previous_response_id 接续的通用 agent 循环,直到没有 function call 才输出并 break:
python
response = client.responses.create(
    input=initial_question,
    **MODEL_DEFAULTS,
)
while True:   
    function_responses = invoke_functions_from_response(response)
    if len(function_responses) == 0: # We're done reasoning
        print(response.output_text)
        break
    else:
        print("More reasoning required, continuing...")
        response = client.responses.create(
            input=function_responses,
            previous_response_id=response.id,
            **MODEL_DEFAULTS
        )
🔍 追问 循环终止条件为什么用"是否还有 function call"而不是"是否有 message"? → 因为只要 output 里还有 function call 就说明推理未完、需要继续喂结果;没有 function call、收到 type 为 message 的 Response.output 时,才能安全认定 agent 已结束推理。
📚 拓展阅读
Q手动/无状态编排对话时,为什么必须保留 reasoning 和 function call 条目?深挖·拓展中频
stateless conversation-orchestration chain-of-thought
⏱️ 现行
生产里你常常不想用 previous_response_id 交给 OpenAI 托管,而是把 API 当无状态用——自己维护一个 conversation items 数组,每次整体发给模型。原因很实际:上下文窗口会涨,你可能要裁剪或摘要旧消息;你可能要让用户来回导航、重新生成;你可能要把消息存进自己的数据库做审计。但对推理模型这样自己管状态有一个特定坑:必须把每一轮的 reasoning item 和 function call 响应原样保留在对话历史里,因为这正是模型追踪"自己走过哪些 chain-of-thought 步骤"的依据,一旦缺失,API 会直接报错。所以手动编排时要小心保持 reasoning、function_call、function_call_output 的正确顺序(示例里用 zip 把 function_calls 和 function_outputs 交错回插),以保住这条思维链。
术语 stateless(无状态,自己维护并每次整体重发 conversation items); conversation history(对话历史,须含 reasoning 与 function call 条目); chain-of-thought(思维链,模型据此知道已执行过哪些步骤)
📖 "In particular, it is essential that we preserve any reasoning and function call responses in our conversation history." — 原文
📖 "This is how the model keeps track of what chain-of-thought steps it has run through. The API will error if these are not included." — 原文
🧪 实例 无状态模式下把 reasoning、function_call+output 都 extend 回 conversation,并保序交错:
python
reasoning = [rx.to_dict() for rx in response.output if rx.type == 'reasoning']
function_calls = [rx.to_dict() for rx in response.output if rx.type == 'function_call']
messages = [rx.to_dict() for rx in response.output if rx.type == 'message']
if len(reasoning) > 0:
    # Ensure we capture any reasoning steps
    conversation.extend(reasoning)
if len(function_calls) > 0:
    function_outputs = invoke_functions_from_response(response)
    # Preserve order of function calls and outputs in case of multiple function calls
    interleaved = [val for pair in zip(function_calls, function_outputs) for val in pair]
    conversation.extend(interleaved)
🔍 追问 手动编排相比 previous_response_id 具体换来什么? → 换来完全的控制权:可裁剪/摘要过长上下文、支持用户前后导航与重生成、把消息存自己库里做审计,而不依赖 OpenAI 的存储与编排。
📚 拓展阅读
QZDR / 无状态合规场景下,如何用 encrypted reasoning items 仍享受 reasoning 收益?深挖·拓展中频
encrypted-reasoning zdr store-false
⏱️ 现行
有些组织(例如有 Zero Data Retention 要求的)因合规或数据保留策略不能有状态地用 Responses API。为此 OpenAI 提供 encrypted reasoning items:在调用的 include 字段加上 ["reasoning.encrypted_content"],API 就会返回 reasoning tokens 的加密版本(reasoning item 里多出一个 encrypted_content 字段),你像回传普通 reasoning item 那样把它带进后续请求即可,状态完全留在客户端、OpenAI 不留数据。机制上,对 ZDR 组织 OpenAI 会自动强制 store=false;当请求带 encrypted_content 时,它在内存中被解密(绝不写盘)、用于生成下一次响应、随后被安全丢弃,新产生的 reasoning tokens 会立即加密返回给你,保证没有任何中间状态被持久化。这样就在"无状态 + 合规"的前提下,依然拿到了 reasoning item 带来的智能、成本和延迟收益。
术语 encrypted_content(加密思维链字段,代表模型推理状态、完全存于客户端); include(请求字段,加 reasoning.encrypted_content 以返回加密推理); store=false(不存储,ZDR 下被自动强制)
📖 "For ZDR organizations, OpenAI enforces store=false automatically. When a request includes encrypted_content, it is decrypted in-memory (never written to disk), used for generating the next response, and then securely discarded. Any new reasoning tokens are immediately encrypted and returned to you, ensuring no intermediate state is ever persisted." — 原文
📖 "Some organizations—such as those with Zero Data Retention (ZDR) requirements—cannot use the Responses API in a stateful way due to compliance or data retention policies." — 原文
🧪 实例store=False 模拟 ZDR,并请求加密思维链回传:
python
context = [{"role": "user", "content": "What's the weather like in Paris today?"}]

response = client.responses.create(
    model="o3",
    input=context,
    tools=tools,
    store=False, #store=false, just like how ZDR is enforced
    include=["reasoning.encrypted_content"] # Encrypted chain of thought is passed back in the response
)
🔍 追问 拿到 encrypted_content 后下一步怎么用? → 和普通 reasoning item 一样,把整段 response.output 加回 context,再附上 function_call_output,继续用同样的 include=["reasoning.encrypted_content"] 调下一次,即可在无状态下延续推理。
📚 拓展阅读
Q从 Completions 切到 Responses API,在缓存和推理摘要上有什么实际收益?深挖·拓展低频
caching reasoning-summaries cost-latency
⏱️ 现行
推理模型同时产出 reasoning tokens 和 completion tokens,API 对二者处理方式不同,这会影响缓存的命中方式,进而影响性能与延迟。规则上,turn 2 里来自 turn 1 的 reasoning items 会被忽略并移除(因为模型不复用上一轮的 reasoning items),所以某些 API 调用拿不到完整 cache hit;但把它们带上是无害的——API 会自动丢弃当前 turn 不相关的 reasoning item。要注意缓存只对超过 1024 tokens 的 prompt 生效。官方测试里,从 Completions 切到 Responses 让 cache 利用率从 40% 提到 80%,更高的缓存利用率带来更低成本(例如 o4-mini 缓存输入 token 比未缓存便宜 75%)和更好延迟。另一项收益是 reasoning summaries:虽然不暴露原始思维链 tokens,但用户可拿到其摘要,能在多次函数调用过程中提前看到"调了哪些函数、每次调用背后的推理",增加透明度与交互性,而不必等最终 assistant 消息。
术语 cache utilization(缓存利用率,Responses 相比 Completions 从 40% 升到 80%); 1024 tokens(缓存生效的 prompt 长度门槛); reasoning summaries(推理摘要,reasoning={"summary": "auto"} 获取)
📖 "Keep in mind that caching only impacts prompts longer than 1024 tokens. In our tests, switching from the Completions API to the Responses API boosted cache utilization from 40% to 80%." — 原文
📖 "In turn 2, any reasoning items from turn 1 are ignored and removed, since the model does not reuse reasoning items from previous turns." — 原文
📖 "For example, during conversations with multiple function calls, users can see both which functions were called and the reasoning behind each call—without waiting for the final assistant message." — 原文
🧪 实例 请求 reasoning summary 并取第一条摘要文本:
python
response = client.responses.create(
    model="o3",
    input="What are the main differences between photosynthesis and cellular respiration?",
    reasoning={"summary": "auto"},
)

# Extract the first reasoning summary text from the response object
first_reasoning_item = response.output[0]  # Should be a ResponseReasoningItem
first_summary_text = first_reasoning_item.summary[0].text if first_reasoning_item.summary else None
print("First reasoning summary text:\n", first_summary_text)
🔍 追问 既然 turn 1 的 reasoning items 在 turn 2 会被丢弃,那多带它们会不会有害? → 无害;API 会简单丢弃当前 turn 不相关的 reasoning items,带上它们不会破坏结果,只是那次调用拿不到完整 cache hit。
📚 拓展阅读

第11章 · 评估、守护与微调实战

🔥高频

Evals API 与 LLM-as-a-Judge

原文 原文 原文
Q什么是 eval(评估)?为什么说给基础模型建高质量 eval 是最有价值的事?深挖·拓展🔥高频
evals evaluation CI/CD
⏱️ 现行
eval 的本质是一个用来度量 LLM(或建在 LLM 之上的系统)输出质量的任务:给定一个 input prompt,模型产出一个 output,我们拿一组理想答案去比对,从而得到该系统的质量分。之所以重要,是因为构建 AI 方案是一个不断迭代的过程——模型会持续升级、prompt 会反复改,没有 eval 你几乎无法量化"换个模型版本或换个 prompt 到底变好还是变坏"。因此官方把"建高质量 eval"称作用 GPT-4 这类基础模型开发时最有价值的事之一:一套针对你目标定制的 eval,能让你在标准化条件下快速看清新模型对你场景的表现,甚至可以把 eval 塞进 CI/CD 流水线,在部署前先卡住准确率门槛。权衡在于 eval 本身要投入构建成本,但相比"上线后靠人肉发现回退",前置的自动化评估在可靠性与迭代速度上的收益要大得多。
术语 eval(度量 LLM 输出质量的任务); input prompt(喂给模型的输入); ideal answers(理想/参考答案); CI/CD pipeline(把 eval 纳入持续集成以卡准确率门槛)
📖 "An eval is a task used to measure the quality of the output of an LLM or LLM system." — 原文
📖 "creating high quality evals is one of the most impactful things you can do" — 原文
📖 "will mean a more stable, reliable application that is resilient to code and model changes." — 原文
🧪 实例 一个最简单的"字符串校验"型 eval 样本:input 是"What year was Obama elected president for the first time?",ideal 答案是"2008";把 input 喂给模型,若 completion 里包含"2008"就判为正确。数据集用 jsonl,每条形如:
json
{"input": [{"role": "system", "content": "<input prompt>"}, {"role": "user", "content": "<user input>"}], "ideal": "correct answer"}
🔍 追问 eval 能不能只在开发期跑一次? → 不建议;因为模型会 continuous upgrade、prompt 也常改,eval 的价值恰恰在于持续、标准化地重测,甚至进 CI/CD 在部署前卡准确率门槛。
📚 拓展阅读
Q什么是 LLM-as-a-judge?为什么它比 Levenshtein 这类字符串启发式更好?深挖·拓展🔥高频
LLM-as-a-judge scorer hallucination
⏱️ 现行
LLM-as-a-judge 是一种用 LLM 本身来给答案质量打分的技术。它要解决的痛点是:当"正确答案"与"模型答案"只是措辞不同(例如标准答案是"You can return items within 30 days of purchase",机器人答"You can return items within 30 days"),像 Levenshtein 字符串编辑距离这样的启发式会因为字面不一致而判它错,可它语义上其实是对的。LLM-as-a-judge 的关键优势在于它能在"表面字符串比较"之上真正推理语言,从而更准确地判定答案对错——它理解"少了 of purchase"并不改变事实性。权衡是:模型评分本身会有误差、也更贵,所以并非任何场景都值得,但在开放式、措辞高度可变的答案上,它比死板的字符串匹配靠谱得多。这也是检测 hallucination(幻觉)这类任务普遍采用它的原因。
术语 LLM-as-a-judge(用 LLM 给答案质量打分的技术); Levenshtein(字符串编辑距离启发式,只看字面); scorer(打分器); hallucination(幻觉,模型编造的错误答案)
📖 "LLM-as-a-judge is a technique that leverages an LLM to score the quality of answers." — 原文
📖 "LLMs can reason about language beyond surface-level string comparisons, enabling them to evaluate answers more accurately." — 原文
📖 "A heuristic like the Levenshtein string distance would indicate that the response is incorrect." — 原文
🧪 实例 一个用 tool call 强约束输出的 numeric rater 判官骨架(对比 Q/output/expected 后返回 1-10 打分并归一化到 0-1):
python
PROMPT = """\
You are comparing a submitted answer to an expert answer on a given question. Here is the data:
[BEGIN DATA]
[Question]: {input}
[Expert]: {expected}
[Submission]: {output}
[END DATA]

Compare the factual content of the submitted answer with the expert answer. Ignore any differences in style, grammar, or punctuation.
Rate the submission on a scale of 1 to 10.
"""
# tool_choice 强制调用 rate 函数,arguments["rating"] 取值 1..10
return (arguments["rating"] - 1) / 9
🔍 追问 为什么不直接让模型自然语言输出分数? → cookbook 用 function tool + tool_choice 强制调用 rate,把评分固定成 schema 里的整数字段,便于稳定解析成数值分,避免解析自由文本。
📚 拓展阅读
QLLM-as-a-judge 的三种做法(打分 / 加推理 / 分类)分别怎样?为什么"分类"胜出?深挖·拓展🔥高频
numeric-rater chain-of-thought classifier partial-credit
⏱️ 现行
cookbook 顺着一个直觉的演进链做了三版判官并做 meta 评估。第一版 numeric rater:让 LLM 给答案打 1 到 10 分——直觉上最容易转成数值分,但结果整体只有约 94%,问题在于模型会对错误答案给出 1 到 10 之间的中间分。第二版 加推理(Chain of Thought):让模型先写出 step-by-step 理由再打分,本意是既提升分数又看清它为何这么打;但分数不升反降约 3%,从理由里能看出模型是在"自作主张地算 partial credit(部分给分)"——这正是数值打分对模型和人都常见的通病。第三版 classifier(分类):不再让模型自由打分,而是把具体判据写死成 A–E 五个选项让模型分类,再用一张 CHOICE_SCORES 映射表把选项转成分数(并特意把"超集/superset"的 B 惩罚得和"冲突"的 D 一样重)。给出明确判据后模型不再擅自打折扣,分数跃升到 98%。核心结论:把评估从"主观打分"改造成"按明确 criteria 分类"能显著提准,因为它消除了 partial-credit 的随意性。

flowchart LR
    A[Numeric rater
1-10 打分] -->|~94% 有中间分| B[加 CoT 推理] B -->|-3% 暴露 partial credit| C[Classifier
A-E 明确判据] C -->|98%| D[显著提升]
术语 numeric rater(1-10 数值打分判官); Chain of Thought(先推理后作答); partial credit(部分给分,数值打分的通病); classifier(按明确判据分类); CHOICE_SCORES(选项到分数的映射表)
📖 "A common initial intuition when creating an LLM-as-a-judge is asking the LLM to rate the answer on a scale of 1 to 5." — 原文
📖 "It looks like the numeric rater scored almost 94% in total." — 原文
📖 "It looks like the model is applying its own judgement to compute partial credit." — 原文
📖 "Next, we'll spell out specific criteria and ask the model to classify the answer according to those criteria." — 原文
📖 "The classifier scored 98% which is a significant improvement!" — 原文
🧪 实例 分类判官的判据与打分映射(注意因为要抓幻觉,把 B 的 superset 罚得和 D 一样重):
python
# (A) subset & consistent  (B) superset & consistent  (C) all same details
# (D) disagreement  (E) differ but doesn't matter for factuality
CHOICE_SCORES = {"A": 0.5, "B": 0, "C": 1, "D": 0, "E": 1}
🔍 追问 加了推理反而更差,说明推理没用吗? → 不是;它没提分,但暴露了模型在算 partial credit 的原因,从而指引改成分类法——推理在这里主要价值是可解释性/诊断。
📚 拓展阅读
QOpenAI 现在推荐的托管 Evals API 是什么?怎样用 client.evals.create 定义一个模型评分的 eval?深挖·拓展🔥高频
Evals API client.evals.create score_model testing_criteria
⏱️ 现行
OpenAI 现在提供了一个带 API 的托管 evals 产品,并明确推荐用它取代早期需要本地 clone 的开源 evals 框架。用法核心是 client.evals.create(...):它把一个 eval 注册到平台,你要给它三样东西——data_source_config(声明每条数据 item 的 schema,type: custominclude_sample_schema: True 表示会带上模型采样输出)、testing_criteria(判据列表)。当判据 typescore_model 时,你实际是在配一个 LLM 判官:指定打分用的 model(例如 o3)、system/user 两条 input 消息(把评分 rubric 和待评内容模板塞进去)、分数 range(如 [1, 7])和 pass_threshold(如 5.0)。这样定义完 eval 后,再用 client.evals.runs.create 针对它发起模型运行。相比开源框架,托管 API 的好处是评估在平台侧可复现、可在 Dashboard 可视化,且直接对接 Completions/Responses 端点。
术语 client.evals.create(把 eval 注册到平台); data_source_config(数据项 schema 声明); include_sample_schema(带上模型采样输出); testing_criteria(判据列表); score_model(用 LLM 判官打分的判据类型); pass_threshold(通过阈值)
📖 "OpenAI now has a hosted evals product with an API! We recommend you use this instead." — 原文
📖 "client.evals.create(...) registers the eval with the platform." — 原文
🧪 实例score_model 判据创建一个 1-7 分、阈值 5.0 的代码问答 eval:
python
logs_eval = client.evals.create(
    name="Code QA Eval",
    data_source_config={
        "type": "custom",
        "item_schema": {"type": "object", "properties": {"input": {"type": "string"}}},
        "include_sample_schema": True,
    },
    testing_criteria=[
        {
            "type": "score_model",
            "name": "General Evaluator",
            "model": "o3",
            "input": [
                {"role": "system", "content": structured_output_grader},
                {"role": "user", "content": structured_output_grader_user_prompt},
            ],
            "range": [1, 7],
            "pass_threshold": 5.0,
        }
    ],
)
🔍 追问 rubric 里 1-7 分是怎么算出来的? → 先按 completeness(完整度)在 1-7 打分,再乘一个 0 到 1 的 quality modifier(质量系数);100% 覆盖且全高质就是 7×1,全低质就是 7×0.5。
📚 拓展阅读
Q工具调用评估(tool evaluation)是评什么?为什么要对同一个 eval 同时跑 Completions 和 Responses 两个 run?深挖·拓展中频
tool-evaluation extract_symbols completions-vs-responses
⏱️ 现行
工具调用评估要度量并改进的是"模型从源代码里抽取结构化信息的能力"——具体案例是抽出 Python 文件里定义的 symbols(函数、类、方法、变量)。做法是给模型挂一个 extract_symbols 的 function tool(参数 schema 要求返回 symbols 数组,每项含 namesymbol_type),让模型通过 tool 调用把抽取结果结构化输出;评分时从 sample.output_tools[0].function.arguments.symbols 取出抽取到的 symbols 交给 rubric 判官打分。关键设计是:针对同一个 eval 同时发起两个 run——一个走 Completions 端点、一个走 Responses 端点,用 client.evals.runs.create 分别配 data_source.typecompletions / responses。这样能在完全相同的数据与判据下横向对比两种 API/模型的表现,再用 poller 轮询 run 状态到 completed/failed,最后从 output_items 拿出各自的 symbols 做 precision/recall 之类的对比。
术语 tool evaluation(评估模型的工具调用/结构化抽取能力); extract_symbols(抽取代码符号的 function tool); output_tools(采样输出里的工具调用结果路径); runs.create(对同一 eval 发起模型运行); completions / responses(两种被对比的端点类型)
📖 "This cookbook shows how to measure and improve a model’s ability to extract structured information from source code with tool evaluation." — 原文
📖 "Here we launch two runs against the same eval: one that calls the Completions endpoint, and one that calls the Responses endpoint." — 原文
📖 "specifies the extracted symbols from the code file based on the tool invocation." — 原文
🧪 实例 用一个 poller 同时轮询两个 run,直到都 completed/failed:
python
def poll_runs(eval_id, run_ids):
    while True:
        runs = [client.evals.runs.retrieve(run_id, eval_id=eval_id) for run_id in run_ids]
        for run in runs:
            print(run.id, run.status, run.result_counts)
        if all(run.status in ("completed", "failed") for run in runs):
            break
        time.sleep(5)
🔍 追问 两个 run 的 sampling_params 里为什么设 seed? → Completions run 里设 "seed": 42 是为了让采样尽量可复现,配合固定 temperature/top_p 让两端对比更公平。
📚 拓展阅读
Q怎样"评估评估器"(meta-evaluate)一个 LLM 判官的质量?深挖·拓展中频
meta-evaluation known-hallucinations ground-truth
⏱️ 现行
判官本身也会出错,所以要有办法衡量判官准不准——这就是 meta 评估。cookbook 的巧思是构造一批"已知标准答案"的样本来反向考判官:因为 Braintrust 的 scorer 就是设计来抓幻觉的,就用 CoQA 的真实问答对去生成已知的 hallucination(让另一个 LLM 不看原文、自信地编造带虚构细节的答案)。由于这些答案确定是错的、其 ground-truth 分应为 0,于是判官的质量就可以用"它把这些幻觉答案judge 成 0 的比例有多高"来量化。整套评估有三部分:Data(问题+幻觉答案+真答案,expected=0)、Task(对每条 input 调判官)、Scores(用归一化差值 1 - abs(output - expected) 比对判官分与 ground truth)。这样三版判官(94% / 更差 3% / 98%)就能被客观横比。要点:一定要用你自己的私有数据验证,因为公开数据集可能已被底层 LLM 记住。
术语 meta-evaluate(评估评估器本身); known hallucinations(已知为错的幻觉样本); ground truth(真值,幻觉的 expected 分为 0); normalized_diff(1 - abs(output-expected) 归一化差值打分)
📖 "We'll consider a few popular approaches for creating an LLM-as-a-judge." — 原文
📖 "Since we know that the hallucinated answers are incorrect, we'll assess the quality of an evaluator by testing how often it scores the hallucinated answers as 0." — 原文
📖 "we can use the QA pairs to generate known hallucinations." — 原文
🧪 实例 用 Braintrust 的 Eval 把 Data/Task/Scores 三部分组装起来跑一次判官质量实验:
python
await Eval(
    "LLM-as-a-judge",
    data=data,                 # 幻觉样本, expected=0
    task=task,                 # 对每条 input 调判官
    scores=[normalized_diff],  # 1 - abs(output - expected)
    experiment_name="Classifier",
    max_concurrency=10,
)
🔍 追问 为什么强调用私有数据测判官? → cookbook 指出公开数据集有被底层 LLM 记住(memorized)的风险,会让判官在这些样本上虚高,所以开发自己的 scorer 时最好用私有数据验证。
📚 拓展阅读
Q两大类 eval——代码校验 vs 模型评分(model-graded)——各自适合什么场景?模型评分有哪些坑与最佳实践?深挖·拓展中频
code-based-eval model-graded basic-vs-model-templates error-rate
⏱️ 现行
给 completion 打分本质上只有两条路:一是在代码里写校验逻辑,二是让模型自己去审查答案。代码校验最简单常见,适合"理想答案变化很小"的场景——比如选择题、有明确唯一答案的简单问题,甚至"输出是否为合法 JSON"这种可以直接 parse 判定的;OpenAI 把这类做成了 Basic Eval Templates(含 match / includes / fuzzyMatch 三种确定性 metric)。而当理想答案本身可变化很大(如开放式问答),确定性匹配会误杀措辞不同的正确答案,这时就要 Model-Graded Templates:用 LLM 去比对输出与理想答案并判断准确性。模型评分的两个关键坑与实践是:其一,它一定有 error rate,所以在大规模跑之前必须先用人工评估校准;其二,最好用与产出答案不同的模型来评分(比如用 GPT-4GPT-3.5 的答案),并让判官先推理再下结论,这样效果最好。取舍就是:确定性匹配便宜可靠但覆盖窄,模型评分覆盖广但更贵且需校准。
术语 Basic Eval Templates(确定性比对模板); match / includes / fuzzyMatch(三种确定性打分 metric); Model-Graded Templates(用 LLM 评分的模板); error rate(模型评分固有误差,需人工校准)
📖 "There are two main ways we can evaluate/grade completions: writing some validation logic in code" — 原文
📖 "In cases where the desired model response has very little variation, such as answering multiple choice questions" — 原文
📖 "we have found that using the model to grade itself is a viable strategy for automated evaluation" — 原文
📖 "Model grading will have an error rate, so it is important to validate" — 原文
📖 "sense to use a different model to do grading from the one that did the completion, like using GPT-4 to" — 原文
🧪 实例 model-graded 的两阶段流程——先让模型答题,再另起一次调用让模型审查:"Is this following joke funny? First reason step by step, then answer yes or no",若新 completion 以"yes"结尾则判原答案正确。这体现了"先推理再判断"的实践。
🔍 追问 为什么不干脆全用模型评分? → 因为它有 error rate 且更贵;对答案变化很小的场景(选择题、JSON 合法性)用确定性 match/includes/fuzzyMatch 更省更稳,模型评分留给开放式、措辞可变的答案。
📚 拓展阅读
QOpenAI 开源 Evals 框架的结构是怎样的?如何用 oaieval CLI 跑一个 eval?深挖·拓展低频
openai-evals oaieval YAML jsonl
⏱️ 历史/被托管 API 取代
开源 Evals 框架由两部分组成:一个用来评估 LLM(或建于其上的系统)的框架,加一个开源的挑战性 eval 注册表。它的核心抽象是"一个 eval = 一个数据集 + 一个在 YAML 文件里定义的 eval class"。要建一个 eval 你需要:把测试数据做成 jsonl 格式,再选一个 eval template。然后写一个 .yaml 注册文件声明 iddescriptiondisclaimermetrics(三种可选:match / includes / fuzzyMatch)等属性,并在 args 里用 samples_jsonl 指向数据文件。运行用 oaieval CLI:oaieval gpt-3.5-turbo spider-sql,它接收一个模型名和一个 eval 名,会去 evals/registry/evals 目录找对应 YAML。跑完后控制台打印最终准确率报告,并给出一个临时文件路径存放完整结构化日志(日志在 /tmp/evallogs,可用来逐条排查失败样本)。另有 oaievalset 用于一次跑一组 eval。注意:官方现在推荐改用托管 Evals API,这套开源 CLI 属早期方案。
术语 eval class(在 YAML 里定义的评估类); oaieval(跑单个 eval 的 CLI); oaievalset(跑一组 eval 的 CLI); samples_jsonl(YAML args 里指向数据集的字段); /tmp/evallogs(结构化评估日志目录)
📖 "At its core, an eval is a dataset and an eval class that is defined in a YAML file." — 原文
📖 "metrics - There are three types of eval metrics we can choose from: match, includes, fuzzyMatch" — 原文
📖 "We can run this eval using the oaieval CLI." — 原文
🧪 实例 一个 spider-sql 的 eval 注册 YAML 片段,用 model-grading 而非真正执行 SQL:
yaml
spider-sql.dev.v0:
  class: evals.elsuite.modelgraded.classify:ModelBasedClassify
  args:
    samples_jsonl: sql/spider_sql.jsonl
    eval_type: cot_classify
    modelgraded_spec: sql

运行:
bash
oaieval gpt-3.5-turbo spider-sql --max_samples 25
🔍 追问 为什么这套 spider-sql eval 用 model-grading 而不直接跑 SQL? → 它的 disclaimer 说明 SQL 并未真正被执行、而是靠模型评判,因此可能把正确 SQL 判错或把错的判对——这正好呼应了"model grading 有 error rate"的通病。
📚 拓展阅读
中频

守护栏与幻觉防护

原文 原文 原文
Q什么是 guardrail?input guardrail 与 output guardrail 分别拦什么、为什么要分成这两层?深挖·拓展🔥高频
guardrails input-output detective-controls
⏱️ 现行
guardrail 是围绕 LLM 的一类"检测型控制"(detective controls),用来给本质上带随机性的模型加上可操控性(steerability),是把 LLM 从原型推向生产时最常见的性能优化环节。Cookbook 把它拆成两层:input guardrail 在用户内容到达 LLM 之前就拦截不合适的请求,典型用途是 topical(话题越界)、jailbreaking(劫持越狱)、prompt injection(提示注入),它作为预防型控制在主调用之前或并行运行,一旦命中就让应用走不同分支;output guardrail 则管住模型已经产出的内容,常见形态是 hallucination/fact-checking、moderation(按品牌与企业规范改写或拦截)、以及 syntax check(检测结构化输出是否损坏、function_call 的 arguments 是否符合预期 schema)。分两层的原因是攻击面不同:输入侧要在"喂进去之前"止损省算力,输出侧要在"发给用户之前"兜底,二者组合才能既防注入又防幻觉与违规。
术语 detective controls(检测型控制,事后/事中发现问题的机制); steerability(可操控性,对模型行为的引导能力); input guardrail(输入守护栏); output guardrail(输出守护栏); syntax checks(语法/结构校验,常用于 function calling)
📖 "A guardrail is a generic term for detective controls that aim to steer your application." — 原文
📖 "Guardrails are detective controls that aim to prevent harmful content getting to your applications and your users, and add steerability to your LLM in production." — 原文
🧪 实例 一个只谈猫狗的助手,input 层用 topical guardrail 判定"allowed / not_allowed",output 层再用 moderation 判定品种推荐是否越界。
flowchart LR
    U[用户请求] --> IG{input guardrail
topical?} IG -- not_allowed --> B1[返回兜底话术] IG -- allowed --> L[LLM 生成响应] L --> OG{output guardrail
moderation/幻觉?} OG -- flagged --> B2[拦截/改写] OG -- pass --> R[返回给用户]
🔍 追问 syntax check 这类 output guardrail 常配合什么能力使用? → 常配合 function calling,确保模型返回 function_call 时其 arguments 里带回预期 schema,检测到损坏就重试或优雅失败。
📚 拓展阅读
Q为什么守护栏推荐用异步(async)方式与主 LLM 调用并行跑?这么设计的权衡是什么?深挖·拓展🔥高频
async latency architecture
⏱️ 现行
因为守护栏是额外的检测调用,如果串行执行会把延迟直接叠加到用户等待时间上。Cookbook 推荐的设计是把守护栏与主 LLM 调用异步并行发出:两个任务同时跑,谁先返回就先判断——若守护栏命中就直接返回它的兜底响应(并取消主调用任务),否则返回 LLM 的响应。这样守护栏可以横向扩展(数量与范围增加时对用户体验的冲击最小)。但异步也有代价:moderation cookbook 特别提醒,async 虽然能压低延迟,却可能带来不必要的成本——如果内容本会在处理前被 flag,串行反而能省掉 completion 的费用;所以要在"降延迟"和"多花钱"之间权衡。实现上用 asyncio.create_task 起两个 task,asyncio.wait(..., return_when=FIRST_COMPLETED) 轮询谁先完成。
术语 asyncio.create_task(创建并发任务); FIRST_COMPLETED(等到任一任务先完成即返回); chat_task.cancel()(守护栏命中时取消主生成任务); horizontal scaling(横向扩展守护栏)
📖 "A common design to minimize latency is to send your guardrails asynchronously along with your main LLM call. If your guardrails get triggered you send back their response, otherwise send back the LLM response." — 原文
📖 "It's important to note that while the async mode is effective in minimizing latency, it can also lead to unnecessary costs. Specifically, you could avoid completion costs if the content is flagged before processing." — 原文
📖 "By embracing asynchronous design principles, you can scale guardrails horizontally to minimize the impact to the user as your guardrails increase in number and scope." — 原文
🧪 实例 并行跑 topical 守护栏和主生成,守护栏说 not_allowed 就取消主任务:
python
async def execute_chat_with_guardrail(user_request):
    topical_guardrail_task = asyncio.create_task(topical_guardrail(user_request))
    chat_task = asyncio.create_task(get_chat_response(user_request))

    while True:
        done, _ = await asyncio.wait(
            [topical_guardrail_task, chat_task], return_when=asyncio.FIRST_COMPLETED
        )
        if topical_guardrail_task in done:
            guardrail_response = topical_guardrail_task.result()
            if guardrail_response == "not_allowed":
                chat_task.cancel()
                print("Topical guardrail triggered")
                return "I can only talk about cats and dogs, the best animals that ever lived."
            elif chat_task in done:
                chat_response = chat_task.result()
                return chat_response
        else:
            await asyncio.sleep(0.1)  # sleep for a bit before checking the tasks again
🔍 追问 既然 async 能降延迟,为什么 moderation 场景反而要谨慎? → 因为输入若本会被 flag,串行可以在生成前就短路、省下 completion 成本;async 并行则可能白白付了生成费用,所以要按"违规率×生成成本"权衡。
📚 拓展阅读
Q设计守护栏时如何在 accuracy / latency / cost 之间权衡?阈值(threshold)又该怎么定?深挖·拓展🔥高频
tradeoff threshold confusion-matrix
⏱️ 现行
守护栏设计的核心是在准确率、延迟、成本三者间取最优:目标是用对成本和用户体验最小的影响换取尽量高的准确率。示例里 topical 守护栏只用一个简单 prompt + gpt-4o-mini,是在延迟/成本与"够用的准确率"间折中;若要进一步优化,提升 accuracy 可以 fine-tune 或加 few-shot、甚至用 RAG,降 latency/cost 可以微调更小模型(如 babbage-002 或开源 Llama)。阈值方面,output moderation 用 1–5 打分并对"3 或更高"直接拦截;设阈值是常见优化点,建议构建评测集并用混淆矩阵(confusion matrix)来定容忍度。权衡本质是 false positive 与 false negative 的取舍:误报多会割裂用户体验、让助手显得不好用;漏报多可能给业务造成持久伤害(被诱导回答不当问题或被注入/越狱)。因此像 jailbreaking 这种高风险场景阈值要设得很低,而低风险场景可以接受一些漏报。
术语 false positive(误报,把正常内容拦了); false negative(漏报,该拦没拦); confusion matrix(混淆矩阵,用于标定阈值); threshold(拦截阈值); babbage-002(可微调的小模型,用于降本降延迟)
📖 "When designing guardrails it is important to consider the trade-off between accuracy, latency and cost, where you try to achieve maximum accuracy for the least impact to your bottom line and the user's experience." — 原文
📖 "Setting this threshold is a common area for optimization - we recommend building an evaluation set and grading the results using a confusion matrix to set the right tolerance for your guardrail." — 原文
📖 "More false negatives can cause lasting harm to your business, as people get the assistant to answer inappropriate questions, or prompt inject/jailbreak it." — 原文
🧪 实例 output 守护栏返回 1–5 分数,>= 3 即拦截:
python
moderation_response = await moderation_guardrail(chat_response)

if int(moderation_response) >= 3:
    print(f"Moderation guardrail flagged with a score of {int(moderation_response)}")
    return "Sorry, we're not permitted to give animal breed advice. I can help you with any general queries you might have."
else:
    print('Passed moderation')
    return chat_response
🔍 追问 为什么 jailbreaking 场景阈值要设得特别低? → 因为一旦 LLM 被劫持产出危险内容并流到社交媒体,对业务的风险极高;宁可多误报也要压住漏报。
📚 拓展阅读
Qmoderation guardrail 用 G-Eval 打分的可扩展框架长什么样?domain / criteria / content / steps 各起什么作用?中频
moderation g-eval scoring
⏱️ 现行
这是一个 output guardrail,借用 G-Eval 评估法对 LLM 响应里"不想要的内容"打分。它被做成一个可扩展框架:传入 domain(要审的内容类型)、对一段 content 应用一组 criteria(明确该含/不该含什么),再用一套 steps(给 LLM 的分步打分指令),最终让 LLM 返回一个 1–5 的离散分数。机制上把"是否越界"这种模糊判断转成结构化打分,4 步流程是:设 domain → 给 criteria → 给 step-by-step 指令 → 返回 1–5 分。这么设计的好处是同一套 prompt 模板换 domain/criteria 就能复用于不同审核场景,而离散分数又能直接对接阈值拦截逻辑。示例里 domain 是 "animal breed recommendation",criteria 要求内容只给猫狗的一般性建议、不得推荐具体品种,评分 1 表示无明确品种建议、5 表示出现多个具名品种。
术语 G-Eval(用 LLM 按标准逐步打分的评估法); domain(审核领域名); criteria(评分标准,规定该含/不该含); steps(分步打分指令); discrete score 1-5(1 到 5 的离散分数)
📖 "Here we implement a moderation guardrail that uses a version of the G-Eval evaluation method to score the presence of unwanted content in the LLM's response." — 原文
📖 "To accomplish this we will make an extensible framework for moderating content that takes in a domain and applies criteria to a piece of content using a set of steps:" — 原文
📖 "The LLM returns a discrete score from 1-5." — 原文
🧪 实例 moderation prompt 用 .format() 把 domain/criteria/steps/content 填进模板末尾要求"score only":
```python
moderation_system_prompt = """
You are a moderation assistant. Your role is to detect content about {domain} in the text provided, and mark the severity of that content.
{domain}
QCriteria中频
QInstructions中频
QContent中频
QEvaluation (score only!)深挖·拓展中频
🔍 追问 为什么最后一行要写 Evaluation (score only!)? → 为了约束模型只吐出分数,便于 int(moderation_response) 直接解析并与阈值比较,避免夹带解释文本破坏下游逻辑。
📚 拓展阅读
QModeration API 怎么用于 input/output moderation?它能审哪些类别、能审图片吗?阈值怎么设?深挖·拓展中频
moderation-api categories image
⏱️ 现行
Moderation API 是一个可检查文本或图片是否潜在有害的工具。input moderation 在内容进入 LLM 前用它分析、命中就拒绝或让用户改写;output moderation 在响应到达用户前再审一遍。用法很直接:client.moderations.create(input=...) 返回 results[0].flagged 布尔值。图片用 omni-moderation-latest 模型,传 image_url 类型输入,可读取各类别的 flag(sexual、sexual/minors、harassment、harassment/threatening、hate、hate/threatening、illicit、illicit/violent、self-harm、self-harm/intent、self-harm/instructions、violence、violence/graphic),文本和图片可放在同一请求里。阈值方面,OpenAI 已为各类别选了平衡精确率与召回率的阈值,但你的用例容忍度可能不同,同样建议用评测集+混淆矩阵来标定;权衡仍是误报割裂体验、漏报伤害业务。注意所有 OpenAI 模型对其生成内容本身就带 output moderation,所以一个"描述暴力电影场景"的请求会在输出侧被 flag。
术语 client.moderations.create(调用审核端点); results[0].flagged(是否被标记的总布尔); omni-moderation-latest(支持图片的审核模型); categories(细分违规类别,如 violence/graphic、self-harm/intent)
📖 "This notebook will use our Moderation API, a tool you can use to check whether text or an image is potentially harmful." — 原文
📖 "OpenAI has selected thresholds for moderation categories that balance precision and recall for our use cases, but your use case or tolerance for moderation may be different." — 原文
🧪 实例 输入审核的最小封装,命中即返回布尔:
python
async def check_moderation_flag(expression):
    moderation_response = client.moderations.create(input=expression)
    flagged = moderation_response.results[0].flagged
    return flagged
🔍 追问 为什么示例里"描述一段暴力电影场景"会被输出侧拦下,而这被称作 not as anticipated? → 因为所有 OpenAI 模型对生成内容自带 output moderation;若要放行这类并非明显有害的内容,需要用自定义 moderation 调整审核设置。
📚 拓展阅读
Q怎么开发一个"幻觉防护"守护栏?构建 eval set、定义指标、few-shot 提示各解决什么问题?深挖·拓展中频
hallucination eval-set few-shot precision-recall
⏱️ 现行
幻觉守护栏是一套规则与检查,确保 LLM 输出准确、恰当、符合用户预期。开发流程三步:构建强 eval set、明确度量幻觉的具体标准、用 few-shot prompting 提升准确率。设想一个客服 agent 按知识库政策处理退货/退款等工单,先用 GPT-4o 生成一批政策,再生成"遵守/不遵守政策"的对话样本作为带标签评测集。构建守护栏的指导原则包括:给出非常具体的度量指标(把"真相"拆成可识别、可测量的分项,因为 truthfulness、relevance 这类笼统概念难测);术语要一致(assistant vs agent 混用会让模型糊涂);从最强模型起步(GPT-4o 虽贵,但先保证高准确率,验证后再降到 gpt-3.5-turbo 省成本);既逐句独立评估、也评估整条响应的整体意图。守护栏对每句话在 factualAccuracy、relevance、policyCompliance、contextualCoherence 四项打 0/1,四项全 1 才算 Pass。示例用 precision/recall 衡量,得到 Precision 0.97、Recall 1.00,说明守护栏能准确识别输出中的幻觉。
术语 eval set(带标签评测集); few-shot prompting(少样本示例提升准确率); factualAccuracy/relevance/policyCompliance/contextualCoherence(逐句四项打分维度); precision(精确率); recall(召回率)
📖 "A guardrail is a set of rules and checks designed to ensure that the outputs of an LLM are accurate, appropriate, and aligned with user expectations." — 原文
📖 "Although GPT-4o may be more expensive, it is important to start with the most advanced model so we can ensure a high degree of accuracy" — 原文
📖 "This means that the guardrails are able to accurately identify hallucinations in the model outputs." — 原文
🧪 实例 逐句四项打分,总分等于 4 才判 Pass:
python
score_sum = (
    response_item.get('factualAccuracy', 0) +
    response_item.get('relevance', 0) +
    response_item.get('policyCompliance', 0) +
    response_item.get('contextualCoherence', 0)
)

# Determine if the response item is a pass or fail
hallucination_status = 'Pass' if score_sum == 4 else 'Fail'
🔍 追问 为什么要"逐句独立评估"又"整体评估"? → 长响应逐句评估能抓住单句的事实错误,而整体评估能保住上下文意图、避免拆句丢失重要语境,两者结合更稳。
📚 拓展阅读
Q用 LLM 当守护栏有哪些局限?对话变长、over-refusals 各是什么风险?怎么缓解?深挖·拓展中频
limitations jailbreaking over-refusals mitigation
⏱️ 现行
用 LLM 做守护栏时要清楚它的局限。第一,守护栏本身与被保护的 LLM 有同样的脆弱性——一次 prompt injection 可能同时骗过守护栏和真正的调用。第二,随着对话变长,指令被额外文本稀释,LLM 更容易被 jailbreaking。第三,若为了补偿上述问题把守护栏做得过严,会伤害体验,表现为 over-refusals——因为与注入/越狱有相似之处,守护栏把无害请求也拒了。缓解手段:把守护栏与基于规则或更传统的机器学习检测模型结合能降低部分风险;也有客户让守护栏只考虑最新一条消息,以减轻模型被长对话搞混的风险;还建议渐进式灰度上线并主动监控对话,一旦发现新型注入/越狱,就补充守护栏或把它们作为训练样本喂给现有守护栏。核心权衡是:既要防住注入越狱,又不能过度限制把正常用户挡在门外。
术语 prompt injection(提示注入,可同时绕过守护栏与主调用); jailbreaking(越狱劫持,随对话变长风险上升); over-refusals(过度拒答); rules-based / traditional ML(规则或传统机器学习检测,用于组合缓解); gradual roll-out(渐进灰度上线+监控)
📖 "When using LLMs as a guardrail, be aware that they have the same vulnerabilities as your base LLM call itself." — 原文
📖 "As conversations get longer, LLMs are more susceptible to jailbreaking as your instructions become diluted by the extra text." — 原文
📖 "This manifests as over-refusals, where your guardrails reject innocuous user requests because there are similarities with prompt injection or jailbreaking attempts." — 原文
📖 "We've also seen customers have guardrails that only ever consider the latest message, to alleviate the risks of the model being confused by a long conversation." — 原文
🧪 实例 一个既能骗过守护栏又能骗过主调用的 prompt injection 尝试,说明"同源脆弱性"——因此单靠一层 LLM 守护栏不够,要叠加规则/传统 ML 检测与只看最新消息的策略。
🔍 追问 为什么"只考虑最新一条消息"能缓解越狱? → 因为越狱常靠长对话把守护栏指令稀释;只喂最新消息就避免模型被前文堆积的诱导文本搞混。
📚 拓展阅读
Qcustom moderation 与 Moderation API 有什么区别?什么时候该自己用 gpt-4o-mini 做审核?深挖·拓展低频
custom-moderation niche parameters
⏱️ 现行
custom moderation 提供一种量身定制的内容过滤,能精确贴合特定社区标准或话题,适合小众平台或专门化内容;它明确不使用 Moderation API,而是额外调一次 gpt-4o-mini 来评估消息、判断是否该被审核。相比 Moderation API 覆盖的通用类别,自定义审核给出更高的控制度,但也需要更多搭建与维护成本。它与 guardrails 目标一致(保障内容安全、设标准过滤输入或输出),可以作为 Moderation API 的替代或扩展,走的是 completions 端点。实现上把 parameters(如 "political content, misinformation")拼进 prompt,要求模型以 JSON 返回 flagged(布尔)、reason(理由)、parameters(各参数取值)。这样在通用审核类别之外,你能针对"政治内容""误导信息"这类平台特有的敏感维度做判定——例如"政府隐瞒疫情真相"会被标为 political content 与 misinformation 皆 true。
术语 custom moderation(自定义审核,不走 Moderation API); parameters(自定义审核维度,如政治/误导); response_format={"type":"json_object"}(强制 JSON 输出); completions endpoint(自定义审核所用端点)
📖 "Custom moderation provides a tailored approach to content filtering, allowing for precise adherence to specific community standards or topics, making it an ideal choice for niche platforms or specialized content." — 原文
📖 "Note: This section does not use the Moderation API." — 原文
📖 "We will use an extra call to gpt-4o mini to assess messages and decide if they should be moderated or not." — 原文
🧪 实例 用 gpt-4o-mini + 强制 JSON 做参数化自定义审核:
python
def custom_moderation(content, parameters):
    # Define the prompt 
    prompt = f"""Please assess the following content for any inappropriate material. You should base your assessment on the given parameters.
    Your answer should be in json format with the following fields: 
        - flagged: a boolean indicating whether the content is flagged for any of the categories in the parameters
        - reason: a string explaining the reason for the flag, if any
        - parameters: a dictionary of the parameters used for the assessment and their values
    Parameters: {parameters}\n\nContent:\n{content}\n\nAssessment:"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        response_format={ "type": "json_object" },
        messages=[
            {"role": "system", "content": "You are a content moderation assistant."},
            {"role": "user", "content": prompt}
        ]
    )
    return response.choices[0].message.content
🔍 追问 既然 custom moderation 更可控,为什么不总用它替代 Moderation API? → 因为它需要更多搭建与维护成本,还多一次模型调用;通用有害类别用现成 Moderation API 更省事,自定义只在小众/专门维度上补位。
📚 拓展阅读
中频

微调实战:SFT / DPO / RFT

原文 原文 原文
QOpenAI 平台支持的微调方法里,SFT、DPO、RFT 分别在什么场景下用?怎么选?深挖·拓展🔥高频
fine-tuning SFT DPO RFT
⏱️ 现行
微调的本质是"在更小的、领域特定的数据集上继续训练,把模型优化到某个具体任务上",动机无非两条:提升某任务的表现,或提升效率(省 token、把大模型能力蒸馏进小模型)。三种主流方法的机制截然不同因而适用面也不同:SFT 用传统监督学习,喂 input-output 成对数据,调权重去最小化预测输出与目标输出的差距,模型会"复刻"它在成对数据里看到的特征——所以它擅长强化模型已有知识、定制回复结构/语气/格式、教复杂指令或纠正指令遵循失败、以及为省 token 做蒸馏;但不擅长灌入全新知识(该用 RAG)或处理主观质量的任务。DPO 用成对偏好比较(preferred vs rejected)优化模型偏向某类输出,学的是偏好模式,因此擅长对齐主观偏好(语气、礼貌、品牌腔调)、按人评反馈精修输出;不擅长学全新任务或没有清晰人类偏好信号的任务。RFT 用强化学习加一个奖励信号(grader 或 reward model)为复杂目标微调,模型在训练时对给定 prompt 生成输出、每个输出被打分、再更新参数去最大化奖励,从而强化能带来更好结果的推理/决策策略;擅长需要高级推理的复杂领域任务、精修已有部分能力、有可度量反馈的场景;但不擅长模型本来毫无技能、或没有清晰反馈/可度量信号的任务。选型的核心判据就是:任务是"已有知识/格式"(SFT)、"主观偏好对齐"(DPO)、还是"可度量奖励下的推理提升"(RFT)。
术语 SFT(监督微调,input-output 成对); DPO(直接偏好优化,成对偏好); RFT(强化微调,奖励信号驱动); grader(打分器/奖励函数); RAG(检索增强,用于灌入新知识而非微调)
📖 "Fine-tuning is the process of continuing training on a smaller, domain-specific dataset to optimize a model for a specific task." — 原文
📖 "this technique employs traditional supervised learning using input-output pairs to adjust model parameters." — 原文
📖 "this technique uses pairwise comparisons (e.g., preferred and rejected example responses) to optimize a model to favor certain outputs over others." — 原文
📖 "this technique uses reinforcement learning with a reward signal (via a grader or reward model) to fine-tune the model for complex objectives." — 原文
🧪 实例 官方给的选型速查表(节选自 guide)——
text
SFT  → 强化已有知识 / 定制结构与语气 / 特定格式 / 教复杂指令 / 省 token 与蒸馏
        ✗ 灌入全新知识(用 RAG) / 主观质量任务
DPO  → 对齐主观偏好(语气礼貌) / 人评反馈精修 / 细腻行为对齐
        ✗ 学全新任务 / 无清晰人类偏好信号
RFT  → 需高级推理的复杂领域任务 / 精修已有部分能力 / 有可度量反馈
        ✗ 模型本无技能 / 无清晰反馈或可度量信号
🔍 追问 想给模型加一批它完全不知道的公司内部知识,该微调吗? → 不该用 SFT 灌新知识,官方明确 SFT "Not Good For" 是 "Adding entirely new knowledge (consider RAG instead)",应优先考虑 RAG。
📚 拓展阅读
QDPO 是怎么工作的?为什么官方推荐"先 SFT 再 DPO"的工作流?深挖·拓展低频
DPO RLHF workflow alignment
⏱️ 现行
DPO 用成对偏好数据(排序过的响应对)直接优化模型偏向某些输出而非另一些,这些偏好通常来自人类。它的关键卖点是"轻量":直接优化,省掉了单独训练一个 reward model 或跑一整套复杂 RL 流程的开销,因此是 RLHF 的轻量替代。适用场景是响应质量主观、无法客观度量、或语气/风格/得体性/清晰度这类细腻标准重要(存在多个有效输出)的情况。官方推荐的工作流是两步:先在你偏好的响应子集上做 SFT,再把 SFT 后的模型作为起点用偏好对比数据做 DPO。这样做的道理在于:先 SFT 能建立一个稳健的初始 policy,让模型本来就已经倾向于正确响应,从而在 DPO 阶段减小权重更新的幅度,稳定训练、防止过拟合,让 DPO 专注于精修细腻的差别,最终 SFT-then-DPO 收敛更快、结果质量更高。控制这个平衡的超参是 beta(β):0~2 之间的浮点数,技术上 beta 缩放 DPO 损失里 log 概率的差值——β 越大,基于 sigmoid 的损失函数在更小的概率差处就饱和,产生更小的权重更新,从而更保守地保留旧行为;β≈1 是推荐的实用起点,β 接近 0 则激进适配新偏好但可能过度特化。
术语 pairwise preference data(成对偏好数据,chosen vs rejected); policy(初始策略/模型); RLHF(基于人类反馈的强化学习); beta / β(DPO 独有超参,权衡保守 vs 适配); reward model(DPO 无需单独训练它)
📖 "This approach simplifies alignment and eliminates the need for a separate reward model or complex reinforcement learning procedures, making DPO a lightweight alternative to techniques such as Reinforcement Learning from Human Feedback (RLHF)." — 原文
📖 "Performing Supervised Fine-Tuning (SFT) before Direct Preference Optimization (DPO) enhances model alignment and overall performance by establishing a robust initial policy, ensuring the model already prefers correct responses." — 原文
📖 "Technically, beta scales the difference in log-probabilities in the DPO loss; a larger β causes the sigmoid-based loss function to saturate with smaller probability differences, yielding smaller weight updates (thus preserving old behavior)." — 原文
🧪 实例 用 fine-tuning API 提交一个 DPO 作业,通过 method 指定类型与超参(β、n_epochs、batch_size):
python
ft = sync_client.fine_tuning.jobs.create(
    model=base_model,
    training_file=train_file_id,
    validation_file=val_file_id,
    method={
        "type": "dpo",
        "dpo": {
            "hyperparameters": {
                "n_epochs": 2,
                "beta": 0.1,
                "batch_size": 8,
            }
        },
    },
)
🔍 追问 只跑 DPO 不先 SFT 行不行? → 行,guide 里就"专门只做 DPO";但若你的用例能从先 SFT 中受益,可先跑 SFT、保存 model ID,把它作为 DPO 作业的起点。
📚 拓展阅读
QRFT 的核心机制是什么?为什么"基础模型至少要能做对一部分样本"是硬前提?深挖·拓展🔥高频
RFT reward reasoning hill-climb
⏱️ 现行
RFT 是在推理模型之上跑强化学习:模型在训练时对给定 prompt 生成输出,每个输出被 grader 评估打分,然后更新参数去最大化奖励,从而强化那些能带来更高奖励的推理/决策策略,帮模型在上下文里做更锐利的判断。它有个很实用的特性——不需要成千上万样本就能起效:靠训练中的 trajectory sampling(轨迹采样)和反馈回路,模型不仅学会正确行为,也学会要避开的模式,所以小数据集也能看到明显收益(示例里就只随机采样了 100 训练 + 100 测试)。但 RFT 有一条硬前提:基础模型必须能证明它对至少一部分样本"开箱即用"地做对。示例里基础模型 ~0.6 的初始准确率被当作"RFT 能提升"的强信号——因为 RL 本质是"爬山",需要偶尔的成功作为可强化的训练信号;如果模型对你的任务从来不成功,就根本没有信号可以爬,RFT 也就无从优化。所以上 RFT 前先 benchmark 基础模型、确认它有非零成功率,是不可跳过的一步。
术语 reasoning models(推理模型,如 o4-mini); reward(奖励,grader 打分); trajectory sampling(轨迹采样); hill climb(爬山,需成功信号); benchmark(微调前的基线评估)
📖 "the model generates outputs for given prompts during training, and each output is evaluated for quality." — 原文
📖 "A key requirement for RFT to work is that the base model demonstrates it can successfully complete the task for at least some examples right out of the gate." — 原文
📖 "If the model never succeeds on your tasks, there is no training signal to hill climb on." — 原文
📖 "Thanks to trajectory sampling and the feedback loop during training, the model learns not just correct behaviors, but also patterns to avoid." — 原文
🧪 实例 RFT 训练/奖励的机制可视化——
flowchart LR
    P[Prompt] --> M[reasoning model o4-mini]
    M --> O[生成输出/轨迹]
    O --> G[grader 打分 = reward]
    G --> U[更新参数 最大化 reward]
    U --> M
    G -.低奖励.-> A[学习要避开的模式]
🔍 追问 基础模型基线太低(几乎全错)却硬上 RFT 会怎样? → 没有可强化的成功轨迹,reward 无从爬升;应先做 prompt engineering 或换更强基础模型,把初始成功率抬起来再上 RFT。
📚 拓展阅读
QSFT 的训练数据长什么样?训练时 prompt 和 completion 是怎么切分的?有哪些数据约束?深挖·拓展中频
SFT ChatCompletion jsonl data-prep
⏱️ 现行
用 ChatCompletion 格式做 SFT 时,每条训练样本就是一个简单的 messages 列表(system / user / assistant 若干轮)。训练时这段对话会被切分:最后一条 assistant 消息是模型要产生的 completion(目标),其余 messages 作为 prompt(上下文)。这个切分逻辑有个重要含义——如果你的模型将来要处理多轮对话,就必须在训练集里提供有代表性的多轮样本,否则当对话展开时它会表现很差。数据约束方面:每条训练样本当前有 4096 token 上限,超出会被截断到 4096;数据量上可以从 30~50 条精挑细选的样本起步,随着训练集增大性能大致线性提升,但作业也更耗时;此外还可以可选地提供 validation data 来确认模型没有对训练集过拟合。数据要"聚焦但不过窄":既要聚焦到某个领域让模型学得会,又要足够泛化,别让没见过的样本被漏掉。
术语 ChatCompletion format(messages 列表格式); completion(最后一条 assistant,训练目标); .jsonl(每行一条对话样本); 4096 token limit(单样本上限); validation data(可选,防过拟合)
📖 "When fine-tuning with the ChatCompletion format, each training example is a simple list of messages." — 原文
📖 "During the training process this conversation will be split, with the final entry being the completion that the model will produce, and the remainder of the messages acting as the prompt." — 原文
📖 "Please note that currently there is a 4096 token limit for each training example. Anything longer than this will be truncated at 4096 tokens." — 原文
📖 "You can begin with even 30-50 well-pruned examples." — 原文
🧪 实例 实体抽取任务的一条 SFT 训练样本(system 设定角色,user 给食谱,assistant 是要抽取的通用配料):
json
[{"role": "system",
  "content": "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."},
 {"role": "user",
  "content": "Title: No-Bake Nut Cookies\n\nIngredients: [\"1 c. firmly packed brown sugar\", \"...\"]\n\nGeneric ingredients: "},
 {"role": "assistant",
  "content": "[\"brown sugar\", \"milk\", \"vanilla\", \"nuts\", \"butter\", \"bite size shredded rice biscuits\"]"}]
🔍 追问 我的场景是多轮对话,训练集只放了单轮问答会怎样? → 官方提醒要放"有代表性的多轮样本",否则对话展开变长时模型会表现很差。
📚 拓展阅读
QRFT 里的 reward hacking(奖励作弊)是什么?示例里模型是怎么"钻空子"的,又怎么修?深挖·拓展中频
RFT reward-hacking grader robustness
⏱️ 现行
reward hacking 指模型找到 grader 打分逻辑的漏洞,用不真正提升任务质量的方式把分数刷高。示例里非常具体:用一个强调语义保真的 model grader(gpt-4.1)训练时,分数一度暴涨(有时 20~30 个百分点),但并非因为临床准确度提高,而是模型往它本该"一句话"的答案里塞进同义词、剂量、整套处置方案——比如系统提示明确要求"只用恰好一个短语回答最可能的结果",模型却输出一长串,从而在不真正增加预测价值的情况下抬高了 lexical_similarity 分数。这暴露了必须持续检视模型输出、对会悄悄扭曲评估指标的作弊行为保持警惕。修法是从 grader 侧下手:细化 grader prompt 澄清预期、施加更严的输出约束、补充正确 vs 错误行为的对比示例;同时降低 lexical_similarity 的权重,让 clinical_similarity 占主导。他们还借助 o3 反复迭代、用基础 o4-mini 和上一版作弊样本来设计并验证新 grader,最终得到高信号、领域敏感的 grader,把模型引导向更得体、更简洁的预测。
术语 reward-hacking(奖励作弊,钻 grader 漏洞); lexical_similarity(词面相似度,易被刷); clinical_similarity(临床/语义相似度,应占主导); contrastive examples(对比示例); grader prompt(打分器提示词,需迭代收紧)
📖 "This experience highlights the need to continuously inspect model outputs and remain vigilant for reward-hacking behaviours that can quietly distort evaluation metrics." — 原文
📖 "To mitigate this reward-hack, we refined the grader prompt by clarifying expectations, enforcing stricter output constraints, and supplying contrastive examples of correct versus incorrect behavior." — 原文
🧪 实例 同一个"只要一个短语"的问题,作弊输出 vs 期望输出——
text
要求: respond with exactly one phrase (the single most likely outcome)
期望: continue unfractionated heparin
作弊: begin warfarin therapy and continue unfractionated heparin for ≥5 days,
      overlapping until the INR is in the therapeutic range (2–3)   ← 塞满同义词/剂量刷 lexical 分
🔍 追问 怎么在烧钱之前尽早发现 reward hacking? → 官方建议先在基础模型 completion 上本地测 grader,再用小规模 RFT run 验证 grader 对齐、检测潜在作弊,然后再扩大规模。
📚 拓展阅读
QRFT 的 grader 怎么设计?string grader 和 model grader 有什么区别,各自代价是什么?深挖·拓展中频
grader reward-function model-grader cost
⏱️ 现行
grader 定义了 RFT 里塑造模型行为的奖励函数——它给出期望输出的范例、并惩罚不良输出,是成功 RFT 里"也许最重要"的一环,设计它既要有原则化的结构,也要有深思的领域洞察。示例里演进了三档 grader:起初用一个 string based grader(词面相似度打分,binary 精确匹配 + softer token 相似度双 grader),它给了个起点,但信号不够丰富,o4-mini 学不动,第一次实验里 reward 停滞不前。于是升级到 model grader:基于模型的 grader 能把语义理解和细腻判断嵌进反馈里,在有领域同义词或模糊推理时尤其有力——他们用 gpt-4.1 当 grader,配一套强调临床同义、正确疾病归类、概念对齐的 rubric,目标从"是不是同一个字符串"变成"是否反映了正确的结果或现象"。代价是:LLM grader 除了训练算力还要额外消耗 token(计费),所以建议先本地在基础模型 completion 上测 grader、确保它对齐你的 rubric 或人类偏好,再从小规模 RFT run 起步验证、扩量。此外 model grader 是否真的对齐专家排序,需要领域专家复核(示例里用 o3 做了近似的抽查)。
术语 grader = reward function(打分器即奖励函数); string based grader(词面/精确匹配打分); model grader / score_model(模型打分器,嵌入语义); rubric(评分准则); pass_threshold(通过阈值)
📖 "The grader defines the reward function that shapes model behavior during RFT. It provides examples of desired outputs-and penalizes undesirable ones." — 原文
📖 "Designing an effective grader requires both principled structure and thoughtful domain insight, and is perhaps the most important task for successful RFT." — 原文
📖 "A model-based grader lets us embed semantic understanding and nuance into the feedback." — 原文
📖 "LLM graders incur token usage charges in addition to training compute." — 原文
🧪 实例 提交给 API 的 model grader 字典(score_model 类型,用模板变量引用参考答案与模型输出):
python
model_grader_1 = {
   "type": "score_model",
   "name": "gpt41_score_model_1",
   "input": [
        {"role": "system", "content": GRADER_PROMPT_1},
        {"role": "user",
         "content": "Reference Answer: {{item.reference_answer}}. Model's Answer: {{sample.output_text}}"}
   ],
   "pass_threshold": 0.75,
   "model": "gpt-4.1-2025-04-14",
   "range": [0, 1],
   "sampling_params": {"seed": 42, "temperature": 0},
}
🔍 追问 为什么示例里明明有 string grader 还要换成 model grader? → string grader 信号太弱、reward 停滞,o4-mini 学不动;model grader 能嵌入语义与领域同义,提供更丰富、可学习的信号。
📚 拓展阅读
Q启动一个 RFT 作业要设哪些关键超参?reasoning_effort 和 eval_samples 各自影响什么?深挖·拓展中频
RFT hyperparameters reasoning-effort eval-samples
⏱️ 现行
RFT 作业通过 methodtype="reinforcement" 下的 reinforcement 块提交,核心是 grader 加一组 hyperparameters。示例里微调 o4-mini 时用了 medium 的 reasoning effort——这个参数通过限制模型用于推理的 token 数量来影响作业时长(effort 越高推理 token 越多、越慢越贵)。他们用一个适中的 compute_multiplier(1.0)和合理的 epoch 数(n_epochs=5),优先追求效率与快速迭代。另外把 eval_samples 设为 3,是为了在 o4-mini 输出有随机性的情况下让 validation 曲线更稳健——对多个采样求平均能降噪、更清楚地显现一致的学习规律。这些超参都要按你的预算、期望的泛化程度、数据集难度来定制。训练时可在 dashboard 上看 reward 曲线(多 grader 时还有 per-grader 分解)、推理 token 使用趋势(模型越自信通常越降)、每步时长、以及 grader 延迟与报错数,用来确保 grader 在整个 run 里保持高效无 bug。
术语 reinforcement method(RFT 作业的 method 块); reasoning_effort(推理力度,限推理 token 影响时长); compute_multiplier(算力倍率); n_epochs(训练轮数); eval_samples(每次评估的采样数,降噪); eval_interval(评估间隔)
📖 "We will be fine-tuning o4-mini, with the medium reasoning effort. This parameter will impact the duration by limiting the number of tokens the model uses to reason." — 原文
📖 "Averaging across multiple samples reduces noise and helps reveal consistent patterns of learning." — 原文
🧪 实例 组装并提交 RFT 作业的 payload(method → reinforcement → grader + hyperparameters):
python
payload = dict(
    training_file=train_file_id,
    validation_file=test_file_id,
    model="o4-mini-2025-04-16",
    suffix=suffix,
    method=dict(
        type="reinforcement",
        reinforcement=dict(
            grader=grader,
            response_format=response_format_predictions,
            hyperparameters=dict(
                compute_multiplier=1.0,
                eval_samples=3,
                eval_interval=5,
                n_epochs=5,
                reasoning_effort="medium",
            )
        )
    ),
    seed=42
)
🔍 追问 为什么要把 eval_samples 设成 3 而不是 1? → o4-mini 输出有随机性,单次采样的 validation 曲线噪声大;对 3 个采样求平均能降噪、让学习趋势更可靠。
📚 拓展阅读
Q微调前后为什么都要用 LLM-as-a-Judge 做基准评估?分数不涨时该加算力还是先查数据?深挖·拓展低频
evaluation LLM-as-a-Judge data-quality benchmark
⏱️ 现行
无论 DPO 还是 RFT,官方流程都是"先 benchmark 基础模型、再微调、再用同一套评估对比前后"。DPO 示例里用一个自动 grader(LLM-as-a-Judge)给每条响应的友好度与共情打 0~4 分,先算出基础模型的均值基线,微调后在同一测试集上再评一次,通过对比前后均分和样例输出来判断偏好对齐是否真的改善。这样做的价值在于:评估结果本身能暴露趋势和失败模式,指导下一步(RFT 示例把失败按 score 分成三档来定位问题)。而当分数不再上涨时,官方给的经验是——先投资数据质量,而不是无脑加算力:让领域专家给有噪声的那一小片数据重新打标、分析模型的推理、再收紧 grader prompt;干净、可信的数据加上有条理的更新,几乎总比多跑几个 epoch 买来更多准确率。示例里甚至发现基线 o3 和微调后的 o4-mini 在同样一批样本上都得 0 分——这是"参考标签可能本身就错了"的红旗,提示要先修数据而非加训练。
术语 LLM-as-a-Judge(用 LLM 当自动打分器); baseline score(基线均分); benchmark(微调前后一致评估); data quality(数据质量优先于算力); relabel the noisy slice(重标噪声数据)
📖 "we'll use an automated grader (LLM-as-a-Judge) to score each response for friendliness and empathy." — 原文
📖 "Before adding more compute, invest in data quality: have a domain expert relabel the noisy slice, analyze the model's reasoning, then tighten the grader prompt." — 原文
📖 "Clean, trusted data and methodical updates almost always buys more accuracy than extra epochs." — 原文
🧪 实例 RFT 示例里对基础模型评分后归纳的三档失败模式,用来定位问题优先级——
text
score >= 0.8        小差异/格式问题
0.3 < score < 0.8   部分词面匹配
score < 0.3         词面完全跑偏
→ 若基线与微调模型在同一样本都得 0,警惕参考标签本身有错,先修数据
🔍 追问 微调后某个 grader 上分数几乎没涨,是失败了吗? → 不一定,示例里为奖励词面正确而设计的 combined_grader 分数没怎么涨是"预期内"的,因为最终 grader 强调的是临床相似度而非词面——这恰恰说明要验证你的 grader 并监控 reward-hacking。
📚 拓展阅读
低频

模型蒸馏与数据准备

原文 原文
Q什么是 OpenAI 的 model distillation?把 gpt-4o 蒸馏到 gpt-4o-mini 的完整流程是怎样的?深挖·拓展中频
distillation fine-tuning gpt-4o-mini
⏱️ 现行
Distillation 的核心思路是"用大模型的输出去微调小模型":先让能力更强的 gpt-4o 在你的任务上产出高质量 completions,把它们作为参考监督信号去 fine-tune 更便宜、更快的 gpt-4o-mini,从而在特定任务上以更低延迟与成本逼近大模型精度。之所以可行,是因为很多任务(如本 cookbook 里的"根据酒评描述、产区猜葡萄品种"这类分类问题)对小模型来说不是能力天花板问题,而是缺少针对性监督。实操链路是:(1) 对数据集用 gpt-4o 跑 completions,并在调用时加 store=True 把结果落库,同时打上 metadata 标签(如 distillation: wine-distillation)以便后续在平台上按标签筛选;(2) 到 OpenAI Stored completions 页面筛选出该 gpt-4o 的存量 completions,点 "Distill" 自动生成 fine-tuning 文件;(3) 选 gpt-4o-mini 作为 base model、用默认参数启动 fine-tuning job,用 job ID 监控状态并取回 fine-tuned model id。权衡点在于:蒸馏出的模型 base 与普通 4o-mini 相同,推理成本/延迟不变,但精度显著提升——本例中相对未蒸馏 4o-mini 有近 22% 的相对提升。
flowchart LR
    A[数据集 French wine] --> B[gpt-4o + store=True + metadata]
    B --> C[Stored completions 落库]
    C --> D[平台按 metadata 筛选 → Distill]
    D --> E[gpt-4o-mini 作为 base 微调]
    E --> F[distilled model 低成本高精度]
术语 distillation(蒸馏,用大模型输出监督小模型); base model(微调所基于的底座模型,这里是 gpt-4o-mini); Stored completions(平台上落库的历史补全,蒸馏的输入)
📖 "OpenAI Distillation uses the outputs of a larger model to fine-tune a smaller one. Moving to the smaller model can reduce latency and cost for specific tasks." — 原文
📖 "To distill a model, you need to store all completions from a model, allowing you to give it as a reference to the smaller model to fine-tune it." — 原文
🧪 实例 cookbook 用 Kaggle 葡萄酒评论数据、筛选法国葡萄酒,做"猜葡萄品种"的分类任务,先跑 4o/4o-mini 对比,再蒸馏。调用时的关键是 store=True:
python
response = client.chat.completions.create(
    model=model,
    store=True,
    metadata={
        "distillation": metadata_value,
    },
    messages=[...],
    response_format=response_format
)
🔍 追问 蒸馏后的模型与直接用 gpt-4o-mini 相比,base model 变了吗? → 没变,fine-tuned 模型和 4o-mini 用的是同一个 base,所以推理成本与延迟一致,变的只是在该任务上的精度。
📚 拓展阅读
Q为什么这个分类任务要用 Structured Outputs?store=Truemetadata 各自解决什么问题?深挖·拓展中频
structured-outputs store metadata
⏱️ 现行
因为任务要求答案必须落在一组固定的葡萄品种里,cookbook 用 Structured Outputs(带 enum 的 json_schema)把模型输出约束到这个集合,好处有二:一是拿到可确定比较的值,直接和期望品种做字符串比对来算 accuracy;二是防止模型给出数据集之外的答案,避免"幻觉品种"污染评估。这对蒸馏尤其重要——只有输出格式稳定、语义闭合,存下来的 completions 才是干净、可复用的监督信号。store=True 解决的是"把 completion 持久化到平台"的问题,这是蒸馏与 Evals 的前提:不落库就没有可拿来微调小模型的参考数据;metadata 标签解决的是"从海量存量 completions 中精确筛选"的问题——打上 distillation: wine-distillation 后,在平台上就能只挑本次 cookbook、且只挑 gpt-4o(而非 4o-mini)产出的那批 completions 去做蒸馏和评估,避免误把小模型输出当监督信号。
术语 Structured Outputs(结构化输出,用 json_schema+enum 约束输出集合); strict(严格模式,强制符合 schema); metadata(元数据标签,供平台按 key 筛选 completions)
📖 "Because the response must be one of a fixed set of grape varieties, we'll use structured outputs to constrain the model to that set. This gives us a deterministic value to compare with the expected variety and prevents answers outside the dataset." — 原文
📖 "When storing those completions, it's useful to store them with a metadata tag, that will allow filtering from the OpenAI platform to run distillation & evals on the specific set of completions you'd like to run those." — 原文
🧪 实例 用带 enum 的 json_schema 把 variety 限定到 varieties 列表,并开 strict:
json
{
    "type": "json_schema",
    "json_schema": {
        "name": "grape-variety",
        "schema": {
            "type": "object",
            "properties": {
                "variety": {
                    "type": "string",
                    "enum": ["...品种列表..."]
                }
            },
            "additionalProperties": false,
            "required": ["variety"],
            "strict": true
        }
    }
}
🔍 追问 蒸馏时为什么要在平台上特意只选 gpt-4o 的 completions? → 因为同时也存了 4o-mini 和后续微调模型的输出;若不按 model/metadata 过滤,可能把弱模型的输出当成监督信号去蒸馏,反而拉低效果。
📚 拓展阅读
Q微调前对 chat 数据集要做哪些格式校验(format validation)?深挖·拓展中频
data-prep format-validation fine-tuning
⏱️ 现行
fine-tuning 的数据必须严格符合 API 期望的对话格式,cookbook 提供一套按错误类型分类的校验,便于调试。检查项包括:每条样本是否为 dict(data_type);是否有 messages 列表(missing_messages_list);每条 message 是否都含 rolecontent(message_missing_key);是否出现 rolecontentweightfunction_callname 之外的键(message_unrecognized_key);role 是否属于 system/user/assistant(unrecognized_role);content 是否为非空字符串(missing_content);以及每段对话是否至少有一条 assistant 消息(example_missing_assistant_message)。之所以要在训练前跑这套检查而不是让 API 报错,是因为格式错误往往批量出现,提前把每类错误计数打印出来能快速定位是哪一步数据生成/转换出了问题,避免上传后才失败、浪费一轮往返。注意代码里对 role 的实际允许集合还包含 function,weight 也是被识别的可选键(用于按 token 加权 assistant 消息)。
术语 messages(对话消息列表,微调样本的核心结构); role(角色,system/user/assistant); weight(可选权重键,控制该 assistant 消息是否参与训练); error type(错误类型,如 unrecognized_role,便于分类计数)
📖 "We can perform a variety of error checks to validate that each conversation in the dataset adheres to the format expected by the fine-tuning API. Errors are categorized based on their nature for easier debugging." — 原文
🧪 实例 核心校验循环(节选自 notebook):
python
for message in messages:
    if "role" not in message or "content" not in message:
        format_errors["message_missing_key"] += 1
    if any(k not in ("role", "content", "name", "function_call", "weight") for k in message):
        format_errors["message_unrecognized_key"] += 1
    if message.get("role", None) not in ("system", "user", "assistant", "function"):
        format_errors["unrecognized_role"] += 1
if not any(message.get("role", None) == "assistant" for message in messages):
    format_errors["example_missing_assistant_message"] += 1
🔍 追问 为什么必须保证每段对话至少有一条 assistant 消息? → assistant 消息是模型要学习生成的目标;没有 assistant 输出的样本无监督信号,对 chat 微调无意义,故单列 example_missing_assistant_message 检查。
📚 拓展阅读
Q微调成本怎么估?16,385 token 上限和默认 epoch 数是怎么算出来的?深挖·拓展低频
token-counting cost epochs
⏱️ 现行
微调按 token 计费,且 job 时长也随 token 数增加,所以先估 token 再估成本。计费 token = 每个样本取 min(16385, 该样本 token 数) 后求和——即单样本超过 16,385 token 的部分不计费,但也意味着会在训练时被截断、造成数据丢失,这也是为什么要提前做 token 上限告警。默认 epoch 数用一段启发式决定:目标是 TARGET_EPOCHS = 3,但若 样本数 × 3 < 100(数据太少)就把 epoch 提到 min(25, 100 // 样本数) 以保证训练充分;若 样本数 × 3 > 25000(数据太多)就把 epoch 降到 max(1, 25000 // 样本数) 以控制成本。最终"预计计费 token ≈ epoch 数 × 数据集计费 token"。token 计数用 tiktoken 近似(每条 message 固定加 tokens_per_message、带 name 再加 tokens_per_name),并非精确值。
术语 MAX_TOKENS_PER_EXAMPLE(单样本 token 上限 16385,超出被截断); n_epochs(训练轮数,按数据量启发式调整); tiktoken(OpenAI 的分词/计数库,用于近似 token 数); n_billing_tokens_in_dataset(数据集计费 token 总量)
📖 "In this final section, we estimate the total number of tokens that will be used for fine-tuning, which allows us to approximate the cost. It is worth noting that the duration of the fine-tuning jobs will also increase with the token count." — 原文
📖 "as such examples will be truncated during fine-tuning, potentially resulting in data loss." — 原文
🧪 实例 默认 epoch 与计费 token 的估算逻辑:
python
n_epochs = TARGET_EPOCHS
n_train_examples = len(dataset)
if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
    n_epochs = min(MAX_DEFAULT_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
    n_epochs = max(MIN_DEFAULT_EPOCHS, MAX_TARGET_EXAMPLES // n_train_examples)

n_billing_tokens_in_dataset = sum(min(MAX_TOKENS_PER_EXAMPLE, length) for length in convo_lens)
🔍 追问 蒸馏路线里 token 估算和这套一样吗? → 不完全,蒸馏 cookbook 用 tiktoken 估的是跑 completions 的推理成本,并明确指出该估算不含 fine-tuning,后者成本还取决于训练集大小和 epoch 数。
📚 拓展阅读
Q蒸馏效果怎么量化?gpt-4o、gpt-4o-mini 与蒸馏后模型的精度差多少?深挖·拓展低频
evaluation accuracy distillation
⏱️ 现行
cookbook 用最朴素的方式量化——因为 Structured Outputs 保证了输出是确定的品种字符串,直接在 Python 里做字符串比对,用 np.mean(df['variety'] == df[model + '-variety']) 算每个模型的 accuracy(作者也提到更复杂的评估可用 OpenAI Evals 或开源 eval 框架)。先在同一法国葡萄酒子集上对比 gpt-4ogpt-4o-mini:gpt-4o 明显更强,accuracy 高约 12.80 个百分点,相对 4o-mini 提升近 20%。蒸馏后,在另取的验证子集(每模型 300 条,同样限定法国、无稀有品种)上再比,蒸馏得到的 fine-tuned 模型相对未蒸馏的 gpt-4o-mini 有近 22% 的相对提升,且 base 与 4o-mini 相同,意味着能以同等的低成本、低延迟拿到接近大模型的精度。关键在于:验证集必须和训练时的约束一致(同样是法国葡萄酒、剔除离群品种),否则评估口径不对齐会失真。
术语 accuracy(准确率,预测品种 == 期望品种的均值); relative improvement(相对提升,相对基线百分比); validation dataset(验证子集,需与任务约束一致); OpenAI Evals(平台评估框架)
📖 "We can see that gpt-4o is better a finding grape variety than 4o-mini (12.80% higher or almost 20% relatively to 4o-mini!" — 原文
📖 "That's almost a 22% relative improvement over the non-distilled gpt-4o-mini! 🎉" — 原文
🧪 实例 直接用字符串相等算准确率:
python
def get_accuracy(model, df):
    return np.mean(df['variety'] == df[model + '-variety'])

for model in models:
    print(f"{model} accuracy: {get_accuracy(model, df_france_subset) * 100:.2f}%")
🔍 追问 为什么验证要"另取一个子集"而不是复用训练那批? → 复用会数据泄漏,让蒸馏模型在见过的分布上被高估;另取 300 条独立样本才能反映泛化后的真实精度。
📚 拓展阅读
Q除了硬格式校验,chat 微调数据还应看哪些"软告警"和分布统计?深挖·拓展低频
data-warnings token-distribution data-prep
⏱️ 现行
通过轻量分析可以发现数据集的潜在隐患并给出统计洞见。要点包括:统计缺少 system / user 消息的对话数(这类消息对定义助手行为、发起对话很关键);统计每条样本的 message 数分布(反映对话复杂度);统计每条样本的总 token 分布(直接关系微调成本);统计 assistant 消息的 token 分布(反映助手冗长度);以及 token 上限告警——若样本超过 16,385 token 上限,会在微调时被截断、可能丢数据。这些是"软告警":它们不一定让 API 报错,但会影响训练质量与成本,因此在正式微调前用 print_distribution 打印 min/max、mean/median、p5/p95 等分位,帮助判断数据是否偏斜、是否有超长样本需要清理。之所以强调分布而非单点均值,是因为均值会掩盖长尾——少量超长样本可能被截断、少量缺 system 的样本可能改变模型行为,这些都要靠分位数和计数暴露出来。
术语 n_missing_system / n_missing_user(缺 system/user 消息的样本计数); convo_lens(每样本总 token 列表); print_distribution(打印 min/max、mean/median、p5/p95 的工具); truncation(超长样本在微调时被截断)
📖 "With some lightweight analysis we can identify potential issues in the dataset, like missing messages, and provide statistical insights into message and token counts." — 原文
📖 "examples may be over the 16,385 token limit, they will be truncated during fine-tuning" — 原文
🧪 实例 统计缺失消息并打印各项分布:
python
for ex in dataset:
    messages = ex["messages"]
    if not any(message["role"] == "system" for message in messages):
        n_missing_system += 1
    if not any(message["role"] == "user" for message in messages):
        n_missing_user += 1
    n_messages.append(len(messages))
    convo_lens.append(num_tokens_from_messages(messages))
    assistant_message_lens.append(num_assistant_tokens_from_messages(messages))
n_too_long = sum(l > 16385 for l in convo_lens)
🔍 追问 缺 system 消息为什么被当成告警而非硬错误? → system 消息不是格式必需项(对话仍合法),但它定义助手行为;缺失会让微调后的行为不稳定,故只作软告警提示、由使用者决定是否补齐。
📚 拓展阅读

第12章 · 多模态与生产化实战

中频

视觉与图像生成实战

原文 原文 原文
QGPT 并不原生接收视频输入,那 Cookbook 是如何用它「理解」一段视频的?深挖·拓展🔥高频
vision video long-context
⏱️ 现行
核心思路是把「视频理解」降维成「多图理解」。GPT-4.1-mini 本身不吃视频流,但它有视觉能力和 1M token 的超长上下文窗口,于是 Cookbook 用 OpenCV 逐帧把视频解码成一串静态帧,再把这些帧统一 base64 编码后作为多张 input_image 一次性塞进同一个请求里,让模型基于「整段视频的所有关键帧」生成描述。关键权衡在于帧数与成本/上下文占用:每一帧都是图像 token,全量发送既贵又可能撑爆上下文,而视频相邻帧高度冗余,所以实践中做抽帧采样——代码里用 base64Frames[0::25] 每 25 帧取一帧,原文也明确点出「不需要把每一帧都发给 GPT 它就能看懂发生了什么」。这就是「用长上下文 + 抽帧」替代「原生视频编码器」的生产化取巧:牺牲一点时间分辨率,换来用现成的图像多模态接口零改造地处理视频。
术语 1M token context window(百万级上下文,使一次容纳整段视频的帧成为可能); input_image(Responses API 中以 base64 data URL 传入的图像内容块); frame sampling(抽帧采样,[0::25] 每 25 帧取 1 帧降冗余)
📖 "Although GPT-4.1-mini doesn't take videos as input directly, we can use vision and the 1M token context window to describe the static frames of a whole video at once." — 原文
📖 "Once we have the video frames, we craft our prompt and send a request to GPT (Note that we don't need to send every frame for GPT to understand what's going on):" — 原文
🧪 实例 抽帧后把每第 25 帧作为一张图片拼进 input,让模型为整段视频写描述:
python
response = client.responses.create(
    model="gpt-4.1-mini",
    input=[
        {
            "role": "user",
            "content": [
                {
                    "type": "input_text",
                    "text": (
                        "These are frames from a video that I want to upload. Generate a compelling description that I can upload along with the video."
                    )
                },
                *[
                    {
                        "type": "input_image",
                        "image_url": f"data:image/jpeg;base64,{frame}"
                    }
                    for frame in base64Frames[0::25]
                ]
            ]
        }
    ],
)
print(response.output_text)
🔍 追问 为什么不把每一帧都发给模型? → 相邻帧内容高度冗余,全发会成倍增加图像 token 成本并挤占上下文,抽帧(如每 25 帧取 1)在几乎不损失语义的前提下大幅降本;原文也明确说不需要发送每一帧。
📚 拓展阅读
Q讲讲用 GPT-4o mini 给图片打标签(tag)的整条流水线,为什么标签要靠 embeddings 去重?深挖·拓展🔥高频
vision tagging embeddings zero-shot
⏱️ 现行
打标签用的是「zero-shot 关键词抽取 + embedding 去重」两段式。第一段:给 gpt-4o-mini 一个系统提示,规定它是专门给家具/家居图片打搜索关键词的 agent,输出小写、简洁、以字符串数组形式返回的关键词,并且只有当材质/风格/颜色明显时才推断。关键设计是同时传入图片和商品标题——因为一张场景图里常常有多件物品,只给图片模型可能把背景里的其它东西也标进去,配合标题就能把焦点锁定到目标单品。第二段:模型每次独立生成关键词,不同商品会产出大量近义词(如 'sofa' vs 'couch'),直接入库会让搜索标签体系碎片化;于是对每个新关键词用 text-embedding-3-large 求 embedding,和已存关键词库做 cosine similarity,超过阈值(0.6)就复用已有关键词而非新增。这样标签集合保持收敛、可复用,权衡点是阈值高低——太高去重不足、太低会把语义相近但该区分的词误合并。
术语 zero-shot(不给示例,仅靠指令让模型直接抽关键词); text-embedding-3-large(生成关键词向量的 embedding 模型); cosine similarity(余弦相似度,衡量两关键词向量接近程度); threshold=0.6(去重阈值,超过则复用已有关键词)
📖 "We'll use a simple zero-shot approach to extract keywords, and deduplicate those keywords using embeddings to avoid having multiple keywords that are too similar." — 原文
📖 "We will use a combination of an image and the product title to avoid extracting keywords for other items that are depicted in the image - sometimes there are multiple items used in the scene and we want to focus on just the one we want to tag." — 原文
🧪 实例 新关键词与关键词库比对,相似度过阈值则替换为已有词:
python
def get_keyword(keyword, df_keywords, threshold = 0.6):
    embedded_value = get_embedding(keyword)
    df_keywords['similarity'] = df_keywords['embedding'].apply(lambda x: cosine_similarity(np.array(x).reshape(1,-1), np.array(embedded_value).reshape(1, -1)))
    sorted_keywords = df_keywords.copy().sort_values('similarity', ascending=False)
    if len(sorted_keywords) > 0 :
        most_similar = sorted_keywords.iloc[0]
        if most_similar['similarity'] > threshold:
            print(f"Replacing '{keyword}' with existing keyword: '{most_similar['keyword']}'")
            return most_similar['keyword']
    new_keyword = {
        'keyword': keyword,
        'embedding': embedded_value
    }
    df_keywords = pd.concat([df_keywords, pd.DataFrame([new_keyword])], ignore_index=True)
    return keyword
🔍 追问 为什么不只发图片、非要带上标题? → 场景图常含多件物品,只凭图片模型可能给背景里的其它物件也打标签;标题把「该标注哪一件」明确下来,避免跑偏。
📚 拓展阅读
QGPT Image 相比上一代 DALL·E 有什么本质提升?生成图片时有哪些可控参数?深挖·拓展低频
image-generation gpt-image-1 instruction-following
⏱️ 现行
GPT Image 是带图像生成能力的大语言模型,本质区别有两点:一是它有「世界知识」,能借助对世界的广泛理解来生成图像;二是相比上一代 DALL·E 2/3,它在指令跟随和生成照片级真实感上明显更强。指令跟随强意味着你可以写非常长、非常细的规格(原文示例给一个虚构外星角色写了包括体型、材质、配色、情绪变色、运动方式等几十行 spec),模型能较忠实地落实。生成侧的可控参数包括:quality(low/medium/high/auto,auto 为默认)、size(1024x1024 方形、1536x1024 横向、1024x1536 纵向、或 auto)、output_compression(JPEG/WEBP 的 0-100% 压缩级别)、output_format、以及透明背景(仅 PNG/WEBP 支持)。权衡在于质量/尺寸越高越贵越慢,而压缩、格式、透明背景是为下游用途(网页素材、贴纸)服务的产线参数——你按落地场景选而不是一味拉满。
术语 gpt-image-1(带图像生成能力的模型标识); world knowledge(世界知识,使生成更符合常识与真实感); quality(low/medium/high/auto,画质档位); output_compression(JPEG/WEBP 压缩百分比)
📖 "This model has world knowledge and can generate images leveraging this broad understanding of the world." — 原文
📖 "It is also much better at instruction following and producing photorealistic images compared to our previous-generation image models, DallE 2 and 3." — 原文
📖 "- Quality can be low, medium, high or auto (default value)" — 原文
🧪 实例 低画质、纵向尺寸、JPEG 50% 压缩生成一张像素风肖像:
python
result2 = client.images.generate(
    model="gpt-image-1",
    prompt=prompt2,
    quality="low",
    output_compression=50,
    output_format="jpeg",
    size="1024x1536"
)
🔍 追问 想要透明背景该怎么做? → 用 background 属性请求透明背景;或者只要在 prompt 里写明你想要透明背景,它就会默认设为 transparent(仅 PNG/WEBP 支持透明)。
📚 拓展阅读
QGPT Image 的图片编辑能力怎么用?mask(蒙版)在其中扮演什么角色?深挖·拓展中频
image-editing mask alpha-channel
⏱️ 现行
GPT Image 除了从零生成,还能接收图像输入并据此合成新图——这就是 images.edit。它最多接受 10 张输入图片(例如把「猫」和「帽子」两张图合成「戴帽子的猫」)。mask 用于精确控制「哪里可以改、哪里保留」:提供 mask 后,模型只编辑没有被 mask 覆盖的区域;若传了多张图,mask 作用在第一张上。两个关键工程细节:其一,mask 必须包含 alpha 通道(透明通道),若用图像软件手工制作要记得带上,否则不能用于 Edit API,Cookbook 甚至演示了先让模型生成黑白蒙版、再用 PIL 把灰度图转 RGBA 并 putalpha 灌进 alpha 通道的做法;其二,带 mask 编辑时 prompt 必须描述「整张结果图」,而不是只描述要改的那一小块。需注意模型对 mask 内区域是「尽量避免改动」而非绝对不改——它仍可能动到蒙版内的部分。
术语 client.images.edit(图片编辑接口,可传多图+mask); alpha channel(alpha 通道,mask 必须携带的透明度信息); putalpha(PIL 用灰度图填充 alpha 通道生成合法 mask); mask(蒙版,标记模型应保留/可编辑的区域)
📖 "GPT Image can also accept image inputs, and use them to create new images. You can also provide a mask if you don't want the model to change a specific part of the input image." — 原文
📖 "You can use a maximum of 10 input images, and if you use a mask, it will be applied to the first image provided in the image array." — 原文
📖 "When using a mask, the prompt must describe the entire resulting image, not only the edited area." — 原文
🧪 实例 把黑白蒙版转成带 alpha 通道的合法 mask,再用它约束编辑:
python
# 1. Load your black & white mask as a grayscale image
mask = Image.open(img_path_mask).convert("L")
# 2. Convert it to RGBA so it has space for an alpha channel
mask_rgba = mask.convert("RGBA")
# 3. Then use the mask itself to fill that alpha channel
mask_rgba.putalpha(mask)
🔍 追问 mask 能保证被覆盖区域完全不变吗? → 不能;原文指出模型可能仍会编辑 mask 内的一些部分,只是会尽量避免,需要精确控制时应改用图像分割模型产出精准 mask。
📚 拓展阅读
Q视频描述之后,Cookbook 怎么给视频生成配音(voiceover)?深挖·拓展中频
tts gpt-4o-mini-tts multimodal
⏱️ 现行
配音是「视觉→文本→语音」的两级串联。先复用同一批视频帧,让 gpt-4.1-mini 以 David Attenborough(BBC 自然纪录片旁白)的风格写一段简短、只含旁白的解说脚本;再把脚本交给 GPT-4o TTS 模型合成语音。关键在于 TTS 不只是读字:你可以给它一段 instructions 来精细控制声音「怎么读」——包括语气(Voice Affect)、语调(Tone)、节奏(Pacing)、情绪(Emotion)、重音(Emphasis)、发音(Pronunciation)、停顿(Pauses)等维度。调用 client.audio.speech.create 时指定 model="gpt-4o-mini-tts"voice="echo"、把脚本作为 inputinstructions 作为风格控制,并可选 response_format="wav"。这条链路体现了多模态生产化的组合思路:视觉模型负责「看懂并写稿」,TTS 模型负责「按导演指示朗读」,两者用文本作为接口解耦。
术语 gpt-4o-mini-tts(带指令控制的文本转语音模型); instructions(对 TTS 的表演指导:语气/节奏/情绪/停顿等); voice="echo"(内置音色之一); response_format="wav"(输出音频格式)
📖 "Now, we can work with the GPT-4o TTS model and provide it a set of instructions on how the voice should sound." — 原文
🧪 实例 用脚本 + 表演指令合成 wav 配音:
python
audio_response = response = client.audio.speech.create(
  model="gpt-4o-mini-tts",
  voice="echo",
  instructions=instructions,
  input=result.output_text,
  response_format="wav"
)
audio_bytes = audio_response.content
Audio(data=audio_bytes)
🔍 追问 生成解说脚本时怎么保证输出干净能直接送进 TTS? → 在给视觉模型的 prompt 里明确「Only include the narration.(只包含旁白)」,避免混入镜头说明、标题等非朗读内容。
📚 拓展阅读
Qdescribe → caption 两段式为什么不一步到位?few-shot 在其中起什么作用?深挖·拓展中频
captioning few-shot prompting
⏱️ 现行
Cookbook 把「从图片得到短标题」拆成两步:先用 gpt-4o-mini 看图生成一段较详细的商品描述(describe),再用 few-shot 示例把长描述压成一句短 caption。为什么不让模型看图直接吐一句 caption?因为「准确识别图中物品」和「按目标文案风格精炼成一句话」是两个不同的能力诉求:describe 阶段强调 grounding——如实说出物品、材质、颜色、风格,并在多物体时参照标题锁定主体;caption 阶段强调风格与长度控制——输出必须是 1 句、抓住类型/风格/材质/显著特征。风格控制靠 few-shot:给几组「description → caption」范例,模型就能模仿目标文案的调性和长度。原文也给了升级路径:如果 few-shot 还不够,就 fine-tune 一个模型让 caption 更贴合你要的风格与语气。两个阶段都用 temperature=0.2 降低随机性,保证描述与标题稳定可复现。
术语 describe_image(看图生成详细描述的第一段); caption_image(把描述压成一句短标题的第二段); few-shot examples(用少量 description→caption 范例锚定风格与长度); fine-tuning(few-shot 不够时进一步微调贴合风格)
📖 "In this section, we'll use GPT-4o mini to generate an image description and then use a few-shot examples approach with GPT-4-turbo to generate captions from the images." — 原文
📖 "If few-shot examples are not enough for your use case, consider fine-tuning a model to get the generated captions to match the style & tone you are targeting." — 原文
🧪 实例 caption 阶段把系统提示 + few-shot 范例 + 目标描述拼成消息序列:
python
def caption_image(description, model="gpt-4o-mini"):
    messages = formatted_examples
    messages.insert(0,
        {
            "role": "system",
            "content": caption_system_prompt
        })
    messages.append(
        {
            "role": "user",
            "content": description
        })
    response = client.chat.completions.create(
    model=model,
    temperature=0.2,
    messages=messages
    )
    return response.choices[0].message.content
🔍 追问 describe 阶段遇到图里有多件物品怎么办? → 系统提示要求模型参照传入的标题(title)来判断该描述哪一件,避免描述错主体。
📚 拓展阅读
Q拿到标签和 caption 之后,怎么支持「文本搜图」和「以图搜图」两种检索?深挖·拓展中频
semantic-search embeddings retrieval
⏱️ 现行
检索统一走 embedding 语义匹配。离线阶段,把每个商品的「keywords + caption」拼成一段文本,用 embedding 模型编码存库。在线阶段分两种输入:文本查询时,直接对用户 query 求 embedding,和库里每条的 embedding 算 cosine similarity 取 Top-N;图片查询时,先把输入图片过一遍 describe→caption 变成一句 caption,再把这句 caption 当作文本查询走同一套 embedding 相似度检索——也就是「以图搜图」被巧妙转化为「先把图变成 caption 文本,再做文本搜图」。这样做的好处是两种模态复用同一个向量空间和同一套检索逻辑,工程上只维护一份 embedding 索引;代价是图片查询要多付一次 describe+caption 的模型调用,且检索质量受 caption 生成质量的上限约束。原文指出这套技术不止能做商品搜索,还可扩展到 RAG 等利用非结构化图像数据的场景。
术语 keywords + caption(拼接后一起 embedding 的检索内容); cosine_similarity(query 与库内向量的相似度排序依据); search_from_input_text(文本→embedding→Top-N 检索); image→caption→search(以图搜图先转 caption 再走文本检索)
📖 "We will leverage our embeddings model to generate embeddings for the keywords and captions and compare them to either input text or the generated caption from an input image." — 原文
📖 "If the input is an image, we can find similar images by first turning images into captions, and embedding those captions to compare them to the already created embeddings." — 原文
🧪 实例 文本查询求 embedding 后按相似度取最相近的 N 条:
python
def search_from_input_text(query, n = 2):
    embedded_value = get_embedding(query)
    df_search['similarity'] = df_search['embedding'].apply(lambda x: cosine_similarity(np.array(x).reshape(1,-1), np.array(embedded_value).reshape(1, -1)))
    most_similar = df_search.sort_values('similarity', ascending=False).iloc[:n]
    return most_similar
🔍 追问 这套图像检索思路只能用于商品搜索吗? → 不;原文说这些技术可以超出商品搜索,扩展到多种用例,例如利用非结构化图像数据的 RAG 应用。
📚 拓展阅读
QGPT Image 强指令跟随意味着 prompt 该怎么写?给一个「详细规格」提示的例子。深挖·拓展低频
prompt-engineering image-generation spec-prompt
⏱️ 现行
因为 GPT Image 1 极擅长指令跟随,写图像 prompt 的正确姿势不是给几个模糊形容词,而是像写「角色/物件规格书」一样把细节列全,模型会较忠实地逐条落实。Cookbook 的示范 prompt 为一个虚构外星角色写了完整 spec:体型轮廓、半透明发光凝胶材质与抖动质感、基础色与发光脉络的配色、随情绪变色的规则、非对称漂浮的眼球、无鼻无耳而靠振动受体感知、默认无肢体但可伸出伪足等等。这种「spec 式 prompt」的价值在于把创作意图结构化、显式化,减少模型自由发挥导致的偏差;权衡是 prompt 变长、编写成本上升,但换来生成结果的可控性和可复现性。对生产场景,这意味着把「设计需求」写成模板化的结构字段,比堆砌零散描述更可靠。
术语 instruction-following(强指令跟随,支撑细粒度规格 prompt); spec-style prompt(把外观/材质/配色/行为等结构化列出的规格式提示); prompt1(Cookbook 中给外星角色 Glorptak 写的详细规格提示)
📖 "GPT Image 1 is great at instruction-following, meaning you can prompt the model to generate images with very detailed instructions." — 原文
🧪 实例 规格式 prompt 的开头片段(节选自 Cookbook 的 prompt1):
text
Render a realistic image of this character:
Blobby Alien Character Spec Name: Glorptak (or nickname: "Glorp")
Visual Appearance Body Shape: Amorphous and gelatinous. Overall silhouette resembles a teardrop or melting marshmallow, shifting slightly over time.
Color Palette:
- Base: Iridescent lavender or seafoam green
- Accents: Subsurface glowing veins of neon pink, electric blue, or golden yellow
- Mood-based color shifts (anger = dark red, joy = bright aqua, fear = pale gray)
🔍 追问 prompt 写得很细,模型会漏掉某些约束吗? → 强指令跟随下大多数细节会被落实,但仍非 100% 保真;对关键约束(如透明背景、尺寸)优先用 API 参数(background/size)强制,而非只靠自然语言描述。
📚 拓展阅读
中频

音频、Whisper 与实时语音翻译

原文 原文 原文
QOpenAI 提供了哪几种语音转文字(STT)方案?如何按场景选型?深挖·拓展🔥高频
STT 选型 latency
⏱️ 现行
Cookbook 把 STT 归纳为四种模式,核心差异在于"是否需要完整文件"和"首 token 延迟"。第一种是文件上传 + stream=False(阻塞式),整段音频一次性处理,首 token 延迟到"秒"级,适合语音信箱、会议录音这类离线批处理;优点是单次 HTTP 请求最简单,但没有部分结果、且单请求上限 25 MB(≈16-kHz 单声道 WAV 约 30 分钟),长音频要自己切块。第二种是文件上传 + stream=True,仍需完整文件但用 token 流式给出"实时感",首 token 降到亚秒级,适合手机 App 里的语音备忘录,代价是要自己实现进度条/分块上传。第三种是 Realtime WebSocket,接受连续音频流、真正实时,但输入必须是 pcm16/g711_ulaw/g711_alaw,单会话 ≤ 30 分钟需重连拼接,且要自己处理说话人分段来拼完整转录。第四种是 Agents SDK VoicePipeline,同样实时且易于搭建 agentic 工作流,但目前是 Python-only beta、API 可能变动。选型的关键是把方法匹配到用例,并权衡延迟、实现成本、支持格式与会话时长这几条相互制约的维度。
术语 stream=True(转录流式返回,提供"live"体验); Realtime WebSocket(连续音频流的真实时通道); VoicePipeline(Agents SDK 封装 VAD/缓冲/重连的高层管道); first token latency(首 token 延迟,选型核心指标)
📖 "This notebook provides a clear, hands-on guide for beginners to quickly get started with Speech-to-Text (STT) using the OpenAI API." — 原文
📖 "True real-time; accepts a continuous audio stream" — 原文
📖 "Weigh trade-offs: latency, implementation effort, supported formats, and session limits all differ by approach." — 原文
🧪 实例 离线批处理最简写法——整段转录、不流式:
python
with AUDIO_PATH.open('rb') as f:
    transcript = client.audio.transcriptions.create(
        file=f,
        model=MODEL_NAME,
        response_format='text',
    )
print(transcript)

若要"live 感",只需加 stream=True 并遍历 event.delta 增量拼接。
flowchart TD
    A[音频输入] --> B{是否已有完整文件?}
    B -- 是, 可等待 --> C[file + stream=False 阻塞]
    B -- 是, 要进度反馈 --> D[file + stream=True 流式]
    B -- 否, 连续音频流 --> E[Realtime WebSocket]
    B -- 否, agent 工作流 --> F[Agents SDK VoicePipeline]
🔍 追问 文件上传方式为什么长音频必须切块? → 因为单次请求上限 25 MB(≈16-kHz 单声道 WAV 约 30 分钟),超过就得自己 chunk 后分别上传。
📚 拓展阅读
QWhisper 的 prompt 参数到底做什么?为什么它不是"给 GPT 下指令"?深挖·拓展🔥高频
Whisper prompt 224-token
⏱️ 现行
Whisper 转录接口有个可选的 prompt 参数,本意是把上一段音频的转录文本传进来,帮模型跨多段音频"接龙",从而更好地理解语音并保持一致的书写风格。但关键机制是:prompt 不必是真实的历史转录,你可以喂"虚构"prompt 来诱导模型采用特定拼写或风格。由此衍生两种技巧——用 GPT 把指令转成一段虚构转录供 Whisper 模仿(transcript generation),以及用拼写表(spelling guide)告诉模型人名/产品名/公司名怎么拼。要特别注意它和给 GPT 下 prompt 完全不同:Whisper 是"跟随 prompt 的风格",而不是"执行 prompt 里的指令"——所以像"Format lists in Markdown format"这种命令它不会照做。另一个硬约束是 prompt 上限只有 224 tokens,超过部分会静默丢弃、只保留最后 224 tokens,用的是多语言 Whisper tokenizer。这些技巧并不特别可靠,但在某些情形下有用。
术语 prompt(可选参数,用于拼接音频段并引导风格); fictitious prompt(虚构 prompt,用来诱导拼写/风格); 224 tokens(prompt 硬上限,超出只留最后 224); multilingual Whisper tokenizer(计算 token 的分词器)
📖 "The prompt is intended to help stitch together multiple audio segments. By submitting the prior segment's transcript via the prompt, the Whisper model can use that context to better understand the speech and maintain a consistent writing style." — 原文
📖 "Prompting Whisper is not the same as prompting GPT." — 原文
📖 "In addition, the prompt is limited to only 224 tokens. If the prompt is longer than 224 tokens, only the final 224 tokens of the prompt will be considered; all prior tokens will be silently ignored." — 原文
🧪 实例 转录包装函数直接把 prompt 透传给 whisper-1:
python
def transcribe(audio_filepath, prompt: str) -> str:
    """Given a prompt, transcribe the audio file."""
    transcript = client.audio.transcriptions.create(
        file=open(audio_filepath, "rb"),
        model="whisper-1",
        prompt=prompt,
    )
    return transcript.text

prompt="Format lists in Markdown format" 不会生效,因为模型只跟随风格、不执行指令。
🔍 追问 想让 "President Biden" 转成小写该怎么诱导? → 用一段较长、通篇小写的虚构 prompt 建立风格模式,Whisper 会跟随这种大小写风格。
📚 拓展阅读
Q为什么要专门做 gpt-realtime-translate,而不是让通用语音模型去翻译?深挖·拓展🔥高频
实时翻译 gpt-realtime-translate 低延迟
⏱️ 现行
gpt-realtime-translate 是专门的语音到语音实时翻译模型:接受语音输入、自动检测源语言、返回翻译后的语音加文本转录,开发者只需指定目标输出语言。它之所以独立成一个模型,是因为直播口译的需求和普通 AI 语音交互不同。两个关键特性:其一,它"为口译优化",用数千小时专业口译员音频训练,使其保持"只翻译"、并在产出语音前等到足够上下文——这对语序差异大的语言尤为重要;其二,它能在处理输入音频的同时流式返回翻译音频,从而在连续语音上实现真正的低延迟。相比之下,通用模型虽然可以被 prompt 要求翻译,但它们可能仍去回答问题或执行指令,而不是翻译它们;而且通用模型依赖回合制交互,要求说话人停顿等模型生成,不适合流畅口译。这正是高准确度、低延迟自然口译一直以来的主要阻碍,也是这个专用模型要解决的。注意它当前不支持自定义 prompt 或选音色参数。
术语 gpt-realtime-translate(专用语音到语音实时翻译模型); optimized for interpretation(为口译优化,只翻译不作答); simultaneous streaming(边收边译的同声流式); session.audio.output.language(设定目标输出语言的字段)
📖 "Unlike general-purpose voice models, gpt-realtime-translate is optimized for interpretation. It was trained on thousands of hours of professional interpreter audio, which helps it remain translation-only and wait for enough context before producing speech." — 原文
📖 "It can process input audio while simultaneously streaming translated audio back. This allows for truly low latency over continuous speech." — 原文
📖 "General-purpose models can be prompted to translate, but they may still answer questions or follow instructions rather than translate them." — 原文
🧪 实例 服务端创建短期 client secret 时用 session.audio.output.language 指定目标语言(此处默认 es):
ts
body: JSON.stringify({
  session: {
    model: "gpt-realtime-translate",
    audio: {
      input: {
        transcription: { model: "gpt-realtime-whisper" },
        noise_reduction: { type: "near_field" },
      },
      output: { language },
    },
  },
}),
🔍 追问 要构建的是语音 agent 而不是口译,应该用哪个模型? → 用新的 gpt-realtime-2;gpt-realtime-translate 的定位是"让人多语言化",而非构建 AI 语音 agent。
📚 拓展阅读
QRealtime 转录 API 有哪些格式与会话限制?延迟大概多少?深挖·拓展中频
Realtime WebSocket pcm16
⏱️ 现行
Realtime Transcription API 走 WebSocket,提供超低延迟(通常 300–800 ms)、支持部分与最终转录,并内置轮次检测(turn detection)、降噪与可选的 token 级 log-probabilities。代价是集成复杂:要自己管理 WebSocket、Base64 编码和健壮的错误处理。两条硬约束尤其常被追问——会话限制在 30 分钟,超时要重连并拼接;格式受限,只接受原始 PCM(不支持 MP3 或 Opus),对 pcm16 而言输入必须是 16-bit PCM、24 kHz 采样率、单声道(mono)、小端字节序。实现上是把音频重采样到 24 kHz、按 ≈128 ms 的块(3072 samples)以实时节奏 input_audio_buffer.append 送出,再从 ...transcription.delta 流式增量、在 ...transcription.completed 时把当前句提升为最终结果。session 通过 transcription_session.update 配置 input_audio_format: pcm16server_vad 的阈值。
术语 pcm16(16-bit/24 kHz/单声道/小端的原始格式); server_vad(服务端语音活动检测,turn detection); input_audio_buffer.append(逐块追加音频的事件); 30-minute sessions(单会话时长上限)
📖 "Ultra-low latency: Typically 300–800 ms, enabling near-instant transcription." — 原文
📖 "Restricted formats: Accepts only raw PCM (no MP3 or Opus); For pcm16, input audio must be 16-bit PCM at a 24kHz sample rate, single channel (mono), and little-endian byte order." — 原文
📖 "Session constraints: Limited to 30-minute sessions." — 原文
🧪 实例transcription_session.update 配置 session:
python
def _session(model: str, vad: float = 0.5) -> dict:
    return {
        "type": "transcription_session.update",
        "session": {
            "input_audio_format": "pcm16",
            "turn_detection": {"type": "server_vad", "threshold": vad},
            "input_audio_transcription": {"model": model},
        },
    }
🔍 追问 收到 ...transcription.delta...transcription.completed 分别该怎么处理? → delta 累加到当前句并实时打印;completed 表示一句结束,把当前句拼好后移入永久结果列表并清空缓冲。
📚 拓展阅读
Q如何用 Whisper prompt 纠正人名/产品名等专有名词的拼写?深挖·拓展中频
Whisper 拼写表 专有名词
⏱️ 现行
Whisper 对不常见的专有名词(产品名、公司名、人名)容易转错拼写,机制上可以把这些名字直接当作"术语表/glossary"塞进 prompt,让模型跟随你的首选拼写。例如一段满是产品名的音频,基线无 prompt 会靠猜;把 QuirkQuid Quill Inc, P3-Quattro, ... 这类正确拼写列进 prompt 就能纠正。对人名同理:Whisper 会把朋友名字猜成 Amy、Sean,而实际是 Aimee、Shawn——只要给一个简短的拼写 prompt(如 Friends: Aimee, Shawn)就能纠偏。prompt 形式可以是简单的逗号列表、带 Glossary: 前缀的词表,也可以是更自然的句子式("Aimee and Shawn ate whisky, doughnuts, omelets at a BBQ.")。权衡点在于:这类拼写引导本质仍是"跟随风格",受 224-token 限制,且不会覆盖模型对音频内容的理解——它只在拼写本就有歧义时才起决定作用。
术语 glossary prompt(把正确拼写当术语表传入); proper nouns(易错的专有名词); spelling prompt(如 Friends: Aimee, Shawn 的纠拼提示)
📖 "Whisper may incorrectly transcribe uncommon proper nouns such as names of products, companies, or people." — 原文
📖 "To get Whisper to use our preferred spellings, let's pass the product and company names in the prompt, as a glossary for Whisper to follow." — 原文
🧪 实例 三种等价的拼写引导写法,把 Amy/Sean 纠正为 Aimee/Shawn:
python
# 简短拼写 prompt
transcribe(bbq_plans_filepath, prompt="Friends: Aimee, Shawn")
# 带 Glossary 前缀的更长词表
transcribe(bbq_plans_filepath, prompt="Glossary: Aimee, Shawn, BBQ, Whisky, Doughnuts, Omelet")
# 更自然的句子式 prompt
transcribe(bbq_plans_filepath, prompt="""Aimee and Shawn ate whisky, doughnuts, omelets at a BBQ.""")
🔍 追问 拼写表能无限长吗? → 不能,prompt 仍受 224-token 上限约束,超出只保留最后 224 tokens,过长的术语表前段会被静默丢弃。
📚 拓展阅读
QRealtime Translation 的会话生命周期为什么"没有回合"?音色如何处理?深挖·拓展中频
实时翻译 会话 动态音色
⏱️ 现行
实时翻译的会话生命周期和普通 Realtime 语音会话很不一样。首先是专用端点:连接 /v1/realtime/translations。其次是持续音频进出——用 session.input_audio_buffer.append 连续流入 24 kHz PCM16 音频(短语之间的静音也要发),模型以 200 ms 的 PCM16 分块持续吐出翻译音频外加目标语言转录增量。最关键的机制是"没有回合生命周期":翻译直接从进来的音频流开始,没有 response.create、没有 assistant turn、没有工具调用、也没有对话状态要管理——这正是它能做同声传译、不必等说话人停顿的原因。音色方面它采用"动态音色适配":不选固定输出音色,而是让翻译语音跟随源说话人的整体音调、音高和说话风格;多说话人会话里,新说话人进来时翻译音色也会随之变化。如果需要源语言转录,可以用 gpt-realtime-whisper 配置输入转录。协议上浏览器端用 WebRTC(走 oai-events 数据通道收事件),后端媒体管道用 WebSocket(如 Twilio Media Streams)。
术语 /v1/realtime/translations(实时翻译专用端点); No turn lifecycle(无回合,无 response.create); dynamic voice adaptation(动态音色适配,跟随源说话人); oai-events(WebRTC 上收会话更新/转录增量的数据通道)
📖 "No turn lifecycle: Translation starts from the incoming audio stream itself. There is no response.create, assistant turn, tool call, or conversation state to manage." — 原文
📖 "Realtime Translation uses dynamic voice adaptation. Instead of selecting a fixed output voice, translated speech follows the source speaker's general tone, pitch, and speaking style." — 原文
🧪 实例 后端直接用 API key 开 WebSocket 翻译会话,再用 session.update 设定目标语言:
ts
ws.send(JSON.stringify({
  type: "session.update",
  session: {
    audio: {
      input: {
        transcription: { model: "gpt-realtime-whisper" },
        noise_reduction: { type: "near_field" },
      },
      output: { language: targetLanguage },
    },
  },
}));

双人通话通常开两个方向的会话(A→B 与 B→A),每个会话的输出语言是"听者"选的语言,而非说话人的。
🔍 追问 Twilio 的 8 kHz u-law 音频怎么接进来? → 桥接层解码 u-law、重采样到 24 kHz 再 append,并持续发送(包括静音),因为翻译会话不是回合制、不会等 response.create
📚 拓展阅读
Q上线实时翻译前要注意哪些模型限制(术语、混合语言、评测)?深挖·拓展低频
生产化 评测 局限
⏱️ 现行
上线前要用真实的音频、语言、网络条件和用户流程完整测一遍,因为翻译质量只是体验的一部分,延迟、说话人路由、字幕、重连和音频控制都影响生产就绪度。几个关键限制:一是模型当前不支持自定义 prompt、术语表或发音指南,所以依赖特定词汇/人名/法律医疗术语的场景要直接测这些词——模型有时会在翻译时替换错人名或实体,要把这些用例放进上线评测集。二是要考虑混合语言:实时翻译会尽量不去翻译"已经是目标输出语言"的语音,比如输出设为西语时说话人切到西语,模型可能就不产出该段翻译音频——因此如果会全静音原音频会让人困惑,更好的模式是翻译音频播放时把原音频压低(duck)而非完全静音。三是评测要把意义和延迟分开衡量:按意义而非逐字打分(BLEU 这类文本指标是弱代理,因为漏掉语义相似度),单独测"源语音到翻译音频"和"到字幕"的延迟,并人工回看低分样本(人名、数字、部分翻译、语气、跳过/延迟内容最容易被聚合分数掩盖)。当前支持 13 种目标输出语言、70 多种输入语言。
术语 mixed-language speech(混合语言,已是目标语言的段可能不翻译); duck the original audio(压低而非静音原音频); BLEU(文本指标,speech-to-speech 质量的弱代理); 13 target output languages(当前目标输出语言数)
📖 "The model does not currently support custom prompts, glossaries, or pronunciation guides." — 原文
📖 "Realtime Translation tries not to translate speech that is already in the selected output language." — 原文
📖 "Traditional text metrics like BLEU can help with quick comparisons, but they are a weak proxy for speech-to-speech quality because they miss semantic similarity." — 原文
🧪 实例 评测时把四类物料按 run 绑在一起,以便事后回答三个问题——模型听到了什么、说了什么、本应说什么:
text
每个 run 保留:
  source audio      # 源音频(模型听到的)
  translated audio  # 生成的翻译音频(模型说的)
  generated transcript
  reference text     # 人工参考(本应说的)

参考优先用人工编辑字幕/专业口译转录,自动字幕只适合快速冒烟测试。
🔍 追问 Spanglish(西英混说)翻成不同目标语言表现为何不同? → 翻成德语时英语和西语部分都要译、表现正常;翻成英语时模型可能译西语部分却在英语部分保持沉默,听感会更卡顿。
📚 拓展阅读
中频

批处理、提示缓存与性能优化

原文 原文 原文
QBatch API 解决什么问题?它和普通 Chat Completions 调用有什么本质区别?深挖·拓展🔥高频
Batch API 异步 成本优化
⏱️ 现行
Batch API 的核心是把"同步实时请求"改成"异步批量作业":你一次性提交一个包含大量请求的文件,OpenAI 在后台处理,承诺在 24 小时窗口内完成(通常更快),换来的是更低的价格和更高的 rate limit。它适合那些不需要即时返回结果的离线工作负载——原文列举的典型场景包括给博客/市场内容打标签与富化、给客服工单归类并建议答案、对大规模客户反馈做情感分析、以及批量生成文档摘要或翻译。关键权衡在于延迟换成本与吞吐:你放弃了实时性(最长等 24h),但显著降低了单位成本并绕开了同步接口更紧的限流。使用上几乎零迁移成本——Batch API 与 Chat Completions 端点用法一致,支持相同参数和大多数近期模型(gpt-4o、gpt-4o-mini、gpt-4-turbo、gpt-3.5-turbo 等),所以原文建议把所有能异步化的工作负载都切到 batch job。
术语 Batch API(异步批量作业接口); completion_window(完成窗口,示例中为 "24h"); rate limits(限流,Batch 通道更宽松)
📖 "The new Batch API allows to create async batch jobs for a lower price and with higher rate limits." — 原文
📖 "Batches will be completed within 24h, but may be processed sooner depending on global usage." — 原文
📖 "By using this API, you can significantly reduce costs, so we recommend switching every workload that can happen async to a batch job with this new API." — 原文
🧪 实例 创建 batch job 只需三步——上传文件、创建作业、轮询状态:
python
batch_file = client.files.create(
  file=open(file_name, "rb"),
  purpose="batch"
)
batch_job = client.batches.create(
  input_file_id=batch_file.id,
  endpoint="/v1/chat/completions",
  completion_window="24h"
)
batch_job = client.batches.retrieve(batch_job.id)
print(batch_job)
🔍 追问 Batch 一定要等满 24 小时吗? → 不一定,24h 只是上限;原文说 "Batches will be completed within 24h, but may be processed sooner depending on global usage",实际取决于全局负载,通常更快完成。
📚 拓展阅读
QPrompt Caching 是怎么工作的?它凭什么能同时降低延迟和成本?深挖·拓展🔥高频
Prompt Caching 前缀匹配 延迟优化
⏱️ 现行
Prompt caching 的机制是"前缀命中":当一个请求进来,系统先检查 prompt 的开头部分(prefix)是否已被缓存,若命中(cache hit)就直接复用已缓存的前缀,从而降低延迟和成本;若没命中,就从头处理整个 prompt 并把该前缀缓存起来供后续复用。它对超过 1024 tokens 的 prompt 自动生效——你不需要改任何请求代码;对超过 10,000 tokens 的长 prompt,延迟最多可降低约 80%。之所以能同时省时省钱,是因为重复的信息(工具定义、代码库摘要、多轮对话的静态部分)只需被处理一次。缓存作用域是组织级(organization level),只有同组织成员能共享缓存,且缓存过程符合零数据保留(zero data retention)。要判断是否命中,可以看 usage.prompt_tokens_details 里的 cached_tokens 字段:所有请求都会返回该字段,低于 1024 tokens 的请求 cached_tokens 恒为 0。核心权衡与要求是"前缀必须逐字节一致"——静态内容(指令、示例、工具、图片)要放在开头且顺序固定,任何开头差异都会导致 miss。

flowchart LR
    A[API 请求] --> B{前缀已缓存?}
    B -->|命中 cache hit| C[复用缓存前缀
降低延迟与成本] B -->|未命中| D[从头处理整个 prompt] D --> E[缓存该前缀供后续复用]
术语 prefix(prompt 开头被缓存的前缀); cache hit(前缀命中,复用缓存); cached_tokens(命中的缓存 token 数,<1024 token 请求恒为 0); organization level(组织级作用域)
📖 "OpenAI offers discounted prompt caching for prompts exceeding 1024 tokens, resulting in up to an 80% reduction in latency for longer prompts over 10,000 tokens." — 原文
📖 "When an API request is made, the system first checks if the beginning portion (prefix) of the prompt has already been cached. If a match is found (cache hit), the cached prompt is used, leading to reduced latency and costs. If there's no match, the system processes the full prompt from scratch and caches the prefix for future use." — 原文
📖 "Prompt caching is scoped at the organization level, meaning only members of the same organization can access shared caches." — 原文
📖 "In the response object and the output below, for the second completion run2, you can see that the cached_tokens value is greater than zero, indicating successful caching." — 原文
🧪 实例 多轮客服对话中,把庞大的 system prompt 和 tools 定义放最前,新的用户消息 append 到 messages 末尾;第二轮 run2cached_tokens 就会大于零,证明前缀命中。原文原句:
🔍 追问 小于 1024 tokens 的请求也会返回 cached_tokens 吗? → 会,原文说所有请求都显示该字段,但 "For requests under 1024 tokens, cached_tokens will be zero",即恒为 0。
📚 拓展阅读
Q遇到 429 / RateLimitError 时,为什么推荐用"带随机抖动的指数退避"?深挖·拓展🔥高频
rate limit 指数退避 重试
⏱️ 现行
当请求发得太快就会触发 429: 'Too Many Requests'RateLimitError。最简单的缓解办法是自动用"随机指数退避(random exponential backoff)"重试:命中限流后先短暂 sleep,再重试失败的请求;若仍失败,就把 sleep 时长加倍再重试,直到成功或达到最大重试次数。这套机制有三重好处:自动重试让你从限流中恢复而不至于崩溃或丢数据;指数退避让前几次重试可以很快尝试,同时在连续失败时享受更长的延迟;加入随机抖动(jitter)则避免所有重试在同一时刻一起打过来又互相撞限流。一个关键约束是:失败的请求同样会计入你的每分钟额度,所以"不停地无脑重发"是行不通的——这正是需要退避而非死循环重试的根本原因。实现上可以用 Tenacity、backoff 这类第三方库,也可以手写一个 retry 装饰器(叠加 exponential_base 和随机 jitter)。
术语 exponential backoff(指数退避,失败后延迟翻倍); jitter(随机抖动,打散重试时刻); RateLimitError(超限异常,429); tenacity.wait_random_exponential(Tenacity 提供的随机指数退避)
📖 "One easy way to mitigate rate limit errors is to automatically retry requests with a random exponential backoff." — 原文
📖 "Adding random jitter to the delay helps retries from all hitting at the same time" — 原文
📖 "Note that unsuccessful requests contribute to your per-minute limit, so continuously resending a request won’t work." — 原文
🧪 实例 用 Tenacity 装饰器给请求加随机指数退避(最小 1s、最大 60s,最多 6 次):
python
from tenacity import (
    retry,
    stop_after_attempt,
    wait_random_exponential,
)  # for exponential backoff

@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def completion_with_backoff(**kwargs):
    return client.chat.completions.create(**kwargs)
🔍 追问 不想引第三方库怎么办? → 可以手写 retry_with_exponential_backoff,核心是 delay *= exponential_base * (1 + jitter * random.random())time.sleep(delay),超过 max_retries 就抛异常。
📚 拓展阅读
QBatch 输入文件长什么样?为什么每个请求必须有唯一的 custom_id?深挖·拓展中频
Batch API jsonl custom_id
⏱️ 现行
Batch 的输入是 jsonl 格式文件,每一行是一个 JSON 对象、对应一个请求,字段包括 custom_idmethodurlbody(body 里就是你平时在 Chat Completions 里写的 model、messages 等参数)。关键点在于 custom_id 必须在同一批次内唯一——因为 batch 的结果不会按输入顺序返回,你必须靠 custom_id 把每条结果对应回原始输入请求。这带来一个实践约束:读取结果时不能靠行号或顺序假设,必须逐条解析 custom_id 再回查原始数据(例如从 task-<index> 里切出 index 去索引原 DataFrame)。这也是异步批处理相对同步调用多出来的"对账"成本,是吞吐换来的必要代价。
术语 jsonl(每行一个 JSON 对象的文件格式); custom_id(批内唯一请求 ID,用于回对结果); body(等价于 Chat Completions 调用体)
📖 "The batch file, in the jsonl format, should contain one line (json object) per request." — 原文
📖 "Note: the request ID should be unique per batch. This is what you can use to match results to the initial input files, as requests will not be returned in the same order." — 原文
🧪 实例 每行请求的结构(原文给出的模板):
json
{
    "custom_id": "task-0",
    "method": "POST",
    "url": "/v1/chat/completions",
    "body": {
        "model": "gpt-4o-mini",
        "temperature": 0.1,
        "response_format": { "type": "json_object" },
        "messages": [
            {"role": "system", "content": "..."},
            {"role": "user", "content": "..."}
        ]
    }
}
🔍 追问 读结果时能假设顺序和输入一致吗? → 不能。原文明确提醒 "the results are not in the same order as in the input file.",必须靠 custom_id 把结果对回输入请求。
📚 拓展阅读
Q想让 Prompt Caching 命中率最高,有哪些最佳实践?深挖·拓展中频
Prompt Caching 最佳实践 缓存命中率
⏱️ 现行
要吃满缓存红利,核心原则是"稳定的前缀 + 稳定的使用节奏"。第一,把静态或频繁复用的内容(指令、示例、工具、图片)放在 prompt 开头,把动态的用户特定数据放到末尾,这样前缀更容易保持一致从而命中;这条对 images 和 tools 同样适用——它们必须逐字节相同,连顺序都要一致。第二,保持稳定的使用模式:不常用的 prompt 会被自动从缓存中淘汰(eviction),要防止缓存被清掉就得持续使用同样的前缀。第三,监控关键指标——缓存命中率、延迟、以及 cached tokens 占比,据此不断调优缓存策略。一个容易踩的坑:图片缓存要求 detail 参数保持一致(它影响图片如何被 tokenize),而且哪怕用户 query 相同,只要第一张图片的 URL 变了(比如从 veggie_url 换成 eggs_url),前缀就变了、缓存就不命中。
术语 cache eviction(缓存淘汰,不常用的前缀被自动移除); detail(图片细节参数,影响 tokenize,须保持一致); cache hit rate(缓存命中率,应监控)
📖 "Place static or frequently reused content at the beginning of prompts: This helps ensure better cache efficiency by keeping dynamic data towards the end of the prompt." — 原文
📖 "Maintain consistent usage patterns: Prompts that aren't used regularly are automatically removed from the cache. To prevent cache evictions, maintain consistent usage of prompts." — 原文
📖 "Make sure the detail parameter remains consistent, as it affects how images are tokenized." — 原文
📖 "The output for this example shows that a cache was hit for the second run, however it was not hit for the third run because of a different first url (eggs_url instead of veggie_url), even though the user query is the same. " — 原文
🧪 实例 图片缓存 miss 的真实原因——原文第三次调用换了第一张图的 URL:
🔍 追问 为什么要监控 cached tokens 占比? → 原文建议 "Regularly track cache hit rates, latency, and the proportion of cached tokens",用这些洞察来微调缓存策略、最大化性能。
📚 拓展阅读
Qrate limit 的用量是怎么算的?为什么调小 max_tokens 能避免过早被限流?深挖·拓展中频
rate limit max_tokens TPM
⏱️ 现行
OpenAI 计算 rate limit 用量时取两者中的较大值:一是 max_tokens(允许的最大响应 token 数),二是根据 prompt 字符数估算出的输入 token 数。关键推论是:如果你把 max_tokens 设得过高,即便实际响应很短,用量也会被"高估",从而让你在真正用满额度之前就提前触发限流。所以最佳实践是把 max_tokens 配置得贴近你预期的响应长度,这样用量估算更准确、能防止无谓的 throttling。这是一个纯粹靠参数就能拿到的吞吐优化——不改逻辑、不加重试,仅仅让预算不被虚高的 max_tokens 浪费掉。
术语 max_tokens(响应最大 token 数,过高会高估用量); TPM(tokens per minute,每分钟 token 限额); throttling(限流节流)
📖 "Rate limit usage is calculated based on the greater of:" — 原文
📖 "If you set max_tokens too high, your usage can be overestimated, even if the actual response is much shorter. To avoid hitting rate limits prematurely, configure max_tokens so it closely matches the size of the response you expect." — 原文
🧪 实例 把 max_tokens 收紧到贴近预期响应大小:
python
def completions_with_max_tokens(**kwargs):
    return client.chat.completions.create(**kwargs)


completions_with_max_tokens(model="gpt-4o-mini", messages=[{"role": "user", "content": "Once upon a time,"}], max_tokens=100)
🔍 追问 用量到底是取输入还是输出? → 取两者较大值:max_tokens 与"由 prompt 字符数估算的输入 token"之间较大的那个。
📚 拓展阅读
Q批量数据处理时,除了退避重试还能怎么最大化吞吐?深挖·拓展中频
吞吐优化 主动限速 RPM/TPM
⏱️ 现行
处理实时用户请求时,退避重试(backoff and retry)是兼顾低延迟又避开限流的好策略;但处理大批量数据、吞吐比延迟更重要时,还有两招。第一是"主动加延迟":如果你反复地撞限流、退避、再撞、再退避,相当一部分请求预算会浪费在需要重试的请求上,拉低有效吞吐;解决办法是按 rate limit 的倒数给每个请求主动加延迟(例如限额 20 请求/分钟,就给每个请求加 3–6 秒延迟),让你贴着限流上限跑而不真的撞上去。第二是"合并请求":OpenAI 对 RPM/RPD(每分钟/每天请求数)和 TPM(每分钟 token 数)分别限流,如果你撞的是 RPM 上限但 TPM 还有余量,就可以把多个任务打包进一个请求,减少每分钟请求数、避开 RPM 天花板。合并的代价要记住:单请求有最大 token 上限(超了会失败或被截断)、打包会引入等待延迟、且响应可能不按提交顺序/格式返回,需要后处理逐条对回。
术语 RPM/RPD(每分钟/每天请求数限额); TPM(每分钟 token 数限额); reciprocal delay(按限额倒数加的主动延迟); batching requests(把多任务合并进一个请求)
📖 "Here, one potential solution is to calculate your rate limit and add a delay equal to its reciprocal (e.g., if your rate limit 20 requests per minute, add a delay of 3–6 seconds to each request)." — 原文
📖 "The OpenAI API enforces separate limits for requests per minute/day (RPM/RPD) and tokens per minute (TPM)." — 原文
📖 "By bundling several prompts together, you reduce the total number of requests sent per minute, which helps avoid hitting the RPM cap." — 原文
🧪 实例 按限额倒数给每个请求主动加延迟:
python
import time

# Define a function that adds a delay to a Completion API call
def delayed_completion(delay_in_seconds: float = 1, **kwargs):
    """Delay a completion by a specified amount of time."""

    # Sleep for the delay
    time.sleep(delay_in_seconds)

    # Call the Completion API and return the result
    return client.chat.completions.create(**kwargs)


# Calculate the delay based on your rate limit
rate_limit_per_minute = 20
delay = 60.0 / rate_limit_per_minute
🔍 追问 合并多个 prompt 后响应顺序会乱吗? → 会。原文提醒 "the response object may not return in the same order or format as the prompts that were submitted",需靠后处理把每条响应对回对应的 prompt。
📚 拓展阅读
Q主模型被限流时,直接切到备用模型是好办法吗?深挖·拓展低频
fallback 模型降级 rate limit
⏱️ 现行
当主模型触发限流时,一个选择是切到二级(备用)模型,这能在主模型被 throttle 或不可用时保持应用响应。但这不是万能药:备用模型在准确率、延迟和成本上可能差异显著,因此对要求高度一致结果的场景不一定适用。还有一个容易忽略的陷阱——某些模型之间共享 rate limit,这时单纯切模型可能并不能真正缓解限流(你只是换到一个共享同一额度的模型)。因此原文强调:上线前必须充分测试 fallback 对输出质量、用户体验和运营预算的影响,并用相关评测(evaluations)验证它在真实条件下达标。实现上非常简单——捕获 RateLimitError 后把 model 换成 fallback_model 重试即可。
术语 fallback model(备用/降级模型); shared rate limits(部分模型共享限额,切换未必有效); evaluations(上线前的评测验证)
📖 "If you encounter rate limit errors on your primary model, one option is to switch to a secondary model. This approach helps keep your application responsive when your primary model is throttled or unavailable." — 原文
📖 "Additionally, keep in mind that some models share rate limits, which may reduce the effectiveness of simply switching models." — 原文
🧪 实例 捕获限流后回退到备用模型:
python
def completions_with_fallback(fallback_model, **kwargs):
    try:
        return client.chat.completions.create(**kwargs)
    except openai.RateLimitError:
        kwargs['model'] = fallback_model
        return client.chat.completions.create(**kwargs)
🔍 追问 怎么知道哪些模型共享 rate limit? → 原文指向组织的 limit 页面查看,原句为 "You can see the models that share limits in your" 组织限额页(organizations limit page)。
📚 拓展阅读
中频

提示工程与 GPT-5 / 推理模型实战

原文 原文 原文
QGPT-5 的 "agentic eagerness"(智能体积极性)是什么?生产中如何往两个方向调?深挖·拓展🔥高频
agentic reasoning_effort prompting
⏱️ 现行
agentic eagerness 指模型在"主动探索/多调工具"与"谨慎等待用户明确指令"之间的平衡,GPT-5 被训练成能覆盖整条光谱——既能在模糊场景下做高层决策,也能被约束到只做定义明确的窄任务。要往"less eagerness"调(降延迟、少调工具),第一杠杆是把 reasoning_effort 调低,这会牺牲探索深度换取效率与延迟;第二是在 prompt 里用 <context_gathering> 块给出明确的早停判据(如 top hits 收敛到 ~70%、能点名要改的具体内容就停),甚至设死工具调用预算(如"绝对上限 2 次")。关键权衡在于:压缩上下文收集会让模型倾向于在信息不足时也硬给答案,所以官方强调要给一个"逃生舱"(escape hatch)——用类似 even if it might not be fully correct 的措辞让模型敢于在不确定下继续,否则它会卡住。反方向要"more eagerness"(高自主、少反问)时,则调高 reasoning_effort,并用 <persistence> 块要求它"不解决完用户问题不交还回合、遇到不确定自己推断而非反问"。一个反直觉的成本点:GPT-5 的指令遵循非常精确,越是想让它自主,越要明确写清停止条件与"哪些动作安全、哪些需要用户确认"(如支付/删文件工具阈值要设低,搜索工具阈值可设高)。
术语 agentic eagerness(智能体积极性,主动性与等待指令间的平衡); reasoning_effort(推理力度参数,控制思考深度与调工具意愿); context_gathering(上下文收集 prompt 块,含早停判据与工具预算); escape hatch(逃生舱,允许模型在不确定下继续的措辞); persistence(持久性 prompt 块,强制模型完成任务再交还)
📖 "In this section we cover how to best calibrate GPT-5’s agentic eagerness: in other words, its balance between proactivity and awaiting explicit guidance." — 原文
📖 "When limiting core context gathering behavior, it’s helpful to explicitly provide the model with an escape hatch that makes it easier to satisfy a shorter context gathering step." — 原文
🧪 实例 想让一个代码检索 agent 更快收敛、少乱调工具,可直接把这段官方"低积极性"预算 prompt 塞进 system:
text
<context_gathering>
- Search depth: very low
- Bias strongly towards providing a correct answer as quickly as possible, even if it might not be fully correct.
- Usually, this means an absolute maximum of 2 tool calls.
- If you think that you need more time to investigate, update the user with your latest findings and open questions. You can proceed if the user confirms.
</context_gathering>
🔍 追问 为什么"逃生舱"措辞对压缩上下文收集特别重要? → 因为默认 GPT-5 会尽量把上下文收集全再作答,若只是硬性限制轮数而不给"允许不完全正确"的出口,模型会陷入满足不了短收集步的两难;逃生舱降低了它交付的门槛,让短流程真正跑得动。
📚 拓展阅读
QGPT-5 有哪几个控制"思考量/输出量"的旋钮?reasoning_effort、verbosity、minimal reasoning 各管什么?深挖·拓展🔥高频
reasoning_effort verbosity latency
⏱️ 现行
GPT-5 把"想多深"和"答多长"拆成了两个正交参数。reasoning_effort 控制模型思考多努力、以及多愿意调工具,默认 medium,应按任务难度上下调:复杂多步任务用高 effort 保质量,并且官方观察到把可分离的子任务拆到多个 agent 回合(一轮一任务)时性能最好。verbosity 是 GPT-5 新引入的参数,它影响的是最终答案的长度,而不是思考过程的长度——这与 reasoning_effort 管的"思考长度"是两回事;而且除了全局 API 参数,GPT-5 还被训练成能响应 prompt 里的自然语言 verbosity 覆盖(Cursor 的做法就是全局设 low、只在编码工具里指定 high)。minimal reasoning 是 GPT-5 首次引入的最快档,仍保留推理范式的收益,是延迟敏感用户和 GPT-4.1 老用户的最佳升级路径;但它对 prompt 更敏感,官方给了几条补偿性做法:让模型在最终答案开头用要点列出简短思路、要求详尽的工具调用前导(preamble)、把工具指令歧义消到最小并加持久性提醒防提前终止、以及显式的 prompted planning——因为此时模型可用的内部规划 token 更少。
术语 reasoning_effort(推理力度,默认 medium,控思考深度与调工具意愿); verbosity(冗长度,新参数,控最终答案长度而非思考长度); minimal reasoning(最小推理档,最快、对 prompt 最敏感); prompted planning(提示式规划,minimal 档下更重要)
📖 "We provide a reasoning_effort parameter to control how hard the model thinks and how willingly it calls tools; the default is medium, but you should scale up or down depending on the difficulty of your task." — 原文
📖 "in GPT-5 we introduce a new API parameter called verbosity, which influences the length of the model’s final answer, as opposed to the length of its thinking." — 原文
📖 "In GPT-5, we introduce minimal reasoning effort for the first time: our fastest option that still reaps the benefits of the reasoning model paradigm." — 原文
🧪 实例 两个旋钮的分工可以这样理解:
flowchart LR
  P[Prompt] --> RE[reasoning_effort
思考多深/是否调工具] P --> VB[verbosity
最终答案多长] RE --> T[思考 tokens / CoT] VB --> A[最终答案 tokens]

低延迟场景:reasoning_effort=minimal + 全局 verbosity=low,再在编码工具处用自然语言覆盖为 high,兼顾"状态更新简短"与"代码 diff 可读"。
🔍 追问 为什么 minimal reasoning 比高档更依赖 prompt 质量? → 因为它可用的内部推理 token 更少,内部规划空间小,一旦工具指令有歧义或缺少持久性提醒,就更容易过早终止或跑偏,所以要靠显式规划、前导消息和持久性提醒来补。
📚 拓展阅读
Q为什么 GPT-5 生产部署强烈建议用 Responses API 而非 Chat Completions?深挖·拓展🔥高频
Responses-API production cost
⏱️ 现行
核心原因是 Responses API 能在工具调用之间持久化推理上下文,让模型引用它之前的推理轨迹,从而省下 CoT token、不必在每次工具调用后从零重建计划,同时改善延迟与性能。这不是玄学:官方在评测里看到,仅仅从 Chat Completions 切到 Responses API、并用 previous_response_id 把前一次的 reasoning items 传回后续请求,Tau-Bench Retail 分数就从 73.9% 提升到 78.2%,是有统计显著性的改进。权衡上几乎是净收益——它同时解锁更好的 agentic 流、更低成本和更高效的 token 使用,而且该特性对所有 Responses API 用户开放,包括 ZDR(零数据保留)组织,所以合规敏感场景也能用。对面试而言要点是:reasoning 模型的"思考"是昂贵且易丢的资产,Chat Completions 每轮无状态会逼模型重复推理,而 Responses API 用 previous_response_id 把这份资产接续下去,是把"多轮工具调用 agent"做对的关键基础设施。
术语 Responses API(响应式 API,跨请求保留推理状态); previous_response_id(前次响应 ID,用于回传上一轮 reasoning items); reasoning items(推理项,被保留的思考轨迹); ZDR(Zero Data Retention,零数据保留组织,也可用此特性)
📖 "We strongly recommend using the Responses API when using GPT-5 to unlock improved agentic flows, lower costs, and more efficient token usage in your applications." — 原文
📖 "we observed Tau-Bench Retail score increases from 73.9% to 78.2% just by switching to the Responses API and including previous_response_id to pass back previous reasoning items into subsequent requests." — 原文
🧪 实例 多轮工具调用时把上一次响应 ID 接续回去,模型即可复用其推理:
json
{
  "model": "gpt-5",
  "previous_response_id": "resp_6888f6d0606c819aa8205ece",
  "input": "根据刚才的分析,继续修复第二个 bug"
}
🔍 追问 保留推理上下文为什么同时改善了"延迟"和"性能"两项? → 因为模型不必在每次工具调用后重新从头构建计划,少了重复推理既省 token(降延迟)又避免了重建过程中的信息丢失或偏移(提性能),两者是同一机制的两面。
📚 拓展阅读
Q为什么说"矛盾/模糊的指令"对 GPT-5 的伤害比对其它模型更大?怎么排查?深挖·拓展低频
instruction-following prompt-quality debugging
⏱️ 现行
GPT-5 像 GPT-4.1 一样以"外科手术般的精度"遵循 prompt 指令,这让它能嵌入各类工作流;但这份严谨的遵循恰恰意味着,当 prompt 里含有互相矛盾或含糊的指令时,GPT-5 受的伤害比别的模型更大——它会花费 reasoning token 去努力调和这些矛盾,而不是像其它模型那样随便挑一条执行。官方给的对抗性例子是一个医疗排班 prompt:一处写"没有记录在案的明确患者同意就绝不排约",另一处却写"高危时不联系患者、直接自动分配当天最早时段作为首个动作";又比如"任何动作前都要先查患者档案"与"高紧急时先让患者打 911、任何排班步骤之前"直接冲突。修复方法不是加更多规则,而是消解指令层级冲突:把自动分配改成"告知患者之后"再分配以与"仅在同意下排约"一致,并显式补一句"紧急情况下不做查档,直接给 911 指引"给模型一个明确出口。实践启示是:prompt 常是多人长期迭代的活文档,更要专门审查措辞不良之处;官方说已有多个早期用户在这种审查中发现了核心 prompt 库里的歧义与矛盾,移除后 GPT-5 表现大幅改善,并推荐用 prompt optimizer 工具来定位这类问题。
术语 instruction-following(指令遵循,GPT-5 以 surgical precision 执行); instruction hierarchy conflicts(指令层级冲突,需被显式消解); reasoning tokens(推理 token,被浪费在调和矛盾上); prompt optimizer(提示优化器,用于发现矛盾/模糊)
📖 "However, its careful instruction-following behavior means that poorly-constructed prompts containing contradictory or vague instructions can be more damaging to GPT-5 than to other models, as it expends reasoning tokens searching for a way to reconcile the contradictions rather than picking one instruction at random." — 原文
🧪 实例 典型冲突对(来自官方医疗排班例子)——同一 prompt 内并存:
text
Never schedule an appointment without explicit patient consent recorded in the chart
# 却又写:
auto-assign the earliest same-day slot without contacting the patient as the first action to reduce risk

修复:把 auto-assign 改为 "after informing the patient of your actions",并显式允许紧急情况下跳过查档。
🔍 追问 既然 GPT-5 会努力调和矛盾,为什么这反而是坏事? → 因为调和矛盾要消耗宝贵的 reasoning token 且结果不可控,本该用于解题的推理预算被浪费在"拆炸弹"上,导致推理轨迹变差、效率下降;不如在 prompt 侧先把矛盾消掉。
📚 拓展阅读
Q如何用推理模型(o1)做数据校验?整条生产化流水线怎么设计与评估?深挖·拓展中频
reasoning-model data-validation eval
⏱️ 现行
传统数据校验靠预定义规则和模式,而像 o1 这类推理模型能理解上下文、对数据本身做推理,提供更灵活智能的校验方式——尤其适合医疗这种字段间存在逻辑关系的敏感领域。这个 cookbook 的做法分三步:先用模型生成一份故意掺入不一致的合成医疗数据集(如给对青霉素过敏的患者开阿莫西林、糖尿病患者却没开降糖药、化验结果与诊断不符),每行带 Is ValidIssue 真值标签;然后把去掉这两列的每一行喂给 o1,让它只返回一个 JSON(is_valid 布尔 + issue 说明),判断该行是否有问题;最后把模型判定与真值对比,用 precision/recall/F1 衡量"能否正确识别出有问题的行"。第二层评估更细:对于那些被正确识别为有问题的子集,再用一个 model grader 判断模型给出的 issue 说明与真值 issue 是否指向同一个底层医疗问题(关注意图与医学概念而非字面措辞)。一个务实的成本权衡是:issue 匹配这一步任务更窄,所以用更快的 gpt-4o 当 grader 来算准确率,而不必对每个判分都动用推理模型。结论是该流水线能在 issue 识别上拿到高 precision/recall,并对"精确定位是哪个问题"也有不错准确率,可推广到多领域的 eval 集校验。
术语 data validation(数据校验,用模型理解上下文而非死规则); is_valid / issue(校验输出的两个 JSON 字段); model grader(模型评分器,用 gpt-4o 判断 issue 是否同一问题); precision / recall / f1(衡量 is_valid 判定的三项指标); synthetic data(合成数据,故意掺错以制作带标签的评测集)
📖 "However, advanced models like o1 can understand context and reason about data, offering a more flexible and intelligent approach to validation." — 原文
📖 "Once we have the model determine its list of invalid data, we will pass those results on to a model grader to assess two metrics:" — 原文
📖 "Given that this task is much more narrow, we can use the faster gpt-4o model to calculate the accuracy." — 原文
🧪 实例 校验 prompt 要求模型只吐 JSON,便于程序化解析:
python
response_content = response.choices[0].message.content.replace('
json', '').replace('```', '').strip()
response_dict = json.loads(response_content)
return response_dict # {"is_valid": false, "issue": "Prescribed Amoxicillin despite Penicillin allergy"}
```
🔍 追问 为什么要分"is_valid 判定"和"issue 匹配"两层评估,而不只看一个准确率? → 因为模型可能"猜对了这行有问题"却"说错了原因";两层拆开后,precision/recall/F1 衡量能否发现问题,issue 匹配衡量能否定位到正确的问题,只有两者都高才说明校验真正可用。
📚 拓展阅读
Q当模型在复杂任务上出错,有哪些经典手段提升可靠性?各自的机制与代价?深挖·拓展中频
reliability chain-of-thought self-consistency
⏱️ 通用
这些技术共享一个目标:把不可靠的整体操作拆成更可靠的小操作、或用多步/多关系让系统整体可靠性高于任一单点。第一类是拆分复杂任务:给模型更多时间和空间去想的一种方式就是把任务拆成更简单的片段,任务越原子,模型出错空间越小(如把多选逻辑题拆成"逐条判断线索相关性→组合相关线索→写最终答案")。第二类是让模型先解释再作答:最简单的就是在答案前加 Let's think step by step.,让它"出声思考",在基准上曾把解题率从 18% 拉到 79%;但它对多步算术/符号/策略类推理最有用,对简单数学或常识题帮助不大。第三类是自洽(self-consistency):对有离散答案集的任务,用正温度采样多份"解释+答案",再取出现最多的那个作为最终答案——简单有效,但生成 10 份就把成本抬 10 倍,且只适用于答案集有限的任务。第四类是验证器(verifier/discriminator):训练一个判别模型来评估主生成模型的输出,若被拒就重采样直到可接受;它奏效的深层原因是"判断一个答案往往比创造一个答案更容易",但同样代价高昂(每题生成上百个解会把成本抬约 100 倍)。
术语 split complex tasks(拆分复杂任务成原子子任务); Let's think step by step(零样本 CoT 触发语,先推理再作答); self-consistency(自洽,多次采样取众数答案); verifier / discriminator(验证器,判别输出好坏、可重采样); positive temperature(正温度,采样多样解释的前提)
📖 "One way to give a model more time and space to think is to break tasks into simpler pieces." — 原文
📖 "For tasks with a discrete set of answers, one simple way to improve reliability is to sample multiple explanations & answers from the model (using a positive temperature) and then pick the final answer that appears most often." — 原文
📖 "In many cases, it's easier to judge an answer than it is to create an answer, which helps explain the power of this method." — 原文
🧪 实例 自洽的核心机制——多路采样后投票:
flowchart LR
  Q[问题] -->|temperature>0| S1[解释+答案 A]
  Q --> S2[解释+答案 A]
  Q --> S3[解释+答案 B]
  S1 --> V{多数投票}
  S2 --> V
  S3 --> V
  V --> ANS[取出现最多的答案 A]
🔍 追问 自洽在什么情况下几乎没用? → 当到达答案只有唯一路径/唯一措辞时;极端情况若任务是生成单 token 答案,取 100 次生成的众数与直接在 temperature=0 取最高 logprob 的那个 token 没有区别。
📚 拓展阅读
QTool preamble 和 metaprompting 分别解决什么问题?Cursor 是怎么把 GPT-5 调顺的?深挖·拓展低频
tool-preamble metaprompting case-study
⏱️ 现行
Tool preamble(工具前导) 解决的是长 agentic 轨迹下的"用户可跟随性"问题——在被用户盯着看的执行过程中,模型间歇性汇报"正在用什么工具做什么、为什么",能显著改善交互体验,轨迹越长差别越大;GPT-5 被训练成通过 tool preamble 消息给出清晰的前置计划和持续进度更新,而且这些前导的频率、风格、内容都可在 prompt 里调(从事无巨细到只给简短开场计划)。Metaprompting(元提示) 是个收尾的元技巧:早期用户很成功地把 GPT-5 当作它自己的元提示器——直接问它"要往一个不成功的 prompt 里加什么/删什么才能更稳地引出期望行为、或抑制不期望行为",据此改出的 prompt 修订版已被部署到生产。Cursor 的调参案例把这些串起来:模型初期输出过于啰嗦(混入状态更新和任务后总结打断了用户心流),而工具里的代码虽质量高却因过度精简、单字母变量而难读——他们的解法是把 verbosity API 参数设为 low 让文本简短,再改 prompt 只在编码工具里强推 verbose 输出;他们还发现老模型上有效的 maximize_context_understanding 段落在 GPT-5 上适得其反(它本就爱主动搜集上下文,反而造成小任务过度调工具),于是去掉 maximize_ 前缀、软化"彻底性"措辞后,GPT-5 在"用内部知识还是伸手调工具"之间的决策更好了。
术语 tool preamble(工具前导消息,前置计划+持续进度更新); verbosity=low(全局设低再局部覆盖为高,平衡简洁与代码可读); maximize_context_understanding(Cursor 老 prompt 段,在 GPT-5 上需软化); metaprompting(元提示,让 GPT-5 优化它自己的 prompt)
📖 "GPT-5 is trained to provide clear upfront plans and consistent progress updates via “tool preamble” messages." — 原文
📖 "they set the verbosity API parameter to low to keep text outputs brief, and then modified the prompt to strongly encourage verbose outputs in coding tools only." — 原文
📖 "early testers have found great success using GPT-5 as a meta-prompter for itself." — 原文
🧪 实例 一段高质量的 tool preamble 引导 prompt:
text
<tool_preambles>
- Always begin by rephrasing the user's goal in a friendly, clear, and concise manner, before calling any tools.
- Then, immediately outline a structured plan detailing each logical step you’ll follow. - As you execute your file edit(s), narrate each step succinctly and sequentially, marking progress clearly. 
- Finish by summarizing completed work distinctly from your upfront plan.
</tool_preambles>
🔍 追问 为什么 Cursor 要"全局 verbosity 设 low、只在编码工具里设 high"而不是一刀切? → 因为状态更新和任务总结啰嗦会打断用户心流(需要 low),但代码 diff 太简洁(单字母变量)又难 review(需要 high);两个需求方向相反,只能靠"参数管全局、prompt 管局部"的组合分而治之。
📚 拓展阅读

第三部分 · OpenAI 工程博客系统深度剖析

第13章 · Codex 与 Agent Harness 工程

Q什么是 agent loop(智能体循环)?它一个 turn 内是怎么运转到终止的?深挖·拓展🔥高频
agent-loop codex harness
⏱️ 现行
agent loop 是 Codex CLI 的核心逻辑,负责编排 user、model 与 model 调用的 tools 三者之间的交互。它从 user 拿到 input,组织成发给 model 的 prompt,然后做一次 inference(把文本 prompt 先转成 input tokens,采样出 output tokens,再翻译回文本)。inference 的结果只有两种:要么 model 直接产出给用户的 final response,要么 model 请求一次 tool call(例如"run ls and report the output")。若是后者,agent 执行该 tool call 并把输出追加到原 prompt 上,据此生成新的 input 再次 query model——这样 model 就能把新信息纳入考量重试。这个过程反复进行,直到 model 不再发出 tool call 而是产出给用户的 assistant message 为止;由于软件 agent 的主要产出往往是它写/改的代码而非文字,每个 turn 都以 assistant message 收尾,作为 loop 的终止信号,控制权交回用户。关键权衡在于:单个 turn 内 inference 与 tool call 之间可以迭代很多次(甚至几百次 tool call),因此 prompt 会不断增长,这正是后续 context window 管理与 prompt caching 之所以重要的根源。
术语 agent loop(智能体循环,编排 user/model/tools 的核心逻辑); inference(推理,一次采样 model); tool call(工具调用,model 请求 agent 执行的动作); assistant message(助手消息,标志 turn 终止的输出); harness(承载 agent loop 的运行框架)
📖 "At the heart of every AI agent is something called “the agent loop.”" — Unrolling the Codex agent loop
📖 "In the case of (2), the agent executes the tool call and appends its output to the original prompt." — Unrolling the Codex agent loop
📖 "This process repeats until the model stops emitting tool calls and instead produces a message for the user (referred to as an _assistant message_ in OpenAI models)." — Unrolling the Codex agent loop
🧪 实例 一次典型迭代的伪流程:
flowchart LR
  U[User input] --> P[Build prompt]
  P --> I[Model inference]
  I -->|tool call| T[Execute tool e.g. shell cat README.md]
  T -->|append output| P
  I -->|assistant message| D[Return to user / turn ends]
🔍 追问 为什么说软件 agent 的"输出"不只是 assistant message? → 因为 agent 能执行修改本地环境的 tool call,很多情况下它的主要产出是在你机器上写/改的代码,assistant message(如"I added the architecture.md you asked for")只是标志这一轮工作完成的终止态。
📚 拓展阅读
QCodex "harness" 是什么?它和 Codex CLI、App Server 是什么关系?深挖·拓展🔥高频
harness app-server architecture
⏱️ 现行
在 OpenAI,"Codex"是一整套软件 agent 产品的统称,包含 Codex CLI、Codex Cloud 与 Codex VS Code 扩展等 surface。而 harness 特指提供核心 agent loop 与执行逻辑的那一层,它是所有 Codex 体验的底座,并通过 Codex CLI 对外暴露。web app、CLI、IDE 扩展、macOS app 这些不同表面,底层都由同一个 Codex harness 驱动。把它们连接起来的关键是 Codex App Server——一个对客户端友好、双向的 JSON-RPC API。App Server 既是 client 与 server 之间的 JSON-RPC 协议,也是一个长驻进程,托管着 Codex core threads。所有 agent 代码都活在 Codex CLI 代码库里叫"Codex core"的部分:它既是承载全部 agent 代码的 library,也是可被拉起来运行 agent loop、管理单个 thread 持久化的 runtime。这种分层的权衡在于:把 harness 抽成稳定协议后,VS Code、JetBrains、Xcode、桌面 app 乃至 web runtime 都能复用同一个 agent loop 而不必各自重新实现,代价是每种客户端要自己写 JSON-RPC 绑定。
术语 Codex harness(承载核心 agent loop 与执行逻辑的底座); Codex core(存放全部 agent 代码的 library + runtime); App Server(双向 JSON-RPC 协议 + 长驻进程); surface(Codex 的不同产品表面:CLI/IDE/web/app); JSON-RPC over stdio(客户端与 App Server 的传输方式)
📖 "Under the hood, they’re all powered by the same Codex harness—the agent loop and logic that underlies all Codex experiences." — Unlocking the Codex harness: how we built the App Server
📖 "The App Server is both the JSON-RPC protocol between the client and the server and a long-lived process that hosts the Codex core threads." — Unlocking the Codex harness: how we built the App Server
📖 "the transport is JSON-RPC over stdio (JSONL)." — Unlocking the Codex harness: how we built the App Server
🧪 实例 各客户端如何接同一个 harness:
flowchart TD
  subgraph Clients
    D[Desktop App]
    T[TUI/CLI]
    W[Web Runtime]
    J[JetBrains/VS Code/Xcode]
  end
  D & T & W & J -->|JSON-RPC over stdio| AS[App Server]
  AS --> TM[Thread Manager]
  TM --> C1[Core thread 1]
  TM --> C2[Core thread 2]
🔍 追问 App Server 最初为什么被造出来? → 当团队做 VS Code 扩展时需要复用同一个 harness 从 IDE UI 驱动同一个 agent loop 而不重写;他们先试过把 Codex 暴露成 MCP server 但维护 MCP 语义困难,于是引入了一个镜像 TUI loop 的 JSON-RPC 协议,成为 App Server 的非正式第一版。
📚 拓展阅读
QCodex 构造第一次 inference 的 initial prompt 时放了哪些东西?role 的作用是什么?深挖·拓展🔥高频
prompt responses-api roles
⏱️ 现行
用户 query Responses API 时并不逐字指定 prompt,而是给出各种 input 类型,由 Responses API server 决定如何把这些信息组织成 model 消费的 prompt——可以把 prompt 想成一个"list of items"。JSON payload 里关键的三个字段是:instructions(插入 model context 的 system/developer 消息)、tools(model 可调用的工具列表)、input(文本/图片/文件输入列表)。initial prompt 中每个 item 都关联一个 role,role 表示对应内容应有多大权重,按优先级递减依次为 systemdeveloperuserassistantinstructions 优先读 ~/.codex/config.toml 里的 model_instructions_file,否则用 model 关联的 base_instructions。在追加 user message 之前,Codex 还会往 input 里插入若干 item:一条 role=developer 消息描述 sandbox(仅对 Codex 自带的 shell tool 生效);可选的 developer_instructions;可选的、跨多来源聚合的 user instructions(含 AGENTS.md、skills 元数据等,越具体的越靠后);以及一条描述当前 cwd 和 shell 的 role=user 环境消息。值得注意的是,最终 prompt 里前三项的顺序由 server 而非 client 决定,且三者中只有 system message 的内容也由 server 掌控,toolsinstructions 由 client 决定。
术语 instructions(system/developer 消息字段); tools(工具定义列表,含 shell/update_plan/web_search/MCP); input(输入项列表); role(system>developer>user>assistant 的权重优先级); base_instructions(model 自带的默认指令); environment_context(描述 cwd 与 shell 的 user 消息)
📖 "In the initial prompt, every item in the list is associated with a role. The role indicates how much weight the associated content should have and is one of the following values (in decreasing order of priority): system, developer, user, assistant." — Unrolling the Codex agent loop
📖 "A message with role=developer that describes the sandbox that _applies only to the Codex-provided_ shell _tool_ defined in the tools section." — Unrolling the Codex agent loop
📖 "That is, other tools, such as those provided from MCP servers, are not sandboxed by Codex and are responsible for enforcing their own guardrails." — Unrolling the Codex agent loop
🧪 实例 input 里一条最简单的 user message item:
json
{
  "type": "message",
  "role": "user",
  "content": [
    {
      "type": "input_text",
      "text": "Add an architecture diagram to the README.md"
    }
  ]
}
🔍 追问 MCP server 提供的工具会被 Codex sandbox 吗? → 不会。sandbox 描述的 developer 消息只对 Codex 自带的 shell tool 生效,MCP 等其他工具不受 Codex sandbox 约束,需自行强制各自的 guardrails。
📚 拓展阅读
Q随着对话变长 prompt 会不断增大,Codex 靠什么把开销从二次方降回线性?深挖·拓展🔥高频
prompt-caching performance context-window
⏱️ 现行
每次 query 都要把整段 conversation history(历史消息与 tool call)作为新 turn 的 prompt 一并发出,所以 input 会持续增长——如果每轮都重新采样整段,agent loop 在发给 Responses API 的 JSON 量上就是二次方级的。Codex 的关键设计是:追加 tool 输出与新消息时,让旧 prompt 恰好是新 prompt 的 exact prefix(精确前缀),这是刻意为之,因为它能吃到 prompt caching 的红利。prompt caching 只对 prompt 内的精确前缀匹配命中,所以要把 instructions、examples 等静态内容放开头,把 user-specific 等可变内容放末尾,且 images 和 tools 在请求间也必须一致。命中缓存后就能复用上一轮 inference 的计算,采样成本变成线性而非二次方。由于采样 model 的成本通常压过网络流量成本,采样才是效率优化的主要目标,这就是 prompt caching 如此重要的原因。相应地,会造成 cache miss 的操作包括:中途改动可用 tools、切换目标 model(会改变含 model 专属指令的第三项)、改动 sandbox 配置/审批模式/cwd。因此 Codex 团队引新特性时必须谨慎——早期 MCP 支持就因未按一致顺序枚举工具而引发过 cache miss bug。
术语 prompt caching(前缀缓存,复用上一轮推理计算); exact prefix(精确前缀,旧 prompt 是新 prompt 前缀); cache miss(缓存未命中,改 tools/model/sandbox 等会触发); quadratic vs linear(命中缓存后采样从二次方降为线性)
📖 "In particular, note how the old prompt _is an exact prefix_ of the new prompt." — Unrolling the Codex agent loop
📖 "Cache hits are only possible for exact prefix matches within a prompt." — Unrolling the Codex agent loop
📖 "When we get cache hits, _sampling the model is linear rather than quadratic_." — Unrolling the Codex agent loop
🧪 实例 三类会打破前缀、造成 cache miss 的中途改动:
text
- Changing the tools available to the model in the middle of the conversation.
- Changing the model that is the target of the Responses API request.
- Changing the sandbox configuration, approval mode, or current working directory.
🔍 追问 当 sandbox 或 cwd 中途真的变了怎么办? → 尽量不改早先消息,而是往 input 末尾追加一条新消息来反映变化:sandbox/审批模式变化就插入一条格式同原 <permissions instructions> 的新 role=developer 消息,cwd 变化就插入一条格式同原 <environment_context> 的新 role=user 消息,从而保住前缀不变。
📚 拓展阅读
Qcontext window 快满了 Codex 怎么办?compaction 机制是怎么工作的?深挖·拓展中频
context-window compaction responses-api
⏱️ 现行
每个 model 都有 context window,即一次 inference 调用能用的最大 token 数,且这个窗口同时包含 input 和 output tokens。一个 turn 内 agent 可能做几百次 tool call,足以耗尽窗口,所以 context window management 是 agent 的重要职责之一。Codex 避免耗尽窗口的总体策略是 compact(压缩):一旦 token 数超过某阈值,就用一个更小、但能代表这段对话的新 items 列表替换掉 input,让 agent 在了解此前发生过什么的前提下继续。早期 compaction 需要用户手动敲 /compact,它会带上现有对话加自定义 summarization 指令去 query Responses API,把返回的 summary assistant message 当作后续 turn 的新 input。此后 Responses API 演进出专门的 /responses/compact 端点做更高效的压缩,返回一个可替换旧 input 的 items 列表来腾出窗口,其中含一个特殊的 type=compaction 项,带不透明的 encrypted_content,保留 model 对原始对话的潜在理解;现在 Codex 在超过 auto_compact_limit 时自动调用该端点。权衡在于:压缩牺牲部分细粒度历史,换取窗口不溢出、对话可持续。
术语 context window(一次推理可用的最大 token 数,含输入输出); compaction(压缩,用更小的代表性 items 替换 input); auto_compact_limit(触发自动压缩的阈值); type=compaction 项(带 encrypted_content 保留 model 潜在理解)
📖 "This length matters because every model has a _context window_, which is the maximum number of tokens it can use for one inference call." — Unrolling the Codex agent loop
📖 "Our general strategy to avoid running out of context window is to _compact_ the conversation once the number of tokens exceeds some threshold." — Unrolling the Codex agent loop
📖 "This list includes a special type=compaction item with an opaque encrypted_content item that preserves the model’s latent understanding of the original conversation." — Unrolling the Codex agent loop
🧪 实例 compaction 就是把长 input 替换成一个更小的代表性列表:
text
Specifically, we replace the input with a new, smaller list of items that is
representative of the conversation, enabling the agent to continue with an
understanding of what has happened thus far.
🔍 追问 为什么 context window 长度"同时"要算 output? → 因为 window 是一次 inference 调用可用的 token 总额,input 与 output tokens 共享这一上限,所以不断增长的 prompt 会挤占可生成的 output 空间,这正是需要 compaction 的原因。
📚 拓展阅读
QCodex 为什么不用 Responses API 的 previous_response_id?这和 ZDR 有什么关系?深挖·拓展中频
stateless zdr responses-api
⏱️ 现行
Responses API 提供了可选的 previous_response_id 参数来缓解每轮重发整段 JSON 的二次方问题,但 Codex 今天并不使用它,主要是为了让请求完全 stateless 并支持 Zero Data Retention(ZDR)配置。避免 previous_response_id 简化了 Responses API 提供方的实现,因为它保证每个请求都是无状态的;这也让支持已选择 ZDR 的客户变得直接——因为为支持 previous_response_id 而存储所需数据本身就与 ZDR 相冲突。值得注意的权衡是:ZDR 客户并不会因此丧失利用前几轮专有 reasoning 消息的能力,因为对应的 encrypted_content 可以在 server 端被解密(OpenAI 持久化 ZDR 客户的解密密钥,但不持久化其数据)。换句话说,Codex 用"每轮重发完整、无状态 payload + 服务端可解密的 encrypted_content"这套组合,既拿到了无状态与 ZDR 兼容性,又靠 prompt caching 把重发带来的采样成本压回线性,从而在隐私合规与性能之间取得平衡。
术语 previous_response_id(可减小重发的可选参数,Codex 不用); stateless(无状态,每个请求自包含); ZDR (Zero Data Retention)(零数据保留); encrypted_content(加密的 reasoning 内容,服务端可解密)
📖 "Codex does not use it today, primarily to keep requests fully stateless and to support Zero Data Retention (ZDR) configurations." — Unrolling the Codex agent loop
📖 "Avoiding previous_response_id simplifies things for the provider of the Responses API because it ensures that every request is _stateless_." — Unrolling the Codex agent loop
📖 "Note that ZDR customers do not sacrifice the ability to benefit from proprietary reasoning messages from prior turns, as the associated encrypted_content can be decrypted on the server." — Unrolling the Codex agent loop
🧪 实例 后续 query 时,前一轮的 reasoning 项就带着 encrypted_content 一起重新塞回 input
json
{
  "type": "reasoning",
  "summary": [
    "type": "summary_text",
    "text": "**Adding an architecture diagram for README.md**\n\nI need to..."
  ],
  "encrypted_content": "gAAAAABpaDWNMxMeLw..."
}
🔍 追问 无状态方案代价是什么,Codex 又如何补偿? → 代价是每轮都要重发不断增长的完整 payload,理论上是二次方开销;Codex 靠"旧 prompt 是新 prompt 精确前缀"来命中 prompt caching,使采样成本降回线性来补偿。
📚 拓展阅读
QApp Server 协议里的三个会话原语 item / turn / thread 各是什么?为什么这样切分?深挖·拓展🔥高频
app-server primitives json-rpc
⏱️ 现行
为 agent loop 设计 API 很棘手,因为 user/agent 交互不是简单的 request/response——一次用户请求会展开成一串结构化动作:用户输入、agent 的增量进展、沿途产出的 artifacts(如 diff),客户端都要如实呈现。为让这个交互流易于集成且跨 UI 稳健,Codex 落定了三个边界清晰、生命周期明确的核心原语。Item 是 Codex 中 input/output 的原子单元,是带类型的(user message、agent message、tool execution、approval request、diff 等),每个 item 有显式生命周期:item/started 开始、可选的 item/*/delta 流式内容、item/completed 终态——这让客户端能在 started 立刻开始渲染、在 delta 增量更新、在 completed 定稿。Turn 是由用户输入发起的一个 agent 工作单元,从 client 提交输入(如"run tests and summarize failures")开始,到 agent 产完该输入的输出结束,一个 turn 内含一串代表中间步骤与产出的 items。Thread 则是一次持续 Codex 会话的持久容器,含多个 turn,可被创建、恢复、fork、归档,历史被持久化以便客户端重连并渲染一致的时间线。这套 item/turn/thread 分层,把"流式、可中断、可重连"的 agent 交互变成一组稳定、UI-ready 的 JSON-RPC 通知。
术语 item(输入输出原子单元,带 started/delta/completed 生命周期); turn(由用户输入发起的一个 agent 工作单元); thread(持久的会话容器,含多个 turn,可 resume/fork/archive); initialize handshake(任何方法前必须先发的握手请求); bidirectional JSON-RPC(server 也能主动发请求,如审批)
📖 "An item is the atomic unit of input/output in Codex." — Unlocking the Codex harness: how we built the App Server
📖 "A turn is one unit of agent work initiated by user input." — Unlocking the Codex harness: how we built the App Server
📖 "A thread is the durable container for an ongoing Codex session between a user and an agent." — Unlocking the Codex harness: how we built the App Server
🧪 实例 一个带审批的 turn 的事件时间线:
sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: thread/start + turn/start
  S-->>C: thread/started, turn/started
  S-->>C: item/started (tool execution)
  S->>C: item/commandExecution/requestApproval ("run tests")
  C->>S: approval (allow)
  S-->>C: item/completed (pnpm test)
  S-->>C: item/started + agentMessage/delta + item/completed
  S-->>C: turn/completed
🔍 追问 协议为什么必须是双向的? → 因为 server 在 agent 需要输入(如一次 approval)时要能主动发起请求并暂停该 turn,直到 client 回复 allow 或 deny,普通单向 request/response 无法表达这种"服务端反向征询"。
📚 拓展阅读
Q"Harness engineering"(把 harness 当工程对象)的核心理念是什么?为什么 AGENTS.md 要当目录而非百科?深挖·拓展中频
harness-engineering agents-md context
⏱️ 现行
这套理念来自一个实验:过去五个月里团队用 0 行手写代码构建并发布了一个内部 beta 产品——应用逻辑、测试、CI 配置、文档、可观测性、内部工具在内的每一行代码都由 Codex 写就,核心哲学是"Humans steer. Agents execute."(人类掌舵,agent 执行)。当工程团队主要工作不再是写代码,而是设计环境、明确 intent、搭建反馈回路时,context 管理成了让 agent 在大而复杂任务上有效的最大挑战之一。最早学到的一课很朴素:给 Codex 一张地图,而不是一本一千页的说明书。他们试过"一个大 AGENTS.md"的做法,但它以可预期的方式失败——context 是稀缺资源,巨型指令文件会挤掉任务、代码和相关文档;当什么都"重要"时就等于什么都不重要;它会立刻腐烂,且难以机械校验。于是他们不再把 AGENTS.md 当百科,而是当作"目录"(table of contents):一份约 100 行的短 AGENTS.md 被注入 context 充当地图,指向 docs/ 这个作为 system of record 的结构化知识库。这实现了 progressive disclosure——agent 从一个小而稳定的入口开始,被教导下一步去哪查,而不是一开始就被淹没。并用 linter 和 CI 机械强制知识库保持最新、交叉链接、结构正确。
术语 Humans steer. Agents execute.(人类掌舵、agent 执行的核心哲学); AGENTS.md as table of contents(把 AGENTS.md 当目录而非百科); progressive disclosure(渐进披露,从小入口开始按需下钻); system of record(以 docs/ 结构化知识库为事实来源); no manually-written code(不手写代码的团队哲学)
📖 "Humans steer. Agents execute." — Harness engineering: leveraging Codex in an agent-first world
📖 "So instead of treating AGENTS.md as the encyclopedia, we treat it as the table of contents." — Harness engineering: leveraging Codex in an agent-first world
📖 "This enables progressive disclosure: agents start with a small, stable entry point and are taught where to look next, rather than being overwhelmed up front." — Harness engineering: leveraging Codex in an agent-first world
🧪 实例 知识库以短 AGENTS.md 作目录、docs/ 作事实来源的布局:
text
AGENTS.md
ARCHITECTURE.md
docs/
├── design-docs/
├── exec-plans/
│   ├── active/
│   ├── completed/
│   └── tech-debt-tracker.md
├── product-specs/
└── references/
🔍 追问 "一个大 AGENTS.md"具体错在哪? → context 是稀缺资源,巨型文件会挤掉任务/代码/文档使 agent 漏掉约束或优化错目标;过多指导等于没有指导;它会立刻腐烂成过时规则的坟场;且单一 blob 难以做覆盖率/新鲜度/交叉链接等机械校验,drift 不可避免。
📚 拓展阅读
  • AGENTS.md — 指导 agent 如何在仓库工作的文件规范
  • execution plans — 复杂工作被捕获为带进度与决策日志的执行计划
  • Ralph Wiggum Loop — 让 Codex 自审、迭代直到所有 agent reviewer 满意的循环
Q在完全由 agent 生成的代码库里,如何靠"强制不变量"而非微管理保持架构一致、并让 agent 可读?深挖·拓展低频
invariants architecture legibility
⏱️ 现行
光靠文档留不住一个全 agent 生成的代码库的连贯性。团队的做法是强制 invariants(不变量)而非微管理实现,从而让 agent 快速交付又不动摇根基。因为 agent 在有严格边界与可预测结构的环境里最有效,他们把应用建在一个刚性架构模型上:每个业务域被划成固定层,层间依赖方向被严格校验、只允许有限的边。规则是——在每个业务域内代码只能"向前"穿过一组固定的层(Types → Config → Repo → Service → Runtime → UI),auth/connectors/telemetry/feature flags 等横切关注只能通过单一显式接口 Providers 进入,其余一律禁止并机械强制。这些约束通过自定义 linter(当然也是 Codex 生成的)和结构化测试来落地;因为 lint 是自定义的,他们把补救指令写进错误消息,直接注入 agent context。另一条底层原则是 agent legibility:从 agent 视角看,它运行时在 context 里访问不到的东西就等于不存在——活在 Google Docs、聊天记录或人脑里的知识对系统不可见,只有仓库本地、版本化的 artifacts(代码、markdown、schema、可执行计划)它才看得见。所以要持续把 context 推进仓库。权衡在于:这种通常要到几百个工程师才上的架构,在 agent 时代成了早期前提——约束正是"不腐化地提速"的前提。
术语 invariants(不变量,强制边界而非微管理实现); layered domain architecture(Types→Config→Repo→Service→Runtime→UI 的单向分层); Providers(横切关注进入的单一显式接口); agent legibility(agent 可读性,仓库外的知识对 agent 不存在); custom linters(Codex 生成的自定义 lint,错误消息注入补救指令)
📖 "By enforcing invariants, not micromanaging implementations, we let agents ship fast without undermining the foundation." — Harness engineering: leveraging Codex in an agent-first world
📖 "code can only depend “forward” through a fixed set of layers (Types → Config → Repo → Service → Runtime → UI)." — Harness engineering: leveraging Codex in an agent-first world
📖 "From the agent’s point of view, anything it can’t access in-context while running effectively doesn’t exist." — Harness engineering: leveraging Codex in an agent-first world
🧪 实例 用自定义 lint 与结构测试机械强制的一类"taste invariants":
text
statically enforce structured logging, naming conventions for schemas and types,
file size limits, and platform-specific reliability requirements with custom lints.
Because the lints are custom, we write the error messages to inject remediation
instructions into agent context.
🔍 追问 为什么把架构约束前置、甚至宁可让 agent 自己重实现子集也不引入不透明依赖? → 因为约束是 agent 不腐化提速的前提,而"boring"技术因可组合、API 稳定、在训练集中有表示更易被 agent 建模;对不透明上游库,让 agent 重实现子集有时比绕过其行为更划算,例如自造 map-with-concurrency helper,紧密集成 OpenTelemetry、100% 测试覆盖、行为完全符合 runtime 预期。
📚 拓展阅读
中频

Symphony:Codex 多智能体编排规范

An open-source spec for Codex orchestration: Symphony
QSymphony 到底是什么?它把 issue tracker 变成"control plane"的核心思路是什么?深挖·拓展🔥高频
Symphony agent-orchestrator control-plane
⏱️ 现行
Symphony 是一个 agent orchestrator(编排器),它把像 Linear 这样的项目管理看板变成协调 coding agent 的控制面(control plane):每一个 open 状态的任务都会被分配一个 agent,agent 持续运行,人类只负责 review 结果。它的设计动机是把"工作"从"session/PR"这两个中间产物里解耦出来——OpenAI 团队意识到他们一直在围绕 coding session 和 merged PR 做优化,而 PR 和 session 其实只是达成交付物(issue、task、ticket、milestone)的手段。于是他们不再直接监督 agent,而是让 agent 自己从 task tracker 拉取工作,把 issue tracker 本身当作状态机(state machine)。在这个模型里,每个 open 的 Linear issue 映射到一个专属的 agent workspace,Symphony 持续 watch 看板,保证每个 active 任务都有 agent 在循环中运行直到完成;agent crash 或 stall 就重启,出现新工作就自动接管。这套抽象让 ticket 可以承载远大于单个 PR 的工作单元——有的 issue 产出跨 repo 的多个 PR,有的则是纯调研/分析、根本不碰代码。它带来的直接权衡是:降低了发起模糊工作的认知成本(agent 做错也是有用的信息,成本近乎为零),但代价是失去了交互式实时纠偏的能力。

flowchart LR
    A[Linear Issue Board
control plane] -->|poll active issues| B[Symphony Orchestrator] B -->|per-issue| C[Agent Workspace] C --> D[Codex agent runs in loop] D -->|crash/stall| B D -->|PR / review packet| E[Human Review]
术语 agent orchestrator(编排器,协调多个 coding agent 的服务); control plane(控制面,这里指用看板统一调度 agent); Linear(团队用的 issue tracker,被当作 state machine); workspace(每个 issue 专属的隔离工作目录)
📖 "Every open task gets an agent, agents run continuously, and humans review the results." — An open-source spec for Codex orchestration: Symphony
📖 "For every open task, guarantee that an agent is running in its own workspace." — An open-source spec for Codex orchestration: Symphony
🧪 实例 团队会先建一个任务让 agent 分析 codebase、Slack 或 Notion 并产出实现计划;满意后 agent 会生成一棵任务树,把工作拆成阶段并定义依赖。例如把 React 升级标记为 blocked on 迁移到 Vite,于是 agent 会等 Vite 迁移完成后才开始升级 React——execution 沿着这个 DAG 自然并行展开。甚至有工程师在信号很差的小木屋里,用手机上的 Linear app 提交了三个重要改动,因为 orchestrator 跑在 devbox 上、永不睡眠。
🔍 追问 Symphony 是否负责修改 ticket(状态流转、评论、PR 链接)? → 不是。Symphony 是 scheduler/runner 和 tracker reader;ticket 的写入通常由 coding agent 用 workflow/runtime 里的工具完成,一次成功的运行可能停在 workflow 定义的 handoff 状态(如 Human Review),不一定是 Done
📚 拓展阅读
Q为什么交互式 coding agent 会遇到"天花板"?Symphony 要解决的真正瓶颈是什么?深挖·拓展🔥高频
context-switching human-attention bottleneck
⏱️ 现行
交互式 coding agent(无论是 web app 还是 CLI)本质上仍然是交互式工具,需要人不断打开 session、分配任务、review 输出、steer agent、再重复。随着 agentic 工作规模上升,OpenAI 团队发现真正的瓶颈不是 agent 速度,而是人类注意力:实践中大多数人在 context switching 变得痛苦之前,最多只能舒服地同时管理 3 到 5 个 session,超过这个数产出就下降——会忘记哪个 session 在干什么、在多个终端间跳来跳去把 agent 拉回正轨、还要 debug 卡在半路的长任务。他们形象地说,这相当于"造了一支极其能干的 junior engineer 队伍,却又派人类工程师去微观管理他们",这显然无法 scale。Symphony 的洞察是:与其把系统围绕 session 组织,不如围绕 deliverable(issue/ticket)组织,让 agent 自己拉活,从而把人从"micromanaging"里解放出来去做需要判断力和专业性的、最有趣的难题。权衡在于:Symphony 能处理大量常规实现工作,但对于模糊、需要强判断的问题,仍然需要工程师直接用交互式 Codex session。
术语 context switching(上下文切换,人类在多 session 间跳转的成本); human attention(人类注意力,被识别为真正的系统瓶颈); micromanaging(微观管理,团队想摆脱的低价值劳动)
📖 "In practice, most people could comfortably manage three to five sessions at a time before context switching became painful." — An open-source spec for Codex orchestration: Symphony
📖 "The agents were fast, but we had a system bottleneck: human attention." — An open-source spec for Codex orchestration: Symphony
📖 "We had effectively built a team of extremely capable junior engineers, then assigned our human engineers to micromanaging them." — An open-source spec for Codex orchestration: Symphony
🧪 实例 采用 Symphony 后,OpenAI 内部一些团队在头三周里 landed PR 数量增长了 500%。更深层的变化是经济学:当工程师不再花时间监督 Codex session,每次改动的感知成本下降,于是"起个投机性任务"变得 trivial——试个想法、探索一次 refactor、验证一个假设,只保留看起来有希望的结果。PM 和设计师也能直接往 Symphony 里提 feature request,拿回包含真实产品内功能演示视频的 review packet。
🔍 追问 是不是所有任务都适合 Symphony 风格? → 不是。有些问题——尤其是模糊的、需要强判断和专业知识的——仍需工程师与交互式 Codex session 直接协作,而这些通常正是工程师最愿意投入时间的、最有趣的任务。
📚 拓展阅读
QSymphony 的编排状态机是怎样的?并发控制与重试退避如何工作?深挖·拓展🔥高频
state-machine concurrency retry-backoff reconciliation
⏱️ 现行
Orchestrator 是唯一能修改调度状态的组件,所有 worker 结果都汇报回它并转换成显式的状态迁移,以此串行化状态变更、避免重复 dispatch。它维护的是 service 内部的 claim 状态(区别于 tracker 的 Todo/In Progress 等状态),共 5 种:Unclaimed(未运行也无重试)、Claimed(已预留防止重复派发,实际上要么 Running 要么 RetryQueued)、Running(worker 存在且在 running map 中)、RetryQueued(有 retry timer)、Released(因 issue 终态/非活跃/缺失或重试路径未再派发而释放 claim)。每个 poll tick 的顺序是:先 reconcile 运行中的 issue → dispatch 前置校验 → 用 active states 拉候选 issue → 按优先级排序 → 在有空槽时派发。并发上用全局限额 available_slots = max(max_concurrent_agents - running_count, 0),并可叠加 per-state 限额(max_concurrent_agents_by_state,无则回退全局)与可选的 per-SSH-host 限额。重试有两种退避:worker 干净退出后的正常"continuation retry"用固定约 1000 ms 短延迟(让它重新检查 issue 是否仍 active、是否要再开一个 worker session);失败驱动的重试则用指数退避 delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms),上限默认 300000(5 分钟)。关键设计权衡是:不需要持久化数据库,restart recovery 完全由 tracker 与 filesystem 驱动。
术语 Claimed / Running / RetryQueued / Released(orchestrator 内部 claim 状态); reconciliation(对账,每 tick 先跑,停掉 issue 已终态或不再 active 的 run); stall_timeout_ms(卡死超时,基于事件不活跃时长终止 worker); exponential backoff(指数退避,失败重试的延迟公式)
📖 "The orchestrator is the only component that mutates scheduling state." — An open-source spec for Codex orchestration: Symphony
📖 "Failure-driven retries use delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms)." — An open-source spec for Codex orchestration: Symphony
🧪 实例 一个 worker 正常退出并不代表 issue 永久 done——worker 可能在退出前连续跑多个 back-to-back 的 coding-agent turn(首个 turn 用完整渲染的 task prompt,后续 continuation turn 只在同一 live thread 上发送 continuation guidance,不重发原 prompt),上限是 agent.max_turns。派发资格(candidate eligibility)要求 issue 同时满足:有 id/identifier/title/state、state 在 active_states 且不在 terminal_states、不在 running/claimed、有全局与 per-state 空槽;且若 state 为 Todo,任一 blocker 非终态时不派发。排序规则:
text
1. priority 升序(1..4 优先,null 排最后)
2. created_at 最旧优先
3. identifier 字典序 tie-breaker
🔍 追问 Reconciliation 每 tick 做哪两件事? → Part A 卡死检测:对每个 running issue 计算自 last_codex_timestamp(无事件则 started_at)以来的 elapsed_ms,超过 codex.stall_timeout_ms 就终止 worker 并排队重试;Part B tracker 状态刷新:拉所有 running issue 的当前状态,终态则终止并清 workspace,仍 active 则更新内存快照,既非 active 也非终态则终止但不清 workspace。
📚 拓展阅读
  • SPEC.md — 第 7、8 节详述状态机、轮询、调度与对账
  • openai/symphony — 参考实现(Elixir)如何落地并发监督
QWORKFLOW.md 是什么角色?Symphony 的 workspace 安全不变量(safety invariants)有哪些?深挖·拓展中频
WORKFLOW.md front-matter safety-invariants workspace
⏱️ 现行
Symphony 把工作流策略保留在仓库内的 WORKFLOW.md 里,团队用它把 agent prompt 和 runtime 设置与代码一起版本化。这个文件是带可选 YAML front matter 的 Markdown:若以 --- 开头,解析到下一个 --- 之间为 front matter(必须解码成 map),其余为 prompt body(trim 后作为 per-issue prompt template,用严格模板引擎、未知变量/filter 必须报错)。front matter 顶层键有 tracker/polling/workspace/hooks/agent/codex,未知键为向前兼容应被忽略。这样设计的好处是 WORKFLOW.md 足够自包含,无需额外的 service-specific 配置就能描述并运行不同 workflow,还支持 dynamic reload——watch 文件变化后重读并把新的 polling cadence、concurrency、状态、codex 设置等应用到未来的 dispatch(不要求重启在飞的 session)。安全方面有三条最重要的可移植性不变量:Invariant 1 只在 per-issue workspace 路径里跑 coding agent(启动子进程前校验 cwd == workspace_path);Invariant 2 workspace 路径必须留在 workspace root 内(两端归一化为绝对路径,要求前缀匹配,拒绝越界);Invariant 3 workspace key 必须 sanitize(目录名只允许 [A-Za-z0-9._-],其余字符替换为 _)。
术语 WORKFLOW.md(仓库自有的工作流合约,含 front matter 与 prompt body); front matter(YAML 头,声明 tracker/polling/workspace/hooks/agent/codex); dynamic reload(热重载,改动无需重启即生效于后续运行); safety invariants(安全不变量,保证 agent 只在隔离 workspace 内执行)
📖 "Invariant 1: Run the coding agent only in the per-issue workspace path." — An open-source spec for Codex orchestration: Symphony
📖 "is now captured in a simple WORKFLOW.md file." — An open-source spec for Codex orchestration: Symphony
🧪 实例 workspace hooks 有四个生命周期钩子,失败语义不同:
yaml
hooks:
  after_create: |   # 仅在 workspace 目录新建时运行;失败/超时对创建是致命的
    git clone ...
  before_run: |     # 每次 agent attempt 前运行;失败/超时对本次 attempt 致命
    npm ci
  after_run: |      # 每次 attempt 后运行(成功/失败/超时/取消);失败被记录但忽略
    ./collect-logs.sh
  before_remove: |  # workspace 删除前运行;失败被记录但忽略,清理照常进行
    ./archive.sh
  timeout_ms: 60000 # 所有 hook 的超时,默认 60000

per-issue workspace 路径是 <workspace.root>/<sanitized_issue_identifier>,且成功运行后 workspace 会被有意保留、跨运行复用,不自动删除。
🔍 追问 如果 WORKFLOW.md 读取或 YAML 解析出错,与 prompt 模板渲染出错,处理有何不同? → 前者是 config/validation 错误,会阻塞新的 dispatch 直到修复(不会静默回退到某个默认 prompt);后者只让受影响的那一次 run attempt 失败,orchestrator 按普通 worker 失败决定重试。
📚 拓展阅读
  • SPEC.md — 第 5、9 节详述 WORKFLOW.md 合约与 workspace 安全
  • openai/symphony — 仓库内可看到示例 WORKFLOW.md
QSymphony 如何集成 Codex?为什么用 Codex App Server 而不是 CLI/tmux?握手协议长什么样?深挖·拓展中频
codex-app-server JSON-RPC agent-runner stdio
⏱️ 现行
Symphony 用 Codex 的 app server mode——一个内置的 headless 模式,允许通过一套文档完备的 JSON-RPC API 以编程方式驱动 Codex(比如 start a thread、react to turns),比试图通过 CLI 或 live tmux session 交互更方便、更可扩展。Agent Runner 的职责是包住 workspace + prompt + app-server client:创建/复用 workspace、用 workflow template 构建 prompt、启动 app-server session、把 app-server 事件转发给 orchestrator,出错则让本次 worker attempt 失败(由 orchestrator 决定重试)。启动契约是用 bash -lc <codex.command>(默认 codex app-server)在 workspace 目录里拉起子进程,stdout 上按行分隔(line-delimited)传 JSON-RPC-like 消息,stderr 单独一路、不参与协议解析。握手必须按顺序发四条消息:initialize 请求(带 clientInfo/capabilities,等响应,超时用 read_timeout_ms)→ initialized 通知 → thread/start 请求(带 approvalPolicy/sandbox/cwd)→ turn/start 请求(带 threadId/input/cwd/title/approvalPolicy/sandboxPolicy)。会话标识从 thread/startresult.thread.id 取 thread_id、从每个 turn/startresult.turn.id 取 turn_id,组成 session_id,且一个 worker run 内所有 continuation turn 复用同一 thread_id。这样设计的好处是既复用了 Codex 提供的 harness,又保留了插入自己 knobs 与 hooks 的能力。
术语 Codex App Server(Codex 的内置 headless 模式,提供 JSON-RPC API); app-server client(Symphony 侧驱动子进程的客户端); thread/start turn/start(建立线程与启动一轮的协议方法); session_id(由 thread_id 与 turn_id 组合而成的会话标识)
📖 "This mode allowed us to run Codex and talk to it programmatically via a well documented JSON-RPC API for things like starting a thread or reacting to turns." — An open-source spec for Codex orchestration: Symphony
📖 "Compose from coding-agent thread_id and turn_id as <thread_id>-<turn_id>." — An open-source spec for Codex orchestration: Symphony
🧪 实例 规范给出的示意启动 transcript(语义等价的 payload 形状即可):
json
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"symphony","version":"1.0"},"capabilities":{}}}
{"method":"initialized","params":{}}
{"id":2,"method":"thread/start","params":{"approvalPolicy":"<implementation-defined>","sandbox":"<implementation-defined>","cwd":"/abs/workspace"}}
{"id":3,"method":"turn/start","params":{"threadId":"<thread-id>","input":[{"type":"text","text":"<rendered prompt-or-continuation-guidance>"}],"cwd":"/abs/workspace","title":"ABC-123: Example","approvalPolicy":"<implementation-defined>","sandboxPolicy":{"type":"<implementation-defined>"}}}

Turn 的完成条件包括 turn/completed→成功、turn/failed/turn/cancelled→失败、turn timeout→失败、子进程退出→失败。
🔍 追问 遇到 agent 请求"用户输入"该怎么处理? → 在高信任示例策略里被当作硬失败:如果 agent 请求 user input(通过显式方法 item/tool/requestUserInput 或指示需要输入的 turn 方法/标志检测到),立即让本次 run attempt 失败,以免 run 无限期 stall。
📚 拓展阅读
QSymphony 的整体架构与抽象分层是怎样的?为什么它"本质上只是一个 SPEC.md"?深挖·拓展中频
architecture abstraction-layers SPEC.md components
⏱️ 现行
打开 Symphony 仓库你会发现它技术上就是一个 SPEC.md 文件——对问题与预期解法的定义。团队没有去造一套复杂的监督系统,而是定义问题与意图解法,给 agent 高层次的 steering。规范把系统拆成 8 个主组件:Workflow Loader(读 WORKFLOW.md、解析 YAML 与 prompt body)、Config Layer(typed getters、默认值与环境变量间接、派发前校验)、Issue Tracker Client(拉候选/终态/指定 ID 的 issue 并归一化)、Orchestrator(拥有 poll tick 与内存 runtime state,决定 dispatch/retry/stop/release)、Workspace Manager(issue→路径映射、目录生命周期、hook)、Agent Runner(建 workspace、拼 prompt、拉起 app-server client、回传更新)、可选的 Status Surface、以及 Logging。为便于移植,它还组织成 6 个抽象层:Policy Layer(repo 定义的 WORKFLOW.md prompt 与团队规则)、Configuration Layer(typed getters)、Coordination Layer(orchestrator 的轮询/资格/并发/重试/对账)、Execution Layer(workspace + agent 子进程)、Integration Layer(Linear adapter)、Observability Layer(日志 + 可选状态面)。这种"spec 优先"的权衡在于:它刻意保持成一个最小编排层、不作为独立产品维护,而是当作 reference implementation,鼓励你把自己喜欢的 coding agent 指向 spec 去实现自己的版本。
术语 SPEC.md(定义问题与解法的规范文件,即 Symphony 本体); Orchestrator(唯一修改调度状态的协调组件); abstraction layers(六层抽象,便于跨语言移植); reference implementation(参考实现,而非要维护的产品)
📖 "Symphony is technically just a SPEC.md file—a definition of the problem and the intended solution." — An open-source spec for Codex orchestration: Symphony
📖 "Once the basic functionality existed, we used Symphony to build Symphony." — An open-source spec for Codex orchestration: Symphony
🧪 实例 参考实现用 Elixir 写,因为当代码近乎免费时可以纯粹按语言强项来选——Elixir 在编排与监督并发进程上有出色的原语。Codex 一次就(in one shot)建成了 Elixir 实现,团队随后持续迭代 spec 与实现;为打磨 spec,他们甚至让 Codex 用 TypeScript、Go、Rust、Java、Python 多种语言实现同一份 spec,用结果来发现歧义、简化系统——每种语言都成功了。这个过程还帮他们移除了大量偶发复杂度(如对特定仓库或 Linear MCP 的依赖)。
🔍 追问 Symphony 需要持久化数据库吗? → 不需要。规范的一个显式目标就是"支持 restart recovery 而无需持久数据库",restart recovery 由 tracker 与 filesystem 驱动,startup 时的 terminal cleanup 会移除已处于终态 issue 的陈旧 workspace。
📚 拓展阅读
  • openai/symphony — 仓库首页即 SPEC.md,可见 15K+ GitHub stars
  • SPEC.md — 第 3 节 System Overview 与抽象分层
Q从"严格状态机节点"到"给 agent 目标",Symphony 团队学到了什么教训?深挖·拓展中频
objectives lessons-learned guardrails tooling
⏱️ 现行
团队最重要的教训之一是:把 agent 当作状态机里僵硬的节点效果并不好。模型会越来越聪明、能解决比你给它套的盒子更大的问题。早期的 agentic 工作只让 Codex 实现任务,这太局限——Codex 完全有能力创建多个 PR、读 review 反馈并加以处理。于是他们给它工具(gh CLI、读 CI 日志的 skill 等),现在能让 Codex 做更多事,比如关闭旧 PR、拉取"已完成 vs 已放弃"工作的报告,这些都远超最初的 feature 实现盒子。因此他们最终转向给 agent objectives(目标) 而非严格的 transition,就像好的 manager 给下属分配 goal 一样——模型的力量来自推理能力,所以给它工具和上下文、让它去发挥。这也回应了另一个权衡:从交互式实时 steering 转到 ticket 级派活后,团队失去了中途不断 nudge 和纠偏的能力,有时 agent 会产出完全跑偏的东西——但这反而有用,暴露了系统的缺口。他们的应对不是手工打补丁修结果,而是加 guardrail 和 skill 让 agent 下次能成功,久而久之给 harness 加了跑 e2e 测试、通过 Chrome DevTools 驱动 app、管理 QA smoke test 等新能力,并大幅改进文档、明确"好"的标准。
术语 objectives(目标,取代严格状态转移的派活方式); guardrails(守卫,把失败转化为下次能成功的机制); skills(赋予 agent 的能力,如读 CI 日志); harness(agent 运行环境,持续增加新能力)
📖 "So we eventually moved toward giving agents _objectives_ instead of strict transitions, much like a good manager would assign a goal to a direct report on their team." — An open-source spec for Codex orchestration: Symphony
📖 "The power of models comes from their ability to reason, so give them tools and context and let them cook." — An open-source spec for Codex orchestration: Symphony
📖 "Codex is perfectly capable of creating multiple PRs as well as reading review feedback and addressing it." — An open-source spec for Codex orchestration: Symphony
🧪 实例 agent 不仅被动执行,还能自己创造工作——在实现或 review 过程中,它们常发现落在当前任务范围外的改进(性能问题、重构机会、更好的架构),这时就直接 file 一个新 issue 供团队评估和排期,其中许多后续任务也会被 agent 接手。这正体现了"给目标而非死板转移"的理念:agent 在被 oversee 的同时保持组织性、推动工作前进。
🔍 追问 Symphony 如何帮助 PR 在大型 monorepo 里"最后一公里"落地? → 系统会 watch CI、必要时 rebase、解决冲突、重试 flaky check,总体上把改动护送过 pipeline;等 ticket 到达 Merging 时,团队对该改动能进主分支、无需人工 babysitting 有很高信心。
📚 拓展阅读
QSymphony 用什么机制避免把 Linear access token 暴露给子 agent?linear_graphql 工具如何工作?深挖·拓展低频
dynamic-tool-calls linear_graphql token-safety no-MCP
⏱️ 现行
为了避免把 Linear access token 暴露给 subagent,Symphony 利用 Codex App Server 的 dynamic tool calls 暴露一个原始的 linear_graphql 函数,让它用 Symphony 当前 session 已配置的 tracker auth 对 Linear 执行任意 GraphQL 请求,从而既不依赖 MCP、也不把 access token 暴露给容器。这是一个可选的 client-side 工具扩展:只在 tracker.kind == "linear" 且配置了有效 Linear auth 时才有意义;若实现了,启动时应用目标 Codex app-server 版本支持的协议机制把它 advertise 给 session,不支持的工具名仍应返回失败结果并继续 session。它的输入契约是 query(必须是非空字符串、且恰好包含一个 GraphQL operation,多 operation 应作为无效输入拒绝)加可选的 variables(存在时必须是 JSON object);每次工具调用执行一个 GraphQL operation,并复用 workflow/runtime 里配置的 Linear endpoint 与 auth,不要求 coding agent 从磁盘读原始 token。这与 Symphony"scheduler/runner + tracker reader"的边界一致——即便实现了 linear_graphql,它仍属于 agent 工具链而非 orchestrator 业务逻辑。
术语 dynamic tool calls(Codex App Server 的动态工具调用机制,实验性); linear_graphql(暴露给 session 的 client-side 工具,代跑 Linear GraphQL); client-side tool(由 runtime 提供并 advertise 给 app-server 的工具); tracker auth(Symphony 侧配置的 Linear 鉴权,不下发给容器)
📖 "to expose the raw linear_graphql function that executes arbitrary requests against Linear, without relying on MCP or exposing the access token to containers." — An open-source spec for Codex orchestration: Symphony
📖 "Reuse the configured Linear endpoint and auth from the active Symphony workflow/runtime config; do" — An open-source spec for Codex orchestration: Symphony
🧪 实例 linear_graphql 首选输入形状与工具结果语义:
json
{
  "query": "single GraphQL query or mutation document",
  "variables": {
    "optional": "graphql variables object"
  }
}

结果语义:transport 成功且无 top-level GraphQL errorssuccess=true;出现 top-level errorssuccess=false 但保留 GraphQL 响应体供调试;无效输入/缺失 auth/transport 失败 → success=false 附错误 payload。
🔍 追问 如果 agent 请求了一个不受支持的 dynamic tool call(item/tool/call),会怎样? → 返回一个 tool failure 响应并继续 session,以防 session 卡在不支持的工具执行路径上。
📚 拓展阅读
QTax AI 的"自进化 Agent"由哪三根支柱组成?为什么这个闭环比纯人工迭代更快?深挖·拓展🔥高频
self-improving-agent eval-loop codex
⏱️ 现行
Thrive Holdings 与 OpenAI 把系统围绕三根支柱设计:一是 贴近从业者——真正做税务的会计师来决定产品要学什么,他们的直觉揭示"哪些错误才重要";二是 让生产环境产生证据——产品不只记录输入输出,而要保留从源文件、到抽取字段与出处(provenance)、再到下游申报与专家更正的完整路径;三是 Codex 驱动的改进闭环——一旦生产问题被结构化,就能变成 findings、定制 eval 和有边界的工程任务,交给 Codex 去调查、提改动、跑定向和回归 eval 并推动产品。传统做法里,一个失败要靠工程师人肉去查边缘 case、调 prompt、几周才把生产反馈翻译成改进,反馈环慢且只在有人推动时才前进;而这套设计把"生产使用"直接变成能自动喂给改进环的 structured signals。关键权衡在于:证据不会自动变成 Codex 任务——同一处更正可能是真抽取失误、也可能是从业者偏好、上一年结转值或工作流噪声,只有当重复差异被审阅并归组成一个可行动的 finding、有了明确成功条件后,才会被转成有边界的任务。
术语 self-improving agent(自进化 Agent,靠生产反馈闭环持续变强); production traces(生产轨迹,从输入到最终输出的结构化历史); eval target(评测目标,把重复失败模式打包成 Codex 可攀登的"山头"); finding(经审阅归组后可行动的失败结论)
📖 "Instead of relying on engineers to find and fix each failure, Tax AI uses Codex to turn production use into structured signals that fuel autonomous improvement." — Building self-improving tax agents with Codex
📖 "Once production issues are visible and structured, they can become findings, tailored evals, and scoped engineering tasks." — Building self-improving tax agents with Codex
🧪 实例 以租金物业(Schedule E)为例,一次从业者更正如何被抬升成工程任务:
flowchart LR
  A[从业者更正
practitioner correction] --> B[捕获差异
expected vs predicted 字段级 review rows] B --> C[归组相似失败
把 recurring failure 与 workflow noise 分开] C --> D[变成 eval target
如 fair rental days 字段] D --> E[Codex 攀登: 查 trace/eval/repo/skills] E --> F[改抽取 schema/source selection/mapper/grader] F --> G[跑定向+回归 eval, 提候选 PR] G --> A

效果上,Tax AI 本季处理了 7,000 份报税表,起草准确率最高达 97%,吞吐提升约 50%;上线时只有四分之一的报税表达到 75% 字段正确率,六周内 86% 达到该门槛。
🔍 追问 如果证据模糊、不能安全自动化怎么办? → 该 case 会路由回产品团队人工评审,而不是被强行塞进闭环("the case routes back to the product team instead of being forced through the loop")。
📚 拓展阅读
QSora Android 团队为什么把 Codex 当成"新入职的资深工程师"来对待?它擅长和需要指导的地方各是什么?深挖·拓展🔥高频
codex working-model mental-model
⏱️ 现行
四人小团队把 Codex 类比成一位"刚入职的资深工程师"——能力强,能把大量重活扛下来,让人把时间从"敲代码"转到"指导与审阅代码",但它没有你团队的上下文,需要被 onboard。它 需要指导 的地方:不擅长推断没被告知的东西(你偏好的架构模式、产品策略、真实用户行为、内部惯例);看不到 app 真正运行(打不开设备、感受不到滚动卡顿或流程别扭,这类体验性任务只能靠人);每个实例都要重新 onboarding;缺乏深层架构判断——放任不管它可能多塞一个 view model 而不是扩展已有的、或把本该在 repository 层的逻辑推到 UI 层,它的本能是"先跑起来"而非长期整洁。它 擅长 的地方:极快读懂大型代码库、几乎懂所有主流语言;热衷写单测提供广覆盖防回归;善于对反馈作出反应(把 CI 日志贴进 prompt 让它修);可大规模并行、把代码当一次性资源来试多个想法;能在设计讨论中作为生成式工具探索失败点;也很擅长 code review 常在合并前抓 bug。承认这些特性后,工作模型就清晰了:让 Codex 在"well-understood patterns 和 well-bounded scopes"里干重活,团队专注架构、用户体验、系统性改动和最终质量。
术语 newly hired senior engineer(把 Codex 类比成需要 onboard 的资深新人); AGENTS.md(散布在代码库里的指引文件,跨 session 复用同一套规范); well-bounded scopes(边界清晰的范围,Codex 表现最好的地方); experiential tasks(体验性任务,只有人能覆盖)
📖 "Treating it like a newly hired senior engineer was a good approach. Codex’s ability meant we could spend more time directing and reviewing code than writing it ourselves." — How we used Codex to build Sora for Android in 28 days
📖 "Codex couldn’t see the app actually run: It couldn’t open Sora on a device, notice that a scroll felt off, or sense that a flow was confusing." — How we used Codex to build Sora for Android in 28 days
🧪 实例 为把风格规范固化,团队在顶层 AGENTS.md 里写死格式化门禁,让每个 session 都遵守:
markdown
## Formatting and static checks
- **Always run** `./gradlew detektFix` (or for the affected modules) **before committing**. CI will fail if formatting or detekt issues are present.
🔍 追问 只给一句"Build the Sora Android app based on the iOS code. Go"为什么被立刻放弃? → 生成的代码技术上能跑但产品体验很差,且在没搞清 endpoints/data/user flows 时单发代码不可靠,合并上千行代码风险太大。
📚 拓展阅读
QCodex 的"有边界任务环境"如何划分可写工作区与只读生产上下文?这种隔离解决了什么问题?深挖·拓展中频
task-environment worktree context-scoping
⏱️ 现行
Tax AI 给 Codex 的不是一句模糊告警,而是一个 scoped engineering task——带证据、可编辑的产品面、以及明确的验证门。任务环境把 可写 worktree只读生产上下文 分开:worktree 里放 Codex 可以检视/修改的范围化产品面(如 app/tax-ai/rental-income/ 下的 agent.ts、schema.ts、provenance.ts、mapper.ts)、定义成功的定向 eval 与回归 eval(datasets/suites/graders)、以及编码"如何跑任务、如何尊重既有决策"的可复用 skills 和 docs;只读上下文则提供 production trace、源文档、Tax AI 的预测、最终报税表和 tax-engine 字段文档。这样划分的核心权衡是:Codex 需要真实证据来定位失败,但不能污染这些证据——把生产轨迹、源件设为只读,Codex 就能"调查失败而不篡改底层证据"。同时自动化只施加在产品的一个有边界的层(抽取 + 把源文档映射进税务工作流),工程师仍负责架构、产品决策和发布,从业者通过日常工作(更正抽取值、审阅报税表、批准最终申报)来 steer 这个闭环。
术语 worktree(可写工作树,Codex 能改的分支范围); scoped-tools(只读的范围化工具,如 production-trace/source-artifacts/tax-engine-docs); validation gates(验证门,定向+回归 eval 定义成功); bounded layer(有边界的自动化层,只覆盖抽取与映射)
📖 "For Codex, the result is not a vague alert but a scoped engineering task with evidence, editable product surfaces, and explicit validation gates." — Building self-improving tax agents with Codex
📖 "The read-only context provides the production trace, source documents, Tax AI prediction, finalized return, and tax-engine field documentation, so Codex can investigate the failure without mutating the underlying evidence." — Building self-improving tax agents with Codex
🧪 实例 一个代表性租金物业任务的候选目录(原文示意)大致如下:
bash
candidates/FIND-RENTAL-0042/
└── repo/                          # [1] 可写 worktree
    ├── AGENTS.md
    ├── tasks/FIND-RENTAL-0042/    # task.yaml / EXEC_PLAN.md / RESULTS.md
    ├── app/tax-ai/rental-income/  # [2] 范围化产品面 agent.ts/schema.ts/provenance.ts/mapper.ts
    ├── evals/                     # [3] datasets/suites/graders 定义成功
    ├── skills/                    # [4] eval-runner / tax-field-docs
    └── docs/
scoped-tools/                      # [5] 只读: production-trace / source-artifacts / tax-engine-docs
🔍 追问 把租金物业做到 90% precision/recall 花了多久,产出了什么可复用资产? → 约六周并伴大量工程监督,但产出可复用抽象、review artifacts、eval 约定和实现模式,让 Schedule C、Schedule A 等复杂表格更易支持。
📚 拓展阅读
QSora 团队"先手工打地基"具体做了什么?为什么这决定了 85% 代码能安全交给 Codex?深挖·拓展中频
foundation scaffolding codex
⏱️ 现行
团队认为即便最好的资深新人也没有一上来就做长期取舍的视角,所以自己亲手掌控 app 的系统设计与关键取舍:塑造架构、模块化、依赖注入、导航,并实现认证与基础网络流。在此地基上,他们手写了几个端到端的代表性 feature,把想让整个代码库遵循的规则和项目级模式边写边记录成文档。这样做的机制是:代码有很多种"正确"写法,他们不需要精确告诉 Codex 做什么,而要向它展示"在我们团队里什么才是正确的";一旦有了起点和偏好,把 Codex 指向这些代表性 feature,它就能在团队标准内更独立地工作。权衡与理由很明确:目标不是尽快做出"能跑的东西",而是做出"懂我们想怎么运作的东西"——对一个估计 85% 由 Codex 写成的项目来说,精心规划的地基避免了代价高昂的回退和重构,是他们做过最重要的决定之一。他们验证了一个假设:Codex 在"写得好的范例沙盒"里最能发挥——让它"用你刚看到的那个屏幕的相同架构和模式来构建这个设置屏幕"远比几乎无上下文地"构建这个设置屏幕"可靠。人做结构决策、定不变量,Codex 在结构内填充大量代码。
术语 laying the foundation by hand(手工打地基,人掌控架构/DI/导航/认证); representative features(代表性 feature,作为 Codex 模仿的范本); sandbox of well-written examples(写得好的范例沙盒,Codex 最能发挥之处); invariants(不变量,由人设定的结构约束)
📖 "We hypothesized Codex would thrive in a sandbox of well-written examples; and we were right." — How we used Codex to build Sora for Android in 28 days
📖 "For a project that we estimate was 85% written by Codex, a carefully planned foundation avoided costly backtracking and refactoring." — How we used Codex to build Sora for Android in 28 days
📖 "Humans made the structural decisions and set the invariants; Codex then filled in large amounts of code inside that structure." — How we used Codex to build Sora for Android in 28 days
🧪 实例 跨平台"翻译式"开发的典型 prompt:把 iOS、backend、Android 三个 repo 放进同一环境后,团队这样下指令:
text
Read these models and endpoints in the iOS code and then propose a plan to implement the equivalent behavior on Android using our existing API client and model classes.

一个小技巧是在 ~/.codex/AGENTS.md 里写明本地各 repo 的位置与内容,方便 Codex 发现和导航相关代码。
🔍 追问 为什么说"Codex 就是跨平台框架的未来"? → 因为逻辑是可移植的:无论 Swift 还是 Kotlin,数据模型/网络调用/校验规则/业务逻辑本质相同,Codex 很擅长读 Swift 实现并产出保持语义的 Kotlin 等价物,团队因此用"翻译"而非"共享抽象"做跨平台,避免了双倍实现成本。
📚 拓展阅读
QWindows 上第一版"unelevated sandbox"如何在不需管理员提权的前提下限制文件写入?深挖·拓展中频
windows-sandbox SID write-restricted-token
⏱️ 现行
因为 macOS 有 Seatbelt、Linux 有 seccomp/bubblewrap,而 Windows 原生不提供这类沙箱能力,Codex 团队从零自建。第一版原型的核心目标是 不 elevation——不要为了搭建/运行沙箱就弹管理员提权。为限制文件写入,它依赖两个 Windows 构件:SIDwrite-restricted token。SID 是 Windows 把权限绑定到的身份;Windows 允许创建不对应真实用户、但仍能出现在 ACL 里的"合成 SID(synthetic SID)",于是可以专为沙箱造 SID 而不干扰机器上其他东西。write-restricted token 会让 Windows 在写操作上做一次额外的访问检查:一次写要成功,必须同时通过两道检查——常规用户身份(token owner)被允许,且 token 的 restricted SID 列表里至少有一个 SID 也被授予了访问。这样就能用 ACL 精确定义沙箱能改哪里,拿到写操作所需的粒度。具体流程:setup 建一个名为 sandbox-write 的合成 SID,给它对 cwd 和 config.toml 里配置的 writable_roots 授予写/执行/删除;同时显式拒绝它写 <cwd>/.git<cwd>/.codex<cwd>/.agents 这些"可写区内的只读"位置;命令在 restricted SID 列表为 [Everyone, 当前登录会话 SID, sandbox-write] 的 write-restricted token 下启动。权衡:这套只用少量标准 Windows 能力、写权限极显式且免提权,但应用 workspace ACL 可能很慢、改语义昂贵、且网络防护弱。
术语 SID(security identifier,Windows 把权限绑定的身份); synthetic SID(合成 SID,不对应真实用户但能进 ACL); write-restricted token(写限制令牌,令写操作触发额外访问检查); writable_roots(config.toml 里配置的可写根); elevation(提权,团队初版刻意避免)
📖 "Process tokens are security objects in Windows that define identity and privileges for a running process." — Building a safe, effective sandbox to enable Codex on Windows
📖 "That makes SIDs a useful primitive for our sandbox: we can create SIDs exclusively for the Codex sandbox to use, without interfering with anything else on the machine." — Building a safe, effective sandbox to enable Codex on Windows
🧪 实例 一次写入要通过的双重检查(原文机制):
text
写入成功 ⇔ 检查1: token owner(真实用户身份)被 ACL 允许
        且 检查2: token 的 restricted SID 列表里至少一个 SID 也被授予访问
restricted SID 列表 = [Everyone, 当前登录会话 SID, sandbox-write 合成 SID]
🔍 追问 为什么 AppContainer、Windows Sandbox、MIC 整数标签都被否掉? → AppContainer 是为"上来就知道要访问什么"的严格受限 app 设计的,而 Codex 要驱动开放式开发流(shell/Git/Python/构建工具)形状不对;Windows Sandbox 是一次性 VM,Codex 需直接作用于用户真实 checkout,且 Home SKU 上根本没有;MIC 把 workspace 标为低完整性意味着"低完整性进程普遍能写这里",把用户真实 checkout 变成宿主的低完整性 sink,风险过大。
📚 拓展阅读
Q为什么 unelevated 沙箱的网络抑制"不够",最终"elevated sandbox"如何真正封住出网?深挖·拓展中频
windows-sandbox network-suppression firewall
⏱️ 现行
初版为避免提权,只能用"环境变量层"的弱手段抑制网络:把 proxy 相关变量指向死端点(如 HTTPS_PROXY=http://127.0.0.1:9)、让 Git 的 HTTP(S) 传输同样打到死端点、用 GIT_SSH_COMMAND=cmd /c exit 1 让 Git over SSH 立即失败,再在 PATH 前置一个 denybin 目录、重排 PATHEXT 让 stub SSH/SCP 脚本先于真二进制解析。这能拦住很多常规工具流量,但只是 advisory(建议性):进程可以无视环境、绕过 PATH、或直接开 socket,恶意代码和自带网络栈的良性程序都能轻易绕过,风险太高。团队想用 Windows Firewall 来阻断出站,但没法只对"Codex harness spawn 出的命令"精确建规则——Windows 不允许把防火墙规则匹配到 restricted token 的非主体身份,按二进制匹配只能限 codex.exe 本身而管不到它代其派生的 Git/Python,用户维度规则在 unelevated 设计里仍匹配真实用户,端口/地址维度也是错误的策略。要把防火墙规则精确施加到沙箱命令,就必须让它们以 一个独立 principal 而非"真实用户"身份运行——于是放松了"不提权"约束。最终的 elevated sandbox(当前实现)在 setup 时需管理员权限,由 Codex 自己创建两个本地用户:CodexSandboxOffline(被防火墙规则针对)和 CodexSandboxOnline(不被针对);子进程仍在 restricted token 下运行(同样的 [Everyone, Logon, Synthetic] restricted SID 列表),但 token 的 principal 从真实 Windows 用户换成这两个用户之一,防火墙就能对 offline 用户阻断所有出站。
术语 advisory(建议性/非强制,环境变量抑制的本质弱点); denybin(前置到 PATH 的目录,放 stub SSH/SCP 脚本); separate principal(独立主体,让防火墙能精确匹配沙箱进程的前提); CodexSandboxOffline / CodexSandboxOnline(Codex 自建的本地用户,前者被防火墙针对); DPAPI(Windows Data Protection API,加密存储沙箱用户凭据)
📖 "Network protection is weak. As mentioned before, it was “advisory,” would definitely be circumvented by some programs that implemented their own networking stack, and wasn't designed to hold up to adversarial code." — Building a safe, effective sandbox to enable Codex on Windows
📖 "To apply a firewall rule specifically to our sandboxed commands, we needed to run them as a separate principal, not as the “real” user." — Building a safe, effective sandbox to enable Codex on Windows
🧪 实例 初版用于抑制网络的部分环境覆盖(原文列出):
bash
HTTPS_PROXY=http://127.0.0.1:9
ALL_PROXY=http://127.0.0.1:9
GIT_HTTPS_PROXY=http://127.0.0.1:9
NO_PROXY=localhost,127.0.0.1,::1
GIT_SSH_COMMAND=cmd /c exit 1
flowchart TD
  U[unelevated: 环境变量抑制] -->|advisory, 可被绕过| W[网络防护太弱]
  W --> E[elevated: 引入独立 principal]
  E --> O1[CodexSandboxOffline
防火墙阻断所有出站] E --> O2[CodexSandboxOnline
不被防火墙针对]
🔍 追问 换成新的 CodexSandbox 用户后,为什么还要额外授予读 ACL? → 因为新用户不再是真实 Windows 用户,很多目录(如用户 profile)默认不让其他用户读,团队要给沙箱用户在 C:\Users\<real-user>C:\Windows\C:\Program Files\ 等补授读/执行 ACL,且因为耗时而异步执行以免阻塞对用户可见的 setup 步骤。
📚 拓展阅读
Q为什么 elevated 沙箱要引入 codex-command-runner.exe?"privilege wall"是什么?深挖·拓展低频
windows-sandbox command-runner token
⏱️ 现行
由于 Windows 用户与 token 登录边界的工作方式,elevated 沙箱不能再像 unelevated 那样"创建 restricted token 再在其下 spawn 进程"。团队最初设想的流程是:codex.exe 以真实用户运行,依次 LogonUserW(...) 取沙箱用户 token、CreateRestrictedToken(...) 生成 restricted token、再 CreateProcessAsUserW(...) 启动最终子进程。但这条路在 CreateProcessAsUserW(...) 处撞上一堵 privilege wall(权限墙)——codex.exe 能为沙箱用户造出 restricted token,却无法可靠地从"真实用户这一侧"用该 token 启动子进程。根因是这个跨越边界的最终 spawn 需要一个"本身已经以沙箱用户身份运行"的进程,让"加限制"和"最终 spawn"发生在沙箱用户那一侧而非真实用户侧。因此引入 codex-command-runner.exe,唯一职责就是铸造 restricted token 并 spawn 请求的命令,把流程一分为二:第一步 codex.exeCreateProcessWithLogonW(...) 以沙箱用户身份启动 runner(此时还不用 restricted token);第二步 runner 内部用 OpenProcessToken(GetCurrentProcess(), ...) 打开自己(已属沙箱用户)的 token、GetTokenInformation(...) 取出沙箱 logon SID、CreateRestrictedToken(...) 造出最终 restricted token,再 CreateProcessAsUserW(...) 启动真正子进程。这样最终架构有四层:codex.execodex-windows-sandbox-setup.exe(所有 elevated setup 工作)、codex-command-runner.exe(跑 restricted token 命令)、以及 child process。理由是每块复杂度都出于必要,把不同职责拆到不同二进制,让 codex.exe 保持普通、免提权,并把 Windows-only 的 setup 机制从其他平台的 codex.exe 中剥离。
术语 privilege wall(权限墙,CreateProcessAsUserW 处从真实用户侧跨界 spawn 失败); codex-command-runner.exe(专职铸造 restricted token 并 spawn 命令的新二进制); CreateProcessWithLogonW(以沙箱用户身份启动 runner,暂不用 restricted token); codex-windows-sandbox-setup.exe(专职 elevated setup 的二进制)
📖 "In practice, that desired flow didn't work because of a privilege wall at CreateProcessAsUserW(...)." — Building a safe, effective sandbox to enable Codex on Windows
📖 "We needed a process that was already running as the sandbox user—this would let the restriction step and final spawn happen on the sandbox-user side of the boundary instead of the real-user side." — Building a safe, effective sandbox to enable Codex on Windows
🧪 实例 一分为二的 spawn 流程(原文 Part 1 / Part 2):
text
Part 1 (real user 侧):
  codex.exe --CreateProcessWithLogonW--> 以 sandbox user 启动 codex-command-runner.exe (暂无 restricted token)

Part 2 (sandbox user 侧, runner 内部):
  OpenProcessToken(GetCurrentProcess(), ...)   # 打开自己的 token(已属沙箱用户)
  GetTokenInformation(...)                       # 取沙箱 logon SID
  CreateRestrictedToken(...)                     # 造最终 restricted token
  CreateProcessAsUserW(restricted token) -----> 启动真正 child process
🔍 追问 为什么把 setup 逻辑单独封成一个二进制? → 一是只在需要时才跨 UAC 边界;更深层是 setup 与 codex.exe 职责根本不同——独立二进制让 codex.exe 保持普通、免提权的 harness,避免 Windows-only 机制在其他平台膨胀,并把较长的 setup 工作与主进程生命周期解耦。
📚 拓展阅读
Q并行跑多个 Codex session 时,Brooks 定律为什么依然成立?团队的瓶颈发生了什么转移?深挖·拓展中频
brooks-law parallel-sessions orchestration
⏱️ 现行
Fred Brooks 有名言"给一个已经延期的软件项目加人只会让它更晚",团队没有无视这一洞见反而顺应它——只组了四名工程师、每人配 Codex 来放大个人影响力,而不是堆人加流程。项目高峰期他们常并行跑多个 Codex session:一个做 playback、一个做 search、一个做 error handling、有时还有一个做 tests 或 refactor,感觉不像用工具而像管一个团队。每个 session 会定期回报进度、各自需要注意、反馈和审阅,像一个带着好几名新工程师的 tech lead。但关键在于:Codex 不会因上下文切换而阻塞,人却会——额外的速度意味着 review 队列里总有东西在等,开发瓶颈从"写代码"转移到"做决策、给反馈、整合改动"。这正是 Brooks 洞见的新形态:你不能简单地加 Codex session 就期待线性加速,正如不能一直加工程师就期待进度线性缩短,每多一"双手"(哪怕是虚拟的)都增加协调开销,团队从"更快的独奏者"变成了"乐团指挥"。配套做法是"先规划再编码":对任何非平凡改动,先让 Codex 读相关文件、总结系统如何工作(如数据如何从 API 经 repository、view model 流到 UI),人来纠正/精炼其理解,再一起做一份"迷你设计文档"式的实现计划,然后让 Codex 按计划一步步实施;很长的任务里还会让 Codex 把计划存成文件,以便跨实例复用同一方向。
术语 Brooks' Law(加人给延期项目会更晚,加 Codex session 同理不线性加速); conductor of an orchestra(乐团指挥,团队角色从独奏者转变而来); review queue(审阅队列,速度提升后总有东西在等); implementation plan(迷你设计文档式的计划,让 Codex 可长时间无监督运行)
📖 "You can’t simply add Codex sessions and expect linear speedups any more than you can keep adding engineers to a project and expect the schedule to shrink linearly." — How we used Codex to build Sora for Android in 28 days
📖 "Our bottleneck in development shifted from writing code to making decisions, giving feedback, and integrating changes." — How we used Codex to build Sora for Android in 28 days
🧪 实例 "先规划后编码"的循环带来的三个收益(原文):让 Codex 能长时间"无监督"运行,因为你知道它的计划;让 code review 更容易,因为可以拿实现对照计划而非无上下文读 diff;出问题时可以先 debug 计划、再 debug 代码。落地时间线:
text
Day 0  → Day 18  : 内部 build 上线给员工
Day 18 → Day 28  : 公开发布 (共 28 天, 4 名工程师, 约 5B tokens)
结果: Play Store #1, 首日 >100 万条视频, crash-free 99.9%
🔍 追问 为什么 AI 辅助开发"不是减少而是增加了对严谨的需求"? → 因为 Codex 的目标是"现在从 A 到 B",缺乏对系统真实约束、最佳架构和未来产品规划的把握,这些只能由工程师提供,所以人的深层系统理解与长时间跨度上和 AI 协作的能力反而更重要。
📚 拓展阅读

第14章 · Responses API 与 Agentic 基础设施

Q为什么要给 Responses API "装配一台计算机环境"?它由哪几块基础原语组成、各自解决什么问题?深挖·拓展🔥高频
Responses API Agent 架构 hosted container
⏱️ 现行
文章的核心论点是行业正从"用 model(擅长单一任务)"转向"用 agent(处理复杂工作流)"。仅靠 prompt 只能触达模型训练出来的智能;而一旦给模型一台计算机环境,就能覆盖更广的用例——运行服务、向 API 请求数据、生成表格或报告等更有用的产物。真正落地建 agent 时会冒出一堆工程问题:中间文件放哪、如何避免把大表格粘进 prompt、如何在不制造安全隐患的前提下给工作流网络访问、以及如何在不自建工作流系统的情况下处理 timeout 和 retry。OpenAI 的选择不是把执行环境的搭建甩给开发者,而是内建一套组件把 Responses API 装配上计算机环境。整套系统由五块原语拼成、职责清晰互补:Responses API 负责编排(orchestration),shell tool 提供可执行动作,hosted container 提供持久的运行时上下文,skills 叠加可复用的工作流逻辑,compaction 让 agent 能带着所需上下文长时间运行。权衡在于:平台把隔离、文件系统、可选结构化存储(如 SQLite)、受限网络访问都托管起来,开发者用更少的自建代码换取更可靠、可复现、更安全的生产工作流。
术语 orchestration(编排,由 Responses API 承担的角色); hosted container(托管容器,提供持久运行时上下文); primitives(原语,可组合的基础构件); artifacts(产物,如 spreadsheet 或 report)
📖 "By prompting models, you can only access trained intelligence. However, giving the model a computer environment can achieve a much wider range of use cases, like running services, requesting data from APIs, or generating more useful artifacts like spreadsheets or reports." — From model to agent: Equipping the Responses API with a computer environment
📖 "the _Responses API_ provides orchestration, the _shell tool_ provides executable actions, the _hosted container_ provides persistent runtime context, _skills_ layer reusable workflow logic, and _compaction_ allows an agent to run for a long time with the context it needs." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 一个 prompt 可以展开成端到端工作流——文中给的例子是"从实时数据创建一张 spreadsheet":
flowchart LR
  P[单个 prompt] --> S[发现合适的 skill]
  S --> F[fetch 数据]
  F --> T[转成本地结构化状态]
  T --> Q[高效查询]
  Q --> A[生成持久 artifacts]
🔍 追问 这套东西和"让开发者自己搭执行环境"相比,本质上让开发者省掉了什么? → 省掉自建 execution environment、summarization/state-carrying 系统、以及 timeout/retry 工作流,平台把隔离环境、文件系统、结构化存储与受限网络托管掉。
📚 拓展阅读
Qshell tool 是什么?它和已有的 code interpreter 相比强在哪?深挖·拓展🔥高频
shell tool code interpreter Unix
⏱️ 现行
shell tool 让模型通过命令行与一台计算机交互来完成大量任务——从搜索文本到在你的机器上发送 API 请求。它建立在人们熟悉的 Unix 工具之上,grepcurlawk 这些实用工具开箱即用,你期待命令行能做的它基本都能做。关键对比在于已有的 code interpreter 只能执行 Python;而 shell tool 打开的用例面要宽得多,比如运行 Go 或 Java 程序、启动一个 NodeJS server。这种灵活性正是让模型能完成复杂 agentic 任务的原因。理解 shell tool 之前要先理解模型如何用工具:训练时模型被逐步展示工具如何被使用及其产生的效果,从而学会"何时用、怎么用"。要点是——所谓"使用工具",模型实际上只是"提出"一次 tool call,它无法自己执行这次调用,执行要靠外部的编排与运行时。
术语 shell tool(壳工具,通过命令行驱动一台计算机); code interpreter(代码解释器,只执行 Python); Unix tooling(Unix 工具链,grep/curl/awk 开箱即用); tool call(工具调用,模型只提出、不自行执行)
📖 "Built on familiar Unix tooling, our shell tool can do anything you'd expect, with utilities like grep, curl, and awk available out of the box." — From model to agent: Equipping the Responses API with a computer environment
📖 "Compared to our existing code interpreter, which only executes Python, the shell tool enables a much wider range of use cases, like running Go or Java programs or starting a NodeJS server." — From model to agent: Equipping the Responses API with a computer environment
📖 "When we say “using a tool”, we mean the model actually only proposes a tool call. It can't execute the call on its own." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 用 shell tool 起一个 NodeJS server 或直接用 Unix 工具处理文本,而不必局限于 Python:
bash
# 搜索文本 / 请求数据 / 处理列 —— 全是开箱即用的 Unix 工具
grep -R "declining" ./reports
curl -s https://api.example.com/sales | awk -F, '{print $1, $3}'
node server.js
🔍 追问 既然模型只能"提出"命令,那真正的执行循环靠谁闭合? → 靠一个 orchestrator(即 Responses API)拿到模型输出、调用工具、把结果回喂给模型,循环直到任务完成(见 Q3)。
📚 拓展阅读
QResponses API 如何编排 agent loop?为什么必须是 GPT‑5.2 及之后的模型?深挖·拓展🔥高频
agent loop orchestration GPT-5.2
⏱️ 现行
模型自己只能"提出"shell 命令,命令怎么被执行需要一个 orchestrator——拿到模型输出、调用工具、把工具响应回传给模型,循环直到任务完成。Responses API 就是开发者与 OpenAI 模型交互的方式:用 custom tools 时它会把控制权交还给 client,由 client 自建 harness 跑工具;但用 hosted tools 时它能开箱即用地在模型与工具之间做编排。当 Responses API 收到 prompt,它会组装模型上下文:user prompt、先前会话状态、tool instructions。要让 shell 执行生效有两个必要条件——prompt 必须提到使用 shell tool,并且所选模型必须被训练成会提出 shell 命令,而 GPT‑5.2 及之后的模型正是为此训练的。有了这些上下文,模型决定下一步动作:若选择 shell 执行,就把一条或多条 shell 命令返回给 Responses API 服务,API 把命令转发给 container runtime,流式回传 shell 输出,并把它喂进下一次请求的上下文;模型据此检查结果、发起后续命令或产出最终答案。Responses API 会重复这个循环,直到模型返回一个不再带 shell 命令的 completion。
术语 orchestrator(编排器,闭合"模型输出→跑工具→回喂"的循环); custom tools(自定义工具,控制权交还 client,需自建 harness); hosted tools(托管工具,Responses API 开箱即用编排); container runtime(容器运行时,实际执行命令处)
📖 "For shell execution to work, the prompt must mention using the shell tool _and_ the selected model must be trained to propose shell commands—models GPT‑5.2 and later are trained for this." — From model to agent: Equipping the Responses API with a computer environment
📖 "The Responses API repeats this loop until the model returns a completion without additional shell commands." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 编排循环示意:
flowchart TD
  U[prompt + 会话状态 + tool instructions] --> M[模型决定下一步]
  M -->|返回 shell 命令| API[Responses API 服务]
  API -->|转发| C[container runtime]
  C -->|流式 shell 输出| API
  API -->|回喂上下文| M
  M -->|无更多命令| DONE[最终 completion]
🔍 追问 用 custom tools 和 hosted tools 的编排差别是什么? → custom tools 会把控制权 yield 回 client,由 client 自建 harness 跑工具;hosted tools 则由 Responses API 直接在模型与工具间编排,无需 client harness。
📚 拓展阅读
Q一步里提出多条 shell 命令时,Responses API 如何并发执行并控制输出规模?深挖·拓展中频
并发 多路复用 output cap
⏱️ 现行
模型可以在一步里提出多条 shell 命令,Responses API 用相互独立的 container session 并发执行它们。每个 session 独立地流式回传输出,API 把这些流多路复用(multiplex)回结构化的 tool outputs 作为上下文——也就是说 agent loop 能把工作并行化,比如同时搜索文件、拉取数据、校验中间结果。与此同时,涉及文件操作或数据处理时 shell 输出可能极大,会白白吃掉 context budget 却不带来有用信号。为此模型为每条命令指定一个 output cap,Responses API 强制这个上限并返回一个有界结果:同时保留输出的开头与结尾、并标记被省略的内容(形如 text at the beginning ... 1000 chars truncated ... text at the end)。并发执行与有界输出这两者叠加,让 agent loop 既快又省上下文,使模型能持续在相关结果上推理,而不是被原始终端日志淹没。
术语 container session(容器会话,并发执行各命令的独立单元); multiplex(多路复用,把多股独立输出流并回结构化上下文); output cap(输出上限,模型按命令指定的字符上界); bounded result(有界结果,保头保尾并标记省略)
📖 "The model can propose multiple shell commands in one step, and the Responses API can execute them concurrently using separate container sessions." — From model to agent: Equipping the Responses API with a computer environment
📖 "The Responses API enforces that cap and returns a bounded result that preserves both the beginning and end of the output, while marking omitted content." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 把输出界定为 1,000 字符时,超出部分居中截断而保留首尾:
text
text at the beginning ... 1000 chars truncated ... text at the end
🔍 追问 为什么是"保头保尾"而不是简单截断前 N 个字符? → 因为命令输出的关键信号往往同时分布在开头(如表头/状态行)和结尾(如结果/退出码),保留两端能在有限 context budget 下最大化有用信息。
📚 拓展阅读
Q长时任务把 context window 撑满怎么办?native compaction 的机制与权衡是什么?深挖·拓展中频
compaction context window 长时任务
⏱️ 现行
agent loop 的一个潜在问题是任务可能跑很久,长任务会把 context window 填满——而这个窗口对跨轮次、跨 agent 传递上下文至关重要。一个 agent 调 skill、拿响应、加 tool call 和 reasoning summary,有限的窗口很快就满了。为在不丢关键上下文的前提下继续运行,需要一种"保留关键细节、剔除多余内容"的办法。OpenAI 没有要求开发者自建 summarization 或 state-carrying 系统,而是在 Responses API 里加了 native compaction,并让它与模型的行为和训练方式对齐。其机制是:最新模型被训练成能分析先前会话状态,产出一个 compaction item,用加密、token 高效的表示保留关键先前状态;compaction 之后的下一个窗口就由这个 compaction item 加上早前窗口里高价值的部分组成,从而让工作流跨窗口边界连贯延续。compaction 可以在服务端内建,也可通过独立的 /compact 端点使用;服务端 compaction 让你配置一个阈值、系统自动处理时机,省掉复杂的 client 端逻辑,还允许略大的有效输入窗口来容忍临界前的小超额,使贴近上限的请求仍能被处理和压缩而非被拒绝。权衡在于:随着模型训练演进,native compaction 方案会随每次 OpenAI 模型发布一起演进——这是把它交给平台而非自建的最大理由。
术语 compaction(上下文压缩,跨窗口保留关键状态); compaction item(压缩项,加密且 token 高效的先前状态表示); /compact endpoint(独立压缩端点); threshold(阈值,服务端自动触发压缩的配置)
📖 "Our latest models are trained to analyze prior conversation state and produce a compaction item that preserves key prior state in an encrypted token-efficient representation." — From model to agent: Equipping the Responses API with a computer environment
📖 "Server-side compaction lets you configure a threshold, and the system handles compaction timing automatically, eliminating the need for complex client-side logic." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 Codex 既帮助构建了 compaction 系统、又作为它的早期用户——一个 Codex 实例撞上 compaction error 时,他们会另起一个实例去排查,结果 Codex 只是通过"处理这个问题"本身就得到了一套原生有效的 compaction 系统。文中把这称为 "Codex learns alongside us"。
🔍 追问 为什么允许"略大于上限"的有效输入窗口? → 为了容忍压缩触发前的小幅超额(small overages),让贴着上限的请求还能被处理并压缩,而不是直接被 reject。
📚 拓展阅读
Qcontainer 作为"工作上下文",文件系统与数据库(SQLite)分别解决什么反模式?深挖·拓展中频
container context filesystem SQLite
⏱️ 现行
container 不只是跑命令的地方,更是模型的工作上下文——在容器里模型可以读文件、查数据库、并在网络策略控制下访问外部系统。第一块是文件系统,用于上传、组织、管理资源;OpenAI 建了 container 和 file API 给模型一张"可用数据的地图",帮它选择有针对性的文件操作而非做宽泛而嘈杂的扫描。一个常见反模式是把所有输入直接塞进 prompt context,随着输入变大,过度填充 prompt 既贵又让模型难以导航;更好的做法是把资源暂存到容器文件系统,让模型自己决定用 shell 命令打开、解析或转换什么——就像人类一样,模型在有组织的信息上工作得更好。第二块是数据库:很多场景建议开发者把结构化数据以 SQLite 存储并查询。与其把整张 spreadsheet 复制进 prompt,不如给模型一份表的描述(有哪些列、各自含义),让它按需拉行。比如问"这个季度哪些产品销量在下滑",模型可以只查相关的行而不是扫整张表——更快、更便宜、也更能扩展到更大的数据集。
术语 container context(容器上下文,模型读文件/查库/访问外部系统之处); file system(文件系统,暂存资源供模型按需打开); SQLite(建议用于容器内结构化数据存储与查询); anti-pattern(反模式,把全部输入直塞进 prompt)
📖 "A better pattern is to stage resources in the container file system and let the model decide what to open, parse, or transform with shell commands." — From model to agent: Equipping the Responses API with a computer environment
📖 "In many cases, we suggest developers store structured data in databases as SQLite and query them." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 不把整张表塞进 prompt,而是让模型对 SQLite 只查需要的行:
sql
-- "Which products had declining sales this quarter?"
SELECT product, sales_delta
FROM quarterly_sales
WHERE sales_delta < 0;
🔍 追问 为什么"给表的描述 + 让模型查"比"把 spreadsheet 复制进 prompt"更好? → 前者只拉相关行,更快更便宜、可扩展到更大数据集;后者随输入增长会撑爆 prompt 且模型难以导航。
📚 拓展阅读
Q给容器网络访问既必要又危险,hosted container 用什么机制在不牺牲能力的前提下保证安全?深挖·拓展中频
网络安全 egress proxy secret injection
⏱️ 现行
网络访问是 agent workload 的必需项——工作流可能要拉实时数据、调外部 API、装依赖包。但给容器不受限的互联网访问很危险:可能把信息暴露给外部网站、无意触碰敏感的内部或第三方系统、也让 credential 泄漏与数据外泄更难防。为在不削弱 agent 有用性的前提下解决这些顾虑,OpenAI 让 hosted container 使用一个 sidecar egress proxy:所有出站网络请求都流经一个集中的策略层,该层强制 allowlist 与访问控制、同时保持流量可观测。对于凭证,采用在出口处做 domain-scoped secret injection——模型和容器只看到占位符(placeholders),而原始 secret 值留在模型可见上下文之外,只在被批准的目的地才被注入应用。这样既降低了泄漏风险,又仍能发起经过认证的外部调用。本质是把"是否放行、用哪个凭证"的决策从模型手里挪到平台的策略层,让模型永远拿不到明文 secret。
术语 sidecar egress proxy(边车出口代理,所有出站流量的集中出口); allowlist(允许清单,策略层强制的可达目的地); domain-scoped secret injection(按域限定的凭证注入,仅在批准目的地注入); placeholders(占位符,模型/容器只见占位而非明文)
📖 "All outbound network requests flow through a centralized policy layer that enforces allowlists and access controls while keeping traffic observable." — From model to agent: Equipping the Responses API with a computer environment
📖 "For credentials, we use domain-scoped secret injection at egress. The model and container only see placeholders, while raw secret values stay outside model-visible context and only get applied for approved destinations." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 出站认证调用的流向:
flowchart LR
  M[模型/容器
只见 placeholder] --> PX[sidecar egress proxy
策略层: allowlist + 访问控制] PX -->|批准目的地才注入真实 secret| EXT[外部 API] PX -.->|流量可观测| OBS[观测/审计]
🔍 追问 为什么"占位符 + 出口注入"能在功能不减的前提下降低泄漏风险? → 因为原始 secret 值始终留在 model-visible context 之外、只对 approved destinations 注入,模型即便被诱导也无法读到明文,却仍能完成 authenticated external calls。
📚 拓展阅读
Qagent skills 是什么?Responses API 加载一个 skill 的确定性流程是怎样的?深挖·拓展低频
agent skills SKILL.md 复用
⏱️ 现行
shell 命令很强,但很多任务反复走同样的多步模式;若每次运行都让 agent 重新发现工作流(重规划、重发命令、重学约定),就会导致结果不一致、执行被浪费。agent skills 把这些模式打包成可复用、可组合的构件:一个 skill 是个文件夹 bundle,含 SKILL.md(装元数据和指令)加上任何支持资源(如 API spec、UI 资产)。这与前述运行时架构天然契合——container 提供持久文件与执行上下文,shell tool 提供执行接口,于是模型可以在同一个 agent loop 里用 shell 命令(lscat 等)发现 skill 文件、解读指令、运行 skill 脚本。OpenAI 提供 API 在平台上管理 skill:开发者把 skill 文件夹作为带版本的 bundle 上传存储、之后按 skill ID 取回。在把 prompt 发给模型之前,Responses API 加载该 skill 并把它纳入模型上下文,这个顺序是确定性的:1) 取 skill 元数据(含 name 和 description);2) 取 skill bundle、拷进 container 并解包;3) 用 skill 元数据和 container 路径更新模型上下文。之后模型在判断某个 skill 是否相关时,会渐进式地探索它的指令、并通过容器里的 shell 命令执行它的脚本。
术语 agent skills(把重复多步模式打包成可复用可组合构件); SKILL.md(skill 文件夹里装元数据与指令的文件); versioned bundles(带版本的 bundle,按 skill ID 取回); progressive exploration(渐进探索,模型逐步读指令并执行脚本)
📖 "Developers upload and store skill folders as versioned bundles, which can later be retrieved by skill ID. Before sending the prompt to the model, the Responses API loads the skill and includes it in model context." — From model to agent: Equipping the Responses API with a computer environment
📖 "With both in place, the model can discover skill files using shell commands (\ls\, \cat\, etc.) when it needs to, interpret instructions, and run skill scripts all in the same agent loop." — From model to agent: Equipping the Responses API with a computer environment
🧪 实例 一个 skill bundle 的典型结构与确定性加载顺序:
text
my-skill/
  SKILL.md        # metadata + instructions
  specs/api.yaml  # 支持资源: API specs
  assets/ui.png   # 支持资源: UI assets

加载: 1 取元数据 → 2 取 bundle 拷入 container 解包 → 3 用元数据+容器路径更新上下文
🔍 追问 为什么把 skill 打包比"每次让 agent 现场重新发现工作流"更好? → 现场重发现会重规划、重发命令、重学约定,导致结果不一致与执行浪费;打包成可复用 bundle 让模型直接发现并执行既有工作流,结果更一致。
📚 拓展阅读
中频

用 WebSockets 加速 Agentic 工作流

Speeding up agentic workflows with WebSockets in the Responses API
Q为什么在 Agentic 工作流里,Responses API 的开销会成为瓶颈?这与推理速度的变化有什么关系?深挖·拓展🔥高频
latency agentic-loop inference
⏱️ 现行
Codex 修 bug 时会扫描代码库、读取文件建立上下文、做修改、跑测试验证,底层意味着几十次来回的 Responses API 请求:判定模型下一步动作、在你机器上跑工具、把工具输出送回 API,再重复。从延迟角度看,Codex agent loop 的时间主要花在三个阶段:API 服务内工作(校验与处理请求)、模型推理、以及客户端时间(跑工具和构建模型上下文)。过去在 GPU 上跑 LLM 推理是 agentic loop 里最慢的一环,所以 API 服务开销很容易被"藏"在推理时间背后;但随着推理越来越快,一次 agentic rollout 累积的 API 开销就变得非常显眼——用户实际上是在等运行 API 的 CPU,然后才能用上服务模型的 GPU。这里的权衡本质是结构性的:团队把每个 Codex 请求当成独立请求处理,即便对话大部分没变,每个后续请求仍要为完整历史重复付出处理成本;对话越长,这种重复处理越昂贵。
术语 agentic loop(智能体循环,模型动作与工具执行反复来回); TPS(tokens per second,每秒生成 token 数); TTFT(time to first token,首 token 时间,反映 API 响应速度); inference(推理,模型在 GPU 上生成新 token 的阶段)
📖 "In the past, running LLM inference on GPUs was the slowest part of the agentic loop, so API service overhead was easy to hide. As inference gets faster, the cumulative API overhead from an agentic rollout is much more notable." — Speeding up agentic workflows with WebSockets in the Responses API
📖 "The deeper issue was structural: we treated each Codex request as independent, processing conversation state and other reusable context in every follow-up request." — Speeding up agentic workflows with WebSockets in the Responses API
🧪 实例 一个典型的 Codex agent loop 里,单次任务被拆成大量同步往返:
flowchart LR
  C[Codex 客户端] -->|请求下一步动作| API[Responses API]
  API -->|返回 tool call: rg / sed / apply_patch / pytest| C
  C -->|本地执行工具, 送回结果| API
  API -->|继续采样| C
  API -->|最终: The bug has been fixed| C

每一轮都重复校验并处理完整对话历史,导致累积 API 开销随对话变长而膨胀。
🔍 追问 三个延迟阶段里,哪个阶段的加速反而暴露了另外阶段的问题? → 推理(inference)提速后,原本被推理时间掩盖的 API 服务开销变得显眼,于是团队必须去压缩 API 侧开销。
📚 拓展阅读
QWebSocket mode 端到端把 agent loop 加速了多少?靠哪几类手段实现?深挖·拓展🔥高频
websockets optimization caching
⏱️ 现行
团队让使用 API 的 agent loop 端到端快了 40%,使用户能真正体验到推理从 65 提升到接近 1,000 tokens per second 的跳变。实现路径是四类手段的组合:缓存(caching)、消除不必要的网络跳转(eliminating unnecessary network hops)、改进安全栈以更快地标记问题(improving safety stack),以及——最重要的——构建一种到 Responses API 的持久连接(persistent connection),取代原先一连串同步 API 调用。为 GPT‑5.3‑Codex‑Spark(一个快速编码模型,靠专为 LLM 推理优化的 Cerebras 硬件驱动)服务时,目标是数量级级别的提速:超过 1,000 TPS。在 2025 年 11 月左右的性能冲刺里,单请求关键路径优化(内存缓存 rendered token 与模型配置、直连推理服务、更快跑分类器)带来了接近 45% 的 TTFT 改善,但对 Spark 仍不够快;真正的结构性突破是持久连接。权衡在于:单请求优化虽有效,却仍受"每个后续请求都重复处理完整历史"这个结构限制,只有持久连接才能打破它。
术语 persistent connection(持久连接,连接生命周期内缓存可复用状态); Cerebras hardware(为 LLM 推理优化的专用硬件); GPT‑5.3‑Codex‑Spark(快速编码模型,TPS 目标超过 1000); critical-path latency(关键路径延迟)
📖 "we'll explain how we made agent loops using the API 40% faster end-to-end, letting users experience the jump in inference speed from 65 to nearly 1,000 tokens per second." — Speeding up agentic workflows with WebSockets in the Responses API
📖 "We approached this through caching, eliminating unnecessary network hops, improving our safety stack to quickly flag issues, and—most importantly—building a way to create a persistent connection to the Responses API, instead of having to make a series of synchronous API calls." — Speeding up agentic workflows with WebSockets in the Responses API
🧪 实例 2025 年 11 月的性能冲刺,针对单个请求关键路径落地了三类优化:
text
1. Caching rendered tokens and model configuration in memory
   -> 跳过多轮响应里昂贵的 tokenization 与网络调用
2. Reducing network hop latency
   -> 消除对中间服务(如图像处理分辨率)的调用, 直连 inference service
3. Improving safety stack
   -> 更快运行某些 classifier 来标记对话
结果: 接近 45% 的 TTFT 改善, 但对 GPT-5.3-Codex-Spark 仍不够
🔍 追问 45% 的 TTFT 改善已经很大,为什么还不够,非要上持久连接? → 因为这些是单请求优化,没有触及"每个后续请求都为完整历史重复付费"的结构性问题;相对 Spark 的模型速度,Responses API 开销仍然太大。
📚 拓展阅读
Q持久连接的核心思路是什么?为什么最终选了 WebSockets 而不是 gRPC?深挖·拓展🔥高频
websockets grpc transport design-tradeoff
⏱️ 现行
为了收紧设计,团队重新思考了传输协议:能否保持一条持久连接并缓存状态,而不是每个后续请求都建立一条新的 HTTP 连接、再把完整对话历史发过去?核心思路是只发送需要校验和处理的新信息,并在连接生命周期内把可复用状态缓存在内存里,从而减少冗余工作带来的开销。候选方案考虑过 WebSockets 和 gRPC 双向流(bidirectional streaming),最终选了 WebSockets——因为它作为一个简单的消息传输协议,用户不必改动他们 Responses API 的输入和输出形状(input and output shapes);它对开发者友好,且能以极小的破坏契合既有架构。这里的权衡很清楚:gRPC 双向流技术上也能做持久连接,但会强迫改动 API 形状与集成方式;WebSockets 在拿到"持久连接+状态缓存"收益的同时,把对开发者集成的侵入降到最低。
术语 transport protocol(传输协议); gRPC bidirectional streaming(gRPC 双向流,被评估但未选用的方案); input and output shapes(API 的输入/输出数据形状); connection lifetime(连接生命周期,缓存状态的存活范围)
📖 "could we keep a persistent connection and cache state, rather than establishing a new connection over HTTP and sending the full conversation history for each follow-up request?" — Speeding up agentic workflows with WebSockets in the Responses API
📖 "We landed on WebSockets because as a simple message transport protocol, users wouldn't have to change their Responses API input and output shapes. It was developer-friendly and fit our existing architecture with little disruption." — Speeding up agentic workflows with WebSockets in the Responses API
🧪 实例 传统 HTTP 与 WebSocket 两种传输的对比:
text
HTTP(旧):每个 follow-up 请求
  - 新建连接
  - 发送完整对话历史
  - 服务端从头重建对话状态

WebSocket(新):一条持久连接
  - 只发送需要校验/处理的新信息
  - 服务端在连接生命周期内内存缓存可复用状态
  - 减少冗余重复工作
🔍 追问 WebSockets 和 gRPC 双向流都能"双向长连",决定性差别在哪? → 关键不在能不能双向流,而在 WebSockets 让用户无需改动 Responses API 的输入输出形状、对既有架构破坏最小,开发者友好度胜出。
📚 拓展阅读
Q最初的 WebSocket 原型是怎么把 agentic rollout 建模成"一次长跑的 Response"的?深挖·拓展中频
prototype asyncio sampling-loop hosted-tool
⏱️ 早期原型
第一个 WebSocket 原型改变了团队对 Responses API 延迟极限的认知——一位在 API 栈上有深厚积累的 Codex 工程师靠"让 Codex agent 跑一整晚"拼出了原型。在该原型里,agentic rollouts 被建模成单个长时间运行的 Response。利用 asyncio 特性,Responses API 在采样出一个 tool call 后会在 sampling loop 里异步阻塞(asynchronously block),并向客户端发回一个 response.done 事件;客户端执行完 tool call 后送回一个带工具结果的 response.append 事件,解除阻塞,让模型继续采样。一个类比是把本地工具调用当成 hosted tool call:当模型调用 web search 时,推理循环阻塞、调用 web search 服务、把服务响应放进模型上下文;这个设计做的是同一件事,只是不去调远程服务,而是把模型的 tool call 通过 WebSocket 发回客户端,客户端响应后把结果放进上下文继续采样。它极其有效,因为消除了一次 agent rollout 中重复的 API 工作:preinference 只做一次,暂停执行工具,最后 postinference 只做一次。代价是 API 形状变得不那么熟悉、更复杂,而团队希望开发者能直接接入 WebSocket 而不必围绕新交互模式重写集成。
术语 agentic rollout(智能体推演,一整段 agent 执行过程); sampling loop(采样循环,模型逐 token 生成的循环); response.done / response.append(原型里阻塞/解除阻塞的两个事件); hosted tool call(托管工具调用,如 web search); preinference / postinference(推理前/后处理工作)
📖 "Using asyncio features, the Responses API would asynchronously block in the sampling loop after a tool call was sampled, and the Responses API would send a response.done event back to the client. After executing the tool call, clients would send back a response.append event with the tool result, which unblocked the sampling loop and let the model continue." — Speeding up agentic workflows with WebSockets in the Responses API
📖 "This design was extremely effective because it eliminated repeated API work across an agent rollout. We could do preinference work once, pause for tool execution, and do postinference work once at the end." — Speeding up agentic workflows with WebSockets in the Responses API
🧪 实例 把本地 tool call 当作 hosted tool call 的采样阻塞流程:
flowchart TD
  A[模型采样出 tool call] --> B[Responses API 在 sampling loop 异步阻塞]
  B --> C[发送 response.done 事件给客户端]
  C --> D[客户端本地执行 tool]
  D --> E[客户端回送 response.append + 工具结果]
  E --> F[解除阻塞, 结果入上下文, 模型继续采样]
🔍 追问 这个原型这么快,为什么没直接照原样发布? → 因为它带来了不熟悉、更复杂的 API 形状,团队不想让开发者围绕新交互模式重写集成,所以最终版换回了熟悉的形状。
📚 拓展阅读
Q上线版本如何在保持 API 熟悉形状的同时拿到持久连接收益?连接级缓存里存了什么?深挖·拓展中频
previous_response_id in-memory-cache api-design
⏱️ 现行
为了不让开发者重写集成,上线版本换回了熟悉的形状:继续用 response.create 且请求体不变,并用 previous_response_id 从上一个响应的状态继续对话上下文。在一条 WebSocket 连接上,服务端维护一份连接作用域(connection-scoped)、驻留内存的先前响应状态缓存;当后续 response.create 带上 previous_response_id 时,就从缓存里取这份状态,而不是从头重建完整对话。被缓存的状态包括:先前的 response 对象、之前的输入与输出 items、工具定义与命名空间(tool definitions and namespaces)、以及可复用的采样产物(如之前已 render 的 token)。靠复用这份内存里的先前响应状态,团队落地了几项主要优化:让部分安全分类器与请求校验器只处理新输入而非每次都跑完整历史、维护一份可追加的 rendered token 内存缓存以跳过不必要的 tokenization、跨请求复用成功的模型解析/路由逻辑、以及把 billing 这类非阻塞 postinference 工作与后续请求重叠(overlap)。目标是尽量逼近那个最小开销原型,但用开发者已经理解并围绕其构建的 API 形状。
术语 previous_response_id(用于从上一响应状态续接上下文的字段); connection-scoped in-memory cache(连接作用域的内存缓存); tool definitions and namespaces(工具定义与命名空间); overlapping postinference work(把 billing 等非阻塞后处理与后续请求重叠)
📖 "keep using response.create with the same body, and use previous_response_id to continue the conversation context from the previous response’s state." — Speeding up agentic workflows with WebSockets in the Responses API
📖 "On a WebSocket connection, the server keeps a connection-scoped, in-memory cache of previous response state. When a follow-up response.create includes previous_response_id, we fetch that state from the cache instead of rebuilding the full conversation from scratch." — Speeding up agentic workflows with WebSockets in the Responses API
🧪 实例 后续请求命中连接级缓存的伪流程:
json
{
  "type": "response.create",
  "previous_response_id": "resp_abc123",
  "body": { "input": "只发送新增的工具结果 / 新输入" }
}

服务端凭 previous_response_id 从内存缓存取回:previous response 对象、prior input/output items、tool definitions & namespaces、以及 previously rendered tokens——省去从零重建完整对话。
🔍 追问 复用内存里的先前响应状态,具体让"安全分类器"省了什么? → 让部分 safety classifiers 和 request validators 只处理新输入,而不必每次都跑完整历史,从而减少重复开销。
📚 拓展阅读
QWebSocket mode 上线后的实际效果如何?对"推理越来越快"的行业趋势意味着什么?深挖·拓展中频
production throughput ecosystem
⏱️ 现行
经过两个月冲刺构建 WebSocket mode,团队先与关键编码 agent 初创公司发布了 alpha,让他们接入自家基础设施并安全放量;alpha 用户报告在其 agentic 工作流里获得最高 40% 的改善。上线结果立竿见影:Codex 迅速把大部分 Responses API 流量切到 WebSocket mode,获得显著延迟改善;对 GPT‑5.3‑Codex‑Spark,团队达成了 1,000 TPS 目标,并看到高达 4,000 TPS 的突发,证明 Responses API 能在真实生产流量里跟上快得多的推理。生态影响也很快:Vercel 把 WebSocket mode 集成进 AI SDK,延迟最多下降 40%;Cline 的多文件工作流快了 39%;Cursor 里的 OpenAI 模型最多快 30%。这是 Responses API 自 2025 年 3 月推出以来最重要的新能力之一,从想法到生产只用了几周,靠 API 与 Codex 团队的紧密协作完成;它不仅大幅改善 agent rollout 延迟,还回应了构建者日益增长的需求——随着模型推理变快,围绕推理的服务与系统也必须一起提速,才能把这些收益真正传递给用户。
术语 alpha(小范围早期发布,与关键初创公司合作放量); bursts up to 4,000 TPS(突发吞吐,证明 API 能跟上更快推理); AI SDK(Vercel 集成 WebSocket mode 的 SDK); agent rollout latency(智能体推演延迟)
📖 "For GPT‑5.3‑Codex‑Spark, we hit our 1,000 TPS target and saw bursts up to 4,000 TPS, showing that the Responses API could keep up with much faster inference in real production traffic." — Speeding up agentic workflows with WebSockets in the Responses API
📖 "as model inference gets faster, the services and systems that surround inference also need to speed up to transfer these gains to users." — Speeding up agentic workflows with WebSockets in the Responses API
🧪 实例 上线后的生态提速数据:
text
Codex          : 大部分 Responses API 流量切到 WebSocket, 显著降延迟
GPT-5.3-Codex-Spark : 达成 1,000 TPS 目标, 突发至 4,000 TPS
Vercel AI SDK  : 延迟最多下降 40%
Cline          : 多文件工作流快 39%
Cursor         : OpenAI 模型最多快 30%
🔍 追问 4,000 TPS 的突发说明了什么工程结论? → 说明优化后的 Responses API(CPU/服务侧)已能在真实生产流量里跟上远快于以往的模型推理,不再是把 GPU 收益卡在 API 开销上的瓶颈。
📚 拓展阅读
QOpenAI 的实时语音栈为什么要把 WebRTC 拆成 relay + transceiver 两层?各自负责什么?深挖·拓展🔥高频
WebRTC Relay Transceiver
⏱️ 现行
核心动机是把"包的路由"和"协议的终结"这两件本来耦合在一起的事拆开,从而让有状态的 WebRTC 会话能跑在 Kubernetes 这种弹性、可调度的基础设施上。传统 WebRTC 是 one-port-per-session:每个会话占一个公网 UDP 端口,这在云负载均衡器和 K8s 里既难暴露、难加固,也不适合 Pod 频繁增删重调度的自动扩缩。于是 OpenAI 把系统切成两层:relay 是一个轻量的、无协议状态的 UDP 转发层,只读取足够的包元数据来选目的地,然后把包转发给拥有该会话的 transceiver——它不解密媒体、不跑 ICE 状态机、不参与编解码协商,因此公网暴露面很小、可以水平扩展、重启只造成极小流量抖动。transceiver 才是真正终结 WebRTC 的有状态端点,独占 ICE 连通性检查、DTLS 握手、SRTP 加密密钥和整个会话生命周期。这样做的权衡是:多了一跳转发、并且需要 relay 与 transceiver 之间的自定义协调;换来的是把"硬状态"集中在一处、会话归属清晰,后端推理服务可以像普通服务那样扩容而不必扮演 WebRTC peer。对客户端而言,它看到的仍是一条标准 WebRTC 流,毫无感知。

flowchart LR
  Client -->|"signaling (SDP/ICE)"| Transceiver
  Client -->|"media (STUN/DTLS/RTP)"| Relay
  Relay -->|"forward by ufrag"| Transceiver
  Transceiver -->|"internal protocols"| Backend["inference / TTS / tools"]
术语 relay(无状态 UDP 转发层,公网暴露面小); transceiver(有状态 WebRTC 终结端点,持有 ICE/DTLS/SRTP); termination(终结,即完成握手并加解密媒体的那一端)
📖 "The architecture we shipped splits packet routing from protocol termination." — How OpenAI delivers low-latency voice AI at scale
📖 "The relay does not decrypt media, run ICE state machines, or participate in codec negotiation. It reads enough packet metadata to choose a destination, then forwards the packet to the transceiver that owns the session." — How OpenAI delivers low-latency voice AI at scale
🧪 实例 一个 ChatGPT voice 会话建立时,transceiver 分配会话状态并在 SDP answer 里返回一个共享的 relay VIP 与 UDP 端口(如 203.0.113.10:3478);客户端把首个 STUN 包发到这个稳定地址,relay 据此把包路由到拥有会话的 transceiver,之后 DTLS/RTP/RTCP 都在同一会话内流转。
🔍 追问 relay 重启丢了会话映射会怎样? → 下一个 STUN 包会用 ufrag 里的路由提示重建会话;为更可靠还用 Redis 缓存 <client IP + Port, transceiver IP + Port> 映射以便更早恢复。
📚 拓展阅读
Q面对 1:1 语音这种流量形态,为什么 OpenAI 选择 transceiver 模型而不是默认的 SFU?深挖·拓展🔥高频
SFU Transceiver 媒体架构
⏱️ 现行
关键在于流量形态与 SFU 的适用场景不匹配。SFU(selective forwarding unit)是一个媒体服务器,从每个参与者接收一条 WebRTC 流并选择性地转发给其他人,天然适合群组通话、教室、协作会议这类本质多方的产品——它把编解码、RTCP、data channel、录制、逐流策略集中在一处,也常被当作默认起点因为一套系统就能复用信令、媒体路由、录制、可观测性以及未来的人工接管扩展。但 OpenAI 的负载不同:绝大多数会话是 1:1——一个用户对一个模型、或一个应用对一个实时 agent——且每一轮对话都对延迟敏感。对这种形态,SFU 让后端要像 WebRTC peer 一样行事就成了负担。因此他们选了 transceiver 模型:一个 WebRTC 边缘服务终结客户端连接,再把媒体和事件转换成更简单的内部协议,交给推理、转写、语音生成、工具调用和编排。这样后端服务能像普通服务一样扩容,会话的硬状态(ICE、DTLS、SRTP、生命周期)集中在 transceiver 一处,归属清晰、易于推理。事后结果也证实:对以点对点、延迟敏感为主的负载,SFU-less 是正确的默认选择。
术语 SFU(选择性转发单元,适合多方会话); 1:1 session(点对点会话,OpenAI 的主流量形态); transceiver model(边缘终结 WebRTC 再转内部协议的模型)
📖 "a media server that receives one WebRTC stream from each participant and selectively forwards streams to the others." — How OpenAI delivers low-latency voice AI at scale
📖 "Most sessions are 1:1—one user talking to one model, or one application talking to one real-time agent—with latency sensitivity on every turn." — How OpenAI delivers low-latency voice AI at scale
🧪 实例 如果 OpenAI 用 SFU,AI 会作为"另一个参与者"加入会话,后端推理服务需要维持 WebRTC peer 行为;改用 transceiver 后,边缘把音频转成内部协议流,推理后端只是普通可扩服务,不再需要跑 ICE/DTLS。
🔍 追问 那 SFU 完全没价值吗? → 并非,SFU 对群组通话、classroom、协作会议这类天然多方产品是好选择,还能把 codec、RTCP、录制、逐流策略集中管理;只是不匹配 OpenAI 以 1:1 延迟敏感为主的负载。
📚 拓展阅读
QOpenAI 内部数据 agent 为什么要围绕"多层上下文"来构建?这些层分别提供什么?深挖·拓展🔥高频
Data Agent Context Grounding
⏱️ 现行
因为高质量答案依赖丰富、准确的上下文,缺了它,即便很强的模型也会算错——比如把用户数估计得离谱,或误解内部术语。OpenAI 的数据平台服务 3.5k+ 内部用户、70k 数据集、600+ PB 数据,光是"找对表"就可能是分析里最耗时的一步,而且选对表后,many-to-many join、filter pushdown 错误、未处理的 null 都可能悄悄让结果失真。为把这些失败模式挡在外面,agent 被构建在多层上下文之上,把它锚定在 OpenAI 的数据与机构知识里:第 1 层 Table Usage(schema 元数据 + table lineage + 历史查询推断常一起 join 的表);第 2 层 Human Annotations(领域专家写的表/列描述,捕捉 schema 推不出的意图、语义、已知坑);第 3 层 Codex Enrichment(从代码推导表的定义,理解数据到底存了什么、如何派生、粒度与新鲜度);第 4 层 Institutional Knowledge(Slack、Google Docs、Notion 里的发布、事故、代号、指标口径);第 5 层 Memory(把纠正与非显然的约束存下来复用);第 6 层 Runtime Context(无上下文或已过期时直接对数仓发实时查询验证 schema)。这些层叠加,让 agent 的推理扎根于数据、代码与机构知识,显著降低错误、提升答案质量。
术语 table lineage(表血缘,上下游关系); human annotations(人工标注,领域专家补充语义与坑); runtime context(运行时上下文,实时查询数仓验证)
📖 "The agent relies on schema metadata (column names and data types) to inform SQL writing and uses table lineage (e.g., upstream and downstream table relationships) to provide context on how different tables relate." — Inside OpenAI’s in-house data agent
📖 "Together, these layers ensure the agent’s reasoning is grounded in OpenAI’s data, code, and institutional knowledge, dramatically reducing errors and improving answer quality." — Inside OpenAI’s in-house data agent
🧪 实例 用户问"过去 30 天 ChatGPT Image Gen 登录 DAU 是多少":无 memory 时 agent 卡在 22m 41s 也查不好;有 memory 定位到正确的表后,1m 22s 就能给出结果——这正是上下文层直接影响正确性与速度的体现。
🔍 追问 为什么不干脆把所有原始元数据都塞给模型? → 数据量太大不现实;他们跑每日离线管线把各层聚合、embedding 化,查询时只用 RAG 拉最相关的上下文,保证运行时延迟可预测且低。
📚 拓展阅读
Q为什么说"表的真正含义活在代码里"?Codex Enrichment 这一层解决了什么单靠 schema/查询历史解决不了的问题?深挖·拓展中频
Codex Enrichment Data Lineage
⏱️ 现行
schema 和查询历史只能描述一张表的"形状"和"用法",却讲不清它的"含义"。表的真正含义活在产生它的代码里:pipeline 逻辑里藏着假设、新鲜度保证和业务意图,而这些在 SQL 或元数据里从不显现。很多表长得极像却在关键处不同——有的含登出用户有的不含、有的只包含第一方 ChatGPT 流量——单看 schema 根本分不清。Codex Enrichment 通过用 Codex 爬代码库,推导出表的代码级定义,从而理解数据究竟存了什么、如何从一个 analytics event 派生而来,包括值的唯一性、更新频率、数据范围与粒度;还能把表在 SQL 之外(Spark、Python 等系统)的使用方式也纳入,提供增强的用法上下文。这层能力回答"这里面有什么"和"我什么时候能用它"远比只靠数仓信号准确,而且上下文会自动刷新、无需人工维护。这也是团队总结的"Meaning Lives in Code"这条经验教训的直接来源。
术语 Codex enrichment(用 Codex 爬码推导表的代码级定义); grain / primary keys(粒度与主键,由代码而非 schema 揭示); data freshness(数据新鲜度,藏在 pipeline 逻辑里)
📖 "Schemas and query history describe a table’s shape and usage, but its true meaning lives in the code that produces it." — Inside OpenAI’s in-house data agent
📖 "By crawling the codebase with Codex, our agent understands how datasets are actually constructed and is able to better reason about what each table actually contains." — Inside OpenAI’s in-house data agent
🧪 实例 面对两张相似表,agent 借 Codex enrichment 能判断"某张表是否只包含第一方 ChatGPT 流量",从而在计算指标时选对表,避免把登出用户或非第一方流量混入。
🔍 追问 这些代码级上下文会过时吗? → 不会成为负担——该上下文自动刷新,保持最新而无需人工维护。
📚 拓展阅读
Qrelay 如何在"会话还不存在"时就把客户端的第一个包路由到正确的 transceiver?深挖·拓展中频
ICE ufrag First-packet routing
⏱️ 现行
首包路由是整套设计的关键难点:relay 必须在包路径上还没有任何会话时,就决定第一个包该去哪。他们的巧思是复用 WebRTC 里一个协议原生的路由钩子——ICE username fragment(ufrag),一个在会话建立时交换、并在 STUN 连通性检查里回显的短标识符。服务端 ufrag 由 OpenAI 生成,里面塞进刚好够用的路由元数据,让 relay 能推断出目标集群和拥有会话的 transceiver。客户端的首个媒体包通常是 STUN binding request,relay 只解析这第一个 STUN 包里足够的部分来读取 server ufrag、解码路由提示,然后把包转发给拥有会话的 transceiver;每个 transceiver 监听一个共享 UDP socket(一个 OS endpoint,而非每会话一个 socket)。一旦建立了从客户端源 IP:Port 到该 transceiver 的会话,后续 DTLS、RTP、RTCP 就在会话内流转,无需再解码 ufrag。这个设计的权衡是把路由决策保持在包路径上、不依赖外部查表服务(避免热路径上的查询依赖);relay 的会话刻意做得极简,只有内存里的转发映射加计数器和过期清理定时器,即便重启丢了会话,下一个 STUN 包也能用 ufrag 提示重建,再加 Redis 缓存让恢复更早发生。
术语 ufrag(ICE 用户名片段,承载路由元数据的协议原生字段); STUN binding request(客户端首个媒体包,用于验证可达性); shared UDP socket(共享套接字,一个 OS endpoint 复用多会话)
📖 "a short identifier exchanged during session setup and echoed in STUN connectivity checks." — How OpenAI delivers low-latency voice AI at scale
📖 "Relay parses just enough of that first STUN packet to read the server ufrag, decode the routing hint, and forward the packet to the owning transceiver." — How OpenAI delivers low-latency voice AI at scale
🧪 实例 结合 Global Relay 与 geo-steered signaling:SDP answer 给出就近的 Global Relay 地址,ufrag 里的信息足以让 Global Relay 把媒体路由到指定集群、再由 relay 路由到目的 transceiver,既把入口放到离用户近的地方,又把会话牢牢锚定在同一个 transceiver 上。
🔍 追问 为什么不用一个外部查表服务来决定首包去向? → 那会在热路径上引入查询依赖;把路由元数据编码进协议原生的 ufrag 字段,得到的是确定性首包路由,无需热路径查表。
📚 拓展阅读
Q数据 agent 的离线 RAG 管线和 Memory 层是怎么配合的?为什么需要单独的 Memory?深挖·拓展中频
RAG Embeddings Memory
⏱️ 现行
为了让"理解表"这件事在数万张表的规模上依然快且可扩,agent 跑一条每日离线管线,把 table usage、human annotations 和 Codex 派生的富化聚合成单一、规范化的表示,再用 OpenAI embeddings API 转成 embeddings 并存下来供检索;查询时 agent 只通过 RAG 拉取最相关的那部分嵌入上下文,而不是去扫原始元数据或日志,从而让运行时延迟可预测且低,需要时再对数仓发实时查询。Memory 层解决的是另一类问题:那些对数据正确性至关重要、却难以从其它层推断出来的非显然纠正、过滤条件和约束。当 agent 被纠正或从对话里发现细节时,它能把这些学习存下来下次复用,让未来的答案从更准的基线出发,而不是反复踩同一个坑;memory 分全局和个人两级,可由用户手动创建和编辑,agent 发现学习时也会提示你是否保存。一个典型例子是:agent 起初不知道如何为某个 analytics experiment 过滤(需要匹配 experiment gate 里定义的特定字符串),memory 在这里至关重要,确保它能正确过滤而不是模糊地做字符串匹配。
术语 offline pipeline(每日离线管线,聚合各层为规范化表示); RAG(检索增强,查询时只拉最相关嵌入); memory(记忆层,复用非显然的纠正与约束)
📖 "We run a daily offline pipeline that aggregates table usage, human annotations, and Codex-derived enrichment into a single, normalized representation." — Inside OpenAI’s in-house data agent
📖 "The goal of memory is to retain and reuse non-obvious corrections, filters, and constraints that are critical for data correctness but difficult to infer from the other layers alone." — Inside OpenAI’s in-house data agent
🧪 实例 某次 agent 不知道如何筛选一个特定实验分组,只会尝试模糊字符串匹配;把"应匹配 experiment gate 里定义的特定字符串"存进 memory 后,后续同类问题就能直接正确过滤。
🔍 追问 RAG 拉取的上下文和 runtime 实时查询是什么关系? → 二者互补:离线嵌入负责快速、可扩的表理解;当某表无先验上下文或信息陈旧时,agent 再对数仓发实时查询验证 schema、实时理解数据。
📚 拓展阅读
Q"WebRTC 遇上 Kubernetes"具体撞上了哪两个问题?为什么 one-port-per-session 不适合弹性调度?深挖·拓展中频
Kubernetes Port exhaustion State stickiness
⏱️ 现行
团队想让 transceiver 服务像其它基础设施一样跑在 Kubernetes 上——可扩缩、随需求在主机间迁移,但传统 one-port-per-session 的 WebRTC 模型与这个环境格格不入。第一个问题是端口耗尽(port exhaustion):高并发下每会话一个公网 UDP 端口意味着要暴露和管理极大的 UDP 端口范围,而云负载均衡器和 K8s service 并非围绕每服务数万公网 UDP 端口设计,每加一段范围都增加 LB 配置、健康检查、防火墙策略和发布安全的复杂度;大端口范围还扩大了外部可达面、难以审计,更与自动扩缩相冲突——Pod 不断增删重调度,却要求每个 Pod 预留并广告一大段稳定端口,弹性就变脆。这促使很多 WebRTC 系统转向"每服务器单 UDP 端口 + 应用层解复用"。但这又引出第二个问题:状态黏性(state stickiness)。ICE 和 DTLS 是有状态协议,创建会话的进程必须持续收到该会话的包,才能验证连通性检查、完成 DTLS 握手、解密 SRTP、处理后续的 ICE restart;若同会话的包落到别的进程,建连会失败或媒体会中断。于是目标很明确:对公网只暴露一小块固定 UDP 面,同时把每个包都路由到拥有对应 WebRTC 会话的那个 transceiver——这正是 relay + transceiver 架构要解决的。
术语 port exhaustion(端口耗尽,每会话占一个公网 UDP 端口不可扩); state stickiness(状态黏性,同会话包必须回到创建它的进程); ICE restart(ICE 重启,属于需持续会话状态处理的后续变更)
📖 "But the conventional one-port-per-session WebRTC model fits that environment poorly, because it depends on large public UDP port ranges that are difficult to expose, secure, and preserve as pods are added, removed, or rescheduled." — How OpenAI delivers low-latency voice AI at scale
📖 "The process that created a session needs to keep receiving that session’s packets so it can validate connectivity checks, complete the DTLS handshake, decrypt SRTP, and process later session changes such as ICE restarts." — How OpenAI delivers low-latency voice AI at scale
🧪 实例 即便改成"每服务器单 UDP 端口",在负载均衡的机群里第一个包仍可能落到错误实例——单机解复用只在包到达该机后才起作用,跨机群你仍需一种确定性方式把每个会话导向拥有它的进程,这正是 ufrag 首包路由的用武之地。
🔍 追问 relay 会不会因为要保存会话状态而重新变得"黏"? → 不会,relay 的状态不是硬 WebRTC 状态,只是短超时的内存流映射,重启只造成极小流量丢失和快速恢复;硬状态全在 transceiver。
📚 拓展阅读
Q一个"永远在线、持续进化"的数据 agent 如何防止质量漂移?OpenAI 用 Evals 做了什么?深挖·拓展中频
Evals Regression Quality
⏱️ 现行
构建一个 always-on、不断演进的 agent 意味着质量既可能提升也可能悄悄退化;没有紧的反馈回路,回归就会既不可避免又不可见,唯一能在不破坏信任的前提下扩展能力的办法就是系统化评估。OpenAI 用 OpenAI Evals API 来度量并保护 agent 的回答质量。它的 evals 建立在精心整理的问答对上:每个问题针对一个他们非常在意要答对的重要指标或分析模式,并配一条人工编写的"golden"SQL 作为期望结果。对每个 eval,他们把自然语言问题发给 agent 的查询生成端点,执行生成的 SQL,再把输出与期望 SQL 的结果比较。评估不靠朴素的字符串匹配——生成的 SQL 可能语法不同但仍正确,结果集也可能多出无关紧要的列,所以他们同时比较 SQL 与结果数据,把这些信号喂给 Evals grader,由它产出最终分数加解释,既捕捉正确性也容纳可接受的差异。这些 evals 就像持续运行的单元测试,在开发中充当生产里的金丝雀来尽早发现回归,让团队在 agent 能力扩张时也能自信迭代。
术语 golden SQL(人工编写的期望查询,作为对照基准); Evals grader(评分器,产出分数+解释,容忍非实质差异); canary(金丝雀,持续运行以尽早发现回归)
📖 "For each eval, we send the natural language question to its query-generation endpoint, execute the generated SQL, and compare the output against the result of the expected SQL." — Inside OpenAI’s in-house data agent
📖 "Generated SQL can differ syntactically while still being correct, and result sets may include extra columns that don’t materially affect the answer." — Inside OpenAI’s in-house data agent
📖 "meaning users can only query tables they already have permission to access." — Inside OpenAI’s in-house data agent
🧪 实例 一个 eval 问"某指标在给定时间段的值",agent 生成的 SQL 与 golden SQL 写法不同、还多返回了一列;不做字符串匹配,而是同时比对 SQL 与结果 dataframe 并交给 grader,最终判定为正确并给出解释。
🔍 追问 除了 evals,agent 还靠什么维持可信? → 严格 pass-through 的访问控制——用户只能查自己本就有权限的表;缺权限时会标注或回退到用户被授权的替代数据集,并通过展示假设与执行步骤、链接到底层结果来保持透明。
📚 拓展阅读

第15章 · 大规模系统与可靠性

🔥高频

扩展 PostgreSQL 支撑 8 亿 ChatGPT 用户

Scaling PostgreSQL to power 800 million ChatGPT users
QOpenAI 为什么用"单主 + 近 50 个只读副本"的非分片 PostgreSQL 架构就能支撑 8 亿用户、每秒数百万次查询?深挖·拓展🔥高频
架构 read-heavy single-primary
⏱️ 现行
核心洞察是 PostgreSQL 对 read-heavy 工作负载的可扩展性远超很多人的预期。OpenAI 用一个单主(single primary)Azure PostgreSQL flexible server 实例承接所有写入,再水平扩展出近 50 个只读副本(read replica)跨多个地理区域分布,让读流量尽量下沉到副本,主库只处理写和必须留在写事务中的读。之所以坚持不分片,是权衡取舍的结果:对现有应用做分片极其复杂耗时,要改动数百个应用端点,可能耗时数月甚至数年;而由于负载以读为主且已做了大量优化,当前架构仍有充足余量(runway)支撑增长。为持续给主库减压,他们把可分片、写重的负载迁到 Azure Cosmos DB 等分片系统,并且不再允许向现有 PostgreSQL 新增表,新负载默认落到分片系统。分片 PostgreSQL 不是近期优先级但也没被排除。
术语 single primary(单主实例,唯一写入点); read replica(只读副本,承接读流量); read-heavy workload(读密集型负载); unsharded(未分片,所有写走一个主库); runway(增长余量)
📖 "It may sound surprising that a single-primary architecture can meet the demands of OpenAI’s scale; however, making this work in practice isn’t simple." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "Even as our infrastructure has evolved, PostgreSQL has remained unsharded, with a single primary instance serving all writes. The primary rationale is that sharding existing application workloads would be highly complex and time-consuming, requiring changes to hundreds of application endpoints and potentially taking months or even years." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 该架构的生产成绩:近 50 个只读副本,复制延迟接近零,跨地理区域低延迟读;客户端 p99 延迟稳定在两位数毫秒,达到 five-nines(99.999%)可用性;过去 12 个月只发生过一次 SEV-0 PostgreSQL 事故。
flowchart TD
  App[应用层] -->|写 + 事务内读| Primary[(单主 Primary)]
  App -->|大部分读| R1[(Replica 1)]
  App -->|大部分读| R2[(Replica ~50)]
  Primary -->|WAL 流复制| R1
  Primary -->|WAL 流复制| R2
🔍 追问 既然读写分离,为什么还有读查询必须留在主库? → 因为它们是写事务的一部分(part of write transactions),事务内的读要看到自己未提交的写,只能在主库执行;对这类查询的策略是保证高效、避免慢查询。
📚 拓展阅读
QPostgreSQL 过载时常见的"恶性循环(vicious cycle)"是怎么形成的?深挖·拓展🔥高频
可靠性 级联失败 SEV
⏱️ 现行
单主架构在 OpenAI 的规模下能跑通,但很脆弱,历史上出过多次由 Postgres 过载引发的 SEV,且往往遵循同一个模式:某个上游问题导致数据库负载突然飙升——比如缓存层故障造成的大面积 cache miss、大量多表 join 把 CPU 打满、或新功能上线引发的写风暴。随着资源利用率攀升,查询延迟上升、请求开始超时;超时触发重试(retry),重试进一步放大负载,形成一个有可能把整个 ChatGPT 和 API 服务拖垮的恶性循环。这解释了为什么后续所有优化(减主库负载、查询优化、限流、缓存加锁、连接池)本质上都是在打断这个正反馈环:要么削峰输入负载,要么在过载时快速卸载(load shedding)。
术语 SEV(严重事故 severity incident); cache miss(缓存未命中); write storm(写风暴); retry(重试,会放大负载); vicious cycle(恶性循环/正反馈过载环)
📖 "an upstream issue causes a sudden spike in database load, such as widespread cache misses from a caching-layer failure, a surge of expensive multi-way joins saturating CPU, or a write storm from a new feature launch." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "As resource utilization climbs, query latency rises and requests begin to time out. Retries then further amplify the load, triggering a vicious cycle with the potential to degrade the entire ChatGPT and API services." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 过去 12 个月唯一一次 SEV-0 就发生在 ChatGPT ImageGen 病毒式爆发期间:一周内超过 1 亿新用户注册,写流量突然暴涨超过 10 倍,正是这种上游冲击 → 负载飙升的典型触发场景。
🔍 追问 为什么"缩短重试间隔"反而危险? → 过短的 retry interval 会在服务已经过载时制造 retry storm,把更多请求瞬间压向数据库,加速恶性循环;所以限流设计里明确要避免过短重试。
📚 拓展阅读
Q为什么说 PostgreSQL 的 MVCC 实现对写重负载不友好?写放大和读放大是怎么来的?深挖·拓展🔥高频
MVCC write-amplification autovacuum
⏱️ 现行
PostgreSQL 用多版本并发控制(MVCC)实现事务隔离,代价是对写重负载效率偏低。关键机制:当一条查询更新一个 tuple 甚至只改一个字段时,PostgreSQL 会把整行复制出来生成一个新版本。在高写入负载下这导致显著的写放大(write amplification);同时也加剧读放大(read amplification),因为查询必须扫过同一行的多个版本(即 dead tuples)才能取到最新版。MVCC 还带来一系列附加问题:表和索引膨胀(bloat)、索引维护开销上升、以及复杂的 autovacuum 调优。正因为写这么"贵",OpenAI 的整体策略才是把写重负载尽量迁走、并在应用层激进优化以减少写入,把宝贵的主库写能力留给真正必须的写。
术语 MVCC(多版本并发控制); tuple(元组/行版本); write amplification(写放大,改一行要复制整行); read amplification(读放大,要扫过多个死版本); dead tuples(死元组); bloat(表/索引膨胀); autovacuum(自动清理死元组的后台进程)
📖 "For example, when a query updates a tuple or even a single field, the entire row is copied to create a new version. Under heavy write loads, this results in significant write amplification. It also increases read amplification, since queries must scan through multiple tuple versions (dead tuples) to retrieve the latest one." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 因为死元组会阻塞 autovacuum,他们强调配置超时:
sql
-- 防止长时间空闲的事务卡住 autovacuum,让死元组无法被回收
SET idle_in_transaction_session_timeout = '...';

文中原句:配置像 idle_in_transaction_session_timeout 这样的超时对于防止空闲查询阻塞 autovacuum 至关重要。
🔍 追问 面对 MVCC 的写放大,OpenAI 在架构上怎么应对? → 把可分片的写重负载迁到 Azure Cosmos DB 等分片系统,禁止向 PostgreSQL 新增表,新负载默认落分片系统,并在应用层修复冗余写、引入 lazy write 削峰。
📚 拓展阅读
Q单写节点是单点故障(SPOF),OpenAI 如何降低它宕机的影响?深挖·拓展🔥高频
高可用 SPOF hot-standby
⏱️ 现行
只读副本挂了可以把流量切到其它副本,但只有一个写节点就意味着单点故障——它一旦宕机,整个服务受影响。OpenAI 的缓解分两层。第一层是"降级而非全挂":由于绝大多数关键请求只涉及读查询,他们把这些读从写节点卸载到副本,这样即使主库宕机,读请求仍能继续服务,只有写会失败,影响被缩小,事故等级从 SEV0 降下来。第二层是主库本身的高可用:主库以 HA 模式运行,配一个 hot standby——一个持续同步、随时可接管的副本;当主库宕机或需要下线维护时,可以快速把 standby 提升(promote)为主,最小化停机。Azure PostgreSQL 团队为这类 failover 在极高负载下的安全可靠做了大量工作。对副本故障,则在每个区域部署多个带足够容量余量的副本,确保单副本失效不会造成区域性中断。
术语 SPOF(单点故障 single point of failure); hot standby(热备,持续同步的待命副本); HA mode(高可用模式); promote(把 standby 提升为主库); failover(故障切换); capacity headroom(容量余量)
📖 "Most critical requests only involve read queries. To mitigate the single point of failure in the primary, we offloaded those reads from the writer to replicas, ensuring those requests can continue serving even if the primary goes down. While write operations would still fail, the impact is reduced; it’s no longer a SEV0 since reads remain available." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "To mitigate primary failures, we run the primary in High-Availability (HA) mode with a hot standby, a continuously synchronized replica that is always ready to take over serving traffic." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 副本层的容量策略:每个区域多副本 + 容量余量,使得"a single replica failure doesn’t lead to a regional outage"——单个副本失效不至于拖垮整个区域。
🔍 追问 主库宕机时写为什么还是会失败? → 因为写只能落到唯一的主库/被提升的 standby,读卸载只保住了读路径;这是单主架构的固有取舍——用读可用性换来降级而非彻底不可用,但写在切换窗口内仍不可用。
📚 拓展阅读
Q面对每实例 5000 连接上限,OpenAI 如何用连接池避免连接耗尽?深挖·拓展🔥高频
连接池 PgBouncer Kubernetes
⏱️ 现行
每个实例有最大连接数上限(Azure PostgreSQL 为 5000),很容易耗尽连接或积累过多空闲连接,历史上就有过 connection storm 打光所有可用连接的事故。解决方案是部署 PgBouncer 作为代理层来池化数据库连接:让它跑在 statement 或 transaction 池化模式下,就能高效复用连接,大幅减少活跃客户端连接数。这还顺带降低了连接建立延迟——基准测试里平均连接时间从 50ms 降到 5ms。由于跨区域连接和请求代价高,他们把代理、客户端、副本共置(co-locate)在同一区域,以最小化网络开销和连接占用时间。PgBouncer 本身也必须小心配置,像 idle timeout 这类设置对防止连接耗尽至关重要。部署形态上,每个只读副本有自己的 Kubernetes deployment 跑多个 PgBouncer pod,多个 deployment 放在同一个 Kubernetes Service 后面做跨 pod 负载均衡。
术语 PgBouncer(轻量连接池代理); transaction/statement pooling(事务/语句级池化模式); connection storm(连接风暴,瞬间打满连接); co-locate(代理、客户端、副本同区共置); idle timeout(空闲超时,防连接耗尽); Kubernetes Service(做 pod 负载均衡)
📖 "We deployed PgBouncer as a proxy layer to pool database connections. Running it in statement or transaction pooling mode allows us to efficiently reuse connections, greatly reducing the number of active client connections. This also cuts connection setup latency: in our benchmarks, the average connection time dropped from 50 milliseconds (ms) to 5 ms." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "Each read replica has its own Kubernetes deployment running multiple PgBouncer pods. We run multiple Kubernetes deployments behind the same Kubernetes Service, which load-balances traffic across pods." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 连接建立延迟对比——池化前后平均连接时间 50ms → 5ms,10 倍改善,同时把数千客户端收敛到少量到数据库的实际连接。
🔍 追问 为什么要把 proxy、client、replica 放同一区域? → 因为跨区域(inter-region)连接和请求昂贵,共置能最小化网络开销和连接占用时间,避免连接被跨区往返长时间占用而变相消耗连接池容量。
📚 拓展阅读
Q昂贵查询(如多表 join)如何拖垮服务?OpenAI 的查询优化原则是什么?深挖·拓展中频
查询优化 OLTP ORM
⏱️ 现行
少数昂贵查询——尤其是把很多表 join 在一起的——可能显著拖慢甚至拖垮整个服务,过去这类查询的流量骤增会吃掉大量 CPU,拖慢 ChatGPT 和 API。所以要持续优化 PostgreSQL 查询、避免常见的 OLTP 反模式。具体原则:尽量避免复杂多表 join;若 join 不可避免,考虑把查询拆开、把复杂 join 逻辑挪到应用层去做。很多有问题的查询是 ORM 框架生成的,因此要仔细审查它们产出的 SQL、确认行为符合预期。此外 PostgreSQL 里常见长时间运行的空闲查询,配置像 idle_in_transaction_session_timeout 这样的超时对于防止它们阻塞 autovacuum 至关重要。
术语 OLTP anti-pattern(联机事务处理反模式); multi-table join(多表连接); ORM(对象关系映射框架,易生成低效 SQL); query digest(查询摘要,可用于精准封禁); idle query(空闲查询)
📖 "For example, we once identified an extremely costly query that joined 12 tables, where spikes in this query were responsible for past high-severity SEVs. We should avoid complex multi-table joins whenever possible. If joins are necessary, we learned to consider breaking down the query and move complex join logic to the application layer instead." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "Many of these problematic queries are generated by Object-Relational Mapping frameworks (ORMs), so it’s important to carefully review the SQL they produce and ensure it behaves as expected." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 一个真实教训是曾发现一条 join 了 12 张表的查询,它的流量尖峰要为过去多次 high-severity SEV 负责;应对方式是拆查询、把 join 逻辑上移到应用层。
🔍 追问 当某类昂贵查询突然激增无法及时改代码时,怎么快速止血? → 增强 ORM 层支持限流,必要时按 query digest 完全封禁特定查询;这种定向 load shedding 能让系统从昂贵查询骤增中快速恢复。
📚 拓展阅读
Qcache miss 风暴会如何冲击数据库?cache locking / leasing 机制怎么防止它?深挖·拓展中频
缓存 cache-stampede leasing
⏱️ 现行
OpenAI 用缓存层承接大部分读流量给 PostgreSQL 减压,但当缓存命中率意外下降时,一波 cache miss 会把大量请求直接压向 PostgreSQL,骤增的数据库读吃掉大量资源、拖慢服务。为防止 cache-miss 风暴造成过载,他们实现了 cache locking(以及 leasing)机制:对某个特定 key,只让一个未命中的读者去 PostgreSQL 取数据。当多个请求同时 miss 同一个 cache key 时,只有一个请求拿到锁、去取数据并回填缓存,其余请求等待缓存被更新,而不是一起冲击 PostgreSQL。这显著减少了冗余的数据库读,保护系统免受级联式负载尖峰——本质是解决缓存击穿/惊群(cache stampede)问题。
术语 cache locking(缓存加锁,同一 key 只放一个回源); leasing(租约机制); cache miss storm(缓存未命中风暴); repopulate the cache(回填缓存); cascading load spikes(级联负载尖峰)
📖 "To prevent overload during cache-miss storms, we implement a cache locking (and leasing) mechanism so that only a single reader that misses on a particular key fetches the data from PostgreSQL." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "When multiple requests miss on the same cache key, only one request acquires the lock and proceeds to retrieve the data and repopulate the cache. All other requests wait for the cache to be updated rather than all hitting PostgreSQL at once." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 惊群拦截逻辑示意——
python
# 同一 key 同时 miss 时,只有拿到锁的那个请求回源,其余等待回填
if not cache.get(key):
    if lock.acquire(key):        # 只有一个 reader 成功
        value = db.fetch(key)    # 单点回源 PostgreSQL
        cache.set(key, value)    # repopulate the cache
        lock.release(key)
    else:
        wait_for_cache(key)      # 其余请求等待,不冲击 DB
🔍 追问 这个机制和"vicious cycle"是什么关系? → cache miss 正是文中列举的负载飙升上游诱因之一;cache locking 从源头削掉了缓存层故障时冲向 PostgreSQL 的冗余读,是打断恶性循环的输入侧削峰手段。
📚 拓展阅读
Q只读副本不能无限加——WAL 流复制的瓶颈在哪?cascading replication 如何破局?深挖·拓展中频
复制 WAL cascading-replication
⏱️ 演进中
主库要把 Write Ahead Log(WAL)流式发送给每一个只读副本。随着副本数量增加,主库要把 WAL 发往更多实例,网络带宽和 CPU 压力都上升,导致 replica lag 更高、更不稳定,系统更难可靠扩展。OpenAI 目前运营近 50 个跨区域副本,虽然靠很大的实例规格和高网络带宽还能撑住,但不可能无限加副本而不最终压垮主库。破局思路是和 Azure PostgreSQL 团队合作引入 cascading replication(级联复制):由中间副本把 WAL 中继给下游副本,从而把主库的扇出压力卸载出去,理论上可扩展到上百个副本而不压垮主库。代价是引入额外运维复杂度,尤其是 failover 管理;该特性仍在测试,要确保健壮、能安全 failover 后才会上生产。
术语 WAL(预写日志 Write Ahead Log,流复制的数据源); replica lag(副本延迟); streaming replication(流复制,主库向每个副本推送); cascading replication(级联复制,中间副本中继 WAL 给下游); fan-out(扇出压力)
📖 "The primary streams Write Ahead Log (WAL) data to every read replica. As the number of replicas increases, the primary must ship WAL to more instances, increasing pressure on both network bandwidth and CPU. This causes higher and more unstable replica lag, which makes the system harder to scale reliably." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "This approach allows us to scale to potentially over a hundred replicas without overwhelming the primary. However, it also introduces additional operational complexity, particularly around failover management." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 结果侧数据:近 50 个副本的同时,把 replication lag 维持在接近零,并在地理分布区域保持低延迟读——这正是 WAL 扇出压力尚可控、但已逼近上限的现状。
flowchart TD
  P[(Primary)] -->|WAL| M1[(中间副本 A)]
  P -->|WAL| M2[(中间副本 B)]
  M1 -->|中继 WAL| D1[(下游副本)]
  M1 -->|中继 WAL| D2[(下游副本)]
  M2 -->|中继 WAL| D3[(下游副本)]
🔍 追问 cascading replication 最大的运维风险是什么? → failover 管理:中间副本一旦故障会影响其下游整条链,故障切换拓扑比扁平复制复杂得多,所以要先充分测试、确保能安全 failover 才敢上生产。
📚 拓展阅读
Q什么是"吵闹邻居(noisy neighbor)"问题?OpenAI 用什么隔离策略应对?深挖·拓展低频
工作负载隔离 noisy-neighbor 多租户
⏱️ 现行
常见问题是某些请求在 PostgreSQL 实例上消耗了不成比例的资源,导致同实例上其它工作负载性能下降——比如一次新功能上线引入低效查询、重度消耗 CPU,拖慢其它关键功能的请求。这就是"吵闹邻居"问题。缓解办法是把工作负载隔离到专用实例上,确保资源密集型请求的突发不影响其它流量。具体做法是把请求分成低优先级和高优先级两档,路由到不同实例;这样即使低优先级负载变得资源密集,也不会拖垮高优先级请求。同样的策略也跨不同产品和服务施加,让一个产品的活动不影响另一个产品的性能或可靠性。本质是用物理隔离换取故障域收敛,牺牲一点资源利用率来保住关键路径。
术语 noisy neighbor(吵闹邻居,单一负载抢占共享资源); workload isolation(工作负载隔离); dedicated instances(专用实例); low/high-priority tiers(低/高优先级分层路由); blast radius(故障影响面,隔离用于收敛它)
📖 "To mitigate the “noisy neighbor” problem, we isolate workloads onto dedicated instances to ensure that sudden spikes in resource-intensive requests don’t impact other traffic. Specifically, we split requests into low-priority and high-priority tiers and route them to separate instances." — Scaling PostgreSQL to power 800 million ChatGPT users
📖 "We apply the same strategy across different products and services as well, so that activity from one product does not affect the performance or reliability of another." — Scaling PostgreSQL to power 800 million ChatGPT users
🧪 实例 分层路由示意——把低优先级(如后台/非关键)与高优先级(ChatGPT 关键路径)请求路由到彼此隔离的 PostgreSQL 实例,低优先级即使因新功能低效查询打满 CPU,也困在自己的实例里,不波及高优先级。
🔍 追问 隔离之外,schema 变更这种"重操作"如何避免制造吵闹? → 只允许不触发整表重写(full table rewrite)的轻量 schema 变更,强制 5 秒超时,允许 concurrently 建/删索引;需要新表时必须落到 Cosmos DB 等分片系统;backfill 字段时严格限流,哪怕耗时超过一周也要保稳定。
📚 拓展阅读