61.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--提取金额
在前一篇文章中,我们已经成功地利用百度OCR技术识别了图片中的文字内容,实现了图片到文本的初步转换。然而,实际的记账场景中,用户往往更关心图片中所包含的金额信息。因此,在本篇文章中,我们将进一步完善这一功能,重点介绍如何从OCR识别的结果中准确提取出金额数据,并将其作为结构化信息返回给调用方。
一、设置提取金额 Prompt
为了提高金额提取的准确性,我们需要设置一个专门提取文字内容中总金额的Prompt,指导AI识别和提取金额信息。
1.1 什么是 Prompt
Prompt,中文常译为“提示词”或“提示语”,是在与人工智能模型(如大语言模型或OCR后处理模型)交互时,用户用来引导模型生成特定内容或执行特定任务的文本指令。通过精心设计的Prompt,可以让AI更好地理解用户的意图,从而输出更符合需求的结果。在金额提取的场景中,Prompt可以明确告诉AI需要关注文本中的金额信息,并以特定格式返回。例如,可以这样设置Prompt:“请从以下文本中提取出所有出现的金额,并以列表形式返回。”这样的指令能够帮助AI聚焦于金额相关的数据,提升提取的准确率和效率。通过不断优化和调整Prompt内容,可以进一步提升AI在实际应用中的表现。
1.2 使用 PromptPilot 优化 Prompt
在实际应用中,设计一个有效的Prompt可能需要反复试验和调整。为此,我们可以借助PromptPilot工具来优化我们的Prompt。PromptPilot是一款专门用于Prompt设计和优化的工具,它可以帮助我们测试不同的Prompt版本,并分析其效果,从而找到最适合我们需求的Prompt。
在本次金额提取的场景中,我们可以使用PromptPilot来测试不同的Prompt设计,观察它们在提取金额时的表现。通过不断调整Prompt的措辞和结构,我们可以逐步优化其效果,确保AI能够准确地识别和提取出文本中的金额信息。最终,我们将选择一个经过优化的Prompt,用于实际的金额提取任务。
我们首先进入到PromptPilot的首页,点击 帮我生成一个Prompt 按钮,进入到Prompt生成页面,如下图:
接着,我们在Prompt生成页面中,输入我们初步设计的Prompt内容,提取输入内容中的总金额和消费类型,如果带有货币符号将其转换为标准的货币简称,常见货币符号按照标准简称转换,如 $ 转换为 USD,£ 转换为 GBP,€ 转换为 EUR 等,输出格式是:{“amount”:xxx,“category”:xxx,“currency”:xxx},然后选择类型为文本理解,之后点击小飞机按钮,提交生成请求,如下图:
在等待几秒钟后,PromptPilot会展示生成出来的Prompt结果,我们可以看到生成的Prompt内容与我们输入的意思基本一致,说明我们的初步设计已经比较符合要求。接着,我们点击页面右下角的验证Prompt 按钮进行验证Prompt的效果。由于我们的专栏并非专门的AI技术专栏,因此我们这里就不再赘述如何验证Prompt的效果,以及如何调优Prompt,感兴趣的读者可以参考相关的文档进行学习和实践。经过一番调优后,我们最终确定了一个较为满意的Prompt,用于后续的金额提取任务。这是我们最终确定的Prompt内容:你的任务是从输入内容中提取总金额和消费类型,并将带有货币符号的金额转换为标准的货币简称。在处理过程中,需要将常见货币符号按照标准简称进行转换,消费类型必须是中文,例如 $ 转换为 USD,£ 转换为 GBP,€ 转换为 EUR 等。请按照以下格式输出结果:{“amount”:xxx,“category”:xxx,“currency”:xxx},我们将这段Prompt内容保存到Nacos配置中心的SP.ResourceService
配置中,如下所示:
"Prompts":{"OCRAmount":"你的任务是从输入内容中提取总金额和消费类型,并将带有货币符号的金额转换为标准的货币简称。在处理过程中,需要将常见货币符号按照标准简称进行转换,消费类型必须是中文,例如 $ 转换为 USD,£ 转换为 GBP,€ 转换为 EUR 等。\r\n请按照以下格式输出结果:\r\n{\"amount\":xxx,\"category\":xxx,\"currency\":xxx}"
},
二、实现提取金额功能
有了优化后的Prompt,我们接下来就可以实现提取金额的功能了。我们将创建一个新的API接口/api/assistant/extract-amount-and-category
,用于接收OCR识别出来的文本内容,并返回提取出来的金额信息。
2.1 Service 实现
首先我们在SP.ResourceService
项目中,创建一个新的服务接口IAssistantService
,在它里面定义提取金额的方法ExtractAmountAndCategoryAsync
,代码如下所示:
using SP.ResourceService.Models.Response;namespace SP.ResourceService.Service;/// <summary>
/// AI助手服务
/// </summary>
public interface IAssistantService
{/// <summary>/// 提取文字中的金额和消费类型/// </summary>/// <param name="text">文字内容</param>/// <returns>金额和消费类型</returns>Task<AmountAndCategoryExtractionResponse> ExtractAmountAndCategoryAsync(string text);
}
在接口定义完成后,我们需要实现具体的业务逻辑。在 SP.ResourceService.Service.Impl
中新建 DeepSeekAssistantServiceImpl
类,实现 IAssistantService
接口。在该实现类中,ExtractAmountAndCategoryAsync
方法会接收OCR识别得到的文本内容,随后调用 DeepSeek 模型进行处理,提取出文本中的总金额、消费类型以及货币信息。这一步可以通过调用 DeepSeek 服务的API接口来完成,将文本和Prompt一同发送给AI服务,获取结构化的提取结果。收到AI返回的数据后,我们需要对其进行解析,然后将这些数据封装到 AmountAndCategoryExtractionResponse
响应模型中返回。这样,整个金额提取的服务端逻辑就完成了。代码如下:
using System.Text.Json;
using Microsoft.Extensions.Options;
using RestSharp;
using SP.ResourceService.Models.Config;
using SP.ResourceService.Models.Enumeration;
using SP.ResourceService.Models.AI.DeepSeek;
using SP.ResourceService.Models.Response;namespace SP.ResourceService.Service.Impl;/// <summary>
/// DeepSeek助手服务实现
/// </summary>
public class DeepSeekAssistantServiceImpl : IAssistantService
{/// <summary>/// 提示词配置选项/// </summary>private readonly PromptsOptions _promptsOptions;/// <summary>/// DeepSeek 配置选项/// </summary>private readonly DeepSeekOptions _deepSeekOptions;private readonly ILogger<DeepSeekAssistantServiceImpl> _logger;/// <summary>/// 构造函数/// </summary>/// <param name="promptsOptions"></param>/// <param name="deepSeekOptions"></param>/// <param name="logger"></param>public DeepSeekAssistantServiceImpl(IOptions<PromptsOptions> promptsOptions,IOptions<DeepSeekOptions> deepSeekOptions, ILogger<DeepSeekAssistantServiceImpl> logger){ValidatePromptsConfiguration(promptsOptions.Value);ValidateDeepSeekConfiguration(deepSeekOptions.Value);_promptsOptions = promptsOptions.Value;_deepSeekOptions = deepSeekOptions.Value;_logger = logger;}/// <summary>/// 提取文字中的金额和消费类型/// </summary>/// <param name="text">文字内容</param>/// <returns>金额和消费类型</returns>public async Task<AmountAndCategoryExtractionResponse> ExtractAmountAndCategoryAsync(string text){string url = _deepSeekOptions.BaseUrl + _deepSeekOptions.Chat;string apiKey = _deepSeekOptions.APIKey;var options = new RestClientOptions(url){MaxTimeout = -1,};var client = new RestClient(options);var request = new RestRequest(url, Method.Post);request.AddHeader("Content-Type", "application/json");request.AddHeader("Accept", "application/json");request.AddHeader("Authorization", "Bearer " + apiKey);// 构造请求体RequestData requestData = new RequestData();// 新建角色Message systemMessage = new Message();systemMessage.Content = _promptsOptions.OCRAmount;systemMessage.Role = AIRole.System;List<Message> messages = new List<Message>();Message userMessage = new Message();userMessage.Content = text;userMessage.Role = AIRole.User;messages.Add(systemMessage);messages.Add(userMessage);requestData.Messages = messages;requestData.Temperature = 0.7d;string body = JsonSerializer.Serialize(requestData);request.AddStringBody(body, DataFormat.Json);RestResponse response = await client.ExecuteAsync(request);DeepSeekChatResponse deepSeekChatResponse = JsonSerializer.Deserialize<DeepSeekChatResponse>(response.Content);List<Choice> choices = deepSeekChatResponse.Choices;if (choices != null && choices.Count > 0){_logger.LogInformation(response.Content);string content = choices[0].Message.Content;AmountAndCategoryExtractionResponse result = JsonSerializer.Deserialize<AmountAndCategoryExtractionResponse>(content);return result;}else{_logger.LogError("DeepSeek未返回有效的回答,"+response.Content);return new AmountAndCategoryExtractionResponse();}}/// <summary>/// 校验Prompts参数/// </summary>/// <param name="promptsOptions"></param>private void ValidatePromptsConfiguration(PromptsOptions promptsOptions){if (string.IsNullOrWhiteSpace(promptsOptions.OCRAmount)){throw new ArgumentException("OCR金额提示词不能为空");}}/// <summary>/// 校验DeepSeek参数/// </summary>/// <param name="deepSeekOptions"></param>/// <exception cref="NotImplementedException"></exception>private void ValidateDeepSeekConfiguration(DeepSeekOptions deepSeekOptions){if (string.IsNullOrWhiteSpace(deepSeekOptions.APIKey)){throw new ArgumentException("DeepSeek API Key不能为空");}if (string.IsNullOrWhiteSpace(deepSeekOptions.BaseUrl)){throw new ArgumentException("DeepSeek BaseUrl不能为空");}if (string.IsNullOrWhiteSpace(deepSeekOptions.Chat)){throw new ArgumentException("DeepSeek Chat地址不能为空");}}
}
DeepSeekAssistantServiceImpl
类是一个专门用于与DeepSeek AI服务进行交互的实现类,它承担着从用户输入的文本中智能提取金额和分类信息的核心功能。在构造函数里接收多个依赖项,包括HTTP客户端工厂、日志记录器以及两个配置选项对象。HTTP客户端工厂用于创建与DeepSeek API通信的HTTP客户端,确保网络请求的可靠性和性能。日志记录器则负责记录整个处理过程中的关键信息和错误,便于后续的问题排查和系统监控。两个配置选项对象分别是PromptsOptions
和DeepSeekOptions
,前者包含了与AI交互时使用的提示词模板,后者则包含了DeepSeek服务的连接配置信息,如API密钥、基础URL等。在类的初始化过程中,构造函数会立即调用ValidatePromptsConfiguration
和ValidateDeepSeekConfiguration
这两个验证方法来验证配置的完整性和正确性。
核心的业务逻辑集中在ExtractAmountAndCategoryAsync
方法中,这个方法首先会验证输入参数的有效性,然后构建发送给DeepSeek API的请求体,这个请求体采用了标准的OpenAI兼容格式,包含模型名称、消息数组和其他参数。在消息构建过程中,系统会将预配置的提示词模板与用户输入的文本进行组合,形成完整的AI指令。对于HTTP请求的发送过程,方法会创建一个HTTP客户端实例,设置必要的请求头信息,包括授权令牌和内容类型,然后通过POST方法发送到DeepSeek。整个网络通信过程都被包装在异常处理机制中,确保网络错误或服务异常不会导致整个应用程序崩溃。在收到响应时,首先会检查HTTP响应的状态码,确保请求成功执行。然后对响应内容进行JSON反序列化,提取出AI生成的回答。由于AI服务的返回结果可能存在不确定性,代码会验证返回的选择列表是否包含有效内容。如果AI成功返回了结果,系统会进一步解析其中的结构化数据,将其转换为应用程序可以使用的AmountAndCategoryExtractionResponse
对象。错误处理和日志记录贯穿于整个处理流程,当AI服务没有返回有效回答时,会记录详细的错误信息,包括原始的响应内容,并且系统会返回一个空的响应对象,而不是抛出异常,确保调用方不会因为AI服务的问题而影响整个业务流程。
私有配置验证方法ValidatePromptsConfiguration
方法验证了OCR金额提示词配置的完整性,ValidateDeepSeekConfiguration
方法则验证DeepSeek服务连接所需的所有关键参数,包括API密钥、基础URL和聊天接口地址。
2.2 Controller 实现
在 Controller 层的实现中,我们只需注入前面实现的 IAssistantService
,然后在对应的 API 方法中调用 ExtractAmountAndCategoryAsync
方法即可。具体来说,可以在 AssistantController
中新增一个 POST 接口 /api/assistant/extract-amount-and-category
,该接口接收 OCR 识别后的文本内容作为请求体,通过调用服务层的方法完成金额和消费类型的提取,并将结构化的结果返回给前端。代码如下:
using Microsoft.AspNetCore.Mvc;
using SP.ResourceService.Service;namespace SP.ResourceService.Controllers
{/// <summary>/// AI助手控制器/// </summary>[Route("api/assistant")][ApiController]public class AssistantController : ControllerBase{/// <summary>/// AI助手服务/// </summary>private readonly IAssistantService _assistantService;/// <summary>/// 构造函数/// </summary>/// <param name="assistantService"></param>public AssistantController(IAssistantService assistantService){_assistantService = assistantService;}/// <summary>/// 提取文字中的金额和消费类型/// </summary>/// <param name="text">文字内容</param>/// <returns>金额和消费类型</returns>[HttpPost("extract-amount-and-category")]public async Task<IActionResult> ExtractAmountAndCategory([FromBody] string text){var result = await _assistantService.ExtractAmountAndCategoryAsync(text);return Ok(result);}}
}
在上述代码中,我们定义了一个新的API控制器AssistantController
,它包含一个POST方法ExtractAmountAndCategory
,该方法接收OCR识别后的文本内容作为输入参数。通过调用注入的IAssistantService
服务的ExtractAmountAndCategoryAsync
方法,我们能够将文本内容传递给服务层进行处理,并获取提取出来的金额和消费类型。最后,使用Ok(result)
将结果以HTTP 200状态码返回给前端调用方。
三、总结
至此,我们已经完成了从OCR识别结果中提取金额和消费类型的功能实现。通过调用新的API接口/api/assistant/extract-amount-and-category
,我们可以将OCR识别得到的文本内容发送给后端服务,后端会利用DeepSeek模型进行处理,并返回结构化的金额和消费类型信息。