Agent 开发教程:OpenAI Responses 接口
2026/9/5 1:50:18 网站建设 项目流程

目录

Responses 接口格式

非流式对话

流式输出

多轮对话

提交工具与调用

上传图片


教程地址:

https://docs.whuanle.cn/zh/learn_ai_agent

Responses 接口格式

前面介绍了 OpenAI 的 chat 接口协议格式,有很多概念已经介绍过了,所以在本章中主要介绍实际代码测试过程,不再重复介绍各类概念。

非流式对话

Responses 接口使用比较简单的,示例如下:

OpenAIClient factory = new( credential: new ApiKeyCredential("1234"), options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1"), Transport = new HttpClientPipelineTransport(new HttpClient(new LoggingHandler())) }); var client = factory.GetResponsesClient(); ClientResult<ResponseResult> result = await client.CreateResponseAsync("qwen/qwen3.5-9b", "1+1=?"); ResponseResult response = result.Value; Console.WriteLine($"[ASSISTANT]: {response.GetOutputText()}");

实际请求:

{"model":"qwen/qwen3.5-9b","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"1+1=?"}]}]}

Responses 接口参数和结构跟/v1/chat/completions有所差异,但是角色等概念是一样的,因此这里就不展开太多说明了,因为官方 SDK 很多内容都不加注释,代码量也比较少,所以也不好讲解。

流式和非流式区别有两个点。

  • • 流式返回使用AsyncCollectionResult<T>,非流式使用ClientResult

  • • 流式的<T>类型 比较多,通过类型拆解可以获取数据,而非流式只有 ResponseResult 这个类型。

ResponseResult 类型让 AI 注释了一下:

public partial class ResponseResult { /// 获取或设置是否以后台模式处理本次响应。 /// 后台模式用于耗时较长的异步任务:请求会立即返回,响应在后台生成, /// 调用方可稍后再轮询/取回结果,而不必保持长连接等待。 [CodeGenMember("Background")] public bool? BackgroundModeEnabled { get; set; } /// 获取或设置终端用户的标识符,用于 OpenAI 侧的滥用监测。 [CodeGenMember("User")] public string EndUserId { get; set; } /// 获取或设置模型的推理(reasoning)行为配置。 /// 用于控制模型在作答前投入多少"思考",例如 <see cref="ResponseReasoningOptions.ReasoningEffortLevel"/> /// 表示推理力度(None/Minimal/Low/Medium/High),<see cref="ResponseReasoningOptions.ReasoningSummaryVerbosity"/> /// 表示推理摘要的详细程度(Auto/Concise/Detailed)。 [CodeGenMember("Reasoning")] public ResponseReasoningOptions ReasoningOptions { get; set; } /// 获取或设置本次响应输出 token 数的上限。 /// 达到该上限时响应会被截断,<see cref="Status"/> 会变为 <c>Incomplete</c>, /// 原因记录在 <see cref="IncompleteStatusDetails"/>(Reason = MaxOutputTokens)。 [CodeGenMember("MaxOutputTokens")] public int? MaxOutputTokenCount { get; set; } /// 获取或设置本次响应中工具调用次数的上限。 [CodeGenMember("MaxToolCalls")] public int? MaxToolCallCount { get; set; } /// 获取或设置文本输出的格式配置。 /// 通过 <see cref="ResponseTextOptions.TextFormat"/> 指定输出为纯文本、JSON 对象,或遵循某个 JSON Schema。 [CodeGenMember("Text")] public ResponseTextOptions TextOptions { get; set; } /// 获取或设置对上下文做截断的策略。 /// <see cref="ResponseTruncationMode.Auto"/> 表示模型自动截断以适配上下文窗口; /// <see cref="ResponseTruncationMode.Disabled"/> 表示不截断。 [CodeGenMember("Truncation")] public ResponseTruncationMode? TruncationMode { get; set; } /// 获取或设置当响应以 <see cref="ResponseStatus.Incomplete"/> 结束时,导致未完成的具体原因。 /// 可能的原因见 <see cref="ResponseIncompleteStatusDetails"/>,例如达到输出 token 上限或命中内容过滤。 [CodeGenMember("IncompleteDetails")] public ResponseIncompleteStatusDetails IncompleteStatusDetails { get; set; } /// 获取本次响应生成的输出项集合。 /// 这是模型回复的核心载体,按生成顺序排列,元素类型派生自 <see cref="ResponseItem"/>。 /// 常见的有 <c>MessageResponseItem</c>(文本/图片等消息)、工具调用项、推理项等。 /// 要快速拼接所有文本输出,直接调用 <see cref="GetOutputText"/>。 [CodeGenMember("Output")] public IList<ResponseItem> OutputItems { get; } /// 获取或设置是否允许模型在同一轮中并行发起多个工具调用。 [CodeGenMember("ParallelToolCalls")] public bool ParallelToolCallsEnabled { get; set; } /// 获取或设置模型在本次响应中选择工具的方式。 /// 例如自动选择(Auto)、不调用任何工具(None)、必须调用(Required), /// 或指定调用某个具体工具(如某个函数)。具体取值见 <see cref="ResponseToolChoice"/>。 [CodeGenMember("ToolChoice")] public ResponseToolChoice ToolChoice { get; set; } /// 获取或设置为每个输出 token 返回的"最高概率候选词"数量(top logprobs)。 /// 设置后可用于研究模型在每个位置的概率分布;不设置则不返回 logprobs 信息。 [CodeGenMember("TopLogprobs")] public int? TopLogProbabilityCount { get; set; } /// 获取或设置本次响应所属的会话(conversation)上下文信息。 /// 通过 <see cref="ResponseConversationOptions.ConversationId"/> 标识该响应归属的会话, /// 用于基于 Conversation 的状态化多轮交互。 [CodeGenMember("Conversation")] public ResponseConversationOptions ConversationOptions { get; set; } /// 资源的类型标识,对 <see cref="ResponseResult"/> 始终为 <c>"response"</c>。 [CodeGenMember("Object")] [EditorBrowsable(EditorBrowsableState.Never)] public string Object { get; set; } = "response"; /// 获取本次响应所使用的系统/开发者指令(instructions)。 /// OpenAI 返回的指令可能是纯字符串,也可能是 <see cref="ResponseItem"/> 数组, /// 这里统一以 <see cref="ResponseItem"/> 列表的形式暴露,以兼容两种形态。 [CodeGenMember("Instructions")] public IList<ResponseItem> Instructions { get; } }

流式输出

跟 Chat Completions 一样,Responses 也支持流式输出,日常使用基本都是用这种。

调用前需要在CreateResponseOptions里把StreamingEnabled设为true,然后用CreateResponseStreamingAsync,返回一个AsyncCollectionResult<StreamingResponseUpdate>,同样用await foreach逐条消费。

这一步比 Chat 严格:如果忘了把StreamingEnabled设成true,SDK 会直接抛InvalidOperationException,提示你去调用 streaming 版本。反过来,非流式的CreateResponse也不允许把StreamingEnabled设成true

OpenAIClient factory = new( credential: new ApiKeyCredential("1234"), options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1"), Transport = new HttpClientPipelineTransport(new HttpClient(new LoggingHandler())) }); ResponsesClient client = factory.GetResponsesClient(); CreateResponseOptions options = new("qwen/qwen3.5-9b", [ResponseItem.CreateUserMessageItem("太阳系有多少颗行星?")]) { StreamingEnabled = true, }; AsyncCollectionResult<StreamingResponseUpdate> updates = client.CreateResponseStreamingAsync(options); Console.Write("[助手] "); await foreach (StreamingResponseUpdate update in updates) { if(update is StreamingResponseCreatedUpdate streamingResponseCreatedUpdate) { Console.WriteLine("Statr"); } if(update is StreamingResponseReasoningTextDeltaUpdate reasoningResponseItem) { Console.Write(reasoningResponseItem.Delta); } if (update is StreamingResponseOutputTextDeltaUpdate textUpdate) { Console.Write(textUpdate.Delta); } }

实际请求会带上stream: true

{ "model": "qwen/qwen3.5-9b", "input": [{ "type": "message", "role": "user", "content": [{ "type": "input_text", "text": "太阳系有多少颗行星?" }] }], "stream": true }

跟 Chat 的delta里塞reasoning_content/content不同,Responses 的 SSE 事件是按类型分发的:每条流式消息对应一个StreamingResponseUpdate的子类,你只需要is判断一次,就知道它代表什么。常用的几类:

更新类型

作用

StreamingResponseCreatedUpdate

响应已创建,带初始的ResponseResult(含响应 id 等)

StreamingResponseOutputItemAddedUpdate

一个新的输出项开始生成(一条消息、一次工具调用等)

StreamingResponseOutputTextDeltaUpdate

正文文本增量,真正的“逐字输出”在这里,取.Delta

StreamingResponseOutputTextDoneUpdate

正文文本片段结束,.Text是这一段的完整内容

StreamingResponseReasoningTextDeltaUpdate

深度思考(reasoning)正文增量

StreamingResponseReasoningSummaryTextDeltaUpdate

深度思考的摘要增量

StreamingResponseFunctionCallArgumentsDeltaUpdate

工具调用参数的增量(参数也是流式拼出来的)

StreamingResponseOutputItemDoneUpdate

一个输出项生成完毕,.Item是该输出项的完整对象

StreamingResponseCompletedUpdate

整个响应结束,含完整的ResponseResultUsage

StreamingResponseIncompleteUpdate

因 token 上限或内容过滤提前结束

StreamingResponseErrorUpdate

出错了

所以 Chat 里那种手写 JSON 去捞reasoning_content的做法,在 Responses 这里不需要了,直接判断类型即可:

await foreach (StreamingResponseUpdate update in updates) { switch (update) { case StreamingResponseReasoningTextDeltaUpdate reasoning: Console.Write(reasoning.Delta); break; case StreamingResponseOutputTextDeltaUpdate text: Console.Write(text.Delta); break; } }

流式过程中,如果要把结果回填到下一轮的input里,不要自己拼,等StreamingResponseOutputItemDoneUpdate触发,直接拿它的.Item(已经是完整的ResponseItem)加进InputItems就行,这是官方示例的做法,能避免增量拼接错位的问题。

多轮对话

Responses 多轮对话也是有两种思路。

第一种:服务端托管。OpenAI 的 Responses 接口本身是有状态的——每一次响应都会返回一个idresponse.XXXXX),下一次请求只要带上previousResponseId,服务端会自动把上一次的输入输出接上,本地不需要维护messages数组。

这对工具调用场景尤其友好,因为工具调用产生的function_call/function_call_output项会被服务端一起记住。

ResponsesClient client = factory.GetResponsesClient(); // 第一轮 ResponseResult first = await client.CreateResponseAsync("qwen/qwen3.5-9b", "1+1=?"); Console.WriteLine($"[ASSISTANT]: {first.GetOutputText()}"); // 第二轮,带上 previousResponseId 即可,不必自己拼历史 ResponseResult second = await client.CreateResponseAsync( model: "qwen/qwen3.5-9b", inputItems: [ResponseItem.CreateUserMessageItem("再加上 2 呢")], previousResponseId: first.Id); Console.WriteLine($"[ASSISTANT]: {second.GetOutputText()}");

第二种:本地拼装。如果你想完全自己控制上下文(比如走第三方兼容服务、或者要裁剪历史),就仿照 Chat 的写法,把上一轮的输出项整个塞回input

List<ResponseItem> inputItems = [ ResponseItem.CreateUserMessageItem("1+1=?"), ]; ResponseResult first = await client.CreateResponseAsync(new CreateResponseOptions("qwen/qwen3.5-9b", inputItems)); // 把模型这一轮的输出项(含 assistant 消息)追加到 input 里 inputItems.AddRange(first.OutputItems); inputItems.Add(ResponseItem.CreateUserMessageItem("再加上 2 呢")); ResponseResult second = await client.CreateResponseAsync(new CreateResponseOptions("qwen/qwen3.5-9b", inputItems));

注意第二种方式里我们用的是inputItems.AddRange(first.OutputItems)——OutputItems里不仅有 assistant 文本消息,可能还包含 reasoning、工具调用等项,整个原样回填是安全的,模型会自己忽略不需要的部分。如果你在对接的是非 OpenAI 官方的兼容服务(比如本地 ollama/lm-studio 之类的),建议优先用本地拼装,previousResponseId不一定被支持。

提交工具与调用

Responses 的工具调用整体流程跟 Chat 一样(模型返回要调的函数 → 本地执行 → 把结果回填 → 再请求一次),但写法要清爽很多,原因有两个:

  1. 1.不用维护role:tool消息。回填结果用的是专门的FunctionCallOutputResponseItem,它和函数调用项FunctionCallResponseItem天然配对,靠CallId关联,不需要塞进某种角色的content里。

  2. 2.判断结束不用看FinishReason。直接遍历response.OutputItems,看到FunctionCallResponseItem就执行、回填、再来一轮,循环到输出里没有函数调用为止。

我们沿用 Chat 提交工具与调用 的灯控例子,函数定义不变:

static Dictionary<int, bool> Lights = new() { { 1, false }, { 2, false }, { 3, false } }; static IReadOnlyDictionary<int, bool> GetLightState() => Lights; static IReadOnlyDictionary<int, bool> OpenOrCloseLight(int index, bool state) { Lights[index] = state; return GetLightState(); }

定义工具用的是ResponseTool.CreateFunctionTool,跟 Chat 的ChatTool.CreateFunctionTool几乎一样,多了一个strictModeEnabled(开启后模型会严格按 schema 生成参数,建议关掉以便本地兼容服务):

FunctionTool getLightState = ResponseTool.CreateFunctionTool( functionName: nameof(GetLightState), functionDescription: "获取所有灯的状态", functionParameters: null, strictModeEnabled: false); FunctionTool openOrCloseLight = ResponseTool.CreateFunctionTool( functionName: nameof(OpenOrCloseLight), functionDescription: "打开或关闭灯", functionParameters: BinaryData.FromBytes(""" { "type": "object", "properties": { "index": { "type": "integer", "description": "light index" }, "state": { "type": "boolean", "description": "open or close light." } }, "required": ["index", "state"] } """u8.ToArray()), strictModeEnabled: false);

多轮执行循环,注意和 Chat 版本的对比:

ResponsesClient client = factory.GetResponsesClient(); List<ResponseItem> inputItems = [ResponseItem.CreateUserMessageItem("获取所有灯的状态,并把 1、3 号的灯打开")]; bool requiresAction; do { requiresAction = false; CreateResponseOptions options = new("mimo-v2.5-pro", inputItems) { Tools = { getLightState, openOrCloseLight }, }; ResponseResult response = await client.CreateResponseAsync(options); // 关键点:把这一轮的输出项整个回填到 input 里 // 这样 function_call 和它对应的 function_call_output 自然成对出现在历史中 inputItems.AddRange(response.OutputItems); foreach (ResponseItem outputItem in response.OutputItems) { if (outputItem is FunctionCallResponseItem functionCall) { switch (functionCall.FunctionName) { case nameof(GetLightState): { string result = JsonSerializer.Serialize(GetLightState()); // 用 CallId 把输出和调用配对,不再需要 role:tool inputItems.Add(new FunctionCallOutputResponseItem(functionCall.CallId, result)); break; } case nameof(OpenOrCloseLight): { using JsonDocument args = JsonDocument.Parse(functionCall.FunctionArguments); int index = args.RootElement.GetProperty("index").GetInt32(); bool state = args.RootElement.GetProperty("state").GetBoolean(); string result = JsonSerializer.Serialize(OpenOrCloseLight(index, state)); inputItems.Add(new FunctionCallOutputResponseItem(functionCall.CallId, result)); break; } default: throw new NotImplementedException(); } requiresAction = true; } } } while (requiresAction); // 最后打印一下 assistant 的消息 foreach (ResponseItem item in inputItems) { if (item is MessageResponseItem msg && msg.Role == MessageRole.Assistant && msg.Content.Count > 0) { Console.WriteLine($"[ASSISTANT]: {msg.Content[0].Text}"); } }

实际请求(第一次):

{ "model": "mimo-v2.5-pro", "input": [{ "type": "message", "role": "user", "content": [{ "type": "input_text", "text": "获取所有灯的状态,并把 1、3 号的灯打开" }] }], "tools": [ { "type": "function", "name": "GetLightState", "description": "获取所有灯的状态" }, { "type": "function", "name": "OpenOrCloseLight", "description": "打开或关闭灯", "parameters": { "type": "object", "properties": { "...": "..." }, "required": ["index", "state"] } } ] }

注意 Responses 的 schema 跟 Chat 不太一样:Chat 里函数信息是嵌在function字段下({"type":"function","function":{"name":...}}),Responses 直接把namedescriptionparameters平铺在工具对象上({"type":"function","name":...}),少了一层嵌套。

模型返回的输出项里会出现一条函数调用:

{ "type": "function_call", "id": "fc_xxx", "call_id": "call_xxx", "name": "OpenOrCloseLight", "arguments": "{\"index\":1,\"state\":true}" }

你执行完,构造一条FunctionCallOutputResponseItem,它的call_id必须和上面的call_id对上:

{ "type": "function_call_output", "call_id": "call_xxx", "output": "{\"1\":true,\"2\":false,\"3\":false}" }

跟 Chat 对比一下,几个让人省心的点:

  • 没有FinishReason判断。直接看OutputItems里有没有FunctionCallResponseItem即可,有就继续循环,没有就结束。

  • 配对靠CallId,不是消息顺序。回填用new FunctionCallOutputResponseItem(functionCall.CallId, output),不用操心往哪个角色后面插。

  • 整体历史就是一份input数组function_callfunction_call_output都是数组里的一项,模型一眼就能看懂“我之前调了什么、得到了什么”,语义比role:tool清晰得多。

如果是流式 + 工具调用,套路一样,只是改用CreateResponseStreamingAsync,并在StreamingResponseOutputItemDoneUpdate里把.Item加回options.InputItems、判断它是不是FunctionCallResponseItem,完整示例可以参考官方仓库的Example04_FunctionCallingStreamingAsync.cs

上传图片

Responses 里附图跟 Chat 原理相同——把用户消息的content从单个字符串换成ResponseContentPart数组,混排文字和图片——只是构造方法和类型名换了。

ResponsesClient client = factory.GetResponsesClient(); using Stream imageStream = File.OpenRead("34e7caa2-2852-458d-96cc-babff3c65c03.png"); BinaryData imageBytes = BinaryData.FromStream(imageStream); // 注意:BinaryData 必须带 MediaType,否则 SDK 会抛异常 imageBytes.MediaType = "image/png"; List<ResponseItem> inputItems = [ ResponseItem.CreateUserMessageItem( [ ResponseContentPart.CreateInputTextPart("识别图片内容."), ResponseContentPart.CreateInputImagePart(imageBytes), ]), ]; ResponseResult response = await client.CreateResponseAsync(new CreateResponseOptions("qwen/qwen3.5-9b", inputItems)); Console.WriteLine($"[ASSISTANT]: {response.GetOutputText()}");

ResponseContentPart常用的几个工厂方法:

方法

说明

CreateInputTextPart(string)

一段文字

CreateInputImagePart(BinaryData, detail?)

本地图片字节(必须设置BinaryData.MediaType,内部会转成data:image/png;base64,...

CreateInputImagePart(Uri, detail?)

公开可访问的图片 URL

CreateInputImagePart(string fileId, detail?)

先用文件接口上传得到的file_id

实际请求里,图片被打包成input_image类型:

{ "model": "qwen/qwen3.5-9b", "input": [{ "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "识别图片内容." }, { "type": "input_image", "image_url": "data:image/png;base64,iVBORw0..." } ] }] }

Chat 里图片类型叫image_url,Responses 里改叫input_image,字段也从image_url.url简化成image_url(直接就是字符串),这是两套接口命名风格上的一个小区别,对接兼容服务时留意一下即可。

引入地址

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询