先说个背景。我一直在做一款叫“孢子记账”的个人财务工具,整个系列写到今天,前面已经覆盖了从 .NET 10 的项目搭建、数据层设计、账户与账单的核心模块,再到多端同步和本地优先的架构。到了“产品智能化”这个阶段,我开始认真思考一个问题:记账软件到底该不该上 AI,以及上了 AI 之后,它究竟是锦上添花的噱头,还是真的能替用户省下每天三分钟的记账时间?
这篇前言是整个“智能化”系列的开篇。我不打算上来就贴模型、堆代码,而是想把这一阶段的规划思路、技术选型的取舍、以及为什么在 .NET 10 的体系下我会选择某条具体落地方案,完整地讲清楚。适合正在做 .NET 应用、想在现有产品里引入 AI 能力,但又不确定该从哪一步开始的人参考。全文不会依赖某个特定平台或服务,重点讲的是一套可移植的工程方法。
1. 产品智能化,到底要解决记账软件的什么痛点
1.1 记账用户真正的问题不是“不愿意记”,而是“记起来太累”
我调研过一批孢子记账的真实用户,也看过不少同类产品的社区反馈。结论很有意思:大多数人下载记账 App 的动机都是积极的,想搞清楚钱花到哪去了,想控制预算,想实现某个存钱目标。但三个月后还在坚持手动记账的人,可能连三成不到。
流失的原因高度集中:每次消费后要打开 App、选分类、填金额、选账户、写备注,一套流程下来快的也要十几秒,慢的超过半分钟。一天下来消费五六笔,就是几分钟的纯操作负担。而传统记账软件的分类设计又特别“反人性”,餐饮、交通、购物、娱乐、医疗……一级分类下面还有二级分类,用户记一笔外卖要在“餐饮-外卖”里翻找半天。
这里就暴露了一个核心矛盾:记账软件原本是为了帮用户理清财务,但录入成本高到一定程度后,反而成了坚持不下去的原因。产品智能化的首要目标,不是做一个看起来很酷的 AI 聊天框,而是把用户从繁琐的录入操作里解放出来。
1.2 “智能”在记账场景下的三个具体落点
想清楚目标之后,我把“智能化”拆成了三个可验证、可量化的能力,而不是一个笼统的概念。
第一个是智能分类。用户只输入“楼下老王超市 32.5”,系统自动识别这是一笔便利店消费,归入“食品酒水-日常零食”。第二个是智能摘要和备注补全。用户可以不写备注,系统根据商户名和金额自动生成一条像样的记录说明。第三个是周期性记账和消费预警。系统发现用户每月 15 号都有一笔固定房租支出,主动在下个月 15 号前提醒,或者在某类目消费明显超出历史均值时给出提示。
这三个能力有一个共同的特点:都可以用传统程序实现一个“能用”的版本,但只有引入模型能力后才能做到“好用”。比如智能分类这件事,如果只用规则匹配“超市”、“便利店”这些关键词,用户写“永辉”就识别不出来了,但基于语义的模型可以理解这是一个零售商户。所以我定义的第一阶段目标很朴素:提升录入效率,降低用户坚持记账的阻力。
1.3 设定一条“渐进式智能化”路线图
我不打算一上来就上大模型。原因很现实:成本不可控、响应延迟不可控、隐私风险也不可控。我采用的是一条渐进式路线,分三步走。
第一步是规则和统计。用正则、关键词库、以及用户历史分类数据做兜底,这套方案能覆盖大约 60% 的常见账单,而且响应速度是毫秒级的,适合纯离线场景。
第二步是轻量级模型。在端侧或本地服务部署一个小规模的文本分类模型,针对中文记账场景做微调,用来覆盖规则无法命中但语义明确的账单。
第三步是云端大模型辅助。只有在前两层都无法给出高置信度结果时,才把脱敏后的文本送到云端通用模型做一次兜底推理,并对结果做二次校验。
这套路线图本质上是在准确性、成本、延迟和隐私之间做一个可调节的权衡。规则层解决“高频简单”的问题,本地模型解决“语义模糊”的问题,云端兜底解决“长尾复杂”的问题。每一层都有回退机制,哪怕云端服务不可用,整个记账流程也不会被阻断。
2. .NET 10 在这个项目里的角色,以及智能化模块的技术选型
2.1 为什么这个阶段还坚持用 .NET 10 而不是单独起一个 Python 服务
不少朋友听说我要在 .NET 项目里做智能化,第一反应都是:你是不是应该用 Python 写模型服务,然后让 .NET 去调用?
这个建议有道理,但放在孢子记账这个项目里不成立。我的核心诉求是降低部署运维复杂度。如果引入一个 Python 服务,意味着新增一套运行时、一套依赖管理、一套进程守护和监控方案,而且前后端类型模型还得维护两套定义,schema 对齐本身就是持续的负担。
.NET 10 的定位已经不是一个“只能写业务后端”的框架了。原生 AOT 编译大幅降低了容器镜像体积和冷启动时间,新的性能改进让高并发场景更稳,而微软在 ML.NET 上的持续投入以及整个 .NET 生态对 HTTP/gRPC 调用的深度支持,都让我有信心在 .NET 10 里直接承接智能化模块的编排层,甚至部分推理逻辑。
我最终采用的结构是:模型推理交给独立的推理引擎(可以是本地加载 ONNX 模型,也可以是远端服务),.NET 10 负责预处理、特征工程、调用编排、结果后处理和缓存策略。这样既保留了 .NET 的工程优势,又没把 AI 能力锁死在某一家服务上。
2.2 本地分类模型与云端大模型的取舍边界
在技术选型上,我最纠结的问题就是用哪一类模型来承载“智能分类”这个核心能力。
纯云端大模型的优势是语义理解能力极强,分类准确率高,甚至能理解一些很口语化的文本,比如“给女朋友买了束花”这种句子。但它的问题也很明显:一是按调用量计费,如果用户每天记账 20 笔,一年下来成本非常可观;二是每次请求都要走网络,弱网环境下体验很差;三是账单文本涉及消费隐私,把用户每一笔支出都发到云端,不管怎么说都有心理门槛。
我最终决定把“本地优先、云端兜底”作为设计原则。本地使用一个轻量级的文本分类模型,通过 ONNX Runtime 加载,支持在服务端运行。这个模型不需要很大,参数量控制在几千万到一亿这个级别,重点是词典要覆盖中文口语化消费表达,比如“路边摊”、“打车”、“外卖”这类词。
云端兜底则是在本地模型的置信度低于某个阈值时才触发。例如本地模型觉得这笔账有 45% 的概率是“餐饮”,35% 的概率是“购物”,这种情况宁可多花一次调用成本,也要交给更强的模型去判断。通过设置置信度阈值,可以把云端调用量控制在总请求量的 10% 到 15% 左右,成本和体验基本能达到一个平衡。
2.3 为什么把核心逻辑抽象成“分类器接口”而不是直接耦合具体实现
工程上最容易踩的坑,就是在一开始就和一个具体技术绑定。哪怕你心里已经认定要用某一种方案,写代码的时候也一定要先定义边界。
我定义了一个叫做IBillClassifier的接口,所有智能化分类的逻辑都收敛到这个接口后面。接口输入是原始记账文本和上下文信息(比如金额、时间、当前分类),输出是一组候选分类和置信度。然后我分别实现三个版本:规则分类器、本地模型分类器、云端模型分类器,并由一个路由层决定实际调用哪一版。
这个抽象带来的直接好处是,我可以随时调整实现而不影响上层业务。今天发现规则分类器在“宠物支出”上表现很好,那就让路由层优先走规则;明天云端模型突然降价了,我可以提高云端兜底的触发频率,改一个配置就行。上层的账单存储、统计报表代码完全不用动。
3. 超级前置准备:从数据清洗到评估集构建
3.1 账本文本的数据清洗,决定智能化效果的上限
做模型的人都听过一句话:垃圾进,垃圾出。在记账场景里,这句话要再加一个前提:即使是传统的规则分类,数据清洗同样决定了效果上限。
当我把用户提交过的真实记账文本拿出来看,才发现问题有多严重。大量文本里混着表情符号、错别字、拼音缩写、英文大小写不一致的情况。比如“kfc”和“KFC”是同一个商户,但作为文本特征它俩完全不同;再比如“奈雪的茶”,用户可能写成“奈雪の茶”或“奈雪”,模型如果没见过这些变体,就很难把它们的语义统一起来。
我的清洗管线分成五层:去空白和不可见字符、统一英文字母大小写、全角转半角、表情符号移除或替换、以及一个针对中文购物习惯的同义词归一化表。这个归一化表相当有用,里面放着“可乐”、“肥宅快乐水”这类映射关系。清洗完成后,每条账单文本都会变成一个标准化字符串,所有下游模型和规则面对的都是同一个输入格式。
3.2 六类基础特征,让本地小模型也能跑出不错的效果
光把文本清洗干净还不够。一个几千万参数的小模型,指望它像大模型那样“看一眼就懂”是不现实的。所以我额外构造了几类特征,帮助模型降低理解难度。
第一类是基于商户关键词的字符特征,比如文本里是否包含“超市”、“餐厅”、“滴滴”这些高频商户词。第二类是金额区间特征,一顿午饭通常在 20 到 50 元,一笔超市采购可能在 50 到 300 元,金额对分类是有区分度的。第三类是时间特征,凌晨 2 点的消费大概率是夜宵或者娱乐,而不是工作日的通勤支出。第四类是账户类型,同一个商户,从信用卡刷和从储值卡扣,代表的消费场景可能有差异。第五类是历史分类先验,用户过去把“星巴克”归到“餐饮-咖啡”的概率很高,这个先验信息很宝贵。第六类是文本长度和结构特征,有些文本是纯商户名,有些带了数量词,结构不同也值得编码。
这些特征被拼成一个向量,和文本语义特征一起送入模型。实测下来,单纯靠文本分词做分类,准确率在 80% 左右;加上这些工程特征之后,能提升到 88% 左右。对小模型来说,这几个百分点的提升非常可观。
3.3 构建个人专属评估集:不能只看准确率
智能分类上线之前,我特别强调要做评估集。没有评估集,你就说不清楚改动到底变好了还是变坏了,每天都像在摸黑调参。
我的做法是从生产环境抽取一段时间的真实账单数据,剔除含个人敏感信息的字段后,人工打上标准分类标签。然后把它们切成三份:训练集、验证集和测试集。训练集用于模型微调,验证集用于调参,测试集用于最终验收。这里有个关键点:测试集必须和训练集完全隔离。很多项目翻车都是因为无意间把同一个商户的数据同时放进了训练集和测试集,导致模型评估结果虚高,上线后立刻打回原形。
评估指标上,我除了看整体准确率,还会按类目拆开看精确率和召回率。比如“医疗健康”这个类目如果召回率只有 50%,意味着用户有一半的医疗账单被分错了,这种错误比“购物”和“娱乐”之间的混淆更让人崩溃。此外我还设了一个“拒绝率”指标,模型给不出高置信度结果时应该宁可拒绝、也不要硬猜,拒绝率控制在合理范围能有效保护用户体验。
4. 核心实现:从项目结构调整到可落地的代码骨架
4.1 在 .NET 10 解决方案里为什么新增独立的智能化服务项目
孢子记账的后端是标准的 .NET 10 Web API,按传统三层结构分成 Domain、Application、Infrastructure 和 Host 四个项目。智能化能力刚起步时,我是准备直接把代码塞进 Application 层的,后来发现不行。
问题在于依赖方向。智能分类不是一个单纯的下游业务,它有自己的生命周期:模型要预热、要缓存在内存里、要定期重新加载;云端调用要做流控和熔断;评估脚本要走独立的数据管道。这些逻辑如果混在 Application 层,会让业务代码变得很重,而且部署时耦合在一个进程里也不容易独立扩缩容。
最终我把它拆成了一个独立的Spore.AI服务项目,通过 gRPC 和主 API 通信。主 API 收到记账请求后,调用 AI 服务的分类接口,拿到候选分类后再结合用户的上下文偏好做最终决策。这样 AI 服务的版本可以独立迭代,和主业务互不干扰。如果需要扩容,单独把 AI 服务多部署几个副本就行。
4.2 分类器路由层的实现思路
路由层是智能化模块的“大脑”,负责决定一次分类请求应该走哪条路径。它的逻辑不复杂,但顺序和退出条件很重要。
第一步是查缓存。如果同一段文本在过去一天内已经被分类过,并且分类结果被用户接受或没有明确纠错,直接返回缓存结果。这一步能挡住大量重复请求。第二步是跑规则集。如果规则库中有明确的关键词命中,并且没有和其他规则冲突,返回规则结果。第三步是把文本送入本地模型,获取带置信度的候选输出。如果最高置信度超过阈值,比如 0.9,直接返回。第四步是置信度处于中间地带时,触发云端模型兜底。第五步是最后防线:如果云端超时或不可用,返回低置信度结果,并提示用户在界面上手动确认。
这个设计最核心的思想是“层与层之间要有降级依赖”。每一层都有明确的主战场,但也有义务为下一层兜底不完整的情况。从最终效果看,大约 60% 的请求命中缓存或规则,30% 由本地模型处理,只有 10% 左右真正走到云端。
4.3 模型抽象与接口设计要点
我定义了一个简化的接口供上层调用,核心逻辑用 .NET 风格表达如下:
public enum ClassifierKind { RuleEngine = 0, LocalOnnx = 1, CloudEndpoint = 2, Unknown = 99 } public sealed record BillClassifyRequest { public required string RawText { get; init; } public decimal Amount { get; init; } public DateTime OccurredAt { get; init; } public int AccountType { get; init; } public long UserId { get; init; } } public sealed record BillClassifyResult { public required string CategoryCode { get; init; } public required float Confidence { get; init; } public required ClassifierKind Source { get; init; } public string? SuggestedRemark { get; init; } }这里有个细节值得注意:BillClassifyRequest里我特意带了UserId。为什么?因为分类器的热启动数据需要学习用户个人的分类偏好。同一个“美团”消费,A 用户可能习惯记成“餐饮-外卖”,B 用户可能记成“生活服务-跑腿”,模型返回的通用分类未必符合个人习惯,所以在上层需要一个基于用户历史的修正策略。
Source字段也要保留,这是为了做监控和复盘。通过统计不同来源的比例,我可以判断当前路由配置是否合理。如果某一天云端调用比例突然升到 30%,说明本地模型效果在退化或线上数据分布变了,需要重新训练。
4.4 本地模型部署:ONNX Runtime 与 .NET 的整合
本地模型推理这一层,我用的是 ONNX Runtime 的 .NET 库。整体流程分三步:把训练好的模型导出为 ONNX 格式,在服务启动时加载到内存,运行时接受向量化文本输入并输出分类概率。
加载模型时的关键是实例管理。模型文件在内存里占的空间不小,如果每次请求都重新加载一次必然性能崩溃。我在服务里用单例方式持有推理会话对象,同时加了一个线程安全的锁来保证首次并发请求不会重复加载。预热也是必须的,我在服务启动完成后会主动构造几条测试数据跑一遍推理,确保模型文件和预处理管线的加载没有问题,而不是等第一个真实请求来了才慢吞吞地初始化。
预处理这块我特别提一下。ONNX 模型要求的输入是固定维度的数字向量,所以文本必须先经过分词、映射、填充或截断,这些逻辑在 .NET 里写起来并不复杂,但要注意字典之外的词如何处理。我的方案是统一映射为一个[UNK]标记,同时保留一个额外的特征维度记录未登录词的数量,给模型一个“这段文本有不少我不认识的词”的提示。
4.5 云端兜底服务的降级与容错
云端兜底是整条链路里最需要重视稳定性的部分。我做了一个独立的调用客户端,内置超时控制和连续失败熔断机制。
超时时间设置为 3 秒。如果一个云端请求在 3 秒内没有返回结果,宁可放弃这次兜底,也不能让用户在记账时一直转圈。熔断机制的触发条件是连续 5 次失败或超时,一旦触发,云端服务在接下来 30 秒内不再被调用,直接走本地模型或者规则的结果。这样做是考虑到云端服务不可用时,网络重试大概率也是失败,只会浪费资源。等 30 秒窗口过去后,再放一个小比例的流量去探活,成功一次就关闭熔断,恢复正常。
这里还要处理一个响应校验的问题。云端模型返回的分类结果不一定有效,比如返回了一个我们不支持的类目代码,或者置信度很低。所以在接入云端结果之前,必须用本地维护的分类体系做一次映射校验,无效结果直接丢弃。
5. 踩坑记录:智能化落地中最容易翻车的五个细节
5.1 千万别忽略中文分词导致的语义漂移
中文文本分类最容易踩的坑就是分词。很多库里把分词器的优先级放得很高,但没有意识到分词结果会直接影响模型输入。“小龙虾”如果被切成“小/龙虾”,语义就完全变了;而“海底捞”这种品牌词如果被切得七零八落,模型就更难理解了。
我的做法是在分词基础上加入一个自定义词典,把常见商户名、餐饮品牌、App 名称、消费场景词全部维护进去,让分词器优先按整词切分。维护这个词典是个持续的体力活,但每加一批新词,线上误判率都会肉眼可见地下降。
5.2 金额特征不是越大越好,要做区间归一化
有些开发者在设计特征时直接把金额原始值塞给模型。这在记账场景里是个失误。用户单笔消费可能是一杯 3 块钱的豆浆,也可能是一笔 30000 块的培训费,金额跨度极大,原始值会让模型对“贵”和“便宜”产生错误的线性理解。
我把金额映射成语义区间,比如 0 到 200 元区间映射为 1,这是小额日常消费;200 到 1000 元映射为 2,通常对应聚餐、购物、充值;1000 元以上映射为 3,一般对应大额支出或特殊场景。区间码作为特征输入后,模型更容易捕捉到“金额大小和类目之间的关联”。
5.3 用户纠错数据是金矿,但要用对方式
用户在使用孢子记账时,如果系统给出的智能分类不对,通常会在界面上手动改回正确分类。每一次纠错都是一条标注样本。但这里有个陷阱:用户的纠错行为是带个人偏见的,不能直接作为模型的硬标签。
比如某个用户习惯把所有“肯德基”的消费都记成“亲子餐饮”,这虽然和他的真实场景相符,但如果把这类数据全部灌进模型,模型会产生严重偏向。我的处理方式是把用户纠错数据分成两层:第一层用于统计该用户的个人偏好,生成一份“用户级修正字典”,只影响这个用户的分类结果;第二层是只有当一个类目的大量用户表现出相同的修正方向时,才把它汇总进全局训练集。这样既保留了个人习惯,又不污染模型的通用能力。
5.4 云端调用守住“只发必要信息”的底线
账单文本隐私问题必须从架构层面解决,而不是写一句“我们会保护用户隐私”就糊弄过去。我调用云端兜底服务时,发送的请求体里只包含清洗后的文本、金额所在区间(不是精确金额)、以及时间段的粗糙度(比如“中午”而不是具体时间戳)。
同时我在传输层对请求内容做了脱敏掩码处理,尽量不携带真实商户名的完整信息,而是把关键词替换成分类向量的紧凑表示。当然,这会略微损失云端模型的语义理解能力,但从隐私保护角度看是值得的。用户把每一笔消费记录托付给你,守住信息边界是基本底线。
5.5 不要过度依赖“准确率”这一个指标
我前期犯过一个明显错误:只看整体准确率,结果单看指标很漂亮,用户满意度却在下降。后来才意识到问题来自“高频类目掩盖低频类目”。普通用户餐饮频次最高,餐饮分类即使偶尔错一两笔,对整体准确率的影响也很小。但医疗、教育、宠物这类低频类目一旦分错,用户会立刻觉得这个 AI “不聪明”。
后续我把评估重心改成矩阵式指标:按类目逐一查看精确率和召回率,并对每一类都设置最低阈值。低于阈值的类目,宁可走人工确认流程,也不能让模型随便给个低置信度结果。上线之后,每周自动跑一次分布报告,观察各类目的准确率变化趋势,及时发现问题。
6. 系列规划:这个“智能化”主题接下来会写什么
既然这篇是前言,我大致交代一下后续文章的取材方向,方便大家判断值不值得跟进。
第一篇会聚焦在本地的轻量级文本分类模型上,从数据标注、特征工程、模型微调到导出 ONNX 的完整流程,并给出训出来的模型在孢子记账语料上的效果报告。第二篇讲云端兜底服务怎么接、如何做流控和熔断,以及线上调用量的真实成本数据。第三篇结合 .NET 10 的依赖注入和后台任务体系,讲智能定时提醒和周期性账单识别怎么实现。第四篇聊智能报表,不只是统计图表,而是让 AI 基于用户的收支结构生成一段人话版的分析,这里会涉及自然语言生成和提示词工程在 .NET 侧的落地方式。
每篇文章我都会保证一件事:能跑的代码大于概念性的废话。大家如果对某个环节特别感兴趣,也可以提前留言,我会适当调整后续内容的优先级。
最后说几句个人感受。做产品智能化最忌讳的就是“为了 AI 而 AI”。我见过太多项目,花大力气接入了大模型,最后发现用户根本不需要一个会聊天的记账机器人。孢子记账的智能化,核心只有一条主线:让用户花更少的时间完成记账这个动作,然后把省下来的精力放在真正有价值的财务规划上。沿着这条主线往下走,每一步取舍都不会跑偏。