← 返回博客
架构2026-08-25 20:30:237 分钟 · 2,152 2

从 RAG 到 Agentic RAG:给博客 AI 一双自己查资料的手

博客 AI 助手的第二次进化:把固定检索流水线升级为 ReAct 工具编排循环。五个只读工具的提示词工程、伪流式与 tool_calls 的工程取舍、防死循环保险丝,以及为什么用 200 行 Go 手写而不上 LangChain。

上一篇《给博客装上一个「了解我」的 AI》发出后,助手稳定运行了几天——直到我自己问出两个把它问哑的问题:

  • 「博主最近写的文章主要在讲什么?」——它支支吾吾拼凑不出完整答案;
  • 「这个博客一共多少篇文章?运营多久了?」——它老实回答"暂无相关内容"。

这两个失败很有意思,因为它们不是实现 bug,而是架构天花板:Top-K 召回的结构性盲区。这篇文章记录我把它从"单程检索流水线"升级为"Agentic RAG 工具编排"的全过程——以及为什么整个过程只用了 200 行 Go,没碰任何框架。

一、RAG 的天花板:两类结构上答不了的问题

传统 RAG 的链路是死的:问题 → 向量化 → 余弦 Top-K → 拼 prompt → 生成。这条流水线有两类问题天生处理不了:

多跳复合题。「最近写的文章主要讲什么」需要:先拿到最新文章列表 → 再逐篇了解内容 → 再综合概括。这是三步推理,而 Top-K 召回只有一步——你没法保证"最新文章"恰好落在与问题余弦最相似的五块里(事实上大概率不在)。

实时统计题。「一共多少篇文章、运营多久了」的答案根本不在任何文章内容里——它存在于数据库的 COUNT(*) 里。知识库检索在原理上就够不着。

共同根因:决策权在代码手里。检索什么、检索几次、要不要换个方式找,全是硬编码的;模型只是流水线末端的内容填充工。

二、心智模型转换:从流水线到授权

Agentic RAG 的核心是一次决策权移交:

RAG:     问题 → [代码写死:检索→拼接] → LLM 填空
Agent:   问题 → LLM 看问题 + 工具清单 → 自己决定调什么工具
              → 代码执行 → 结果喂回 → LLM 再决定下一步
              → …直到它认为可以作答

这就是 ReAct 循环(Reason + Act):模型每轮输出两种东西之一——要么「我要调用某工具」(附参数),要么「最终答案」。代码退化为两件事:维护工具箱执行模型的指令

关键认知:工具不是功能插件,而是模型的感官和手脚。它看不到你的数据库,search_articles 就是它的眼睛。

三、工具箱设计:description 就是提示词工程

给了模型五个只读工具:

semantic_search(query)      // 向量语义检索——概念性、含义性问题
search_articles(keyword)    // MySQL ngram 全文精确搜索——专有名词/报错信息
get_article(id)             // 读指定文章全文
latest_articles(n)          // 最新文章列表
site_stats()                // 文章总数/总字数/运营天数

这里有个比写代码更重要的环节:每个工具的 description 是写给模型看的提示词,措辞直接决定路由准确率。我的版本里刻意写了对比引导:

"description": "语义检索博客知识库。适合含义性/概念性的问题…"
"description": "按关键词全文精确搜索博客文章。适合查找专有名词、报错信息、技术术语…"

实测效果出乎意料地好:问 DYNAMIC_SERVER_USAGE 报错怎么解决 时,模型自主选择了 search_articles 而不是语义检索——正是 description 里"报错信息、技术术语"的引导。而问概念性问题时它又回到语义路。工具路由不需要代码 if-else,description 本身就是软规则。

两条设计原则贯穿始终:

  1. 全部只读。不给任何写入能力,模型幻觉或被注入的最坏后果是多查几次库。
  2. 粒度互补而非重叠。语义/关键词/详情/列表/统计各管一个正交维度,避免模型在两个等价工具间摇摆。

四、三个工程决策

决策一:循环内非流式 + 回答伪流式

Agent 的中间轮次需要解析 tool_calls。技术上 GLM 支持流式返回 tool_calls——但 delta 分片要按 index 逐块聚合 arguments 字符串,边界情况多、调试痛苦。我的取舍:循环内全部非流式调用(拿完整 JSON,简单可靠),最终答案拿到后切成小块按 SSE 协议推给前端——打字机观感不变,前端协议零改动。

代价是首字延迟略增(等整段生成完才开始推),换来的是后端复杂度砍半。对一个个人博客的问答助手,这是正确的交换。

决策二:末轮禁用工具——防死循环的保险丝

理论上模型可能无限调工具(每次结果都让它想再查点什么)。防线很便宜:

for round := 0; round < agentMaxRounds; round++ {
    useTools := round < agentMaxRounds-1 // 最后一轮禁用工具,强制作答
    content, toolCalls, err := glmChat(msgs, useTools)
    ...
}

第 6 轮时请求里干脆不带 tools 参数——模型物理上调不了工具,只能基于已有信息收卷。比"prompt 里求它别再调了"可靠得多:约束要做在机制里,不要做在恳求里

决策三:sources 确定性生成原则的延伸

上一篇文章说过,参考来源链接必须由后端从数据库主键确定性生成,不能让 LLM 编 URL。Agent 形态下没有单一的"检索步骤"了,怎么溯源?答案是换收集口径:凡是工具返回中出现过的文章,一律登记为候选来源(去重、按触碰顺序)。get_article(147) 触碰了 147,它就出现在参考区。

配合上一篇的拒答联动(正文含"暂无相关内容"则来源置空),Agent 形态下依然成立——实测无关问题拒答时来源为零条。

五、五场景实测矩阵

问题类型模型的自主决策结果
多跳复合:"最近写的文章讲什么"latest_articles → 综合概括准确列出 5 篇主题 ✓
实时统计:"多少篇文章运营多久"site_stats"77 篇、3703 天" ✓
精确词:DYNAMIC_SERVER_USAGE自选 search_articles命中目标文章 ✓
无关问题:火锅店推荐先查证 → 拒答"暂无相关内容"+零来源 ✓
同题二次缓存命中 8ms ✓

诚实的账本:单轮 RAG 回答约 2 秒,Agent 多跳场景 5-15 秒——能力换延迟是真实存在的交易。缓解手段是缓存(相同问题 8ms)和工具粒度设计(让模型尽量一轮搞定)。日配额熔断改按用户请求计数(内部循环不重复计数),账单风险仍然数学封顶于零。

六、什么时候不该上 Agent

保持上一篇文章的坐标系诚实:如果你的问答域是封闭且高频固定的(客服 FAQ、文档检索),纯 RAG 流水线更快更稳,加 Agent 是负优化。Agent 的价值区间是问题分布不可预设的场景——访客会问什么你猜不到,那就把"怎么找"也交给现场决策。

我的落地形态其实是折中:原有 RAG 链路没有被废弃,而是降格为工具箱里的 semantic_search——快路径体验不变,慢路径解锁新能力。Agentic 化不是推倒重来,是给流水线装上调度器。

七、为什么用 Go 手写 200 行,而不是 LangChain

LangChain 的 Agent 抽象(AgentExecutor、Tool 封装、LCEL 管道)确实能少写代码。但对照我的约束:单一循环、五个工具、需要精确控制每轮的 token 和事件下发节奏——框架的抽象层在这里提供的只是"少写 for 循环",付出的却是调试黑盒的代价。

手写的全部骨架就三样东西:一个 chatMsg 结构体、一个 glmChat 函数、一个 execTool 分发器。200 行 Go,每个环节可打断、可日志、可解释。如果哪天要做多 Agent 协作或者复杂的状态机编排,我会认真评估 LangGraph——但单循环工具编排这个量级,手写是更诚实的方案。


至此,博客助手完成了三级进化:关键词检索 → 手写 RAG 流水线 → Agentic 工具编排(第一篇见《给博客装上一个「了解我」的 AI》)。每一级都在回答同一个问题的不同侧面:怎样让机器既知道边界、又能自主行动。

现在你可以去右下角问它:「这个博客一共多少篇文章?最近写的都在讲什么?」——看它自己掏工具、翻资料、算数字,最后给你一个带来源链接的答案。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/151