上一阶段,我们把知识存下来了,能增量更新了,文档一变它就跟着更新。眼瞅着已经像个像样的本地知识库了。
然后我拿它问了个问题。它答得又快又顺,语气里带着一股「我说的就是对的」的笃定。我照它给的答案翻回原文一看,好家伙,它把两个不同版本的数据拼一块儿了,还拼得严丝合缝。
那一刻我明白了:前面几个阶段解决的是「答不出来」,这一阶段要解决的是「答错了,你还看不出来」。
01 会答还不够,自信的答案才最危险
大模型有个老毛病:它打死不说「我不知道」。
你问它个问题,只要塞给它一点上下文,它就想给你一个完整的、流畅的、听起来特别有道理的答案。哪怕资料里压根没有这个信息,它也能现编一个,还编得贼像真的。
这毛病放在文档问答里,尤其要命。你的助手是给资料干活的,资料里没有的东西,它一答就是凭空捏造。可你没法一眼看出,它这是在引用资料,还是在自由发挥。
所以第六阶段,我们干三件事:
让它敢说「资料里没有」,别硬编 让它的每个结论,都能追到具体片段 资料互相打架的时候,别和稀泥,老实说「有冲突」
一句话:把「会答」升级成「可信」。
02 可信不是一句口号,是三个具体的动作
先把这个阶段的三个能力拆开讲,后面再一个个说怎么做。
先说可信回答。资料不够的时候,助手要拒绝回答,别硬凑一个答案出来。
再说引用核验。回答里的每个关键结论,都得标清楚它来自哪个文件、哪个片段,你能顺着引用翻到原文。
最后是冲突处理。多份资料对同一件事说法不一致的时候,助手要把矛盾摆出来,别自作主张统一成一句话。
这三件事,前两件是「管住它别乱说」,第三件是「逼它把话说清楚」。
03 第一招:给检索加一道「相关度门槛」
第五阶段的检索,是找出 top_k 个最相似的片段,然后直接丢给模型回答。这里有个漏洞:哪怕「最相似」的那个片段,也可能跟你的问题八竿子打不着。
打个比方。资料里全是合同条款,你问它「下周团建去哪」,向量检索照样会把合同里「距离最近」的片段捞出来,模型再硬着头皮给你编个答案。
所以我们要加一道门槛:检索出来的片段,距离得低于某个阈值,才准进入回答环节。超过阈值的,直接判「资料不足」。
你可以把这理解成给助手请了个门卫:相关性不够的,一律挡在门外。
代码里就两步。第一步,检索时把距离一起带出来:
def retrieve_with_scores(question, chunks, index, embeddings, top_k):query_vector = embeddings.embed_query(question)query_matrix = np.array([query_vector]).astype(”float32”)search_k = min(top_k, len(chunks))distances, indices = index.search(query_matrix, search_k)results = []for dist, idx in zip(distances[0], indices[0]):results.append(RetrievedChunk(chunk=chunks[int(idx)], distance=float(dist)))return results
第二步,按阈值过滤,只留「足够相关」的片段:
def filter_grounded_chunks(retrieved, max_distance):return [item for item in retrieved if item.distance <= max_distance]
过滤完一条都不剩,就说明资料里没有能回答这个问题的东西。这时候助手该老实说一句「没找到足够相关的资料,我不能硬编」,别接着往下答。
这一步的价值在于:把「模型会不会编」这个管不住的问题,换成了「距离有没有过阈值」这个能管住的问题。
04 第二招:让每个结论都能追到原文
拒答管的是「资料里没有」。可资料里有的时候,模型照样能答偏。它会挑片段里顺眼的半句,剩下的半句自己脑补。
所以光拒答还不够,还得给回答上「引用」。
做法很简单:在提示词里要求模型,回答时给每个关键结论标上编号,比如「峰谷定价今日生效[1]」,结尾再单独列一份依据,写清楚每个编号对应哪个文件、哪个片段、原文长啥样。
说白了,就是给每个结论「上户口」,让它们都有迹可循。
提示词的关键部分长这样:
1. 只能根据下面的资料回答,不要使用你脑子里的外部知识补充。2. 回答里的每一个关键结论,都要在后面标注引用了哪份资料。3. 如果资料里没有答案,直接说「资料中没有明确提到」,不要硬编。4. 如果多份资料互相矛盾,不要强行合并,分开列出并写明各自来源。
输出格式固定成三段:
回答、# 依据、# 冲突提示。
有了这三段,用户拿到答案之后,就从「信不信你」变成了「我去查一下」。信不信,用户可以自己查,这才是可信的根基。
05 第三招:资料打架,别和稀泥
真实的知识库很少是干净的。同一份数据,A 文档写的是旧口径,B 文档写的是新口径,两份都进了向量库,检索的时候可能一起被捞出来。
这时候如果让模型自己处理,它大概率会「聪明地」把两个说法揉成一个,还揉得挺通顺。可这一揉,事实就没了。
这就好比两个同事跟你说同一件事,一个说东一个说西。这时候你要做的,是如实上报「他俩说法不一致」,别自己拍板选一个。
冲突处理要做的恰恰相反:把矛盾摆到台面上。
提示词第四条就是干这个的:多份资料说法不一致时,分开列出,写明各自来源,再明确提示「存在冲突,暂不能下结论」。
让模型承认「我不知道谁对」,比让它假装知道谁对,难得多,也有价值得多。前者是诚实,后者是幻觉。
06 完整流程跑一遍
把三招串起来,完整的可信问答流程长这样:
向量检索(带距离) → 相关性过滤(拒答) → 可信回答(引用 + 冲突)拿个真实场景演示一下。假设 test_docs 里放了两份文件,一份《项目周报》写着「Q3 目标提升 15%」,另一份《项目复盘》写着「Q3 目标提升 8%」。
你问它「Q3 的目标是多少」,它给出的回答大概是这个结构:
回答
资料中存在两份不一致的目标值,暂不能直接下结论。
依据
[1] 项目周报.md / 片段 3:Q3 目标提升 15% [2] 项目复盘.md / 片段 2:Q3 目标提升 8%
冲突提示
两份资料对 Q3 目标的描述不一致(15% 与 8%),建议核对资料更新时间,或向数据提供方确认。
这就是我们想要的效果:它没自作聪明地塞给你一个「正确」数字,它把两个数字和来源都摆出来,判断权交还给你。
07 阈值怎么标定,别拿一个数字套所有场景
上面那个 max_distance 阈值,我给的默认值是 1.0。但说句实话,这数字不是放之四海而皆准的。
它取决于你用的 embedding 模型、你的语料、甚至你的切块方式。同一个阈值,在 A 项目里可能太松,在 B 项目里又太紧。
这就跟买西装一个道理:均码的衣服,有人穿着像袍子,有人穿着像紧身衣。阈值得量身定做。
实用的标定办法是这样:
准备几个「明确相关」的问题,跑一遍检索,记下距离的分布 再准备几个「明确不相关」的问题,跑一遍,记下它们的距离 取两个分布之间的一块「安全区」当阈值
这样标出来的阈值,才贴合你的语料。别去网上抄个现成数字直接用。
08 说清楚这套东西的边界
这套东西能不能保证答案一定正确?不能。
它保证的是另外三件事:
资料里没有的,它不会硬编给你 它给的每个结论,你都能查到出处 资料打架的时候,它不会替你做决定
换句话说,它把「可信」从一句口号,变成了三个可以验证的动作。但引用也可能标错,阈值也可能漏掉真正相关的片段,这些风险还在。
所以这套东西的正确用法,是让 AI 把证据摆出来,你来判断。别把判断权交给 AI。可信的关键从来不在模型身上,在你能不能核查。
09 下一步
到这里,我们的文档助手已经能读、能查、能存、能答得可信了。但它还只会「说」,不会「做」。
下一阶段,给它加上工具执行的能力:让它不只是回答,还能把总结写进文件、生成报告、整理行动项。从「回答问题」跨到「产出结果」。
那一步迈出去,它才算真正开始像一个能帮你干活的助手。
想跟着系列一起动手,在公众号后台回复 Agent学习,领取第 1—6 阶段的完整配套代码。
这是「智能体AI漫游指南」第六阶段。往前翻可以找到前五个阶段:让 AI 读文件、处理长文档、先查资料再回答、向量检索、索引持久化。建议按顺序看,每一阶段都建立在前一阶段之上。
夜雨聆风