GLM-OCR跨平台实战:.NET Core后端服务集成

最近在做一个内部文档处理系统,需要从各种上传的图片里提取文字。一开始试了几个开源的OCR方案,效果总是不太稳定,要么识别率不高,要么对中文支持不好。后来团队引入了GLM-OCR,效果确实提升了不少,但怎么把它优雅地集成到我们现有的.NET Core后端服务里,成了一个新的问题。

直接在每个Controller里写HttpClient调用?代码太乱,也不好维护。网络偶尔波动导致调用失败怎么办?返回的JSON结构有点复杂,每次都手动解析也挺麻烦。这篇文章,我就来分享一下我们团队最终落地的方案:如何用.NET Core的标准姿势,把GLM-OCR封装成一个稳定、易用、好维护的后端服务。

1. 场景与核心挑战

我们的系统是一个典型的.NET Core WebAPI项目,运行在Linux服务器上。用户通过前端页面上传合同、发票、名片等图片,后端需要快速、准确地提取出文字内容,然后进行后续的结构化处理和入库。

直接调用GLM-OCR的HTTP接口听起来简单,但放到生产环境,就得考虑几个实际问题:

  • 连接管理:频繁地创建和销毁HttpClient实例是性能大忌,也容易导致端口耗尽。
  • 网络容错:服务间调用难免遇到网络抖动或目标服务短暂不可用,需要重试机制,不能一失败就报错给用户。
  • 响应处理:OCR接口返回的JSON结构包含文本块、坐标、置信度等多层信息,解析代码要清晰、高效,并且容易应对接口字段的变化。
  • 服务抽象:最好能把OCR能力封装成一个标准的服务(Service),这样业务代码调用起来就像调用本地方法一样简单,后续换用其他OCR提供商也方便。

接下来,我们就围绕这几个点,一步步构建解决方案。

2. 项目结构与基础准备

首先,我们创建一个新的ASP.NET Core Web API项目,或者在你现有的项目中添加相关代码。我们假设你已经有一个运行中的GLM-OCR服务,它提供了一个HTTP API端点,例如 http://your-ocr-server:8000/v1/ocr

我们需要安装几个关键的NuGet包来助力:

dotnet add package Microsoft.Extensions.Http
dotnet add package Polly
dotnet add package Polly.Extensions.Http
dotnet add package System.Text.Json
  • Microsoft.Extensions.Http:提供了IHttpClientFactory,这是我们管理HttpClient生命周期的核心。
  • PollyPolly.Extensions.Http:用来定义和执行各种弹性策略,比如重试、熔断。
  • System.Text.Json:.NET Core高性能的JSON序列化库,我们会用它来解析响应。

在项目里,我们规划了以下几个核心部分:

  1. 一个Models文件夹,存放请求和响应的数据模型类。
  2. 一个Services文件夹,存放我们的OCR服务实现。
  3. Program.cs(或Startup.cs)中进行依赖注入配置。

3. 定义数据模型

先根据GLM-OCR接口的文档,定义我们调用时需要发送的数据和预期返回的数据结构。这能让我们的代码强类型化,避免魔法字符串。

Models文件夹下创建两个类:

OcrRequest.cs

namespace YourProjectName.Models;

public class OcrRequest
{
    /// <summary>
    /// Base64编码的图片数据
    /// </summary>
    public string ImageData { get; set; } = string.Empty;

    /// <summary>
    /// 可选参数,例如识别语言等
    /// </summary>
    public Dictionary<string, object>? Parameters { get; set; }
}

OcrResponse.cs

using System.Text.Json.Serialization;

namespace YourProjectName.Models;

public class OcrResponse
{
    [JsonPropertyName("text")]
    public string FullText { get; set; } = string.Empty;

    [JsonPropertyName("blocks")]
    public List<TextBlock> Blocks { get; set; } = new List<TextBlock>();

    [JsonPropertyName("status")]
    public string Status { get; set; } = "unknown";

    [JsonPropertyName("message")]
    public string? Message { get; set; }
}

public class TextBlock
{
    [JsonPropertyName("box")]
    public List<List<int>>? Box { get; set; } // 文字框坐标

    [JsonPropertyName("text")]
    public string Text { get; set; } = string.Empty;

    [JsonPropertyName("score")]
    public float Confidence { get; set; }
}

这里用了[JsonPropertyName]特性来映射JSON字段名,这样即使接口返回的字段名是text,我们也能用FullText这样更符合C#命名规范的属性来访问。

4. 构建核心OCR服务

现在来创建服务层。在Services文件夹下,我们定义一个接口和它的实现。

IGlmOcrService.cs

using YourProjectName.Models;

namespace YourProjectName.Services;

public interface IGlmOcrService
{
    Task<OcrResponse> RecognizeTextAsync(OcrRequest request, CancellationToken cancellationToken = default);
}

GlmOcrService.cs 这是重头戏,我们一步步来实现。

using System.Net.Http.Json; // 用于方便的JSON序列化/反序列化
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
using YourProjectName.Models;

namespace YourProjectName.Services;

public class GlmOcrService : IGlmOcrService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<GlmOcrService> _logger;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;

    // 构造函数注入配置好的HttpClient和Logger
    public GlmOcrService(HttpClient httpClient, ILogger<GlmOcrService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;

        // 使用Polly定义重试策略:针对网络超时和5xx服务器错误重试3次,每次间隔指数退避
        _retryPolicy = Policy
            .HandleResult<HttpResponseMessage>(r =>
                (int)r.StatusCode >= 500 || // 服务器错误
                r.StatusCode == System.Net.HttpStatusCode.RequestTimeout // 超时
            )
            .Or<HttpRequestException>() // 网络请求异常
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 2, 4, 8秒
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    _logger.LogWarning("OCR API调用失败,正在进行第 {RetryCount} 次重试。错误:{Error}",
                        retryCount, outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString());
                });
    }

    public async Task<OcrResponse> RecognizeTextAsync(OcrRequest request, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(request.ImageData))
        {
            throw new ArgumentException("图片数据不能为空。");
        }

        try
        {
            // 使用Polly包装的HttpClient执行请求
            var response = await _retryPolicy.ExecuteAsync(async () =>
            {
                // 这里假设OCR服务接收JSON body,包含image_data字段
                var requestBody = new { image_data = request.ImageData, parameters = request.Parameters };
                return await _httpClient.PostAsJsonAsync("", requestBody, cancellationToken); // 地址在配置中设置
            });

            // 确保响应成功
            response.EnsureSuccessStatusCode();

            // 使用System.Text.Json反序列化响应内容
            var ocrResult = await response.Content.ReadFromJsonAsync<OcrResponse>(cancellationToken: cancellationToken);

            if (ocrResult == null)
            {
                throw new InvalidOperationException("OCR服务返回了空或无法解析的响应。");
            }

            _logger.LogInformation("OCR识别成功,共识别出 {BlockCount} 个文本块。", ocrResult.Blocks.Count);
            return ocrResult;
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "调用OCR服务时发生网络错误。");
            // 可以返回一个包含错误状态的OcrResponse,或者直接抛出,根据业务逻辑决定
            return new OcrResponse
            {
                Status = "error",
                Message = $"网络请求失败:{ex.Message}"
            };
        }
        catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            _logger.LogInformation("OCR识别任务被用户取消。");
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "处理OCR响应时发生未知错误。");
            throw;
        }
    }
}

代码要点解析:

  1. 依赖注入:通过构造函数注入HttpClientILogger,这是.NET Core服务的标准做法。
  2. Polly重试策略:在构造函数中定义了一个重试策略。如果遇到服务器错误(5xx)或超时,它会自动重试最多3次,并且每次重试的等待时间指数级增加(2秒、4秒、8秒),避免给故障服务造成更大压力。
  3. 核心调用RecognizeTextAsync方法中,使用_retryPolicy.ExecuteAsync来执行实际的HTTP POST请求。PostAsJsonAsync方法会自动将对象序列化为JSON并设置正确的Content-Type。
  4. 响应处理:使用ReadFromJsonAsync将响应的JSON流直接反序列化成我们定义好的OcrResponse对象,非常高效。
  5. 异常处理与日志:使用ILogger记录了不同级别的日志,这对于生产环境调试和监控至关重要。对不同的异常(网络异常、取消请求、解析异常)进行了分类处理。

5. 配置依赖注入

服务写好了,怎么让它跑起来呢?需要在Program.cs中把它注册到依赖注入容器里。

using YourProjectName.Services;

var builder = WebApplication.CreateBuilder(args);

// 添加服务到容器
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 关键配置:注册OCR服务
builder.Services.AddHttpClient<IGlmOcrService, GlmOcrService>((serviceProvider, client) =>
{
    // 从配置文件中读取OCR服务的基础地址
    var configuration = serviceProvider.GetRequiredService<IConfiguration>();
    client.BaseAddress = new Uri(configuration["OcrService:BaseUrl"] ?? "http://localhost:8000/v1/");
    
    // 可以配置一些默认的HTTP客户端设置,比如超时
    client.Timeout = TimeSpan.FromSeconds(30);
    
    // 如果需要,可以在这里添加默认请求头,例如API Key
    // client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
})
.AddPolicyHandlerFromRegistry((policyRegistry, request) =>
{
    // 这里可以配置更丰富的策略,比如熔断器
    // 我们已经在GlmOcrService内部实现了重试,这里也可以添加一个基础的超时策略
    return policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>("RetryPolicy") ?? Policy.NoOpAsync<HttpResponseMessage>();
});

// 如果你有多个HttpClient需要不同的策略,可以这样配置一个策略注册表
// builder.Services.AddPolicyRegistry();

var app = builder.Build();

// 配置HTTP请求管道
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

appsettings.json中配置你的OCR服务地址:

{
  "OcrService": {
    "BaseUrl": "http://your-ocr-server:8000/v1/"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

6. 在Controller中调用

最后,在API控制器中注入并使用我们的OCR服务,就非常简单了。

using Microsoft.AspNetCore.Mvc;
using YourProjectName.Models;
using YourProjectName.Services;

namespace YourProjectName.Controllers;

[ApiController]
[Route("api/[controller]")]
public class OcrController : ControllerBase
{
    private readonly IGlmOcrService _ocrService;

    public OcrController(IGlmOcrService ocrService)
    {
        _ocrService = ocrService;
    }

    [HttpPost("recognize")]
    public async Task<ActionResult<OcrResponse>> RecognizeText([FromBody] OcrRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var result = await _ocrService.RecognizeTextAsync(request);
        
        if (result.Status == "error")
        {
            // 根据业务逻辑,可以返回400或500等状态码
            return BadRequest(result);
        }
        
        return Ok(result);
    }
}

现在,你的前端或者其他服务就可以向/api/ocr/recognize发送一个包含Base64图片数据的POST请求,后端会稳定、高效地返回OCR识别结果。

7. 总结

回顾一下,我们通过几个步骤在.NET Core后端项目中集成了GLM-OCR:

  1. 定义清晰的模型:用强类型类来映射请求和响应,让代码更安全、易读。
  2. 利用IHttpClientFactory:这是管理HttpClient生命周期的官方推荐做法,解决了资源管理和DNS刷新等问题。
  3. 引入Polly实现弹性策略:简单的重试机制就能显著提升服务间调用的成功率,应对临时性故障。
  4. 封装成可注入的服务:将OCR能力抽象成一个服务接口和实现,业务代码调用简单,也符合单一职责原则,未来替换底层OCR引擎成本很低。
  5. 完善的日志和异常处理:这对生产环境排查问题至关重要。

这套方案在我们自己的项目中运行稳定,大大提升了文档处理流程的自动化程度和可靠性。如果你也在做类似的功能集成,不妨试试这个模式,可以根据自己的实际需求调整重试策略、超时时间或者添加熔断等更高级的弹性机制。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐