RAG 资产分析系统搭建实战

RAG 资产分析系统:封面图

第一章:RAG 基础知识

1.1 什么是 RAG

RAG(Retrieval-Augmented Generation,检索增强生成)是一种将信息检索与大语言模型生成结合的技术架构。核心理念是:不让 LLM 凭空编答案,而是先检索外部知识库中相关片段,再基于片段生成回答

举一个典型场景:用户问"D0 入催率怎么算?",传统 LLM 可能回答"大概是逾期订单除以总订单",口径模糊甚至不准确。RAG 会先检索到指标字典中的定义"D0 订单入催 = 1 - dpd0_repay / dpd0_cnt",再基于这个片段生成准确答案。答案可溯源,口径可查,显著降低幻觉问题

1.2 核心组件

RAG 系统包含五个核心环节:

① 分块(Chunking):将长文档切分为适合检索的小片段。分块质量直接决定检索效果。常见策略有按标题层级、按段落、按固定字符数切分。chunk size 太大会引入噪声且可能超出演示上下文上限,太小会丢失上下文关联。

② 向量化(Embedding):将文本映射为稠密向量,使语义相近的文本在向量空间中距离接近。Embedding 是向量检索的基础,决定了检索的语义理解能力。常用模型包括 OpenAI text-embedding-3(效果好但收费)、BGE-small-zh(国产开源,CPU 可跑)、m3e-base(中文友好)等。

③ 检索(Retrieval):根据用户 query 从向量数据库或索引中召回最相关的 top_k 片段。检索是 RAG 的核心,决定了能给 LLM 提供什么知识。

方式 原理 优点 缺点
BM25 词频统计 + 逆文档频率 不需要 Embedding 模型,本地可跑,对专有名词效果好 无法处理同义词/语义相似
向量检索 语义向量相似度 理解语义,支持同义词匹配 需要 Embedding 模型和向量数据库
混合检索 BM25 + 向量融合 两者兼顾,召回更全 实现复杂度高

④ Prompt 组装:将检索结果、用户问题、系统指令组装成 LLM 的输入。Prompt 设计决定模型能否按预期工作。常见策略包括:明确回答格式(先结论后依据)、加置信度约束(证据不足时回答"不知道")、加边界规则(明确哪些问题不能答)、加引用格式(答案需附带来源)。

⑤ LLM 生成:基于组装好的 Prompt 输出最终答案。生成模型可本地部署(qwen/LLaMA/ChatGLM 系列)或调用 API(GPT-4/Claude 系列)。

1.3 分块策略

分块策略的选择需要根据文档结构和业务场景调整:

策略 原理 适用场景 潜在问题
按标题层级 # 标题为边界切分 Markdown、结构化文档 单个标题可能没有实质内容
按段落 以段落换行符切分 段落明确的文档 段落可能过长
固定大小 按字符数硬切(如 900char) 通用场景 可能切断语义单元
递归切分 递归地按段落→句子切 通用场景 实现复杂

在实际落地中,分块质量比算法选择更重要。常见优化手段包括:过滤无意义的纯标题碎片、清洗 Markdown 噪音(列表符号、序号、代码标记)、过滤过短内容(少于 40 字符)、按内容密度过滤(标题行占比超过 70% 的片段)。

1.4 检索策略选择

向量检索和 BM25 并不是非此即彼的选择,实际系统中可以同时保留,通过配置切换。

BM25 的适用场景:专有名词密集的业务文档(如"入催率"、“利差”、“件均”),对精确匹配的召回效果不输向量检索,且不需要额外的 Embedding 模型,适合资源受限的环境。缺点是对同义词、口语化表达(如"借多少钱" vs “合同金额”)召回效果差。

向量检索的适用场景:语义理解要求高的场景,如同义词丰富的知识库、多语言文档、需要语义相似度排序的情况。代价是需要额外的 Embedding 模型和向量数据库资源。

更优的方案是混合检索 + Rerank:第一阶段用 BM25 或向量检索快速召回 top_20,第二阶段用 CrossEncoder 或 BGE-Rerank 精排取 top_4。两阶段检索能显著提升相关性。

1.5 评估方法

RAG 评估通常从检索和生成两个维度分开进行。

业界主流评估框架

框架 核心思路
RAGAS 从 Faithfulness、Answer Relevance、Context Relevance 三个维度用 LLM 评分
ARES 引入 LLM 作为评判,结合参考答案为答案打分
Trulens 提供检索和生成双侧的可量化追踪仪表盘
LangSmith 支持线上 RAG 的 trace 和评估,支持人工标注集成

检索侧评估

指标 含义 评估目标
Recall@K top_k 结果中包含正确答案的比例 召回是否全面
MRR 正确答案在结果中的平均排名倒数 最佳结果是否靠前
NDCG 综合相关性加权的排名评估 综合排名质量

生成侧评估

方法 说明 局限性
LLM-as-Judge 用 GPT-4/Claude 评判生成答案的质量 Prompt 模板驱动,主观性较强
BLEU/ROUGE 基于 n-gram 重合度的自动评分 无法判断事实正确性
人工评估 随机抽样,人工判断答案正确性 成本高,难以规模化

生成侧评估的难点在于:自动化指标(BLEU/ROUGE)只能衡量文本重合度,无法判断答案的事实正确性和语义一致性。因此在精度要求高的业务场景中,人工抽检仍是不可或缺的环节。

我的 RAG 评估实践

结合业界方法和实际资源限制,我采用以下策略:

  • 检索侧:使用 Recall@K 和 MRR 量化检索质量,定期抽检 top_6 结果的相关性
  • 生成侧:以人工评估为主,每次迭代随机抽取 20-30 条真实 query 进行评测
  • 端到端:在实际会议查询场景中收集 Bad Case,持续优化系统

1.6 优化方向

RAG 是一个需要持续迭代的系统,常见优化方向包括:

  • 检索侧:Query 改写(让 query 更适合检索)、Query 扩展(同义词补充)、混合检索 + Rerank 精排
  • 生成侧:Self-RAG 让模型判断是否需要检索、Corrective-RAG 检查幻觉并重新检索
  • 知识库侧:知识分层(按重要性/置信度分级)、增量索引(新文档自动更新)

第二章:背景与动机

2.1 业务背景

我是一名信贷行业的商业分析师,日常工作中依赖一套多 Agent 系统进行数据分析和报告产出。其中 asset agent 是使用频率最高的 Agent,主要服务于两类核心场景:

  • 会议数据查询:在会议过程中,快速查询特定维度的资产数据(如某渠道、某包体、某客群的入催率、利差等)
  • 报告生成:日常生成日报、周报时,需要反复查询数据并按固定口径进行汇总

2.2 asset agent 的能力与演进

asset agent 负责资产数据分析的全维度问题——放款规模、入催率、利差、渠道分布、包体表现……每次会议中当我需要回答"本周前置新客的 D0 入催率是多少?较上周环比如何?“这类问题时,asset agent 需要从数据库或 Excel 文件中拉取数据、筛选维度、计算指标、输出结论。

此前的实现方案是通过调用资产分析的 Skills 完成——Skills 中预先封装了所有维度和指标的定义、计算口径和分析逻辑,agent 根据用户问题调用对应 Skill 执行分析。

然而,随业务发展,Skills 中积累的维度和指标持续扩展:

维度/指标 持续增加的内容
渠道 Facebook / Google / TikTok / Organic / 非投,投放包 7 个
包体 7 个投放包 + APK 包
客群 老客 / 新客(纯新/非纯新)/ 老客多笔 / 新客多笔
产品 前置/后置 × 单笔/多笔
指标 D-1/D0 入催、D0 利差、至今利差、件均、坏账率…
RG 等级 A / B / C / D / E / F / G

Skills 方案的局限性逐渐显现

  • 可读性差:Skills 中的口径以代码形式存在,调用方只能获得结果,无法直接查看分析过程
  • 维护成本高:维度和指标增加时需同步更新 Skills 代码,跨场景复用困难
  • 扩展性有限:新增分析维度或临时性探索时,Skills 结构难以灵活扩展

因此,需要引入 RAG 知识库作为 Skills 的补充:将描述性知识(定义、口径、SOP)迁移至知识库,由 RAG 提供问答式检索服务;Skills 保留结构化的计算逻辑,两者各司其职。

2.3 知识管理的压力

即使引入知识库,知识本身的管理也面临挑战。资产分析知识分散在多个位置:

  • 飞书文档:指标字典、维度定义、分析 SOP
  • 代码注释:部分口径逻辑内嵌于 Python 脚本
  • DM 对话:临时讨论出的口径约定,仅存于对话记录

问题随之暴露:

  1. 口径不统一:同一术语在不同文档中定义不一致,agent 回答时可能产生偏差
  2. 更新不同步:代码逻辑变更后,文档未同步更新,导致知识不一致
  3. 复用性差:口径知识难以快速迁移至报告自动生成等新场景
  4. 维护成本高:理解 agent 的能力边界需要较长时间

资产分析的维度和指标将持续增加,知识管理的复杂度将持续上升。我需要建立一套系统化的知识管理机制,这便是 RAG + 知识向量库的原始驱动力。

2.4 为什么是 RAG

业界知识管理的主流技术已转向 RAG。AI Native 应用和企业知识库问答场景中,RAG 几乎成为默认技术选型:

方案 业界现状
纯文档搜索 上一代方案,缺乏语义理解能力
硬编码规则 维护成本高,难以规模化
纯 LLM 记忆 上下文窗口有限,大规模知识无法承载
RAG 业界主流方案,适用于几乎所有知识问答场景

RAG 的核心能力——自然语言问答 + 知识可更新 + 答案可溯源——恰好匹配会议快速查询和报告生成的核心需求。

作为 AI 应用开发者,掌握 RAG 也是必备技能。我有明确的学习目标:分块策略、检索链路、Prompt 约束、效果评估,唯有亲手实践方能真正掌握;并在 asset agent 的真实场景中验证 RAG 对工作效率的实际提升


第三章:搭建系统流程

3.1 搭建流程总览

环境准备(安装依赖/Ollama/拉取模型)
    ↓
配置管理(编写 config.yaml)
    ↓
知识库建设(整理 md 文件)
    ↓
索引构建(python build_index_v2.py)
    ↓
服务启动(python app.py)
    ↓
验证测试(/health → /search → /ask)

3.2 环境准备

Python 依赖fastapi uvicorn pydantic pyyaml jieba rank_bm25 chromadb requests

Ollama 模型:用 ollama pull qwen2.5:0.5b。一个模型两用——既做 Embedding 也做生成,节省资源。

3.3 项目结构

asset_rag/
├── app.py              # FastAPI 服务,核心推理逻辑
├── build_index_v2.py   # 索引构建脚本
├── config.yaml         # 所有可配置参数
├── prompt.md           # 系统 Prompt 模板
├── index.json          # BM25 索引(构建后生成)
└── chroma_db/          # 向量数据库(启用向量模式后)

asset_knowledge_base/   # 知识库,与服务分离
├── 01_指标口径/
├── 02_维度定义/
├── 03_数据字典/
└── 04_分析SOP/

设计思路:知识库独立维护,重建索引后服务自动感知新知识。

3.4 配置管理

所有参数集中在 config.yaml

ollama_base_url: http://127.0.0.1:11434
ollama_model: qwen2.5:0.5b
top_k: 6
max_context_chunks: 4
chunk_max_chars: 900
use_vector: false    # 切换检索模式

3.5 核心代码:服务入口

app = FastAPI(title='asset-local-rag')

@app.post('/ask')
def ask(req: AskRequest):
    hits = retrieve(req.question, req.top_k or TOP_K)
    answer = call_ollama(req.question, hits)
    return {'answer': answer, 'citations': hits}

3.6 核心代码:统一检索入口

def retrieve(query: str, top_k: int):
    if USE_VECTOR:
        return retrieve_vector(query, top_k)   # Chroma 向量
    else:
        return retrieve_bm25(query, top_k)     # BM25

BM25 检索:用 rank_bm25 + jieba 分词,对专有名词召回效果好,不需要 Embedding 模型。

向量检索use_vector: true 时启用,用 Ollama 做 Embedding,存在 Chroma 中。

3.7 核心代码:Prompt 组装

def call_ollama(question: str, contexts: list):
    context_text = '\n\n'.join([
        f"[来源: {c['path']}]\n{c['text']}"
        for c in contexts[:MAX_CONTEXT]
    ])
    prompt = f"{PROMPT}\n\n知识片段:\n{context_text}\n\n用户问题:{question}"
    resp = requests.post(f'{OLLAMA_URL}/api/generate',
                         json={'model': OLLAMA_MODEL, 'prompt': prompt})
    return resp.json()['response'].strip()

设计要点:最多取 4 个 chunk 给 LLM,每个 chunk 标注来源。

3.8 索引构建

python3 build_index_v2.py

build_index_v2.py 读取所有 md 文件,按 # 标题切分,过滤无意义碎片和 Markdown 噪音,输出 index.json

3.9 启动与验证

python3 app.py

# 健康检查
curl http://127.0.0.1:8787/health

# RAG 问答测试
curl -X POST http://127.0.0.1:8787/ask \
  -H "Content-Type: application/json" \
  -d '{"question": "D0入催率怎么算?"}'

第四章:知识库建设

4.1 设计原则

知识库是 RAG 的"大脑”,质量直接决定检索效果。遵循三个原则:

  • 边界明确:第一版只覆盖 asset_data 相关内容
  • 结构分层:按"口径 → 维度 → 数据字典 → 分析 SOP"分层组织
  • 格式规范:统一 Markdown 格式,每个概念独占一个 # 标题

4.2 目录结构

asset_knowledge_base/
├── 01_指标口径/指标字典.md      # 指标定义和计算公式
├── 02_维度定义/维度字典.md      # 渠道、客群、包体等维度分类
├── 03_数据字典/asset_data.md   # 资产数据表字段说明
└── 04_分析SOP/                 # 放款下降/首逾恶化/利差恶化等分析思路

分层作用:口径层(定义"是什么")、数据层(定义"有什么字段")、应用层(定义"怎么做")。

4.3 md 写作规范

每个概念独占一个 # 标题,避免被切散到不同 chunk:

## D0 订单入催率
定义:到期日当天仍未还款的订单占比。
公式:D0订单入催 = 1 - dpd0_repay / dpd0_cnt

避免纯列表式写作,列表项要有解释说明。

4.4 知识边界划定

能回答 不能回答
放款规模、件均、入催率、利差 通过率、审批链路
渠道/包体/客群维度定义 催收策略
指标口径、数据字段说明 operation_data

边界控制在 Prompt 层实现,知识库本身不做特殊处理。

4.5 维护流程

整理 md 文件 → 提交到 asset_knowledge_base/ → 执行 build_index_v2.py → 服务自动感知

第五章:分块策略迭代

5.1 分块的重要性

分块是 RAG 中最容易被忽视但影响极大的环节。分块质量直接决定检索回来的内容是否完整。

5.2 v1:按标题 + 字符硬切

# 标题分割,900 char 以内直接作为 chunk,超过则按段落累积切分。

v1 暴露的问题:

问题 现象 影响
纯标题碎片 # 二级标题 没有正文也被当成 chunk 检索回来是空内容
Markdown 噪音 列表符号、序号干扰 Embedding 向量质量下降
上下文割裂 定义和公式被切成两半 检索回来不完整

5.3 v2:过滤 + 清洗

过滤无意义碎片(少于 40 字符、标题行占比超过 70% 的不要)和清洗 Markdown 噪音(去掉列表符号、序号、代码标记)后再建索引。

def is_meaningful_chunk(text: str) -> bool:
    if len(stripped) < 40: return False
    if title_lines / len(lines) > 0.7: return False
    return True

5.4 反思

900 char 硬编码在某些场景仍有局限:公式和说明被切到不同 chunk、表格被切碎、跨章节引用失效。v2 是资源受限下的实用解,不是最优解。


第六章:检索链路设计

6.1 BM25 检索

BM25 是稀疏检索算法,本质是 TF-IDF 的升级版:对 query 分词 → 计算 TF → 引入 IDF 惩罚常见词 → 文档长度归一化。

优点: 不需要 Embedding 模型,本地可跑;对专有名词(“D0入催”、“件均”)召回效果好。

缺点: 只能字面匹配,同义词(“件均” vs “平均合同金额”)无法召回。

6.2 向量检索

通过 config.yamluse_vector: true/false 切换。用 Ollama 做 Embedding,存在 Chroma 中。

缺点: 需要额外的 Embedding 模型和向量数据库资源,实测在 2 vCPU / 3.8GB 机器上速度较慢。

6.3 统一检索入口

def retrieve(query: str, top_k: int):
    if USE_VECTOR:
        return retrieve_vector(query, top_k)
    else:
        return retrieve_bm25(query, top_k)

两种模式通过配置切换,对上层透明。

6.4 检索结果示例

{
  "id": "01_指标口径/指标字典.md#3",
  "path": "01_指标口径/指标字典.md",
  "text": "## D0 订单入催\n\n定义:到期日当天仍未还款...\n公式:D0订单入催 = 1 - dpd0_repay / dpd0_cnt",
  "score": 8.67
}

score 是 BM25 相关性得分,可用于判断置信度。


第七章:Prompt 工程与安全约束

7.1 系统 Prompt 设计

Prompt 设计决定模型能否按预期工作,遵循三个原则:

  • 范围白名单:明确列出能回答的主题
  • 拒绝黑名单:明确列出不能回答的领域和标准回复话术
  • 不确定性约束:证据不足时不能编造,明确说"不知道"

7.2 我的 Prompt

你是 asset agent 的本地 RAG 问答模型。

严格规则:
- 只能基于提供的知识片段回答
- 只能围绕 nigeria_asset.asset_data 第一版范围回答
- 涉及通过率、催收执行等超出范围的问题,必须回答"当前知识库不支持该问题"
- 不要编造字段、口径、结论

7.3 用户 Prompt 组装

context_text = '\n\n'.join([
    f"[来源: {c['path']}]\n{c['text']}"
    for c in contexts[:MAX_CONTEXT]  # 最多取 4 个 chunk
])
prompt = f"{PROMPT}\n\n知识片段:\n{context_text}\n\n用户问题:{question}"

关键设计: MAX_CONTEXT = 4 避免超过模型上下文上限,每个 chunk 标注来源方便溯源。

7.4 边界测试效果

问题 预期行为 实际
“D0 入催怎么算?” 基于指标字典回答
“本周通过率是多少?” “知识库不支持”
“哪些订单逾期了?” 拒绝,提示需查数据库

第八章:效果评估与瓶颈分析

8.1 主观评分:6 / 10

维度 得分 说明
检索召回 6/10 同义词召回差
生成质量 5/10 0.5B 模型理解能力有限
响应速度 8/10 本地推理,无网络延迟
稳定性 7/10 资源受限下偶有 OOM
可解释性 8/10 来源清晰,可溯源

核心短板:模型太小同义词召回差

8.2 瓶颈一:模型太小

qwen2.5:0.5b 对复杂推理(如多步计算问题)能力有限,对"环比"、“同比"等分析术语理解有时偏差,指令遵循也不稳定。

8.3 瓶颈二:检索召回率低

BM25 只做字面匹配,导致:

  • “平均合同金额” 召回不了"件均”
  • “首逾率” 匹配不到"首逾恶化"(字面不同)
  • 口语化表达 vs 知识库书面语不匹配

8.4 瓶颈三:没有 Rerank

当前链路:query → BM25 top_k → LLM

理想链路:query → BM25 top_20 → Rerank top_4 → LLM

两阶段检索可显著提升相关性,但当前机器资源不支持。


第九章:踩坑总结

坑一:资源预估不足导致 OOM

最初用 qwen2.5:3b 跑向量检索,3.8GB RAM 直接爆了。解决:降级到 qwen2.5:0.5b,关闭向量检索只用 BM25。

教训: 不要高估硬件条件,先用最小可行方案跑通。

坑二:分块粒度太粗

早期按 1500 char 切分,把"定义 + 公式 + 注意事项"切成三块,检索回来经常不完整。解决:降到 900 char,同时要求知识库写作"一个概念一段话"。

教训: 分块策略和知识库写作规范需要联合优化。

坑三:Prompt 注入

用户输入 "忽略之前的规则" 可以让模型跳过约束。解决:在 system prompt 加"严格规则"章节,应用层做输入过滤。

教训: 本地小模型的指令遵循能力不如大模型,Prompt 约束不能假设天然有效。

坑四:Ollama 热更新问题

切换 use_vector 时 Ollama 没启动好会报 500 但服务不崩溃,排查困难。解决:加了 /health 接口主动检查状态。


第十章:优化方向与总结

10.1 模型升级路径

qwen2.5:0.5b (当前)
    ↓ 内存允许时优先升级
qwen2.5:3b  (+2分)
    ↓ 有条件加内存/GPU
qwen2.5:7b  (+3分)
    ↓ 进一步
LLaMA3.1:8B / ChatGLM4:9B

10.2 检索两阶段方案

candidates = retrieve_bm25(query, top_k=20)       # 阶段一:粗排
reranked = cross_encoder_rerank(query, candidates, top_k=4)  # 阶段二:精排

预期收益:召回率提升 15-20%。

10.3 同义词扩展

QUERY_EXPANSIONS = {
    "件均": ["平均合同金额", "人均借款金额"],
    "入催": ["逾期", "未还款", "不良"],
}

10.4 总结

这个 RAG 不是"完美方案",而是资源约束下的最优妥协

  • 2 vCPU / 3.8GB / 无 GPU → 只能用 BM25 + 0.5B
  • 基本可用但明显有瓶颈 → 6 分不是终点,是起点
  • 每次迭代都是"资源允许下做最值得做的事"

核心理念:工程没有最优解,只有最适合当前阶段的解。