LangGraph构建可解释个人财务分析系统
2026/6/8 6:04:35 网站建设 项目流程

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_transactionscategorize_node只读cleaned_transactionsuser_corrections,写categorized_transactions。这种强契约关系带来三个直接好处:

  1. 可测试性:你可以单独给categorize_node传入10条清洗后的流水,验证它的分类准确率,不用启动整个流程;
  2. 可干预性:当发现某条流水分类错误,你只需往user_corrections里加一条记录,下次运行时categorize_node会自动优先采用你的修正,无需改代码;
  3. 可追溯性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”、“海底捞(国贸店)”、“海底捞-外卖”会被视为四个不同商户,导致分析失真。我的标准化分三步:

  1. 基础清洗:用正则删除括号及内容(r'(.*?)')、删除地址信息(r'[\u4e00-\u9fa5]{2,}市.*?区.*?路.*?\d+号')、统一空格和标点;
  2. 模糊匹配归一:用rapidfuzz库计算商户名与预设“标准商户库”的相似度。标准库是我手工整理的200+高频商户,如{"海底捞": ["海底捞", "haidilao", "hai di lao", "海底捞火锅"]}。匹配阈值设为0.8,低于则保留原名;
  3. 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_correctionsanalysis_history。每周日凌晨,系统自动执行:

  1. 收集过去7天所有confidence < 0.7且被用户修正的样本;
  2. 用这些样本微调一个轻量级分类器(LogisticRegression + TF-IDF);
  3. 将新分类器预测结果与LLM结果对比,对差异大的样本生成新的Few-Shot示例;
  4. 更新提示词模板,加入最新示例。

这个闭环让系统越用越懂你。上线三个月后,我的“外卖”子类目准确率从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逻辑,是否把“需要人工介入”当作功能亮点,而不是设计缺陷?

答案清晰了,代码自然就出来了。毕竟,最好的技术,永远是让人感觉不到技术的存在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询