问题与目标
大模型参数中的知识可能过时或无法追溯。两步式 RAG 先检索固定数量的知识片段,再把证据交给模型生成答案。流程是确定的,不让模型自行决定是否检索,适合知识库问答的第一版。
本篇实现稳定来源编号、上下文预算和证据不足回退。引用表示答案依据了哪个片段,不代表片段本身一定正确,因此文档版本与来源质量仍需治理。

检索和生成被明确分成两步;来源对象与答案同时返回,证据不足走拒答分支。
核心概念
text
问题 → Retriever → 候选过滤/去重 → 上下文组装 → LLM → 答案与引用
上下文组装要解决:
- 只保留当前用户有权访问的片段;
- 为每个片段分配本次请求内稳定编号;
- 控制总长度并避免相邻重复 Chunk 占满预算;
- 把来源元数据与正文明确分隔;
- 证据不足时不要求模型猜测。
可运行实现
以下示例假设 retriever.invoke() 返回 Document:
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
def build_context(docs, max_chars: int = 2400) -> dict:
blocks, sources, used = [], [], 0
for index, doc in enumerate(docs, start=1):
source = doc.metadata.get("source", "unknown")
text = doc.page_content.strip()
block = f"[S{index}] source={source}\n{text}"
if used + len(block) > max_chars:
break
blocks.append(block)
sources.append({"id": f"S{index}", "source": source})
used += len(block)
return {"context": "\n\n".join(blocks), "sources": sources}
prompt = ChatPromptTemplate.from_messages([
("system", """只根据 <context> 回答。每个事实使用 [S数字] 引用。
证据不足时只回答“现有知识库无法确认”,不得使用参数知识补全。"""),
("human", "<context>\n{context}\n</context>\n问题:{question}"),
])
def retrieve_and_prepare(inputs: dict) -> dict:
docs = retriever.invoke(inputs["question"])
prepared = build_context(docs)
return {"question": inputs["question"], **prepared}
chain = RunnableLambda(retrieve_and_prepare) | prompt | model | StrOutputParser()
print(chain.invoke({"question": "关键告警需要通知谁?"}))
程序还应在输出后检查引用格式与编号范围;若答案写了 [S4] 而本次只有三个来源,结果应标记为无效。最终 API 最好同时返回结构化来源列表,前端不要从自然语言中猜 URL。
常见问题与排查
Prompt 写了“必须引用”就认为引用可信
校验每个引用存在,并抽查被引用片段是否真的支持对应断言。引用完整性与忠实性是两个指标。
把 Top-k 全部拼进上下文
按预算、相关性、去重和权限筛选。更多上下文可能增加噪声并降低模型对关键证据的注意力。
证据不足仍生成通顺答案
设置最低检索条件只是第一层,还要加入明确拒答提示、结果检查和离线“不可回答问题”测试集。
来源编号跨请求复用
S1 只在当前回答内有意义。持久追踪使用稳定 Chunk ID、文档版本和页码,显示编号只是阅读层。
小结
两步式 RAG 的可靠性来自固定流程与证据边界:先检索、再组装、后生成,最后验证引用。它比 Agentic RAG 更容易评估和排查,应成为知识问答的基线实现。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


