登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

OpenAI Responses API 怎样迁移旧工具调用代码

来源:17golang原创

时间:2026-10-23 05:38:47 260浏览 收藏

把旧工具调用代码迁移到 OpenAI Responses API,不能只把接口名称换掉。至少要一起改动工具声明、响应读取和工具结果回传:声明使用扁平 function 结构,从 response.output 找 function_call,再用对应的 call_id 回传 function_call_output。最终文本则从 output_text 读取。

官方地址:https://developers.openai.com/api/docs/guides/migrate-to-responses

这里的“旧代码”指 Chat Completions 自定义函数调用,不包含 Assistants,也不涉及流式事件处理。下面我会用一个只读的收纳盒位置查询小项目串起迁移点。示例没有在本文制作过程中请求真实 API,配图均为静态说明图,不代表运行结果。

先把迁移目标缩到一个只读工具

如果我要迁移一段已经能工作的旧代码,我会先保留原业务函数,只替换模型交互层。本例只有 lookup_box:输入 item_name,返回 found 和 location。数据来自程序内字典;它不具备写数据库、运行脚本或访问任意地址的能力。

旧代码关注点Responses 写法
messages 请求历史input 输入项;本例配合 previous_response_id
tools 内嵌 function 对象type、name、parameters、strict 同层
message.tool_callsresponse.output 中的 function_call 项
工具返回消息function_call_output,携带对应 call_id
choices 中的最终文本response.output_text 便捷属性

output 是带类型的输出项集合,不保证第一个元素就是工具调用,更不能把所有输出项都当成文本。工具声明只是让模型知道可选能力,真正的业务函数仍由应用执行。

Responses 工具声明和输出项的静态字段关系说明图
说明图:声明边界与输出边界分开看;name 关联工具,call_id 关联一次具体调用,图中不表示请求执行顺序。

准备客户端与严格的函数声明

在服务端安装当前 OpenAI Python SDK,并通过部署环境设置 OPENAI_API_KEY。OPENAI_MODEL 由你填写为账户可用、支持 Responses 自定义函数调用的模型。不要把密钥放进文章示例、浏览器代码或提交记录。下面的代码使用当前 SDK 的 Responses 客户端写法,不固定模型名称与 SDK 版本。

strict 显式设为 True,所有字段列入 required,且 additionalProperties 为 False。这里没有可选字段;复杂业务若需要可选值,应按官方严格模式支持的 schema 表达。即使启用了严格模式,服务器仍须检查工具白名单、参数范围和业务授权。

用 call_id 把工具结果接回响应链

这份完整示例采用服务器保存响应链的方式,显式 store=True,并通过 previous_response_id 指向上一次 response.id。部署前须确认这种保存方式符合项目的数据处理要求;不能把它直接照搬到要求不保存响应的环境。每次请求都会重发 instructions 和 tools:previous_response_id 不是应用配置的自动继承开关,尤其上一条 instructions 不会自动带入下一条请求。

import json
import os
from openai import OpenAI

# 只读示例数据,不连接仓库系统,也不执行模型给出的代码。
BOXES = {"收纳盒A": "二号架上层", "收纳盒B": "三号架下层"}
TOOLS = [{
    "type": "function",
    "name": "lookup_box",
    "description": "查询收纳盒的登记位置",
    "strict": True,
    "parameters": {
        "type": "object",
        "properties": {"item_name": {"type": "string"}},
        "required": ["item_name"],
        "additionalProperties": False,
    },
}]
INSTRUCTIONS = "使用 lookup_box 查询位置;找不到时明确说未登记,不得编造位置。"

def execute_tool(call):
    # 服务端白名单和参数检查不能用模型声明代替。
    if call.name != "lookup_box":
        raise ValueError("收到未允许的工具名称")
    args = json.loads(call.arguments)
    if not isinstance(args, dict) or set(args) != {"item_name"}:
        raise ValueError("工具参数结构不符")
    name = args["item_name"]
    if not isinstance(name, str) or not name.strip() or len(name) > 80:
        raise ValueError("物品名称无效")
    location = BOXES.get(name)
    return {"found": location is not None, "location": location}

def answer(client, model, question):
    current_input = question
    previous_id = None
    # 上限是模型请求次数,不是无限递归调用工具。
    for _ in range(4):
        request = {
            "model": model, "instructions": INSTRUCTIONS,
            "tools": TOOLS, "input": current_input, "store": True,
        }
        if previous_id is not None:
            request["previous_response_id"] = previous_id
        response = client.responses.create(**request)
        if response.status != "completed":
            raise RuntimeError("响应未完成,请交由应用处理失败状态")
        calls = [item for item in response.output if item.type == "function_call"]
        if not calls:
            text = response.output_text.strip()
            if not text:
                raise RuntimeError("没有工具调用,也没有可用回答")
            return text
        results = []
        # 一次响应可能带多个调用,逐个保留各自的 call_id。
        for call in calls:
            result = execute_tool(call)
            results.append({
                "type": "function_call_output",
                "call_id": call.call_id,
                "output": json.dumps(result, ensure_ascii=False),
            })
        current_input = results
        previous_id = response.id
    raise RuntimeError("达到请求轮数上限,尚未取得最终回答")

if __name__ == "__main__":
    # 密钥交给 SDK 从环境读取,模型由部署方选择支持此接口的型号。
    model = os.environ.get("OPENAI_MODEL")
    if not model:
        raise SystemExit("请先配置 OPENAI_MODEL 和 OPENAI_API_KEY")
    try:
        with OpenAI(timeout=30.0, max_retries=0) as client:
            print(answer(client, model, "收纳盒A放在哪里?"))
    except Exception:
        # 示例不打印异常原文,避免把请求或敏感配置带进日志。
        raise SystemExit("调用未完成,请在受控日志中排查配置、网络或工具参数")

代码里的两个 ID 分工不同:response.id 标识整次响应,用于续接上下文;call.call_id 标识该响应中的一次工具调用,用于绑定结果。不要拿输出项自身的 id 或 response.id 填进 function_call_output.call_id。

遇到多个 function_call,代码逐个处理并一次回传结果列表。未知工具、JSON 解析失败和不合规参数会停止本轮,由应用接管,而不是伪造成功结果。业务“未登记”则是合法查询结果,found=False、location=None 会被序列化为 JSON 后传回模型。

响应链标识与工具结果标识的静态边界说明图
说明图:响应关联组解释 previous_response_id 与 response.id;工具关联组解释 call_id 与 function_call_output。instructions 和 tools 属于本次请求配置。

迁移后先看这几个失败出口

读者试运行时,先查询字典中存在的收纳盒,再查询一个未登记名称,观察业务返回能否与最终文字对应。这里描述的是建议试验,不是本文实际执行记录;模型是否调用工具及怎样表述回答,仍以你的真实请求为准。最终回答是自然语言,不应代替工具原始结果作为库存或位置的权威数据。

本例只接收 completed 响应,对未完成状态直接报错;生产代码可按官方响应状态分别处理。即使响应完成,也要先看有没有工具调用,再判断最终文本。四次请求仍未结束时应停止并交由业务处理,不要无限追加请求。timeout 限制单次请求等待,max_retries=0 避免 SDK 自动重试;它们不构成业务上的幂等保证。

如果旧系统使用流式输出,还要单独迁移事件解析、参数增量拼接和完成事件处理,不能把旧事件处理器原封不动接到这个非流式例子上。若选择不保存响应、手动维护 input,就必须按官方说明保留需要的输出项,包括适用模型的 reasoning 项,不能只摘取 function_call 而丢掉其他上下文。

常见问题与集成判断

只替换 messages 为 input 行不行?不行。工具 schema 和返回项结构也变了,旧的 message.tool_calls 路径不会自动变成 Responses 的读取方式。

output_text 为空就说明失败吗?不一定。模型可能正在请求工具,先判断 function_call。没有工具调用、也没有可用文本时,应用再按明确的异常出口处理。

什么时候可以接入写入型工具?先让只读示例的参数、调用关联和失败出口跑通,再额外设计授权、人工确认及业务幂等。本文工具循环本身不提供这些能力。

迁移的核心不是换一个接口名,而是把“声明了什么能力”“模型请求了哪次调用”“应用回传哪次结果”这三层分开。先保住 call_id 和上下文,再扩展复杂工具,代码会更容易解释和维护。

官方参考:https://developers.openai.com/api/docs/guides/function-calling;迁移指南地址见文首。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>