《从零搭建RAG系统第6天:查询LLM生成问题答案》

0 阅读5分钟

《从零搭建RAG系统第6天:查询LLM生成问题答案》

大家好,我是老赵,一名程序员老兵,平时主要从事企业级应用开发,最近打算从零学习、并落地一套完整的RAG检索增强生成系统。不搞虚头理论,全程边学边做,把遇到的问题、踩过的坑、能直接跑通的代码,全都真实记录下来。

前五天我们完成了环境搭建、Milvus部署、文档向量化以及检索匹配、并安装了Ollama在本地运行LLM模型。今天第6天的目标是将优化后的检索片段交给大模型(LLM),让模型基于检索到的上下文生成准确、简练的回答,完成RAG的最后一段闭环。

一、今日目标

  1. 将第4天优化后的检索片段与用户问题拼装成高质量prompt;
  2. 调用LLM:本地轻量模型qwen3:4b;
  3. 提供后处理与答案可信度控制技巧(来源标注、简短回答);
  4. 给出可直接运行的 qa_pipeline    .py 完整示例。

二、前置准备(衔接 Day4)

  • 环境:已激活 rag-env、Milvus 已运行且第3天的向量已存入;
  • 第4天脚本已能输出 optimized_results(格式为列表,包含键 匹配度文档片段);
  • 可选:如果使用云 API,请准备好相应的 API Key(本文优先推荐本地方式以降低门槛)。

三、关键思路(简要)

  • optimized_results 中选取若干(通常 1~3 个)最高相关片段作为上下文;
  • 使用明确的Prompt模板,引导LLM基于上下文回答,并要求给出引用来源;
  • 设定模型生成参数(如 temperaturemax_tokens)以控制答案的创造性与长度;
  • 对生成结果做简单后处理:裁剪、去重、对匹配度低的结果触发回退策略(如提示“未找到足够证据”)。

四、Prompt 模板(参考,可调整)

你是一个专业的技术文档助手。下面给出一些文档片段和用户问题,请基于这些片段给出准确、简短的回答(最多3段),并在末尾用“来源:”标注使用到的片段序号。
上下文片段:
1. 【片段1内容】
2. 【片段2内容】
...
用户问题:{question}
要求:
- 以简洁的中文回答问题;
- 若证据不足,请说明“未检索到足够相关内容”;
- 在回答末尾列出“来源:片段1, 片段2”形式的引用。

五、实现(代码示例)

下面的示例假设你已在 question_retrieval.py 中得到了 optimized_results,接下来我们把生成逻辑独立成 question_answering.py

import sys

from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama.llms import OllamaLLM

class CustomStreamingHandler(BaseCallbackHandler):
    def on_llm_new_token(self, token, **kwargs):
        sys.stdout.write(token)
        sys.stdout.flush()

template = """你是一个专业的技术文档助手。参考下面的上下文片段回答问题,回答要简明扼要。\n\n
上下文片段:\n{ctx_text}\n\n
用户问题:{question}\n\n
要求:用中文回答,若证据不足请说明。最后标注来源片段编号。"""
prompt = ChatPromptTemplate.from_template(template)
model = OllamaLLM(base_url="http://localhost:11434",
                  model="llama3.2:latest",
                  callbacks=[CustomStreamingHandler()])
chain = prompt | model
optimized_results = [
    {"匹配度": 0.12, "文档片段": "RAG 是检索增强生成框架,将检索到的相关文档作为上下文提供给 LLM。"},
    {"匹配度": 0.33, "文档片段": "RAG 可以降低模型幻觉,提高事实性回答。"}
]
contexts = [r['文档片段'] for r in optimized_results[:3]]
chain.invoke({"question": "什么是RAG?", "ctx_text": "\n\n".join(contexts)})

六、后处理与可信度控制

  1. 源证据标注:在Prompt中强制要求模型列出使用到的片段编号,便于溯源与人工审核;
  2. 回退策略:当 optimized_results 为空或匹配度较差(如平均距离>阈值),直接返回“未检索到足够相关信息”,避免模型凭空生成错误信息;
  3. 分段生成:对于复杂问题,可先要求模型给出“摘要+参考片段”,再要求补充细节,降低hallucination;
  4. 多模型对比(可选):并行调用两个不同模型,若输出一致可信度更高;若不一致,触发人工审阅。

七、常见问题与解决方案

  • 若模型回答无证据或回答错误:检查上下文片段是否覆盖问题关键词,尝试扩大limit或降低chunk_size后重新向Milvus载入;
  • 回答啰嗦或生成多余信息:把temperature设为0.0并在prompt中明确“最多3段”;
  • 本地模型显存不够:使用量化模型或把 device 改为 -1(CPU),并减小 max_length

八、完整示例(整合Day4的检索与Day6的生成,qa_pipeline.py

示例给出一个从检索到生成的完整小脚本,便于本地跑通:

# qa_pipeline.py - 端到端示例(检索 + 生成)
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
from pymilvus import connections, Collection
from sentence_transformers import SentenceTransformer
from mfj_langchain.custom_streaming_handler import CustomStreamingHandler

def retrieve(question, top_k=3):
    connections.connect(alias='default', host='localhost', port='19530')
    collection_name = 'rag_test_collection'
    collection = Collection(collection_name)
    collection.load()
    embedder = SentenceTransformer('BAAI/bge-small-zh')
    q_emb = embedder.encode(question)
    search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
    results = collection.search(data=[q_emb], anns_field='doc_embedding', limit=top_k, param=search_params, output_fields=['doc_text'])
    connections.disconnect('default')
    optimized_results = [r.entity.get('doc_text') for r in results[0] if r.distance <= 0.6]
    return optimized_results

def generate_answer_local(question, contexts):
    model = OllamaLLM(base_url="http://localhost:11434",
                      model="qwen3:4b",
                      temperature=0.0,
                      callbacks=[CustomStreamingHandler()])
    prompt = build_prompt_template()
    chain = prompt | model
    chain.invoke({"question": question, "ctx_text": "\n\n".join(contexts)})

def build_prompt_template():
    template = """"你是一个专业的技术文档助手。参考下面的上下文片段回答问题,回答要简明扼要。\n\n
    上下文片段:\n{ctx_text}\n\n
    用户问题:{question}\n\n
    要求:用中文回答,若证据不足请说明。最后标注来源片段编号。"""
    prompt = ChatPromptTemplate.from_template(template)
    return prompt

if __name__ == '__main__':
    q = "什么是RAG?"
    contexts = retrieve(q, top_k=3)
    if not contexts:
        print("未检索到足够相关内容,无法生成答案。")
    else:
        generate_answer_local(q, contexts)

九、今日小结

  1. 第6天主要完成“检索结果 → LLM生成答案”的接入与实战示例;
  2. 推荐先用本地轻量模型验证流程,再迁移到云端模型以提升效果;
  3. 关键调参点:检索阈值(Day4)、生成时 temperaturemax_tokens
  4. 下一步:使用FastAPI搭建API接口,实现远程调用。

我是老赵,一名程序员老兵,全程真实记录RAG从零搭建全过程。本系列会持续更新,最后整理成完整视频教程+源码+部署手册。

关注 老赵全栈实战,不迷路,一起从0落地RAG系统。