找回密码
 立即注册
首页 业界区 业界 还在手写JSON调教大模型?.NET 9有新玩法

还在手写JSON调教大模型?.NET 9有新玩法

猷浮 昨天 07:10
引言

.NET 9 迎来了一项备受期待的功能更新:对JSON Schema的原生支持。这一新增功能极大地简化了JSON Schema的生成与使用。JSON Schema作为一种描述JSON数据结构的标准格式,能够帮助我们有效地验证数据结构和类型。尤其在与大语言模型(LLM)进行交互的场景中,它扮演着至关重要的角色,可以精确定义模型输入与输出的数据格式,从而确保通信的准确性和可靠性。
1.png

.NET 9 的新利器:JsonSchemaExporter

让我们从一个简单的例子开始。在.NET 9中,我们可以利用 System.Text.Json 命名空间下的新工具 JsonSchemaExporter,轻松地将一个C#类转换为对应的JSON Schema。
首先,我们定义一个名为 Book 的C#类。这个类包含三个属性:Title、Author 和 PublishYear。其中,Title 是一个必需的字符串属性,Author 是一个可空字符串属性,而 PublishYear 是一个整数。
  1. // 定义一个名为 Book 的类
  2. public class Book
  3. {
  4.     // 必须的字符串属性 Title
  5.     public required string Title { get; set; }
  6.     // 可选的字符串属性 Author,允许为 null
  7.     public string? Author { get; set; }
  8.     // 整数属性 PublishYear
  9.     public int PublishYear { get; set; }
  10. }
复制代码
接下来,在主程序中,我们只需调用 JsonSchemaExporter.GetJsonSchemaAsNode 方法,并传入我们的 Book 类型,即可生成其JSON Schema。
  1. class Program
  2. {
  3.     static void Main()
  4.     {
  5.         // 使用 JsonSchemaExporter 为 .NET 类型生成 JSON schema
  6.         var schema = JsonSchemaExporter.GetJsonSchemaAsNode(
  7.             JsonSerializerOptions.Default,
  8.             typeof(Book)
  9.         );
  10.         // 输出生成的 JSON schema
  11.         Console.WriteLine(schema);
  12.     }
  13. }
复制代码
运行上述代码,我们将得到以下JSON Schema输出:
  1. {
  2.   "type": [
  3.     "object",
  4.     "null"
  5.   ],
  6.   "properties": {
  7.     "Title": {
  8.       "type": [
  9.         "string",
  10.         "null"
  11.       ]
  12.     },
  13.     "Author": {
  14.       "type": [
  15.         "string",
  16.         "null"
  17.       ]
  18.     },
  19.     "PublishYear": {
  20.       "type": "integer"
  21.     }
  22.   },
  23.   "required": [
  24.     "Title"
  25.   ]
  26. }
复制代码
这个特性使得我们能够以标准化的JSON Schema形式来表示一个.NET类型,这对于实现远程过程调用(RPC)或与OpenAI、Google Gemini等AI服务进行集成时非常有用。
实战:结合OpenAI实现结构化输出

我们知道,从 gpt-4o-0806 开始,OpenAI便支持了通过JSON Schema来约束模型的输出格式。这个功能极大地便利了开发者,尤其是在需要模型返回复杂数据结构时。然而,手动编写和维护这些JSON Schema既繁琐又容易出错。现在,借助.NET 9的 JsonSchemaExporter,这个过程变得前所未有的简单。
注意:以下示例需要依赖 Azure.AI.OpenAI NuGet包 2.2.0 或更高版本。
让我们来看一个实际的例子。假设我们需要让大模型分析一段关于篮子里放球、取球的描述,并返回一个包含各种颜色球类数量的结构化数据。
首先,定义我们期望的输出数据结构 BallCounts 类:
  1. public class BallCounts
  2. {
  3.     public string? Think { get; init; } // 模型的思考过程
  4.     public int Red { get; init; }
  5.     public int Blue { get; init; }
  6.     public int Yellow { get; init; }
  7.     public int Green { get; init; }
  8.     public int Confidence { get; init; } // 模型对答案的置信度
  9. }
复制代码
然后,在调用模型时,我们可以将这个 BallCounts 类型动态生成JSON Schema,并直接传递给 ChatCompletionOptions 的 ResponseFormat。
  1. // 初始化OpenAI客户端
  2. OpenAIClient api = new AzureOpenAIClient(new Uri($"https://{Util.GetPassword("azure-ai-resource")}.openai.azure.com/"), new AzureKeyCredential(Util.GetPassword("azure-ai-key")));
  3. ChatClient cc = api.GetChatClient("gpt-4o");
  4. // 准备请求
  5. var result = await cc.CompleteChatAsync(
  6.     [
  7.         new SystemChatMessage("你是人工智能助理"),
  8.         new UserChatMessage("""
  9.         有个空篮子,放里面放1个红色球,再往里面放1个蓝色球,再把红色球和黄色球拿出来,再放2个绿色球,请问这个篮子里面有几个球?分别是什么颜色?
  10.         """),
  11.     ],
  12.     new ChatCompletionOptions()
  13.     {
  14.         Temperature = 0,
  15.         // 关键点:动态生成并设置JSON Schema
  16.         ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
  17.             nameof(BallCounts),
  18.             BinaryData.FromBytes(
  19.                 JsonSerializer.SerializeToUtf8Bytes(
  20.                     JsonSchemaExporter.GetJsonSchemaAsNode(
  21.                         JsonSerializerOptions.Default,
  22.                         typeof(BallCounts),
  23.                         new JsonSchemaExporterOptions()
  24.                         {
  25.                             // 确保非可空引用类型不被视为可空
  26.                             TreatNullObliviousAsNonNullable = true
  27.                         }
  28.                     )
  29.                 )
  30.             )
  31.         )
  32.     });
  33. // 反序列化并输出结果
  34. var ballCountsResult = JsonSerializer.Deserialize<BallCounts>(result.Value.Content[0].Text, JsonSerializerOptions.Default);
  35. Console.WriteLine(JsonSerializer.Serialize(ballCountsResult, new JsonSerializerOptions { WriteIndented = true }));
复制代码
模型返回的将是严格符合我们C#类结构的JSON:
  1. {
  2.   "Think": "Let's break down the steps:\n\n1. Start with an empty basket.\n2. Add 1 red ball. (Basket: 1 red)\n3. Add 1 blue ball. (Basket: 1 red, 1 blue)\n4. Remove the red ball. (Basket: 1 blue)\n5. Remove the yellow ball. (Since there is no yellow ball in the basket, this step doesn't change anything. Basket: 1 blue)\n6. Add 2 green balls. (Basket: 1 blue, 2 green)\n\nSo, the basket contains 3 balls: 1 blue and 2 green.",
  3.   "Red": 0,
  4.   "Blue": 1,
  5.   "Yellow": 0,
  6.   "Green": 2,
  7.   "Confidence": 100
  8. }
复制代码
随心所欲,轻松扩展

这个方法的强大之处在于其灵活性。如果我们想调整输出结构,比如增加两种颜色(紫色和橙色),并将 Confidence 属性改为布尔类型的 Sure,我们 只需要修改C#类定义 即可。
  1. public class BallCounts
  2. {
  3.     public string? Think { get; init; }
  4.     public int Red { get; init; }
  5.     public int Blue { get; init; }
  6.     public int Yellow { get; init; }
  7.     public int Green { get; init; }
  8.     public int Purple { get; init; } // 新增
  9.     public int Orange { get; init; } // 新增
  10.     public bool Sure { get; init; }   // 修改
  11. }
复制代码
我们无需改动任何调用逻辑,只需重新运行程序,模型就会自动适应新的 BallCounts 结构,输出的JSON会神奇地包含所有新的颜色属性,并且 Sure 属性也被正确地处理为布尔值。
  1. {
  2.   "Think": "Let's break down the steps:\n\n1. Start with an empty basket.\n2. Add 1 red ball.\n3. Add 1 blue ball.\n4. Remove the red ball.\n5. Remove the yellow ball (though there was no yellow ball added, so this step doesn't change the count).\n6. Add 2 green balls.\n\nAfter these steps, the basket contains:\n- 1 blue ball\n- 2 green balls\n\nTotal: 3 balls.",
  3.   "Red": 0,
  4.   "Blue": 1,
  5.   "Yellow": 0,
  6.   "Green": 2,
  7.   "Purple": 0,
  8.   "Orange": 0,
  9.   "Sure": true
  10. }
复制代码
可见,模型完美地适应了新的数据契约。这种开发体验简直不要太方便!
对比:新方法 vs. 传统方法

有趣的是,在OpenAI官方的.NET SDK文档中,关于结构化响应的示例 (openai-dotnet/README.md) 并没有采用 JsonSchemaExporter,而是手动编写了一大段JSON Schema字符串。这可能是因为官方文档旨在展示其库的原始能力,而非特定于.NET 9的便捷特性。
这是官网的示例代码,我们可以看到它相当繁琐:
  1. // ...
  2. ChatCompletionOptions options = new()
  3. {
  4.     ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
  5.         jsonSchemaFormatName: "math_reasoning",
  6.         jsonSchema: BinaryData.FromBytes("""
  7.             {
  8.                 "type": "object",
  9.                 "properties": {
  10.                     "steps": {
  11.                         "type": "array",
  12.                         "items": {
  13.                             "type": "object",
  14.                             "properties": {
  15.                                 "explanation": { "type": "string" },
  16.                                 "output": { "type": "string" }
  17.                             },
  18.                             "required": ["explanation", "output"],
  19.                             "additionalProperties": false
  20.                         }
  21.                     },
  22.                     "final_answer": { "type": "string" }
  23.                 },
  24.                 "required": ["steps", "final_answer"],
  25.                 "additionalProperties": false
  26.             }
  27.             """u8.ToArray()),
  28.         jsonSchemaIsStrict: true)
  29. };
  30. // ...
复制代码
与我们前面的方法相比,这种硬编码Schema的方式不仅工作量大,而且在需求变更时极难维护。而.NET 9的 JsonSchemaExporter 真正实现了“定义一次,处处使用”的优雅编程范式。
值得一提的是,据我了解,Google Gemini、Ollama等其他主流模型服务提供商也已支持JSON Schema格式化。这意味着您学到的这项技巧具有广泛的适用性。
总结

.NET 9中引入的 JsonSchemaExporter 无疑是.NET开发者与大语言模型协作时的一大福音。它将繁琐、易错的JSON Schema手动编写过程,转变为基于C#类型定义的自动化、强类型、可维护的流程。
2.png

这项功能特别好,它是将基于概率、本质不稳定的语言模型进行工程化,提升其输出结果稳定性和可靠性的关键手段。强烈建议您在自己的项目中,尤其是在与大模型交互时,全面拥抱JSON Schema来定义数据契约。借助.NET 9,我们现在可以无比轻松地生成和使用JSON Schema,从而显著提高代码的可读性、可维护性和整体开发效率。
感谢阅读,欢迎评论点赞!
欢迎加入「.NET骚操作」技术交流群495782587

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册