很多人第一次接入 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 不难,难的是把"不稳定输出"变成"稳定系统能力"。