给个人博客接入 AI,最直接的方式是检索几段文章,再交给大模型回答。但真正使用后,会发现“能够回答”和“回答得可靠”之间还有不少距离。

例如,问“最新的一篇文章是什么”,不应该收到十篇文章的分页目录;问“有哪些 Kafka 相关文章”,不应该看到一堆只是偶然提到 Kafka 的文章;接着问“讲一下这篇文章”,助手也不应该突然忘记刚刚推荐了什么。

Neko 是我博客里的 AI 助手。本文基于 2026 年 9 月的代码实现,介绍它如何把问题理解、资料查询、回答生成和文章上下文拆开处理。它是受控的问答流程,而不是能够自主修改或发布博客的通用 Agent。

一、先理解问题,再决定怎么查

早期遇到的问题,并不全是模型能力不足。有些错误来自程序把不同任务塞进了同一条检索链路。

“最新的一篇文章是什么”需要排序;“Kafka 为什么快”需要解释;“有哪些 Kafka 相关文章”需要推荐。这三件事不应该用同一种方式完成。

当前链路先让模型输出结构化查询计划,由程序校验并执行,而不是直接让模型自由回答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户问题 + 最近历史 + 当前页面 + 上轮文章关联
↓
请求校验、人机验证与额度检查
↓
读取线上公开索引,核对文章关联
↓
模型理解意图,程序校验
↓
┌─────────────────┼─────────────────┐
↓ ↓ ↓
目录与统计 推荐相关文章 内容问答
程序精确查询 检索后再筛选 取资料后生成
└─────────────────┼─────────────────┘
↓
返回答案、引用与文章关联

模型负责理解自然语言,程序负责限制执行范围和保证查询规则。比如“最新一篇”对应按发布日期排序并取一篇;只有完整目录等任务才进入分页逻辑。

当前支持的主要分支如下:

问题类型 示例 处理方式
最新文章 最新的一篇是什么? 从公开目录排序后直接回答
目录和统计 列出所有 Go 文章 按明确条件筛选、计数或分页
文章推荐 有哪些 Kafka 相关文章? 混合检索后筛选相关候选
文章追问 讲一下这篇文章 定位文章,只读取该文章片段
普通知识问答 Kafka 为什么吞吐量高? 检索资料,按需联网,再生成解释
需要澄清 多篇候选时只说“这篇” 请用户指定对象

这里保留了一个重要区别:明确的主题目录和计数仍可能采用关键词筛选,并不等于语义分类统计。系统不能拿“检索到几篇”冒充“全站总共几篇”。

二、检索只是找候选,不是最终推荐

普通问答和相关文章推荐使用关键词与向量混合检索。

关键词检索适合精确术语,比如 Kafka、KRaft;向量检索适合表达不同但含义接近的问题,比如“消息积压怎么办”和“消费者处理能力不足”。两者互补,而不是简单地用向量取代关键词。

1
2
3
4
5
6
7
8
9
10
11
12
13
经过上下文改写的独立问题
↓
关键词召回候选
+
bge-m3 生成查询向量
↓
Vectorize 返回语义候选
↓
与当前公开索引重新核对
↓
RRF 融合排名并限制单篇占比
↓
交给后续推荐筛选或回答生成

当前实现使用 bge-m3 生成 1024 维向量,语义检索最多取 20 条匹配。两路结果通过 RRF 融合,最终保留最多 6 个片段,每篇文章最多占 2 个。

RRF 根据候选在各路结果中的排名累加分数,避免直接混合量纲不同的关键词分数和向量相似度。同一片段在两路都排名靠前,就更容易进入最终结果。这里的“双路”是逻辑上的两种召回方式,不意味着它们在代码中并发执行。

向量结果也不是可以直接使用的正文。程序会将匹配 ID 与当前公开索引重新对应,无法对应的旧片段不会进入回答。向量服务异常或没有有效结果时,系统退回关键词检索。

为什么还需要推荐筛选

语义相近,不代表文章真正讲了用户关心的内容。一篇 Agent 文章可能提到消息队列,但未必值得推荐给正在寻找 Kafka 资料的人。

因此,推荐分支会额外调用模型,从候选中选择真正相关的文章,并提供推荐理由与原文依据。程序检查候选编号、重复项、返回数量,以及依据是否确实出现在候选标题或正文中。

这能约束凭空生成来源,但不能证明模型对相关性的判断永远正确。原文包含一句话,也不等于整篇文章都围绕这个主题展开。

最终只有筛选通过的文章会展示给用户,并进入下一轮文章关联。筛选失败时不会把未经确认的候选直接当作推荐;推荐成功时也只承诺“本轮找到的相关笔记”,不声称已经列全。

三、“这篇文章”为什么需要独立上下文

仅仅把聊天记录发给模型,并不能稳定解决文章指代。

假设上一轮回答推荐了一篇 Kafka 文章,又顺带说明其他三篇候选不相关。如果程序把所有引用都当作下一轮候选,“讲一下这篇文章”就会变成一道四选一的问题。

Neko 将自然语言历史和结构化文章关联分开传递。

1
2
3
4
5
6
7
8
9
10
你:有哪些 Kafka 相关文章?
→ 混合检索得到候选
→ 筛选后只推荐一篇
→ 返回这篇文章的路径关联

你:讲一下这篇文章
→ 携带最近历史和文章路径关联
→ 后端在当前公开索引中核实路径
→ 唯一候选成为追问对象
→ 重新取该文章片段,生成解释

客户端携带的文章路径只是定位线索,不是可信正文,也不能绕过公开索引。上一轮回答同样只用于理解对话,不能代替文章内容。

单篇文章分支默认包含文章开头,再从该文章内部选择相关片段,总计最多 6 个。因此这仍是基于片段的解释,不保证每次都读取全文。文章较长、片段不足时,应明确说明限制。

当前上下文还有几个边界:

  • 后端最多接收最近 6 条消息,是消息条数而不是对话轮数。
  • 上一轮确实有多篇候选,而用户未指定对象时,仍需要澄清。
  • 文章已经退出公开索引时,不用旧回答或当前页面悄悄替代它。
  • 新问题明显切换主题时,不强行绑定上一轮文章。
  • 前端成功完成回答后才更新历史和关联;回答被截断时清空新文章关联。

这是一种短期会话状态,不是跨会话的永久记忆。普通知识回答目前也不会自动把所有参考文章变成追问对象。

四、哪些回答需要模型,哪些不需要

并不是每个最终答案都由模型自由生成。

最新文章、目录和统计由程序根据公开索引组织答案;推荐结果由程序根据筛选后的条目生成列表。只有文章解释和普通问答等分支,才进入自由文本生成。

这样做既减少调用,也让排序、数量、引用编号和展示结果更一致。

场景 通常的聊天模型调用次数
最新文章、目录、统计 1 次:理解意图
推荐相关文章 2 次:理解意图、筛选候选
单篇文章解释 2 次:理解意图、生成解释
普通知识问答 2~3 次:理解意图、可选资料充分性判断、生成回答

这些数字不包含向量化调用、重试或特殊分支。没有候选时,推荐筛选也可以直接返回空结果。

普通问答会根据联网设置、时效需求和资料充分性决定是否搜索网络。网络搜索由 Tavily 提供,博客来源和网络来源分别编号,避免把外部资料包装成博客作者的观点。

推荐和单篇文章解释默认不联网,以免悄悄扩大任务范围;用户允许并强制联网时,才走相应补充分支。目录直答不会因此变成网络搜索。

流式返回的不只有正文

Worker 使用 SSE 返回不同类型的事件:

事件 作用
status 展示正在理解、检索或整理等阶段
sources 返回参考来源及检索状态
context 返回下一轮可用的文章关联
delta 返回正文内容
done 标记结束及是否达到长度上限
error 明确报告失败

模型生成的解释可以逐段显示;程序直接生成的目录或推荐列表也使用同一协议,但不一定逐字输出。前端只有收到完成标记,才将本轮视为成功结束。

五、失败处理与知识更新

服务异常不等于用户没问清楚

“需要澄清”和“服务失败”必须分开。

多篇文章无法确定指代,是需要澄清;意图模型超时、返回非法结构,则是系统异常。当前意图解析对部分可重试错误自动重试一次,每次默认最多 8 秒;仍失败就报告服务异常,不偷偷退回猜测性筛选。

请求入口还包含来源校验、输入限制、可配置的人机验证,以及按 IP 和全站统计的额度保护。后端记录模型调用、Token、检索和完成状态,帮助区分问题发生在哪个阶段。

这些约束并不保证系统永远不会误判,但能让失败更可解释,而不是靠一句模糊回复掩盖错误。

发布文章和同步向量是两个步骤

知识更新不发生在每次聊天过程中。

1
2
3
4
5
6
7
8
9
新增或修改文章
↓
本地构建,生成公开索引
↓
发布静态站点,让线上索引更新
↓
同步同一构建版本的文章向量
↓
后续对话使用更新后的目录与语义检索

这里有一个容易混淆的细节:聊天时 Worker 读取线上公开索引;向量同步脚本默认读取本地构建生成的公开索引。发布与同步需要使用一致的文章版本,而不是假定同步脚本会自动抓取线上最新文章。

仅发布而未同步时,新文章仍可能被目录查询和关键词检索找到,但语义检索可能漏掉它。

当前同步逻辑基于片段内容生成 ID,只为新增或变化的片段生成向量。旧向量默认保留,可以在确认发布后显式清理;聊天检索仍会过滤无法对应当前公开索引的旧结果。Vectorize 是最终一致的,提交同步不代表所有查询立即可见,线上索引缓存也可能带来短暂延迟。

同步令牌只属于本地运维配置,不应进入文章、前端脚本或版本库。博客助手在聊天时不需要读取本地同步令牌。

六、这套设计解决了什么,又没解决什么

这次改造的重点,不是增加一个更复杂的提示词,而是把职责边界划清:模型理解意图,程序执行确定性查询;检索提供候选,筛选决定推荐;历史帮助定位,当前正文提供事实依据。

它改善了关键词误匹配、推荐混入无关文章、文章追问失焦和服务错误表达不清等问题,但仍有现实限制:最多 6 个片段可能漏掉内容,意图与相关性判断仍可能出错,短期历史不是长期记忆,向量更新也存在同步延迟。

如果继续迭代,可以考虑扩大候选池并增加更细的重排序,为长文章设计分层摘要,并建立覆盖“最新文章、主题推荐、连续追问、检索失败”的固定评测集。这些是后续方向,不是当前已经具备的能力。

对个人博客来说,可靠的 AI 助手不一定需要自主行动。更重要的是:知道什么时候应该查目录,什么时候应该读文章,什么时候可以补充资料,以及什么时候应该承认还不能确定。