我用 AI 做记账 App:数据库、接口和规则引擎该怎么设计

0 阅读7分钟

11_cover_ai_budget_app_case.png

一个自然语言记账 App,真正难的不是把页面搭出来,而是怎么把用户输入的一句话稳定地变成结构化数据。

比如:

  • 我这个月预算1000元
  • 今天加油300元
  • 昨天晚上吃饭花了58
  • 这个月预算用了多少

这些输入都很短,但系统背后要判断的事情不少:

  • 这是预算,还是账单,还是查询?
  • 金额是多少?
  • 是收入还是支出?
  • 分类是什么?
  • 日期是今天、昨天,还是某个具体日期?
  • 识别不确定时,要不要直接保存?

这篇就讲这个项目里最核心的三块设计:

  • 数据库模型怎么定
  • API 怎么拆
  • 规则引擎为什么这么做
flowchart LR
    A["一句话输入"] --> B["POST /api/parse"]
    B --> C["结构化识别结果"]
    C --> D{"用户确认?"}
    D -- "是" --> E["POST /api/budgets<br/>或<br/>POST /api/transactions"]
    D -- "否" --> F["返回修改输入"]
    E --> G["写入数据库"]

为什么要先把数据结构定清楚

很多 AI 编程项目会先做页面,再反推接口和数据模型。

这在简单 CRUD 场景里还能勉强跑,但在自然语言记账里风险很大。

因为如果你不先定义结构化结果是什么,后面的解析逻辑就没有统一目标。

这个项目在需求和架构阶段确定下来以后,核心数据对象很快就收敛成了 4 个:

  • users
  • categories
  • monthly_budgets
  • transactions

这几个对象已经足够支撑第一版主链路。

flowchart TD
    A["users"] --> B["categories"]
    A --> C["monthly_budgets"]
    A --> D["transactions"]
    B --> D

这个项目的核心数据模型是怎么拆的

1. 用户

第一版虽然没有登录,但我还是保留了 users 表。

原因不是“提前设计多用户”,而是预算、账单、分类这些数据天然都有归属关系。即使 MVP 阶段固定 user_id = 1,保留用户表也能让后续演进更顺。

2. 分类

分类表的职责是承接自动分类和后续统计。

第一版里我把分类设计成:

  • 系统默认分类
  • 未来可扩展为用户自定义分类

而且分类还区分了:

  • 支出分类
  • 收入分类
  • 两者都可用

这样做的好处是,自动识别和统计都能复用同一套分类体系。

3. 月预算

预算表按月存储,每个用户每月一条记录。

这个项目里,year_month 先直接用 YYYY-MM 字符串表示。

这不是最“学术”的设计,但对这个场景非常直接:

  • 前端查询简单
  • 服务端统计简单
  • 预算设置和预算读取都很清晰

4. 账单

账单表是最核心的数据对象。

它至少要承接这些信息:

  • 用户
  • 分类
  • 收支方向
  • 金额
  • 日期
  • 备注
  • 原始输入
  • 输入来源
  • 识别状态
  • 是否异常

这里有两个字段特别重要:

一个是 raw_input,一个是 parse_status

raw_input 让系统后续能回看用户到底输入了什么。

parse_status 则是为了处理“识别成功”和“需要确认”这两种状态。自然语言产品如果没有这个概念,后面很容易把错误数据直接落库。

为什么 API 要把 parse 和 save 分开

这是这个项目里一个非常关键的产品和技术决策。

很多人会本能地想把流程做成:

输入一句话 -> 后端直接识别并保存

这看起来更顺,但实际问题很大。

自然语言输入天生有歧义。

比如:

  • 金额没识别出来
  • 文本里有两个金额
  • 分类不明确
  • 日期不清楚

如果系统不让用户确认,错误账单就会直接写入数据库。

所以这个项目把 API 拆成两步:

  1. POST /api/parse
  2. POST /api/budgetsPOST /api/transactions

第一步只做识别,不落库。

第二步在用户确认后,才真正保存预算或账单。

这个拆法虽然多了一步接口,但它换来的是可控性和更好的用户信任。

第一版 API 是怎么分层的

在这个项目里,后端 API 最终收敛成几组:

  • POST /api/parse
  • POST /api/budgets
  • GET /api/budgets/current
  • GET /api/categories
  • POST /api/transactions
  • GET /api/transactions
  • GET /api/stats/overview
  • GET /api/stats/categories
  • GET /api/alerts/current

这套接口有个特点:不是完全按表 CRUD 去设计,而是按用户能感知到的业务动作去设计。

比如:

  • 用户要设置预算,就给预算接口。
  • 用户要看首页,就给聚合统计接口。
  • 用户要知道有没有超预算,就给提醒接口。

这比“把每张表都开放一套原始接口”更贴近 MVP 目标。

为什么识别第一版用规则引擎

这个项目的识别链路,第一版没有直接上大模型,而是采用了规则引擎。

整个识别流程大致是:

  1. 文本预处理
  2. 意图判断
  3. 金额提取
  4. 日期提取
  5. 收支方向判断
  6. 分类映射
  7. 结果校验
flowchart LR
    A["文本预处理"] --> B["意图识别"]
    B --> C["金额提取"]
    C --> D["日期提取"]
    D --> E["收支方向判断"]
    E --> F["分类映射"]
    F --> G["结果校验"]
    G --> H["返回识别结果"]

这样做的好处是非常实际的:

  • 高频输入稳定
  • 成本低
  • 调试容易
  • 测试容易
  • 每一步都可以单独增强

比如用户输入:

今天加油300元

规则链路就能比较确定地识别出:

  • 意图:创建账单
  • 类型:支出
  • 金额:300
  • 分类:交通
  • 日期:当天

对于 MVP 来说,这已经够用了。

为什么“先识别、再确认、再保存”是自然语言产品的底线

这个记账 App 的一个关键产品原则是:

先识别、再确认、再保存

这不是多余动作,而是自然语言输入产品最关键的保险丝。

因为第一版规则引擎再稳定,也不可能覆盖所有表达方式。

所以系统必须允许用户先看到结构化结果:

  • 类型
  • 金额
  • 分类
  • 日期
  • 备注

确认没问题以后再入库。

从工程角度看,这也让问题更容易定位:

  • 是解析错了
  • 是保存错了
  • 还是统计错了

链路一旦拆清楚,后面调试和测试都会简单很多。

查询类自然语言为什么也能在这套结构里扩展

这个项目后面又扩展了一批查询类输入,比如:

  • 这个月餐饮花了多少
  • 这个月预算用了多少
  • 最近7天交通和餐饮一共多少
  • 本周收入多少

之所以后面能继续往这边扩展,很大程度上就是因为第一版已经把识别和保存拆开了。

查询类输入本质上也是:

  • 先做意图识别
  • 再做参数提取
  • 最后调用统计模块返回结果

如果第一版把所有事情都做成“输入一句话直接落库”,后面要支持查询自然语言,结构上就会别扭很多。

一个适合直接复用的提示词模板

如果你要让 AI 帮你设计类似项目的数据和接口,可以直接问:

下面是产品的 MVP 需求和技术方案:
<粘贴需求和架构结果>

请继续输出:
1. 核心数据模型
2. 表之间的关系
3. API 清单
4. parse 和 save 的职责边界
5. 规则引擎的识别流程
6. 哪些场景需要用户确认
7. 哪些异常要阻止直接保存

要求:
- 优先满足 MVP
- 不要过度设计
- 说明每个设计背后的原因

最后

这个记账 App 的数据库、接口和解析设计,最核心的一点不是“技术上多复杂”,而是三件事始终保持一致:

  • 结构化数据是什么
  • 用户输入要经过哪些判断
  • 什么情况下才能真正保存

一旦这三件事清楚了,前端确认弹窗、统计接口、异常提示、测试用例,其实都会顺着长出来。

下一篇我会继续写:这个项目从骨架搭建到前后端联调,是怎么一步步落地的,以及 AI 参与编码时为什么一定要小步推进、边改边验证。

如果你也在做带自然语言输入的产品,正在纠结数据模型、接口边界或者识别流程怎么定,可以直接私信我聊聊。我最近在系统整理 AI 编程实战案例,也在做 AI 代码审查、单元测试补齐和 AI 编程流程咨询。