14【.NET10 实战--孢子记账--产品智能化】--智能生成预算

经过前面三篇文章的铺垫,我们先后完成了硅基流动平台的接入配置、大模型接入方式的技术选型,以及 LLM 调用层的统一封装,孢子记账的智能化基础设施已经基本就绪。从现在开始,我们的目光将从"怎么接入 AI"转向"用 AI 做什么",将大模型能力真正嵌入到具体的业务场景中,让用户能够实实在在地感受到智能化带来的体验提升。而我们将要攻克的第一个智能化功能,就是智能生成预算

一、痛点

预算管理是孢子记账的核心功能模块之一,也是个人财务管理中承上启下的关键环节。如果说记账是对过去的记录,那么预算就是对未来的规划,它帮助用户在消费发生之前建立一道理性防线,避免冲动支出,从而真正实现"管住钱"的目标。然而在传统的记账流程中,这道防线恰恰是最容易被用户绕过的。

制定预算这件事,本质上需要用户完成一次小型的"自我财务审计"。用户需要回顾自己过去一段时间在各个分类上的支出情况,结合当月的收入变化、季节性消费波动以及计划内的大额支出等因素,综合判断每个分类该分配多少额度。对于本身就具备财务意识、对自己的消费习惯了然于胸的用户来说,这或许只是几分钟的事情。但对于占绝大多数的普通用户而言,这已经构成了一道不低的认知门槛。很多用户打开预算设置页面之后,面对一排空白输入框和一串分类名称,脑子里只有一个念头 "我该填多少?"当这个问题没有明确答案时,放弃就成了最自然的选择。

二、智能生成预算的设计思路

针对用户在制定预算时面临的认知挑战,我们的设计思路可以概括为一句话:让 AI 来帮用户算账。用户不再需要自己对着空白表单绞尽脑汁,而是由系统自动拉取历史消费数据,交给大模型进行分析和推理,最终产出一份可直接使用或微调的预算方案。整个功能的交互流程可以拆解为数据采集、数据聚合、智能推理和结果确认四个阶段。

在数据采集阶段,用户首先选择想要制定的预算类型(月度、季度、年度),这个选择决定了后续预算的时间粒度。系统收到请求之后,会从数据库中拉取用户过去十二个月的支出明细数据。之所以选择十二个月作为数据窗口,是因为一个完整的年度周期能够覆盖工资调整、节假日消费高峰、季节性支出波动等各类周期性因素,为大模型提供足够丰富的分析素材,避免因数据窗口过窄而导致的判断偏差。

接下来进入数据聚合阶段。原始的交易明细是零散的、逐条的,直接将这些数据丢给大模型不仅会超出上下文窗口的限制,也会让模型难以从中提取有意义的规律。因此我们需要在服务端对数据进行一次预处理,将过去十二个月的所有支出按照分类进行分组,统计出每个分类在每个月的支出总额,以及该分类在过去十二个月的总支出。这样一来,原本杂乱无章的交易流水就被整理成了一张结构清晰的"支出结构图谱",大模型拿到的不再是一堆数字,而是一份能够直接用于推理的财务画像。

有了这份支出结构图谱作为输入,流程进入最核心的智能推理阶段。我们将聚合后的数据连同用户选择的预算类型一起,构造为结构化的提示词发送给大模型。大模型的任务不是简单地计算平均数然后原样返回,而是需要结合数据中体现的消费趋势、周期性波动以及各分类之间的比例关系,综合判断出一个既不过于激进也不过于保守的预算方案。

最后一个阶段是结果确认。大模型生成的预算方案会以前端可视化的方式呈现给用户,用户可以逐一查看每个分类的建议额度,也可以对比系统建议与历史均值之间的差异。如果用户对方案整体满意,点击保存即可将预算写入数据库,如果方案不合适,用户也可以直接拒绝并重新生成。在整个流程中,AI 扮演的是"参谋"角色,最终的决策权始终掌握在用户手中,这种"人机协同"的交互模式既发挥了 AI 的数据分析能力,也保留了用户对自身财务的掌控感。

三、核心代码实现

在讲解了智能生成预算的设计思路之后,接下来我们来看看这个功能的核心代码实现,其他部分的代码由于不是核心逻辑代码,因此在此不再赘述。以下是核心代码:

csharp 复制代码
using SP.FinanceService.Models.Request;
using SP.FinanceService.Models.Response;
using SP.FinanceService.Models.Enumeration;
using System.Text.Json;
using SP.Common.LLM;
using SP.Common.Redis;
using SP.Common;
using SP.Common.ExceptionHandling.Exceptions;

namespace SP.FinanceService.Service.Impl;

/// <summary>
/// 预算生成服务实现类
/// </summary>
public class BudgetGenerationServerImpl : IBudgetGenerationServer
{
    /// <summary>
    /// 预算服务接口
    /// </summary>
    private readonly IBudgetServer _budgetService;

    /// <summary>
    /// 记账服务接口
    /// </summary>
    private readonly IAccountingServer _accountingServer;

    /// <summary>
    /// 记账分类服务接口
    /// </summary>
    private readonly ITransactionCategoryServer _transactionCategoryServer;

    /// <summary>
    /// 大模型服务
    /// </summary>
    private readonly IOpenAIService _openAIService;

    /// <summary>
    /// Redis缓存服务接口
    /// </summary>
    private readonly IRedisService _redisService;

    /// <summary>
    /// 上下文服务
    /// </summary>
    private readonly ContextSession _contextSession;

    /// <summary>
    // redis缓存键前缀
    // </summary>
    private const string RedisCacheKeyPrefix = "budget_generation_preview:";


    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="budgetService">预算服务接口</param>
    /// <param name="accountingServer">记账服务接口</param>
    /// <param name="transactionCategoryServer">记账分类服务接口</param>
    /// <param name="openAIService">大模型服务接口</param>
    /// <param name="redisService">Redis缓存服务接口</param>
    /// <param name="contextSession">上下文服务接口</param>
    public BudgetGenerationServerImpl(IBudgetServer budgetService, IAccountingServer accountingServer, ITransactionCategoryServer transactionCategoryServer, IOpenAIService openAIService, IRedisService redisService, ContextSession contextSession)
    {
        _budgetService = budgetService;
        _accountingServer = accountingServer;
        _transactionCategoryServer = transactionCategoryServer;
        _openAIService = openAIService;
        _redisService = redisService;
        _contextSession = contextSession;
    }

    /// <summary>
    /// AI生成预算,返回预览数据
    /// </summary>
    /// <param name="request">生成请求</param>
    /// <returns>预览数据</returns>
    public async Task<List<BudgetGenerationResponse>> GenerateBudget(BudgetGenerationRequest request)
    {
        // 1. 构建AI输入数据
        var aiInputData = BuildAiInputData();
        if (aiInputData == null)
            return new List<BudgetGenerationResponse>();

        // 2. 获取预算周期信息
        var (month, beginDate, endDate) = GetBudgetPeriodInfo(request.Period);

        // 3. 构建提示词并调用AI服务
        var prompt = BuildBudgetPrompt(aiInputData, month, beginDate, endDate);
        var budgetResponses = await _openAIService.ChatStructuredAsync<List<BudgetGenerationResponse>>(prompt);

        // 4. 缓存预览数据
        await CacheBudgetPreviewAsync(budgetResponses);

        // 5. 返回预览数据
        return budgetResponses ?? new List<BudgetGenerationResponse>();
    }


    /// <summary>
    /// 确认/取消生成预算,返回生成结果
    /// </summary>
    /// <param name="id">AI生成预算id</param>
    /// <param name="confirm">是否确认生成</param>
    /// <returns>任务</returns>
    public async System.Threading.Tasks.Task ConfirmBudget(long id, bool confirm)
    {
        // 查询缓存中的预算预览数据
        var cacheKey = $"{RedisCacheKeyPrefix}{_contextSession.UserId}";
        var cachedData = await _redisService.GetAsync<List<BudgetGenerationResponse>>(cacheKey);
        if (cachedData == null)
        {
            throw new BusinessException("未找到预算预览数据,请重新生成预算");
        }
        if (!confirm)
        {
            // 取消生成,删除缓存并返回0
            await _redisService.RemoveAsync(cacheKey);
            return;
        }
        // 确认生成,将预览数据保存为正式预算
        var budgetAdds = new List<BudgetAddRequest>();

        foreach (var item in cachedData)
        {
            budgetAdds.Add(new BudgetAddRequest
            {
                TransactionCategoryId = item.TransactionCategoryId,
                Amount = item.Amount,
                Period = (PeriodEnum)item.Period,
                Remark = item.Remark,
                StartTime = item.StartTime,
                EndTime = item.EndTime
            });
        }
        await _budgetService.Adds(budgetAdds);
    }

    /// <summary>
    /// 构建AI生成预算所需的输入数据
    /// </summary>
    /// <returns>输入数据,若无数据则返回null</returns>
    private object? BuildAiInputData()
    {
        // 查询用户最近12个月的记账数据
        var accountingResponses = _accountingServer.GetAccountingsByTimeRange(
            DateTime.Now.AddMonths(-12), DateTime.Now);
        if (accountingResponses == null)
            return null;

        // 获取支出分类数据
        var categoryResponses = _transactionCategoryServer.QueryByType(TransactionCategoryEnmu.Expenditure);
        if (categoryResponses == null)
            return null;

        var categoryIds = categoryResponses.Select(c => c.Id).ToList();

        // 过滤出支出的记账数据
        var categorySummaries = accountingResponses
            .Where(a => categoryIds.Contains(a.TransactionCategoryId));

        // 按支出分类分组,计算每个支出分类的总额
        var categoryTotalAmounts = categorySummaries
            .GroupBy(a => a.TransactionCategoryId)
            .Select(g => new
            {
                CategoryId = g.Key,
                TotalAmount = g.Sum(a => a.Amount)
            })
            .ToList();

        // 按支出分类和月份分组,计算每个月的支出分类总额
        var monthlyCategoryAmounts = categorySummaries
            .GroupBy(a => new { a.TransactionCategoryId, Month = a.RecordDate.Month })
            .Select(g => new
            {
                CategoryId = g.Key.TransactionCategoryId,
                Month = g.Key.Month,
                TotalAmount = g.Sum(a => a.Amount)
            })
            .ToList();

        // 组装为AI输入格式
        return categoryTotalAmounts.Select(c => new
        {
            CategoryId = c.CategoryId,
            CategoryName = categoryResponses.FirstOrDefault(r => r.Id == c.CategoryId)?.Name,
            TotalAmount = c.TotalAmount,
            MonthlyAmounts = monthlyCategoryAmounts
                .Where(m => m.CategoryId == c.CategoryId)
                .Select(m => new { m.Month, m.TotalAmount })
                .ToList()
        }).ToList();
    }

    /// <summary>
    /// 根据预算周期获取月份数、起始日期和结束日期
    /// </summary>
    /// <param name="period">预算周期</param>
    /// <returns>月份数、起始日期、结束日期</returns>
    private static (int month, DateTime beginDate, DateTime endDate) GetBudgetPeriodInfo(PeriodEnum period)
    {
        var beginDate = DateTime.Now;
        var endDate = period switch
        {
            PeriodEnum.Month => DateTime.Now.AddMonths(1),
            PeriodEnum.Quarter => DateTime.Now.AddMonths(3),
            PeriodEnum.Year => DateTime.Now.AddMonths(12),
            _ => DateTime.Now
        };

        var month = period switch
        {
            PeriodEnum.Month => 1,
            PeriodEnum.Quarter => 3,
            PeriodEnum.Year => 12,
            _ => 0
        };

        return (month, beginDate, endDate);
    }

    /// <summary>
    /// 构建AI生成预算的提示词
    /// </summary>
    /// <param name="aiInputData">AI输入数据</param>
    /// <param name="month">预算月数</param>
    /// <param name="beginDate">预算起始日期</param>
    /// <param name="endDate">预算结束日期</param>
    /// <returns>提示词字符串</returns>
    private static string BuildBudgetPrompt(object aiInputData, int month, DateTime beginDate, DateTime endDate)
    {
        return $"请根据以下数据生成未来{month}个月的预算,预算起止日期为{beginDate:yyyy-MM-dd}至{endDate:yyyy-MM-dd},要求按照支出分类进行预算分配,并且给出每个支出分类的预算金额:{JsonSerializer.Serialize(aiInputData)}";
    }

    /// <summary>
    /// 将生成的预算预览数据缓存到Redis,过期时间30分钟
    /// </summary>
    /// <param name="data">预算预览数据</param>
    private async System.Threading.Tasks.Task CacheBudgetPreviewAsync(List<BudgetGenerationResponse> data)
    {
        var cacheKey = $"{RedisCacheKeyPrefix}{_contextSession.UserId}";
        await _redisService.SetAsync(cacheKey, JsonSerializer.Serialize(data), 30 * 60);
    }
}

BudgetGenerationServerImpl 是整个智能生成预算功能的核心实现类,它通过依赖注入持有了六个关键服务接口。IAccountingServer 负责从数据库中拉取用户的记账明细,ITransactionCategoryServer 提供支出分类的元数据(分类 ID 与名称的映射),这两者共同构成了数据采集层的基础。IOpenAIService 是我们在上一篇文章中封装的 LLM 调用层,本次直接以 ChatStructuredAsync<T> 的方式使用,调用大模型并自动将返回的 JSON 反序列化为强类型的 List<BudgetGenerationResponse>IRedisService 承担了预览数据的临时存储职责,而 IBudgetServer 则在用户确认后将预览数据正式持久化到预算表。ContextSession 从当前请求上下文中提取用户 ID,用于构造 Redis 缓存键的隔离前缀,确保不同用户之间的预览数据互不干扰。

整个生成流程的入口是 GenerateBudget 方法,它按照五个步骤依次执行。首先调用 BuildAiInputData 构建大模型所需的输入数据,这一步会查询用户过去十二个月的支出明细,并按分类进行两次分组聚合:一次仅按分类汇总出该分类的十二个月总支出,另一次按分类加月份汇总出每个分类每个月的支出额,最终组装成一个同时包含分类名称、总支出和月度明细的结构化对象。如果用户没有任何历史记账数据,方法直接返回空列表。接下来通过 GetBudgetPeriodInfo 根据用户选择的周期类型(月度、季度、年度)计算出预算覆盖的月份数和起止日期,这里利用 C# 的 switch 表达式将枚举值映射为具体的月份偏移量,代码简洁且不易出错。然后调用 BuildBudgetPrompt 将聚合后的数据与预算周期信息拼接为一段自然语言提示词,核心指令是"根据以下数据生成未来 N 个月的预算,按照支出分类进行预算分配,并给出每个分类的预算金额"。提示词末尾附上 JSON 序列化后的数据,大模型拿到这段提示词后就可以同时理解"任务指令"和"数据上下文",从而产出结构化的预算建议。

大模型返回的 List<BudgetGenerationResponse> 并不会直接写入数据库,而是先通过 CacheBudgetPreviewAsync 存入 Redis,设置30分钟的过期时间。这样做的目的是将"生成"和"确认"两个操作解耦------用户可以在预览页面上仔细审视每个分类的建议金额,甚至反复对比不同周期下的方案差异,而不必担心每次查看都重新调用一次大模型(既浪费算力也增加延迟)。当用户做出最终决定后,前端调用 ConfirmBudget 方法,传入是否确认的布尔标记。如果用户选择取消,方法会直接清除 Redis 中的预览缓存,不留下任何数据痕迹;如果用户选择确认,则将缓存中的 BudgetGenerationResponse 列表转换为 BudgetAddRequest 列表,调用 IBudgetServer.Adds 批量写入预算表。这种"先预览、后确认"的两阶段提交模式,既保证了用户体验的流畅性,也为后续可能的扩展(比如在确认前支持用户手动微调某个分类的金额)预留了灵活的改造空间。

四、小结

智能生成预算功能的实现,充分体现了我们在前面几篇文章中构建的智能化基础设施的价值。通过大模型的强大分析能力,我们成功地将用户过去的消费行为转化为对未来的理性规划,帮助用户跨过了制定预算时的认知门槛,让"管住钱"不再是一个抽象的目标,而是一个具体可行的行动方案。在后续的文章中,我们还将继续挖掘大模型在个人财务管理中的更多应用场景,持续提升孢子记账的智能化水平。

相关推荐
大大大大晴天️12 小时前
Flink Resource Providers 深度解析:机制原理、部署模式与最佳实践
大数据·flink
Ztopcloud极拓云视角13 小时前
ChatGPT超级应用改版技术解析:Codex集成架构与多模型路由实战
人工智能·chatgpt·架构
秋919 小时前
从 Python 后端工程师转型 AI Engineer(AI 工程化)的完整补课清单(2026实战版)
开发语言·人工智能·python
啦啦啦_999920 小时前
5. 迁移学习
人工智能·机器学习·迁移学习
A.说学逗唱的Coke20 小时前
【AI·Coding】TDD × SDD × AI Coding:从“测试驱动“到“规范驱动“的智能协作实践
人工智能·驱动开发·tdd
云烟成雨TD20 小时前
Spring AI Alibaba 1.x 系列【78】沙箱(Sandbox)
java·人工智能·spring
tq108620 小时前
基于SLIP的防幻觉的指南
人工智能
听你说3220 小时前
科技护航极限征程 三诺生物助力雄关330长城越野赛
大数据·科技·健康医疗
电商API_1800790524720 小时前
bilibili关键字搜索视频列表|获取视频详情API调用示例
大数据·数据挖掘·网络爬虫·音视频
甲维斯21 小时前
Kimi版超级玛丽效果“惊人”,配额不足5厘米!
前端·人工智能