[控场AI]
· 10 分钟阅读· 5,492 字

4个.NET聊天图像生成接线错误及修复

4个.NET聊天图像生成接线错误及修复

拆解.NET聊天管道中图像生成的四个静默失败接线错误及离线测试捕获方法。

本文基于Microsoft.Extensions.AI 10.10的实践,分析在聊天管道中集成图像生成时常见的四个接线错误:中间件顺序颠倒导致生成器不被调用、将HostedImageGenerationTool误用为功能开关、未控制历史图像数据推回上下文窗口、以及从错误位置查找生成图像。每个错误都伴随运行时静默失败现象,文章提供了离线测试策略,通过断言生成器调用次数和图像查找结果来捕获问题,无需API密钥。最后给出上线前检查清单,强调编译期无警告时需依赖协议层测试。

在chat pipeline中集成图像生成时,代码能编译通过,运行时却静默失败:用户请求生成图片,助手回复文本却无图,且无异常警告。本文基于Microsoft.Extensions.AI 10.10的实践经验,拆解四个常见接线错误、运行时表现及用离线测试捕获的方法。

4个.NET聊天图像生成接线错误

核心组件速览

API设计简洁,由三部分组成:

  • IImageGenerator:执行实际生成(输入提示,输出图像字节或URI)。
  • HostedImageGenerationTool:放入ChatOptions.Tools,标记“本对话允许生成图像”。
  • UseImageGeneration():添加ImageGeneratingChatClient中间件,将标记替换为模型可调用的真实函数:GenerateImage、EditImage和GetImagesForEdit。

每个组件都有一种“能编译但运行时出错”的接线方式。下面逐一拆解。

ChatClientBuilder采用管道模式构建IChatClient,每个Use调用都会将中间件包装在当前客户端之外。因此第一个Use添加的是最外层中间件,后添加的反而更靠近底层模型。理解这一点是排查中间件顺序问题的关键:图像中间件需要先于函数调用中间件添加,才能确保它先将标记转换为可执行函数,再传递给内层的函数调用器。

错误1:中间件顺序颠倒导致空回复

最初的问题是:输入“生成一个蓝绿色六边形”,得到空文本回复,无图片,生成器调用计数为0。

ChatClientBuilder中,第一个Use是最外层。图像中间件必须位于函数调用中间件之外,因为它负责将标记转换为GenerateImage等函数,底层的函数调用器才能看到真实函数并执行。

错误顺序:函数调用在外,图像中间件在内。此时函数调用器收不到可执行函数,生成器调用0次,响应中只有ImageGenerationToolCallContent(模型请求图片但无人应答)。

正确顺序:图像中间件在外,函数调用在内。

// 错误
IChatClient broken = new ChatClientBuilder(model)
    .UseFunctionInvocation()
    .UseImageGeneration(imageGenerator)
    .Build();

// 正确
IChatClient chat = new ChatClientBuilder(model)
    .UseImageGeneration(imageGenerator)
    .UseFunctionInvocation()
    .Build();

捕获方法:测试断言生成器确实被调用。只检查“无异常”的测试会通过错误管道。

错误2:将标记当作功能开关滥用

HostedImageGenerationTool不生成任何东西,甚至不是AIFunction。它是一个名为image_generation的标记,可携带默认ImageGenerationOptions。两个后果:

  1. 它是权限开关。若一半流量是普通问答,不要给每个请求都附加它。
  2. 它的选项是限制模型请求的关口。例如,通过Count和尺寸限制,避免模型接受“生成20个变体”而拖垮磁盘和账单。
static ChatOptions CreateOptions(bool allowImages, int requestedCount)
{
    var options = new ChatOptions { Instructions = "You are a helpful assistant." };
    if (!allowImages) return options;

    options.Tools = [
        new HostedImageGenerationTool
        {
            Options = new ImageGenerationOptions
            {
                MediaType = "image/png",
                Count = Math.Clamp(requestedCount, 1, 4), // 由你的CDN账单决定,而非模型
                ResponseFormat = ImageGenerationResponseFormat.Data,
            },
        },
    ];
    return options;
}

捕获方法:测试中不附带标记时,生成器调用计数保持0。

HostedImageGenerationTool的实现本质是一个AITool派生类,但它不包含任何可执行逻辑,仅携带ImageGenerationOptions元数据。中间件会扫描ChatOptions.Tools中是否存在该标记,若存在则注入GenerateImage等函数定义。因此它的存在与否直接决定了模型是否能看到图像生成能力,这使它成为一种隐式的权限开关,而非普通的功能工具。

错误3:将图像像素推回上下文窗口

图像相比token体量巨大。对话第三轮时,不应将之前所有PNG以原始字节发送给模型。ImageGeneratingChatClient的DataContentHandling设置解决此问题:

  • AllImages(默认):历史中所有图像以带图像ID的短文本占位符(含meai_image:)到达模型,而非字节。
  • GeneratedImages:仅替换管道生成的图像,用户上传的图像仍然通过,适合需要模型查看参考图的场景。
  • None:全部通过,仅适用于真正的多模态模型且预算充足。

显式设置时需手动构建层:

IChatClient functionLayer = new ChatClientBuilder(model)
    .UseFunctionInvocation()
    .Build();

IChatClient chat = new ImageGeneratingChatClient(
    functionLayer,
    imageGenerator,
    ImageGeneratingChatClient.DataContentHandling.AllImages);

顺序与错误1相同,只是从内向外写。

占位符并非装饰。EditImage依赖imageId定位图片,若聊天历史缩减器或摘要器丢弃占位符行,后续轮次的编辑操作将失去目标。确保历史修剪逻辑保留这些占位符。

捕获方法:发送包含PNG DataContent的用户消息,通过捕获内部客户端,断言其收到含meai_image:的文本且无DataContent。

DataContentHandling是ImageGeneratingChatClient的构造参数,控制历史消息中DataContent的替换策略。AllImages模式下,所有图像(包括用户上传的)都会被替换为meai_image:占位符文本,并附带图像ID;GeneratedImages仅替换由管道生成的图像;None则保留原始字节。占位符中的图像ID用于后续EditImage调用定位原始图像,若历史修剪时丢失占位符,编辑功能将无法找到目标。

错误4:在错误位置查找图像

最隐蔽的错误:图像其实一直都在。成功生成后,响应内容为:

  • ImageGenerationToolCallContent
  • ImageGenerationToolResultContent
  • TextContent(助手“您的图像已准备好”的回复)

顶层没有DataContent项。中间件将函数调用和结果重写为图像特定内容类型,PNG位于结果的Outputs内。若映射器从最后一条消息抓取DataContent,将一无所获,UI会显示“图像已准备好”却没有图像。

正确映射:

static IEnumerable<DataContent> FindImages(ChatResponse response)
{
    foreach (var content in response.Messages.SelectMany(m => m.Contents))
    {
        if (content is ImageGenerationToolResultContent result)
        {
            foreach (var image in (result.Outputs ?? []).OfType<DataContent>())
                yield return image;
        }
    }
    // 完整实现还包括媒体类型检查、多模态模型的顶层DataContent、保存到磁盘及base64预览
}

捕获方法:单元测试断言FindImages返回至少一个图像。

离线测试:无需API密钥验证协议

测试协议而非像素。使用离线IChatClient(根据关键词发出GenerateImage调用)和离线IImageGenerator(返回确定性PNG字节),核心测试简短:

[Fact]
public async Task Generate_turn_runs_the_generator_and_returns_an_image()
{
    using var model = new OfflineImageAwareChatClient(); // 发出GenerateImage,无需API密钥
    using var generator = new OfflineImageGenerator();   // 相同提示,相同字节
    using IChatClient chat = new ChatClientBuilder(model)
        .UseImageGeneration(generator)
        .UseFunctionInvocation()
        .Build();

    var response = await chat.GetResponseAsync(
        "generate an image of a teal hexagon",
        CreateOptions(allowImages: true, requestedCount: 1));

    Assert.Equal(1, generator.CallCount);   // 捕获错误1
    Assert.Single(FindImages(response));     // 捕获错误4
}

交换两个Use行,第一个断言失败;破坏映射器,第二个断言失败。两个伴随测试覆盖其余:无标记时生成器计数为0(错误2),AllImages下传入PNG以占位符到达内部客户端(错误3)。

上线前检查清单

  • UseImageGeneration()在构建器中先于UseFunctionInvocation()。
  • 测试断言生成器被调用,而不仅是无异常。
  • 仅在允许图像处附加HostedImageGenerationTool,并在服务端钳制Count和尺寸。
  • 除非有理由,DataContentHandling保持AllImages。
  • 历史缩减器保留meai_image:占位符。
  • 映射器读取ImageGenerationToolResultContent.Outputs。
  • MEAI001在项目文件中统一抑制并注释,而非散落pragma。
  • 提供商密钥存入user-secrets或密钥保管库,绝不放入appsettings.json。

这些错误的共同点是编译期无警告、运行时静默失败,仅靠“无异常”测试无法捕获。通过离线测试验证协议层行为,能在无API密钥的情况下提前暴露问题。完整可运行项目包含61个离线测试,所有代码片段均在仓库中实现。

(本文基于技术博客内容编译,完整项目见原文链接。)

#.NET图像生成#Microsoft.Extensions.AI#聊天图像生成#中间件顺序#DataContentHandling#IImageGenerator#离线测试#AI集成
分享:

相关推荐