简介:这是一份面向个人开发者与财务自动化爱好者的智能记账工具源码包,基于 OpenAI 接口与 Python 实现,可将账单、发票等非结构化财务文档自动解析为结构化账目,适合具备一定 Python 基础、希望实践 AI 财务场景的读者。压缩包共 51 个文件,约 37KB,以 36 个 py 脚本为核心,辅以 yaml/yml 配置、json、ini、makefile 等工程文件,并包含 README 说明与测试用例,目录按 prompts、routers、models、utils、tests 等模块划分,结构清晰。资源围绕数据获取、预处理、信息抽取、结构化、整合验证与报告可视化六个环节展开,涵盖邮件与 PDF 解析、OpenAI 自然语言理解、Notion 集成及日志与错误处理等实现思路。目前已有 238 人学习下载,适合作为 AI 记账方向的练手项目或二次开发起点。
1. 从手动记账到对话入账:这套 Python 工具到底省掉了哪几步
每个月月底对着账单流水一行行敲进 Excel 的人,大概都动过「能不能让程序自己记」的念头。这套基于 OpenAI 接口实现的自动记账工具,核心思路就是把「记一笔」这件事从填表变成说话——你输入「午饭 28 块微信」,它返回一条结构化账目,自动落到本地文件或数据库里。整个项目是纯 Python 实现,没有前端框架依赖,跑起来就是一个命令行交互程序,适合两类人:一是想拿它当日常记账工具、又不想装一堆 App 的开发者;二是想找一个「LLM 接口调用 + 结构化输出解析」练手项目的新手。它解决的不是记账本身,而是「自然语言到结构化数据」这一段最烦人的转换。下面按「它怎么把一句话变成一条账」到「怎么改、怎么防翻车」的顺序拆开讲。
2. 自然语言转结构化账目:OpenAI 接口调用与字段解析
2.1 为什么用 Chat Completions 而不是自己写正则
手动记账最痛的点在于输入格式不统一。有人写「午饭28」,有人写「中午吃饭花了二十八块」,还有人写「打车 15.5 支付宝」。正则表达式能覆盖前两种,第三种带支付渠道的就容易漏。这套工具选择调用 OpenAI 的对话接口,把「理解意图」这件事交给模型,代码只负责把模型返回的 JSON 解析成固定字段。
常见做法是构造一个 system prompt,明确告诉模型输出格式。比如要求返回{"amount": float, "category": str, "note": str, "payment": str}四个字段,并且只返回 JSON、不要额外解释。这样做的代价是每次记账都要发一次网络请求,延迟在几百毫秒到两秒之间;好处是新增一种说法不用改代码,改 prompt 就行。
我一般会把 prompt 拆成两部分:一部分是固定的字段定义和输出约束,另一部分是动态拼接的当前日期和用户输入。日期必须由代码注入,不能让模型自己猜,否则「昨天午饭」这种相对时间会算错。
2.2 一次完整调用的代码骨架
下面这段是核心调用逻辑,省略了异常重试部分,后面单独讲。
import os import json from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) SYSTEM_PROMPT = """你是一个记账助手。用户会用自然语言描述一笔支出。 请提取以下字段并以 JSON 返回,不要输出任何其他文字: - amount: 金额,浮点数,单位元 - category: 分类,从[餐饮,交通,购物,居住,娱乐,医疗,其他]中选一个 - note: 简短备注,不超过10个字 - payment: 支付方式,从[微信,支付宝,现金,银行卡,其他]中选一个 如果信息缺失,amount 填 0,其余填"其他"。""" def parse_expense(user_input: str, today: str) -> dict: # today 由调用方传入,格式 YYYY-MM-DD,用于解析"昨天""上周五"等相对时间 resp = client.chat.completions.create( model="gpt-4o-mini", # 记账场景对模型能力要求不高,mini 足够 messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"今天是{today}。{user_input}"} ], temperature=0, # 结构化输出必须关掉随机性 response_format={"type": "json_object"} # 强制返回合法 JSON ) raw = resp.choices[0].message.content return json.loads(raw)逻辑说明:temperature=0是关键参数,记账不需要创造性,任何随机性都会导致同一句话两次解析出不同分类。response_format设为json_object后,模型不会返回带 markdown 代码块的文本,省掉一层清洗。model选 mini 版本是因为字段抽取属于简单任务,用大模型成本翻十几倍但准确率提升有限。
参数说明:today必须由外部传入而不是在函数内取datetime.now(),原因是批量补录历史账目时,相对时间要基于账目发生日而不是运行日。这个坑我在测试时踩过——补录上周的账,结果「昨天」全算成了运行日的前一天。
2.3 字段校验与本地落盘
模型返回的 JSON 不能直接信。金额可能是字符串"28",分类可能返回不在枚举里的值,支付方式可能为空。落盘前必须过一层校验。
VALID_CATEGORIES = {"餐饮", "交通", "购物", "居住", "娱乐", "医疗", "其他"} VALID_PAYMENTS = {"微信", "支付宝", "现金", "银行卡", "其他"} def validate(record: dict) -> dict: # 金额强制转 float,失败则置 0 并标记待确认 try: record["amount"] = round(float(record.get("amount", 0)), 2) except (TypeError, ValueError): record["amount"] = 0.0 record["note"] = "[金额待确认] " + record.get("note", "") # 分类和支付方式做白名单过滤 if record.get("category") not in VALID_CATEGORIES: record["category"] = "其他" if record.get("payment") not in VALID_PAYMENTS: record["payment"] = "其他" return record校验之后写入 CSV 或 SQLite。CSV 的好处是随时能用 Excel 打开核对,坏处是并发写入容易冲突;SQLite 适合长期积累后做统计查询。我一般先用 CSV 跑一周,确认解析准确率稳定后再迁到 SQLite。
3. 把工具跑起来:环境配置、交互循环与数据存储
3.1 依赖安装与密钥管理
项目依赖很轻,核心就一个 openai 包。Python 版本建议 3.9 以上,因为response_format参数在旧版 SDK 里不支持。
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install openai python-dotenv密钥不要写死在代码里。常见做法是建一个.env文件,内容为OPENAI_API_KEY=你的key,然后在入口文件顶部用load_dotenv()加载。.env必须加进.gitignore,否则推到公开仓库等于把 key 送人。我见过有人把 key 写在代码注释里然后截图发群,这种血泪经验不用多解释。
3.2 交互循环与退出机制
命令行工具的核心是一个 while 循环,读一行、解析一行、写一行。但直接input()裸循环有两个问题:一是 Ctrl+C 退出时可能丢最后一条未落盘的记录;二是连续输入多条时没有反馈,不知道哪条成功了。
from datetime import date from dotenv import load_dotenv load_dotenv() def main(): print("输入账目,回车提交;输入 q 退出。") buffer = [] while True: try: line = input("> ").strip() except (EOFError, KeyboardInterrupt): break if line.lower() == "q": break if not line: continue try: record = parse_expense(line, date.today().isoformat()) record = validate(record) record["date"] = date.today().isoformat() buffer.append(record) print(f" 已记录:{record['category']} {record['amount']}元 {record['note']}") except Exception as e: print(f" 解析失败:{e},请换一种说法") # 退出时统一落盘,避免每条都写文件 save_to_csv(buffer) print(f"本次共记录 {len(buffer)} 条。")逻辑说明:用buffer暂存而不是每条都写文件,是为了减少磁盘 IO,也方便退出时一次性处理。save_to_csv以追加模式打开文件,如果文件不存在则先写表头。异常捕获放在单条解析上而不是整个循环,这样一条失败不会导致程序退出。
参数说明:date.today().isoformat()返回2025-01-15这种格式,比时间戳可读性好,也方便后续按日期筛选。如果要做时区处理,把date.today()换成带时区的datetime.now(tz)即可,但记账场景一般不需要精确到小时。
3.3 CSV 与 SQLite 的选型对比
| 维度 | CSV | SQLite |
|---|---|---|
| 打开方式 | Excel、文本编辑器 | 需要 SQL 客户端或代码 |
| 并发写入 | 差,多进程会覆盖 | 好,支持锁 |
| 统计查询 | 需要 pandas 或手动遍历 | 直接 SQL 聚合 |
| 迁移成本 | 低,纯文本 | 中,需要建表 |
| 适合阶段 | 前两周验证解析准确率 | 长期使用、月度统计 |
我的习惯是先用 CSV 跑一段时间,确认模型对常见说法的解析准确率能到九成以上,再写一个迁移脚本把 CSV 导入 SQLite。迁移脚本不复杂,读 CSV 逐行 insert 即可,但要注意金额字段的类型转换。
4. 避坑与排查:五条实际踩过的记录
4.1 模型返回的 JSON 带 markdown 代码块
现象:json.loads报Expecting value: line 1 column 1,打印原始内容发现是```json\n{...}\n```。
原因:早期 SDK 或未设置response_format时,模型习惯把 JSON 包在代码块里。
解决:设置response_format={"type": "json_object"};如果 SDK 版本不支持,就在json.loads前加一层清洗,用正则去掉首尾的```标记。
4.2 金额被解析成字符串导致统计求和报错
现象:CSV 里金额列混着28和"28",用 pandas 求和时字符串被当 0 处理。
原因:模型有时返回数字,有时返回字符串,json.loads不会强制类型。
解决:在validate里统一float()转换,转换失败置 0 并标记。不要依赖模型保证类型一致。
4.3 相对时间解析错位
现象:补录「昨天午饭」,记录日期却是运行日的前一天,而不是账目实际发生日。
原因:prompt 里的「今天」用了datetime.now(),补录场景下这个值不对。
解决:把日期作为参数传入parse_expense,补录时传账目发生日。这个改动很小,但影响所有涉及「昨天」「上周」的输入。
4.4 网络超时导致整条记录丢失
现象:输入后卡住十几秒,然后报连接错误,这条账没记上。
原因:默认没有设超时,网络抖动时请求挂起。
解决:在OpenAI()初始化时传timeout=10,并在调用外层加一次重试。重试不要超过两次,否则用户等太久。重试仍失败就把原始输入追加到一个pending.txt,下次启动时提示补录。
4.5 分类枚举太细导致模型乱选
现象:把分类从 7 个扩到 20 个后,「其他」的比例反而上升,模型经常选一个不相关的细分类。
原因:枚举值越多,模型在边界模糊时越容易随机选。
解决:分类控制在 10 个以内,且每个分类在 prompt 里给一个简短示例。比如「餐饮:吃饭、外卖、咖啡」。示例比枚举名更能锚定模型判断。
5. 进阶技巧:用 few-shot 示例把解析准确率再提一档
跑通基础版本后,如果发现某些说法反复解析错,最有效的改进不是换更大的模型,而是在 system prompt 里加 few-shot 示例。做法是把几条「输入 → 期望 JSON」的样例拼进 prompt,让模型照着格式走。
FEW_SHOT = """ 示例1: 输入:午饭28微信 输出:{"amount":28.0,"category":"餐饮","note":"午饭","payment":"微信"} 示例2: 输入:打车15.5支付宝 输出:{"amount":15.5,"category":"交通","note":"打车","payment":"支付宝"} 示例3: 输入:买书花了六十多 输出:{"amount":60.0,"category":"购物","note":"买书","payment":"其他"} """ SYSTEM_PROMPT = SYSTEM_PROMPT + "\n" + FEW_SHOT逻辑说明:示例要覆盖三类边界——带支付方式的、金额是约数的、分类容易混淆的。每条示例控制在两行以内,太多会挤占上下文窗口,反而降低对当前输入的注意力。示例里的金额和分类必须和枚举定义一致,否则模型会学到矛盾的规则。
参数说明:few-shot 示例放在 system prompt 末尾比放在开头效果好,因为模型对靠近用户输入的内容注意力更高。如果示例超过 5 条,考虑改用微调或换用支持 function calling 的接口,把字段定义做成工具描述,让模型按 schema 输出。
验证改进效果的方法很简单:准备 30 条真实说法的测试集,改 prompt 前后各跑一遍,统计字段完全正确的比例。我一般要求金额和分类两项都对的才算通过,支付方式允许有误差。测试集要包含口语、缩写、带渠道、带约数四种类型,每种至少 5 条。
从那以后我每次改 prompt 都强制走一遍这个 30 条测试集,不再凭感觉判断「好像变准了」。这套工具的价值不在于用了什么模型,而在于把「输入容错」和「输出校验」这两层做扎实,模型换哪家都能跑。希望帮到你。
本文还有配套的精品资源,点击获取