我要提问
PROJECT CASE / 003

智能问答大模型应用实战

Python + LangChain + LLM + Milvus,从语料到流式上线的 RAG 工程落地。

智能问答大模型应用实战:Python + LLM 搭建领域 RAG 问答系统

智能问答大模型应用实战项目示意图

项目背景

某企业内部知识库沉淀了上千份产品手册、FAQ 和历史工单,员工查问题主要靠关键词搜索 + 翻文档,平均定位一个答案要 8 分钟。早期尝试直接接通用大模型,但模型对企业私有术语一问三不知,还会出现「一本正经地编造」的幻觉,根本不敢直接用。

本次目标是搭一套基于 RAG(检索增强生成)的领域问答系统:用企业自有语料做向量检索,把检索到的片段塞进 Prompt 让模型「看着答」,把通用模型 + 私有知识结合起来。技术栈选 Python + LangChain + Milvus + 国内某开源大模型,要求回答可溯源、首字延迟低于 2s、支持流式输出。

RAG 不是「调个 API」那么简单,真正的工程量在语料处理、检索质量与 Prompt 调优这三处,模型反而不是瓶颈。

架构设计

系统分「离线索引」与「在线问答」两条链路。离线把语料清洗、切片、向量化后写入 Milvus;在线接收用户问题,先检索 Top-K 相关片段,再用 Prompt 组装后调大模型流式生成,同时返回引用来源。

离线索引:文档 → 清洗 → 切片(Chunk) → Embedding → Milvus
在线问答:问题 → 改写 → 向量检索 TopK → 重排序 → Prompt 组装 → LLM 流式生成 → 引用溯源

关键设计:切片按语义段落 + 重叠窗口,避免把一句话拦腰切断;检索后用交叉编码器做一次重排序(rerank),把真正相关的片段往前顶;Prompt 里强约束「只能依据给定资料回答,资料中没有就回答不知道」,从源头压制幻觉。

核心实现

1)语料切片:用 LangChain 的 RecursiveCharacterTextSplitter,按段落 → 句号 → 字符层层递进切,并保留 80 字符重叠,保证上下文不断裂。

# index/chunker.py —— 语义切片 + 重叠窗口
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=80,
    separators=["\n\n", "\n", "。", ";", ",", " ", ""],
)

def chunk_docs(docs: list[str]) -> list[str]:
    chunks = []
    for d in docs:
        chunks.extend(splitter.split_text(d))
    return chunks

2)向量检索:用 bge-large-zh 做 Embedding 写入 Milvus,查询时取 Top-8,再用 bge-reranker 重排序保留 Top-3,显著提升相关性。

# retrieve/search.py —— 向量检索 + 重排序
from pymilvus import Collection

def search(question: str, top_k: int = 8) -> list[dict]:
    q_vec = embed(question)                      # query 向量化
    col = Collection("kb_chunks")
    col.load()
    hits = col.search(
        data=[q_vec], anns_field="vector",
        param={"metric_type": "IP", "params": {"nprobe": 16}},
        limit=top_k, output_fields=["text", "doc_id"],
    )[0]
    passages = [{"text": h.entity.get_value("text"),
                 "doc_id": h.entity.get_value("doc_id"),
                 "score": h.score} for h in hits]
    return rerank(question, passages, top_n=3)   # 交叉编码器重排

3)Prompt 组装与流式生成:把检索片段编号塞进 system prompt,要求模型引用编号;用 SSE 把生成 token 流式推给前端,首字延迟控制在 2s 内。

# answer/chain.py —— Prompt 组装 + 流式生成
SYS_PROMPT = """你是企业知识助手,只能依据下方资料回答。
资料中没有的内容,必须回答"资料中未提及"。
回答末尾用 [1][2] 形式标注引用的资料编号。
资料:
{context}
"""

def answer_stream(question: str):
    passages = search(question)
    context = "\n\n".join(
        f"[{i+1}] doc={p['doc_id']}\n{p['text']}" for i, p in enumerate(passages)
    )
    messages = [
        {"role": "system", "content": SYS_PROMPT.format(context=context)},
        {"role": "user", "content": question},
    ]
    for chunk in llm.stream(messages):           # SSE 逐 token 推送
        yield chunk
    yield {"sources": [{"doc_id": p["doc_id"]} for p in passages]}
  • 检索召回率(Top3 命中):63% → 88%(加 rerank 后)
  • 幻觉率(人工抽检):22% → 4%
  • 首字延迟:6.4s → 1.7s(流式后)
  • 平均定位答案时间:8 分钟 → 12 秒

技术难点

难点一:切片粒度。切太粗,一个 chunk 塞太多无关内容稀释相关性;切太细,上下文断裂答不全。最终通过对比 256/512/1024 三种 chunk_size 在评测集上的召回率,确定 512 + 80 重叠为最佳。评测集是人工标注的 200 个真实问题,这是质量基石。

难点二:幻觉压制。即便给了资料,模型偶尔仍会「脑补」。我们在 Prompt 里加了「资料中未提及」硬约束 + 输出引用编号,并在生成后做一次「引用校验」:检查回答里的编号是否都在检索片段范围内,不在则判定为高风险并降级为「未找到相关资料」。

# answer/verify.py —— 引用校验,压制幻觉
import re

def verify(answer: str, passages: list[dict]) -> bool:
    refs = set(int(x) for x in re.findall(r"\[(\d+)\]", answer))
    valid = set(range(1, len(passages) + 1))
    # 引用越界或全是资料外的编号 → 判定幻觉
    if refs and not refs.issubset(valid):
        return False
    return True

难点三:流式输出的中断与异常处理。SSE 连接中途断开时,已生成的内容要能落库做「未完成会话续写」。我们在每个 token 推送后异步写 Redis 缓存,连接断开时标记会话状态,用户重连可续上。

踩坑复盘

坑 1:Embedding 模型与 LLM 不匹配导致检索差。最初用某闭源 Embedding,但中文领域召回率只有 63%。换成 bge-large-zh 后召回到 81%。教训:中文场景 Embedding 模型选型比 LLM 更关键,必须用真实语料跑评测集,不能只看榜单。

坑 2:Milvus 索引参数没调。默认 nprobe=10 在百万级向量下召回率掉得明显,调到 16 后召回率 +5%,延迟仅增加 8ms。nprobe 是召回与延迟的核心旋钮,要按数据量校准。

坑 3:把检索片段一股脑塞 Prompt 导致超 token 上限。早期没做长度控制,长问题检索回 8 个片段直接超模型上下文窗口被截断。加了「片段总长度不超过 3000 token、超出按 rerank 分数裁剪」的策略后稳定。教训:Prompt 长度必须有预算控制。

RAG 的胜负手不在模型大小,而在「评测集 + 切片粒度 + rerank + Prompt 约束」这四件地基是否夯实。

项目总结

系统上线后覆盖 9 个内部知识域,员工问答满意度从原搜索方案的 54% 提到 86%,平均定位答案时间从 8 分钟降到 12 秒。整个项目最大的认知更新是:RAG 不是模型问题而是工程问题,一个扎实的评测集 + rerank + Prompt 约束,比换更大的模型收益更直接。

  • 检索命中率:63% → 88%
  • 幻觉率:22% → 4%
  • 首字延迟:6.4s → 1.7s
  • 用户满意度:54% → 86%
返回项目列表