课程LubanAgent 实战课:8 课从零到部署你的业务 Agent / 第二章 · 核心能力:真模型、工具与知识库 / 第 5 课 给 Agent 加知识:知识库、切块与防错答守门
— 11 min read

第 5 课 给 Agent 加知识:知识库、切块与防错答守门

上传规章文档切块入库,用检索实验校准相似度阈值(切块大小直接影响分数分布),配好阈值、兜底拒答、引用编号三道代码守门,做到宁可说不知道、绝不错着答。

第 5 课:给 Agent 加知识——RAG 与防错答守门

学完本课你能:把一份规章文档变成可检索的知识库挂到 Agent 上;用检索测试接口亲手校准阈值;配置三道守门让 Agent「宁可说不知道,绝不错着答」 | 预计耗时 45 分钟 | 前置:第 4 课 | 难度 ★★☆

小图现在的短板一眼可见:问它"书能借多久",它会回答——但答案是从模型训练记忆里来的,跟你们学校图书馆的真实规章没有任何关系。问"逾期怎么罚",它编一个听起来合理的数字。这就是第 0 课说的幻觉,这一课正面解决它。

解决思路第 0 课推演过:把规章文档切块入库,用户提问时检索最相关的几块塞给模型,让它看着材料答题。这一课你会走完 RAG 的全链路:上传 → 切块 → 检索实验 → 校准阈值 → 守门配置 → 亲眼看着它拒答。

其中"校准阈值"是这一课真正的主角,也是很多人用 RAG 最容易偷懒跳过的一步——跳过的后果是守门形同虚设。我们不会跳。

1. 学习目标

  • 把一份 Markdown 规章变成知识库并挂到 Agent
  • 用检索测试接口做对比实验:换切块参数,看分数怎么变
  • 理解三道守门(阈值 / 兜底拒答 / 引用编号)各挡什么,以及它们为什么必须是代码而不是 Prompt
  • 亲眼验证 knowledge.no_hit 事件与零 token 拒答

2. 概念讲解:阈值是调出来的,不是拍出来的

RAG 的链路你已经在第 0 课推演过(切块 → 向量化 → 检索 → 注入)。这里讲链路上最容易做错的一环:相似度阈值

检索返回的每个片段带一个相似度分数(cosine,0 到 1)。分数多高算"相关"?这没有万能答案——它取决于你的 embedding 模型、你的语料、你的切块参数。唯一可靠的办法是实测:拿几个"应该命中"的问题和几个"不该命中"的问题各查一遍,看分数分布,把阈值卡在两团分数中间。

更重要的是切块对分数的影响。我们马上会做一个真实实验:同一份规章,切成 2 大块时,"书能借多久"(正例)得分 0.101,"怎么开发票"(反例)得分 0.128——反例反而更高,此时无论阈值定多少守门都是坏的。切成 6 小块后,正例升到 0.195-0.309,反例掉到 0.126 以下——分界清晰了。直觉上不难理解:块越大,一块里混杂的主题越多,向量就越"什么都有点像、什么都不像"。

三道守门各挡什么:

  • score_threshold(阈值):低于阈值的片段直接丢弃,不进模型的上下文。
  • no_hit_fallback(兜底拒答):过滤后一个片段都不剩时,代码直接用这句话作答,模型根本不出场——零 token、零发挥空间。事件流里能看到 knowledge.no_hit,Run 的结束原因是 knowledge_no_hit
  • cite: true(引用编号):命中时模型被要求只依据编号片段作答、答案带 [n] 引用——用户能核对出处。

注意这三道防线的实现位置:全在模型出场之前或之外,由代码执行。对比"在 Prompt 里写'你不知道就说不知道'"——第 0 课验证过那拦不死。这就是"确定性规则由代码执行"在 RAG 上的落点。

3. 动手:主线步骤

步骤 1:准备规章文档

新建 library-rules.md(放哪都行,上传后就用不着本地文件了),内容是一份小型借阅规章:

# 图书馆借阅规章

## 借阅期限与数量
- 本科生可同时借阅 10 册,借期 30 天
- 研究生可同时借阅 20 册,借期 45 天
- 教职工可同时借阅 30 册,借期 60 天

## 续借规则
- 每册图书可续借 1 次,续借期与原借期相同
- 有他人预约的图书不可续借
- 逾期图书不可续借

## 逾期处理
- 逾期图书按每天每册 0.1 元收取滞纳金
- 逾期超过 30 天的,冻结借阅权限直至归还并缴清滞纳金

## 预约与委托借阅
- 已借出的图书可在线预约,归还后为你保留 3 天
- 本馆未收藏的图书可通过馆际互借申请,处理周期约 5 个工作日

## 校园卡
- 校园卡丢失请立即挂失,挂失期间产生的借阅记录由持卡人负责
- 补办新卡收取工本费 20 元,三个工作日后凭学生证到前台领取

## 馆内服务
- 自习区开放时间:周一至周日 7:00-22:30
- 电子阅览室需刷校园卡进入,每人每日限用 4 小时

步骤 2:上传到知识库

工作台知识库页 → 上传:选这个文件,名称填 library-rules,切块大小 300 / 重叠 30(先按这个来,马上会调),描述随意。

不想点页面的话,等价的 API:

curl -X POST http://localhost:8000/v1/knowledge/sources \
  -F "files=@library-rules.md" -F "name=library-rules" \
  -F "description=图书馆借阅规章" -F "chunk_size=300" -F "overlap=30"

✅ 你应该看到:返回 "chunk_count": 2——这份 993 字节的文档被切成了 2 块。记住这个数字,它马上会成为问题。

步骤 3:检索实验——发现问题

知识库页的检索测试面板(或 API POST /v1/knowledge/query)逐个查这几个问题,记下每个的 top1 分数:

for q in "书能借多久" "怎么续借" "食堂几点吃饭" "怎么开发票"; do
  echo "Q: $q"
  curl -s -X POST http://localhost:8000/v1/knowledge/query \
    -H "Content-Type: application/json" \
    -d "{\"source\": \"library-rules\", \"query\": \"$q\", \"top_k\": 3}" \
    | python3 -c "import sys,json; r=json.load(sys.stdin); print('  ', round(r[0]['score'],3), r[0]['content'][:30])"
done

✅ 你应该看到(FakeEmbedding 下的实测值,你的数字会略有出入但格局相同):正例"书能借多久"约 0.10,反例"怎么开发票"约 0.13——反例分数更高,两团分数糊在一起。此刻阈值无论定多少,要么放走反例、要么误杀正例。2 块的切分把不相关的内容挤进了同一块,向量失去了区分度。

步骤 4:调切块,重做实验

把切块改小——知识库页的"重新切块"(改参数不重新上传,文件已在服务端):

curl -X POST http://localhost:8000/v1/knowledge/sources/library-rules/rechunk \
  -H "Content-Type: application/json" -d '{"chunk_size": 120, "overlap": 12}'

重跑步骤 3 的实验。

✅ 你应该看到:chunk_count 变成 6;正例升到 0.20-0.31 区间,反例掉到 0.13 以下("食堂几点吃饭"直接 0.0)。两团分数分开了,阈值有地方卡了。把你实测的两组数字记下来——这是你自己的校准记录,比任何教程给的"推荐值"都可靠。

步骤 5:配置守门并挂载

app/agents/my_lib_assistant.yaml:人设里加一句使用规则,加 knowledge 段(阈值用你自己的校准结果,下面是本教程语料的实测值 0.15):

instruction: |
  ...(前几行不动)
  规章问题依据知识库检索到的片段作答,引用带 [n] 编号;检索不到就说不知道,不编。
  ...
knowledge:
  sources: [library-rules]
  top_k: 3
  # 0.15 是 FakeEmbedding 对本语料 6 块切分的实测分界(正例 0.195-0.309 / 反例 <=0.126)
  # 换 embedding 模型或扩文档后必须用 /v1/knowledge/query 重新校准
  score_threshold: 0.15
  no_hit_fallback: 抱歉,这个问题我在馆藏规章里查不到依据,建议到图书馆前台咨询确认。

导入:

.venv/bin/luban init

✅ 你应该看到:my-lib-assistant 版本 +1,工作台编辑器的"记忆与知识"区能看到三个守门字段。

步骤 6:命中——看着材料答题

调试台问小图:书最多能借多久?我是本科生

✅ 你应该看到:回复类似"本科生可同时借阅 10 册,借期 30 天**[1]**"——数字来自你的规章文档,[1] 是引用编号。这个回答模型"编"不出来:它只见过检索给它的那一小块文本。

步骤 7:未命中——代码守门,模型不出场

问一个语料里完全没有的问题:食堂几点开门吃饭

✅ 你应该看到:回复一字不差是你配的 no_hit_fallback 那句话。看事件流(curl 或调试台):knowledge.no_hit 事件带完整现场——{"sources": ["library-rules"], "score_threshold": 0.15, "hits_raw": 3, "hits_kept": 0}(检回 3 块、阈值后 0 块);没有 token.delta——模型根本没被调用,这次拒答零成本、零发挥空间。Run 结束原因 knowledge_no_hit

步骤 8:故意把守门弄坏,再看一遍

把 YAML 里阈值改成 0.05,init,重问"食堂几点开门吃饭"。

✅ 你应该看到:这次没有拒答——低分片段被放行,模型拿着不相关的规章片段开始"凑答案"(可能聊到自习区开放时间之类的)。看完把阈值改回 0.15。这一趟不是白走:你现在对"阈值松一格会发生什么"有体感了,以后看到 Agent 开始似是而非地答非所问,第一反应应该是查阈值。

4. 完成自检清单

  • 知识库里 library-rules 显示 6 块(chunk_size 120)
  • 检索实验记录在案:正例/反例两组分数,阈值卡在中间
  • 命中问题带 [n] 引用,数字与文档一致
  • 未命中问题走 no_hit:兜底话术原样返回、事件流有 knowledge.no_hit、无 token.delta
  • 故意调低阈值的破坏实验做过并恢复了

5. 常见坑

阈值照抄教程/别人的值。 本课的 0.15 只对本教程的语料和切块负责。你的文档、你的切块、将来换真 embedding 模型——任何一个变了都要重跑步骤 3 的校准。照抄阈值是守门失效的第一大原因。

正例反例分不开。 回到切块:块太大就调小;还不行检查文档本身是不是"一锅烩"(一章塞了十个主题)——RAG 喜欢结构清晰、一节一题的文档。

"应该拒答"的问题被答了。 先查检索测试接口看这个问题的实际分数——可能它就是过了阈值的灰色地带(比如问"发票",语料里"滞纳金""工本费"这些词有一定相似度)。灰色地带的处理是业务决策:把话术写细让人设兜住,或接受它答(本课步骤里"开发票"的问题模型靠人设边界正确处理了——守门不是唯一防线,人设是第二道)。

改了 YAML 里的 knowledge 段没生效。 老朋友了:luban init

换文档后分数全变了。 正常,重新校准。知识库页改参数重新切块很快,别攒着不调。

6. 练习

练习 A(模仿):给规章加一条新规定(比如"寒暑假借期自动延长 15 天"),重新切块,问对应问题验证回答更新了。体会:知识更新不用改代码、不用改人设、不用重新部署——这就是"知识是数据"的含义。

练习 B(变式):把 top_k 从 3 调成 1,再问一个跨两条规定的问题("我逾期了还能续借吗"——答案需要"逾期不可续借"+"滞纳金"两块)。观察回答质量的变化,理解 top_k 是"给模型几块材料"的预算。

练习 C(综合,为第 6 课铺垫):设计 3 个你自己的对抗性问题——措辞上像规章问题、内容上语料没有(比如"图书馆能代收快递吗")。用检索测试接口查它们的分数,看你的阈值守不守得住。把这三个问题记下来——第 6 课它们会变成你评测集里的 expect_refusal 用例,从"手动抽查"升级成"每次改动自动回归"。

7. 小结与下一课预告

小图集齐了三块能力:人设(谁)、工具(手)、知识(脑外挂)。更重要的是你掌握了防错答的完整工事——阈值挡低分、兜底拒答挡全空、引用编号逼模型亮出处,三道全是代码守的。

但你现在守护质量的方式还是手动抽查:改了人设,自己问几句看看。改的东西多了、要看的行为多了,手动就守不住了。下一课把抽查固化成评测集:每个问题配上机器可判的断言,一条命令全量回归,还能对比两个版本谁好谁坏。你第 5 课练习 C 的三个对抗问题,到时候直接派上用场。


延伸阅读:用户手册 §10(知识与记忆——全部字段与记忆策略) | 参考产出:reference/05-lib-faq.yaml | 对照范本:app/agents/cs_faq.yaml(内置客服的 RAG 守门配置)、app/agents/dept_project.yaml(部门问答范本)

目录