这是以下挑战的参赛作品:Hacktoberfest 周末挑战:为朋友而构建

我构建了什么

我的朋友自己修理他的 BMW E90 325i。不是专业地修,也不完全算是爱好,只是那种宁愿自己弄懂车,也不愿交给一个不会解释清楚的人去修的人会做的事。

看他干活,真正的瓶颈从来不是扳手。而是信息太分散了。一个扭矩规格藏在 2013 年的论坛帖子里。一个零件号在另一个标签页里。一半的“指南”是一个视频,里面的人先讲了四分钟才碰车。他躺在用千斤顶支架撑起的车底下,双手脏兮兮的,还要在手机屏幕上捏合缩放。

所以我给他做了BMW Repair Workshop:一个针对他那款车的快速、移动优先的参考工具。二十个维修流程,七份参考指南,每一个扭矩值和零件号都列在表格里,而不是埋在文字里。还有一个 Ask 框,他可以用自己的话描述问题,然后得到只基于那些流程构建的答案。

有意思的部分不是这个网站。而是我被禁止构建的东西。

塑造一切的约束

我最初的计划很明显:把维修手册喂进 RAG 流水线,让一个智能体基于它回答问题。

那个计划大约一小时后就被否决了。真正的 BMW 维修文档(Bentley 的手册、BMW 自己的 TIS 系统)是商业的、受版权保护的,而且是卖钱的。它还被到处镜像,按章节拆分的副本一搜就有,还有整个网站专门免费提供它。

广泛可得不等于许可。所以我一点都没用。没用那些 PDF,没用那些镜像,也没从里面“只提取事实”,那只是换了个帽子的同一回事。

这留给我一个更难的问题,而事实证明,也是一个更好的项目。

内容实际来自哪里:

  • 流程和规格是根据一般公开的汽车知识编写的,另有少量数据对照公开发布的来源(车主论坛、零售商安装指南)进行了交叉核对
  • 每一条目都在 Source 字段中说明自己的出处,并在页面上可见
  • 每个扭矩数值和零件编号都带有明确的“请对照 RealOEM 或你的经销商核实”的提示,因为我不是机械师,这些内容来自通用知识,而非他那辆车的文档

最后这一点比听起来更重要。一份悄悄把未经验证的数值当作事实呈现的维修参考,比没有参考更糟,因为有人会照着它去拧螺栓。

演示

在线: https://georgievalex.github.io/bmw-repair-assistant/

试着在 Ask 框里输入一些含糊的话,就像你平时实际会说的那样:

  • “我的冷却液一直在减少,但地上没有水渍”
  • “过颠簸路面时前面有咔哒声,我应该先检查什么”
  • “我该怎么重建自动变速箱?”← 看它如何拒绝

代码

https://github.com/GeorgievAlex/bmw-repair-assistant

我是如何构建它的

核心是开源 AI: Gemma(gemma-4-31B-it),Google 的开源权重模型,通过DigitalOcean Serverless Inference提供服务。

DigitalOcean 在这里负责实际的模型服务。我向inference.do-ai.run/v1/chat/completions发送一个 OpenAI 兼容的聊天补全请求,用 DigitalOcean 模型访问密钥进行身份验证,Gemma 在他们的基础设施上运行。无需配置 GPU,无需托管模型,无需保持容器热启动。支出上限是他们预付的 Inference & Agents 余额,且自动充值已关闭,这意味着失控的最坏情况是损失有上限、Ask 框陷入沉默,而不是收到一张意外的账单。

这种组合——由一个我未被锁定的提供商服务的开源权重模型——用一句话概括了下面大部分关于“开放创新”的论点。

架构,刻意无聊:

browser → GitHub Pages (static site, no backend)
            ↓ POST { question }
          Cloudflare Worker  ← holds the model access key
            ↓
          DigitalOcean Serverless Inference → Gemma

该网站是 100% 静态的。内容以 Markdown 形式存放在仓库中,一个无依赖的 Node 脚本将其编译成搜索索引,并为每个流程生成一个静态 HTML 页面。搜索在客户端运行。没有数据库,没有框架,没有构建工具链。

我最满意的设计决策:没有 RAG 流水线。

我开始构建一个,然后测量了语料库。整个知识库大约 11,000 个 token。这能放进单个提示词里还绰绰有余。

所以每个问题都会发送全部二十七条条目直接交给模型。没有嵌入,没有向量存储,没有分块,也没有任何可能悄悄取错分块并据此自信作答的检索步骤。模型每次都能看到完整语料库。

这样构建更快,避免了一整类 bug,而且在这个规模下比基于分块的检索更准确。每个问题约花费 $0.002。伸手去拿向量数据库的冲动很强烈,而先做测量让我免于这么做。

保持诚实。系统提示词在这里发挥了真正的作用:

  • 只根据提供的条目作答
  • 绝不编造扭矩值、零件编号或步骤
  • 把每个条目中的验证注意事项带入答案
  • 保留安全警告(弹簧压缩器、高温冷却系统、在行驶高度下拧紧衬套),而不是为了简洁而删减它们
  • 说明答案来自哪个流程

当被要求重建自动变速箱时,它会说自己没有这样的流程,并指向最相关的条目。这种拒绝是整个项目中最重要的一项行为。

为什么开放创新很重要?

四个理由,按我实际相信的程度递增排列。

它很便宜。每个问题 $0.002。整个系统运行在免费静态托管上,前面有一个免费 worker,背后是一个开源权重模型。闭源前沿模型也能用,但对于一项并不需要它的任务来说成本更高。

模型选择是一次替换,而不是一次迁移。Gemma 只是一行配置。如果有更好的模型发布,或者 Gemma 在其他地方变得更便宜,我改一个字符串就行。项目里其他任何部分都不知道、也不关心是哪个模型在作答。

它是可检查的。当模型做出奇怪的事情时,我可以阅读产生它的完整输入:一个公开仓库中的 27 个 Markdown 文件,以及一个 160 行 worker 中的系统提示词。没有隐藏的检索步骤来决定模型看到什么。对于会告诉人们把刹车卡钳拧多紧的东西,我希望能够解释它给出的任何答案。

而真正推动这个项目的理由是:开放的方式是我唯一能诚实构建的方式。

这里的闭源路径并不是专有模型。而是专有的手册,以及围绕它的未授权镜像生态。那些镜像在你不需要它们之前一直能用。它们会消失,会被下架,来源不明,而且其中没有任何内容能告诉你某个数字从何而来。

我转而构建的东西很小,坦诚面对自身的局限,并且完全可审查。每一条都说明它来自哪里。每个数字都提示你去核实。模型只能依据存放在公开仓库中、并标明来源的内容来发言。

这比 BMW 真正的维修手册要差。但它比盗版副本要好得多,因为你能确切看到自己在信任什么。

我一路犯的错

我猜了一个模型 ID。 gemma-4看起来挺合理。真正的那个是gemma-4-31B-it,而错误的 ID 只会以一个光秃秃的404从推理 API 里失败,没有任何东西提示你“检查你的配置”。二十分钟没了。

我在答案渲染器里写进了一个 XSS 漏洞。我的转义辅助函数用了textContent→innerHTML这个技巧,它会转义<,>,&但不引号字符,因为引号在文本节点里并不特殊。然后我把那个“已转义”的输出直接塞进了一个alt="..."属性里。一个乱入的"出现在说明文字里,哪怕是我自己的笔误,都会突破这个属性。后来用正确的属性上下文转义修好了。

我跟朋友说 CORS 会保护 API 预算。它不会。CORS 由浏览器强制执行,所以它能阻止另一个网站从页面 JavaScript 调用这个 worker,但对curl完全不起作用。速率限制和设有上限的预付费余额才是真正的控制手段。

我发布的参考页面有整整一天把## Heading渲染成了字面文本,因为我最初的参考文档是纯散文,从未走过标题那条路径。

我没做的事

还没有 VIN 解码,所以网站无法把规格缩小到确切的车型配置,这在跨越多个发动机变体的底盘上很重要。没有实际零件的照片,因为我找到的唯一一张授权正确的照片是一张公有领域的发动机照片,而在 BMW 刹车页面上放一张别人的刹车卡钳的通用图库照片,正是这个项目本该避免的那种小不诚实。没有针对第二个底盘做测试,不过数据结构已经按条目携带底盘和发动机代码,所以添加一个只是内容工作,不是重写。

还有最重要的:那个网站上的每一个技术数据都需要一双技工的眼睛来审视。来源说明里写得很清楚。这就引出了我唯一没法自己完成的部分。

交给他

他在修车厂时我发给了他,附了四个问题。最重要的是第一个:这上面有什么地方实际上是错的吗?

他过了一遍自己亲手做过的活儿,没有任何内容与他所知道的相矛盾。我想把这一点是什么、不是什么说清楚:这是一位在职技工读了它,没有发现任何不对劲的地方,这是真实的信号。它不是对照文档逐行审核。每个数字上的那些提醒说明原封不动地留在那里。

关于 AI,他比我预想的更有分寸,这种分寸我很喜欢:

“它给出的答案是有依据的,当然你还是得核实,比如诊断之类的”

他没被告知就自己得出了这个项目本身的立场。对于这样一个工具来说,这差不多是最好的结果了:它有用,而且没有让他变得轻信。

然后他纠正了一个我整个输入设计都建立在它之上的假设。我一直在考虑语音输入,理由是脏手和手机屏幕合不来。他并不想要:

“我总是更喜欢文字,因为长答案很快就会被忘掉,而且我干活时戴着手套,摘下来问一下不是问题”

两件我没考虑过的事。手套反正要摘。更有意思的是:语音给出的答案会蒸发,而文字会留在屏幕上,当你手忙脚乱地躺在车底下时。这种留存本身就是功能。我本来会做出错误的东西。

关于外观,我原本担心太朴素:

“我喜欢这个想法,它看起来简单、老派——没有奇怪的图片、广告之类的东西”

还有一个真正的功能请求,他提出来之后又自己把它说没了:

“也许它缺了像视频教程本身这样的东西,但我想这不可能,比如去 YouTube 上搜有没有人对我的车/发动机做过这个维修”

这是可能的,只是不是他以为的那种方式。我没法托管或嵌入视频。但我可以交出一个已经缩小到他确切的底盘和发动机的搜索,这样他就能得到BMW E90 N52B25 brake pads and rotors change而不是针对另一辆车的通用结果。现在每个流程都有一个“看它完成”的链接来实现这一点。它在我写完这篇文章之前就已经上线了。

他还要求了排气和变速箱的流程。排气是一个简单的车道作业,已经添加了。变速箱我部分拒绝了:卡片涵盖了油液和油底壳保养,然后明确说明内部大修是专业工作台作业,没有诚实的车道版本。根据一般知识编写大修流程,而在这个作业中错误的间隙意味着变速箱报废,这正是整个项目旨在避免的失败模式。说“这不是我应该写的东西”与模型拒绝同样的问题一样,都是同一纪律的一部分。

我没预料到的那句话:

“你是我首选的 AI 专家,而且你自己也是个好机械师,所以我很希望你能在未来继续做这个,这样我、你和其他车迷都能用”

我不是一个好机械师。我只是写下了我能验证的东西,并对不能验证的东西保持谨慎。但“这样我、你和其他车迷都能用”比我笔记中的任何内容都更好地描述了这个项目为什么值得继续。

他的反馈实际改变了什么

所有这些现在都在网站上上线了,是在他的消息和这篇文章发布之间上线的:

他说 改变了什么
想找到这个作业的视频,针对他的发动机,以为不可能 每个流程都有一个“看它完成”的链接,一个预先缩小范围的搜索BMW E90 N52B25 <job>
要求一个排气流程 已添加,完整作业
要求“拆解变速箱” 添加了油液和油底壳保养;内部大修明确拒绝,作为专业工作台作业
更喜欢文字而不是语音,并给出了理由 语音输入从路线图中完全移除

最后一行是我自己会搞错的。我正要构建的功能正是他积极不想要的功能,而我通过更努力地思考永远不会发现这一点。

他还有一个请求,我无法实现:

“如果网站能为我处理那些维修和改动就好了,但 AI 还没到那一步 :D”

两点都说对了。而且我觉得那个笑话很好地概括了这件事的定位。这里 AI 有用的版本不是那个替你干活的版本。而是那个告诉他卡钳导向螺栓是 30 Nm、指出它从哪条流程得到的这个数值、提醒他去核实,然后在他做自己已经会做的工作时让到一边的版本。

奖项类别

DigitalOcean 最佳使用奖—— DigitalOcean Serverless Inference 为 Ask 功能背后的开源权重模型提供服务,通过inference.do-ai.run,支出由预付费的 Inference & Agents 余额封顶。

Gemma 最佳使用奖——gemma-4-31B-it是负责推理的开源权重模型,被限制在网站自身的内容范围内,并被指示在不确定时拒绝回答而非编造。

致谢

该项目没有任何依赖,代码全部手写,内容也是为本项目撰写的,而非取自他处。不过有两点还是值得说明:

  • 网站导航中的 GitHub 标志是 GitHub 的 logo,仅用于链接到代码仓库。
  • 这篇博文和该项目均在 AI 协助下完成,已在 front matter 中声明。网站中的规格参数来自通用知识,每一条都在页面上说明你应该自行核实。