用 Claude Code 构建咖啡日志平台

我特别喜欢精品咖啡。2025年7月开始,我用 Roastguide 记录自己的咖啡习惯,这个 iOS 应用设计得很棒,适合我这种咖啡爱好者。它很好地追踪了冲煮、烘焙商和咖啡袋,但时间长了,我希望数据能放在自己控制的系统里。我想能查询、备份数据,还能调整一些流程,让它更贴合我的冲煮和消费习惯。
虽然我以前对上一代编程工具持怀疑态度,但最近的发展让人难以忽视,我想找个借口从头构建一个实质性项目来积累经验。
于是我搭建了 b{rew}log,一个自托管的咖啡日志平台。它能追踪烘焙商、烘焙批次、咖啡袋、冲煮记录、设备和咖啡馆访问。项目已上线 coffee.jnsgr.uk,欢迎来逛逛,看看我对滤泡咖啡的痴迷有多深!
这是我用 Claude Code 或任何智能体编程工具构建的最复杂项目。技术选型、架构和视觉设计都是我负责的,但代码几乎都不是我亲手写的。
这篇文章会介绍 Brewlog 的核心功能、应用设计思路,最后分享一些关于智能体编程的观察和技巧。
核心功能
Brewlog 追踪烘焙商、烘焙批次、咖啡袋、冲煮记录、设备和咖啡馆访问。下图是它的首页,汇总了最近几次冲煮、当前打开的咖啡袋及剩余量,还有一些基本统计数据。登录后,页面会提供快速控件,方便重复冲煮或冲煮特定咖啡:
冲煮记录是我自己制作的咖啡。每次冲煮都关联到一个特定的咖啡袋,并记录配方(粉量、水量、时间、温度)、使用的设备和品尝笔记。时间久了,这会积累起我的冲煮历史、冲煮方法以及我那些积灰的冲煮设备的使用频率。下图展示了特定冲煮记录的详情页:
此外,brewlog 还记录我去过的咖啡馆和“杯测”记录,后者指我享用过但并非自己冲煮的咖啡。
最有趣的部分是丰富的统计页面,它展示了一张交互式地图,显示我的咖啡产地、烘焙地和消费地,还有常见的风味笔记和冲煮时间。
基于 LLM 的咖啡袋扫描
Roastguide 有个不错的功能:拍一张咖啡袋照片,它就能获取详细信息。据我了解,他们的实现依赖于维护者管理的烘焙商和咖啡数据库。我的应用无法访问那个数据库,我也不想花太多时间构建数据管道来存储所有可能咖啡/烘焙商的大量数据。
在 brewlog 中,我用 LLM 提取实现了这个功能:拍张照片,发送到 OpenRouter 托管的多模态模型。提示词指示模型从图像中提取文本,执行网络搜索,并返回一个包含咖啡或烘焙商详细信息的 JSON 对象。OpenRouter 让我能轻松切换模型,无需担心 API 差异。
实际使用中,效果出奇地好。大多数精品咖啡袋上都印满了有用信息,视觉模型很擅长读取。主要失败模式是混淆网络上的烘焙名称,偶尔会编造品尝笔记,但流程中包含一个审核步骤,修正这些错误成本很低。
每次扫描的成本几乎可以忽略不计。我一直在用 google/gemini-2.5-flash 处理 LLM 提取功能,每次扫描成本大约 0.01 到 0.02 美元。
设计决策
后端
Brewlog 的后端用 Rust 和 Axum 构建。我学习 Rust 有一段时间了,想通过一个更实质性的项目进一步深入。模板用 Askama 处理,它的编译时模板与 Claude 配合得很好,因为模板错误会作为 Rust 编译器错误出现,智能体可以自动发现并修复。
数据库是 SQLite。单文件,备份方便,移动简单。对于这种单用户应用,SQLite 绰绰有余,省去了单独数据库服务器的麻烦。迁移文件嵌入在二进制文件中,应用启动时自动运行。
前端
我希望应用有现代感,但大部分渲染放在服务端。我不想提供一个庞大的客户端 JavaScript 单页应用。我一直对 HTMX 这类技术很好奇,去年一篇关于用 Eta、HTMX 和 Lit 构建应用的文章里的观察让我深有同感。
最近我发现了 Datastar,它的目标与 HTMX 类似,但更新,模板更简洁清晰。HTMX 交换 HTML 片段,而 Datastar 增加了响应式信号系统,可以修补来自服务端的 HTML 和 JSON 数据。
这对 Claude Code 是个考验,因为 Datastar 太新了,可能没出现在模型的训练数据中,但它阅读和消化文档的能力相当惊人。
虽然智能体偶尔会退回到原生的 fetch 调用或手动 JavaScript DOM 操作,但我把这看作是我指令的漏洞,不是模型的问题。每次发生这种情况,我就更新 CLAUDE.md,强化 Datastar 模式,防止智能体再犯同样的错误。
单用户,仅支持通行密钥
Brewlog 特意设计为单用户。我很乐意让别人自己托管实例,但没兴趣提供更广泛的服务。代码结构使得改成多用户并不复杂,但我不确定自己会不会这么做。
可能有点不寻常的是,不支持用户名和密码认证。认证只支持通过 WebAuthn 使用通行密钥,或者用 API 令牌。
通行密钥能防钓鱼,从学术和实际角度都更难被暴力破解,这为我的服务消除了一整类滥用风险。
全面的 CLI 客户端
在 Canonical 工作时,我从 @niemeyer 那里学到的一点是:确保这类应用添加新 API 端点时,同时增加对应的 CLI 命令或标志。
他还向我展示了发布单个二进制文件的威力,这个文件既是服务器,也是自身的客户端。brewlog 二进制文件既运行托管 API 和 Web UI 的服务器,也作为应用的一流 API 客户端。
如果你在构建 API 驱动的应用,我强烈推荐这种模式。它保持了较小的表面积,为你(和智能体……)提供了一种一流的、可脚本化的方式来调用 API 并排查数据转换中的问题。
领域驱动设计
代码库遵循领域驱动设计方法,分为四层:领域层(纯业务逻辑,无外部依赖)、基础设施层(数据库、HTTP 客户端、第三方 API)、应用层(HTTP 服务器、路由、中间件、服务)和表示层(CLI 命令和 Web 视图模型):
┌─────────────────────────────────────────────────────────────┐
│ Presentation │
│ ┌─────────────────────────┐ ┌──────────────────────────┐ │
│ │ CLI │ │ Web │ │
│ │ roasters, roasts, bags │ │ views, templates │ │
│ │ brews, cups, gear ... │ │ roasters, roasts, bags │ │
│ └─────────────────────────┘ └──────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Application │
│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
│ │ Routes │ │ Services │ │ Server / State │ │
│ │ /api/* │ │ BagService │ │ Axum router │ │
│ │ /app/* │ │ BrewService │ │ AppState (DI) │ │
│ │ │ │ RoastService .. │ │ │ │
│ └──────────────┘ └──────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Domain (pure Rust — no framework dependencies) │
│ ┌──────────────────────┐ ┌─────────────────────────────┐ │
│ │ Entities & Values │ │ Repository Traits │ │
│ │ coffee/ roasters, │ │ trait RoasterRepository │ │
│ │ roasts, bags, │ │ trait RoastRepository │ │
│ │ brews, cups, gear │ │ trait BagRepository ... │ │
│ │ auth/ users, │ │ │ │
│ │ sessions, tokens │ │ Errors, IDs, Listing, │ │
│ │ analytics/ │ │ Formatting │ │
│ └──────────────────────┘ └─────────────────────────────┘ │
├──────────────────────────────────┬──────────────────────────┤
│ Infrastructure │ │
│ ┌────────────────────────────┐ │ ┌───────────────────┐ │
│ │ Repositories (SQLite) │ │ │ External Clients │ │
│ │ SqlRoasterRepository │ │ │ OpenRouter (AI) │ │
│ │ SqlRoastRepository │ │ │ Foursquare │ │
│ │ SqlBagRepository ... │ │ │ WebAuthn │ │
│ │ (implement domain traits) │ │ │ Backup │ │
│ └────────────────────────────┘ │ └───────────────────┘ │
└──────────────────────────────────┴──────────────────────────┘
在这个模型中,依赖关系向内流动。领域层不知道 Axum、SQLite 或任何其他实现细节。仓库特征在领域层定义,在基础设施层实现。这为未来提供了灵活性,比如我想从 SQLite 迁移到 PostgreSQL,或者改变存储图像的方式等。
它也简化了测试。领域驱动设计方法鼓励松耦合和依赖注入等实践,这通常能带来更简单的集成测试。例如,我可以让智能体针对纯领域逻辑编写聚焦的测试,而无需引入 Axum 或 SQLite。
智能体编程模式
这个项目的一大动机是学习新技术和技能,它确实做到了,但也强化了我一直在用的一些模式。
将预提交钩子作为契约
我从来不太喜欢预提交钩子。在项目中使用格式化工具和 linter 我一直很自律,但不喜欢把它们设置成自动运行。开始用智能体后,这改变了。
我把 pre‑commit 当作我和智能体之间的契约。我指示智能体在返回给我输入或宣布成功之前,总是先修复 linting/testing 失败。具体来说,我一直在用 prek,它是 pre-commit 的 Rust 重写版。
为智能体设置更严格的 lint 规则
用智能体时,我也更倾向于设置更严格、更挑剔的 lint 规则,来强制执行一些属性,比如函数或文件的最大行数。我自己写代码时会本能地做这些改动。手动编辑时,函数变得臃肿是很明显的,但我很早就注意到,智能体似乎比我更能接受 100 多行的函数。
如果你在意这些约束(函数长度等),就把它们编码成 lint 规则,而不是指望智能体内化你的偏好。
顺便提一句,我以前从没用过 flake-parts,但它能在 Nix 开发环境中自动配置预提交钩子和 treefmt 这类格式化工具,真的很方便。
指令自更新
之前没意识到这些工具写自己的指令有多厉害。看到别人仓库里那些又长又复杂的 AGENTS.md 或 CLAUDE.md 文件,我还纳闷过——后来才明白,当你和智能体一起解决问题,或者纠正它犯的错误之后,可以立刻让它把刚才发生的事情总结到 CLAUDE.md 里,这样下次就不会再犯了。
计划模式
刚开始我大量使用“计划模式”。这个模式的设定是:智能体可以探索代码库、在线阅读文档、和你一起规划新功能,但它不能修改代码或你的系统。
用起来效果不错,但有几次它好像陷入了“死循环”,试图在复杂的失败场景中规划出路。
有一次特别夸张,循环持续了一个多小时。后来我切换到“智能体模式”,给了它一个后续提示,让它直接用 kubectl 检查 Kubernetes 上运行的应用,结果它只用了大概 5 分钟就诊断并解决了同样的问题。问题在于,“计划模式”下某些“工具”是被明确禁止使用的,但根据你要实现的目标,这有时会成为很大的阻碍。
现在我更喜欢另一种方式:保持在“智能体模式”,然后提示智能体说我想在共享文档(比如 ./plans/new-feature.md)里制定计划,并且在我允许之前它只能编辑那个文件。接着我和智能体进入循环:它写计划,我在文件里写行内评论,它根据评论调整——如此反复直到我满意。在让智能体实施计划之前,我会开启一个新会话,让它带着全新的上下文去执行。目前为止,这个方法效果很好。
工作树与并行智能体
开始处理更复杂的功能后,我很快对运行并行智能体产生了兴趣。有时候一个智能体处理一个成熟的任务要花很长时间,而这段时间我本可以用来处理路线图上的其他事项。
最初我尝试让它们在同一个分支上并发运行,结果可想而知——它们互相冲突,把代码搞得一团糟。我一直是 git 工作树 的粉丝,而在这里它简直是完美匹配:让我能在代码库的不同实例上运行并发的智能体。
即使用了工作树,我仍然倾向于处理正交的关注点,这样就不会产生大的合并冲突。但能同时处理前端功能、后端功能和 CI 流水线,这种体验确实是个不错的升级。
视觉提示
处理前端组件时,我建立了一个很顺手的工作流:先给 UI 截图,用 Flameshot 做标记,然后把图片粘贴回 Claude Code 并附上下一步提示。
我发现这是驱动 Web 组件迭代的有效方式。感觉更像是在和真人结对设计:我圈出间距问题或没对齐的元素,智能体就会根据它看到的内容提出 CSS 调整建议。
总结
构建 Brewlog 的过程让我很兴奋,而且我已经每天用它大概一个月了。这和我过去做项目的方式不同,既让人感到解放,又同样令人不安。
虽然这个项目里有些代码可能没有我平时那么注重细节,但如果没有智能体,我可能永远都不会动手去建它。
这个过程教会我的东西让我惊讶。每天用 Claude Code 捣鼓 Brewlog,我都能学到新东西——无论是关于 Tailwind CSS 的技巧,还是某种架构模式。
我觉得从这些工具获得有用输出的关键在于:你仍然是驾驶员。你仍然要对交付的东西负责,这意味着要对生成的代码进行适当的审查。
Brewlog 已经上线 coffee.jnsgr.uk,并且舒适地运行在 fly.io 的免费实例上。
如果你喜欢精品咖啡,并且想拥有自己的数据,不妨试试看,然后告诉我你的体验。
最后,我要感谢 Roastguide 的作者们,感谢他们在应用上做的出色工作,感谢他们在 Discord 社区投入的精力,也感谢他们愿意给我一份数据转储,让我能用来填充 Brewlog 的首次部署!
觉得有用?分享给更多人



