• [MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据?


    针对IChatClient的结构化输出可以通过调用如下这些重载的GetResponseAsync扩展方法来完成。具体的实现很简单,这些方法最终会利用指定或者默认的JsonSerializerOptions针对泛型参数T生成一个ChatResponseFormatJson对象,并作为ChatOptionsResponseFormat属性。这个ResponseFormat承载的JSON Schema将提供给LLM指导它按照定义的格式生成输出内容。当IChatClient接收到LLM的响应结果时,利用匹配的JsonSerializerOptions对响应结果进行反序列化后,封装成一个ChatResponse对象返回给调用方。

    public static class ChatClientStructuredOutputExtensions
    {
        public static Task<ChatResponse<T>> GetResponseAsync<T>(
            this IChatClient chatClient, 
            IEnumerable messages, 
            ChatOptions? options = null, bool? useJsonSchemaResponseFormat = null, 
            CancellationToken cancellationToken = default(CancellationToken));
        public static Task<ChatResponse<T>> GetResponseAsync<T>(
            this IChatClient chatClient, 
            string chatMessage, 
            ChatOptions? options = null, 
            bool? useJsonSchemaResponseFormat = null, 
            CancellationToken cancellationToken = default(CancellationToken));
        public static Task<ChatResponse<T>> GetResponseAsync<T>(
            this IChatClient chatClient, 
            ChatMessage chatMessage, 
            ChatOptions? options = null,
            bool? useJsonSchemaResponseFormat = null, 
            CancellationToken cancellationToken = default(CancellationToken));
        public static Task<ChatResponse<T>> GetResponseAsync<T>(
            this IChatClient chatClient, 
            string chatMessage, 
            JsonSerializerOptions serializerOptions, 
            ChatOptions? options = null, 
            bool? useJsonSchemaResponseFormat = null, 
            CancellationToken cancellationToken = default(CancellationToken));
        public static Task<ChatResponse<T>> GetResponseAsync<T>(
            this IChatClient chatClient, 
            ChatMessage chatMessage, 
            JsonSerializerOptions serializerOptions, 
            ChatOptions? options = null, 
            bool? useJsonSchemaResponseFormat = null, 
            CancellationToken cancellationToken = default(CancellationToken));
        public static async Task<ChatResponse<T>> GetResponseAsync<T>(
            this IChatClient chatClient, 
            IEnumerable messages, 
            JsonSerializerOptions serializerOptions, 
            ChatOptions? options = null, 
            bool? useJsonSchemaResponseFormat = null, 
            CancellationToken cancellationToken = default(CancellationToken));
    }
    

    1. 调用GetResponseAsync方法获取结构化输出

    在如下的演示程序中,我们定义了一个描述个人基本信息的Profile类。我们利用OpenAIClient创建了一个IChatClient对象,并调用了GetResponseAsync方法来从指定的一段文本中提取个人信息。在得到作为响应的Response对象后,利用Result属性提取反序列化响应内容生成的Profile对象,调试断言表明这个Profile对象与我们预设的Profile对象是相等的。

    using dotenv.net;
    using Microsoft.Extensions.AI;
    using OpenAI;
    using System.ClientModel;
    using System.Diagnostics;
    using System.Text.Json;
    
    DotEnv.Load();
    var model = Environment.GetEnvironmentVariable("MODEL")!;
    var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
    var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
    var openAIClient = new OpenAIClient(
        credential: new ApiKeyCredential(key: apiKey),
        options: new OpenAIClientOptions
        {
            Endpoint = new Uri(openAIUrl)
        });
    
    var chatClient = openAIClient.GetResponsesClient().AsIChatClient(defaultModelId: model);
    var profile = new Profile { Name = "张三" , Gender = Gender.Male, Age = 26 };
    var prompt = "从下面内容中提取有效的个人信息:我叫张三,男,今年26岁";
    var response1 = await chatClient.GetResponseAsync(chatMessage: prompt);
    Debug.Assert(profile == response1.Result);
    
    public enum Gender
    {
        Male,
        Female,
    }
    class Profile:IEquatable<Profile>
    {
        public string? Name { get; set; }
        public Gender Gender { get; set; }
        public int Age { get; set; }
        public bool Equals(Profile? other)
        { 
            if(other is null) return false;
            return Name == other.Name && Gender == other.Gender && Age == other.Age;
        }
    }
    

    对于程序涉及的LLM调用,如下的两段JSON为发送的请求和接收的响应。可以看出针对Profile类型的JSON Schema被包含在发送给LLM的请求中,而LLM生成的响应内容则被成功地反序列化成了一个Profile对象。

    {
      "model": "gpt-5.2-chat",
      "text": {
        "format": {
          "type": "json_schema",
          "name": "Profile",
          "schema": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "gender": {
                "type": "string",
                "enum": [
                  "Male",
                  "Female"
                ]
              },
              "age": {
                "type": "integer"
              }
            },
            "additionalProperties": false,
            "required": [
              "name",
              "gender",
              "age"
            ]
          }
        }
      },
      "input": [
        {
          "type": "message",
          "role": "user",
          "content": [
            {
              "type": "input_text",
              "text": "从下面内容中提取有效的个人信息:我叫张三,男,今年26岁"
            }
          ]
        }
      ]
    }
    
    {
      "id": "resp_0962768f225127bc006a002eb6441881978b7b0150d57cdff9",
      "object": "response",
      "created_at": 1778396854,
      "status": "completed",
      "background": false,
      "completed_at": 1778396858,
      "content_filters": [
        {
          "blocked": false,
          "source_type": "prompt",
          "content_filter_raw": [],
          "content_filter_results": {
            "hate": {
              "filtered": false,
              "severity": "safe"
            },
            "sexual": {
              "filtered": false,
              "severity": "safe"
            },
            "violence": {
              "filtered": false,
              "severity": "safe"
            },
            "self_harm": {
              "filtered": false,
              "severity": "safe"
            }
          },
          "content_filter_offsets": {
            "start_offset": 0,
            "end_offset": 585,
            "check_offset": 0
          }
        },
        {
          "blocked": false,
          "source_type": "completion",
          "content_filter_raw": [],
          "content_filter_results": {
            "hate": {
              "filtered": false,
              "severity": "safe"
            },
            "sexual": {
              "filtered": false,
              "severity": "safe"
            },
            "violence": {
              "filtered": false,
              "severity": "safe"
            },
            "self_harm": {
              "filtered": false,
              "severity": "safe"
            }
          },
          "content_filter_offsets": {
            "start_offset": 0,
            "end_offset": 824,
            "check_offset": 0
          }
        }
      ],
      "error": null,
      "frequency_penalty": 0.0,
      "incomplete_details": null,
      "instructions": null,
      "max_output_tokens": null,
      "max_tool_calls": null,
      "model": "gpt-5.2-chat",
      "output": [
        {
          "id": "rs_0962768f225127bc006a002eb6c400819798b1da517e1a1eda",
          "type": "reasoning",
          "summary": []
        },
        {
          "id": "msg_0962768f225127bc006a002eba09b48197b34ee13d7b475432",
          "type": "message",
          "status": "completed",
          "content": [
            {
              "type": "output_text",
              "annotations": [],
              "logprobs": [],
              "text": "{\"name\":\"张三\",\"gender\":\"Male\",\"age\":26}"
            }
          ],
          "role": "assistant"
        }
      ],
      "parallel_tool_calls": true,
      "presence_penalty": 0.0,
      "previous_response_id": null,
      "prompt_cache_key": null,
      "prompt_cache_retention": null,
      "reasoning": {
        "effort": "medium",
        "summary": null
      },
      "safety_identifier": null,
      "service_tier": "default",
      "store": true,
      "temperature": 1.0,
      "text": {
        "format": {
          "type": "json_schema",
          "description": null,
          "name": "Profile",
          "schema": {
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "gender": {
                "type": "string",
                "enum": [
                  "Male",
                  "Female"
                ]
              },
              "age": {
                "type": "integer"
              }
            },
            "additionalProperties": false,
            "required": [
              "name",
              "gender",
              "age"
            ]
          },
          "strict": true
        },
        "verbosity": "medium"
      },
      "tool_choice": "auto",
      "tools": [],
      "top_logprobs": 0,
      "top_p": 0.85,
      "truncation": "disabled",
      "usage": {
        "input_tokens": 69,
        "input_tokens_details": {
          "cached_tokens": 0
        },
        "output_tokens": 217,
        "output_tokens_details": {
          "reasoning_tokens": 192
        },
        "total_tokens": 286
      },
      "user": null,
      "metadata": {}
    }
    

    2. 通过设置ChatOptions的ResponseFormat属性来获取结构化输出

    GetResponseAsync方法最终会利用指定或者默认的JsonSerializerOptions针对泛型参数T生成一个ChatResponseFormatJson对象,并作为ChatOptionsResponseFormat属性。当我们直接调用GetResponseAsync方法时,ChatOptionsResponseFormat属性返回的JSON Schema将作为调用LLM提示词的一部分,用于指导LLM生成符合结构的输出。上面的演示程序与下面这段其实是完全等效的。

    using dotenv.net;
    using Microsoft.Extensions.AI;
    using OpenAI;
    using System.ClientModel;
    using System.Diagnostics;
    using System.Text.Json;
    
    DotEnv.Load();
    var model = Environment.GetEnvironmentVariable("MODEL")!;
    var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
    var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
    var openAIClient = new OpenAIClient(
        credential: new ApiKeyCredential(key: apiKey),
        options: new OpenAIClientOptions
        {
            Endpoint = new Uri(openAIUrl)
        });
    
    var chatClient = openAIClient.GetResponsesClient().AsIChatClient(defaultModelId: model);
    var options = new ChatOptions
    {
        ResponseFormat = ChatResponseFormat.ForJsonSchema()
    };
    var response = await chatClient.GetResponseAsync(chatMessage: prompt, options: options);
    var profile2 = JsonSerializer.Deserialize(response1.Messages.Last().Text, AIJsonUtilities.DefaultOptions);
    Debug.Assert(profile == new Profile { Name = "张三", Gender = Gender.Male, Age = 26 });
    
    public enum Gender
    {
        Male,
        Female,
    }
    class Profile:IEquatable<Profile>
    {
        public string? Name { get; set; }
        public Gender Gender { get; set; }
        public int Age { get; set; }
        public bool Equals(Profile? other)
        { 
            if(other is null) return false;
            return Name == other.Name && Gender == other.Gender && Age == other.Age;
        }
    }
    
  • 相关阅读:
    可视化工具Netron介绍
    JAVA - 反射
    二、SpringBoot自动装配及SPI的理解
    (附源码)小程序springboot口腔诊所预约系统 毕业设计 201738
    ROS机器人实战,对标古月老师HRMRP机器人(一)——机器人总体方案设计
    Rabin Karp 算法详解及Python实现
    MySQL 基本操作1
    Discovery服务发现与Eureka自我保护机制及总结步骤
    【视觉基础篇】10 # 图形系统如何表示颜色?
    Nginx在前后端分离项目中的配置
  • 原文地址:https://www.cnblogs.com/artech/p/20208525/agent-pipeline-04