基于OpenAI的自动记账工具:从自然语言到结构化账目

发布时间:2026/10/17 17:05:49

基于OpenAI的自动记账工具:从自然语言到结构化账目 简介这是一份面向个人开发者与财务自动化爱好者的智能记账工具源码包基于 OpenAI 接口与 Python 实现用于将银行流水、发票、账单等非结构化财务文档自动解析为可用的记账数据适合具备一定 Python 基础、希望实践 AI 财务场景的读者。压缩包共 51 个文件约 37KB以 36 个 py 脚本为核心辅以 yaml、yml 配置定义任务与账单流程另有 json、ini、makefile、pylintrc 等工程化文件整体结构轻量、便于二次开发。目录中可见 prompts、routers、models、utils、tests 等模块分别承担提示词管理、接口路由、数据模型、日志与错误处理、单元测试等职责并预留 notion 集成入口方便对接外部记账服务。目前已有 238 人学习下载。通过该资源可掌握从数据获取、预处理、信息抽取到结构化落库与报告可视化的完整链路理解 OpenAI 自然语言理解在财务场景中的落地方式并借鉴其工程组织与测试思路。1. 自动记账这件事难的不是记账而是「记」很多人第一次听到「基于 OpenAI 实现的自动记账工具」脑子里浮现的是又一个记账 App。但真正动手做过的人会告诉你记账类工具最劝退的环节从来不是账本本身而是「记」这个动作——手动选分类、填金额、挑账户三步下来十几秒坚持不过两周。我见过太多人下载了记账软件三天热度之后账本停在第一页。这个方案要解决的就是这个断点你只需要用自然语言说一句「昨天打车 38午饭跟同事 AA 付了 65」剩下的金额提取、分类判断、账户归属、时间解析全部交给模型和一段 Python 逻辑去完成。它适合两类人一类是想自己搭一套私人记账流水、又不想被 App 绑架的开发者另一类是手里已经有一堆微信/支付宝导出的 CSV想批量清洗成结构化账目的数据党。核心链路其实很短——文本进、结构化 JSON 出、落库、对账难的是让模型稳定地吐出你要的字段。2. 从一句话到一条账目解析链路怎么搭2.1 为什么用「函数调用」而不是让模型直接写 SQL新手最容易踩的坑是直接给模型一句「把这句话转成 INSERT 语句」。看起来省事实际上你会遇到三个问题模型偶尔会编造不存在的字段名、金额单位时有时无、日期格式每次都不一样。更麻烦的是一旦模型输出被拼进 SQL注入风险就跟着来了。常见做法是用 OpenAI 的 function calling现在叫 tool calling能力把「记一笔账」定义成一个带严格 schema 的函数让模型只负责填参数不负责拼语句。这样模型输出天然是 JSON字段类型由 schema 约束落库那一步由你自己的代码控制。我一般会把 schema 写成这样# tools.py # 定义记账工具的参数 schema模型只能按这个结构填 RECORD_TOOL { type: function, function: { name: record_transaction, description: 记录一笔个人收支流水, parameters: { type: object, properties: { amount: { type: number, description: 金额正数表示收入负数表示支出单位元 }, category: { type: string, enum: [餐饮, 交通, 购物, 居住, 医疗, 娱乐, 工资, 理财, 其他] }, account: { type: string, description: 支付账户如 微信/支付宝/银行卡/现金 }, occurred_at: { type: string, description: 发生时间ISO8601 格式缺省用当前时间 }, note: { type: string, description: 原始描述或补充备注 } }, required: [amount, category, account] } } }这段 schema 的关键在enum和required。enum把分类锁死在一个闭集里模型没法自由发挥造出「吃喝玩乐」这种新分类required保证金额、分类、账户三个字段一定存在缺了模型会被强制重试。amount用正负号区分收支比单独加一个type字段更省事后面统计时直接sum就行。2.2 调用模型并拿到结构化结果有了 schema调用逻辑就变得很直白。下面这段是核心解析函数注意tool_choice这个参数——它强制模型必须调用这个工具而不是跟你闲聊。# parser.py import json from openai import OpenAI from tools import RECORD_TOOL client OpenAI() # 从环境变量读取 API Key SYSTEM_PROMPT 你是一个记账助手。用户会用口语描述一笔收支 你需要调用 record_transaction 工具提取字段。 规则 1. 支出金额为负数收入为正数 2. 无法判断账户时默认填「微信」 3. 相对时间昨天、上周五请换算成具体日期 4. 不要向用户追问直接给出最合理的判断。 def parse_text(user_text: str) - dict: resp client.chat.completions.create( modelgpt-4o-mini, # 解析任务用轻量模型足够 messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_text}, ], tools[RECORD_TOOL], tool_choice{type: function, function: {name: record_transaction}}, temperature0, # 解析任务要确定性温度归零 ) call resp.choices[0].message.tool_calls[0] return json.loads(call.function.arguments)逻辑说明tool_choice指定具体函数名模型没有「不调用」的选项这比在 prompt 里写「请务必输出 JSON」可靠得多。temperature0是为了让同样的输入尽量得到同样的输出记账这种场景不需要创造性。gpt-4o-mini这类小模型处理字段抽取完全够用成本比大模型低一个数量级批量清洗 CSV 时这个差距会被放大到很可观。参数说明model换成更强的模型不会让字段准确率显著提升反而拖慢速度除非你的输入里夹杂大量方言和隐喻。temperature不要设成 0.7 以上否则同一句话两次解析可能给出不同分类。2.3 落库SQLite 起步别一上来就上 PostgreSQL个人记账的数据量一年撑死几千条SQLite 完全够用而且零运维。表结构建议把原始文本也存下来方便日后回溯模型判断对不对。# storage.py import sqlite3 from datetime import datetime def init_db(pathledger.db): conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, amount REAL NOT NULL, category TEXT NOT NULL, account TEXT NOT NULL, occurred_at TEXT NOT NULL, note TEXT, raw_text TEXT, -- 保留原始输入便于复盘 created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() return conn def insert_tx(conn, tx: dict, raw: str): conn.execute( INSERT INTO transactions (amount, category, account, occurred_at, note, raw_text) VALUES (?, ?, ?, ?, ?, ?), (tx[amount], tx[category], tx[account], tx.get(occurred_at) or datetime.now().isoformat(), tx.get(note, ), raw) ) conn.commit()raw_text这一列是我强烈建议加的。模型分类偶尔会翻车比如把「买药」归到「购物」事后你只看到一条错误分类根本想不起来当时说了什么。留着原文排查时一眼就能定位是模型的问题还是你表述的问题。3. 批量清洗历史账单CSV 导入的完整流程3.1 先看清你的 CSV 长什么样从支付平台导出的账单格式五花八门。常见的有「交易时间 / 交易类型 / 交易对方 / 商品说明 / 收/支 / 金额」这种列结构也有把金额和收支方向揉在一列的。动手写代码前先花五分钟用 pandas 看一眼import pandas as pd df pd.read_csv(bill.csv, encodingutf-8) print(df.columns.tolist()) print(df.head(3)) print(df[收/支].value_counts())这一步能帮你确认三件事编码是不是 UTF-8很多导出是 GBK读进来会乱码、金额列有没有带「¥」符号、收支方向是单独一列还是靠正负号。我踩过的坑是直接假设金额是数字结果pd.read_csv把它读成字符串后面求和全是拼接。3.2 逐行喂给模型但要控制并发和成本批量场景下逐行调用模型是最直观的做法但几千行串行跑会等到天荒地老。用线程池并发同时给个上限避免触发速率限制。from concurrent.futures import ThreadPoolExecutor from parser import parse_text from storage import init_db, insert_tx import pandas as pd def clean_amount(v): # 去掉货币符号和千分位逗号转成 float return float(str(v).replace(¥, ).replace(,, ).strip()) def process_row(row): text f{row[交易时间]} {row[交易对方]} {row[商品说明]} {row[金额]} try: tx parse_text(text) return tx, text except Exception as e: return None, text # 失败的行先跳过后面单独处理 def batch_import(csv_path, max_workers5): df pd.read_csv(csv_path, encodingutf-8) conn init_db() with ThreadPoolExecutor(max_workersmax_workers) as pool: for tx, raw in pool.map(process_row, df.to_dict(records)): if tx: insert_tx(conn, tx, raw) conn.close()max_workers5是个保守值实测再往上加容易撞到每分钟请求数限制反而因为重试拖慢整体。失败的行不要直接丢收集起来最后统一重跑往往是因为某一行文本特别长或者含特殊字符。3.3 用规则兜底别让模型处理它不擅长的模型擅长理解「星巴克」是餐饮但不擅长做精确的算术和格式转换。金额清洗、日期标准化这类确定性工作用代码做比用模型做又快又准。我的分工原则是语义判断交给模型格式规整交给代码。比如日期模型返回的可能是「2024-03-15」也可能是「3月15日」落库前统一用dateutil解析一遍from dateutil import parser as date_parser def normalize_date(s): try: return date_parser.parse(s).isoformat() except (ValueError, TypeError): return None # 解析不了就交给上层用当前时间兜底4. 避坑与排查那些让账本对不上的细节4.1 金额正负号反了月底统计全错现象月底一看支出是负数收入也是负数总支出算出来是个正数。原因多半是模型把「支出」也返回了正数而你的统计逻辑假设支出为负。解决在落库前加一道校验如果category属于支出类且amount 0强制取反反之收入类强制取正。别指望 prompt 里写一句「支出为负」模型就永远遵守加代码校验才是后悔药。4.2 同一笔账被记了两次现象批量导入时发现某些行重复。原因通常是并发重试——某次请求超时了你的代码重试但第一次其实已经成功落库。解决给transactions表加一个基于raw_text occurred_at的唯一索引插入时用INSERT OR IGNORE。这样即使重试也不会产生重复行。4.3 分类总是往「其他」里塞现象大量账目分类是「其他」模型像是偷懒。原因有两个一是你的enum里分类太粗模型找不到合适的就往兜底项放二是 prompt 里没给分类的定义。解决在 system prompt 里给每个分类配一句说明比如「餐饮指正餐、外卖、咖啡奶茶购物指实物商品」。实测加了定义之后「其他」的占比能从三成降到一成以内。4.4 中文编码把 CSV 读成乱码现象pd.read_csv之后列名是æäº¤æ这种。原因文件是 GBK 编码你用 UTF-8 读。解决先试encodingutf-8报UnicodeDecodeError就换encodinggbk。更稳的做法是用chardet探测编码但个人项目里手动试两次就够了。4.5 模型偶尔返回空参数现象json.loads报错或者amount字段是None。原因输入文本太短或太模糊比如「花了点钱」模型无法确定金额。解决在parse_text里加一层校验amount缺失时抛出明确异常让上层决定是丢弃还是标记为待补充。不要让它静默写入一条金额为 0 的记录那比报错更危险。5. 让账本自己会说话从流水到月度洞察账目落库只是起点真正有价值的是让模型基于结构化数据做二次分析。这一步和前面的解析是反过来的——前面是「文本转结构」这里是「结构转洞察」。我一般会写一个查询函数把某个月的流水拉出来拼成一段紧凑的文本再让模型生成一段人话总结。def monthly_insight(conn, year_month2024-03): rows conn.execute( SELECT category, SUM(amount) AS total, COUNT(*) AS cnt FROM transactions WHERE occurred_at LIKE ? GROUP BY category ORDER BY total ASC, (f{year_month}%,) ).fetchall() summary \n.join( f{r[0]}: 合计 {r[1]:.2f} 元, 共 {r[2]} 笔 for r in rows ) prompt f以下是我 {year_month} 的支出分类汇总\n{summary}\n \ 请用三句话指出最值得注意的一点并给一条可执行的省钱建议。 resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0.5, ) return resp.choices[0].message.content这里temperature可以稍微调高到 0.5让总结读起来不那么机械。注意查询里ORDER BY total ASC因为支出是负数升序排列意味着花得最多的排在最前面模型一眼就能看到大头在哪。进阶一点的做法是把连续几个月的分类汇总都喂进去让模型找趋势。比如「餐饮支出连续三个月上升」这种结论靠人翻账本很难发现但模型对比几组数字就能指出来。参数上要注意的是喂给模型的汇总文本别太长超过几百行反而会稀释重点按月聚合到分类级别就够了。还有一个我常用的技巧让模型给每笔大额支出打一个「必要性」标签分「必要 / 可选 / 冲动」三档存回数据库。月底一统计冲动消费占比一目了然。这个标签同样用 tool calling 实现schema 里加一个enum字段即可和前面的记账工具是同一套模式。我自己用这套东西跑了小半年最大的教训是别追求一次把分类体系设计完美。一开始我列了二十多个分类结果模型经常在相近分类之间摇摆后来砍到九个准确率反而上去了。分类是给人看的够用就行剩下的细节交给note字段。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/17 17:05:49

金融数据服务平台架构实践:实时指标计算与规则引擎设计

前阵子一直在忙一个代号叫 financial-services 内部项目的重构,这个项目说白了就是一套面向金融服务场景的通用数字化服务平台。不是那种给人一种高深莫测感觉的量化交易系统,也不涉及任何复杂金融模型,它更像是一套“运营中台”——负责把分…

2026/10/17 17:00:49

小新Yoga预装软件卸不掉?从备份到注册表的彻底清理指南

小新 / Yoga 系列应用卸不掉?异常软件强力卸载教程,附数据备份提醒!碰到小新或 Yoga 系列笔记本预装软件卸不掉的问题,确实挺闹心的。明明在应用列表里点了“卸载”,结果提示正在运行、需要权限,或者干脆点…

2026/10/17 17:00:49

计算机三级网络技术:三层架构与IP规划实战解析

简介:本资源是面向全国计算机等级考试三级网络技术考生的系统化备考资料,聚焦IP地址规划、路由设计、局域网与宽带城域网架构等核心考点,助力零基础或强化复习者高效突破重难点。资料以单个PDF文件形式呈现,内容完整、排版清晰&am…

2026/10/17 18:20:53

STM32F107裸机实现IEEE 1588 PTP精确时间同步方案解析

简介:基于STM32F107微控制器的PTP IEEE1588V2时钟同步实现资料,面向嵌入式开发者和工业自动化、电力系统等需要高精度时间同步的应用场景。资源包含硬件评估板固件、用户手册、烧录指南以及LwIP协议栈集成的PTP源码工程,可帮助开发者从零搭建…

2026/10/17 18:20:53

快慢指针原理与工程实践:从环检测到内存受限场景优化

1. 快慢指针不是“两个for循环的懒人替代品”,而是空间换时间的精密节拍器快慢指针(Fast-Slow Pointer)这个词,最近在某高校算法实训课的讨论区里被反复提起——但多数学生一听到就下意识皱眉,觉得是“又要背模板”。我…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑