很多人第一次把说明书或内部文档丢给大模型时,都会遇到同一个尴尬场景:问“这个接口的默认超时是多少”,模型答得头头是道,数字却和文档对不上。问题往往不在模型本身,而在于检索到的片段和最终生成的答案之间断了一环——没人检查答案是否真的来自资料。LlamaIndex 作为开源框架,把数据接入、索引构建、检索和智能体编排串成了一条链路,但“接上知识库就准”从来不是默认结果。下面按文档准备、索引、检索、答案校验的顺序,走一遍可复现的路径,重点看误差从哪来、哪些参数值得调。

先想清楚:误差通常出在哪一层
文档型问答的链路可以拆成四段:切分、索引、检索、生成。每一段都会引入误差,但表现方式不同。
切分阶段的典型问题是把一段完整说明拦腰截断,比如把参数表拆成两半,检索时只召回半张表,模型看到的上下文本身就是残缺的。索引阶段的问题多半来自元数据缺失,多个版本的文档混在一起,检索命中了旧版说明。检索阶段最常见的是相似度阈值设置不当,要么召回一堆无关片段稀释上下文,要么阈值过高导致明明有资料却返回空结果。生成阶段则是模型在上下文不足时自行补全,编出看似合理但文档里没有的内容。
分清这四层,才能判断一次错误回答该改哪里。下面逐层展开。
文档准备与切分:别让上下文先天残缺
LlamaIndex 的入口是读取器(Reader),它把 PDF、Markdown、Word 等格式统一转成 Document 对象,再交给切分器拆成 Node。文档管理相关的能力支持对索引做插入、删除、更新和刷新,这意味着后续新增文档不必重建整个索引。
切分的关键参数是块大小和重叠长度。块太小,单块信息不完整;块太大,检索精度下降,因为一块里混了多个主题。对于技术文档,一个实用起点是 512 到 1024 字符的块,配合 10% 到 20% 的重叠,让跨块句子不至于丢失。
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
documents = SimpleDirectoryReader("./docs").load_data()
splitter = SentenceSplitter(
chunk_size=800, # 每块目标字符数
chunk_overlap=120, # 相邻块重叠,缓解截断
)
nodes = splitter.get_nodes_from_documents(documents)
比参数更容易被忽略的是元数据。给每个节点保留来源文件名、章节标题、文档版本,检索后就能在答案里标注出处,也方便排查“命中了哪份文档”。如果内部资料有多个版本共存,建议在加载阶段就按版本分目录,而不是靠检索时过滤。
索引与检索:召回质量决定上限
LlamaIndex 提供多种索引结构,文档问答最常用的是向量索引。构建时把节点向量化写入向量存储,查询时把问题向量化后做相似度检索。
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex(nodes)
retriever = index.as_retriever(similarity_top_k=4)
results = retriever.retrieve("接口默认超时时间是多少")
similarity_top_k 控制召回片段数量。设得太小,可能漏掉关键段落;设得太大,无关内容会挤占模型上下文,反而拉低答案质量。四到六条是常见起点,具体取决于文档密度。
检索结果里每条都带 score 字段,这是判断“有没有真的检索到”的第一手依据。实践中可以设一个最低相似度门槛,低于门槛的结果直接丢弃。这样做的代价是可能返回空结果,但空结果比错误答案更容易处理——下一节会讲怎么处理。
如果纯向量检索效果不稳定,可以叠加关键词检索做混合召回,或者引入重排序模型对候选片段二次排序。这类调整属于参数取舍,建议先用固定的一组测试问题记录改动前后的命中情况,再决定是否保留。
答案校验:区分“没检索到”和“模型补全”
这是整条链路里最容易被跳过、却最影响可信度的一步。做法是把检索到的片段和生成的答案放在一起做检查。
第一层检查是空结果处理。如果检索返回为空或全部低于阈值,就不应该让模型自由发挥。可以显式拦截并返回固定话术,比如“未在文档中找到相关内容”,同时把原始问题记录到日志,用于后续补充资料。
if not results or max(r.score or 0 for r in results) < 0.35:
answer = "未在现有文档中找到与该问题相关的内容。"
else:
response = index.as_query_engine(similarity_top_k=4).query(question)
answer = str(response)
第二层检查是引用核对。让模型在回答时附带来源节点编号,再逐条验证这些编号是否真的出现在本次检索结果中。如果答案引用了不存在的编号,说明模型在补全,应该重新生成或降级为“不确定”。
第三层检查是失败重试。当校验不通过时,可以换一种检索策略再试一次,比如提高 top_k、放宽阈值,或改用关键词检索兜底。重试次数建议限制在一到两次,避免在确实没有资料的问题上反复消耗。
| 检查项 | 判断依据 | 不通过时的处理 |
|---|---|---|
| 检索是否为空 | 结果条数与最高分 | 返回固定话术,记录问题 |
| 引用是否有效 | 答案引用的节点编号是否在结果内 | 重新生成或标注不确定 |
| 答案是否越界 | 关键数字、参数是否能在片段中找到 | 触发重试或人工复核 |
把校验接进智能体循环
如果只是单轮问答,上面的流程已经够用。要让智能体处理“先查文档、再调用工具”的简单协作,可以把检索和校验包装成工具,由智能体决定调用顺序。
from llama_index.core.tools import FunctionTool
def search_docs(query: str) -> str:
results = retriever.retrieve(query)
if not results:
return "NO_RESULT"
return "n".join(r.node.get_content() for r in results)
search_tool = FunctionTool.from_defaults(fn=search_docs)
这里返回 NO_RESULT 而不是空字符串,是为了让智能体明确知道“没查到”,而不是把空上下文当成“可以随便答”。工具协作的边界建议控制在一到两个工具,多智能体集群和企业级权限系统不在本文范围内。
常见问题与排查顺序
遇到答案不准时,按下面的顺序排查通常最快定位问题。
先看检索结果本身。把 retrieve 的返回打印出来,确认相关片段是否真的被召回。如果没召回,问题在切分或索引,不在生成。
再看命中片段是否完整。如果召回的是半张参数表或断开的句子,回到切分参数调整块大小和重叠。
然后看阈值是否过严或过松。过严导致空结果频繁出现,过松导致无关内容混入。用同一组问题对比不同阈值下的表现。
最后才怀疑模型。如果检索片段完整、阈值合理,模型仍然编造内容,那多半是提示词没有约束“只依据提供的上下文回答”,需要在系统提示里明确这一点。
LlamaIndex 把这条链路的各环节都做成了可替换的组件,好处是每一层都能单独调优,代价是需要理解每层的作用。文档型智能体的可靠性不来自某个神奇参数,而来自检索、生成、校验三者的对照关系。先把校验补上,再谈调参,往往比反复换模型更有效。

评论列表 (1条):
加载更多评论 Loading...