2025 AI 工程化实战:从 0 到 1 搭一个可落地的 RAG 应用

一、为什么还是 RAG,不直接微调?

  • 数据敏感 / 频繁更新:业务私有知识库变化快,RAG 的检索拼装相比微调上线周期更短。
  • 成本与可控性:微调与持续再训练成本高,RAG 以结构化组件可局部优化(embedding、召回、重排、缓存等)。
  • 合规与解释性:RAG 能回链路据(source of truth),更便于审计与解释。

结论:先用 RAG 快速可用,有稳定数据形态和 ROI 后,再考虑小范围指令微调/adapter。


二、目标与边界

我们要做一个文档问答型 RAG 应用:支持 PDF/Markdown/网页抓取,中文优先,返回答案 + 证据片段 ,带流式输出观测日志离线评测成本监控

成功标准:

  1. Top-k 证据可读、命中率高于基线(FAQ 测试集)。
  2. 平均首 token 延迟 < 1.5s,完整回答 < 5s(中等上下文)。
  3. 召回/重排/生成各环节可独立替换,便于 A/B。
  4. 单次问答平均可变现成本 < ¥0.02--0.2(视模型与长度而定)。

三、系统架构(模块化)

scss 复制代码
             ┌──────────┐
     Query → │  API 层  │  ← Metrics/Tracing
             └────┬─────┘
                  │
           ┌──────▼──────┐
           │  Orchestrator│  (Prompt模板/上下文裁剪/策略)
           └───┬─────┬───┘
               │     │
     ┌─────────▼─┐ ┌─▼─────────┐
     │  检索层    │ │  重排层    │
     │(向量+BM25) │ │(Cross-Enc)│
     └──────┬────┘ └─────┬──────┘
            │              │
       ┌────▼────┐    ┌───▼─────┐
       │  知识库  │    │  缓存层 │(答案/embedding)
       │ (pgvector│    │ (Redis) │
       └────┬─────┘    └───┬─────┘
            │              │
        ┌───▼──────────────▼───┐
        │   生成层(LLM Provider)│
        └───────────────────────┘
  • 检索:向量召回(dense)+ BM25(sparse)混合,常见组合是 pgvector + Meilisearch/Elastic。
  • 重排:Cross-Encoder(如 bge-reranker)把候选段落与问题两两打分。
  • 生成:大模型(可选闭源/开源)。
  • 缓存:问题归一化+Top-k 证据签名做二级缓存。
  • 观测:请求链路、Token 用量、延迟分解、命中率、答案可用度反馈。

四、数据入库:清洗与切块(Chunking)

切块原则

  • 语义自然段 为主,配合滑窗(overlap 50--120 token)减少跨段断裂。
  • 每块携带来源元数据:文档ID、页码、标题层级、更新时间、URL。
  • 中文 PDF 优先抽文本层;无文本层走 OCR(注意版面噪声)。
  • 统一字符集与标点(中文全角/半角),去脚注/页眉/目录噪声。

推荐切块大小:300--500 tokens;FAQ/条例可更小(150--300)。


五、落地选型(示例)

  • 向量库:PostgreSQL + pgvector(易运维,事务一致性好;MVP 足够)
  • 倒排检索:Meilisearch(部署轻 / 中文需要自定义分词)或 Elasticsearch(成熟但重)
  • Embedding :开源 bge-m3 / bge-large-zh-v1.5(中文强),或云端嵌入 API
  • 重排bge-reranker-v2-m3(CrossEncoder),小模型延迟低
  • LLM:按预算切换(如中型闭源 8--32k ctx;或本地 Qwen/Llama 系列)
  • 服务层:FastAPI(Python)+ Uvicorn
  • 缓存:Redis(问答结果 / 片段集合签名)
  • 观测:OpenTelemetry + Prometheus + Grafana(或直接接入 APM)

以上都是可替换件。先跑通,再迭代。


六、最小可用 Demo(FastAPI + pgvector)

1)数据库建表

sql 复制代码
-- PostgreSQL
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE IF NOT EXISTS documents (
  id TEXT PRIMARY KEY,
  title TEXT,
  url TEXT,
  updated_at TIMESTAMP
);

CREATE TABLE IF NOT EXISTS chunks (
  id TEXT PRIMARY KEY,
  doc_id TEXT REFERENCES documents(id),
  content TEXT NOT NULL,
  meta JSONB,
  embedding vector(1024) -- 视模型维度
);

-- 索引(ivfflat 需设置列表数,基于数据规模调参)
CREATE INDEX ON chunks USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
CREATE INDEX chunks_docid_idx ON chunks (doc_id);

2)入库脚本(简化)

python 复制代码
# ingest.py
import json, uuid
import psycopg2, psycopg2.extras
from pathlib import Path
from some_embedder import embed  # 替换为你的 embedding 调用

def chunk_text(text, size=400, overlap=80):
    tokens = text.split()
    out = []
    for i in range(0, len(tokens), size - overlap):
        out.append(" ".join(tokens[i:i+size]))
    return out

conn = psycopg2.connect("postgresql://user:pwd@localhost:5432/rag")
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)

doc_id = str(uuid.uuid4())
cur.execute("INSERT INTO documents (id, title, url, updated_at) VALUES (%s,%s,%s,NOW())",
            (doc_id, "示例文档", "https://example.com/doc",))

text = Path("sample.txt").read_text(encoding="utf-8")
for chunk in chunk_text(text):
    vec = embed(chunk)  # list[float]
    cur.execute("""
      INSERT INTO chunks (id, doc_id, content, meta, embedding)
      VALUES (%s,%s,%s,%s,%s)
    """, (str(uuid.uuid4()), doc_id, chunk, json.dumps({"source":"sample.txt"}), vec))
conn.commit()

3)检索 + 重排 + 生成(API)

python 复制代码
# app.py
from fastapi import FastAPI
from pydantic import BaseModel
import psycopg2, psycopg2.extras
from some_embedder import embed
from cross_encoder import rerank  # 返回[(chunk, score), ...]
from llm import generate_stream  # 生成器,yield tokens

app = FastAPI()
conn = psycopg2.connect("postgresql://user:pwd@localhost:5432/rag")

class Q(BaseModel):
    query: str
    topk: int = 8

@app.post("/qa")
def qa(q: Q):
    qvec = embed(q.query)
    cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
    cur.execute("""
      SELECT content, meta
      FROM chunks
      ORDER BY embedding <-> %s
      LIMIT %s
    """, (qvec, q.topk))
    candidates = [{"content": r["content"], "meta": r["meta"]} for r in cur.fetchall()]
    ranked = rerank(q.query, candidates, topn=4)  # CrossEncoder 重排

    context = "\n\n".join([c["content"] for c, _ in ranked])
    prompt = f"""你是检索增强问答助手。结合下列资料回答问题,标注出引用顺序[1][2]:
资料:
{context}

问题:{q.query}
请给出中文答案,并在结尾附上引用列表。
"""
    # 流式返回(伪代码)
    return generate_stream(prompt, references=[c["meta"] for c, _ in ranked])

生产中需要:异常兜底、输入清洗、长度裁剪、流式 SSE、超时/重试、打点埋点。


七、评测与回归:离线 & 在线

1)构造评测集

  • 从客服/搜索日志中抽取真实问题 (去隐私),人工配对目标答案应命中片段
  • 覆盖:事实性、枚举类、政策条款、歧义问题、极短与极长问题。

2)指标

  • 检索:Recall@k、nDCG@k、命中段数/平均位置。
  • 生成 :答案可用度 (3/5 分以上算可用)、事实性 (ClaimCheck/RAGAS 支持)、格式遵循度
  • 体验:TTFT(首 token 时间)、TBT(完整输出时间)、拒答率。
  • 成本:平均 Token、平均请求费用,缓存命中率。

3)回归流程

  • 每次更换 embedding / 重排 / 模型 / 切块策略 → 自动跑评测 → 导出对比报告。
  • 线上观测:把用户"有帮助/无帮助"点选接回训练集(RLHF/分类器可选)。

八、提示词(Prompt)与裁剪策略

结构化提示(简化示例):

css 复制代码
System:
你是企业知识库问答助手。必须基于"资料"回答,若资料无答案,请明确说"不确定"。

User:
问题:{query}
资料(按相关性降序):
[1] {chunk1}
[2] {chunk2}
[3] {chunk3}
要求:
- 先直接给出答案(不超过150字)。
- 然后列出要点(<=5条)。
- 最后输出"引用:[1][2]..."
- 如果不确定,请输出"需要更多资料"并建议下一步。

裁剪要点

  • 先重排再拼接,避免把低质片段塞进上下文。
  • 控制上下文总长(<= 模型 ctx 的 1/3--1/2),优先保留含数字/实体/定义的段落。
  • 若问题主题明确,做片段内句级裁剪(减少噪声)。

九、性能与成本优化清单

召回

  • 双检索:Dense + BM25;用户短问时 BM25 权重可提高。
  • Embedding 归一化 + 余弦距离;同义词词表/Query 扩展(如"退货/退款/退货政策")。

重排

  • Cross-Encoder 小模型优先;把 topk 从 10→4,延迟可降 40--60%。
  • 对于 FAQ 类问题,可缓存重排结果(query hash + corpus version)。

生成

  • 流式 输出提升主观速度;JSON Mode提升结构化输出稳定度。
  • 减少温度随机性(temperature 0--0.3),避免啰嗦(max_tokens 抑制)。
  • 分段生成:先摘要后扩写,有助于控时控费。

缓存

  • Q&A 结果缓存:cache_key = normalize(query) + top_docs_signature
  • Embedding 缓存:同一段落/版本只算一次。

并发与连接

  • 统一 HTTP 连接池;对外模型服务开启重试 + 抖动退避。
  • 限流熔断 + 过载时降级为"检索结果直出摘要"。

粗略成本估算

  • 设平均上下文总长 2k tokens、输出 400 tokens:

    • 以 ¥X/1k tokens 计费,则单次 ≈ 2*X + 0.4*X = 2.4X
    • 缓存命中 30% 时,平均可降至 ~1.7X
  • Embedding:一次入库向量化成本按文档量线性增长,可通过夜间批处理增量更新摊销。


十、安全与合规

  • 越权风险:对问题做权限校验,片段级 ACL;未授权片段禁止进入上下文。
  • 隐私:入库前脱敏(邮箱/手机号/地址等),日志中掩码。
  • 滥用输入:Prompt 注入("忽略以上")→ 在系统层过滤"越权指令",仅以片段为信任源。
  • 输出审计:命中证据与答案强绑定;敏感领域(法务/医疗)严格拒答模板。

十一、上线与观测

  • 可观测:为每次问答生成 trace_id,记录:

    • embedding/检索/重排/生成 各阶段耗时
    • top-k 文本与打分
    • 模型 token 用量
    • 最终答案/引用
  • 仪表盘

    • P50/P95 延迟、TTFT、TBT
    • 召回命中率、用户"有帮助率"
    • 缓存命中、各环节错误率
    • 每日成本 & 单问题成本分布

十二、常见坑位与避坑策略

  1. 中文 PDF OCR 乱序 → 预处理版面,按列合并;必要时用版面恢复(layout detection)。
  2. 切块过大 → 召回噪声高、重排压力大;先减块长再看指标。
  3. 只用向量检索 → 对短问/专有名词效果差;务必加 BM25。
  4. 重排缺席 → 回答飘忽;Cross-Encoder 是性价比最高的提升点。
  5. 上下文塞满 → 生成变慢且跑题;严格做裁剪与顺序控制。
  6. 无评测闭环 → 每次"感觉更好"都是幻觉;先做小评测集再谈上线。
  7. 忽视权限 → 片段泄露风险大;片段级 ACL 是底线。

十三、进阶:多步 Agent 与工具调用(可选)

  • 当问题需要计算/查表/检索多个域 时,引入工具路由

    • Step1:问题分类(FAQ/计算/搜索/跨域)
    • Step2:按类走"RAG → 计算器/SQL/HTTP 搜索 → 汇总"
  • 控制 Agent 最大步数工具白名单,避免无限循环与越权爬取。


十四、项目结构建议

bash 复制代码
/rag-app
  ├─ ingest/           # 文档清洗/切块/入库
  ├─ retriever/        # 向量/BM25 混检
  ├─ reranker/         # CrossEncoder
  ├─ generator/        # LLM Provider 封装
  ├─ prompts/          # 模板 & A/B 变体
  ├─ eval/             # 离线评测脚本与数据
  ├─ telemetry/        # 打点/Tracing
  ├─ api/              # FastAPI 路由与SSE
  └─ deploy/           # docker-compose / k8s 清单

十五、可执行的下一步

  1. 选一批 50--100 条真实问答,做标注评测集
  2. 用 pgvector + Cross-Encoder 跑通 MVP(上面的 FastAPI 即可)。
  3. 接入观测:记录每次问答的 top-k 片段、分数与 Token。
  4. 首轮 A/B:对比不同切块不同 topk不同重排
  5. 上线后 2 周做一次错误分类(召回错 / 重排错 / 生成幻觉),针对性改进。

附:Docker Compose(示例骨架)

yaml 复制代码
version: "3.9"
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ragpwd
    ports: ["5432:5432"]
    volumes:
      - ./data/pg:/var/lib/postgresql/data
  redis:
    image: redis:7
    ports: ["6379:6379"]
  api:
    build: .
    environment:
      DB_DSN: postgresql://postgres:ragpwd@pg:5432/postgres
      REDIS_URL: redis://redis:6379/0
    ports: ["8000:8000"]
    depends_on: [pg, redis]

结语

RAG 的工程化核心不是"换个更大的模型",而是把数据、检索、重排、生成、评测、观测、成本拆成可迭代的部件------每一项都能量化、替换、复盘。当这条链路跑顺了,你就拥有了一个能持续演进、成本可控、可解释的 AI 应用底座。

相关推荐
星期天要睡觉20 小时前
计算机视觉(opencv)——实时颜色检测
人工智能·python·opencv·计算机视觉
艾醒(AiXing-w)20 小时前
探索大语言模型(LLM): 大模型应用与对应的硬件选型一览表
人工智能·语言模型·自然语言处理
阿里云云原生20 小时前
Qoder 重磅升级,推出 Quest Remote 功能,像发邮件一样将任务委派到云端
人工智能
搞科研的小刘选手21 小时前
2025计算机视觉和影像计算国际学术会议(CVIC 2025)
人工智能·机器学习·计算机视觉·数据挖掘·数字孪生·影像计算·电磁与光学成像
GoppViper21 小时前
维星AI GEO优化:AI搜索引擎时代,企业如何抢占流量C位?
人工智能·搜索引擎
战场小包21 小时前
PaddleOCR-VL,超强文字识别能力,PDF的拯救者
人工智能·百度飞桨
做科研的周师兄21 小时前
【机器学习入门】8.2 主成分分析:一文吃透主成分分析(PCA)—— 从原理到核心逻辑
人工智能·算法·决策树·机器学习·流程图
天天讯通21 小时前
任务型与聊天型语音机器人有什么区别
人工智能·机器人
福客AI21 小时前
电商客服机器人与客服AI软件:打通电商“服务-运营”数据闭环
人工智能