响应生成

45 阅读29分钟

Alex:在我看来,在整个 RAG 流程中,生成这一步是我们最不需要担心的。虽然它是 RAG 系统的门面,但也是我们最难控制的部分。它是最终交付给学习者的内容。由于当前大模型能力已经极强,生成任务主要由这些大模型来完成。

Lewis:这有道理。不过,在 RAG 系统中,生成并不只是一个简单输出;它更像是一种策略性表达。大模型可以根据每个学习者的个体特征,为他们生成专门定制的内容。

Anna:没错!内容生成过程可以充满创造性和灵活性。从我的实践经验看,精心设计 prompts 对提升输出质量至关重要,因为这可以引导大模型生成更多样、更连贯的内容。

Lewis:除了 prompt 设计,生成阶段的另一个关键方面是生成模型的选择。你可以选择调用 API,也可以部署本地模型;不同模型提供的答案质量可能差异很大。以 DeepSeek 为例,它不仅提供 API 调用方式,也提供可本地部署的开源版本。DeepSeek 针对不同应用场景定制了专门的聊天模型 V3 和推理模型 R1。聊天模型更适合一般对话,而推理模型则更适合复杂、专业的学术讨论需求。

image.png

图 8.1:AI 响应生成步骤。

此外,当知识库中没有找到真实答案时,RAG 系统往往会给出一个看似合理但实际上错误的答案,而不是直接承认自己无法回答。类似问题还包括生成响应中缺乏有效内容等。这些问题可以通过在生成过程中采用 Self-RAG 和 RRR 等主动优化策略来缓解。

通过优化 prompts 提升 LLM 输出质量

当知识库中的信息不足,而系统可能给出错误答案时,优化 prompts 可以帮助提高生成答案的准确性。一个常见例子是告诉模型:

If you are not sure of the answer, please state that you do not know

也就是:“如果你不确定答案,请说明你不知道。” 这种方式会鼓励模型识别自身局限,并更透明地表达不确定性。

接下来,我们将从三个角度探索如何使用 prompt engineering 改善 LLM 输出。

使用模板和示例引导生成结果

首先,我们讨论如何使用模板和示例,引导 LLM 生成我们期望的结果。

使用预定义模板可以引导 LLM 以特定格式输出内容。模型会基于已知信息生成符合模板描述的连贯内容,从而确保输出一致性。

下面是一个使用预定义模板的具体示例:

from langchain import PromptTemplate

template = """
As an experienced game planner, you are skilled at constructing character analyses based on script content.

[Background Information]
{context}

[Analysis Task]
Please write a character analysis report based on the above material in the following format:

---
Character Name: <Fill in the character’s Chinese name>
Background Story: Describe their origin, personality, key plot points, and relationships and conflicts with other characters.
Skill Characteristics: Main abilities or skills (such as magic, weapons, special abilities, etc.).
Combat Strategy: Recommended tactical playstyle (how players should respond); Weakness analysis (how and under what conditions the character is restrained).
---

Note: The content must be closely related to the material, logically clear, and professionally worded, suitable for planning reviews or producer briefings.
"""

prompt = PromptTemplate(input_variables=["context"], template=template)

在 prompts 中提供示例,例如 few-shot examples,可以让 LLM 模仿示例的格式和风格,引导模型生成符合预期的输出。

例如,在 RAG 知识库的开发场景中,我们可能希望 LLM 分析自动化测试日志,帮助研发人员和测试工程师识别可能与失败测试用例相关的代码段,并提供初步修复建议,如下所示:

from langchain.prompts import PromptTemplate

template = """
You are responsible for analyzing the reasons for program failures from test logs, and locating the relevant code segments.

[Example 1]
Case ID: TC_001
Execution Log Summary: Input A=5, B=0; called function **divide(a, b)**; returned error: **ZeroDivisionError**

Analysis Conclusion:
- Root Cause: The divide function does not handle division by zero exception
- Related Code: `math_utils.c` line 24
- Fix Suggestion: Add exception detection logic for zero divisors

[Example 2]
Case ID: TC_002
Execution Log Summary: Input user ID is empty, called function **login(user_id)**; program returned abnormal status code **500**

Analysis Conclusion:
- Root Cause: The login API does not perform parameter validation for empty user IDs
- Related Code: `auth.c` line 12
- Fix Suggestion: Add a non-empty check at the function entry.

[Current Task]
Please refer to the above format and analyze the following test case:

Case ID: {case_id}
Execution Log Summary:
{log}

Please output a structured analysis including root cause, related code, and suggestions.
"""

prompt = PromptTemplate(input_variables=["case_id", "log"], template=template)

增强生成的多样性和全面性

为了增强大模型生成内容的多样性,可以先生成多个候选答案,然后将它们与检索结果进行比较,并选择最优答案;也可以在 prompt 中鼓励大模型考虑不同观点或可能性,从而生成更加全面的答案。

下面是一个具体示例:

## 定义检索到的文档内容
def get_code_snippet() -> str:
    """
    获取需要分析的代码片段。
    返回:
        str: 包含代码片段的字符串
    """
    return """
def handle_request(request):
    # Check if the token is included in the request headers
    if ‘token’ not in request.headers:
        return {‘status’: 401, ‘message’: ‘Unauthorized’}, 401
    try:
        # Check user permissions
        check_permission(request.headers[‘token’])
        # Process request logic
        return process_request(request)
    except AccessDenied:
        return {‘status’: 403, ‘message’: ‘Forbidden’}, 403
    except Exception as e:
        return {‘status’: 500, ‘message’: str(e)}, 500
"""

retrieved_content = get_code_snippet()

## 定义 Prompt,以增强生成的多样性和全面性
prompt = f"""
Please describe possible error handling mechanisms based on the following code snippet:
{retrieved_content}

Note: Please provide multiple different analytical perspectives, covering **input exceptions**, **permission control**, **call chain**, and other aspects."""

接下来,我们将看看如何实现 fact-checking 机制,以提升内容准确性。

引入 fact-checking 机制以增强事实准确性

有时,fact-checking 是必要的。这可以通过在 prompt 中强调回答只能基于检索内容来实现,或者将生成内容限制在特定主题或范围内,以减少无依据内容的生成。通过比较生成内容与检索文档之间的一致性,可以有效避免错误信息。

例如,考虑下面这个例子:

### 定义检索文档内容
retrieved_content = get_code_snippet()

### 定义 Prompt,强调只能基于检索内容回答
prompt = f"""
Please identify potential flaws in the following code based on its content:
{retrieved_content}

Note: Please analyze only based on the code above without introducing external knowledge."""

通过输出解析控制输出格式

在某些情况下,需要以非常严格的方式输出内容。例如,当 Agent 后续需要调用函数或工具时,就必须在 prompt 中清楚描述期望的输出格式。比如,当你要求大语言模型以列表形式呈现答案,或者限制响应的长度和结构时。如果大模型没有按照你的预期组织信息,也有一些方法可以更好地引导它理解你的要求,确保输出符合目标格式。

LangChain 和 LlamaIndex 作为主流框架,都提供了相应组件,用于在调用大语言模型之后解析生成输出。这样可以标准化输出格式,以满足不同应用场景的需求。

LangChain 输出解析机制

LangChain 提供了一系列 OutputParser 组件,用于解析大模型输出,包括:

StrOutputParser:默认输出解析器,直接返回大模型的文本输出。

JSONOutputParser:用于解析 JSON 格式输出,确保大模型输出符合 JSON 规范。

PydanticOutputParser:基于 Pydantic 执行结构化解析,适合复杂数据结构。

RegexParser:通过正则表达式匹配提取关键内容。

StructuredOutputParser:基于 JSON Schema 解析结构化数据。

下面的代码示例演示如何使用 LangChain 的 JSONOutputParser

from langchain_core.output_parsers import JsonOutputParser
from langchain_deepseek import ChatDeepSeek
from langchain.prompts import PromptTemplate

## 定义输出格式
parser = JsonOutputParser()
prompt = PromptTemplate.from_template("Please return the user’s information in JSON format: {query}")

## 调用大模型并解析
llm = ChatDeepSeek(model="deepseek-chat")
output = llm(prompt.format(query="User ID 123"))
parsed_output = parser.parse(output.content)

print(parsed_output)

输出如下:

{'user_id': '123'}

LlamaIndex 输出解析机制

LlamaIndex 的输出解析主要体现在两个组件中:response synthetization 和 structured output parsing。

检索之后,LlamaIndex 会执行 response synthetization。也就是说,它会先召回相关文档,然后使用 response_synthesizer 处理召回数据,最后使用大模型生成最终答案。下面是一个展示解析机制的不错示例:

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.response_synthesizers import get_response_synthesizer
from llama_index.core.response_synthesizers.type import ResponseMode
from llama_index.core.prompts import PromptTemplate
from pydantic import BaseModel, Field
from typing import List

## 定义游戏信息结构
class GameInfo(BaseModel):
    title: str = Field(description="Game title")
    developer: str = Field(description="Developer")
    release_date: str = Field(description="Release date")
    platforms: List[str] = Field(description="Supported platforms")
    main_features: List[str] = Field(description="Main features")
    story_summary: str = Field(description="Story summary")
    reception: str = Field(description="Market reception")

## 加载数据
documents = SimpleDirectoryReader("data/黑神话").load_data()
index = VectorStoreIndex.from_documents(documents)

## 使用 COMPACT 模式的基础解析模式
print("=== Basic Parsing Mode ===")
synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.COMPACT,
    verbose=True    # 显示详细信息
)

query_engine = index.as_query_engine(response_synthesizer=synthesizer)
response = query_engine.query("Please summarize the main content of the game ‘Black Myth: Wukong’")
print(response)

## 使用 REFINE 模式的结构化解析模式
print("\n=== Structured Parsing Mode ===")
synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.REFINE,
    output_cls=GameInfo,  # 指定输出类
    verbose=True
)

query_engine = index.as_query_engine(response_synthesizer=synthesizer)
response = query_engine.query("Please extract the key information of ‘Black Myth: Wukong’")

## 安全处理 Response
if hasattr(response, 'response'):
    print(response.response)
else:
    print(response)

## 使用 TREE_SUMMARIZE 模式的表格格式解析
print("\n=== Game Features Table Parsing ===")
table_prompt = PromptTemplate(
    template="Please present the following game features in table form:\n{query_str}\nFormat requirements:\n| Category | Content |\n|--|--|\n"
)

synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.TREE_SUMMARIZE,
    summary_template=table_prompt,
    verbose=True
)

query_engine = index.as_query_engine(response_synthesizer=synthesizer)
response = query_engine.query("Please summarize the main features of ‘*Black Myth: Wukong*’ in table form")
print(response)

## 使用 COMPACT_ACCUMULATE 模式进行要点分析
print("\n=== Game Highlights Bullet Point Analysis ===")
bullet_prompt = PromptTemplate(
    template="Please present the following game highlights in bullet point form:\n{query_str}\nFormat requirements:\n1. \n2. \n3. "
)

synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.COMPACT_ACCUMULATE,
    text_qa_template=bullet_prompt,
    verbose=True,
    use_async=True  # 启用异步处理
)

query_engine = index.as_query_engine(response_synthesizer=synthesizer)
response = query_engine.query("Please summarize the highlights of ‘*Black Myth: Wukong*’ in bullet point form")
print(response)

## 使用 SIMPLE_SUMMARIZE 模式进行剧情分析
print("\n=== Game Storyline Analysis ===")
story_prompt = PromptTemplate(
    template="Please present the following game story as a timeline:\n{query_str}\nFormat requirements:\n- Time point: Event\n"
)

synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.SIMPLE_SUMMARIZE,
    text_qa_template=story_prompt,
    verbose=True
)

query_engine = index.as_query_engine(response_synthesizer=synthesizer)
response = query_engine.query("Please summarize the story development of ‘*Black Myth: Wukong*’ in timeline form")
print(response)

让我们拆解前面的代码,逐个理解每个功能。

基础解析模式

《黑神话:悟空》是由 Game Science 开发并发行的一款动作角色扮演游戏。它被誉为中国第一款 AAA 游戏,基于中国经典小说《西游记》。在这款游戏中,学习者控制一个被称为“天命人”的角色,通过收集孙悟空失落的身体部件,也就是 Root Vessels,踏上寻找并复活孙悟空的旅程。该课程包含一个战斗系统,具备多种棍法战斗风格、资源管理机制,并融入中国文化和自然地标,以及佛教和道教元素。故事发生在《西游记》事件之后,并包含对香港电影《大话西游》的引用。

结构化解析模式

结构化解析模式由 titlerelease_datestory_summaryreception 等参数组成。学习者扮演来自花果山的 Lingming Stone Monkey,也就是 The Destined One,踏上取回失落 Root Vessels、拯救并复活孙悟空的旅程。在寻找 Root Vessels 的过程中,The Destined One 击败各种妖王和 Boss,以取回悟空的 Root Vessels。

下面的表格展示了游戏特点:

CategoryContent
Game Type动作冒险游戏
Game Style中国神话主题,结合东方文化元素
Gameplay独特战斗系统,包括武器、技能和终极招式组合
Game World开放世界,包含自由探索和解谜
Role-playing学习者可以扮演孙悟空,体验他的冒险故事
Art Style精美视觉和动画,展现传统中国绘画风格
Music Score原创音乐,与游戏场景相配合,增强氛围
Game Difficulty挑战性较高,要求学习者灵活运用战斗技巧

下面是游戏亮点的逐点分析:

独特的中国神话主题:该课程基于中国神话故事,让学习者体验独特的东方神话世界观。

开放世界与自由探索:学习者可以自由探索游戏世界,发现隐藏任务和秘密。

创新战斗系统:该课程采用实时战斗,让学习者在战斗中使用各种武器和技能,提供不同战斗体验。

对游戏剧情进行分析,可以注意到:

远古时期:孙悟空被封印在山中。

唐代:山中发现了封印孙悟空的石碑。

唐代:孙悟空重新获得自由,并开始他的冒险。

此外,LlamaIndex 还支持与其他输出解析框架集成。例如,可以通过 GuarDrails 实现最终输出安全保护和价值对齐。具体使用说明请参考其官方文档。

OpenAI 的 JSON mode 和 structured output

OpenAI 传统的 JSON mode 允许通过将 response_format 设置为:

{ "type": "json_object" }

来激活 JSON response mode。启用该模式后,系统只会生成可解析为有效 JSON 对象的字符串。需要注意的是,虽然该模式指定输出格式必须是 JSON,但它并不确保内容符合某个具体类型。例如,每个字段的名称和类型不一定总是与预期完全一致。

在 OpenAI 最新的,也就是本书写作时的 Responses API Structured Outputs 功能中,可以进一步确保模型生成的响应符合用户提供的 JSON schema。这意味着你不再需要担心模型遗漏必需 key,或者生成无效枚举值,也不必使用很强硬的 prompts 来强制格式一致。对于能力较弱的模型,精心设计 prompts 仍然非常重要。

下面是 Responses API structured output 的示例:

from openai import OpenAI
import json

client = OpenAI()

response = client.responses.create(
    model="gpt-4o",
    input=[
        {"role": "system", "content": "Extract the event information."},
        {"role": "user", "content": "Alice and Bob are going to a science fair on Friday."}
    ],
    text={
        "format": {
            "type": "json_schema",
            "name": "calendar_event",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                        },
                    "date": {
                        "type": "string"
                        },
                    "participants": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                },
                "required": ["name", "date", "participants"],
                "additionalProperties": False
                },
            "strict": True
            }
        }
    )

event = json.loads(response.output_text)

在前面的示例中,"type": "json_schema""schema" 参数明确指定了输出 JSON 文件的精确格式。

Pydantic 解析

Pydantic 提供了一个通用框架,能够将输入文本转换为结构化 Pydantic 对象。LlamaIndex 提供了几类 Pydantic programs,每类都有特定应用场景。这里列举几个:

LLM Text Completion Pydantic Programs:这类程序处理输入文本,并将其转换为用户定义的结构化对象。它们结合了 text completion API 和输出解析功能。

LLM Function Calling Pydantic Programs:根据用户需求,这类程序将输入文本转换为特定结构化对象。这个过程依赖 LLM function calling API。

Prepackaged Pydantic Programs:这类程序被设计用于将输入文本转换成预定义结构化对象。

下面是一个 Pydantic program 示例:

from pydantic import BaseModel, Field
from typing import List, Optional
from llama_index.program.openai import OpenAIPydanticProgram

### 定义代码问题模型
class CodeIssue(BaseModel):
    """代码中存在的问题"""
    line_number: int = Field(..., description="Line number where the issue is located")
    issue_type: str = Field(..., description="Type of issue, such as security vulnerability, performance issue, code style, etc.")
    description: str = Field(..., description="Detailed description of the issue")
    severity: str = Field(..., description="Severity of the issue: high/medium/low")

### 定义代码分析报告模型
class CodeAnalysis(BaseModel):
    """代码分析报告"""
    file_name: str = Field(..., description="File name being analyzed")
    issues: List[CodeIssue] = Field(default_factory=list, description="List of identified issues")
    overall_quality: str = Field(..., description="Overall code quality assessment: excellent/good/fair/poor")
    recommendations: List[str] = Field(default_factory=list, description="Suggestions for improvement")

### 创建 OpenAI Pydantic Program
program = OpenAIPydanticProgram.from_defaults(
    output_cls=CodeAnalysis,
    prompt_template_str="""
    Please analyze the following code and generate a detailed analysis report:

    {code}

    Requirements:
    1. Identify potential issues in the code
    2. Assess code quality
    3. Provide suggestions for improvement
    """,
    verbose=True
)

### 示例代码
sample_code = """
def process_data(data):
if data is None:
return
for item in data:
if item > 100:
print("Large value found")
else:
print("Small value")

让我们运行下面的代码片段来获得质量分析:

try:
    analysis = program(code=sample_code)
    print(f"File Analysis Report: {analysis.file_name}")
    print(f"Overall Quality: {analysis.overall_quality}")
    print("\nIssues Found: ")
    for issue in analysis.issues:
        print(f"- Line {issue.line_number}: {issue.issue_type}")
        print(f"  Description: {issue.description}")
        print(f"  Severity: {issue.severity}")
    print("\nImprovement Suggestions: ")
    for rec in analysis.recommendations:
        print(f"- {rec}")
except Exception as e:
    print(f"Error occurred during analysis: {e}")

前面的代码会生成如下输出:

File Analysis Report: code.py
Overall Quality: fair
Issues Found:

Alex,你可以从前面的输出中观察到以下几点:

第 3 行存在一个潜在 bug。如果 data 为 None,函数会直接返回而不处理数据,这可能导致意外行为。不过,严重程度为 medium。

类似地,第 5 行也存在另一个潜在 bug。这里将 item 与 100 进行比较可能并不合适。因此,应考虑使用 > 而不是 >。该 bug 的严重程度仍然是 low。

我建议的改进是:

更恰当地处理 data 为 None 的情况。

考虑在与 100 比较时使用 > 而不是 >

Function call 解析

通过使用 Function Calling 或 Tool Calling,大语言模型可以返回结构化数据,并解析 function_call 字段,动态生成后续函数调用的名称和参数。

下面的代码示例演示如何使用 Function Calling 进行解析:

from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field

## 定义 Tool Schema
class get_weather(BaseModel):
    """获取天气信息"""
    location: str = Field(..., description="City name")
    temperature: float = Field(..., description="Temperature")

## 初始化大模型
llm = ChatDeepSeek(model="deepseek-chat")

## 绑定 Tool
llm_with_tools = llm.bind_tools([get_weather])

## 发送请求
response = llm_with_tools.invoke("Please tell me the weather in Shanghai")

## 解析输出
if response.tool_calls:
    for tool_call in response.tool_calls:
        print(f"Tool name: {tool_call['name']}")
        print(f"Parameters: {tool_call['args']}")
else:
    print("No tool call")

输出如下:

Tool name: get_weather
Parameters: {'location': 'Shanghai', 'temperature': 25}

虽然前面的例子很简单,但它构成了 Agentic 智能系统的起点。当大模型能够自主判断下一步需要调用哪个工具或函数,并根据检索知识或用户输入,以符合函数参数接口要求的格式输出数据时,就相当于为这些系统配备了一双能够灵活使用各种工具的手。

未来,通过 MCP、RAG 和 Agent,应用将能够连接外部系统,实现更强大的功能。准确的解析机制,是让大模型应用与外部系统顺畅互联的前提。

通过模型选择提升输出质量

调用大模型的方式有多种,通常分为三类:API 调用、在开源模型平台上部署,以及本地部署。

使用 APIs 访问大模型,例如 OpenAI 的 GPT-4 模型、Anthropic 的 Claude 模型等,是开发者快速集成大模型能力的常见方式。这种方式允许用户通过云服务直接调用 APIs,不需要本地部署。不过,这种方法依赖网络连接,成本也相对较高。

Hugging Face 和 ModelScope 是开源模型平台,各有优势。前者提供丰富的预训练模型库,例如 transformers,以及易用工具链,支持开发者进行模型微调和部署;后者由阿里云推动,重点面向中文场景,提供本地化模型和算力支持,从而降低中文 NLP 开发门槛。

Ollama 和 vLLM 是本地部署工具中的代表。Ollama 以轻量级设计著称,支持快速启动和运行 Llama 系列等开源模型,适合本地调试;vLLM 由加州大学伯克利分校团队开发,通过高效的 PagedAttention 技术优化内存管理,显著提升大模型推理速度,以满足生产环境中的高并发需求。

Alex,你要注意,第一种方法主要用于访问闭源商业模型,而后两种常用于访问开源模型。以下是目前常用模型:

GPT series,OpenAI:作为大模型时代的先行者,GPT 系列在各方面表现优秀,尤其擅长逻辑推理和指令遵循,是创新领域的领先者。

Claude series,Anthropic:作为新一代长上下文处理专家,支持最高 200K tokens 长上下文,Claude 3 强调安全性和低幻觉率。在数学和编程 benchmark 中表现突出,呈现出超越 GPT 的趋势。

Gemini series,Google:虽然 Google 的大模型不像前两者那样惊艳,但 Gemini 系列仍然有自身独特特点。它采用统一多模态架构,支持超长上下文,并增强了推理和编码能力。

DeepSeek series,DeepSeek:这是中国国内领先的大模型,采用 MoE 架构,也就是 Mixture of Experts,针对数学和编码能力进行了专门优化,并支持最长 128K tokens 上下文。DeepSeek-R1 是开源且可商用的。此外,DeepSeek 也发布了易部署的小型蒸馏版本。

Llama series,Meta:这是开源大模型,参数规模从 8B 到 70B 不等。Llama 以高推理效率和长上下文支持著称,并拥有丰富工具生态。

Qwen,阿里云:它针对中文处理进行了显著优化,参数规模覆盖 1.8B 到 72B,并支持多轮对话和插件扩展。

Mixtral,Mistral AI:它采用 MoE 架构,以较少的激活参数实现高性能,开源且可商用。

这些模型通常都在追求更低推理成本、更大上下文长度和多模态扩展,同时推动开源和商业生态并行发展,目标是实现技术民主化。

不同模型在性能、效率和应用场景上存在细微差异。选择模型时的关键考量如下:

模型规模和性能:一般来说,更大的模型具有更强生成能力,能够理解更复杂上下文。不过,随着模型规模增大,对计算资源和推理时间的需求也会显著上升。因此,在追求高性能的同时,需要在性能和资源消耗之间找到平衡。

任务适配性:不同模型可能在特定任务上表现更好。例如,一些模型擅长代码生成,而另一些模型可能更适合自然语言理解或对话生成。根据具体任务需求选择模型,有助于提升输出质量。

微调和定制化:通过微调技术,可以将预训练模型调整到特定领域或任务,从而提升其在特定应用场景中的表现。参数高效微调技术,例如 Adapter Learning 和 Low-Rank Adaptation,LoRA,通过引入少量可训练参数,实现高效模型定制。

自反馈机制:引入自反馈机制,让模型对自身初始输出进行自评估和修订,可以进一步提升生成内容的准确性和一致性。具体来说,该过程包括让模型先生成初始输出,然后审查该输出并提供反馈,最后基于反馈优化初始输出。

现在你已经知道如何选择模型了,接下来我们看看如何整合结果。

将检索结果整合进生成过程的方法

Alex:Lewis,除了通过调整 prompts 和切换不同大模型来提升输出质量之外,还有没有更复杂、更高级的生成技术?

Lewis:当然有。传统 RAG 流程会直接把检索到的信息输入生成器,也就是大模型。在这种情况下,检索和生成相对独立,检索结果只在输入阶段使用。检索质量直接影响生成器输出。如果检索失败或包含噪声,生成表现就会下降。由于这个过程不修改生成器,所以实现相对简单。

不过,retriever 和 generator 之间这种相对独立的结构,并不是整合二者的唯一方式。论文 A Survey on RAG Meeting LLMs: Towards Retrieval-Augmented Large Language Models 讨论了在生成过程中整合检索结果的方法。来源:arxiv.org/abs/2405.06…

检索过程与传统 RAG 相同,但在生成过程部分,列出了三种整合方式:

Input-level integration:该方法将检索内容与原始 query 一起传给生成器,类似传统 RAG 的处理方式。

image.png

图 8.2:从输入系统、粗略搜索增强到答案生成的数据流。

Output-level integration:先运行大模型基于输入生成输出,然后将该输出与检索结果结合起来,并使用加权整合方法进行优化,得到最终结果。

Intermediate-layer integration:作为最复杂的整合方法,它会将检索结果直接引入生成器内部层,实现检索结果和大模型的深度融合,以增强生成表现。

输入层整合

输入层整合会将检索内容和用户原始 query 组合起来,然后一起传给生成器处理。这个过程类似于开卷考试时查找答案,并把这些参考资料作为输入,帮助生成器产出更准确输出。

生成器会将用户初始问题与检索到的相关文档、段落或实体拼接,然后通过生成器处理。这样,生成器可以直接利用检索内容来生成答案或文本。

Retrieval-augmented language model,REALM,是输入层整合的典型例子,实际上也代表了标准 RAG 架构。REALM 在预训练阶段引入检索机制,使模型能够实时访问外部知识,从而增强生成表现。具体而言,该模型将检索到的段落与输入问题结合起来,以提高答案准确性。

这种方法的优势在于相对简单且易于实现,同时在处理大量相关文档时也能保持输出连贯。不过,如果检索到的文档数量过多,拼接后的输入可能超过大模型可以处理的最大序列长度,从而无法处理。

输出层整合

输出层整合会将生成器生成的内容与检索信息结合起来,并进行加权处理,以产生更准确结果。这种方法类似于在生成初始答案后,参考外部资料对初始答案进行校对和优化。

生成每个输出时,模型会计算两个概率分布:一个是模型对下一个词预测得到的概率分布,另一个由检索上下文提供,例如相似文本片段。然后,模型对这两个概率分布进行加权整合,生成最终答案。这种整合方式允许模型在生成过程的最终阶段纳入检索信息,从而提升结果准确性。

K-nearest neighbor language model,kNN-LM,是输出层整合的典型例子。生成输出后,该模型会根据检索到的相似上下文调整生成词的概率分布,从而减少错误生成和幻觉。这种方法具有灵活性,因为它可以在生成器输出之后应用,而不干扰模型中间过程,并且能够更好地处理复杂上下文环境。

不过,输出层整合面临的挑战是如何有效加权检索结果和生成器输出。尤其是当检索结果与生成器输出之间存在不一致时,如何找到合适权衡,会成为一个难题。

中间层整合

中间层整合会将检索信息引入生成器内部,也就是中间层,使这些信息能够在生成过程的多个阶段影响模型输出。这种方法类似于写文章时不时参考资料,并不断根据这些资料调整内容。

在这个过程中,检索到的文档片段会被转换成稠密向量,并通过 attention mechanisms 或其他整合模块,在生成器的某个中间层融入生成过程。这使生成器能够在输出生成过程中动态地与检索信息交互,在保持生成过程流畅的同时增强信息交互。

Retrieval-Enhanced Transformer,RETRO,是中间层整合的典型例子。RETRO 将检索到的文本块引入其生成器中间层,使模型能够在生成过程中动态访问这些信息,从而生成更连贯、更准确的文本。

与输入层和输出层整合相比,中间层整合提供了更多信息交互机会,并允许检索信息在生成的不同阶段被多次使用。这种方法特别适合需要处理大量检索信息并生成高质量长文本的任务。

不过,这种方法实现起来相对复杂,因为它需要修改模型内部结构,并需要大量计算资源来支持中间层的多次信息交互。

Self-RAG 生成

我们在本章前面介绍过 CRAG。Self-reflective retrieval-augmented generation,也就是 Self-RAG,与 CRAG 有一定相似性。

Self-RAG 是一种将自反思 / 自评估机制整合进 RAG 架构的策略。这个概念源自华盛顿大学论文 Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection。Self-RAG 的核心在于,通过自动化决策流程,逐步验证检索文档的相关性和生成内容的准确性。该方法允许大模型动态决定是否执行检索,并评估生成内容,从而提升生成文本的质量和事实性,减少偏差或所谓幻觉。

Self-RAG 具有以下三个主要特征:

image.png

图 8.3:RAG 和 Self-RAG 方法对比。

前面的图突出了以下特征:

按需检索:根据输入问题的具体需求,大模型可以动态决定是否执行检索操作,避免不必要的检索过程。

自我评估:在生成过程中,大模型使用特殊 reflection token 评估自身输出,确保生成内容与检索知识一致。

多维控制:通过引入 critic model,可以从多个粒度视角对生成文本进行评估和控制,以提升最终输出质量。

Self-RAG 的主要步骤包括:

判断是否需要检索:首先,系统判断是否有必要从 retriever 获取信息。该步骤的输入是用户 query,也就是 x,输出是是否继续检索的决定,包括 Yes、No 或 Continue,以适应不同处理需求。

判断检索文档的相关性:一旦确定需要检索,系统会评估检索文档 D 中每个文本块 d 的相关性。该步骤输入包括原始问题 x 和检索到的文本块 d。系统通过模型分析判断这些文档内容是否与问题 x 相关。输出是 RelevantIrrelevant

确认对生成内容的支持程度:接下来,系统会检查生成内容是否受到检索文档支持。该步骤输入包括原始问题 x、检索文本块 d 和生成内容 y。系统会评估生成信息是否基于检索文档提供的事实。输出为 Fully SupportedPartially SupportedNo Support

评估生成内容质量:最后,系统会进一步评估生成内容质量,以判断它是否有效回答了问题 x。该步骤输入包括原始问题 x 和生成内容 y。评分范围为 1 to 5,反映生成内容的完整性和有效性。

关于使用 LangGraph 实现 Self-RAG 的示例,请参考本书 GitHub 仓库:github.com/PacktPublis…

RRR 动态生成优化

Rewrite-Retrieve-Read,简称 RRR,源自论文 Query Rewriting for Retrieval-Augmented Large Language Models,是一种优化 RAG 系统的方法。在传统 RAG 系统中,用户原始 query 会被直接用于检索相关文档,随后生成模型基于这些文档生成答案。然而,由于原始 query 可能含糊或不完整,这会影响检索效果。通过 RRR 方法,在检索前重写 query,可以提升检索结果的相关性和质量。

RRR 的主要流程如下:

Rewrite:使用大模型重写用户原始 query,生成更清晰、更具体的 query 版本。

Retrieve:使用重写后的 query 从网络中检索相关文档。

Read:将检索到的文档送入生成模型,生成最终答案。

image.png

图 8.4:RRR 从 query 到 response 的阶段,以及强化学习步骤。

通过在检索之前重写 query,RRR 可以更准确地捕捉用户意图,提升检索结果相关性,从而增强生成答案质量。

三种相似方法的对比如下:

Retrieve-then-read 方法会直接使用输入进行检索,然后让大模型阅读检索文档并生成答案。

RRR 方法会先使用大模型将输入重写成更好的检索 query,再使用该 query 进行 web search,最后让大模型阅读检索文档并生成输出。

Trainable rewrite-retrieve-read 方法会先使用一个小型预训练语言模型重写 query,然后执行 web retrieval 获取文档,最后让大模型阅读检索文档并生成输出。所谓 trainable,是指引入强化学习 reward 机制来优化 rewriter。

下图展示三种方法的对比:

image.png

图 8.5:RRR 动态生成三种方法对比。

RRR 的核心在于通过一个优化、重写和检索的循环过程,提升生成结果质量。系统会基于初始生成内容评估输出,如果答案不完整或不准确,就会检索更多相关文档,并据此修改和增强初始生成内容。

这种方法类似于 Self-RAG 中的自反思机制,可以确保最终生成答案基于充分信息,并由高质量文档支持。

总结

响应生成是整个 RAG 流程中操作空间相对有限的一个阶段。本章探讨了通过 prompt 优化提升生成内容质量的方法,如何使用输出解析机制控制生成内容格式,以及如何通过选择大模型提升输出质量等内容。

在生成过程中,整合检索结果有三种方式:输入层整合、输出层整合和中间层整合。

此外,Self-RAG 和动态生成优化等技术的应用,可以进一步提升生成内容的准确性和可控性。

在下一章中,我们将探索如何评估 Retrieval-Augmented Generation,也就是 RAG 系统。