前言
春节前 OpenClaw 火起来的时候,我基本是第一时间就开始折腾了。
那段时间挺兴奋的。白天正常工作,晚上回家继续配各种东西:Telegram、Gmail、Calendar、browser、skills、cron、各种本地脚本。经常一弄就到夜里一两点。那种感觉有点像很多年前第一次买 VPS,明明没有特别明确的目的,但就是觉得“这里面一定有东西”。
等基础设施都配得差不多以后,问题反而来了:
到底让它帮我做什么?
如果只是问答、总结网页、写几段代码,那当然也能用,但总觉得不够“个人助理”。真正的助理不应该只是在我打开聊天框的时候回答问题,而应该长期知道我在做什么、帮我处理那些反复发生又很烦的杂事。
我最后选中的第一个真实场景,是差旅报销。
这两年我出差比较多,每次最麻烦的不是出差本身,而是回来报销。各种票据散落在不同地方:
- 12306 的电子发票在邮箱里;
- 酒店 folio 可能是前台给的 PDF,也可能是邮件附件;
- Uber、Grab、高德打车各有自己的邮件格式;
- 国内很多餐饮可以开电子发票;
- 海外 meal 往往只有纸质 receipt,只能拍照;
- 有些国家/地区还涉及外币、汇率、行程单和发票是否齐全的问题。
每次报销时,我都要重新翻邮箱、翻聊天记录、翻相册、对日期、改文件名、核金额。每一件事单独看都不难,但连起来非常消耗精力。
于是我开始想:
能不能让 OpenClaw 帮我从“出差发生”开始,就把报销这件事一路接住?
这听起来像是“让 AI 识别票据并生成表格”,但真正做起来之后我才发现,它完全不是一个简单任务。
因为一个真正可用的报销 agent,至少要回答三个问题:
- 这张票属于哪次差旅? 只看票据本身不够,它必须知道我的 trip map。
- 这张票据合不合格? 国内电子发票、酒店专票+水单、海外 receipt、打车行程单,各有不同规则。
- 怎么避免大模型幻觉? 报销不是写作文,金额、日期、城市、发票号错一个都可能出问题。
这篇文章想记录的,就是我怎么从一个“让 OpenClaw 帮我整理票据”的想法,一步步把它做成一条差旅报销工作流。
整体架构:不是一个功能,而是一条链路
最终我把这件事拆成了四个核心模块:
- Trip Planner:先知道我有哪些差旅,哪天在哪个城市;
- Claim Intake:从 Telegram、Gmail、文件里收集和解析票据;
- Export / Audit:按某次 trip 导出 claim.csv 和证据包,并检查缺失和错误;
- Submit:把整理好的材料录入内部报销系统。
这四个模块里面,最重要的其实是第一个:Trip Planner。
很多人一开始会把报销自动化理解成“票据识别”。但票据识别只解决了“这是什么”——比如这是一张 2026-03-24 的餐饮小票,金额 HKD 248。真正难的是“它应该去哪”——它属于哪次出差?是否在差旅日期内?城市是否匹配?这次出差是否允许这类 expense?
所以整个系统的基础不是 OCR,而是一张可靠的 trip map。
另一个关键设计是:用 skill 约束模型的行为,用 script 固化确定性逻辑。
我不希望每次都让 LLM 自由发挥:“请帮我看看这张票怎么处理”。这种方式 demo 起来很快,但稳定性很差。后来基本形成了一个原则:
- 稳定规则写进 script;
- 操作流程写进 skill;
- LLM 负责调度、判断异常、补洞、做多模态理解;
- 最终状态落到 SQLite,而不是只存在聊天上下文里。
截至写这篇文章,相关项目已经有 200 多个 commit、180 多个文件、4 万多行内容。前期我还会认真看 AI 每个 commit 改了什么,后面迭代速度实在太快,已经变成“看测试、看审计结果、看最终产物”。这也是我对 agent 编程感受变化很大的地方:人不再逐行控制所有实现,而是在更高层控制目标、边界和验证方式。
第一关:先让它知道我哪天在出差
Trip Planner 最开始看起来很简单:读日历,找出差。
比如日历里有一个多日 event,location 在香港,那就认为这几天是一趟香港出差。或者某一天有外地会议,就认为这天是一日往返。
但真实日历很快教我做人。
挑战一:日历事件并不标准
公司里不同的人创建会议的习惯完全不一样:
- 有人把城市写在 title;
- 有人写在 location;
- 有人只写会议室名字;
- 有人写中文,有人写英文;
- 有些 event 是 tentative;
- 有些取消的会议依然留在日历里;
- 同一个会议可能同时出现在多个 calendar 里。
如果让 LLM 去猜,短期看起来很聪明,长期一定会翻车。比如它把一个 tentative meeting 当成真实出差,后面的票据就可能匹配到一趟根本不存在的 trip。
后来我给 Trip Planner 加了很多硬规则:只信任结构化 location;过滤 cancelled/tentative;必要时重新拉完整 ICS;对 organizer 做去重;对过去 trip 做保留,不能因为一次同步没扫到就删除。
但即便如此,真实世界总有例外。最后我接受了一个很朴素的方案:dummy event + override。
也就是说,如果某次出差日历本身不够标准,我就手动创建一个结构清晰的 dummy event,或者在配置里加 override。它不优雅,但可靠。对于个人助理系统来说,可靠比自动猜测更重要。
挑战二:JSON 文件不可靠
早期 trip map 是 JSON 文件。做 demo 很方便,但后面问题越来越多:
- 字段可能缺失;
- 历史 trip 和未来 trip 容易混在一起;
- backfill 时容易覆盖旧状态;
- 不同脚本读写 JSON 时容易产生不一致;
- 出错后很难知道到底是哪一步写坏了。
后来我把它迁移成了 SQLite-backed trip store,并且通过 script 提供稳定查询接口。LLM 不直接“凭感觉读文件”,而是调用固定脚本查询:某天有哪些 trip、某个 trip 的城市和日期是什么、是否需要 backfill。
这个变化非常关键。因为从这里开始,Trip Planner 不再是一个临时中间文件,而变成了整个报销系统的事实来源。
第二关:Claim Intake,真正的坑都在票据里
有了 trip map 以后,下一步就是收集票据。
我设计了几种入口:
- Telegram topic:吃完饭拍照,直接发到指定话题;
- Gmail intake:电子发票、12306、Uber、酒店邮件从邮箱里抓;
- 手动文件:一些特殊 PDF 或历史材料可以手动放进去;
- repair 流程:导出时发现缺东西,再回头补扫 Gmail 或历史 staged 文件。
这部分花的时间最多,因为票据世界非常混乱。
12306:最规范,但也有坑
12306 算是最规范的。邮件、发票、行程信息相对稳定,parser 可以写得比较确定。
但它也不是完全没有坑。比如退票、红字发票、手续费等场景都要排除或特殊处理。曾经有一次 parser 看到“红字”就直接把内容判断成退款,结果误伤了正常发票。后来我专门重写了 12306 parser,并加了回归测试。
这个案例让我意识到:越是看起来规则明确的东西,越要把边界条件写进测试。
国内电子发票:XML 是救命稻草
国内很多电子发票邮件里会带 XML、PDF、OFD 附件。PDF 给人看,XML 给机器读。
早期如果只让 LLM/OCR 读 PDF,稳定性其实一般;后来我把重点转到 XML 解析上,发票代码、号码、金额、购买方、销售方、日期这些核心字段基本都能稳定拿到。
所以这里的经验很明确:只要有结构化数据,就不要迷信多模态。多模态是 fallback,不是第一选择。
Telegram 小票:不是“收到图片”这么简单
海外 meal 很多只有纸质 receipt。我的理想流程是:吃完饭,拍照,发到 Telegram 的某个报销 topic,OpenClaw 自动记录。
一开始这个流程很不稳定。有时候我发一张小票,它会问:“这张图片是干嘛的?”
这其实很合理。站在一个通用聊天机器人的角度,它并不知道这个 topic 的语境。后来我发现可以在 openclaw.json 里给特定 Telegram topic 配 system prompt,把这个话题定义成 expense intake channel。之后它就稳定很多:收到图片就按票据处理,而不是把它当成普通聊天图片。
这也是 skill/system prompt 的价值:它不是让模型“更聪明”,而是让模型在正确的场景里做正确的事。
酒店 folio:最难处理的一类
酒店材料是我花时间最多的地方之一。
因为酒店报销往往不只是一个发票。可能需要:
- invoice;
- folio / 水单;
- check-in / check-out 日期;
- 房费、税费、服务费拆分;
- 有时还要和 trip 的住宿日期对齐。
而且酒店 folio 格式极其不统一。英文里可能叫 check-in date / arrival / stay period,中文里可能是入住日期/离店日期,香港或台湾酒店还可能出现繁体中文。很多 PDF 文本层也不干净,普通 parser 抽不到关键字段。
最后我用了 Gemini 多模态来解析这类材料,把它作为酒店 folio 的 fallback。确定性 parser 能处理的先处理,处理不了的再交给多模态。这样既保留稳定性,也能覆盖真实世界里那些乱七八糟的扫描件。
Baiwang 发票:一次很典型的 reverse engineering
最让我印象深的是 Baiwang 的发票。
它不是直接在邮件里给一个附件,而是给一个链接。点进去以后是一个 Vue 页面,要等前端渲染完,再点击下载 PDF。
如果用 browser automation,当然也能做:打开网页、等待、点击下载。但这太脆弱了。页面慢一点、按钮变一下、登录态失效,都可能失败。
后来我让 Claude Code 去做 reverse engineering。它直接读前端 JS 的逻辑,找到下载链接是怎么生成的,最后把这个过程固化成 script 放进 skill 里。这样就不需要真的打开浏览器点页面了,而是直接根据链接生成下载地址,拿到 PDF/XML。
这个例子很能代表 agent 的新工作方式:不是“让 AI 代替人点网页”,而是让它理解网页背后的机制,把一次浏览器操作升级成一个稳定脚本。
去重:一张票不一定只有一个来源
去重也是后面才意识到的大坑。
同一笔费用可能有多个来源:
- Gmail 里有一封邮件;
- 邮件里有 PDF、XML、OFD 多个附件;
- Telegram 里我又转发了一遍;
- 酒店 invoice 和 folio 分两封邮件;
- repair 时又从 Gmail 重新扫出来一次。
如果简单用文件 hash 去重,很快就不够了。因为不同附件可能代表同一笔费用;反过来,同一封邮件也可能包含多笔费用。
后来 evidence DB 里区分了两个 identity:
- source identity:这条证据从哪里来,比如 Gmail message id、Telegram message id、某个文件;
- expense identity:它代表现实世界里的哪一笔费用,比如日期、城市、金额、商户、票据类型组合出来的 key。
source identity 用来避免重复处理同一个输入,expense identity 用来合并现实世界里的同一笔费用。对于 key 不够强的情况,先进入 pending 状态,后续 parser 或人工确认后再升级。
这一步让系统从“文件整理器”变成了“证据系统”。claim.csv 不再是事实来源,而只是最终导出的一个视图。
第三关:Export,不只是打包,而是审计
Export 相比 intake 看起来简单:用户说“帮我导出某次 trip”,系统生成 claim.csv,把相关 evidence 打包成 zip,再发回 Telegram。
但这里也踩过很多坑。
早期依赖 JSON trip plan 时,导出经常遇到一些奇怪问题:字段缺失、历史 trip 查不到、future trip 和 historical trip 混淆、某天的一日往返被多日 trip 覆盖。后来通过 SQLite trip store + query script,导出逻辑稳定了很多。
现在的流程大概是:
- 用户在 Telegram 里说要导出某个日期或某次出差;
- OpenClaw 调用 trip 查询脚本解析目标 trip;
- 从 evidence DB 找到匹配的证据;
- 生成
claim.csv; - 把发票、行程单、folio、receipt 等 evidence 放进 zip;
- 运行 audit,检查缺失、弱 key、日期不匹配、城市不匹配;
- 把 zip 发回 Telegram。
后来还加了汇率字段。因为不同国家/地区的报销最终要折算成人民币,不能随便让模型编一个汇率。我接入了付费 API,按指定 expense date 拉汇率,写进导出结果里。
这里的关键不是“自动生成 CSV”,而是生成一个可审计的交付物。如果某条证据只有 weak key,audit 会标出来;如果 hotel folio 和 invoice 没配齐,也会标出来。这样我可以信任它的结果,而不是拿到一个漂亮但不知道对不对的表格。
第四关:Submit,最后一公里永远最脏
最后一步是提交到内部报销系统。
这部分因为涉及公司内部系统,就不展开具体细节了。大概思路是:OpenClaw capture request,让大模型分析浏览器里的请求结构,再把可自动化的步骤写成 skill。
这里我用了 Qoder work / browser automation 一类方式去探索内部系统。很多录入和校验其实可以直接调用页面背后的 API 完成,比 UI 自动化稳定得多。
但文件上传没有完全搞定。模型一直判断是被内部安全系统拦截,直接 API 上传很难稳定复现浏览器里的完整状态。最后还是需要回到 UI automation:打开页面、选择文件、上传、检查状态。
这也很真实。agent 做企业系统自动化,不是“有 API 就万事大吉”。它必须能在 API、browser、文件系统、人工确认之间切换。能 API 就 API,不能 API 就像人一样操作浏览器。
我真正学到的东西
做完这一轮,我对 agent 的理解变化挺大。
1. 未来系统一定会 API 化和 CLI 化
这次最明显的感受是:凡是有 API、有 CLI、有结构化数据的地方,agent 就能稳定工作;凡是只有复杂 UI、没有稳定接口的地方,就很痛苦。
这个趋势我觉得不可逆。未来如果一个系统完全不考虑 agent 访问方式,它就会越来越难被自动化,也越来越难进入新的工作流。
当然 API 不一定是免费的。相反,我觉得高质量 API 会成为新的内容和能力变现方式。比如这次 trip planning、地点解析、汇率查询,我都愿意调用付费 API,因为它们提供的是稳定、准确、可验证的能力。
2. Skill 的本质是 SOP,不是 prompt 魔法
这次也让我更确信:skill 不是“写一段很长的 prompt”。
真正可靠的 skill,背后一定有成熟 SOP:收到什么输入,先检查什么,调用哪个脚本,状态写到哪里,失败怎么标记,什么时候需要人工确认。
如果人类自己都没有稳定流程,直接把一堆混乱经验塞给模型,只会得到一个看起来很聪明但不可靠的系统。
不过 skill 的测试依然是难题。代码可以写 unit test,parser 可以做 fixture,但一个完整 skill 涉及模型行为、工具调用、外部系统、聊天上下文,怎么系统性测试还没有特别成熟的答案。
3. 代码没有消失,但人和代码的关系变了
这个项目里已经有 200 多个 commit、4 万多行内容。前期我还会认真看每个 diff,后来实在看不过来了。
但这不代表代码不重要。恰恰相反,代码更重要了:确定性逻辑必须靠代码固化,状态必须靠数据库保存,审计必须靠脚本检查。
变化在于,人不再需要逐行控制所有代码。我的注意力更多放在:
- 目标是否定义清楚;
- skill 的边界是否正确;
- 状态模型是否可靠;
- audit 能不能发现问题;
- 最终产物是否能被人类复核。
这有点像从“亲手拧每颗螺丝”变成“设计流水线和质检标准”。
4. 个人助理最有价值的不是聪明,而是可靠
报销这个场景一点也不炫酷。它没有科幻感,也不会让人惊呼 AGI。
但它很真实。
它需要 agent 长期记住我的日历、理解我的出差、接住我随手发的票据、从邮箱里找附件、知道哪些证据合格、导出时能审计、出错时能留下线索。
这才是我心目中个人助理应该做的事。
不是每次都完美回答一个问题,而是长期维护一条工作流。
小结
一开始我以为这是一个“AI 帮我报销”的小项目。后来发现,它其实是一个很完整的 agent workflow 实验:
- Trip Planner 解决上下文;
- Claim Intake 解决证据收集;
- Evidence DB 解决状态和去重;
- Export/Audit 解决可交付和可信任;
- Submit 解决最后一公里。
中间踩了很多坑:日历事件不标准、JSON 状态不可靠、Gmail query 污染、12306 退票误判、酒店 folio 多语言解析、Baiwang Vue 页面下载、Telegram topic 语境不稳定、dedupe key 设计、历史 trip 导出、汇率查询、内部系统上传。
但正是这些坑,让它从一个 demo 变成了一个真实可用的系统。
我现在越来越觉得,agent 真正改变的不是“写代码更快”或者“回答问题更聪明”,而是它让个人也可以拥有一套会持续演进的自动化工作流。
以前这种东西需要一个团队做系统集成。现在,一个人、一台机器、一些 skills、一堆 scripts,再加上足够多的迭代,就能把生活和工作里那些麻烦的小流程一点点交给 agent。
这可能才是我最兴奋的地方。