跳转到内容
123xiao | 无名键客

《从原型到生产:中级开发者构建企业级 AI 问答系统的检索增强生成(RAG)实战路径》

字数: 0 阅读时长: 1 分钟

从原型到生产:中级开发者构建企业级 AI 问答系统的检索增强生成(RAG)实战路径

很多团队做企业级 AI 问答系统时,第一版都很像:拿一批文档,切块、向量化、接个大模型,然后做出一个“能回答”的 Demo。
问题是,Demo 能跑,不代表系统能用;系统能用,也不代表能上线。

我自己见过不少中级开发者卡在这个阶段:原型阶段效果看着不错,一到生产环境就开始暴露问题——答案不稳定、检索命中差、延迟飙升、权限穿透、文档更新后回答还是旧内容。RAG(Retrieval-Augmented Generation,检索增强生成)真正难的地方,不是“接上 LLM”,而是把检索、生成、权限、观测、成本、稳定性串成一条可维护的工程链路。

这篇文章我换一个更偏架构落地的角度来讲:如果你已经会做一个基础 RAG Demo,下一步怎么把它演进成企业级问答系统。


背景与问题

企业知识问答和通用聊天不一样,它有几个非常现实的约束:

  • 知识是私有的:内部制度、产品手册、运维 SOP、合同条款不能依赖模型预训练记忆。
  • 答案要可追溯:业务方通常会追问“这句话从哪来的”。
  • 权限不能出错:员工 A 不该看到员工 B 能看到的内容。
  • 文档持续变化:知识库不是一次性导入,而是长期增量更新。
  • 线上指标要可控:延迟、成本、召回率、准确率都得看得见。

如果只做一个最小原型,常见架构往往是:

  1. 文档切块
  2. 生成向量
  3. 存入向量库
  4. 用户提问
  5. 检索 top-k
  6. 拼接上下文
  7. 调 LLM 生成答案

这条链路没错,但到了企业场景,问题会集中爆发:

原型阶段常见症状

  • 问得稍微绕一点就答非所问
  • 明明文档里有,系统就是检索不到
  • 不同提法答案不一致
  • 上下文太长,token 成本高
  • 知识库更新后,旧答案“阴魂不散”
  • 某些敏感文档被错误召回
  • 高并发下 P95 延迟不可接受

所以从架构角度看,企业级 RAG 的目标不是“做出回答”,而是:

在可控成本和安全边界下,稳定地回答、引用正确内容,并支持持续迭代。


核心原理

RAG 的本质,可以拆成三个问题:

  1. 找什么:如何从大量文档中找到可能相关的片段
  2. 信什么:如何让模型更依赖检索证据,而不是自由发挥
  3. 怎么管:如何让整个链路在生产环境可观测、可回滚、可扩展

一个更接近生产的 RAG 链路

flowchart LR
    A[用户问题] --> B[查询预处理]
    B --> C[权限过滤]
    C --> D[多路检索<br/>向量检索/关键词检索]
    D --> E[重排序 Rerank]
    E --> F[上下文构造]
    F --> G[LLM 生成答案]
    G --> H[引用与置信度输出]
    H --> I[日志/评测/反馈闭环]

这和原型版最大的差异有三点:

  • 不只做向量检索,而是混合检索
  • 检索后不直接喂模型,而是加重排序
  • 输出后不算结束,而是进入评测和反馈闭环

1. 数据摄取不是“导一次就完事”

企业知识往往来自:

  • PDF、Word、Excel
  • Wiki、Confluence、飞书文档
  • 工单、FAQ、数据库导出
  • API 实时数据

真正上线后,你会发现解析质量直接决定上限
比如 PDF 中表格丢了结构,制度文档标题层级错乱,最后切块再精细也没用,因为输入已经坏了。

一个更稳妥的数据处理流程通常是:

flowchart TD
    A[原始文档] --> B[解析与清洗]
    B --> C[结构化提取<br/>标题/段落/表格/元数据]
    C --> D[切块 Chunking]
    D --> E[Embedding]
    E --> F[索引写入]
    C --> G[关键词索引]
    F --> H[向量库]
    G --> I[倒排索引]

2. 检索不是只靠向量相似度

很多人做 Demo 时只用向量检索,结果一上线就发现:

  • 专有名词、版本号、SKU、错误码检索效果差
  • 用户按关键词问时,向量结果不稳定
  • 相似语义很多,但真正关键字段没命中

所以企业级问答里,**混合检索(Hybrid Search)**几乎是标配:

  • 向量检索:适合语义相近、表述变化大的问题
  • BM25/关键词检索:适合精确匹配术语、产品名、编号
  • 过滤条件:部门、文档类型、时间范围、权限标签
  • Rerank 重排序:对候选结果做更精细排序

经验上,我更推荐把它理解为:

向量检索负责“广撒网”,关键词检索负责“兜底精准项”,Rerank 决定最后谁进 prompt。

3. 生成不是“把 top-k 全塞进去”

上下文构造是很多系统效果不稳的根源。
如果你简单地把 top-5 拼起来给模型,可能出现:

  • 片段之间互相矛盾
  • 上下文太长,关键信息被淹没
  • prompt 冗余,成本上升
  • 模型把低质量片段当成高优先级证据

所以生产实践里通常会做:

  • 去重相似 chunk
  • 按文档/章节聚合
  • 保留标题和来源
  • 截断无关内容
  • 显式要求“仅基于已给资料回答”
  • 无证据时返回“不确定”

4. 评测比“感觉不错”更重要

RAG 最大的错觉是:你问了几个例子都挺好,于是以为系统可用了。
实际上,企业场景必须建立一套评测集:

  • 是否召回正确文档
  • 是否引用了正确证据
  • 是否回答完整
  • 是否编造
  • 是否违反权限策略

没有评测,你就无法知道:

  • 是切块问题
  • 是 embedding 问题
  • 是检索参数问题
  • 还是 prompt 问题

方案对比与取舍分析

企业级 RAG 不是一套唯一答案,而是一组权衡。

方案一:纯向量检索 + 单轮生成

优点

  • 架构简单
  • 开发快
  • 适合验证场景价值

缺点

  • 对术语和编号不友好
  • 可解释性弱
  • 线上稳定性一般

适用

  • 内部试点
  • 小规模知识库
  • 单部门场景

方案二:混合检索 + Rerank + 引用输出

优点

  • 效果更稳
  • 对复杂查询更鲁棒
  • 更适合上线

缺点

  • 链路更长
  • 成本和延迟更高
  • 需要更多运维能力

适用

  • 企业知识问答主流方案
  • 文档类型复杂
  • 对准确率要求较高

方案三:分层路由 + 多索引 + 多模型

例如:

  • FAQ 命中走轻量模型直接答
  • 制度类走高精度 RAG
  • 数据类问题转 SQL/工具调用
  • 高风险问题走人工兜底

优点

  • 成本更优
  • 体验更像“系统”而不只是聊天框
  • 可针对不同问题做专门优化

缺点

  • 架构复杂度显著上升
  • 调试和评测更难

适用

  • 已有一定规模和调用量
  • 多业务线接入
  • 成本与 SLA 都有要求

容量估算:上线前最好先算清楚

这一部分经常被忽略,但很关键。
你不用一开始算得特别精细,先做数量级估算就能避免很多坑。

1. 存储规模

假设:

  • 文档总量:10 万篇
  • 平均每篇切成 20 个 chunk
  • 总 chunk 数:200 万
  • 每个向量维度:1536
  • 向量类型:float32

仅向量大小约为:

2000000 × 1536 × 4 bytes ≈ 12.3 GB

再加上:

  • 元数据
  • 倒排索引
  • 主键和权限标签
  • 副本

实际存储往往会到几十 GB 甚至更高。

2. 吞吐估算

如果:

  • 峰值 QPS = 20
  • 每次检索 2 路
  • 每次重排序 30 个候选
  • LLM 平均响应 2~5 秒

那么瓶颈很可能不在向量库,而在:

  • Rerank 服务
  • LLM 并发限制
  • Prompt 构造和日志链路

3. 成本敏感点

最烧钱的通常不是 embedding 一次性导入,而是:

  • 高频在线生成
  • 超长上下文
  • 重复问题没有缓存
  • 过大的 top-k
  • 不必要地调用高价模型

实战代码(可运行)

下面给一个可运行的最小 RAG 架构示例
它不依赖外部向量数据库,而是用 Python 做一个简化版流程,帮助你把关键环节串起来:

  • 文档切块
  • TF-IDF 检索
  • 简单关键词打分
  • 混合排序
  • 构造上下文
  • 输出引用答案

这不是最终生产实现,但非常适合先把工程骨架跑通。

目录结构建议

rag_demo/
├── app.py
├── requirements.txt
└── data/
    └── docs.txt

requirements.txt

fastapi==0.110.0
uvicorn==0.27.1
scikit-learn==1.4.1.post1
numpy==1.26.4

示例知识库 data/docs.txt

每段文档之间用 ---DOC--- 分隔:

标题:员工请假制度
内容:员工请假需至少提前1个工作日提交申请。3天以上病假需提供医院证明。年假优先通过HR系统发起。

---DOC---
标题:报销流程说明
内容:差旅报销需在出差结束后10个工作日内提交。发票抬头必须与公司主体一致。超标费用需主管审批。

---DOC---
标题:生产环境发布规范
内容:生产环境发布必须经过灰度验证。高风险变更需准备回滚预案。数据库变更必须在低峰时段执行并提前备份。

app.py

from fastapi import FastAPI, Query
from pydantic import BaseModel
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from typing import List, Dict
import re

app = FastAPI(title="Simple Enterprise RAG Demo")

DOC_PATH = "data/docs.txt"


class SearchResult(BaseModel):
    title: str
    content: str
    score: float


class AnswerResponse(BaseModel):
    question: str
    answer: str
    citations: List[SearchResult]


def load_docs(path: str) -> List[Dict]:
    with open(path, "r", encoding="utf-8") as f:
        raw = f.read()

    docs = []
    for block in raw.split("---DOC---"):
        block = block.strip()
        if not block:
            continue

        title_match = re.search(r"标题:(.*)", block)
        content_match = re.search(r"内容:(.*)", block, re.S)

        title = title_match.group(1).strip() if title_match else "未命名文档"
        content = content_match.group(1).strip() if content_match else block

        docs.append({
            "title": title,
            "content": content,
            "full_text": f"{title}{content}"
        })
    return docs


DOCS = load_docs(DOC_PATH)
CORPUS = [d["full_text"] for d in DOCS]
VECTORIZER = TfidfVectorizer()
DOC_MATRIX = VECTORIZER.fit_transform(CORPUS)


def keyword_score(query: str, text: str) -> float:
    query_terms = [w for w in re.findall(r"[\u4e00-\u9fa5A-Za-z0-9]+", query) if len(w) >= 2]
    if not query_terms:
        return 0.0
    hits = sum(1 for term in query_terms if term.lower() in text.lower())
    return hits / len(query_terms)


def hybrid_search(query: str, top_k: int = 3) -> List[SearchResult]:
    query_vec = VECTORIZER.transform([query])
    semantic_scores = cosine_similarity(query_vec, DOC_MATRIX)[0]

    results = []
    for idx, doc in enumerate(DOCS):
        kw = keyword_score(query, doc["full_text"])
        final_score = 0.7 * semantic_scores[idx] + 0.3 * kw
        results.append(SearchResult(
            title=doc["title"],
            content=doc["content"],
            score=round(float(final_score), 4)
        ))

    results.sort(key=lambda x: x.score, reverse=True)
    return results[:top_k]


def build_answer(question: str, citations: List[SearchResult]) -> str:
    if not citations or citations[0].score < 0.05:
        return "我没有在知识库中找到足够依据,建议补充问题背景或检查知识库。"

    best = citations[0]
    return (
        f"根据《{best.title}》,"
        f"{best.content}"
        f"\n\n如果你需要,我也可以继续把相关限制条件或操作步骤整理成清单。"
    )


@app.get("/ask", response_model=AnswerResponse)
def ask(question: str = Query(..., description="用户问题")):
    citations = hybrid_search(question, top_k=3)
    answer = build_answer(question, citations)
    return AnswerResponse(
        question=question,
        answer=answer,
        citations=citations
    )

启动方式

uvicorn app:app --reload

启动后访问:

curl "http://127.0.0.1:8000/ask?question=病假请假需要什么材料"

示例返回:

{
  "question": "病假请假需要什么材料",
  "answer": "根据《员工请假制度》,员工请假需至少提前1个工作日提交申请。3天以上病假需提供医院证明。年假优先通过HR系统发起。\n\n如果你需要,我也可以继续把相关限制条件或操作步骤整理成清单。",
  "citations": [
    {
      "title": "员工请假制度",
      "content": "员工请假需至少提前1个工作日提交申请。3天以上病假需提供医院证明。年假优先通过HR系统发起。",
      "score": 0.3812
    }
  ]
}

这段代码对应真实生产中的位置

这个示例虽然简化,但已经映射到生产链路中的关键模块:

  • load_docs:文档解析入口
  • hybrid_search:混合检索雏形
  • build_answer:上下文约束和回答模板
  • /ask:API 层

后续你可以逐步替换为:

  • TF-IDF -> Elasticsearch / OpenSearch
  • 本地排序 -> 专门的 rerank 模型
  • 模板回答 -> LLM
  • 本地文档 -> 企业知识摄取流水线
  • 无权限控制 -> 基于用户身份的过滤器

一条更完整的生产时序

sequenceDiagram
    participant U as 用户
    participant API as 问答服务
    participant ACL as 权限服务
    participant IDX as 检索层
    participant RR as Rerank服务
    participant LLM as 大模型
    participant OBS as 观测平台

    U->>API: 提问
    API->>ACL: 获取用户权限范围
    ACL-->>API: 可访问文档标签
    API->>IDX: 混合检索(问题+权限过滤)
    IDX-->>API: 候选文档TopN
    API->>RR: 重排序
    RR-->>API: TopK片段
    API->>LLM: 问题+上下文+约束Prompt
    LLM-->>API: 生成答案
    API->>OBS: 记录检索/生成/耗时/引用
    API-->>U: 答案+引用来源

常见坑与排查

这一节我尽量讲得“像真踩过坑”。因为很多问题不是原理不懂,而是上线后才发现哪里最脆。

1. 检索不到,但文档明明存在

可能原因

  • 切块太碎,关键句被拆散
  • 切块太大,噪声太多
  • 文档解析丢了标题、表格、列表结构
  • 只做向量检索,没有关键词兜底
  • 查询改写能力不足

排查思路

  1. 打印用户原问题
  2. 查看实际检索 query
  3. 看 top-20 是否出现目标文档
  4. 若出现但排名低,问题在 rerank
  5. 若完全没出现,问题在索引、切块、embedding 或过滤条件

实战建议

  • 先抽样 50 个失败问题,做人工诊断
  • 不要一上来就换 embedding,很多时候是切块策略错了
  • 保留标题、章节号、表格摘要,命中率会明显提升

2. 回答看似合理,但引用不对

这是企业场景里特别危险的一类问题。
用户看到答案流畅,往往不会第一时间怀疑,但业务方一核对就会发现“引用张冠李戴”。

可能原因

  • 相似内容过多,重排序不稳
  • prompt 没要求“基于引用回答”
  • 上下文中混入了互相矛盾的片段
  • 模型根据常识补全了答案

排查方法

  • 输出每次请求的最终上下文
  • 记录模型实际引用了哪些 chunk
  • 对比“正确文档是否被检索到”与“是否被放进 prompt”
  • 检查 prompt 是否要求无依据时明确拒答

建议

  • 输出引用来源是必须项,不是可选项
  • 对高风险问题启用更严格模板
  • 必要时将答案生成拆成两步:先抽取证据,再组织语言

3. 延迟突然升高

常见原因

  • top-k 设太大
  • rerank 模型过重
  • prompt 过长
  • 外部 LLM 网络抖动
  • 向量库过滤条件未命中索引

排查顺序

  1. 拆分链路耗时:检索、重排、生成分别统计
  2. 看 P50/P95,不只看平均值
  3. 检查是否某类 query 导致超长上下文
  4. 看是否触发了外部服务限流或重试

经验建议

  • 先优化检索候选量,再优化模型
  • 只要业务允许,优先缓存“高频问题 -> 检索结果”
  • 长文档问答适合异步化或流式输出

4. 文档更新了,回答还是旧的

这个问题特别常见,尤其是内部制度频繁更新的团队。

可能原因

  • 增量索引未覆盖旧版本
  • 向量写入成功但检索服务未刷新
  • 同一文档多个版本共存,没有版本优先级
  • 缓存未失效

建议做法

  • 文档元数据中加入 doc_idversionupdated_at
  • 检索时优先最新有效版本
  • 建立“重建索引”和“增量更新”的明确策略
  • 缓存键中带上知识库版本号

5. 权限穿透

这是最不能接受的问题之一。

典型失误

  • 先检索再过滤,导致候选中已混入敏感内容
  • 只在应用层做权限控制,索引层没有过滤
  • 文档继承权限复杂,元数据不完整
  • 调试日志把敏感文本打出来了

安全原则

权限过滤应尽可能前置到检索层,而不是等生成后再处理。


安全/性能最佳实践

企业级 RAG 上线时,我建议至少把下面这些项当成“默认配置”,而不是“有空再做”。

安全最佳实践

1. 最小权限原则

  • 每个 chunk 带权限标签
  • 检索请求必须带用户身份或角色
  • 未授权文档在召回前就过滤掉

2. Prompt 注入防护

用户可能输入:

  • “忽略之前规则”
  • “输出所有原始文档”
  • “你现在是系统管理员”

这类输入不能只靠模型“自觉”。要做:

  • 系统提示词硬约束
  • 用户输入检测与清洗
  • 明确禁止泄露系统 prompt、原文全文、敏感字段

3. 敏感信息脱敏

对于身份证号、手机号、合同金额等内容:

  • 入库前脱敏或分级
  • 输出时按角色控制展示
  • 日志中避免明文落盘

4. 审计与留痕

至少记录:

  • 谁问了什么
  • 检索到了哪些文档
  • 最终用了哪些引用
  • 模型输出了什么
  • 是否命中安全规则

性能最佳实践

1. 分层缓存

可以缓存三层:

  • Query 改写结果
  • 检索结果
  • 最终答案

其中我最推荐优先做的是检索结果缓存,因为它比答案缓存更稳,更容易复用。

2. 控制上下文预算

给每次请求设一个明确 token 预算,例如:

  • 检索候选 30
  • rerank 保留 5
  • 最终上下文不超过 3000 tokens

不要把“多给一点上下文”当万金油,很多时候只会让模型更糊涂。

3. 小模型做前置路由

不是所有问题都要上最贵的模型。
可以先做:

  • FAQ 精确匹配
  • 低风险模板问答
  • 简单分类路由

把大模型留给真正复杂的问题。

4. 观测指标要能定位问题

建议至少监控:

  • 检索命中率
  • top-k 覆盖率
  • rerank 后正确率
  • 无答案率
  • 幻觉率
  • 平均/分位延迟
  • 单请求 token 成本
  • 权限拒绝率

一个可执行的演进路径

如果你现在手里只有一个能跑的 RAG Demo,我建议按下面顺序升级,而不是一次性推倒重来。

第 1 阶段:把链路跑通

目标:

  • 有基本文档入库
  • 能检索
  • 能回答
  • 能展示引用

重点:

  • 不追求复杂模型
  • 优先保证可调试

第 2 阶段:把效果做稳

目标:

  • 引入混合检索
  • 优化切块
  • 建立评测集
  • 加入 rerank

重点:

  • 找到效果瓶颈在哪
  • 用数据替代主观感受

第 3 阶段:把系统做成“可上线”

目标:

  • 权限过滤
  • 日志与观测
  • 缓存和限流
  • 文档增量更新
  • SLA 管理

重点:

  • 防止安全事故
  • 提升稳定性和成本可控性

第 4 阶段:做成平台能力

目标:

  • 多知识库接入
  • 多租户支持
  • 路由与编排
  • 自动评测和回归测试

重点:

  • 让系统从“一个项目”变成“一个能力”

总结

从原型到生产,RAG 最容易被低估的不是模型能力,而是工程化复杂度。

如果你是中级开发者,我会给你几个非常务实的建议:

  1. 别急着换模型,先把检索链路看透
    很多效果问题,根本不在 LLM,而在解析、切块、召回和重排。

  2. 混合检索几乎是企业场景的默认选项
    纯向量检索可以做 Demo,但上线后通常不够稳。

  3. 引用、权限、评测,比“回答自然”更重要
    企业级问答系统首先要“可靠”,其次才是“聪明”。

  4. 先做可观测,再谈可优化
    没有日志、指标和样本集,你根本不知道该优化哪里。

  5. 系统要允许“不知道”
    一个会拒答的系统,往往比一个处处自信的系统更适合生产。

最后给个边界条件:
如果你的场景知识规模很小、更新不频繁、没有严格权限要求,那么没必要一上来就上复杂架构。
但只要你面向的是企业内部真实流程、多人使用、长期维护的问答系统,RAG 就应该从第一天开始按“生产系统”思考,而不是“聊天 Demo”思考。

这条路不会一步到位,但路径很清晰:先能用,再可靠,最后可规模化。


分享到:

上一篇
《Web3 钱包登录实战:基于 EIP-4361(Sign-In with Ethereum)构建安全可扩展的身份认证体系》
下一篇
《微服务架构下的分布式事务实战:基于 Saga 模式的设计、落地与故障补偿》