摘要:这篇文章讲的是,为什么我会想到给自己的 Hexo 个人网站加一个 AI 助手,以及这个想法怎样一步步变成真正可用、可控、可维护的站内系统。它记录了从 v1.0 到 v1.1 的完整过程:哪些决策是对的,哪些判断是错的,哪些"测试全绿"背后藏着坑,以及最终我学到的几条工程经验。

这不是一篇教程。它是一份工程复盘。

写它的目的,是回答一个问题:"一个想法,是怎样变成可运行的系统的?"中间经历了哪些错误判断、哪些被测试掩盖的问题、哪些工具链冲突,最后 v1.1 交付了什么、还留下了什么。

如果你正在给个人项目加 AI 功能,或者在做 Cloudflare Serverless 开发,这里的踩坑经历可能比最终架构图更有用。


1. 为什么要给个人网站加 AI 助手

问题的起点

这个想法其实不是从"内容太多、搜索不够用"开始的。

我一直在断断续续地完善这个个人网站。有一次调整页面布局时,我注意到右上角还有一个位置一直没有真正派上用场——它空在那里,只是作为界面的一部分存在,却没有承载任何有意义的功能。

我最初的想法很朴素:给这个位置放上一个真正有用的东西,而不是为了填满界面随便塞一个装饰。

于是我开始认真想:这里放什么,才既符合我的想法,也符合这个网站本身的目的?

我的网站主要用来记录科研、机器人、控制、编程和项目实践。它的定位,是一个技术人记录和分享自己工作的地方。那么,放在这个网站上的功能,最自然的角色,应该是"帮助访客理解和查询本站的内容"。

带着这个方向,我在和 ChatGPT 讨论的过程中,逐渐形成了一个具体的想法:做一个"站内 AI 小助手"。

它既能利用起这个原本没有充分发挥作用的位置,也正好契合一个技术网站"帮助访客理解本站内容"的定位。

确定了方向之后,我才开始认真思考它具体应该做成什么样子。而这一步,才是后面所有设计的真正起点。

一开始的设想

最初我对这个"AI 助手"的设想其实很朴素:在网站角落放一个对话面板,访客问什么,它就回答什么。

但很快我发现,如果真这么做,它就是一个"什么都能聊"的通用聊天机器人——这会带来三个我无法接受的问题:

  1. 它会瞎编:问网站没有的内容,它也会用模型自身的知识"自信地"编一个答案。
  2. 无法验证:它说的话没有来源,我不知道该不该信。
  3. 费用不可控:无限制地调用 API,钱花起来没有边界。

所以"AI 助手"这个想法,从一开始就必须收敛成一个更具体的东西。

一句话定义

这不是一个通用聊天机器人。这是一个知识边界封闭的检索增强(RAG)助手

  • 只能回答网站已公开的内容;
  • 所有回答附带可验证的站内来源;
  • API 费用受严格预算控制。

这三条,构成了后面所有设计的锚点。

为什么是 RAG + LLM,而不是让模型自由回答

这是最核心的决策,值得单独说清楚。

LLM 的知识是"训练时刻的、泛化的",它不知道我网站上有哪几篇文章、每篇讲了什么。如果我让模型自由回答"这篇文章讲了什么",它只能靠猜。

RAG(检索增强生成)的做法是:先把网站内容建成可检索的知识库,用户提问时先检索出相关片段,再把这些片段连同问题一起交给模型,让它"基于给定材料"回答。

这样模型就从"凭记忆瞎答"变成了"照着材料答"。知识范围被锁死在网站内容上,回答可以被追溯,超出范围时也能诚实地回答"本站没有相关信息"。

这是整个系统成立的前提。 后面遇到的很多 bug,本质上都是这个前提在不同环节被破坏的表现。


2. 整体架构:每个组件承担什么职责

在深入细节之前,先交代这个系统的组成。

组件 职责
Hexo 静态站 内容的源头(Markdown 文章)
知识库构建脚本 把 Markdown 解析、切块、索引成 knowledge-base.json
前端 AI 面板 用户提问的入口,展示回答与来源卡片
Cloudflare Pages Functions 后端入口,接收 POST /api/assistant,编排整条链路
BM25 检索 在知识库里找出与问题相关的片段
DeepSeek API 基于检索片段生成最终回答
Cloudflare Cache API 缓存命中结果,减少重复调用
Cloudflare D1 Serverless SQLite,做每日预算的原子控制

一条请求的完整链路大致是:

浏览器 AI 面板(question + 当前页面 URL + 对话历史)
  → POST /api/assistant
  → 请求校验 / URL 规范化
  → 注入检测
  → 确定性直返(metadata / site overview / contact)★ 命中则直接返回
  → 缓存检查
  → BM25 站内检索
  → Prompt 构建(注入检索片段 + 当前页面信息)
  → D1 预算预留
  → DeepSeek 调用
  → D1 结算
  → 来源回查(模型只给编号,服务端回查真实 URL)
  → 缓存写入
  → 响应过滤后返回

几个贯穿全文的设计原则,先放在这里:

  1. 确定性任务不经过 LLM:元数据、站点概览、联系方式这类"答案是已知事实"的查询,用代码直接返回,又快又准又不花钱。
  2. 模型永远不直接输出 URL:模型只返回编号,服务端回查知识库得到真实来源,防止模型编造链接。
  3. 缓存键覆盖所有影响回答的维度:一旦改了检索/Prompt 行为,必须让旧缓存失效。
  4. 预算状态机从第一天就正确:每次 API 调用都花钱,费用控制不是附加功能。

3. 知识库与检索:为什么先选 BM25

为什么不用向量检索

这是被问得最多的设计问题。答案是:当前规模不需要。

现在知识库只有几篇文章 + 一个关于页面,构建出几十个语义块、几十 KB 纯文本。在这个规模下:

  • BM25 全文检索的准确率足够好;
  • 向量检索需要额外的 embedding 调用(花钱)或本地模型(复杂度);
  • 混合检索(BM25 粗排 + 精排)等文章数量上去了再加也来得及。

选择原则:用最简单的方案解决当前的问题。BM25 不是最终方案,但它是当前正确的方案。

中文分词:Bigram 的取舍

BM25 假设输入是空格分隔的词,但中文没有天然空格。最简单的方案是 bigram(连续两个字一组):

"并联机构运动学"["并联", "联机", "机构", "构运", "运动", "动学"]

Bigram 不完美(“联机”、"构运"不是真实词),但它有两个巨大优势:零依赖(不需要在 Workers 环境加载结巴分词),倾向于提高召回率——宁可先多召回一些候选,再交给后续评分和排序机制筛选。

构建流程:Markdown → 可检索索引

build-knowledge-base.js 做的事,核心是三点:

  1. 语义切块:按 H2/H3 标题边界切分,相邻短段落合并,超长段落在句子边界拆分。每个 chunk 记录所属章节、位置、是否为摘要。
  2. 稳定 IDdocument_id 由文档 URL 哈希生成。只要 URL 不变,跨构建的 ID 就相同——这对缓存至关重要(ID 变了,所有缓存回答都会失效)。
  3. 草稿排除draft: true 的文章不进知识库。这是"知识边界封闭"的一部分。

新文章发出去,构建时自动入库,AI 就能检索到——不需要任何手动操作。


4. 检索评分:五个实际 Bug 和它们的教训

这一节是我踩过的最密集的坑。五个 bug 都指向同一个主题:评分模型是一个需要反复校准的跷跷板,你永远不知道下一个问题会从哪头翘起来。

Bug 1:元数据评分污染了段落相关性。 初版把文档 metadata(标题、标签)和 chunk 内容放在一起打分。结果搜"运动学",第一名是一篇标签含"运动学"但正文几乎不相关的文章——因为标签 +10 分,正文 bigram 重叠才 +1 分。修复:拆成两个独立评分函数,文档级和段落级分开算。

Bug 2:单字 Bigram 假阳性。 搜"机构",召回一堆含"结构"、"机关"的文章,仅仅因为共享一个字。修复:bigram 重叠数设最低阈值,单 bigram 重叠降级计分。

Bug 3:列表查询时同一篇文章占满 Top-K。 搜"有哪些机器人文章",返回的 5 个 chunk 有 3 个来自同一篇。修复:对"有哪些文章"这类列表意图做文档级去重(document_id 去重,每篇文章只展示一次);而普通内容问答仍允许同一篇文章提供多个相关 chunk 作为上下文证据。

Bug 4:"这篇文章讲了什么"把最短段落当摘要。 用户问摘要,检索返回了"致谢"段落(最短的)。修复:引入 chunk 优先级,摘要类查询按 abstract > conclusion > paragraph > code 排序。

Bug 5:中文 URL 编码不匹配。 浏览器传来的 pathname 是百分号编码的,知识库存的是未编码中文,两者对不上,导致"当前页面"识别失败。修复:统一 decodeURI 后再规范化。

这五个 bug 的共同教训是:检索质量不是一个算法问题,而是一堆边界条件的叠加。 每一个单独看都很小,但合起来就是"AI 助手看起来不聪明"的直接原因。


5. 当前页面感知:让 AI 知道你在看哪篇

用户在文章页打开面板问"这篇文章讲了什么",AI 应该知道"这篇文章"是哪篇,而不是全站乱猜。

实现链路是:前端传 page_context.url → 规范化 → 查知识库得到 {url, title, date} → 注入 Prompt。同时检索也给"当前页面的 chunk"加权,让结果更偏向用户正在看的文章。

这个设计本身是对的,但它是后面一系列问题的起点——它让"当前页面"这个上下文贯穿了检索、Prompt、缓存多个环节,任何一环没接好,"这篇文章"就会答错。


6. 元数据直返:不是所有查询都需要 AI

Preview 验证时发现一个问题:问"这篇文章的标题是什么",DeepSeek 返回的是文章摘要,而不是标题。

根因

LLM 的指令遵循是概率性的,不是确定性的。我在 Prompt 里写了"元数据查询应直接返回字段",但模型在正文和标题语义相关时,倾向于综合所有上下文生成回答,而不是简单地提取标题。

Prompt 只能提高概率,不能保证行为。

为什么选择 bypass 而不是继续改进 Prompt

维度 改进 Prompt 代码直返
确定性 不可保证 正则可预测、可验证
性能 需要检索和模型调用 无需模型调用
成本 消耗 token 零 token
正确性 可能偏离 服务端已知的确定事实

所以我在 assistant-core 里加了一层:命中"标题/URL/发布日期"这类元数据意图时,直接从 currentPageInfo 取字段返回,完全绕过 LLM。

这条经验贯穿了后面整个 v1.1:确定性任务,用代码;只有真正的语义理解,才交给 LLM。


7. 后端设计:预算状态机、Provider 抽象、可信链

Provider 抽象

后端把"调用 AI 模型"抽象成标准接口。开发阶段用 Mock Provider(不需要 API Key 就能跑通整个后端),生产切换 DeepSeek Provider,其他代码零改动。

Mock 有个安全限制:只在 development 环境可用。当时我自认为"生产环境即使误配 mock 也会拒绝启动"——这个假设后来在 v1.1 被推翻了(见第 10 节)。

预算状态机

用 AI API 最大的风险是费用失控。所以每个 API 调用都要经历完整生命周期:

reserved → dispatched → settled / rejected / unknown / cancelled

关键设计是"已发送"和"是否可能计费"必须分开:Provider 调用之后的任何错误(网络超时、上游 5xx)都可能导致实际计费。如果不在调用前标记 dispatched,就无法区分"还能回收的预留"和"已经可能扣费的调用"。

来源可信链

这是防止模型"编造 URL"最可靠的方式:模型回答时只引用编号 [1][2],服务端用编号回查知识库,取出真实的 title/url。模型生成的任何不在知识库里的引用都会被丢弃。

模型永远无法直接决定返回给客户端的 URL。这个设计实现起来不复杂(几十行),但它是安全边界里非常重要的一环。

注入防护

用户输入是不可信数据,防护的思路是分层:检测明显的恶意意图、转义用户输入里的系统标记、响应只保留白名单字段。

这里我刻意不展开具体的检测规则和阈值——它们属于"防御细节",公开写出来只会帮助绕过。要讲的是一条原则:把用户内容当作不可信数据,在多个边界上做防御,而不是指望一层就能拦住。


8. "测试全绿"不代表系统可用

这是整篇文章最重要的部分之一。

某个阶段,测试报告显示所有模块测试全部 PASS。一切看起来很好。但实际主调用链中,markDispatched 根本没被调用。

五类假象

假象 1:未被调用的代码。 模块测试验证了 Provider 能调用、D1 能读写,但没人验证"主流程按正确顺序调用了所有步骤"。markDispatched 方法加进 D1 模块后,旧的 core 没同步更新——模块接口升级了,组合层没跟上。

假象 2:Mock 和生产 SQL 不一致。 D1 测试里的 Mock 用子串匹配生产 SQL,没命中也不报错,只是落进默认分支返回一个假结果。测试框架不知道 Mock 没命中,它看到了 PASS。

假象 3:无条件真断言。 有些"测试"名称描述得很好,但断言只是 true。它们没有验证任何状态变化。

假象 4:集成测试加载阶段就崩了。 某集成测试文件解析阶段就 SyntaxError,0 项测试实际运行,但阶段报告还在引用它的"预期覆盖"。

假象 5:没有统一入口。 每个测试文件能独立跑,但没有一条命令能验证"整个后端是否就绪"。

经验

模块测试验证"每个零件是否正常";集成测试验证"零件拼起来是否按正确顺序工作";组合测试验证"生产环境的装配方式是否正确"。三者缺一不可。模块测试全绿是最容易获得的信号,也是最容易产生虚假安全感的信号。

后来我建立了 npm run test:4b 统一命令:任何子测试失败,总退出码非零。这成为每次修改后的硬门槛。


9. v1.0 上线了,但它真的"能用"吗

v1.0 按时上线,测试全绿,架构文档很漂亮。但真实访客用起来,问题一个接一个冒出来。

有段时间,我陷入了"打补丁"的循环:每个新反馈都单独修一下,按下葫芦浮起瓢。后来我停下来,决定从前端 → API → Router → RAG → Prompt → DeepSeek → Sources → Cache,完整审查一遍真实运行链路,而不是继续针对单个 bug 打补丁。

这次审查揭开了几个我一直没有正视的真相。


10. 真相一:我以为接通了 DeepSeek,其实是 mock

现象

线上回答很"刻板",很多问题的答案几乎固定。我拿线上回答和 mock-provider.js 里的预设字符串一比对——逐字相同。

原来,线上根本没在调用 DeepSeek。它一直用的是 mock。

定位过程

我没有因为"代码看起来会调用 DeepSeek"就相信它。而是做了几件事:

  1. 线上 curl 真实的 /api/assistant,拿到回答;
  2. 把回答和 mock 预设字符串逐字对比;
  3. 检查 Production 环境的 AI_PROVIDER 配置。

结果:最后发现 Production 的 Provider 与运行环境配置都没有按预期生效,因此实际走进了 mock 分支。

根因与教训

这里有两层问题:

第一层,我的代码假设错了。 我在 Provider 抽象里设计了"mock 只在 development 可用",本意是防止误用。但这条防线依赖运行环境配置,而错误的环境配置恰好让这道保护失去了作用。

第二层,Preview 和 Production 是两套配置。 我在 Preview 环境验证过的东西,并不自动等于 Production 也正确。两个环境的变量、绑定、Secret 都要分别确认。

这件事的直接教训是:“代码看起来会调用 X” 和 “X 真的被调用了” 是两回事。 必须用日志、Mock 对照、真实响应这类可验证的证据,确认实际运行链路。这也是我后来在结构化日志里加 provider_type 字段的原因——让 mock 和 deepseek 的成功能一眼区分。


11. 真相二:token 匹配与自然语言之间的鸿沟

v1.1 里有两个问题,本质是同一个根因,值得放在一起讲。

现象

  • 问"这个网站主要有哪些内容?",返回的是某篇具体文章的摘要,而不是网站整体介绍;
  • 问"网站作者联系方式是什么?“,返回"没有找到相关信息”。

而事实上,"关于我"页面里清清楚楚写着作者的联系方式。

定位

这两个问题,我都是先在本地用 search 函数复现,看 BM25 到底召回了什么。

结果发现:知识库里确实有联系方式(在 About 页),也确实有"网站是做什么的"这类信息(散落在 About 页和各文章里)。但问题在于——

用户问题的用词,和知识库正文的用词,在 token 层面没有重合。

问"作者联系方式",用的是"作者"、“联系方式”;而 About 页写的是"我是谁"、“联系我”、“GitHub”、“邮箱”。两边唯一的交集是"联系"这一个 bigram,低于检索的通过阈值,于是 BM25 召回为空。

根因:token 匹配 ≠ 语义理解

这是 BM25 这类关键词检索的根本局限:它匹配的是"字面",不是"意思"。 "联系方式"和"联系我"在语义上是同一件事,但在 bigram 层面几乎不重叠。

修复:确定性直返,而不是放宽阈值

这里有个容易踩错的坑:有人会想"那我把检索阈值调低一点,让它能召回 About 页不就行了?"

不行。放宽全局阈值会让更多不相关问题也被召回,破坏已有的检索质量。

正确的做法是:这类"答案是已知稳定事实"的查询,根本不走检索,用代码直接返回。

于是 v1.1 增加了两个确定性路由:

  • site-router:识别"站点概览 / 最近文章 / 文章列表"三类意图,从知识库文章元数据(标题/日期/分类)直接生成答案和来源;
  • contact-router:识别"作者联系方式"意图,从 About 页提取 GitHub 和邮箱,直接返回。

它们都在检索之前拦截,命中就返回,既不经过 BM25,也不经过 LLM,更不花钱。

经验

确定性任务不应该强行交给 RAG/LLM。元数据、站点概览、联系方式——这些"答案是已知事实"的查询,用代码直接返回,又快又准又零成本。把 LLM 留给真正需要语义理解和内容综合的问题。

这一条,和第 6 节的"元数据直返"是同一条原则在不同场景的延伸。到 v1.1 结束时,这条"确定性直返"的路径已经覆盖了:文章元数据、站点概览、作者联系方式三类。


12. 真相三:本地修好了,线上还是错

现象

多轮对话里,第一问"本文研究的是什么机构?“,第二问"它有几个自由度?”。第二问返回"没有找到相关信息"。

一个典型的多轮检索难题

“它有几个自由度"里的"它"是代词,本身没有任何内容信号。conversation 只注入了 Prompt,没参与检索。所以检索层看到的就只是一个孤零零的"它有几个自由度”,"自由度"这个词的信号又很弱,于是召回为空。

初步的修复方向是:对"有当前页面上下文但不是显式’这篇文章’"的查询,放宽当前页 chunk 的通过门槛,让弱信号也能召回当前文章。

这个修复在本地测试通过了。然后我把代码推到 Preview——结果线上还是 no_results。

真正的根因:stale cache

我一开始以为是检索阈值没改对,反复调。后来才意识到问题在别处:

我改了 search.js 的检索行为,但没有让旧缓存失效。

缓存键里有一个手动版本维度,本意是"改了回答行为就手动 +1 让旧缓存失效"。但这次我忘了改。于是 Cloudflare Cache 里还躺着旧的 no_results 结果,用户问"它有几个自由度"这个精确字符串时,直接命中旧缓存,返回了旧的 no_results。

证据是:换一个等价问法(“它具备几个自由度”)就正常返回了正确答案,只有精确字符串命中 stale 缓存。

经验

修改检索/Prompt 行为后,一定要让旧缓存失效。否则"本地已修、线上仍错",而且线上错得让人以为代码没生效。

这条经验的更广含义是:一个系统里影响"最终回答"的维度,都必须显式地参与缓存失效。 漏掉任何一个,就会出现"改了这个没改那个"的隐性不一致。这也是为什么这个系统的缓存键有十几个维度的原因——每增加一个影响回答的变量,就要在缓存键里补上它。


13. 真相四:渲染是另一座山

前面几个问题都是"回答内容不对"。但还有一类问题:回答内容对了,却显示成一堆符号。

现象

技术问题会触发带公式的回答。DeepSeek 返回的是 Markdown + LaTeX 源码,但前端把 **加粗**\(...\)\[...\] 这些原样显示了出来。用户看到的是原始语法,而不是渲染效果。

为什么不能直接 innerHTML

这里有一个安全上的关键约束:模型输出是不可信数据,绝不能直接塞进 innerHTML

如果我把模型返回的字符串直接 element.innerHTML = answer,那么模型输出的任何 HTML/脚本都会被浏览器执行——这就是一个经典的 XSS 漏洞。模型可能因为用户输入的诱导,输出一段 <script><img onerror=...>,然后被注入页面。

所以必须用"白名单式"的渲染:只解析我允许的 Markdown 结构(粗体、斜体、代码、列表、标题、链接),其余一律按纯文本处理。

零依赖的安全 Markdown 渲染器

我没有引入第三方 Markdown 库,而是手写了一个轻量渲染器:

  1. 先把 Markdown 解析成结构化的 AST(纯函数,可单测);
  2. 再用 createElement + createTextNode 把 AST 变成 DOM 节点。

全程不碰 innerHTML。这样,<script> 这类内容永远只会变成文本节点,而不会变成可执行的元素。链接也做了白名单校验:只允许站内相对路径和 http(s),拒绝 javascript:data: 等危险协议。

LaTeX:懒加载 MathJax

公式渲染用 MathJax(站内博客本身就用它,视觉统一)。为了避免给所有页面都加载这个不小的库,前端做成懒加载:只有回答里真的出现了公式,才异步加载 MathJax。

然后我踩了一个更隐蔽的坑:MathJax 的竞态

Markdown 渲染器写好了,测试也过了,Preview 验证却发现:第一条公式回答里的公式,永远显示成源码,但后来某条公式又能正常排版。

这个"忽好忽坏"的现象让我怀疑是异步竞态。最后用一个无头浏览器加载真实 JS、记录每一步的时序,抓到了根因:

MathJax 的 typesetPromise 不是脚本 onload 时就可用的。 脚本加载完成、onload 触发的那一刻,window.MathJax.typesetPromise 还是 undefined——它要等 MathJax 内部异步的 startup.promise 完成之后才被定义。

而我的代码里有一个"如果 typesetPromise 存在才排版"的守卫。于是在 onload 里直接排版时,守卫为假,第一条公式就永远没被排版。后面的消息因为 MathJax 已经就绪,反而能正常排版。

修复是:startup.promise 完成之后再排版,并且对单个气泡元素排版,而不是反复排版同一个持久容器。

经验

涉及异步初始化的第三方库,onload ≠ “ready”。要等它的初始化 Promise 完成,才能安全地调用它的 API。这类竞态往往"有时好有时坏",极难靠肉眼定位,必须靠带时序的运行时日志。

同时,安全渲染这件事说明:"把内容显示出来"本身也是一个安全边界。 渲染器必须和"信任模型输出"划清界限。


14. v1.0 → v1.1:一段演化的完整回顾

回头看,v1.0 和 v1.1 解决的是两个不同层次的问题。

v1.0 解决的是"有没有":把一条端到端的链路搭起来——知识库、检索、Prompt、Provider、预算、缓存、注入防护、来源可信链。它是骨架,是地基。

v1.1 解决的是"对不对"和"好不好"

  • 接通了真实 DeepSeek(而不是 mock)——“真的能用”;
  • 用确定性直返补上了 BM25 的 token 匹配鸿沟(site overview、contact)——“答得对”;
  • 修了 stale cache 导致的多轮失败——“改动能生效”;
  • 加上了 Markdown/LaTeX 渲染——“显示得好”。

这一轮里最核心的方法论转变,是从"针对单个 bug 打补丁",变成"从前端到后端完整审查真实链路,找到根因再改"。

目前还没解决 / 以后可能优化的

诚实地说,这个系统还远非完美。以下是我清楚知道、但暂时选择不做的:

  • 越界问题可能误召回:问本站不存在的内容,有时会因为某个标签命中而召回一篇不太相关的文章,靠 Prompt 兜底让模型"没信息就说没信息"。检索层没有进一步收紧,因为收紧可能破坏现有的标签查询。这是一个需要在"召回率"和"准确率"之间再权衡的问题。
  • 异常恢复能力仍可增强:后续可以进一步改善上游瞬时异常时的用户体验。
  • 公开接口防护仍可持续增强:随着访问量增长,可以继续完善自动化访问控制与滥用防护。
  • 搜索是 O(n) 线性扫描:当前几篇文章无所谓,文章量大了需要倒排索引或向量检索。
  • 可观测性仍可增强:后续可以补充更完整的运行指标和趋势分析。
  • 较宽的块级公式可能横向滚动:响应式 UI 的小瑕疵。

这些都是"以后可以做",但都不影响当前系统的核心可用性。


15. 写在最后:几条最重要的工程经验

整个项目做下来,代码量不算大(后端十几个文件,千余行核心代码),但边界条件很多。真正花时间的,往往不是写功能,而是处理这些边界。

挑几条我认为最有价值的经验:

1. 先做端到端骨架。 哪怕第一版只返回硬编码的回答,先把整条链路跑通,再逐个模块替换成真实实现。这比把所有模块开发完再拼装安全得多——你能在每一步都验证"替换这个模块后,链路是否还完整"。

2. 预算和计费放在第一位。 每次 API 调用都花钱。状态机要从第一天就正确,不能先用 mock 攒代码、后接真实预算——那样会出现大量假设不一致。

3. 测试要有分层可信度。 真实环境测试 > 组合集成测试 > 单模块 Mock 测试。不能因为 Mock 测试全绿就认为系统可用。当某一层测试不可用时,必须明确标注"这一步还没验证"。

4. 确定性任务不要交给 LLM。 元数据、站点概览、联系方式——这些"答案是已知事实"的操作,用代码完成,又快又准又零成本。不是所有查询都需要 AI。

5. 代码"看起来会调用 X"不等于 X 真的被调用了。 必须用可验证的证据(日志、Mock 对照、真实响应)确认实际链路。Preview 和 Production 是两套配置,要分别验证。

6. 修改影响"最终回答"的任何环节,都要让旧缓存失效。 否则就是"本地已修、线上仍错",而且线上错得极具迷惑性。

7. 模型输出是不可信数据,渲染和安全要一起考虑。 不能因为"要支持 Markdown/公式"就放弃 innerHTML 的安全边界。安全渲染和功能支持不是二选一。

8. 每个修改都要经过 Preview → Production 验收。 单元测试通过只是第一道门槛,真实环境的行为可能完全不同(配置差异、缓存、异步竞态都是单测覆盖不到的)。

给个人网站加 AI 这件事,最终交付的不是一个"聪明的聊天机器人",而是一个知识边界封闭、来源可验证、费用可控、每一步都可追溯的检索系统。它不夸大 AI 的能力,恰恰相反,整个设计都在给 AI 的不可靠性兜底——用检索锁定知识范围,用代码兜住确定性,用预算兜住成本,用来源链兜住编造。

这可能才是"给网站加 AI"真正值得记录的地方。


本文基于 v1.0-ai-assistant 与 v1.1-ai-assistant 的实际项目代码和过程记录撰写。文中涉及的 API Key、内部标识、预算阈值及其他敏感配置均已泛化处理。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐