# Responses API 与结构化实现

日期：2026-06-14

前面的课程讲的是方法：怎样写清楚任务、怎样设计上下文、怎样使用 RAG、工具和 Agent。本讲补上工程实现层：当你要把提示词放进一个真实应用时，应该怎样选择 API 形态、输出契约、工具调用和评估接口。

本讲以 OpenAI API 为例，但核心思想是通用的：自然语言负责表达任务，API 参数和 schema 负责约束系统行为。

本讲常见术语先按下面理解，详细定义和更多例子见 [00.5-术语词典与最小用例](00.5-术语词典与最小用例.md)。

| 术语 | 直觉解释 | 最小例子 |
| --- | --- | --- |
| Responses API | OpenAI 承载生成、工具、多轮状态的主要接口 | 一次请求里传入 `input`、`tools` 和输出格式 |
| Structured Outputs | 让模型按 schema 输出可校验结构 | 返回严格 JSON，不允许额外字段 |
| Reasoning effort | 控制推理预算的参数 | 简单分类用低预算，复杂调研用较高预算 |
| Tool output | 工具执行后的结果文本或结构化数据 | 查询订单后返回 `{status: "shipped"}` |
| Cached tokens | 被缓存命中的输入 token | 稳定系统提示反复使用时降低延迟和成本 |
| SDK | 在代码里调用 API 的开发工具包 | 用 Python SDK 发起 Responses 请求 |
| JSON mode | 只约束输出为 JSON 的模式 | 适合简单 JSON，不等于完整 schema 校验 |

## 一、先分清三层

很多学习材料会把 prompt、API 和 Agent 混在一起。工程实现时先分三层：

| 层级 | 负责什么 | 常见形式 |
| --- | --- | --- |
| Prompt | 任务目标、上下文、边界、输出意图 | instructions、input、system / developer message |
| API 参数 | 模型、推理强度、输出长度、工具、结构化输出 | model、reasoning、text / text.format、tools |
| 应用代码 | 权限、校验、工具执行、日志、重试、评估 | backend、worker、eval runner |

提示词越成熟，越应该把可机器校验的东西移到 API 或代码层，而不是全塞在自然语言里。

## 二、Responses API 适合什么

OpenAI 当前主要推荐用 Responses API 承载直接模型请求、工具调用、多轮状态和内置工具。它适合：

- 文本生成、分析、总结、抽取。
- 需要结构化输出的任务。
- 需要 function calling、file search、web search、remote MCP 等工具的任务。
- 需要 reasoning effort、状态续接或多轮工具链的任务。
- Agent 原型或应用自管 orchestration 的场景。

一个简单判断：

```text
只要任务涉及 reasoning、tool calling、多轮状态或内置工具，优先考虑 Responses API。
```

Chat Completions 仍可能出现在旧项目里，但新项目不要为了兼容旧习惯而忽略 Responses API。迁移旧系统时，应先确认现有 SDK、日志、工具调用格式和评估流程能否一起迁移。

## 三、结构化输出：不要只写“请输出 JSON”

如果输出要被程序消费，单靠提示词要求 JSON 不够稳。更成熟的做法是使用 Structured Outputs 或 JSON Schema。

适合使用 schema 的任务：

- 信息抽取。
- 分类。
- 表单填充。
- 风险标签。
- 自动评分。
- RAG 引用结果。
- 工具参数生成。

Prompt 里仍然要写清楚语义边界，例如“只基于原文”“缺失写 null”。但字段、类型、必填项、枚举值、是否允许额外字段，应尽量交给 schema。

示例：

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["case_id", "parties", "judgment_result", "missing_fields"],
  "properties": {
    "case_id": {
      "type": ["string", "null"]
    },
    "parties": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["role", "name"],
        "properties": {
          "role": {"type": "string"},
          "name": {"type": "string"}
        }
      }
    },
    "judgment_result": {
      "type": "string"
    },
    "missing_fields": {
      "type": "array",
      "items": {"type": "string"}
    }
  }
}
```

配套 prompt 应该短而明确：

```text
请只基于输入文书抽取字段。
原文没有的信息写 null，并把字段名放入 missing_fields。
不要补充案外事实。
```

## 四、Reasoning 参数不是提示词替代品

推理模型通常会在内部完成复杂推理。不要机械要求模型展示完整思维链。更好的方式是：

- 在 prompt 中写清楚目标和成功标准。
- 用 `reasoning.effort` 或平台等价参数控制推理预算。
- 要求输出“结论、依据、不确定项、核查点”，而不是完整思考过程。
- 对复杂任务用 Eval 选择合适 effort，而不是凭感觉固定高档。

常见策略：

| 任务 | 推理配置思路 |
| --- | --- |
| 简单分类、抽取 | 低推理预算，重 schema 和规则 |
| 长文档总结 | 中等推理预算，重引用和核查 |
| 复杂 Agent / 调研 | 较高推理预算，重工具 trace 和停止条件 |
| 离线评估极限能力 | 高推理预算，接受更慢和更贵 |

不要把“更高 reasoning effort”当成万能修复。检索错、schema 错、工具权限错、评估集缺失，都不是单纯加推理能解决的。

## 五、工具调用实现要闭环

Function calling / tool calling 的关键不是“模型会调用工具”，而是应用代码能安全闭环。

最小闭环：

```text
1. 请求模型，并提供可用工具定义。
2. 收到模型生成的工具调用请求。
3. 应用代码校验工具名、权限、参数和风险。
4. 应用代码执行工具。
5. 把工具结果返回给模型。
6. 模型基于工具结果继续回答，或继续请求工具。
7. 达到停止条件后输出最终结果。
```

应用代码必须处理：

- 工具不存在。
- 参数缺失或类型错误。
- 用户权限不足。
- 高风险写操作未确认。
- 工具超时或返回错误。
- 工具结果包含提示注入。
- 模型循环调用工具。

提示词不能替代这些校验。

## 六、内置工具、自定义函数和 MCP 怎么选

| 需求 | 首选 |
| --- | --- |
| 查公开网页 | 平台内置 web search |
| 查上传文件或平台文件库 | file search / retrieval |
| 执行业务系统动作 | 自定义 function calling |
| 连接第三方或内部系统并跨客户端复用 | MCP / connector |
| 本地确定性处理 | 应用代码或脚本 |

MCP server 可能暴露 resources、tools、prompts。它解决的是连接和复用问题，不自动解决权限、安全和审计问题。

接 MCP 前要问：

- 这个 server 暴露了哪些 tools、resources、prompts？
- 认证和 OAuth scope 是否最小化？
- 是否需要工具 allowlist？
- 写操作是否需要人工确认？
- 返回内容是否会泄露密钥、隐私或内部日志？
- 是否有提示注入测试和审计日志？

## 七、缓存友好的请求结构

Prompt caching 的基本思路是让重复的大块稳定前缀复用。请求结构应尽量：

```text
稳定系统规则
-> 稳定工具定义
-> 固定示例
-> 固定背景材料
-> 动态用户输入
-> 动态检索结果
-> 动态工具结果
```

OpenAI 的 prompt caching 通常要求 prompt 至少达到 1024 tokens 才会自动启用缓存。短 prompt 不应该为了缓存强行加废话；只有当稳定内容本来就需要重复传时，才值得围绕缓存优化。

容易破坏缓存的做法：

- 时间戳、随机 ID、用户 ID 放在最前面。
- 工具定义每次顺序不同。
- few-shot 示例随机排序。
- RAG 动态片段放在稳定前缀前。
- 每次把无关历史都塞进上下文。

## 八、API 层最小日志

想要长期优化 prompt，至少记录：

- prompt 版本。
- 模型 ID 或模型别名。
- API 表面：Responses、Chat Completions、Agents SDK 等。
- reasoning effort、temperature、max output tokens 等关键参数。
- input tokens、output tokens、reasoning tokens、cached tokens。
- 工具调用 trace。
- RAG 索引版本和检索片段编号。
- 结构化输出是否通过 schema。
- 用户反馈、人工复核结果、失败原因。

没有这些信息，后面做 Eval、成本优化和回归分析都会变成猜。

## 九、最小实现练习

做一个“合同字段抽取”小项目：

1. 准备 5 份短合同片段。
2. 定义 JSON Schema：合同编号、甲方、乙方、付款期限、违约金、缺失字段。
3. 写 prompt：只基于原文，缺失写 null。
4. 调用 Responses API 或你使用平台的等价接口。
5. 用程序检查 JSON 是否可解析、必填字段是否存在。
6. 记录失败样例并改 prompt 或 schema。

目标不是一次写出完美 prompt，而是建立“prompt + schema + eval”的最小闭环。

## 十、常见误区

### 误区 1：API 参数可以替代任务说明

不对。API 参数控制模型行为边界，prompt 仍要说明业务目标、输入边界和成功标准。

### 误区 2：写了 JSON 示例就等于结构化输出

不对。JSON 示例能引导模型，但不能像 schema 那样提供机器可校验的契约。

### 误区 3：用了工具调用就可以相信模型参数

不对。模型生成的是调用请求，不是可信执行命令。应用代码必须校验。

### 误区 4：最新模型不用 Eval

不对。模型行为会随版本变化。需要稳定输出的应用应固定模型版本或记录模型变更，并跑回归评估。

### 误区 5：缓存优化就是把 prompt 写长

不对。缓存优化的是重复稳定内容。无意义加长 prompt 会增加成本和干扰。

## 十一、理解检查

1. Prompt、API 参数、应用代码分别负责什么？
2. 为什么程序消费的输出应该优先使用 JSON Schema？
3. Function calling 的执行权为什么必须在应用代码手里？
4. Prompt caching 为什么要求稳定内容放前面？
5. 一个 API-backed prompt 上线前至少要记录哪些日志？

## 十二、下一步

下一讲建议学习：`06.6-安全与Prompt Injection专题.md`。当模型能读取外部内容、调用工具、连接 MCP 后，安全边界会比单条 prompt 更重要。
