(二) Dotnet使用Microsoft.Agents.AI生成结构化输出
本文对比了不同AI模型的结构化输出实现方案。支持结构化输出的模型(如ChatGPT)可通过JSON Schema和ResponseFormat配置直接生成结构化响应。对于不支持该功能的模型(如DeepSeek、Qwen),建议采用两种替代方法:通过精确设计的提示词约束输出格式,或利用Function Call机制包装响应。文中详细阐述了普通响应和流式响应的处理逻辑,并提供了适配不同模型特性的代码示

并不是所有的Agent 都支持结构化输出,主要取决于模型服务本身的能力,当前ChatClientAgent 实例是支持的。
同时代理的结构化配置依托于ChatOptions类,是否完全按照结构进行返回,也取决于模型服务是否能够支持。
利用 JSON Schema(若模型支持)
简单一些的方式,构建一个结构化输出类型,使用AIJsonUtilities.CreateJsonSchema通过类型生成schame 结构。以ChatSchemaResponse 类为例。
[Description("会话响应模型")]
public class ChatSchemaResponse
{
[Description("响应码 200 表示理解,500 表示无法理解问题,需在msg中说明具体原因")]
[JsonPropertyName("code")]
public int Code { get; set; }
[Description("仅在Code=500时填写错误说明")]
[JsonPropertyName("msg")]
public string Message { get; set; } = string.Empty;
[Description("包含回答完整内容")]
[JsonPropertyName("data")]
public object Data { get; set; }
}
设置ChatOptions实例中ResponseFormat控制结构化输出格式为ChatResponseFormat.ForJsonSchema()。
对于Chatgpt 响应可以直接设置schame,配置如下:
var schema = AIJsonUtilities.CreateJsonSchema(typeof(ChatSchemaResponse));
chatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema(schema, nameof(ChatSchemaResponse),
"""
- code = 200:表示成功理解并回答
- code = 500:表示无法理解问题,需在msg中说明具体原因
- data:包含英文回答和中文解释的完整内容
- msg:仅在code=500时填写错误说明
""");
获取结构化数据序列化如下:
var response = await agent.RunAsync(history, thread);
var chatsponse = response.Deserialize<ChatSchemaResponse>(JsonSerializerOptions.Web);
对于国内deepseek以及qwen系列,目前并不支持,目前已知有两种方式,一种是通过提示词想定结构,一种是使用function call 方式实现。
使用提示词格式化输出
对提示词进行如下修改,强制模型服务响应时输出对应格式结果。
ChatClientAgentOptions agentOptions = new() {
Instructions = """
你是一个英语助手,拥有10年初中教学经验,对于用户提问,英文回复并用中文解释。理解职责,中文输出自身能力。
严格按照以下JSON格式输出响应:
{"code":200,"msg":"","data":"你的完整回答内容(包含英文回复和中文解释)"}
输出规则:
- code = 200:表示成功理解并回答
- code = 500:表示无法理解问题,需在msg中说明具体原因
- data:包含英文回答和中文解释的完整内容
- msg:仅在code=500时填写错误说明
""",
ChatOptions = chatOptions,
Name = "小美",
};
配置结构化输出格式ResponseFormat。
// 针对deepseek以及qwen模型,设置响应格式为Json
chatOptions.ResponseFormat = ChatResponseFormat.Json;
普通响应
使用RunAsync() 进行响应输出时,代码如下:
var response = await agent.RunAsync(history, thread);
var chatsponse = response.Deserialize<ChatSchemaResponse>(JsonSerializerOptions.Web);
Console.WriteLine("A:" + response);
输出结果正常情况下能够序列化。
流式响应
流式响应输出与普通响应不同,流式输出需要将返回结果AgentRunResponseUpdate进行临时存储,流输出成功后使用ToAgentRunResponse()合并为一个AgentRunResponse对象。
List<AgentRunResponseUpdate> updates = [];
Console.Write("A:");
await foreach (var update in agent.RunStreamingAsync(history, thread))
{
// 输出实时流对话结果
Console.Write(update);
// 将分片数据添加到一个集合AgentRunResponseUpdate中
updates.Add(update);
}
// 将流进行序列化
var respone = updates.ToAgentRunResponse();
var chatsponse = respone.Deserialize<ChatSchemaResponse>(JsonSerializerOptions.Web);
输出结果如下:
通过提示词方式,比较适合结构层级简单,不需要过多关注内容,而只关注结构的数据,同时本地可以不用构建指定强类型对象。
使用Function Call 格式化输出
方式一,使用FunctionCallContent
首先设置对应IChatClient 对应实例启用本地函数。
IChatClient client = new ChatClient(model.ModelId, new ApiKeyCredential(platform.Token), new OpenAIClientOptions
{
Endpoint = new Uri(platform.Url), // 指定api访问地址
})
.AsIChatClient() // ChatClient转换为IChatClient
.AsBuilder()
.UseFunctionInvocation() // 启用function call 本地函数工具调用
.Build();
工具函数如下,其中参数名称为respone,为ChatSchemaResponse 类型:
/// <summary>
/// 问答响应
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Description("问答响应")]
public static void ChatResponse([Description("响应结果")] ChatSchemaResponse response)
{
}
修改提示词,让其调用函数工具,其中ChatResponse 为注册本地函数工具时的名称。
ChatClientAgentOptions agentOptions = new() {
Instructions = """
你是一个英语助手,拥有10年初中教学经验,对于用户提问,英文回复并用中文解释。理解职责,中文输出自身能力。请使用 ChatResponse 保存你的回答,确保响应符合指定的结构化格式要求。
""",
ChatOptions = chatOptions,
Name = "小美",
};
然后配置ChatOptions 的ResponseFormat 为 ChatResponseFormat.Json进行注释。
// 针对deepseek以及qwen模型,设置响应格式为Json
//chatOptions.ResponseFormat = ChatResponseFormat.Json;
普通响应
var response = await agent.RunAsync(history, thread);
var functioncall = response.Messages.SelectMany(x => x.Contents)
.OfType<FunctionCallContent>()
.FirstOrDefault();
// 获取函数回调
if (functioncall != null)
{
if (functioncall.Arguments.TryGetValue("response", out object chatobj))
{
var chatsponse = JsonSerializer.Deserialize<ChatSchemaResponse>(chatobj.ToString(), JsonSerializerOptions.Web);
Console.WriteLine("A:" + chatobj);
}
}
流式响应
List<AgentRunResponseUpdate> updates = [];
Console.Write("A:");
await foreach (var update in agent.RunStreamingAsync(history, thread))
{
// 输出实时流对话结果
Console.Write(update);
updates.Add(update);
}
// 将流进行合并一个响应对象
var response = updates.ToAgentRunResponse();
var functioncall = response.Messages.SelectMany(x => x.Contents)
.OfType<FunctionCallContent>()
.FirstOrDefault();
if (functioncall != null)
{
if (functioncall.Arguments.TryGetValue("response", out object chatobj))
{
var chatsponse = JsonSerializer.Deserialize<ChatSchemaResponse>(chatobj.ToString(), JsonSerializerOptions.Web);
Console.WriteLine("A:" + chatobj);
}
}
运行输出如下:
A:{"code": 200, "data": "Hello! I'm an English assistant with 10 years of experience teaching middle school students. I can help you with:\n\n**我的能力包括:**\n- 英语语法解释和练习\n- 词汇学习和用法指导\n- 阅读理解帮助\n- 写作技巧指导\n- 发音和口语练习\n- 英语学习策略建议\n\n**我的服务方式:**\n- 用英文回答你的问题\n- 用中文解释关键概念\n- 提供实用的学习建议\n- 根据你的水平调整教学方式\n\n请告诉我你需要什么帮助,我会用英文回答并用中文解释重要内容!"}
>Q:
方式二、使用自定义对象提取器
创建一个对象提取类,当前为ChatResponsePicker类,对应函数也不再是静态函数。
/// <summary>
/// 提取类
/// </summary>
public class ChatResponsePicker
{
/// <summary>
/// 承载问答响应结果
/// </summary>
public ChatSchemaResponse Response { get; private set; }
/// <summary>
/// 问答响应
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Description("问答响应")]
public void ChatResponse([Description("响应结果")] ChatSchemaResponse response)
{
Response = response;
// 可进行校验等操作
}
}
修改函数注册为实例函数。
//------------------------- 注册工具 -----------------------------
ChatResponsePicker chatResponsePicker = new();
// 使用AIFunctionFatory创建AIFunction实例
chatOptions.Tools = [AIFunctionFactory.Create(WeatherFunction.GetCurrentWeather),
AIFunctionFactory.Create(chatResponsePicker.ChatResponse)];
//------------------------- 注册工具 -----------------------------
提示词与FunctionCallContent 获取方式一样,不做任何改变。
ChatClientAgentOptions agentOptions = new() {
Instructions = """
你是一个英语助手,拥有10年初中教学经验,对于用户提问,英文回复并用中文解释。理解职责,中文输出自身能力。请使用 ChatResponse 保存你的回答,确保响应符合指定的结构化格式要求。
""",
ChatOptions = chatOptions,
Name = "小美",
};
普通响应
var response = await agent.RunAsync(history, thread);
var chatsponse = chatResponsePicker.Response; // 获取转换结果
流式响应
List<AgentRunResponseUpdate> updates = [];
Console.Write("A:");
await foreach (var update in agent.RunStreamingAsync(history, thread))
{
// 输出实时流对话结果
Console.Write(update);
updates.Add(update);
}
Console.WriteLine("A:" + JsonSerializer.Serialize(chatResponsePicker.Response));
输出结果如下:
A:{"code":200,"msg":"","data":"Hello! I\u0027m an English assistant with 10 years of experience in middle school teaching. I can help you with:\n\n- English grammar explanations\n- Vocabulary building\n- Reading comprehension\n- Writing skills\n- Pronunciation guidance\n- Test preparation\n- Homework assistance\n\nI\u0027ll provide answers in English first, then explain in Chinese to ensure clear understanding. Please feel free to ask any English-related questions!"}
何时选用哪种方式(建议)
- 模型支持 Schema:优先使用 JSON Schema(方式 A),最稳健且类型安全。
- 模型不支持但可以接受严格提示:尝试 Prompt 强制 JSON(方式 B),并准备容错解析策略。
- 需要最高可靠性或要与现有代码无缝集成:使用 Function Call(方式 C),把结构化结果作为函数参数返回并在本地验证。
常见问题与调试建议
- 模型返回额外文本或格式不规范:先做正则或简单 JSON 修正,再反序列化;或改用 Function Call。
- 流式响应丢失字段:在聚合分片时,优先按时间顺序合并文本并在结束后一次性解析。
- 本地函数未被调用:检查是否正确启用了函数调用(UseFunctionInvocation)及工具是否已注册。
小结
本文示例展示了三种常见方法来实现结构化输出:JSON Schema、Prompt 强制 JSON、以及 Function Call。选择应基于模型能力与对返回结果严格性的需求。Function Call 在兼容性与可靠性上通常是最稳妥的方案。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)