搭一个知识库问答机器人,先用一份小文档跑通「切分 → 向量化 → 检索 → 拼上下文 → 生成」这条最短链路,比先纠结用哪个模型更有效。因为大多数效果问题最后都会落回检索:命中了正确片段,普通模型也能答对;没命中,模型只会更流畅地编。动手前先确认三件事:文档总量和格式、内容是否允许发送到外部 API、答案是否必须带出处。这三件事直接决定向量库选型、embedding 部署位置和提示词约束方式。
知识库问答机器人的可靠起点是先把检索链路单独跑通并肉眼验证命中片段,再接生成模型。适合文档几十到上千份、需要引用出处的场景;如果问题本身是精确统计或跨表聚合,纯向量检索通常不合适,应改走 SQL 或关键词。操作上先切分并落盘检查,再建索引、加检索阈值、最后约束提示词只依据资料回答。验证以「问题是否能召回含答案的片段、库外问题是否拒答」为准,不以单次回答流畅度为依据。
先确认当前场景和目标
先分清要做的是内部文档检索还是客服口径问答。内部文档检索更看重可追溯,回答里要能标出文件名甚至页码,检索结果需要保留 source、page 这类元数据;客服口径问答更看重稳定和拒答,宁可说「资料中未提到」,也不要让模型自由发挥。两类目标对应的切分粒度、k 值和提示词约束不一样,混着做通常两头都不满意。
接着确认四个判断条件:文档是纯文本还是带大量表格扫描件的 PDF(后者往往要先做 OCR,不能指望切分器解决);总量是几十份还是上万份(决定要不要上外部向量库,还是本地文件加内存索引就够);更新频率如何(每天更新就要考虑增量重建索引,而不是每次全量重跑);是否允许把文档内容发给外部模型服务(不允许就得把 embedding 和生成都放在可控环境里)。这四个条件没确认就选型,后面大概率要返工。
按这个顺序处理
第一步只做加载和切分,把结果写成 jsonl 文件,先不接任何模型。切分器可以先用下面的骨架,分块大小、重叠量按你的文档长度调整,中文文档的分隔符要显式加上中文标点,否则容易在句子中间断开。

from langchain_text_splitters import RecursiveCharacterTextSplitter
# import 路径随 LangChain 版本拆包有变化,按当前安装的包调整,不要照抄报错就换库
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 替换:按文档平均段落长度调
chunk_overlap=80, # 替换:通常是 chunk_size 的 10%~20%
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""],
)
docs = loader.load() # 替换:目录 / PDF / 网页,各自加载器不同
chunks = splitter.split_documents(docs)
import json
with open("chunks.jsonl", "w", encoding="utf-8") as f:
for i, c in enumerate(chunks):
f.write(json.dumps({
"id": i,
"text": c.page_content,
"source": c.metadata.get("source"),
"page": c.metadata.get("page"),
}, ensure_ascii=False) + "\n")第二步看 chunks.jsonl。抽出十几个片段读一遍,检查有没有标题和正文被拆散、表格只剩半截、单块过长。这一步花十分钟,常常比后面调参数省事。
第三步做向量化和建索引。文档量小的时候,本地持久化向量库就够用;量大或需要多用户并发访问,再换服务端向量库。建索引时把 source、page 一并写进元数据,后面引用出处要用到。embedding 模型一旦选定,重建索引时要沿用同一个模型,中途换模型会导致新旧向量不在同一空间。
第四步封装检索,设置召回数量和相似度阈值。k 值从 3~5 起步,低于阈值的结果直接丢弃,宁可少给上下文也不要塞进不相关的片段。第五步拼提示词,约束模型只能依据给定资料回答、资料里没有就明说,并要求标注来源。骨架如下,「{context}」和「{question}」按所用链的变量名替换。

PROMPT = """你只能依据下面的资料回答问题。
资料中没有的信息,直接回答"资料中未提到",不要推测或补充常识。
回答末尾用 [来源: 文件名] 标注依据;引用多个来源就分别标注。
资料:
{context}
问题:{question}
"""
# 拼装前把检索到的片段加上来源前缀,方便模型标注,也方便你排查第六步用 LCEL 或普通函数把 retriever、提示词、模型串起来。这一层不必一开始就上复杂链路,先是一条直线,跑通再加多轮对话记忆或查询改写。
改完后怎么验证
验证要分两层,先验检索再验生成,不要一上来就只看最终回答。检索层直接调用检索器,把命中的片段打印出来,跳过模型:
for q in ["报销需要哪些材料", "年假能否跨年使用", "公司有没有健身房"]:
hits = retriever.invoke(q) # 或 similarity_search_with_score,按所用向量库接口调整
print("Q:", q)
for h in hits:
print(" -", h.metadata.get("source"), h.page_content[:80].replace("\n", " "))准备十几条问题,覆盖四种情况:答案在库里、答案不在库里、答案分散在两份文档、用户口语化提问。前三条查到的片段里应出现对应原文,来源文件要对得上;库里没有的问题应当召回不到相关片段,或者相似度明显偏低。检索层过关后,再走完整链路看生成结果,重点观察三件事:回答是否引用了正确来源、库外问题是否拒答、上下文是否被截断导致后半段资料丢失。这三点都比回答是否流畅更值得看。

容易忽略的边界
版本是最常见的坑。LangChain 已经拆成多个包,核心链、社区加载器、各家模型适配器常常分开安装,import 路径和参数名在不同版本间并不一致。遇到 ImportError 或参数不被识别,先确认实际安装的包和版本,再对着所装版本的用法改,不要凭记忆猜写法。
权限问题容易被拖到最后才想。如果知识库里存在不同人可见范围不同的文档,检索时必须带元数据过滤,把当前用户无权访问的片段挡在外面,而不是检索完再在提示词里遮一下——上下文一旦拼进去,内容就已经泄露了。文件读取权限、向量库的访问凭据、模型 API 的密钥同样要放在服务端配置里,不要写进前端。
还有几类场景不适合直接套这套流程:需要精确计数、求和、按条件筛选的问题,应走结构化查询;文档只有几份、总量很小的时候,直接全文放进上下文反而简单可靠,不必上向量索引;扫描件和复杂表格要先做解析,切分器解决不了识别问题。检索效果不理想时,先加关键词召回做混合检索或加重排序,再考虑换更大的模型,通常前者收益更直接。