经过前面三篇文章的铺垫,我们先后完成了硅基流动平台的接入配置、大模型接入方式的技术选型,以及 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 批量写入预算表。这种"先预览、后确认"的两阶段提交模式,既保证了用户体验的流畅性,也为后续可能的扩展(比如在确认前支持用户手动微调某个分类的金额)预留了灵活的改造空间。
四、小结
智能生成预算功能的实现,充分体现了我们在前面几篇文章中构建的智能化基础设施的价值。通过大模型的强大分析能力,我们成功地将用户过去的消费行为转化为对未来的理性规划,帮助用户跨过了制定预算时的认知门槛,让"管住钱"不再是一个抽象的目标,而是一个具体可行的行动方案。在后续的文章中,我们还将继续挖掘大模型在个人财务管理中的更多应用场景,持续提升孢子记账的智能化水平。