面向 Delphi 和 .NET 的 AI 文档提取

· 组件
面向 Delphi 和 .NET 的 AI 文档提取

大多数企业软件仍然依赖某个人去读一份 PDF,然后把上面的数字手动输入到表单里。供应商发票、收银小票、前台扫描的身份证。如今的大语言模型已经能很好地读懂这些文档,但如果你问模型要“发票数据”,得到的往往是一段说明性文字,或者一个格式随它心意的 JSON,而且完全没有办法知道它对哪些数值没有把握。

sgcWebSockets 2026.10 为 Delphi、C++Builder 和 .NET 新增了 TsgcAIDocumentExtractor。你给它一个 PDF 或一张图像,它会通过 TsgcAIChat 询问你已经在用的模型,然后返回按 Schema 验证过的 JSON,每个字段都带有置信度。该组件不包含在当前的下载版本中,它将随 2026.10 版本一起发布。

TsgcAIDocumentExtractor 使用 Anthropic Claude 读取一张真实的发票 PDF,并返回带有每个字段置信度的经过验证的 JSON。YouTube 上也有。

输入 PDF,输出 JSON

在窗体上放置一个 TsgcAIChat 和一个 TsgcAIDocumentExtractor,把提取器指向这个聊天组件,选择一个预设,然后调用 ExtractFile。OpenAI、Anthropic 和 Gemini 可以直接读取 PDF 文件。DeepSeek、Grok、Mistral 和 Ollama 只接受图像,因此对于这些提供商,你需要把页面以 PNG 或 JPEG 格式发送。

uses
  sgcAI, sgcAI_Chat, sgcAI_DocumentExtractor;

procedure TForm1.btnExtractClick(Sender: TObject);
begin
  oChat := TsgcAIChat.Create(Self);
  oChat.Provider := aicpAnthropic;
  oChat.ChatOptions.ApiKey := 'your-api-key';

  oExtractor := TsgcAIDocumentExtractor.Create(Self);
  oExtractor.AIChat := oChat;
  oExtractor.Preset := dpInvoice;

  Memo1.Lines.Text := oExtractor.ExtractFile('invoice.pdf');
end;

这就是视频中展示的流程,并且已经针对 Anthropic Claude 在一张真实的发票 PDF 上进行了实机验证。ExtractFiles 会在一次请求中发送同一文档的多个页面,而 ExtractAttachments 接受你已经以 base64 形式保存在内存中的文档。

提取器的工作是通过 TsgcAIChat 完成的,而 TsgcAIChat 本身也获得了同样的能力,可用于你自己的提示词。ChatWithDocument 会随消息一起发送一个 PDF 或图像,ChatWithAttachments 一次发送多个,而 ChatJSONWithAttachments 会针对它们请求一个 JSON 格式的回答。新增的 TLSOptions 属性用于设置每个提供商所使用的 OpenSSL 版本、IOHandler 和证书。

Memo1.Lines.Text := oChat.ChatWithDocument('Summarize this contract', 'contract.pdf');

oFiles := TsgcAIChatAttachments.Create;
try
  oFiles.AddFile('page1.png');
  oFiles.AddFile('page2.png');
  Memo1.Lines.Text := oChat.ChatJSONWithAttachments(
    'List every line item as JSON', oFiles);
finally
  oFiles.Free;
end;

预设与你自己的 Schema

三个预设涵盖了常见的文档类型:dpInvoice、dpReceipt 和 dpIDCard。每一个都自带 JSON Schema 和提取指令,因此不需要再配置任何东西。对于其他任何文档,把 Preset 设为 dpCustom,然后自己编写 Schema。

oExtractor.Preset := dpCustom;
oExtractor.SchemaName := 'purchase_order';
oExtractor.Schema.Text :=
  '{"type": "object", "additionalProperties": false,' +
  ' "required": ["order_number", "supplier", "total"],' +
  ' "properties": {' +
  '   "order_number": {"type": "string"},' +
  '   "supplier": {"type": ["string", "null"]},' +
  '   "total": {"type": "number"}}}';
oExtractor.Instructions.Text := 'Write every amount without a currency symbol.';

使用 OpenAI 时,Schema 会以严格模式发送,因此必须遵守严格模式的规则:每个对象都要声明 "additionalProperties": false,每个属性都要出现在 required 中,可选值要使用可为空的类型,例如 ["string", "null"]。内置的预设已经遵循了这些规则。如果想从某个预设出发再添加一个字段,可以用 TsgcAIDocumentExtractor.GetPresetSchema(dpInvoice) 把它复制出来。

验证与重试

模型的回答永远不会未经检查就直接进入你的代码。提取器会在本地对照 Schema 进行验证:类型、必需属性、嵌套的对象和数组,以及枚举。如果不匹配,组件会告诉模型哪里出了问题,并再请求一次。ValidationRetries 设置重试的次数,默认值为 1。

如果重试之后数据仍然无效,提取器会抛出一个异常。把 RaiseOnInvalid 设为 False,可以照样拿到数据,并从 LastResult.ValidationErrors 中读取问题,每一行都是一条 path: message。

oExtractor.ValidationRetries := 1;
oExtractor.RaiseOnInvalid := False;

Memo1.Lines.Text := oExtractor.ExtractFile('invoice.pdf');
if not oExtractor.LastResult.Valid then
  Memo1.Lines.AddStrings(oExtractor.LastResult.ValidationErrors);

置信度与人工复核

有效的 JSON 不等于正确的 JSON。一个模糊的总额或者一个被涂改过的日期,仍然可能生成一个格式良好的数字。所以模型还会报告它对每个提取字段的把握程度,提取器把这些值交给你。LastResult.Confidence 是其中最低的一个,LastResult.FieldConfidence('total') 返回某一条路径的值,而 OnLowConfidence 会针对每一个低于 MinConfidence 的字段触发一次。

oExtractor.MinConfidence := 0.8;
oExtractor.OnLowConfidence := OnLowConfidenceEvent;
Memo1.Lines.Text := oExtractor.ExtractFile('invoice.pdf');

if oExtractor.LastResult.FieldConfidence('total') < 0.8 then
  ShowMessage('Check the total by hand');

procedure TForm1.OnLowConfidenceEvent(Sender: TObject; const aField: string;
  aConfidence: Double);
begin
  lstReview.Items.Add(Format('%s: %.2f', [aField, aConfidence]));
end;

这让复核队列变成了一个人能够真正处理完的东西:他们只需要检查模型没有把握的那两个字段,而不是整张发票。

本地 OCR,数据不离开本机

有些文档完全不能发送到任何云服务。把一个 OCR 组件关联到提取器,并挑选适合你的引擎。TsgcAIOCRTesseract 运行安装在本机上的 Tesseract OCR 引擎。TsgcAIOCRWindows 使用内置于 Windows 10 及更高版本中的文字识别功能,因此无需安装任何东西。把 OCRMode 设为 domAlways 后,图像永远不会发送给模型。OCR 引擎在本地读取文字,再由 Ollama 这样的文本模型完成提取,因此整条流程都在你自己的硬件上运行。

oOCR := TsgcAIOCRTesseract.Create(Self);
oOCR.Language := 'eng';

oChat.Provider := aicpOllama;
oChat.ChatOptions.Model := 'llama3.1';

oExtractor.OCR := oOCR;
oExtractor.OCRMode := domAlways;
oExtractor.Preset := dpReceipt;
Memo1.Lines.Text := oExtractor.ExtractFile('receipt.png');

Windows 引擎接受 BCP-47 语言标记,例如 en-US,也可以把 Language 留空以使用用户配置文件中的语言。

oWinOCR := TsgcAIOCRWindows.Create(Self);
oWinOCR.Language := 'en-US';

oExtractor.OCR := oWinOCR;
oExtractor.OCRMode := domFallback;
Memo1.Lines.Text := oExtractor.ExtractFile('invoice.png');

第二个示例中使用的 domFallback 是另一个选项。提取器会先尝试视觉请求,只有在该请求失败时才使用识别出的文字,并触发 OnOCRFallback,让你知道发生了这种情况。LastResult.UsedOCR 和 LastResult.OCRText 会在事后告诉你数据是由哪条路径产生的。OCR 步骤读取的是图像,因此扫描版 PDF 要以页面图像的形式输入。

.NET 版的 C#

sgcWebSockets 的 .NET 版本拥有同名的相同组件,位于 esegece.sgcWebSockets 命名空间中。预设、置信度值和事件的工作方式完全相同,两种 OCR 引擎 TsgcAIOCRTesseract 和 TsgcAIOCRWindows 也同样都有。

using esegece.sgcWebSockets;

var chat = new TsgcAIChat();
chat.Provider = TsgcAIChatProvider.aicpAnthropic;
chat.ChatOptions.ApiKey = "your-api-key";

var extractor = new TsgcAIDocumentExtractor();
extractor.AIChat = chat;
extractor.Preset = TsgcAIDocumentPreset.dpInvoice;
extractor.MinConfidence = 0.8;
extractor.OnLowConfidence += (sender, field, confidence) =>
    Console.WriteLine($"review {field}: {confidence:0.00}");

string json = extractor.ExtractFile("invoice.pdf");
Console.WriteLine(json);
Console.WriteLine($"valid: {extractor.LastResult.Valid}, confidence: {extractor.LastResult.Confidence:0.00}");

可用性

TsgcAIDocumentExtractor、TsgcAIOCRTesseract、TsgcAIOCRWindows 以及 TsgcAIChat 新增的方法将随 sgcWebSockets 2026.10 一起发布,面向 Delphi、C++Builder 和 .NET。它们不在你今天可以下载的版本中。2026.10 发布后,会出现在下载页面上。

包含每个属性和事件、以及一份分步发票指南的完整参考文档,位于 TsgcAIDocumentExtractor 帮助中。关于 AI 组件所做的其他一切,请参阅 sgcAI 产品页面和 sgcWebSockets AI 页面。

延伸阅读

有疑问、反馈或需要迁移方面的帮助?联系我们。回复你的会是真正编写这些代码的人。