.NET 实战:调用千问视觉模型实现 OCR(车票识别完整教程)

很多人第一次接入 AI OCR,会踩几个典型坑:

  • ❌ 直接调接口,结果不稳定

  • ❌ AI 返回 JSON 不规范

  • ❌ 网络偶发失败没有兜底

  • ❌ 代码无法复用

这篇直接带你做一件事:

写一套"可复用 + 稳定 + 可扩展"的 OCR 调用组件


🧠 一、核心思路(先搞清本质)

千问视觉模型 ≠ 传统 OCR

它本质是:

"看图 + 按你要求生成结构化文本"

流程如下👇

复制代码
图片 → Base64 → 千问模型 → 文本(JSON) → 清洗 → DTO

🧩 二、请求结构(必须理解)

请求体核心结构👇

复制代码
{
  "model": "qwen-vl",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "image_url",
          "image_url": {
            "url": "data:image/jpeg;base64,xxx"
          }
        },
        {
          "type": "text",
          "text": "你的Prompt"
        }
      ]
    }
  ]
}

👉 重点:

  • 图片必须 Base64

  • Prompt 决定识别质量


🎯 三、示例:车票 OCR Prompt

复制代码
请识别图片中的火车票信息,并严格按JSON格式输出:

{
  "PassengerName": "",
  "TrainNumber": "",
  "FromStation": "",
  "ToStation": "",
  "DepartureTime": "",
  "Seat": "",
  "Price": ""
}

要求:
1. 只输出JSON
2. 不要解释
3. 金额只保留数字
4. 时间格式 yyyy-MM-dd HH:mm:ss
5. 缺失字段返回空字符串

🧱 四、核心设计(工程级)

这一套你可以直接复用到:

  • 发票

  • 水单

  • 提单

  • 合同


✅ 关键设计点

1. 通用泛型方法

cs 复制代码
Task<T> ExtractAsync<T>()

👉 一套代码支持所有 OCR 类型


2. JSON 清洗(必须)

处理:

  • 中文引号

  • 尾逗号


3. 重试机制

防:

  • 网络波动

  • 接口偶发失败


4. DTO 强类型

👉 不要返回 string,必须结构化


🛡️ 五、稳定性策略(精华)

你这套代码真正值钱的地方在这里👇

  • ✅ 重试机制(3次)

  • ✅ JSON 修复

  • ✅ 文件大小限制

  • ✅ 临时文件自动清理

  • ✅ 反序列化兜底


⚠️ 六、生产环境建议

🔐 Key 管理

不要写死,放:

  • 环境变量

  • 配置文件


⚡ 并发控制

OCR 是重资源操作:

👉 建议限流(SemaphoreSlim)


📊 日志

建议记录:

  • 原始返回

  • 清洗后 JSON


🧾 七、完整可运行代码(核心部分)

下面这份代码是精简 + 工程可用版本👇


🧩 1. OCR Helper

cs 复制代码
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

public static class OcrHelper
{
    private static readonly HttpClient _httpClient = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(60)
    };

    private const string ApiUrl = "你的API地址";
    private const string ApiKey = "你的Key";
    private const string Model = "你的模型名称";

    static OcrHelper()
    {
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
    }

    public static async Task<T> ExtractAsync<T>(string imagePath, string prompt)
    {
        var json = await ExtractRawAsync(imagePath, prompt);

        return JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });
    }

    public static async Task<string> ExtractRawAsync(string imagePath, string prompt)
    {
        byte[] bytes = await File.ReadAllBytesAsync(imagePath);

        if (bytes.Length > 5 * 1024 * 1024)
            throw new Exception("图片不能超过5MB");

        var base64 = Convert.ToBase64String(bytes);

        var requestBody = new
        {
            model = Model,
            messages = new object[]
            {
                new
                {
                    role = "user",
                    content = new object[]
                    {
                        new
                        {
                            type = "image_url",
                            image_url = new
                            {
                                url = $"data:image/jpeg;base64,{base64}"
                            }
                        },
                        new
                        {
                            type = "text",
                            text = prompt
                        }
                    }
                }
            },
            stream = false,
            extra_body = new
            {
                enable_thinking = false
            }
        };

        var content = new StringContent(
            JsonSerializer.Serialize(requestBody),
            Encoding.UTF8,
            "application/json"
        );

        var response = await SendWithRetryAsync(content);

        var message = ExtractContent(response);

        return CleanJson(message);
    }

    private static async Task<string> SendWithRetryAsync(StringContent content)
    {
        for (int i = 0; i < 3; i++)
        {
            try
            {
                var response = await _httpClient.PostAsync(ApiUrl, content);
                var text = await response.Content.ReadAsStringAsync();

                if (!response.IsSuccessStatusCode)
                    throw new Exception(text);

                return text;
            }
            catch when (i < 2)
            {
                await Task.Delay(1000 * (i + 1));
            }
        }

        throw new Exception("OCR请求失败");
    }

    private static string ExtractContent(string responseText)
    {
        using var doc = JsonDocument.Parse(responseText);

        return doc.RootElement
            .GetProperty("choices")[0]
            .GetProperty("message")
            .GetProperty("content")
            .GetString();
    }

    private static string CleanJson(string json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return "{}";

        json = json
            .Replace("```json", "")
            .Replace("```", "")
            .Replace(""", "\"")
            .Replace(""", "\"")
            .Trim();

        json = Regex.Replace(json, ",\\s*}", "}");
        json = Regex.Replace(json, ",\\s*]", "]");

        return json;
    }
}

🧾 2. DTO(车票)

cs 复制代码
public class TrainTicketDto
{
    public string PassengerName { get; set; }
    public string TrainNumber { get; set; }
    public string FromStation { get; set; }
    public string ToStation { get; set; }
    public string DepartureTime { get; set; }
    public string Seat { get; set; }
    public string Price { get; set; }
}

🎯 3. 调用示例

cs 复制代码
var prompt = @"请识别图片中的火车票信息,并按JSON输出:
{
  ""PassengerName"": """",
  ""TrainNumber"": """",
  ""FromStation"": """",
  ""ToStation"": """",
  ""DepartureTime"": """",
  ""Seat"": """",
  ""Price"": """"
}";

var result = await OcrHelper.ExtractAsync<TrainTicketDto>(
    "test.jpg",
    prompt
);

Console.WriteLine(result.PassengerName);

🧭 最后总结一句话

AI OCR 不难,难的是把"不稳定输出"变成"稳定系统能力"。

相关推荐
码途漫谈6 分钟前
UI-UX-Pro-Max开源项目介绍
人工智能·ui·ai·开源·ai编程·ux
不懒不懒31 分钟前
【保姆级教程:阿里云百炼 API Key 获取与 OpenAI 兼容调用指南】
阿里云·云计算
van久44 分钟前
Day20:AutoMapper 对象映射
.netcore
記億揺晃着的那天1 小时前
Claude Code 系统提示词里的安全底线:OWASP Top 10
安全·ai·ai编程·vibe coding·claude code
我认不到你1 小时前
拒绝token焦虑 cpa(CLI Proxy API)反代 chatgpt(Codex) 保姆级全图文教程
人工智能·ai·chatgpt
搬砖的小码农_Sky2 小时前
AI Agent:OpenClaw的算法架构
人工智能·算法·ai·架构·人机交互·agi
Orange_sparkle2 小时前
rerank模型原理
ai
豆豆2 小时前
网站建设行业进入“AI辅助”时代:效率提升但别忽略基础
ai·cms·建站系统·建站平台·自助建站·内容管理系统·网站管理系统
阿里云大数据AI技术3 小时前
重构搜索范式:阿里云 Elasticsearch 开启“Agent 原生”时代,打造企业级 AI 记忆湖
人工智能·elasticsearch·阿里云·agent·搜索
神仙别闹3 小时前
基于C# 利用工程活动图 AOE 网设计算法
算法·c#·php