image.png

并不是所有的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);

输出结果正常情况下能够序列化。
Pasted image 20251108224712.png

流式响应

流式响应输出与普通响应不同,流式输出需要将返回结果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);

输出结果如下:
Pasted image 20251108234902.png

通过提示词方式,比较适合结构层级简单,不需要过多关注内容,而只关注结构的数据,同时本地可以不用构建指定强类型对象。

使用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 = "小美",
};

然后配置ChatOptionsResponseFormatChatResponseFormat.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 在兼容性与可靠性上通常是最稳妥的方案。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐