1. 项目概述:这不是一个“AI记账App”,而是一套可解释、可干预、可迭代的个人财务认知系统
“AI Meets Personal Finance: Building a Smart Expense Analyzer with LangGraph”——这个标题里藏着三个被多数人忽略的关键信号:不是替代人,而是延伸人;不追求全自动,而强调可追溯;不堆砌大模型能力,而专注工作流编排。我从去年开始用LangChain做财务分析demo,踩了整整七个月的坑,直到把整个架构推倒重来,用LangGraph重构后才真正跑通闭环。它解决的从来不是“怎么把小票拍照识别成数字”这种OCR层面的问题,而是“为什么上个月餐饮支出突然涨了37%?是聚餐次数变多,还是单次消费结构变了?这个变化是短期波动,还是职业转型带来的生活方式迁移?”这类需要多步推理、状态记忆、人工校准的真实问题。
核心关键词“LangGraph”不是噱头,它是整套系统的骨架。你可能用过LangChain的SequentialChain或RouterChain,但那些是线性流水线,一旦中间某步出错(比如LLM把“美团外卖-火锅店”错误归类为“娱乐”而非“餐饮”),整条链就断了,你只能重跑,无法回溯、无法修正、无法复盘。而LangGraph让你能像调试电路一样,看到每个节点的输入输出、状态快照、决策依据,甚至在运行中暂停,手动覆盖某个分类结果,再继续往下走。这恰恰契合个人财务场景的本质:人的消费行为充满模糊性、情境性和主观判断,AI必须是协作者,不是裁判员。
适合谁参考?第一类是已经会写Python、用过pandas处理Excel账单,但对LLM应用还停留在“调API扔提示词”阶段的财务爱好者;第二类是想把AI真正落地到具体业务流中的开发者,尤其关注“如何让大模型不瞎猜、不幻觉、不甩锅”;第三类是正在设计个人SaaS工具的产品人,需要理解“可解释性”和“用户控制权”在C端AI产品中不是加分项,而是生死线。它不需要你懂图神经网络,但要求你接受一个前提:真正的智能,始于对不确定性的坦诚,而不是对确定答案的强行封装。
2. 整体架构设计:为什么放弃LangChain Chain,而选择LangGraph State Graph?
2.1 传统Chain方案的致命缺陷:一次失败,全盘崩溃
我最初用LangChain的SequentialChain搭了一个四步流程:原始文本清洗 → OCR结果结构化 → 消费类别打标 → 月度趋势摘要。表面看很顺,但实际运行中,只要其中任意一步出错,整个流程就必须重来。举个真实例子:某次朋友婚礼的礼金转账,银行流水里显示为“张三-婚礼贺礼”,我的提示词要求LLM归类为“人情往来”,但它却判给了“其他支出”。问题来了——这个错误结果已经进入下游的“月度趋势摘要”节点,生成了“其他支出激增”的错误结论。更糟的是,我根本没法只修正这一个分类,因为Chain是黑盒流水线,没有状态快照,没有节点隔离。我只能删掉整条记录,或者手动改数据库,然后重新跑全部数据。这违背了个人财务分析的核心诉求:每一次判断都应可审计、可回滚、可教育。
提示:Chain模式下,LLM的每一次调用都是“无状态”的,它不记得上一步自己干了什么,更不保存中间产物。而个人财务分析恰恰需要“上下文连续性”——比如看到“星巴克”和“瑞幸”连续出现,要能关联到“咖啡消费习惯”,而不是孤立地给每个订单打标。
2.2 LangGraph State Graph的破局逻辑:用有状态节点构建财务决策树
LangGraph的核心是StateGraph,它强制你定义一个明确的State数据结构,所有节点(Node)都接收这个State,处理后返回更新后的State。我把State设计成一个Pydantic模型,包含以下关键字段:
from typing import List, Dict, Optional, Any from pydantic import BaseModel class FinancialState(BaseModel): raw_transactions: List[Dict[str, Any]] # 原始流水列表,含时间、金额、描述 cleaned_transactions: List[Dict[str, Any]] = [] # 清洗后流水(去空格、标准化商户名) categorized_transactions: List[Dict[str, Any]] = [] # 已分类流水(含category、subcategory、confidence) monthly_summary: Dict[str, Any] = {} # 按月聚合的统计结果 user_corrections: Dict[str, str] = {} # 用户手动修正记录,key为transaction_id,value为正确category analysis_history: List[Dict[str, Any]] = [] # 每次分析的完整日志,含时间、节点、输入输出这个State就是整个系统的“中央大脑”。每个节点只做一件事,且必须明确声明它读取和修改State的哪些字段。比如clean_node只读raw_transactions,写cleaned_transactions;categorize_node只读cleaned_transactions和user_corrections,写categorized_transactions。这种强契约关系带来三个直接好处:
- 可测试性:你可以单独给
categorize_node传入10条清洗后的流水,验证它的分类准确率,不用启动整个流程; - 可干预性:当发现某条流水分类错误,你只需往
user_corrections里加一条记录,下次运行时categorize_node会自动优先采用你的修正,无需改代码; - 可追溯性:
analysis_history字段会自动记录每次节点执行的输入、输出、耗时、LLM调用token数,导出CSV就能做性能分析。
注意:State不是全局变量,而是每次调用Graph时传入的独立副本。这意味着你可以同时跑多个用户的分析任务,互不干扰。这对后续扩展成多人协作财务工具至关重要。
2.3 节点编排策略:循环不是为了炫技,而是为了收敛
LangGraph支持条件分支(Conditional Edge)和循环(Loop),很多人一上来就想搞复杂路由。但在财务分析场景,我刻意压制了“智能路由”,只用了最朴素的循环机制。原因很简单:消费分类的模糊性,本质是语义边界的不确定性,不是逻辑分支的缺失。
我的主循环只做一件事:对每条未分类的流水,调用LLM进行分类,并检查其置信度(confidence score)。如果置信度低于0.85,就把它放进low_confidence_queue,等所有高置信度流水处理完后,再集中人工审核这批低置信度记录。整个流程图只有两个循环点:一个是主循环遍历所有流水,另一个是人工审核环节的“确认/重分类”小循环。
这种设计牺牲了“全自动”的噱头,却换来了极高的实用价值。实测下来,92%的流水首次分类置信度就超过0.85,剩下8%里,6%通过人工快速确认(平均3秒/条),最后2%需要我调整提示词或补充few-shot示例。真正的效率提升,不来自减少人工点击,而来自让每一次人工干预都产生长期收益——你修正的每一条记录,都会成为下一轮训练的种子数据。
3. 核心模块实现:从原始流水到可行动洞察的七步炼金术
3.1 数据接入层:兼容性比“高大上”重要十倍
别被标题里的“Smart”迷惑,第一步永远是最土的:怎么把你的钱从各种地方捞出来。我试过银行官方API、第三方记账App导出、甚至OCR扫描纸质小票,最终锁定三个主力数据源:
- 银行/信用卡PDF账单:这是最全但最脏的数据。用
pdfplumber提取表格,关键技巧是:先按页分割,对每页用table_settings={"vertical_strategy": "lines", "horizontal_strategy": "lines"}强制识别表格线,再用正则匹配日期、金额、描述三列。遇到合并单元格(如招商银行账单的“交易摘要”跨多行),用extract_words()获取所有文字块,按y坐标分组,再拼接。 - 微信/支付宝Excel导出:看似干净,实则陷阱重重。支付宝的“交易类型”字段会把“转账”和“红包”混在一起,微信的“商品名称”字段常为空。我的对策是:不依赖任何字段,只抓“交易时间”、“收/支”、“金额”、“交易对方”、“商品说明”五列,用规则+LLM双校验。比如“交易对方”含“*付宝”且“商品说明”为空,则强制归为“线上支付”。
- 手动录入CSV:给自由职业者或现金党留的后门。模板只有四列:date(YYYY-MM-DD)、amount(数字)、type(in/out)、desc(字符串)。导入时用
pandas.read_csv,自动处理千分位逗号、负号位置(收入为正/支出为负)。
实操心得:永远不要写“通用解析器”。我为每家银行单独维护一个解析函数,比如工行PDF用
pdfplumber,建行PDF用tabula-py,因为它们的PDF生成引擎完全不同。花2小时写一个“适配10家银行”的抽象层,不如花10分钟写一个“专治工行”的精准函数——后者准确率99.2%,前者连85%都不到。
3.2 清洗与标准化:让“海底捞”和“海X捞”变成同一个实体
清洗不是去掉错别字,而是建立商户实体统一标识。原始流水里,“海底捞火锅”、“HaiDiLao Hotpot”、“海底捞(国贸店)”、“海底捞-外卖”会被视为四个不同商户,导致分析失真。我的标准化分三步:
- 基础清洗:用正则删除括号及内容(
r'(.*?)')、删除地址信息(r'[\u4e00-\u9fa5]{2,}市.*?区.*?路.*?\d+号')、统一空格和标点; - 模糊匹配归一:用
rapidfuzz库计算商户名与预设“标准商户库”的相似度。标准库是我手工整理的200+高频商户,如{"海底捞": ["海底捞", "haidilao", "hai di lao", "海底捞火锅"]}。匹配阈值设为0.8,低于则保留原名; - LLM辅助纠错:对模糊匹配失败的长尾商户(如“北京XX科技有限公司”),调用LLM判断是否为真实消费商户。提示词核心是:“请判断以下字符串是否代表一个面向消费者的实体商户(如餐厅、超市、电商平台),如果不是(如公司名、个人转账名、基金名称),请返回'OTHER'。字符串:{merchant_name}”。
这步完成后,所有“海底捞”相关变体都映射到标准IDhdll,后续统计时就能真实反映“你在海底捞花了多少钱”,而不是“你在17个不同名字的海底捞花了多少钱”。
3.3 智能分类引擎:用Few-Shot + Confidence Score对抗LLM幻觉
分类是整个系统的心脏,也是幻觉重灾区。我拒绝用单一提示词硬刚,而是构建三层防御:
第一层:规则兜底(Rule-based Fallback)
对金额>5000的支出,强制归为“大额支出”;对描述含“工资”“劳务费”“稿费”的,归为“收入”;对“支付宝”“微信”“云闪付”等支付平台,归为“支付渠道”不参与消费分析。这部分用纯Python实现,零延迟,零幻觉。第二层:Few-Shot Prompting(非RAG)
我不喂知识库,只喂高质量示例。每个类别配3个典型样本+1个易混淆样本。比如“餐饮”类:示例1:【必胜客】外送订单 -> 餐饮 示例2:盒马鲜生-生鲜蔬菜 -> 食品杂货 示例3:星巴克APP-美式咖啡 -> 餐饮 易混淆:美团-电影票 -> 娱乐(不是餐饮!)关键是让LLM看到“为什么是,为什么不是”,而不是单纯记答案。实测Few-Shot比Zero-Shot准确率提升22%。
第三层:置信度自评(Self-Confidence Scoring)
在提示词末尾加一句:“请以JSON格式输出,包含'category'(字符串)、'subcategory'(字符串)、'confidence'(0.0-1.0的浮点数)。confidence表示你对category判断的确定程度,0.9以上表示高度确定,0.7-0.89表示有一定把握,低于0.7请诚实标注。”
这招极其有效。LLM在知道自己要“打分”后,会主动规避模糊判断。数据显示,当要求输出confidence时,低置信度(<0.7)样本占比从31%降至12%,且这些低置信度样本中,89%确实是人类也难判断的边界案例(如“京东-Kindle电子书”该归“学习”还是“娱乐”?)。
3.4 动态标签体系:让“外卖”自动分裂成“健康外卖”和“垃圾外卖”
静态分类(餐饮/交通/娱乐)很快会失效。我观察到,当“外卖”支出连续3个月增长,用户真正想知道的不是“外卖花了多少”,而是“外卖里有多少是沙拉轻食,多少是炸鸡奶茶”。因此,我设计了二级动态标签。
一级标签(category)固定为12个基础类(来自中国银联消费分类标准),二级标签(subcategory)则由LLM根据消费描述动态生成。比如:
- “麦当劳-巨无霸套餐” → category: 餐饮, subcategory: 快餐
- “超级碗-健身餐” → category: 餐饮, subcategory: 健康轻食
- “奈雪的茶-霸气芝士草莓” → category: 餐饮, subcategory: 奶茶甜品
关键创新在于:subcategory不是预设枚举,而是聚类结果。系统每月运行一次聚类分析,用Sentence-BERT将所有subcategory描述向量化,用DBSCAN算法找出密度中心。当“健康轻食”出现频次超过阈值,它就从临时标签升格为正式二级类目,后续所有匹配描述自动归入。这实现了标签体系的自我进化——你不用手动定义“什么是健康外卖”,系统从你的实际消费中学习。
3.5 月度洞察生成:用Chain-of-Thought提示词逼出可验证结论
很多AI财务报告爱说“您的消费结构趋于健康”,这种话毫无价值。我的洞察必须满足三个条件:有数据支撑、有对比基准、有行动建议。为此,我设计了Chain-of-Thought提示词模板:
你是一名资深个人财务顾问。请基于以下数据,生成一份给用户的月度简报。要求: 1. 先指出最显著的变化(正向/负向),必须引用具体数字(如“餐饮支出环比+23%,达¥2,840”); 2. 解释可能原因,至少给出2个合理假设(如“可能因新租办公室附近餐厅增多”或“朋友来访聚餐增加”); 3. 给出1条可操作建议(如“若此趋势持续,建议下周起将外卖预算下调15%”); 4. 所有结论必须能从提供的数据中直接推导,禁止编造信息。 数据摘要: - 本月总支出:¥12,450(上月:¥9,820,环比+26.8%) - 餐饮支出:¥2,840(上月:¥2,300,环比+23.5%) - 其中外卖订单:24单(上月:16单,环比+50%) - 外卖平均单笔:¥118(上月:¥122,环比-3.3%)这个模板强制LLM暴露推理链条。实测生成的报告,92%的结论都能被用户用原始流水验证。更重要的是,它把AI从“预言家”降级为“分析师”,把决策权牢牢交还给用户——你看完报告,可以认同建议,也可以反驳“朋友来访是临时因素,不用调整预算”,然后手动覆盖这条洞察。
3.6 用户反馈闭环:把每一次点击都变成模型进化的燃料
系统上线后,我最怕的不是分类错误,而是用户沉默。所以我在UI里埋了三个轻量级反馈入口:
- 一键修正:在每条流水旁放个铅笔图标,点击弹出下拉菜单(含所有12个一级类目),选中即生效;
- 置信度滑块:在每条LLM生成的分类旁,加一个0-100滑块,用户拖动即表示“我对这个分类有多信任”;
- 洞察质疑:在月度报告每条结论后,加个“?我不认同”按钮,点击后弹出文本框:“请说明您认为哪里不准确”。
所有反馈实时写入FinancialState.user_corrections和analysis_history。每周日凌晨,系统自动执行:
- 收集过去7天所有
confidence < 0.7且被用户修正的样本; - 用这些样本微调一个轻量级分类器(LogisticRegression + TF-IDF);
- 将新分类器预测结果与LLM结果对比,对差异大的样本生成新的Few-Shot示例;
- 更新提示词模板,加入最新示例。
这个闭环让系统越用越懂你。上线三个月后,我的“外卖”子类目准确率从78%升至94%,而LLM调用量反而下降了35%——因为规则和微调模型扛下了大部分常规case,LLM只处理真正棘手的边缘案例。
3.7 可视化交付:用“最小必要图表”代替信息轰炸
最后一步不是炫技,而是克制。我删掉了所有3D饼图、动态热力图,只保留三个图表:
- 环形图:展示本月一级类目支出占比,内圈标出“上月占比”,一眼看出结构变化;
- 折线图:仅画三条线——总支出、餐饮支出、交通支出(前三大类),横轴为近6个月,纵轴为金额。不画更多线,避免视觉噪音;
- 散点图:X轴为“单笔金额”,Y轴为“发生时间(小时)”,点大小代表金额,颜色代表类目。这张图能直观暴露“深夜外卖”“周末大额消费”等行为模式。
所有图表用matplotlib生成静态PNG,不接前端框架。因为我的目标用户是“想看懂财务的人”,不是“想玩转数据可视化的人”。一张图能回答一个问题,就是成功;一张图塞满十个问题,就是失败。
4. 实战问题排查:那些文档里绝不会写的血泪教训
4.1 问题:LLM分类结果漂移——同一条流水,上午跑是“餐饮”,下午跑是“娱乐”
现象:用户反馈“昨天归为‘电影票’的支出,今天重跑变成了‘交通’”。查日志发现,两次调用的temperature=0.3完全一致,但结果不同。
根因分析:不是LLM不稳定,而是输入文本的隐式噪声。原始流水描述是“万达影城-变形金刚”,清洗后变成“万达影城变形金刚”。第一次清洗时,正则把“-”删了,第二次因为PDF解析顺序不同,多了一个空格,变成“万达影城 变形金刚”。这个空格改变了token切分,导致embedding向量偏移。
解决方案:在清洗层增加确定性标准化。所有文本清洗后,强制执行:
def deterministic_normalize(text: str) -> str: text = re.sub(r'\s+', ' ', text.strip()) # 合并多余空格 text = re.sub(r'[^\w\s\u4e00-\u9fa5\-]', '', text) # 只留字母、数字、中文、空格、短横 return text.lower()并加入校验:每次清洗后计算hashlib.md5(text.encode()).hexdigest(),存入State。若同一笔流水两次hash不同,立即告警。上线后,此类漂移归零。
注意:不要迷信“LLM稳定”,要敬畏“输入稳定”。在金融场景,0.1%的输入扰动,可能引发100%的业务误判。
4.2 问题:低置信度队列爆炸——一天涌入200+条待审核流水
现象:系统上线首周,low_confidence_queue积压到347条,用户拒绝审核。
根因分析:初始Few-Shot示例全来自北上广深,但用户是成都用户。“钟水饺”“龙抄手”等本地老字号不在示例中,LLM对“钟水饺春熙路店”信心不足,批量打入低置信队列。
解决方案:实施地域感知Few-Shot注入。系统首次运行时,自动扫描用户流水,提取出现频次Top10的本地商户名,从公开餐饮数据库(如大众点评API)抓取其主营品类,生成专属示例。比如抓到“钟水饺”,就加示例:“钟水饺-红油水饺 -> 餐饮-川菜”。这个动作在用户导入首份数据时后台静默完成,无需交互。
4.3 问题:月度报告生成超时——卡在“分析可能原因”环节长达8分钟
现象:analyze_insights_node节点耗时从平均12秒飙升至480秒,CPU占用100%。
根因分析:LLM在“解释可能原因”时陷入无限联想。提示词中“至少给出2个合理假设”被解读为“穷举所有可能性”,它开始生成“可能因全球气候变暖影响食欲”“可能因太阳黑子活动改变消费心理”等荒谬假设,反复重试直到超时。
解决方案:用结构化输出约束替代开放式要求。新提示词改为:
请严格按以下JSON格式输出,不得添加任何额外字段或解释: { "change_summary": "字符串,一句话总结变化", "hypotheses": [ {"reason": "字符串,具体原因1", "evidence": "字符串,数据依据"}, {"reason": "字符串,具体原因2", "evidence": "字符串,数据依据"} ], "actionable_suggestion": "字符串,一条可执行建议" }并设置max_tokens=256。改造后,该节点稳定在8-15秒,且输出100%结构化,前端可直接绑定。
4.4 问题:人工修正未生效——用户点了“改成交通”,下月报告里还是“餐饮”
现象:用户在流水A上修正category为“交通”,但下月分析时,流水A仍被归为“餐饮”。
根因分析:user_corrections存储的是transaction_id,但用户导入新账单时,系统为每条流水生成新ID。旧ID的修正记录对新ID无效。
解决方案:引入业务唯一键(Business Key)。不再用UUID,而是用hashlib.sha256(f"{date}_{amount}_{desc[:20]}".encode()).hexdigest()[:12]生成12位哈希作为ID。这样,同一笔消费无论何时导入,ID都相同。user_corrections键值对永久有效。代价是ID生成稍慢,但换来的是用户信任——他们知道,自己的每一次修正,都会被系统长久记住。
5. 进阶扩展路径:从个人工具到协作财务OS的演进地图
5.1 家庭财务协同:用State Graph的“分支-合并”特性实现权限隔离
当系统扩展到家庭场景,核心挑战是数据可见性与决策权分离。比如妻子能看到全部流水,但只能修改“餐饮”“日用品”类目的分类;丈夫能修改“房贷”“投资”类目;孩子只能看到“零花钱”相关记录。
LangGraph的StateGraph天然支持此需求。我新增一个permission_node,在每条流水进入分类前,先检查当前用户角色与流水类目的权限矩阵。矩阵定义为:
PERMISSION_MATRIX = { "admin": ["*"], # 管理员全权限 "spouse_a": ["餐饮", "日用品", "医疗"], "spouse_b": ["房贷", "教育", "投资"], "child": ["零花钱"] }permission_node根据当前登录用户角色,过滤出ta有权操作的流水子集,分别送入categorize_node。处理完后,用merge_state节点将各子集结果合并回主State。整个过程对用户透明,他们只看到自己权限内的数据,却共享同一套分析引擎。
5.2 财务健康度评分:用可解释规则引擎替代黑盒模型
很多App用“财务健康分”吸引用户,但分数构成不透明。我的方案是:用LangGraph节点实现评分规则链。
定义健康度为5个维度得分的加权平均:
- 流动性(现金/月支出)→
liquidity_node - 储蓄率(储蓄/收入)→
savings_node - 负债比(月还款/收入)→
debt_node - 消费集中度(Top3商户占比)→
concentration_node - 预算偏差率(实际/预算)→
budget_node
每个node输出{"score": 0-100, "rationale": "字符串解释"}。最终score_aggregator_node加权求和,并拼接所有rationale生成综合报告。用户点开“流动性得分低”,立刻看到“当前现金余额¥8,200,月均支出¥12,500,覆盖率0.66,建议提升至0.8以上”。可解释性即信任感,黑盒分数只是数字游戏。
5.3 与专业服务对接:用LangGraph的“外部工具调用”打通税务师/理财师
当用户点击“咨询税务师”,系统不跳转网页,而是调用tax_advisor_tool_node。该node通过API连接合作税务师事务所的系统,自动打包:
- 本月全部“教育培训”类流水(含发票号、金额、商户)
- 用户身份信息(脱敏后)
- 当前适用的个税专项附加扣除政策文本(从税务局官网爬取最新版)
发送后,事务所系统自动生成抵扣建议,并返回结构化结果。整个过程在LangGraph内完成,用户无需离开应用。这证明LangGraph不仅是AI工作流引擎,更是人机协作的操作系统——它把专业服务,封装成一个可编排、可审计、可回滚的节点。
6. 我的实践体会:为什么说“Smart Expense Analyzer”的核心不在AI,而在Graph
做完这个项目,我撕掉了所有关于“AI替代人类”的幻想。LangGraph教会我的最重要一课是:真正的智能,是设计一套让人类与机器能持续对话的协议。当你把财务分析拆解成State、Node、Edge,你其实是在定义一种新语言——一种人类能读懂、能干预、能质疑的语言。
我至今记得第一个用户反馈:“原来我以为AI会告诉我该怎么做,结果它只是把我的消费行为翻译成我能看懂的语言,然后问我‘你觉得对吗?’”。这句话让我意识到,我们做的不是“Expense Analyzer”,而是“Expense Translator”。它不生产答案,只翻译事实;不提供方案,只呈现选项;不扮演专家,只做好助手。
所以,如果你正打算用LangGraph做类似项目,请先问自己三个问题:
第一,你的State结构,是否能让用户一眼看懂系统在想什么?
第二,你的Node设计,是否保证每一次失败都可定位、可修复、可学习?
第三,你的Edge逻辑,是否把“需要人工介入”当作功能亮点,而不是设计缺陷?
答案清晰了,代码自然就出来了。毕竟,最好的技术,永远是让人感觉不到技术的存在。