第 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(部门问答范本)