
个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 大模型开发从0到1 其他栏目: iOS项目总结大全 其他栏目: 我想学python了 其他栏目: iOS UI 文章目录LangChain RAG 引用溯源让每句话都能点开原文一、为什么引用这么重要二、方案一在 prompt 里要求标注编号最简单三、方案二后端映射推荐四、方案三用 structured output 强制带引用五、方案四事后校验引用是否真实最严格六、让引用落到页码级别七、前端怎么展示八、常见坑九、小结LangChain RAG 引用溯源让每句话都能点开原文“这个答案是从哪来的”——这是企业 RAG 被问得最多的问题。答不上来用户就不敢信尤其是医疗、法律、财务这类场景。这篇讲四种引用方案从简单到严格以及怎么验证引用是真的。一、为什么引用这么重要没有引用的 RAG 是这样的用户差旅住宿标准是多少 系统一线城市 600 元二线城市 450 元。 用户真的吗谁说的 系统沉默用户只能自己去翻 200 页的手册核对。信任成本太高系统就没人用。有了引用系统一线城市 600 元二线城市 450 元。 [制度手册 v2.4.md · 第 12 页] [2024差旅补充通知.md · 第 2 页]用户可以点开核对可信度立刻不一样。二、方案一在 prompt 里要求标注编号最简单给上下文里的每个片段编号让模型在答案里标注。defformat_docs(docs):parts[]fori,dinenumerate(docs,1):srcd.metadata.get(source,未知)paged.metadata.get(page,)locf第{page}页ifpageelseparts.append(f[{i}] 来源{src}{loc}\n{d.page_content})return\n\n.join(parts)promptChatPromptTemplate.from_messages([(system,只根据【上下文】回答没有就说我不知道。 **重要**每个事实性陈述后面都要用 [编号] 标注它来自哪一段上下文。 不要标注你没有用到的编号。 【上下文】 {context}),(human,{question}),])chain({context:retriever|RunnableLambda(format_docs),question:RunnablePassthrough()}|prompt|llm|StrOutputParser())输出一线城市每晚不超过 600 元[1]二线城市 450 元[1]。 同职级合住可上浮 30%[2]。优点零成本一行 prompt 搞定。缺点模型可能编造编号——它有时候会标一个根本没用到的 [3]。三、方案二后端映射推荐不让模型负责最终展示只让它输出编号后端把编号换成真实的文件名和链接。importredefrender_with_citations(answer:str,docs:list)-dict:把 [1] [2] 换成真实来源citations[]fori,dinenumerate(docs,1):citations.append({index:i,source:d.metadata.get(source,未知),page:d.metadata.get(page),snippet:d.page_content[:120],url:build_url(d.metadata),# 你的文档系统链接})used{int(n)forninre.findall(r\[(\d)\],answer)}used_citations[cforcincitationsifc[index]inused]return{answer:answer,citations:used_citations}resultchain_with_source.invoke(住宿标准)outrender_with_citations(result[answer],result[context])前端拿到结构化数据可以渲染成可点击的角标{answer:一线城市 600 元[1]二线城市 450 元[1]。,citations:[{index:1,source:制度手册 v2.4.md,page:12,url:/docs/handbook?page12,snippet:第三章 差旅费用…}]}好处模型只管输出编号简单任务不容易错链接、页码这些由后端保证准确。四、方案三用 structured output 强制带引用如果要更严格让模型输出结构化的陈述 引用对fromtypingimportListfromtyping_extensionsimportTypedDict,AnnotatedclassStatement(TypedDict):一条陈述text:Annotated[str,...,陈述的内容]citation_ids:Annotated[List[int],...,支撑这条陈述的上下文编号列表]classAnswer(TypedDict):完整回答statements:Annotated[List[Statement],...,拆分成若干条陈述]not_found:Annotated[bool,...,如果上下文里没有答案则为 True]structured_llmllm.with_structured_output(Answer)promptChatPromptTemplate.from_messages([(system,把回答拆成若干条陈述每条标注用到的上下文编号。上下文里没有答案就把 not_found 设为 true不要编造。),(human,上下文\n{context}\n\n问题{question}),])chain({context:retriever|RunnableLambda(format_docs),question:RunnablePassthrough()}|prompt|structured_llm)resultchain.invoke(住宿标准)forsinresult[statements]:print(f-{s[text]}{s[citation_ids]})这种方式最适合严肃场景每条陈述独立可核对没有引用的陈述会被发现可以做成告警前端可以逐句高亮鼠标悬停显示原文。五、方案四事后校验引用是否真实最严格模型标了 [3]但 [3] 里根本没有这个信息——这是 RAG 最危险的失效看起来有据可查实际是编的。可以用一个 LLM 校验器逐条检查VERIFY_PROMPTChatPromptTemplate.from_template( 判断下面这条陈述是否能从对应的上下文片段中得出。 【上下文片段】 {context_chunk} 【陈述】 {statement} 只回答 yes 或 no。判断标准 - yes陈述的内容在片段中明确出现或可直接推出 - no片段中没有这个信息或陈述添加了片段里没有的细节 )verify_chainVERIFY_PROMPT|llm|StrOutputParser()defverify_citations(answer:str,docs:list)-list:检查每条引用的真实性problems[]# 按句子拆forsentinsplit_sentences(answer):ids[int(n)forninre.findall(r\[(\d)\],sent)]ifnotids:problems.append({sentence:sent,issue:没有引用})continue# 只要有一个引用的片段能支撑即可okFalseforiinids:ifilen(docs):verdictverify_chain.invoke({context_chunk:docs[i-1].page_content,statement:remove_citation_marks(sent),})ifverdict.strip().lower().startswith(yes):okTruebreakifnotok:problems.append({sentence:sent,issue:引用无法支撑})returnproblems成本每条陈述一次 LLM 调用比较贵。折中做法是只对高风险场景开启财务、医疗、法务或者只抽检比如 10% 的请求。六、让引用落到页码级别source只能定位到文件用户还得自己翻。要做到第几页必须在切分时保留位置信息。fromlangchain_community.document_loadersimportPyPDFLoader loaderPyPDFLoader(handbook.pdf)pagesloader.load()# PyPDFLoader 自带 page 字段print(pages[0].metadata)# {source: handbook.pdf, page: 0}切分时 metadata 会自动继承chunkssplitter.split_documents(pages)print(chunks[0].metadata)# {source: handbook.pdf, page: 0} ← 保留了⚠️前提是用split_documents传 Document 列表如果用split_text传字符串metadata 就丢了。Markdown 文档可以用标题路径当位置# 前面讲过 MarkdownHeaderTextSplitter 会写入 h1/h2/h3d.metadata# {h1: 第三章, h2: 差旅费用, source: handbook.md}loc .join(filter(None,[d.metadata.get(h1),d.metadata.get(h2)]))# 第三章 差旅费用七、前端怎么展示几种常见形态形态适用句末角标[1]可点击通用悬停显示原文片段体验好侧边栏列出全部来源长回答点击跳转到文档系统对应页最实用一个最小可用的 HTML 渲染defto_html(answer:str,citations:list)-str:mapping{c[index]:cforcincitations}defrepl(m):iint(m.group(1))cmapping.get(i)ifnotc:returnreturn(fa href{c[url]} classcite ftitle{c[snippet]}[{i}]/a)returnre.sub(r\[(\d)\],repl,answer)八、常见坑现象原因解法模型编造编号编号幻觉后端校验 过滤掉不存在的编号metadata 里没有 page用了split_text而非split_documents改回来引用指向整篇文档切分时没保留位置信息加载阶段就要带 page / 标题路径引用了但不相关检索噪声提高检索质量加引用校验展示时泄露了无权限的来源没做过滤展示前也要按权限过滤最后一条很容易漏你可能检索时过滤了但 citation 列表里如果包含被过滤掉的文档同样会泄露信息。九、小结四种方案prompt 标注编号最简单→ 后端映射成真实链接推荐→ structured output 逐条陈述严格→ LLM 事后校验最严格让模型只输出编号链接/页码由后端保证这是准确性的关键要定位到页码加载时就要带page字段⚠️切分必须用split_documentssplit_text会丢 metadataMarkdown 可用标题路径当位置h1 h2引用可能也是编的高风险场景要做引用校验展示 citation 时也要按权限过滤否则等于泄露。下一篇讲 RAG-Fusion——用多个查询融合提升召回。