用iPhone快捷指令 + AWS Lambda + Bedrock Claude,搭建一个AI自动记账系统
本文最后更新于 2026年7月13日 凌晨
每次买完东西,票据要么随手丢了,要么攒成一沓,想统计一下每月花在餐饮、超市、数码上的钱,最后往往不了了之。这篇文章记录我如何用一套几乎全自动化的流程,把”拍照”这一个动作,变成一条结构化的Notion账单记录。
整体思路很简单:iPhone拍照 → 压缩 → 传给AWS Lambda → Lambda调用Bedrock上的Claude视觉模型识别小票 → 结构化结果写入Notion数据库
全程不需要手动录入任何数字,拍完照几秒钟后,Notion里就多了一条带图片、带商家名、带金额分类的完整记录。
一、整体架构
先上一张架构图,把整条链路的每一环都画出来:
graph LR
A[ iPhone拍照] --> B[快捷指令:压缩图片]
B --> C[快捷指令:Base64编码]
C --> D[POST请求]
D --> E[AWS Lambda]
E --> F[Bedrock Claude视觉模型]
F -->|识别出结构化字段| E
E -->|1.创建上传任务| G[Notion File Upload API]
E -->|2.发送图片字节| G
E -->|3.创建页面写入数据| H[(Notion数据库)]
G -. 图片存储在 .-> H
style A fill:#e8f4fd
style E fill:#fff4e0
style F fill:#fce4ec
style H fill:#e8f5e9
图中可以看到,整条链路只有一次网络请求从手机发出(拍照→压缩→编码→POST),剩下所有的复杂逻辑(调用Bedrock、三段式上传Notion)全部封装在Lambda内部,手机端不需要感知这些细节。
在设计之初,有几个关键决策点:
1. 用AI视觉识别,而非传统OCR
传统OCR(如Textract的AnalyzeExpense)对规整的票据识别效果不错,但字段是固定的,遇到手写、模糊、非标准格式的小票容错率较低。直接用Bedrock上的Claude视觉模型,可以让AI”理解”小票内容,自己判断分类、拼接商品清单,灵活性更高。
2. 图片不落地S3,直接在Lambda内存中流转
图片全程走向是:iPhone → Lambda(内存)→ Notion自己的文件存储。不经过S3这一层,减少了一个存储环节和相关的管理成本。
3. 手机端只做”拍照+发送”,逻辑全部收敛到Lambda
早期我在快捷指令里直接调用Notion的三段式File Upload API(创建上传任务→发送文件→创建页面),验证链路可行后,把这部分逻辑整体迁移到Lambda里,手机端只需要一次网络请求。
二、Notion数据库设计
数据库包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| 商家(Title) | 标题 | AI识别出的店名 |
| 日期 | Date | 小票上的消费时间 |
| 总金额 | Number | 含税总额 |
| 分类 | Select | 餐饮/超市/数码/缴费/其他 |
| 支付方式 | Select | 信用卡/現金/手机支付/其他 |
| 商品明细 | Text | AI拼接的”商品名 价格 数量”列表 |
| 小票原图 | Files & media | 直传Notion自己的存储,不经S3 |
| 记录创建时间 | Created time | Notion自动记录,不需要代码写入 |
几个设计上的取舍:
- 商品条目数量不固定(有的小票只有一件商品,有的有十几件),所以用一个多行文本字段拼接展示,而不是拆成”商品1/商品2…”这种固定列
- “记录创建时间”用Notion原生的Created time类型,创建页面时自动生成,不需要在代码里手动传时间戳,零维护成本
三、Notion API配置
1. 创建Integration,获取Token
Notion的自动化写入,依赖一个叫”Integration”(或者叫”Connection”,不同版本UI叫法略有差异)的机制,本质上是一个专属的API访问凭证。
- 打开 notion.so/my-integrations
- 点”+ New integration”,填写名称,选择关联的workspace
- 创建后,进入配置页面,找到”Internal Integration Secret”这一栏,点”Show”显示出来
- 复制这个token——新建的token是
ntn_开头(旧版本是secret_开头,仍然有效)
安全提示:这个token拥有对workspace的写入权限,绝对不能提交到代码仓库、不能贴在聊天记录或者任何公开场合。如果不小心泄露了,要立刻回到这个页面重置(Refresh)。
2. 把Integration连接到目标数据库
仅仅创建了Integration还不够,必须显式把它”连接”到你想操作的具体数据库,否则调用API时会收到权限错误。
- 打开你要写入的Notion数据库页面(整页视图)
- 右上角点”…”(三个点菜单)
- 找到”Connect to”(部分版本叫”Add connections”)
- 搜索并选中你创建的Integration
3. 获取数据库ID
- 打开这个数据库的整页视图
- 复制浏览器地址栏的URL,形如:
1
https://www.notion.so/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?v=yyyyyyyy notion.so/后面到?v=前面那段32位字符,就是数据库ID(有没有横杠都可以)
4. 每次API请求必带的Headers
1 | |
Notion的API是按版本迭代的,Notion-Version这个头必须显式声明,不同版本之间字段结构可能有差异(比如后期引入的”data source”概念,对多数据源的database有影响)。
四、Notion API踩坑记录
401 Unauthorized:token放错了位置
第一次调试时一直收到401报错,排查后发现:Authorization和Notion-Version被错误地放进了”请求体(JSON)”里,而不是”请求头(Headers)”里。这两个信息属于HTTP协议层面的元数据,必须放在Headers,放进body里服务器完全读不到。
JSON嵌套结构:字典模式下容易迷路
Notion创建页面的请求体是深层嵌套的(parent→properties→商家→title→数组→词典→text→content),在快捷指令这种”字典模式”的界面里手动搭建,很容易把某个字段放错层级(比如误把properties塞进了parent里面)。建议每加一层,就退出来截图核对一次结构。
五、Lambda + Bedrock 核心代码
整体流程
1 | |
自动检测图片格式,而非写死JPEG
考虑到以后可能遇到不同来源的图片(不只是iPhone直接拍照),加了一个基于文件头字节判断真实格式的函数:
1 | |
这样不管图片实际是什么格式,调Bedrock和传Notion时都会用正确的media_type,不会因为类型不匹配而报错。
Prompt设计:明确告知每个字段的数据类型
调试中踩过一个坑:prompt里给total_amount的示例值加了引号,导致AI把它当成字符串返回("1617"而非1617),写入Notion时因为类型不匹配直接报错。
修复方式是双重保险:
- JSON模板本身不给total_amount加引号,暗示这里应该是数字
- 额外用自然语言明确写出每个字段的类型要求:
1 | |
- 代码层面再加一层防御性转换,即便AI偶尔还是返回字符串,也能自动纠正:
1 | |
错误处理:让HTTP错误的详细信息不被吞掉
早期except Exception as e: return _response(500, {"error": str(e)})这种写法,对于urllib.error.HTTPError只会打印出”HTTP Error 400: Bad Request”这种毫无信息量的提示。改成专门捕获这个异常类型,把响应体读出来:
1 | |
这样调试效率大幅提升,能直接看到Notion API返回的具体校验失败原因。
六、最终效果
拍一张真实的小票,几秒后Lambda返回:
1 | |
对应地,Notion数据库里同步出现一条完整记录,商家名、金额、分类全部自动填好,还能点开查看原始小票图片。
七、经验总结
- 先手动跑通,再自动化:一开始在快捷指令里手动调三次Notion API,虽然繁琐,但让我彻底理解了File Upload API的完整流程,后续搬到Lambda时debug效率高很多
- AI的输出格式,要靠明确指令而非隐晦暗示:prompt里的示例格式(带不带引号)会直接影响AI的输出类型,与其让AI”猜”,不如直接用自然语言写清楚类型要求
- 日期时间一定要显式规定时区:AI从图片上识别出的时间字符串本身不带时区信息,如果不在prompt里明确要求(比如固定加上
+09:00),Notion按UTC或默认时区解读时,存进去的时间可能会跟小票上写的对不上,产生几个小时的偏差 - 代码里的防御性写法值得投入:图片格式检测、数字类型转换,这些”看似多余”的代码,恰恰是应对真实世界数据不规整的关键
- 错误信息要完整暴露,不要吞掉细节:排查问题时,笼统的”HTTP 400”和具体的校验失败原因,debug效率天差地别
附:完整Lambda代码
1 | |